import { apiFetch } from '../api.js'; import { DEFAULT_REASONING_EFFORT, createClientTurnId, createDraftSession, isDraftSession, titleFromFirstMessage, upsertSessionInProject } from '../app-core-utils.js'; import { upsertStatusMessage } from '../app-message-state.js'; export function useChatActions(app, runRegistry, turnPolling, rememberRelayOperationLock) { function restoreVoiceTextToInput(text) { const value = String(text || '').trim(); if (!value) { return; } app.setInput((current) => { const base = String(current || '').trimEnd(); if (!base) { return value; } if (base.includes(value)) { return current; } return `${base}\n${value}`; }); } async function submitCodexMessage({ message, attachmentsForTurn = [], clearComposer = false, restoreTextOnError = false }) { const project = app.selectedProject || app.selectedProjectRef.current; const selectedAttachments = Array.isArray(attachmentsForTurn) ? attachmentsForTurn : []; const displayMessage = String(message || '').trim() || (selectedAttachments.length ? '请查看附件。' : ''); if ((!displayMessage && !selectedAttachments.length) || !project) { if (restoreTextOnError && displayMessage) { restoreVoiceTextToInput(displayMessage); } throw new Error(project ? 'message or attachments are required' : '请先选择项目'); } let sessionForTurn = app.selectedSession; if (!sessionForTurn) { sessionForTurn = createDraftSession(project); app.setSelectedSession(sessionForTurn); app.setExpandedProjectIds((current) => ({ ...current, [project.id]: true })); app.setSessionsByProject((current) => upsertSessionInProject(current, project.id, sessionForTurn)); } const turnId = createClientTurnId(); const draftSessionId = isDraftSession(sessionForTurn) ? sessionForTurn.id : null; const outgoingSessionId = draftSessionId ? null : sessionForTurn?.id || null; const optimisticSessionId = draftSessionId || outgoingSessionId || turnId; const initialTitle = draftSessionId && !sessionForTurn.titleLocked ? titleFromFirstMessage(displayMessage) : null; if (clearComposer) { app.setInput(''); app.setAttachments([]); } runRegistry.markRun({ turnId, sessionId: optimisticSessionId, previousSessionId: draftSessionId || outgoingSessionId }); app.setSelectedSession((current) => current?.id === sessionForTurn?.id ? { ...current, turnId, ...(initialTitle ? { title: initialTitle, titleLocked: true } : {}) } : current ); if (initialTitle) { app.setSessionsByProject((current) => ({ ...current, [project.id]: (current[project.id] || []).map((item) => item.id === sessionForTurn.id ? { ...item, title: initialTitle, titleLocked: true } : item ) })); } app.setMessages((current) => upsertStatusMessage( [ ...current, { id: `local-${Date.now()}`, role: 'user', content: displayMessage, timestamp: new Date().toISOString(), sessionId: optimisticSessionId, turnId } ], { sessionId: optimisticSessionId, turnId, kind: 'reasoning', status: 'running', label: '正在思考中', timestamp: new Date().toISOString() } ) ); try { const result = await apiFetch('/api/chat/send', { method: 'POST', body: { projectId: project.id, sessionId: outgoingSessionId, draftSessionId, clientTurnId: turnId, message: displayMessage, permissionMode: app.permissionMode, model: app.selectedModel || app.status.model, reasoningEffort: app.selectedReasoningEffort || app.status.reasoningEffort || DEFAULT_REASONING_EFFORT, attachments: selectedAttachments } }); turnPolling.pollTurnUntilComplete({ turnId: result.turnId || turnId, optimisticSessionId, projectId: project.id, previousSessionId: draftSessionId || outgoingSessionId }); return { turnId: result.turnId || turnId, optimisticSessionId, projectId: project.id }; } catch (error) { rememberRelayOperationLock('send', error); runRegistry.clearRun({ turnId, sessionId: optimisticSessionId, previousSessionId: draftSessionId || outgoingSessionId }); if (clearComposer) { app.setAttachments(selectedAttachments); } if (restoreTextOnError) { restoreVoiceTextToInput(displayMessage); } app.setMessages((current) => upsertStatusMessage(current, { sessionId: optimisticSessionId, turnId, kind: 'turn', status: 'failed', label: '发送失败', detail: error.message, timestamp: new Date().toISOString() }) ); throw error; } } const handleSubmit = async () => { const message = app.input.trim(); if ((!message && !app.attachments.length) || !app.selectedProject) { return; } try { await submitCodexMessage({ message, attachmentsForTurn: app.attachments, clearComposer: true, restoreTextOnError: true }); } catch { // submitCodexMessage already reflects the failure in the chat UI. } }; async function handleVoiceSubmit(transcript) { const message = String(transcript || '').trim(); if (!message) { throw new Error('没有识别到文字'); } return submitCodexMessage({ message, attachmentsForTurn: [], restoreTextOnError: true }); } async function handleAbort() { const abortId = app.selectedSessionRef.current?.id || app.selectedSessionRef.current?.turnId || Object.keys(app.runningById)[0]; if (!abortId) { return; } await apiFetch('/api/chat/abort', { method: 'POST', body: { sessionId: abortId, turnId: app.selectedSessionRef.current?.turnId || null } }).catch(() => null); runRegistry.clearRun({ sessionId: abortId, turnId: app.selectedSessionRef.current?.turnId || null }); } return { submitCodexMessage, handleSubmit, handleVoiceSubmit, handleAbort }; }