import { ChevronDown, ChevronUp, Loader } from 'lucide-react' import { cn } from '@/lib/utils' import { create } from 'zustand' import { RenderMarkdown } from '@/containers/RenderMarkdown' import { useMemo, useState } from 'react' import { twMerge } from 'tailwind-merge' import { useTranslation } from '@/i18n/react-i18next-compat' import ImageModal from '@/containers/dialogs/ImageModal' interface Props { result: string name: string args: object 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}`)} />
) } // For any other types, render as JSON return (
) } const ToolCallBlock = ({ id, name, result, loading, args }: Props) => { const { collapseState, setCollapseState } = useToolCallBlockStore() const { t } = useTranslation() const isExpanded = collapseState[id] ?? (loading ? true : 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 } = useMemo(() => { return parseMCPResponse(result) }, [result]) return (
{loading && (
)}
{args && Object.keys(args).length > 3 && ( <>

Arguments:

)} {result && ( <>

Output:

{hasStructuredContent ? ( /* Render each content item individually based on its type */
{contentItems.map((item, index) => ( ))}
) : ( /* Fallback: render as JSON for valid JSON but unstructured responses */ )} )}
) } export default ToolCallBlock