From 74d2157ad533f44f586fa83bfee8a15dce9cf18c Mon Sep 17 00:00:00 2001 From: "Teja Sri Munnangi (Persistent Systems Inc)" Date: Tue, 16 Jun 2026 16:55:43 +0530 Subject: [PATCH 1/5] commit --- src/App/src/components/content/PlanChat.tsx | 6 +- .../streaming/StreamingAgentMessage.tsx | 61 +++++++++++++++-- .../streaming/StreamingBufferMessage.tsx | 67 ++++++++++++++++++- .../streaming/StreamingPlanResponse.tsx | 4 +- 4 files changed, 127 insertions(+), 11 deletions(-) diff --git a/src/App/src/components/content/PlanChat.tsx b/src/App/src/components/content/PlanChat.tsx index de7fafa16..f65461a33 100644 --- a/src/App/src/components/content/PlanChat.tsx +++ b/src/App/src/components/content/PlanChat.tsx @@ -87,8 +87,10 @@ const PlanChat: React.FC = ({ {renderAgentMessages(agentMessages, undefined, undefined, finalResultRef)} {showProcessingPlanSpinner && renderPlanExecutionMessage()} - {/* Streaming plan updates */} - {showBufferingText && ( + {/* Streaming plan updates — hidden while an approval prompt is pending so + the approval action is presented at the appropriate step instead of + after the thinking process visibly completes. */} + {showBufferingText && !showApprovalButtons && ( ), img: ({ node: _imgNode, ...props }) => ( -
+
+ ), + p: ({ node: _pNode, ...props }) => ( +

+ ), + h1: ({ node: _hNode, ...props }) => ( +

+ ), + h2: ({ node: _hNode, ...props }) => ( +

+ ), + h3: ({ node: _hNode, ...props }) => ( +

+ ), + ul: ({ node: _ulNode, ...props }) => ( +
    + ), + ol: ({ node: _olNode, ...props }) => ( +
      + ), + li: ({ node: _liNode, ...props }) => ( +
    1. + ), + blockquote: ({ node: _bqNode, ...props }) => ( +
      ) }} > diff --git a/src/App/src/components/content/streaming/StreamingBufferMessage.tsx b/src/App/src/components/content/streaming/StreamingBufferMessage.tsx index 6c611754c..b2360e0e2 100644 --- a/src/App/src/components/content/streaming/StreamingBufferMessage.tsx +++ b/src/App/src/components/content/streaming/StreamingBufferMessage.tsx @@ -12,6 +12,67 @@ interface StreamingBufferMessageProps { isStreaming?: boolean; } +/** + * Wrap any raw JSON object/array blocks in markdown fenced code blocks so the + * Details section renders them as formatted code rather than plain text. + * Lines that already sit inside an existing fenced block are left alone. + */ +const formatBufferContent = (content: string): string => { + if (!content) return content; + + const lines = content.split('\n'); + const out: string[] = []; + let insideExistingFence = false; + let i = 0; + + while (i < lines.length) { + const line = lines[i]; + const trimmed = line.trim(); + + if (trimmed.startsWith('```')) { + insideExistingFence = !insideExistingFence; + out.push(line); + i++; + continue; + } + + if (!insideExistingFence && (trimmed.startsWith('{') || trimmed.startsWith('['))) { + // Try to capture a balanced JSON block starting at this line + let depth = 0; + let endIdx = -1; + for (let j = i; j < lines.length; j++) { + const l = lines[j]; + for (const ch of l) { + if (ch === '{' || ch === '[') depth++; + else if (ch === '}' || ch === ']') depth--; + } + if (depth === 0) { + endIdx = j; + break; + } + } + if (endIdx !== -1) { + const block = lines.slice(i, endIdx + 1).join('\n'); + try { + const parsed = JSON.parse(block); + out.push('```json'); + out.push(JSON.stringify(parsed, null, 2)); + out.push('```'); + i = endIdx + 1; + continue; + } catch { + // Not valid JSON — fall through and keep the original line + } + } + } + + out.push(line); + i++; + } + + return out.join('\n'); +}; + // Convert to a proper React component instead of a function const StreamingBufferMessage: React.FC = ({ streamingMessageBuffer, @@ -42,6 +103,8 @@ const StreamingBufferMessage: React.FC = ({ if (!streamingMessageBuffer || streamingMessageBuffer.trim() === "") return null; + const formattedBuffer = formatBufferContent(streamingMessageBuffer); + return (
      = ({ ) }} > - {streamingMessageBuffer} + {formattedBuffer}

@@ -216,7 +279,7 @@ const StreamingBufferMessage: React.FC = ({ ) }} > - {streamingMessageBuffer} + {formattedBuffer} )} diff --git a/src/App/src/components/content/streaming/StreamingPlanResponse.tsx b/src/App/src/components/content/streaming/StreamingPlanResponse.tsx index 7ba887878..7328589d5 100644 --- a/src/App/src/components/content/streaming/StreamingPlanResponse.tsx +++ b/src/App/src/components/content/streaming/StreamingPlanResponse.tsx @@ -61,7 +61,9 @@ const useStyles = makeStyles({ borderRadius: '8px', fontSize: '14px', lineHeight: '1.5', - wordWrap: 'break-word' + wordWrap: 'break-word', + marginLeft: '48px', + boxSizing: 'border-box' }, factsSection: { backgroundColor: 'var(--colorNeutralBackground2)', From f5b0ebc1b27d233ece8a787694611ae0256e973d Mon Sep 17 00:00:00 2001 From: "Teja Sri Munnangi (Persistent Systems Inc)" Date: Tue, 16 Jun 2026 19:51:16 +0530 Subject: [PATCH 2/5] commit --- .../streaming/StreamingBufferMessage.tsx | 61 +++++++++++++++++-- 1 file changed, 55 insertions(+), 6 deletions(-) diff --git a/src/App/src/components/content/streaming/StreamingBufferMessage.tsx b/src/App/src/components/content/streaming/StreamingBufferMessage.tsx index b2360e0e2..c70894c87 100644 --- a/src/App/src/components/content/streaming/StreamingBufferMessage.tsx +++ b/src/App/src/components/content/streaming/StreamingBufferMessage.tsx @@ -13,9 +13,59 @@ interface StreamingBufferMessageProps { } /** - * Wrap any raw JSON object/array blocks in markdown fenced code blocks so the - * Details section renders them as formatted code rather than plain text. - * Lines that already sit inside an existing fenced block are left alone. + * Format a key from snake_case / camelCase / kebab-case into a readable label. + */ +const humanizeKey = (key: string): string => { + if (!key) return key; + const spaced = key + .replace(/[_-]+/g, ' ') + .replace(/([a-z0-9])([A-Z])/g, '$1 $2') + .trim(); + return spaced.replace(/\b\w/g, (c) => c.toUpperCase()); +}; + +/** + * Render a parsed JSON value as readable Markdown (bullet list of + * "**Key**: value" entries, recursing into nested objects/arrays). + */ +const jsonToMarkdown = (value: any, depth = 0): string => { + const indent = ' '.repeat(depth); + + if (value === null || value === undefined) return `${indent}_n/a_`; + + if (Array.isArray(value)) { + if (value.length === 0) return `${indent}_(none)_`; + return value + .map((item) => { + if (item !== null && typeof item === 'object') { + return `${indent}- \n${jsonToMarkdown(item, depth + 1)}`; + } + return `${indent}- ${String(item)}`; + }) + .join('\n'); + } + + if (typeof value === 'object') { + const entries = Object.entries(value); + if (entries.length === 0) return `${indent}_(empty)_`; + return entries + .map(([k, v]) => { + const label = humanizeKey(k); + if (v !== null && typeof v === 'object') { + return `${indent}- **${label}:**\n${jsonToMarkdown(v, depth + 1)}`; + } + return `${indent}- **${label}:** ${v === null || v === undefined ? '' : String(v)}`; + }) + .join('\n'); + } + + return `${indent}${String(value)}`; +}; + +/** + * Detect raw JSON blocks in the streaming buffer and replace them with a + * readable Markdown rendering so the Details section doesn't expose raw JSON. + * Lines that already sit inside an existing fenced code block are left alone. */ const formatBufferContent = (content: string): string => { if (!content) return content; @@ -55,9 +105,8 @@ const formatBufferContent = (content: string): string => { const block = lines.slice(i, endIdx + 1).join('\n'); try { const parsed = JSON.parse(block); - out.push('```json'); - out.push(JSON.stringify(parsed, null, 2)); - out.push('```'); + out.push(jsonToMarkdown(parsed)); + out.push(''); i = endIdx + 1; continue; } catch { From c3d3ed470f72b4e5b6e7ffa83359bead193c9c9c Mon Sep 17 00:00:00 2001 From: "Teja Sri Munnangi (Persistent Systems Inc)" Date: Tue, 16 Jun 2026 20:22:09 +0530 Subject: [PATCH 3/5] commit --- .../streaming/StreamingBufferMessage.tsx | 61 +++++++++++++++---- 1 file changed, 49 insertions(+), 12 deletions(-) diff --git a/src/App/src/components/content/streaming/StreamingBufferMessage.tsx b/src/App/src/components/content/streaming/StreamingBufferMessage.tsx index c70894c87..f57831588 100644 --- a/src/App/src/components/content/streaming/StreamingBufferMessage.tsx +++ b/src/App/src/components/content/streaming/StreamingBufferMessage.tsx @@ -65,29 +65,68 @@ const jsonToMarkdown = (value: any, depth = 0): string => { /** * Detect raw JSON blocks in the streaming buffer and replace them with a * readable Markdown rendering so the Details section doesn't expose raw JSON. - * Lines that already sit inside an existing fenced code block are left alone. + * Handles both bare JSON blocks and fenced code blocks containing JSON + * (e.g. ```json ... ``` or ``` { ... } ```). */ const formatBufferContent = (content: string): string => { if (!content) return content; const lines = content.split('\n'); const out: string[] = []; - let insideExistingFence = false; let i = 0; + const tryRenderJsonBlock = (block: string): string | null => { + const trimmed = block.trim(); + if (!trimmed) return null; + if (!(trimmed.startsWith('{') || trimmed.startsWith('['))) return null; + try { + const parsed = JSON.parse(trimmed); + if (parsed === null || typeof parsed !== 'object') return null; + return jsonToMarkdown(parsed); + } catch { + return null; + } + }; + while (i < lines.length) { const line = lines[i]; const trimmed = line.trim(); + // Fenced code block — try to render as readable JSON if applicable if (trimmed.startsWith('```')) { - insideExistingFence = !insideExistingFence; - out.push(line); - i++; + const fenceLang = trimmed.replace(/^```/, '').trim().toLowerCase(); + // Find closing fence + let endIdx = -1; + for (let j = i + 1; j < lines.length; j++) { + if (lines[j].trim().startsWith('```')) { + endIdx = j; + break; + } + } + if (endIdx === -1) { + // Unterminated fence — pass remaining lines through unchanged + out.push(line); + i++; + continue; + } + + const inner = lines.slice(i + 1, endIdx).join('\n'); + const isJsonLang = fenceLang === 'json' || fenceLang === ''; + const rendered = isJsonLang ? tryRenderJsonBlock(inner) : null; + if (rendered !== null) { + out.push(rendered); + out.push(''); + } else { + // Keep original fenced block as-is + out.push(line); + for (let j = i + 1; j <= endIdx; j++) out.push(lines[j]); + } + i = endIdx + 1; continue; } - if (!insideExistingFence && (trimmed.startsWith('{') || trimmed.startsWith('['))) { - // Try to capture a balanced JSON block starting at this line + // Bare JSON block (not inside a fence) + if (trimmed.startsWith('{') || trimmed.startsWith('[')) { let depth = 0; let endIdx = -1; for (let j = i; j < lines.length; j++) { @@ -103,14 +142,12 @@ const formatBufferContent = (content: string): string => { } if (endIdx !== -1) { const block = lines.slice(i, endIdx + 1).join('\n'); - try { - const parsed = JSON.parse(block); - out.push(jsonToMarkdown(parsed)); + const rendered = tryRenderJsonBlock(block); + if (rendered !== null) { + out.push(rendered); out.push(''); i = endIdx + 1; continue; - } catch { - // Not valid JSON — fall through and keep the original line } } } From 6872b144a8913c9eb03122325dd59834e1bd27b6 Mon Sep 17 00:00:00 2001 From: "Teja Sri Munnangi (Persistent Systems Inc)" Date: Thu, 18 Jun 2026 15:16:09 +0530 Subject: [PATCH 4/5] commit --- .../streaming/StreamingAgentMessage.tsx | 3 +- .../streaming/StreamingBufferMessage.tsx | 150 +-------- src/App/src/utils/jsonFormatter.ts | 310 ++++++++++++++++++ 3 files changed, 314 insertions(+), 149 deletions(-) create mode 100644 src/App/src/utils/jsonFormatter.ts diff --git a/src/App/src/components/content/streaming/StreamingAgentMessage.tsx b/src/App/src/components/content/streaming/StreamingAgentMessage.tsx index 734c54bbd..4179aa0ae 100644 --- a/src/App/src/components/content/streaming/StreamingAgentMessage.tsx +++ b/src/App/src/components/content/streaming/StreamingAgentMessage.tsx @@ -7,6 +7,7 @@ import { Body1, Tag, makeStyles, tokens, Button } from "@fluentui/react-componen import { TaskService } from "@/store"; import { PersonRegular, ArrowDownloadRegular } from "@fluentui/react-icons"; import { getAgentIcon, getAgentDisplayName } from '@/utils/agentIconUtils'; +import { formatJsonInText } from '@/utils/jsonFormatter'; interface StreamingAgentMessageProps { agentMessages: AgentMessageData[]; @@ -306,7 +307,7 @@ const renderAgentMessages = ( ) }} > - {TaskService.cleanHRAgent(msg.content) || ""} + {formatJsonInText(TaskService.cleanHRAgent(msg.content) || "")} diff --git a/src/App/src/components/content/streaming/StreamingBufferMessage.tsx b/src/App/src/components/content/streaming/StreamingBufferMessage.tsx index f57831588..5850e3201 100644 --- a/src/App/src/components/content/streaming/StreamingBufferMessage.tsx +++ b/src/App/src/components/content/streaming/StreamingBufferMessage.tsx @@ -6,159 +6,13 @@ import { CheckmarkCircle20Regular, ArrowTurnDownRightRegular } from '@fluentui/r import ReactMarkdown from "react-markdown"; import remarkGfm from "remark-gfm"; import rehypePrism from "rehype-prism"; +import { formatJsonInText } from "@/utils/jsonFormatter"; interface StreamingBufferMessageProps { streamingMessageBuffer: string; isStreaming?: boolean; } -/** - * Format a key from snake_case / camelCase / kebab-case into a readable label. - */ -const humanizeKey = (key: string): string => { - if (!key) return key; - const spaced = key - .replace(/[_-]+/g, ' ') - .replace(/([a-z0-9])([A-Z])/g, '$1 $2') - .trim(); - return spaced.replace(/\b\w/g, (c) => c.toUpperCase()); -}; - -/** - * Render a parsed JSON value as readable Markdown (bullet list of - * "**Key**: value" entries, recursing into nested objects/arrays). - */ -const jsonToMarkdown = (value: any, depth = 0): string => { - const indent = ' '.repeat(depth); - - if (value === null || value === undefined) return `${indent}_n/a_`; - - if (Array.isArray(value)) { - if (value.length === 0) return `${indent}_(none)_`; - return value - .map((item) => { - if (item !== null && typeof item === 'object') { - return `${indent}- \n${jsonToMarkdown(item, depth + 1)}`; - } - return `${indent}- ${String(item)}`; - }) - .join('\n'); - } - - if (typeof value === 'object') { - const entries = Object.entries(value); - if (entries.length === 0) return `${indent}_(empty)_`; - return entries - .map(([k, v]) => { - const label = humanizeKey(k); - if (v !== null && typeof v === 'object') { - return `${indent}- **${label}:**\n${jsonToMarkdown(v, depth + 1)}`; - } - return `${indent}- **${label}:** ${v === null || v === undefined ? '' : String(v)}`; - }) - .join('\n'); - } - - return `${indent}${String(value)}`; -}; - -/** - * Detect raw JSON blocks in the streaming buffer and replace them with a - * readable Markdown rendering so the Details section doesn't expose raw JSON. - * Handles both bare JSON blocks and fenced code blocks containing JSON - * (e.g. ```json ... ``` or ``` { ... } ```). - */ -const formatBufferContent = (content: string): string => { - if (!content) return content; - - const lines = content.split('\n'); - const out: string[] = []; - let i = 0; - - const tryRenderJsonBlock = (block: string): string | null => { - const trimmed = block.trim(); - if (!trimmed) return null; - if (!(trimmed.startsWith('{') || trimmed.startsWith('['))) return null; - try { - const parsed = JSON.parse(trimmed); - if (parsed === null || typeof parsed !== 'object') return null; - return jsonToMarkdown(parsed); - } catch { - return null; - } - }; - - while (i < lines.length) { - const line = lines[i]; - const trimmed = line.trim(); - - // Fenced code block — try to render as readable JSON if applicable - if (trimmed.startsWith('```')) { - const fenceLang = trimmed.replace(/^```/, '').trim().toLowerCase(); - // Find closing fence - let endIdx = -1; - for (let j = i + 1; j < lines.length; j++) { - if (lines[j].trim().startsWith('```')) { - endIdx = j; - break; - } - } - if (endIdx === -1) { - // Unterminated fence — pass remaining lines through unchanged - out.push(line); - i++; - continue; - } - - const inner = lines.slice(i + 1, endIdx).join('\n'); - const isJsonLang = fenceLang === 'json' || fenceLang === ''; - const rendered = isJsonLang ? tryRenderJsonBlock(inner) : null; - if (rendered !== null) { - out.push(rendered); - out.push(''); - } else { - // Keep original fenced block as-is - out.push(line); - for (let j = i + 1; j <= endIdx; j++) out.push(lines[j]); - } - i = endIdx + 1; - continue; - } - - // Bare JSON block (not inside a fence) - if (trimmed.startsWith('{') || trimmed.startsWith('[')) { - let depth = 0; - let endIdx = -1; - for (let j = i; j < lines.length; j++) { - const l = lines[j]; - for (const ch of l) { - if (ch === '{' || ch === '[') depth++; - else if (ch === '}' || ch === ']') depth--; - } - if (depth === 0) { - endIdx = j; - break; - } - } - if (endIdx !== -1) { - const block = lines.slice(i, endIdx + 1).join('\n'); - const rendered = tryRenderJsonBlock(block); - if (rendered !== null) { - out.push(rendered); - out.push(''); - i = endIdx + 1; - continue; - } - } - } - - out.push(line); - i++; - } - - return out.join('\n'); -}; - // Convert to a proper React component instead of a function const StreamingBufferMessage: React.FC = ({ streamingMessageBuffer, @@ -189,7 +43,7 @@ const StreamingBufferMessage: React.FC = ({ if (!streamingMessageBuffer || streamingMessageBuffer.trim() === "") return null; - const formattedBuffer = formatBufferContent(streamingMessageBuffer); + const formattedBuffer = formatJsonInText(streamingMessageBuffer); return (
{ + if (!key) return key; + const spaced = key + .replace(/[_-]+/g, ' ') + .replace(/([a-z0-9])([A-Z])/g, '$1 $2') + .trim(); + return spaced.replace(/\b\w/g, (c) => c.toUpperCase()); +}; + +/** + * Render a parsed JSON value as readable Markdown (bullet list of + * "**Key**: value" entries, recursing into nested objects/arrays). + */ +export const jsonToMarkdown = (value: any, depth = 0): string => { + const indent = ' '.repeat(depth); + + if (value === null || value === undefined) return `${indent}_n/a_`; + + if (Array.isArray(value)) { + if (value.length === 0) return `${indent}_(none)_`; + return value + .map((item) => { + if (item !== null && typeof item === 'object') { + return `${indent}- \n${jsonToMarkdown(item, depth + 1)}`; + } + return `${indent}- ${String(item)}`; + }) + .join('\n'); + } + + if (typeof value === 'object') { + const entries = Object.entries(value); + if (entries.length === 0) return `${indent}_(empty)_`; + return entries + .map(([k, v]) => { + const label = humanizeKey(k); + if (v !== null && typeof v === 'object') { + return `${indent}- **${label}:**\n${jsonToMarkdown(v, depth + 1)}`; + } + return `${indent}- **${label}:** ${v === null || v === undefined ? '' : String(v)}`; + }) + .join('\n'); + } + + return `${indent}${String(value)}`; +}; + +/** + * Find the end index (inclusive) of a balanced JSON/dict value that starts + * at `content[startIdx]`. Walks the string character-by-character tracking + * string-literal context (both single- and double-quoted) and escape + * sequences, so braces/brackets that appear inside string values do not + * affect the depth count. Returns `-1` if no balanced value can be found. + */ +const findJsonEnd = (content: string, startIdx: number): number => { + const open = content[startIdx]; + if (open !== '{' && open !== '[') return -1; + const close = open === '{' ? '}' : ']'; + + let depth = 0; + let inString = false; + let stringChar = ''; + let escape = false; + + for (let i = startIdx; i < content.length; i++) { + const ch = content[i]; + + if (inString) { + if (escape) { + escape = false; + } else if (ch === '\\') { + escape = true; + } else if (ch === stringChar) { + inString = false; + } + continue; + } + + if (ch === '"' || ch === "'") { + inString = true; + stringChar = ch; + continue; + } + + if (ch === '{' || ch === '[') { + depth++; + } else if (ch === '}' || ch === ']') { + depth--; + if (depth === 0) { + return ch === close ? i : -1; + } + } + } + + return -1; +}; + +/** + * Convert a Python-dict / Python-repr style string into a strict JSON string + * so it can be parsed by `JSON.parse`. Walks character-by-character so that + * single quotes inside double-quoted strings (and vice versa) and escape + * sequences are preserved. + * + * Performs the following transformations on tokens *outside* string literals: + * - Replaces single-quoted string literals with double-quoted ones + * (escaping interior `"`). + * - Replaces `True` / `False` / `None` keywords with `true` / `false` / `null`. + * - Strips trailing commas before `}` or `]`. + */ +const normalizePythonDict = (input: string): string => { + let out = ''; + let i = 0; + const len = input.length; + + while (i < len) { + const ch = input[i]; + + // Double-quoted string — keep as-is, handle escapes + if (ch === '"') { + out += ch; + i++; + while (i < len) { + const c = input[i]; + out += c; + if (c === '\\' && i + 1 < len) { + out += input[i + 1]; + i += 2; + continue; + } + if (c === '"') { + i++; + break; + } + i++; + } + continue; + } + + // Single-quoted string — convert to double-quoted + if (ch === "'") { + out += '"'; + i++; + while (i < len) { + const c = input[i]; + if (c === '\\' && i + 1 < len) { + const next = input[i + 1]; + if (next === "'") { + out += "'"; + } else { + out += c + next; + } + i += 2; + continue; + } + if (c === "'") { + out += '"'; + i++; + break; + } + if (c === '"') { + out += '\\"'; + i++; + continue; + } + out += c; + i++; + } + continue; + } + + // Python keywords — only replace when at a word boundary + const prev = i > 0 ? input[i - 1] : ''; + const isWordBoundary = !prev || !/[A-Za-z0-9_]/.test(prev); + if (isWordBoundary) { + if (input.startsWith('True', i) && !/[A-Za-z0-9_]/.test(input[i + 4] || '')) { + out += 'true'; + i += 4; + continue; + } + if (input.startsWith('False', i) && !/[A-Za-z0-9_]/.test(input[i + 5] || '')) { + out += 'false'; + i += 5; + continue; + } + if (input.startsWith('None', i) && !/[A-Za-z0-9_]/.test(input[i + 4] || '')) { + out += 'null'; + i += 4; + continue; + } + } + + out += ch; + i++; + } + + // Strip trailing commas: `, }` → ` }` and `, ]` → ` ]` + return out.replace(/,(\s*[\]}])/g, '$1'); +}; + +const tryParseJson = (block: string): any | null => { + const trimmed = block.trim(); + if (!trimmed) return null; + // Strict JSON first + try { + const parsed = JSON.parse(trimmed); + if (parsed !== null && typeof parsed === 'object') return parsed; + } catch { + // fall through to fallback + } + // Python-dict / loose JSON fallback (single quotes, True/False/None, trailing commas) + try { + const normalized = normalizePythonDict(trimmed); + const parsed = JSON.parse(normalized); + if (parsed !== null && typeof parsed === 'object') return parsed; + } catch { + // ignore + } + return null; +}; + +/** + * Detect raw JSON / Python-dict blocks anywhere in the input text and + * replace each one with a readable Markdown rendering. Handles: + * - Bare JSON / dict values appearing mid-text + * - JSON inside fenced code blocks (```json ... ``` or ``` ... ```) + * - Python-style dicts with single quotes / True / False / None + * - Multiple independent blocks in the same buffer + * + * Uses a string-aware scanner so quotes and braces inside string values do + * not throw off the balance count. + */ +export const formatJsonInText = (content: string): string => { + if (!content) return content; + + let out = ''; + let i = 0; + + while (i < content.length) { + const ch = content[i]; + + // Handle a fenced code block beginning here + if (ch === '`' && content.startsWith('```', i)) { + const fenceStart = i; + const lineEnd = content.indexOf('\n', i); + const headerEnd = lineEnd === -1 ? content.length : lineEnd; + const fenceLang = content.slice(i + 3, headerEnd).trim().toLowerCase(); + const closeIdx = content.indexOf('```', headerEnd); + if (closeIdx === -1) { + // Unterminated fence; emit the rest verbatim + out += content.slice(i); + i = content.length; + continue; + } + const inner = content.slice(headerEnd + 1, closeIdx); + const isJsonLang = fenceLang === 'json' || fenceLang === '' || fenceLang === 'python'; + if (isJsonLang) { + const innerTrimmedStart = inner.search(/[{[]/); + if (innerTrimmedStart !== -1) { + const endRel = findJsonEnd(inner, innerTrimmedStart); + if (endRel !== -1) { + const block = inner.slice(innerTrimmedStart, endRel + 1); + const parsed = tryParseJson(block); + if (parsed !== null) { + const prefix = inner.slice(0, innerTrimmedStart); + const suffix = inner.slice(endRel + 1); + if (prefix.trim()) out += prefix; + out += jsonToMarkdown(parsed) + '\n'; + if (suffix.trim()) out += suffix; + i = closeIdx + 3; + continue; + } + } + } + } + // Couldn't render as readable JSON — keep fenced block as-is + out += content.slice(fenceStart, closeIdx + 3); + i = closeIdx + 3; + continue; + } + + // Handle a bare JSON / dict value beginning here + if (ch === '{' || ch === '[') { + const endIdx = findJsonEnd(content, i); + if (endIdx !== -1) { + const block = content.slice(i, endIdx + 1); + const parsed = tryParseJson(block); + if (parsed !== null) { + out += jsonToMarkdown(parsed) + '\n'; + i = endIdx + 1; + continue; + } + } + } + + out += ch; + i++; + } + + return out; +}; From 7f95f3938f9032b6f2c24b9bdf5bf8c77c30ff04 Mon Sep 17 00:00:00 2001 From: "Teja Sri Munnangi (Persistent Systems Inc)" Date: Thu, 18 Jun 2026 16:38:20 +0530 Subject: [PATCH 5/5] commit --- .../streaming/StreamingBufferMessage.tsx | 99 ++++++++++++------- 1 file changed, 65 insertions(+), 34 deletions(-) diff --git a/src/App/src/components/content/streaming/StreamingBufferMessage.tsx b/src/App/src/components/content/streaming/StreamingBufferMessage.tsx index 5850e3201..dd7a03339 100644 --- a/src/App/src/components/content/streaming/StreamingBufferMessage.tsx +++ b/src/App/src/components/content/streaming/StreamingBufferMessage.tsx @@ -7,12 +7,12 @@ import ReactMarkdown from "react-markdown"; import remarkGfm from "remark-gfm"; import rehypePrism from "rehype-prism"; import { formatJsonInText } from "@/utils/jsonFormatter"; - + interface StreamingBufferMessageProps { streamingMessageBuffer: string; isStreaming?: boolean; } - + // Convert to a proper React component instead of a function const StreamingBufferMessage: React.FC = ({ streamingMessageBuffer, @@ -22,7 +22,7 @@ const StreamingBufferMessage: React.FC = ({ const [shouldFade, setShouldFade] = useState(false); const contentRef = useRef(null); const prevBufferLength = useRef(0); - + // Trigger fade effect when new content is being streamed useEffect(() => { if (isStreaming && streamingMessageBuffer.length > prevBufferLength.current) { @@ -33,18 +33,18 @@ const StreamingBufferMessage: React.FC = ({ } prevBufferLength.current = streamingMessageBuffer.length; }, [streamingMessageBuffer, isStreaming]); - + // Auto-scroll to bottom when streaming useEffect(() => { if (isStreaming && !isExpanded && contentRef.current) { contentRef.current.scrollTop = contentRef.current.scrollHeight; } }, [streamingMessageBuffer, isStreaming, isExpanded]); - + if (!streamingMessageBuffer || streamingMessageBuffer.trim() === "") return null; - + const formattedBuffer = formatJsonInText(streamingMessageBuffer); - + return (
= ({ AI Thinking Process
- +
- + {/* Content area - collapsed state */} {!isExpanded && (
= ({ pointerEvents: 'none', zIndex: 1 }} /> - +
= ({ onMouseLeave={(e) => { e.currentTarget.style.textDecoration = 'none'; }} - /> - ), - p: ({ node, ...props }) => ( -

- ) - }} + /> + ), + + p: ({ node, ...props }) => ( +

+ ), + + img: ({ node, ...props }) => ( + + ) + }} > {formattedBuffer} @@ -191,7 +207,7 @@ const StreamingBufferMessage: React.FC = ({

)} - + {/* Content area - expanded state */} {isExpanded && ( ); }; - + const MemoizedStreamingBufferMessage = React.memo(StreamingBufferMessage); MemoizedStreamingBufferMessage.displayName = 'StreamingBufferMessage'; export default MemoizedStreamingBufferMessage; \ No newline at end of file