diff --git a/src-tauri/.gitignore b/src-tauri/.gitignore index b21bd68..5467e21 100644 --- a/src-tauri/.gitignore +++ b/src-tauri/.gitignore @@ -1,6 +1,7 @@ # Generated by Cargo # will have compiled files and executables /target/ +/target*/ # Generated by Tauri # will have schema files for capabilities auto-completion diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index f8f094d..46b4c89 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -32,12 +32,19 @@ struct Delta { content: Option, } +#[derive(Serialize, Clone)] +struct TranslationChunkEvent { + request_id: String, + chunk: String, +} + #[tauri::command] async fn translate( app: AppHandle, api_address: String, api_key: String, payload: TranslationPayload, + request_id: String, ) -> Result { let client = Client::new(); // Ensure URL doesn't have double slashes if api_address ends with / @@ -55,6 +62,12 @@ async fn translate( .await .map_err(|e| e.to_string())?; + let status = res.status(); + if !status.is_success() { + let error_text = res.text().await.unwrap_or_else(|_| format!("HTTP {}", status)); + return Err(format!("HTTP {}: {}", status, error_text)); + } + if !payload.stream { let text = res.text().await.map_err(|e| e.to_string())?; return Ok(text); @@ -78,7 +91,11 @@ async fn translate( if let Some(choice) = json.choices.get(0) { if let Some(delta) = &choice.delta { if let Some(content) = &delta.content { - app.emit("translation-chunk", content).map_err(|e| e.to_string())?; + let event = TranslationChunkEvent { + request_id: request_id.clone(), + chunk: content.clone(), + }; + app.emit("translation-chunk", event).map_err(|e| e.to_string())?; } } } diff --git a/src/components/ConversationView.vue b/src/components/ConversationView.vue index de0a2d3..e5c5c15 100644 --- a/src/components/ConversationView.vue +++ b/src/components/ConversationView.vue @@ -20,6 +20,7 @@ import { useClipboard } from '../composables/useClipboard'; const settings = useSettingsStore(); const { activeCopyId, copyWithFeedback } = useClipboard(); +interface TranslationChunkEvent { request_id: string; chunk: string; } // UI States const searchQuery = ref(''); @@ -101,6 +102,7 @@ const myInput = ref(''); const partnerInput = ref(''); const isTranslating = ref(false); const currentStreamingMessageId = ref(null); +const activeStreamRequestId = ref(null); // Dropdowns const myToneDropdownOpen = ref(false); @@ -115,10 +117,14 @@ const handleCreateSession = () => { let unlisten: (() => void) | null = null; onMounted(async () => { - unlisten = await listen('translation-chunk', (event) => { - if (activeSession.value && currentStreamingMessageId.value) { + unlisten = await listen('translation-chunk', (event) => { + if ( + activeSession.value && + currentStreamingMessageId.value && + event.payload.request_id === activeStreamRequestId.value + ) { settings.updateChatMessage(activeSession.value.id, currentStreamingMessageId.value, { - translated: (activeSession.value.messages.find(m => m.id === currentStreamingMessageId.value)?.translated || '') + event.payload + translated: (activeSession.value.messages.find(m => m.id === currentStreamingMessageId.value)?.translated || '') + event.payload.chunk }); // 优化:只有当正在流式输出的消息是最后一条时,才自动滚动到底部 @@ -207,14 +213,17 @@ const translateMessage = async (sender: 'me' | 'partner', retranslateId?: string ], stream: settings.enableStreaming }; + const requestId = crypto.randomUUID(); settings.addLog('request', { type: retranslateId ? 'conversation-retranslate' : 'conversation', ...requestBody }, generateCurl(settings.apiBaseUrl, settings.apiKey, requestBody)); try { + if (settings.enableStreaming) activeStreamRequestId.value = requestId; const response = await invoke('translate', { apiAddress: settings.apiBaseUrl, apiKey: settings.apiKey, - payload: requestBody + payload: requestBody, + requestId }); if (!settings.enableStreaming) { @@ -232,6 +241,7 @@ const translateMessage = async (sender: 'me' | 'partner', retranslateId?: string } finally { isTranslating.value = false; currentStreamingMessageId.value = null; + activeStreamRequestId.value = null; // 只有新消息才滚动到底部 if (!retranslateId) { @@ -317,6 +327,7 @@ const evaluateMessage = async (messageId: string, force = false) => { ], stream: false }; + const requestId = crypto.randomUUID(); settings.addLog('request', { type: 'conversation-eval', ...requestBody }, generateCurl(evalApiBaseUrl, evalApiKey, requestBody)); @@ -324,7 +335,8 @@ const evaluateMessage = async (messageId: string, force = false) => { const response = await invoke('translate', { apiAddress: evalApiBaseUrl, apiKey: evalApiKey, - payload: requestBody + payload: requestBody, + requestId }); const fullResponseJson = JSON.parse(response); settings.addLog('response', fullResponseJson); @@ -358,6 +370,7 @@ const refineMessage = async (messageId: string) => { if (selectedSuggestions.length === 0) return; const suggestionsText = selectedSuggestions.map((s: any) => `- ${s.text}`).join('\n'); + const currentTranslation = msg.translated; isAuditModalOpen.value = false; // 关闭弹窗开始润色 settings.updateChatMessage(activeSession.value.id, messageId, { isRefining: true, translated: '' }); @@ -415,18 +428,21 @@ const refineMessage = async (messageId: string) => { model: refineModelName, messages: [ { role: "system", content: systemPrompt }, - { role: "user", content: `[Source Text]\n${msg.original}\n\n[Current Translation]\n${msg.translated}\n\n[Suggestions]\n${suggestionsText}` } + { role: "user", content: `[Source Text]\n${msg.original}\n\n[Current Translation]\n${currentTranslation}\n\n[Suggestions]\n${suggestionsText}` } ], stream: settings.enableStreaming }; + const requestId = crypto.randomUUID(); settings.addLog('request', { type: 'conversation-refine', ...requestBody }, generateCurl(refineApiBaseUrl, refineApiKey, requestBody)); try { + if (settings.enableStreaming) activeStreamRequestId.value = requestId; const response = await invoke('translate', { apiAddress: refineApiBaseUrl, apiKey: refineApiKey, - payload: requestBody + payload: requestBody, + requestId }); if (!settings.enableStreaming) { @@ -442,6 +458,7 @@ const refineMessage = async (messageId: string) => { } finally { settings.updateChatMessage(activeSession.value.id, messageId, { isRefining: false, evaluation: undefined }); currentStreamingMessageId.value = null; + activeStreamRequestId.value = null; // 只有当润色的是最后一条消息时才滚动到底部 const lastMsg = activeSession.value.messages[activeSession.value.messages.length - 1]; diff --git a/src/components/TranslationView.vue b/src/components/TranslationView.vue index 67f1bf7..fbbe7d9 100644 --- a/src/components/TranslationView.vue +++ b/src/components/TranslationView.vue @@ -53,12 +53,14 @@ const currentHistoryId = ref(null); interface Suggestion { id: number; text: string; importance: number; } interface EvaluationResult { score: number; analysis: string; suggestions?: Suggestion[]; } +interface TranslationChunkEvent { request_id: string; chunk: string; } const evaluationResult = ref(null); const isEvaluating = ref(false); const isRefining = ref(false); const selectedSuggestionIds = ref([]); const appliedSuggestionIds = ref([]); +const activeStreamRequestId = ref(null); const toggleSuggestion = (id: number) => { if (!selectedSuggestionIds.value) selectedSuggestionIds.value = []; @@ -69,9 +71,9 @@ const toggleSuggestion = (id: number) => { let unlisten: (() => void) | null = null; onMounted(async () => { - unlisten = await listen('translation-chunk', (event) => { - if (isTranslating.value || isRefining.value) { - targetText.value += event.payload; + unlisten = await listen('translation-chunk', (event) => { + if ((isTranslating.value || isRefining.value) && event.payload.request_id === activeStreamRequestId.value) { + targetText.value += event.payload.chunk; } }); }); @@ -148,11 +150,12 @@ const evaluateTranslation = async () => { messages: [ { role: "system", content: evaluationSystemPrompt }, { role: "user", content: evaluationUserPrompt } ], stream: false }; + const requestId = crypto.randomUUID(); settings.addLog('request', { type: 'evaluation', ...requestBody }, generateCurl(apiBaseUrl, apiKey, requestBody)); try { - const response = await invoke('translate', { apiAddress: apiBaseUrl, apiKey: apiKey, payload: requestBody }); + const response = await invoke('translate', { apiAddress: apiBaseUrl, apiKey: apiKey, payload: requestBody, requestId }); try { // 解析 API 的原始响应 JSON const fullResponseJson = JSON.parse(response); @@ -209,11 +212,13 @@ const refineTranslation = async () => { messages: [ { role: "system", content: refinementSystemPrompt }, { role: "user", content: refinementUserPrompt } ], stream: settings.enableStreaming }; + const requestId = crypto.randomUUID(); settings.addLog('request', { type: 'refinement', ...requestBody }, generateCurl(apiBaseUrl, apiKey, requestBody)); try { - const response = await invoke('translate', { apiAddress: apiBaseUrl, apiKey: apiKey, payload: requestBody }); + if (settings.enableStreaming) activeStreamRequestId.value = requestId; + const response = await invoke('translate', { apiAddress: apiBaseUrl, apiKey: apiKey, payload: requestBody, requestId }); if (settings.enableStreaming) { settings.addLog('response', targetText.value); @@ -239,6 +244,7 @@ const refineTranslation = async () => { targetText.value = `Error: ${errorMsg}`; } finally { isRefining.value = false; + activeStreamRequestId.value = null; } }; @@ -267,11 +273,13 @@ const translate = async () => { messages: [ { role: "system", content: systemMessage }, { role: "user", content: userMessage } ], stream: settings.enableStreaming }; + const requestId = crypto.randomUUID(); settings.addLog('request', requestBody, generateCurl(settings.apiBaseUrl, settings.apiKey, requestBody)); try { - const response = await invoke('translate', { apiAddress: settings.apiBaseUrl, apiKey: settings.apiKey, payload: requestBody }); + if (settings.enableStreaming) activeStreamRequestId.value = requestId; + const response = await invoke('translate', { apiAddress: settings.apiBaseUrl, apiKey: settings.apiKey, payload: requestBody, requestId }); let finalTargetText = ''; if (settings.enableStreaming) { @@ -300,6 +308,7 @@ const translate = async () => { targetText.value = `Error: ${errorMsg}`; } finally { isTranslating.value = false; + activeStreamRequestId.value = null; } if (settings.enableEvaluation) await evaluateTranslation(); @@ -656,4 +665,4 @@ const translate = async () => { - \ No newline at end of file +