agent-state-machine 2.3.0 → 2.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/bin/cli.js +65 -9
- package/lib/llm.js +134 -22
- package/lib/remote/client.js +19 -6
- package/lib/runtime/agent.js +127 -3
- package/lib/runtime/runtime.js +127 -5
- package/package.json +1 -1
- package/templates/project-builder/config.js +4 -4
- package/vercel-server/api/config/[token].js +76 -0
- package/vercel-server/api/history/[token].js +1 -0
- package/vercel-server/api/ws/cli.js +39 -20
- package/vercel-server/local-server.js +98 -11
- package/vercel-server/public/remote/assets/index-BHvHkNOe.css +1 -0
- package/vercel-server/public/remote/assets/index-BSL55rdk.js +188 -0
- package/vercel-server/public/remote/index.html +2 -2
- package/vercel-server/ui/src/App.jsx +36 -1
- package/vercel-server/ui/src/components/ContentCard.jsx +350 -19
- package/vercel-server/ui/src/components/Footer.jsx +1 -6
- package/vercel-server/ui/src/components/Header.jsx +59 -11
- package/vercel-server/ui/src/components/SettingsModal.jsx +130 -0
- package/vercel-server/ui/src/index.css +53 -0
- package/vercel-server/public/remote/assets/index-BTLc1QSv.js +0 -168
- package/vercel-server/public/remote/assets/index-DLa4X08t.css +0 -1
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
|
6
6
|
<title>{{WORKFLOW_NAME}}</title>
|
|
7
|
-
<script type="module" crossorigin src="/remote/assets/index-
|
|
8
|
-
<link rel="stylesheet" crossorigin href="/remote/assets/index-
|
|
7
|
+
<script type="module" crossorigin src="/remote/assets/index-BSL55rdk.js"></script>
|
|
8
|
+
<link rel="stylesheet" crossorigin href="/remote/assets/index-BHvHkNOe.css">
|
|
9
9
|
</head>
|
|
10
10
|
<body>
|
|
11
11
|
<div id="root"></div>
|
|
@@ -6,6 +6,7 @@ import Footer from "./components/Footer.jsx";
|
|
|
6
6
|
import Header from "./components/Header.jsx";
|
|
7
7
|
import InteractionForm from "./components/InteractionForm.jsx";
|
|
8
8
|
import SendingCard from "./components/SendingCard.jsx";
|
|
9
|
+
import SettingsModal from "./components/SettingsModal.jsx";
|
|
9
10
|
|
|
10
11
|
export default function App() {
|
|
11
12
|
const [history, setHistory] = useState([]);
|
|
@@ -17,6 +18,8 @@ export default function App() {
|
|
|
17
18
|
const [pendingInteraction, setPendingInteraction] = useState(null);
|
|
18
19
|
const [hasNew, setHasNew] = useState(false);
|
|
19
20
|
const [sendingState, setSendingState] = useState(null);
|
|
21
|
+
const [config, setConfig] = useState({ fullAuto: false, autoSelectDelay: 20 });
|
|
22
|
+
const [settingsOpen, setSettingsOpen] = useState(false);
|
|
20
23
|
|
|
21
24
|
useEffect(() => {
|
|
22
25
|
const savedTheme = localStorage.getItem("rf_theme") || (window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light");
|
|
@@ -38,6 +41,7 @@ export default function App() {
|
|
|
38
41
|
const historyUrl = token ? `/api/history/${token}` : "/api/history";
|
|
39
42
|
const eventsUrl = token ? `/api/events/${token}` : "/api/events";
|
|
40
43
|
const submitUrl = token ? `/api/submit/${token}` : "/api/submit";
|
|
44
|
+
const configUrl = token ? `/api/config/${token}` : "/api/config";
|
|
41
45
|
|
|
42
46
|
const fetchData = async () => {
|
|
43
47
|
try {
|
|
@@ -59,12 +63,28 @@ export default function App() {
|
|
|
59
63
|
}
|
|
60
64
|
}
|
|
61
65
|
if (data.workflowName) setWorkflowName(data.workflowName);
|
|
66
|
+
if (data.config) setConfig(data.config);
|
|
62
67
|
setStatus("connected");
|
|
63
68
|
} catch (error) {
|
|
64
69
|
setStatus("disconnected");
|
|
65
70
|
}
|
|
66
71
|
};
|
|
67
72
|
|
|
73
|
+
const updateConfig = async (updates) => {
|
|
74
|
+
try {
|
|
75
|
+
const res = await fetch(configUrl, {
|
|
76
|
+
method: "POST",
|
|
77
|
+
headers: { "Content-Type": "application/json" },
|
|
78
|
+
body: JSON.stringify(updates),
|
|
79
|
+
});
|
|
80
|
+
if (res.ok) {
|
|
81
|
+
setConfig((prev) => ({ ...prev, ...updates }));
|
|
82
|
+
}
|
|
83
|
+
} catch (err) {
|
|
84
|
+
console.error("Failed to update config:", err);
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
|
|
68
88
|
const prevLen = useRef(0);
|
|
69
89
|
useEffect(() => {
|
|
70
90
|
if (history.length > prevLen.current) {
|
|
@@ -152,6 +172,10 @@ export default function App() {
|
|
|
152
172
|
viewMode={viewMode}
|
|
153
173
|
setViewMode={setViewMode}
|
|
154
174
|
history={history}
|
|
175
|
+
fullAuto={config.fullAuto}
|
|
176
|
+
onToggleFullAuto={() => updateConfig({ fullAuto: !config.fullAuto })}
|
|
177
|
+
onOpenSettings={() => setSettingsOpen(true)}
|
|
178
|
+
configDisabled={status !== "connected"}
|
|
155
179
|
/>
|
|
156
180
|
|
|
157
181
|
<main className="main-stage overflow-hidden">
|
|
@@ -206,7 +230,7 @@ export default function App() {
|
|
|
206
230
|
/>
|
|
207
231
|
</div>
|
|
208
232
|
) : (
|
|
209
|
-
<ContentCard item={currentItem} />
|
|
233
|
+
<ContentCard item={currentItem} pageIndex={pageIndex} history={history} />
|
|
210
234
|
)}
|
|
211
235
|
</motion.div>
|
|
212
236
|
</AnimatePresence>
|
|
@@ -223,6 +247,17 @@ export default function App() {
|
|
|
223
247
|
onJumpToLatest={() => setPageIndex(history.length - 1)}
|
|
224
248
|
className={viewMode === "log" ? "opacity-0 pointer-events-none" : "opacity-100"}
|
|
225
249
|
/>
|
|
250
|
+
|
|
251
|
+
<SettingsModal
|
|
252
|
+
isOpen={settingsOpen}
|
|
253
|
+
onClose={() => setSettingsOpen(false)}
|
|
254
|
+
fullAuto={config.fullAuto}
|
|
255
|
+
onToggleFullAuto={() => updateConfig({ fullAuto: !config.fullAuto })}
|
|
256
|
+
autoSelectDelay={config.autoSelectDelay}
|
|
257
|
+
onDelayChange={(delay) => updateConfig({ autoSelectDelay: delay })}
|
|
258
|
+
onStop={() => updateConfig({ stop: true })}
|
|
259
|
+
disabled={status !== "connected"}
|
|
260
|
+
/>
|
|
226
261
|
</div>
|
|
227
262
|
);
|
|
228
263
|
}
|
|
@@ -1,9 +1,30 @@
|
|
|
1
|
-
import { useEffect, useState } from "react";
|
|
1
|
+
import { useEffect, useMemo, useState } from "react";
|
|
2
2
|
import CopyButton from "./CopyButton.jsx";
|
|
3
|
-
import { Bot, Brain, ChevronRight, Search, X } from "lucide-react";
|
|
3
|
+
import { Bot, Brain, ChevronDown, ChevronRight, Search, X } from "lucide-react";
|
|
4
4
|
|
|
5
5
|
const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
6
6
|
|
|
7
|
+
// Format model string for display - extracts model ID from various formats
|
|
8
|
+
const formatModelName = (model) => {
|
|
9
|
+
if (!model) return null;
|
|
10
|
+
// Handle api:provider:model format (e.g., "api:google:gemini-2.5-pro" -> "gemini-2.5-pro")
|
|
11
|
+
if (model.startsWith("api:")) {
|
|
12
|
+
const parts = model.split(":");
|
|
13
|
+
return parts.length >= 3 ? parts.slice(2).join(":") : model;
|
|
14
|
+
}
|
|
15
|
+
// Handle CLI commands with -m/--model flag (e.g., "gemini -m gemini-2.5-flash-lite" -> "gemini-2.5-flash-lite")
|
|
16
|
+
if (model.includes(" ")) {
|
|
17
|
+
const parts = model.split(/\s+/);
|
|
18
|
+
const mIndex = parts.findIndex(p => p === "-m" || p === "--model");
|
|
19
|
+
if (mIndex !== -1 && parts[mIndex + 1]) {
|
|
20
|
+
return parts[mIndex + 1];
|
|
21
|
+
}
|
|
22
|
+
// No -m flag, just return the CLI name
|
|
23
|
+
return parts[0];
|
|
24
|
+
}
|
|
25
|
+
return model;
|
|
26
|
+
};
|
|
27
|
+
|
|
7
28
|
const highlightText = (text, query) => {
|
|
8
29
|
const source = String(text ?? "");
|
|
9
30
|
const q = (query || "").trim();
|
|
@@ -43,6 +64,38 @@ function AgentStartedIcon({ className = "" }) {
|
|
|
43
64
|
);
|
|
44
65
|
}
|
|
45
66
|
|
|
67
|
+
function AgentStartedPulseIcon({ className = "" }) {
|
|
68
|
+
return (
|
|
69
|
+
<div className={`relative mx-auto w-14 h-14 ${className}`} aria-hidden="true">
|
|
70
|
+
<span className="absolute inset-0 rounded-full border border-black/30 dark:border-white/30 animate-ping" />
|
|
71
|
+
<div className="absolute inset-0 rounded-full border border-black dark:border-white flex items-center justify-center bg-white dark:bg-black">
|
|
72
|
+
<Bot className="w-7 h-7" />
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function AgentCompletedIcon({ className = "" }) {
|
|
79
|
+
return (
|
|
80
|
+
<div className={`relative mx-auto w-14 h-14 ${className}`} aria-hidden="true">
|
|
81
|
+
<span className="absolute inset-0 rounded-full border border-black/20 dark:border-white/20 success-ring" />
|
|
82
|
+
<div className="absolute inset-0 rounded-full border-2 border-black dark:border-white bg-black text-white dark:bg-white dark:text-black flex items-center justify-center checkmark-pop">
|
|
83
|
+
<svg
|
|
84
|
+
viewBox="0 0 24 24"
|
|
85
|
+
fill="none"
|
|
86
|
+
stroke="currentColor"
|
|
87
|
+
strokeWidth="2.4"
|
|
88
|
+
strokeLinecap="round"
|
|
89
|
+
strokeLinejoin="round"
|
|
90
|
+
className="w-7 h-7 checkmark-draw"
|
|
91
|
+
>
|
|
92
|
+
<path d="M5 13l4 4L19 7" />
|
|
93
|
+
</svg>
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
46
99
|
function MonoBlock({ children }) {
|
|
47
100
|
return (
|
|
48
101
|
<pre className="text-xs sm:text-sm font-mono leading-relaxed whitespace-pre-wrap break-words overflow-auto max-h-[60vh] p-6 rounded-[24px] border border-black bg-black text-white/90 dark:border-white dark:bg-white dark:text-black/90">
|
|
@@ -143,7 +196,23 @@ function renderJsonWithHighlight(value) {
|
|
|
143
196
|
return nodes;
|
|
144
197
|
}
|
|
145
198
|
|
|
146
|
-
|
|
199
|
+
function formatTokens(count) {
|
|
200
|
+
if (!count) return "0";
|
|
201
|
+
if (count >= 1_000_000) {
|
|
202
|
+
const v = (count / 1_000_000).toFixed(1).replace(/\.0$/, "");
|
|
203
|
+
return `${v}M`;
|
|
204
|
+
}
|
|
205
|
+
if (count >= 10_000) {
|
|
206
|
+
return `${Math.round(count / 1000)}k`;
|
|
207
|
+
}
|
|
208
|
+
if (count >= 1000) {
|
|
209
|
+
const v = (count / 1000).toFixed(1).replace(/\.0$/, "");
|
|
210
|
+
return `${v}k`;
|
|
211
|
+
}
|
|
212
|
+
return count.toString();
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export default function ContentCard({ item, pageIndex = 0, history = [] }) {
|
|
147
216
|
if (!item) return null;
|
|
148
217
|
|
|
149
218
|
const time = new Date(item.timestamp).toLocaleTimeString([], {
|
|
@@ -155,6 +224,49 @@ export default function ContentCard({ item }) {
|
|
|
155
224
|
const [promptQuery, setPromptQuery] = useState("");
|
|
156
225
|
const [showRaw, setShowRaw] = useState(false);
|
|
157
226
|
const [showPromptSearch, setShowPromptSearch] = useState(false);
|
|
227
|
+
const [showTokens, setShowTokens] = useState(false);
|
|
228
|
+
const [showTokenDetails, setShowTokenDetails] = useState(false);
|
|
229
|
+
|
|
230
|
+
// Calculate token stats: point-in-time (up to current page) and total (full session)
|
|
231
|
+
const tokenStats = useMemo(() => {
|
|
232
|
+
let pointInTime = { inputTokens: 0, outputTokens: 0 };
|
|
233
|
+
let total = { inputTokens: 0, outputTokens: 0, cost: 0 };
|
|
234
|
+
const agentBreakdown = [];
|
|
235
|
+
|
|
236
|
+
for (let i = 0; i < history.length; i++) {
|
|
237
|
+
const event = history[i];
|
|
238
|
+
if (event?.event === "AGENT_COMPLETED" && event.usage) {
|
|
239
|
+
const input = event.usage.inputTokens || 0;
|
|
240
|
+
const output = event.usage.outputTokens || 0;
|
|
241
|
+
const cached = event.usage.cachedTokens || 0;
|
|
242
|
+
const cost = event.usage.cost || 0;
|
|
243
|
+
total.inputTokens += input;
|
|
244
|
+
total.outputTokens += output;
|
|
245
|
+
total.cost += cost;
|
|
246
|
+
if (i <= pageIndex) {
|
|
247
|
+
pointInTime.inputTokens += input;
|
|
248
|
+
pointInTime.outputTokens += output;
|
|
249
|
+
}
|
|
250
|
+
agentBreakdown.push({
|
|
251
|
+
agent: event.agent || "Unknown",
|
|
252
|
+
model: event.model || null,
|
|
253
|
+
inputTokens: input,
|
|
254
|
+
outputTokens: output,
|
|
255
|
+
cachedTokens: cached,
|
|
256
|
+
cost: cost,
|
|
257
|
+
timestamp: event.timestamp,
|
|
258
|
+
index: i,
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return {
|
|
264
|
+
pointInTime,
|
|
265
|
+
total,
|
|
266
|
+
agentBreakdown,
|
|
267
|
+
hasTokens: total.inputTokens > 0 || total.outputTokens > 0
|
|
268
|
+
};
|
|
269
|
+
}, [history, pageIndex]);
|
|
158
270
|
|
|
159
271
|
// Full-auto countdown logic (must be at top level for hooks rules)
|
|
160
272
|
const isInteractionEvent = item.event === "PROMPT_REQUESTED" || item.event === "INTERACTION_REQUESTED";
|
|
@@ -507,12 +619,23 @@ export default function ContentCard({ item }) {
|
|
|
507
619
|
content = (
|
|
508
620
|
<div className="space-y-10 py-6">
|
|
509
621
|
<div className="space-y-3 text-center">
|
|
510
|
-
<
|
|
622
|
+
<AgentStartedPulseIcon />
|
|
511
623
|
<div className="space-y-1">
|
|
512
624
|
<div className="text-[11px] font-semibold tracking-[0.28em] uppercase text-black/50 dark:text-white/50">
|
|
513
625
|
Agent started
|
|
514
626
|
</div>
|
|
515
627
|
<h2 className="text-3xl sm:text-4xl font-black tracking-tight">{item.agent}</h2>
|
|
628
|
+
{item.model && (
|
|
629
|
+
<div className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full bg-black/5 dark:bg-white/10 text-xs font-mono text-black/60 dark:text-white/60">
|
|
630
|
+
{item.modelAlias && (
|
|
631
|
+
<>
|
|
632
|
+
<span className="font-semibold">{item.modelAlias}</span>
|
|
633
|
+
<span className="text-black/30 dark:text-white/30">•</span>
|
|
634
|
+
</>
|
|
635
|
+
)}
|
|
636
|
+
{formatModelName(item.model)}
|
|
637
|
+
</div>
|
|
638
|
+
)}
|
|
516
639
|
</div>
|
|
517
640
|
</div>
|
|
518
641
|
|
|
@@ -543,8 +666,18 @@ export default function ContentCard({ item }) {
|
|
|
543
666
|
|
|
544
667
|
<div className="space-y-4">
|
|
545
668
|
<div className="flex flex-wrap items-center justify-between gap-3">
|
|
546
|
-
<div className="
|
|
547
|
-
|
|
669
|
+
<div className="flex items-center gap-3">
|
|
670
|
+
<div className="text-[11px] font-semibold tracking-[0.24em] uppercase text-black/50 dark:text-white/50">
|
|
671
|
+
Prompt
|
|
672
|
+
</div>
|
|
673
|
+
<button
|
|
674
|
+
type="button"
|
|
675
|
+
onClick={() => setShowPromptSearch(true)}
|
|
676
|
+
className="p-1.5 rounded-full text-black/40 dark:text-white/40 hover:text-black dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/10 transition-colors"
|
|
677
|
+
aria-label="Search prompt"
|
|
678
|
+
>
|
|
679
|
+
<Search className="w-4 h-4" />
|
|
680
|
+
</button>
|
|
548
681
|
</div>
|
|
549
682
|
{promptCountLabel ? (
|
|
550
683
|
<div className="text-xs font-mono text-black/40 dark:text-white/40">
|
|
@@ -848,21 +981,56 @@ export default function ContentCard({ item }) {
|
|
|
848
981
|
</div>
|
|
849
982
|
);
|
|
850
983
|
} else if (item.event === "AGENT_COMPLETED") {
|
|
984
|
+
const hasUsage = item.usage && (item.usage.inputTokens > 0 || item.usage.outputTokens > 0);
|
|
985
|
+
|
|
851
986
|
content = (
|
|
852
987
|
<div className="space-y-10 py-6">
|
|
853
988
|
<div className="rounded-[28px] border border-black/10 dark:border-white/10 bg-black/[0.02] dark:bg-white/[0.04] p-6 sm:p-8 text-center space-y-4">
|
|
854
|
-
<
|
|
989
|
+
<AgentCompletedIcon className="w-12 h-12" />
|
|
855
990
|
<div className="text-[11px] font-semibold tracking-[0.24em] uppercase text-black/50 dark:text-white/50">
|
|
856
991
|
Agent completed
|
|
857
992
|
</div>
|
|
858
993
|
<div className="text-2xl sm:text-3xl font-black tracking-tight">
|
|
859
994
|
{item.agent || "Agent"}
|
|
860
995
|
</div>
|
|
861
|
-
|
|
996
|
+
|
|
997
|
+
{/* Token usage display */}
|
|
998
|
+
{hasUsage && (
|
|
999
|
+
<div className="flex flex-wrap items-center justify-center gap-4 text-sm">
|
|
1000
|
+
<div className="flex items-center gap-2">
|
|
1001
|
+
<span className="text-black/50 dark:text-white/50">In:</span>
|
|
1002
|
+
<span className="font-mono font-semibold">{formatTokenCount(item.usage.inputTokens)}</span>
|
|
1003
|
+
</div>
|
|
1004
|
+
<div className="flex items-center gap-2">
|
|
1005
|
+
<span className="text-black/50 dark:text-white/50">Out:</span>
|
|
1006
|
+
<span className="font-mono font-semibold">{formatTokenCount(item.usage.outputTokens)}</span>
|
|
1007
|
+
</div>
|
|
1008
|
+
{item.usage.cachedTokens > 0 && (
|
|
1009
|
+
<div className="flex items-center gap-2 text-black/40 dark:text-white/40">
|
|
1010
|
+
<span>Cached:</span>
|
|
1011
|
+
<span className="font-mono">{formatTokenCount(item.usage.cachedTokens)}</span>
|
|
1012
|
+
</div>
|
|
1013
|
+
)}
|
|
1014
|
+
{item.usage.cost > 0 && (
|
|
1015
|
+
<div className="flex items-center gap-2 text-black/60 dark:text-white/60">
|
|
1016
|
+
<span className="font-mono font-semibold">${item.usage.cost.toFixed(4)}</span>
|
|
1017
|
+
</div>
|
|
1018
|
+
)}
|
|
1019
|
+
</div>
|
|
1020
|
+
)}
|
|
1021
|
+
|
|
1022
|
+
{/* Model badge */}
|
|
1023
|
+
{item.model && (
|
|
1024
|
+
<div className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full bg-black/5 dark:bg-white/10 text-xs font-mono text-black/60 dark:text-white/60">
|
|
1025
|
+
{formatModelName(item.model)}
|
|
1026
|
+
</div>
|
|
1027
|
+
)}
|
|
1028
|
+
|
|
1029
|
+
{typeof item.attempts === "number" && (
|
|
862
1030
|
<div className="text-sm text-black/60 dark:text-white/60">
|
|
863
1031
|
{item.attempts} {item.attempts === 1 ? "attempt" : "attempts"}
|
|
864
1032
|
</div>
|
|
865
|
-
)
|
|
1033
|
+
)}
|
|
866
1034
|
</div>
|
|
867
1035
|
|
|
868
1036
|
{item.output !== undefined ? (
|
|
@@ -901,16 +1069,59 @@ export default function ContentCard({ item }) {
|
|
|
901
1069
|
<div className="content-width flex-1">
|
|
902
1070
|
<div className="flex flex-wrap items-center justify-between gap-4 pt-8 sm:pt-10 pb-6 border-b border-black/10 dark:border-white/10">
|
|
903
1071
|
<div className="flex items-center gap-3">
|
|
904
|
-
{
|
|
905
|
-
<
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
1072
|
+
{tokenStats.hasTokens && (
|
|
1073
|
+
<div className="relative">
|
|
1074
|
+
<button
|
|
1075
|
+
type="button"
|
|
1076
|
+
onClick={() => setShowTokens((prev) => !prev)}
|
|
1077
|
+
className="flex items-center gap-2 px-3 py-1.5 rounded-full text-xs font-mono bg-black/5 dark:bg-white/10 hover:bg-black/10 dark:hover:bg-white/15 transition-colors"
|
|
1078
|
+
aria-expanded={showTokens}
|
|
1079
|
+
aria-label="Toggle token stats"
|
|
1080
|
+
>
|
|
1081
|
+
<span className="text-black/50 dark:text-white/50">↑</span>
|
|
1082
|
+
<span>{formatTokens(tokenStats.pointInTime.inputTokens)}</span>
|
|
1083
|
+
<span className="text-black/50 dark:text-white/50">↓</span>
|
|
1084
|
+
<span>{formatTokens(tokenStats.pointInTime.outputTokens)}</span>
|
|
1085
|
+
<ChevronDown className={`w-3 h-3 text-black/40 dark:text-white/40 transition-transform ${showTokens ? "rotate-180" : ""}`} />
|
|
1086
|
+
</button>
|
|
1087
|
+
{showTokens && (
|
|
1088
|
+
<div className="absolute top-full left-0 mt-2 z-10 min-w-[200px] rounded-2xl border border-black/10 dark:border-white/10 bg-white dark:bg-black shadow-xl shadow-black/10 dark:shadow-white/5 p-4 space-y-3">
|
|
1089
|
+
<div className="space-y-1">
|
|
1090
|
+
<div className="text-[10px] font-semibold tracking-[0.2em] uppercase text-black/40 dark:text-white/40">
|
|
1091
|
+
Up to page {pageIndex + 1}
|
|
1092
|
+
</div>
|
|
1093
|
+
<div className="flex items-center gap-3 text-sm font-mono">
|
|
1094
|
+
<span className="text-black/50 dark:text-white/50">↑</span>
|
|
1095
|
+
<span className="font-semibold">{tokenStats.pointInTime.inputTokens.toLocaleString()}</span>
|
|
1096
|
+
<span className="text-black/50 dark:text-white/50">↓</span>
|
|
1097
|
+
<span className="font-semibold">{tokenStats.pointInTime.outputTokens.toLocaleString()}</span>
|
|
1098
|
+
</div>
|
|
1099
|
+
</div>
|
|
1100
|
+
<div className="border-t border-black/10 dark:border-white/10 pt-3 space-y-1">
|
|
1101
|
+
<div className="text-[10px] font-semibold tracking-[0.2em] uppercase text-black/40 dark:text-white/40">
|
|
1102
|
+
Session total
|
|
1103
|
+
</div>
|
|
1104
|
+
<div className="flex items-center gap-3 text-sm font-mono">
|
|
1105
|
+
<span className="text-black/50 dark:text-white/50">↑</span>
|
|
1106
|
+
<span className="font-semibold">{tokenStats.total.inputTokens.toLocaleString()}</span>
|
|
1107
|
+
<span className="text-black/50 dark:text-white/50">↓</span>
|
|
1108
|
+
<span className="font-semibold">{tokenStats.total.outputTokens.toLocaleString()}</span>
|
|
1109
|
+
</div>
|
|
1110
|
+
</div>
|
|
1111
|
+
<button
|
|
1112
|
+
type="button"
|
|
1113
|
+
onClick={() => {
|
|
1114
|
+
setShowTokens(false);
|
|
1115
|
+
setShowTokenDetails(true);
|
|
1116
|
+
}}
|
|
1117
|
+
className="w-full mt-2 pt-3 border-t border-black/10 dark:border-white/10 text-[10px] font-semibold tracking-[0.16em] uppercase text-black/50 dark:text-white/50 hover:text-black dark:hover:text-white transition-colors text-center"
|
|
1118
|
+
>
|
|
1119
|
+
View details
|
|
1120
|
+
</button>
|
|
1121
|
+
</div>
|
|
1122
|
+
)}
|
|
1123
|
+
</div>
|
|
1124
|
+
)}
|
|
914
1125
|
</div>
|
|
915
1126
|
<div className="flex items-center gap-3">
|
|
916
1127
|
<RawToggle open={showRaw} onToggle={() => setShowRaw((prev) => !prev)} />
|
|
@@ -926,6 +1137,126 @@ export default function ContentCard({ item }) {
|
|
|
926
1137
|
content
|
|
927
1138
|
)}
|
|
928
1139
|
</div>
|
|
1140
|
+
|
|
1141
|
+
{/* Token Details Modal */}
|
|
1142
|
+
{showTokenDetails && (
|
|
1143
|
+
<div className="fixed inset-0 z-50 bg-white text-black dark:bg-black dark:text-white">
|
|
1144
|
+
<div className="h-full w-full overflow-y-auto custom-scroll px-6 sm:px-10 py-10">
|
|
1145
|
+
<div className="max-w-4xl mx-auto space-y-8">
|
|
1146
|
+
<div className="flex flex-wrap items-center justify-between gap-4">
|
|
1147
|
+
<div className="space-y-1">
|
|
1148
|
+
<div className="text-[11px] font-semibold tracking-[0.32em] uppercase text-black/50 dark:text-white/50">
|
|
1149
|
+
Token Usage Details
|
|
1150
|
+
</div>
|
|
1151
|
+
<div className="text-2xl font-bold">
|
|
1152
|
+
{tokenStats.agentBreakdown.length} agent {tokenStats.agentBreakdown.length === 1 ? "call" : "calls"}
|
|
1153
|
+
</div>
|
|
1154
|
+
</div>
|
|
1155
|
+
<button
|
|
1156
|
+
type="button"
|
|
1157
|
+
onClick={() => setShowTokenDetails(false)}
|
|
1158
|
+
className="text-[10px] font-bold tracking-[0.2em] uppercase text-black/60 hover:text-black dark:text-white/60 dark:hover:text-white"
|
|
1159
|
+
>
|
|
1160
|
+
Close
|
|
1161
|
+
</button>
|
|
1162
|
+
</div>
|
|
1163
|
+
|
|
1164
|
+
{/* Totals Summary */}
|
|
1165
|
+
<div className="rounded-[24px] border border-black/10 dark:border-white/10 bg-black/[0.02] dark:bg-white/[0.04] p-6">
|
|
1166
|
+
<div className="text-[10px] font-semibold tracking-[0.2em] uppercase text-black/40 dark:text-white/40 mb-4">
|
|
1167
|
+
Session Totals
|
|
1168
|
+
</div>
|
|
1169
|
+
<div className="grid grid-cols-2 sm:grid-cols-4 gap-6">
|
|
1170
|
+
<div className="space-y-1">
|
|
1171
|
+
<div className="text-xs text-black/50 dark:text-white/50">Input Tokens</div>
|
|
1172
|
+
<div className="text-xl font-mono font-bold">{tokenStats.total.inputTokens.toLocaleString()}</div>
|
|
1173
|
+
</div>
|
|
1174
|
+
<div className="space-y-1">
|
|
1175
|
+
<div className="text-xs text-black/50 dark:text-white/50">Output Tokens</div>
|
|
1176
|
+
<div className="text-xl font-mono font-bold">{tokenStats.total.outputTokens.toLocaleString()}</div>
|
|
1177
|
+
</div>
|
|
1178
|
+
<div className="space-y-1">
|
|
1179
|
+
<div className="text-xs text-black/50 dark:text-white/50">Total Tokens</div>
|
|
1180
|
+
<div className="text-xl font-mono font-bold">{(tokenStats.total.inputTokens + tokenStats.total.outputTokens).toLocaleString()}</div>
|
|
1181
|
+
</div>
|
|
1182
|
+
{tokenStats.total.cost > 0 && (
|
|
1183
|
+
<div className="space-y-1">
|
|
1184
|
+
<div className="text-xs text-black/50 dark:text-white/50">Total Cost</div>
|
|
1185
|
+
<div className="text-xl font-mono font-bold">${tokenStats.total.cost.toFixed(4)}</div>
|
|
1186
|
+
</div>
|
|
1187
|
+
)}
|
|
1188
|
+
</div>
|
|
1189
|
+
</div>
|
|
1190
|
+
|
|
1191
|
+
{/* Agent Breakdown */}
|
|
1192
|
+
<div className="space-y-4">
|
|
1193
|
+
<div className="text-[10px] font-semibold tracking-[0.2em] uppercase text-black/40 dark:text-white/40">
|
|
1194
|
+
Per-Agent Breakdown
|
|
1195
|
+
</div>
|
|
1196
|
+
<div className="space-y-3">
|
|
1197
|
+
{tokenStats.agentBreakdown.map((agent, idx) => (
|
|
1198
|
+
<div
|
|
1199
|
+
key={`${agent.agent}-${idx}`}
|
|
1200
|
+
className="rounded-[20px] border border-black/10 dark:border-white/10 bg-black/[0.02] dark:bg-white/[0.04] p-5"
|
|
1201
|
+
>
|
|
1202
|
+
<div className="flex flex-wrap items-start justify-between gap-4 mb-4">
|
|
1203
|
+
<div className="space-y-1">
|
|
1204
|
+
<div className="text-lg font-bold">{agent.agent}</div>
|
|
1205
|
+
<div className="flex items-center gap-2">
|
|
1206
|
+
{agent.model && (
|
|
1207
|
+
<span className="inline-flex px-2 py-0.5 rounded-full bg-black/5 dark:bg-white/10 text-xs font-mono text-black/60 dark:text-white/60">
|
|
1208
|
+
{formatModelName(agent.model)}
|
|
1209
|
+
</span>
|
|
1210
|
+
)}
|
|
1211
|
+
<span className="text-xs text-black/40 dark:text-white/40">
|
|
1212
|
+
{new Date(agent.timestamp).toLocaleTimeString([], {
|
|
1213
|
+
hour: "2-digit",
|
|
1214
|
+
minute: "2-digit",
|
|
1215
|
+
second: "2-digit",
|
|
1216
|
+
})}
|
|
1217
|
+
</span>
|
|
1218
|
+
</div>
|
|
1219
|
+
</div>
|
|
1220
|
+
{agent.cost > 0 && (
|
|
1221
|
+
<div className="text-right">
|
|
1222
|
+
<div className="text-lg font-mono font-bold">${agent.cost.toFixed(4)}</div>
|
|
1223
|
+
</div>
|
|
1224
|
+
)}
|
|
1225
|
+
</div>
|
|
1226
|
+
<div className="grid grid-cols-2 sm:grid-cols-4 gap-4 text-sm">
|
|
1227
|
+
<div className="space-y-0.5">
|
|
1228
|
+
<div className="text-xs text-black/40 dark:text-white/40">Input</div>
|
|
1229
|
+
<div className="font-mono font-semibold">{agent.inputTokens.toLocaleString()}</div>
|
|
1230
|
+
</div>
|
|
1231
|
+
<div className="space-y-0.5">
|
|
1232
|
+
<div className="text-xs text-black/40 dark:text-white/40">Output</div>
|
|
1233
|
+
<div className="font-mono font-semibold">{agent.outputTokens.toLocaleString()}</div>
|
|
1234
|
+
</div>
|
|
1235
|
+
{agent.cachedTokens > 0 && (
|
|
1236
|
+
<div className="space-y-0.5">
|
|
1237
|
+
<div className="text-xs text-black/40 dark:text-white/40">Cached</div>
|
|
1238
|
+
<div className="font-mono font-semibold text-black/60 dark:text-white/60">{agent.cachedTokens.toLocaleString()}</div>
|
|
1239
|
+
</div>
|
|
1240
|
+
)}
|
|
1241
|
+
<div className="space-y-0.5">
|
|
1242
|
+
<div className="text-xs text-black/40 dark:text-white/40">Total</div>
|
|
1243
|
+
<div className="font-mono font-semibold">{(agent.inputTokens + agent.outputTokens).toLocaleString()}</div>
|
|
1244
|
+
</div>
|
|
1245
|
+
</div>
|
|
1246
|
+
</div>
|
|
1247
|
+
))}
|
|
1248
|
+
</div>
|
|
1249
|
+
</div>
|
|
1250
|
+
|
|
1251
|
+
{tokenStats.agentBreakdown.length === 0 && (
|
|
1252
|
+
<div className="rounded-[20px] border border-black/10 dark:border-white/10 bg-black/[0.02] dark:bg-white/[0.04] p-6 text-center text-black/50 dark:text-white/50">
|
|
1253
|
+
No token usage data available yet.
|
|
1254
|
+
</div>
|
|
1255
|
+
)}
|
|
1256
|
+
</div>
|
|
1257
|
+
</div>
|
|
1258
|
+
</div>
|
|
1259
|
+
)}
|
|
929
1260
|
</div>
|
|
930
1261
|
);
|
|
931
1262
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useEffect, useState } from "react";
|
|
2
2
|
import { ChevronLeft, ChevronRight } from "lucide-react";
|
|
3
3
|
|
|
4
|
-
export default function Footer({ page, total, onNext, onPrev, onJump, hasNew, onJumpToLatest, className = ""
|
|
4
|
+
export default function Footer({ page, total, onNext, onPrev, onJump, hasNew, onJumpToLatest, className = "" }) {
|
|
5
5
|
const [inputValue, setInputValue] = useState(page + 1);
|
|
6
6
|
useEffect(() => setInputValue(page + 1), [page]);
|
|
7
7
|
|
|
@@ -16,11 +16,6 @@ export default function Footer({ page, total, onNext, onPrev, onJump, hasNew, on
|
|
|
16
16
|
|
|
17
17
|
return (
|
|
18
18
|
<footer className={`nav-footer transition-opacity duration-300 ${className}`}>
|
|
19
|
-
{leftSlot ? (
|
|
20
|
-
<div className="fixed bottom-6 left-2 sm:left-4 z-40">
|
|
21
|
-
{leftSlot}
|
|
22
|
-
</div>
|
|
23
|
-
) : null}
|
|
24
19
|
<div className="footer-control">
|
|
25
20
|
<button
|
|
26
21
|
onClick={onPrev}
|
|
@@ -1,13 +1,27 @@
|
|
|
1
|
-
import { Moon, Sun, LayoutList, Presentation } from "lucide-react";
|
|
1
|
+
import { Moon, Sun, LayoutList, Presentation, Play, Pause, Settings } from "lucide-react";
|
|
2
2
|
import CopyButton from "./CopyButton.jsx";
|
|
3
3
|
|
|
4
|
-
export default function Header({
|
|
4
|
+
export default function Header({
|
|
5
|
+
workflowName,
|
|
6
|
+
status,
|
|
7
|
+
theme,
|
|
8
|
+
toggleTheme,
|
|
9
|
+
viewMode,
|
|
10
|
+
setViewMode,
|
|
11
|
+
history,
|
|
12
|
+
fullAuto,
|
|
13
|
+
onToggleFullAuto,
|
|
14
|
+
onOpenSettings,
|
|
15
|
+
configDisabled,
|
|
16
|
+
}) {
|
|
17
|
+
const isConnected = status === "connected";
|
|
18
|
+
|
|
5
19
|
return (
|
|
6
20
|
<header className="fixed top-0 inset-x-0 h-20 px-6 sm:px-10 lg:px-12 flex items-center justify-between z-50 bg-bg/80 backdrop-blur-3xl">
|
|
7
21
|
<div className="flex items-center gap-4">
|
|
8
22
|
<div
|
|
9
23
|
className={`w-2.5 h-2.5 rounded-full border ${
|
|
10
|
-
|
|
24
|
+
isConnected
|
|
11
25
|
? "bg-black border-black dark:bg-white dark:border-white"
|
|
12
26
|
: "bg-transparent border-black/30 dark:border-white/30"
|
|
13
27
|
}`}
|
|
@@ -19,7 +33,37 @@ export default function Header({ workflowName, status, theme, toggleTheme, viewM
|
|
|
19
33
|
</div>
|
|
20
34
|
|
|
21
35
|
<div className="flex items-center gap-2">
|
|
36
|
+
{/* Play/Pause button - quick toggle for full-auto */}
|
|
37
|
+
<button
|
|
38
|
+
onClick={onToggleFullAuto}
|
|
39
|
+
disabled={configDisabled}
|
|
40
|
+
className={`tooltip w-10 h-10 flex items-center justify-center rounded-full transition-colors ${
|
|
41
|
+
configDisabled
|
|
42
|
+
? "opacity-30 cursor-not-allowed"
|
|
43
|
+
: "hover:bg-black/5 dark:hover:bg-white/10"
|
|
44
|
+
}`}
|
|
45
|
+
data-tooltip={fullAuto ? "Pause auto" : "Enable auto"}
|
|
46
|
+
aria-label={fullAuto ? "Disable full-auto mode" : "Enable full-auto mode"}
|
|
47
|
+
>
|
|
48
|
+
{fullAuto ? (
|
|
49
|
+
<Pause className="w-5 h-5 opacity-60" />
|
|
50
|
+
) : (
|
|
51
|
+
<Play className="w-5 h-5 opacity-40 hover:opacity-100" />
|
|
52
|
+
)}
|
|
53
|
+
</button>
|
|
54
|
+
|
|
55
|
+
{/* Settings button */}
|
|
56
|
+
<button
|
|
57
|
+
onClick={onOpenSettings}
|
|
58
|
+
className="tooltip w-10 h-10 flex items-center justify-center rounded-full hover:bg-black/5 dark:hover:bg-white/10 transition-colors"
|
|
59
|
+
data-tooltip="Settings"
|
|
60
|
+
aria-label="Open settings"
|
|
61
|
+
>
|
|
62
|
+
<Settings className="w-5 h-5 opacity-40 hover:opacity-100" />
|
|
63
|
+
</button>
|
|
64
|
+
|
|
22
65
|
<CopyButton text={history || []} label="Copy full history" disabled={!history || history.length === 0} />
|
|
66
|
+
|
|
23
67
|
<button
|
|
24
68
|
onClick={() => setViewMode(viewMode === "present" ? "log" : "present")}
|
|
25
69
|
className="tooltip w-10 h-10 flex items-center justify-center rounded-full hover:bg-black/5 dark:hover:bg-white/10 transition-colors"
|
|
@@ -33,14 +77,18 @@ export default function Header({ workflowName, status, theme, toggleTheme, viewM
|
|
|
33
77
|
)}
|
|
34
78
|
</button>
|
|
35
79
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
80
|
+
<button
|
|
81
|
+
onClick={toggleTheme}
|
|
82
|
+
className="tooltip w-10 h-10 flex items-center justify-center rounded-full hover:bg-black/5 dark:hover:bg-white/10 transition-colors"
|
|
83
|
+
data-tooltip={theme === "dark" ? "Light theme" : "Dark theme"}
|
|
84
|
+
aria-label={theme === "dark" ? "Switch to Light theme" : "Switch to Dark theme"}
|
|
85
|
+
>
|
|
86
|
+
{theme === "dark" ? (
|
|
87
|
+
<Sun className="w-5 h-5 opacity-40 hover:opacity-100" />
|
|
88
|
+
) : (
|
|
89
|
+
<Moon className="w-5 h-5 opacity-40 hover:opacity-100" />
|
|
90
|
+
)}
|
|
91
|
+
</button>
|
|
44
92
|
</div>
|
|
45
93
|
</header>
|
|
46
94
|
);
|