import { useState, useCallback } from 'react';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import rehypeHighlight from 'rehype-highlight';
import { Maximize2, Download, Code, X } from 'lucide-react';
import './MessageBubble.css';
export type MessageRole = 'user' | 'assistant' | 'system';
export interface MediaItem {
type: 'plot' | 'video';
base64: string;
path?: string;
code?: string;
mimetype?: string;
}
export interface ChatMessage {
id: string;
role: MessageRole;
content: string;
plots?: string[]; // base64 PNG strings (legacy)
media?: MediaItem[]; // full media items (plot + video)
toolLabel?: string; // e.g. "Fetching ERA5..."
statusText?: string; // e.g. "🔍 Analyzing..."
arraylakeSnippets?: string[];
isStreaming?: boolean;
}
/* ── Image Modal ── */
function ImageModal({ src, onClose }: { src: string; onClose: () => void }) {
return (
e.stopPropagation()}>
);
}
/* ── Plot Figure ── */
function PlotFigure({ item, onEnlarge }: { item: MediaItem; onEnlarge: (src: string) => void }) {
const [showCode, setShowCode] = useState(false);
const src = item.base64.startsWith('data:') ? item.base64 : `data:image/png;base64,${item.base64}`;
return (
onEnlarge(src)}
style={{ cursor: 'pointer' }}
/>
{item.code && item.code.trim() && (
)}
{showCode && item.code && (
{item.code}
)}
);
}
/* ── Video Figure ── */
function VideoFigure({ item, onEnlarge }: { item: MediaItem; onEnlarge: (src: string) => void }) {
const isGif = item.mimetype === 'image/gif';
const src = item.base64.startsWith('data:') ? item.base64 : `data:${item.mimetype || 'video/mp4'};base64,${item.base64}`;
const handleDownload = () => {
const a = document.createElement('a');
a.href = src;
const ext = isGif ? 'gif' : item.mimetype?.includes('webm') ? 'webm' : 'mp4';
a.download = item.path ? item.path.split('/').pop()! : `eurus_animation.${ext}`;
a.click();
};
if (isGif) {
return (
onEnlarge(src)} style={{ cursor: 'pointer' }} />
);
}
return (
);
}
/* ── Arraylake Snippet ── */
function ArraylakeSnippet({ code }: { code: string }) {
const [open, setOpen] = useState(false);
// Strip markdown fences
const clean = code
.replace(/^\n?📦[^\n]*\n/, '')
.replace(/^```python\n?/, '')
.replace(/\n?```$/, '')
.trim();
return (
{open && (
{clean}
)}
);
}
/* ── Legacy plot ── */
function LegacyPlotImage({ base64, onEnlarge }: { base64: string; onEnlarge: (src: string) => void }) {
const src = base64.startsWith('data:') ? base64 : `data:image/png;base64,${base64}`;
return (
onEnlarge(src)} style={{ cursor: 'pointer' }} />
);
}
/* ── Main Bubble ── */
export default function MessageBubble({ msg }: { msg: ChatMessage }) {
const isUser = msg.role === 'user';
const [modalSrc, setModalSrc] = useState(null);
const handleEnlarge = useCallback((src: string) => setModalSrc(src), []);
return (
<>
{msg.toolLabel && (
⚙ {msg.toolLabel}
)}
{msg.statusText && (
{msg.statusText}
)}
{msg.content}
{/* Legacy plots */}
{msg.plots?.map((b64, i) =>
)}
{/* Rich media (plots + videos) */}
{msg.media?.map((item, i) =>
item.type === 'video'
?
:
)}
{/* Arraylake snippets */}
{msg.arraylakeSnippets?.map((s, i) =>
)}
{msg.isStreaming && !msg.media?.length && !msg.arraylakeSnippets?.length &&
▍}
{modalSrc && setModalSrc(null)} />}
>
);
}