手順を導き出す「Logic Editor 3D」
・ダウンロードされる方はこちら。↓
取扱説明書
1. 概要
このツールは、
プロジェクトや製造プロセスのフローを
可視化・最適化します。
バージョン1.1では、AIとの連携機能が強化され、
AIが生成したデータを柔軟に取り込み、
既存のデータベースへ蓄積・統合することが可能になりました。
2. 画面構成
画面は左右のパネルと中央の3Dビューで構成されています。
左パネル (操作盤):
実行: ゴール(最終成果物)を設定し、計算を実行します。
AI設計: 生成AIを使ってフローを自動構築します(アコーディオン式メニュー)。
リソース: プロジェクトで使用する「モノ・データ」を管理します。
タスク: リソースを変換する「作業・処理」を管理します。
中央パネル (3Dビュー):
計算されたフローを3Dグラフで可視化します。
右パネル (指標):
合計時間、予算、詳細なスケジュール表を表示します。
3. 【重要】AI連携機能の使い方 (New)
・ChatGPTやClaudeなどの生成AIを活用し、
複雑な工程を自動でデータベース化します。
STEP 1: 要望の入力
・左パネルの [AI設計] タブを開き、「STEP 1」をクリックして開きます。
作りたいものや条件を自然な言葉で入力してください。
例: 「肉じゃがを作りたい。買い出しから始めて、予算は1500円以内。」
入力後、[プロンプト生成] ボタンを押します。
STEP 2: AIへ送信
「STEP 2」をクリックして開きます。
専用の命令文(システムプロンプト)が生成されているので、[コピー] ボタンを押します。
ChatGPT や Claude などのAIチャット画面を開き、貼り付けて送信してください。
※このツール内でAIが回答するわけではありません。外部のAIを使用します。
STEP 3: 結果の反映(スマートパース機能)
「STEP 3」をクリックして開きます。
AIチャットから返ってきた回答(JSONコードを含む文章)をすべてコピーします。
このツールのテキストエリアに貼り付けます。
v1.1の新機能: AIが「こちらがJSONです...」のような会話文や、マークダウン記号(```json ... ```)を含めて返してきても、ツールが自動でJSON部分だけを抜き出して読み込みます。
[データを統合・蓄積] ボタンを押します。
データの蓄積について (DB Accumulation)
AIからデータを取り込んだ際、以下のロジックでデータベースが更新されます。
新規タスク: リストに新しく追加されます。
既存タスク: 名前と出力が一致するタスクが既にある場合、AIが生成した新しい条件(時間・コスト・説明文)で**上書き更新(ブラッシュアップ)**されます。
これにより、使えば使うほどデータベースの精度が向上し、再利用しやすくなります。
4. 基本操作フロー(手動)
リソースの定義
[リソース] タブを開きます。
名称を入力し [追加] を押します(例: 野菜, カレー)。
タスクの定義
[タスク] タブを開きます。
タスク名: 作業名を入力(例: 煮込む)。
説明: 作業の詳細メモ(任意)。3Dビューでクリックした際に表示されます。
入力リソース: 必要な材料を選択(Ctrlキーで複数選択可)。
出力リソース: 生成される成果物を選択。
時間・コスト: 数値を入力して [登録]。
スケジュールの生成
[実行] タブを開きます。
目標成果物: 最終ゴールを選択します。
優先モード:
時間短縮: コスト度外視で最速ルート。
コスト削減: 時間がかかっても最安ルート。
バランス: 時間とコストのバランス重視。
[スケジュール生成] をクリックすると、計算結果が表示されます。
5. 3Dビューの操作
左ドラッグ: 回転
右ドラッグ: 平行移動
ホイール: ズーム
ノード(箱)をクリック: タスクの詳細(説明文・時間・コスト)をポップアップ表示
6. 保存と読み込み
保存: 現在のデータベース(リソース・タスク)をJSONファイルとしてダウンロードします。
開く / ドロップ: JSONファイルを読み込みます。画面へのドラッグ&ドロップでも読み込めます。
7. トラブルシューティング
「解析失敗」と表示される:
ゴールに至るまでの材料(リソース)やタスクが不足しています。赤字で表示される不足リソースを確認し、それを作るタスク(または初期リソースとしての定義)を追加してください。
AIデータの読み込みエラー:
AIがJSON形式を崩して回答した場合は読み込めません。
AIに対して「JSON形式のみを出力して」と再生成を依頼してください。
・ソースコードはこちら。↓
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>Logic Editor 3D 日本語版 1.1</title>
<style>
/* --- テーマ設定 --- */
:root {
--bg-dark: #121212;
--bg-panel: #1e1e1e;
--border: #333;
--accent: #00e676;
--accent-sec: #2979ff;
--danger: #ff5252;
--text-main: #e0e0e0;
--text-dim: #888;
}
body, html { margin: 0; padding: 0; width: 100%; height: 100%; overflow: hidden; background-color: var(--bg-dark); color: var(--text-main); font-family: 'Meiryo', sans-serif; }
/* レイアウト: 高さ100vhを厳守し、はみ出しを防ぐ */
#app { display: grid; grid-template-rows: 45px 1fr 30px; height: 100vh; width: 100vw; }
/* 1. メニューバー */
#menubar { background: var(--bg-panel); border-bottom: 1px solid var(--border); display: flex; align-items: center; padding: 0 15px; user-select: none; }
.brand { font-weight: 800; color: var(--accent); margin-right: 25px; font-size: 1.1em; letter-spacing: 1px; display:flex; align-items:center; gap:10px; }
.menu-divider { width: 1px; height: 20px; background: var(--border); margin: 0 10px; }
.btn { background: #2c2c2c; border: 1px solid #444; color: #ccc; padding: 6px 12px; cursor: pointer; border-radius: 4px; font-size: 0.85em; transition: 0.2s; white-space: nowrap; }
.btn:hover { background: #444; color: #fff; }
.btn-primary { background: var(--accent); color: #000; border:none; font-weight:bold; }
.btn-primary:hover { background: #00c853; }
/* 2. メインワークスペース (min-height:0 が重要) */
#workspace { display: grid; grid-template-columns: 360px 1fr 320px; overflow: hidden; min-height: 0; }
/* パネル共通設定 */
.panel { background: var(--bg-panel); display: flex; flex-direction: column; border-right: 1px solid var(--border); z-index: 10; min-height: 0; }
.panel-right { border-right: none; border-left: 1px solid var(--border); }
.panel-header { padding: 0; display: flex; background: #252525; border-bottom: 1px solid var(--border); flex-shrink: 0; }
.tab { flex: 1; padding: 12px 5px; text-align: center; cursor: pointer; color: var(--text-dim); font-size: 0.8em; font-weight: 600; border-bottom: 2px solid transparent; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.tab:hover { color: #fff; background: #2a2a2a; }
.tab.active { color: var(--accent); border-bottom: 2px solid var(--accent); background: #1e1e1e; }
/* スクロールエリアの修正 */
.scroll-area { flex: 1; overflow-y: auto; padding: 15px; min-height: 0; }
.scroll-area::-webkit-scrollbar { width: 8px; }
.scroll-area::-webkit-scrollbar-track { background: #1a1a1a; }
.scroll-area::-webkit-scrollbar-thumb { background: #444; border-radius: 4px; }
.scroll-area::-webkit-scrollbar-thumb:hover { background: #555; }
/* フォーム要素 */
.form-group { margin-bottom: 20px; background: #252525; padding: 15px; border-radius: 6px; border: 1px solid var(--border); }
.label { display: block; font-size: 0.7em; color: var(--text-dim); margin-bottom: 8px; text-transform: uppercase; font-weight:bold; }
.input-row { display: flex; gap: 10px; }
input, select, textarea { width: 100%; background: #121212; border: 1px solid #333; color: #fff; padding: 8px; border-radius: 4px; font-size: 0.9em; box-sizing: border-box; font-family: inherit; }
textarea { resize: vertical; min-height: 60px; line-height: 1.4; }
input:focus, select:focus, textarea:focus { border-color: var(--accent); outline: none; }
/* リストアイテム */
.list-item { background: #252525; border: 1px solid var(--border); padding: 10px; margin-bottom: 5px; border-radius: 4px; display: flex; justify-content: space-between; align-items: center; }
.tag { background: #333; color: #aaa; padding: 2px 6px; border-radius: 3px; font-size: 0.7em; margin-right: 5px; }
.del-icon { color: var(--danger); cursor: pointer; padding: 4px; font-weight:bold; }
/* アコーディオン (AIステップ用) */
details { background: #252525; border: 1px solid var(--border); border-radius: 4px; margin-bottom: 10px; overflow: hidden; }
summary { padding: 10px 15px; cursor: pointer; font-weight: bold; background: #2a2a2a; color: var(--accent-sec); user-select: none; outline: none; font-size: 0.9em; }
summary:hover { background: #333; }
.details-content { padding: 15px; border-top: 1px solid var(--border); }
details[open] summary { border-bottom: 1px solid var(--border); }
/* キャンバス */
#canvas-wrapper { position: relative; background: radial-gradient(circle at center, #1a1a1a 0%, #000 100%); overflow: hidden; }
#canvas-overlay { position: absolute; top: 15px; left: 15px; pointer-events: none; }
.badge { background: rgba(0,0,0,0.7); color: var(--accent); padding: 5px 10px; border-radius: 20px; font-size: 0.8em; border: 1px solid var(--accent); backdrop-filter: blur(4px); }
/* 指標パネル */
.metric-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-bottom: 20px; }
.metric-card { background: #252525; padding: 10px; border-radius: 4px; text-align: center; border: 1px solid var(--border); }
.metric-val { font-size: 1.4em; font-weight: bold; color: #fff; display:block; }
.metric-label { font-size: 0.7em; color: var(--text-dim); text-transform: uppercase; }
.schedule-step { border-left: 2px solid var(--border); padding-left: 15px; margin-bottom: 20px; position: relative; }
.schedule-step::before { content: ''; position: absolute; left: -6px; top: 0; width: 10px; height: 10px; background: #333; border-radius: 50%; }
.schedule-step.critical::before { background: var(--accent); box-shadow: 0 0 5px var(--accent); }
.step-header { font-weight: bold; color: #fff; margin-bottom: 4px; }
.step-desc { font-size: 0.8em; color: #ccc; margin-bottom: 4px; font-style: italic; }
.step-meta { font-size: 0.8em; color: #888; display: flex; gap: 10px; }
/* ステータスバー */
#statusbar { background: var(--bg-panel); border-top: 1px solid var(--border); display: flex; align-items: center; padding: 0 15px; font-size: 0.8em; color: var(--text-dim); justify-content: space-between; }
/* トースト通知 */
#toast-container { position: fixed; bottom: 40px; right: 20px; z-index: 9999; display: flex; flex-direction: column; gap: 10px; pointer-events: none; }
.toast { background: #333; color: #fff; padding: 10px 20px; border-radius: 4px; box-shadow: 0 4px 12px rgba(0,0,0,0.5); border-left: 4px solid var(--accent); font-size: 0.9em; transform: translateX(100%); transition: transform 0.3s; pointer-events: auto; }
.toast.show { transform: translateX(0); }
.toast.error { border-left-color: var(--danger); }
.hidden { display: none !important; }
.copy-box { position: relative; }
.copy-btn { position: absolute; top: 5px; right: 5px; background: rgba(0,0,0,0.5); border: 1px solid #444; color: #fff; padding: 2px 8px; font-size: 0.7em; border-radius: 3px; cursor: pointer; }
</style>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/controls/OrbitControls.js"></script>
</head>
<body>
<div id="app">
<div id="menubar">
<div class="brand">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M2 12h20M2 12l4-4m-4 4l4 4M22 4v16"/></svg>
LOGIC ARCHITECT <span style="font-weight:normal; font-size:0.8em; color:#666;">v4.7</span>
</div>
<div class="menu-divider"></div>
<button class="btn" onclick="app.newProject()">新規</button>
<button class="btn" onclick="app.openProject()">開く</button>
<button class="btn" onclick="app.saveProject()">保存</button>
<div class="menu-divider"></div>
<div style="font-size:0.85em; color:#888;">ファイル: <span id="filename-display" style="color:#fff;">未保存</span></div>
</div>
<div id="workspace">
<div class="panel">
<div class="panel-header">
<div class="tab active" onclick="UIManager.switchTab('run')">実行</div>
<div class="tab" onclick="UIManager.switchTab('ai')">AI設計</div>
<div class="tab" onclick="UIManager.switchTab('types')">リソース</div>
<div class="tab" onclick="UIManager.switchTab('funcs')">タスク</div>
</div>
<div id="tab-run" class="scroll-area">
<div class="form-group">
<div class="label">戦略最適化</div>
<div style="margin-bottom:10px; font-size:0.85em; color:#888;">ゴールを設定して計算します。</div>
<div class="label">目標成果物 (ゴール)</div>
<select id="run-goal"></select>
<div class="label" style="margin-top:10px;">優先モード</div>
<select id="run-mode">
<option value="time">時間短縮 (最速)</option>
<option value="cost">コスト削減 (最安)</option>
<option value="balanced">バランス (スコア)</option>
</select>
<button class="btn btn-primary" style="width:100%; margin-top:15px;" onclick="app.solve()">スケジュール生成</button>
</div>
<div id="diagnostics-box" class="form-group hidden" style="border-color:var(--danger);">
<div class="label" style="color:var(--danger);">解析失敗</div>
<div id="diagnostics-msg" style="font-size:0.85em; color:#ccc;"></div>
</div>
</div>
<div id="tab-ai" class="scroll-area hidden">
<div style="padding:10px; font-size:0.85em; color:#aaa; margin-bottom:5px;">
生成AIにプロジェクトの構造と具体的な作業内容を定義させます。
</div>
<details open>
<summary>STEP 1: 要望の入力</summary>
<div class="details-content">
<textarea id="ai-user-prompt" placeholder="例: カレーを作りたい。隠し味を入れる工程も追加して。"></textarea>
<button class="btn btn-primary" style="width:100%; margin-top:10px;" onclick="app.generateAIPrompt()">プロンプト生成</button>
</div>
</details>
<details>
<summary>STEP 2: AIへ送信</summary>
<div class="details-content">
<div style="font-size:0.8em; color:#888; margin-bottom:5px;">以下のテキストをコピーし、ChatGPT等に送信してください。</div>
<div class="copy-box">
<textarea id="ai-system-prompt" readonly style="height:120px; font-family:monospace; font-size:0.8em; color:#88ff88;"></textarea>
<button class="copy-btn" onclick="app.copyToClipboard()">コピー</button>
</div>
</div>
</details>
<details>
<summary>STEP 3: 結果の反映</summary>
<div class="details-content">
<div style="font-size:0.8em; color:#888; margin-bottom:5px;">AIからの返信(JSON)をここに貼り付けます。余計な文字が含まれていても自動で除去します。</div>
<textarea id="ai-response-json" placeholder='{ "types": [...], "functions": [...] }'></textarea>
<button class="btn" style="width:100%; margin-top:10px; background:var(--accent-sec); color:#fff; border:none;" onclick="app.applyAIResponse()">データを統合・蓄積</button>
</div>
</details>
</div>
<div id="tab-types" class="scroll-area hidden">
<div class="form-group">
<div class="label">リソース追加</div>
<div class="input-row">
<input type="text" id="type-name" placeholder="名称">
<button class="btn" onclick="app.addType()">追加</button>
</div>
</div>
<div id="type-list"></div>
</div>
<div id="tab-funcs" class="scroll-area hidden">
<div class="form-group">
<div class="label">タスク定義</div>
<input type="text" id="func-name" placeholder="タスク名" style="margin-bottom:5px;">
<textarea id="func-desc" placeholder="作業内容の説明(任意)" style="margin-bottom:10px; min-height:40px;"></textarea>
<div class="label">入力リソース (Ctrl+Click)</div>
<select id="func-inputs" multiple style="height:60px; margin-bottom:10px;"></select>
<div class="label">出力リソース</div>
<select id="func-output" style="margin-bottom:10px;"></select>
<div class="input-row">
<div style="flex:1">
<div class="label">時間(h)</div>
<input type="number" id="func-time" value="1" min="0" step="0.5">
</div>
<div style="flex:1">
<div class="label">コスト(円)</div>
<input type="number" id="func-cost" value="0" min="0" step="100">
</div>
</div>
<button class="btn" style="width:100%; margin-top:10px;" onclick="app.addFunction()">登録</button>
</div>
<div id="func-list"></div>
</div>
</div>
<div id="canvas-wrapper">
<div id="canvas-overlay">
<span class="badge" id="view-mode-badge">3D グラフビュー</span>
</div>
</div>
<div class="panel panel-right">
<div class="panel-header">
<div class="tab active">指標と詳細</div>
</div>
<div id="metrics-panel" class="scroll-area">
<div class="metric-grid">
<div class="metric-card">
<span class="metric-val" id="metric-time">0h</span>
<span class="metric-label">合計時間</span>
</div>
<div class="metric-card">
<span class="metric-val" id="metric-cost">¥0</span>
<span class="metric-label">推定予算</span>
</div>
</div>
<div class="label" style="border-bottom:1px solid #333; padding-bottom:5px; margin-bottom:15px;">工程表</div>
<div id="schedule-list">
<div style="text-align:center; color:#555; padding:20px;">
スケジュールなし
</div>
</div>
</div>
</div>
</div>
<div id="statusbar">
<span id="status-msg">Ready</span>
<span style="font-family:monospace; color:#555;">v4.7.0 Smart-Parse</span>
</div>
</div>
<input type="file" id="file-uploader" style="display:none" accept=".json">
<div id="toast-container"></div>
<script>
// --- Toast通知 ---
const Toast = {
show(msg, type = 'info') {
const container = document.getElementById('toast-container');
const el = document.createElement('div');
el.className = `toast ${type}`;
el.innerText = msg;
container.appendChild(el);
requestAnimationFrame(() => el.classList.add('show'));
setTimeout(() => {
el.classList.remove('show');
setTimeout(() => el.remove(), 300);
}, 3000);
}
};
// --- メインアプリ ---
class App {
constructor() {
this.types = [];
this.functions = [];
this.currentProjectName = "無題のプロジェクト";
this.solver = new LogicSolver(this);
this.viz = new Visualizer('canvas-wrapper');
this.setupDragDrop();
this.loadLocalStorage();
this.viz.init();
}
initUI() { UIManager.refreshAll(); }
// --- プロジェクト操作 ---
newProject() {
if(confirm("新規作成しますか?")) {
this.types = [];
this.functions = [];
this.currentProjectName = "無題のプロジェクト";
this.saveLocalStorage();
UIManager.refreshAll();
this.viz.clearScene();
Toast.show("リセットしました");
}
}
saveProject() {
const data = JSON.stringify({ version: "4.7", name: this.currentProjectName, types: this.types, functions: this.functions }, null, 2);
const blob = new Blob([data], {type: "application/json"});
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = this.currentProjectName + ".json";
a.click();
URL.revokeObjectURL(url);
}
openProject() { document.getElementById('file-uploader').click(); }
loadFromFile(file) {
const reader = new FileReader();
reader.onload = (e) => {
try {
const data = JSON.parse(e.target.result);
this.types = data.types || [];
this.functions = data.functions || [];
this.currentProjectName = data.name || "LoadProject";
this.saveLocalStorage();
UIManager.refreshAll();
this.viz.clearScene();
Toast.show("読み込み成功");
} catch(err) { Toast.show("エラー", "error"); }
};
reader.readAsText(file);
}
setupDragDrop() {
const d = document.body;
d.ondragover = (e) => { e.preventDefault(); };
d.ondrop = (e) => {
e.preventDefault();
if(e.dataTransfer.files.length > 0) this.loadFromFile(e.dataTransfer.files[0]);
};
document.getElementById('file-uploader').onchange = (e) => {
if(e.target.files.length > 0) this.loadFromFile(e.target.files[0]);
};
}
// --- データ管理 ---
loadLocalStorage() {
const saved = localStorage.getItem('logic_pro_v47');
if(saved) {
try {
const data = JSON.parse(saved);
this.types = data.types || [];
this.functions = data.functions || [];
} catch(e) {}
} else {
this.loadSampleData();
}
}
saveLocalStorage() {
localStorage.setItem('logic_pro_v47', JSON.stringify({ types: this.types, functions: this.functions }));
UIManager.updateFilename(this.currentProjectName);
}
loadSampleData() {
this.types = ["生食材", "切った野菜", "炒めた具材", "カレー"];
this.functions = [
{ id: "f1", name: "野菜を切る", desc: "包丁で一口大に切る", inputs: ["生食材"], output: "切った野菜", time: 0.5, cost: 0 },
{ id: "f2", name: "炒める", desc: "中火で飴色になるまで", inputs: ["切った野菜"], output: "炒めた具材", time: 0.3, cost: 50 },
{ id: "f3", name: "煮込む", desc: "ルーを入れて煮込む", inputs: ["炒めた具材"], output: "カレー", time: 1.0, cost: 200 }
];
this.saveLocalStorage();
}
// --- ロジック操作 ---
addType() {
const name = document.getElementById('type-name').value.trim();
if(!name) return;
if(!this.types.includes(name)) {
this.types.push(name);
document.getElementById('type-name').value = '';
this.saveLocalStorage();
UIManager.renderTypeList();
UIManager.updateSelects();
}
}
deleteType(name) {
if(this.functions.some(f => f.output === name || f.inputs.includes(name))) return Toast.show("使用中です", "error");
this.types = this.types.filter(t => t !== name);
this.saveLocalStorage();
UIManager.refreshAll();
}
addFunction() {
const name = document.getElementById('func-name').value.trim();
const desc = document.getElementById('func-desc').value.trim();
const inputs = Array.from(document.getElementById('func-inputs').selectedOptions).map(o=>o.value);
const output = document.getElementById('func-output').value;
const time = parseFloat(document.getElementById('func-time').value) || 0;
const cost = parseFloat(document.getElementById('func-cost').value) || 0;
if(!name || !output) return Toast.show("名前と出力は必須です", "error");
this.functions.push({ id: 'func_'+Date.now(), name, desc, inputs, output, time, cost });
this.saveLocalStorage();
UIManager.renderFuncList();
Toast.show("タスク登録完了");
}
deleteFunction(id) {
this.functions = this.functions.filter(f => f.id !== id);
this.saveLocalStorage();
UIManager.renderFuncList();
}
// --- AI機能 ---
generateAIPrompt() {
const req = document.getElementById('ai-user-prompt').value;
if(!req.trim()) return Toast.show("要望を入力してください", "error");
const prompt = `あなたはプロセス設計のプロです。ユーザーの要望に基づき、必要なリソース(types)とタスク(functions)を定義してください。
タスクには「具体的な作業内容(description)」も含めてください。
【ユーザー要望】
${req}
【出力形式 (JSONのみ)】
{
"types": ["リソースA", "リソースB"],
"functions": [
{
"name": "タスク名",
"description": "具体的な作業指示や内容の説明",
"inputs": ["入力A"],
"output": "出力B",
"time": 数値(h),
"cost": 数値(円)
}
]
}
※ inputsは空配列でも可(初期リソースの場合)。outputは必ずtypesに含まれるもの。`;
document.getElementById('ai-system-prompt').value = prompt;
Toast.show("STEP 2を開いてコピーしてください");
document.querySelectorAll('details')[1].open = true; // 次のステップを開く
}
copyToClipboard() {
const el = document.getElementById('ai-system-prompt');
el.select();
document.execCommand('copy');
Toast.show("コピーしました");
}
// AIレスポンスの適用 (スマートパース & DB蓄積)
applyAIResponse() {
let raw = document.getElementById('ai-response-json').value;
// 1. スマートパース: 入力値から最初の '{' と 最後の '}' を探してJSON部分だけ切り出す
const jsonMatch = raw.match(/\{[\s\S]*\}/);
if(jsonMatch) {
raw = jsonMatch[0]; // JSONらしき部分だけ採用
}
// 2. Markdown記法などの除去
raw = raw.replace(/```json/g, '').replace(/```/g, '').trim();
try {
const data = JSON.parse(raw);
if(!data.types || !data.functions) throw new Error();
let addedCount = 0;
let updatedCount = 0;
// Types: 重複チェックして追加
data.types.forEach(t => {
if(!this.types.includes(t)) {
this.types.push(t);
}
});
// Functions: スマート統合
data.functions.forEach(newF => {
const existingIndex = this.functions.findIndex(f =>
f.name === newF.name && f.output === newF.output
);
if (existingIndex !== -1) {
const existing = this.functions[existingIndex];
existing.inputs = newF.inputs;
existing.time = newF.time;
existing.cost = newF.cost;
existing.desc = newF.description || newF.desc || existing.desc;
updatedCount++;
} else {
newF.id = 'ai_' + Math.random().toString(36).substr(2,9);
newF.desc = newF.description || "";
this.functions.push(newF);
addedCount++;
}
});
this.saveLocalStorage();
UIManager.refreshAll();
Toast.show(`DB更新: 追加${addedCount}件 / 更新${updatedCount}件`);
if(data.functions.length > 0) {
const lastFunc = data.functions[data.functions.length-1];
document.getElementById('run-goal').value = lastFunc.output;
}
} catch(e) {
console.error(e);
Toast.show("JSONが見つかりませんでした。AIの回答をそのまま貼り付けても大丈夫なように修正しましたが、JSONコードが含まれているか確認してください。", "error");
}
}
solve() {
const goal = document.getElementById('run-goal').value;
const mode = document.getElementById('run-mode').value;
document.getElementById('diagnostics-box').classList.add('hidden');
const res = this.solver.solve(goal, mode);
if(!res.success) {
document.getElementById('diagnostics-box').classList.remove('hidden');
document.getElementById('diagnostics-msg').innerText = "不足: " + res.missing.join(', ');
UIManager.clearSchedule();
this.viz.clearScene();
return;
}
const schedule = this.solver.flatten(res.tree);
let t=0, c=0;
schedule.forEach(n => { t+=n.time; c+=n.cost; });
UIManager.renderMetrics(t, c);
UIManager.renderSchedule(schedule);
this.viz.renderGraph(schedule);
Toast.show("計算完了");
}
}
class LogicSolver {
constructor(app) { this.app = app; }
solve(target, mode) {
this.missingTypes = new Set();
const res = this.findPath(target, mode, new Set());
return res ? {success:true, tree:res} : {success:false, missing:Array.from(this.missingTypes)};
}
findPath(target, mode, visited) {
const cands = this.app.functions.filter(f => f.output === target);
if(cands.length === 0) {
if(this.app.types.includes(target)) return { type:'INPUT', name:target, inputs:[], totalScore:0, time:0, cost:0 };
this.missingTypes.add(target); return null;
}
let best = null, bestScore = Infinity;
for(const f of cands) {
if(visited.has(f.id)) continue;
const nextVis = new Set(visited).add(f.id);
const inputs = [];
let score = (mode==='time'?f.time : mode==='cost'?f.cost : f.time*10+f.cost);
let valid = true;
for(const inp of f.inputs) {
const sub = this.findPath(inp, mode, nextVis);
if(!sub) { valid=false; break; }
inputs.push(sub);
score += sub.totalScore;
}
if(valid && score < bestScore) {
bestScore = score;
best = { func:f, inputs:inputs, totalScore:score, time:f.time+inputs.reduce((s,n)=>s+n.time,0), cost:f.cost+inputs.reduce((s,n)=>s+n.cost,0) };
}
}
return best;
}
flatten(node, list=[]) {
if(!node || node.type==='INPUT') return list;
node.inputs.forEach(n => this.flatten(n, list));
if(!list.find(i=>i.id===node.func.id)) list.push(node.func);
return list;
}
}
class UIManager {
static switchTab(id) {
['run','ai','types','funcs'].forEach(t => document.getElementById('tab-'+t).classList.add('hidden'));
document.querySelectorAll('.tab').forEach(e => e.classList.remove('active'));
document.getElementById('tab-'+id).classList.remove('hidden');
const idx = ['run','ai','types','funcs'].indexOf(id);
document.querySelectorAll('.tab')[idx].classList.add('active');
}
static updateFilename(name) { document.getElementById('filename-display').innerText = name; }
static refreshAll() { this.renderTypeList(); this.renderFuncList(); this.updateSelects(); }
static renderTypeList() {
const c = document.getElementById('type-list'); c.innerHTML='';
app.types.forEach(t => {
c.innerHTML += `<div class="list-item"><span>${t}</span><span class="del-icon" onclick="app.deleteType('${t}')">×</span></div>`;
});
}
static renderFuncList() {
const c = document.getElementById('func-list'); c.innerHTML='';
app.functions.forEach(f => {
c.innerHTML += `<div class="list-item">
<div style="flex:1">
<div style="font-weight:bold">${f.name}</div>
<div style="font-size:0.7em; color:#aaa; margin-bottom:2px;">${f.desc || ''}</div>
<div style="font-size:0.75em; color:#888;">${f.inputs.join('+')||'無'} ➜ ${f.output}</div>
<div style="font-size:0.7em;"><span class="tag">${f.time}h</span><span class="tag">¥${f.cost}</span></div>
</div>
<span class="del-icon" onclick="app.deleteFunction('${f.id}')">×</span>
</div>`;
});
}
static updateSelects() {
const els = [document.getElementById('run-goal'), document.getElementById('func-output')];
const inEl = document.getElementById('func-inputs');
els.forEach(e => e.innerHTML = ''); inEl.innerHTML = '';
app.types.forEach(t => {
els.forEach(e => e.appendChild(new Option(t,t)));
inEl.appendChild(new Option(t,t));
});
}
static renderMetrics(t, c) {
document.getElementById('metric-time').innerText = t.toFixed(1)+'h';
document.getElementById('metric-cost').innerText = '¥'+c.toLocaleString();
}
static renderSchedule(list) {
const c = document.getElementById('schedule-list'); c.innerHTML='';
list.forEach((item, i) => {
c.innerHTML += `<div class="schedule-step">
<div class="step-header">${i+1}. ${item.name}</div>
${item.desc ? `<div class="step-desc">${item.desc}</div>` : ''}
<div class="step-meta">Output: ${item.output} | ${item.time}h / ¥${item.cost}</div>
</div>`;
});
}
static clearSchedule() { document.getElementById('schedule-list').innerHTML = '<div style="text-align:center;padding:20px;color:#888;">エラー</div>'; }
}
class Visualizer {
constructor(id) { this.container = document.getElementById(id); this.meshMap = new Map(); }
init() {
this.scene = new THREE.Scene(); this.scene.background = new THREE.Color(0x0e0e0e);
const w = this.container.clientWidth, h = this.container.clientHeight;
this.camera = new THREE.PerspectiveCamera(50, w/h, 0.1, 1000); this.camera.position.set(0, 20, 40);
this.renderer = new THREE.WebGLRenderer({antialias:true}); this.renderer.setSize(w, h);
this.container.appendChild(this.renderer.domElement);
this.controls = new THREE.OrbitControls(this.camera, this.renderer.domElement);
this.scene.add(new THREE.AmbientLight(0xffffff, 0.5));
const d = new THREE.DirectionalLight(0xffffff, 1); d.position.set(10,30,10); this.scene.add(d);
this.scene.add(new THREE.GridHelper(100, 20, 0x222222, 0x111111));
this.raycaster = new THREE.Raycaster(); this.mouse = new THREE.Vector2();
this.renderer.domElement.addEventListener('click', e => this.onClick(e));
window.addEventListener('resize', () => {
const nw = this.container.clientWidth, nh = this.container.clientHeight;
this.camera.aspect = nw/nh; this.camera.updateProjectionMatrix(); this.renderer.setSize(nw, nh);
});
this.animate();
}
renderGraph(schedule) {
this.clearScene();
if(!schedule || !schedule.length) return;
schedule.forEach((node, i) => {
const mesh = new THREE.Mesh(new THREE.BoxGeometry(4, 1, 2), new THREE.MeshLambertMaterial({color: 0x00e676}));
mesh.position.set((i * 8) - (schedule.length * 4), 1, (i%2===0?3:-3));
mesh.userData = { info: node };
this.scene.add(mesh); this.meshMap.set(node.id, mesh);
this.addLabel(node.name, mesh.position);
node.inputs.forEach(inp => {
const prov = schedule.find(p => p.output === inp);
if(prov && this.meshMap.get(prov.id)) this.drawCurve(this.meshMap.get(prov.id).position, mesh.position);
else {
const start = mesh.position.clone().add(new THREE.Vector3(-3, -3, 0));
this.drawCurve(start, mesh.position, 0xff9100); this.addLabel(inp, start, 0.6);
}
});
});
this.controls.reset();
}
drawCurve(p1, p2, col=0x2979ff) {
const mid = p1.clone().add(p2).multiplyScalar(0.5); mid.y+=2;
const pts = new THREE.QuadraticBezierCurve3(p1, mid, p2).getPoints(20);
this.scene.add(new THREE.Line(new THREE.BufferGeometry().setFromPoints(pts), new THREE.LineBasicMaterial({color:col})));
}
addLabel(txt, pos, s=1) {
const cvs = document.createElement('canvas'); const ctx = cvs.getContext('2d');
cvs.width=256; cvs.height=64; ctx.font="Bold 30px sans-serif"; ctx.fillStyle="white"; ctx.textAlign="center";
ctx.fillText(txt, 128, 42);
const sp = new THREE.Sprite(new THREE.SpriteMaterial({map:new THREE.CanvasTexture(cvs), transparent:true}));
sp.position.copy(pos); sp.position.y+=2; sp.scale.set(5*s, 1.25*s, 1);
this.scene.add(sp);
}
clearScene() { this.meshMap.clear(); this.scene.children = this.scene.children.filter(c=>["GridHelper","AmbientLight","DirectionalLight"].includes(c.type)); }
onClick(e) {
const r = this.renderer.domElement.getBoundingClientRect();
this.mouse.x = ((e.clientX - r.left)/r.width)*2 -1; this.mouse.y = -((e.clientY - r.top)/r.height)*2 +1;
this.raycaster.setFromCamera(this.mouse, this.camera);
const isects = this.raycaster.intersectObjects(this.scene.children);
if(isects.length > 0 && isects[0].object.userData.info) {
const i = isects[0].object.userData.info;
Toast.show(`${i.name}\n${i.desc || ''}\n(${i.time}h / ¥${i.cost})`);
}
}
animate() { requestAnimationFrame(()=>this.animate()); this.controls.update(); this.renderer.render(this.scene, this.camera); }
}
var app; window.onload = () => { app = new App(); app.initUI(); };
</script>
</body>
</html>

コメント