@aion0/forge 0.4.15 → 0.5.0
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.
- package/CLAUDE.md +1 -1
- package/README.md +2 -2
- package/RELEASE_NOTES.md +170 -13
- package/app/api/agents/route.ts +17 -0
- package/app/api/delivery/[id]/route.ts +62 -0
- package/app/api/delivery/route.ts +40 -0
- package/app/api/mobile-chat/route.ts +13 -7
- package/app/api/monitor/route.ts +10 -6
- package/app/api/pipelines/[id]/route.ts +16 -3
- package/app/api/tasks/route.ts +2 -1
- package/app/api/workspace/[id]/agents/route.ts +35 -0
- package/app/api/workspace/[id]/memory/route.ts +23 -0
- package/app/api/workspace/[id]/smith/route.ts +22 -0
- package/app/api/workspace/[id]/stream/route.ts +28 -0
- package/app/api/workspace/route.ts +100 -0
- package/app/global-error.tsx +10 -4
- package/app/icon.ico +0 -0
- package/app/layout.tsx +2 -2
- package/app/login/LoginForm.tsx +96 -0
- package/app/login/page.tsx +7 -98
- package/app/page.tsx +2 -2
- package/bin/forge-server.mjs +23 -4
- package/check-forge-status.sh +9 -0
- package/cli/mw.ts +2 -2
- package/components/ConversationEditor.tsx +411 -0
- package/components/ConversationGraphView.tsx +347 -0
- package/components/ConversationTerminalView.tsx +303 -0
- package/components/Dashboard.tsx +36 -39
- package/components/DashboardWrapper.tsx +9 -0
- package/components/DeliveryFlowEditor.tsx +491 -0
- package/components/DeliveryList.tsx +230 -0
- package/components/DeliveryWorkspace.tsx +589 -0
- package/components/DocTerminal.tsx +12 -4
- package/components/DocsViewer.tsx +10 -2
- package/components/HelpTerminal.tsx +13 -8
- package/components/InlinePipelineView.tsx +111 -0
- package/components/MobileView.tsx +20 -0
- package/components/MonitorPanel.tsx +9 -4
- package/components/NewTaskModal.tsx +32 -0
- package/components/PipelineEditor.tsx +49 -6
- package/components/PipelineView.tsx +482 -64
- package/components/ProjectDetail.tsx +314 -56
- package/components/ProjectManager.tsx +49 -4
- package/components/SessionView.tsx +27 -13
- package/components/SettingsModal.tsx +790 -124
- package/components/SkillsPanel.tsx +34 -8
- package/components/TaskBoard.tsx +3 -0
- package/components/WebTerminal.tsx +259 -45
- package/components/WorkspaceTree.tsx +221 -0
- package/components/WorkspaceView.tsx +2224 -0
- package/docs/LOCAL-DEPLOY.md +15 -15
- package/install.sh +2 -2
- package/lib/agents/claude-adapter.ts +104 -0
- package/lib/agents/generic-adapter.ts +64 -0
- package/lib/agents/index.ts +242 -0
- package/lib/agents/types.ts +70 -0
- package/lib/artifacts.ts +106 -0
- package/lib/cloudflared.ts +1 -1
- package/lib/delivery.ts +787 -0
- package/lib/forge-skills/forge-inbox.md +37 -0
- package/lib/forge-skills/forge-send.md +40 -0
- package/lib/forge-skills/forge-status.md +32 -0
- package/lib/forge-skills/forge-workspace-sync.md +37 -0
- package/lib/help-docs/00-overview.md +8 -2
- package/lib/help-docs/01-settings.md +159 -2
- package/lib/help-docs/05-pipelines.md +95 -6
- package/lib/help-docs/07-projects.md +35 -1
- package/lib/help-docs/11-workspace.md +204 -0
- package/lib/help-docs/CLAUDE.md +5 -2
- package/lib/init.ts +62 -12
- package/lib/pipeline.ts +537 -1
- package/lib/settings.ts +115 -22
- package/lib/skills.ts +249 -372
- package/lib/task-manager.ts +113 -33
- package/lib/telegram-bot.ts +33 -1
- package/lib/telegram-standalone.ts +1 -1
- package/lib/terminal-server.ts +2 -2
- package/lib/terminal-standalone.ts +1 -1
- package/lib/workspace/__tests__/state-machine.test.ts +388 -0
- package/lib/workspace/__tests__/workspace.test.ts +311 -0
- package/lib/workspace/agent-bus.ts +416 -0
- package/lib/workspace/agent-worker.ts +667 -0
- package/lib/workspace/backends/api-backend.ts +262 -0
- package/lib/workspace/backends/cli-backend.ts +479 -0
- package/lib/workspace/index.ts +82 -0
- package/lib/workspace/manager.ts +136 -0
- package/lib/workspace/orchestrator.ts +1804 -0
- package/lib/workspace/persistence.ts +310 -0
- package/lib/workspace/presets.ts +170 -0
- package/lib/workspace/skill-installer.ts +188 -0
- package/lib/workspace/smith-memory.ts +498 -0
- package/lib/workspace/types.ts +231 -0
- package/lib/workspace/watch-manager.ts +288 -0
- package/lib/workspace-standalone.ts +790 -0
- package/middleware.ts +1 -0
- package/next-env.d.ts +1 -1
- package/package.json +5 -2
- package/src/config/index.ts +13 -2
- package/src/core/db/database.ts +1 -0
- package/start.sh +10 -0
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect, useCallback, useMemo } from 'react';
|
|
4
|
+
import {
|
|
5
|
+
ReactFlow,
|
|
6
|
+
Background,
|
|
7
|
+
Controls,
|
|
8
|
+
Handle,
|
|
9
|
+
Position,
|
|
10
|
+
useNodesState,
|
|
11
|
+
useEdgesState,
|
|
12
|
+
type Node,
|
|
13
|
+
type Edge,
|
|
14
|
+
type NodeProps,
|
|
15
|
+
MarkerType,
|
|
16
|
+
} from '@xyflow/react';
|
|
17
|
+
import '@xyflow/react/dist/style.css';
|
|
18
|
+
import YAML from 'yaml';
|
|
19
|
+
|
|
20
|
+
// ─── Color palette ────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
const AGENT_PALETTE = [
|
|
23
|
+
{ bg: '#1e2a4a', border: '#3b5998', accent: '#6b8cce', badge: 'bg-blue-500/20 text-blue-400' },
|
|
24
|
+
{ bg: '#2a1e4a', border: '#6b3fa0', accent: '#a07bd6', badge: 'bg-purple-500/20 text-purple-400' },
|
|
25
|
+
{ bg: '#1e3a2a', border: '#3a8a5a', accent: '#5ebd7e', badge: 'bg-green-500/20 text-green-400' },
|
|
26
|
+
{ bg: '#3a2a1e', border: '#a06030', accent: '#d09060', badge: 'bg-orange-500/20 text-orange-400' },
|
|
27
|
+
{ bg: '#3a1e2a', border: '#a03060', accent: '#d06090', badge: 'bg-pink-500/20 text-pink-400' },
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
// ─── Custom Nodes ─────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
interface PromptNodeData { label: string; prompt: string; [key: string]: unknown }
|
|
33
|
+
interface AgentNodeData { label: string; agentId: string; agent: string; role: string; colorIndex: number; [key: string]: unknown }
|
|
34
|
+
interface StopNodeData { label: string; condition: string; maxRounds: number; [key: string]: unknown }
|
|
35
|
+
interface ForgeNodeData { label: string; [key: string]: unknown }
|
|
36
|
+
|
|
37
|
+
function PromptNode({ data }: NodeProps<Node<PromptNodeData>>) {
|
|
38
|
+
return (
|
|
39
|
+
<div className="bg-[#1a2a1a] border-2 border-green-500/50 rounded-xl shadow-lg min-w-[220px] max-w-[300px]">
|
|
40
|
+
<div className="px-4 py-2 border-b border-green-500/30 flex items-center gap-2">
|
|
41
|
+
<span className="text-green-400 text-sm">▶</span>
|
|
42
|
+
<span className="text-xs font-bold text-green-300">{data.label}</span>
|
|
43
|
+
</div>
|
|
44
|
+
<div className="px-4 py-2">
|
|
45
|
+
<div className="text-[9px] text-gray-400 whitespace-pre-wrap line-clamp-3">{data.prompt || 'No prompt'}</div>
|
|
46
|
+
</div>
|
|
47
|
+
<Handle type="source" position={Position.Bottom} className="!bg-green-400 !w-3 !h-3" />
|
|
48
|
+
</div>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function AgentNode({ data }: NodeProps<Node<AgentNodeData>>) {
|
|
53
|
+
const palette = AGENT_PALETTE[data.colorIndex % AGENT_PALETTE.length];
|
|
54
|
+
return (
|
|
55
|
+
<div
|
|
56
|
+
className="rounded-xl shadow-lg min-w-[200px] max-w-[260px]"
|
|
57
|
+
style={{ background: palette.bg, border: `2px solid ${palette.border}` }}
|
|
58
|
+
>
|
|
59
|
+
<Handle type="target" position={Position.Top} className="!w-3 !h-3" style={{ background: palette.accent }} />
|
|
60
|
+
|
|
61
|
+
<div className="px-4 py-2 flex items-center gap-2" style={{ borderBottom: `1px solid ${palette.border}` }}>
|
|
62
|
+
<span className={`text-[8px] px-1.5 py-0.5 rounded font-bold ${palette.badge}`}>{data.agent}</span>
|
|
63
|
+
<span className="text-xs font-bold text-white">{data.agentId}</span>
|
|
64
|
+
</div>
|
|
65
|
+
<div className="px-4 py-2">
|
|
66
|
+
<div className="text-[9px] text-gray-400 line-clamp-3">{data.role || 'No role defined'}</div>
|
|
67
|
+
</div>
|
|
68
|
+
|
|
69
|
+
<Handle type="source" position={Position.Bottom} className="!w-3 !h-3" style={{ background: palette.accent }} />
|
|
70
|
+
</div>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function ForgeNode({ data }: NodeProps<Node<ForgeNodeData>>) {
|
|
75
|
+
return (
|
|
76
|
+
<div className="bg-[#1a1a3a] border-2 border-[#7c5bf0]/60 rounded-xl shadow-lg min-w-[180px]">
|
|
77
|
+
<Handle type="target" position={Position.Top} className="!bg-[#7c5bf0] !w-3 !h-3" />
|
|
78
|
+
<div className="px-4 py-3 flex items-center gap-2 justify-center">
|
|
79
|
+
<span className="text-[#7c5bf0] text-sm">⚡</span>
|
|
80
|
+
<span className="text-xs font-bold text-[#7c5bf0]">{data.label}</span>
|
|
81
|
+
</div>
|
|
82
|
+
<Handle type="source" position={Position.Bottom} className="!bg-[#7c5bf0] !w-3 !h-3" />
|
|
83
|
+
</div>
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function StopNode({ data }: NodeProps<Node<StopNodeData>>) {
|
|
88
|
+
return (
|
|
89
|
+
<div className="bg-[#2a1a1a] border-2 border-red-500/50 rounded-xl shadow-lg min-w-[200px]">
|
|
90
|
+
<Handle type="target" position={Position.Top} className="!bg-red-400 !w-3 !h-3" />
|
|
91
|
+
<div className="px-4 py-2 border-b border-red-500/30 flex items-center gap-2">
|
|
92
|
+
<span className="text-red-400 text-sm">■</span>
|
|
93
|
+
<span className="text-xs font-bold text-red-300">{data.label}</span>
|
|
94
|
+
</div>
|
|
95
|
+
<div className="px-4 py-2 space-y-0.5">
|
|
96
|
+
{data.condition && <div className="text-[9px] text-gray-400">{data.condition}</div>}
|
|
97
|
+
<div className="text-[8px] text-gray-500">Max {data.maxRounds} rounds</div>
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const nodeTypes = {
|
|
104
|
+
prompt: PromptNode,
|
|
105
|
+
agent: AgentNode,
|
|
106
|
+
forge: ForgeNode,
|
|
107
|
+
stop: StopNode,
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
// ─── Parse YAML → ReactFlow nodes/edges ───────────────────
|
|
111
|
+
|
|
112
|
+
interface ConvParsed {
|
|
113
|
+
name: string;
|
|
114
|
+
description?: string;
|
|
115
|
+
input?: Record<string, string>;
|
|
116
|
+
agents: { id: string; agent: string; role: string; project?: string }[];
|
|
117
|
+
maxRounds: number;
|
|
118
|
+
stopCondition?: string;
|
|
119
|
+
initialPrompt: string;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function parseConvYaml(raw: string): ConvParsed | null {
|
|
123
|
+
try {
|
|
124
|
+
const p = YAML.parse(raw);
|
|
125
|
+
if (!p || p.type !== 'conversation') return null;
|
|
126
|
+
return {
|
|
127
|
+
name: p.name || 'unnamed',
|
|
128
|
+
description: p.description,
|
|
129
|
+
input: p.input,
|
|
130
|
+
agents: p.agents || [],
|
|
131
|
+
maxRounds: p.max_rounds || p.maxRounds || 10,
|
|
132
|
+
stopCondition: p.stop_condition || p.stopCondition || '',
|
|
133
|
+
initialPrompt: p.initial_prompt || p.initialPrompt || '',
|
|
134
|
+
};
|
|
135
|
+
} catch { return null; }
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function buildFlowGraph(conv: ConvParsed): { nodes: Node[]; edges: Edge[] } {
|
|
139
|
+
const nodes: Node[] = [];
|
|
140
|
+
const edges: Edge[] = [];
|
|
141
|
+
const agentCount = conv.agents.length;
|
|
142
|
+
|
|
143
|
+
// Layout constants
|
|
144
|
+
const centerX = 300;
|
|
145
|
+
const startY = 30;
|
|
146
|
+
const agentSpacing = 220;
|
|
147
|
+
const verticalGap = 140;
|
|
148
|
+
|
|
149
|
+
// 1. Initial Prompt node
|
|
150
|
+
nodes.push({
|
|
151
|
+
id: 'prompt',
|
|
152
|
+
type: 'prompt',
|
|
153
|
+
position: { x: centerX - 110, y: startY },
|
|
154
|
+
data: { label: 'Initial Prompt', prompt: conv.initialPrompt },
|
|
155
|
+
draggable: true,
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// 2. Forge broker node
|
|
159
|
+
const forgeY = startY + verticalGap;
|
|
160
|
+
nodes.push({
|
|
161
|
+
id: 'forge',
|
|
162
|
+
type: 'forge',
|
|
163
|
+
position: { x: centerX - 90, y: forgeY },
|
|
164
|
+
data: { label: 'Forge Broker' },
|
|
165
|
+
draggable: true,
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
edges.push({
|
|
169
|
+
id: 'prompt-forge',
|
|
170
|
+
source: 'prompt',
|
|
171
|
+
target: 'forge',
|
|
172
|
+
markerEnd: { type: MarkerType.ArrowClosed, color: '#5ebd7e' },
|
|
173
|
+
style: { stroke: '#5ebd7e', strokeWidth: 2 },
|
|
174
|
+
animated: true,
|
|
175
|
+
label: 'start',
|
|
176
|
+
labelStyle: { fill: '#888', fontSize: 9 },
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// 3. Agent nodes — spread horizontally
|
|
180
|
+
const agentY = forgeY + verticalGap;
|
|
181
|
+
const totalWidth = (agentCount - 1) * agentSpacing;
|
|
182
|
+
const agentStartX = centerX - totalWidth / 2;
|
|
183
|
+
|
|
184
|
+
conv.agents.forEach((a, i) => {
|
|
185
|
+
const x = agentStartX + i * agentSpacing - 100;
|
|
186
|
+
nodes.push({
|
|
187
|
+
id: `agent-${a.id}`,
|
|
188
|
+
type: 'agent',
|
|
189
|
+
position: { x, y: agentY },
|
|
190
|
+
data: { label: a.id, agentId: a.id, agent: a.agent, role: a.role, colorIndex: i },
|
|
191
|
+
draggable: true,
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
// Forge → Agent (send prompt)
|
|
195
|
+
edges.push({
|
|
196
|
+
id: `forge-agent-${a.id}`,
|
|
197
|
+
source: 'forge',
|
|
198
|
+
target: `agent-${a.id}`,
|
|
199
|
+
markerEnd: { type: MarkerType.ArrowClosed, color: AGENT_PALETTE[i % AGENT_PALETTE.length].accent },
|
|
200
|
+
style: { stroke: AGENT_PALETTE[i % AGENT_PALETTE.length].accent, strokeWidth: 2 },
|
|
201
|
+
animated: true,
|
|
202
|
+
label: `send R${i + 1}`,
|
|
203
|
+
labelStyle: { fill: '#888', fontSize: 8 },
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
// Agent → Forge (response back) — curved
|
|
207
|
+
edges.push({
|
|
208
|
+
id: `agent-${a.id}-forge`,
|
|
209
|
+
source: `agent-${a.id}`,
|
|
210
|
+
target: 'forge',
|
|
211
|
+
markerEnd: { type: MarkerType.ArrowClosed, color: AGENT_PALETTE[i % AGENT_PALETTE.length].accent },
|
|
212
|
+
style: { stroke: AGENT_PALETTE[i % AGENT_PALETTE.length].accent, strokeWidth: 1, strokeDasharray: '6 3' },
|
|
213
|
+
animated: true,
|
|
214
|
+
label: 'response',
|
|
215
|
+
labelStyle: { fill: '#666', fontSize: 8 },
|
|
216
|
+
type: 'smoothstep',
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
// 4. Inter-agent data flow edges (Agent A output → Agent B input via Forge)
|
|
221
|
+
if (agentCount >= 2) {
|
|
222
|
+
for (let i = 0; i < agentCount - 1; i++) {
|
|
223
|
+
const from = conv.agents[i];
|
|
224
|
+
const to = conv.agents[i + 1];
|
|
225
|
+
edges.push({
|
|
226
|
+
id: `flow-${from.id}-${to.id}`,
|
|
227
|
+
source: `agent-${from.id}`,
|
|
228
|
+
target: `agent-${to.id}`,
|
|
229
|
+
markerEnd: { type: MarkerType.ArrowClosed, color: '#7c5bf0' },
|
|
230
|
+
style: { stroke: '#7c5bf0', strokeWidth: 2, strokeDasharray: '4 4' },
|
|
231
|
+
animated: true,
|
|
232
|
+
label: 'context →',
|
|
233
|
+
labelStyle: { fill: '#7c5bf0', fontSize: 9, fontWeight: 600 },
|
|
234
|
+
type: 'smoothstep',
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
// Loop back: last agent → first agent (next round)
|
|
238
|
+
if (conv.maxRounds > 1) {
|
|
239
|
+
edges.push({
|
|
240
|
+
id: `loop-${conv.agents[agentCount - 1].id}-${conv.agents[0].id}`,
|
|
241
|
+
source: `agent-${conv.agents[agentCount - 1].id}`,
|
|
242
|
+
target: `agent-${conv.agents[0].id}`,
|
|
243
|
+
markerEnd: { type: MarkerType.ArrowClosed, color: '#d09060' },
|
|
244
|
+
style: { stroke: '#d09060', strokeWidth: 2, strokeDasharray: '8 4' },
|
|
245
|
+
animated: true,
|
|
246
|
+
label: `next round`,
|
|
247
|
+
labelStyle: { fill: '#d09060', fontSize: 9, fontWeight: 600 },
|
|
248
|
+
type: 'smoothstep',
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// 5. Stop condition node
|
|
254
|
+
const stopY = agentY + verticalGap + 20;
|
|
255
|
+
nodes.push({
|
|
256
|
+
id: 'stop',
|
|
257
|
+
type: 'stop',
|
|
258
|
+
position: { x: centerX - 100, y: stopY },
|
|
259
|
+
data: { label: 'Stop Condition', condition: conv.stopCondition || 'max rounds reached', maxRounds: conv.maxRounds },
|
|
260
|
+
draggable: true,
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
// All agents → stop
|
|
264
|
+
conv.agents.forEach((a, i) => {
|
|
265
|
+
edges.push({
|
|
266
|
+
id: `agent-${a.id}-stop`,
|
|
267
|
+
source: `agent-${a.id}`,
|
|
268
|
+
target: 'stop',
|
|
269
|
+
markerEnd: { type: MarkerType.ArrowClosed, color: '#ef4444' },
|
|
270
|
+
style: { stroke: '#ef4444', strokeWidth: 1, opacity: 0.4 },
|
|
271
|
+
label: 'DONE?',
|
|
272
|
+
labelStyle: { fill: '#666', fontSize: 7 },
|
|
273
|
+
type: 'smoothstep',
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
return { nodes, edges };
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// ─── Main Component ───────────────────────────────────────
|
|
281
|
+
|
|
282
|
+
export default function ConversationEditor({ initialYaml, onSave, onClose }: {
|
|
283
|
+
initialYaml: string;
|
|
284
|
+
onSave: (yaml: string) => void;
|
|
285
|
+
onClose: () => void;
|
|
286
|
+
}) {
|
|
287
|
+
const [yamlText, setYamlText] = useState(initialYaml);
|
|
288
|
+
const [error, setError] = useState('');
|
|
289
|
+
const [showYaml, setShowYaml] = useState(false);
|
|
290
|
+
const [nodes, setNodes, onNodesChange] = useNodesState<Node>([]);
|
|
291
|
+
const [edges, setEdges, onEdgesChange] = useEdgesState<Edge>([]);
|
|
292
|
+
|
|
293
|
+
const parsed = useMemo(() => parseConvYaml(yamlText), [yamlText]);
|
|
294
|
+
|
|
295
|
+
// Rebuild graph when YAML changes
|
|
296
|
+
useEffect(() => {
|
|
297
|
+
if (!parsed) { setNodes([]); setEdges([]); return; }
|
|
298
|
+
const graph = buildFlowGraph(parsed);
|
|
299
|
+
setNodes(graph.nodes);
|
|
300
|
+
setEdges(graph.edges);
|
|
301
|
+
}, [parsed, setNodes, setEdges]);
|
|
302
|
+
|
|
303
|
+
const validate = (text: string): string => {
|
|
304
|
+
try {
|
|
305
|
+
const p = YAML.parse(text);
|
|
306
|
+
if (!p.name) return 'Missing "name"';
|
|
307
|
+
if (p.type !== 'conversation') return 'type must be "conversation"';
|
|
308
|
+
if (!p.agents || !Array.isArray(p.agents) || p.agents.length < 2) return 'Need at least 2 agents';
|
|
309
|
+
for (const a of p.agents) {
|
|
310
|
+
if (!a.id) return 'Agent missing "id"';
|
|
311
|
+
if (!a.agent) return `Agent "${a.id}" missing "agent"`;
|
|
312
|
+
}
|
|
313
|
+
if (!p.initial_prompt && !p.initialPrompt) return 'Missing "initial_prompt"';
|
|
314
|
+
return '';
|
|
315
|
+
} catch (e: any) {
|
|
316
|
+
return `YAML error: ${e.message}`;
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
const handleSave = () => {
|
|
321
|
+
const err = validate(yamlText);
|
|
322
|
+
if (err) { setError(err); return; }
|
|
323
|
+
onSave(yamlText);
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
return (
|
|
327
|
+
<div className="flex-1 flex flex-col min-h-0" style={{ background: '#0a0a1a' }}>
|
|
328
|
+
{/* Top bar */}
|
|
329
|
+
<div className="h-10 border-b border-[#3a3a5a] flex items-center px-4 gap-3 shrink-0">
|
|
330
|
+
<span className="text-xs font-bold text-white">Conversation Editor</span>
|
|
331
|
+
{parsed && <span className="text-[10px] text-gray-400 font-mono">{parsed.name}</span>}
|
|
332
|
+
{parsed && (
|
|
333
|
+
<span className="text-[8px] px-1.5 py-0.5 rounded bg-purple-500/20 text-purple-400">
|
|
334
|
+
{parsed.agents.length} agents · {parsed.maxRounds} rounds
|
|
335
|
+
</span>
|
|
336
|
+
)}
|
|
337
|
+
<div className="flex-1" />
|
|
338
|
+
{error && <span className="text-[9px] text-red-400 truncate max-w-[250px]">{error}</span>}
|
|
339
|
+
<button
|
|
340
|
+
onClick={() => setShowYaml(v => !v)}
|
|
341
|
+
className={`text-[10px] px-2 py-0.5 rounded border ${showYaml ? 'border-[#7c5bf0] text-[#7c5bf0]' : 'border-[#3a3a5a] text-gray-400'} hover:text-white`}
|
|
342
|
+
>{showYaml ? 'Graph' : 'YAML'}</button>
|
|
343
|
+
<button onClick={handleSave} className="text-xs px-3 py-1 bg-green-600 text-white rounded hover:opacity-90">Save</button>
|
|
344
|
+
<button
|
|
345
|
+
onClick={() => { if (!yamlText || yamlText === initialYaml || confirm('Discard changes?')) onClose(); }}
|
|
346
|
+
className="text-xs px-3 py-1 text-gray-400 hover:text-white"
|
|
347
|
+
>Close</button>
|
|
348
|
+
</div>
|
|
349
|
+
|
|
350
|
+
{/* Content */}
|
|
351
|
+
{showYaml ? (
|
|
352
|
+
<textarea
|
|
353
|
+
value={yamlText}
|
|
354
|
+
onChange={e => { setYamlText(e.target.value); setError(''); }}
|
|
355
|
+
className="flex-1 p-4 text-xs font-mono bg-[#0a0a1a] text-gray-300 resize-none focus:outline-none leading-relaxed"
|
|
356
|
+
spellCheck={false}
|
|
357
|
+
/>
|
|
358
|
+
) : (
|
|
359
|
+
<div className="flex-1 relative">
|
|
360
|
+
{parsed ? (
|
|
361
|
+
<ReactFlow
|
|
362
|
+
nodes={nodes}
|
|
363
|
+
edges={edges}
|
|
364
|
+
onNodesChange={onNodesChange}
|
|
365
|
+
onEdgesChange={onEdgesChange}
|
|
366
|
+
nodeTypes={nodeTypes}
|
|
367
|
+
fitView
|
|
368
|
+
fitViewOptions={{ padding: 0.3 }}
|
|
369
|
+
nodesConnectable={false}
|
|
370
|
+
style={{ background: '#0a0a1a' }}
|
|
371
|
+
minZoom={0.3}
|
|
372
|
+
maxZoom={2}
|
|
373
|
+
>
|
|
374
|
+
<Background color="#1a1a3a" gap={20} />
|
|
375
|
+
<Controls />
|
|
376
|
+
</ReactFlow>
|
|
377
|
+
) : (
|
|
378
|
+
<div className="flex-1 flex items-center justify-center h-full">
|
|
379
|
+
<div className="text-center space-y-2">
|
|
380
|
+
<div className="text-sm text-gray-500">Invalid or empty conversation YAML</div>
|
|
381
|
+
<button
|
|
382
|
+
onClick={() => setShowYaml(true)}
|
|
383
|
+
className="text-xs px-3 py-1 bg-[#7c5bf0] text-white rounded hover:opacity-90"
|
|
384
|
+
>Edit YAML</button>
|
|
385
|
+
</div>
|
|
386
|
+
</div>
|
|
387
|
+
)}
|
|
388
|
+
|
|
389
|
+
{/* Floating legend */}
|
|
390
|
+
{parsed && (
|
|
391
|
+
<div className="absolute bottom-4 left-4 bg-[#0a0a1a]/90 border border-[#3a3a5a] rounded-lg p-3 space-y-1.5 backdrop-blur-sm">
|
|
392
|
+
<div className="text-[8px] font-bold text-gray-400 uppercase">Legend</div>
|
|
393
|
+
<div className="flex items-center gap-2 text-[8px] text-gray-400">
|
|
394
|
+
<span className="w-3 h-0.5 bg-green-500 inline-block" /> Initial prompt
|
|
395
|
+
</div>
|
|
396
|
+
<div className="flex items-center gap-2 text-[8px] text-gray-400">
|
|
397
|
+
<span className="w-3 h-0.5 bg-[#7c5bf0] inline-block" style={{ borderBottom: '2px dashed #7c5bf0' }} /> Context flow
|
|
398
|
+
</div>
|
|
399
|
+
<div className="flex items-center gap-2 text-[8px] text-gray-400">
|
|
400
|
+
<span className="w-3 h-0.5 bg-orange-500 inline-block" style={{ borderBottom: '2px dashed #d09060' }} /> Next round loop
|
|
401
|
+
</div>
|
|
402
|
+
<div className="flex items-center gap-2 text-[8px] text-gray-400">
|
|
403
|
+
<span className="w-3 h-0.5 bg-red-500/40 inline-block" /> Stop check
|
|
404
|
+
</div>
|
|
405
|
+
</div>
|
|
406
|
+
)}
|
|
407
|
+
</div>
|
|
408
|
+
)}
|
|
409
|
+
</div>
|
|
410
|
+
);
|
|
411
|
+
}
|