未来っぽいグラフ作成ツール「Neuro Linker」
・ダウンロードされる方はこちら。↓
・ソースコードはこちら。↓
<!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>

コメント