import { useEffect, useMemo, useRef, useState } from "react"; import { Alert, Button, Empty, Input, Result, Segmented, Skeleton, Tabs, Tag, message } from "antd"; import { useParams, useSearchParams } from "react-router-dom"; import { AudioOutlined, CalendarOutlined, CaretRightFilled, ClockCircleOutlined, CopyOutlined, FastForwardOutlined, FileTextOutlined, LockOutlined, PauseOutlined, RobotOutlined, ShareAltOutlined, 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"; type PreviewPageTab = "summary" | "transcript"; const TEXT = { statusTranscribing: "\u8f6c\u5199\u4e2d", statusSummarizing: "\u603b\u7ed3\u4e2d", statusCompleted: "\u5df2\u5b8c\u6210", statusPending: "\u5f85\u5904\u7406", hintTranscribing: "\u4f1a\u8bae\u5185\u5bb9\u4ecd\u5728\u6574\u7406\u4e2d\uff0c\u9884\u89c8\u4f1a\u6301\u7eed\u8865\u5168\u3002", hintSummarizing: "AI \u6b63\u5728\u751f\u6210\u4f1a\u8bae\u603b\u7ed3\uff0c\u5df2\u5b8c\u6210\u5185\u5bb9\u4f1a\u4f18\u5148\u5c55\u793a\u3002", hintCompleted: "\u4f1a\u8bae\u7eaa\u8981\u3001\u5206\u6790\u548c\u8f6c\u5f55\u5185\u5bb9\u5df2\u751f\u6210\u5b8c\u6210\u3002", hintPending: "\u5f53\u524d\u4f1a\u8bae\u5c1a\u672a\u751f\u6210\u5b8c\u6574\u5185\u5bb9\uff0c\u8bf7\u7a0d\u540e\u91cd\u8bd5\u3002", missingMeetingId: "\u672a\u63d0\u4f9b\u4f1a\u8bae\u7f16\u53f7", loadFailed: "\u4f1a\u8bae\u9884\u89c8\u52a0\u8f7d\u5931\u8d25", noMeetingData: "\u672a\u627e\u5230\u4f1a\u8bae\u6570\u636e", previewLabel: "\u4f1a\u8bae\u9884\u89c8", untitledMeeting: "\u672a\u547d\u540d\u4f1a\u8bae", meetingTime: "\u4f1a\u8bae\u65f6\u95f4", hostCreator: "\u4e3b\u6301/\u521b\u5efa", participantsCount: "\u53c2\u4f1a\u4eba\u6570", tagsCount: "\u6807\u7b7e\u6570\u91cf", notSet: "\u672a\u8bbe\u7f6e", notFilled: "\u672a\u586b\u5199", pageSummary: "\u603b\u7ed3\u4e0e\u5206\u6790", pageTranscript: "\u8f6c\u5f55\u4e0e\u97f3\u9891", copyLink: "\u590d\u5236\u94fe\u63a5", shareNow: "\u7acb\u5373\u5206\u4eab", shareCopied: "\u9884\u89c8\u94fe\u63a5\u5df2\u590d\u5236", shareFallbackCopied: "\u5f53\u524d\u8bbe\u5907\u4e0d\u652f\u6301\u7cfb\u7edf\u5206\u4eab\uff0c\u5df2\u4e3a\u4f60\u590d\u5236\u94fe\u63a5", shareFailed: "\u5206\u4eab\u5931\u8d25\uff0c\u8bf7\u5148\u590d\u5236\u94fe\u63a5", accessCheck: "\u8bbf\u95ee\u6821\u9a8c", passwordRequired: "\u8be5\u4f1a\u8bae\u9700\u8981\u8bbf\u95ee\u5bc6\u7801", passwordHint: "\u8bf7\u8f93\u5165\u4f1a\u8bae\u7684 access_password \u540e\u7ee7\u7eed\u8bbf\u95ee\u9884\u89c8\u5185\u5bb9\u3002", passwordPlaceholder: "\u8bf7\u8f93\u5165 access_password", openPreview: "\u8fdb\u5165\u9884\u89c8", invalidPassword: "\u8bbf\u95ee\u5bc6\u7801\u9519\u8bef", basicInfo: "\u57fa\u672c\u4fe1\u606f", meetingOverview: "\u4f1a\u8bae\u6982\u51b5", creator: "\u521b\u5efa\u4eba", host: "\u4e3b\u6301\u4eba", createdAt: "\u521b\u5efa\u65f6\u95f4", audioStatus: "\u97f3\u9891\u72b6\u6001", participants: "\u53c2\u4f1a\u4eba\u5458", tags: "\u4f1a\u8bae\u6807\u7b7e", aiAnalysis: "AI \u5206\u6790", analysis: "\u4f1a\u8bae\u5206\u6790", previewExtra: "\u9884\u89c8\u9875\u4ec5\u8bfb\u5c55\u793a", audioPlaybackWarning: "\u97f3\u9891\u4fdd\u5b58\u5931\u8d25\uff0c\u53ef\u80fd\u5f71\u54cd\u56de\u653e\u3002", summaryOverview: "\u5168\u6587\u6982\u8981", summaryEmpty: "\u6682\u65e0\u6982\u8981\u5185\u5bb9", analysisChapters: "\u7ae0\u8282", analysisSpeakers: "\u53d1\u8a00\u4eba", analysisKeyPoints: "\u5173\u952e\u8981\u70b9", analysisTodos: "\u5f85\u529e\u4e8b\u9879", noChapterAnalysis: "\u6682\u65e0\u7ae0\u8282\u5206\u6790", noSpeakerAnalysis: "\u6682\u65e0\u53d1\u8a00\u4eba\u5206\u6790", noKeyPoints: "\u6682\u65e0\u5173\u952e\u8981\u70b9", noTodos: "\u6682\u65e0\u5f85\u529e\u4e8b\u9879", chapterFallback: "\u7ae0\u8282", speakerFallback: "\u53d1\u8a00\u4eba", speakerSummary: "\u53d1\u8a00\u6982\u8ff0", keyPointFallback: "\u8981\u70b9", noChapterSummary: "\u6682\u65e0\u7ae0\u8282\u63cf\u8ff0", noSpeakerSummary: "\u6682\u65e0\u53d1\u8a00\u603b\u7ed3", noKeyPointSummary: "\u6682\u65e0\u8981\u70b9\u8bf4\u660e", summarySection: "\u4f1a\u8bae\u7eaa\u8981", fullSummary: "\u5b8c\u6574\u7eaa\u8981", noSummary: "\u6682\u65e0\u4f1a\u8bae\u7eaa\u8981", transcriptSection: "\u4f1a\u8bae\u8f6c\u5f55", transcriptTitle: "\u9010\u6bb5\u8f6c\u5f55", noDuration: "\u6682\u65e0\u65f6\u957f", audioUnavailable: "\u97f3\u9891\u6587\u4ef6\u4e0d\u53ef\u7528\uff0c\u4ec5\u5c55\u793a\u8f6c\u5f55\u5185\u5bb9\u3002", noTranscript: "\u6682\u65e0\u8f6c\u5f55\u5185\u5bb9", unknownSpeaker: "\u672a\u77e5\u53d1\u8a00\u4eba", disclaimer: "智能内容由用户会议内容 + AI 模型生成,我们不对内容准确性和完整性做任何保证,亦不代表我们的观点或态度", shareText: "\u6211\u5411\u4f60\u5206\u4eab\u4e86\u4e00\u4e2a\u4f1a\u8bae\u9884\u89c8\u94fe\u63a5", audioSaved: "\u5df2\u4fdd\u5b58", audioSaveFailed: "\u4fdd\u5b58\u5931\u8d25", audioUploaded: "\u5df2\u4e0a\u4f20", audioNotSaved: "\u672a\u4fdd\u5b58", }; const STATUS_META: Record = { 1: { label: TEXT.statusTranscribing, className: "is-processing", hint: TEXT.hintTranscribing }, 2: { label: TEXT.statusSummarizing, className: "is-processing", hint: TEXT.hintSummarizing }, 3: { label: TEXT.statusCompleted, className: "is-complete", hint: TEXT.hintCompleted }, }; 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]; } async function copyText(text: string) { if (navigator.clipboard?.writeText) { await navigator.clipboard.writeText(text); return; } const textarea = document.createElement("textarea"); textarea.value = text; textarea.setAttribute("readonly", "true"); textarea.style.position = "fixed"; textarea.style.opacity = "0"; document.body.appendChild(textarea); textarea.select(); document.execCommand("copy"); document.body.removeChild(textarea); } export default function MeetingPreview() { const { id } = useParams(); const [searchParams] = useSearchParams(); const audioRef = useRef(null); const transcriptItemRefs = useRef>({}); const [meeting, setMeeting] = useState(null); const [transcripts, setTranscripts] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(""); const [analysisTab, setAnalysisTab] = useState("chapters"); const [pageTab, setPageTab] = useState("summary"); const [activeTranscriptId, setActiveTranscriptId] = useState(null); const [passwordRequired, setPasswordRequired] = useState(false); const [passwordVerified, setPasswordVerified] = useState(false); const [accessPassword, setAccessPassword] = useState(""); const [passwordError, setPasswordError] = useState(""); const [audioPlaying, setAudioPlaying] = useState(false); const [audioCurrentTime, setAudioCurrentTime] = useState(0); const [audioDuration, setAudioDuration] = useState(0); const [audioPlaybackRate, setAudioPlaybackRate] = useState(1); const [isMobile, setIsMobile] = useState(() => typeof window !== "undefined" ? window.matchMedia("(max-width: 767px)").matches : false, ); const presetAccessPassword = useMemo(() => (searchParams.get("accessPassword") || "").trim(), [searchParams]); useEffect(() => { let mounted = true; const load = async () => { if (!id) { setError(TEXT.missingMeetingId); setLoading(false); return; } setLoading(true); setError(""); setMeeting(null); setTranscripts([]); setPasswordRequired(false); setPasswordVerified(false); setAccessPassword(presetAccessPassword); setPasswordError(""); try { const meetingId = Number(id); const accessRes = await getMeetingPreviewAccess(meetingId); if (!mounted) { return; } const requiresPassword = !!accessRes.data.data.passwordRequired; setPasswordRequired(requiresPassword); if (requiresPassword) { if (!presetAccessPassword) { setLoading(false); return; } try { const previewRes = await getPublicMeetingPreview(meetingId, presetAccessPassword); if (!mounted) { return; } setMeeting(previewRes.data.data.meeting); setTranscripts(previewRes.data.data.transcripts || []); setPasswordVerified(true); return; } catch (requestError: any) { if (!mounted) { return; } setPasswordError(requestError?.response?.data?.msg || requestError?.msg || TEXT.invalidPassword); setPasswordVerified(false); 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 || requestError?.msg || TEXT.loadFailed); } finally { if (mounted) { setLoading(false); } } }; load(); return () => { mounted = false; }; }, [id, presetAccessPassword]); useEffect(() => { if (typeof window === "undefined") { return; } const mediaQuery = window.matchMedia("(max-width: 767px)"); const handleChange = (event: MediaQueryListEvent) => { setIsMobile(event.matches); }; setIsMobile(mediaQuery.matches); mediaQuery.addEventListener("change", handleChange); return () => { mediaQuery.removeEventListener("change", handleChange); }; }, []); 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 transcriptSpeakers = useMemo(() => { const speakers = transcripts .map((item) => item.speakerName || item.speakerLabel || item.speakerId || "") .map((item) => item.trim()) .filter(Boolean); return Array.from(new Set(speakers)); }, [transcripts]); const tags = useMemo(() => splitDisplayItems(meeting?.tags), [meeting?.tags]); const keywords = useMemo(() => analysis.keywords || [], [analysis.keywords]); const statusMeta = STATUS_META[meeting?.status || 0] || { label: TEXT.statusPending, className: "is-warning", hint: TEXT.hintPending, }; const audioStatusLabel = useMemo(() => { if (meeting?.audioSaveStatus === "SUCCESS") { return TEXT.audioSaved; } if (meeting?.audioSaveStatus === "FAILED") { return TEXT.audioSaveFailed; } if (meeting?.audioUrl) { return TEXT.audioUploaded; } return TEXT.audioNotSaved; }, [meeting?.audioSaveStatus, meeting?.audioUrl]); const shareUrl = typeof window !== "undefined" ? window.location.href : ""; const participantCountValue = isMobile && transcriptSpeakers.length > 0 ? transcriptSpeakers.length : participants.length; const meetingDuration = useMemo(() => { if (transcripts.length > 0) { const last = transcripts[transcripts.length - 1]; return last.endTime || 0; } return 0; }, [transcripts]); useEffect(() => { if (!activeTranscriptId) { return; } const target = transcriptItemRefs.current[activeTranscriptId]; if (!target) { return; } target.scrollIntoView({ behavior: "smooth", block: "nearest" }); }, [activeTranscriptId]); const handleTranscriptSeek = (item: MeetingTranscriptVO) => { if (!audioRef.current) { return; } audioRef.current.currentTime = Math.max(0, (item.startTime || 0) / 1000); audioRef.current.play().catch(() => {}); }; const toggleAudioPlayback = () => { if (!audioRef.current) return; if (audioPlaying) { audioRef.current.pause(); } else { audioRef.current.play().catch(() => {}); } }; const handleAudioProgressChange = (e: React.ChangeEvent) => { if (!audioRef.current) return; const time = parseFloat(e.target.value); audioRef.current.currentTime = time; setAudioCurrentTime(time); }; const cyclePlaybackRate = () => { if (!audioRef.current) return; const nextRate = audioPlaybackRate === 1 ? 1.5 : audioPlaybackRate === 1.5 ? 2 : 1; audioRef.current.playbackRate = nextRate; setAudioPlaybackRate(nextRate); }; const formatPlayerTime = (seconds: number) => { if (!seconds || isNaN(seconds)) return '00:00'; const m = Math.floor(seconds / 60); const s = Math.floor(seconds % 60); return `${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`; }; const handleAudioTimeUpdate = () => { if (!audioRef.current) return; const currentSeconds = audioRef.current.currentTime; setAudioCurrentTime(currentSeconds); // Also update duration if it's available now if (audioRef.current.duration && audioDuration !== audioRef.current.duration) { setAudioDuration(audioRef.current.duration); } if (transcripts.length === 0) return; const currentMs = currentSeconds * 1000; const currentItem = transcripts.find( (item) => currentMs >= (item.startTime || 0) && currentMs <= (item.endTime || 0), ); setActiveTranscriptId(currentItem?.id || null); }; const handleAudioEnded = () => { setAudioPlaying(false); }; const handleAudioPlay = () => setAudioPlaying(true); const handleAudioPause = () => setAudioPlaying(false); const handleAudioLoadedMetadata = () => { if (audioRef.current) { setAudioDuration(audioRef.current.duration); } }; const renderMeetingTitle = (title?: string) => { const safeTitle = title || TEXT.untitledMeeting; return safeTitle.split(/(\d+)/).map((part, index) => /\d+/.test(part) ? ( {part} ) : ( {part} ), ); }; 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 || TEXT.invalidPassword); } finally { setLoading(false); } }; const handleCopyLink = async () => { try { await copyText(shareUrl); message.success(TEXT.shareCopied); } catch { message.error(TEXT.shareFailed); } }; const handleShareNow = async () => { try { if (navigator.share) { await navigator.share({ title: meeting?.title || TEXT.previewLabel, text: TEXT.shareText, url: shareUrl, }); return; } await copyText(shareUrl); message.success(TEXT.shareFallbackCopied); } catch { message.error(TEXT.shareFailed); } }; if (loading && (!passwordRequired || passwordVerified)) { return (
); } if (passwordRequired && !passwordVerified) { return (
{TEXT.accessCheck}

{TEXT.passwordRequired}

{TEXT.passwordHint}

setAccessPassword(event.target.value)} onPressEnter={handlePasswordSubmit} />
{passwordError ? : null}
); } if (error) { return (
); } if (!meeting) { return (
); } const summaryTab = (
{TEXT.basicInfo}

{TEXT.meetingOverview}

{TEXT.creator} {meeting.creatorName || TEXT.notSet}
{TEXT.host} {meeting.hostName || TEXT.notSet}
{TEXT.createdAt} {meeting.createdAt ? dayjs(meeting.createdAt).format("YYYY.MM.DD HH:mm") : TEXT.notSet}
{TEXT.audioStatus} {audioStatusLabel}
{participants.length > 0 ? (
{TEXT.participants}
{participants.map((item) => ( {item} ))}
) : null} {tags.length > 0 ? (
{TEXT.tags}
{tags.map((item) => ( {item} ))}
) : null}
{TEXT.aiAnalysis}

{TEXT.analysis}

{TEXT.previewExtra}
{meeting.audioSaveStatus === "FAILED" ? ( ) : null} {keywords.length > 0 ? (
{keywords.map((item) => ( {item} ))}
) : null}
{TEXT.summaryOverview}

{analysis.overview || TEXT.summaryEmpty}

{[ { label: TEXT.analysisChapters, value: "chapters" }, { label: TEXT.analysisSpeakers, value: "speakers" }, { label: TEXT.analysisKeyPoints, value: "actions" }, { label: TEXT.analysisTodos, value: "todos" }, ].map((tab) => (
setAnalysisTab(tab.value as AnalysisTab)} > {tab.label}
))}
{analysisTab === "chapters" ? ( analysis.chapters.length > 0 ? ( analysis.chapters.map((item, index) => (
{item.time || "--:--"}
{item.title || `${TEXT.chapterFallback} ${index + 1}`} {item.summary || TEXT.noChapterSummary}
)) ) : (
{TEXT.noChapterAnalysis}
) ) : null} {analysisTab === "speakers" ? ( analysis.speakerSummaries.length > 0 ? ( analysis.speakerSummaries.map((item, index) => (
{(item.speaker || "S").slice(0, 1)}
{item.speaker || `${TEXT.speakerFallback} ${index + 1}`}
{TEXT.speakerSummary}
{item.summary || TEXT.noSpeakerSummary}
)) ) : (
{TEXT.noSpeakerAnalysis}
) ) : null} {analysisTab === "actions" ? ( analysis.keyPoints.length > 0 ? ( analysis.keyPoints.map((item, index) => (
{String(index + 1).padStart(2, "0")}
{item.title || `${TEXT.keyPointFallback} ${index + 1}`} {item.summary || TEXT.noKeyPointSummary} {(item.speaker || item.time) ? (
{item.speaker ? {item.speaker} : null} {item.time ? {item.time} : null}
) : null}
)) ) : (
{TEXT.noKeyPoints}
) ) : null} {analysisTab === "todos" ? ( analysis.todos.length > 0 ? ( analysis.todos.map((item, index) => (
{item}
)) ) : (
{TEXT.noTodos}
) ) : null}
{TEXT.summarySection}

{TEXT.fullSummary}

{meeting.summaryContent ? ( {meeting.summaryContent} ) : ( )}
); const transcriptTab = (
{TEXT.transcriptSection}

{TEXT.transcriptTitle}

{meetingDuration > 0 ? formatDurationRange(0, meetingDuration) : TEXT.noDuration}
{meeting.audioSaveStatus === "FAILED" ? ( ) : null}
{transcripts.length > 0 ? ( transcripts.map((item) => { const speakerKey = item.speakerName || item.speakerLabel || item.speakerId || "speaker"; return (
{ transcriptItemRefs.current[item.id] = node; }} className={`meeting-preview-transcript-item ${activeTranscriptId === item.id ? "is-active" : ""}`} onClick={() => handleTranscriptSeek(item)} >
{(speakerKey || "S").slice(0, 1)}
{speakerKey} {formatDurationRange(item.startTime, item.endTime)}
{item.content || TEXT.noTranscript}
); }) ) : ( )}
); return (
{TEXT.previewLabel}
{statusMeta.label}

{renderMeetingTitle(meeting.title)}

{statusMeta.hint}

{TEXT.meetingTime} {meeting.meetingTime ? dayjs(meeting.meetingTime).format("YYYY.MM.DD HH:mm") : TEXT.notSet}
{TEXT.hostCreator} {meeting.hostName || meeting.creatorName || TEXT.notSet}
{TEXT.participantsCount} {participantCountValue || TEXT.notFilled}
{TEXT.tagsCount} {tags.length || TEXT.notSet}
setPageTab(key as PreviewPageTab)} items={[ { key: "summary", label: TEXT.pageSummary, children: summaryTab }, { key: "transcript", label: TEXT.pageTranscript, children: transcriptTab }, ]} />
{TEXT.disclaimer}
{meeting.audioUrl && (