import React, { useEffect, useMemo, useRef } from 'react'; interface ResearchProgressModalProps { open: boolean; title?: string; status?: string; messages: Array<{ timestamp: string; message: string }>; error?: string | null; onClose: () => void; } type Tone = 'info' | 'active' | 'success' | 'warning' | 'error'; type StageState = 'upcoming' | 'active' | 'done' | 'error'; const statusThemes: Record< string, { label: string; description: string; color: string; background: string } > = { pending: { label: 'Queued', description: 'Preparing the research workflow…', color: '#1f2937', background: '#e5e7eb' }, running: { label: 'In Progress', description: 'Gathering sources and extracting insights.', color: '#1d4ed8', background: '#dbeafe' }, completed: { label: 'Completed', description: 'Research results are ready to review.', color: '#047857', background: '#d1fae5' }, success: { label: 'Completed', description: 'Research results are ready to review.', color: '#047857', background: '#d1fae5' }, succeeded: { label: 'Completed', description: 'Research results are ready to review.', color: '#047857', background: '#d1fae5' }, finished: { label: 'Completed', description: 'Research results are ready to review.', color: '#047857', background: '#d1fae5' }, failed: { label: 'Needs Attention', description: 'We hit an issue while running research.', color: '#b91c1c', background: '#fee2e2' } }; const toneStyles: Record = { info: { bg: '#f8fafc', border: '#e2e8f0', text: '#0f172a' }, active: { bg: '#eff6ff', border: '#bfdbfe', text: '#1d4ed8' }, success: { bg: '#ecfdf5', border: '#bbf7d0', text: '#047857' }, warning: { bg: '#fff7ed', border: '#fed7aa', text: '#c2410c' }, error: { bg: '#fef2f2', border: '#fecaca', text: '#b91c1c' } }; const stageDefinitions = [ { id: 'cache', label: 'Cache Check', description: 'Looking for saved research results to speed things up.', icon: '🗂️', keywords: ['cache', 'cached', 'stored'] }, { id: 'discovery', label: 'Source Discovery', description: 'Exploring trusted sources across the web.', icon: '🔎', keywords: ['search', 'source', 'gather', 'google', 'discover'] }, { id: 'analysis', label: 'Insight Extraction', description: 'Extracting data points, statistics, and quotes.', icon: '🧠', keywords: ['analysis', 'analyz', 'extract', 'insight', 'processing'] }, { id: 'assembly', label: 'Structuring Findings', description: 'Packaging insights and preparing summaries.', icon: '📝', keywords: ['assembling', 'structuring', 'summary', 'completed', 'ready', 'post-processing'] } ] as const; type StageId = (typeof stageDefinitions)[number]['id']; interface MessageMeta { timestamp: string; timeLabel: string; raw: string; title: string; subtitle?: string; icon: string; tone: Tone; stage: StageId | null; } const completionStatuses = new Set(['completed', 'success', 'succeeded', 'finished']); const formatTime = (timestamp: string) => { try { return new Intl.DateTimeFormat('en-US', { hour: 'numeric', minute: '2-digit', second: '2-digit' }).format(new Date(timestamp)); } catch { return timestamp; } }; const inferStage = (text: string): StageId | null => { const lower = text.toLowerCase(); for (const stage of stageDefinitions) { if (stage.keywords.some(keyword => lower.includes(keyword))) { return stage.id; } } return null; }; const friendlyMappings: Array<{ keywords: string[]; title: string; subtitle?: string; icon: string; tone: Tone; stage?: StageId; }> = [ { keywords: ['checking cache', 'cache'], title: 'Checking existing research cache', subtitle: 'Looking for previously generated insights so we can respond instantly.', icon: '🗂️', tone: 'info', stage: 'cache' }, { keywords: ['found cached research', 'loading cached'], title: 'Loaded cached research results', subtitle: 'Serving saved insights to keep things fast.', icon: '⚡', tone: 'success', stage: 'cache' }, { keywords: ['starting research'], title: 'Launching fresh research', subtitle: 'Bootstrapping the workflow and validating your request.', icon: '🚀', tone: 'active', stage: 'discovery' }, { keywords: ['search', 'query', 'sources', 'web'], title: 'Collecting authoritative sources', subtitle: 'Evaluating top-ranked pages, studies, and reports.', icon: '🔎', tone: 'active', stage: 'discovery' }, { keywords: ['extracting', 'analyzing', 'analysis', 'insight'], title: 'Extracting key insights', subtitle: 'Summarising statistics, trends, and quotes that matter.', icon: '🧠', tone: 'active', stage: 'analysis' }, { keywords: ['assembling', 'compiling', 'structuring', 'post-processing'], title: 'Structuring the research package', subtitle: 'Organising findings into ready-to-use sections.', icon: '🧩', tone: 'info', stage: 'assembly' }, { keywords: ['completed successfully', 'research completed', 'ready'], title: 'Research completed successfully', subtitle: 'All insights are ready for the outline phase.', icon: '✅', tone: 'success', stage: 'assembly' }, { keywords: ['failed', 'error', 'limit exceeded'], title: 'Research encountered an issue', subtitle: 'Review the error message below and try again.', icon: '⚠️', tone: 'error' } ]; const sanitizeTitle = (text: string) => text.replace(/^[^a-zA-Z0-9]+/, '').trim(); const mapMessageToMeta = (message: { timestamp: string; message: string }): MessageMeta => { const raw = message.message || ''; const lower = raw.toLowerCase(); const mapping = friendlyMappings.find(entry => entry.keywords.some(keyword => lower.includes(keyword)) ); if (mapping) { return { timestamp: message.timestamp, timeLabel: formatTime(message.timestamp), raw, title: mapping.title, subtitle: mapping.subtitle, icon: mapping.icon, tone: mapping.tone, stage: mapping.stage ?? inferStage(raw) }; } const stage = inferStage(raw); return { timestamp: message.timestamp, timeLabel: formatTime(message.timestamp), raw, title: sanitizeTitle(raw) || 'Update received', icon: '📝', tone: 'info', stage }; }; const stageStateCopy: Record = { upcoming: { label: 'Pending', color: '#6b7280', background: '#f3f4f6', border: '#e5e7eb' }, active: { label: 'In Progress', color: '#2563eb', background: '#eff6ff', border: '#bfdbfe' }, done: { label: 'Completed', color: '#047857', background: '#ecfdf5', border: '#bbf7d0' }, error: { label: 'Needs Attention', color: '#b91c1c', background: '#fee2e2', border: '#fecaca' } }; const ResearchProgressModal: React.FC = ({ open, title = 'Research in progress', status, messages, error, onClose }) => { const scrollRef = useRef(null); const normalizedStatus = (status || '').toLowerCase(); const statusKey = error ? 'failed' : normalizedStatus; const statusInfo = statusThemes[statusKey] || statusThemes.pending; const processedMessages = useMemo(() => { if (!messages || messages.length === 0) { return [] as MessageMeta[]; } return messages.map(mapMessageToMeta); }, [messages]); useEffect(() => { if (scrollRef.current) { scrollRef.current.scrollTo({ top: scrollRef.current.scrollHeight, behavior: 'smooth' }); } }, [processedMessages.length]); const latestMessage = processedMessages.length > 0 ? processedMessages[processedMessages.length - 1] : null; const stagesWithState = useMemo(() => { const states: StageState[] = stageDefinitions.map(() => 'upcoming'); let highestCompletedIndex = -1; processedMessages.forEach(meta => { if (!meta.stage) { return; } const idx = stageDefinitions.findIndex(stage => stage.id === meta.stage); if (idx === -1) { return; } if (meta.tone === 'error' || /error|failed/i.test(meta.raw)) { states[idx] = 'error'; } else { states[idx] = 'done'; if (idx > highestCompletedIndex) { highestCompletedIndex = idx; } } }); if (!error) { const firstPending = states.findIndex(state => state === 'upcoming'); if (firstPending !== -1 && !completionStatuses.has(normalizedStatus)) { states[firstPending] = 'active'; } else if (completionStatuses.has(normalizedStatus)) { for (let i = 0; i < states.length; i += 1) { if (states[i] !== 'error') { states[i] = 'done'; } } } } else if (highestCompletedIndex >= 0) { states[highestCompletedIndex] = 'error'; } return stageDefinitions.map((stage, index) => ({ ...stage, state: states[index] })); }, [error, normalizedStatus, processedMessages]); if (!open) { return null; } return (

{title}

We are gathering sources, extracting insights, and assembling a research brief tailored to your topic.

{statusInfo.label} {statusInfo.description}
{stagesWithState.map(stage => { const copy = stageStateCopy[stage.state]; return (
{stage.icon} {stage.label}
{stage.description}
{copy.label}
); })}
{latestMessage && (
{latestMessage.icon}
{latestMessage.title}
{latestMessage.timeLabel}
{latestMessage.subtitle && (
{latestMessage.subtitle}
)} {latestMessage.raw && (
{latestMessage.raw}
)}
)}
{processedMessages.length === 0 && (
Awaiting progress updates…
)} {processedMessages.map((meta, index) => { const styles = toneStyles[meta.tone]; return (
{meta.icon}
{meta.title}
{meta.timeLabel}
{meta.subtitle && (
{meta.subtitle}
)} {meta.raw && (
{meta.raw}
)}
); })}
{error && (
Error: {error}
)}
); }; export default ResearchProgressModal;