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 name: string id: number loading: boolean } type ToolCallBlockState = { collapseState: { [id: number]: boolean } setCollapseState: (id: number, expanded: boolean) => void } const useToolCallBlockStore = create((set) => ({ collapseState: {}, setCollapseState: (id, expanded) => set((state) => ({ collapseState: { ...state.collapseState, [id]: expanded, }, })), })) // 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 && ( )}
{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' }} /> )}
) } export default ToolCallBlock