import { useEffect, useMemo, useRef, useState } from "react"; import { useParams } from "react-router-dom"; import { Alert, Button, Empty, Input, Result, Segmented, Skeleton, Tag } from "antd"; import { AudioOutlined, CalendarOutlined, ClockCircleOutlined, FileTextOutlined, LockOutlined, RobotOutlined, TeamOutlined, UserOutlined, } from "@ant-design/icons"; import dayjs from "dayjs"; import ReactMarkdown from "react-markdown"; import { getMeetingPreviewAccess, getPublicMeetingPreview, type MeetingTranscriptVO, type MeetingVO, } from "../../api/business/meeting"; import { buildMeetingAnalysis } from "./meetingAnalysis"; import "./MeetingPreview.css"; type AnalysisTab = "chapters" | "speakers" | "actions" | "todos"; const STATUS_META: Record = { 1: { label: "閺夌儐鍓欓崯鎾寸▔?, className: "is-processing", hint: "濞村吋淇洪鍛村礃閸涱収鍟囧ù鐘茬Т濠€顏堝极鐎靛憡鍊炲☉鎿冨弿缁辨繃锛愰崟顕呮綌闁轰胶澧楀畵浣瑰濮橆厼鐦紓渚囧弨钘熼柛蹇嬪妸閳? }, 2: { label: "闁诡剝宕电划銊︾▔?, className: "is-processing", hint: "AI 婵繐绲藉﹢顏堟偨閻旂鐏囬柟顒冨吹缁劑鏁嶅畝鍕垫殨閻熸瑥鐗撻妴澶愭椤厾绐楅柡鍕⒔閵囨艾顔忛幓鎺旀殮闁瑰瓨鍔楀▓鎴﹀礃閸涱収鍟囬柕? }, 3: { label: "鐎瑰憡褰冮悾顒勫箣?, className: "is-complete", hint: "濞村吋淇洪鍛棯椤忓浂娲i柕鍡曠閸ㄥ酣寮搁幇顒佸闁告鍠愰弸鍐啅閼碱剚鏅搁柟瀛樺姇閻n剟骞嬮幇鈹惧亾? }, }; function formatDurationRange(startTime?: number, endTime?: number) { const format = (milliseconds?: number) => { const safeMs = Math.max(0, milliseconds || 0); const totalSeconds = Math.floor(safeMs / 1000); const minutes = Math.floor(totalSeconds / 60); const seconds = totalSeconds % 60; return `${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`; }; return `${format(startTime)} - ${format(endTime)}`; } function splitDisplayItems(value?: string) { return (value || "") .split(/[闁?闁靛棔绠?) .map((item) => item.trim()) .filter(Boolean); } function transcriptColorSeed(speakerKey: string) { const palette = ["#315f8b", "#b86432", "#557a46", "#6d4fa7", "#a33f57", "#0f766e"]; const score = Array.from(speakerKey).reduce((sum, char) => sum + char.charCodeAt(0), 0); return palette[score % palette.length]; } export default function MeetingPreview() { const { id } = useParams(); const audioRef = useRef(null); const [meeting, setMeeting] = useState(null); const [transcripts, setTranscripts] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(""); const [analysisTab, setAnalysisTab] = useState("chapters"); const [activeTranscriptId, setActiveTranscriptId] = useState(null); const [passwordRequired, setPasswordRequired] = useState(false); const [passwordVerified, setPasswordVerified] = useState(false); const [accessPassword, setAccessPassword] = useState(""); const [passwordError, setPasswordError] = useState(""); useEffect(() => { let mounted = true; const fetchData = async () => { if (!id) { setError("闁哄牜浜濊ぐ浣圭瑹濞戞绐楅悹渚囧枤缁鳖亪宕?); setLoading(false); return; } setLoading(true); setError(""); setMeeting(null); setTranscripts([]); setPasswordRequired(false); setPasswordVerified(false); setAccessPassword(""); setPasswordError(""); try { const meetingId = Number(id); const accessRes = await getMeetingPreviewAccess(meetingId); if (!mounted) { return; } const requiresPassword = !!accessRes.data.data.passwordRequired; setPasswordRequired(requiresPassword); if (requiresPassword) { setLoading(false); return; } const previewRes = await getPublicMeetingPreview(meetingId); if (!mounted) { return; } setMeeting(previewRes.data.data.meeting); setTranscripts(previewRes.data.data.transcripts || []); setPasswordVerified(true); } catch (requestError: any) { if (!mounted) { return; } setError(requestError?.response?.data?.msg || "濞村吋淇洪鍛紣閸曨噮娼旈柛鏃傚Ь濞村洦寰勬潏顐バ?); } finally { if (mounted) { setLoading(false); } } }; fetchData(); return () => { mounted = false; }; }, [id]); const analysis = useMemo( () => buildMeetingAnalysis(meeting?.analysis, meeting?.summaryContent, meeting?.tags || ""), [meeting?.analysis, meeting?.summaryContent, meeting?.tags], ); const participants = useMemo(() => splitDisplayItems(meeting?.participants), [meeting?.participants]); const tags = useMemo(() => splitDisplayItems(meeting?.tags), [meeting?.tags]); const statusMeta = STATUS_META[meeting?.status || 0] || { label: "鐎垫澘鎳庨ˇ鈺呮偠?, className: "is-warning", hint: "鐟滅増鎸告晶鐘冲濮樻剚鍞村ù鐘茬У濠€顓㈡偨閻旂鐏囬悗鐟版湰閺嗭綁宕橀崨顓у晣闁挎稑鐭侀顒傜矙瀹ュ懏鍊甸柛鎰Х閻︻垶濡?, }; const handleTranscriptSeek = (item: MeetingTranscriptVO) => { if (!audioRef.current) { return; } audioRef.current.currentTime = Math.max(0, (item.startTime || 0) / 1000); audioRef.current.play().catch(() => {}); }; const handleAudioTimeUpdate = () => { if (!audioRef.current || transcripts.length === 0) { return; } const currentMs = audioRef.current.currentTime * 1000; const currentItem = transcripts.find( (item) => currentMs >= (item.startTime || 0) && currentMs <= (item.endTime || 0), ); setActiveTranscriptId(currentItem?.id || null); }; const handlePasswordSubmit = async () => { if (!id) { return; } setLoading(true); setPasswordError(""); try { const previewRes = await getPublicMeetingPreview(Number(id), accessPassword.trim()); setMeeting(previewRes.data.data.meeting); setTranscripts(previewRes.data.data.transcripts || []); setPasswordVerified(true); } catch (requestError: any) { setPasswordError(requestError?.response?.data?.msg || requestError?.msg || "閻犱礁娼″Λ鍓佲偓闈涙閻栨粓鏌ㄥ▎鎺濆殩"); } finally { setLoading(false); } }; if (loading && (!passwordRequired || passwordVerified)) { return (
); } if (passwordRequired && !passwordVerified) { return (
Access Check

Password Required

Enter access_password to view this meeting preview.

setAccessPassword(event.target.value)} onPressEnter={handlePasswordSubmit} />
{passwordError ? : null}
); } if (error) { return (
濞村吋淇洪鍛紣閸曨噮娼?
{statusMeta.label}

{meeting.title || "闁哄牜浜滈幊锟犲触瀹ュ嫮绐楅悹?}

{statusMeta.hint}

濞村吋淇洪鍛村籍閸洘锛?/span> {meeting.meetingTime ? dayjs(meeting.meetingTime).format("YYYY.MM.DD HH:mm") : "闁哄牜浜i鏇犵磾?}
濞戞挾绮€?闁告帗绋戠紓?/span> {meeting.hostName || meeting.creatorName || "闁哄牜浜i鏇犵磾?}
闁告瑥鍊风槐鐗堢閻戞ɑ娈?/span> {participants.length || "闁哄牜浜滈敐鐐哄礃?}
闁哄秴娲ㄩ鐑藉极娴兼潙娅?/span> {tags.length || "闁哄牜浜i鏇犵磾?}
闁糕晝鍎ゅ﹢鐗堢┍閳╁啩绱?

濞村吋淇洪鍛潡閸屾艾鏋?/h2>

闁告帗绋戠紓鎾寸?/span> {meeting.creatorName || "闁哄牜浜i鏇犵磾?}
濞戞挾绮€垫梹绂?/span> {meeting.hostName || "闁哄牜浜i鏇犵磾?}
闁告帗绋戠紓鎾诲籍閸洘锛?/span> {meeting.createdAt ? dayjs(meeting.createdAt).format("YYYY.MM.DD HH:mm") : "闁哄牜浜i鏇犵磾?}
闂傚﹥濞婇。鍫曟偐閼哥鍋?/span> {meeting.audioSaveStatus || "NONE"}
{participants.length > 0 ? (
闁告瑥鍊风槐鐗堢閸濆嫭鍠?/div>
{participants.map((item) => ( {item} ))}
) : null} {tags.length > 0 ? (
濞村吋淇洪鍛村冀閸モ晩鍔?/div>
{tags.map((item) => ( {item} ))}
) : null}
闁哄懘缂氶崗姗€鏌呴悢娲绘綌

濞村吋淇洪鍛村礆閸℃鈧?/h2>

濞戞挸绨肩槐鎵媼椤旀鍤婇柟顖氭噸缁绘岸骞愭担鍛婂€遍柛娆欑到缁?/div>
{meeting.status < 3 ? ( ) : null} {analysis.keywords.length > 0 ? (
{analysis.keywords.map((item) => ( {item} ))}
) : null}
闁稿繈鍔嶉弸鍐潡閸屾繍娲?/div>

{analysis.overview || "闁哄棗鍊瑰Λ銈咁潡閸屾繍娲i柛鎰噹椤?}

block value={analysisTab} onChange={(value) => setAnalysisTab(value)} options={[ { label: "缂佹梻濮炬俊?, value: "chapters" }, { label: "闁告瑦鍨奸埢?, value: "speakers" }, { label: "閻熸洑鑳堕崑?, value: "actions" }, { label: "鐎垫澘鎳庢慨?, value: "todos" }, ]} />
{analysisTab === "chapters" && (analysis.chapters.length ? ( analysis.chapters.map((item, index) => (
{item.time || "--:--"}
{item.title || `缂佹梻濮炬俊?${index + 1}`} {item.summary || "闁哄棗鍊瑰Λ銈囩博閻樺搫螡闁硅绻楅崼?}
)) ) : (
(
{(item.speaker || "闁?).slice(0, 1)}
{item.speaker || `闁告瑦鍨奸埢鍫熺?${index + 1}`}
闁告瑦鍨奸埢鍫濐潡閸屾繂鐗?/div>
{item.summary || "闁哄棗鍊瑰Λ銈夊矗閹达絺鏋呴柟顒冨吹缁?}
)) ) : (
(
{String(index + 1).padStart(2, "0")}
{item.title || `閻熸洑鑳堕崑?${index + 1}`} {item.summary || "闁哄棗鍊瑰Λ銈囨啺娴e搫浠悹鍥х摠濡?} {(item.speaker || item.time) && (
{item.speaker ? {item.speaker} : null} {item.time ? {item.time} : null}
)}
)) ) : (
(
{item}
)) ) : (
AI 闁诡剝宕电划?

閻庣懓鏈弳锝囩棯椤忓浂娲?/h2>

{meeting.summaryContent ? (
{meeting.summaryContent}
) : (
闁告鍠愰弸鍐媼閺夎法绉?

濞村吋淇洪鍛姜椤掆偓缂?/h2>

闁绘劗鎳撻崵顔尖枔娴e啯鍎伴柛娆樺灥閻戯附娼鍕従濡?
{meeting.audioSaveStatus === "FAILED" ? ( ) : null} {meeting.audioUrl ? (
闁哄懘缂氶崗姗€宕橀崨顓у晣闁?AI 婵☆垪鈧磭鈧兘鎮介悢绋跨亣闁挎稑濂旂划搴ㄦ偨閵娿倗鑹惧ù鍏间亢椤斿懏绌遍埄鍐х礀濡澘瀚~宥夋晬瀹€鍐惧殲缂備焦鎸搁幃搴ㄥ储閻斿娼楀ù鍏间亢椤斿懐鎷犻鐑嗘殧閻庣櫢绻濆Σ鍕椽瀹€鈧垾妯兼媼閵堝啠鍋?
); }