x修复前端无法自动总结,修复点击说话人修改问题,新增双击编辑文本内容问题
parent
a3ae293d42
commit
cc1817078a
|
|
@ -370,6 +370,12 @@ def get_meeting_details(meeting_id: int, current_user: dict = Depends(get_curren
|
|||
meeting_data.transcription_status = TranscriptionTaskStatus(**transcription_status_data)
|
||||
except Exception as e:
|
||||
print(f"Warning: Failed to get transcription status for meeting {meeting_id}: {e}")
|
||||
try:
|
||||
llm_status_data = async_meeting_service.get_meeting_llm_status(meeting_id)
|
||||
if llm_status_data:
|
||||
meeting_data.llm_status = TranscriptionTaskStatus(**llm_status_data)
|
||||
except Exception as e:
|
||||
print(f"Warning: Failed to get llm status for meeting {meeting_id}: {e}")
|
||||
return create_api_response(code="200", message="获取会议详情成功", data=meeting_data)
|
||||
|
||||
@router.get("/meetings/{meeting_id}/transcript")
|
||||
|
|
@ -543,14 +549,21 @@ async def upload_audio(
|
|||
|
||||
model_code = model_code.strip() if model_code else None
|
||||
|
||||
# 0. 如果没有传入 prompt_id,尝试获取默认模版ID
|
||||
# 0. 如果没有传入 prompt_id,优先使用会议已配置模版,否则回退默认模版
|
||||
if prompt_id is None:
|
||||
with get_db_connection() as connection:
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
cursor.execute("SELECT prompt_id FROM meetings WHERE meeting_id = %s", (meeting_id,))
|
||||
meeting_row = cursor.fetchone()
|
||||
if meeting_row and meeting_row.get('prompt_id') and int(meeting_row['prompt_id']) > 0:
|
||||
prompt_id = int(meeting_row['prompt_id'])
|
||||
else:
|
||||
cursor = connection.cursor()
|
||||
cursor.execute(
|
||||
"SELECT id FROM prompts WHERE task_type = 'MEETING_TASK' AND is_default = TRUE AND is_active = TRUE LIMIT 1"
|
||||
)
|
||||
prompt_id = cursor.fetchone()[0]
|
||||
prompt_row = cursor.fetchone()
|
||||
prompt_id = prompt_row[0] if prompt_row else None
|
||||
|
||||
# 1. 文件类型验证
|
||||
file_extension = os.path.splitext(audio_file.filename)[1].lower()
|
||||
|
|
@ -779,12 +792,17 @@ def get_meeting_transcription_status(meeting_id: int, current_user: dict = Depen
|
|||
return create_api_response(code="500", message=f"Failed to get meeting transcription status: {str(e)}")
|
||||
|
||||
@router.post("/meetings/{meeting_id}/transcription/start")
|
||||
def start_meeting_transcription(meeting_id: int, current_user: dict = Depends(get_current_user)):
|
||||
def start_meeting_transcription(
|
||||
meeting_id: int,
|
||||
background_tasks: BackgroundTasks,
|
||||
current_user: dict = Depends(get_current_user)
|
||||
):
|
||||
try:
|
||||
with get_db_connection() as connection:
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
cursor.execute("SELECT meeting_id FROM meetings WHERE meeting_id = %s", (meeting_id,))
|
||||
if not cursor.fetchone():
|
||||
cursor.execute("SELECT meeting_id, prompt_id FROM meetings WHERE meeting_id = %s", (meeting_id,))
|
||||
meeting = cursor.fetchone()
|
||||
if not meeting:
|
||||
return create_api_response(code="404", message="Meeting not found")
|
||||
cursor.execute("SELECT file_path FROM audio_files WHERE meeting_id = %s LIMIT 1", (meeting_id,))
|
||||
audio_file = cursor.fetchone()
|
||||
|
|
@ -796,6 +814,13 @@ def start_meeting_transcription(meeting_id: int, current_user: dict = Depends(ge
|
|||
"task_id": existing_status['task_id'], "status": existing_status['status']
|
||||
})
|
||||
task_id = transcription_service.start_transcription(meeting_id, audio_file['file_path'])
|
||||
background_tasks.add_task(
|
||||
async_meeting_service.monitor_and_auto_summarize,
|
||||
meeting_id,
|
||||
task_id,
|
||||
meeting.get('prompt_id') if meeting.get('prompt_id') not in (None, 0) else None,
|
||||
None
|
||||
)
|
||||
return create_api_response(code="200", message="Transcription task started successfully", data={
|
||||
"task_id": task_id, "meeting_id": meeting_id
|
||||
})
|
||||
|
|
@ -914,6 +939,12 @@ def generate_meeting_summary_async(meeting_id: int, request: GenerateSummaryRequ
|
|||
cursor.execute("SELECT meeting_id FROM meetings WHERE meeting_id = %s", (meeting_id,))
|
||||
if not cursor.fetchone():
|
||||
return create_api_response(code="404", message="Meeting not found")
|
||||
transcription_status = transcription_service.get_meeting_transcription_status(meeting_id)
|
||||
if transcription_status and transcription_status.get('status') in ['pending', 'processing']:
|
||||
return create_api_response(code="409", message="转录进行中,暂不允许重新总结", data={
|
||||
"task_id": transcription_status.get('task_id'),
|
||||
"status": transcription_status.get('status')
|
||||
})
|
||||
# 传递 prompt_id 和 model_code 参数给服务层
|
||||
task_id = async_meeting_service.start_summary_generation(meeting_id, request.user_prompt, request.prompt_id, request.model_code)
|
||||
background_tasks.add_task(async_meeting_service._process_task, task_id)
|
||||
|
|
|
|||
|
|
@ -84,6 +84,7 @@ class Meeting(BaseModel):
|
|||
audio_duration: Optional[float] = None
|
||||
summary: Optional[str] = None
|
||||
transcription_status: Optional[TranscriptionTaskStatus] = None
|
||||
llm_status: Optional[TranscriptionTaskStatus] = None
|
||||
prompt_id: Optional[int] = None
|
||||
prompt_name: Optional[str] = None
|
||||
overall_status: Optional[str] = None
|
||||
|
|
@ -125,7 +126,7 @@ class BatchSpeakerTagUpdateRequest(BaseModel):
|
|||
|
||||
class TranscriptUpdateRequest(BaseModel):
|
||||
segment_id: int
|
||||
new_text: str
|
||||
text_content: str
|
||||
|
||||
class BatchTranscriptUpdateRequest(BaseModel):
|
||||
updates: List[TranscriptUpdateRequest]
|
||||
|
|
|
|||
|
|
@ -407,6 +407,7 @@ class AsyncMeetingService:
|
|||
'meeting_id': int(task_data.get('meeting_id', 0)),
|
||||
'created_at': task_data.get('created_at'),
|
||||
'updated_at': task_data.get('updated_at'),
|
||||
'message': task_data.get('message'),
|
||||
'result': task_data.get('result'),
|
||||
'error_message': task_data.get('error_message')
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import {
|
|||
EyeOutlined, FileTextOutlined, PartitionOutlined,
|
||||
SaveOutlined, CloseOutlined,
|
||||
StarFilled, RobotOutlined, DownloadOutlined,
|
||||
DownOutlined,
|
||||
DownOutlined, CheckOutlined,
|
||||
MoreOutlined, AudioOutlined
|
||||
} from '@ant-design/icons';
|
||||
import MarkdownRenderer from '../components/MarkdownRenderer';
|
||||
|
|
@ -65,6 +65,7 @@ const MeetingDetails = ({ user }) => {
|
|||
const [summaryTaskProgress, setSummaryTaskProgress] = useState(0);
|
||||
const [summaryTaskMessage, setSummaryTaskMessage] = useState('');
|
||||
const [summaryPollInterval, setSummaryPollInterval] = useState(null);
|
||||
const [activeSummaryTaskId, setActiveSummaryTaskId] = useState(null);
|
||||
const [llmModels, setLlmModels] = useState([]);
|
||||
const [selectedModelCode, setSelectedModelCode] = useState(null);
|
||||
|
||||
|
|
@ -87,10 +88,26 @@ const MeetingDetails = ({ user }) => {
|
|||
// 总结内容编辑(同窗口)
|
||||
const [isEditingSummary, setIsEditingSummary] = useState(false);
|
||||
const [editingSummaryContent, setEditingSummaryContent] = useState('');
|
||||
const [inlineSpeakerEdit, setInlineSpeakerEdit] = useState(null);
|
||||
const [inlineSpeakerEditSegmentId, setInlineSpeakerEditSegmentId] = useState(null);
|
||||
const [inlineSpeakerValue, setInlineSpeakerValue] = useState('');
|
||||
const [inlineSegmentEditId, setInlineSegmentEditId] = useState(null);
|
||||
const [inlineSegmentValue, setInlineSegmentValue] = useState('');
|
||||
const [savingInlineEdit, setSavingInlineEdit] = useState(false);
|
||||
|
||||
const audioRef = useRef(null);
|
||||
const transcriptRefs = useRef([]);
|
||||
const isMeetingOwner = user?.user_id === meeting?.creator_id;
|
||||
const hasUploadedAudio = Boolean(audioUrl);
|
||||
const isTranscriptionRunning = ['pending', 'processing'].includes(transcriptionStatus?.status);
|
||||
const summaryDisabledReason = isUploading
|
||||
? '音频上传中,暂不允许重新总结'
|
||||
: !hasUploadedAudio
|
||||
? '请先上传音频后再总结'
|
||||
: isTranscriptionRunning
|
||||
? '转录进行中,完成后会自动总结'
|
||||
: '';
|
||||
const isSummaryActionDisabled = isUploading || !hasUploadedAudio || isTranscriptionRunning || summaryLoading;
|
||||
|
||||
/* ══════════════════ 数据获取 ══════════════════ */
|
||||
|
||||
|
|
@ -109,6 +126,9 @@ const MeetingDetails = ({ user }) => {
|
|||
setLoading(true);
|
||||
const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.MEETINGS.DETAIL(meeting_id)));
|
||||
setMeeting(response.data);
|
||||
if (response.data.prompt_id) {
|
||||
setSelectedPromptId(response.data.prompt_id);
|
||||
}
|
||||
setAccessPasswordEnabled(Boolean(response.data.access_password));
|
||||
setAccessPasswordDraft(response.data.access_password || '');
|
||||
|
||||
|
|
@ -119,6 +139,17 @@ const MeetingDetails = ({ user }) => {
|
|||
if (['pending', 'processing'].includes(ts.status)) {
|
||||
startStatusPolling(ts.task_id);
|
||||
}
|
||||
} else {
|
||||
setTranscriptionStatus(null);
|
||||
setTranscriptionProgress(0);
|
||||
}
|
||||
|
||||
if (response.data.llm_status) {
|
||||
setSummaryTaskProgress(response.data.llm_status.progress || 0);
|
||||
setSummaryTaskMessage(response.data.llm_status.message || '');
|
||||
if (['pending', 'processing'].includes(response.data.llm_status.status)) {
|
||||
startSummaryPolling(response.data.llm_status.task_id);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
|
|
@ -127,6 +158,7 @@ const MeetingDetails = ({ user }) => {
|
|||
} catch { setAudioUrl(null); }
|
||||
|
||||
fetchTranscript();
|
||||
fetchSummaryHistory();
|
||||
} catch {
|
||||
message.error('加载会议详情失败');
|
||||
} finally {
|
||||
|
|
@ -160,7 +192,13 @@ const MeetingDetails = ({ user }) => {
|
|||
setTranscriptionProgress(status.progress || 0);
|
||||
if (['completed', 'failed', 'error', 'cancelled'].includes(status.status)) {
|
||||
clearInterval(interval);
|
||||
if (status.status === 'completed') fetchTranscript();
|
||||
if (status.status === 'completed') {
|
||||
fetchTranscript();
|
||||
fetchMeetingDetails();
|
||||
setTimeout(() => {
|
||||
fetchSummaryHistory();
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
} catch { clearInterval(interval); }
|
||||
}, 3000);
|
||||
|
|
@ -186,10 +224,66 @@ const MeetingDetails = ({ user }) => {
|
|||
} catch {}
|
||||
};
|
||||
|
||||
const startSummaryPolling = (taskId, options = {}) => {
|
||||
const { closeDrawerOnComplete = false } = options;
|
||||
if (!taskId) return;
|
||||
if (summaryPollInterval && activeSummaryTaskId === taskId) return;
|
||||
if (summaryPollInterval) clearInterval(summaryPollInterval);
|
||||
|
||||
setActiveSummaryTaskId(taskId);
|
||||
setSummaryLoading(true);
|
||||
|
||||
const poll = async () => {
|
||||
try {
|
||||
const statusRes = await apiClient.get(buildApiUrl(API_ENDPOINTS.TASKS.SUMMARY_STATUS(taskId)));
|
||||
const status = statusRes.data;
|
||||
setSummaryTaskProgress(status.progress || 0);
|
||||
setSummaryTaskMessage(status.message || '');
|
||||
|
||||
if (status.status === 'completed') {
|
||||
clearInterval(interval);
|
||||
setSummaryPollInterval(null);
|
||||
setActiveSummaryTaskId(null);
|
||||
setSummaryLoading(false);
|
||||
if (closeDrawerOnComplete) {
|
||||
setShowSummaryDrawer(false);
|
||||
}
|
||||
fetchSummaryHistory();
|
||||
fetchMeetingDetails();
|
||||
} else if (status.status === 'failed') {
|
||||
clearInterval(interval);
|
||||
setSummaryPollInterval(null);
|
||||
setActiveSummaryTaskId(null);
|
||||
setSummaryLoading(false);
|
||||
message.error(status.error_message || '生成总结失败');
|
||||
}
|
||||
} catch (error) {
|
||||
clearInterval(interval);
|
||||
setSummaryPollInterval(null);
|
||||
setActiveSummaryTaskId(null);
|
||||
setSummaryLoading(false);
|
||||
message.error(error?.response?.data?.message || '获取总结状态失败');
|
||||
}
|
||||
};
|
||||
|
||||
const interval = setInterval(poll, 3000);
|
||||
setSummaryPollInterval(interval);
|
||||
poll();
|
||||
};
|
||||
|
||||
const fetchSummaryHistory = async () => {
|
||||
try {
|
||||
const res = await apiClient.get(buildApiUrl(`/api/meetings/${meeting_id}/llm-tasks`));
|
||||
setSummaryHistory(res.data.tasks?.filter(t => t.status === 'completed') || []);
|
||||
const tasks = res.data.tasks || [];
|
||||
setSummaryHistory(tasks.filter(t => t.status === 'completed'));
|
||||
const latestRunningTask = tasks.find(t => ['pending', 'processing'].includes(t.status));
|
||||
if (latestRunningTask) {
|
||||
startSummaryPolling(latestRunningTask.task_id);
|
||||
} else if (!activeSummaryTaskId) {
|
||||
setSummaryLoading(false);
|
||||
setSummaryTaskProgress(0);
|
||||
setSummaryTaskMessage('');
|
||||
}
|
||||
} catch {}
|
||||
};
|
||||
|
||||
|
|
@ -217,6 +311,12 @@ const MeetingDetails = ({ user }) => {
|
|||
formData.append('audio_file', file);
|
||||
formData.append('meeting_id', meeting_id);
|
||||
formData.append('force_replace', 'true');
|
||||
if (meeting?.prompt_id) {
|
||||
formData.append('prompt_id', String(meeting.prompt_id));
|
||||
}
|
||||
if (selectedModelCode) {
|
||||
formData.append('model_code', selectedModelCode);
|
||||
}
|
||||
setIsUploading(true);
|
||||
try {
|
||||
await apiClient.post(buildApiUrl(API_ENDPOINTS.MEETINGS.UPLOAD_AUDIO), formData);
|
||||
|
|
@ -264,6 +364,81 @@ const MeetingDetails = ({ user }) => {
|
|||
document.getElementById('audio-upload-input')?.click();
|
||||
};
|
||||
|
||||
const startInlineSpeakerEdit = (speakerId, currentTag, segmentId) => {
|
||||
setInlineSpeakerEdit(speakerId);
|
||||
setInlineSpeakerEditSegmentId(`speaker-${speakerId}-${segmentId}`);
|
||||
setInlineSpeakerValue(currentTag || `发言人 ${speakerId}`);
|
||||
};
|
||||
|
||||
const cancelInlineSpeakerEdit = () => {
|
||||
setInlineSpeakerEdit(null);
|
||||
setInlineSpeakerEditSegmentId(null);
|
||||
setInlineSpeakerValue('');
|
||||
};
|
||||
|
||||
const saveInlineSpeakerEdit = async () => {
|
||||
if (inlineSpeakerEdit == null) return;
|
||||
const nextTag = inlineSpeakerValue.trim();
|
||||
if (!nextTag) {
|
||||
message.warning('发言人名称不能为空');
|
||||
return;
|
||||
}
|
||||
setSavingInlineEdit(true);
|
||||
try {
|
||||
await apiClient.put(buildApiUrl(`/api/meetings/${meeting_id}/speaker-tags/batch`), {
|
||||
updates: [{ speaker_id: inlineSpeakerEdit, new_tag: nextTag }]
|
||||
});
|
||||
setTranscript(prev => prev.map(item => (
|
||||
item.speaker_id === inlineSpeakerEdit
|
||||
? { ...item, speaker_tag: nextTag }
|
||||
: item
|
||||
)));
|
||||
setSpeakerList(prev => prev.map(item => (
|
||||
item.speaker_id === inlineSpeakerEdit
|
||||
? { ...item, speaker_tag: nextTag }
|
||||
: item
|
||||
)));
|
||||
setEditingSpeakers(prev => ({ ...prev, [inlineSpeakerEdit]: nextTag }));
|
||||
message.success('发言人名称已更新');
|
||||
cancelInlineSpeakerEdit();
|
||||
} catch (error) {
|
||||
message.error(error?.response?.data?.message || '更新发言人名称失败');
|
||||
} finally {
|
||||
setSavingInlineEdit(false);
|
||||
}
|
||||
};
|
||||
|
||||
const startInlineSegmentEdit = (segment) => {
|
||||
setInlineSegmentEditId(segment.segment_id);
|
||||
setInlineSegmentValue(segment.text_content || '');
|
||||
};
|
||||
|
||||
const cancelInlineSegmentEdit = () => {
|
||||
setInlineSegmentEditId(null);
|
||||
setInlineSegmentValue('');
|
||||
};
|
||||
|
||||
const saveInlineSegmentEdit = async () => {
|
||||
if (inlineSegmentEditId == null) return;
|
||||
setSavingInlineEdit(true);
|
||||
try {
|
||||
await apiClient.put(buildApiUrl(`/api/meetings/${meeting_id}/transcript/batch`), {
|
||||
updates: [{ segment_id: inlineSegmentEditId, text_content: inlineSegmentValue }]
|
||||
});
|
||||
setTranscript(prev => prev.map(item => (
|
||||
item.segment_id === inlineSegmentEditId
|
||||
? { ...item, text_content: inlineSegmentValue }
|
||||
: item
|
||||
)));
|
||||
message.success('转录内容已更新');
|
||||
cancelInlineSegmentEdit();
|
||||
} catch (error) {
|
||||
message.error(error?.response?.data?.message || '更新转录内容失败');
|
||||
} finally {
|
||||
setSavingInlineEdit(false);
|
||||
}
|
||||
};
|
||||
|
||||
const changePlaybackRate = (nextRate) => {
|
||||
setPlaybackRate(nextRate);
|
||||
if (audioRef.current) {
|
||||
|
|
@ -298,6 +473,18 @@ const MeetingDetails = ({ user }) => {
|
|||
};
|
||||
|
||||
const generateSummary = async () => {
|
||||
if (isUploading) {
|
||||
message.warning('音频上传中,暂不允许重新总结');
|
||||
return;
|
||||
}
|
||||
if (!hasUploadedAudio) {
|
||||
message.warning('请先上传音频后再总结');
|
||||
return;
|
||||
}
|
||||
if (isTranscriptionRunning) {
|
||||
message.warning('转录进行中,暂不允许重新总结');
|
||||
return;
|
||||
}
|
||||
setSummaryLoading(true);
|
||||
setSummaryTaskProgress(0);
|
||||
try {
|
||||
|
|
@ -306,29 +493,26 @@ const MeetingDetails = ({ user }) => {
|
|||
prompt_id: selectedPromptId,
|
||||
model_code: selectedModelCode
|
||||
});
|
||||
const taskId = res.data.task_id;
|
||||
const interval = setInterval(async () => {
|
||||
const statusRes = await apiClient.get(buildApiUrl(API_ENDPOINTS.TASKS.SUMMARY_STATUS(taskId)));
|
||||
const s = statusRes.data;
|
||||
setSummaryTaskProgress(s.progress || 0);
|
||||
setSummaryTaskMessage(s.message);
|
||||
if (s.status === 'completed') {
|
||||
clearInterval(interval);
|
||||
startSummaryPolling(res.data.task_id, { closeDrawerOnComplete: true });
|
||||
} catch (error) {
|
||||
message.error(error?.response?.data?.message || '生成总结失败');
|
||||
setSummaryLoading(false);
|
||||
setShowSummaryDrawer(false);
|
||||
fetchSummaryHistory();
|
||||
fetchMeetingDetails();
|
||||
} else if (s.status === 'failed') {
|
||||
clearInterval(interval);
|
||||
setSummaryLoading(false);
|
||||
message.error('生成总结失败');
|
||||
}
|
||||
}, 3000);
|
||||
setSummaryPollInterval(interval);
|
||||
} catch { setSummaryLoading(false); }
|
||||
};
|
||||
|
||||
const openSummaryDrawer = () => {
|
||||
if (isUploading) {
|
||||
message.warning('音频上传中,暂不允许重新总结');
|
||||
return;
|
||||
}
|
||||
if (!hasUploadedAudio) {
|
||||
message.warning('请先上传音频后再总结');
|
||||
return;
|
||||
}
|
||||
if (isTranscriptionRunning) {
|
||||
message.warning('转录进行中,完成后会自动总结');
|
||||
return;
|
||||
}
|
||||
setShowSummaryDrawer(true);
|
||||
fetchSummaryHistory();
|
||||
};
|
||||
|
|
@ -444,7 +628,13 @@ const MeetingDetails = ({ user }) => {
|
|||
</div>
|
||||
|
||||
<Space style={{ flexShrink: 0 }}>
|
||||
<Button icon={<SyncOutlined />} onClick={openSummaryDrawer}>重新总结</Button>
|
||||
<Tooltip title={summaryDisabledReason}>
|
||||
<span>
|
||||
<Button icon={<SyncOutlined />} onClick={openSummaryDrawer} disabled={isSummaryActionDisabled}>
|
||||
重新总结
|
||||
</Button>
|
||||
</span>
|
||||
</Tooltip>
|
||||
<Button icon={<DownloadOutlined />} onClick={downloadSummaryMd}>下载总结</Button>
|
||||
<Tooltip title="分享二维码">
|
||||
<Button icon={<QrcodeOutlined />} onClick={() => setShowQRModal(true)} />
|
||||
|
|
@ -574,16 +764,78 @@ const MeetingDetails = ({ user }) => {
|
|||
icon={<UserOutlined />}
|
||||
style={{ backgroundColor: getSpeakerColor(item.speaker_id), flexShrink: 0 }}
|
||||
/>
|
||||
{inlineSpeakerEdit === item.speaker_id && inlineSpeakerEditSegmentId === `speaker-${item.speaker_id}-${item.segment_id}` ? (
|
||||
<Space.Compact onClick={(e) => e.stopPropagation()}>
|
||||
<Input
|
||||
size="small"
|
||||
autoFocus
|
||||
value={inlineSpeakerValue}
|
||||
onChange={(e) => setInlineSpeakerValue(e.target.value)}
|
||||
onPressEnter={saveInlineSpeakerEdit}
|
||||
style={{ width: 180 }}
|
||||
/>
|
||||
<Button
|
||||
size="small"
|
||||
type="text"
|
||||
icon={<CheckOutlined />}
|
||||
loading={savingInlineEdit}
|
||||
onClick={saveInlineSpeakerEdit}
|
||||
/>
|
||||
<Button
|
||||
size="small"
|
||||
type="text"
|
||||
icon={<CloseOutlined />}
|
||||
disabled={savingInlineEdit}
|
||||
onClick={cancelInlineSpeakerEdit}
|
||||
/>
|
||||
</Space.Compact>
|
||||
) : (
|
||||
<Text
|
||||
strong
|
||||
style={{ color: '#1677ff', cursor: 'pointer', fontSize: 13 }}
|
||||
onClick={e => { e.stopPropagation(); openTranscriptEditDrawer(index); }}
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
startInlineSpeakerEdit(item.speaker_id, item.speaker_tag, item.segment_id);
|
||||
}}
|
||||
>
|
||||
{item.speaker_tag || `发言人 ${item.speaker_id}`}
|
||||
<EditOutlined style={{ fontSize: 11, marginLeft: 3 }} />
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
<Text style={{ fontSize: 14, lineHeight: 1.7, color: '#333' }}>{item.text_content}</Text>
|
||||
{inlineSegmentEditId === item.segment_id ? (
|
||||
<div onClick={(e) => e.stopPropagation()}>
|
||||
<Input.TextArea
|
||||
autoFocus
|
||||
autoSize={{ minRows: 2, maxRows: 6 }}
|
||||
value={inlineSegmentValue}
|
||||
onChange={(e) => setInlineSegmentValue(e.target.value)}
|
||||
onPressEnter={(e) => {
|
||||
if (e.ctrlKey || e.metaKey) {
|
||||
saveInlineSegmentEdit();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Space style={{ marginTop: 8 }}>
|
||||
<Button size="small" type="primary" icon={<CheckOutlined />} loading={savingInlineEdit} onClick={saveInlineSegmentEdit}>
|
||||
保存
|
||||
</Button>
|
||||
<Button size="small" icon={<CloseOutlined />} disabled={savingInlineEdit} onClick={cancelInlineSegmentEdit}>
|
||||
取消
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
) : (
|
||||
<Text
|
||||
style={{ fontSize: 14, lineHeight: 1.7, color: '#333', cursor: 'text' }}
|
||||
onDoubleClick={(e) => {
|
||||
e.stopPropagation();
|
||||
startInlineSegmentEdit(item);
|
||||
}}
|
||||
>
|
||||
{item.text_content}
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -125,7 +125,7 @@ const PromptConfigPage = ({ user }) => {
|
|||
items={[
|
||||
{
|
||||
key: 'config',
|
||||
label: '提示词配置',
|
||||
label: '系统提示词配置',
|
||||
children: (
|
||||
<div className="console-tab-panel">
|
||||
<div className="console-tab-toolbar">
|
||||
|
|
|
|||
Loading…
Reference in New Issue