'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 (
{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 {systemNote}