@schlessera/brain-ui-react 0.34.0 → 0.35.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/README.md +12 -0
- package/dist/components/activity/activity-page.d.ts.map +1 -1
- package/dist/components/activity/activity-page.js +8 -149
- package/dist/components/activity/activity-page.js.map +1 -1
- package/dist/components/activity/activity-rollups.d.ts +5 -0
- package/dist/components/activity/activity-rollups.d.ts.map +1 -0
- package/dist/components/activity/activity-rollups.js +39 -0
- package/dist/components/activity/activity-rollups.js.map +1 -0
- package/dist/components/activity/activity-run-detail.d.ts +11 -0
- package/dist/components/activity/activity-run-detail.d.ts.map +1 -0
- package/dist/components/activity/activity-run-detail.js +111 -0
- package/dist/components/activity/activity-run-detail.js.map +1 -0
- package/dist/components/activity/activity-run-list.d.ts +15 -0
- package/dist/components/activity/activity-run-list.d.ts.map +1 -0
- package/dist/components/activity/activity-run-list.js +25 -0
- package/dist/components/activity/activity-run-list.js.map +1 -0
- package/dist/components/chat/brain-markdown-code.d.ts +4 -0
- package/dist/components/chat/brain-markdown-code.d.ts.map +1 -0
- package/dist/components/chat/brain-markdown-code.js +47 -0
- package/dist/components/chat/brain-markdown-code.js.map +1 -0
- package/dist/components/chat/brain-markdown-entities.d.ts +14 -0
- package/dist/components/chat/brain-markdown-entities.d.ts.map +1 -0
- package/dist/components/chat/brain-markdown-entities.js +93 -0
- package/dist/components/chat/brain-markdown-entities.js.map +1 -0
- package/dist/components/chat/brain-markdown-links.d.ts +36 -0
- package/dist/components/chat/brain-markdown-links.d.ts.map +1 -0
- package/dist/components/chat/brain-markdown-links.js +176 -0
- package/dist/components/chat/brain-markdown-links.js.map +1 -0
- package/dist/components/chat/brain-markdown-share.d.ts +15 -0
- package/dist/components/chat/brain-markdown-share.d.ts.map +1 -0
- package/dist/components/chat/brain-markdown-share.js +40 -0
- package/dist/components/chat/brain-markdown-share.js.map +1 -0
- package/dist/components/chat/brain-markdown.d.ts +1 -34
- package/dist/components/chat/brain-markdown.d.ts.map +1 -1
- package/dist/components/chat/brain-markdown.js +9 -402
- package/dist/components/chat/brain-markdown.js.map +1 -1
- package/dist/components/chat/composer.d.ts.map +1 -1
- package/dist/components/chat/composer.js +14 -1
- package/dist/components/chat/composer.js.map +1 -1
- package/dist/components/chat/session-drawer.d.ts.map +1 -1
- package/dist/components/chat/session-drawer.js +25 -10
- package/dist/components/chat/session-drawer.js.map +1 -1
- package/dist/components/chat/tool-views.d.ts +2 -2
- package/dist/components/chat/tool-views.d.ts.map +1 -1
- package/dist/components/chat/tool-views.js +2 -16
- package/dist/components/chat/tool-views.js.map +1 -1
- package/dist/components/chat/use-markdown-highlight.d.ts +12 -0
- package/dist/components/chat/use-markdown-highlight.d.ts.map +1 -0
- package/dist/components/chat/use-markdown-highlight.js +56 -0
- package/dist/components/chat/use-markdown-highlight.js.map +1 -0
- package/dist/components/connectivity/connection-gate.d.ts.map +1 -1
- package/dist/components/connectivity/connection-gate.js +94 -8
- package/dist/components/connectivity/connection-gate.js.map +1 -1
- package/dist/components/connectivity/connection-state.d.ts +12 -0
- package/dist/components/connectivity/connection-state.d.ts.map +1 -0
- package/dist/components/connectivity/connection-state.js +23 -0
- package/dist/components/connectivity/connection-state.js.map +1 -0
- package/dist/components/graph/graph-canvas-lazy.d.ts +2 -0
- package/dist/components/graph/graph-canvas-lazy.d.ts.map +1 -0
- package/dist/components/graph/graph-canvas-lazy.js +4 -0
- package/dist/components/graph/graph-canvas-lazy.js.map +1 -0
- package/dist/components/graph/graph-controls.js +4 -1
- package/dist/components/graph/graph-controls.js.map +1 -1
- package/dist/components/graph/graph-maintenance.d.ts +8 -0
- package/dist/components/graph/graph-maintenance.d.ts.map +1 -0
- package/dist/components/graph/graph-maintenance.js +115 -0
- package/dist/components/graph/graph-maintenance.js.map +1 -0
- package/dist/components/graph/graph-page.d.ts.map +1 -1
- package/dist/components/graph/graph-page.js +11 -306
- package/dist/components/graph/graph-page.js.map +1 -1
- package/dist/components/graph/graph-scene.d.ts +8 -0
- package/dist/components/graph/graph-scene.d.ts.map +1 -0
- package/dist/components/graph/graph-scene.js +208 -0
- package/dist/components/graph/graph-scene.js.map +1 -0
- package/dist/components/graph/graph-spinner.d.ts +2 -0
- package/dist/components/graph/graph-spinner.d.ts.map +1 -0
- package/dist/components/graph/graph-spinner.js +6 -0
- package/dist/components/graph/graph-spinner.js.map +1 -0
- package/dist/components/layout/app-shell.d.ts.map +1 -1
- package/dist/components/layout/app-shell.js +2 -1
- package/dist/components/layout/app-shell.js.map +1 -1
- package/dist/components/quick-actions/add-modal.d.ts.map +1 -1
- package/dist/components/quick-actions/add-modal.js +47 -4
- package/dist/components/quick-actions/add-modal.js.map +1 -1
- package/dist/components/quick-actions/search-modal.d.ts.map +1 -1
- package/dist/components/quick-actions/search-modal.js +46 -18
- package/dist/components/quick-actions/search-modal.js.map +1 -1
- package/dist/components/quick-actions/streaming-modal.d.ts.map +1 -1
- package/dist/components/quick-actions/streaming-modal.js +4 -1
- package/dist/components/quick-actions/streaming-modal.js.map +1 -1
- package/dist/components/settings/devices-agents-tab.d.ts +5 -0
- package/dist/components/settings/devices-agents-tab.d.ts.map +1 -0
- package/dist/components/settings/devices-agents-tab.js +96 -0
- package/dist/components/settings/devices-agents-tab.js.map +1 -0
- package/dist/components/settings/one-time-agent-credential.d.ts +3 -0
- package/dist/components/settings/one-time-agent-credential.d.ts.map +1 -0
- package/dist/components/settings/one-time-agent-credential.js +55 -0
- package/dist/components/settings/one-time-agent-credential.js.map +1 -0
- package/dist/components/settings/settings-panel.d.ts.map +1 -1
- package/dist/components/settings/settings-panel.js +16 -3
- package/dist/components/settings/settings-panel.js.map +1 -1
- package/dist/hooks/use-vpn-status.d.ts +6 -1
- package/dist/hooks/use-vpn-status.d.ts.map +1 -1
- package/dist/hooks/use-vpn-status.js +67 -30
- package/dist/hooks/use-vpn-status.js.map +1 -1
- package/dist/hooks/use-websocket.d.ts +3 -2
- package/dist/hooks/use-websocket.d.ts.map +1 -1
- package/dist/hooks/use-websocket.js +65 -254
- package/dist/hooks/use-websocket.js.map +1 -1
- package/dist/hooks/websocket-handlers/activity.d.ts +5 -0
- package/dist/hooks/websocket-handlers/activity.d.ts.map +1 -0
- package/dist/hooks/websocket-handlers/activity.js +10 -0
- package/dist/hooks/websocket-handlers/activity.js.map +1 -0
- package/dist/hooks/websocket-handlers/chat.d.ts +17 -0
- package/dist/hooks/websocket-handlers/chat.d.ts.map +1 -0
- package/dist/hooks/websocket-handlers/chat.js +175 -0
- package/dist/hooks/websocket-handlers/chat.js.map +1 -0
- package/dist/hooks/websocket-handlers/connection.d.ts +6 -0
- package/dist/hooks/websocket-handlers/connection.d.ts.map +1 -0
- package/dist/hooks/websocket-handlers/connection.js +27 -0
- package/dist/hooks/websocket-handlers/connection.js.map +1 -0
- package/dist/hooks/websocket-handlers/index.d.ts +4 -0
- package/dist/hooks/websocket-handlers/index.d.ts.map +1 -0
- package/dist/hooks/websocket-handlers/index.js +19 -0
- package/dist/hooks/websocket-handlers/index.js.map +1 -0
- package/dist/hooks/websocket-handlers/mask.d.ts +4 -0
- package/dist/hooks/websocket-handlers/mask.d.ts.map +1 -0
- package/dist/hooks/websocket-handlers/mask.js +14 -0
- package/dist/hooks/websocket-handlers/mask.js.map +1 -0
- package/dist/hooks/websocket-handlers/provider.d.ts +4 -0
- package/dist/hooks/websocket-handlers/provider.d.ts.map +1 -0
- package/dist/hooks/websocket-handlers/provider.js +31 -0
- package/dist/hooks/websocket-handlers/provider.js.map +1 -0
- package/dist/hooks/websocket-handlers/types.d.ts +24 -0
- package/dist/hooks/websocket-handlers/types.d.ts.map +1 -0
- package/dist/hooks/websocket-handlers/types.js +2 -0
- package/dist/hooks/websocket-handlers/types.js.map +1 -0
- package/dist/lib/api-client.d.ts +32 -0
- package/dist/lib/api-client.d.ts.map +1 -1
- package/dist/lib/api-client.js +11 -0
- package/dist/lib/api-client.js.map +1 -1
- package/dist/lib/format-time.d.ts +3 -0
- package/dist/lib/format-time.d.ts.map +1 -0
- package/dist/lib/format-time.js +25 -0
- package/dist/lib/format-time.js.map +1 -0
- package/dist/lib/push-registration.d.ts +7 -0
- package/dist/lib/push-registration.d.ts.map +1 -0
- package/dist/lib/push-registration.js +26 -0
- package/dist/lib/push-registration.js.map +1 -0
- package/dist/stores/activity-store-helpers.d.ts +50 -0
- package/dist/stores/activity-store-helpers.d.ts.map +1 -0
- package/dist/stores/activity-store-helpers.js +143 -0
- package/dist/stores/activity-store-helpers.js.map +1 -0
- package/dist/stores/activity-store-types.d.ts +48 -0
- package/dist/stores/activity-store-types.d.ts.map +1 -0
- package/dist/stores/activity-store-types.js +2 -0
- package/dist/stores/activity-store-types.js.map +1 -0
- package/dist/stores/activity-store.d.ts +2 -78
- package/dist/stores/activity-store.d.ts.map +1 -1
- package/dist/stores/activity-store.js +22 -142
- package/dist/stores/activity-store.js.map +1 -1
- package/dist/stores/connection-store.d.ts +14 -2
- package/dist/stores/connection-store.d.ts.map +1 -1
- package/dist/stores/connection-store.js +17 -0
- package/dist/stores/connection-store.js.map +1 -1
- package/dist/stores/file-store.d.ts.map +1 -1
- package/dist/stores/file-store.js +54 -18
- package/dist/stores/file-store.js.map +1 -1
- package/dist/stores/principal-store.d.ts +16 -0
- package/dist/stores/principal-store.d.ts.map +1 -0
- package/dist/stores/principal-store.js +29 -0
- package/dist/stores/principal-store.js.map +1 -0
- package/dist/stores/ui-store.d.ts +1 -1
- package/dist/stores/ui-store.d.ts.map +1 -1
- package/dist/styles.css +1 -1
- package/package.json +2 -2
- package/src/components/activity/activity-page.tsx +6 -477
- package/src/components/activity/activity-rollups.tsx +94 -0
- package/src/components/activity/activity-run-detail.tsx +301 -0
- package/src/components/activity/activity-run-list.tsx +97 -0
- package/src/components/chat/brain-markdown-code.tsx +60 -0
- package/src/components/chat/brain-markdown-entities.tsx +116 -0
- package/src/components/chat/brain-markdown-links.tsx +247 -0
- package/src/components/chat/brain-markdown-share.ts +47 -0
- package/src/components/chat/brain-markdown.tsx +27 -535
- package/src/components/chat/composer.tsx +14 -1
- package/src/components/chat/session-drawer.tsx +29 -10
- package/src/components/chat/tool-views.tsx +2 -13
- package/src/components/chat/use-markdown-highlight.ts +63 -0
- package/src/components/connectivity/connection-gate.tsx +128 -14
- package/src/components/connectivity/connection-state.ts +37 -0
- package/src/components/graph/graph-canvas-lazy.tsx +4 -0
- package/src/components/graph/graph-controls.tsx +4 -1
- package/src/components/graph/graph-maintenance.tsx +304 -0
- package/src/components/graph/graph-page.tsx +11 -840
- package/src/components/graph/graph-scene.tsx +548 -0
- package/src/components/graph/graph-spinner.tsx +9 -0
- package/src/components/layout/app-shell.tsx +2 -0
- package/src/components/quick-actions/add-modal.tsx +49 -4
- package/src/components/quick-actions/search-modal.tsx +46 -18
- package/src/components/quick-actions/streaming-modal.tsx +3 -1
- package/src/components/settings/devices-agents-tab.tsx +231 -0
- package/src/components/settings/one-time-agent-credential.tsx +131 -0
- package/src/components/settings/settings-panel.tsx +20 -5
- package/src/hooks/use-vpn-status.ts +71 -31
- package/src/hooks/use-websocket.ts +74 -275
- package/src/hooks/websocket-handlers/activity.ts +13 -0
- package/src/hooks/websocket-handlers/chat.ts +210 -0
- package/src/hooks/websocket-handlers/connection.ts +30 -0
- package/src/hooks/websocket-handlers/index.ts +59 -0
- package/src/hooks/websocket-handlers/mask.ts +17 -0
- package/src/hooks/websocket-handlers/provider.ts +35 -0
- package/src/hooks/websocket-handlers/types.ts +27 -0
- package/src/lib/api-client.ts +40 -1
- package/src/lib/format-time.ts +20 -0
- package/src/lib/push-registration.ts +29 -0
- package/src/stores/activity-store-helpers.ts +164 -0
- package/src/stores/activity-store-types.ts +55 -0
- package/src/stores/activity-store.ts +21 -217
- package/src/stores/connection-store.ts +33 -2
- package/src/stores/file-store.ts +42 -12
- package/src/stores/principal-store.ts +38 -0
- package/src/stores/ui-store.ts +1 -1
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import { Fragment, useEffect, useMemo, useState } from "react";
|
|
2
|
+
import { ArrowLeft, ChevronRight } from "lucide-react";
|
|
3
|
+
import { useShallow } from "zustand/react/shallow";
|
|
4
|
+
import { isFailureOutcome, type ActivitySpan } from "@schlessera/brain-ui-sdk/protocol";
|
|
5
|
+
|
|
6
|
+
import { api, type ActivityRunRollup } from "../../lib/api-client.js";
|
|
7
|
+
import {
|
|
8
|
+
useActivityStore,
|
|
9
|
+
narrativeEventsFor,
|
|
10
|
+
runEvents,
|
|
11
|
+
runSpans,
|
|
12
|
+
} from "../../stores/activity-store.js";
|
|
13
|
+
import { cn } from "../../lib/utils.js";
|
|
14
|
+
import { formatDuration, formatRelativeTime } from "../chat/tool-views.js";
|
|
15
|
+
import {
|
|
16
|
+
SpanEventBlock,
|
|
17
|
+
SpanPayload,
|
|
18
|
+
SpanStatusDot,
|
|
19
|
+
formatEffectiveCost,
|
|
20
|
+
formatSpanUsage,
|
|
21
|
+
spanToolLabel,
|
|
22
|
+
} from "./span-bits.js";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Chat-less run detail (cron runs; pruned runs show their rollup) — the full
|
|
26
|
+
* view of one run: its rollup header, the span tree with per-span usage,
|
|
27
|
+
* attributes and recorded events, and a raw-trace escape hatch for anything
|
|
28
|
+
* the rendered view does not have an opinion about.
|
|
29
|
+
*/
|
|
30
|
+
export function RunDetail({ runId, onBack }: { runId: string; onBack: () => void }) {
|
|
31
|
+
const streamed = useActivityStore(useShallow((s) => runSpans(s, runId)));
|
|
32
|
+
const events = useActivityStore(useShallow((s) => runEvents(s, runId)));
|
|
33
|
+
const applySnapshot = useActivityStore((s) => s.applySnapshot);
|
|
34
|
+
const [pruned, setPruned] = useState<object | null>(null);
|
|
35
|
+
const [rollup, setRollup] = useState<ActivityRunRollup | null>(null);
|
|
36
|
+
const [missing, setMissing] = useState(false);
|
|
37
|
+
const [rawOpen, setRawOpen] = useState(false);
|
|
38
|
+
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
let active = true;
|
|
41
|
+
api
|
|
42
|
+
// Payload bodies are excluded from run detail by default; this view's
|
|
43
|
+
// rows expand to them, so opt in.
|
|
44
|
+
.activityRun(runId, { includePayloads: true })
|
|
45
|
+
.then((detail) => {
|
|
46
|
+
if (!active) return;
|
|
47
|
+
if (detail.rollup) setRollup(detail.rollup);
|
|
48
|
+
if (detail.detailPruned) {
|
|
49
|
+
setPruned(detail.rollup ?? {});
|
|
50
|
+
} else if (detail.spans) {
|
|
51
|
+
applySnapshot({
|
|
52
|
+
type: "activity_snapshot",
|
|
53
|
+
view: "run",
|
|
54
|
+
runId,
|
|
55
|
+
spans: detail.spans,
|
|
56
|
+
events: detail.events ?? [],
|
|
57
|
+
highWaterSeq: { [runId]: detail.highWaterSeq ?? 0 },
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
})
|
|
61
|
+
.catch(() => {
|
|
62
|
+
if (active) setMissing(true);
|
|
63
|
+
});
|
|
64
|
+
return () => {
|
|
65
|
+
active = false;
|
|
66
|
+
};
|
|
67
|
+
}, [runId, applySnapshot]);
|
|
68
|
+
|
|
69
|
+
const root = streamed.find((s) => !s.parentSpanId);
|
|
70
|
+
const title = rollup?.jobName ?? rollup?.name ?? (root ? spanToolLabel(root) : runId);
|
|
71
|
+
// The root carries the run's usage for backend-recorded turns; a run whose
|
|
72
|
+
// usage only exists on children (the span-sink shape) sums them instead.
|
|
73
|
+
// Never both, so nothing is double-counted.
|
|
74
|
+
const totalUsage = useMemo(() => {
|
|
75
|
+
if (root?.usage) return formatSpanUsage(root);
|
|
76
|
+
const children = streamed.filter((s) => s.parentSpanId && s.usage);
|
|
77
|
+
if (children.length === 0) return null;
|
|
78
|
+
const sum = children.reduce(
|
|
79
|
+
(acc, s) => ({
|
|
80
|
+
inputTokens: acc.inputTokens + (s.usage?.inputTokens ?? 0),
|
|
81
|
+
outputTokens: acc.outputTokens + (s.usage?.outputTokens ?? 0),
|
|
82
|
+
cacheReadTokens: acc.cacheReadTokens + (s.usage?.cacheReadTokens ?? 0),
|
|
83
|
+
cacheCreationTokens: acc.cacheCreationTokens + (s.usage?.cacheCreationTokens ?? 0),
|
|
84
|
+
}),
|
|
85
|
+
{ inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheCreationTokens: 0 }
|
|
86
|
+
);
|
|
87
|
+
return formatSpanUsage({ ...children[0]!, usage: { ...sum, model: undefined } });
|
|
88
|
+
}, [root, streamed]);
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<div className="flex h-full flex-col overflow-y-auto">
|
|
92
|
+
<div className="flex items-center gap-2 border-b border-border-subtle px-4 py-3">
|
|
93
|
+
<button
|
|
94
|
+
type="button"
|
|
95
|
+
onClick={onBack}
|
|
96
|
+
className="rounded-md p-1.5 text-muted-foreground transition-colors hover:bg-surface-raised hover:text-foreground"
|
|
97
|
+
aria-label="Back"
|
|
98
|
+
>
|
|
99
|
+
<ArrowLeft className="h-4 w-4" />
|
|
100
|
+
</button>
|
|
101
|
+
<div className="min-w-0 flex-1">
|
|
102
|
+
<h1 className="truncate text-sm font-medium">{title}</h1>
|
|
103
|
+
<p className="truncate font-[family-name:var(--font-mono)] text-[10px] text-muted-foreground/60">
|
|
104
|
+
{runId}
|
|
105
|
+
</p>
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
<div className="mx-auto w-full max-w-3xl space-y-1 p-4">
|
|
109
|
+
{missing && <p className="text-xs text-muted-foreground">Unknown run.</p>}
|
|
110
|
+
{rollup && (
|
|
111
|
+
<div className="mb-2 flex flex-wrap items-center gap-x-3 gap-y-0.5 rounded-lg border border-border-subtle bg-surface px-3 py-2 text-[11px] text-muted-foreground">
|
|
112
|
+
<span className="text-foreground/80">{rollup.origin}</span>
|
|
113
|
+
{rollup.outcome && (
|
|
114
|
+
<span className={cn(isFailureOutcome(rollup.outcome) && "text-destructive")}>
|
|
115
|
+
{rollup.outcome}
|
|
116
|
+
</span>
|
|
117
|
+
)}
|
|
118
|
+
<span>{formatRelativeTime(rollup.startedAt)}</span>
|
|
119
|
+
{rollup.durationMs !== null && <span>{formatDuration(rollup.durationMs)}</span>}
|
|
120
|
+
<span>
|
|
121
|
+
list{" "}
|
|
122
|
+
<span className="font-[family-name:var(--font-mono)] text-foreground/80">
|
|
123
|
+
{formatEffectiveCost(rollup.costUsd)}
|
|
124
|
+
</span>
|
|
125
|
+
</span>
|
|
126
|
+
<span>
|
|
127
|
+
effective{" "}
|
|
128
|
+
<span className="font-[family-name:var(--font-mono)] text-foreground/80">
|
|
129
|
+
{formatEffectiveCost(
|
|
130
|
+
rollup.effectiveCostUsd,
|
|
131
|
+
rollup.pricingEstimate,
|
|
132
|
+
rollup.billingMode
|
|
133
|
+
)}
|
|
134
|
+
</span>
|
|
135
|
+
</span>
|
|
136
|
+
{rollup.billingMode && (
|
|
137
|
+
<span>{rollup.billingMode === "api" ? "API billed" : "subscription billed"}</span>
|
|
138
|
+
)}
|
|
139
|
+
{rollup.pricingEstimate && <span>~ estimated rates</span>}
|
|
140
|
+
{totalUsage && (
|
|
141
|
+
<span className="font-[family-name:var(--font-mono)] text-foreground/80">
|
|
142
|
+
{totalUsage}
|
|
143
|
+
</span>
|
|
144
|
+
)}
|
|
145
|
+
{rollup.failureReason && (
|
|
146
|
+
<span className="w-full whitespace-pre-wrap break-words pt-1 text-destructive/80">
|
|
147
|
+
{rollup.failureReason}
|
|
148
|
+
</span>
|
|
149
|
+
)}
|
|
150
|
+
</div>
|
|
151
|
+
)}
|
|
152
|
+
{pruned && (
|
|
153
|
+
<div className="rounded-lg border border-border-subtle bg-surface p-3 text-xs text-muted-foreground">
|
|
154
|
+
Detail pruned — only the rollup remains.
|
|
155
|
+
<pre className="mt-2 overflow-x-auto text-[11px]">{JSON.stringify(pruned, null, 2)}</pre>
|
|
156
|
+
</div>
|
|
157
|
+
)}
|
|
158
|
+
{streamed.map((span) => (
|
|
159
|
+
<DetailSpanRow key={span.spanId} span={span} depth={depthOf(span, streamed)} />
|
|
160
|
+
))}
|
|
161
|
+
{!pruned && !missing && streamed.length === 0 && (
|
|
162
|
+
<p className="text-xs text-muted-foreground">No spans recorded for this run.</p>
|
|
163
|
+
)}
|
|
164
|
+
{/* The escape hatch: whatever the rendered tree has no opinion about
|
|
165
|
+
is still readable here, so the view is never LESS than the trace. */}
|
|
166
|
+
{streamed.length > 0 && (
|
|
167
|
+
<div className="pt-3">
|
|
168
|
+
<button
|
|
169
|
+
type="button"
|
|
170
|
+
onClick={() => setRawOpen((v) => !v)}
|
|
171
|
+
className="flex items-center gap-1 text-[11px] text-muted-foreground transition-colors hover:text-foreground"
|
|
172
|
+
>
|
|
173
|
+
<ChevronRight
|
|
174
|
+
className={cn("h-3 w-3 transition-transform", rawOpen && "rotate-90")}
|
|
175
|
+
/>
|
|
176
|
+
Raw trace
|
|
177
|
+
</button>
|
|
178
|
+
{rawOpen && (
|
|
179
|
+
<pre className="mt-1 max-h-[60vh] overflow-auto rounded-lg border border-border-subtle bg-surface p-2 font-[family-name:var(--font-mono)] text-[10px] leading-relaxed text-muted-foreground">
|
|
180
|
+
{JSON.stringify({ runId, rollup, spans: streamed, events }, null, 2)}
|
|
181
|
+
</pre>
|
|
182
|
+
)}
|
|
183
|
+
</div>
|
|
184
|
+
)}
|
|
185
|
+
</div>
|
|
186
|
+
</div>
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* One span tree row. EVERY span expands — a tool span to its recorded payload
|
|
192
|
+
* (AE7), any span to its usage, attributes and recorded events. The uniform
|
|
193
|
+
* affordance is the point: a cron root or a turn root used to be a dead dot
|
|
194
|
+
* with a duration, which is strictly less than the trace it was summarizing.
|
|
195
|
+
*/
|
|
196
|
+
function DetailSpanRow({ span, depth }: { span: ActivitySpan; depth: number }) {
|
|
197
|
+
const [expanded, setExpanded] = useState(false);
|
|
198
|
+
const usage = formatSpanUsage(span);
|
|
199
|
+
// Approval wait is not execution — same boundary the subagent drill-in uses.
|
|
200
|
+
const duration =
|
|
201
|
+
span.endedAt !== undefined
|
|
202
|
+
? formatDuration(span.endedAt - (span.waitUntil ?? span.startedAt))
|
|
203
|
+
: "…";
|
|
204
|
+
return (
|
|
205
|
+
<div style={{ paddingLeft: `${depth * 16}px` }}>
|
|
206
|
+
<div
|
|
207
|
+
className="flex cursor-pointer items-center gap-2 text-xs text-muted-foreground hover:text-foreground"
|
|
208
|
+
onClick={() => setExpanded((v) => !v)}
|
|
209
|
+
>
|
|
210
|
+
<SpanStatusDot span={span} />
|
|
211
|
+
<span className="truncate font-[family-name:var(--font-mono)]">
|
|
212
|
+
{spanToolLabel(span)}
|
|
213
|
+
</span>
|
|
214
|
+
{span.kind !== "tool" && (
|
|
215
|
+
<span className="shrink-0 rounded bg-surface-raised px-1 text-[9px] uppercase text-muted-foreground/70">
|
|
216
|
+
{span.kind}
|
|
217
|
+
</span>
|
|
218
|
+
)}
|
|
219
|
+
{span.outcome && span.outcome !== "success" && (
|
|
220
|
+
<span className="text-[10px] uppercase">{span.outcome}</span>
|
|
221
|
+
)}
|
|
222
|
+
{span.outcomeReason && (
|
|
223
|
+
<span className="truncate text-[10px] text-muted-foreground/60">
|
|
224
|
+
{span.outcomeReason}
|
|
225
|
+
</span>
|
|
226
|
+
)}
|
|
227
|
+
<span className="ml-auto flex shrink-0 items-center gap-2 font-[family-name:var(--font-mono)] text-[10px] text-muted-foreground/50">
|
|
228
|
+
{usage && <span className="hidden sm:inline">{usage}</span>}
|
|
229
|
+
<span>{duration}</span>
|
|
230
|
+
</span>
|
|
231
|
+
<ChevronRight
|
|
232
|
+
className={cn("h-3 w-3 shrink-0 transition-transform", expanded && "rotate-90")}
|
|
233
|
+
/>
|
|
234
|
+
</div>
|
|
235
|
+
{expanded && <DetailSpanBody span={span} usage={usage} />}
|
|
236
|
+
</div>
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** Everything recorded against one span, under its row. */
|
|
241
|
+
function DetailSpanBody({ span, usage }: { span: ActivitySpan; usage: string | null }) {
|
|
242
|
+
const narrative = useActivityStore(useShallow((s) => narrativeEventsFor(s, span.spanId)));
|
|
243
|
+
const attrs = Object.entries(span.attrs ?? {});
|
|
244
|
+
return (
|
|
245
|
+
<div className="mb-1 ml-1 space-y-1.5 border-l border-border-subtle pl-2 pt-1">
|
|
246
|
+
<div className="flex flex-wrap gap-x-3 gap-y-0.5 px-2 text-[10px] text-muted-foreground/70">
|
|
247
|
+
<span>started {new Date(span.startedAt).toLocaleString()}</span>
|
|
248
|
+
{span.waitUntil !== undefined && (
|
|
249
|
+
<span>approval wait {formatDuration(span.waitUntil - span.startedAt)}</span>
|
|
250
|
+
)}
|
|
251
|
+
{usage && <span className="font-[family-name:var(--font-mono)]">{usage}</span>}
|
|
252
|
+
{span.usage?.costUsd !== undefined && (
|
|
253
|
+
<span className="font-[family-name:var(--font-mono)]">
|
|
254
|
+
{formatEffectiveCost(span.usage.costUsd)} list
|
|
255
|
+
</span>
|
|
256
|
+
)}
|
|
257
|
+
</div>
|
|
258
|
+
{span.subagent?.summary && (
|
|
259
|
+
<p className="px-2 text-[11px] text-muted-foreground">{span.subagent.summary}</p>
|
|
260
|
+
)}
|
|
261
|
+
{attrs.length > 0 && (
|
|
262
|
+
<dl className="grid grid-cols-[minmax(0,auto)_minmax(0,1fr)] gap-x-2 gap-y-0.5 px-2 text-[10px]">
|
|
263
|
+
{attrs.map(([key, value]) => (
|
|
264
|
+
<Fragment key={key}>
|
|
265
|
+
<dt className="truncate font-[family-name:var(--font-mono)] text-muted-foreground/60">
|
|
266
|
+
{key}
|
|
267
|
+
</dt>
|
|
268
|
+
<dd className="min-w-0 break-words font-[family-name:var(--font-mono)] text-muted-foreground">
|
|
269
|
+
{typeof value === "string" ? value : JSON.stringify(value)}
|
|
270
|
+
</dd>
|
|
271
|
+
</Fragment>
|
|
272
|
+
))}
|
|
273
|
+
</dl>
|
|
274
|
+
)}
|
|
275
|
+
{narrative.length > 0 && (
|
|
276
|
+
<div className="space-y-1.5 px-2">
|
|
277
|
+
{narrative.map((event) => (
|
|
278
|
+
<SpanEventBlock key={`${event.spanId}:${event.eventIndex}`} event={event} />
|
|
279
|
+
))}
|
|
280
|
+
</div>
|
|
281
|
+
)}
|
|
282
|
+
{span.kind === "tool" && <SpanPayload spanId={span.spanId} />}
|
|
283
|
+
{span.kind !== "tool" && narrative.length === 0 && attrs.length === 0 && (
|
|
284
|
+
<p className="px-2 text-[11px] text-muted-foreground/60">
|
|
285
|
+
Nothing else recorded for this step.
|
|
286
|
+
</p>
|
|
287
|
+
)}
|
|
288
|
+
</div>
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function depthOf(span: ActivitySpan, all: ActivitySpan[]): number {
|
|
293
|
+
let depth = 0;
|
|
294
|
+
let current: ActivitySpan | undefined = span;
|
|
295
|
+
while (current?.parentSpanId) {
|
|
296
|
+
depth += 1;
|
|
297
|
+
current = all.find((s) => s.spanId === current!.parentSpanId);
|
|
298
|
+
if (depth > 6) break;
|
|
299
|
+
}
|
|
300
|
+
return depth;
|
|
301
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { AlertTriangle, Bot, Clock, Timer } from "lucide-react";
|
|
2
|
+
import { useShallow } from "zustand/react/shallow";
|
|
3
|
+
import { isFailureOutcome, type ActivitySpan } from "@schlessera/brain-ui-sdk/protocol";
|
|
4
|
+
|
|
5
|
+
import type { ActivityRunSummary } from "../../lib/api-client.js";
|
|
6
|
+
import { useNow } from "../../hooks/use-now.js";
|
|
7
|
+
import { useActivityStore } from "../../stores/activity-store.js";
|
|
8
|
+
import { cn } from "../../lib/utils.js";
|
|
9
|
+
import { formatDuration, formatRelativeTime } from "../chat/tool-views.js";
|
|
10
|
+
import { runCostText, spanToolLabel } from "./span-bits.js";
|
|
11
|
+
|
|
12
|
+
export function LiveRow({
|
|
13
|
+
span,
|
|
14
|
+
onOpen,
|
|
15
|
+
}: {
|
|
16
|
+
span: ActivitySpan;
|
|
17
|
+
onOpen: (row: { runId: string; origin: string; sessionId?: string | null }) => void;
|
|
18
|
+
}) {
|
|
19
|
+
const children = useActivityStore(
|
|
20
|
+
useShallow((s) => Object.values(s.spans[span.runId] ?? {}).filter((x) => x.parentSpanId))
|
|
21
|
+
);
|
|
22
|
+
const now = useNow();
|
|
23
|
+
const current = children.filter((c) => c.outcome === undefined).at(-1);
|
|
24
|
+
const currentLabel = current ? spanToolLabel(current) : null;
|
|
25
|
+
return (
|
|
26
|
+
<button
|
|
27
|
+
type="button"
|
|
28
|
+
onClick={() => onOpen({ runId: span.runId, origin: span.origin, sessionId: span.sessionId })}
|
|
29
|
+
className="flex w-full items-center gap-2 rounded-lg border border-border-subtle bg-surface px-3 py-2 text-left text-xs transition-colors hover:bg-surface-raised"
|
|
30
|
+
>
|
|
31
|
+
<span className="h-2 w-2 shrink-0 animate-pulse rounded-full bg-primary" />
|
|
32
|
+
{span.origin === "cron" ? (
|
|
33
|
+
<Clock className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
|
|
34
|
+
) : (
|
|
35
|
+
<Bot className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
|
|
36
|
+
)}
|
|
37
|
+
<span className="truncate font-medium">
|
|
38
|
+
{span.jobName ?? span.name}
|
|
39
|
+
</span>
|
|
40
|
+
{/* A span-only run (plain cron job) has no current step — name+elapsed
|
|
41
|
+
is its whole live story. */}
|
|
42
|
+
{currentLabel && (
|
|
43
|
+
<span className="truncate text-muted-foreground">· {currentLabel}</span>
|
|
44
|
+
)}
|
|
45
|
+
<span className="ml-auto shrink-0 font-[family-name:var(--font-mono)] text-[10px] text-muted-foreground/60">
|
|
46
|
+
{formatDuration(now - span.startedAt)}
|
|
47
|
+
</span>
|
|
48
|
+
</button>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function RunRow({
|
|
53
|
+
run,
|
|
54
|
+
onOpen,
|
|
55
|
+
}: {
|
|
56
|
+
run: ActivityRunSummary;
|
|
57
|
+
onOpen: (row: ActivityRunSummary) => void;
|
|
58
|
+
}) {
|
|
59
|
+
const failed = isFailureOutcome(run.outcome);
|
|
60
|
+
// Effective cost only — list price lives on the Spend card and the detail
|
|
61
|
+
// view. "—" is unknown, never $0.00 (AE3); a pre-pricing server that never
|
|
62
|
+
// sent the field keeps the original list-cost span instead (see runCostText).
|
|
63
|
+
const cost = runCostText(run);
|
|
64
|
+
return (
|
|
65
|
+
<button
|
|
66
|
+
type="button"
|
|
67
|
+
onClick={() => onOpen(run)}
|
|
68
|
+
className={cn(
|
|
69
|
+
"flex w-full items-center gap-2 rounded-lg px-3 py-2 text-left text-xs transition-colors hover:bg-surface-raised",
|
|
70
|
+
failed && "border border-destructive/30 bg-destructive/5"
|
|
71
|
+
)}
|
|
72
|
+
>
|
|
73
|
+
{run.origin === "cron" ? (
|
|
74
|
+
<Clock className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
|
|
75
|
+
) : (
|
|
76
|
+
<Bot className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
|
|
77
|
+
)}
|
|
78
|
+
<span className="truncate">{run.jobName ?? run.name}</span>
|
|
79
|
+
{failed && <AlertTriangle className="h-3 w-3 shrink-0 text-destructive" />}
|
|
80
|
+
{run.outcome && run.outcome !== "success" && (
|
|
81
|
+
<span className={cn("text-[10px] uppercase", failed ? "text-destructive" : "text-muted-foreground")}>
|
|
82
|
+
{run.outcome}
|
|
83
|
+
</span>
|
|
84
|
+
)}
|
|
85
|
+
<span className="ml-auto flex shrink-0 items-center gap-2 font-[family-name:var(--font-mono)] text-[10px] text-muted-foreground/60">
|
|
86
|
+
{cost !== null && <span>{cost}</span>}
|
|
87
|
+
{run.durationMs !== null && (
|
|
88
|
+
<span className="flex items-center gap-0.5">
|
|
89
|
+
<Timer className="h-3 w-3" />
|
|
90
|
+
{formatDuration(run.durationMs)}
|
|
91
|
+
</span>
|
|
92
|
+
)}
|
|
93
|
+
<span>{formatRelativeTime(run.startedAt)}</span>
|
|
94
|
+
</span>
|
|
95
|
+
</button>
|
|
96
|
+
);
|
|
97
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import React, { useRef } from "react";
|
|
2
|
+
import { CopyButton } from "./copy-button.js";
|
|
3
|
+
import { MermaidBlock } from "./mermaid-block.js";
|
|
4
|
+
|
|
5
|
+
/** Concatenate all text descendants of a React node tree. */
|
|
6
|
+
function extractText(node: React.ReactNode): string {
|
|
7
|
+
if (typeof node === "string") return node;
|
|
8
|
+
if (typeof node === "number") return String(node);
|
|
9
|
+
if (Array.isArray(node)) return node.map(extractText).join("");
|
|
10
|
+
if (React.isValidElement(node)) {
|
|
11
|
+
return extractText((node.props as { children?: React.ReactNode }).children);
|
|
12
|
+
}
|
|
13
|
+
return "";
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* If a <pre>'s children are a ```mermaid / ```mmd fence, recover the raw
|
|
18
|
+
* diagram source. Text is extracted recursively because rehype-highlight
|
|
19
|
+
* and the linkify pass may have wrapped parts of it in elements.
|
|
20
|
+
*/
|
|
21
|
+
function mermaidSourceFrom(children: React.ReactNode): string | null {
|
|
22
|
+
for (const child of React.Children.toArray(children)) {
|
|
23
|
+
if (!React.isValidElement(child)) continue;
|
|
24
|
+
const props = child.props as { className?: string; children?: React.ReactNode };
|
|
25
|
+
if (typeof props.className === "string" && /\blanguage-(?:mermaid|mmd)\b/.test(props.className)) {
|
|
26
|
+
return extractText(props.children).replace(/\n$/, "");
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function MarkdownPre({ children, ...props }: React.ComponentPropsWithoutRef<"pre">) {
|
|
33
|
+
const mermaid = mermaidSourceFrom(children);
|
|
34
|
+
if (mermaid !== null) return <MermaidBlock source={mermaid} />;
|
|
35
|
+
return <CodePre {...props}>{children}</CodePre>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function CodePre({ children, ...props }: React.ComponentPropsWithoutRef<"pre">) {
|
|
39
|
+
const ref = useRef<HTMLPreElement>(null);
|
|
40
|
+
return (
|
|
41
|
+
<div className="group relative">
|
|
42
|
+
<pre
|
|
43
|
+
ref={ref}
|
|
44
|
+
className="overflow-x-auto rounded-lg border border-border bg-surface p-4 font-[family-name:var(--font-mono)] text-[13px] leading-relaxed"
|
|
45
|
+
{...props}
|
|
46
|
+
>
|
|
47
|
+
{children}
|
|
48
|
+
</pre>
|
|
49
|
+
<CopyButton getText={() => ref.current?.textContent ?? ""} />
|
|
50
|
+
</div>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function MarkdownTable({ children, ...props }: React.ComponentPropsWithoutRef<"table">) {
|
|
55
|
+
return (
|
|
56
|
+
<div className="overflow-x-auto rounded-lg border border-border">
|
|
57
|
+
<table {...props}>{children}</table>
|
|
58
|
+
</div>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { renderBarePathsInText } from "./brain-markdown-links.js";
|
|
3
|
+
|
|
4
|
+
const ENTITY_TAGS: Record<string, string> = {
|
|
5
|
+
co: "entity-co",
|
|
6
|
+
p: "entity-p",
|
|
7
|
+
proj: "entity-proj",
|
|
8
|
+
ev: "entity-ev",
|
|
9
|
+
d: "entity-d",
|
|
10
|
+
st: "entity-st",
|
|
11
|
+
f: "entity-f",
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
// Use a unicode marker that won't appear in normal text to wrap entities
|
|
15
|
+
// so they survive markdown parsing without relying on rehypeRaw
|
|
16
|
+
const ENTITY_START = "\u200B\u200B"; // zero-width spaces as delimiter
|
|
17
|
+
const ENTITY_SEP = "\u200B";
|
|
18
|
+
const ENTITY_END = "\u200B\u200B\u200B";
|
|
19
|
+
|
|
20
|
+
export function renderEntityTags(md: string): string {
|
|
21
|
+
// Convert entity tags to marker-based format:
|
|
22
|
+
// START tag SEP content END (START/SEP/END are ZWSP runs, see above)
|
|
23
|
+
let result = md.replace(
|
|
24
|
+
/<(co|p|proj|ev|d|st|f)>([\s\S]*?)<\/\1>/g,
|
|
25
|
+
(_match, tag, content) => `${ENTITY_START}${tag}${ENTITY_SEP}${content}${ENTITY_END}`
|
|
26
|
+
);
|
|
27
|
+
// Clean up any orphaned/unmatched entity tags
|
|
28
|
+
result = result.replace(/<\/?(?:co|p|proj|ev|d|st|f)>/g, "");
|
|
29
|
+
|
|
30
|
+
// Log and strip any remaining non-standard HTML-like tags the LLM may have output
|
|
31
|
+
result = result.replace(/<\/?([a-z][a-z0-9]*)\b[^>]*>/gi, (match, tag) => {
|
|
32
|
+
const allowed = [
|
|
33
|
+
"span", "strong", "em", "b", "i", "a", "br", "hr", "p",
|
|
34
|
+
"h1", "h2", "h3", "h4", "h5", "h6",
|
|
35
|
+
"ul", "ol", "li", "code", "pre", "blockquote",
|
|
36
|
+
"table", "thead", "tbody", "tr", "th", "td",
|
|
37
|
+
"div", "img", "del", "sup", "sub",
|
|
38
|
+
];
|
|
39
|
+
if (allowed.includes(tag.toLowerCase())) return match;
|
|
40
|
+
console.warn("[entity-tags] Stripping unrecognized tag:", match);
|
|
41
|
+
return "";
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
return result;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Parse entity markers in a text string. Each entity content is run through
|
|
49
|
+
* `innerTransform` so file-path linkification (or any other inline transform)
|
|
50
|
+
* applies inside `<f>...</f>` spans too.
|
|
51
|
+
*/
|
|
52
|
+
export function renderEntitiesInText(
|
|
53
|
+
text: string,
|
|
54
|
+
innerTransform: (s: string) => React.ReactNode = (s) => s
|
|
55
|
+
): React.ReactNode[] {
|
|
56
|
+
const pattern = new RegExp(
|
|
57
|
+
`${ENTITY_START}(co|p|proj|ev|d|st|f)${ENTITY_SEP}(.*?)${ENTITY_END}`,
|
|
58
|
+
"g"
|
|
59
|
+
);
|
|
60
|
+
const nodes: React.ReactNode[] = [];
|
|
61
|
+
let lastIndex = 0;
|
|
62
|
+
let match;
|
|
63
|
+
let key = 0;
|
|
64
|
+
|
|
65
|
+
while ((match = pattern.exec(text)) !== null) {
|
|
66
|
+
if (match.index > lastIndex) {
|
|
67
|
+
nodes.push(innerTransform(text.slice(lastIndex, match.index)));
|
|
68
|
+
}
|
|
69
|
+
const cls = ENTITY_TAGS[match[1]] || "";
|
|
70
|
+
nodes.push(
|
|
71
|
+
<span key={key++} className={cls}>
|
|
72
|
+
{innerTransform(match[2])}
|
|
73
|
+
</span>
|
|
74
|
+
);
|
|
75
|
+
lastIndex = pattern.lastIndex;
|
|
76
|
+
}
|
|
77
|
+
if (lastIndex < text.length) {
|
|
78
|
+
nodes.push(innerTransform(text.slice(lastIndex)));
|
|
79
|
+
}
|
|
80
|
+
return nodes;
|
|
81
|
+
}
|
|
82
|
+
/** Process a text string for entity markers and/or bare file paths. */
|
|
83
|
+
function transformTextString(
|
|
84
|
+
text: string,
|
|
85
|
+
opts: { entityTags: boolean; fileLinks: boolean }
|
|
86
|
+
): React.ReactNode {
|
|
87
|
+
const linkify = opts.fileLinks
|
|
88
|
+
? (s: string): React.ReactNode => {
|
|
89
|
+
if (!s) return s;
|
|
90
|
+
const parts = renderBarePathsInText(s);
|
|
91
|
+
if (parts.length === 1 && typeof parts[0] === "string") return parts[0];
|
|
92
|
+
return <>{parts}</>;
|
|
93
|
+
}
|
|
94
|
+
: (s: string): React.ReactNode => s;
|
|
95
|
+
|
|
96
|
+
if (opts.entityTags && text.includes(ENTITY_START)) {
|
|
97
|
+
return <>{renderEntitiesInText(text, opts.fileLinks ? (s) => linkify(s) : undefined)}</>;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return linkify(text);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Recursively process React children to expand entity markers / file paths in text nodes */
|
|
104
|
+
export function processChildText(children: React.ReactNode, opts: { entityTags: boolean; fileLinks: boolean }): React.ReactNode {
|
|
105
|
+
if (typeof children === "string") {
|
|
106
|
+
return transformTextString(children, opts);
|
|
107
|
+
}
|
|
108
|
+
if (Array.isArray(children)) {
|
|
109
|
+
return children.map((child, i) =>
|
|
110
|
+
typeof child === "string"
|
|
111
|
+
? <React.Fragment key={i}>{transformTextString(child, opts)}</React.Fragment>
|
|
112
|
+
: child
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
return children;
|
|
116
|
+
}
|