From dcb3f794d329fcbfaa34a17f47e4795be0ddd3d0 Mon Sep 17 00:00:00 2001 From: Sam Hoang Van Date: Tue, 10 Jun 2025 15:06:42 +0700 Subject: [PATCH] Feat: render mcp content (#5229) --- web-app/src/containers/ToolCallBlock.tsx | 176 +++++++++++++++++++++-- 1 file changed, 167 insertions(+), 9 deletions(-) diff --git a/web-app/src/containers/ToolCallBlock.tsx b/web-app/src/containers/ToolCallBlock.tsx index f0f6ca934..f1e50c939 100644 --- a/web-app/src/containers/ToolCallBlock.tsx +++ b/web-app/src/containers/ToolCallBlock.tsx @@ -2,6 +2,13 @@ import { ChevronDown, ChevronUp, Loader } from 'lucide-react' import { cn } from '@/lib/utils' import { create } from 'zustand' import { RenderMarkdown } from './RenderMarkdown' +import { useMemo, useState } from 'react' +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog' interface Props { result: string @@ -26,23 +33,130 @@ const useToolCallBlockStore = create((set) => ({ })), })) +// Types for MCP response content +interface MCPContentItem { + type: string + data?: string + text?: string + mimeType?: string +} + +interface MCPResponse { + content?: MCPContentItem[] +} + +// Utility function to create data URL from base64 and mimeType +const createDataUrl = (base64Data: string, mimeType: string): string => { + // Handle case where base64 data might already include data URL prefix + if (base64Data.startsWith('data:')) { + return base64Data + } + return `data:${mimeType};base64,${base64Data}` +} + +// Parse MCP response and extract content items +const parseMCPResponse = (result: string) => { + try { + const parsed: MCPResponse = JSON.parse(result) + const content = parsed.content || [] + + return { + parsedResult: parsed, + contentItems: content, + hasStructuredContent: content.length > 0, + parseError: false, + } + } catch { + // Fallback: JSON parsing failed, treat as plain text + return { + parsedResult: result, + contentItems: [], + hasStructuredContent: false, + parseError: true, + } + } +} + +// Component to render individual content items based on type +const ContentItemRenderer = ({ + item, + index, + onImageClick, +}: { + item: MCPContentItem + index: number + onImageClick?: (imageUrl: string, alt: string) => void +}) => { + if (item.type === 'image' && item.data && item.mimeType) { + const imageUrl = createDataUrl(item.data, item.mimeType) + return ( +
+ {`Result { + // Hide broken images + e.currentTarget.style.display = 'none' + }} + onClick={() => onImageClick?.(imageUrl, `Result image ${index + 1}`)} + /> +
+ ) + } + + if (item.type === 'text' && item.text) { + return ( +
+ +
+ ) + } + + // For any other types, render as JSON + return ( +
+ +
+ ) +} + const ToolCallBlock = ({ id, name, result, loading }: Props) => { const { collapseState, setCollapseState } = useToolCallBlockStore() const isExpanded = collapseState[id] ?? false + const [modalImage, setModalImage] = useState<{ + url: string + alt: string + } | null>(null) const handleClick = () => { const newExpandedState = !isExpanded setCollapseState(id, newExpandedState) } + const handleImageClick = (imageUrl: string, alt: string) => { + setModalImage({ url: imageUrl, alt }) + } + + const closeModal = () => { + setModalImage(null) + } + + // Parse the MCP response and extract content items + const { parsedResult, contentItems, hasStructuredContent, parseError } = + useMemo(() => { + return parseMCPResponse(result) + }, [result]) + return (
-
+
{loading && ( )} @@ -66,16 +180,60 @@ const ToolCallBlock = ({ id, name, result, loading }: Props) => { )} >
- + {hasStructuredContent ? ( + /* Render each content item individually based on its type */ +
+ {contentItems.map((item, index) => ( + + ))} +
+ ) : parseError ? ( + /* Handle JSON parse error - render as plain text */ +
+
+ Raw Response: +
+
+ {parsedResult as string} +
+
+ ) : ( + /* Fallback: render as JSON for valid JSON but unstructured responses */ + + )}
+ + {/* Image Modal */} + !open && closeModal()}> + + + {modalImage?.alt || 'Image'} + +
+ {modalImage && ( + {modalImage.alt} { + e.currentTarget.style.display = 'none' + }} + /> + )} +
+
+
) }