Skip to content

Commit c322ee7

Browse files
samejrericallam
authored andcommitted
Render JSON object output in our code block component rather than as a string
1 parent 3a49af8 commit c322ee7

File tree

4 files changed

+64
-12
lines changed

4 files changed

+64
-12
lines changed

apps/webapp/app/components/runs/v3/ai/AIChatMessages.tsx

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,13 +109,25 @@ function UserSection({ text }: { text: string }) {
109109
// Assistant response (with markdown/raw toggle)
110110
// ---------------------------------------------------------------------------
111111

112+
function isJsonString(value: string): boolean {
113+
const trimmed = value.trimStart();
114+
if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) return false;
115+
try {
116+
JSON.parse(value);
117+
return true;
118+
} catch {
119+
return false;
120+
}
121+
}
122+
112123
export function AssistantResponse({
113124
text,
114125
headerLabel = "Assistant",
115126
}: {
116127
text: string;
117128
headerLabel?: string;
118129
}) {
130+
const isJson = isJsonString(text);
119131
const [mode, setMode] = useState<"rendered" | "raw">("rendered");
120132
const [copied, setCopied] = useState(false);
121133

@@ -125,6 +137,21 @@ export function AssistantResponse({
125137
setTimeout(() => setCopied(false), 2000);
126138
}
127139

140+
if (isJson) {
141+
return (
142+
<div className="flex flex-col gap-1.5 py-2.5">
143+
<SectionHeader label={headerLabel} />
144+
<CodeBlock
145+
code={text}
146+
maxLines={20}
147+
showLineNumbers={false}
148+
showCopyButton
149+
language="json"
150+
/>
151+
</div>
152+
);
153+
}
154+
128155
return (
129156
<div className="flex flex-col gap-1.5 py-2.5">
130157
<SectionHeader

apps/webapp/app/components/runs/v3/ai/AISpanDetails.tsx

Lines changed: 33 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { Header3 } from "~/components/primitives/Headers";
55
import { Paragraph } from "~/components/primitives/Paragraph";
66
import { TabButton, TabContainer } from "~/components/primitives/Tabs";
77
import { useHasAdminAccess } from "~/hooks/useUser";
8+
import { CodeBlock } from "~/components/code/CodeBlock";
89
import { AIChatMessages, AssistantResponse, ChatBubble } from "./AIChatMessages";
910
import { AIStatsSummary, AITagsRow } from "./AIModelSummary";
1011
import { AIToolsInventory } from "./AIToolsInventory";
@@ -76,15 +77,13 @@ export function AISpanDetails({
7677
}
7778

7879
function OverviewTab({ aiData }: { aiData: AISpanData }) {
79-
const { userText, outputText, outputToolNames } = extractInputOutput(aiData);
80+
const { userText, outputText, outputObject, outputToolNames } = extractInputOutput(aiData);
8081

8182
return (
8283
<div className="flex flex-col px-3">
83-
{/* Tags + Stats */}
8484
<AITagsRow aiData={aiData} />
8585
<AIStatsSummary aiData={aiData} />
8686

87-
{/* Input (last user prompt) */}
8887
{userText && (
8988
<div className="flex flex-col gap-1.5 py-2.5">
9089
<Header3>Input</Header3>
@@ -94,9 +93,20 @@ function OverviewTab({ aiData }: { aiData: AISpanData }) {
9493
</div>
9594
)}
9695

97-
{/* Output (assistant response or tool calls) */}
9896
{outputText && <AssistantResponse text={outputText} headerLabel="Output" />}
99-
{outputToolNames.length > 0 && !outputText && (
97+
{!outputText && outputObject && (
98+
<div className="flex flex-col gap-1.5 py-2.5">
99+
<Header3>Output</Header3>
100+
<CodeBlock
101+
code={outputObject}
102+
maxLines={20}
103+
showLineNumbers={false}
104+
showCopyButton
105+
language="json"
106+
/>
107+
</div>
108+
)}
109+
{outputToolNames.length > 0 && !outputText && !outputObject && (
100110
<div className="flex flex-col gap-1.5 py-2.5">
101111
<Header3>Output</Header3>
102112
<ChatBubble>
@@ -112,12 +122,26 @@ function OverviewTab({ aiData }: { aiData: AISpanData }) {
112122
}
113123

114124
function MessagesTab({ aiData }: { aiData: AISpanData }) {
125+
const showFallbackText = aiData.responseText && !hasAssistantItem(aiData.items);
126+
const showFallbackObject =
127+
!showFallbackText && aiData.responseObject && !hasAssistantItem(aiData.items);
128+
115129
return (
116130
<div className="px-3">
117131
<div className="flex flex-col">
118132
{aiData.items && aiData.items.length > 0 && <AIChatMessages items={aiData.items} />}
119-
{aiData.responseText && !hasAssistantItem(aiData.items) && (
120-
<AssistantResponse text={aiData.responseText} />
133+
{showFallbackText && <AssistantResponse text={aiData.responseText!} />}
134+
{showFallbackObject && (
135+
<div className="flex flex-col gap-1.5 py-2.5">
136+
<Header3>Assistant</Header3>
137+
<CodeBlock
138+
code={aiData.responseObject!}
139+
maxLines={20}
140+
showLineNumbers={false}
141+
showCopyButton
142+
language="json"
143+
/>
144+
</div>
121145
)}
122146
</div>
123147
</div>
@@ -158,22 +182,21 @@ function CopyRawFooter({ rawProperties }: { rawProperties: string }) {
158182
function extractInputOutput(aiData: AISpanData): {
159183
userText: string | undefined;
160184
outputText: string | undefined;
185+
outputObject: string | undefined;
161186
outputToolNames: string[];
162187
} {
163188
let userText: string | undefined;
164189
let outputText: string | undefined;
165190
const outputToolNames: string[] = [];
166191

167192
if (aiData.items) {
168-
// Find the last user message
169193
for (let i = aiData.items.length - 1; i >= 0; i--) {
170194
if (aiData.items[i].type === "user") {
171195
userText = (aiData.items[i] as { type: "user"; text: string }).text;
172196
break;
173197
}
174198
}
175199

176-
// Find the last assistant or tool-use item as the output
177200
for (let i = aiData.items.length - 1; i >= 0; i--) {
178201
const item = aiData.items[i];
179202
if (item.type === "assistant") {
@@ -189,12 +212,11 @@ function extractInputOutput(aiData: AISpanData): {
189212
}
190213
}
191214

192-
// Fall back to responseText if no assistant item found
193215
if (!outputText && aiData.responseText) {
194216
outputText = aiData.responseText;
195217
}
196218

197-
return { userText, outputText, outputToolNames };
219+
return { userText, outputText, outputObject: aiData.responseObject, outputToolNames };
198220
}
199221

200222
function hasAssistantItem(items: DisplayItem[] | undefined): boolean {

apps/webapp/app/components/runs/v3/ai/extractAISpanData.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,8 @@ export function extractAISpanData(
7777
inputCost: num(triggerLlm.input_cost),
7878
outputCost: num(triggerLlm.output_cost),
7979
totalCost: num(triggerLlm.total_cost),
80-
responseText: str(aiResponse.text) || str(aiResponse.object) || undefined,
80+
responseText: str(aiResponse.text) || undefined,
81+
responseObject: str(aiResponse.object) || undefined,
8182
toolDefinitions: toolDefs,
8283
items: buildDisplayItems(aiPrompt.messages, aiResponse.toolCalls, toolDefs),
8384
};

apps/webapp/app/components/runs/v3/ai/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,8 @@ export type AISpanData = {
9494

9595
// Response text (final assistant output)
9696
responseText?: string;
97+
// Structured object response (JSON) — mutually exclusive with responseText
98+
responseObject?: string;
9799

98100
// Tool definitions (from ai.prompt.tools)
99101
toolDefinitions?: ToolDefinition[];

0 commit comments

Comments
 (0)