@brainpilot/web 0.0.3 → 0.0.5

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.
Files changed (97) hide show
  1. package/dist/assets/index-C-8G4D4j.js +448 -0
  2. package/dist/assets/index-C501m5OS.css +1 -0
  3. package/dist/index.html +2 -2
  4. package/index.html +13 -0
  5. package/package.json +9 -3
  6. package/src/App.tsx +10 -0
  7. package/src/__tests__/api.test.ts +103 -0
  8. package/src/__tests__/messageGroups.test.ts +80 -0
  9. package/src/__tests__/newUiComponents.test.tsx +101 -0
  10. package/src/__tests__/newUiEvents.test.ts +236 -0
  11. package/src/components/chat/AskUserCard.tsx +123 -0
  12. package/src/components/chat/AutoRetryIndicator.tsx +71 -0
  13. package/src/components/chat/ComposerInput.tsx +73 -0
  14. package/src/components/chat/ComposerSendButton.tsx +26 -0
  15. package/src/components/chat/MarkdownMessage.tsx +24 -0
  16. package/src/components/chat/MessageStream.tsx +464 -0
  17. package/src/components/chat/PromptComposer.tsx +398 -0
  18. package/src/components/chat/SystemMessageBubble.tsx +46 -0
  19. package/src/components/demo/DemoFileTree.tsx +146 -0
  20. package/src/components/demo/DemoView.tsx +668 -0
  21. package/src/components/demo/TraceNodeModal.tsx +76 -0
  22. package/src/components/demo/demoBundle.ts +218 -0
  23. package/src/components/demo/demoCache.ts +42 -0
  24. package/src/components/files/FilePreviewView.tsx +153 -0
  25. package/src/components/files/FileSidebar.tsx +664 -0
  26. package/src/components/files/filePreview.ts +113 -0
  27. package/src/components/primitives/CustomSelect.tsx +200 -0
  28. package/src/components/primitives/IconButton.tsx +27 -0
  29. package/src/components/quota/DiskQuotaCriticalDialog.tsx +56 -0
  30. package/src/components/quota/DiskQuotaWarningDialog.tsx +65 -0
  31. package/src/components/quota/QuotaFileManager.tsx +197 -0
  32. package/src/components/search/SearchDialog.tsx +101 -0
  33. package/src/components/session/AgentNetwork.tsx +1240 -0
  34. package/src/components/session/AgentTraceViews.tsx +381 -0
  35. package/src/components/session/AnalyticsTab.tsx +386 -0
  36. package/src/components/session/GlobalOverview.tsx +108 -0
  37. package/src/components/session/NodeTooltip.tsx +127 -0
  38. package/src/components/session/TimelineTab.tsx +320 -0
  39. package/src/components/session/TraceGraphView.tsx +301 -0
  40. package/src/components/session/TraceNodeDetail.tsx +142 -0
  41. package/src/components/session/agentAnalytics.ts +397 -0
  42. package/src/components/session/agentNetworkShared.ts +329 -0
  43. package/src/components/session/traceLayout.ts +150 -0
  44. package/src/components/settings/SettingsDialog.tsx +719 -0
  45. package/src/components/shell/DesktopShell.tsx +236 -0
  46. package/src/components/shell/SandboxBuildingOverlay.tsx +73 -0
  47. package/src/components/shell/SandboxStatus.tsx +287 -0
  48. package/src/components/shell/TerminalDrawer.tsx +387 -0
  49. package/src/components/sidebar/Sidebar.tsx +187 -0
  50. package/src/config.ts +10 -0
  51. package/src/contexts/AppProviders.tsx +20 -0
  52. package/src/contexts/AuthContext.tsx +61 -0
  53. package/src/contexts/PreferencesContext.tsx +125 -0
  54. package/src/contexts/SSEContext.tsx +175 -0
  55. package/src/contexts/SandboxContext.tsx +310 -0
  56. package/src/contexts/SessionContext.tsx +608 -0
  57. package/src/contexts/draftStore.ts +103 -0
  58. package/src/contexts/messageFilters.ts +29 -0
  59. package/src/contexts/messageGroups.ts +77 -0
  60. package/src/contexts/messageReducer.ts +401 -0
  61. package/src/contexts/newUiEvents.ts +190 -0
  62. package/src/contracts/backend.ts +846 -0
  63. package/src/contracts/demoBundle.ts +83 -0
  64. package/src/i18n/messages/analytics.ts +96 -0
  65. package/src/i18n/messages/chat.ts +108 -0
  66. package/src/i18n/messages/contexts.ts +40 -0
  67. package/src/i18n/messages/demo.ts +80 -0
  68. package/src/i18n/messages/files.ts +82 -0
  69. package/src/i18n/messages/network.ts +186 -0
  70. package/src/i18n/messages/profile.ts +40 -0
  71. package/src/i18n/messages/quota.ts +36 -0
  72. package/src/i18n/messages/sandbox.ts +116 -0
  73. package/src/i18n/messages/search.ts +16 -0
  74. package/src/i18n/messages/settings.ts +184 -0
  75. package/src/i18n/messages/shell.ts +38 -0
  76. package/src/i18n/messages/sidebar.ts +52 -0
  77. package/src/i18n/messages/terminal.ts +22 -0
  78. package/src/i18n/messages/trace.ts +84 -0
  79. package/src/i18n/messages.ts +32 -0
  80. package/src/i18n/translate.ts +46 -0
  81. package/src/i18n/types.ts +15 -0
  82. package/src/i18n/useT.ts +15 -0
  83. package/src/main.tsx +13 -0
  84. package/src/mocks/backend.ts +722 -0
  85. package/src/styles/global.css +7429 -0
  86. package/src/styles/tokens.css +161 -0
  87. package/src/utils/api.ts +627 -0
  88. package/src/utils/download.ts +18 -0
  89. package/src/utils/format.ts +7 -0
  90. package/src/utils/zip.ts +119 -0
  91. package/src/vite-env.d.ts +1 -0
  92. package/tsconfig.app.json +22 -0
  93. package/tsconfig.json +7 -0
  94. package/tsconfig.node.json +13 -0
  95. package/vite.config.ts +13 -0
  96. package/dist/assets/index-Cd0Mi_WU.css +0 -1
  97. package/dist/assets/index-FGg-DeYR.js +0 -448
@@ -0,0 +1,381 @@
1
+ import { useEffect, useMemo, useRef, useState } from "react";
2
+ import { Network, Pause, Play, RefreshCw, Search, UserRoundCog, X } from "lucide-react";
3
+ import { TraceGraph, TraceNode } from "../../contracts/backend";
4
+ import { useSessions } from "../../contexts/SessionContext";
5
+ import { useT } from "../../i18n/useT";
6
+ import { api } from "../../utils/api";
7
+ import { CustomSelect } from "../primitives/CustomSelect";
8
+ import { IconButton } from "../primitives/IconButton";
9
+ import { AgentNetwork } from "./AgentNetwork";
10
+ import { TraceGraphView } from "./TraceGraphView";
11
+ import { TraceNodeDetail } from "./TraceNodeDetail";
12
+ import {
13
+ formatTime,
14
+ getNodeKind,
15
+ getStatusLabelKey,
16
+ normalizeStatus,
17
+ } from "./traceLayout";
18
+
19
+ export function AgentsPanel() {
20
+ const { agents, currentSession, agentFilters, setAgentFilter, messages } = useSessions();
21
+ const t = useT();
22
+
23
+ return (
24
+ <section className="workspace-panel" aria-labelledby="agents-panel-heading">
25
+ <div className="workspace-panel__inner workspace-panel__inner--trace">
26
+ <header className="workspace-panel__header">
27
+ <div>
28
+ <span className="workspace-panel__eyebrow">
29
+ <Network size={11} style={{ marginRight: 4, verticalAlign: "-1px" }} />
30
+ {t("trace.agents.eyebrow")}
31
+ </span>
32
+ <h2 id="agents-panel-heading">{t("trace.agents.title")}</h2>
33
+ </div>
34
+ <UserRoundCog size={18} />
35
+ </header>
36
+
37
+ {!currentSession ? (
38
+ <p className="workspace-panel__empty">{t("trace.agents.emptyNoSession")}</p>
39
+ ) : (
40
+ <AgentNetwork
41
+ agents={agents}
42
+ agentFilters={agentFilters}
43
+ messages={messages}
44
+ onSetAgentFilter={setAgentFilter}
45
+ />
46
+ )}
47
+
48
+ {currentSession && agents.length === 0 ? (
49
+ <p className="workspace-panel__empty">{t("trace.agents.emptyNoEvents")}</p>
50
+ ) : null}
51
+ </div>
52
+ </section>
53
+ );
54
+ }
55
+
56
+ export function TracePanel() {
57
+ const { currentSession } = useSessions();
58
+ const t = useT();
59
+ const [trace, setTrace] = useState<TraceGraph | null>(null);
60
+ const [selectedNodeId, setSelectedNodeId] = useState<string | null>(null);
61
+ const [query, setQuery] = useState("");
62
+ const [statusFilter, setStatusFilter] = useState("all");
63
+ const [typeFilter, setTypeFilter] = useState("all");
64
+ const [direction, setDirection] = useState<"LR" | "TB">("LR");
65
+ const [zoom, setZoom] = useState(1);
66
+ const [isAutoRefresh, setIsAutoRefresh] = useState(true);
67
+ const [isLoading, setIsLoading] = useState(false);
68
+ const [error, setError] = useState<string | null>(null);
69
+ const [playbackIndex, setPlaybackIndex] = useState(0);
70
+ const [isPlaying, setIsPlaying] = useState(false);
71
+ const [fitToken, setFitToken] = useState(0);
72
+ const wasUserAdjustedRef = useRef(false);
73
+ const prevNodeCountRef = useRef(0);
74
+
75
+ const allNodes = trace?.nodes ?? [];
76
+ const playbackNodes = useMemo(() => allNodes.slice(0, playbackIndex), [allNodes, playbackIndex]);
77
+
78
+ const filteredNodes = useMemo(() => {
79
+ const normalizedQuery = query.trim().toLowerCase();
80
+ return playbackNodes.filter((node) => {
81
+ const status = normalizeStatus(node.status);
82
+ const nodeKind = getNodeKind(node);
83
+ const matchesStatus = statusFilter === "all" || status === statusFilter;
84
+ const matchesType = typeFilter === "all" || nodeKind === typeFilter;
85
+ const searchText = [
86
+ node.id,
87
+ node.title,
88
+ node.description,
89
+ node.summary,
90
+ node.reason,
91
+ node.context,
92
+ node.agent,
93
+ nodeKind,
94
+ ...node.toolCalls,
95
+ ...node.artifacts.map((artifact) => artifact.path),
96
+ ].filter(Boolean).join(" ").toLowerCase();
97
+ return matchesStatus && matchesType && (!normalizedQuery || searchText.includes(normalizedQuery));
98
+ });
99
+ }, [query, statusFilter, playbackNodes, typeFilter]);
100
+
101
+ const visibleNodeIds = useMemo(() => new Set(filteredNodes.map((node) => node.id)), [filteredNodes]);
102
+ const visibleNodes = useMemo(
103
+ () => filteredNodes.map((node) => ({
104
+ ...node,
105
+ parentIds: node.parentIds.filter((parentId) => visibleNodeIds.has(parentId)),
106
+ parents: node.parents.filter((parent) => visibleNodeIds.has(parent.id)),
107
+ childIds: node.childIds.filter((childId) => visibleNodeIds.has(childId)),
108
+ })),
109
+ [filteredNodes, visibleNodeIds],
110
+ );
111
+ const statusOptions = useMemo(
112
+ () => Array.from(new Set((trace?.nodes ?? []).map((node) => normalizeStatus(node.status)).filter(Boolean))).sort(),
113
+ [trace?.nodes],
114
+ );
115
+ const typeOptions = useMemo(
116
+ () => Array.from(new Set((trace?.nodes ?? []).map(getNodeKind).filter(Boolean))).sort(),
117
+ [trace?.nodes],
118
+ );
119
+ const selectedNode = useMemo<TraceNode | null>(() => {
120
+ if (!trace) {
121
+ return null;
122
+ }
123
+ if (visibleNodes.length === 0) {
124
+ return null;
125
+ }
126
+ return visibleNodes.find((node) => node.id === selectedNodeId) ?? visibleNodes[0] ?? null;
127
+ }, [selectedNodeId, trace, visibleNodes]);
128
+
129
+ const loadTrace = async (silent = false) => {
130
+ if (!currentSession) {
131
+ setTrace(null);
132
+ setSelectedNodeId(null);
133
+ return;
134
+ }
135
+ if (!silent) {
136
+ setIsLoading(true);
137
+ }
138
+ setError(null);
139
+ try {
140
+ const nextTrace = await api.sessions.getTrace(currentSession.id);
141
+ setTrace(nextTrace);
142
+ if (!silent) {
143
+ prevNodeCountRef.current = nextTrace.nodes.length;
144
+ if (!wasUserAdjustedRef.current) {
145
+ setPlaybackIndex(nextTrace.nodes.length);
146
+ }
147
+ }
148
+ setSelectedNodeId((current) => {
149
+ if (current && nextTrace.nodes.some((node) => node.id === current)) {
150
+ return current;
151
+ }
152
+ return nextTrace.nodes[0]?.id ?? null;
153
+ });
154
+ } catch (err) {
155
+ if (!silent) {
156
+ setTrace(null);
157
+ setError(err instanceof Error ? err.message : t("trace.loadFailed"));
158
+ }
159
+ } finally {
160
+ if (!silent) {
161
+ setIsLoading(false);
162
+ }
163
+ }
164
+ };
165
+
166
+ useEffect(() => {
167
+ void loadTrace();
168
+ }, [currentSession?.id]);
169
+
170
+ useEffect(() => {
171
+ if (!currentSession || !isAutoRefresh) {
172
+ return;
173
+ }
174
+ const interval = window.setInterval(() => void loadTrace(true), 3000);
175
+ return () => window.clearInterval(interval);
176
+ }, [currentSession?.id, isAutoRefresh]);
177
+
178
+ useEffect(() => {
179
+ if (selectedNodeId && visibleNodes.length > 0 && !visibleNodeIds.has(selectedNodeId)) {
180
+ setSelectedNodeId(visibleNodes[0].id);
181
+ }
182
+ }, [selectedNodeId, visibleNodeIds, visibleNodes]);
183
+
184
+ useEffect(() => {
185
+ setPlaybackIndex(allNodes.length);
186
+ prevNodeCountRef.current = allNodes.length;
187
+ wasUserAdjustedRef.current = false;
188
+ setIsPlaying(false);
189
+ }, [currentSession?.id]);
190
+
191
+ useEffect(() => {
192
+ if (allNodes.length === 0) {
193
+ setPlaybackIndex(0);
194
+ prevNodeCountRef.current = 0;
195
+ return;
196
+ }
197
+ if (!wasUserAdjustedRef.current) {
198
+ if (allNodes.length > prevNodeCountRef.current) {
199
+ if (playbackIndex >= prevNodeCountRef.current || prevNodeCountRef.current === 0) {
200
+ setPlaybackIndex(allNodes.length);
201
+ }
202
+ } else if (allNodes.length < playbackIndex) {
203
+ setPlaybackIndex(allNodes.length);
204
+ }
205
+ }
206
+ prevNodeCountRef.current = allNodes.length;
207
+ }, [allNodes.length, playbackIndex]);
208
+
209
+ useEffect(() => {
210
+ if (!isPlaying || allNodes.length === 0) {
211
+ return;
212
+ }
213
+ const interval = window.setInterval(() => {
214
+ setPlaybackIndex((current) => {
215
+ if (current >= allNodes.length) {
216
+ setIsPlaying(false);
217
+ return current;
218
+ }
219
+ return current + 1;
220
+ });
221
+ setFitToken((token) => token + 1);
222
+ }, 800);
223
+ return () => window.clearInterval(interval);
224
+ }, [isPlaying, allNodes.length]);
225
+
226
+ const togglePlayback = () => {
227
+ if (allNodes.length === 0) {
228
+ return;
229
+ }
230
+ if (playbackIndex >= allNodes.length) {
231
+ setPlaybackIndex(0);
232
+ }
233
+ wasUserAdjustedRef.current = false;
234
+ setIsPlaying((current) => !current);
235
+ };
236
+
237
+ const handleSliderChange = (value: number) => {
238
+ setIsPlaying(false);
239
+ wasUserAdjustedRef.current = true;
240
+ setPlaybackIndex(value);
241
+ };
242
+
243
+ return (
244
+ <section className="workspace-panel" aria-labelledby="trace-panel-heading">
245
+ <div className="workspace-panel__inner workspace-panel__inner--trace">
246
+ <header className="workspace-panel__header trace-header">
247
+ <div>
248
+ <span className="workspace-panel__eyebrow">{t("trace.eyebrow")}</span>
249
+ <h2 id="trace-panel-heading">{t("trace.title")}</h2>
250
+ </div>
251
+ <div className="trace-toolbar">
252
+ <div className="trace-segmented" aria-label={t("trace.aria.layoutDir")}>
253
+ <button className={direction === "LR" ? "is-active" : ""} onClick={() => setDirection("LR")} type="button">LR</button>
254
+ <button className={direction === "TB" ? "is-active" : ""} onClick={() => setDirection("TB")} type="button">TB</button>
255
+ </div>
256
+ <div className="trace-refresh-group" aria-label={t("trace.aria.refreshControls")}>
257
+ <IconButton className={isLoading ? "is-active" : ""} disabled={!currentSession} label={t("trace.aria.refresh")} onClick={() => void loadTrace()}>
258
+ <RefreshCw size={15} />
259
+ </IconButton>
260
+ <button
261
+ aria-pressed={isAutoRefresh}
262
+ className={`trace-live-toggle ${isAutoRefresh ? "is-active" : ""}`}
263
+ disabled={!currentSession}
264
+ onClick={() => setIsAutoRefresh((current) => !current)}
265
+ title={t("trace.autoRefreshTitle")}
266
+ type="button"
267
+ >
268
+ <span aria-hidden="true" />
269
+ {t("trace.live")}
270
+ </button>
271
+ </div>
272
+ </div>
273
+ </header>
274
+
275
+ {error ? <p className="workspace-panel__empty workspace-panel__empty--error">{error}</p> : null}
276
+ {!currentSession ? <p className="workspace-panel__empty">{t("trace.emptyNoSession")}</p> : null}
277
+
278
+ {trace ? (
279
+ <>
280
+ <div className="trace-meta">
281
+ <span>{trace.meta.projectName || currentSession?.title || t("trace.untitled")}</span>
282
+ <span>{t("trace.focus", { focus: String(trace.meta.currentFocus || "-") })}</span>
283
+ <span>{t("trace.nodes", { visible: visibleNodes.length, total: trace.nodes.length })}</span>
284
+ <span>{t("trace.created", { time: formatTime(trace.meta.createdAt) })}</span>
285
+ </div>
286
+
287
+ <div className="trace-controls">
288
+ <label className="trace-search">
289
+ <Search size={14} />
290
+ <input placeholder={t("trace.searchPlaceholder")} value={query} onChange={(event) => setQuery(event.target.value)} />
291
+ {query ? (
292
+ <button aria-label={t("trace.aria.clearSearch")} onClick={() => setQuery("")} type="button">
293
+ <X size={13} />
294
+ </button>
295
+ ) : null}
296
+ </label>
297
+ <div className="trace-control">
298
+ <span>{t("trace.status")}</span>
299
+ <CustomSelect
300
+ ariaLabel={t("trace.aria.statusFilter")}
301
+ className="trace-control__select"
302
+ onChange={setStatusFilter}
303
+ options={[
304
+ { label: t("trace.allStatus"), value: "all" },
305
+ ...statusOptions.map((status) => {
306
+ const key = getStatusLabelKey(status);
307
+ return { label: key ? t(key) : status, value: status };
308
+ }),
309
+ ]}
310
+ value={statusFilter}
311
+ />
312
+ </div>
313
+ <div className="trace-control">
314
+ <span>{t("trace.type")}</span>
315
+ <CustomSelect
316
+ ariaLabel={t("trace.aria.typeFilter")}
317
+ className="trace-control__select"
318
+ onChange={setTypeFilter}
319
+ options={[
320
+ { label: t("trace.allTypes"), value: "all" },
321
+ ...typeOptions.map((type) => ({ label: type, value: type })),
322
+ ]}
323
+ value={typeFilter}
324
+ />
325
+ </div>
326
+ </div>
327
+
328
+ <div className="trace-layout">
329
+ <div className="trace-map" aria-label={t("trace.aria.graph")}>
330
+ <TraceGraphView
331
+ nodes={visibleNodes}
332
+ direction={direction}
333
+ selectedNodeId={selectedNode?.id ?? null}
334
+ onSelectNode={setSelectedNodeId}
335
+ zoom={zoom}
336
+ onZoomChange={setZoom}
337
+ fitToken={fitToken}
338
+ emptyLabel={t("trace.noMatch")}
339
+ zoomLabels={{
340
+ controls: t("trace.aria.zoomControls"),
341
+ zoomIn: t("trace.aria.zoomIn"),
342
+ zoomOut: t("trace.aria.zoomOut"),
343
+ reset: t("trace.aria.resetZoom"),
344
+ }}
345
+ />
346
+ {allNodes.length > 0 ? (
347
+ <div className="trace-playback-bar" aria-label={t("trace.aria.playbackControls")}>
348
+ <button
349
+ aria-label={isPlaying ? t("trace.aria.pause") : t("trace.aria.play")}
350
+ className="trace-playback-bar__button"
351
+ onClick={togglePlayback}
352
+ type="button"
353
+ >
354
+ {isPlaying ? <Pause size={14} /> : <Play size={14} />}
355
+ </button>
356
+ <input
357
+ aria-label={t("trace.aria.playbackProgress")}
358
+ className="trace-playback-bar__slider"
359
+ max={allNodes.length}
360
+ min={0}
361
+ onChange={(e) => handleSliderChange(Number(e.target.value))}
362
+ type="range"
363
+ value={playbackIndex}
364
+ />
365
+ <span className="trace-playback-bar__count">
366
+ {playbackIndex} / {allNodes.length}
367
+ </span>
368
+ </div>
369
+ ) : null}
370
+ </div>
371
+
372
+ <article className="trace-detail">
373
+ <TraceNodeDetail node={selectedNode} onSelectNode={setSelectedNodeId} t={t} />
374
+ </article>
375
+ </div>
376
+ </>
377
+ ) : null}
378
+ </div>
379
+ </section>
380
+ );
381
+ }