未来っぽいグラフ作成ツール「Neuro Linker」


画像
3Dワークスペースの画面
画像
円グラフのプレビュー画面


画像
棒グラフのプレビュー画面
画像
折れ線グラフのプレビュー画面
画像
表のプレビュー画面



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


・ソースコードはこちら。↓

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>Neuro Linker 1.1</title>
    <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
    <style>
        :root {
            --bg: #050505;
            --panel: rgba(12, 16, 20, 0.96);
            --accent: #00ffcc;
            --text: #e0f0ff;
            --border: #334455;
            --menu-bg: #0f1115;
            --sel-box: rgba(0, 255, 204, 0.2);
            --sel-border: #00ffcc;
            --danger: #ff4466;
            --warn: #ffcc00;
        }
        body { margin: 0; overflow: hidden; background: var(--bg); font-family: 'Segoe UI', 'Roboto', monospace; color: var(--text); user-select: none; }

        /* --- UI LAYER --- */
        #ui-layer { position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; z-index: 10; }
        .interactive { pointer-events: auto; }

        /* Menu Bar */
        #menubar {
            position: absolute; top: 0; left: 0; width: 100%; height: 32px;
            background: var(--menu-bg); border-bottom: 1px solid #333;
            display: flex; align-items: center; padding-left: 10px; pointer-events: auto; z-index: 100;
            box-shadow: 0 2px 10px rgba(0,0,0,0.5);
        }
        .menu-item {
            padding: 0 16px; height: 100%; display: flex; align-items: center; cursor: pointer;
            font-size: 12px; color: #bbb; position: relative; letter-spacing: 0.5px;
        }
        .menu-item:hover { background: #333; color: var(--accent); }
        .submenu {
            position: absolute; top: 32px; left: 0; background: var(--panel);
            border: 1px solid var(--border); min-width: 220px; display: none; flex-direction: column;
            box-shadow: 0 5px 20px rgba(0,0,0,0.8); border-radius: 0 0 4px 4px;
        }
        .menu-item:hover .submenu { display: flex; }
        .sub-item {
            padding: 10px 20px; color: #ccc; cursor: pointer; transition: 0.1s; border-bottom: 1px solid rgba(255,255,255,0.05); font-size: 12px;
        }
        .sub-item:hover { background: rgba(0, 255, 204, 0.1); color: var(--accent); padding-left: 25px; }
        .ctx-sep { height: 1px; background: #333; margin: 4px 0; }

        /* Left Library Pane */
        #lib-pane {
            position: absolute; top: 33px; left: 0; width: 250px; bottom: 0;
            background: var(--panel); border-right: 1px solid var(--border);
            display: flex; flex-direction: column; transition: transform 0.3s ease; pointer-events: auto; z-index: 90;
            outline: none;
        }
        #lib-pane.collapsed { transform: translateX(-250px); }
        #lib-toggle {
            position: absolute; top: 50%; right: -15px; width: 15px; height: 50px;
            background: var(--border); border-radius: 0 5px 5px 0; cursor: pointer;
            display: flex; align-items: center; justify-content: center; font-size: 10px; color: #aaa;
        }
        .lib-tabs { display: flex; border-bottom: 1px solid var(--border); }
        .lib-tab { flex: 1; padding: 10px; text-align: center; cursor: pointer; font-size: 11px; background: #080a0c; color: #666; }
        .lib-tab.active { background: var(--panel); color: var(--accent); font-weight: bold; border-bottom: 2px solid var(--accent); }
        
        #lib-toolbar { padding: 5px; border-bottom: 1px solid #333; }
        .lib-btn { 
            width: 100%; padding: 8px; background: #222; border: 1px solid #444; color: #ccc; 
            font-size: 11px; cursor: pointer; text-align: center; border-radius: 3px; display: block; box-sizing: border-box;
        }
        .lib-btn:hover { border-color: var(--accent); color: var(--accent); background: rgba(0,255,204,0.1); }

        #lib-content { flex: 1; overflow-y: auto; padding: 5px; outline: none; }
        .lib-item {
            padding: 8px; margin-bottom: 4px; background: #1a1d22; border: 1px solid #333; border-radius: 3px;
            font-size: 11px; cursor: pointer; display: flex; justify-content: space-between; align-items: center;
        }
        .lib-item:hover { border-color: #555; background: #252a30; }
        .lib-item.selected { background: var(--accent); color: #000; border-color: #fff; }
        .drop-zone-active { background: rgba(0, 255, 204, 0.1) !important; border: 2px dashed var(--accent) !important; }

        /* Mode Toolbar */
        #mode-bar {
            position: absolute; bottom: 30px; left: 50%; transform: translateX(-50%);
            display: flex; gap: 20px; background: rgba(10, 12, 16, 0.9); padding: 10px 20px;
            border: 1px solid #333; border-radius: 50px; backdrop-filter: blur(10px);
            align-items: center; box-shadow: 0 10px 30px rgba(0,0,0,0.5); pointer-events: auto;
        }
        .tool-group { display: flex; flex-direction: column; gap: 2px; align-items: center; }
        .group-label { font-size: 9px; color: #556677; text-transform: uppercase; letter-spacing: 1px; font-weight: bold; }
        .btn-row { display: flex; gap: 8px; }
        .mode-btn {
            background: transparent; border: 1px solid #444; color: #888;
            padding: 8px 16px; cursor: pointer; font-weight: bold; font-size: 11px;
            text-transform: uppercase; transition: 0.2s; border-radius: 20px; min-width: 70px; text-align: center;
        }
        .mode-btn:hover { color: #fff; border-color: #666; background: #222; }
        .mode-btn.active { border-color: var(--accent); color: var(--accent); background: rgba(0,255,204,0.15); box-shadow: 0 0 15px rgba(0,255,204,0.2); }

        /* Selection Box */
        #selection-box {
            position: absolute; border: 1px solid var(--sel-border); background: var(--sel-box);
            display: none; pointer-events: none; z-index: 200;
        }

        /* Context Menu */
        .ctx-menu {
            position: absolute; display: none; flex-direction: column;
            background: var(--panel); border: 1px solid var(--accent);
            box-shadow: 0 0 25px rgba(0,255,204,0.1); min-width: 180px; z-index: 5000;
            border-radius: 4px; overflow: hidden; pointer-events: auto;
        }
        .ctx-item { padding: 10px 15px; cursor: pointer; font-size: 12px; color: var(--text); border-bottom: 1px solid rgba(255,255,255,0.05); }
        .ctx-item:hover { background: var(--accent); color: #000; }
        
        /* Toast */
        #toast-container { position: absolute; top: 50px; right: 20px; display: flex; flex-direction: column; gap: 10px; z-index: 9000; pointer-events: none; }
        .toast {
            background: rgba(20, 25, 30, 0.95); color: #fff; padding: 12px 20px; border-left: 4px solid var(--accent);
            box-shadow: 0 5px 15px rgba(0,0,0,0.5); border-radius: 2px; font-size: 12px;
            opacity: 0; transform: translateX(50px); transition: 0.3s; pointer-events: auto; display: flex; align-items: center; gap: 10px;
        }
        .toast.show { opacity: 1; transform: translateX(0); }
        .toast.warn { border-color: #ffcc00; } .toast.error { border-color: #ff4466; }

        /* --- ANIMATED DIALOGS --- */
        .modal-overlay {
            position: fixed; top: 0; left: 0; width: 100%; height: 100%;
            background: rgba(0,0,0,0.7); z-index: 2000; 
            display: none; justify-content: center; align-items: center;
            opacity: 0; pointer-events: none; visibility: hidden; transition: opacity 0.3s;
            perspective: 1500px;
        }
        .modal-overlay.active { display: flex; opacity: 1; pointer-events: auto; visibility: visible; }

        .dialog-box {
            width: 500px; max-width: 90%; background: #0a0f14; border: 1px solid #334455;
            box-shadow: 0 0 60px rgba(0,0,0,0.8); display: flex; flex-direction: column; border-radius: 6px;
            transform-style: preserve-3d; opacity: 0; transform-origin: center;
        }

        .modal-overlay.opening .dialog-box { animation: animFlipOpen 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275) forwards; }
        .modal-overlay.closing .dialog-box { animation: animFlipClose 0.3s cubic-bezier(0.6, -0.28, 0.735, 0.045) forwards; }

        @keyframes animFlipOpen { 0% { transform: translateZ(-500px) rotateX(-45deg); opacity: 0; } 100% { transform: translateZ(0) rotateX(0deg); opacity: 1; } }
        @keyframes animFlipClose { 0% { transform: translateZ(0) rotateX(0deg); opacity: 1; } 100% { transform: translateZ(-300px) rotateX(45deg); opacity: 0; } }

        .dlg-header {
            padding: 12px 20px; background: rgba(255,255,255,0.05); border-bottom: 1px solid #333; 
            color: var(--accent); font-weight: bold; font-size: 13px; display: flex; justify-content: space-between;
        }
        .dlg-body { padding: 20px; display: flex; flex-direction: column; gap: 15px; max-height: 70vh; overflow-y: auto; }
        .btn-bar { display: flex; justify-content: flex-end; gap: 10px; padding: 15px 20px; border-top: 1px solid #222; background: rgba(0,0,0,0.2); }
        
        input, textarea, select { 
            background: #111; border: 1px solid #444; color: #eee; padding: 10px; 
            font-family: monospace; width: 100%; box-sizing: border-box; font-size: 12px; border-radius: 4px;
        }
        input:focus, textarea:focus { outline: none; border-color: var(--accent); background: #151515; }

        .lbl-tag { 
            font-size: 9px; color: var(--accent); background: rgba(0, 255, 204, 0.08); 
            padding: 3px 8px; border-radius: 3px; display: inline-block; margin-bottom: 5px; letter-spacing: 1px;
            font-weight: bold; border: 1px solid rgba(0,255,204,0.2);
        }

        .btn { background: #222; color: #ccc; border: 1px solid #444; padding: 8px 18px; cursor: pointer; font-size: 11px; border-radius: 3px; }
        .btn:hover { border-color: var(--accent); color: var(--accent); }
        .btn-primary { border-color: var(--accent); color: var(--accent); }

        /* Preview Area */
        #preview-area { width: 100%; height: 450px; background: #080808; border: 1px solid #333; overflow: hidden; position: relative; border-radius: 4px; }
        table { width: 100%; border-collapse: collapse; font-size: 12px; }
        th, td { border: 1px solid #444; padding: 8px; text-align: left; }
        th { color: var(--accent); background: #111; }
        tr:nth-child(even) { background: rgba(255,255,255,0.02); }
    </style>
    <script type="importmap">
        { "imports": { "three": "https://unpkg.com/three@0.160.0/build/three.module.js", "three/addons/": "https://unpkg.com/three@0.160.0/examples/jsm/" } }
    </script>
</head>
<body>
    <div id="selection-box"></div>
    <div id="toast-container"></div>

    <div id="menubar">
        <div class="menu-item">
            PROJECT
            <div class="submenu">
                <div class="sub-item" onclick="proj.action('new')">New Project</div>
                <div class="sub-item" onclick="proj.action('open')">Open Project...</div>
                <div class="sub-item" onclick="proj.action('save')">Save</div>
                <div class="sub-item" onclick="proj.action('saveas')">Save As...</div>
                <div class="ctx-sep"></div>
                <div class="sub-item" onclick="proj.genProject('input')">✨ Gen AI (Input)</div>
                <div class="sub-item" onclick="proj.genProject('api')">🤖 Gen AI (API)</div>
                <div class="ctx-sep"></div>
                <div class="sub-item" onclick="app.executeAll()" style="color:var(--accent);">▶ Execute Workflow</div>
                <div class="ctx-sep"></div>
                <div class="sub-item" onclick="ui.openProjectProps()">Properties</div>
            </div>
        </div>
        <div class="menu-item">
            ADD NODE
            <div class="submenu">
                <div class="sub-item" onclick="app.addNodeAtCenter('source')">+ Data Source</div>
                <div class="sub-item" onclick="app.addNodeAtCenter('process')">+ Process (JS)</div>
                <div class="sub-item" onclick="app.addNodeAtCenter('agent')">+ AI Agent</div>
                <div class="ctx-sep"></div>
                <div class="sub-item" onclick="app.addNodeAtCenter('loop')">+ Loop (Iterator)</div>
                <div class="sub-item" onclick="app.addNodeAtCenter('gate')">+ Gate (Condition)</div>
                <div class="sub-item" onclick="app.addNodeAtCenter('delay')">+ Delay (Wait)</div>
                <div class="ctx-sep"></div>
                <div class="sub-item" onclick="app.addNodeAtCenter('action')">+ DOM Action (RPA)</div>
                <div class="ctx-sep"></div>
                <div class="sub-item" onclick="app.addNodeAtCenter('join')">+ Sync (Join)</div>
                <div class="sub-item" onclick="app.addNodeAtCenter('view')">+ View Result</div>
            </div>
        </div>
        <div class="menu-item" style="margin-left:auto; cursor:default; color:#666; font-weight:bold;">NEURO LINKER v4.4</div>
    </div>

    <div id="lib-pane" tabindex="0">
        <div id="lib-toggle" onclick="lib.togglePane()">◀</div>
        <div class="lib-tabs">
            <div class="lib-tab active" id="tab-obj" onclick="lib.setTab('object')">OBJECTS</div>
            <div class="lib-tab" id="tab-flow" onclick="lib.setTab('workflow')">WORKFLOWS</div>
        </div>
        <div id="lib-toolbar">
            <div class="lib-btn" onclick="ui.showLibSaveDialog()">+ Add to Library</div>
        </div>
        <div id="lib-content" ondragover="lib.handleDragOver(event)" ondragleave="lib.handleDragLeave(event)" ondrop="lib.handleDrop(event)">
            <div style="padding:20px; text-align:center; color:#555;">Drag selection here to save.</div>
        </div>
    </div>

    <div id="ui-layer">
        <div class="status" id="status-text" style="position:absolute; top:45px; left:265px; color:#556677; font-size:10px;">READY</div>
        <div id="mode-bar">
            <div class="tool-group">
                <div class="group-label">Object</div>
                <div class="btn-row">
                    <button class="mode-btn active" onclick="app.setMode('select')">Select</button>
                    <button class="mode-btn" onclick="app.setMode('move')">Move</button>
                    <button class="mode-btn" onclick="app.setMode('link')">Link</button>
                </div>
            </div>
            <div style="width:1px; height:30px; background:#333; margin:0 5px;"></div>
            <div class="tool-group">
                <div class="group-label">Camera</div>
                <div class="btn-row">
                    <button class="mode-btn" onclick="app.setMode('rotate')">Rotate</button>
                    <button class="mode-btn" onclick="app.setMode('pan')">Pan</button>
                    <button class="mode-btn" onclick="app.setMode('zoom')">Zoom</button>
                    <button class="mode-btn" onclick="app.focusSelection()">Focus</button>
                </div>
            </div>
        </div>
    </div>

    <div id="ctx-node" class="ctx-menu interactive">
        <div class="ctx-item" onclick="app.ctxAction(event, 'exec')">▶ Execute</div>
        <div class="ctx-sep"></div>
        <div class="ctx-item" id="ctx-preview" onclick="app.ctxAction(event, 'preview')" style="display:none; color:var(--warn); font-weight:bold;">👁 Preview Results</div>
        <div class="ctx-item" onclick="app.ctxAction(event, 'edit')">Open Editor</div>
        <div class="ctx-item" onclick="app.ctxAction(event, 'saveLib')">Save to Library</div>
        <div class="ctx-item" onclick="app.ctxAction(event, 'copy')">Copy (C)</div>
        <div class="ctx-item" onclick="app.ctxAction(event, 'delete')" style="color:var(--danger)">Delete (Del)</div>
    </div>
    
    <div id="ctx-global" class="ctx-menu interactive">
        <div class="ctx-item" onclick="app.addNodeAtCenter('process')">Add Process</div>
        <div class="ctx-item" onclick="app.addNodeAtCenter('agent')">Add AI Agent</div>
        <div class="ctx-item" onclick="app.pasteAtCursor()">Paste (V)</div>
    </div>

    <div id="editor-dialog" class="modal-overlay">
        <div class="dialog-box interactive" style="width: 800px;">
            <div class="dlg-header"><span id="editor-title">EDITOR</span><button style="background:none; border:none; color:#555; cursor:pointer;" onclick="ui.closeDialog('editor-dialog')">✕</button></div>
            <div class="dlg-body">
                <div style="display:flex; gap:10px; margin-bottom:5px;">
                    <button class="btn" onclick="ui.editorAction('load-file')">📂 Load</button>
                    <button class="btn" onclick="ui.editorAction('save-file')">💾 Save</button>
                    <div style="width:1px; background:#444; margin:0 5px;"></div>
                    <button class="btn" onclick="ui.editorAction('ai-input')">✨ AI (Input)</button>
                    <button class="btn" onclick="ui.editorAction('ai-api')">🤖 AI (API)</button>
                </div>
                <div class="lbl-tag">CONTENT</div>
                <textarea id="editor-content" style="height:400px; font-size:13px; line-height:1.5; color:#aaffff; background:#080a0c; font-family:'Consolas', monospace;"></textarea>
            </div>
            <div class="btn-bar">
                <button class="btn" onclick="ui.closeDialog('editor-dialog')">Cancel</button>
                <button class="btn btn-primary" onclick="ui.saveEditor()">Apply</button>
            </div>
        </div>
    </div>

    <div id="prop-dialog" class="modal-overlay">
        <div class="dialog-box interactive">
            <div class="dlg-header"><span>PROPERTIES</span><button onclick="ui.closeDialog('prop-dialog')">✕</button></div>
            <div class="dlg-body"><div><div class="lbl-tag">API KEY</div><input id="prop-ai-key" type="password"></div></div>
            <div class="btn-bar"><button class="btn btn-primary" onclick="ui.saveProjectProps()">Save</button></div>
        </div>
    </div>

    <div id="preview-dialog" class="modal-overlay">
        <div class="dialog-box interactive" style="width:900px;">
            <div class="dlg-header"><span>RESULT PREVIEW</span><button onclick="ui.closeDialog('preview-dialog')">✕</button></div>
            <div class="dlg-body">
                <div style="display:flex; gap:10px; margin-bottom:5px;">
                    <button class="btn" onclick="ui.setPreviewMode('bar')">📊 Bar</button>
                    <button class="btn" onclick="ui.setPreviewMode('line')">📈 Line</button>
                    <button class="btn" onclick="ui.setPreviewMode('pie')">🥧 Pie</button>
                    <button class="btn" onclick="ui.setPreviewMode('table')">📋 Table</button>
                    <button class="btn" onclick="ui.setPreviewMode('text')">📝 Text</button>
                </div>
                <div id="preview-area"></div>
            </div>
            <div class="btn-bar"><button class="btn" onclick="ui.closeDialog('preview-dialog')">Close</button></div>
        </div>
    </div>

    <div id="input-dialog" class="modal-overlay">
        <div class="dialog-box interactive" style="width:400px;">
            <div class="dlg-header"><span>INPUT REQUIRED</span><button onclick="ui.closeDialog('input-dialog')">✕</button></div>
            <div class="dlg-body">
                <div id="input-msg" style="color:#ccc;">Enter name:</div>
                <input id="input-val" type="text" autocomplete="off">
            </div>
            <div class="btn-bar">
                <button class="btn" onclick="ui.closeDialog('input-dialog')">Cancel</button>
                <button class="btn btn-primary" id="input-ok-btn">OK</button>
            </div>
        </div>
    </div>

    <div id="ai-manual-dialog" class="modal-overlay">
        <div class="dialog-box interactive" style="width:650px;">
            <div class="dlg-header"><span>AI GENERATION (NODE)</span><button style="background:none; border:none; color:#555; cursor:pointer;" onclick="ui.closeDialog('ai-manual-dialog')">✕</button></div>
            <div class="dlg-body">
                <div class="lbl-tag">1. DESCRIBE GOAL</div>
                <div style="display:flex; gap:10px;">
                    <input type="text" id="ai-goal-input" placeholder="e.g. Filter rows where value > 100">
                    <button class="btn" onclick="ui.genPrompt()">Generate Prompt</button>
                </div>
                <div class="lbl-tag" style="margin-top:10px;">2. COPY & SEND TO AI</div>
                <div style="display:flex; gap:10px;">
                    <textarea id="ai-prompt-output" style="height:70px;" readonly></textarea>
                    <button class="btn" onclick="ui.copyPrompt('ai-prompt-output')">Copy</button>
                </div>
                <div class="lbl-tag" style="margin-top:10px;">3. PASTE CODE</div>
                <textarea id="ai-code-input" style="height:120px;" placeholder="Paste code here..."></textarea>
                <div style="display:flex; justify-content:flex-end;">
                    <button class="btn" onclick="ui.pasteTo('ai-code-input')">Paste</button>
                </div>
            </div>
            <div class="btn-bar">
                <button class="btn" onclick="ui.closeDialog('ai-manual-dialog')">Cancel</button>
                <button class="btn btn-primary" onclick="ui.applyAICode()">Apply</button>
            </div>
        </div>
    </div>

    <div id="ai-proj-dialog" class="modal-overlay">
        <div class="dialog-box interactive" style="width:700px;">
            <div class="dlg-header"><span>PROJECT GENERATION (AI)</span><button style="background:none; border:none; color:#555; cursor:pointer;" onclick="ui.closeDialog('ai-proj-dialog')">✕</button></div>
            <div class="dlg-body">
                <div class="lbl-tag">1. DESCRIBE THE SYSTEM</div>
                <div style="display:flex; gap:10px;">
                    <input type="text" id="ai-proj-input" placeholder="e.g. Load CSV data, filter by Year > 2023, then show Chart">
                    <button class="btn" onclick="proj.genPrompt()">Generate Prompt</button>
                </div>
                
                <div id="ai-proj-manual-section">
                    <div class="lbl-tag" style="margin-top:10px;">2. COPY PROMPT</div>
                    <div style="display:flex; gap:10px;">
                        <textarea id="ai-proj-prompt-out" style="height:140px; font-size:11px;" readonly></textarea>
                        <button class="btn" onclick="ui.copyPrompt('ai-proj-prompt-out')">Copy</button>
                    </div>
                    
                    <div class="lbl-tag" style="margin-top:10px;">3. PASTE JSON RESULT</div>
                    <textarea id="ai-proj-json-in" style="height:140px;" placeholder="Paste JSON here..."></textarea>
                    <div style="display:flex; justify-content:flex-end;">
                        <button class="btn" onclick="ui.pasteTo('ai-proj-json-in')">Paste</button>
                    </div>
                </div>
            </div>
            <div class="btn-bar">
                <button class="btn" onclick="ui.closeDialog('ai-proj-dialog')">Cancel</button>
                <button class="btn btn-primary" onclick="proj.applyGen()">Generate Project</button>
            </div>
        </div>
    </div>

    <script type="module">
        import * as THREE from 'three';
        import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
        import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
        import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
        import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js';
        import TWEEN from 'three/addons/libs/tween.module.js';

        // --- TOAST SYSTEM ---
        const toast = {
            show: (msg, type='info') => {
                const c = document.getElementById('toast-container');
                const t = document.createElement('div');
                t.className = `toast ${type}`;
                t.innerHTML = `<span style="font-size:16px">${type=='error'?'❌':type=='warn'?'⚠️':'ℹ️'}</span> ${msg}`;
                c.appendChild(t);
                requestAnimationFrame(()=>t.classList.add('show'));
                setTimeout(()=> { t.classList.remove('show'); setTimeout(()=>t.remove(), 300); }, 3000);
            },
            info: (m) => toast.show(m, 'info'),
            warn: (m) => toast.show(m, 'warn'),
            error: (m) => toast.show(m, 'error')
        };
        window.toast = toast;

        // --- STATE ---
        const state = {
            nodes: [], links: [], mode: 'select', selectedNodes: [], dragNode: null, linkStartNode: null,
            cursorPos: new THREE.Vector3(), projectHandle: null,
            clipboard: null,
            boxSel: { start: new THREE.Vector2(), active: false },
            nextNodeId: 1, executionId: 0,
            projectSettings: { aiKey: "" },
            inputCallback: null,
            genMode: 'input'
        };

        const COLORS = { source: 0x00ffff, process: 0xffaa00, agent: 0xaa00ff, join: 0xff0055, action: 0xff8800, loop: 0xffee00, gate: 0xffee00, delay: 0x888888, view: 0x00ff00 };

        // --- THREE.JS SETUP ---
        const scene = new THREE.Scene(); scene.fog = new THREE.FogExp2(0x020202, 0.0025);
        const camera = new THREE.PerspectiveCamera(50, window.innerWidth/window.innerHeight, 0.1, 2000); camera.position.set(0, 40, 60);
        const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true }); renderer.setSize(window.innerWidth, window.innerHeight);
        document.body.appendChild(renderer.domElement);
        renderer.domElement.draggable = false; 
        renderer.domElement.style.touchAction = 'none';

        const composer = new EffectComposer(renderer); composer.addPass(new RenderPass(scene, camera));
        composer.addPass(new UnrealBloomPass(new THREE.Vector2(window.innerWidth, window.innerHeight), 0.8, 0.4, 0.6));
        
        const controls = new OrbitControls(camera, renderer.domElement); 
        controls.enableDamping = true; controls.enabled = false; 

        scene.add(new THREE.GridHelper(200, 50, 0x222222, 0x111111)); scene.add(new THREE.AmbientLight(0xffffff, 0.3));
        const dl = new THREE.DirectionalLight(0xffffff, 0.8); dl.position.set(20,50,20); scene.add(dl);
        const raycaster = new THREE.Raycaster(); const mouse = new THREE.Vector2(); const plane = new THREE.Plane(new THREE.Vector3(0,1,0), 0);

        // --- CLASSES ---
        class SpatialNode {
            constructor(type, pos, id=null) {
                this.id = id || state.nextNodeId++;
                this.type = type;
                this.name = `${type.toUpperCase()}_${this.id}`;
                this.content = "// Default Content";
                this.inputs = []; this.outputs = []; 
                this.group = new THREE.Group(); this.group.position.copy(pos); this.group.userData = { isNode: true, obj: this };
                this.resultData = null; // STORE DATA
                
                let geo, col = COLORS[type] || 0xffffff;
                if(type==='source') geo = new THREE.CylinderGeometry(2,2,3,16); 
                else if(type==='agent') geo = new THREE.IcosahedronGeometry(2.5, 0); 
                else if(type==='action') geo = new THREE.DodecahedronGeometry(2.5, 0);
                else if(type==='loop') geo = new THREE.TorusGeometry(2, 0.8, 8, 20);
                else geo = new THREE.BoxGeometry(3,3,3);

                const mat = new THREE.MeshStandardMaterial({ color: col, emissive: col, emissiveIntensity: 0.3 });
                this.mesh = new THREE.Mesh(geo, mat);
                if(type==='view') this.mesh.rotation.x = -0.2;
                this.mesh.position.y = 2;
                
                const ringGeo = new THREE.RingGeometry(3.5, 3.8, 32); ringGeo.rotateX(-Math.PI/2);
                this.ring = new THREE.Mesh(ringGeo, new THREE.MeshBasicMaterial({ color: 0xffff00, transparent:true, opacity:0 }));
                this.group.add(this.mesh); this.group.add(this.ring);
                
                this.labelSprite = this.createLabel(this.name); this.group.add(this.labelSprite);
                scene.add(this.group);
                state.nodes.push(this);
                if(this.id >= state.nextNodeId) state.nextNodeId = this.id + 1;
            }
            createLabel(txt) {
                const cvs = document.createElement('canvas'); cvs.width = 256; cvs.height = 64; const ctx = cvs.getContext('2d');
                ctx.font = 'bold 36px Arial'; ctx.lineWidth = 4; ctx.strokeStyle = 'black'; ctx.strokeText(txt, 128, 45);
                ctx.fillStyle = "#ffffff"; ctx.textAlign = 'center'; ctx.fillText(txt, 128, 45);
                const sp = new THREE.Sprite(new THREE.SpriteMaterial({ map: new THREE.CanvasTexture(cvs) }));
                sp.position.y = 5.5; sp.scale.set(6, 1.5, 1); return sp;
            }
            updateLabel() { this.group.remove(this.labelSprite); this.labelSprite = this.createLabel(this.name); this.group.add(this.labelSprite); }
            setSelected(bool) { this.ring.material.opacity = bool ? 1 : 0; }
            serialize() { 
                return { id: this.id, type: this.type, name: this.name, x: this.group.position.x, y: this.group.position.y, z: this.group.position.z, content: this.content }; 
            }
            
            // --- SMART EXECUTION ---
            async execute(inputData = null) { 
                this.flash(); 
                toast.info(`Exec: ${this.name}`);
                
                let output = inputData;
                try {
                    // SOURCE: Try CSV, if fails, assume it's JS and try to run it
                    if(this.type === 'source') {
                        if(this.content.trim().startsWith('const') || this.content.trim().startsWith('var') || this.content.trim().startsWith('let') || this.content.includes('function')) {
                            // Smart Fix: If no return, try to extract the last defined variable
                            let code = this.content;
                            if(!code.includes('return ')) {
                                const match = code.match(/(const|let|var)\s+(\w+)\s*=/g);
                                if(match) {
                                    const lastVar = match[match.length-1].replace(/(const|let|var)\s+(\w+)\s*=.*/, '$2');
                                    code += `\nreturn ${lastVar};`;
                                }
                            }
                            const func = new Function(code);
                            output = func();
                        } else {
                            // Standard CSV Parsing
                            const lines = this.content.trim().split('\n');
                            if(lines.length > 0 && lines[0].includes(',')) {
                                const headers = lines[0].split(',').map(s=>s.trim());
                                output = lines.slice(1).map(line => { 
                                    if(!line.trim()) return null;
                                    const v = line.split(','); let o={}; 
                                    headers.forEach((h,i)=>{ if(v[i] !== undefined) o[h]=v[i].trim(); }); 
                                    return o; 
                                }).filter(x=>x);
                            }
                        }
                    } 
                    // PROCESS: Smart Fix for defined variables without return
                    else if(this.type === 'process' || this.type === 'filter') {
                        if(inputData) {
                            let code = this.content;
                            // Inject input data as 'data'
                            // Auto-Return Fix
                            if(!code.includes('return ') && code.includes('const ')) {
                                const match = code.match(/const\s+(\w+)\s*=/g);
                                if(match) {
                                    const lastVar = match[match.length-1].replace(/const\s+(\w+)\s*=.*/, '$1');
                                    code += `\nreturn ${lastVar};`;
                                }
                            }
                            const func = new Function('data', code);
                            output = func(inputData);
                        }
                    }
                    // VIEW: Just store data
                    else if(this.type === 'view') {
                        this.resultData = inputData;
                    }
                } catch(e) {
                    console.error("Exec Error", e);
                    toast.warn(`${this.name}: ${e.message}`);
                }

                this.outputs.forEach(l => { l.pulse(); setTimeout(()=>l.to.execute(output), 500); }); 
            }
            flash() { const m=this.mesh.material; const old=m.emissiveIntensity; m.emissiveIntensity=3.0; setTimeout(()=>m.emissiveIntensity=old, 200); }
        }

        class LaserLink {
            constructor(n1, n2) {
                this.from = n1; this.to = n2;
                n1.outputs.push(this); n2.inputs.push(this);
                const geo = new THREE.CylinderGeometry(0.2, 0.2, 1, 8, 1, true); geo.translate(0, 0.5, 0); geo.rotateX(Math.PI/2);
                const mat = new THREE.MeshBasicMaterial({ color: n1.mesh.material.color, transparent: true, opacity: 0.8 });
                this.mesh = new THREE.Mesh(geo, mat);
                const coneGeo = new THREE.ConeGeometry(0.6, 1.5, 8); coneGeo.rotateX(Math.PI/2);
                this.arrow = new THREE.Mesh(coneGeo, new THREE.MeshBasicMaterial({ color: n1.mesh.material.color }));
                this.packet = new THREE.Mesh(new THREE.SphereGeometry(0.4), new THREE.MeshBasicMaterial({color:0xffffff})); this.packet.visible=false;
                scene.add(this.mesh); scene.add(this.arrow); scene.add(this.packet);
                state.links.push(this); this.update();
            }
            update() {
                if(!this.from || !this.to) return;
                const p1 = this.from.group.position.clone().add(new THREE.Vector3(0,2,0));
                const p2 = this.to.group.position.clone().add(new THREE.Vector3(0,2,0));
                this.mesh.position.copy(p1); this.mesh.lookAt(p2); 
                const dist = p1.distanceTo(p2);
                this.mesh.scale.set(1, 1, dist - 1.5);
                const dir = p2.clone().sub(p1).normalize();
                const arrowPos = p2.clone().sub(dir.multiplyScalar(2.0));
                this.arrow.position.copy(arrowPos);
                this.arrow.lookAt(this.to.group.position.clone().add(new THREE.Vector3(0,2,0)));
            }
            pulse() { 
                this.packet.visible=true; let t={v:0}; 
                new TWEEN.Tween(t).to({v:1}, 500).onUpdate(()=>{
                    const p1 = this.from.group.position.clone().add(new THREE.Vector3(0,2,0));
                    const p2 = this.to.group.position.clone().add(new THREE.Vector3(0,2,0));
                    this.packet.position.lerpVectors(p1, p2, t.v);
                }).onComplete(()=>this.packet.visible=false).start(); 
            }
            dispose() { 
                scene.remove(this.mesh); scene.remove(this.arrow); scene.remove(this.packet); 
                this.mesh.geometry.dispose(); this.arrow.geometry.dispose();
                this.from.outputs = this.from.outputs.filter(l=>l!==this);
                this.to.inputs = this.to.inputs.filter(l=>l!==this);
            }
        }

        // --- LIBRARY MANAGER ---
        const lib = {
            currentTab: 'object',
            selectedItemName: null,
            togglePane: () => { document.getElementById('lib-pane').classList.toggle('collapsed'); },
            setTab: (t) => {
                lib.currentTab = t;
                document.querySelectorAll('.lib-tab').forEach(e => e.classList.remove('active'));
                document.getElementById(t === 'object' ? 'tab-obj' : 'tab-flow').classList.add('active');
                lib.refresh();
            },
            async getDirHandle(name) {
                if(!state.projectHandle) return null;
                return await state.projectHandle.getDirectoryHandle(name, {create:true});
            },
            async refresh() {
                const list = document.getElementById('lib-content');
                list.innerHTML = "";
                lib.selectedItemName = null;
                if(!state.projectHandle) { list.innerHTML = "<div style='padding:20px; text-align:center;'>Open a Project to use Library.</div>"; return; }
                const dirName = lib.currentTab === 'object' ? 'ObjectLib' : 'WorkFlowLib';
                try {
                    const dir = await lib.getDirHandle(dirName);
                    for await (const [name, handle] of dir.entries()) {
                        if(name.endsWith('.json')) {
                            const el = document.createElement('div');
                            el.className = 'lib-item';
                            el.draggable = true;
                            el.innerHTML = `<span>${name.replace('.json','')}</span>`;
                            el.onclick = () => lib.selectItem(el, name);
                            el.ondblclick = () => { lib.selectItem(el, name); lib.pasteSelectedItem(true); };
                            el.ondragstart = (e) => { 
                                e.dataTransfer.setData('application/json', JSON.stringify({type: 'lib-load', file: name, tab: lib.currentTab})); 
                            };
                            list.appendChild(el);
                        }
                    }
                } catch(e) { console.error(e); }
            },
            selectItem: (el, name) => {
                document.querySelectorAll('.lib-item').forEach(e=>e.classList.remove('selected'));
                el.classList.add('selected');
                lib.selectedItemName = name;
                document.getElementById('lib-pane').focus();
            },
            getConnectedGraph(startNodes) {
                const visitedNodes = new Set();
                const queue = [...startNodes];
                while(queue.length > 0) {
                    const n = queue.shift();
                    if(visitedNodes.has(n)) continue;
                    visitedNodes.add(n);
                    state.links.forEach(l => {
                        if(l.from === n && !visitedNodes.has(l.to)) queue.push(l.to);
                        if(l.to === n && !visitedNodes.has(l.from)) queue.push(l.from);
                    });
                }
                const nodes = Array.from(visitedNodes);
                const links = state.links.filter(l => visitedNodes.has(l.from) && visitedNodes.has(l.to));
                return { nodes, links };
            },
            async saveFromSelection(customName) {
                if(state.selectedNodes.length === 0) return toast.warn("Nothing selected.");
                if(!state.projectHandle) { toast.error("No project open."); return; }
                let nodesToSave = [], linksToSave = [];
                if (lib.currentTab === 'workflow') {
                    const graph = lib.getConnectedGraph(state.selectedNodes);
                    nodesToSave = graph.nodes; linksToSave = graph.links;
                } else {
                    nodesToSave = state.selectedNodes;
                    linksToSave = state.links.filter(l => state.selectedNodes.includes(l.from) && state.selectedNodes.includes(l.to));
                }
                const data = {
                    nodes: nodesToSave.map(n => n.serialize()),
                    links: linksToSave.map(l => ({ from: l.from.id, to: l.to.id }))
                };
                const dirName = lib.currentTab === 'object' ? 'ObjectLib' : 'WorkFlowLib';
                const dir = await lib.getDirHandle(dirName);
                const fh = await dir.getFileHandle(`${customName}.json`, {create:true});
                const w = await fh.createWritable();
                await w.write(JSON.stringify(data, null, 2));
                await w.close();
                lib.refresh();
                toast.info(`Saved "${customName}" to ${dirName}`);
            },
            async deleteSelectedItem() {
                if(!lib.selectedItemName) return;
                if(!confirm(`Delete ${lib.selectedItemName}?`)) return;
                const dirName = lib.currentTab === 'object' ? 'ObjectLib' : 'WorkFlowLib';
                const dir = await lib.getDirHandle(dirName);
                await dir.removeEntry(lib.selectedItemName);
                lib.refresh();
            },
            async pasteSelectedItem(atCenter=false) {
                if(!lib.selectedItemName) return;
                const dirName = lib.currentTab === 'object' ? 'ObjectLib' : 'WorkFlowLib';
                const dir = await lib.getDirHandle(dirName);
                const fh = await dir.getFileHandle(lib.selectedItemName);
                const content = JSON.parse(await (await fh.getFile()).text());
                state.clipboard = content;
                if(atCenter) {
                    const center = new THREE.Vector3();
                    raycaster.setFromCamera(new THREE.Vector2(0,0), camera);
                    raycaster.ray.intersectPlane(plane, center);
                    state.cursorPos.copy(center);
                }
                app.pasteAtCursor();
            },
            handleDragOver: (e) => { e.preventDefault(); document.getElementById('lib-content').classList.add('drop-zone-active'); },
            handleDragLeave: (e) => { document.getElementById('lib-content').classList.remove('drop-zone-active'); },
            handleDrop: (e) => {
                e.preventDefault();
                document.getElementById('lib-content').classList.remove('drop-zone-active');
                const dataStr = e.dataTransfer.getData('application/json');
                if (dataStr) {
                    try {
                        const data = JSON.parse(dataStr);
                        if(data.type === 'canvas-save') {
                            ui.showInputDialog("Save to Library (Enter Name):", (name) => { lib.saveFromSelection(name); });
                        }
                    } catch(e) { }
                }
            }
        };

        // --- KEYBOARD & INPUT ---
        window.addEventListener('keydown', (e) => {
            if(e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
            if(document.activeElement.id === 'lib-pane' || e.target.closest('#lib-pane')) {
                if(e.key === 'Delete') lib.deleteSelectedItem();
                if(e.key.toLowerCase() === 'v') lib.pasteSelectedItem(true);
                return; 
            }
            const camSpeed = 2;
            if(e.key === 'ArrowUp') { camera.position.z -= camSpeed; controls.target.z -= camSpeed; }
            if(e.key === 'ArrowDown') { camera.position.z += camSpeed; controls.target.z += camSpeed; }
            if(e.key === 'ArrowLeft') { camera.position.x -= camSpeed; controls.target.x -= camSpeed; }
            if(e.key === 'ArrowRight') { camera.position.x += camSpeed; controls.target.x += camSpeed; }
            if(e.key === 'PageUp') { camera.position.y = Math.max(5, camera.position.y - 5); }
            if(e.key === 'PageDown') { camera.position.y += 5; }
            if(e.key === 'Home') app.focusSelection();
            if(e.key === 'Delete') app.ctxAction(e, 'delete');
            if(e.key.toLowerCase() === 'c') app.copySelection();
            if(e.key.toLowerCase() === 'v') app.pasteAtCursor();
        });

        // --- APP CONTROLLER ---
        const app = {
            setMode: (m) => {
                state.mode = m; 
                document.querySelectorAll('.mode-btn').forEach(b => b.classList.remove('active'));
                const btn = document.querySelector(`button[onclick="app.setMode('${m}')"]`); if(btn) btn.classList.add('active');
                const isCamMode = ['rotate', 'pan', 'zoom'].includes(m);
                controls.enabled = isCamMode;
                if(isCamMode) {
                    if(m==='rotate') controls.mouseButtons = { LEFT: THREE.MOUSE.ROTATE, MIDDLE: THREE.MOUSE.DOLLY, RIGHT: THREE.MOUSE.PAN };
                    else if(m==='pan') controls.mouseButtons = { LEFT: THREE.MOUSE.PAN, MIDDLE: THREE.MOUSE.ROTATE, RIGHT: THREE.MOUSE.PAN };
                    else if(m==='zoom') controls.mouseButtons = { LEFT: THREE.MOUSE.DOLLY, MIDDLE: THREE.MOUSE.DOLLY, RIGHT: THREE.MOUSE.PAN };
                } else {
                    controls.enabled = false;
                }
                renderer.domElement.draggable = false;
            },
            resetCamera: () => {
                new TWEEN.Tween(camera.position).to({x:0, y:40, z:60}, 800).easing(TWEEN.Easing.Cubic.Out).start();
                new TWEEN.Tween(controls.target).to({x:0, y:0, z:0}, 800).easing(TWEEN.Easing.Cubic.Out).start();
                app.setMode('select');
            },
            focusSelection: () => {
                if(state.selectedNodes.length === 0) { toast.warn("No node selected"); return; }
                const p = state.selectedNodes[0].group.position;
                new TWEEN.Tween(camera.position).to({x:p.x, y:p.y+40, z:p.z+30}, 800).easing(TWEEN.Easing.Cubic.Out).start();
                new TWEEN.Tween(controls.target).to({x:p.x, y:p.y, z:p.z}, 800).easing(TWEEN.Easing.Cubic.Out).onComplete(() => {
                    app.setMode('rotate'); 
                }).start();
            },
            selectNode: (node, ctrlKey) => {
                if(ctrlKey) {
                    if(state.selectedNodes.includes(node)) {
                        node.setSelected(false);
                        state.selectedNodes = state.selectedNodes.filter(n => n !== node);
                    } else {
                        state.selectedNodes.push(node);
                        node.setSelected(true);
                    }
                } else {
                    if(!state.selectedNodes.includes(node)) {
                        app.clearSelection();
                        state.selectedNodes.push(node);
                        node.setSelected(true);
                    }
                }
            },
            clearSelection: () => {
                state.selectedNodes.forEach(n => n.setSelected(false));
                state.selectedNodes = [];
            },
            copySelection: () => {
                if(state.selectedNodes.length === 0) return;
                const nodesData = state.selectedNodes.map(n => n.serialize());
                const linksData = state.links.filter(l => state.selectedNodes.includes(l.from) && state.selectedNodes.includes(l.to))
                                           .map(l => ({ from: l.from.id, to: l.to.id }));
                state.clipboard = { nodes: nodesData, links: linksData };
                toast.info(`Copied ${nodesData.length} items`);
            },
            pasteAtCursor: () => {
                if(!state.clipboard) return;
                const center = state.clipboard.nodes.reduce((acc, n) => ({x:acc.x+n.x, z:acc.z+n.z}), {x:0, z:0});
                center.x /= state.clipboard.nodes.length; center.z /= state.clipboard.nodes.length;
                const offset = { x: state.cursorPos.x - center.x, z: state.cursorPos.z - center.z };
                const idMap = {};
                state.clipboard.nodes.forEach(d => {
                    const n = new SpatialNode(d.type, new THREE.Vector3(d.x + offset.x, d.y, d.z + offset.z));
                    n.content = d.content; n.name = d.name + "_Copy"; n.updateLabel(); idMap[d.id] = n;
                });
                state.clipboard.links.forEach(l => { if(idMap[l.from] && idMap[l.to]) new LaserLink(idMap[l.from], idMap[l.to]); });
                app.clearSelection();
                Object.values(idMap).forEach(n => n.setSelected(true));
                state.selectedNodes = Object.values(idMap);
                toast.info("Pasted");
            },
            addNodeAtCenter: (type) => {
                raycaster.setFromCamera(new THREE.Vector2(0,0), camera);
                const pt = new THREE.Vector3(); raycaster.ray.intersectPlane(plane, pt);
                const n = new SpatialNode(type, pt); 
                app.clearSelection(); app.selectNode(n, false);
                document.querySelectorAll('.ctx-menu').forEach(e => e.style.display='none');
            },
            ctxAction: (e, act) => {
                if(e) e.stopPropagation(); 
                document.querySelectorAll('.ctx-menu').forEach(e => e.style.display='none');
                
                const targetNodes = state.selectedNodes;
                
                if(act==='delete') {
                    state.links = state.links.filter(l => {
                        if(targetNodes.includes(l.from) || targetNodes.includes(l.to)) { l.dispose(); return false; }
                        return true;
                    });
                    targetNodes.forEach(n => { scene.remove(n.group); });
                    state.nodes = state.nodes.filter(n => !targetNodes.includes(n));
                    state.selectedNodes = [];
                }
                if(act==='copy') app.copySelection();
                if(act==='saveLib') { ui.showLibSaveDialog(); }
                if(act==='edit') { 
                    if(targetNodes.length === 1) ui.openEditor(targetNodes[0]); 
                    else if(targetNodes.length === 0) toast.warn("No node selected");
                    else toast.warn("Select one node to edit"); 
                }
                if(act === 'exec') { 
                    if(targetNodes.length > 0) targetNodes.forEach(n => n.execute(null)); 
                    else toast.warn("No node selected to execute");
                }
                // PREVIEW ACTION
                if(act === 'preview') {
                    if(targetNodes.length === 1 && targetNodes[0].type === 'view') {
                        ui.openPreview(targetNodes[0]);
                    } else {
                        toast.warn("Select one View node");
                    }
                }
            },
            executeAll: () => {
                state.executionId++;
                state.nodes.filter(n => n.type === 'source').forEach(n => n.execute(null));
                toast.info("Execution Started");
            }
        };

        // --- UI ---
        const ui = {
            targetNode: null,
            openDialog: (id) => { const d = document.getElementById(id); d.classList.remove('closing'); d.classList.add('active', 'opening'); },
            closeDialog: (id) => { const d = document.getElementById(id); d.classList.remove('opening'); d.classList.add('closing'); setTimeout(() => { d.classList.remove('active', 'closing'); }, 400); },

            showInputDialog: (msg, cb) => {
                document.getElementById('input-msg').innerText = msg;
                document.getElementById('input-val').value = "";
                state.inputCallback = cb;
                ui.openDialog('input-dialog');
                setTimeout(() => document.getElementById('input-val').focus(), 400);
            },
            showLibSaveDialog: () => {
                if(state.selectedNodes.length === 0) return toast.warn("Nothing selected");
                ui.showInputDialog("Save to Library (Enter Name):", (name) => { lib.saveFromSelection(name); });
            },
            openEditor: (node) => { 
                ui.targetNode = node; 
                document.getElementById('editor-title').innerText = `EDIT: ${node.name}`; 
                document.getElementById('editor-content').value = node.content; 
                ui.openDialog('editor-dialog'); 
            },
            saveEditor: () => { 
                if(ui.targetNode) ui.targetNode.content = document.getElementById('editor-content').value; 
                ui.closeDialog('editor-dialog'); 
                toast.info("Saved"); 
            },
            editorAction: async (act) => {
                if(act === 'load-file') {
                    if(!state.projectHandle) return alert("Open project first");
                    try {
                        const typeDir = await state.projectHandle.getDirectoryHandle(getDirForType(ui.targetNode.type), {create:true});
                        const [fh] = await window.showOpenFilePicker({ startIn: typeDir }); 
                        const file = await fh.getFile();
                        document.getElementById('editor-content').value = await file.text();
                    } catch(e) { }
                }
                if(act === 'save-file') {
                    if(!state.projectHandle) return alert("Open project first");
                    const typeDir = await state.projectHandle.getDirectoryHandle(getDirForType(ui.targetNode.type), {create:true});
                    const fh = await typeDir.getFileHandle(`${ui.targetNode.name}.txt`, {create:true});
                    const w = await fh.createWritable();
                    await w.write(document.getElementById('editor-content').value);
                    await w.close();
                    toast.info(`Saved to ${getDirForType(ui.targetNode.type)}`);
                }
                if(act === 'ai-input') { ui.openDialog('ai-manual-dialog'); }
                if(act === 'ai-api') {
                    if(!state.projectSettings.aiKey) return alert("Set API Key in Properties");
                    document.getElementById('editor-content').value += "\n\n// AI Gen (API): Executed.";
                }
            },
            genPrompt: () => {
                const goal = document.getElementById('ai-goal-input').value;
                const type = ui.targetNode ? ui.targetNode.type : "Node";
                document.getElementById('ai-prompt-output').value = `[TASK] Write content for a "${type}" node in Neuro Linker.\n[GOAL] ${goal}`;
            },
            copyPrompt: (id) => { document.getElementById(id).select(); document.execCommand('copy'); toast.info("Copied"); },
            pasteTo: async (id) => { try { const text = await navigator.clipboard.readText(); document.getElementById(id).value = text; } catch(e){alert("Use Ctrl+V");} },
            applyAICode: () => { document.getElementById('editor-content').value = document.getElementById('ai-code-input').value; ui.closeDialog('ai-manual-dialog'); },
            openProjectProps: () => ui.openDialog('prop-dialog'),
            saveProjectProps: () => { state.projectSettings.aiKey = document.getElementById('prop-ai-key').value; ui.closeDialog('prop-dialog'); },
            
            // --- PREVIEW SYSTEM ---
            openPreview: (node) => { 
                ui.targetNode = node; 
                ui.openDialog('preview-dialog'); 
                
                // Smart Preview: If node content looks like code (new Chart, getElementById), execute it
                if (node.content && (node.content.includes('new Chart') || node.content.includes('document.getElementById'))) {
                    ui.renderScriptPreview();
                } else {
                    ui.renderPreview('bar'); 
                }
            },
            setPreviewMode: (mode) => ui.renderPreview(mode),
            renderScriptPreview: () => {
                const area = document.getElementById('preview-area');
                area.innerHTML = '';
                
                // 1. Detect ID from code (e.g. getElementById('myChart'))
                const match = ui.targetNode.content.match(/getElementById\(['"](.+?)['"]\)/);
                const id = match ? match[1] : 'myChart';
                
                // 2. Inject Canvas
                const canvas = document.createElement('canvas');
                canvas.id = id;
                canvas.style.width = '100%';
                canvas.style.height = '100%';
                area.appendChild(canvas);
                
                // 3. Execute Script
                try {
                    const func = new Function(ui.targetNode.content);
                    func();
                } catch(e) {
                    console.error(e);
                    area.innerHTML += `<div style="color:red; padding:10px;">Script Error: ${e.message}</div>`;
                }
            },
            renderPreview: (mode) => {
                const area = document.getElementById('preview-area'); area.innerHTML = "";
                const data = ui.targetNode.resultData; 
                
                if(!data || !Array.isArray(data) || data.length === 0) { 
                    area.innerHTML = "<div style='padding:50px; color:#666; text-align:center;'>NO DATA AVAILABLE<br>Please run 'EXECUTE'</div>"; 
                    return; 
                }

                const keys = Object.keys(data[0]);
                const labelKey = keys.includes('label') ? 'label' : (keys.find(k => typeof data[0][k] === 'string') || keys[0]);
                const valueKey = keys.includes('value') ? 'value' : (keys.find(k => typeof data[0][k] === 'number' || !isNaN(parseFloat(data[0][k]))) || keys[1]);

                if(mode === 'text') { 
                    area.innerHTML = `<pre style="padding:15px; color:#fff; font-family:monospace; height:100%; overflow:auto;">${JSON.stringify(data, null, 2)}</pre>`; 
                } 
                else if (mode === 'table') {
                    let h = "<div style='height:100%; overflow:auto;'><table><thead><tr>"; 
                    keys.forEach(k=>h+=`<th>${k}</th>`); 
                    h+="</tr></thead><tbody>";
                    data.forEach(r => { h+="<tr>"; keys.forEach(k=>h+=`<td>${r[k]}</td>`); h+="</tr>"; }); 
                    area.innerHTML = h + "</tbody></table></div>";
                } else {
                    const w = 800, h = 420, pad = 50;
                    let svg = `<svg width="100%" height="100%" viewBox="0 0 ${w} ${h}" xmlns="http://www.w3.org/2000/svg" style="background:#080808">`;
                    const vals = data.map(d=>parseFloat(d[valueKey])||0); 
                    
                    let minVal = Math.min(0, ...vals);
                    let maxVal = Math.max(0, ...vals);
                    if(minVal === 0 && maxVal === 0) maxVal = 1;
                    const range = maxVal - minVal;
                    const zeroY = h - pad - ((0 - minVal) / range) * (h - pad*2);

                    if(mode === 'bar') {
                        const barW = (w - pad*2) / vals.length;
                        vals.forEach((v, i) => { 
                            const barH = (Math.abs(v) / range) * (h - pad*2);
                            let y = zeroY;
                            let col = "#00ffcc";
                            if(v >= 0) { y = zeroY - barH; } else { col = "#ff4466"; }
                            
                            svg += `<rect x="${pad + i*barW + 5}" y="${y}" width="${Math.max(1, barW-10)}" height="${Math.max(1, barH)}" fill="${col}" fill-opacity="0.8"/>`;
                            svg += `<text x="${pad + i*barW + barW/2}" y="${h-15}" fill="#888" font-size="10" text-anchor="middle" transform="rotate(0, ${pad + i*barW + barW/2}, ${h-15})">${String(data[i][labelKey]).substring(0,10)}</text>`;
                        });
                        svg += `<line x1="${pad}" y1="${zeroY}" x2="${w-pad}" y2="${zeroY}" stroke="#555" stroke-width="1"/>`;
                    } else if (mode === 'line') {
                        const step = (w - pad*2) / (vals.length-1 || 1); let pts = "";
                        const getY = (v) => h - pad - ((v - minVal)/range)*(h-pad*2);
                        vals.forEach((v,i) => pts += `${pad + i*step},${getY(v)} `);
                        svg += `<polyline points="${pts}" fill="none" stroke="#ffaa00" stroke-width="2"/>`;
                        svg += `<line x1="${pad}" y1="${getY(0)}" x2="${w-pad}" y2="${getY(0)}" stroke="#333" stroke-dasharray="4"/>`;
                        vals.forEach((v,i) => svg += `<circle cx="${pad + i*step}" cy="${getY(v)}" r="4" fill="#fff"/>`);
                    } else if (mode === 'pie') {
                        const absVals = vals.map(Math.abs);
                        const total = absVals.reduce((a,b)=>a+b, 0); 
                        let acc = 0; const cx = w/2, cy = h/2, r = 160; const colors = ["#00ffcc", "#ff00ff", "#ffaa00", "#0088ff", "#ff5555"];
                        absVals.forEach((v,i) => { 
                            if(v<=0)return; 
                            const ang = (v/total) * Math.PI * 2; 
                            const x1 = cx + r*Math.cos(acc), y1 = cy + r*Math.sin(acc); 
                            const x2 = cx + r*Math.cos(acc+ang), y2 = cy + r*Math.sin(acc+ang); 
                            const large = ang > Math.PI ? 1 : 0; 
                            svg += `<path d="M ${cx} ${cy} L ${x1} ${y1} A ${r} ${r} 0 ${large} 1 ${x2} ${y2} Z" fill="${colors[i%colors.length]}" stroke="#000"/>`;
                            svg += `<text x="${cx + (r+30)*Math.cos(acc+ang/2)}" y="${cy + (r+30)*Math.sin(acc+ang/2)}" fill="#fff" font-size="12" text-anchor="middle">${String(data[i][labelKey]).substring(0,10)}</text>`; 
                            acc += ang; 
                        });
                    }
                    area.innerHTML = svg + "</svg>";
                }
            }
        };

        document.getElementById('input-ok-btn').addEventListener('click', () => { const val = document.getElementById('input-val').value; if(val && state.inputCallback) state.inputCallback(val); ui.closeDialog('input-dialog'); });
        document.getElementById('input-val').addEventListener('keydown', (e) => { if(e.key === 'Enter') document.getElementById('input-ok-btn').click(); });

        function getDirForType(t) { 
            const map = { source:'Data', process:'Process', agent:'Agent', join:'Join', view:'View', action:'Action' };
            return map[t] || 'Misc';
        }

        const proj = {
            async action(act) {
                if(act === 'new') { if(confirm("New Project?")) { state.nodes.forEach(n=>scene.remove(n.group)); state.nodes=[]; state.links.forEach(l=>l.dispose()); state.links=[]; if(await proj.ensureHandle()) await proj.initFolders(); } }
                if(act === 'open') { 
                    try {
                        const h = await window.showDirectoryPicker(); state.projectHandle = h;
                        const f = await h.getFileHandle('settings.json');
                        const json = JSON.parse(await (await f.getFile()).text());
                        proj.loadFromJSON(json);
                        document.getElementById('status-text').innerText = "PROJ: " + h.name;
                        lib.refresh();
                    } catch(e) { console.error(e); }
                }
                if(act === 'save') {
                    if(!state.projectHandle && !(await proj.ensureHandle())) return;
                    await proj.initFolders();
                    const d = { nextId: state.nextNodeId, nodes: state.nodes.map(n=>n.serialize()), links: state.links.map(l=>({from:l.from.id, to:l.to.id})) };
                    const w = await (await state.projectHandle.getFileHandle('settings.json',{create:true})).createWritable();
                    await w.write(JSON.stringify(d,null,2)); await w.close();
                    toast.info("Project Saved");
                    lib.refresh();
                }
                if(act === 'saveas') {
                    try {
                        const h = await window.showDirectoryPicker();
                        state.projectHandle = h;
                        await this.action('save');
                        document.getElementById('status-text').innerText = "PROJ: " + h.name;
                    } catch (e) { /* cancelled */ }
                }
            },
            async ensureHandle() { try { state.projectHandle = await window.showDirectoryPicker(); return true; } catch(e){return false;} },
            async initFolders() {
                const dirs = ['Data', 'Process', 'Agent', 'Join', 'View', 'Action', 'ObjectLib', 'WorkFlowLib', 'Misc'];
                for(const d of dirs) await state.projectHandle.getDirectoryHandle(d, {create:true});
            },
            
            genProject(m) { 
                state.genMode = m;
                ui.openDialog('ai-proj-dialog');
                const manual = document.getElementById('ai-proj-manual-section');
                manual.style.display = (m === 'input') ? 'block' : 'none';
                if(m === 'api' && !state.projectSettings.aiKey) toast.warn("Setup API Key first");
            },
            
            genPrompt() {
                const req = document.getElementById('ai-proj-input').value;
                document.getElementById('ai-proj-prompt-out').value = `
[ROLE] System Architect for "Neuro Linker v1.1".
[STRICT RULES]
1. Node Types: source, process, agent, action, loop, gate, delay, join, view.
2. Layout: Align nodes logically on X axis.
3. Output: JSON only.
4. IMPORTANT: Use single quotes (') instead of double quotes (") inside content strings to prevent JSON syntax errors.

[GOAL] ${req}

[OUTPUT SCHEMA]
{
  "nodes": [
    { "id": 1, "type": "source", "name": "Src", "x": -10, "y": 0, "z": 0, "content": "// data" },
    { "id": 2, "type": "process", "name": "Proc", "x": 10, "y": 0, "z": 0, "content": "// js code" }
  ],
  "links": [ { "from": 1, "to": 2 } ]
}
`;
                if(state.genMode === 'api') {
                    setTimeout(() => { 
                        toast.info("API Sim: Done"); 
                        this.applyGen(true); 
                    }, 1000);
                }
            },
            
            applyGen(useSim = false) {
                try {
                    let json;
                    if(useSim || state.genMode === 'api') {
                        json = {
                            nodes: [
                                {id:1, type:'source', name:'Sim_Data', x:-15, y:0, z:0, content:'// Generated Data'},
                                {id:2, type:'process', name:'Sim_Proc', x:0, y:0, z:0, content:'// Generated Logic'},
                                {id:3, type:'view', name:'Sim_View', x:15, y:0, z:0}
                            ],
                            links: [{from:1, to:2}, {from:2, to:3}]
                        };
                    } else {
                        json = JSON.parse(document.getElementById('ai-proj-json-in').value);
                    }
                    this.loadFromJSON(json);
                    ui.closeDialog('ai-proj-dialog');
                    toast.info("Project Generated");
                } catch(e) { 
                    console.error(e);
                    alert("JSON Syntax Error: " + e.message + "\nCheck for unescaped quotes in AI response.");
                    toast.error("JSON Error");
                }
            },

            loadFromJSON(json) {
                state.nodes.forEach(n=>scene.remove(n.group)); state.nodes=[]; 
                state.links.forEach(l=>l.dispose()); state.links=[];
                state.nextNodeId = 1;
                json.nodes.forEach(d => {
                    const n = new SpatialNode(d.type, new THREE.Vector3(d.x, d.y, d.z), d.id);
                    n.name = d.name; n.content = d.content || ""; n.updateLabel();
                });
                if(json.links) {
                    json.links.forEach(l => {
                        const f = state.nodes.find(n=>n.id===l.from);
                        const t = state.nodes.find(n=>n.id===l.to);
                        if(f&&t) new LaserLink(f,t);
                    });
                }
            }
        };

        // --- INTERACTION HANDLERS ---
        window.addEventListener('pointerdown', (e) => {
            if(e.button!==0 || e.target.closest('.interactive') || e.target.closest('#menubar') || e.target.closest('#mode-bar') || e.target.closest('#lib-pane')) return;
            
            mouse.x = (e.clientX/window.innerWidth)*2-1; mouse.y = -(e.clientY/window.innerHeight)*2+1;
            raycaster.setFromCamera(mouse, camera);
            const hits = raycaster.intersectObjects(scene.children, true);
            const hit = hits.find(h => h.object.parent && h.object.parent.userData.isNode);
            raycaster.ray.intersectPlane(plane, state.cursorPos);

            if (state.mode === 'select') {
                if (hit) {
                    const node = hit.object.parent.userData.obj;
                    app.selectNode(node, e.ctrlKey);
                    renderer.domElement.draggable = true;
                } else if(!e.ctrlKey) {
                    app.clearSelection();
                    state.boxSel.active = true;
                    state.boxSel.start.set(e.clientX, e.clientY);
                    const box = document.getElementById('selection-box');
                    box.style.display = 'block';
                    box.style.left = e.clientX + 'px'; box.style.top = e.clientY + 'px';
                    box.style.width = '0px'; box.style.height = '0px';
                    renderer.domElement.draggable = false;
                }
            }
            else if (state.mode === 'move') {
                renderer.domElement.draggable = false;
                if (hit) {
                    const node = hit.object.parent.userData.obj;
                    if(!state.selectedNodes.includes(node)) { app.clearSelection(); app.selectNode(node, false); }
                    state.dragNode = node; 
                }
            }
            else if (state.mode === 'link') {
                renderer.domElement.draggable = false;
                if (hit) {
                    const node = hit.object.parent.userData.obj;
                    state.linkStartNode = node; 
                }
            }
        });

        window.addEventListener('pointermove', (e) => {
            if(state.dragNode && state.mode==='move') {
                mouse.x = (e.clientX/window.innerWidth)*2-1; mouse.y = -(e.clientY/window.innerHeight)*2+1;
                raycaster.setFromCamera(mouse, camera);
                const pt = new THREE.Vector3(); raycaster.ray.intersectPlane(plane, pt);
                if(pt) {
                    const delta = pt.clone().sub(state.dragNode.group.position);
                    state.selectedNodes.forEach(n => n.group.position.add(delta));
                    state.links.forEach(l => l.update());
                }
            }
            if(state.boxSel.active && state.mode === 'select') {
                const currentX = e.clientX; const currentY = e.clientY;
                const startX = state.boxSel.start.x; const startY = state.boxSel.start.y;
                const x = Math.min(currentX, startX); const y = Math.min(currentY, startY);
                const w = Math.abs(currentX - startX); const h = Math.abs(currentY - startY);
                const box = document.getElementById('selection-box');
                box.style.left = x + 'px'; box.style.top = y + 'px';
                box.style.width = w + 'px'; box.style.height = h + 'px';
            }
            mouse.x = (e.clientX/window.innerWidth)*2-1; mouse.y = -(e.clientY/window.innerHeight)*2+1;
            raycaster.setFromCamera(mouse, camera); raycaster.ray.intersectPlane(plane, state.cursorPos);
        });

        const endDrag = () => {
            if(state.boxSel.active) {
                state.boxSel.active = false;
                const box = document.getElementById('selection-box');
                const startX = parseFloat(box.style.left); const startY = parseFloat(box.style.top);
                const w = parseFloat(box.style.width); const h = parseFloat(box.style.height);
                box.style.display = 'none';

                if(w > 5 || h > 5) {
                    state.nodes.forEach(n => {
                        const p = n.group.position.clone().project(camera);
                        const sx = (p.x * .5 + .5) * window.innerWidth;
                        const sy = (-(p.y * .5) + .5) * window.innerHeight;
                        if(sx > startX && sx < (startX + w) && sy > startY && sy < (startY + h)) {
                             app.selectNode(n, true);
                        }
                    });
                }
            }
            if(state.dragNode) state.dragNode = null;
            if(state.linkStartNode) {
                 mouse.x = (event.clientX/window.innerWidth)*2-1; mouse.y = -(event.clientY/window.innerHeight)*2+1;
                 raycaster.setFromCamera(mouse, camera);
                 const hits = raycaster.intersectObjects(scene.children, true);
                 const hit = hits.find(h => h.object.parent && h.object.parent.userData.isNode);
                 if(hit) {
                     const target = hit.object.parent.userData.obj;
                     if(target !== state.linkStartNode) new LaserLink(state.linkStartNode, target);
                 }
                 state.linkStartNode = null;
            }
            renderer.domElement.draggable = false;
        };

        window.addEventListener('pointerup', endDrag);

        document.body.addEventListener('dragstart', (e) => {
            if(state.mode === 'select' && e.target === renderer.domElement && state.selectedNodes.length > 0) {
                e.dataTransfer.setData('application/json', JSON.stringify({type: 'canvas-save'}));
                e.dataTransfer.effectAllowed = 'copy';
                const img = new Image(); img.src = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
                e.dataTransfer.setDragImage(img, 0, 0);
            } else if (e.target.classList.contains('lib-item')) {
            } else { e.preventDefault(); }
        });

        document.body.addEventListener('dragover', e => e.preventDefault());
        document.body.addEventListener('drop', async (e) => {
            e.preventDefault();
            const rect = renderer.domElement.getBoundingClientRect();
            mouse.x = ((e.clientX - rect.left) / rect.width) * 2 - 1;
            mouse.y = -((e.clientY - rect.top) / rect.height) * 2 + 1;
            raycaster.setFromCamera(mouse, camera);
            raycaster.ray.intersectPlane(plane, state.cursorPos);

            const dataStr = e.dataTransfer.getData('application/json');
            if (dataStr) {
                try {
                    const data = JSON.parse(dataStr);
                    if(data.type === 'lib-load' && e.target === renderer.domElement) {
                        const dirName = data.tab === 'object' ? 'ObjectLib' : 'WorkFlowLib';
                        const dir = await lib.getDirHandle(dirName);
                        const f = await dir.getFileHandle(data.file);
                        const content = JSON.parse(await (await f.getFile()).text());
                        state.clipboard = content;
                        app.pasteAtCursor(); 
                    }
                    if(data.type === 'canvas-save' && e.target.closest('#lib-content')) {
                        ui.showLibSaveDialog();
                    }
                } catch(err) { console.error(err); }
            }
        });

        window.addEventListener('contextmenu', (e) => {
            if(e.target.closest('.interactive') || e.target.closest('#lib-pane')) return;
            e.preventDefault();
            mouse.x = (e.clientX/window.innerWidth)*2-1; mouse.y = -(e.clientY/window.innerHeight)*2+1;
            raycaster.setFromCamera(mouse, camera);
            const hits = raycaster.intersectObjects(scene.children, true);
            const hit = hits.find(h => h.object.parent && h.object.parent.userData.isNode);
            const m = hit ? document.getElementById('ctx-node') : document.getElementById('ctx-global');
            document.querySelectorAll('.ctx-menu').forEach(e=>e.style.display='none');
            
            if(hit) {
                const node = hit.object.parent.userData.obj;
                if(!state.selectedNodes.includes(node)) { app.clearSelection(); app.selectNode(node, false); }
                // Show/Hide Preview Item
                const pvBtn = document.getElementById('ctx-preview');
                if(node.type === 'view') pvBtn.style.display = 'block';
                else pvBtn.style.display = 'none';
            }
            m.style.display = 'flex'; m.style.left = e.pageX + 'px'; m.style.top = e.pageY + 'px';
        });
        window.addEventListener('click', (e) => { if(!e.target.closest('.ctx-menu')) document.querySelectorAll('.ctx-menu').forEach(e=>e.style.display='none'); });
        window.addEventListener('dblclick', (e) => {
            if(e.target.closest('.interactive')) return;
            mouse.x = (e.clientX/window.innerWidth)*2-1; mouse.y = -(e.clientY/window.innerHeight)*2+1;
            raycaster.setFromCamera(mouse, camera);
            const hits = raycaster.intersectObjects(scene.children, true);
            const hit = hits.find(h => h.object.parent && h.object.parent.userData.isNode);
            if(hit) ui.openEditor(hit.object.parent.userData.obj);
        });

        function animate() { requestAnimationFrame(animate); TWEEN.update(); controls.update(); composer.render(); }
        animate();
        app.setMode('select'); 

        window.app = app; window.ui = ui; window.proj = proj; window.lib = lib;
        new SpatialNode('source', new THREE.Vector3(-10,0,0));
    </script>
</body>
</html>

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

コメント

コメントするには、 ログイン または 会員登録 をお願いします。
未来っぽいグラフ作成ツール「Neuro Linker」|古井和雄
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