From e0dd5961d3e1cb3038c9869746d7b4101ec51a9a Mon Sep 17 00:00:00 2001 From: Maxwell Calkin Date: Mon, 9 Mar 2026 02:22:14 -0400 Subject: [PATCH 1/2] fix: safely render structured output objects to prevent React error #31 When LLM providers (especially Anthropic) return structured content blocks like { text, type } objects, the UI crashes with React error #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 #2725 AI Disclosure: This commit was authored by Claude Opus 4.6 (Anthropic), an AI agent operated by Maxwell Calkin (@MaxwellCalkin). --- .../app/chat/components/message/message.tsx | 30 +++---- apps/sim/app/chat/hooks/use-chat-streaming.ts | 11 +-- .../components/chat-message/chat-message.tsx | 6 +- apps/sim/lib/core/utils/safe-render.test.ts | 82 +++++++++++++++++++ apps/sim/lib/core/utils/safe-render.ts | 67 +++++++++++++++ apps/sim/stores/chat/store.ts | 11 ++- 6 files changed, 177 insertions(+), 30 deletions(-) create mode 100644 apps/sim/lib/core/utils/safe-render.test.ts create mode 100644 apps/sim/lib/core/utils/safe-render.ts diff --git a/apps/sim/app/chat/components/message/message.tsx b/apps/sim/app/chat/components/message/message.tsx index 7a8f4546d4e..8aa3203b98b 100644 --- a/apps/sim/app/chat/components/message/message.tsx +++ b/apps/sim/app/chat/components/message/message.tsx @@ -3,6 +3,7 @@ import { memo, useMemo, useState } from 'react' import { Check, Copy, File as FileIcon, FileText, Image as ImageIcon } from 'lucide-react' import { Tooltip } from '@/components/emcn' +import { safeRenderValue } from '@/lib/core/utils/safe-render' import { ChatFileDownload, ChatFileDownloadAll, @@ -50,9 +51,14 @@ export const ClientChatMessage = memo( return typeof message.content === 'object' && message.content !== null }, [message.content]) - // Since tool calls are now handled via SSE events and stored in message.toolCalls, - // we can use the content directly without parsing - const cleanTextContent = message.content + // Safely convert content to a renderable string to prevent React error #31 + // when workflow nodes return structured objects (e.g. { text, type }) + const cleanTextContent = useMemo(() => { + if (isJsonObject) { + return JSON.stringify(message.content, null, 2) + } + return safeRenderValue(message.content) + }, [message.content, isJsonObject]) const content = message.type === 'user' ? ( @@ -161,14 +167,14 @@ export const ClientChatMessage = memo( )} {/* Only render message bubble if there's actual text content (not just file count message) */} - {message.content && !String(message.content).startsWith('Sent') && ( + {cleanTextContent && !cleanTextContent.startsWith('Sent') && (
{isJsonObject ? ( -
{JSON.stringify(message.content, null, 2)}
+
{cleanTextContent}
) : ( - {message.content as string} + {cleanTextContent} )}
@@ -184,11 +190,9 @@ export const ClientChatMessage = memo(
{isJsonObject ? ( -
-                      {JSON.stringify(cleanTextContent, null, 2)}
-                    
+
{cleanTextContent}
) : ( - + )}
@@ -208,11 +212,7 @@ export const ClientChatMessage = memo(