手順を導き出す「Logic Editor 3D」



画像
AI入力画面

・ダウンロードされる方はこちら。↓

取扱説明書

1. 概要

このツールは、
プロジェクトや製造プロセスのフローを
可視化・最適化します。

バージョン1.1では、AIとの連携機能が強化され、
AIが生成したデータを柔軟に取り込み、
既存のデータベースへ蓄積・統合することが可能になりました。

2. 画面構成

画面は左右のパネルと中央の3Dビューで構成されています。

  1. 左パネル (操作盤):

    • 実行: ゴール(最終成果物)を設定し、計算を実行します。

    • AI設計: 生成AIを使ってフローを自動構築します(アコーディオン式メニュー)。

    • リソース: プロジェクトで使用する「モノ・データ」を管理します。

    • タスク: リソースを変換する「作業・処理」を管理します。

  2. 中央パネル (3Dビュー):

    • 計算されたフローを3Dグラフで可視化します。

  3. 右パネル (指標):

    • 合計時間、予算、詳細なスケジュール表を表示します。

3. 【重要】AI連携機能の使い方 (New)

・ChatGPTやClaudeなどの生成AIを活用し、
 複雑な工程を自動でデータベース化します。

STEP 1: 要望の入力

・左パネルの [AI設計] タブを開き、「STEP 1」をクリックして開きます。

  • 作りたいものや条件を自然な言葉で入力してください。

  • 例: 「肉じゃがを作りたい。買い出しから始めて、予算は1500円以内。」

  • 入力後、[プロンプト生成] ボタンを押します。

STEP 2: AIへ送信

「STEP 2」をクリックして開きます。

  1. 専用の命令文(システムプロンプト)が生成されているので、[コピー] ボタンを押します。

  2. ChatGPT や Claude などのAIチャット画面を開き、貼り付けて送信してください。

    • ※このツール内でAIが回答するわけではありません。外部のAIを使用します。

STEP 3: 結果の反映(スマートパース機能)

「STEP 3」をクリックして開きます。

  1. AIチャットから返ってきた回答(JSONコードを含む文章)をすべてコピーします。

  2. このツールのテキストエリアに貼り付けます。

    • v1.1の新機能: AIが「こちらがJSONです...」のような会話文や、マークダウン記号(```json ... ```)を含めて返してきても、ツールが自動でJSON部分だけを抜き出して読み込みます。

  3. [データを統合・蓄積] ボタンを押します。

データの蓄積について (DB Accumulation)

AIからデータを取り込んだ際、以下のロジックでデータベースが更新されます。

  • 新規タスク: リストに新しく追加されます。

  • 既存タスク: 名前と出力が一致するタスクが既にある場合、AIが生成した新しい条件(時間・コスト・説明文)で**上書き更新(ブラッシュアップ)**されます。

    • これにより、使えば使うほどデータベースの精度が向上し、再利用しやすくなります。

4. 基本操作フロー(手動)

リソースの定義

  1. [リソース] タブを開きます。

  2. 名称を入力し [追加] を押します(例: 野菜, カレー)。

タスクの定義

  1. [タスク] タブを開きます。

  2. タスク名: 作業名を入力(例: 煮込む)。

  3. 説明: 作業の詳細メモ(任意)。3Dビューでクリックした際に表示されます。

  4. 入力リソース: 必要な材料を選択(Ctrlキーで複数選択可)。

  5. 出力リソース: 生成される成果物を選択。

  6. 時間・コスト: 数値を入力して [登録]

スケジュールの生成

  1. [実行] タブを開きます。

  2. 目標成果物: 最終ゴールを選択します。

  3. 優先モード:

    • 時間短縮: コスト度外視で最速ルート。

    • コスト削減: 時間がかかっても最安ルート。

    • バランス: 時間とコストのバランス重視。

  4. [スケジュール生成] をクリックすると、計算結果が表示されます。

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>

いいなと思ったら応援しよう!

コメント

コメントするには、 ログイン または 会員登録 をお願いします。
手順を導き出す「Logic Editor 3D」|古井和雄
word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word

mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1