fix: safely render structured output objects to prevent React error #31#3483
fix: safely render structured output objects to prevent React error #31#3483MaxwellCalkin wants to merge 2 commits intosimstudioai:mainfrom
Conversation
…imstudioai#31 When LLM providers (especially Anthropic) return structured content blocks like { text, type } objects, the UI crashes with React error simstudioai#31 because these objects are passed directly as React children instead of being converted to strings first. This adds a safeRenderValue() utility that: - Extracts the .text property from { text, type } content blocks - Joins text from arrays of content blocks (Anthropic format) - Falls back to JSON.stringify for other objects - Returns primitives unchanged Applied to all chat message rendering paths: - Public deployed chat (ClientChatMessage) - Floating workspace chat (ChatMessage) - Chat streaming hook (stopStreaming content concatenation) - Chat store (appendMessageContent) Fixes simstudioai#2725 AI Disclosure: This commit was authored by Claude Opus 4.6 (Anthropic), an AI agent operated by Maxwell Calkin (@MaxwellCalkin).
PR SummaryLow Risk Overview Updates both chat message renderers plus streaming stop/append paths to always convert existing Written by Cursor Bugbot for commit 4443719. This will update automatically on new commits. Configure here. |
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
Greptile SummaryThis PR introduces a centralized Key changes:
Confidence Score: 3/5
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[LLM Response\ncontent arrives] --> B{content type?}
B -->|string| C[Pass through unchanged]
B -->|null / undefined| D[Return empty string]
B -->|number / boolean| E[String conversion]
B -->|object with .text string| F[Extract .text value]
B -->|array of content blocks| G[Map each item:\n- string → as-is\n- object with .text → extract text\n- other → JSON.stringify]
B -->|other object| H[JSON.stringify fallback]
C --> Z[safeRenderValue: string output]
D --> Z
E --> Z
F --> Z
G --> Z
H --> Z
Z --> R1[chat-message.tsx\nworkspace chat\nuses safeRenderValue directly]
Z --> R2[message.tsx\npublic chat — non-object path\nuses safeRenderValue]
Z --> R3[store.ts appendMessageContent\nuses safeRenderValue before concat]
Z --> R4[use-chat-streaming.ts stopStreaming\nuses safeRenderValue before concat]
X[message.tsx isJsonObject=true\npublic chat — object path] -->|JSON.stringify bypass| R2
style X fill:#ffcccc,stroke:#cc0000
Last reviewed commit: e0dd596 |
| const cleanTextContent = useMemo(() => { | ||
| if (isJsonObject) { | ||
| return JSON.stringify(message.content, null, 2) | ||
| } | ||
| return safeRenderValue(message.content) | ||
| }, [message.content, isJsonObject]) |
There was a problem hiding this comment.
Inconsistency: isJsonObject branch uses JSON.stringify instead of safeRenderValue
When isJsonObject is true (i.e., when a structured content block like { text: "Hello", type: "text" } is returned), cleanTextContent is computed via JSON.stringify, which displays the raw JSON structure in the <pre> block rather than extracting the .text field.
This is inconsistent with chat-message.tsx in the workspace surface, which uses safeRenderValue unconditionally and correctly extracts the .text field. As a result, the same content block displays differently across the two chat surfaces: raw JSON in public chat vs. extracted text in workspace chat.
Consider using safeRenderValue for both branches:
| const cleanTextContent = useMemo(() => { | |
| if (isJsonObject) { | |
| return JSON.stringify(message.content, null, 2) | |
| } | |
| return safeRenderValue(message.content) | |
| }, [message.content, isJsonObject]) | |
| // Safely convert content to a renderable string to prevent React error #31 | |
| // when LLM returns structured objects (e.g. { text, type }) | |
| const cleanTextContent = useMemo(() => { | |
| return safeRenderValue(message.content) | |
| }, [message.content]) |
If displaying formatted JSON for non-content-block objects is intentional, the isJsonObject flag can still control the <pre> vs <span> rendering decision without affecting text extraction.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
The isJsonObject branch was bypassing safeRenderValue and calling
JSON.stringify directly, which displayed raw JSON for structured
objects like { text, type } instead of extracting the text field.
Now all content goes through safeRenderValue which extracts .text
from content blocks and falls back to JSON.stringify only when
no text field is found.
Addresses review feedback from Greptile and Cursor bots.
AI Disclosure: This commit was authored by Claude Opus 4.6 (Anthropic),
an AI agent operated by Maxwell Calkin (@MaxwellCalkin).
Summary
Fixes #2725
When LLM providers (especially Anthropic) return structured content blocks like
{ text: "...", type: "text" }or arrays of content blocks[{ text: "...", type: "text" }], the chat UI crashes with React error #31 ("Objects are not valid as a React child") because these objects are passed directly as React children.This PR introduces a centralized
safeRenderValue()utility that safely converts any value to a renderable string:.textfrom{ text, type }content block objectsJSON.stringifyfor other objectsChanges
apps/sim/lib/core/utils/safe-render.ts— centralized utility functionapps/sim/lib/core/utils/safe-render.test.ts— 15 unit tests (all passing)apps/sim/app/chat/components/message/message.tsx— public deployed chat now usessafeRenderValueinstead of rendering raw contentapps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/components/chat-message/chat-message.tsx— floating workspace chat now usessafeRenderValueapps/sim/app/chat/hooks/use-chat-streaming.ts—stopStreamingsafely converts content before string concatenationapps/sim/stores/chat/store.ts—appendMessageContentsafely converts existing content before concatenationTest plan
safeRenderValue{ text: "...", type: "text" }content blocks