// ConversationSourcePanel — landing-and-mode-orchestration-redesign decision 5, // updated by `unified-segment-citation-card` (leaf is now SegmentCitationCard, // per-group display cap + show-more, separate play / jump actions). // // Renders ONE episode-grouped panel below a chat answer: // header →「答案參考來源(共 N 集 · M 段引用)」(zh) / // "Answer sources (N episodes · M citations)" (en) // groups → per-episode collapsible block; within each, at most // CITATION_DISPLAY_CAP cards render initially with a「顯示更多」 // affordance to reveal the rest incrementally. // leaf → SegmentCitationCard (play-in-place + jump-to-transcript). // // The panel shows ONLY the chunks actually cited by the answer (`citations`); // the displayed count is decoupled from retrieval top_k (D6). // // Props: // citations : cited chunks (ChunkHit shape; audio_url enriched here // via audioUrlFor so the play button can render) // lang // queryId : forwarded into the citation_click beacon on jump // audioUrlFor : (episode_id) → audio_url | null (from QueryPage episodes) // onPlaySegment : (segment) → play in place, no navigation // onJumpToTranscript : (segment, position?, queryId?) → beacon + navigate const ConversationSourcePanel = ({ citations, lang, queryId, audioUrlFor, onPlaySegment, onJumpToTranscript }) => { const t = lang === 'zh'; const { isMobile } = useViewport(); const list = Array.isArray(citations) ? citations : []; const [collapsed, setCollapsed] = React.useState({}); // episode_id → bool const [shownMap, setShownMap] = React.useState({}); // episode_id → number shown const CAP = (typeof window !== 'undefined' && window.CITATION_DISPLAY_CAP) || 5; if (list.length === 0) return null; // Group preserving first-appearance order so RRF ranking shines through. const order = []; const groups = {}; let globalIdx = 0; for (const c of list) { const epId = c.episode_id != null ? String(c.episode_id) : `__noep_${globalIdx}`; if (!groups[epId]) { groups[epId] = { episode_id: c.episode_id, episode_title: c.episode_title, items: [] }; order.push(epId); } groups[epId].items.push({ ...c, _globalIdx: globalIdx }); globalIdx += 1; } const N = order.length; const M = list.length; const header = t ? `答案參考來源(共 ${N} 集 · ${M} 段引用)` : `Answer sources (${N} episode${N === 1 ? '' : 's'} · ${M} citation${M === 1 ? '' : 's'})`; return (