graphcore-studio 0.2.0__py3-none-win32.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- graphcore/__init__.py +303 -0
- graphcore/graphcore.dll +0 -0
- graphcore_studio-0.2.0.dist-info/METADATA +171 -0
- graphcore_studio-0.2.0.dist-info/RECORD +28 -0
- graphcore_studio-0.2.0.dist-info/WHEEL +5 -0
- graphcore_studio-0.2.0.dist-info/entry_points.txt +3 -0
- graphcore_studio-0.2.0.dist-info/licenses/LICENSE +21 -0
- include/graphcore/c_api.h +51 -0
- include/graphcore/graphcore.hpp +83 -0
- lib/cmake/GraphCore/GraphCoreConfig.cmake +26 -0
- lib/cmake/GraphCore/GraphCoreConfigVersion.cmake +65 -0
- lib/cmake/GraphCore/GraphCoreTargets-release.cmake +19 -0
- lib/cmake/GraphCore/GraphCoreTargets.cmake +107 -0
- lib/graphcore.lib +0 -0
- lib/graphcore_c.lib +0 -0
- studio/__init__.py +1 -0
- studio/cli.py +9 -0
- studio/engine.py +338 -0
- studio/example_plugin.py +8 -0
- studio/server.py +366 -0
- studio/templates/01-research.json +114 -0
- studio/templates/02-structured.json +86 -0
- studio/templates/03-tools.json +70 -0
- studio/templates/04-team.json +117 -0
- studio/templates/05-model.json +19 -0
- studio/web/app.js +108 -0
- studio/web/index.html +43 -0
- studio/web/style.css +9 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
{
|
|
2
|
+
"format": "graphcore.studio.v1",
|
|
3
|
+
"id": "supervisor-team",
|
|
4
|
+
"name": "Supervisor & specialists",
|
|
5
|
+
"description": "A supervisor delegates to a specialist through native conditional routing.",
|
|
6
|
+
"inputs": {
|
|
7
|
+
"input": "Design a local-first agent system"
|
|
8
|
+
},
|
|
9
|
+
"nodes": [
|
|
10
|
+
{
|
|
11
|
+
"id": "input",
|
|
12
|
+
"type": "input",
|
|
13
|
+
"label": "Your request",
|
|
14
|
+
"x": 40,
|
|
15
|
+
"y": 180,
|
|
16
|
+
"config": {}
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"id": "supervisor",
|
|
20
|
+
"type": "agent",
|
|
21
|
+
"label": "Supervisor",
|
|
22
|
+
"x": 310,
|
|
23
|
+
"y": 180,
|
|
24
|
+
"config": {
|
|
25
|
+
"provider": "demo",
|
|
26
|
+
"model": "llama3.2",
|
|
27
|
+
"system": "Classify the request. Return exactly technical or general.",
|
|
28
|
+
"prompt": "Classify: {{input}}",
|
|
29
|
+
"demo_response": "technical",
|
|
30
|
+
"output_key": "route"
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"id": "route",
|
|
35
|
+
"type": "condition",
|
|
36
|
+
"label": "Choose a specialist",
|
|
37
|
+
"x": 580,
|
|
38
|
+
"y": 180,
|
|
39
|
+
"config": {
|
|
40
|
+
"field": "route",
|
|
41
|
+
"expected": "technical"
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"id": "technical",
|
|
46
|
+
"type": "agent",
|
|
47
|
+
"label": "Technical specialist",
|
|
48
|
+
"x": 860,
|
|
49
|
+
"y": 50,
|
|
50
|
+
"config": {
|
|
51
|
+
"provider": "demo",
|
|
52
|
+
"model": "llama3.2",
|
|
53
|
+
"system": "You are a software architect. Give a concise technical answer.",
|
|
54
|
+
"prompt": "Answer: {{input}}",
|
|
55
|
+
"demo_response": "Technical brief: {{input}}\nUse a local UI, a typed workflow document, a C++ scheduler, and Python adapters.",
|
|
56
|
+
"output_key": "answer"
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
"id": "general",
|
|
61
|
+
"type": "agent",
|
|
62
|
+
"label": "General specialist",
|
|
63
|
+
"x": 860,
|
|
64
|
+
"y": 330,
|
|
65
|
+
"config": {
|
|
66
|
+
"provider": "demo",
|
|
67
|
+
"model": "llama3.2",
|
|
68
|
+
"system": "You are a helpful generalist. Give a clear practical answer.",
|
|
69
|
+
"prompt": "Answer: {{input}}",
|
|
70
|
+
"demo_response": "Practical brief: {{input}}\nStart small, test the result, and keep a person involved.",
|
|
71
|
+
"output_key": "answer"
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
"id": "output",
|
|
76
|
+
"type": "output",
|
|
77
|
+
"label": "Team response",
|
|
78
|
+
"x": 1140,
|
|
79
|
+
"y": 180,
|
|
80
|
+
"config": {
|
|
81
|
+
"template": "{{answer}}"
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
],
|
|
85
|
+
"edges": [
|
|
86
|
+
{
|
|
87
|
+
"source": "input",
|
|
88
|
+
"target": "supervisor",
|
|
89
|
+
"port": "next"
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
"source": "supervisor",
|
|
93
|
+
"target": "route",
|
|
94
|
+
"port": "next"
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
"source": "route",
|
|
98
|
+
"target": "technical",
|
|
99
|
+
"port": "true"
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
"source": "route",
|
|
103
|
+
"target": "general",
|
|
104
|
+
"port": "false"
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
"source": "technical",
|
|
108
|
+
"target": "output",
|
|
109
|
+
"port": "next"
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
"source": "general",
|
|
113
|
+
"target": "output",
|
|
114
|
+
"port": "next"
|
|
115
|
+
}
|
|
116
|
+
]
|
|
117
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"format": "graphcore.studio.v1",
|
|
3
|
+
"id": "chat-model-playground",
|
|
4
|
+
"name": "Chat model playground",
|
|
5
|
+
"description": "Compose system and user messages, insert workflow values, and run with an offline fixture or your provider.",
|
|
6
|
+
"inputs": {
|
|
7
|
+
"question": "What makes a workflow graph useful?",
|
|
8
|
+
"audience": "A new developer"
|
|
9
|
+
},
|
|
10
|
+
"nodes": [
|
|
11
|
+
{"id":"start","type":"input","label":"Input","x":40,"y":150,"config":{}},
|
|
12
|
+
{"id":"chat","type":"model","label":"Chat Model","x":350,"y":150,"config":{"provider":"demo","model":"openai/gpt-4o-mini","messages":[{"role":"system","content":"Explain ideas clearly for {{audience}}. Be concise."},{"role":"user","content":"{{question}}"}],"temperature":0.2,"max_tokens":1024,"demo_response":"A workflow graph makes each step visible, reusable, and easy to route. (Offline demo fixture for: {{question}})","output_key":"answer"}},
|
|
13
|
+
{"id":"finish","type":"output","label":"Output","x":690,"y":150,"config":{"template":"{{answer}}"}}
|
|
14
|
+
],
|
|
15
|
+
"edges": [
|
|
16
|
+
{"source":"start","target":"chat","port":"next"},
|
|
17
|
+
{"source":"chat","target":"finish","port":"next"}
|
|
18
|
+
]
|
|
19
|
+
}
|
studio/web/app.js
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const $ = s => document.querySelector(s);
|
|
3
|
+
const esc = value => String(value ?? '').replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
|
4
|
+
const clone = value => JSON.parse(JSON.stringify(value));
|
|
5
|
+
const icons = {
|
|
6
|
+
input:'<path d="M4 3h9l4 4v11H4zM12 3v5h5M7 12h7M7 15h4"/>',
|
|
7
|
+
agent:'<rect x="4" y="6" width="14" height="11" rx="3"/><path d="M11 2v4M8 11h.01M14 11h.01M8 14h6M1 10v4M21 10v4"/>',
|
|
8
|
+
model:'<path d="M3 5h16M3 11h16M3 17h10"/><circle cx="18" cy="17" r="3"/>',
|
|
9
|
+
condition:'<path d="M5 3v4c0 4 12 0 12 5v6M5 7v11M2 15l3 3 3-3M14 15l3 3 3-3"/>',
|
|
10
|
+
tool:'<path d="M8 4L2 10l6 6M14 4l6 6-6 6M12 2l-3 17"/>',
|
|
11
|
+
schema:'<path d="M7 3H4v15h3M15 3h3v15h-3M8 10l2 2 4-5"/>',
|
|
12
|
+
approval:'<path d="M7 9V5a2 2 0 014 0v5M11 8a2 2 0 014 0v3M15 10a2 2 0 014 0v4c0 4-4 6-7 5l-6-6a2 2 0 012-3l2 2V8"/>',
|
|
13
|
+
output:'<path d="M12 3h6v15H4V8M2 3h8M7 0l3 3-3 3M7 12h7M7 15h4"/>'
|
|
14
|
+
};
|
|
15
|
+
const META = {
|
|
16
|
+
input:{name:'Input',sub:'Where your workflow begins',group:'FLOW',color:'#709688',bg:'#edf6f1'},
|
|
17
|
+
agent:{name:'Agent',sub:'Reason, write, and collaborate',group:'INTELLIGENCE',color:'#9480c8',bg:'#f1ecfb'},
|
|
18
|
+
model:{name:'Chat Model',sub:'Compose role-based model messages',group:'INTELLIGENCE',color:'#718dc4',bg:'#edf2fb'},
|
|
19
|
+
condition:{name:'Condition',sub:'Route with if / else logic',group:'FLOW',color:'#c59d63',bg:'#fbf3e6'},
|
|
20
|
+
tool:{name:'Python tool',sub:'Bring your libraries along',group:'INTEGRATIONS',color:'#779bc1',bg:'#edf3fb'},
|
|
21
|
+
schema:{name:'Structured output',sub:'Validate with a typed schema',group:'INTELLIGENCE',color:'#b381a4',bg:'#f9edf5'},
|
|
22
|
+
approval:{name:'Human approval',sub:'Pause for a human decision',group:'FLOW',color:'#bf956e',bg:'#fbf2e9'},
|
|
23
|
+
output:{name:'Output',sub:'Return your final result',group:'FLOW',color:'#7aa39a',bg:'#edf6f4'}
|
|
24
|
+
};
|
|
25
|
+
const icon = kind => `<span class="node-icon" style="--node-color:${META[kind]?.color};--node-bg:${META[kind]?.bg}"><svg viewBox="0 0 22 22">${icons[kind]||icons.agent}</svg></span>`;
|
|
26
|
+
const S = {workflow:null,selected:null,library:'nodes',tab:'input',bootstrap:null,view:{x:35,y:55,z:.8},undo:[],redo:[],run:null,nodeStatus:{},connection:null,inputText:'{}',poll:null,dirty:false};
|
|
27
|
+
let toastTimer;
|
|
28
|
+
function toast(message){$('#toast').textContent=message;$('#toast').hidden=false;clearTimeout(toastTimer);toastTimer=setTimeout(()=>$('#toast').hidden=true,4500);}
|
|
29
|
+
async function api(path, body){const response=await fetch(path,{method:body===undefined?'GET':'POST',headers:body===undefined?{}:{'Content-Type':'application/json','X-Studio-Token':S.bootstrap.csrf},body:body===undefined?undefined:JSON.stringify(body)});const data=await response.json();if(!response.ok)throw Error(data.error||'Request failed');return data;}
|
|
30
|
+
function mark(){S.undo.push(clone(S.workflow));if(S.undo.length>60)S.undo.shift();S.redo=[];}
|
|
31
|
+
function changed(){S.dirty=true;$('#save-status').textContent='Unsaved changes';try{localStorage.setItem('graphcore.studio.draft',JSON.stringify(S.workflow));}catch{}$('#undo-button').disabled=!S.undo.length;$('#redo-button').disabled=!S.redo.length;}
|
|
32
|
+
function load(workflow){if(S.poll){clearTimeout(S.poll);S.poll=null;}S.workflow=clone(workflow);for(const n of S.workflow.nodes){if(['agent','model'].includes(n.type)&&n.config&&n.config.provider&& !['demo','openrouter'].includes(n.config.provider)){const old=n.config.provider;n.config.provider='openrouter';if(!String(n.config.model||'').includes('/'))n.config.model=old==='openai'?'openai/'+(n.config.model||'gpt-4o-mini'):old==='anthropic'?'anthropic/'+(n.config.model||'claude-sonnet-4'):old==='google_genai'?'google/'+(n.config.model||'gemini-2.5-flash'):'openai/gpt-4o-mini';}}S.selected=workflow.nodes.find(n=>n.type==='agent')?.id||workflow.nodes[0]?.id;S.inputText=JSON.stringify(workflow.inputs||{},null,2);S.undo=[];S.redo=[];S.run=null;S.nodeStatus={};S.connection=null;S.tab='input';S.dirty=false;renderAll();$('#save-status').textContent='Local draft';requestAnimationFrame(fit);try{localStorage.setItem('graphcore.studio.draft',JSON.stringify(S.workflow));}catch{}}
|
|
33
|
+
function renderAll(){if(!S.workflow)return;$('#workflow-name').value=S.workflow.name;$('#breadcrumb').textContent=S.workflow.name;$('#workflow-description').textContent=S.workflow.description||'Build a team of agents. Connect the way they think.';renderLibrary();renderNodes();renderInspector();renderConsole();$('#undo-button').disabled=!S.undo.length;$('#redo-button').disabled=!S.redo.length;}
|
|
34
|
+
function renderLibrary(){const text=$('#node-search').value.toLowerCase();document.querySelectorAll('[data-library]').forEach(b=>b.classList.toggle('active',b.dataset.library===S.library));let html='';if(S.library==='templates'){html=S.bootstrap.templates.map((t,i)=>`<button class="template-card" data-template="${i}">${icon(i===1?'schema':i===2?'tool':'agent')}<strong>${esc(t.name)}</strong><p>${esc(t.description)}</p><small>${t.nodes.length} nodes ↗ Open template</small></button>`).join('');html+='<button class="template-card" id="blank-template"><strong>+ Start from scratch</strong><p>A blank canvas for your next idea.</p></button>';}else{for(const group of ['INTELLIGENCE','FLOW','INTEGRATIONS']){const kinds=Object.keys(META).filter(k=>META[k].group===group&&`${META[k].name} ${META[k].sub}`.toLowerCase().includes(text));if(!kinds.length)continue;html+=`<div class="library-group">${group}</div>`;for(const kind of kinds){const m=META[kind];html+=`<button class="palette-node" draggable="true" data-kind="${kind}" aria-label="Add ${m.name}">${icon(kind)}<span><strong>${m.name}</strong><small>${m.sub}</small></span><span class="grab">⠿</span></button>`;}}}$('#node-library').innerHTML=html;document.querySelectorAll('.palette-node').forEach(el=>{el.addEventListener('dragstart',e=>{e.dataTransfer.setData('application/graphcore-node',el.dataset.kind);e.dataTransfer.effectAllowed='copy';});el.onclick=()=>{const area=$('#canvas-area');addNode(el.dataset.kind,(area.clientWidth/2-S.view.x)/S.view.z-113,(area.clientHeight/2-S.view.y)/S.view.z-54);};});document.querySelectorAll('[data-template]').forEach(b=>b.onclick=()=>confirmReplace(()=>load(S.bootstrap.templates[+b.dataset.template])));if($('#blank-template'))$('#blank-template').onclick=()=>confirmReplace(()=>load({format:'graphcore.studio.v1',id:'workflow_'+Date.now(),name:'Untitled workflow',description:'A blank canvas for your next idea.',inputs:{input:'Hello, world'},nodes:[],edges:[]}));}
|
|
35
|
+
function confirmReplace(action){if(!S.dirty){action();return;}showModal('Replace this draft?',`<p class="modal-copy">Save or export your current workflow first if you want to keep it.</p><div class="modal-actions"><button class="button" id="keep-draft">Keep editing</button><button class="button primary" id="replace-draft">Replace draft</button></div>`);$('#keep-draft').onclick=closeModal;$('#replace-draft').onclick=()=>{closeModal();action();};}
|
|
36
|
+
function nodeSubtitle(n){const c=n.config||{};if(n.type==='agent'||n.type==='model')return c.provider==='demo'?'Offline fixture':`${c.provider||'demo'} · ${c.model||'choose model'}`;if(n.type==='condition')return `${c.field||'field'} equals ${JSON.stringify(c.expected)}`;if(n.type==='tool')return c.tool||'word_count';if(n.type==='schema')return c.engine==='pydantic'?'Pydantic validation':'Portable schema validation';return META[n.type]?.sub||n.type;}
|
|
37
|
+
function renderNodes(){const w=S.workflow;$('#nodes').innerHTML=w.nodes.map(n=>{const m=META[n.type]||META.agent;const selected=S.selected===n.id;const c=n.config||{};return `<div class="canvas-node ${esc(n.type)} ${selected?'selected':''} ${S.nodeStatus[n.id]||''}" id="node-${esc(n.id)}" data-node="${esc(n.id)}" style="left:${Number(n.x)||0}px;top:${Number(n.y)||0}px" tabindex="0" role="group" aria-label="${esc(n.label)} node"><div class="node-head">${icon(n.type)}<div><strong>${esc(n.label||m.name)}</strong><small>${esc(nodeSubtitle(n))}</small></div><span class="node-menu">⋮</span></div><div class="node-foot"><span>${n.type==='condition'?'C++ routing':n.type==='agent'?'Agent':esc(m.name)}</span><code>${esc(n.type==='input'?'workflow inputs':n.type==='output'?'output':c.output_key||n.type==='condition'&&'true / false'||n.id)}</code></div>${n.type!=='input'?`<button class="port input" data-target="${esc(n.id)}" aria-label="Connect to ${esc(n.label)}"></button>`:''}${n.type==='condition'?`<button class="port output true" data-source="${esc(n.id)}" data-port="true" aria-label="True branch from ${esc(n.label)}"></button><button class="port output false" data-source="${esc(n.id)}" data-port="false" aria-label="False branch from ${esc(n.label)}"></button>`:n.type!=='output'?`<button class="port output" data-source="${esc(n.id)}" data-port="next" aria-label="Connect from ${esc(n.label)}"></button>`:''}</div>`;}).join('');$('#empty-canvas').hidden=w.nodes.length>0;$('#graph-stats').textContent=`${w.nodes.length} nodes · ${w.edges.length} connections`;const real=w.nodes.find(n=>['agent','model'].includes(n.type)&&(n.config?.provider||'demo')!=='demo');$('#mode-label').textContent=real?`${real.config.provider} model · Python integration`:'Offline demo · no model calls';document.querySelectorAll('.canvas-node').forEach(el=>{el.addEventListener('pointerdown',startNodeDrag);el.addEventListener('keydown',e=>{if(e.key==='Enter'){selectNode(el.dataset.node);}});});document.querySelectorAll('.port.output').forEach(el=>{el.addEventListener('keydown',e=>{if(e.key==='Enter'){e.preventDefault();S.connection={source:el.dataset.source,port:el.dataset.port};toast('Select an input port to connect.');}});el.addEventListener('pointerdown',e=>{e.stopPropagation();S.connection={source:el.dataset.source,port:el.dataset.port};$('#canvas-hint').textContent='Click an input dot to connect · Esc to cancel';el.classList.add('connecting');});});document.querySelectorAll('.port.input').forEach(el=>{el.addEventListener('keydown',e=>{if(e.key==='Enter'&&S.connection){e.preventDefault();connect(S.connection.source,el.dataset.target,S.connection.port);}});el.addEventListener('pointerdown',e=>{e.stopPropagation();if(S.connection)connect(S.connection.source,el.dataset.target,S.connection.port);});});applyView();drawEdges();}
|
|
38
|
+
function dimensions(node){const el=document.getElementById('node-'+node.id);return {w:226,h:el?.offsetHeight||108};}
|
|
39
|
+
function anchor(node,side,port){const d=dimensions(node);return {x:(Number(node.x)||0)+(side==='out'?d.w:0),y:(Number(node.y)||0)+d.h*(port==='true'?.35:port==='false'?.72:.5)};}
|
|
40
|
+
function curve(a,b){const distance=Math.max(70,Math.abs(b.x-a.x)*.5);return `M ${a.x} ${a.y} C ${a.x+distance} ${a.y}, ${b.x-distance} ${b.y}, ${b.x} ${b.y}`;}
|
|
41
|
+
function drawEdges(){if(!S.workflow)return;const nodes=new Map(S.workflow.nodes.map(n=>[n.id,n]));let html='<defs><marker id="arrow" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="5" markerHeight="5" orient="auto-start-reverse"><path d="M 0 0 L 10 5 L 0 10 z" style="fill:#c8bfdc;stroke:none"/></marker></defs>';for(const e of S.workflow.edges){const a=nodes.get(e.source),b=nodes.get(e.target);if(!a||!b)continue;const start=anchor(a,'out',e.port),end=anchor(b,'in');html+=`<path d="${curve(start,end)}" marker-end="url(#arrow)" class="${S.nodeStatus[a.id]==='completed'&&S.nodeStatus[b.id]?'ran':''}"/><text x="${start.x+14}" y="${start.y-10}">${e.port&&e.port!=='next'?esc(e.port):''}</text>`;}if(S.connection?.point){const n=nodes.get(S.connection.source);if(n)html+=`<path d="${curve(anchor(n,'out',S.connection.port),S.connection.point)}" style="stroke:#a28bd2;stroke-dasharray:5 4"/>`;}$('#edges').innerHTML=html;}
|
|
42
|
+
function applyView(){$('#world').style.transform=`translate(${S.view.x}px,${S.view.y}px) scale(${S.view.z})`;$('#zoom-label').textContent=Math.round(S.view.z*100)+'%';}
|
|
43
|
+
function fit(){const area=$('#canvas-area'),nodes=S.workflow.nodes;if(!nodes.length){S.view={x:50,y:70,z:1};applyView();return;}const minX=Math.min(...nodes.map(n=>n.x)),minY=Math.min(...nodes.map(n=>n.y));const width=Math.max(...nodes.map(n=>n.x+226))-minX,height=Math.max(...nodes.map(n=>n.y+115))-minY;const z=Math.max(.3,Math.min(1.1,(area.clientWidth-100)/width,(area.clientHeight-115)/height));S.view={z,x:(area.clientWidth-width*z)/2-minX*z,y:(area.clientHeight-height*z)/2-minY*z+6};applyView();}
|
|
44
|
+
function zoom(factor,x,y){const area=$('#canvas-area');x??=area.clientWidth/2;y??=area.clientHeight/2;const z=Math.max(.25,Math.min(1.8,S.view.z*factor)),r=z/S.view.z;S.view.x=x-(x-S.view.x)*r;S.view.y=y-(y-S.view.y)*r;S.view.z=z;applyView();}
|
|
45
|
+
function worldPoint(e){const rect=$('#canvas-area').getBoundingClientRect();return {x:(e.clientX-rect.left-S.view.x)/S.view.z,y:(e.clientY-rect.top-S.view.y)/S.view.z};}
|
|
46
|
+
let drag=null;
|
|
47
|
+
function startNodeDrag(e){if(e.target.closest('.port')||e.button!==0)return;e.stopPropagation();const id=e.currentTarget.dataset.node;selectNode(id,false);const n=S.workflow.nodes.find(n=>n.id===id);drag={type:'node',id,x:e.clientX,y:e.clientY,originalX:n.x,originalY:n.y,moved:false};}
|
|
48
|
+
document.addEventListener('pointermove',e=>{if(S.connection){S.connection.point=worldPoint(e);drawEdges();}if(!drag)return;if(drag.type==='pan'){S.view.x=drag.vx+e.clientX-drag.x;S.view.y=drag.vy+e.clientY-drag.y;applyView();return;}const dx=(e.clientX-drag.x)/S.view.z,dy=(e.clientY-drag.y)/S.view.z;if(!drag.moved&&Math.abs(dx)+Math.abs(dy)>3){mark();drag.moved=true;}if(!drag.moved)return;const node=S.workflow.nodes.find(n=>n.id===drag.id);node.x=Math.max(-500,Math.round((drag.originalX+dx)/5)*5);node.y=Math.max(-500,Math.round((drag.originalY+dy)/5)*5);const el=document.getElementById('node-'+node.id);el.style.left=node.x+'px';el.style.top=node.y+'px';drawEdges();});
|
|
49
|
+
document.addEventListener('pointerup',e=>{if(drag?.moved)changed();drag=null;if(S.connection){const el=document.elementFromPoint(e.clientX,e.clientY)?.closest('.port.input');if(el)connect(S.connection.source,el.dataset.target,S.connection.port);}});
|
|
50
|
+
function selectNode(id,repaint=true){S.selected=id;if(innerWidth<=850)document.querySelector('.inspector').classList.add('mobile-open');if(repaint)renderNodes();else document.querySelectorAll('.canvas-node').forEach(el=>el.classList.toggle('selected',el.dataset.node===id));renderInspector();}
|
|
51
|
+
function connect(source,target,port='next'){if(source===target){toast('Connect to another node. Use a condition to build a loop.');return;}mark();S.workflow.edges=S.workflow.edges.filter(e=>!(e.source===source&&(e.port||'next')===port));S.workflow.edges.push({source,target,port});S.connection=null;$('#canvas-hint').textContent='Drag to arrange · connect the dots to build a flow';changed();renderNodes();renderInspector();}
|
|
52
|
+
function defaults(kind){return {input:{},agent:{provider:'demo',model:'openai/gpt-4o-mini',system:'You are a helpful assistant.',prompt:'{{input}}',demo_response:'Demo response: {{input}}',output_key:'response'},model:{provider:'demo',model:'openai/gpt-4o-mini',messages:[{role:'system',content:'You are a helpful assistant.'},{role:'user',content:'{{input}}'}],temperature:.2,max_tokens:1024,timeout:60,demo_response:'Demo response: {{input}}',output_key:'response'},condition:{field:'approved',expected:true},tool:{tool:'word_count',input_field:'input',output_key:'tool_result'},schema:{input_field:'record',output_key:'validated',engine:'builtin',fields:[{name:'summary',type:'string',required:true}]},approval:{prompt:'Approve this result?',output_key:'approved'},output:{template:'{{input}}'}}[kind];}
|
|
53
|
+
function addNode(kind,x,y){if(kind==='input'&&S.workflow.nodes.some(n=>n.type==='input')){toast('A workflow has one Input node.');return;}mark();const id=kind+'_'+Math.random().toString(36).slice(2,7);const config=defaults(kind);if(config.output_key)config.output_key=id;S.workflow.nodes.push({id,type:kind,label:META[kind].name,x:Math.round(x),y:Math.round(y),config});S.selected=id;if(innerWidth<=850)document.querySelector('.inspector').classList.add('mobile-open');changed();renderNodes();renderInspector();}
|
|
54
|
+
function field(label,key,value,options={}){let input;if(options.select)input=`<select data-config="${key}">${options.select.map(([v,t])=>`<option value="${esc(v)}" ${v===value?'selected':''}>${esc(t)}</option>`).join('')}</select>`;else if(options.area)input=`<textarea data-config="${key}" ${options.json?'data-json="true" class="code"':''} rows="${options.rows||4}">${esc(value)}</textarea>`;else input=`<input data-config="${key}" value="${esc(value)}" ${options.json?'data-json="true" class="code"':''}>`;return `<label class="field"><span>${esc(label)}</span>${input}${options.help?`<small>${esc(options.help)}</small>`:''}</label>`;}
|
|
55
|
+
function availableVariables(){const vars=new Map();const add=(key,label)=>{if(/^[A-Za-z_][A-Za-z0-9_.]*$/.test(key))vars.set(key,label);};for(const [key,value] of Object.entries(S.workflow.inputs||{})){add(key,`Input · ${key}`);if(value&&typeof value==='object'&&!Array.isArray(value))for(const child of Object.keys(value))add(`${key}.${child}`,`Input · ${key}.${child}`);}for(const other of S.workflow.nodes){if(['input','condition','output'].includes(other.type))continue;const key=other.config?.output_key||other.id;add(key,`Output · ${other.label||key}`);if(other.type==='schema')for(const f of other.config?.fields||[])add(`${key}.${f.name}`,`Output · ${key}.${f.name}`);}return [...vars].sort(([a],[b])=>a.localeCompare(b));}
|
|
56
|
+
function messageEditor(messages){const variables=availableVariables();return `<div class="message-list">${messages.map((message,i)=>`<section class="message-card"><header><select data-message-role="${i}" aria-label="Message ${i+1} role">${['system','user','assistant'].map(role=>`<option value="${role}" ${role===message.role?'selected':''}>${role}</option>`).join('')}</select><button class="message-remove" data-message-remove="${i}" aria-label="Remove message ${i+1}" ${messages.length<=1?'disabled':''}>×</button></header><textarea class="message-content" data-message-content="${i}" rows="4" aria-label="${message.role} message">${esc(message.content)}</textarea><div class="message-tools"><select data-message-variable="${i}" aria-label="Insert workflow variable into message ${i+1}"><option value="">+ Insert variable…</option>${variables.map(([key,label])=>`<option value="${esc(key)}">{{${esc(key)}}} · ${esc(label)}</option>`).join('')}</select><small>Inserted at the cursor</small></div></section>`).join('')}</div><button class="button small" id="add-message">+ Add message</button>`;}
|
|
57
|
+
function modelEditor(node){
|
|
58
|
+
const c=node.config||(node.config={});
|
|
59
|
+
if(!Array.isArray(c.messages)||!c.messages.length)c.messages=[{role:'system',content:c.system||'You are a helpful assistant.'},{role:'user',content:c.prompt||'{{input}}'}];
|
|
60
|
+
let provider=c.provider||'demo';
|
|
61
|
+
if(provider!=='demo'&&provider!=='openrouter'){c.provider='openrouter';if(!String(c.model||'').includes('/'))c.model='openai/gpt-4o-mini';provider='openrouter';}
|
|
62
|
+
const router=S.bootstrap.providers.find(p=>p.id==='openrouter');
|
|
63
|
+
const providers=[['demo','Offline fixture · no model call'],['openrouter',`OpenRouter${router?.installed?'':' · install openrouter SDK'}`]];
|
|
64
|
+
let html='<div class="inspector-section">MODEL BACKEND</div>';
|
|
65
|
+
html+=field('Provider','provider',provider,{select:providers});
|
|
66
|
+
if(provider==='demo')html+=field('Demo response fixture','demo_response',c.demo_response||'Demo response: {{input}}',{area:true,rows:2,help:'Used for offline testing. It does not call a model.'});
|
|
67
|
+
else {
|
|
68
|
+
html+=field('OpenRouter model ID','model',c.model||'openai/gpt-4o-mini',{help:'Use the model slug shown in the OpenRouter model catalog, for example openai/gpt-4o-mini.'});
|
|
69
|
+
html+='<div class="inspector-note">Add <strong>OPENROUTER_API_KEY</strong> in <strong>Model integrations</strong>. OpenRouter model calls run through GraphCore’s Python bridge.</div>';
|
|
70
|
+
if(router&&!router.installed)html+='<div class="inspector-note">Install the SDK in the Python environment running Studio: <code>python -m pip install openrouter</code>, then restart Studio.</div>';
|
|
71
|
+
}
|
|
72
|
+
html+='<div class="inspector-section">MESSAGES</div><div class="inspector-note">Compose system, user, and assistant messages. Use the variable menu to insert values from workflow inputs and upstream node outputs.</div>';
|
|
73
|
+
html+=messageEditor(c.messages);
|
|
74
|
+
html+='<div class="inspector-section">MODEL SETTINGS</div>';
|
|
75
|
+
html+=field('Temperature','temperature',c.temperature??.2);
|
|
76
|
+
html+=field('Maximum output tokens','max_tokens',c.max_tokens??1024);
|
|
77
|
+
html+=field('Request timeout (seconds)','timeout',c.timeout??60);
|
|
78
|
+
html+=`<label class="check-field"><input type="checkbox" id="json-output" ${c.json_output?'checked':''}> Request JSON output</label>`;
|
|
79
|
+
return html;
|
|
80
|
+
}
|
|
81
|
+
function renderInspector(){const node=S.workflow.nodes.find(n=>n.id===S.selected);if(!node){$('#inspector-content').innerHTML='<div class="inspector-empty"><span class="empty-symbol" style="margin:auto auto 15px">◇</span><strong>Make it your own.</strong>Select a node to configure its behavior.</div>';return;}const kind=node.type,c=node.config||{},m=META[kind];$('#inspector-count').textContent=String(S.workflow.nodes.indexOf(node)+1).padStart(2,'0');let html=`<div class="inspector-hero">${icon(kind)}<div><strong>${esc(m.name)}</strong><p>${esc(m.sub)}</p></div></div><label class="field"><span>Node name</span><input id="node-label" value="${esc(node.label)}"></label>`;
|
|
82
|
+
if(kind==='input'){html+='<div class="inspector-note">Set the initial state in the <strong>Input</strong> panel below the canvas. Other nodes can reference fields with <strong>{{field_name}}</strong>.</div>';}
|
|
83
|
+
if(kind==='agent'||kind==='model'){html+=modelEditor(node);}
|
|
84
|
+
if(kind==='condition'){html+='<div class="inspector-section">IF THIS IS TRUE</div>';html+=field('State field','field',c.field||'approved',{help:'A top-level state field. Compared directly by C++.'});html+=field('Equals (JSON value)','expected',JSON.stringify(c.expected??true),{json:true,help:'Examples: true, 5, "research". Types must match.'});html+='<div class="inspector-note">Connect both the <strong>true</strong> and <strong>false</strong> outputs. A missing state field fails the run.</div>';}
|
|
85
|
+
if(kind==='tool'){html+='<div class="inspector-section">PYTHON INTEGRATION</div>';html+=field('Registered function','tool',c.tool||'word_count',{select:S.bootstrap.tools.map(t=>[t.name,t.name])});html+=field('Input state field','input_field',c.input_field||'input');html+='<div class="inspector-note">Connect your own Python libraries through a registered tool. Start Studio with <strong>--plugin path/to/tools.py</strong>. No code is executed from the canvas.</div>';}
|
|
86
|
+
if(kind==='schema'){html+='<div class="inspector-section">VALIDATION</div>';html+=field('Input state field','input_field',c.input_field||'record');html+=field('Validation engine','engine',c.engine||'builtin',{select:[['builtin','Built-in strict validation'],['pydantic',S.bootstrap.pydantic?'Pydantic · installed':'Pydantic · installation needed']],help:S.bootstrap.pydantic?'Pydantic is available in the server environment.':'Pydantic is optional. Install it in the server environment to enable it.'});html+='<div class="inspector-section">OUTPUT SCHEMA</div><div class="schema-legend"><span>Field name / type</span><span>Required</span></div>';html+=(c.fields||[]).map((f,i)=>`<div class="schema-row" data-index="${i}"><input aria-label="Schema field name" data-schema="name" value="${esc(f.name)}"><select aria-label="Schema field type" data-schema="type">${['string','integer','number','boolean','object','array'].map(t=>`<option ${t===f.type?'selected':''}>${t}</option>`).join('')}</select><input class="required" type="checkbox" aria-label="Required field" data-schema="required" ${f.required!==false?'checked':''}><button data-remove-field="${i}" aria-label="Remove field">×</button></div>`).join('');html+='<button class="button small" id="add-schema-field">+ Add field</button>';}
|
|
87
|
+
if(kind==='approval'){html+='<div class="inspector-section">HUMAN IN THE LOOP</div>';html+=field('Question / review text','prompt',c.prompt||'',{area:true,rows:6,help:'The run saves a checkpoint and pauses until you respond.'});html+='<div class="inspector-note">The response is saved to the output field. Add a <strong>Condition</strong> after this node to branch on approval or rejection.</div>';}
|
|
88
|
+
if(kind==='output'){html+=field('Result template','template',c.template||'{{input}}',{area:true,rows:7,help:'Combine state fields into your final result.'});}
|
|
89
|
+
if(!['input','condition','output'].includes(kind)){html+='<div class="inspector-section">STATE</div>';html+=field('Save output to','output_key',c.output_key||node.id,{help:'Other nodes can read this field.'});}
|
|
90
|
+
html+='<div class="inspector-section">CONNECTIONS</div>';const ports=kind==='condition'?['true','false']:kind==='output'?[]:['next'];for(const port of ports){const edge=S.workflow.edges.find(e=>e.source===node.id&&(e.port||'next')===port);html+=`<div class="connection-row"><span>${port} →</span><select data-route="${port}" aria-label="${port} target"><option value="">Not connected</option>${S.workflow.nodes.filter(n=>n.id!==node.id&&n.type!=='input').map(n=>`<option value="${esc(n.id)}" ${edge?.target===n.id?'selected':''}>${esc(n.label)}</option>`).join('')}</select></div>`;}if(!ports.length)html+='<div class="field-help">This node completes the workflow.</div>';html+='<button class="button delete-node" id="delete-node">Delete node</button>';$('#inspector-content').innerHTML=html;
|
|
91
|
+
$('#node-label').onchange=e=>{mark();node.label=e.target.value||META[kind].name;changed();renderNodes();};document.querySelectorAll('[data-config]').forEach(el=>el.onchange=()=>{let value=el.value;try{if(el.dataset.json)value=JSON.parse(value);if(['temperature','max_tokens','timeout'].includes(el.dataset.config))value=Number(value);mark();node.config[el.dataset.config]=value;if(el.dataset.config==='provider'&&value==='openrouter'&&!String(node.config.model||'').includes('/'))node.config.model='openai/gpt-4o-mini';changed();renderNodes();if(['provider','engine'].includes(el.dataset.config))renderInspector();}catch(error){toast('Invalid JSON value: '+error.message);el.setAttribute('aria-invalid','true');}});document.querySelectorAll('[data-message-role]').forEach(el=>el.onchange=()=>{mark();node.config.messages[+el.dataset.messageRole].role=el.value;changed();});document.querySelectorAll('[data-message-content]').forEach(el=>{el.addEventListener('input',()=>{if(!el.dataset.historyMarked){mark();el.dataset.historyMarked='1';}node.config.messages[+el.dataset.messageContent].content=el.value;changed();});el.addEventListener('blur',()=>{delete el.dataset.historyMarked;});});document.querySelectorAll('[data-message-variable]').forEach(el=>el.onchange=()=>{const name=el.value;if(!name)return;const area=[...document.querySelectorAll('[data-message-content]')].find(a=>a.dataset.messageContent===el.dataset.messageVariable);const token='{{'+name+'}}';const start=area.selectionStart??area.value.length,end=area.selectionEnd??start;area.setRangeText(token,start,end,'end');node.config.messages[+el.dataset.messageVariable].content=area.value;changed();area.focus();el.value='';});document.querySelectorAll('[data-message-remove]').forEach(el=>el.onclick=()=>{mark();node.config.messages.splice(+el.dataset.messageRemove,1);changed();renderInspector();});if($('#add-message'))$('#add-message').onclick=()=>{if(node.config.messages.length>=20){toast('A model can have up to 20 messages.');return;}mark();node.config.messages.push({role:'user',content:''});changed();renderInspector();document.querySelectorAll('[data-message-content]')[node.config.messages.length-1]?.focus();};if($('#json-output'))$('#json-output').onchange=e=>{mark();node.config.json_output=e.target.checked;changed();};document.querySelectorAll('[data-schema]').forEach(el=>el.onchange=()=>{mark();node.config.fields[+el.closest('[data-index]').dataset.index][el.dataset.schema]=el.type==='checkbox'?el.checked:el.value;changed();});document.querySelectorAll('[data-remove-field]').forEach(el=>el.onclick=()=>{mark();node.config.fields.splice(+el.dataset.removeField,1);changed();renderInspector();});if($('#add-schema-field'))$('#add-schema-field').onclick=()=>{mark();node.config.fields.push({name:'field_'+(node.config.fields.length+1),type:'string',required:true});changed();renderInspector();};document.querySelectorAll('[data-route]').forEach(el=>el.onchange=()=>{if(el.value)connect(node.id,el.value,el.dataset.route);else{mark();S.workflow.edges=S.workflow.edges.filter(e=>!(e.source===node.id&&(e.port||'next')===el.dataset.route));changed();renderNodes();}});$('#delete-node').onclick=deleteSelected;}
|
|
92
|
+
function deleteSelected(){if(!S.selected)return;mark();S.workflow.nodes=S.workflow.nodes.filter(n=>n.id!==S.selected);S.workflow.edges=S.workflow.edges.filter(e=>e.source!==S.selected&&e.target!==S.selected);S.selected=null;changed();renderNodes();renderInspector();}
|
|
93
|
+
function renderConsole(){document.querySelectorAll('[data-console]').forEach(b=>b.classList.toggle('active',b.dataset.console===S.tab));const r=S.run;const active=r&&['queued','running','cancelling'].includes(r.status);$('#stop-button').hidden=!active;$('#stop-button').disabled=r?.status==='cancelling';$('#run-button').disabled=!!active||!S.bootstrap.native;$('#run-status').textContent=r?`${r.status.charAt(0).toUpperCase()+r.status.slice(1)}${r.duration_ms?' · '+(r.duration_ms/1000).toFixed(2)+'s':''}`:'Ready to run';$('#event-count').textContent=r?.events.length||0;let html='';if(S.tab==='input'){html=`<div class="input-pane"><div><textarea id="run-input" spellcheck="false" aria-label="Workflow input JSON">${esc(S.inputText)}</textarea></div><div class="input-hint"><strong>Give your workflow a starting point.</strong>Pass a JSON object. Reference its fields in prompts with <code>{{input}}</code>.<br>Then click <strong style="display:inline;font-size:10px">Run workflow</strong>.</div></div>`;}else if(!r){html='<div class="console-empty"><span class="empty-symbol">▷</span><div><strong>A little input. A lot of possibility.</strong>Run your workflow to see results here.</div></div>';}else if(S.tab==='state'){html=`<pre class="state-json">${esc(JSON.stringify(r.state,null,2))}</pre>${!Object.keys(r.state).length?'<p class="field-help">Final state appears when the run completes or pauses. Intermediate updates are in the execution trace.</p>':''}`;}else if(S.tab==='trace'){html=r.events.map(e=>`<div class="trace-row"><time>${esc(e.time.slice(11,23))}</time><strong>${esc(e.type)}</strong><code>${esc(e.node||'')}</code>${e.updates?`<span title="${esc(JSON.stringify(e.updates))}">↳ ${esc(Object.keys(e.updates).join(', '))}</span>`:''}</div>`).join('')||'<p class="field-help">Waiting for native runtime events…</p>';}else if(r.status==='suspended'){html=`<div class="approval-panel"><span class="run-badge suspended">Awaiting human input</span><div class="question">${esc(r.prompt)}</div><div class="approval-form"><input id="approval-response" value="true" aria-label="Approval response as JSON"><button class="button primary" id="resume-button">Continue run →</button></div><p class="field-help">Enter a JSON response: true, false, or a quoted message.</p></div>`;}else if(r.error){html=`<div class="error-box">${esc(r.error)}</div><button class="button small" id="recover-button" style="margin-top:8px">Recover from checkpoint</button>`;}else if(r.status==='completed'){html=`<pre class="output-text">${esc(typeof r.state.output==='string'?r.state.output:JSON.stringify(r.state.output??r.state,null,2))}</pre>`;}else{html='<div class="console-empty"><span class="empty-symbol">◌</span><div><strong>Your workflow is running.</strong>Follow each step in the execution trace.</div></div>';}$('#console-content').innerHTML=html;if($('#run-input'))$('#run-input').oninput=e=>{S.inputText=e.target.value;try{const obj=JSON.parse(S.inputText);if(obj&&typeof obj==='object'&&!Array.isArray(obj)){S.workflow.inputs=obj;changed();}}catch{}};if($('#resume-button'))$('#resume-button').onclick=async()=>{try{const response=JSON.parse($('#approval-response').value);await api(`/api/runs/${r.id}/resume`,{response});S.run.status='queued';renderConsole();pollRun(r.id);}catch(error){toast(error.message);}};if($('#recover-button'))$('#recover-button').onclick=async()=>{try{await api(`/api/runs/${r.id}/recover`,{});S.run.status='queued';S.run.error='';renderConsole();pollRun(r.id);}catch(error){toast(error.message);}};}
|
|
94
|
+
async function pollRun(id){clearTimeout(S.poll);try{const run=await api('/api/runs/'+id);if(S.run&&S.run.id!==id)return;S.run=run;S.nodeStatus={};for(const e of run.events){if(e.type==='node.started')S.nodeStatus[e.node]='running';if(e.type==='node.completed')S.nodeStatus[e.node]='completed';if(e.type==='node.failed')S.nodeStatus[e.node]='failed';if(e.type==='run.interrupted')S.nodeStatus[e.node]='suspended';}if(['failed','cancelled'].includes(run.status)){for(const [key,value]of Object.entries(S.nodeStatus))if(value==='running')S.nodeStatus[key]='failed';}if(run.status==='suspended'&&S.tab==='trace')S.tab='output';renderNodes();if(S.tab!=='input')renderConsole();else{const active=['queued','running','cancelling'].includes(run.status);$('#run-button').disabled=active;$('#run-status').textContent=run.status;$('#stop-button').hidden=!active;}if(['queued','running','cancelling'].includes(run.status))S.poll=setTimeout(()=>pollRun(id),250);else updateHistoryCount();}catch(error){toast(error.message);}}
|
|
95
|
+
async function startRun(){try{const inputs=JSON.parse(S.inputText);const data=await api('/api/runs',{workflow:clone(S.workflow),inputs,max_steps:100});S.run={id:data.id,status:'queued',events:[],state:{},duration_ms:0};S.nodeStatus={};S.tab='output';renderConsole();pollRun(data.id);}catch(error){showModal('Cannot run this workflow',`<div class="error-box">${esc(error.message)}</div>`);}}
|
|
96
|
+
function showModal(title,html){$('#modal-title').textContent=title;$('#modal-content').innerHTML=html;if(!$('#modal').open)$('#modal').showModal();}function closeModal(){$('#modal').close();}
|
|
97
|
+
function environmentRow(name,status={},custom=false){return `<div class="environment-row ${custom?'custom':''}" data-env-row><label><span>${custom?'<input data-env-name value="'+esc(name)+'" placeholder="VARIABLE_NAME" aria-label="Environment variable name">':'<strong>'+esc(name)+'</strong>'}<small>${status.stored?'Saved in local .env':status.configured?'Available from Studio environment':'Not set'}</small></span><input type="password" data-env-value="${esc(name)}" autocomplete="new-password" placeholder="${status.stored?'Saved · leave empty to keep':status.configured?'Already available · enter to override':'Enter value'}" aria-label="Value for ${esc(name)}"></label><label class="env-remove"><input type="checkbox" data-env-remove="${esc(name)}"> Remove</label></div>`;}
|
|
98
|
+
async function openIntegrations(){try{const data=await api('/api/settings/environment');const status=new Map(data.variables.map(v=>[v.name,v]));let html='<p class="modal-copy">Configure provider credentials and other <code>.env</code> variables here. Studio saves them in its local data folder and loads them before model initialization. Secret values are write-only in this UI.</p><div class="env-security">Local file: <code>studio/data/.env</code> · file permissions are restricted where the operating system supports it.</div><div class="inspector-section">PROVIDER VARIABLES</div>';const names=[...new Set(S.bootstrap.providers.flatMap(p=>p.env||[]))];if(!names.length)html+='<p class="field-help">Provider integrations do not declare API key variables.</p>';for(const name of names)html+=environmentRow(name,status.get(name));html+='<div class="inspector-section">CUSTOM .ENV VARIABLES</div><div id="custom-env-rows">';for(const v of data.variables.filter(v=>!names.includes(v.name)))html+=environmentRow(v.name,v,true);html+='</div><button class="button small" id="add-env-variable">+ Add variable</button><div class="modal-actions"><button class="button" id="cancel-env">Cancel</button><button class="button primary" id="save-env">Save integrations</button></div><p class="field-help">Changes take effect immediately for new model calls. Shell-defined variables are available too. Avoid putting secrets in workflow JSON.</p>';showModal('Model integrations & environment',html);$('#cancel-env').onclick=closeModal;$('#add-env-variable').onclick=()=>{$('#custom-env-rows').insertAdjacentHTML('beforeend',environmentRow('',{},true));document.querySelector('#custom-env-rows [data-env-row]:last-child [data-env-name]')?.focus();};$('#save-env').onclick=saveEnvironment;}catch(error){toast(error.message);}}
|
|
99
|
+
async function saveEnvironment(){const values={},remove=[];for(const row of document.querySelectorAll('[data-env-row]')){const nameField=row.querySelector('[data-env-name]'),secret=row.querySelector('[data-env-value]'),removeBox=row.querySelector('[data-env-remove]');const name=(nameField?nameField.value:secret.dataset.envValue).trim();if(!name)continue;if(removeBox?.checked){remove.push(name);continue;}if(secret.value)values[name]=secret.value;}try{const result=await api('/api/settings/environment',{values,remove});closeModal();toast('Environment saved locally. New model calls will use these values.');}catch(error){toast(error.message);}}
|
|
100
|
+
async function save(){try{await api('/api/workflows',{workflow:S.workflow});S.dirty=false;$('#save-status').textContent='Saved locally';toast('Workflow saved on this machine.');}catch(error){toast(error.message);}}
|
|
101
|
+
function showJSON(){showModal('Your workflow, as a document',`<p class="modal-copy">Export a portable JSON workflow, or edit and apply it here.</p><textarea id="workflow-json" class="modal-code" spellcheck="false">${esc(JSON.stringify(S.workflow,null,2))}</textarea><div class="modal-actions"><button class="button" id="import-json">Import file</button><button class="button" id="export-json">Export JSON</button><button class="button primary" id="apply-json">Apply changes</button></div>`);$('#import-json').onclick=()=>$('#import-file').click();$('#export-json').onclick=()=>{const blob=new Blob([$('#workflow-json').value],{type:'application/json'}),url=URL.createObjectURL(blob),a=document.createElement('a');a.href=url;a.download=(S.workflow.id||'workflow')+'.json';a.click();setTimeout(()=>URL.revokeObjectURL(url),1000);};$('#apply-json').onclick=()=>{try{const value=JSON.parse($('#workflow-json').value);checkDocument(value);mark();S.workflow=value;S.inputText=JSON.stringify(value.inputs||{},null,2);S.selected=value.nodes[0]?.id;changed();closeModal();renderAll();fit();}catch(error){toast(error.message);}};}
|
|
102
|
+
function checkDocument(w){if(!w||w.format!=='graphcore.studio.v1'||!Array.isArray(w.nodes)||!Array.isArray(w.edges)||w.nodes.length>100||w.edges.length>300)throw Error('Unsupported workflow document');const ids=new Set();for(const n of w.nodes){if(!n||!Object.prototype.hasOwnProperty.call(META,n.type)||typeof n.id!=='string'||!/^[A-Za-z_][A-Za-z0-9_]{0,63}$/.test(n.id)||ids.has(n.id)||!Number.isFinite(n.x)||!Number.isFinite(n.y)||!n.config||typeof n.config!=='object'||Array.isArray(n.config))throw Error('Invalid node definition');if(n.type==='schema'&&(!Array.isArray(n.config.fields)||n.config.fields.some(f=>!f||typeof f.name!=='string'||typeof f.type!=='string')))throw Error('Invalid schema fields');ids.add(n.id);}for(const e of w.edges)if(!e||!ids.has(e.source)||!ids.has(e.target))throw Error('Connection references a missing node');if(typeof w.name!=='string'||typeof w.id!=='string')throw Error('Workflow name and ID are required');}
|
|
103
|
+
async function openSaved(){try{const items=await api('/api/workflows');showModal('Open a workflow',items.length?items.map((w,i)=>`<button class="saved-row" data-saved="${i}"><strong>${esc(w.name)}</strong><small>${w.nodes?.length||0} nodes →</small></button>`).join(''):'<p class="modal-copy">No saved workflows yet. Save your draft, or start from a template.</p>');document.querySelectorAll('[data-saved]').forEach(b=>b.onclick=()=>{const w=items[+b.dataset.saved];try{checkDocument(w);closeModal();confirmReplace(()=>load(w));}catch(error){toast(error.message);}});}catch(error){toast(error.message);}}
|
|
104
|
+
async function updateHistoryCount(){try{$('#run-count').textContent=(await api('/api/runs')).length;}catch{}}
|
|
105
|
+
async function history(){try{const runs=await api('/api/runs');showModal('Run history',runs.length?runs.map(r=>`<button class="saved-row" data-run="${esc(r.id)}"><span><strong>${esc(r.name)}</strong><br><small>${esc(new Date(r.created).toLocaleString())} · ${(r.duration_ms/1000).toFixed(2)}s</small></span><span class="run-badge ${esc(r.status)}">${esc(r.status)}</span></button>`).join(''):'<p class="modal-copy">Your runs will appear here, including approvals waiting for a response.</p>');document.querySelectorAll('[data-run]').forEach(b=>b.onclick=async()=>{try{const r=await api('/api/runs/'+b.dataset.run);closeModal();confirmReplace(()=>{load(r.workflow);S.run=r;S.tab='output';pollRun(r.id);});}catch(error){toast(error.message);}});}catch(error){toast(error.message);}}
|
|
106
|
+
function help(){showModal('A visual workspace for local agents',`<div class="modal-copy"><p><strong>Build:</strong> drag nodes from the library or click to add one. Drag a node to arrange it.</p><p><strong>Connect:</strong> click an output dot, then an input dot. You can also choose targets in Node settings.</p><p><strong>Configure:</strong> select a node, set its prompts, tools, schema, or condition. Use <code>{{field}}</code> to reference state.</p><p><strong>Run:</strong> edit the Input JSON and run. Inspect Output, State, or the native execution trace.</p><p><strong>Models:</strong> choose OpenRouter on a Chat Model or Agent node, enter an OpenRouter model ID, and configure OPENROUTER_API_KEY under Model integrations. The offline fixture makes no AI request.</p><p><strong>Shortcuts:</strong> <kbd>Ctrl/Cmd Z</kbd> undo, <kbd>Delete</kbd> remove selected node, <kbd>Escape</kbd> cancel connection. Scroll to pan; Ctrl/Cmd + scroll to zoom.</p><p>Scheduling, checkpoints, and equality conditions run in C++. Model and tool integrations run in Python. Agent handoffs are sequential in this release.</p></div>`);}
|
|
107
|
+
async function boot(){try{S.bootstrap=await api('/api/bootstrap');$('#engine-status').innerHTML=`<i></i> ${S.bootstrap.native?'C++ engine connected':'Engine needs a build'}`;if(!S.bootstrap.native)toast(S.bootstrap.native_error);let workflow=S.bootstrap.templates[0];try{const draft=JSON.parse(localStorage.getItem('graphcore.studio.draft'));if(draft){checkDocument(draft);workflow=draft;}}catch{}load(workflow);updateHistoryCount();}catch(error){$('#engine-status').textContent='Server unavailable';toast(error.message);}}
|
|
108
|
+
$('#node-search').oninput=renderLibrary;document.querySelectorAll('[data-library]').forEach(b=>b.onclick=()=>{S.library=b.dataset.library;renderLibrary();});document.querySelectorAll('[data-console]').forEach(b=>b.onclick=()=>{S.tab=b.dataset.console;renderConsole();});$('#workflow-name').onchange=e=>{mark();S.workflow.name=e.target.value||'Untitled workflow';$('#breadcrumb').textContent=S.workflow.name;changed();};$('#settings-toggle').onclick=()=>document.querySelector('.inspector').classList.toggle('mobile-open');$('#close-inspector').onclick=()=>document.querySelector('.inspector').classList.remove('mobile-open');$('#save-button').onclick=save;$('#open-button').onclick=openSaved;$('#run-button').onclick=startRun;$('#history-button').onclick=history;$('#integrations-button').onclick=openIntegrations;$('#builder-button').onclick=()=>$('#canvas-area').focus();$('#help-button').onclick=help;$('#add-note-button').onclick=help;$('#json-button').onclick=showJSON;$('#modal-close').onclick=closeModal;$('#validate-button').onclick=async()=>{try{const data=await api('/api/validate',{workflow:S.workflow});showModal(data.errors.length?'A few connections need attention':'Your workflow is ready',data.errors.length?`<div class="error-box">${esc(data.errors.join('\n'))}</div>`:'<div class="success-box">All nodes and routes are valid.<br>Run the workflow to test its behavior with your input.</div>');}catch(error){toast(error.message);}};$('#stop-button').onclick=async()=>{try{await api(`/api/runs/${S.run.id}/cancel`,{});toast('Stop requested. Active callbacks must finish before execution stops.');pollRun(S.run.id);}catch(error){toast(error.message);}};$('#zoom-in').onclick=()=>zoom(1.15);$('#zoom-out').onclick=()=>zoom(1/1.15);$('#fit-button').onclick=fit;$('#undo-button').onclick=()=>{if(!S.undo.length)return;S.redo.push(clone(S.workflow));S.workflow=S.undo.pop();changed();renderAll();};$('#redo-button').onclick=()=>{if(!S.redo.length)return;S.undo.push(clone(S.workflow));S.workflow=S.redo.pop();changed();renderAll();};$('#import-file').onchange=async e=>{const file=e.target.files[0];if(!file)return;try{if(file.size>2*1024*1024)throw Error('Workflow file exceeds 2 MiB');const value=JSON.parse(await file.text());checkDocument(value);closeModal();confirmReplace(()=>{load(value);changed();});}catch(error){toast(error.message);}e.target.value='';};const canvas=$('#canvas-area');canvas.addEventListener('dragover',e=>{e.preventDefault();e.dataTransfer.dropEffect='copy';});canvas.addEventListener('drop',e=>{e.preventDefault();const kind=e.dataTransfer.getData('application/graphcore-node');if(META[kind]){const p=worldPoint(e);addNode(kind,p.x-113,p.y-54);}});canvas.addEventListener('pointerdown',e=>{if(e.button!==0||e.target.closest('.canvas-node,.canvas-controls'))return;drag={type:'pan',x:e.clientX,y:e.clientY,vx:S.view.x,vy:S.view.y};});canvas.addEventListener('wheel',e=>{e.preventDefault();if(e.ctrlKey||e.metaKey){const rect=canvas.getBoundingClientRect();zoom(Math.exp(-e.deltaY*.005),e.clientX-rect.left,e.clientY-rect.top);}else{S.view.x-=e.deltaX;S.view.y-=e.deltaY;applyView();}},{passive:false});document.addEventListener('keydown',e=>{if(e.key==='Escape'){S.connection=null;drawEdges();$('#canvas-hint').textContent='Drag to arrange · connect the dots to build a flow';}if(e.target.matches('input,textarea,select')||$('#modal').open)return;if((e.metaKey||e.ctrlKey)&&e.key.toLowerCase()==='z'){e.preventDefault();(e.shiftKey?$('#redo-button'):$('#undo-button')).click();}if(e.key==='Delete'||e.key==='Backspace'){e.preventDefault();deleteSelected();}if(e.key==='/'){e.preventDefault();$('#node-search').focus();}});window.addEventListener('resize',()=>drawEdges());boot();
|
studio/web/index.html
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
|
|
5
|
+
<title>GraphCore Studio · Local agent builder</title>
|
|
6
|
+
<link rel="stylesheet" href="/style.css"><script src="/app.js" defer></script>
|
|
7
|
+
</head>
|
|
8
|
+
<body>
|
|
9
|
+
<header class="appbar">
|
|
10
|
+
<a class="brand" href="/" aria-label="GraphCore Studio"><span class="brandmark">g<span>c</span></span><strong>GraphCore<span> STUDIO</span></strong></a>
|
|
11
|
+
<div class="workspace-name"><span class="folder-icon">▧</span> Personal workspace <span class="crumb">/</span> <span id="breadcrumb">Research & review</span></div>
|
|
12
|
+
<div class="appbar-right"><span id="engine-status" class="engine-pill"><i></i> Connecting…</span><button id="help-button" class="icon-button" aria-label="Help">?</button><span class="avatar">L</span></div>
|
|
13
|
+
</header>
|
|
14
|
+
<div class="workspace">
|
|
15
|
+
<aside class="library">
|
|
16
|
+
<div class="library-top"><div class="eyebrow">WORKSPACE</div><button class="workspace-item active" id="builder-button"><span>▦</span> Workflow builder</button><button class="workspace-item" id="history-button"><span>◷</span> Run history <span id="run-count" class="count">0</span></button><button class="workspace-item" id="integrations-button"><span>⌘</span> Model integrations</button></div>
|
|
17
|
+
<div class="library-tabs"><button class="active" data-library="nodes">Nodes</button><button data-library="templates">Templates</button></div>
|
|
18
|
+
<div class="search-wrap"><span>⌕</span><input id="node-search" placeholder="Search nodes…" aria-label="Search nodes"><kbd>/</kbd></div>
|
|
19
|
+
<div id="node-library"></div>
|
|
20
|
+
<div class="library-foot"><span class="local-dot"></span><div><strong>Your machine. Your workflows.</strong><p>Designed to run locally.</p></div></div>
|
|
21
|
+
</aside>
|
|
22
|
+
<main class="main">
|
|
23
|
+
<section class="workflow-header">
|
|
24
|
+
<div><div class="workflow-title"><input id="workflow-name" aria-label="Workflow name" value="Research & review"><span class="version-tag">DRAFT</span></div><p id="workflow-description">Build a team of agents. Connect the way they think.</p></div>
|
|
25
|
+
<div class="actions"><button id="open-button" class="button quiet" title="Open a saved workflow">Open</button><button id="save-button" class="button">Save</button><button id="run-button" class="button primary"><span>▶</span> Run workflow</button></div>
|
|
26
|
+
</section>
|
|
27
|
+
<div class="canvas-toolbar"><div class="toolbar-tabs"><button class="active" id="canvas-tab">◇ Canvas</button><button id="json-button">{ } Workflow JSON</button></div><div class="toolbar-right"><span id="save-status">Local draft</span><span class="divider"></span><button id="undo-button" class="icon-button" aria-label="Undo" title="Undo (Ctrl/Cmd+Z)">↶</button><button id="redo-button" class="icon-button" aria-label="Redo">↷</button><button id="settings-toggle" class="button small">Settings</button><button id="validate-button" class="button small">✓ Validate</button></div></div>
|
|
28
|
+
<div class="canvas-area" id="canvas-area" tabindex="0" aria-label="Workflow canvas">
|
|
29
|
+
<div class="canvas-banner"><span class="tiny-dot"></span> <span id="mode-label">Offline demo · no model calls</span></div>
|
|
30
|
+
<div id="world"><svg id="edges" aria-label="Workflow connections"></svg><div id="nodes"></div></div>
|
|
31
|
+
<div id="empty-canvas" hidden><h2>Your next agent starts here.</h2><p>Drag a node onto the canvas, or choose a template.</p></div>
|
|
32
|
+
<div class="canvas-hint" id="canvas-hint">Drag to arrange · connect the dots to build a flow</div>
|
|
33
|
+
<div class="canvas-controls"><button id="zoom-out" aria-label="Zoom out">−</button><span id="zoom-label">100%</span><button id="zoom-in" aria-label="Zoom in">+</button><span class="divider"></span><button id="fit-button" aria-label="Fit workflow" title="Fit workflow">⌗</button><span class="divider"></span><button id="add-note-button" title="Show canvas help" aria-label="Canvas help">?</button></div>
|
|
34
|
+
</div>
|
|
35
|
+
<section class="console" id="console"><div class="console-header"><div class="console-tabs"><button class="active" data-console="input">Input</button><button data-console="output">Output</button><button data-console="trace">Execution trace <span id="event-count">0</span></button><button data-console="state">State</button></div><div class="console-meta"><span id="run-status">Ready to run</span><button id="stop-button" class="button small danger" hidden>Stop</button></div></div><div id="console-content"></div></section>
|
|
36
|
+
<footer class="statusbar"><span><i class="local-dot"></i> C++ execution engine <span class="dot-separator">·</span> Python integrations</span><span id="graph-stats">0 nodes · 0 connections</span></footer>
|
|
37
|
+
</main>
|
|
38
|
+
<aside class="inspector"><div class="inspector-heading"><strong>Node settings</strong><span id="inspector-count">01</span><button id="close-inspector" class="icon-button" aria-label="Close node settings">×</button></div><div id="inspector-content"></div></aside>
|
|
39
|
+
</div>
|
|
40
|
+
<div id="toast" role="status" hidden></div>
|
|
41
|
+
<dialog id="modal"><div class="modal-header"><div><div class="eyebrow">GRAPHCORE STUDIO</div><h2 id="modal-title"></h2></div><button id="modal-close" class="icon-button" aria-label="Close dialog">×</button></div><div id="modal-content"></div></dialog>
|
|
42
|
+
<input type="file" id="import-file" accept=".json,application/json" hidden>
|
|
43
|
+
</body></html>
|
studio/web/style.css
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
:root{--purple:#7161d9;--purple-dark:#5946c0;--purple-soft:#f0edfc;--ink:#272735;--muted:#868593;--line:#e9e8ef;--panel:#fff;--bg:#f9f9fc;--green:#34896e;--mono:ui-monospace,SFMono-Regular,Consolas,monospace;--sans:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}*{box-sizing:border-box}body{margin:0;font-family:var(--sans);font-size:13px;color:var(--ink);background:var(--bg)}button,input,textarea,select{font:inherit}button{cursor:pointer}button:disabled{opacity:.45;cursor:not-allowed}button,a,input,select,textarea{outline-offset:3px}button{color:inherit}button:focus-visible,a:focus-visible{outline:2px solid var(--purple)}[hidden]{display:none!important}.appbar{height:64px;background:white;border-bottom:1px solid var(--line);display:flex;align-items:center;padding:0 22px;gap:30px}.brand{display:flex;align-items:center;gap:10px;text-decoration:none;color:var(--ink);min-width:210px}.brandmark{width:31px;height:31px;background:#7664dc;color:white;border-radius:9px;font-weight:800;font-size:20px;letter-spacing:-4px;padding:1px 7px}.brandmark span{position:relative;top:4px}.brand strong{font-size:17px;letter-spacing:-.6px}.brand strong>span{font-size:8px;letter-spacing:1.4px;margin-left:5px;font-weight:600;color:var(--muted)}.workspace-name{display:flex;gap:12px;align-items:center;color:#8c8b98;font-size:12px}.workspace-name #breadcrumb{color:#52515e}.folder-icon{font-size:19px}.crumb{color:#d2cfdc}.appbar-right{margin-left:auto;display:flex;gap:15px;align-items:center}.engine-pill{display:flex;align-items:center;gap:7px;background:#f5faf7;border:1px solid #e3eee6;border-radius:6px;padding:6px 9px;font-size:10px;color:#49836b;white-space:nowrap}.engine-pill i,.local-dot,.tiny-dot{display:inline-block;width:6px;height:6px;background:#5aab85;border-radius:50%}.avatar{border:1px solid #e2dcf3;color:#796699;background:#f2edf9;display:grid;place-items:center;width:29px;height:29px;border-radius:50%;font-size:11px}.icon-button{border:0;background:transparent;width:28px;height:28px;border-radius:5px;color:#92909c;font-size:17px}.icon-button:hover{background:#f3f1f9;color:var(--purple)}.workspace{display:grid;grid-template-columns:218px minmax(400px,1fr) 290px;height:calc(100vh - 64px);min-height:650px}.library{border-right:1px solid var(--line);background:#fdfdfe;display:flex;flex-direction:column;min-height:0}.library-top{padding:24px 14px 18px}.eyebrow{font-size:9px;letter-spacing:1.4px;color:#9694a3;font-weight:650}.library-top .eyebrow{padding:0 12px;margin-bottom:12px}.workspace-item{border:0;background:transparent;border-radius:6px;display:flex;align-items:center;width:100%;text-align:left;padding:10px 12px;font-size:11px;gap:11px;color:#898695;margin:2px 0}.workspace-item.active{background:var(--purple-soft);color:#7562cc;font-weight:600}.workspace-item>span:first-child{font-size:17px}.count{margin-left:auto;background:#f0eff4;padding:2px 5px;border-radius:4px;font-size:9px}.library-tabs{display:flex;border-bottom:1px solid var(--line);padding:0 18px;gap:25px}.library-tabs button{background:none;border:none;color:var(--muted);font-size:11px;padding:10px 2px}.library-tabs button.active{color:#5a526c;border-bottom:2px solid var(--purple);font-weight:600}.search-wrap{display:flex;align-items:center;margin:18px 16px 11px;border:1px solid #e9e7ef;border-radius:6px;background:white;padding:5px 8px;color:#aaa6b4;gap:6px}.search-wrap>span{font-size:21px}.search-wrap input{border:0;min-width:0;width:100%;font-size:10px;outline:none;color:#6d697c;background:none}.search-wrap kbd{border:1px solid #eeeaf2;border-radius:3px;padding:1px 4px;font-size:9px}.library-group{font-size:9px;letter-spacing:1px;color:#a5a0ae;margin:16px 22px 8px;font-weight:600}.palette-node{display:flex;align-items:center;gap:10px;padding:10px;border:1px solid transparent;border-radius:7px;margin:2px 12px;cursor:grab;background:transparent;width:calc(100% - 24px);text-align:left}.palette-node:hover{background:white;border-color:#e9e5f2;box-shadow:0 2px 8px #28203a05}.node-icon{width:31px;height:31px;border-radius:8px;display:grid;place-items:center;flex-shrink:0;background:var(--node-bg,#eeeafc);color:var(--node-color,#8070cc)}.node-icon svg{width:17px;height:17px;fill:none;stroke:currentColor;stroke-width:1.6;stroke-linecap:round;stroke-linejoin:round}.palette-node strong{display:block;font-size:11px;font-weight:550}.palette-node small{display:block;font-size:9px;color:#9d99a8;margin-top:3px}.palette-node .grab{margin-left:auto;color:#c5c0ce;font-size:13px}.library-foot{margin-top:auto;border-top:1px solid var(--line);padding:17px 18px;display:flex;gap:8px}.library-foot .local-dot{margin-top:4px}.library-foot strong{font-size:9px;font-weight:550;color:#8b8797}.library-foot p{font-size:9px;color:#aaa5b2;margin:5px 0 0}#node-library{overflow:auto}.template-card{display:block;background:white;border:1px solid var(--line);border-radius:9px;margin:12px;width:calc(100% - 24px);padding:14px;text-align:left;transition:border-color .15s}.template-card:hover{border-color:#b6a8e6}.template-card strong{font-size:11px;display:block;margin:9px 0}.template-card p{font-size:10px;color:#96909f;line-height:1.6;margin:0}.template-card small{font-size:9px;color:var(--purple);display:block;margin-top:12px}.main{min-width:0;display:flex;flex-direction:column;min-height:0}.workflow-header{background:white;display:flex;align-items:center;justify-content:space-between;padding:23px 25px 18px;gap:12px}.workflow-title{display:flex;align-items:center;gap:7px}.workflow-title input{border:0;font-size:20px;font-weight:600;letter-spacing:-.7px;width:240px;max-width:28vw;background:transparent;padding:0;color:#343040}.version-tag{font-size:8px;color:#a09bab;border:1px solid #edeaf1;padding:3px 5px;border-radius:4px;letter-spacing:.6px}.workflow-header p{color:#9d96a6;font-size:10px;margin:8px 0 0}.actions{display:flex;gap:7px}.button{border:1px solid #e5e1ed;background:white;border-radius:6px;padding:8px 13px;font-size:11px;white-space:nowrap;box-shadow:0 1px 2px #21173703}.button:hover{background:#faf8ff;border-color:#cfc5e5}.button.primary{background:var(--purple);color:white;border-color:var(--purple);box-shadow:0 3px 7px #6552b721}.button.primary:hover{background:var(--purple-dark)}.button.primary span{font-size:9px;margin-right:5px}.button.quiet{background:transparent;border-color:transparent;color:#938a9f;box-shadow:none}.button.small{font-size:10px;padding:5px 9px}.button.danger{color:#b76673;border-color:#ebd6dd}.canvas-toolbar{height:45px;display:flex;align-items:center;justify-content:space-between;background:white;border-bottom:1px solid var(--line);padding:0 24px}.toolbar-tabs{display:flex;height:100%;gap:20px}.toolbar-tabs button{border:0;background:none;color:#a19aa9;font-size:10px;padding:0 1px}.toolbar-tabs button.active{border-bottom:2px solid #8b75d1;color:#76648e}.toolbar-right{display:flex;align-items:center;gap:8px}.toolbar-right>span:first-child{font-size:9px;color:#a7a0af}.divider{width:1px;height:16px;background:#e9e4ef;display:inline-block;margin:0 5px}.canvas-area{position:relative;flex:1;min-height:250px;overflow:hidden;background-color:#fafafd;background-image:radial-gradient(#dbd6e6 .8px,transparent .8px);background-size:18px 18px;touch-action:none}.canvas-banner{position:absolute;left:21px;top:17px;z-index:3;display:flex;align-items:center;gap:7px;font-size:9px;background:#fffdfd;border:1px solid #ebe7f0;border-radius:5px;padding:6px 9px;color:#9c92aa;pointer-events:none}.canvas-banner .tiny-dot{background:#ae9cce;width:5px;height:5px}#world{position:absolute;left:0;top:0;transform-origin:0 0}#edges{position:absolute;left:0;top:0;width:1px;height:1px;overflow:visible;pointer-events:none;z-index:0}#edges path{fill:none;stroke:#c8bfdc;stroke-width:1.6}#edges path.ran{stroke:#9680d7;stroke-width:2}#edges text{font-size:10px;fill:#a69bb6;font-family:var(--sans)}.canvas-node{position:absolute;width:226px;min-height:108px;background:white;border:1px solid #e2dce9;border-radius:10px;box-shadow:0 3px 6px #35244205,0 10px 24px #35244204;user-select:none;cursor:grab}.canvas-node.selected{border-color:#a18ad8;box-shadow:0 0 0 3px #ebe4fb,0 10px 24px #35244208}.canvas-node.running{border-color:#8870d6;box-shadow:0 0 0 3px #ece5ff;animation:pulse 1.4s infinite}.canvas-node.completed:after{content:'✓';position:absolute;top:-7px;right:-7px;width:18px;height:18px;background:#66a88b;border:3px solid white;border-radius:50%;color:white;text-align:center;font-size:9px;line-height:12px}.canvas-node.failed{border-color:#d8838d}.node-head{display:flex;align-items:center;gap:10px;padding:15px 15px 11px}.node-head strong{display:block;font-size:12px;font-weight:600;color:#51455f}.node-head small{display:block;font-size:9px;color:#aaa0b3;margin-top:4px}.node-menu{margin-left:auto;align-self:flex-start;color:#c3baca;font-size:16px}.node-foot{border-top:1px solid #f1edf5;display:flex;align-items:center;justify-content:space-between;padding:9px 14px;font-size:9px;color:#aa9eb6;gap:6px}.node-foot code{font:8px var(--mono);padding:2px 4px;border:1px solid #eee8f3;background:#fcfaff;border-radius:3px;max-width:140px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.port{position:absolute;border:2px solid #b7a6cc;background:#fff;width:10px;height:10px;border-radius:50%;padding:0;top:50%;transform:translateY(-50%);z-index:3;cursor:crosshair}.port:hover,.port.connecting{border-color:var(--purple);background:var(--purple);box-shadow:0 0 0 5px #e8dcfb}.port.input{left:-6px}.port.output{right:-6px}.port.true{top:35%;border-color:#8eba9d}.port.false{top:72%;border-color:#d5b484}.port-label{position:absolute;right:10px;font-size:8px;pointer-events:none}.port-label.true{top:28%;color:#87a894}.port-label.false{top:67%;color:#bcaa8c}.canvas-node.condition .node-head{padding-right:35px}.canvas-hint{position:absolute;bottom:20px;left:22px;font-size:9px;color:#b3a9bf;pointer-events:none}.canvas-controls{position:absolute;bottom:15px;right:18px;display:flex;align-items:center;background:white;border:1px solid #e7e0ee;border-radius:7px;padding:4px;box-shadow:0 2px 9px #49335b05}.canvas-controls button{background:none;border:0;width:27px;height:25px;color:#9583ad;font-size:18px;border-radius:3px}.canvas-controls button:hover{background:var(--purple-soft)}.canvas-controls>span:not(.divider){font-size:9px;color:#9888ad;min-width:36px;text-align:center}.console{height:208px;min-height:160px;background:white;border-top:1px solid var(--line);display:flex;flex-direction:column}.console-header{height:43px;flex-shrink:0;border-bottom:1px solid #f0ecf4;display:flex;align-items:center;justify-content:space-between;padding:0 22px}.console-tabs{display:flex;gap:22px;height:100%}.console-tabs button{border:0;background:transparent;font-size:10px;color:#a193ae;padding:0}.console-tabs button.active{color:#80708f;border-bottom:2px solid #9b82bc}.console-tabs button span{background:#f5f0fa;border-radius:3px;padding:1px 4px;font-size:8px;margin-left:4px}.console-meta{display:flex;gap:8px;align-items:center;font-size:9px;color:#aba0b7}#console-content{overflow:auto;flex:1;padding:14px 22px}.input-pane{display:flex;gap:22px;height:100%}.input-pane>div{flex:1}.input-pane textarea{width:100%;height:100%;min-height:100px;border:1px solid #ede7f4;border-radius:6px;background:#fdfbff;color:#877397;font:10px/1.8 var(--mono);resize:none;padding:9px 12px}.input-hint{max-width:240px;padding:6px 0;font-size:10px;line-height:1.8;color:#a394ae}.input-hint strong{font-size:11px;display:block;color:#7c6d8b;font-weight:550;margin-bottom:5px}.input-hint code{font-size:9px;background:#f6f1fa;padding:2px 4px;border-radius:4px}.output-text{margin:0;white-space:pre-wrap;font:11px/1.8 var(--sans);color:#665775}.state-json{margin:0;white-space:pre-wrap;font:10px/1.8 var(--mono);color:#7a628d}.console-empty{display:flex;align-items:center;justify-content:center;gap:15px;height:100%;color:#b3a5bf;font-size:11px}.console-empty strong{display:block;font-size:12px;color:#8b7b98;margin-bottom:5px;font-weight:500}.empty-symbol{width:40px;height:40px;border:1px solid #eae2f2;border-radius:10px;display:grid;place-items:center;font-size:21px;color:#b29fc4;background:#fdfbff}.trace-row{display:flex;align-items:center;gap:11px;padding:5px 0;font-size:10px;color:#a292b1;border-bottom:1px solid #f8f4fc}.trace-row time{font-family:var(--mono);font-size:9px;color:#bbafc5}.trace-row strong{font-weight:500;color:#8c789c;min-width:130px}.trace-row code{font:9px var(--mono);color:#a28baf}.approval-panel{padding:2px 0}.approval-panel .question{white-space:pre-wrap;font-size:11px;line-height:1.6;color:#7d668d;max-height:150px;overflow:auto;margin:6px 0 12px}.approval-form{display:flex;gap:8px;align-items:center}.approval-form input{padding:7px;border:1px solid #dfd3e9;border-radius:5px;background:white;flex:1;min-width:100px;font-size:10px}.statusbar{height:29px;flex-shrink:0;border-top:1px solid #eee8f3;background:#fdfbff;display:flex;align-items:center;justify-content:space-between;padding:0 18px;color:#ac9abb;font-size:8px}.statusbar .local-dot{width:4px;height:4px;margin-right:5px;background:#a799bf}.dot-separator{margin:0 7px;color:#c6bacf}.inspector{border-left:1px solid var(--line);background:white;display:flex;flex-direction:column;min-height:0}.inspector-heading{display:flex;align-items:center;justify-content:space-between;padding:23px 21px 18px;border-bottom:1px solid var(--line)}.inspector-heading strong{font-size:11px;font-weight:550;color:#77707e}.inspector-heading span{font:9px var(--mono);color:#b8adbf;border:1px solid #eee8f3;padding:2px 4px;border-radius:4px}#inspector-content{overflow:auto;padding:20px;flex:1}.inspector-hero{display:flex;align-items:center;gap:10px;margin-bottom:20px}.inspector-hero strong{font-size:12px;display:block;font-weight:600;color:#665374}.inspector-hero p{font-size:9px;line-height:1.5;color:#ac9bb6;margin:4px 0 0}.inspector-section{font-size:9px;letter-spacing:1px;color:#a090ad;border-top:1px solid #f1eaf6;padding-top:18px;margin:21px 0 14px}.field{display:block;margin:14px 0}.field>span{font-size:10px;color:#8a759a;display:flex;justify-content:space-between;margin-bottom:7px}.field input,.field select,.field textarea{width:100%;border:1px solid #e7dfee;background:#fff;border-radius:5px;padding:8px 9px;font-size:10px;color:#8d799e;outline-color:#b39bd0}.field textarea{resize:vertical;min-height:80px;line-height:1.65}.field small,.field-help{font-size:9px;line-height:1.7;color:#b0a0bd;display:block;margin-top:5px}.field select{appearance:auto}.field input:focus,.field textarea:focus{border-color:#b8a0cf}.field .code{font-family:var(--mono);font-size:9px}.check-field{display:flex;gap:8px;align-items:center;font-size:10px;color:#937da4;margin:13px 0}.inspector-note{font-size:9px;line-height:1.8;border:1px solid #e9dff1;background:#fcf9ff;padding:10px;border-radius:6px;color:#ab95bb;margin:16px 0}.inspector-note strong{color:#90769f;font-weight:550}.connection-row{display:flex;align-items:center;gap:6px;border:1px solid #efe6f4;border-radius:5px;padding:7px 8px;margin:6px 0;font-size:9px;color:#a087b0}.connection-row strong{font-weight:400;flex:1}.connection-row button{border:0;background:none;color:#bc9dc7;font-size:14px}.connection-row select{border:0;max-width:145px;font-size:9px;color:#9c81ae;background:none}.inspector-empty{text-align:center;color:#b1a0bd;padding:45px 10px;font-size:11px;line-height:1.8}.inspector-empty strong{display:block;font-size:13px;color:#8e759f;font-weight:500}.delete-node{margin-top:18px;width:100%;font-size:10px;color:#ba8ca2}.schema-row{display:flex;gap:4px;margin:5px 0}.schema-row input{width:85px;min-width:0}.schema-row select{width:78px}.schema-row input,.schema-row select{border:1px solid #e9dff0;padding:5px;font-size:9px;border-radius:4px;color:#967da8}.schema-row button{border:none;background:none;color:#baa2c9}.schema-row .required{width:auto}.schema-legend{font-size:8px;color:#b89ac6;display:flex;justify-content:space-between;margin-bottom:7px}dialog{border:1px solid #e6dced;border-radius:14px;width:min(700px,90vw);max-height:85vh;padding:25px;box-shadow:0 25px 100px #35224426;color:var(--ink)}dialog::backdrop{background:#30263f33;backdrop-filter:blur(3px)}.modal-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:20px}.modal-header h2{font-size:21px;letter-spacing:-.5px;font-weight:550;margin:7px 0}.modal-header .eyebrow{font-size:8px}.modal-code{width:100%;height:350px;resize:vertical;font:11px/1.7 var(--mono);background:#faf7fd;border:1px solid #e9e0f0;border-radius:7px;padding:15px;color:#826994}.modal-actions{display:flex;gap:8px;justify-content:flex-end;margin-top:15px}.modal-copy{font-size:12px;color:#94839f;line-height:1.8}.modal-copy kbd{border:1px solid #e8ddef;border-radius:4px;font-size:10px;padding:2px 5px}.saved-row{width:100%;padding:14px;background:#fff;border:1px solid #eee5f4;margin:5px 0;border-radius:7px;display:flex;align-items:center;justify-content:space-between;text-align:left;color:#8f76a0}.saved-row small{color:#b29bbf;font-size:10px}.run-badge{font-size:9px;padding:4px 7px;border-radius:5px;background:#f5eefb;color:#a184b4}.run-badge.completed{background:#eef8f3;color:#6eaa8d}.run-badge.failed{background:#fff0f3;color:#ba7e90}.run-badge.suspended{background:#fff8ea;color:#b29a6a}#toast{position:fixed;bottom:40px;left:50%;transform:translateX(-50%);padding:11px 18px;background:#44324f;color:#fff;border-radius:7px;box-shadow:0 5px 20px #32213e25;font-size:11px;z-index:50;max-width:80vw;white-space:pre-wrap}#empty-canvas{position:absolute;left:50%;top:45%;transform:translate(-50%,-50%);text-align:center;width:350px;color:#aa99b8;pointer-events:none}#empty-canvas h2{font-size:19px;font-weight:500;color:#8e76a1}#empty-canvas p{font-size:11px}.error-box{background:#fff4f6;border:1px solid #f0dbe2;color:#af7a8b;padding:13px;border-radius:6px;font-size:11px;line-height:1.7;white-space:pre-wrap}.success-box{background:#f0f9f3;border:1px solid #dceee2;color:#72967f;padding:15px;border-radius:7px;font-size:12px;line-height:1.7}@keyframes pulse{50%{box-shadow:0 0 0 6px #ece5ff55}}@media(min-width:1700px){.workspace{grid-template-columns:240px minmax(400px,1fr) 320px}.console{height:240px}.workflow-header{padding-top:27px;padding-bottom:24px}}@media(max-width:1150px){.workspace{grid-template-columns:185px minmax(350px,1fr) 245px}.brand{min-width:165px}.workspace-name{display:none}.workflow-header{padding:18px}.workflow-title input{font-size:17px;width:180px}.workflow-header p{max-width:240px;line-height:1.5}.actions{gap:4px}.actions .quiet{display:none}.actions .button{padding:8px;font-size:10px}.console-tabs{gap:12px}.input-hint{display:none}.version-tag{display:none}}@media(max-width:850px){.workspace{grid-template-columns:150px minmax(350px,1fr)}.inspector{display:none}.appbar{padding:0 15px}.library-top{padding:18px 8px}.palette-node small{display:none}.palette-node .grab{display:none}.library-foot{padding:12px}.library-foot strong{font-size:8px}.canvas-hint{display:none}.engine-pill{font-size:9px}.console{height:190px}.workflow-header p{display:none}}
|
|
2
|
+
|
|
3
|
+
/* Settings remain usable in the narrow in-app browser. */
|
|
4
|
+
#settings-toggle,#close-inspector{display:none}
|
|
5
|
+
@media(max-width:850px){#settings-toggle,#close-inspector{display:inline-block}.inspector.mobile-open{display:flex;position:fixed;right:0;top:64px;bottom:0;width:290px;z-index:12;box-shadow:-10px 0 40px #392b5020}.inspector-heading{gap:12px}.inspector-heading strong{flex:1}.toolbar-tabs{gap:10px}.toolbar-right{gap:3px}.toolbar-right>span:first-child{display:none}.canvas-toolbar{padding:0 16px}.workspace{grid-template-columns:135px minmax(350px,1fr)}.node-head strong{font-size:14px}.node-head small{font-size:11px}.node-foot{font-size:10px}.node-foot code{font-size:10px}}
|
|
6
|
+
/* Main controls and help copy retain readable contrast on a light surface. */
|
|
7
|
+
.field>span,.field input,.field select,.field textarea{color:#675773}.field small,.field-help{color:#8b7b98}.inspector-note{color:#897298}.node-head small,.node-foot{color:#8d7c9c}.palette-node small{color:#8b8495}.workflow-header p{color:#8b8295}.input-hint{color:#8a7b97}.console-tabs button{color:#897795}.canvas-hint{color:#9685a6}.toolbar-tabs button{color:#8e7e9d}
|
|
8
|
+
.message-card{border:1px solid #ebe4f1;border-radius:7px;background:#fdfbff;padding:9px;margin:9px 0}.message-card header,.message-tools{display:flex;align-items:center;gap:8px}.message-card header{justify-content:space-between;margin-bottom:8px}.message-card header select,.message-tools select{border:1px solid #e5dbea;border-radius:4px;background:white;color:#675773;font-size:10px;padding:5px 7px}.message-remove{border:0;background:transparent;color:#a78bac;font-size:17px}.message-remove:disabled{opacity:.35;cursor:not-allowed}.message-content{display:block;width:100%;min-height:68px;resize:vertical;border:1px solid #e7dfee;border-radius:5px;padding:8px;font:10px/1.6 var(--mono);color:#675773;background:white}.message-tools{margin-top:7px;justify-content:space-between}.message-tools select{max-width:100%;min-width:0;flex:1}.message-tools small{font-size:8px;color:#91849a;white-space:nowrap}
|
|
9
|
+
.environment-row{display:flex;align-items:center;gap:12px;padding:10px 0;border-bottom:1px solid #f0ebf4}.environment-row>label:first-child{flex:1;min-width:0;display:grid;grid-template-columns:minmax(120px,1fr) minmax(160px,1.2fr);align-items:center;gap:10px}.environment-row strong{display:block;font:10px var(--mono);color:#675773;overflow-wrap:anywhere}.environment-row small{display:block;margin-top:4px;font-size:9px;color:#8b7b98}.environment-row input[type=password],.environment-row input[data-env-name]{min-width:0;width:100%;border:1px solid #e7dfee;border-radius:5px;padding:8px;font-size:10px;color:#675773}.environment-row.custom>label:first-child{grid-template-columns:minmax(120px,.8fr) minmax(160px,1.2fr)}.env-remove{display:flex;align-items:center;gap:4px;color:#917f9b;font-size:9px;white-space:nowrap}.env-security{padding:9px 11px;background:#f8f5fb;border-radius:6px;color:#867791;font-size:10px}.env-security code{font:9px var(--mono)}@media(max-width:600px){.environment-row>label:first-child,.environment-row.custom>label:first-child{grid-template-columns:1fr;gap:6px}.env-remove{font-size:8px}}
|