This commit is contained in:
2026-04-20 12:40:24 -04:00
parent 2e7789df63
commit fbd1728aee
4 changed files with 59 additions and 15 deletions

View File

@@ -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

View File

@@ -32,12 +32,19 @@ struct Delta {
content: Option<String>,
}
#[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<String, String> {
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())?;
}
}
}

View File

@@ -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<string | null>(null);
const activeStreamRequestId = ref<string | null>(null);
// Dropdowns
const myToneDropdownOpen = ref(false);
@@ -115,10 +117,14 @@ const handleCreateSession = () => {
let unlisten: (() => void) | null = null;
onMounted(async () => {
unlisten = await listen<string>('translation-chunk', (event) => {
if (activeSession.value && currentStreamingMessageId.value) {
unlisten = await listen<TranslationChunkEvent>('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<string>('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<string>('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<string>('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];

View File

@@ -53,12 +53,14 @@ const currentHistoryId = ref<string | null>(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<EvaluationResult | null>(null);
const isEvaluating = ref(false);
const isRefining = ref(false);
const selectedSuggestionIds = ref<number[]>([]);
const appliedSuggestionIds = ref<number[]>([]);
const activeStreamRequestId = ref<string | null>(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<string>('translation-chunk', (event) => {
if (isTranslating.value || isRefining.value) {
targetText.value += event.payload;
unlisten = await listen<TranslationChunkEvent>('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<string>('translate', { apiAddress: apiBaseUrl, apiKey: apiKey, payload: requestBody });
const response = await invoke<string>('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<string>('translate', { apiAddress: apiBaseUrl, apiKey: apiKey, payload: requestBody });
if (settings.enableStreaming) activeStreamRequestId.value = requestId;
const response = await invoke<string>('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<string>('translate', { apiAddress: settings.apiBaseUrl, apiKey: settings.apiKey, payload: requestBody });
if (settings.enableStreaming) activeStreamRequestId.value = requestId;
const response = await invoke<string>('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 () => {
</div>
</div>
</div>
</template>
</template>