feat: 添加会议转录编辑组件和优化关键词处理
- 在 `MeetingDetail` 页面中添加 `ActiveTranscriptRow` 组件,支持会议转录的编辑和保存 - 优化关键词处理逻辑,移除重复代码并dev_na
parent
ffc19fa572
commit
60754bbd26
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import {
|
||||
Avatar,
|
||||
|
|
@ -100,6 +100,7 @@ const ANALYSIS_EMPTY: MeetingAnalysis = {
|
|||
todos: [],
|
||||
};
|
||||
|
||||
|
||||
const splitLines = (value?: string | null) =>
|
||||
(value || '')
|
||||
.split(/\r?\n/)
|
||||
|
|
@ -175,7 +176,7 @@ const parseOverviewSection = (markdown: string) =>
|
|||
const parseKeywordsSection = (markdown: string, tags: string) => {
|
||||
const section = extractSection(markdown, ['关键词', '关键字', '标签']);
|
||||
const fromSection = parseBulletList(section)
|
||||
.flatMap((line) => line.split(/[,、,]/))
|
||||
.flatMap((line) => line.split(/[,、]/))
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
|
|
@ -300,7 +301,7 @@ const MeetingProgressDisplay: React.FC<{ meetingId: number; onComplete: () => vo
|
|||
const isError = percent < 0;
|
||||
|
||||
const formatEta = (seconds?: number) => {
|
||||
if (!seconds || seconds <= 0) return '正在分析中';
|
||||
if (!seconds || seconds <= 0) return '计算中';
|
||||
if (seconds < 60) return `${seconds} 秒`;
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const remainSeconds = seconds % 60;
|
||||
|
|
@ -336,7 +337,7 @@ const MeetingProgressDisplay: React.FC<{ meetingId: number; onComplete: () => vo
|
|||
<Text strong style={{ fontSize: 18, color: isError ? '#ff4d4f' : '#5d67ff', display: 'block', marginBottom: 8 }}>
|
||||
{progress?.message || '正在准备计算资源...'}
|
||||
</Text>
|
||||
<Text type="secondary">分析过程中请稍候,你可以先处理其他工作。</Text>
|
||||
<Text type="secondary">分析进行中,请稍候,你可以先处理其他工作。</Text>
|
||||
</div>
|
||||
<Divider style={{ margin: '32px 0' }} />
|
||||
<Row gutter={24}>
|
||||
|
|
@ -413,6 +414,117 @@ const SpeakerEditor: React.FC<{
|
|||
);
|
||||
};
|
||||
|
||||
type ActiveTranscriptRowProps = {
|
||||
item: MeetingTranscriptVO;
|
||||
meetingId: number;
|
||||
isOwner: boolean;
|
||||
isEditing: boolean;
|
||||
isSaving: boolean;
|
||||
speakerLabelMap: Map<string, string>;
|
||||
onSeek: (timeMs: number) => void;
|
||||
onStartEdit: (item: MeetingTranscriptVO, event: React.MouseEvent) => void;
|
||||
onDraftBlur: (item: MeetingTranscriptVO, value: string) => void;
|
||||
onDraftKeyDown: (item: MeetingTranscriptVO, value: string, event: React.KeyboardEvent<HTMLTextAreaElement>) => void;
|
||||
onSpeakerUpdated: () => void;
|
||||
};
|
||||
|
||||
const ActiveTranscriptRow = React.memo<ActiveTranscriptRowProps>(({
|
||||
item,
|
||||
meetingId,
|
||||
isOwner,
|
||||
isEditing,
|
||||
isSaving,
|
||||
speakerLabelMap,
|
||||
onSeek,
|
||||
onStartEdit,
|
||||
onDraftBlur,
|
||||
onDraftKeyDown,
|
||||
onSpeakerUpdated,
|
||||
}) => {
|
||||
const [draftValue, setDraftValue] = useState(item.content || '');
|
||||
const speakerTagLabel = item.speakerLabel ? (speakerLabelMap.get(item.speakerLabel) || item.speakerLabel) : '';
|
||||
|
||||
useEffect(() => {
|
||||
if (isEditing) {
|
||||
setDraftValue(item.content || '');
|
||||
}
|
||||
}, [isEditing, item.content]);
|
||||
|
||||
return (
|
||||
<List.Item className="transcript-row" onClick={() => onSeek(item.startTime)}>
|
||||
<div className="transcript-time">{formatTime(item.startTime)}</div>
|
||||
<div className="transcript-entry">
|
||||
<div className="transcript-meta">
|
||||
<Avatar icon={<UserOutlined />} className="transcript-avatar" />
|
||||
{isOwner ? (
|
||||
<Popover
|
||||
content={(
|
||||
<SpeakerEditor
|
||||
meetingId={meetingId}
|
||||
speakerId={item.speakerId}
|
||||
initialName={item.speakerName}
|
||||
initialLabel={item.speakerLabel}
|
||||
onSuccess={onSpeakerUpdated}
|
||||
/>
|
||||
)}
|
||||
title="编辑发言人"
|
||||
trigger="click"
|
||||
>
|
||||
<span className="transcript-speaker editable" onClick={(event) => event.stopPropagation()}>
|
||||
{item.speakerName || item.speakerId || '发言人'} <EditOutlined style={{ fontSize: 12 }} />
|
||||
</span>
|
||||
</Popover>
|
||||
) : (
|
||||
<span className="transcript-speaker">{item.speakerName || item.speakerId || '发言人'}</span>
|
||||
)}
|
||||
<Text type="secondary">{formatTime(item.startTime)}</Text>
|
||||
{speakerTagLabel && <Tag color="blue">{speakerTagLabel}</Tag>}
|
||||
</div>
|
||||
{isEditing ? (
|
||||
<div
|
||||
className={`transcript-bubble transcript-bubble-editing ${isSaving ? 'is-saving' : ''}`}
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
>
|
||||
<Input.TextArea
|
||||
autoFocus
|
||||
value={draftValue}
|
||||
style={{ width: '100%' }}
|
||||
onChange={(event) => setDraftValue(event.target.value)}
|
||||
onKeyDown={(event) => onDraftKeyDown(item, draftValue, event)}
|
||||
onBlur={(event) => {
|
||||
event.stopPropagation();
|
||||
onDraftBlur(item, draftValue);
|
||||
}}
|
||||
autoSize={{ minRows: 1, maxRows: 8 }}
|
||||
className="transcript-bubble-input"
|
||||
bordered={false}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className={`transcript-bubble ${isOwner ? 'editable' : ''}`}
|
||||
onDoubleClick={isOwner ? (event) => onStartEdit(item, event) : undefined}
|
||||
>
|
||||
{item.content}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</List.Item>
|
||||
);
|
||||
}, (prevProps, nextProps) => (
|
||||
prevProps.item === nextProps.item
|
||||
&& prevProps.meetingId === nextProps.meetingId
|
||||
&& prevProps.isOwner === nextProps.isOwner
|
||||
&& prevProps.isEditing === nextProps.isEditing
|
||||
&& prevProps.isSaving === nextProps.isSaving
|
||||
&& prevProps.speakerLabelMap === nextProps.speakerLabelMap
|
||||
&& prevProps.onSeek === nextProps.onSeek
|
||||
&& prevProps.onStartEdit === nextProps.onStartEdit
|
||||
&& prevProps.onDraftBlur === nextProps.onDraftBlur
|
||||
&& prevProps.onDraftKeyDown === nextProps.onDraftKeyDown
|
||||
&& prevProps.onSpeakerUpdated === nextProps.onSpeakerUpdated
|
||||
));
|
||||
|
||||
const MeetingDetail: React.FC = () => {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const navigate = useNavigate();
|
||||
|
|
@ -434,7 +546,6 @@ const MeetingDetail: React.FC = () => {
|
|||
const [selectedKeywords, setSelectedKeywords] = useState<string[]>([]);
|
||||
const [addingHotwords, setAddingHotwords] = useState(false);
|
||||
const [editingTranscriptId, setEditingTranscriptId] = useState<number | null>(null);
|
||||
const [transcriptDraft, setTranscriptDraft] = useState('');
|
||||
const [savingTranscriptId, setSavingTranscriptId] = useState<number | null>(null);
|
||||
const [audioCurrentTime, setAudioCurrentTime] = useState(0);
|
||||
const [audioDuration, setAudioDuration] = useState(0);
|
||||
|
|
@ -461,6 +572,10 @@ const MeetingDetail: React.FC = () => {
|
|||
analysis.todos.length
|
||||
);
|
||||
const visibleKeywords = expandKeywords ? analysis.keywords : analysis.keywords.slice(0, 9);
|
||||
const speakerLabelMap = useMemo(
|
||||
() => new Map(speakerLabels.map((item) => [item.itemValue, item.itemLabel])),
|
||||
[speakerLabels],
|
||||
);
|
||||
|
||||
const isOwner = useMemo(() => {
|
||||
if (!meeting) return false;
|
||||
|
|
@ -516,7 +631,7 @@ const MeetingDetail: React.FC = () => {
|
|||
};
|
||||
}, [meeting?.audioUrl, audioPlaybackRate]);
|
||||
|
||||
const fetchData = async (meetingId: number) => {
|
||||
const fetchData = useCallback(async (meetingId: number) => {
|
||||
try {
|
||||
const [detailRes, transcriptRes] = await Promise.all([getMeetingDetail(meetingId), getTranscripts(meetingId)]);
|
||||
setMeeting(detailRes.data.data);
|
||||
|
|
@ -526,7 +641,7 @@ const MeetingDetail: React.FC = () => {
|
|||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
const loadAiConfigs = async () => {
|
||||
try {
|
||||
|
|
@ -625,11 +740,10 @@ const MeetingDetail: React.FC = () => {
|
|||
});
|
||||
};
|
||||
|
||||
/*
|
||||
const handleAddSelectedHotwords = async () => {
|
||||
const keywords = selectedKeywords.map((item) => item.trim()).filter(Boolean);
|
||||
if (!keywords.length) {
|
||||
message.warning('请先勾选关键词');
|
||||
message.warning('请先选择关键词');
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -644,7 +758,7 @@ const MeetingDetail: React.FC = () => {
|
|||
const toCreate = keywords.filter((item) => !existingWords.has(item));
|
||||
|
||||
if (!toCreate.length) {
|
||||
message.info('所选关键词已全部存在于热词库');
|
||||
message.info('所选关键词已存在于热词库');
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -658,106 +772,7 @@ const MeetingDetail: React.FC = () => {
|
|||
weight: 2,
|
||||
status: 1,
|
||||
isPublic: 0,
|
||||
remark: meeting ? `来源于会议《${meeting.title}》` : '来源于会议关键词',
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
const skippedCount = keywords.length - toCreate.length;
|
||||
message.success(skippedCount > 0 ? `已新增 ${toCreate.length} 个热词,跳过 ${skippedCount} 个重复项` : `已新增 ${toCreate.length} 个热词`);
|
||||
setSelectedKeywords([]);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
setAddingHotwords(false);
|
||||
}
|
||||
};
|
||||
|
||||
};
|
||||
*/
|
||||
|
||||
/*
|
||||
const handleAddSelectedHotwords = async () => {
|
||||
const keywords = selectedKeywords.map((item) => item.trim()).filter(Boolean);
|
||||
if (!keywords.length) {
|
||||
message.warning('请先勾选关键词');
|
||||
return;
|
||||
}
|
||||
|
||||
setAddingHotwords(true);
|
||||
try {
|
||||
const existingRes = await getHotWordPage({ current: 1, size: 500, word: '' });
|
||||
const existingWords = new Set(
|
||||
(existingRes.data?.data?.records || [])
|
||||
.map((item) => item.word?.trim())
|
||||
.filter(Boolean),
|
||||
);
|
||||
const toCreate = keywords.filter((item) => !existingWords.has(item));
|
||||
|
||||
if (!toCreate.length) {
|
||||
message.info('所选关键词已全部存在于热词库');
|
||||
return;
|
||||
}
|
||||
|
||||
await Promise.all(
|
||||
toCreate.map((word) =>
|
||||
saveHotWord({
|
||||
word,
|
||||
pinyinList: [],
|
||||
matchStrategy: 1,
|
||||
category: '',
|
||||
weight: 2,
|
||||
status: 1,
|
||||
isPublic: 0,
|
||||
remark: meeting ? `来源于会议《${meeting.title}》` : '来源于会议关键词',
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
const skippedCount = keywords.length - toCreate.length;
|
||||
message.success(skippedCount > 0 ? `已新增 ${toCreate.length} 个热词,跳过 ${skippedCount} 个重复项` : `已新增 ${toCreate.length} 个热词`);
|
||||
setSelectedKeywords([]);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
setAddingHotwords(false);
|
||||
}
|
||||
};
|
||||
*/
|
||||
|
||||
const handleAddSelectedHotwords = async () => {
|
||||
const keywords = selectedKeywords.map((item) => item.trim()).filter(Boolean);
|
||||
if (!keywords.length) {
|
||||
message.warning('Please select keywords first');
|
||||
return;
|
||||
}
|
||||
|
||||
setAddingHotwords(true);
|
||||
try {
|
||||
const existingRes = await getHotWordPage({ current: 1, size: 500, word: '' });
|
||||
const existingWords = new Set(
|
||||
(existingRes.data?.data?.records || [])
|
||||
.map((item) => item.word?.trim())
|
||||
.filter(Boolean),
|
||||
);
|
||||
const toCreate = keywords.filter((item) => !existingWords.has(item));
|
||||
|
||||
if (!toCreate.length) {
|
||||
message.info('Selected keywords already exist in hot words');
|
||||
return;
|
||||
}
|
||||
|
||||
await Promise.all(
|
||||
toCreate.map((word) =>
|
||||
saveHotWord({
|
||||
word,
|
||||
pinyinList: [],
|
||||
matchStrategy: 1,
|
||||
category: '',
|
||||
weight: 2,
|
||||
status: 1,
|
||||
isPublic: 0,
|
||||
remark: meeting ? `From meeting: ${meeting.title}` : 'From meeting keywords',
|
||||
remark: meeting ? `来源于会议:${meeting.title}` : '来源于会议关键词',
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
|
@ -765,8 +780,8 @@ const MeetingDetail: React.FC = () => {
|
|||
const skippedCount = keywords.length - toCreate.length;
|
||||
message.success(
|
||||
skippedCount > 0
|
||||
? `Added ${toCreate.length} hot words, skipped ${skippedCount} duplicates`
|
||||
: `Added ${toCreate.length} hot words`,
|
||||
? `已新增 ${toCreate.length} 个热词,跳过 ${skippedCount} 个重复项`
|
||||
: `已新增 ${toCreate.length} 个热词`,
|
||||
);
|
||||
setSelectedKeywords([]);
|
||||
} catch (error) {
|
||||
|
|
@ -776,20 +791,18 @@ const MeetingDetail: React.FC = () => {
|
|||
}
|
||||
};
|
||||
|
||||
const handleStartEditTranscript = (item: MeetingTranscriptVO, event: React.MouseEvent) => {
|
||||
const handleStartEditTranscript = useCallback((item: MeetingTranscriptVO, event: React.MouseEvent) => {
|
||||
event.stopPropagation();
|
||||
setEditingTranscriptId(item.id);
|
||||
setTranscriptDraft(item.content || '');
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleCancelEditTranscript = (event?: React.SyntheticEvent) => {
|
||||
const handleCancelEditTranscript = useCallback((event?: React.SyntheticEvent) => {
|
||||
event?.stopPropagation();
|
||||
setEditingTranscriptId(null);
|
||||
setTranscriptDraft('');
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleSaveTranscript = async (item: MeetingTranscriptVO, nextContent?: string) => {
|
||||
const content = (nextContent ?? transcriptDraft).trim();
|
||||
const handleSaveTranscript = useCallback(async (item: MeetingTranscriptVO, nextContent?: string) => {
|
||||
const content = (nextContent ?? item.content ?? '').trim();
|
||||
if (!content) {
|
||||
message.warning('转录内容不能为空');
|
||||
return;
|
||||
|
|
@ -816,24 +829,33 @@ const MeetingDetail: React.FC = () => {
|
|||
} finally {
|
||||
setSavingTranscriptId(null);
|
||||
}
|
||||
};
|
||||
}, [fetchData, handleCancelEditTranscript, meeting]);
|
||||
|
||||
const handleTranscriptDraftKeyDown = (item: MeetingTranscriptVO, event: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
const handleTranscriptDraftKeyDown = useCallback((item: MeetingTranscriptVO, value: string, event: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
if (event.key === 'Escape') {
|
||||
handleCancelEditTranscript();
|
||||
return;
|
||||
}
|
||||
if ((event.ctrlKey || event.metaKey) && event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
void handleSaveTranscript(item);
|
||||
void handleSaveTranscript(item, value);
|
||||
}
|
||||
};
|
||||
}, [handleCancelEditTranscript, handleSaveTranscript]);
|
||||
|
||||
const seekTo = (timeMs: number) => {
|
||||
const handleTranscriptDraftBlur = useCallback((item: MeetingTranscriptVO, value: string) => {
|
||||
void handleSaveTranscript(item, value);
|
||||
}, [handleSaveTranscript]);
|
||||
|
||||
const handleTranscriptSpeakerUpdated = useCallback(() => {
|
||||
if (!meeting) return;
|
||||
void fetchData(meeting.id);
|
||||
}, [fetchData, meeting]);
|
||||
|
||||
const seekTo = useCallback((timeMs: number) => {
|
||||
if (!audioRef.current) return;
|
||||
audioRef.current.currentTime = timeMs / 1000;
|
||||
audioRef.current.play();
|
||||
};
|
||||
}, []);
|
||||
|
||||
const toggleAudioPlayback = () => {
|
||||
if (!audioRef.current) return;
|
||||
|
|
@ -1022,7 +1044,7 @@ const MeetingDetail: React.FC = () => {
|
|||
)}
|
||||
</div>
|
||||
<div className="summary-actions">
|
||||
<span className={`status-pill ${hasAnalysis ? 'success' : 'warning'}`}>{hasAnalysis ? '已生成' : '待生成'}</span>
|
||||
<span className={`status-pill ${hasAnalysis ? 'success' : 'warning'}`}>{hasAnalysis ? '已生成' : '待生成'}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -1113,8 +1135,8 @@ const MeetingDetail: React.FC = () => {
|
|||
analysis.speakerSummaries.map((item, index) => (
|
||||
<div className="speaker-summary-card" key={`${item.speaker}-${index}`}>
|
||||
<div className="speaker-summary-side">
|
||||
<div className="speaker-avatar">{(item.speaker || '发').slice(0, 1)}</div>
|
||||
<div className="speaker-summary-name">{item.speaker || `发言人 ${index + 1}`}</div>
|
||||
<div className="speaker-avatar">{(item.speaker || '发').slice(0, 1)}</div>
|
||||
<div className="speaker-summary-name">{item.speaker || `发言人${index + 1}`}</div>
|
||||
</div>
|
||||
<div className="speaker-summary-body">
|
||||
<div className="speaker-summary-meta">发言概述</div>
|
||||
|
|
@ -1172,7 +1194,7 @@ const MeetingDetail: React.FC = () => {
|
|||
<div className="section-divider-note">
|
||||
<div className="section-divider-line" />
|
||||
<div className="section-divider-text">
|
||||
智能内容由 AI 模型生成,我们不对内容准确性和完整性做任何保证,亦不代表我们的观点或态度
|
||||
智能内容由 AI 模型生成,我们不对内容准确性和完整性作任何保证,也不代表我们的观点或态度
|
||||
</div>
|
||||
<div className="section-divider-line" />
|
||||
</div>
|
||||
|
|
@ -1182,85 +1204,19 @@ const MeetingDetail: React.FC = () => {
|
|||
<List
|
||||
dataSource={transcripts}
|
||||
renderItem={(item) => (
|
||||
<List.Item className="transcript-row" onClick={() => seekTo(item.startTime)}>
|
||||
<div className="transcript-time">{formatTime(item.startTime)}</div>
|
||||
<div className="transcript-entry">
|
||||
<div className="transcript-meta">
|
||||
<Avatar icon={<UserOutlined />} className="transcript-avatar" />
|
||||
{isOwner ? (
|
||||
<Popover
|
||||
content={
|
||||
<SpeakerEditor
|
||||
meetingId={meeting.id}
|
||||
speakerId={item.speakerId}
|
||||
initialName={item.speakerName}
|
||||
initialLabel={item.speakerLabel}
|
||||
onSuccess={() => fetchData(meeting.id)}
|
||||
/>
|
||||
}
|
||||
title="编辑发言人"
|
||||
trigger="click"
|
||||
>
|
||||
<span className="transcript-speaker editable" onClick={(event) => event.stopPropagation()}>
|
||||
{item.speakerName || item.speakerId || '发言人'} <EditOutlined style={{ fontSize: 12 }} />
|
||||
</span>
|
||||
</Popover>
|
||||
) : (
|
||||
<span className="transcript-speaker">{item.speakerName || item.speakerId || '发言人'}</span>
|
||||
)}
|
||||
<Text type="secondary">{formatTime(item.startTime)}</Text>
|
||||
{item.speakerLabel && (
|
||||
<Tag color="blue">
|
||||
{speakerLabels.find((label) => label.itemValue === item.speakerLabel)?.itemLabel || item.speakerLabel}
|
||||
</Tag>
|
||||
)}
|
||||
</div>
|
||||
{editingTranscriptId === item.id ? (
|
||||
<div
|
||||
className="transcript-bubble transcript-bubble-editing"
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
>
|
||||
<Input.TextArea
|
||||
autoFocus
|
||||
value={transcriptDraft}
|
||||
style={{ width: '100%' }}
|
||||
onChange={(event) => setTranscriptDraft(event.target.value)}
|
||||
onKeyDown={(event) => handleTranscriptDraftKeyDown(item, event as unknown as React.KeyboardEvent<HTMLDivElement>)}
|
||||
onBlur={(event) => {
|
||||
event.stopPropagation();
|
||||
void handleSaveTranscript(item, event.target.value);
|
||||
}}
|
||||
autoSize={{ minRows: 1, maxRows: 8 }}
|
||||
className="transcript-bubble-input"
|
||||
bordered={false}
|
||||
/>
|
||||
{false && false && <div className="transcript-bubble-actions">
|
||||
<Text type="secondary" className="transcript-bubble-hint">
|
||||
Ctrl+Enter 保存,Esc 取消
|
||||
</Text>
|
||||
<Button size="small" onClick={handleCancelEditTranscript}>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
type="primary"
|
||||
loading={savingTranscriptId === item.id}
|
||||
onClick={() => handleSaveTranscript(item)}
|
||||
>
|
||||
保存
|
||||
</Button>
|
||||
</div>}
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className={`transcript-bubble ${isOwner ? 'editable' : ''}`}
|
||||
onDoubleClick={isOwner ? (event) => handleStartEditTranscript(item, event) : undefined}
|
||||
>
|
||||
{item.content}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</List.Item>
|
||||
<ActiveTranscriptRow
|
||||
item={item}
|
||||
meetingId={meeting.id}
|
||||
isOwner={isOwner}
|
||||
isEditing={editingTranscriptId === item.id}
|
||||
isSaving={savingTranscriptId === item.id}
|
||||
speakerLabelMap={speakerLabelMap}
|
||||
onSeek={seekTo}
|
||||
onStartEdit={handleStartEditTranscript}
|
||||
onDraftBlur={handleTranscriptDraftBlur}
|
||||
onDraftKeyDown={handleTranscriptDraftKeyDown}
|
||||
onSpeakerUpdated={handleTranscriptSpeakerUpdated}
|
||||
/>
|
||||
)}
|
||||
locale={{ emptyText: meeting.status < 3 ? '识别任务进行中...' : '暂无数据' }}
|
||||
/>
|
||||
|
|
@ -2026,7 +1982,7 @@ const MeetingDetail: React.FC = () => {
|
|||
</Select>
|
||||
</Form.Item>
|
||||
<Divider />
|
||||
<Text type="secondary">重新总结会基于当前语音转写全文重新生成纪要,原有总结内容将被覆盖。</Text>
|
||||
<Text type="secondary">重新总结会基于当前语音转录全文重新生成纪要,原有总结内容将被覆盖。</Text>
|
||||
</Form>
|
||||
</Drawer>
|
||||
)}
|
||||
|
|
@ -2035,3 +1991,4 @@ const MeetingDetail: React.FC = () => {
|
|||
};
|
||||
|
||||
export default MeetingDetail;
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue