'use client'; import React, { useState } from 'react'; import { ChevronDown, X, Crosshair, LayoutGrid, Image as ImageIcon, Info, FileText, Mic } from 'lucide-react'; import { cn } from '@/lib/utils'; import type { PlacedBlock } from '@/lib/semantic-blocks/types'; import { getBlockById } from '@/lib/semantic-blocks/registry'; import type { PendingImage, PendingFile, PendingAudio } from '@/lib/llm/multi-agent-orchestrator'; import type { ContentBlock } from '@/lib/llm/types'; interface FocusContextData { domPath: string; snippet: string; } interface SemanticBlockData { name: string; domPath: string; position: string; description: string; } interface MessageContextProps { focusContext?: FocusContextData | null; semanticBlocks?: (PlacedBlock[] | SemanticBlockData[]); images?: PendingImage[]; /** For readOnly mode: image content blocks from the stored message */ imageBlocks?: ContentBlock[]; files?: PendingFile[]; /** For readOnly mode: attached file names from the stored message */ fileNames?: { name: string }[]; audioClips?: PendingAudio[]; /** For readOnly mode: input_audio content blocks from the stored message */ audioBlocks?: ContentBlock[]; /** Non-dismissable system note (e.g. "user declined project creation") */ systemNote?: string | null; onClearFocus?: () => void; onRemoveBlock?: (placementId: string) => void; onClearBlocks?: () => void; onRemoveImage?: (imageId: string) => void; onClearImages?: () => void; onRemoveFile?: (fileId: string) => void; onClearFiles?: () => void; onRemoveAudio?: (audioId: string) => void; onClearAudio?: () => void; /** readOnly: start the context expanded (e.g. an attachment-only message) */ defaultOpen?: boolean; readOnly?: boolean; } function isPlacedBlock(b: PlacedBlock | SemanticBlockData): b is PlacedBlock { return 'blockId' in b; } function getBlockName(b: PlacedBlock | SemanticBlockData): string { if (isPlacedBlock(b)) return getBlockById(b.blockId)?.name || b.blockId; return b.name; } function getBlockDescription(b: PlacedBlock | SemanticBlockData): string { if (isPlacedBlock(b)) return getBlockById(b.blockId)?.description || ''; return b.description; } function getBlockDomPath(b: PlacedBlock | SemanticBlockData): string { return b.domPath; } function getBlockPosition(b: PlacedBlock | SemanticBlockData): string { return b.position; } function getBlockKey(b: PlacedBlock | SemanticBlockData, i: number): string { if (isPlacedBlock(b)) return b.placementId; return `sb-${i}`; } /** Collapsible section within the unified context component */ function Section({ icon: Icon, label, summary, onClear, children, defaultOpen = false, readOnly = false, }: { icon: React.ElementType; label: string; summary?: string; onClear?: () => void; children: React.ReactNode; defaultOpen?: boolean; readOnly?: boolean; }) { const [open, setOpen] = useState(defaultOpen); return (
{open && (
{children}
)}
); } /** Code snippet box — shared between focus and block descriptions */ function SnippetBox({ children, className }: { children: React.ReactNode; className?: string }) { return (
      {children}
    
); } export function MessageContext({ focusContext, semanticBlocks, images, imageBlocks, files, fileNames, audioClips, audioBlocks, systemNote, onClearFocus, onRemoveBlock, onClearBlocks, onRemoveImage, onClearImages, onRemoveFile, onClearFiles, onRemoveAudio, onClearAudio, defaultOpen, readOnly = false, }: MessageContextProps) { const hasBlocks = semanticBlocks && semanticBlocks.length > 0; const hasImages = images && images.length > 0; const hasImageBlocks = imageBlocks && imageBlocks.some(b => b.type === 'image_url'); const hasFiles = files && files.length > 0; const hasFileNames = fileNames && fileNames.length > 0; const hasAudio = audioClips && audioClips.length > 0; const hasAudioBlocks = audioBlocks && audioBlocks.some(b => b.type === 'input_audio'); const hasFocus = !!focusContext; const hasNote = !!systemNote; if (!hasFocus && !hasBlocks && !hasImages && !hasImageBlocks && !hasFiles && !hasFileNames && !hasAudio && !hasAudioBlocks && !hasNote) return null; // Build summary for collapsed readOnly view const summaryParts: string[] = []; if (hasNote) summaryParts.push('note'); if (hasFocus) summaryParts.push('focus'); if (hasBlocks) summaryParts.push(`${semanticBlocks!.length} block${semanticBlocks!.length !== 1 ? 's' : ''}`); if (hasImages) summaryParts.push(`${images!.length} image${images!.length !== 1 ? 's' : ''}`); if (!hasImages && hasImageBlocks) { const count = imageBlocks!.filter(b => b.type === 'image_url').length; summaryParts.push(`${count} image${count !== 1 ? 's' : ''}`); } const fileCount = (files?.length ?? 0) || (fileNames?.length ?? 0); if (fileCount > 0) summaryParts.push(`${fileCount} file${fileCount !== 1 ? 's' : ''}`); const audioCount = (audioClips?.length ?? 0) || (audioBlocks?.filter(b => b.type === 'input_audio').length ?? 0); if (audioCount > 0) summaryParts.push(`${audioCount} recording${audioCount !== 1 ? 's' : ''}`); if (readOnly) { return ; } return (
{/* Header */}
Included in next message
{/* Sections */} {hasNote && (
40 ? systemNote!.slice(0, 37) + '...' : systemNote!} readOnly={true} >

{systemNote}

)} {hasFocus && (
').slice(-1)[0]} onClear={onClearFocus} readOnly={readOnly} >
{focusContext!.domPath}
{focusContext!.snippet && ( {focusContext!.snippet} )}
)} {hasBlocks && (
{semanticBlocks!.map((block, i) => (
{getBlockName(block)} {!readOnly && onRemoveBlock && isPlacedBlock(block) && ( )}
{getBlockPosition(block)} {getBlockDomPath(block)}
{getBlockDescription(block) && ( {getBlockDescription(block)} )}
))}
)} {hasImages && (
{images!.map((img) => (
Attached {!readOnly && onRemoveImage && ( )}
))}
)} {hasFiles && (
{files!.map((f) => (
{f.name} {!readOnly && onRemoveFile && ( )}
))}
)} {hasAudio && (
{audioClips!.map((clip) => (
{clip.data ? (
))}
)}
); } /** Read-only version for user messages in chat history */ function ReadOnlyContext({ focusContext, semanticBlocks, imageBlocks, fileNames, audioBlocks, summary, defaultOpen, }: { focusContext?: FocusContextData | null; semanticBlocks?: (PlacedBlock[] | SemanticBlockData[]); imageBlocks?: ContentBlock[]; fileNames?: { name: string }[]; audioBlocks?: ContentBlock[]; summary: string; defaultOpen?: boolean; }) { const [open, setOpen] = useState(defaultOpen ?? false); const hasBlocks = semanticBlocks && semanticBlocks.length > 0; const hasFocus = !!focusContext; const actualImageBlocks = imageBlocks?.filter(b => b.type === 'image_url') || []; const hasImages = actualImageBlocks.length > 0; const hasFiles = fileNames && fileNames.length > 0; const actualAudioBlocks = audioBlocks?.filter(b => b.type === 'input_audio') || []; const hasAudio = actualAudioBlocks.length > 0; return (
{open && (
{hasFocus && (
Focus {focusContext!.domPath.split(' > ').slice(-1)[0]}
{focusContext!.snippet && ( {focusContext!.snippet} )}
)} {hasBlocks && (
Blocks
{semanticBlocks!.map((block, i) => (
{getBlockName(block)} {getBlockPosition(block)} {getBlockDomPath(block).split(' > ').slice(-2).join(' > ')}
{getBlockDescription(block) && ( {getBlockDescription(block)} )}
))}
)} {hasImages && (
Images ({actualImageBlocks.length})
{actualImageBlocks.map((block, i) => ( block.type === 'image_url' && ( Attached ) ))}
)} {hasFiles && (
Files ({fileNames!.length})
{fileNames!.map((f, i) => ( {f.name} ))}
)} {hasAudio && (
Voice ({actualAudioBlocks.length})
{actualAudioBlocks.map((block, i) => ( block.type === 'input_audio' && (
)}
)}
); }