import React, { useMemo } from 'react'; import { Table2, FileText, FileCode, FileSpreadsheet, History } from 'lucide-react'; import MessageBubble from './MessageBubble'; import OrchestratorMessage from './OrchestratorMessage'; import FailsafePauseBanner from './FailsafePauseBanner'; import HumanInputSlot from './HumanInputSlot'; import HumanTurnIndicator from './HumanTurnIndicator'; import { DEFAULT_DEMO_PERSONAS, DEFAULT_PARTICIPANT_IDS } from '../utils/storage'; /** Update when the Neon contact destination changes on the redesigned site. */ const NEON_CONTACT_URL = 'https://www.neon.ai/contact'; const [ELENA_ID, MARCUS_ID, AMIRA_ID] = DEFAULT_PARTICIPANT_IDS; /** Dialogue-only turns for the empty-state sample; speaker ids come from the demo trio. */ const SAMPLE_PREVIEW_TURNS = [ { kind: 'orchestrator', text: 'Question: Should a company build its own AI system or rely on a third-party provider? ' + 'Each participant: share your initial view.', }, { kind: 'participant', speakerId: ELENA_ID, text: 'Third-party APIs keep upfront cost low, but per-seat and per-token fees compound fast at scale — ' + 'above a few million in annual AI spend, owning the stack often wins on total cost over three to five years.', }, { kind: 'participant', speakerId: MARCUS_ID, text: 'Build gives you control over models, prompts, and integrations; buy gets you to production in weeks. ' + "I'd only build if the product is the AI itself, not if AI is supporting something else.", }, { kind: 'participant', speakerId: AMIRA_ID, addressedTo: MARCUS_ID, replyingTo: [MARCUS_ID], text: 'Where data lives matters more than who hosts the model. A vendor with strong certifications can beat a sloppy ' + 'in-house deployment — but regulated workloads need clarity on retention, subprocessors, and whether prompts leave your boundary.', }, { kind: 'orchestrator', text: 'No single answer fits every company — the panel leans toward buy for speed and lower early risk, build when AI is ' + 'core IP or scale makes vendor fees dominate, with privacy and compliance as the tiebreaker.', }, ]; function buildSamplePreviewMessages(nameById) { return SAMPLE_PREVIEW_TURNS.map((turn) => { if (turn.kind === 'orchestrator') { return { kind: 'orchestrator', text: turn.text }; } const speaker_id = turn.speakerId; return { kind: 'participant', role: 'participant', speaker_id, speaker_name: nameById[speaker_id] || '', text: turn.text, ...(turn.addressedTo ? { addressed_to: turn.addressedTo, replying_to: turn.replyingTo } : {}), }; }); } /** * Renders the conversation: a mix of participant bubbles, orchestrator * status banners, and the failsafe-pause continue control. Participant * coloring is derived from each participant's index in the active * roster, so colors are stable per-participant for the whole chat. * * After "End of Chat" arrives we also render a download strip below the * stats line that mirrors the header DownloadMenu items 1:1 (Summary * table view, .txt, .md, .csv, full API log). Per UX request these * stack vertically on narrow viewports. */ export default function ChatArea({ messages, systemMessages, isRunning, hasEnoughParticipantsToStart, statusText, pause, onContinuePause, participants, showResponseTime, showChatStats, awaitingHuman, humanSubmitting, onHumanSubmit, onHumanSkip, onShowTableView, onDownloadChatTxt, onDownloadChatMd, onDownloadCsvTable, onDownloadApiLog, hasApiLog, }) { const speakerIdxFor = useMemo(() => { const map = {}; (participants || []).forEach((p, i) => { map[p.participant_id] = i; }); return map; }, [participants]); const participantNameById = useMemo(() => { const m = {}; (participants || []).forEach((p) => { m[p.participant_id] = p.name; }); return m; }, [participants]); const defaultDemoNameById = useMemo(() => { const m = {}; DEFAULT_DEMO_PERSONAS.forEach((p) => { m[p.participant_id] = p.name; }); return m; }, []); /** Names for sample bubbles: live roster when present, else catalog mirror in storage. */ const previewNameById = useMemo(() => { const m = { ...defaultDemoNameById }; DEFAULT_PARTICIPANT_IDS.forEach((id) => { if (participantNameById[id]) m[id] = participantNameById[id]; }); return m; }, [defaultDemoNameById, participantNameById]); /** Palette index: match sidebar roster order, fall back to default trio order. */ const previewSpeakerIdxFor = useMemo(() => { const map = {}; DEFAULT_PARTICIPANT_IDS.forEach((id, defaultIdx) => { map[id] = speakerIdxFor[id] ?? defaultIdx; }); return map; }, [speakerIdxFor]); const samplePreviewMessages = useMemo( () => buildSamplePreviewMessages(previewNameById), [previewNameById], ); const hasContent = (messages?.length || 0) + (systemMessages?.length || 0) > 0; const chatEnded = (systemMessages || []).some(s => s.text === 'End of Chat'); const stats = useMemo(() => { if (!chatEnded || !messages || messages.length === 0) return null; const participantMsgs = messages.filter(m => m.role !== 'orchestrator'); const totalTime = participantMsgs.reduce( (sum, m) => sum + (m.elapsed_seconds || 0), 0, ); return { count: participantMsgs.length, totalTime: totalTime.toFixed(1) }; }, [chatEnded, messages]); return (
{!hasContent && !isRunning && (

Watch a panel of AI experts debate a question, challenge each other, and reason toward a more considered answer.

{hasEnoughParticipantsToStart ? 'Three expert personas are ready to go — press Start Chat, or add, remove, or edit them to fit your question.' : 'Add at least 2 participants from the header dropdown, then start a conversation.'}

)} {(messages || []).map((msg, i) => { if (msg.role === 'system') { return (
{msg.text}
); } if (msg.role === 'orchestrator') { return ; } const idx = speakerIdxFor[msg.speaker_id] ?? i; const prev = i > 0 ? messages[i - 1] : null; return ( ); })} {awaitingHuman && (
)} {(systemMessages || []).map((sys, i) => (
{sys.text}
))} {showChatStats && stats && (
{stats.count} participant messages · {stats.totalTime}s total generation time
)} {chatEnded && (

Want a panel like this running on your own infrastructure?{' '} Talk to Neon

)} {isRunning && statusText && !awaitingHuman && (
{statusText}
)}
); }