x修复前端无法自动总结,修复点击说话人修改问题,新增双击编辑文本内容问题

codex/dev
AlanPaine 2026-04-02 11:07:41 +00:00
parent a3ae293d42
commit cc1817078a
5 changed files with 329 additions and 44 deletions

View File

@ -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) meeting_data.transcription_status = TranscriptionTaskStatus(**transcription_status_data)
except Exception as e: except Exception as e:
print(f"Warning: Failed to get transcription status for meeting {meeting_id}: {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) return create_api_response(code="200", message="获取会议详情成功", data=meeting_data)
@router.get("/meetings/{meeting_id}/transcript") @router.get("/meetings/{meeting_id}/transcript")
@ -543,14 +549,21 @@ async def upload_audio(
model_code = model_code.strip() if model_code else None model_code = model_code.strip() if model_code else None
# 0. 如果没有传入 prompt_id尝试获取默认模版ID # 0. 如果没有传入 prompt_id优先使用会议已配置模版,否则回退默认模版
if prompt_id is None: if prompt_id is None:
with get_db_connection() as connection: with get_db_connection() as connection:
cursor = connection.cursor() cursor = connection.cursor(dictionary=True)
cursor.execute( cursor.execute("SELECT prompt_id FROM meetings WHERE meeting_id = %s", (meeting_id,))
"SELECT id FROM prompts WHERE task_type = 'MEETING_TASK' AND is_default = TRUE AND is_active = TRUE LIMIT 1" meeting_row = cursor.fetchone()
) if meeting_row and meeting_row.get('prompt_id') and int(meeting_row['prompt_id']) > 0:
prompt_id = cursor.fetchone()[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_row = cursor.fetchone()
prompt_id = prompt_row[0] if prompt_row else None
# 1. 文件类型验证 # 1. 文件类型验证
file_extension = os.path.splitext(audio_file.filename)[1].lower() 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)}") return create_api_response(code="500", message=f"Failed to get meeting transcription status: {str(e)}")
@router.post("/meetings/{meeting_id}/transcription/start") @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: try:
with get_db_connection() as connection: with get_db_connection() as connection:
cursor = connection.cursor(dictionary=True) cursor = connection.cursor(dictionary=True)
cursor.execute("SELECT meeting_id FROM meetings WHERE meeting_id = %s", (meeting_id,)) cursor.execute("SELECT meeting_id, prompt_id FROM meetings WHERE meeting_id = %s", (meeting_id,))
if not cursor.fetchone(): meeting = cursor.fetchone()
if not meeting:
return create_api_response(code="404", message="Meeting not found") 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,)) cursor.execute("SELECT file_path FROM audio_files WHERE meeting_id = %s LIMIT 1", (meeting_id,))
audio_file = cursor.fetchone() 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": existing_status['task_id'], "status": existing_status['status']
}) })
task_id = transcription_service.start_transcription(meeting_id, audio_file['file_path']) 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={ return create_api_response(code="200", message="Transcription task started successfully", data={
"task_id": task_id, "meeting_id": meeting_id "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,)) cursor.execute("SELECT meeting_id FROM meetings WHERE meeting_id = %s", (meeting_id,))
if not cursor.fetchone(): if not cursor.fetchone():
return create_api_response(code="404", message="Meeting not found") 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 参数给服务层 # 传递 prompt_id 和 model_code 参数给服务层
task_id = async_meeting_service.start_summary_generation(meeting_id, request.user_prompt, request.prompt_id, request.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) background_tasks.add_task(async_meeting_service._process_task, task_id)

View File

@ -84,6 +84,7 @@ class Meeting(BaseModel):
audio_duration: Optional[float] = None audio_duration: Optional[float] = None
summary: Optional[str] = None summary: Optional[str] = None
transcription_status: Optional[TranscriptionTaskStatus] = None transcription_status: Optional[TranscriptionTaskStatus] = None
llm_status: Optional[TranscriptionTaskStatus] = None
prompt_id: Optional[int] = None prompt_id: Optional[int] = None
prompt_name: Optional[str] = None prompt_name: Optional[str] = None
overall_status: Optional[str] = None overall_status: Optional[str] = None
@ -125,7 +126,7 @@ class BatchSpeakerTagUpdateRequest(BaseModel):
class TranscriptUpdateRequest(BaseModel): class TranscriptUpdateRequest(BaseModel):
segment_id: int segment_id: int
new_text: str text_content: str
class BatchTranscriptUpdateRequest(BaseModel): class BatchTranscriptUpdateRequest(BaseModel):
updates: List[TranscriptUpdateRequest] updates: List[TranscriptUpdateRequest]

View File

@ -407,6 +407,7 @@ class AsyncMeetingService:
'meeting_id': int(task_data.get('meeting_id', 0)), 'meeting_id': int(task_data.get('meeting_id', 0)),
'created_at': task_data.get('created_at'), 'created_at': task_data.get('created_at'),
'updated_at': task_data.get('updated_at'), 'updated_at': task_data.get('updated_at'),
'message': task_data.get('message'),
'result': task_data.get('result'), 'result': task_data.get('result'),
'error_message': task_data.get('error_message') 'error_message': task_data.get('error_message')
} }

View File

@ -13,7 +13,7 @@ import {
EyeOutlined, FileTextOutlined, PartitionOutlined, EyeOutlined, FileTextOutlined, PartitionOutlined,
SaveOutlined, CloseOutlined, SaveOutlined, CloseOutlined,
StarFilled, RobotOutlined, DownloadOutlined, StarFilled, RobotOutlined, DownloadOutlined,
DownOutlined, DownOutlined, CheckOutlined,
MoreOutlined, AudioOutlined MoreOutlined, AudioOutlined
} from '@ant-design/icons'; } from '@ant-design/icons';
import MarkdownRenderer from '../components/MarkdownRenderer'; import MarkdownRenderer from '../components/MarkdownRenderer';
@ -65,6 +65,7 @@ const MeetingDetails = ({ user }) => {
const [summaryTaskProgress, setSummaryTaskProgress] = useState(0); const [summaryTaskProgress, setSummaryTaskProgress] = useState(0);
const [summaryTaskMessage, setSummaryTaskMessage] = useState(''); const [summaryTaskMessage, setSummaryTaskMessage] = useState('');
const [summaryPollInterval, setSummaryPollInterval] = useState(null); const [summaryPollInterval, setSummaryPollInterval] = useState(null);
const [activeSummaryTaskId, setActiveSummaryTaskId] = useState(null);
const [llmModels, setLlmModels] = useState([]); const [llmModels, setLlmModels] = useState([]);
const [selectedModelCode, setSelectedModelCode] = useState(null); const [selectedModelCode, setSelectedModelCode] = useState(null);
@ -87,10 +88,26 @@ const MeetingDetails = ({ user }) => {
// //
const [isEditingSummary, setIsEditingSummary] = useState(false); const [isEditingSummary, setIsEditingSummary] = useState(false);
const [editingSummaryContent, setEditingSummaryContent] = useState(''); 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 audioRef = useRef(null);
const transcriptRefs = useRef([]); const transcriptRefs = useRef([]);
const isMeetingOwner = user?.user_id === meeting?.creator_id; 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); setLoading(true);
const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.MEETINGS.DETAIL(meeting_id))); const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.MEETINGS.DETAIL(meeting_id)));
setMeeting(response.data); setMeeting(response.data);
if (response.data.prompt_id) {
setSelectedPromptId(response.data.prompt_id);
}
setAccessPasswordEnabled(Boolean(response.data.access_password)); setAccessPasswordEnabled(Boolean(response.data.access_password));
setAccessPasswordDraft(response.data.access_password || ''); setAccessPasswordDraft(response.data.access_password || '');
@ -119,6 +139,17 @@ const MeetingDetails = ({ user }) => {
if (['pending', 'processing'].includes(ts.status)) { if (['pending', 'processing'].includes(ts.status)) {
startStatusPolling(ts.task_id); 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 { try {
@ -127,6 +158,7 @@ const MeetingDetails = ({ user }) => {
} catch { setAudioUrl(null); } } catch { setAudioUrl(null); }
fetchTranscript(); fetchTranscript();
fetchSummaryHistory();
} catch { } catch {
message.error('加载会议详情失败'); message.error('加载会议详情失败');
} finally { } finally {
@ -160,7 +192,13 @@ const MeetingDetails = ({ user }) => {
setTranscriptionProgress(status.progress || 0); setTranscriptionProgress(status.progress || 0);
if (['completed', 'failed', 'error', 'cancelled'].includes(status.status)) { if (['completed', 'failed', 'error', 'cancelled'].includes(status.status)) {
clearInterval(interval); clearInterval(interval);
if (status.status === 'completed') fetchTranscript(); if (status.status === 'completed') {
fetchTranscript();
fetchMeetingDetails();
setTimeout(() => {
fetchSummaryHistory();
}, 1000);
}
} }
} catch { clearInterval(interval); } } catch { clearInterval(interval); }
}, 3000); }, 3000);
@ -186,10 +224,66 @@ const MeetingDetails = ({ user }) => {
} catch {} } 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 () => { const fetchSummaryHistory = async () => {
try { try {
const res = await apiClient.get(buildApiUrl(`/api/meetings/${meeting_id}/llm-tasks`)); 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 {} } catch {}
}; };
@ -217,6 +311,12 @@ const MeetingDetails = ({ user }) => {
formData.append('audio_file', file); formData.append('audio_file', file);
formData.append('meeting_id', meeting_id); formData.append('meeting_id', meeting_id);
formData.append('force_replace', 'true'); 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); setIsUploading(true);
try { try {
await apiClient.post(buildApiUrl(API_ENDPOINTS.MEETINGS.UPLOAD_AUDIO), formData); await apiClient.post(buildApiUrl(API_ENDPOINTS.MEETINGS.UPLOAD_AUDIO), formData);
@ -264,6 +364,81 @@ const MeetingDetails = ({ user }) => {
document.getElementById('audio-upload-input')?.click(); 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) => { const changePlaybackRate = (nextRate) => {
setPlaybackRate(nextRate); setPlaybackRate(nextRate);
if (audioRef.current) { if (audioRef.current) {
@ -298,6 +473,18 @@ const MeetingDetails = ({ user }) => {
}; };
const generateSummary = async () => { const generateSummary = async () => {
if (isUploading) {
message.warning('音频上传中,暂不允许重新总结');
return;
}
if (!hasUploadedAudio) {
message.warning('请先上传音频后再总结');
return;
}
if (isTranscriptionRunning) {
message.warning('转录进行中,暂不允许重新总结');
return;
}
setSummaryLoading(true); setSummaryLoading(true);
setSummaryTaskProgress(0); setSummaryTaskProgress(0);
try { try {
@ -306,29 +493,26 @@ const MeetingDetails = ({ user }) => {
prompt_id: selectedPromptId, prompt_id: selectedPromptId,
model_code: selectedModelCode model_code: selectedModelCode
}); });
const taskId = res.data.task_id; startSummaryPolling(res.data.task_id, { closeDrawerOnComplete: true });
const interval = setInterval(async () => { } catch (error) {
const statusRes = await apiClient.get(buildApiUrl(API_ENDPOINTS.TASKS.SUMMARY_STATUS(taskId))); message.error(error?.response?.data?.message || '生成总结失败');
const s = statusRes.data; setSummaryLoading(false);
setSummaryTaskProgress(s.progress || 0); }
setSummaryTaskMessage(s.message);
if (s.status === 'completed') {
clearInterval(interval);
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 = () => { const openSummaryDrawer = () => {
if (isUploading) {
message.warning('音频上传中,暂不允许重新总结');
return;
}
if (!hasUploadedAudio) {
message.warning('请先上传音频后再总结');
return;
}
if (isTranscriptionRunning) {
message.warning('转录进行中,完成后会自动总结');
return;
}
setShowSummaryDrawer(true); setShowSummaryDrawer(true);
fetchSummaryHistory(); fetchSummaryHistory();
}; };
@ -444,7 +628,13 @@ const MeetingDetails = ({ user }) => {
</div> </div>
<Space style={{ flexShrink: 0 }}> <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> <Button icon={<DownloadOutlined />} onClick={downloadSummaryMd}>下载总结</Button>
<Tooltip title="分享二维码"> <Tooltip title="分享二维码">
<Button icon={<QrcodeOutlined />} onClick={() => setShowQRModal(true)} /> <Button icon={<QrcodeOutlined />} onClick={() => setShowQRModal(true)} />
@ -574,16 +764,78 @@ const MeetingDetails = ({ user }) => {
icon={<UserOutlined />} icon={<UserOutlined />}
style={{ backgroundColor: getSpeakerColor(item.speaker_id), flexShrink: 0 }} style={{ backgroundColor: getSpeakerColor(item.speaker_id), flexShrink: 0 }}
/> />
<Text {inlineSpeakerEdit === item.speaker_id && inlineSpeakerEditSegmentId === `speaker-${item.speaker_id}-${item.segment_id}` ? (
strong <Space.Compact onClick={(e) => e.stopPropagation()}>
style={{ color: '#1677ff', cursor: 'pointer', fontSize: 13 }} <Input
onClick={e => { e.stopPropagation(); openTranscriptEditDrawer(index); }} size="small"
> autoFocus
{item.speaker_tag || `发言人 ${item.speaker_id}`} value={inlineSpeakerValue}
<EditOutlined style={{ fontSize: 11, marginLeft: 3 }} /> onChange={(e) => setInlineSpeakerValue(e.target.value)}
</Text> 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();
startInlineSpeakerEdit(item.speaker_id, item.speaker_tag, item.segment_id);
}}
>
{item.speaker_tag || `发言人 ${item.speaker_id}`}
<EditOutlined style={{ fontSize: 11, marginLeft: 3 }} />
</Text>
)}
</div> </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> </div>
), ),
}; };

View File

@ -125,7 +125,7 @@ const PromptConfigPage = ({ user }) => {
items={[ items={[
{ {
key: 'config', key: 'config',
label: '提示词配置', label: '系统提示词配置',
children: ( children: (
<div className="console-tab-panel"> <div className="console-tab-panel">
<div className="console-tab-toolbar"> <div className="console-tab-toolbar">