mirror of
https://github.com/ParisNeo/lollms_hub.git
synced 2026-05-04 03:01:01 -04:00
This commit updates several API routes, data store definitions, and related templates for the admin section. Changes include: - Refactoring in `admin.py`, `datastores.py`, `evaluations.py`, and `proxy.py`. - Updates to schema settings and static assets. - Modifications to various admin templates (`base.html`, `dashboard.html`, etc.). - Updates to agent management logic (`agentManager.ts`, `extensionState.ts`) and UI components.
1736 lines
81 KiB
HTML
1736 lines
81 KiB
HTML
{% extends "admin/base.html" %}
|
||
{% block title %}Workflow Architect{% endblock %}
|
||
|
||
{% block content %}
|
||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||
<!-- Core LiteGraph Dependencies -->
|
||
<link rel="stylesheet" type="text/css" href="/static/vendor/litegraph.css">
|
||
<script type="text/javascript" src="/static/vendor/gl-matrix.js"></script>
|
||
<script type="text/javascript" src="/static/vendor/litegraph.js"></script>
|
||
<script src="{{ url_for('static', path='vendor/marked.min.js') }}"></script>
|
||
<!-- CodeMirror for Node Studio -->
|
||
<link rel="stylesheet" href="{{ url_for('static', path='vendor/codemirror.min.css') }}">
|
||
<link rel="stylesheet" href="{{ url_for('static', path='vendor/codemirror-monokai.min.css') }}">
|
||
<script src="{{ url_for('static', path='vendor/codemirror.min.js') }}"></script>
|
||
<script src="{{ url_for('static', path='vendor/codemirror-javascript.min.js') }}"></script>
|
||
<script src="{{ url_for('static', path='vendor/codemirror-python.min.js') }}"></script>
|
||
<script src="{{ url_for('static', path='vendor/js-yaml.min.js') }}"></script>
|
||
|
||
<style>
|
||
/* --- Modal Scrollbar Fix --- */
|
||
.help-modal-content {
|
||
max-height: 60vh;
|
||
overflow-y: auto;
|
||
padding-right: 10px;
|
||
}
|
||
|
||
/* --- Layout Overrides --- */
|
||
main {
|
||
padding: 0 !important;
|
||
margin: 0 !important;
|
||
overflow: hidden !important;
|
||
height: 100% !important;
|
||
display: flex !important;
|
||
flex-direction: column !important;
|
||
}
|
||
|
||
main > div.max-w-7xl {
|
||
max-width: 100% !important;
|
||
width: 100% !important;
|
||
height: 100% !important;
|
||
margin: 0 !important;
|
||
padding: 0 !important;
|
||
display: flex !important;
|
||
flex-direction: column !important;
|
||
flex: 1 !important;
|
||
}
|
||
|
||
h1 { display: none !important; }
|
||
|
||
/* Root Wrapper */
|
||
.architect-wrapper {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
background: #030305;
|
||
height: 100%;
|
||
width: 100%;
|
||
min-height: 0;
|
||
min-width: 0;
|
||
position: relative;
|
||
}
|
||
|
||
/* --- Layout Management --- */
|
||
#library-view, #designer-view, #studio-view {
|
||
display: none;
|
||
}
|
||
|
||
/* Use flex display when NOT hidden for proper structural growth */
|
||
#library-view:not(.hidden) { display: flex !important; }
|
||
#designer-view:not(.hidden) { display: flex !important; }
|
||
#studio-view:not(.hidden) { display: flex !important; }
|
||
|
||
/* --- VIEW 1: Library Overlay --- */
|
||
#library-view {
|
||
position: absolute;
|
||
inset: 0;
|
||
background: #030305;
|
||
z-index: 100;
|
||
padding: 2rem;
|
||
flex-direction: column;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.workflow-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||
gap: 1.5rem;
|
||
margin-top: 2rem;
|
||
}
|
||
|
||
.workflow-card {
|
||
background: #0a0a0f;
|
||
border: 1px solid #1e293b;
|
||
border-radius: 16px;
|
||
padding: 1.5rem;
|
||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.workflow-card:hover {
|
||
border-color: #6366f1;
|
||
transform: translateY(-4px);
|
||
box-shadow: 0 20px 40px rgba(0,0,0,0.6);
|
||
background: #0d0d14;
|
||
}
|
||
|
||
/* --- VIEW 2: Designer --- */
|
||
#designer-view {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
min-height: 0;
|
||
}
|
||
|
||
#designer-view.hidden { display: none !important; }
|
||
|
||
/* Designer Toolbar */
|
||
.arch-toolbar {
|
||
background: rgba(10, 10, 15, 0.95);
|
||
border-bottom: 1px solid #1e293b;
|
||
padding: 0.75rem 1.5rem;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
z-index: 50;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
/* Designer Content Area */
|
||
#conception-container {
|
||
flex: 1;
|
||
display: flex !important;
|
||
flex-direction: row !important;
|
||
min-height: 0;
|
||
height: 100%;
|
||
width: 100%;
|
||
overflow: hidden;
|
||
}
|
||
|
||
/* Left Sidebar */
|
||
.block-library {
|
||
flex-shrink: 0;
|
||
width: 280px;
|
||
height: 100%;
|
||
background: #0a0a0f;
|
||
border-right: 1px solid #1e293b;
|
||
display: flex;
|
||
flex-direction: column;
|
||
z-index: 30;
|
||
}
|
||
|
||
.library-scroll {
|
||
flex: 1;
|
||
overflow-y: auto;
|
||
padding: 1rem;
|
||
}
|
||
|
||
.repository-area {
|
||
height: 35%; /* Fixed ratio for repositories */
|
||
border-top: 1px solid #1e293b;
|
||
background: #050507;
|
||
padding: 1rem;
|
||
display: flex;
|
||
flex-direction: column;
|
||
min-height: 200px;
|
||
}
|
||
|
||
.repository-list {
|
||
flex: 1;
|
||
overflow-y: auto;
|
||
margin-top: 0.5rem;
|
||
}
|
||
|
||
/* Center Workspace */
|
||
.workspace-area {
|
||
position: relative;
|
||
flex: 1;
|
||
height: 100%;
|
||
min-width: 0;
|
||
background: #030305;
|
||
overflow: hidden;
|
||
display: flex;
|
||
}
|
||
|
||
.canvas-container {
|
||
flex: 1;
|
||
width: 100%;
|
||
height: 100%;
|
||
position: relative;
|
||
}
|
||
|
||
#conception-canvas {
|
||
display: block;
|
||
width: 100%;
|
||
height: 100%;
|
||
}
|
||
|
||
/* Sidebar Node Item */
|
||
.node-item {
|
||
padding: 0.75rem 1rem;
|
||
background: #161b22;
|
||
border: 1px solid #30363d;
|
||
border-radius: 12px;
|
||
cursor: grab;
|
||
font-size: 11px;
|
||
font-weight: bold;
|
||
color: #c9d1d9;
|
||
transition: all 0.2s;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
margin-bottom: 0.5rem;
|
||
}
|
||
.node-item:hover {
|
||
background: #1f2937;
|
||
border-color: #6366f1;
|
||
transform: scale(1.02);
|
||
color: white;
|
||
}
|
||
</style>
|
||
|
||
<div class="architect-wrapper">
|
||
<!-- View 1: Workflow Library -->
|
||
<div id="library-view" class="custom-scrollbar">
|
||
<div class="max-w-7xl mx-auto w-full">
|
||
<div class="flex flex-col md:flex-row justify-between items-start md:items-center border-b border-white/10 pb-8 gap-4">
|
||
<div>
|
||
<a href="{{ url_for('admin_dashboard') }}" class="text-xs font-bold text-indigo-400 hover:text-indigo-300 flex items-center gap-2 mb-3 uppercase tracking-widest">
|
||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"></path></svg>
|
||
Dashboard
|
||
</a>
|
||
<div class="flex items-center gap-4">
|
||
<h1 class="text-4xl font-black text-white tracking-tight">Workflow Architect</h1>
|
||
<button onclick="window.startCustomTour(ARCHITECT_TOUR, 'tour_conception_v1', true, true)" class="mt-2 flex items-center gap-2 text-[10px] font-black uppercase text-indigo-400 hover:text-white transition-colors bg-white/5 px-3 py-1 rounded-lg">
|
||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.79 4 4 0 2.21-1.79 4-4 4-1.742 0-3.223-.835-3.772-2M12 18.5v.01M12 5.5v.01"></path></svg>
|
||
Replay Tour
|
||
</button>
|
||
</div>
|
||
<p class="text-gray-500 mt-2">Design complex AI cognitive graphs with modular reusable flows.</p>
|
||
</div>
|
||
<div class="flex gap-4">
|
||
<button onclick="window.openGraphArchitect()" class="bg-purple-600 hover:bg-purple-500 text-white px-6 py-4 rounded-2xl font-black uppercase text-sm shadow-xl shadow-purple-500/10 transition-all flex items-center gap-2">
|
||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"></path></svg>
|
||
AI Architect
|
||
</button>
|
||
<button onclick="toggleStudio(true)" class="bg-white/5 hover:bg-white/10 text-indigo-400 px-6 py-4 rounded-2xl font-black uppercase text-sm border border-indigo-500/20 transition-all flex items-center gap-2">
|
||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4"></path></svg>
|
||
Node Studio
|
||
</button>
|
||
<button onclick="document.getElementById('new-project-menu').classList.toggle('hidden')" class="bg-indigo-600 hover:bg-indigo-500 text-white px-8 py-4 rounded-2xl font-black uppercase text-sm shadow-2xl shadow-indigo-500/20 transition-all transform hover:scale-105 active:scale-95 flex items-center gap-2">
|
||
+ New Project
|
||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7-7-7-7"></path></svg>
|
||
</button>
|
||
<div id="new-project-menu" class="hidden absolute right-0 mt-2 w-64 rounded-xl shadow-lg bg-[#0a0a0f] border border-white/10 ring-1 ring-black ring-opacity-5 z-[200]">
|
||
<div id="new-project-menu-items" class="py-2 p-2">
|
||
<!-- Populated via renderNewProjectMenu() -->
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="library-grid" class="workflow-grid">
|
||
<!-- Populated via loadLibrary() -->
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- View 3: Node Studio (IDE) -->
|
||
<div id="studio-view" class="hidden h-full flex flex-col bg-[#050507]">
|
||
<div class="arch-toolbar border-b border-white/10">
|
||
<div class="flex items-center gap-4">
|
||
<button onclick="toggleStudio(false)" class="p-2 hover:bg-white/5 rounded-xl text-indigo-400">
|
||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"></path></svg>
|
||
</button>
|
||
<h2 class="text-xl font-black text-white uppercase tracking-tighter">Node Studio</h2>
|
||
</div>
|
||
<div class="flex gap-2">
|
||
<button onclick="window.openNodeArchitect()" class="bg-purple-600 hover:bg-purple-500 px-4 py-2 rounded-lg text-xs font-black uppercase tracking-widest text-white shadow-lg shadow-purple-500/20 transition-all flex items-center gap-2">
|
||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"></path></svg>
|
||
Architect with AI
|
||
</button>
|
||
<button onclick="exportNode()" class="px-4 py-2 bg-white/5 hover:bg-white/10 rounded-lg text-xs font-bold uppercase">Export .hubnode</button>
|
||
<button onclick="saveNode()" class="px-6 py-2 bg-indigo-600 hover:bg-indigo-500 rounded-lg text-xs font-black uppercase tracking-widest text-white">Deploy Node</button>
|
||
</div>
|
||
</div>
|
||
<div class="flex-grow flex min-h-0">
|
||
<!-- Sidebar: My Nodes -->
|
||
<div class="w-64 border-r border-white/10 p-4 space-y-4 overflow-y-auto custom-scrollbar">
|
||
<h3 class="text-[10px] font-black text-gray-500 uppercase tracking-widest">Your Custom Nodes</h3>
|
||
<div id="custom-nodes-list" class="space-y-1"></div>
|
||
<div class="flex flex-col gap-2">
|
||
<button onclick="initNewNode()" class="w-full py-2 border border-dashed border-white/20 rounded-lg text-[10px] font-bold text-gray-500 hover:border-indigo-500 hover:text-indigo-400">+ Manual New Node</button>
|
||
<button onclick="window.openNodeSculptor()" class="w-full py-2 bg-purple-600/20 text-purple-400 border border-purple-500/30 rounded-lg text-[10px] font-black uppercase tracking-widest hover:bg-purple-600/40 transition-all">✨ AI Node Sculptor</button>
|
||
</div>
|
||
</div>
|
||
<!-- Dual Editor (Tabs) -->
|
||
<div class="flex-1 flex flex-col bg-[#0a0a0f]">
|
||
<div class="flex bg-black/40 border-b border-white/10 px-4 gap-4">
|
||
<button onclick="switchStudioTab('py')" id="studio-tab-py" class="py-2 px-4 text-[10px] font-black uppercase border-b-2 border-emerald-500 text-emerald-400">🐍 Python Logic</button>
|
||
<button onclick="switchStudioTab('js')" id="studio-tab-js" class="py-2 px-4 text-[10px] font-black uppercase border-b-2 border-transparent text-gray-500">📜 JavaScript UI</button>
|
||
</div>
|
||
<div id="cm-py-container" class="flex-grow"></div>
|
||
<div id="cm-js-container" class="flex-grow hidden"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- AI Node Sculptor Template -->
|
||
<template id="ai-node-sculptor-template">
|
||
<div class="space-y-4">
|
||
<div class="p-3 bg-purple-500/10 border border-purple-500/30 rounded-lg">
|
||
<h4 class="text-[10px] font-black text-purple-400 uppercase tracking-widest mb-1">AI Node Sculptor</h4>
|
||
<p class="text-[11px] text-gray-400 leading-relaxed">Describe a specialized node you need (e.g. "A node that connects to the Spotify API and returns the current track"). The AI will generate both the Python execution logic and the LiteGraph UI.</p>
|
||
</div>
|
||
<textarea id="build-node-prompt" class="w-full h-32 p-3 bg-black/40 border border-white/10 rounded-xl text-sm text-white focus:border-purple-500 outline-none" placeholder="Describe the node behavior and inputs/outputs..."></textarea>
|
||
{% include 'partials/_file_upload.html' %}
|
||
<div id="build-node-status" class="hidden bg-black/60 border border-white/10 rounded-lg p-3 h-24 overflow-y-auto font-mono text-[10px] text-gray-400 space-y-1 custom-scrollbar"></div>
|
||
<button onclick="window.executeNodeBuild()" id="node-build-run-btn" class="w-full py-3 bg-purple-600 hover:bg-purple-500 rounded-xl font-black text-xs uppercase tracking-widest transition-all">Sculpt Node Class</button>
|
||
</div>
|
||
</template>
|
||
|
||
<!-- AI Build Modal Template (Global Scope) -->
|
||
<template id="ai-graph-builder-template">
|
||
<div class="space-y-4">
|
||
<div class="p-3 bg-indigo-500/10 border border-indigo-500/30 rounded-lg">
|
||
<h4 class="text-[10px] font-black text-indigo-400 uppercase tracking-widest mb-1">Workflow Architect AI</h4>
|
||
<p class="text-[11px] text-gray-400 leading-relaxed">Describe the flow you want to create. You can attach API documentation, logic diagrams, or images as context.</p>
|
||
</div>
|
||
|
||
<textarea id="build-graph-prompt" class="w-full h-32 p-3 bg-black/40 border border-white/10 rounded-xl text-sm text-white focus:border-indigo-500 outline-none" placeholder="e.g. Build a RAG flow that summarizes web results before answering..."></textarea>
|
||
|
||
<div class="flex flex-col gap-2">
|
||
<span class="text-[10px] font-black text-gray-500 uppercase tracking-widest px-1">Attached Context</span>
|
||
<div id="g-previews" class="flex flex-wrap gap-2 min-h-[32px] p-2 bg-black/20 border border-white/5 rounded-lg"></div>
|
||
</div>
|
||
|
||
<div class="flex gap-2">
|
||
<button type="button" onclick="document.getElementById('g-img-input').click()" class="flex items-center gap-2 text-[10px] font-bold py-2 px-3 bg-white/5 rounded-lg border border-white/10 hover:bg-white/10 transition-colors">📷 Images</button>
|
||
<button type="button" onclick="document.getElementById('g-doc-input').click()" class="flex items-center gap-2 text-[10px] font-bold py-2 px-3 bg-white/5 rounded-lg border border-white/10 hover:bg-white/10 transition-colors">📄 Docs</button>
|
||
<button type="button" onclick="window.triggerGraphWebImport()" class="flex items-center gap-2 text-[10px] font-bold py-2 px-3 bg-indigo-600/20 text-indigo-300 rounded-lg border border-indigo-500/30 hover:bg-indigo-600/30 transition-all">🌍 Web Import</button>
|
||
</div>
|
||
|
||
<input type="file" id="g-img-input" class="hidden" multiple accept="image/*">
|
||
<input type="file" id="g-doc-input" class="hidden" multiple accept=".txt,.md,.pdf,.csv">
|
||
|
||
<div id="build-graph-status" class="hidden bg-black/60 border border-white/10 rounded-lg p-3 h-24 overflow-y-auto font-mono text-[10px] text-gray-400 space-y-1 custom-scrollbar"></div>
|
||
<button onclick="window.executeGraphBuild()" id="graph-build-run-btn" class="w-full py-3 bg-indigo-600 hover:bg-indigo-500 rounded-xl font-black text-xs uppercase tracking-widest transition-all">Generate Graph</button>
|
||
</div>
|
||
</template>
|
||
|
||
<!-- View 2: Graph Designer -->
|
||
<div id="designer-view" class="hidden">
|
||
<div class="arch-toolbar">
|
||
<div class="flex items-center">
|
||
<button onclick="toggleLibrary(true)" class="bg-white/5 hover:bg-white/10 p-2.5 rounded-xl text-indigo-400 mr-4 transition-colors" title="Back to Library">
|
||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path></svg>
|
||
</button>
|
||
<div class="flex flex-col">
|
||
<input type="text" id="workflow-name" placeholder="Untitled Workflow" class="bg-transparent text-xl font-black text-white outline-none focus:text-indigo-400 border-none p-0 h-7 w-80">
|
||
<span class="text-[9px] text-gray-500 uppercase font-black tracking-widest mt-0.5">Designer Mode</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="flex gap-2">
|
||
<div class="flex bg-white/5 rounded-xl p-1 mr-4">
|
||
<button onclick="undo()" class="p-2 hover:bg-white/10 rounded-lg text-gray-400 hover:text-white transition-all" title="Undo"><svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 10h10a8 8 0 018 8v2M3 10l6 6m-6-6l6-6"></path></svg></button>
|
||
<button onclick="redo()" class="p-2 hover:bg-white/10 rounded-lg text-gray-400 hover:text-white transition-all" title="Redo"><svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 10h-10a8 8 0 00-8 8v2M21 10l-6 6m6-6l-6-6"></path></svg></button>
|
||
</div>
|
||
<button onclick="saveWorkflow()" class="bg-indigo-600 hover:bg-indigo-500 px-4 py-2 rounded-xl font-black text-xs uppercase tracking-widest text-white shadow-lg shadow-indigo-500/20 transition-all flex items-center gap-2">
|
||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7H5a2 2 0 00-2 2v9a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-3m-1 4l-3 3m0 0l-3-3m3 3V4"></path></svg>
|
||
Save
|
||
</button>
|
||
<button onclick="saveAsTemplate()" class="bg-white/5 hover:bg-white/10 border border-white/10 px-4 py-2 rounded-xl font-black text-xs uppercase tracking-widest text-indigo-400 transition-all">
|
||
Template
|
||
</button>
|
||
<button onclick="rearrangeNodes()" class="bg-white/5 hover:bg-white/10 border border-white/10 px-4 py-2 rounded-xl font-black text-xs uppercase tracking-widest text-amber-400 transition-all flex items-center gap-2">
|
||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16m-7 6h7"></path></svg>
|
||
Auto Layout
|
||
</button>
|
||
<button onclick="window.openGraphArchitect('edit')" class="bg-purple-600/20 hover:bg-purple-600/40 border border-purple-500/30 px-4 py-2 rounded-xl font-black text-xs uppercase tracking-widest text-purple-300 transition-all flex items-center gap-2">
|
||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"></path></svg>
|
||
Update with AI
|
||
</button>
|
||
<button onclick="testInPlayground()" class="bg-emerald-600 hover:bg-emerald-500 px-6 py-2 rounded-xl font-black text-xs uppercase tracking-widest text-white shadow-lg shadow-emerald-500/20 transition-all flex items-center gap-2">
|
||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
|
||
Test
|
||
</button>
|
||
<button onclick="clearGraph()" class="bg-white/5 hover:bg-red-900/20 px-4 rounded-xl font-bold text-xs uppercase text-gray-500 hover:text-red-400 transition-all">Reset</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="conception-container">
|
||
<aside class="block-library">
|
||
<div class="library-scroll custom-scrollbar" id="sidebar-node-list">
|
||
<!-- Categories will be injected here via JS -->
|
||
</div>
|
||
</aside>
|
||
|
||
<main class="workspace-area">
|
||
<div id="canvas-wrapper" class="canvas-container">
|
||
<canvas id="conception-canvas"></canvas>
|
||
</div>
|
||
</main>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
// --- DYNAMIC NODE REGISTRATION ---
|
||
// Registers nodes before sidebar/graph initialization to prevent "Replacing node type" warnings
|
||
try {
|
||
{{ dynamic_nodes_js | safe }}
|
||
} catch(e) {
|
||
console.error("LiteGraph dynamic registration failed:", e);
|
||
}
|
||
|
||
// --- DATA INJECTION FROM SERVER ---
|
||
const model_groups = {{ (model_groups or {}) | tojson | safe }};
|
||
window.logic_blocks = {{ (logic_blocks or[]) | tojson | safe }};
|
||
window.datastores_list = {{ (datastores or[]) | tojson | safe }};
|
||
window.memory_systems_list = {{ (memory_systems or[]) | tojson | safe }};
|
||
|
||
// Flatten model_groups into a prioritized list for LiteGraph combos
|
||
window.available_models = ["auto"];
|
||
|
||
if (model_groups["Proxy Features"]) {
|
||
model_groups["Proxy Features"].forEach(m => {
|
||
if (m !== "auto" && !window.available_models.includes(m)) window.available_models.push(m);
|
||
});
|
||
}
|
||
|
||
Object.keys(model_groups).forEach(server => {
|
||
if (server !== "Proxy Features") {
|
||
model_groups[server].forEach(m => {
|
||
if (!window.available_models.includes(m)) window.available_models.push(m);
|
||
});
|
||
}
|
||
});
|
||
|
||
// --- GLOBAL SCOPE ---
|
||
var graph = null;
|
||
var canvas = null;
|
||
|
||
// Modern UI Overrides for LiteGraph
|
||
LiteGraph.NODE_WIDTH = 200;
|
||
LiteGraph.NODE_WIDGET_HEIGHT = 28;
|
||
LiteGraph.NODE_SLOT_HEIGHT = 24;
|
||
LiteGraph.WIDGET_MARGIN = 8;
|
||
|
||
// Helper to calculate total required height for a node
|
||
function getAutoHeight(node) {
|
||
const titleHeight = LiteGraph.NODE_TITLE_HEIGHT;
|
||
const slotsHeight = Math.max(node.inputs ? node.inputs.length : 0, node.outputs ? node.outputs.length : 0) * LiteGraph.NODE_SLOT_HEIGHT;
|
||
const widgetsHeight = node.widgets ? node.widgets.length * (LiteGraph.NODE_WIDGET_HEIGHT + 6) : 0;
|
||
return titleHeight + slotsHeight + widgetsHeight + 20; // 20px padding
|
||
}
|
||
|
||
// --- NODE REGISTRATION ---
|
||
// Standard and Custom nodes are now loaded dynamically from the NodeRegistry
|
||
|
||
// --- MARKDOWN MODAL LOGIC (SKILLS & PERSONALITIES) ---
|
||
let currentEditingNode = null;
|
||
let SERVER_SKILLS = [];
|
||
let SERVER_TOOLS = [];
|
||
|
||
async function fetchServerTools() {
|
||
try {
|
||
const resp = await fetch("/api/v1/api/tools");
|
||
if (resp.ok) SERVER_TOOLS = await resp.json();
|
||
} catch(e) { console.error("Failed to load tools", e); }
|
||
}
|
||
|
||
async function fetchServerSkills() {
|
||
try {
|
||
const resp = await fetch("{{ url_for('api_get_skills') }}");
|
||
if (resp.ok) SERVER_SKILLS = await resp.json();
|
||
} catch(e) { console.error("Failed to load skills from server", e); }
|
||
}
|
||
|
||
const BOOTSTRAP_PERSONALITIES =[
|
||
{
|
||
name: "Python Developer",
|
||
raw: "---\nname: Python Developer\nauthor: Admin\ndescription: A senior python developer persona.\nmcps:\n - http://localhost:8000\n---\nYou are an expert Python developer. You write clean, modular, and well-documented code."
|
||
},
|
||
{
|
||
name: "Legal Assistant",
|
||
raw: "---\nname: Legal Assistant\nauthor: Admin\ndescription: Specializes in analyzing contracts.\n---\nYou are a diligent legal assistant. Always reference specific clauses when analyzing contracts."
|
||
}
|
||
];
|
||
|
||
const BOOTSTRAP_TOOLS =[
|
||
{
|
||
name: "Get Weather",
|
||
raw: "---\nname: get_weather\ndescription: Get current weather at a location\nparameters:\n type: object\n properties:\n location:\n type: string\n description: The city and state, e.g., San Francisco, CA\n required: [location]\n---\n# Get Weather Tool"
|
||
},
|
||
{
|
||
name: "Search Web",
|
||
raw: "---\nname: search_web\ndescription: Search the web for information\nparameters:\n type: object\n properties:\n query:\n type: string\n description: The search query\n required: [query]\n---\n# Search Web Tool"
|
||
}
|
||
];
|
||
|
||
window.openMarkdownModal = function(node, mode) {
|
||
currentEditingNode = node;
|
||
const presets = mode === 'skill' ? SERVER_SKILLS : (mode === 'tool' ? BOOTSTRAP_TOOLS : BOOTSTRAP_PERSONALITIES);
|
||
const titleColor = mode === 'skill' ? 'indigo' : (mode === 'tool' ? 'sky' : 'fuchsia');
|
||
|
||
let bootstrapOptions = presets.map((p, i) => `<option value="${i}">${p.name}</option>`).join('');
|
||
|
||
const modalBody = `
|
||
<div class="space-y-4 text-left flex flex-col h-[70vh]">
|
||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 flex-shrink-0">
|
||
<div class="bg-${titleColor}-500/10 border border-${titleColor}-500/20 p-3 rounded-lg">
|
||
<h4 class="text-[10px] font-bold text-${titleColor}-400 uppercase mb-2">1. Select Preset</h4>
|
||
<div class="flex gap-2">
|
||
<select id="preset-select" class="text-xs bg-black/40 rounded p-1.5 border border-white/10 flex-grow outline-none">
|
||
<option value="">-- Apply Preset --</option>
|
||
${bootstrapOptions}
|
||
</select>
|
||
<button type="button" onclick="applyPreset('${mode}')" class="bg-${titleColor}-600 hover:bg-${titleColor}-500 px-3 py-1 rounded text-xs font-bold transition-colors">Apply</button>
|
||
</div>
|
||
</div>
|
||
<div class="bg-purple-500/10 border border-purple-500/20 p-3 rounded-lg">
|
||
<h4 class="text-[10px] font-bold text-purple-400 uppercase mb-2">2. Import .md File</h4>
|
||
<input type="file" id="import-md-file" accept=".md" class="w-full text-[10px] file:mr-2 file:py-1 file:px-2 file:rounded file:border-0 file:bg-purple-600 file:text-white hover:file:bg-purple-500 transition-colors">
|
||
</div>
|
||
</div>
|
||
|
||
<div class="flex-grow flex flex-col min-h-0 mt-2">
|
||
<h4 class="text-[10px] font-bold text-gray-500 uppercase mb-1 px-1">3. Content (Markdown + YAML Frontmatter)</h4>
|
||
<textarea id="markdown-editor" class="flex-grow w-full bg-black/40 border border-white/10 rounded-lg p-4 font-mono text-sm text-gray-200 outline-none focus:border-${titleColor}-500 custom-scrollbar resize-none"></textarea>
|
||
</div>
|
||
|
||
<div class="flex justify-end pt-4 border-t border-white/10 flex-shrink-0">
|
||
<button type="button" onclick="saveAndCloseMarkdownModal('${mode}')" class="bg-green-600 hover:bg-green-500 px-10 py-2 rounded font-black text-xs uppercase tracking-widest text-white shadow-lg transition-all">Save Node Logic</button>
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
window.showModal(`Configure ${mode === 'skill' ? 'Skill' : (mode === 'tool' ? 'Tool' : 'Personality')} Instance`, modalBody);
|
||
|
||
const editor = document.getElementById('markdown-editor');
|
||
editor.value = node.properties.raw || "";
|
||
|
||
// Handle File Import
|
||
setTimeout(() => {
|
||
const fileInput = document.getElementById('import-md-file');
|
||
if(fileInput) {
|
||
fileInput.addEventListener('change', async (e) => {
|
||
const file = e.target.files[0];
|
||
if(!file) return;
|
||
const text = await file.text();
|
||
document.getElementById('markdown-editor').value = text;
|
||
e.target.value = null;
|
||
});
|
||
}
|
||
}, 100);
|
||
};
|
||
|
||
window.applyPreset = function(mode) {
|
||
const select = document.getElementById('preset-select');
|
||
const idx = select.value;
|
||
if (idx === "") return;
|
||
const presets = mode === 'skill' ? SERVER_SKILLS : BOOTSTRAP_PERSONALITIES;
|
||
document.getElementById('markdown-editor').value = presets[idx].raw;
|
||
};
|
||
|
||
window.saveAndCloseMarkdownModal = function(mode) {
|
||
const rawContent = document.getElementById('markdown-editor').value;
|
||
currentEditingNode.properties.raw = rawContent;
|
||
|
||
// Parse frontmatter
|
||
const match = rawContent.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
|
||
let name = "Unnamed";
|
||
|
||
if (match) {
|
||
try {
|
||
const metadata = jsyaml.load(match[1]);
|
||
const body = match[2];
|
||
currentEditingNode.properties.metadata = metadata || {};
|
||
currentEditingNode.properties.content = body.trim();
|
||
if (metadata && metadata.name) {
|
||
name = metadata.name;
|
||
}
|
||
} catch (e) {
|
||
alert("Invalid YAML frontmatter: " + e.message);
|
||
return;
|
||
}
|
||
} else {
|
||
currentEditingNode.properties.metadata = {};
|
||
currentEditingNode.properties.content = rawContent.trim();
|
||
// Try to extract H1
|
||
const titleMatch = rawContent.match(/^#\s+(.+)/m);
|
||
if (titleMatch && titleMatch[1]) {
|
||
name = titleMatch[1].trim();
|
||
}
|
||
}
|
||
|
||
currentEditingNode.title = (mode === 'personality' ? '🎭 ' : (mode === 'tool' ? '🔧 ' : '📜 ')) + name.toUpperCase();
|
||
|
||
currentEditingNode.size = currentEditingNode.computeSize();
|
||
currentEditingNode.setDirtyCanvas(true, true);
|
||
document.getElementById('modal-close-btn').click();
|
||
currentEditingNode = null;
|
||
pushHistoryState();
|
||
};
|
||
|
||
|
||
// --- LIBRARIES & RESIZING ---
|
||
let GLOBAL_PERSONALITIES = [];
|
||
let GLOBAL_SKILLS = [];
|
||
|
||
async function fetchLibraries() {
|
||
try {
|
||
const [pRes, sRes] = await Promise.all([
|
||
fetch("{{ url_for('api_get_personalities') }}"),
|
||
fetch("{{ url_for('api_get_skills') }}")
|
||
]);
|
||
if (pRes.ok) GLOBAL_PERSONALITIES = await pRes.json();
|
||
if (sRes.ok) GLOBAL_SKILLS = await sRes.json();
|
||
} catch(e) { console.error("Lib load fail", e); }
|
||
}
|
||
|
||
function setupNodeResizing(node) {
|
||
node.onPropertyChanged = function(name, value) {
|
||
this.size = this.computeSize();
|
||
this.setDirtyCanvas(true, true);
|
||
pushHistoryState();
|
||
return true;
|
||
};
|
||
node.onAdded = function() {
|
||
this.size = this.computeSize();
|
||
};
|
||
}
|
||
|
||
// --- ROUTER MODAL LOGIC ---
|
||
window.openRouterModal = (node) => {
|
||
currentEditingNode = node;
|
||
const slots = node.inputs.slice(1);
|
||
|
||
let html = `<div class="space-y-6 text-left max-h-[75vh] overflow-y-auto custom-scrollbar pr-2">
|
||
<p class="text-[11px] text-gray-500 uppercase font-black tracking-widest">Decision Matrix for ${node.inputs.length - 1} Paths</p>
|
||
`;
|
||
|
||
slots.forEach((input, idx) => {
|
||
const slotIdx = idx + 1;
|
||
const rule = node.properties.rules.find(r => r.slot === slotIdx) || { label: input.label, conditions: [], intent: "" };
|
||
|
||
html += `
|
||
<div class="p-4 bg-white/5 border border-white/10 rounded-xl space-y-4">
|
||
<div class="flex justify-between items-center border-b border-white/5 pb-2">
|
||
<span class="text-xs font-black text-indigo-400">PATH ${slotIdx}: ${input.label}</span>
|
||
<input type="text" id="label-${slotIdx}" value="${rule.label}" placeholder="Label" class="text-[10px] bg-black/40 border border-white/10 rounded px-2 py-1 w-32">
|
||
</div>
|
||
|
||
<div class="space-y-2">
|
||
<label class="block text-[9px] font-bold text-gray-500 uppercase px-1">Logical Conditions</label>
|
||
<div id="cond-container-${slotIdx}" class="space-y-2">
|
||
${(rule.conditions || []).map((c, cIdx) => `
|
||
<div class="flex gap-2 items-center bg-black/20 p-2 rounded border border-white/5">
|
||
<select class="cond-type text-[10px] bg-transparent border border-white/10 rounded" data-slot="${slotIdx}">
|
||
<option value="keyword" ${c.type === 'keyword' ? 'selected' : ''}>Keyword</option>
|
||
<option value="regex" ${c.type === 'regex' ? 'selected' : ''}>Regex</option>
|
||
<option value="min_len" ${c.type === 'min_len' ? 'selected' : ''}>Min Length</option>
|
||
<option value="max_len" ${c.type === 'max_len' ? 'selected' : ''}>Max Length</option>
|
||
<option value="has_images" ${c.type === 'has_images' ? 'selected' : ''}>Has Images</option>
|
||
<option value="user" ${c.type === 'user' ? 'selected' : ''}>User is</option>
|
||
</select>
|
||
<input type="text" class="cond-val text-[10px] bg-black/40 border border-white/10 rounded flex-grow" value="${c.value || ''}">
|
||
<button type="button" onclick="this.parentElement.remove()" class="text-red-500 hover:text-red-400">×</button>
|
||
</div>
|
||
`).join('')}
|
||
</div>
|
||
<button type="button" onclick="addCondToModal(${slotIdx})" class="text-[9px] text-indigo-400 hover:underline">+ Add Condition</button>
|
||
</div>
|
||
|
||
${node.properties.use_semantic ? `
|
||
<div class="pt-2 border-t border-white/5">
|
||
<label class="block text-[9px] font-bold text-purple-400 uppercase mb-1">AI Intent Classifier Context</label>
|
||
<textarea id="intent-${slotIdx}" rows="2" placeholder="Describe the intent for this path..." class="w-full text-xs bg-black/40 border border-white/10 rounded p-2">${rule.intent || ''}</textarea>
|
||
</div>` : ''}
|
||
</div>
|
||
`;
|
||
});
|
||
|
||
html += `
|
||
<div class="flex justify-end pt-4 sticky bottom-0 bg-[#0a0a0f]">
|
||
<button onclick="saveRouterRules()" class="bg-indigo-600 hover:bg-indigo-500 px-10 py-2 rounded font-black text-xs uppercase tracking-widest text-white shadow-lg">Save Router Matrix</button>
|
||
</div>
|
||
</div>`;
|
||
|
||
window.showModal("Advanced Router Matrix", html);
|
||
};
|
||
|
||
window.addCondToModal = (slot) => {
|
||
const container = document.getElementById(`cond-container-${slot}`);
|
||
const div = document.createElement('div');
|
||
div.className = "flex gap-2 items-center bg-black/20 p-2 rounded border border-white/5";
|
||
div.innerHTML = `
|
||
<select class="cond-type text-[10px] bg-transparent border border-white/10 rounded" data-slot="${slot}">
|
||
<option value="keyword">Keyword</option>
|
||
<option value="regex">Regex</option>
|
||
<option value="min_len">Min Length</option>
|
||
<option value="max_len">Max Length</option>
|
||
<option value="has_images">Has Images</option>
|
||
<option value="user">User is</option>
|
||
</select>
|
||
<input type="text" class="cond-val text-[10px] bg-black/40 border border-white/10 rounded flex-grow" placeholder="Value...">
|
||
<button type="button" onclick="this.parentElement.remove()" class="text-red-500 hover:text-red-400">×</button>
|
||
`;
|
||
container.appendChild(div);
|
||
};
|
||
|
||
window.saveRouterRules = () => {
|
||
const node = currentEditingNode;
|
||
const newRules = [];
|
||
for(let i=1; i < node.inputs.length; i++) {
|
||
const container = document.getElementById(`cond-container-${i}`);
|
||
const conds = [];
|
||
if (container) {
|
||
container.querySelectorAll('.flex').forEach(div => {
|
||
conds.push({
|
||
type: div.querySelector('.cond-type').value,
|
||
value: div.querySelector('.cond-val').value
|
||
});
|
||
});
|
||
}
|
||
|
||
const intentEl = document.getElementById(`intent-${i}`);
|
||
newRules.push({
|
||
slot: i,
|
||
label: document.getElementById(`label-${i}`).value,
|
||
conditions: conds,
|
||
intent: intentEl ? intentEl.value : ""
|
||
});
|
||
}
|
||
node.properties.rules = newRules;
|
||
document.getElementById('modal-close-btn').click();
|
||
pushHistoryState();
|
||
};
|
||
|
||
// --- DYNAMIC PLUGIN NODES ---
|
||
try {
|
||
{{ dynamic_nodes_js | safe }}
|
||
} catch(e) {
|
||
console.error("Failed to load plugin nodes:", e);
|
||
}
|
||
|
||
// --- UI ENGINE ---
|
||
|
||
// Fix Canvas Stretching & DPI Scaling
|
||
function resizeCanvas() {
|
||
const wrapper = document.getElementById('canvas-wrapper');
|
||
const canvasEl = document.getElementById('conception-canvas');
|
||
if (wrapper && canvasEl && canvas) {
|
||
const w = wrapper.clientWidth;
|
||
const h = wrapper.clientHeight;
|
||
|
||
if (w === 0 || h === 0) return;
|
||
|
||
// Update DOM element size explicitly
|
||
canvasEl.width = w;
|
||
canvasEl.height = h;
|
||
|
||
// Update LiteGraph internal state
|
||
canvas.resize(w, h);
|
||
canvas.setDirty(true, true);
|
||
}
|
||
}
|
||
// Use ResizeObserver for perfect fluid expansion
|
||
const ro = new ResizeObserver(() => {
|
||
resizeCanvas();
|
||
});
|
||
ro.observe(document.getElementById('canvas-wrapper'));
|
||
|
||
const pColor = getComputedStyle(document.documentElement).getPropertyValue('--color-primary-500').trim() || '#6366f1';
|
||
LiteGraph.NODE_SELECTED_BGCOLOR = pColor;
|
||
|
||
// --- UNDO/REDO STATE MANAGEMENT ---
|
||
let historyStack = [];
|
||
let historyIndex = -1;
|
||
let isRestoring = false;
|
||
let saveTimeout = null;
|
||
|
||
function pushHistoryState() {
|
||
if (isRestoring) return;
|
||
clearTimeout(saveTimeout);
|
||
saveTimeout = setTimeout(() => {
|
||
const state = JSON.stringify(graph.serialize());
|
||
// Don't push if it hasn't changed from current
|
||
if (historyIndex >= 0 && historyStack[historyIndex] === state) return;
|
||
|
||
// Truncate future if we diverged
|
||
historyStack = historyStack.slice(0, historyIndex + 1);
|
||
historyStack.push(state);
|
||
// Limit stack size to prevent memory bloat
|
||
if (historyStack.length > 30) historyStack.shift();
|
||
else historyIndex++;
|
||
}, 300); // debounce rapid changes
|
||
}
|
||
|
||
window.undo = function() {
|
||
if (historyIndex > 0) {
|
||
historyIndex--;
|
||
isRestoring = true;
|
||
graph.configure(JSON.parse(historyStack[historyIndex]));
|
||
isRestoring = false;
|
||
}
|
||
};
|
||
|
||
window.redo = function() {
|
||
if (historyIndex < historyStack.length - 1) {
|
||
historyIndex++;
|
||
isRestoring = true;
|
||
graph.configure(JSON.parse(historyStack[historyIndex]));
|
||
isRestoring = false;
|
||
}
|
||
};
|
||
|
||
// --- NODE HELP MODAL LOGIC ---
|
||
const NODE_HELP = {
|
||
"hub/input": "**ENTRY NODE:**\nCaptures the user's incoming message and settings from the Chat Playground.\n\n### Outputs:\n- **Messages:** Full conversation history.\n- **Settings:** Port, temperature, etc.\n- **Input:** Just the text of the *last* user message.",
|
||
"hub/output": "**EXIT NODE:**\nDefines the final response sent back to the chat bubble. Connect your AI's answer here.",
|
||
"hub/llm_chat": "**LLM CHAT:**\nCore conversational engine for standard interaction.\n\n### Inputs:\n- **Messages:** Full history (usually from Input or System Modifier).\n- **Settings:** Custom generation parameters.\n- **Tool [N]:** Connect **Tool Selectors** or **MCPs** to enable function calling.",
|
||
"hub/agent": "**AUTONOMOUS AGENT:**\nA multi-turn 'looping' engine. It plans, acts, and corrects itself.\n\n### Cognitive Loop:\n1. **Thought:** The model plans its next move.\n2. **Action:** It calls connected Tools to gather info.\n3. **Observation:** It reviews the result.\n4. **Final Answer:** It only exits the loop when the user's request is satisfied.\n\n### Inputs:\n- **Tool [N]:** Use this to give the agent 'limbs' (Filesystem, Web Search, APIs).",
|
||
"hub/system_modifier": "**SYSTEM MODIFIER:**\nSurgically updates the 'Soul' of the model.\n\n### Usage:\nConnect a string output (like from a Personality or Composer) to the 'System Prompt' slot. If **Replace Existing** is checked, it nukes prior instructions—ideal for forced persona shifts.",
|
||
"hub/datastore": "**RAG DATASTORE:**\nSemantic retrieval from your local knowledge bases.\n\n### Usage:\nConnect a search string (like user input) to the 'Query' slot. It will return the top-N relevant chunks from the selected store.",
|
||
"hub/web_search": "**WEB SEARCH:**\nReal-time internet access for the AI.\n\n### Providers:\n- **Wikipedia:** General facts.\n- **ArXiv:** Scientific papers.\n- **Google:** Live web results.",
|
||
"hub/personality": "**PERSONALITY LOADER:**\nInjects a persona from your **Personalities Studio**.\n\n### Output:\nReturns the raw SOUL.md content as a system instruction string.",
|
||
"hub/system_composer": "**SYSTEM COMPOSER:**\nCombines multiple knowledge sources into a single, structured system prompt. Useful for merging a Persona with RAG context.",
|
||
"hub/mcp": "**MCP (MODEL CONTEXT PROTOCOL):**\nThe industry standard for AI tool connectivity.\n\n### Transports:\n- **SSE (Server-Sent Events):** Connect to remote tool servers (e.g. Brave Search MCP, Google Maps MCP).\n- **Stdio:** Execute a local process that provides tools (e.g. `npx @modelcontextprotocol/server-filesystem`).\n\n### Wiring:\nConnect the output to an **Autonomous Agent** to allow the model to discover and call these external tools automatically.",
|
||
"hub/expert": "**EXPERT BUILDER:**\nCreates a structured 'Expert Bundle' containing a Model + Persona + Temperature.\n\n### Usage:\nConnect the output to the expert slots of a **Mixture of Experts (MoE)** node for parallel brainstorming.",
|
||
"hub/autorouter": "**SMART ROUTER:**\nDirects the flow based on logic.\n\n### Modes:\n- **Rules:** If keywords exist, go to Path A. Else Path B.\n- **Semantic:** Ask a small model to classify the intent (e.g. 'Is this about coding?') and route to the best specialist.",
|
||
"hub/note": "**DOCUMENTATION NOTE:**\nAdd markdown explanations to your graph. Essential for AI Architect handovers."
|
||
};
|
||
|
||
window.showNodeHelp = function(nodeType) {
|
||
const text = NODE_HELP[nodeType] || "No help available.";
|
||
// Use the marked library (already loaded in conception.html) to render Markdown
|
||
const html = marked.parse(text);
|
||
window.showModal(
|
||
"Node Information",
|
||
`<div class="help-modal-content text-sm text-gray-300 leading-relaxed prose prose-invert">${html}</div>`
|
||
);
|
||
};
|
||
|
||
// --- WORKFLOW REUSE ENGINE (Sub-Graphs) ---
|
||
function NodeSubGraph() {
|
||
this.title = "SUB-WORKFLOW";
|
||
this.properties = { workflow_name: "" };
|
||
this.addWidget("combo", "Workflow", this.properties.workflow_name, (v) => {
|
||
this.properties.workflow_name = v;
|
||
this.title = "FLOW: " + v.toUpperCase();
|
||
this.refreshSlots();
|
||
pushHistoryState();
|
||
}, { values: [] });
|
||
this.addWidget("button", "ℹ️ Help", null, () => { showNodeHelp("hub/subgraph"); });
|
||
this.color = "#4338ca";
|
||
this.bgcolor = "#1e1b4b";
|
||
}
|
||
|
||
NodeSubGraph.prototype.onAdded = function() {
|
||
// Update list on add
|
||
this.widgets[0].options.values = loadedWorkflows.map(w => w.name);
|
||
this.refreshSlots();
|
||
};
|
||
|
||
NodeSubGraph.prototype.refreshSlots = function() {
|
||
const flow = loadedWorkflows.find(w => w.name === this.properties.workflow_name);
|
||
if (!flow) return;
|
||
|
||
// Clear existing slots (keeping widget at 0)
|
||
this.inputs = [];
|
||
this.outputs = [];
|
||
this.addWidget("button", "ℹ️ Help", null, () => { showNodeHelp("hub/subgraph"); });
|
||
|
||
// Parse graph data
|
||
const nodes = flow.graph.nodes;
|
||
nodes.forEach(n => {
|
||
if (n.type === "hub/input") this.addInput(n.title || "Input", "messages");
|
||
if (n.type === "hub/output") this.addOutput(n.title || "Output", "string");
|
||
});
|
||
this.size = this.computeSize();
|
||
};
|
||
|
||
LiteGraph.registerNodeType("hub/subgraph", NodeSubGraph);
|
||
|
||
// --- TEMPLATE ENGINE ---
|
||
let serverTemplates = [];
|
||
|
||
async function loadTemplates() {
|
||
try {
|
||
const resp = await fetch("{{ url_for('admin_list_templates') }}");
|
||
if (!resp.ok) {
|
||
console.error("Templates fetch failed:", resp.status);
|
||
return;
|
||
}
|
||
serverTemplates = await resp.json();
|
||
renderNewProjectMenu();
|
||
} catch(e) {
|
||
console.error("Failed to load templates:", e);
|
||
}
|
||
}
|
||
|
||
function renderNewProjectMenu() {
|
||
const menu = document.getElementById('new-project-menu-items');
|
||
if (!menu) return;
|
||
|
||
// Group templates by category
|
||
const groups = {};
|
||
serverTemplates.forEach(t => {
|
||
if (!groups[t.category]) groups[t.category] = [];
|
||
groups[t.category].push(t);
|
||
});
|
||
|
||
let html = `
|
||
<button onclick="createNewWorkflow('blank')" class="w-full text-left px-4 py-3 text-sm text-gray-300 hover:bg-white/5 rounded-lg flex flex-col group border-b border-white/5">
|
||
<span class="font-bold text-white group-hover:text-indigo-400">Blank Project</span>
|
||
<span class="text-[10px] text-gray-500">Start from a clean slate</span>
|
||
</button>
|
||
`;
|
||
|
||
Object.keys(groups).forEach(cat => {
|
||
html += `<div class="px-4 py-2 text-[9px] font-black text-gray-600 uppercase tracking-tighter mt-2">${cat} Templates</div>`;
|
||
groups[cat].forEach(t => {
|
||
const color = cat === 'Standard' ? 'indigo' : 'emerald';
|
||
html += `
|
||
<button onclick="applyTemplateById('${t.id}')" class="w-full text-left px-4 py-3 text-sm text-gray-300 hover:bg-white/5 rounded-lg flex flex-col group mt-1">
|
||
<span class="font-bold text-white group-hover:text-${color}-400">${t.name}</span>
|
||
<span class="text-[10px] text-gray-500">${t.description}</span>
|
||
</button>
|
||
`;
|
||
});
|
||
});
|
||
menu.innerHTML = html;
|
||
}
|
||
|
||
window.applyTemplateById = (id) => {
|
||
const t = serverTemplates.find(x => x.id === id);
|
||
if (!t) return;
|
||
|
||
clearGraph();
|
||
try {
|
||
graph.configure(t.graph);
|
||
document.getElementById('workflow-name').value = "New " + t.name;
|
||
|
||
// Auto-layout on template start
|
||
rearrangeNodes();
|
||
|
||
historyStack = [JSON.stringify(graph.serialize())];
|
||
document.getElementById('new-project-menu').classList.add('hidden');
|
||
document.getElementById('library-view').classList.add('hidden');
|
||
document.getElementById('designer-view').classList.remove('hidden');
|
||
setTimeout(() => { if(window.resizeCanvas) window.resizeCanvas(); }, 100);
|
||
} catch(e) { console.error("Template load failed", e); }
|
||
};
|
||
|
||
window.saveAsTemplate = () => {
|
||
const name = prompt("Template Name:", document.getElementById('workflow-name').value);
|
||
if (!name) return;
|
||
const desc = prompt("Short Description:");
|
||
|
||
fetch("{{ url_for('admin_save_template') }}", {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': '{{ csrf_token }}' },
|
||
body: JSON.stringify({
|
||
name: name,
|
||
description: desc,
|
||
graph: graph.serialize()
|
||
})
|
||
}).then(r => r.json()).then(data => {
|
||
alert(data.message);
|
||
loadTemplates();
|
||
});
|
||
};
|
||
|
||
// --- Unified View Controller ---
|
||
window.switchArchitectView = function(viewName) {
|
||
const views = {
|
||
library: document.getElementById('library-view'),
|
||
designer: document.getElementById('designer-view'),
|
||
studio: document.getElementById('studio-view')
|
||
};
|
||
|
||
Object.keys(views).forEach(v => {
|
||
if (v === viewName) {
|
||
views[v].classList.remove('hidden');
|
||
} else {
|
||
views[v].classList.add('hidden');
|
||
}
|
||
});
|
||
|
||
if (viewName === 'designer') {
|
||
setTimeout(() => { if(window.resizeCanvas) window.resizeCanvas(); }, 50);
|
||
}
|
||
if (viewName === 'library') loadLibrary();
|
||
};
|
||
|
||
window.toggleLibrary = function(forceShowLibrary = false) {
|
||
window.switchArchitectView(forceShowLibrary ? 'library' : 'designer');
|
||
};
|
||
|
||
window.createNewWorkflow = function(templateType = 'blank') {
|
||
clearGraph();
|
||
|
||
if (templateType !== 'blank' && typeof TEMPLATES !== 'undefined' && TEMPLATES[templateType]) {
|
||
try {
|
||
graph.configure(TEMPLATES[templateType]);
|
||
document.getElementById('workflow-name').value = "New " + templateType.charAt(0).toUpperCase() + templateType.slice(1) + " Project";
|
||
historyStack = [JSON.stringify(graph.serialize())];
|
||
} catch(e) {
|
||
console.error("Failed to load template", e);
|
||
}
|
||
}
|
||
|
||
document.getElementById('new-project-menu').classList.add('hidden');
|
||
window.switchArchitectView('designer');
|
||
};
|
||
|
||
document.addEventListener('click', (e) => {
|
||
const menu = document.getElementById('new-project-menu');
|
||
const btn = e.target.closest('button[onclick*="new-project-menu"]');
|
||
if (!btn && menu && !menu.classList.contains('hidden')) {
|
||
menu.classList.add('hidden');
|
||
}
|
||
});
|
||
|
||
async function loadLibrary() {
|
||
// This is the full grid view
|
||
const grid = document.getElementById('library-grid');
|
||
grid.innerHTML = '<div class="p-10 text-center text-gray-500 italic">Scanning archives...</div>';
|
||
|
||
try {
|
||
const resp = await fetch("{{ url_for('admin_list_workflows') }}");
|
||
loadedWorkflows = await resp.json();
|
||
|
||
if (loadedWorkflows.length === 0) {
|
||
grid.innerHTML = '<div class="col-span-full p-20 text-center border-2 border-dashed border-white/5 rounded-2xl"><p class="text-gray-600">Your library is currently empty.</p></div>';
|
||
return;
|
||
}
|
||
|
||
grid.innerHTML = loadedWorkflows.map((w, index) => `
|
||
<div class="workflow-card">
|
||
<div class="flex justify-between items-start mb-4">
|
||
<div class="h-10 w-10 rounded bg-indigo-500/20 flex items-center justify-center text-indigo-400">
|
||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 4a2 2 0 114 0v1a1 1 0 001 1h3a1 1 0 011 1v3a1 1 0 01-1 1h-1a2 2 0 100 4h1a1 1 0 011 1v3a1 1 0 01-1 1h-3a1 1 0 01-1-1v-1a2 2 0 10-4 0v1a1 1 0 01-1 1H7a1 1 0 01-1-1v-3a1 1 0 00-1-1H4a2 2 0 110-4h1a1 1 0 001-1V7a1 1 0 011-1h3a1 1 0 001-1V4z"></path></svg>
|
||
</div>
|
||
<div class="flex gap-2">
|
||
<button onclick="restoreWorkflowByIndex(${index})" class="p-2 hover:bg-white/10 rounded text-indigo-400" title="Edit Graph">
|
||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"></path></svg>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<h3 class="text-white font-bold text-lg mb-1">${w.name}</h3>
|
||
<p class="text-gray-500 text-xs mb-6">Internal Sub-graph compatible</p>
|
||
<div class="flex gap-3">
|
||
<a href="/admin/playground?model=${encodeURIComponent(w.name)}" class="flex-grow text-center py-2 bg-white/5 hover:bg-indigo-600 rounded-lg text-xs font-bold transition-all">TEST IN PLAYGROUND</a>
|
||
<button onclick="deleteWorkflow(${w.id})" class="p-2 text-red-500 hover:bg-red-500/10 rounded-lg">
|
||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path></svg>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
`).join('');
|
||
} catch (e) {
|
||
console.error("Load failed", e);
|
||
grid.innerHTML = '<div class="p-10 text-red-400">Failed to load library. Check console for details.</div>';
|
||
}
|
||
}
|
||
function addNode(type, properties = {}) {
|
||
if (!graph || !canvas) return;
|
||
const node = LiteGraph.createNode(type);
|
||
if(!node) return;
|
||
|
||
// Handle sub-graph properties
|
||
if (properties.workflow_name) {
|
||
node.properties.workflow_name = properties.workflow_name;
|
||
node.title = "FLOW: " + properties.workflow_name.toUpperCase();
|
||
if (node.refreshSlots) node.refreshSlots();
|
||
}
|
||
|
||
const center = canvas.convertCanvasToOffset([canvas.canvas.width/2, canvas.canvas.height/2]);
|
||
node.pos = [center[0] - 100, center[1] - 50];
|
||
|
||
// Force size computation before adding to graph
|
||
const size = node.computeSize();
|
||
node.size[0] = Math.max(size[0], 280);
|
||
node.size[1] = size[1];
|
||
|
||
graph.add(node);
|
||
}
|
||
|
||
async function saveWorkflow() {
|
||
const nameInput = document.getElementById('workflow-name');
|
||
if(!nameInput || !graph) return;
|
||
const name = nameInput.value;
|
||
if (!name) return alert("Workflow needs an ID name.");
|
||
const resp = await fetch("{{ url_for('admin_save_workflow') }}", {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': '{{ csrf_token }}' },
|
||
body: JSON.stringify({ name, graph: graph.serialize() })
|
||
});
|
||
if (resp.ok) { alert("Successfully deployed."); loadWorkflows(); }
|
||
}
|
||
|
||
// Store loaded workflows globally to avoid JSON parsing in onclick handlers
|
||
let loadedWorkflows = [];
|
||
|
||
async function loadWorkflows() {
|
||
const list = document.getElementById('workflow-list');
|
||
if(!list) return;
|
||
try {
|
||
const resp = await fetch("{{ url_for('admin_list_workflows') }}");
|
||
loadedWorkflows = await resp.json();
|
||
|
||
if (loadedWorkflows.length === 0) {
|
||
list.innerHTML = '<p class="text-[9px] text-gray-700 italic">No saved flows.</p>';
|
||
return;
|
||
}
|
||
|
||
list.innerHTML = loadedWorkflows.map((w, index) => `
|
||
<div class="saved-flow-card">
|
||
<button onclick='restoreWorkflowByIndex(${index})' class="text-[10px] font-black text-gray-400 hover:text-indigo-400 truncate text-left flex-grow uppercase">${w.name}</button>
|
||
<div class="flex items-center gap-1">
|
||
<a href="/admin/playground?model=${encodeURIComponent(w.name)}" class="text-[9px] font-black bg-indigo-600/20 text-indigo-400 border border-indigo-500/30 px-1.5 py-0.5 rounded hover:bg-indigo-600/40 transition-colors">TEST</a>
|
||
<button onclick="deleteWorkflow(${w.id})" class="text-red-500 hover:text-red-400 px-2">×</button>
|
||
</div>
|
||
</div>
|
||
`).join('');
|
||
} catch(e) {
|
||
console.error("Load failed", e);
|
||
list.innerHTML = '<p class="text-red-500 text-[9px]">Failed to load flows.</p>';
|
||
}
|
||
}
|
||
|
||
window.restoreWorkflowByIndex = (index) => {
|
||
const w = loadedWorkflows[index];
|
||
if (w) {
|
||
loadGraph(w.graph, w.name);
|
||
window.switchArchitectView('designer');
|
||
}
|
||
};
|
||
|
||
window.loadGraph = (data, name) => {
|
||
if (!graph) {
|
||
console.error("Designer engine (graph) not initialized yet.");
|
||
return;
|
||
}
|
||
|
||
isRestoring = true;
|
||
|
||
// Robustness: ensure data is an object
|
||
let graphData = data;
|
||
if (typeof data === 'string') {
|
||
try {
|
||
graphData = JSON.parse(data);
|
||
} catch(e) {
|
||
console.error("Failed to parse graph data string", e);
|
||
isRestoring = false;
|
||
return;
|
||
}
|
||
}
|
||
|
||
// 1. Clear current state to prevent merging/collision
|
||
graph.clear();
|
||
|
||
try {
|
||
// 2. Load the new data
|
||
if (graphData && graphData.nodes) {
|
||
graph.configure(graphData);
|
||
|
||
// --- UX FIX: Force dynamic resize for all nodes after loading ---
|
||
graph._nodes.forEach(node => {
|
||
const size = node.computeSize();
|
||
// Apply a minimum width of 280px for standard look
|
||
node.size[0] = Math.max(size[0], 280);
|
||
node.size[1] = size[1];
|
||
});
|
||
|
||
// 3. Auto-layout on load to prevent overlapping/crumbled views
|
||
if (typeof window.rearrangeNodes === 'function') {
|
||
window.rearrangeNodes();
|
||
}
|
||
}
|
||
} catch (err) {
|
||
console.error("LiteGraph configuration failed:", err);
|
||
} finally {
|
||
document.getElementById('workflow-name').value = name || "";
|
||
isRestoring = false;
|
||
|
||
// 4. Reset history stack for the newly loaded graph
|
||
if (graph) {
|
||
historyStack = [JSON.stringify(graph.serialize())];
|
||
historyIndex = 0;
|
||
}
|
||
|
||
// 5. Force UI sync
|
||
if (canvas) canvas.setDirty(true, true);
|
||
}
|
||
}
|
||
|
||
// Initialize an empty state on load
|
||
setTimeout(pushHistoryState, 500);
|
||
|
||
window.deleteWorkflow = async (id) => {
|
||
if (!confirm("Are you sure you want to permanently delete this workflow?")) return;
|
||
try {
|
||
const response = await fetch(`/admin/conception/${id}`, {
|
||
method: 'DELETE',
|
||
headers: { 'X-CSRF-Token': '{{ csrf_token }}' }
|
||
});
|
||
if (response.ok) {
|
||
// Refresh both potential views to be safe
|
||
if (typeof loadLibrary === 'function') await loadLibrary();
|
||
if (typeof loadWorkflows === 'function') await loadWorkflows();
|
||
} else {
|
||
const err = await response.json();
|
||
alert("Failed to delete: " + (err.detail || "Server error"));
|
||
}
|
||
} catch (e) {
|
||
console.error("Delete request error:", e);
|
||
alert("Connection error: " + e.message);
|
||
}
|
||
};
|
||
|
||
window.testInPlayground = async () => {
|
||
const name = document.getElementById('workflow-name').value;
|
||
if (!name) {
|
||
alert("Please name the workflow first.");
|
||
return;
|
||
}
|
||
// Auto-save before testing to ensure consistency
|
||
const nameInput = document.getElementById('workflow-name');
|
||
const resp = await fetch("{{ url_for('admin_save_workflow') }}", {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': '{{ csrf_token }}' },
|
||
body: JSON.stringify({ name, graph: graph.serialize() })
|
||
});
|
||
|
||
if (resp.ok) {
|
||
window.location.href = `/admin/playground?model=${encodeURIComponent(name)}`;
|
||
} else {
|
||
alert("Failed to sync workflow before test.");
|
||
}
|
||
};
|
||
|
||
// --- Node Studio Editors (CodeMirror) ---
|
||
let jsEditor, pyEditor;
|
||
|
||
function initEditors() {
|
||
pyEditor = CodeMirror(document.getElementById("cm-py-container"), {
|
||
mode: "python",
|
||
theme: "monokai",
|
||
lineNumbers: true,
|
||
indentUnit: 4,
|
||
matchBrackets: true
|
||
});
|
||
|
||
jsEditor = CodeMirror(document.getElementById("cm-js-container"), {
|
||
mode: "javascript",
|
||
theme: "monokai",
|
||
lineNumbers: true,
|
||
indentUnit: 4,
|
||
matchBrackets: true
|
||
});
|
||
|
||
const fixSize = () => {
|
||
jsEditor.setSize("100%", "100%");
|
||
pyEditor.setSize("100%", "100%");
|
||
};
|
||
fixSize();
|
||
window.addEventListener("resize", fixSize);
|
||
}
|
||
|
||
window.switchStudioTab = (lang) => {
|
||
const pyCont = document.getElementById('cm-py-container');
|
||
const jsCont = document.getElementById('cm-js-container');
|
||
const pyTab = document.getElementById('studio-tab-py');
|
||
const jsTab = document.getElementById('studio-tab-js');
|
||
|
||
if (lang === 'py') {
|
||
pyCont.classList.remove('hidden'); jsCont.classList.add('hidden');
|
||
pyTab.classList.replace('border-transparent', 'border-emerald-500');
|
||
pyTab.classList.replace('text-gray-500', 'text-emerald-400');
|
||
jsTab.classList.replace('border-emerald-500', 'border-transparent');
|
||
jsTab.classList.replace('text-emerald-400', 'text-gray-500');
|
||
pyEditor.refresh();
|
||
} else {
|
||
pyCont.classList.add('hidden'); jsCont.classList.remove('hidden');
|
||
jsTab.classList.replace('border-transparent', 'border-emerald-500');
|
||
jsTab.classList.replace('text-gray-500', 'text-emerald-400');
|
||
pyTab.classList.replace('border-emerald-500', 'border-transparent');
|
||
pyTab.classList.replace('text-emerald-400', 'text-gray-500');
|
||
jsEditor.refresh();
|
||
}
|
||
};
|
||
|
||
window.initNewNode = () => {
|
||
if (!confirm("Clear editors for a fresh node pair?")) return;
|
||
|
||
pyEditor.setValue(`from typing import Dict, Any\nfrom app.nodes.base import BaseNode\n\nclass MyNewNode(BaseNode):\n node_type = "custom/my_node"\n node_title = "My New Node"\n node_category = "Custom"\n\n async def execute(self, engine, node: Dict[str, Any], output_slot_idx: int) -> Any:\n return "Hello from Python"`);
|
||
|
||
jsEditor.setValue(`function MyNodeUI() {\n this.addInput("In", "string");\n this.addOutput("Out", "string");\n this.title = "MY NEW NODE";\n this.size = this.computeSize();\n}\nLiteGraph.registerNodeType("custom/my_node", MyNodeUI);`);
|
||
};
|
||
|
||
window.saveNode = async () => {
|
||
const py = pyEditor.getValue();
|
||
const js = jsEditor.getValue();
|
||
|
||
if (!py.trim() || !js.trim()) return alert("Both Python logic and JS UI are required.");
|
||
|
||
const typeMatch = js.match(/registerNodeType\("([^"]+)"/);
|
||
if (!typeMatch) return alert("JS code must call registerNodeType and define a unique ID.");
|
||
|
||
const fileName = typeMatch[1].split('/').pop() + "_node";
|
||
|
||
try {
|
||
const resp = await fetch("/admin/node-builder/save", {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': '{{ csrf_token }}' },
|
||
body: JSON.stringify({
|
||
name: fileName,
|
||
py_code: py,
|
||
js_code: js
|
||
})
|
||
});
|
||
|
||
const result = await resp.json();
|
||
if (resp.ok && result.success) {
|
||
alert(result.message);
|
||
loadCustomNodes();
|
||
} else {
|
||
alert("Deployment failed: " + (result.error || "Check logs"));
|
||
}
|
||
} catch (e) {
|
||
alert("Network error: " + e.message);
|
||
}
|
||
};
|
||
|
||
async function loadCustomNodes() {
|
||
try {
|
||
const resp = await fetch("/admin/api/nodes");
|
||
if (!resp.ok) return;
|
||
const nodes = await resp.json();
|
||
const list = document.getElementById('custom-nodes-list');
|
||
list.innerHTML = nodes.map(n => `
|
||
<div class="p-2 bg-white/5 rounded-lg flex justify-between items-center group">
|
||
<span class="text-xs text-gray-300 font-mono">${n.name}.py</span>
|
||
<button onclick="loadNode('${n.name}')" class="text-[10px] text-indigo-400 font-bold uppercase opacity-0 group-hover:opacity-100 transition-opacity">Load File</button>
|
||
</div>
|
||
`).join('');
|
||
} catch (e) { console.error(e); }
|
||
}
|
||
|
||
window.loadNode = async (name) => {
|
||
try {
|
||
const resp = await fetch(`/admin/api/nodes/${name}`);
|
||
if (!resp.ok) return;
|
||
const data = await resp.json();
|
||
jsEditor.setValue(data.js || '');
|
||
pyEditor.setValue(data.py || '');
|
||
} catch (e) { alert("Failed to load: " + e.message); }
|
||
};
|
||
|
||
window.toggleStudio = (show) => {
|
||
if (show) {
|
||
window.switchArchitectView('studio');
|
||
if (!jsEditor) initEditors();
|
||
loadCustomNodes();
|
||
} else {
|
||
window.switchArchitectView('designer');
|
||
}
|
||
};
|
||
|
||
function clearGraph() {
|
||
isRestoring = true;
|
||
graph.clear();
|
||
isRestoring = false;
|
||
document.getElementById('workflow-name').value = '';
|
||
|
||
// Reset history
|
||
historyStack = [JSON.stringify(graph.serialize())];
|
||
historyIndex = 0;
|
||
}
|
||
|
||
/**
|
||
* Correctly rearranges all nodes in the graph using a Longest-Path algorithm.
|
||
* Ensures nodes are always placed to the right of all their dependencies.
|
||
*/
|
||
window.rearrangeNodes = function() {
|
||
if (!graph || graph._nodes.length === 0) return;
|
||
|
||
const allNodes = graph._nodes;
|
||
// Filter out notes for separate placement
|
||
const noteNodes = allNodes.filter(n => n.type === "hub/note");
|
||
const logicNodes = allNodes.filter(n => n.type !== "hub/note");
|
||
|
||
const levels = new Map();
|
||
const spacingX = 550;
|
||
const spacingY = 60;
|
||
const startX = 80;
|
||
|
||
// 1. Separate Layer: Notes at the very top
|
||
let currentNoteY = 50;
|
||
noteNodes.forEach(note => {
|
||
note.pos[0] = startX;
|
||
note.pos[1] = currentNoteY;
|
||
currentNoteY += note.size[1] + 40;
|
||
});
|
||
|
||
// Calculate offset for logic nodes to start below notes
|
||
const logicStartY = noteNodes.length > 0 ? currentNoteY + 40 : 80;
|
||
|
||
// 2. Logic Layer: Standard Longest Path Leveling
|
||
logicNodes.forEach(n => levels.set(n.id, 0));
|
||
|
||
let changed = true;
|
||
let iterations = 0;
|
||
const maxIterations = logicNodes.length * 2;
|
||
|
||
while (changed && iterations < maxIterations) {
|
||
changed = false;
|
||
iterations++;
|
||
Object.values(graph.links).forEach(link => {
|
||
if (!link) return;
|
||
const origin = graph.getNodeById(link.origin_id);
|
||
const target = graph.getNodeById(link.target_id);
|
||
if (origin && target && origin.type !== "hub/note" && target.type !== "hub/note") {
|
||
const minTargetLevel = levels.get(origin.id) + 1;
|
||
if (levels.get(target.id) < minTargetLevel) {
|
||
levels.set(target.id, minTargetLevel);
|
||
changed = true;
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
const levelGroups = {};
|
||
logicNodes.forEach(node => {
|
||
const lv = levels.get(node.id);
|
||
if (!levelGroups[lv]) levelGroups[lv] = [];
|
||
levelGroups[lv].push(node);
|
||
});
|
||
|
||
Object.keys(levelGroups).sort((a, b) => a - b).forEach(lv => {
|
||
let currentY = logicStartY;
|
||
levelGroups[lv].forEach(node => {
|
||
node.pos[0] = startX + (lv * spacingX);
|
||
node.pos[1] = currentY;
|
||
currentY += node.size[1] + spacingY;
|
||
});
|
||
});
|
||
|
||
// 3. Update UI
|
||
if (canvas) {
|
||
canvas.setDirty(true, true);
|
||
// Center the view on the new layout
|
||
canvas.ds.offset[0] = 0;
|
||
canvas.ds.offset[1] = 0;
|
||
}
|
||
pushHistoryState();
|
||
};
|
||
|
||
window.addWorkflowAsNode = function(name) {
|
||
addNode("hub/subgraph", { workflow_name: name });
|
||
};
|
||
|
||
// --- Page Tour Logic ---
|
||
const ARCHITECT_TOUR = [
|
||
{
|
||
target: '#library-view h1',
|
||
text: "Welcome to the <b>Forge</b>. Here you design the cognitive pathways of your AI cluster."
|
||
},
|
||
{
|
||
target: 'button[onclick*="openGraphArchitect"]',
|
||
text: "<b>AI Architect</b>: Don't want to wire things manually? Describe a workflow in plain English, and the Hub Agent will build the graph for you."
|
||
},
|
||
{
|
||
target: 'button[onclick*="toggleStudio"]',
|
||
text: "<b>Node Studio</b>: Use this to code your own Python/JS nodes if the standard library isn't enough."
|
||
},
|
||
{
|
||
target: '.workflow-card:first-child',
|
||
text: "Your saved workflows appear here. You can even embed one workflow inside another as a single <b>Sub-graph</b> node."
|
||
},
|
||
{
|
||
target: '#new-project-menu',
|
||
text: "Start with a <b>Standard Template</b> like the 'RAG Pipeline' or 'Parallel MoE' to see advanced orchestration in action."
|
||
}
|
||
];
|
||
|
||
// Trigger on load passing the independent setting flag
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
if (window.startCustomTour) {
|
||
window.startCustomTour(ARCHITECT_TOUR, 'tour_conception_v1', {{ settings.tour_workflows | tojson }});
|
||
}
|
||
});
|
||
|
||
// --- AI GRAPH ARCHITECT (Robust Definition) ---
|
||
window.openGraphArchitect = (mode = 'build') => {
|
||
console.log(`Dispatching AI Architect (${mode})...`);
|
||
const template = document.getElementById('ai-graph-builder-template');
|
||
|
||
const title = mode === 'edit' ? "Refine Workflow with AI" : "Build Workflow with AI Architect";
|
||
window.showModal(title, template.innerHTML);
|
||
|
||
// Update the button for the specific mode
|
||
const runBtn = document.getElementById('graph-build-run-btn');
|
||
if (mode === 'edit') {
|
||
runBtn.innerText = "Apply AI Refinement";
|
||
runBtn.onclick = () => window.executeGraphBuild('edit');
|
||
}
|
||
|
||
// Ensure inputs are bound after modal content is injected into DOM
|
||
setTimeout(() => {
|
||
const promptEl = document.getElementById('build-graph-prompt');
|
||
if (promptEl) promptEl.value = window.Architect.prompt || "";
|
||
|
||
const imgInput = document.getElementById('g-img-input');
|
||
const docInput = document.getElementById('g-doc-input');
|
||
|
||
if (imgInput) imgInput.onchange = (e) => window.Architect.handleFiles(e.target.files, () => window.Architect.renderPreviews('g-previews'));
|
||
if (docInput) docInput.onchange = (e) => window.Architect.handleFiles(e.target.files, () => window.Architect.renderPreviews('g-previews'));
|
||
|
||
window.Architect.renderPreviews('g-previews');
|
||
}, 100);
|
||
};
|
||
|
||
window.triggerGraphWebImport = () => {
|
||
window.Architect.triggerWebImport('build-graph-prompt', window.openGraphArchitect);
|
||
};
|
||
|
||
window.executeGraphBuild = (mode = 'build') => {
|
||
const endpoint = mode === 'edit' ? "{{ url_for('admin_api_edit_workflow') }}" : "{{ url_for('admin_api_build_workflow') }}";
|
||
|
||
const originalPrepare = window.Architect.prepareFormData;
|
||
|
||
// CRITICAL FIX: Override preparation logic BEFORE calling executeBuild
|
||
if (mode === 'edit') {
|
||
window.Architect.prepareFormData = function(id) {
|
||
const fd = originalPrepare.call(this, id);
|
||
fd.append("current_graph", JSON.stringify(graph.serialize()));
|
||
return fd;
|
||
};
|
||
}
|
||
|
||
window.Architect.executeBuild(
|
||
'build-graph-prompt',
|
||
endpoint,
|
||
'build-graph-status',
|
||
'graph-build-run-btn',
|
||
(data) => {
|
||
// Switch view
|
||
document.getElementById('library-view').classList.add('hidden');
|
||
document.getElementById('designer-view').classList.remove('hidden');
|
||
setTimeout(() => { if(window.resizeCanvas) window.resizeCanvas(); }, 100);
|
||
|
||
clearGraph();
|
||
graph.configure(data.graph);
|
||
|
||
// Polish
|
||
rearrangeNodes();
|
||
if (mode === 'build') {
|
||
document.getElementById('workflow-name').value = "AI Generated Flow";
|
||
}
|
||
|
||
// Restore original state
|
||
window.Architect.prepareFormData = originalPrepare;
|
||
}
|
||
).catch(() => {
|
||
// Restore on error
|
||
window.Architect.prepareFormData = originalPrepare;
|
||
});
|
||
};
|
||
|
||
// --- AI NODE SCULPTOR ---
|
||
window.openNodeSculptor = () => {
|
||
const template = document.getElementById('ai-node-sculptor-template');
|
||
window.showModal("Sculpt New Node with AI", template.innerHTML);
|
||
|
||
// Bind UI inputs for Architect module
|
||
setTimeout(() => {
|
||
document.getElementById('m-img-input').onchange = (e) => window.Architect.handleFiles(e.target.files, () => window.Architect.renderPreviews('m-previews'));
|
||
document.getElementById('m-doc-input').onchange = (e) => window.Architect.handleFiles(e.target.files, () => window.Architect.renderPreviews('m-previews'));
|
||
}, 100);
|
||
};
|
||
|
||
window.executeNodeBuild = () => {
|
||
window.Architect.executeBuild(
|
||
'build-node-prompt',
|
||
"{{ url_for('admin_api_save_custom_node') }}?mode=ai_sculpt",
|
||
'build-node-status',
|
||
'node-build-run-btn',
|
||
(data) => {
|
||
alert(data.message);
|
||
loadCustomNodes();
|
||
// Optionally load the generated code into the editor
|
||
if (data.filename) loadNode(data.filename.replace('.py', ''));
|
||
}
|
||
);
|
||
};
|
||
|
||
// --- SSE Status Listener for Graph Builder ---
|
||
const buildGraphEvtSource = new EventSource("{{ url_for('sse_events') }}");
|
||
buildGraphEvtSource.onmessage = (e) => {
|
||
const msg = JSON.parse(e.data);
|
||
if (msg.type === 'snapshot' || !msg.id || !msg.id.startsWith('sys_build_graph_')) return;
|
||
|
||
const box = document.getElementById('build-graph-status');
|
||
if (!box || !msg.error) return;
|
||
|
||
box.classList.remove('hidden');
|
||
const entry = document.createElement('div');
|
||
entry.className = msg.type === 'error' ? 'text-red-400' : 'text-indigo-300';
|
||
entry.innerHTML = `<span class="opacity-50">[${new Date().toLocaleTimeString()}]</span> ${msg.error}`;
|
||
box.appendChild(entry);
|
||
box.scrollTop = box.scrollHeight;
|
||
};
|
||
|
||
loadWorkflows();
|
||
toggleLibrary(); // Start in library mode
|
||
|
||
// --- STARTUP ---
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
// 2. Build Sidebar from Registered Nodes
|
||
const regNodes = {{ (registered_nodes or []) | tojson | safe }};
|
||
const sidebar = document.getElementById('sidebar-node-list');
|
||
|
||
if (!sidebar) {
|
||
console.error("Architect Error: sidebar-node-list element not found.");
|
||
return;
|
||
}
|
||
|
||
const cats = {};
|
||
regNodes.forEach(n => {
|
||
if(!n || !n.category) return;
|
||
if(!cats[n.category]) cats[n.category] = [];
|
||
cats[n.category].push(n);
|
||
});
|
||
|
||
Object.keys(cats).forEach(cat => {
|
||
const catDiv = document.createElement('div');
|
||
catDiv.className = "node-category-label " + (sidebar.children.length > 0 ? "mt-6" : "");
|
||
catDiv.innerText = cat;
|
||
sidebar.appendChild(catDiv);
|
||
|
||
cats[cat].forEach(n => {
|
||
const item = document.createElement('div');
|
||
item.className = "node-item";
|
||
item.innerHTML = `
|
||
<span class="text-lg opacity-70">${n.icon || '🧩'}</span>
|
||
<span class="font-bold">${n.title}</span>
|
||
`;
|
||
item.onclick = () => addNode(n.type);
|
||
sidebar.appendChild(item);
|
||
});
|
||
});
|
||
|
||
// --- KEYBOARD SHORTCUTS (Undo/Redo) ---
|
||
window.addEventListener('keydown', (e) => {
|
||
const isZ = e.key.toLowerCase() === 'z';
|
||
const isY = e.key.toLowerCase() === 'y';
|
||
const cmdOrCtrl = e.ctrlKey || e.metaKey;
|
||
|
||
if (cmdOrCtrl && isZ) {
|
||
e.preventDefault();
|
||
if (e.shiftKey) {
|
||
window.redo(); // Ctrl+Shift+Z
|
||
} else {
|
||
window.undo(); // Ctrl+Z
|
||
}
|
||
} else if (cmdOrCtrl && isY) {
|
||
e.preventDefault();
|
||
window.redo(); // Ctrl+Y
|
||
}
|
||
});
|
||
|
||
// Initialize Core
|
||
graph = new LGraph();
|
||
|
||
// 1. Engine Config
|
||
graph.config = {
|
||
limit_to_container: false,
|
||
align_to_grid: true
|
||
};
|
||
|
||
// --- UX POLISH: Prevent messy link segments and snapping noise ---
|
||
LGraphCanvas.link_type = LiteGraph.STRAIGHT_LINK;
|
||
LGraphCanvas.snap_distance = 15;
|
||
|
||
// 2. Bind History
|
||
graph.onNodeAdded = pushHistoryState;
|
||
graph.onNodeRemoved = pushHistoryState;
|
||
graph.onNodeConnectionChange = pushHistoryState;
|
||
|
||
// 3. Initialize Canvas
|
||
canvasEl = document.getElementById("conception-canvas");
|
||
canvas = new LGraphCanvas(canvasEl, graph);
|
||
|
||
// 4. UI Polish
|
||
canvas.background_image = null;
|
||
canvas.render_canvas_border = false;
|
||
canvas.show_info = false;
|
||
canvas.allow_dragnode_stack = true;
|
||
|
||
// 5. Force Initial Size & Route Injection
|
||
setTimeout(async () => {
|
||
await fetchLibraries();
|
||
await fetchServerTools();
|
||
resizeCanvas();
|
||
|
||
// Wait for workflows to load from DB
|
||
await loadWorkflows();
|
||
loadTemplates();
|
||
fetchServerSkills();
|
||
|
||
// Check if we should jump straight to a specific graph
|
||
const urlParams = new URLSearchParams(window.location.search);
|
||
const targetName = urlParams.get('name');
|
||
|
||
if (targetName) {
|
||
const w = loadedWorkflows.find(x => x.name === targetName);
|
||
if (w) {
|
||
loadGraph(w.graph, w.name);
|
||
window.toggleLibrary(false); // Show designer
|
||
return;
|
||
}
|
||
}
|
||
|
||
// Default to library if nothing specific requested or found
|
||
window.toggleLibrary(true);
|
||
}, 150);
|
||
|
||
window.addEventListener('resize', resizeCanvas);
|
||
});
|
||
</script>
|
||
{% endblock %} |