596 lines
26 KiB
TypeScript
596 lines
26 KiB
TypeScript
|
|
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<number, { label: string; className: string; hint: string }> = {
|
|||
|
|
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<HTMLAudioElement | null>(null);
|
|||
|
|
const [meeting, setMeeting] = useState<MeetingVO | null>(null);
|
|||
|
|
const [transcripts, setTranscripts] = useState<MeetingTranscriptVO[]>([]);
|
|||
|
|
const [loading, setLoading] = useState(true);
|
|||
|
|
const [error, setError] = useState("");
|
|||
|
|
const [analysisTab, setAnalysisTab] = useState<AnalysisTab>("chapters");
|
|||
|
|
const [activeTranscriptId, setActiveTranscriptId] = useState<number | null>(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 (
|
|||
|
|
<div className="meeting-preview-page">
|
|||
|
|
<div className="meeting-preview-shell meeting-preview-loading">
|
|||
|
|
<div className="meeting-preview-card meeting-preview-hero">
|
|||
|
|
<Skeleton active paragraph={{ rows: 4 }} />
|
|||
|
|
</div>
|
|||
|
|
<div className="meeting-preview-card meeting-preview-section">
|
|||
|
|
<Skeleton active paragraph={{ rows: 8 }} />
|
|||
|
|
</div>
|
|||
|
|
<div className="meeting-preview-card meeting-preview-section">
|
|||
|
|
<Skeleton active paragraph={{ rows: 10 }} />
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (passwordRequired && !passwordVerified) {
|
|||
|
|
return (
|
|||
|
|
<div className="meeting-preview-page">
|
|||
|
|
<div className="meeting-preview-shell meeting-preview-empty">
|
|||
|
|
<div className="meeting-preview-card meeting-preview-section meeting-preview-password-gate">
|
|||
|
|
<div className="meeting-preview-section-header">
|
|||
|
|
<div>
|
|||
|
|
<div className="meeting-preview-section-kicker">
|
|||
|
|
<LockOutlined />
|
|||
|
|
Access Check
|
|||
|
|
</div>
|
|||
|
|
<h2 className="meeting-preview-section-title">Password Required</h2>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<p className="meeting-preview-subtitle">
|
|||
|
|
Enter access_password to view this meeting preview.
|
|||
|
|
</p>
|
|||
|
|
|
|||
|
|
<div className="meeting-preview-password-form">
|
|||
|
|
<Input.Password
|
|||
|
|
value={accessPassword}
|
|||
|
|
placeholder="Enter access_password"
|
|||
|
|
onChange={(event) => setAccessPassword(event.target.value)}
|
|||
|
|
onPressEnter={handlePasswordSubmit}
|
|||
|
|
/>
|
|||
|
|
<Button type="primary" onClick={handlePasswordSubmit} loading={loading} disabled={!accessPassword.trim()}>
|
|||
|
|
Open Preview
|
|||
|
|
</Button>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{passwordError ? <Alert type="error" showIcon message={passwordError} /> : null}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (error) {
|
|||
|
|
return (
|
|||
|
|
<div className="meeting-preview-page">
|
|||
|
|
<div className="meeting-preview-shell meeting-preview-empty">
|
|||
|
|
<div className="meeting-preview-card meeting-preview-section">
|
|||
|
|
<Result status="error" title="濞村吋淇洪鍛紣閸曨噮娼旈柛鏃傚Ь濞村洦寰勬潏顐バ? subTitle={error} />
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (!meeting) {
|
|||
|
|
return (
|
|||
|
|
<div className="meeting-preview-page">
|
|||
|
|
<div className="meeting-preview-shell meeting-preview-empty">
|
|||
|
|
<div className="meeting-preview-card meeting-preview-section">
|
|||
|
|
<Empty description="闁哄牜浜濇竟姗€宕氭0浣虹獥閻犱緡鍠楅弳鐔煎箲? />
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<div className="meeting-preview-page">
|
|||
|
|
<div className="meeting-preview-shell">
|
|||
|
|
<section className="meeting-preview-card meeting-preview-hero">
|
|||
|
|
<div className="meeting-preview-eyebrow">
|
|||
|
|
<div className="meeting-preview-eyebrow-label">
|
|||
|
|
<FileTextOutlined />
|
|||
|
|
濞村吋淇洪鍛紣閸曨噮娼?
|
|||
|
|
</div>
|
|||
|
|
<span className={`meeting-preview-status ${statusMeta.className}`}>{statusMeta.label}</span>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<h1 className="meeting-preview-title">{meeting.title || "闁哄牜浜滈幊锟犲触瀹ュ嫮绐楅悹?}</h1>
|
|||
|
|
<p className="meeting-preview-subtitle">{statusMeta.hint}</p>
|
|||
|
|
|
|||
|
|
<div className="meeting-preview-metrics">
|
|||
|
|
<div className="meeting-preview-metric">
|
|||
|
|
<span className="meeting-preview-metric-label">濞村吋淇洪鍛村籍閸洘锛?/span>
|
|||
|
|
<span className="meeting-preview-metric-value">
|
|||
|
|
{meeting.meetingTime ? dayjs(meeting.meetingTime).format("YYYY.MM.DD HH:mm") : "闁哄牜浜i鏇犵磾?}
|
|||
|
|
</span>
|
|||
|
|
</div>
|
|||
|
|
<div className="meeting-preview-metric">
|
|||
|
|
<span className="meeting-preview-metric-label">濞戞挾绮€?闁告帗绋戠紓?/span>
|
|||
|
|
<span className="meeting-preview-metric-value">{meeting.hostName || meeting.creatorName || "闁哄牜浜i鏇犵磾?}</span>
|
|||
|
|
</div>
|
|||
|
|
<div className="meeting-preview-metric">
|
|||
|
|
<span className="meeting-preview-metric-label">闁告瑥鍊风槐鐗堢閻戞ɑ娈?/span>
|
|||
|
|
<span className="meeting-preview-metric-value">{participants.length || "闁哄牜浜滈敐鐐哄礃?}</span>
|
|||
|
|
</div>
|
|||
|
|
<div className="meeting-preview-metric">
|
|||
|
|
<span className="meeting-preview-metric-label">闁哄秴娲ㄩ鐑藉极娴兼潙娅?/span>
|
|||
|
|
<span className="meeting-preview-metric-value">{tags.length || "闁哄牜浜i鏇犵磾?}</span>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</section>
|
|||
|
|
|
|||
|
|
<div className="meeting-preview-panels">
|
|||
|
|
<section className="meeting-preview-card meeting-preview-section">
|
|||
|
|
<div className="meeting-preview-section-header">
|
|||
|
|
<div>
|
|||
|
|
<div className="meeting-preview-section-kicker">
|
|||
|
|
<CalendarOutlined />
|
|||
|
|
闁糕晝鍎ゅ﹢鐗堢┍閳╁啩绱?
|
|||
|
|
</div>
|
|||
|
|
<h2 className="meeting-preview-section-title">濞村吋淇洪鍛潡閸屾艾鏋?/h2>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div className="meeting-preview-metrics">
|
|||
|
|
<div className="meeting-preview-metric">
|
|||
|
|
<span className="meeting-preview-metric-label">闁告帗绋戠紓鎾寸?/span>
|
|||
|
|
<span className="meeting-preview-metric-value">{meeting.creatorName || "闁哄牜浜i鏇犵磾?}</span>
|
|||
|
|
</div>
|
|||
|
|
<div className="meeting-preview-metric">
|
|||
|
|
<span className="meeting-preview-metric-label">濞戞挾绮€垫梹绂?/span>
|
|||
|
|
<span className="meeting-preview-metric-value">{meeting.hostName || "闁哄牜浜i鏇犵磾?}</span>
|
|||
|
|
</div>
|
|||
|
|
<div className="meeting-preview-metric">
|
|||
|
|
<span className="meeting-preview-metric-label">闁告帗绋戠紓鎾诲籍閸洘锛?/span>
|
|||
|
|
<span className="meeting-preview-metric-value">
|
|||
|
|
{meeting.createdAt ? dayjs(meeting.createdAt).format("YYYY.MM.DD HH:mm") : "闁哄牜浜i鏇犵磾?}
|
|||
|
|
</span>
|
|||
|
|
</div>
|
|||
|
|
<div className="meeting-preview-metric">
|
|||
|
|
<span className="meeting-preview-metric-label">闂傚﹥濞婇。鍫曟偐閼哥鍋?/span>
|
|||
|
|
<span className="meeting-preview-metric-value">{meeting.audioSaveStatus || "NONE"}</span>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{participants.length > 0 ? (
|
|||
|
|
<div className="meeting-preview-overview">
|
|||
|
|
<div className="meeting-preview-overview-label">闁告瑥鍊风槐鐗堢閸濆嫭鍠?/div>
|
|||
|
|
<div className="meeting-preview-tags">
|
|||
|
|
{participants.map((item) => (
|
|||
|
|
<span key={item} className="meeting-preview-tag">
|
|||
|
|
<TeamOutlined style={{ marginRight: 8 }} />
|
|||
|
|
{item}
|
|||
|
|
</span>
|
|||
|
|
))}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
) : null}
|
|||
|
|
|
|||
|
|
{tags.length > 0 ? (
|
|||
|
|
<div className="meeting-preview-overview">
|
|||
|
|
<div className="meeting-preview-overview-label">濞村吋淇洪鍛村冀閸モ晩鍔?/div>
|
|||
|
|
<div className="meeting-preview-tags">
|
|||
|
|
{tags.map((item) => (
|
|||
|
|
<Tag key={item} bordered={false} className="meeting-preview-tag">
|
|||
|
|
{item}
|
|||
|
|
</Tag>
|
|||
|
|
))}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
) : null}
|
|||
|
|
</section>
|
|||
|
|
|
|||
|
|
<section className="meeting-preview-card meeting-preview-section">
|
|||
|
|
<div className="meeting-preview-section-header">
|
|||
|
|
<div>
|
|||
|
|
<div className="meeting-preview-section-kicker">
|
|||
|
|
<RobotOutlined />
|
|||
|
|
闁哄懘缂氶崗姗€鏌呴悢娲绘綌
|
|||
|
|
</div>
|
|||
|
|
<h2 className="meeting-preview-section-title">濞村吋淇洪鍛村礆閸℃鈧?/h2>
|
|||
|
|
</div>
|
|||
|
|
<div className="meeting-preview-section-extra">濞戞挸绨肩槐鎵媼椤旀鍤婇柟顖氭噸缁绘岸骞愭担鍛婂€遍柛娆欑到缁?/div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{meeting.status < 3 ? (
|
|||
|
|
<Alert
|
|||
|
|
className="meeting-preview-alert"
|
|||
|
|
type="info"
|
|||
|
|
showIcon
|
|||
|
|
message="濞村吋淇洪鍛瀹ュ懏韬璺哄閹﹥绋?
|
|||
|
|
description="鐟滅増鎸告晶鐘炽亜閻㈠憡妗ㄥù鍏肩煯缁鳖參宕楅崼婵堟綌缂佲偓閸濆嫬鍤掗柣銏㈠枑閸ㄦ岸鎯冮崟顐㈡暥閻庡湱娅㈢槐婵堚偓鐟版湰閺嗭綁骞€閼姐倗娉㈤柛婊冭嫰鐢偊寮崶褏娈洪柛锔哄妺閹广垽宕濋垾宕囨殮闁瑰瓨鍔曢幃妤冩偘閵夆晝绉烽柕?
|
|||
|
|
/>
|
|||
|
|
) : null}
|
|||
|
|
|
|||
|
|
{analysis.keywords.length > 0 ? (
|
|||
|
|
<div className="meeting-preview-tags">
|
|||
|
|
{analysis.keywords.map((item) => (
|
|||
|
|
<span key={item} className="meeting-preview-tag">
|
|||
|
|
{item}
|
|||
|
|
</span>
|
|||
|
|
))}
|
|||
|
|
</div>
|
|||
|
|
) : null}
|
|||
|
|
|
|||
|
|
<div className="meeting-preview-overview">
|
|||
|
|
<div className="meeting-preview-overview-label">闁稿繈鍔嶉弸鍐潡閸屾繍娲?/div>
|
|||
|
|
<p className="meeting-preview-overview-copy">{analysis.overview || "闁哄棗鍊瑰Λ銈咁潡閸屾繍娲i柛鎰噹椤?}</p>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div className="meeting-preview-analysis-tabs">
|
|||
|
|
<Segmented<AnalysisTab>
|
|||
|
|
block
|
|||
|
|
value={analysisTab}
|
|||
|
|
onChange={(value) => setAnalysisTab(value)}
|
|||
|
|
options={[
|
|||
|
|
{ label: "缂佹梻濮炬俊?, value: "chapters" },
|
|||
|
|
{ label: "闁告瑦鍨奸埢?, value: "speakers" },
|
|||
|
|
{ label: "閻熸洑鑳堕崑?, value: "actions" },
|
|||
|
|
{ label: "鐎垫澘鎳庢慨?, value: "todos" },
|
|||
|
|
]}
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div className="meeting-preview-analysis-panel">
|
|||
|
|
{analysisTab === "chapters" &&
|
|||
|
|
(analysis.chapters.length ? (
|
|||
|
|
analysis.chapters.map((item, index) => (
|
|||
|
|
<div className="meeting-preview-chapter" key={`${item.title}-${index}`}>
|
|||
|
|
<div className="meeting-preview-chapter-time">{item.time || "--:--"}</div>
|
|||
|
|
<div>
|
|||
|
|
<strong className="meeting-preview-item-title">{item.title || `缂佹梻濮炬俊?${index + 1}`}</strong>
|
|||
|
|
<span className="meeting-preview-item-copy">{item.summary || "闁哄棗鍊瑰Λ銈囩博閻樺搫螡闁硅绻楅崼?}</span>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
))
|
|||
|
|
) : (
|
|||
|
|
<div className="meeting-preview-list-empty">
|
|||
|
|
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="闁哄棗鍊瑰Λ銈囩博閻樺搫螡闂侇偆鍠曢~? />
|
|||
|
|
</div>
|
|||
|
|
))}
|
|||
|
|
|
|||
|
|
{analysisTab === "speakers" &&
|
|||
|
|
(analysis.speakerSummaries.length ? (
|
|||
|
|
analysis.speakerSummaries.map((item, index) => (
|
|||
|
|
<div className="meeting-preview-speaker-card" key={`${item.speaker}-${index}`}>
|
|||
|
|
<div className="meeting-preview-speaker-head">
|
|||
|
|
<div className="meeting-preview-speaker-avatar">{(item.speaker || "闁?).slice(0, 1)}</div>
|
|||
|
|
<div>
|
|||
|
|
<div className="meeting-preview-speaker-name">{item.speaker || `闁告瑦鍨奸埢鍫熺?${index + 1}`}</div>
|
|||
|
|
<div className="meeting-preview-speaker-role">闁告瑦鍨奸埢鍫濐潡閸屾繂鐗?/div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div className="meeting-preview-item-copy">{item.summary || "闁哄棗鍊瑰Λ銈夊矗閹达絺鏋呴柟顒冨吹缁?}</div>
|
|||
|
|
</div>
|
|||
|
|
))
|
|||
|
|
) : (
|
|||
|
|
<div className="meeting-preview-list-empty">
|
|||
|
|
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="闁哄棗鍊瑰Λ銈夊矗閹达絺鏋呴柟顒冨吹缁? />
|
|||
|
|
</div>
|
|||
|
|
))}
|
|||
|
|
|
|||
|
|
{analysisTab === "actions" &&
|
|||
|
|
(analysis.keyPoints.length ? (
|
|||
|
|
analysis.keyPoints.map((item, index) => (
|
|||
|
|
<div className="meeting-preview-keypoint" key={`${item.title}-${index}`}>
|
|||
|
|
<div className="meeting-preview-keypoint-index">{String(index + 1).padStart(2, "0")}</div>
|
|||
|
|
<div>
|
|||
|
|
<strong className="meeting-preview-item-title">{item.title || `閻熸洑鑳堕崑?${index + 1}`}</strong>
|
|||
|
|
<span className="meeting-preview-item-copy">{item.summary || "闁哄棗鍊瑰Λ銈囨啺娴e搫浠悹鍥х摠濡?}</span>
|
|||
|
|
{(item.speaker || item.time) && (
|
|||
|
|
<div className="meeting-preview-item-meta">
|
|||
|
|
{item.speaker ? <span className="meeting-preview-meta-pill">{item.speaker}</span> : null}
|
|||
|
|
{item.time ? <span className="meeting-preview-meta-pill">{item.time}</span> : null}
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
))
|
|||
|
|
) : (
|
|||
|
|
<div className="meeting-preview-list-empty">
|
|||
|
|
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="闁哄棗鍊瑰Λ銈囨啺娴e搫浠柛銉у仱閵? />
|
|||
|
|
</div>
|
|||
|
|
))}
|
|||
|
|
|
|||
|
|
{analysisTab === "todos" &&
|
|||
|
|
(analysis.todos.length ? (
|
|||
|
|
analysis.todos.map((item, index) => (
|
|||
|
|
<div className="meeting-preview-todo" key={`${item}-${index}`}>
|
|||
|
|
<span className="meeting-preview-todo-dot" />
|
|||
|
|
<span className="meeting-preview-item-copy">{item}</span>
|
|||
|
|
</div>
|
|||
|
|
))
|
|||
|
|
) : (
|
|||
|
|
<div className="meeting-preview-list-empty">
|
|||
|
|
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="闁哄棗鍊瑰Λ銈咁嚗閸涱厼顫炲ù婊冾儔閵? />
|
|||
|
|
</div>
|
|||
|
|
))}
|
|||
|
|
</div>
|
|||
|
|
</section>
|
|||
|
|
|
|||
|
|
<section className="meeting-preview-card meeting-preview-section">
|
|||
|
|
<div className="meeting-preview-section-header">
|
|||
|
|
<div>
|
|||
|
|
<div className="meeting-preview-section-kicker">
|
|||
|
|
<FileTextOutlined />
|
|||
|
|
AI 闁诡剝宕电划?
|
|||
|
|
</div>
|
|||
|
|
<h2 className="meeting-preview-section-title">閻庣懓鏈弳锝囩棯椤忓浂娲?/h2>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div className="meeting-preview-markdown">
|
|||
|
|
{meeting.summaryContent ? (
|
|||
|
|
<div className="markdown-body">
|
|||
|
|
<ReactMarkdown>{meeting.summaryContent}</ReactMarkdown>
|
|||
|
|
</div>
|
|||
|
|
) : (
|
|||
|
|
<Empty description="闁哄棗鍊瑰Λ銈夊箑閼姐倗娉㈤柛鎰噹椤? />
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
</section>
|
|||
|
|
|
|||
|
|
<section className="meeting-preview-card meeting-preview-section">
|
|||
|
|
<div className="meeting-preview-section-header">
|
|||
|
|
<div>
|
|||
|
|
<div className="meeting-preview-section-kicker">
|
|||
|
|
<AudioOutlined />
|
|||
|
|
闁告鍠愰弸鍐媼閺夎法绉?
|
|||
|
|
</div>
|
|||
|
|
<h2 className="meeting-preview-section-title">濞村吋淇洪鍛姜椤掆偓缂?/h2>
|
|||
|
|
</div>
|
|||
|
|
<div className="meeting-preview-section-extra">
|
|||
|
|
<ClockCircleOutlined style={{ marginRight: 6 }} />
|
|||
|
|
闁绘劗鎳撻崵顔尖枔娴e啯鍎伴柛娆樺灥閻戯附娼鍕従濡?
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{meeting.audioSaveStatus === "FAILED" ? (
|
|||
|
|
<Alert
|
|||
|
|
className="meeting-preview-alert"
|
|||
|
|
type="warning"
|
|||
|
|
showIcon
|
|||
|
|
message="鐟滅増娲熼悡鍫曞棘閸ワ附顐藉☉鎾崇Т瑜版煡鎮?
|
|||
|
|
description={meeting.audioSaveMessage || "濞村吋淇洪鍛啅閹绘帞鏆氶柟瀛樺姧缁辨繃鎷呴崱娑氬従濡増鍨崇换姘扁偓娑櫭妵鎴犳嫻閵夘垳绀夌憸鐗堟尭婢х娀宕i鍥у幋闁哄被鍎冲﹢鍛村棘閸パ呮憻閺夌儐鍓欑紞宥夊Υ?}
|
|||
|
|
/>
|
|||
|
|
) : null}
|
|||
|
|
|
|||
|
|
{meeting.audioUrl ? (
|
|||
|
|
<audio
|
|||
|
|
ref={audioRef}
|
|||
|
|
className="meeting-preview-transcript-audio"
|
|||
|
|
src={meeting.audioUrl}
|
|||
|
|
controls
|
|||
|
|
preload="metadata"
|
|||
|
|
onTimeUpdate={handleAudioTimeUpdate}
|
|||
|
|
/>
|
|||
|
|
) : null}
|
|||
|
|
|
|||
|
|
<div className="meeting-preview-transcript-list">
|
|||
|
|
{transcripts.length ? (
|
|||
|
|
transcripts.map((item) => {
|
|||
|
|
const speakerName = item.speakerLabel || item.speakerName || item.speakerId || "闁告瑦鍨奸埢鍫熺?;
|
|||
|
|
const avatarColor = transcriptColorSeed(speakerName);
|
|||
|
|
return (
|
|||
|
|
<button
|
|||
|
|
key={item.id}
|
|||
|
|
type="button"
|
|||
|
|
className={`meeting-preview-transcript-item ${activeTranscriptId === item.id ? "is-active" : ""}`}
|
|||
|
|
onClick={() => handleTranscriptSeek(item)}
|
|||
|
|
>
|
|||
|
|
<div className="meeting-preview-transcript-head">
|
|||
|
|
<div className="meeting-preview-transcript-speaker">
|
|||
|
|
<div className="meeting-preview-transcript-avatar" style={{ backgroundColor: avatarColor }}>
|
|||
|
|
{(speakerName || "闁?).slice(0, 1)}
|
|||
|
|
</div>
|
|||
|
|
<div className="meeting-preview-transcript-name">
|
|||
|
|
<UserOutlined style={{ marginRight: 6, color: avatarColor }} />
|
|||
|
|
{speakerName}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div className="meeting-preview-transcript-time">
|
|||
|
|
{formatDurationRange(item.startTime, item.endTime)}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div className="meeting-preview-transcript-copy">{item.content || "闁哄棗鍊瑰Λ銈嗘姜椤掆偓缂嶅秹宕橀崨顓у晣"}</div>
|
|||
|
|
</button>
|
|||
|
|
);
|
|||
|
|
})
|
|||
|
|
) : (
|
|||
|
|
<Empty description="闁哄棗鍊瑰Λ銈嗘姜椤掆偓缂嶅秹宕橀崨顓у晣" />
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
</section>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div className="meeting-preview-disclaimer">
|
|||
|
|
闁哄懘缂氶崗姗€宕橀崨顓у晣闁?AI 婵☆垪鈧磭鈧兘鎮介悢绋跨亣闁挎稑濂旂划搴ㄦ偨閵娿倗鑹惧ù鍏间亢椤斿懏绌遍埄鍐х礀濡澘瀚~宥夋晬瀹€鍐惧殲缂備焦鎸搁幃搴ㄥ储閻斿娼楀ù鍏间亢椤斿懐鎷犻鐑嗘殧閻庣櫢绻濆Σ鍕椽瀹€鈧垾妯兼媼閵堝啠鍋?
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
}
|