agent-state-machine 2.0.15 → 2.1.1
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/bin/cli.js +1 -1
- package/lib/index.js +33 -0
- package/lib/remote/client.js +7 -2
- package/lib/runtime/agent.js +102 -67
- package/lib/runtime/index.js +13 -0
- package/lib/runtime/interaction.js +304 -0
- package/lib/runtime/prompt.js +39 -12
- package/lib/runtime/runtime.js +11 -10
- package/package.json +1 -1
- package/templates/project-builder/agents/assumptions-clarifier.md +0 -1
- package/templates/project-builder/agents/code-reviewer.md +0 -1
- package/templates/project-builder/agents/code-writer.md +0 -1
- package/templates/project-builder/agents/requirements-clarifier.md +0 -1
- package/templates/project-builder/agents/response-interpreter.md +25 -0
- package/templates/project-builder/agents/roadmap-generator.md +0 -1
- package/templates/project-builder/agents/sanity-checker.md +45 -0
- package/templates/project-builder/agents/sanity-runner.js +161 -0
- package/templates/project-builder/agents/scope-clarifier.md +0 -1
- package/templates/project-builder/agents/security-clarifier.md +0 -1
- package/templates/project-builder/agents/security-reviewer.md +0 -1
- package/templates/project-builder/agents/task-planner.md +0 -1
- package/templates/project-builder/agents/test-planner.md +0 -1
- package/templates/project-builder/scripts/interaction-helpers.js +33 -0
- package/templates/project-builder/scripts/workflow-helpers.js +2 -47
- package/templates/project-builder/workflow.js +214 -54
- package/vercel-server/api/session/[token].js +3 -3
- package/vercel-server/api/submit/[token].js +5 -3
- package/vercel-server/local-server.js +33 -6
- package/vercel-server/public/remote/index.html +17 -0
- package/vercel-server/ui/index.html +9 -1012
- package/vercel-server/ui/package-lock.json +2650 -0
- package/vercel-server/ui/package.json +25 -0
- package/vercel-server/ui/postcss.config.js +6 -0
- package/vercel-server/ui/src/App.jsx +236 -0
- package/vercel-server/ui/src/components/ChoiceInteraction.jsx +127 -0
- package/vercel-server/ui/src/components/ConfirmInteraction.jsx +51 -0
- package/vercel-server/ui/src/components/ContentCard.jsx +161 -0
- package/vercel-server/ui/src/components/CopyButton.jsx +27 -0
- package/vercel-server/ui/src/components/EventsLog.jsx +82 -0
- package/vercel-server/ui/src/components/Footer.jsx +66 -0
- package/vercel-server/ui/src/components/Header.jsx +38 -0
- package/vercel-server/ui/src/components/InteractionForm.jsx +42 -0
- package/vercel-server/ui/src/components/TextInteraction.jsx +72 -0
- package/vercel-server/ui/src/index.css +145 -0
- package/vercel-server/ui/src/main.jsx +8 -0
- package/vercel-server/ui/tailwind.config.js +19 -0
- package/vercel-server/ui/vite.config.js +11 -0
|
@@ -1,1019 +1,16 @@
|
|
|
1
1
|
<!DOCTYPE html>
|
|
2
2
|
<html lang="en">
|
|
3
|
-
|
|
4
|
-
<head>
|
|
3
|
+
<head>
|
|
5
4
|
<meta charset="UTF-8" />
|
|
6
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
|
-
<title>{{WORKFLOW_NAME}}
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
<script>
|
|
11
|
-
tailwind.config = { darkMode: "class" };
|
|
12
|
-
</script>
|
|
13
|
-
|
|
14
|
-
<script src="https://unpkg.com/react@18/umd/react.development.js" crossorigin></script>
|
|
15
|
-
<script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js" crossorigin></script>
|
|
16
|
-
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
|
|
17
|
-
|
|
18
|
-
<style>
|
|
19
|
-
:root {
|
|
20
|
-
--bg: #ffffff;
|
|
21
|
-
--fg: #000000;
|
|
22
|
-
--muted: rgba(0, 0, 0, 0.6);
|
|
23
|
-
--hairline: rgba(0, 0, 0, 0.12);
|
|
24
|
-
--hairline-strong: rgba(0, 0, 0, 0.2);
|
|
25
|
-
--focus: rgba(0, 0, 0, 0.35);
|
|
26
|
-
--chip: rgba(0, 0, 0, 0.06);
|
|
27
|
-
--chip-strong: rgba(0, 0, 0, 0.1);
|
|
28
|
-
--danger: rgba(0, 0, 0, 0.85);
|
|
29
|
-
--ok: rgba(0, 0, 0, 0.85);
|
|
30
|
-
--warn: rgba(0, 0, 0, 0.85);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
.dark {
|
|
34
|
-
--bg: #000000;
|
|
35
|
-
--fg: #ffffff;
|
|
36
|
-
--muted: rgba(255, 255, 255, 0.65);
|
|
37
|
-
--hairline: rgba(255, 255, 255, 0.14);
|
|
38
|
-
--hairline-strong: rgba(255, 255, 255, 0.22);
|
|
39
|
-
--focus: rgba(255, 255, 255, 0.35);
|
|
40
|
-
--chip: rgba(255, 255, 255, 0.08);
|
|
41
|
-
--chip-strong: rgba(255, 255, 255, 0.14);
|
|
42
|
-
--danger: rgba(255, 255, 255, 0.92);
|
|
43
|
-
--ok: rgba(255, 255, 255, 0.92);
|
|
44
|
-
--warn: rgba(255, 255, 255, 0.92);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
html,
|
|
48
|
-
body {
|
|
49
|
-
height: 100%;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
body {
|
|
53
|
-
background: var(--bg);
|
|
54
|
-
color: var(--fg);
|
|
55
|
-
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
|
56
|
-
letter-spacing: 0.01em;
|
|
57
|
-
margin: 0;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
::-webkit-scrollbar {
|
|
61
|
-
width: 10px;
|
|
62
|
-
height: 10px;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
::-webkit-scrollbar-track {
|
|
66
|
-
background: transparent;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
::-webkit-scrollbar-thumb {
|
|
70
|
-
background: var(--chip-strong);
|
|
71
|
-
border-radius: 999px;
|
|
72
|
-
border: 2px solid transparent;
|
|
73
|
-
background-clip: padding-box;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
::-webkit-scrollbar-thumb:hover {
|
|
77
|
-
background: var(--hairline-strong);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
.hairline {
|
|
81
|
-
border: 1px solid var(--hairline);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
.hairline-strong {
|
|
85
|
-
border: 1px solid var(--hairline-strong);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
.divider {
|
|
89
|
-
border-top: 1px solid var(--hairline);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
.kbd {
|
|
93
|
-
display: inline-flex;
|
|
94
|
-
align-items: center;
|
|
95
|
-
padding: 2px 6px;
|
|
96
|
-
border: 1px solid var(--hairline);
|
|
97
|
-
border-bottom-color: var(--hairline-strong);
|
|
98
|
-
border-radius: 8px;
|
|
99
|
-
background: transparent;
|
|
100
|
-
color: var(--muted);
|
|
101
|
-
font-size: 11px;
|
|
102
|
-
line-height: 1;
|
|
103
|
-
user-select: none;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
.markdown-body {
|
|
107
|
-
white-space: pre-wrap;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
.agent-prompt summary {
|
|
111
|
-
list-style: none;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
.agent-prompt summary::-webkit-details-marker {
|
|
115
|
-
display: none;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
@keyframes blink {
|
|
119
|
-
|
|
120
|
-
0%,
|
|
121
|
-
100% {
|
|
122
|
-
opacity: 1;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
50% {
|
|
126
|
-
opacity: 0.4;
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
.blink {
|
|
131
|
-
animation: blink 1.1s steps(2, end) infinite;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
.glow-dot {
|
|
135
|
-
background: var(--fg);
|
|
136
|
-
box-shadow: 0 0 0 2px var(--chip), 0 0 10px var(--fg);
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
button,
|
|
140
|
-
textarea {
|
|
141
|
-
-webkit-tap-highlight-color: transparent;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
button:focus-visible,
|
|
145
|
-
textarea:focus-visible {
|
|
146
|
-
outline: 2px solid var(--focus);
|
|
147
|
-
outline-offset: 2px;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
/* === ONE COLUMN, "INPUT" LEFT / "OUTPUT" RIGHT, ALL LTR === */
|
|
151
|
-
.edge-wrap {
|
|
152
|
-
width: 100%;
|
|
153
|
-
padding-left: 12px;
|
|
154
|
-
padding-right: 12px;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
@media (min-width: 640px) {
|
|
158
|
-
.edge-wrap {
|
|
159
|
-
padding-left: 16px;
|
|
160
|
-
padding-right: 16px;
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
.io-row {
|
|
165
|
-
display: flex;
|
|
166
|
-
width: 100%;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
.io-left {
|
|
170
|
-
justify-content: flex-start;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
.io-right {
|
|
174
|
-
justify-content: flex-end;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
.io-center {
|
|
178
|
-
justify-content: center;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
.io-card {
|
|
182
|
-
width: min(880px, 100%);
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
.io-in {
|
|
186
|
-
text-align: left;
|
|
187
|
-
direction: ltr;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
.io-out {
|
|
191
|
-
text-align: right;
|
|
192
|
-
direction: ltr;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
.rtl-safe {
|
|
196
|
-
direction: ltr;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
/* centered chrome (header/footer/empty states) */
|
|
200
|
-
.center-wrap {
|
|
201
|
-
width: min(880px, 100%);
|
|
202
|
-
margin-left: auto;
|
|
203
|
-
margin-right: auto;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
/* NEW: center the “meta rows” inside their cards */
|
|
207
|
-
.meta-center {
|
|
208
|
-
text-align: center;
|
|
209
|
-
}
|
|
210
|
-
</style>
|
|
211
|
-
</head>
|
|
212
|
-
|
|
213
|
-
<body>
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
|
6
|
+
<title>{{WORKFLOW_NAME}}</title>
|
|
7
|
+
</head>
|
|
8
|
+
<body>
|
|
214
9
|
<div id="root"></div>
|
|
215
|
-
|
|
216
10
|
<script>
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
</script>
|
|
220
|
-
|
|
221
|
-
<script type="text/babel">
|
|
222
|
-
const { useEffect, useMemo, useRef, useState } = React;
|
|
223
|
-
|
|
224
|
-
const Icon = ({ name }) => {
|
|
225
|
-
const common = "w-4 h-4";
|
|
226
|
-
if (name === "sun") {
|
|
227
|
-
return (
|
|
228
|
-
<svg className={common} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
229
|
-
<path strokeLinecap="round" d="M12 3v2M12 19v2M4 12H2M22 12h-2M5.6 5.6l1.4 1.4M17 17l1.4 1.4M18.4 5.6 17 7M7 17l-1.4 1.4" />
|
|
230
|
-
<circle cx="12" cy="12" r="4" />
|
|
231
|
-
</svg>
|
|
232
|
-
);
|
|
233
|
-
}
|
|
234
|
-
if (name === "moon") {
|
|
235
|
-
return (
|
|
236
|
-
<svg className={common} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
237
|
-
<path strokeLinecap="round" strokeLinejoin="round" d="M21 13.2A8.5 8.5 0 0 1 10.8 3 7.5 7.5 0 1 0 21 13.2z" />
|
|
238
|
-
</svg>
|
|
239
|
-
);
|
|
240
|
-
}
|
|
241
|
-
if (name === "copy") {
|
|
242
|
-
return (
|
|
243
|
-
<svg className={common} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
244
|
-
<rect x="9" y="9" width="13" height="13" rx="2" />
|
|
245
|
-
<path strokeLinecap="round" d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
|
|
246
|
-
</svg>
|
|
247
|
-
);
|
|
248
|
-
}
|
|
249
|
-
if (name === "check") {
|
|
250
|
-
return (
|
|
251
|
-
<svg className={common} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
252
|
-
<path strokeLinecap="round" strokeLinejoin="round" d="M20 6 9 17l-5-5" />
|
|
253
|
-
</svg>
|
|
254
|
-
);
|
|
255
|
-
}
|
|
256
|
-
return null;
|
|
257
|
-
};
|
|
258
|
-
|
|
259
|
-
function StatusBadge({ status }) {
|
|
260
|
-
const labels = { connected: "Live", disconnected: "Offline", connecting: "Connecting..." };
|
|
261
|
-
const dotClass =
|
|
262
|
-
status === "connecting" ? "blink" : status === "connected" ? "blink glow-dot" : "";
|
|
263
|
-
return (
|
|
264
|
-
<div className="flex items-center gap-2">
|
|
265
|
-
<div className={`w-2 h-2 rounded-full ${dotClass}`} />
|
|
266
|
-
<span className="text-[10px] uppercase tracking-wider font-bold" style={{ color: "var(--muted)" }}>
|
|
267
|
-
{labels[status] || status}
|
|
268
|
-
</span>
|
|
269
|
-
</div>
|
|
270
|
-
);
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
function CopyButton({ text, className = "" }) {
|
|
274
|
-
const [copied, setCopied] = useState(false);
|
|
275
|
-
const handleCopy = async () => {
|
|
276
|
-
const content = typeof text === "object" ? JSON.stringify(text, null, 2) : String(text);
|
|
277
|
-
try {
|
|
278
|
-
await navigator.clipboard.writeText(content);
|
|
279
|
-
setCopied(true);
|
|
280
|
-
setTimeout(() => setCopied(false), 1500);
|
|
281
|
-
} catch (e) {
|
|
282
|
-
const ta = document.createElement("textarea");
|
|
283
|
-
ta.value = content;
|
|
284
|
-
document.body.appendChild(ta);
|
|
285
|
-
ta.select();
|
|
286
|
-
document.execCommand("copy");
|
|
287
|
-
document.body.removeChild(ta);
|
|
288
|
-
setCopied(true);
|
|
289
|
-
setTimeout(() => setCopied(false), 1500);
|
|
290
|
-
}
|
|
291
|
-
};
|
|
292
|
-
return (
|
|
293
|
-
<button
|
|
294
|
-
onClick={handleCopy}
|
|
295
|
-
className={`inline-flex items-center gap-2 px-3 py-2 rounded-xl hairline hover:hairline-strong transition ${className}`}
|
|
296
|
-
title="Copy"
|
|
297
|
-
type="button"
|
|
298
|
-
style={{ color: "var(--fg)", background: "transparent" }}
|
|
299
|
-
>
|
|
300
|
-
{copied ? <Icon name="check" /> : <Icon name="copy" />}
|
|
301
|
-
<span className="text-[11px] tracking-[0.14em] font-semibold">{copied ? "COPIED" : "COPY"}</span>
|
|
302
|
-
</button>
|
|
303
|
-
);
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
function Toggle({ onClick, label, title, children }) {
|
|
307
|
-
return (
|
|
308
|
-
<button
|
|
309
|
-
onClick={onClick}
|
|
310
|
-
className="inline-flex items-center gap-2 px-3 py-2 rounded-xl hairline hover:hairline-strong transition"
|
|
311
|
-
title={title}
|
|
312
|
-
type="button"
|
|
313
|
-
style={{ background: "transparent", color: "var(--fg)" }}
|
|
314
|
-
>
|
|
315
|
-
{children}
|
|
316
|
-
<span className="text-[11px] tracking-[0.14em] font-semibold">{label}</span>
|
|
317
|
-
</button>
|
|
318
|
-
);
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
function JsonView({ data, label, onTop = false, timestamp, align = "left" }) {
|
|
322
|
-
const [viewMode, setViewMode] = useState("clean"); // clean | raw
|
|
323
|
-
const isObject = typeof data === "object" && data !== null;
|
|
324
|
-
const rawContent = isObject ? JSON.stringify(data, null, 2) : String(data);
|
|
325
|
-
const hasToggle = isObject || rawContent.includes("\\n");
|
|
326
|
-
|
|
327
|
-
const cleanParts = useMemo(() => {
|
|
328
|
-
if (!isObject) return null;
|
|
329
|
-
const keys = Object.keys(data || {});
|
|
330
|
-
if (!keys.length) return null;
|
|
331
|
-
return keys.map((k) => {
|
|
332
|
-
const val = data[k];
|
|
333
|
-
const renderedVal = typeof val === "string" ? val : JSON.stringify(val, null, 2);
|
|
334
|
-
return (
|
|
335
|
-
<div key={k} className="py-4">
|
|
336
|
-
<div className="text-[11px] tracking-[0.18em] font-semibold mb-2" style={{ color: "var(--muted)" }}>
|
|
337
|
-
{k.toUpperCase()}
|
|
338
|
-
</div>
|
|
339
|
-
<div className="markdown-body text-[13px] leading-relaxed">{String(renderedVal)}</div>
|
|
340
|
-
</div>
|
|
341
|
-
);
|
|
342
|
-
});
|
|
343
|
-
}, [data, isObject]);
|
|
344
|
-
|
|
345
|
-
const ioClass = align === "right" ? "io-out" : "io-in";
|
|
346
|
-
|
|
347
|
-
return (
|
|
348
|
-
<div className={`hairline rounded-2xl ${onTop ? "rounded-tr-none" : ""} overflow-hidden ${ioClass}`} style={{ background: "transparent" }}>
|
|
349
|
-
<div className="rtl-safe flex items-center justify-between px-5 py-4 divider">
|
|
350
|
-
<div className="flex items-center gap-3 min-w-0">
|
|
351
|
-
<div className="text-[11px] tracking-[0.22em] font-semibold truncate" style={{ color: "var(--muted)" }}>
|
|
352
|
-
{label}
|
|
353
|
-
</div>
|
|
354
|
-
{timestamp && (
|
|
355
|
-
<span className="text-[11px] tracking-[0.12em]" style={{ color: "var(--muted)" }}>
|
|
356
|
-
{timestamp}
|
|
357
|
-
</span>
|
|
358
|
-
)}
|
|
359
|
-
</div>
|
|
360
|
-
|
|
361
|
-
<div className="flex items-center gap-2">
|
|
362
|
-
{hasToggle && (
|
|
363
|
-
<button
|
|
364
|
-
onClick={() => setViewMode((v) => (v === "clean" ? "raw" : "clean"))}
|
|
365
|
-
className="px-3 py-2 rounded-xl hairline hover:hairline-strong transition"
|
|
366
|
-
type="button"
|
|
367
|
-
title={viewMode === "clean" ? "Switch to raw" : "Switch to clean"}
|
|
368
|
-
style={{ background: "transparent", color: "var(--fg)" }}
|
|
369
|
-
>
|
|
370
|
-
<span className="text-[11px] tracking-[0.14em] font-semibold">
|
|
371
|
-
{viewMode === "clean" ? "RAW" : "CLEAN"}
|
|
372
|
-
</span>
|
|
373
|
-
</button>
|
|
374
|
-
)}
|
|
375
|
-
<CopyButton text={data} />
|
|
376
|
-
</div>
|
|
377
|
-
</div>
|
|
378
|
-
|
|
379
|
-
<div className="px-5 py-4">
|
|
380
|
-
<div className="markdown-body text-[13px] leading-relaxed overflow-x-auto">
|
|
381
|
-
{viewMode === "clean" ? (cleanParts ?? rawContent) : rawContent}
|
|
382
|
-
</div>
|
|
383
|
-
</div>
|
|
384
|
-
</div>
|
|
385
|
-
);
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
function InteractionForm({ interaction, onSubmit, disabled }) {
|
|
389
|
-
const [response, setResponse] = useState("");
|
|
390
|
-
const [submitting, setSubmitting] = useState(false);
|
|
391
|
-
const textareaRef = useRef(null);
|
|
392
|
-
|
|
393
|
-
useEffect(() => {
|
|
394
|
-
setResponse(interaction.question || "");
|
|
395
|
-
}, [interaction.slug, interaction.question]);
|
|
396
|
-
|
|
397
|
-
useEffect(() => {
|
|
398
|
-
const el = textareaRef.current;
|
|
399
|
-
if (!el) return;
|
|
400
|
-
el.style.height = "auto";
|
|
401
|
-
el.style.height = `${el.scrollHeight}px`;
|
|
402
|
-
}, [response]);
|
|
403
|
-
|
|
404
|
-
const handleSubmit = async (e) => {
|
|
405
|
-
e.preventDefault();
|
|
406
|
-
if (!response.trim() || submitting) return;
|
|
407
|
-
setSubmitting(true);
|
|
408
|
-
try {
|
|
409
|
-
await onSubmit(interaction.slug, interaction.targetKey, response.trim());
|
|
410
|
-
setResponse("");
|
|
411
|
-
} finally {
|
|
412
|
-
setSubmitting(false);
|
|
413
|
-
}
|
|
414
|
-
};
|
|
415
|
-
|
|
416
|
-
return (
|
|
417
|
-
<section className="hairline rounded-2xl overflow-hidden io-in">
|
|
418
|
-
<div className="rtl-safe px-6 py-5 divider flex items-center justify-between">
|
|
419
|
-
<div className="min-w-0">
|
|
420
|
-
<div className="text-[11px] tracking-[0.22em] font-semibold" style={{ color: "var(--muted)" }}>
|
|
421
|
-
INPUT REQUIRED
|
|
422
|
-
</div>
|
|
423
|
-
<div className="mt-2 text-[12px] tracking-[0.16em] uppercase" style={{ color: "var(--muted)" }}>
|
|
424
|
-
Safe to delete all text and type your answer.
|
|
425
|
-
</div>
|
|
426
|
-
</div>
|
|
427
|
-
<div className="flex items-center gap-2">
|
|
428
|
-
{disabled && <span className="kbd" title="CLI offline">OFFLINE</span>}
|
|
429
|
-
</div>
|
|
430
|
-
</div>
|
|
431
|
-
|
|
432
|
-
<form onSubmit={handleSubmit} className="px-6 py-5">
|
|
433
|
-
<textarea
|
|
434
|
-
ref={textareaRef}
|
|
435
|
-
value={response}
|
|
436
|
-
onChange={(e) => setResponse(e.target.value)}
|
|
437
|
-
disabled={disabled || submitting}
|
|
438
|
-
className="w-full rounded-2xl hairline p-4 text-[13px] leading-relaxed min-h-[120px]"
|
|
439
|
-
style={{
|
|
440
|
-
background: "transparent",
|
|
441
|
-
color: "var(--fg)",
|
|
442
|
-
borderColor: "var(--hairline)",
|
|
443
|
-
outline: "none",
|
|
444
|
-
resize: "none",
|
|
445
|
-
direction: "ltr",
|
|
446
|
-
textAlign: "left",
|
|
447
|
-
}}
|
|
448
|
-
/>
|
|
449
|
-
<div className="mt-4 flex items-center justify-between gap-3 rtl-safe">
|
|
450
|
-
<div className="text-[11px] tracking-[0.14em]" style={{ color: "var(--muted)" }}>
|
|
451
|
-
{submitting ? "SENDING…" : " "}
|
|
452
|
-
</div>
|
|
453
|
-
<button
|
|
454
|
-
type="submit"
|
|
455
|
-
disabled={disabled || submitting || !response.trim()}
|
|
456
|
-
className="px-4 py-2 rounded-xl hairline hover:hairline-strong transition disabled:opacity-40 disabled:cursor-not-allowed"
|
|
457
|
-
style={{ background: "transparent", color: "var(--fg)" }}
|
|
458
|
-
>
|
|
459
|
-
<span className="text-[11px] tracking-[0.18em] font-semibold">SUBMIT</span>
|
|
460
|
-
</button>
|
|
461
|
-
</div>
|
|
462
|
-
</form>
|
|
463
|
-
</section>
|
|
464
|
-
);
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
function App() {
|
|
468
|
-
const [history, setHistory] = useState([]);
|
|
469
|
-
const [loading, setLoading] = useState(true);
|
|
470
|
-
const [error, setError] = useState(null);
|
|
471
|
-
const [status, setStatus] = useState("connecting");
|
|
472
|
-
const [workflowName, setWorkflowName] = useState(window.WORKFLOW_NAME_TEMPLATE || "");
|
|
473
|
-
const [theme, setTheme] = useState(() => {
|
|
474
|
-
const saved = localStorage.getItem("rf_theme");
|
|
475
|
-
if (saved === "light" || saved === "dark") return saved;
|
|
476
|
-
return window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
|
477
|
-
});
|
|
478
|
-
const [pendingInteraction, setPendingInteraction] = useState(null);
|
|
479
|
-
|
|
480
|
-
const token =
|
|
481
|
-
window.SESSION_TOKEN && window.SESSION_TOKEN !== "{{" + "SESSION_TOKEN" + "}}" ? window.SESSION_TOKEN : null;
|
|
482
|
-
|
|
483
|
-
const historyUrl = token ? `/api/history/${token}` : "/api/history";
|
|
484
|
-
const eventsUrl = token ? `/api/events/${token}` : "/api/events";
|
|
485
|
-
const submitUrl = token ? `/api/submit/${token}` : "/api/submit";
|
|
486
|
-
|
|
487
|
-
useEffect(() => localStorage.setItem("rf_theme", theme), [theme]);
|
|
488
|
-
|
|
489
|
-
// Helper to check if workflow is currently running based on history
|
|
490
|
-
const isWorkflowRunning = (entries) => {
|
|
491
|
-
// Find the most recent workflow lifecycle event (history is newest-first)
|
|
492
|
-
for (const entry of entries) {
|
|
493
|
-
if (entry.event === "WORKFLOW_STARTED") return true;
|
|
494
|
-
if (entry.event === "WORKFLOW_STOPPED" ||
|
|
495
|
-
entry.event === "WORKFLOW_COMPLETED" ||
|
|
496
|
-
entry.event === "WORKFLOW_FAILED") return false;
|
|
497
|
-
}
|
|
498
|
-
return false; // No lifecycle events found
|
|
499
|
-
};
|
|
500
|
-
|
|
501
|
-
useEffect(() => {
|
|
502
|
-
if (history.length === 0) { setPendingInteraction(null); return; }
|
|
503
|
-
|
|
504
|
-
const resolvedSlugs = new Set();
|
|
505
|
-
let pending = null;
|
|
506
|
-
const workflowRunning = isWorkflowRunning(history);
|
|
507
|
-
|
|
508
|
-
for (const entry of history) {
|
|
509
|
-
const isResolution =
|
|
510
|
-
entry.event === "INTERACTION_RESOLVED" ||
|
|
511
|
-
entry.event === "PROMPT_ANSWERED" ||
|
|
512
|
-
entry.event === "INTERACTION_SUBMITTED";
|
|
513
|
-
const isRequest = entry.event === "INTERACTION_REQUESTED" || entry.event === "PROMPT_REQUESTED";
|
|
514
|
-
|
|
515
|
-
if (isResolution && entry.slug) resolvedSlugs.add(entry.slug);
|
|
516
|
-
|
|
517
|
-
if (isRequest && entry.slug && !resolvedSlugs.has(entry.slug) && !pending) {
|
|
518
|
-
pending = {
|
|
519
|
-
slug: entry.slug,
|
|
520
|
-
targetKey: entry.targetKey || `_interaction_${entry.slug}`,
|
|
521
|
-
question: entry.question,
|
|
522
|
-
};
|
|
523
|
-
}
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
// Only show pending interaction if workflow is running
|
|
527
|
-
setPendingInteraction(workflowRunning ? pending : null);
|
|
528
|
-
}, [history]);
|
|
529
|
-
|
|
530
|
-
const fetchData = async () => {
|
|
531
|
-
try {
|
|
532
|
-
const res = await fetch(historyUrl);
|
|
533
|
-
const data = await res.json();
|
|
534
|
-
if (data.entries) setHistory(data.entries);
|
|
535
|
-
if (data.workflowName) setWorkflowName(data.workflowName);
|
|
536
|
-
|
|
537
|
-
// Check if workflow is currently running based on most recent lifecycle event
|
|
538
|
-
const workflowRunning = isWorkflowRunning(data.entries || []);
|
|
539
|
-
|
|
540
|
-
if (workflowRunning) {
|
|
541
|
-
setStatus("connected");
|
|
542
|
-
} else if (token && data.cliConnected !== undefined) {
|
|
543
|
-
setStatus(data.cliConnected ? "connected" : "disconnected");
|
|
544
|
-
} else if (!token) {
|
|
545
|
-
setStatus("connected");
|
|
546
|
-
} else {
|
|
547
|
-
setStatus("disconnected");
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
setLoading(false);
|
|
551
|
-
return true;
|
|
552
|
-
} catch (err) {
|
|
553
|
-
setError(err.message);
|
|
554
|
-
setLoading(false);
|
|
555
|
-
return false;
|
|
556
|
-
}
|
|
557
|
-
};
|
|
558
|
-
|
|
559
|
-
useEffect(() => {
|
|
560
|
-
fetchData();
|
|
561
|
-
|
|
562
|
-
let eventSource = null;
|
|
563
|
-
let reconnectTimeout = null;
|
|
564
|
-
let pollInterval = null;
|
|
565
|
-
let reconnectAttempts = 0;
|
|
566
|
-
|
|
567
|
-
const connect = () => {
|
|
568
|
-
if (eventSource) eventSource.close();
|
|
569
|
-
eventSource = new EventSource(eventsUrl);
|
|
570
|
-
|
|
571
|
-
eventSource.onopen = () => {
|
|
572
|
-
setStatus("connected");
|
|
573
|
-
reconnectAttempts = 0;
|
|
574
|
-
fetchData();
|
|
575
|
-
};
|
|
576
|
-
|
|
577
|
-
eventSource.onerror = () => {
|
|
578
|
-
setStatus("disconnected");
|
|
579
|
-
eventSource.close();
|
|
580
|
-
const delay = Math.min(1000 * Math.pow(2, reconnectAttempts), 10000);
|
|
581
|
-
reconnectAttempts++;
|
|
582
|
-
reconnectTimeout = setTimeout(connect, delay);
|
|
583
|
-
};
|
|
584
|
-
|
|
585
|
-
eventSource.onmessage = (e) => {
|
|
586
|
-
try {
|
|
587
|
-
if (e.data === "update") { fetchData(); return; }
|
|
588
|
-
const data = JSON.parse(e.data);
|
|
589
|
-
switch (data.type) {
|
|
590
|
-
case "status":
|
|
591
|
-
setStatus(data.cliConnected ? "connected" : "disconnected");
|
|
592
|
-
if (data.workflowName) setWorkflowName(data.workflowName);
|
|
593
|
-
break;
|
|
594
|
-
case "history":
|
|
595
|
-
setHistory(data.entries || []);
|
|
596
|
-
// Update status based on workflow lifecycle
|
|
597
|
-
setStatus(isWorkflowRunning(data.entries || []) ? "connected" : "disconnected");
|
|
598
|
-
break;
|
|
599
|
-
case "event":
|
|
600
|
-
setHistory((prev) => {
|
|
601
|
-
if (data.event === "INTERACTION_SUBMITTED" && data.slug) {
|
|
602
|
-
const hasDupe = prev.some((e) => e.event === "INTERACTION_SUBMITTED" && e.slug === data.slug);
|
|
603
|
-
if (hasDupe) return prev;
|
|
604
|
-
}
|
|
605
|
-
return [data, ...prev];
|
|
606
|
-
});
|
|
607
|
-
// Update status based on workflow lifecycle events
|
|
608
|
-
if (data.event === "WORKFLOW_STARTED") {
|
|
609
|
-
setStatus("connected");
|
|
610
|
-
} else if (data.event === "WORKFLOW_STOPPED" ||
|
|
611
|
-
data.event === "WORKFLOW_COMPLETED" ||
|
|
612
|
-
data.event === "WORKFLOW_FAILED") {
|
|
613
|
-
setStatus("disconnected");
|
|
614
|
-
}
|
|
615
|
-
break;
|
|
616
|
-
case "cli_connected":
|
|
617
|
-
case "cli_reconnected":
|
|
618
|
-
setStatus("connected");
|
|
619
|
-
break;
|
|
620
|
-
case "cli_disconnected":
|
|
621
|
-
setStatus("disconnected");
|
|
622
|
-
break;
|
|
623
|
-
}
|
|
624
|
-
} catch (err) { }
|
|
625
|
-
};
|
|
626
|
-
};
|
|
627
|
-
|
|
628
|
-
connect();
|
|
629
|
-
pollInterval = setInterval(fetchData, 10000);
|
|
630
|
-
|
|
631
|
-
return () => {
|
|
632
|
-
if (eventSource) eventSource.close();
|
|
633
|
-
if (reconnectTimeout) clearTimeout(reconnectTimeout);
|
|
634
|
-
if (pollInterval) clearInterval(pollInterval);
|
|
635
|
-
};
|
|
636
|
-
}, []);
|
|
637
|
-
|
|
638
|
-
const handleSubmit = async (slug, targetKey, response) => {
|
|
639
|
-
const optimisticEvent = {
|
|
640
|
-
timestamp: new Date().toISOString(),
|
|
641
|
-
event: "INTERACTION_SUBMITTED",
|
|
642
|
-
slug,
|
|
643
|
-
targetKey,
|
|
644
|
-
answer: response.substring(0, 200) + (response.length > 200 ? "..." : ""),
|
|
645
|
-
source: "remote",
|
|
646
|
-
};
|
|
647
|
-
setHistory((prev) => [optimisticEvent, ...prev]);
|
|
648
|
-
|
|
649
|
-
try {
|
|
650
|
-
const res = await fetch(submitUrl, {
|
|
651
|
-
method: "POST",
|
|
652
|
-
headers: { "Content-Type": "application/json" },
|
|
653
|
-
body: JSON.stringify({ slug, targetKey, response }),
|
|
654
|
-
});
|
|
655
|
-
|
|
656
|
-
if (!res.ok) {
|
|
657
|
-
setHistory((prev) => prev.filter((e) => e !== optimisticEvent));
|
|
658
|
-
const err = await res.json();
|
|
659
|
-
throw new Error(err.error || "Failed to submit");
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
setTimeout(fetchData, 1000);
|
|
663
|
-
} catch (err) {
|
|
664
|
-
setHistory((prev) => prev.filter((e) => e !== optimisticEvent));
|
|
665
|
-
alert(err.message);
|
|
666
|
-
}
|
|
667
|
-
};
|
|
668
|
-
|
|
669
|
-
const toggleTheme = () => setTheme((p) => (p === "dark" ? "light" : "dark"));
|
|
670
|
-
|
|
671
|
-
const formatTime = (ts) =>
|
|
672
|
-
new Date(ts).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" });
|
|
673
|
-
|
|
674
|
-
const visibleEvents = [...history].reverse();
|
|
675
|
-
|
|
676
|
-
const wrapIO = (side, children, key) => (
|
|
677
|
-
<div key={key} className={`io-row ${side === "right" ? "io-right" : side === "center" ? "io-center" : "io-left"}`}>
|
|
678
|
-
<div className="io-card">{children}</div>
|
|
679
|
-
</div>
|
|
680
|
-
);
|
|
681
|
-
|
|
682
|
-
const renderPromptInputCard = (prompt, key) =>
|
|
683
|
-
wrapIO(
|
|
684
|
-
"left",
|
|
685
|
-
<section className="hairline rounded-2xl rounded-tl-none overflow-hidden io-in">
|
|
686
|
-
<div className="rtl-safe px-5 py-4 divider flex items-center justify-between">
|
|
687
|
-
<div className="text-[11px] tracking-[0.22em] font-semibold" style={{ color: "var(--muted)" }}>
|
|
688
|
-
PROMPT / INPUT
|
|
689
|
-
</div>
|
|
690
|
-
<CopyButton text={prompt} />
|
|
691
|
-
</div>
|
|
692
|
-
<div className="px-5 py-4">
|
|
693
|
-
<div className="markdown-body text-[13px] leading-relaxed overflow-x-auto">{prompt}</div>
|
|
694
|
-
</div>
|
|
695
|
-
</section>,
|
|
696
|
-
key
|
|
697
|
-
);
|
|
698
|
-
|
|
699
|
-
const renderEvent = (item, idx) => {
|
|
700
|
-
const time = formatTime(item.timestamp);
|
|
701
|
-
|
|
702
|
-
if (item.event && item.event.startsWith("WORKFLOW_")) {
|
|
703
|
-
return wrapIO(
|
|
704
|
-
"center",
|
|
705
|
-
<section className="text-center">
|
|
706
|
-
<div className="py-4">
|
|
707
|
-
<div className="text-[11px] tracking-[0.24em] font-semibold" style={{ color: "var(--muted)" }}>
|
|
708
|
-
{item.event.replace("WORKFLOW_", "")} • {time}
|
|
709
|
-
</div>
|
|
710
|
-
{item.error && <div className="mt-3 text-[13px] leading-relaxed markdown-body">{item.error}</div>}
|
|
711
|
-
</div>
|
|
712
|
-
</section>,
|
|
713
|
-
idx
|
|
714
|
-
);
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
// CENTERED inside card
|
|
718
|
-
if (item.event === "AGENT_STARTED") {
|
|
719
|
-
if (item.prompt) {
|
|
720
|
-
return wrapIO(
|
|
721
|
-
"left",
|
|
722
|
-
<section className="hairline rounded-2xl rounded-tl-none overflow-hidden io-in">
|
|
723
|
-
<details className="agent-prompt">
|
|
724
|
-
<summary className="rtl-safe px-5 py-4 divider cursor-pointer">
|
|
725
|
-
<div className="flex items-center justify-between gap-4">
|
|
726
|
-
<div>
|
|
727
|
-
<div className="text-[11px] tracking-[0.22em] font-semibold" style={{ color: "var(--muted)" }}>
|
|
728
|
-
AGENT STARTED
|
|
729
|
-
</div>
|
|
730
|
-
<div className="mt-1 text-[11px] tracking-[0.14em]" style={{ color: "var(--muted)" }}>
|
|
731
|
-
{(item.agent || "").toString()} • {time}
|
|
732
|
-
</div>
|
|
733
|
-
</div>
|
|
734
|
-
<CopyButton text={item.prompt} />
|
|
735
|
-
</div>
|
|
736
|
-
<div className="mt-3 text-[11px] tracking-[0.18em] font-semibold uppercase" style={{ color: "var(--muted)" }}>
|
|
737
|
-
Show Prompt ▼
|
|
738
|
-
</div>
|
|
739
|
-
</summary>
|
|
740
|
-
<div className="px-5 py-4">
|
|
741
|
-
<div className="markdown-body text-[13px] leading-relaxed overflow-x-auto">{item.prompt}</div>
|
|
742
|
-
</div>
|
|
743
|
-
</details>
|
|
744
|
-
</section>,
|
|
745
|
-
idx
|
|
746
|
-
);
|
|
747
|
-
}
|
|
748
|
-
|
|
749
|
-
return wrapIO(
|
|
750
|
-
"left",
|
|
751
|
-
<section className="hairline rounded-2xl rounded-tl-none px-6 py-5 io-in">
|
|
752
|
-
<div className="rtl-safe flex items-center justify-between gap-4">
|
|
753
|
-
<div className="text-[11px] tracking-[0.22em] font-semibold" style={{ color: "var(--muted)" }}>
|
|
754
|
-
AGENT STARTED
|
|
755
|
-
</div>
|
|
756
|
-
<span className="text-[11px] tracking-[0.14em]" style={{ color: "var(--muted)" }}>
|
|
757
|
-
{time}
|
|
758
|
-
</span>
|
|
759
|
-
</div>
|
|
760
|
-
<div className="mt-3 text-[13px] leading-relaxed">
|
|
761
|
-
<span className="font-semibold">{item.agent}</span>
|
|
762
|
-
</div>
|
|
763
|
-
</section>,
|
|
764
|
-
idx
|
|
765
|
-
);
|
|
766
|
-
}
|
|
767
|
-
|
|
768
|
-
if (item.event === "AGENT_RESUMED") {
|
|
769
|
-
return wrapIO(
|
|
770
|
-
"center",
|
|
771
|
-
<section className="meta-center">
|
|
772
|
-
<div className="text-[11px] tracking-[0.22em] font-semibold" style={{ color: "var(--muted)" }}>
|
|
773
|
-
AGENT RESUMED
|
|
774
|
-
</div>
|
|
775
|
-
<div className="mt-2 text-[13px] leading-relaxed">
|
|
776
|
-
<span className="font-semibold">{item.agent}</span>
|
|
777
|
-
</div>
|
|
778
|
-
<div className="mt-1 text-[11px] tracking-[0.14em]" style={{ color: "var(--muted)" }}>
|
|
779
|
-
{time}
|
|
780
|
-
</div>
|
|
781
|
-
</section>,
|
|
782
|
-
idx
|
|
783
|
-
);
|
|
784
|
-
}
|
|
785
|
-
|
|
786
|
-
if (item.event === "AGENT_FAILED") {
|
|
787
|
-
return wrapIO(
|
|
788
|
-
"center",
|
|
789
|
-
<section className="hairline rounded-2xl overflow-hidden">
|
|
790
|
-
<div className="rtl-safe px-5 py-4 divider flex items-center justify-between">
|
|
791
|
-
<div className="text-[11px] tracking-[0.22em] font-semibold" style={{ color: "var(--muted)" }}>
|
|
792
|
-
AGENT FAILED
|
|
793
|
-
</div>
|
|
794
|
-
<span className="text-[11px] tracking-[0.14em]" style={{ color: "var(--muted)" }}>
|
|
795
|
-
{time}
|
|
796
|
-
</span>
|
|
797
|
-
</div>
|
|
798
|
-
<div className="px-5 py-4 meta-center">
|
|
799
|
-
<div className="text-[13px] leading-relaxed"><span className="font-semibold">{item.agent}</span></div>
|
|
800
|
-
<div className="mt-3 text-[13px] leading-relaxed markdown-body">{item.error}</div>
|
|
801
|
-
</div>
|
|
802
|
-
</section>,
|
|
803
|
-
idx
|
|
804
|
-
);
|
|
805
|
-
}
|
|
806
|
-
|
|
807
|
-
if (item.event === "INTERACTION_REQUESTED" || item.event === "PROMPT_REQUESTED") {
|
|
808
|
-
return wrapIO(
|
|
809
|
-
"center",
|
|
810
|
-
<section className="hairline rounded-2xl px-6 py-5">
|
|
811
|
-
<div className="rtl-safe flex items-center justify-between gap-4">
|
|
812
|
-
<div className="text-[11px] tracking-[0.22em] font-semibold" style={{ color: "var(--muted)" }}>
|
|
813
|
-
INTERVENTION REQUIRED
|
|
814
|
-
</div>
|
|
815
|
-
<span className="text-[11px] tracking-[0.14em]" style={{ color: "var(--muted)" }}>
|
|
816
|
-
{time}
|
|
817
|
-
</span>
|
|
818
|
-
</div>
|
|
819
|
-
<div className="mt-3 text-[13px] leading-relaxed markdown-body">
|
|
820
|
-
{item.question ? `"${item.question}"` : `Waiting for response to "${item.slug}"…`}
|
|
821
|
-
</div>
|
|
822
|
-
</section>,
|
|
823
|
-
idx
|
|
824
|
-
);
|
|
825
|
-
}
|
|
826
|
-
|
|
827
|
-
if (item.event === "PROMPT_ANSWERED" || item.event === "INTERACTION_SUBMITTED") {
|
|
828
|
-
const isManual = item.source === "remote";
|
|
829
|
-
return wrapIO(
|
|
830
|
-
"center",
|
|
831
|
-
<section className="hairline rounded-2xl px-6 py-5">
|
|
832
|
-
<div className="rtl-safe flex items-center justify-between gap-4">
|
|
833
|
-
<div className="text-[11px] tracking-[0.22em] font-semibold" style={{ color: "var(--muted)" }}>
|
|
834
|
-
{isManual ? "RESOLVED VIA BROWSER" : "USER ANSWERED"}
|
|
835
|
-
</div>
|
|
836
|
-
<span className="text-[11px] tracking-[0.14em]" style={{ color: "var(--muted)" }}>
|
|
837
|
-
{time}
|
|
838
|
-
</span>
|
|
839
|
-
</div>
|
|
840
|
-
<div className="mt-3 text-[13px] leading-relaxed markdown-body">"{item.answer}"</div>
|
|
841
|
-
</section>,
|
|
842
|
-
idx
|
|
843
|
-
);
|
|
844
|
-
}
|
|
845
|
-
|
|
846
|
-
if (item.event === "AGENT_COMPLETED" || item.event === "INTERACTION_RESOLVED") {
|
|
847
|
-
if (item.event === "AGENT_COMPLETED") {
|
|
848
|
-
return wrapIO(
|
|
849
|
-
"right",
|
|
850
|
-
<section className="io-out">
|
|
851
|
-
<section className="meta-center mb-4">
|
|
852
|
-
<div className="text-[11px] tracking-[0.22em] font-semibold" style={{ color: "var(--muted)" }}>
|
|
853
|
-
DONE
|
|
854
|
-
</div>
|
|
855
|
-
<div className="mt-1 text-[11px] tracking-[0.14em]" style={{ color: "var(--muted)" }}>
|
|
856
|
-
{(item.agent || "").toString()} • {time}
|
|
857
|
-
</div>
|
|
858
|
-
</section>
|
|
859
|
-
{(item.output || item.result) && (
|
|
860
|
-
<JsonView data={item.output || item.result} label="OUTPUT / RESPONSE" align="right" />
|
|
861
|
-
)}
|
|
862
|
-
</section>,
|
|
863
|
-
idx
|
|
864
|
-
);
|
|
865
|
-
}
|
|
866
|
-
|
|
867
|
-
return wrapIO(
|
|
868
|
-
"center",
|
|
869
|
-
<section className="meta-center">
|
|
870
|
-
<div className="text-[11px] tracking-[0.22em] font-semibold" style={{ color: "var(--muted)" }}>
|
|
871
|
-
DONE
|
|
872
|
-
</div>
|
|
873
|
-
<div className="mt-1 text-[11px] tracking-[0.14em]" style={{ color: "var(--muted)" }}>
|
|
874
|
-
{(item.slug || "").toString()} • {time}
|
|
875
|
-
</div>
|
|
876
|
-
</section>,
|
|
877
|
-
idx
|
|
878
|
-
);
|
|
879
|
-
}
|
|
880
|
-
|
|
881
|
-
const stripped = JSON.parse(
|
|
882
|
-
JSON.stringify(item, (key, value) => {
|
|
883
|
-
if (key === "event" || key === "timestamp") return undefined;
|
|
884
|
-
return value;
|
|
885
|
-
})
|
|
886
|
-
);
|
|
887
|
-
|
|
888
|
-
if (item.prompt) {
|
|
889
|
-
return (
|
|
890
|
-
<React.Fragment key={idx}>
|
|
891
|
-
{wrapIO(
|
|
892
|
-
"center",
|
|
893
|
-
<section>
|
|
894
|
-
<JsonView
|
|
895
|
-
data={stripped}
|
|
896
|
-
label={(item.event || "EVENT").toString()}
|
|
897
|
-
timestamp={time}
|
|
898
|
-
align="left"
|
|
899
|
-
/>
|
|
900
|
-
</section>,
|
|
901
|
-
`evt-${idx}`
|
|
902
|
-
)}
|
|
903
|
-
{renderPromptInputCard(item.prompt, `prompt-${idx}`)}
|
|
904
|
-
</React.Fragment>
|
|
905
|
-
);
|
|
906
|
-
}
|
|
907
|
-
|
|
908
|
-
return wrapIO(
|
|
909
|
-
"center",
|
|
910
|
-
<section>
|
|
911
|
-
<JsonView
|
|
912
|
-
data={stripped}
|
|
913
|
-
label={(item.event || "EVENT").toString()}
|
|
914
|
-
timestamp={time}
|
|
915
|
-
align="left"
|
|
916
|
-
/>
|
|
917
|
-
</section>,
|
|
918
|
-
idx
|
|
919
|
-
);
|
|
920
|
-
};
|
|
921
|
-
|
|
922
|
-
if (loading && !history.length) {
|
|
923
|
-
return (
|
|
924
|
-
<div className={theme}>
|
|
925
|
-
<div className="min-h-screen flex items-center justify-center">
|
|
926
|
-
<div className="text-[11px] tracking-[0.22em] font-semibold" style={{ color: "var(--muted)" }}>
|
|
927
|
-
OPENING TERMINAL<span className="blink">…</span>
|
|
928
|
-
</div>
|
|
929
|
-
</div>
|
|
930
|
-
</div>
|
|
931
|
-
);
|
|
932
|
-
}
|
|
933
|
-
|
|
934
|
-
return (
|
|
935
|
-
<div className={theme}>
|
|
936
|
-
<div className="min-h-screen" style={{ background: "var(--bg)", color: "var(--fg)" }}>
|
|
937
|
-
<div className="edge-wrap">
|
|
938
|
-
{/* Header (centered) */}
|
|
939
|
-
<header className="sticky top-0 z-50 py-4" style={{ background: "var(--bg)" }}>
|
|
940
|
-
<div className="center-wrap">
|
|
941
|
-
<div className="divider" style={{ marginBottom: 14 }} />
|
|
942
|
-
|
|
943
|
-
<div className="flex items-center justify-between gap-4">
|
|
944
|
-
<div className="min-w-0 flex items-center gap-3">
|
|
945
|
-
<div className="text-[12px] tracking-[0.24em] font-semibold uppercase whitespace-nowrap" style={{ color: "var(--muted)" }}>
|
|
946
|
-
{workflowName || "WORKFLOW"}
|
|
947
|
-
</div>
|
|
948
|
-
|
|
949
|
-
<StatusBadge status={status} />
|
|
950
|
-
</div>
|
|
951
|
-
|
|
952
|
-
<div className="flex items-center gap-2">
|
|
953
|
-
<Toggle onClick={() => setTheme((p) => (p === "dark" ? "light" : "dark"))} label="" title="Toggle theme">
|
|
954
|
-
{theme === "dark" ? <Icon name="sun" /> : <Icon name="moon" />}
|
|
955
|
-
</Toggle>
|
|
956
|
-
</div>
|
|
957
|
-
</div>
|
|
958
|
-
|
|
959
|
-
<div className="divider" style={{ marginTop: 14 }} />
|
|
960
|
-
</div>
|
|
961
|
-
</header>
|
|
962
|
-
|
|
963
|
-
{/* Error stays right */}
|
|
964
|
-
{error && (
|
|
965
|
-
<div className="io-row io-right mb-8">
|
|
966
|
-
<div className="io-card">
|
|
967
|
-
<div className="hairline rounded-2xl px-5 py-4 io-out meta-center">
|
|
968
|
-
<div className="text-[11px] tracking-[0.22em] font-semibold" style={{ color: "var(--muted)" }}>
|
|
969
|
-
ERROR
|
|
970
|
-
</div>
|
|
971
|
-
<div className="mt-2 text-[13px] leading-relaxed markdown-body">{String(error)}</div>
|
|
972
|
-
</div>
|
|
973
|
-
</div>
|
|
974
|
-
</div>
|
|
975
|
-
)}
|
|
976
|
-
|
|
977
|
-
<main className="space-y-10">
|
|
978
|
-
{visibleEvents.length === 0 && (
|
|
979
|
-
<div className="center-wrap py-16 text-center">
|
|
980
|
-
<div className="text-[11px] tracking-[0.28em] font-semibold" style={{ color: "var(--muted)" }}>
|
|
981
|
-
WAITING FOR EVENTS<span className="blink">…</span>
|
|
982
|
-
</div>
|
|
983
|
-
</div>
|
|
984
|
-
)}
|
|
985
|
-
|
|
986
|
-
{visibleEvents.map(renderEvent)}
|
|
987
|
-
</main>
|
|
988
|
-
|
|
989
|
-
{pendingInteraction && (
|
|
990
|
-
<div className="io-row io-left mt-10">
|
|
991
|
-
<div className="io-card">
|
|
992
|
-
<InteractionForm
|
|
993
|
-
interaction={pendingInteraction}
|
|
994
|
-
onSubmit={handleSubmit}
|
|
995
|
-
disabled={status !== "connected"}
|
|
996
|
-
/>
|
|
997
|
-
</div>
|
|
998
|
-
</div>
|
|
999
|
-
)}
|
|
1000
|
-
|
|
1001
|
-
<footer className="mt-16 pt-8 divider">
|
|
1002
|
-
<div className="center-wrap text-center">
|
|
1003
|
-
<div className="py-6 text-[11px] tracking-[0.28em] font-semibold" style={{ color: "var(--muted)" }}>
|
|
1004
|
-
SUPAMACHINE • TERMINAL v1.4
|
|
1005
|
-
</div>
|
|
1006
|
-
</div>
|
|
1007
|
-
</footer>
|
|
1008
|
-
</div>
|
|
1009
|
-
</div>
|
|
1010
|
-
</div>
|
|
1011
|
-
);
|
|
1012
|
-
}
|
|
1013
|
-
|
|
1014
|
-
const root = ReactDOM.createRoot(document.getElementById("root"));
|
|
1015
|
-
root.render(<App />);
|
|
11
|
+
window.SESSION_TOKEN = "{{SESSION_TOKEN}}";
|
|
12
|
+
window.WORKFLOW_NAME_TEMPLATE = "{{WORKFLOW_NAME}}";
|
|
1016
13
|
</script>
|
|
1017
|
-
|
|
1018
|
-
|
|
14
|
+
<script type="module" src="/src/main.jsx"></script>
|
|
15
|
+
</body>
|
|
1019
16
|
</html>
|