@sentry/junior-dashboard 0.58.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/LICENSE +201 -0
- package/dist/app.d.ts +22 -0
- package/dist/app.js +492 -0
- package/dist/auth.d.ts +30 -0
- package/dist/client/App.d.ts +2 -0
- package/dist/client/api.d.ts +10 -0
- package/dist/client/code.d.ts +15 -0
- package/dist/client/components/CommandRail.d.ts +6 -0
- package/dist/client/components/ConversationList.d.ts +7 -0
- package/dist/client/components/ConversationRowStats.d.ts +6 -0
- package/dist/client/components/ConversationStack.d.ts +5 -0
- package/dist/client/components/ConversationSummary.d.ts +5 -0
- package/dist/client/components/EmptyTelemetry.d.ts +5 -0
- package/dist/client/components/FilterTabs.d.ts +6 -0
- package/dist/client/components/JuniorLogo.d.ts +2 -0
- package/dist/client/components/LoadingView.d.ts +4 -0
- package/dist/client/components/Section.d.ts +6 -0
- package/dist/client/components/SectionHeader.d.ts +6 -0
- package/dist/client/components/SectionTitle.d.ts +5 -0
- package/dist/client/components/StatusBadge.d.ts +6 -0
- package/dist/client/components/ToolFrame.d.ts +10 -0
- package/dist/client/components/Transcript.d.ts +5 -0
- package/dist/client/components/TranscriptHeader.d.ts +7 -0
- package/dist/client/components/TranscriptLoading.d.ts +2 -0
- package/dist/client/components/TranscriptText.d.ts +6 -0
- package/dist/client/components/TranscriptToolView.d.ts +9 -0
- package/dist/client/components/TranscriptTurn.d.ts +8 -0
- package/dist/client/components/TurnDurationChart.d.ts +6 -0
- package/dist/client/components/statusStyles.d.ts +3 -0
- package/dist/client/components/transcriptPreview.d.ts +4 -0
- package/dist/client/components/transcriptRenderModel.d.ts +30 -0
- package/dist/client/components/transcriptStyles.d.ts +4 -0
- package/dist/client/format.d.ts +64 -0
- package/dist/client/pages/CommandCenter.d.ts +6 -0
- package/dist/client/pages/ConversationPage.d.ts +5 -0
- package/dist/client/pages/ConversationsPage.d.ts +5 -0
- package/dist/client/styles.d.ts +2 -0
- package/dist/client/types.d.ts +153 -0
- package/dist/client.d.ts +7 -0
- package/dist/client.js +56012 -0
- package/dist/config.d.ts +4 -0
- package/dist/handler.d.ts +2 -0
- package/dist/handler.js +563 -0
- package/dist/nitro.d.ts +16 -0
- package/dist/nitro.js +80 -0
- package/dist/tailwind.css +2 -0
- package/package.json +60 -0
- package/src/app.ts +421 -0
- package/src/auth.ts +202 -0
- package/src/client/App.tsx +175 -0
- package/src/client/api.ts +99 -0
- package/src/client/code.tsx +139 -0
- package/src/client/components/CommandRail.tsx +74 -0
- package/src/client/components/ConversationList.tsx +94 -0
- package/src/client/components/ConversationRowStats.tsx +21 -0
- package/src/client/components/ConversationStack.tsx +67 -0
- package/src/client/components/ConversationSummary.tsx +40 -0
- package/src/client/components/EmptyTelemetry.tsx +11 -0
- package/src/client/components/FilterTabs.tsx +35 -0
- package/src/client/components/JuniorLogo.tsx +8 -0
- package/src/client/components/LoadingView.tsx +16 -0
- package/src/client/components/Section.tsx +17 -0
- package/src/client/components/SectionHeader.tsx +16 -0
- package/src/client/components/SectionTitle.tsx +10 -0
- package/src/client/components/StatusBadge.tsx +40 -0
- package/src/client/components/ToolFrame.tsx +52 -0
- package/src/client/components/Transcript.tsx +39 -0
- package/src/client/components/TranscriptHeader.tsx +52 -0
- package/src/client/components/TranscriptLoading.tsx +10 -0
- package/src/client/components/TranscriptText.tsx +45 -0
- package/src/client/components/TranscriptToolView.tsx +207 -0
- package/src/client/components/TranscriptTurn.tsx +487 -0
- package/src/client/components/TurnDurationChart.tsx +349 -0
- package/src/client/components/statusStyles.ts +9 -0
- package/src/client/components/transcriptPreview.ts +20 -0
- package/src/client/components/transcriptRenderModel.ts +193 -0
- package/src/client/components/transcriptStyles.ts +11 -0
- package/src/client/format.ts +628 -0
- package/src/client/pages/CommandCenter.tsx +37 -0
- package/src/client/pages/ConversationPage.tsx +145 -0
- package/src/client/pages/ConversationsPage.tsx +59 -0
- package/src/client/styles.ts +6 -0
- package/src/client/types.ts +153 -0
- package/src/client.tsx +79 -0
- package/src/config.ts +72 -0
- package/src/handler.ts +26 -0
- package/src/nitro.ts +110 -0
- package/src/tailwind.css +13 -0
- package/src/virtual-modules.d.ts +5 -0
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
import { HighlightedCode } from "../code";
|
|
4
|
+
import {
|
|
5
|
+
formatBytes,
|
|
6
|
+
formatMessageTimestamp,
|
|
7
|
+
formatMs,
|
|
8
|
+
stringifyPartValue,
|
|
9
|
+
} from "../format";
|
|
10
|
+
import { cn } from "../styles";
|
|
11
|
+
import type { TranscriptPart } from "../types";
|
|
12
|
+
import { ToolFrame } from "./ToolFrame";
|
|
13
|
+
import { isPreviewableValue } from "./transcriptPreview";
|
|
14
|
+
|
|
15
|
+
/** Render a tool call/result pair in rich or raw transcript mode. */
|
|
16
|
+
export function TranscriptToolView(props: {
|
|
17
|
+
call?: TranscriptPart;
|
|
18
|
+
result?: TranscriptPart;
|
|
19
|
+
resultTimestamp?: number;
|
|
20
|
+
timestamp?: number;
|
|
21
|
+
view?: "raw" | "rich";
|
|
22
|
+
}) {
|
|
23
|
+
const toolName =
|
|
24
|
+
props.call?.name ??
|
|
25
|
+
props.result?.name ??
|
|
26
|
+
props.call?.id ??
|
|
27
|
+
props.result?.id ??
|
|
28
|
+
"unknown";
|
|
29
|
+
const input = props.call?.input;
|
|
30
|
+
const output = props.result?.output;
|
|
31
|
+
const outputBytes = props.result
|
|
32
|
+
? new TextEncoder().encode(stringifyPartValue(output)).length
|
|
33
|
+
: undefined;
|
|
34
|
+
const duration =
|
|
35
|
+
typeof props.timestamp === "number" &&
|
|
36
|
+
typeof props.resultTimestamp === "number" &&
|
|
37
|
+
props.resultTimestamp >= props.timestamp
|
|
38
|
+
? formatMs(props.resultTimestamp - props.timestamp)
|
|
39
|
+
: undefined;
|
|
40
|
+
const meta = [
|
|
41
|
+
props.timestamp ? formatMessageTimestamp(props.timestamp) : undefined,
|
|
42
|
+
duration,
|
|
43
|
+
props.result ? formatBytes(outputBytes) : undefined,
|
|
44
|
+
props.result ? undefined : "missing result",
|
|
45
|
+
].filter(isString);
|
|
46
|
+
const args = <ToolArgumentsPreview input={input} />;
|
|
47
|
+
|
|
48
|
+
if (props.view === "raw") {
|
|
49
|
+
return (
|
|
50
|
+
<ToolFrame
|
|
51
|
+
meta={meta}
|
|
52
|
+
raw
|
|
53
|
+
signature={
|
|
54
|
+
<strong className="min-w-0 break-words font-bold text-white">
|
|
55
|
+
{toolName}
|
|
56
|
+
</strong>
|
|
57
|
+
}
|
|
58
|
+
>
|
|
59
|
+
<ToolBodySection>
|
|
60
|
+
<HighlightedCode
|
|
61
|
+
code={stringifyPartValue({
|
|
62
|
+
call: props.call,
|
|
63
|
+
result: props.result,
|
|
64
|
+
})}
|
|
65
|
+
language="json"
|
|
66
|
+
/>
|
|
67
|
+
</ToolBodySection>
|
|
68
|
+
</ToolFrame>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<ToolFrame
|
|
74
|
+
meta={meta}
|
|
75
|
+
signature={
|
|
76
|
+
<>
|
|
77
|
+
<strong className="min-w-0 break-words font-bold text-white">
|
|
78
|
+
{toolName}
|
|
79
|
+
</strong>
|
|
80
|
+
{isPreviewableValue(input) ? (
|
|
81
|
+
<code className="min-w-0 break-words font-[inherit] text-[#b8b8b8]">
|
|
82
|
+
({args})
|
|
83
|
+
</code>
|
|
84
|
+
) : null}
|
|
85
|
+
</>
|
|
86
|
+
}
|
|
87
|
+
>
|
|
88
|
+
{props.call ? (
|
|
89
|
+
<ToolBodySection label="arguments">
|
|
90
|
+
<HighlightedCode
|
|
91
|
+
code={stringifyPartValue(input) || "{}"}
|
|
92
|
+
language="json"
|
|
93
|
+
/>
|
|
94
|
+
</ToolBodySection>
|
|
95
|
+
) : null}
|
|
96
|
+
{props.result ? (
|
|
97
|
+
<ToolBodySection label="result">
|
|
98
|
+
<HighlightedCode
|
|
99
|
+
code={stringifyPartValue(output) || "{}"}
|
|
100
|
+
language="json"
|
|
101
|
+
/>
|
|
102
|
+
</ToolBodySection>
|
|
103
|
+
) : null}
|
|
104
|
+
</ToolFrame>
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function ToolBodySection(props: {
|
|
109
|
+
children: ReactNode;
|
|
110
|
+
label?: string;
|
|
111
|
+
padded?: boolean;
|
|
112
|
+
}) {
|
|
113
|
+
return (
|
|
114
|
+
<div
|
|
115
|
+
className={cn(
|
|
116
|
+
"border-t border-white/10 px-3",
|
|
117
|
+
props.padded === false ? "" : "py-3",
|
|
118
|
+
)}
|
|
119
|
+
>
|
|
120
|
+
{props.label ? (
|
|
121
|
+
<div className="pb-2 font-mono text-[0.78rem] uppercase leading-none text-[#888]">
|
|
122
|
+
{props.label}
|
|
123
|
+
</div>
|
|
124
|
+
) : null}
|
|
125
|
+
{props.children}
|
|
126
|
+
</div>
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function ToolArgumentsPreview(props: { input: unknown }) {
|
|
131
|
+
const input = props.input;
|
|
132
|
+
if (input == null || input === "") return null;
|
|
133
|
+
|
|
134
|
+
if (typeof input === "string") {
|
|
135
|
+
const formatted = stringifyPartValue(input).replace(/\s+/g, " ").trim();
|
|
136
|
+
return <ToolArgValue value={truncateText(formatted, 96)} />;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (Array.isArray(input)) {
|
|
140
|
+
return (
|
|
141
|
+
<ToolArgValue
|
|
142
|
+
value={truncateText(
|
|
143
|
+
stringifyPartValue(input).replace(/\s+/g, " ").trim(),
|
|
144
|
+
96,
|
|
145
|
+
)}
|
|
146
|
+
/>
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (typeof input === "object") {
|
|
151
|
+
const entries = Object.entries(input as Record<string, unknown>).slice(
|
|
152
|
+
0,
|
|
153
|
+
4,
|
|
154
|
+
);
|
|
155
|
+
return (
|
|
156
|
+
<>
|
|
157
|
+
{entries.map(([key, value], index) => (
|
|
158
|
+
<ToolArgEntry
|
|
159
|
+
index={index}
|
|
160
|
+
key={key}
|
|
161
|
+
name={key}
|
|
162
|
+
value={previewArgumentValue(value)}
|
|
163
|
+
/>
|
|
164
|
+
))}
|
|
165
|
+
</>
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return <ToolArgValue value={truncateText(String(input), 96)} />;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function ToolArgEntry(props: { index: number; name: string; value: string }) {
|
|
173
|
+
return (
|
|
174
|
+
<span>
|
|
175
|
+
{props.index > 0 ? <span className="text-[#888]">, </span> : null}
|
|
176
|
+
<span className="text-white">{props.name}</span>
|
|
177
|
+
<span className="text-[#888]">: </span>
|
|
178
|
+
<ToolArgValue value={props.value} />
|
|
179
|
+
</span>
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function ToolArgValue(props: { value: string }) {
|
|
184
|
+
return <span className="text-[#b8b8b8]">{props.value}</span>;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function previewArgumentValue(value: unknown): string {
|
|
188
|
+
if (value == null) return "null";
|
|
189
|
+
if (typeof value === "string") return JSON.stringify(truncateText(value, 48));
|
|
190
|
+
if (typeof value === "number" || typeof value === "boolean") {
|
|
191
|
+
return String(value);
|
|
192
|
+
}
|
|
193
|
+
return truncateText(
|
|
194
|
+
stringifyPartValue(value).replace(/\s+/g, " ").trim(),
|
|
195
|
+
48,
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function truncateText(value: string, maxLength: number): string {
|
|
200
|
+
return value.length > maxLength
|
|
201
|
+
? `${value.slice(0, Math.max(0, maxLength - 3))}...`
|
|
202
|
+
: value;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function isString(value: string | undefined): value is string {
|
|
206
|
+
return typeof value === "string" && value.length > 0;
|
|
207
|
+
}
|
|
@@ -0,0 +1,487 @@
|
|
|
1
|
+
import type { ClipboardEventHandler, ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
import { HighlightedCode } from "../code";
|
|
4
|
+
import {
|
|
5
|
+
detectLanguage,
|
|
6
|
+
formatBytes,
|
|
7
|
+
formatMessageOffset,
|
|
8
|
+
formatMessageTimestamp,
|
|
9
|
+
formatMs,
|
|
10
|
+
formatUsage,
|
|
11
|
+
requesterLabel,
|
|
12
|
+
stringifyPartValue,
|
|
13
|
+
unavailableTranscriptLabel,
|
|
14
|
+
visualStatusForSession,
|
|
15
|
+
} from "../format";
|
|
16
|
+
import { cn } from "../styles";
|
|
17
|
+
import type {
|
|
18
|
+
ConversationTurn,
|
|
19
|
+
TranscriptMessage,
|
|
20
|
+
TranscriptPart,
|
|
21
|
+
} from "../types";
|
|
22
|
+
import { StatusBadge } from "./StatusBadge";
|
|
23
|
+
import { ToolFrame, toolFrameClass } from "./ToolFrame";
|
|
24
|
+
import { TranscriptText } from "./TranscriptText";
|
|
25
|
+
import { TranscriptToolView } from "./TranscriptToolView";
|
|
26
|
+
import {
|
|
27
|
+
countRenderedTranscriptChildren,
|
|
28
|
+
groupTranscriptMessages,
|
|
29
|
+
groupTranscriptParts,
|
|
30
|
+
messageRawText,
|
|
31
|
+
type RenderedTranscriptPart,
|
|
32
|
+
type TranscriptViewMode,
|
|
33
|
+
} from "./transcriptRenderModel";
|
|
34
|
+
import {
|
|
35
|
+
transcriptEmptyClass,
|
|
36
|
+
mutedTranscriptMetaClass,
|
|
37
|
+
} from "./transcriptStyles";
|
|
38
|
+
import { previewToolValue } from "./transcriptPreview";
|
|
39
|
+
|
|
40
|
+
/** Render one conversation turn as actor messages and tool events. */
|
|
41
|
+
export function TurnTranscript(props: {
|
|
42
|
+
number: number;
|
|
43
|
+
turn: ConversationTurn;
|
|
44
|
+
view: TranscriptViewMode;
|
|
45
|
+
}) {
|
|
46
|
+
const status = visualStatusForSession(props.turn);
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<section className="grid min-w-0 grid-cols-[0.875rem_minmax(0,1fr)] gap-3 border-t border-white/10 py-4 first:border-t-0">
|
|
50
|
+
<div className="flex flex-col items-center pt-2" aria-hidden="true">
|
|
51
|
+
<span className={turnMarkerClass(status)} />
|
|
52
|
+
<span className="mt-2 w-px flex-1 bg-[#beaaff]/20" />
|
|
53
|
+
</div>
|
|
54
|
+
<div className="min-w-0">
|
|
55
|
+
<TurnHeader number={props.number} turn={props.turn} />
|
|
56
|
+
<TurnEvents turn={props.turn} view={props.view} />
|
|
57
|
+
</div>
|
|
58
|
+
</section>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function turnMarkerClass(
|
|
63
|
+
status: ReturnType<typeof visualStatusForSession>,
|
|
64
|
+
): string {
|
|
65
|
+
return cn(
|
|
66
|
+
"size-2.5 shrink-0 border",
|
|
67
|
+
status === "active" && "border-emerald-300 bg-emerald-300",
|
|
68
|
+
status === "hung" && "border-amber-300 bg-amber-300",
|
|
69
|
+
status === "failed" && "border-rose-300 bg-rose-300",
|
|
70
|
+
status === "idle" && "border-[#beaaff]/70 bg-[#beaaff]/50",
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
type TranscriptRoleKind = "assistant" | "other" | "system" | "tool" | "user";
|
|
75
|
+
|
|
76
|
+
function transcriptRoleKind(role: string): TranscriptRoleKind {
|
|
77
|
+
const normalized = role.toLowerCase();
|
|
78
|
+
if (normalized === "assistant") return "assistant";
|
|
79
|
+
if (normalized === "user") return "user";
|
|
80
|
+
if (normalized === "system") return "system";
|
|
81
|
+
if (normalized.includes("tool")) return "tool";
|
|
82
|
+
return "other";
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function transcriptRoleLabel(role: string, turn: ConversationTurn): string {
|
|
86
|
+
const kind = transcriptRoleKind(role);
|
|
87
|
+
if (kind === "assistant") return "Junior";
|
|
88
|
+
if (kind === "user") return turnActorLabel(turn);
|
|
89
|
+
if (kind === "system") return "System";
|
|
90
|
+
if (kind === "tool") return "Tool";
|
|
91
|
+
return role;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function transcriptMessageClass(role: string): string {
|
|
95
|
+
const kind = transcriptRoleKind(role);
|
|
96
|
+
|
|
97
|
+
return cn(
|
|
98
|
+
"grid min-w-0 gap-2 border-l-4 py-2 pl-3",
|
|
99
|
+
kind === "assistant" &&
|
|
100
|
+
"border-l-violet-300 bg-[rgba(190,170,255,0.08)] pr-3 text-white",
|
|
101
|
+
kind === "user" && "border-l-white/70 bg-white/[0.04] pr-3 text-[#f4f4f4]",
|
|
102
|
+
kind === "system" &&
|
|
103
|
+
"border-l-amber-300 bg-amber-300/[0.06] pr-3 text-[#f4f4f4]",
|
|
104
|
+
kind === "tool" && "border-l-[#888] text-[#b8b8b8]",
|
|
105
|
+
kind === "other" && "border-l-white/35 text-[#f4f4f4]",
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function transcriptRoleClass(role: string): string {
|
|
110
|
+
const kind = transcriptRoleKind(role);
|
|
111
|
+
|
|
112
|
+
return cn(
|
|
113
|
+
"flex flex-wrap items-baseline gap-2 text-[0.88rem] leading-snug",
|
|
114
|
+
kind === "assistant" && "text-[#d8ccff]",
|
|
115
|
+
kind === "user" && "text-white",
|
|
116
|
+
kind === "system" && "text-amber-200",
|
|
117
|
+
kind === "tool" && "text-[#b8b8b8]",
|
|
118
|
+
kind === "other" && "text-[#f4f4f4]",
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function transcriptRoleLabelClass(role: string): string {
|
|
123
|
+
const kind = transcriptRoleKind(role);
|
|
124
|
+
|
|
125
|
+
return cn(
|
|
126
|
+
"inline-block max-w-full break-all text-[0.98rem] font-extrabold leading-tight",
|
|
127
|
+
kind === "assistant" && "text-violet-200",
|
|
128
|
+
kind === "user" && "text-white",
|
|
129
|
+
kind === "system" && "text-amber-200",
|
|
130
|
+
kind === "tool" && "text-[#b8b8b8]",
|
|
131
|
+
kind === "other" && "text-white",
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function TranscriptMessageShell(props: {
|
|
136
|
+
children: ReactNode;
|
|
137
|
+
onCopy?: ClipboardEventHandler<HTMLElement>;
|
|
138
|
+
role: string;
|
|
139
|
+
}) {
|
|
140
|
+
return (
|
|
141
|
+
<article
|
|
142
|
+
className={transcriptMessageClass(props.role)}
|
|
143
|
+
onCopy={props.onCopy}
|
|
144
|
+
>
|
|
145
|
+
{props.children}
|
|
146
|
+
</article>
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function TurnHeader(props: { number: number; turn: ConversationTurn }) {
|
|
151
|
+
const status = visualStatusForSession(props.turn);
|
|
152
|
+
|
|
153
|
+
return (
|
|
154
|
+
<div className="flex items-start justify-between gap-3 max-md:flex-col">
|
|
155
|
+
<div className="min-w-0">
|
|
156
|
+
<div className="break-all text-[1.05rem] font-bold leading-tight tracking-normal">
|
|
157
|
+
Turn {props.number}
|
|
158
|
+
</div>
|
|
159
|
+
<div className={cn(mutedTranscriptMetaClass(), "mt-1")}>
|
|
160
|
+
{turnMeta(props.turn).join(" · ")}
|
|
161
|
+
{props.turn.sentryTraceUrl ? (
|
|
162
|
+
<>
|
|
163
|
+
{" · "}
|
|
164
|
+
<a
|
|
165
|
+
className="text-white no-underline hover:underline"
|
|
166
|
+
href={props.turn.sentryTraceUrl}
|
|
167
|
+
rel="noreferrer"
|
|
168
|
+
target="_blank"
|
|
169
|
+
>
|
|
170
|
+
View in Sentry
|
|
171
|
+
</a>
|
|
172
|
+
</>
|
|
173
|
+
) : null}
|
|
174
|
+
</div>
|
|
175
|
+
</div>
|
|
176
|
+
<StatusBadge status={status} />
|
|
177
|
+
</div>
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function TurnEvents(props: {
|
|
182
|
+
turn: ConversationTurn;
|
|
183
|
+
view: TranscriptViewMode;
|
|
184
|
+
}) {
|
|
185
|
+
return (
|
|
186
|
+
<div className="grid gap-3 pt-3">
|
|
187
|
+
{props.turn.transcriptAvailable ? (
|
|
188
|
+
groupTranscriptMessages(props.turn.transcript).map((entry, index) =>
|
|
189
|
+
entry.kind === "tool" ? (
|
|
190
|
+
<TranscriptToolView
|
|
191
|
+
call={entry.call}
|
|
192
|
+
key={`${props.turn.id}:${index}`}
|
|
193
|
+
result={entry.result}
|
|
194
|
+
resultTimestamp={entry.resultTimestamp}
|
|
195
|
+
timestamp={entry.timestamp}
|
|
196
|
+
view={props.view}
|
|
197
|
+
/>
|
|
198
|
+
) : (
|
|
199
|
+
<TranscriptMessageView
|
|
200
|
+
key={`${props.turn.id}:${index}`}
|
|
201
|
+
message={entry.message}
|
|
202
|
+
turn={props.turn}
|
|
203
|
+
view={props.view}
|
|
204
|
+
/>
|
|
205
|
+
),
|
|
206
|
+
)
|
|
207
|
+
) : props.turn.transcriptRedacted &&
|
|
208
|
+
props.turn.transcriptMetadata?.length ? (
|
|
209
|
+
<RedactedTranscriptView turn={props.turn} />
|
|
210
|
+
) : (
|
|
211
|
+
<div className={transcriptEmptyClass()}>
|
|
212
|
+
{unavailableTranscriptLabel(props.turn)}
|
|
213
|
+
</div>
|
|
214
|
+
)}
|
|
215
|
+
</div>
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function RedactedTranscriptView(props: { turn: ConversationTurn }) {
|
|
220
|
+
return (
|
|
221
|
+
<>
|
|
222
|
+
{groupTranscriptMessages(props.turn.transcriptMetadata ?? []).map(
|
|
223
|
+
(entry, index) =>
|
|
224
|
+
entry.kind === "tool" ? (
|
|
225
|
+
<RedactedToolView
|
|
226
|
+
call={entry.call}
|
|
227
|
+
key={`${props.turn.id}:redacted:${index}`}
|
|
228
|
+
result={entry.result}
|
|
229
|
+
resultTimestamp={entry.resultTimestamp}
|
|
230
|
+
timestamp={entry.timestamp}
|
|
231
|
+
/>
|
|
232
|
+
) : (
|
|
233
|
+
<RedactedMessageView
|
|
234
|
+
key={`${props.turn.id}:redacted:${index}`}
|
|
235
|
+
message={entry.message}
|
|
236
|
+
turn={props.turn}
|
|
237
|
+
/>
|
|
238
|
+
),
|
|
239
|
+
)}
|
|
240
|
+
</>
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function RedactedMessageView(props: {
|
|
245
|
+
message: TranscriptMessage;
|
|
246
|
+
turn: ConversationTurn;
|
|
247
|
+
}) {
|
|
248
|
+
const offset = formatMessageOffset(props.turn, props.message.timestamp);
|
|
249
|
+
const meta = [formatMessageTimestamp(props.message.timestamp), offset].filter(
|
|
250
|
+
isString,
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
return (
|
|
254
|
+
<TranscriptMessageShell role={props.message.role}>
|
|
255
|
+
<div className={transcriptRoleClass(props.message.role)}>
|
|
256
|
+
<span className={transcriptRoleLabelClass(props.message.role)}>
|
|
257
|
+
{transcriptRoleLabel(props.message.role, props.turn)}
|
|
258
|
+
</span>
|
|
259
|
+
{meta.map((value, index) => (
|
|
260
|
+
<span
|
|
261
|
+
className="font-mono text-[0.78rem] text-[#888]"
|
|
262
|
+
key={`${index}-${value}`}
|
|
263
|
+
>
|
|
264
|
+
{value}
|
|
265
|
+
</span>
|
|
266
|
+
))}
|
|
267
|
+
</div>
|
|
268
|
+
<div className="grid min-w-0 gap-1 font-mono text-[0.9rem] leading-snug text-[#b8b8b8]">
|
|
269
|
+
{props.message.parts.map((part, index) => (
|
|
270
|
+
<RedactedPartLine key={index} part={part} />
|
|
271
|
+
))}
|
|
272
|
+
</div>
|
|
273
|
+
</TranscriptMessageShell>
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function RedactedPartLine(props: { part: TranscriptPart }) {
|
|
278
|
+
if (props.part.type === "text") {
|
|
279
|
+
return <RedactedMetadataRow meta={redactedMessageSize(props.part)} />;
|
|
280
|
+
}
|
|
281
|
+
if (props.part.type === "thinking") {
|
|
282
|
+
return <RedactedMetadataRow />;
|
|
283
|
+
}
|
|
284
|
+
return <RedactedMetadataRow />;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function RedactedMetadataRow(props: { meta?: string }) {
|
|
288
|
+
return (
|
|
289
|
+
<div className="grid min-w-0 grid-cols-[minmax(0,1fr)_auto] items-center gap-3 py-1 max-md:grid-cols-1">
|
|
290
|
+
<RedactedMarker />
|
|
291
|
+
{props.meta ? (
|
|
292
|
+
<span className="min-w-0 break-words text-right text-[#888] max-md:text-left">
|
|
293
|
+
{props.meta}
|
|
294
|
+
</span>
|
|
295
|
+
) : null}
|
|
296
|
+
</div>
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function RedactedMarker() {
|
|
301
|
+
return (
|
|
302
|
+
<code className="inline-flex w-fit font-mono text-[0.82rem] leading-tight text-[#b8b8b8]">
|
|
303
|
+
{"<redacted>"}
|
|
304
|
+
</code>
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function RedactedToolView(props: {
|
|
309
|
+
call?: TranscriptPart;
|
|
310
|
+
result?: TranscriptPart;
|
|
311
|
+
resultTimestamp?: number;
|
|
312
|
+
timestamp?: number;
|
|
313
|
+
}) {
|
|
314
|
+
const toolName =
|
|
315
|
+
props.call?.name ??
|
|
316
|
+
props.result?.name ??
|
|
317
|
+
props.call?.id ??
|
|
318
|
+
props.result?.id ??
|
|
319
|
+
"unknown";
|
|
320
|
+
const duration =
|
|
321
|
+
typeof props.timestamp === "number" &&
|
|
322
|
+
typeof props.resultTimestamp === "number" &&
|
|
323
|
+
props.resultTimestamp >= props.timestamp
|
|
324
|
+
? formatMs(props.resultTimestamp - props.timestamp)
|
|
325
|
+
: undefined;
|
|
326
|
+
const meta = [
|
|
327
|
+
props.timestamp ? formatMessageTimestamp(props.timestamp) : undefined,
|
|
328
|
+
duration,
|
|
329
|
+
props.result ? undefined : "missing result",
|
|
330
|
+
].filter(isString);
|
|
331
|
+
|
|
332
|
+
return (
|
|
333
|
+
<ToolFrame
|
|
334
|
+
meta={meta}
|
|
335
|
+
raw
|
|
336
|
+
signature={
|
|
337
|
+
<>
|
|
338
|
+
<strong className="min-w-0 break-words font-bold text-white">
|
|
339
|
+
{toolName}
|
|
340
|
+
</strong>
|
|
341
|
+
{props.call?.inputKeys?.length ? (
|
|
342
|
+
<code className="min-w-0 break-words font-[inherit] text-[#b8b8b8]">
|
|
343
|
+
({props.call.inputKeys.join(", ")})
|
|
344
|
+
</code>
|
|
345
|
+
) : null}
|
|
346
|
+
</>
|
|
347
|
+
}
|
|
348
|
+
/>
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function redactedMessageSize(part: TranscriptPart): string | undefined {
|
|
353
|
+
if (typeof part.bytes === "number") return formatBytes(part.bytes);
|
|
354
|
+
return typeof part.chars === "number" ? `${part.chars} chars` : undefined;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function turnActorLabel(turn: ConversationTurn): string {
|
|
358
|
+
return (
|
|
359
|
+
requesterLabel(turn.requesterIdentity, turn.requester) ?? "unknown actor"
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function turnMeta(turn: ConversationTurn): string[] {
|
|
364
|
+
return [
|
|
365
|
+
formatMs(turn.cumulativeDurationMs),
|
|
366
|
+
formatUsage(turn.cumulativeUsage),
|
|
367
|
+
].filter((value) => value && value !== "none");
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function TranscriptMessageView(props: {
|
|
371
|
+
message: TranscriptMessage;
|
|
372
|
+
turn: ConversationTurn;
|
|
373
|
+
view: TranscriptViewMode;
|
|
374
|
+
}) {
|
|
375
|
+
const offset = formatMessageOffset(props.turn, props.message.timestamp);
|
|
376
|
+
const renderedParts = groupTranscriptParts(props.message.parts);
|
|
377
|
+
const rawText = messageRawText(props.message);
|
|
378
|
+
const totalRenderedChildren = renderedParts.reduce(
|
|
379
|
+
(count, part) => count + countRenderedTranscriptChildren(part),
|
|
380
|
+
0,
|
|
381
|
+
);
|
|
382
|
+
let seenRenderedChildren = 0;
|
|
383
|
+
|
|
384
|
+
return (
|
|
385
|
+
<TranscriptMessageShell
|
|
386
|
+
role={props.message.role}
|
|
387
|
+
onCopy={(event) => {
|
|
388
|
+
if (props.view !== "rich" || !rawText) return;
|
|
389
|
+
event.clipboardData.setData("text/plain", rawText);
|
|
390
|
+
event.preventDefault();
|
|
391
|
+
}}
|
|
392
|
+
>
|
|
393
|
+
<div className={transcriptRoleClass(props.message.role)}>
|
|
394
|
+
<span className={transcriptRoleLabelClass(props.message.role)}>
|
|
395
|
+
{transcriptRoleLabel(props.message.role, props.turn)}
|
|
396
|
+
</span>
|
|
397
|
+
<span className="font-mono text-[0.78rem] text-[#888]">
|
|
398
|
+
{formatMessageTimestamp(props.message.timestamp)}
|
|
399
|
+
</span>
|
|
400
|
+
{offset ? (
|
|
401
|
+
<span className="font-mono text-[0.78rem] text-[#888]">{offset}</span>
|
|
402
|
+
) : null}
|
|
403
|
+
</div>
|
|
404
|
+
{props.view === "raw" ? (
|
|
405
|
+
<HighlightedCode
|
|
406
|
+
code={rawText || "{}"}
|
|
407
|
+
language={detectLanguage(rawText)}
|
|
408
|
+
/>
|
|
409
|
+
) : (
|
|
410
|
+
<div className="grid min-w-0 gap-2">
|
|
411
|
+
{renderedParts.map((part, index) => {
|
|
412
|
+
const firstChildIndex = seenRenderedChildren;
|
|
413
|
+
seenRenderedChildren += countRenderedTranscriptChildren(part);
|
|
414
|
+
return (
|
|
415
|
+
<TranscriptPartView
|
|
416
|
+
firstChildIndex={firstChildIndex}
|
|
417
|
+
key={index}
|
|
418
|
+
lastChildIndex={totalRenderedChildren - 1}
|
|
419
|
+
part={part}
|
|
420
|
+
/>
|
|
421
|
+
);
|
|
422
|
+
})}
|
|
423
|
+
</div>
|
|
424
|
+
)}
|
|
425
|
+
</TranscriptMessageShell>
|
|
426
|
+
);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function TranscriptPartView(props: {
|
|
430
|
+
firstChildIndex: number;
|
|
431
|
+
lastChildIndex: number;
|
|
432
|
+
part: RenderedTranscriptPart;
|
|
433
|
+
}) {
|
|
434
|
+
if (props.part.kind === "tool") {
|
|
435
|
+
return (
|
|
436
|
+
<TranscriptToolView call={props.part.call} result={props.part.result} />
|
|
437
|
+
);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
const part = props.part.part;
|
|
441
|
+
if (part.type === "text") {
|
|
442
|
+
return (
|
|
443
|
+
<TranscriptText
|
|
444
|
+
firstChildIndex={props.firstChildIndex}
|
|
445
|
+
lastChildIndex={props.lastChildIndex}
|
|
446
|
+
text={part.text ?? ""}
|
|
447
|
+
/>
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
const value = part.output;
|
|
452
|
+
if (part.type === "thinking") {
|
|
453
|
+
const rendered = stringifyPartValue(value);
|
|
454
|
+
return (
|
|
455
|
+
<details className="border border-[#beaaff]/20 bg-white/[0.03] transition-colors hover:border-[#beaaff]/45 hover:bg-[rgba(190,170,255,0.06)]">
|
|
456
|
+
<summary className="grid cursor-pointer grid-cols-[auto_minmax(0,1fr)] items-center gap-3 px-3 py-2 font-mono text-[0.8rem] leading-tight text-[#888] hover:bg-[rgba(190,170,255,0.07)] max-md:grid-cols-1 max-md:gap-1">
|
|
457
|
+
<span className="uppercase text-[#b8b8b8]">thinking</span>
|
|
458
|
+
<span className="min-w-0 truncate">{previewToolValue(value)}</span>
|
|
459
|
+
</summary>
|
|
460
|
+
<HighlightedCode
|
|
461
|
+
code={rendered || "{}"}
|
|
462
|
+
language={detectLanguage(rendered)}
|
|
463
|
+
/>
|
|
464
|
+
</details>
|
|
465
|
+
);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
const rendered = stringifyPartValue(value);
|
|
469
|
+
return (
|
|
470
|
+
<details className={toolFrameClass()}>
|
|
471
|
+
<summary className="grid cursor-pointer grid-cols-[auto_minmax(0,1fr)_auto] items-start gap-3 px-3 py-2 font-mono text-[0.86rem] leading-tight text-[#b8b8b8] hover:bg-white/[0.04] max-md:grid-cols-1 max-md:gap-1">
|
|
472
|
+
<span className="text-[#888]">{part.type}</span>
|
|
473
|
+
<strong className="min-w-0 break-words font-bold text-white">
|
|
474
|
+
{part.name ?? part.id ?? "unknown"}
|
|
475
|
+
</strong>
|
|
476
|
+
<span className="min-w-0 break-words text-right max-md:text-left">
|
|
477
|
+
{previewToolValue(value)}
|
|
478
|
+
</span>
|
|
479
|
+
</summary>
|
|
480
|
+
<HighlightedCode code={rendered || "{}"} language="json" />
|
|
481
|
+
</details>
|
|
482
|
+
);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function isString(value: string | undefined): value is string {
|
|
486
|
+
return typeof value === "string" && value.length > 0;
|
|
487
|
+
}
|