diff --git a/src/components/ConversationView.vue b/src/components/ConversationView.vue index 04b03f7..f4a0011 100644 --- a/src/components/ConversationView.vue +++ b/src/components/ConversationView.vue @@ -18,6 +18,12 @@ import { useLogsStore } from '../stores/logs'; import { cn } from '../lib/utils'; import { listen } from '@tauri-apps/api/event'; import { useClipboard } from '../composables/useClipboard'; +import { + buildConversationEvaluationUserPrompt, + buildConversationRefinementUserPrompt, + buildConversationSystemPrompt, + buildConversationTranslationUserPrompt, +} from '../lib/prompt-builders'; import { executeTranslationRequest, extractAssistantContent, @@ -196,24 +202,21 @@ const translateMessage = async (sender: 'me' | 'partner', retranslateId?: string : 'Auto-detect'; const senderName = sender === 'me' ? activeSession.value.me.name : activeSession.value.partner.name; - const systemPrompt = settings.chatSystemPromptTemplate - .replace(/{ME_NAME}/g, activeSession.value.me.name) - .replace(/{ME_GENDER}/g, activeSession.value.me.gender) - .replace(/{ME_LANG}/g, activeSession.value.me.language.englishName) - .replace(/{PART_NAME}/g, activeSession.value.partner.name) - .replace(/{PART_GENDER}/g, activeSession.value.partner.gender) - .replace(/{PART_LANG}/g, activeSession.value.partner.language.englishName) - .replace(/{HISTORY_BLOCK}/g, historyBlock || 'None (This is the start of conversation)') - .replace(/{SENDER_NAME}/g, senderName) - .replace(/{FROM_LANG}/g, fromLang.englishName) - .replace(/{TO_LANG}/g, toLang.englishName) - .replace(/{TARGET_TONE}/g, targetTone); + const systemPrompt = buildConversationSystemPrompt(settings.chatSystemPromptTemplate, { + me: activeSession.value.me, + partner: activeSession.value.partner, + historyBlock, + senderName, + fromLang, + toLang, + targetTone, + }, 'None (This is the start of conversation)'); const requestBody: TranslationPayload = { model: settings.modelName, messages: [ { role: "system", content: systemPrompt }, - { role: "user", content: `[Text to Translate]\n${text}` } + { role: "user", content: buildConversationTranslationUserPrompt(text) } ], stream: settings.enableStreaming }; @@ -285,22 +288,17 @@ const evaluateMessage = async (messageId: string, force = false) => { ? (TONE_REGISTER_OPTIONS.find(o => o.value === activeSession.value!.me.tone)?.value || 'Polite & Conversational') : 'Auto-detect'; - const systemPrompt = settings.chatEvaluationPromptTemplate - .replace(/{ME_NAME}/g, activeSession.value.me.name) - .replace(/{ME_GENDER}/g, activeSession.value.me.gender) - .replace(/{ME_LANG}/g, activeSession.value.me.language.englishName) - .replace(/{PART_NAME}/g, activeSession.value.partner.name) - .replace(/{PART_GENDER}/g, activeSession.value.partner.gender) - .replace(/{PART_LANG}/g, activeSession.value.partner.language.englishName) - .replace(/{HISTORY_BLOCK}/g, historyBlock || 'None') - .replace(/{TARGET_TONE}/g, targetTone) - .replace(/{SENDER_NAME}/g, senderName) - .replace(/{FROM_LANG}/g, fromLang.englishName) - .replace(/{TO_LANG}/g, toLang.englishName); - // .replace(/{ORIGINAL_TEXT}/g, msg.original) - // .replace(/{CURRENT_TRANSLATION}/g, msg.translated); + const systemPrompt = buildConversationSystemPrompt(settings.chatEvaluationPromptTemplate, { + me: activeSession.value.me, + partner: activeSession.value.partner, + historyBlock, + senderName, + fromLang, + toLang, + targetTone, + }, 'None'); - const userPrompt = `[Source Text]\n${msg.original}\n\n[Current Translation]\n${msg.translated}`; + const userPrompt = buildConversationEvaluationUserPrompt(msg.original, msg.translated); const modelConfig = resolveModelConfig({ apiBaseUrl: settings.apiBaseUrl, @@ -350,7 +348,6 @@ const refineMessage = async (messageId: string) => { const selectedSuggestions = evalData.suggestions.filter((s: any) => selectedSuggestionIds.value.includes(s.id)); if (selectedSuggestions.length === 0) return; - const suggestionsText = selectedSuggestions.map((s: any) => `- ${s.text}`).join('\n'); const currentTranslation = msg.translated; isAuditModalOpen.value = false; // 关闭弹窗开始润色 @@ -375,21 +372,15 @@ const refineMessage = async (messageId: string) => { const toLang = msg.sender === 'me' ? activeSession.value.partner.language : activeSession.value.me.language; const senderName = msg.sender === 'me' ? activeSession.value.me.name : activeSession.value.partner.name; - const systemPrompt = settings.chatRefinementPromptTemplate - .replace(/{ME_NAME}/g, activeSession.value.me.name) - .replace(/{ME_GENDER}/g, activeSession.value.me.gender) - .replace(/{ME_LANG}/g, activeSession.value.me.language.englishName) - .replace(/{PART_NAME}/g, activeSession.value.partner.name) - .replace(/{PART_GENDER}/g, activeSession.value.partner.gender) - .replace(/{PART_LANG}/g, activeSession.value.partner.language.englishName) - .replace(/{HISTORY_BLOCK}/g, historyBlock || 'None') - // .replace(/{ORIGINAL_TEXT}/g, msg.original) - // .replace(/{CURRENT_TRANSLATION}/g, msg.translated) - // .replace(/{SUGGESTIONS}/g, suggestionsText) - .replace(/{TARGET_TONE}/g, targetTone) - .replace(/{SENDER_NAME}/g, senderName) - .replace(/{FROM_LANG}/g, fromLang.englishName) - .replace(/{TO_LANG}/g, toLang.englishName); + const systemPrompt = buildConversationSystemPrompt(settings.chatRefinementPromptTemplate, { + me: activeSession.value.me, + partner: activeSession.value.partner, + historyBlock, + senderName, + fromLang, + toLang, + targetTone, + }, 'None'); const modelConfig = resolveModelConfig({ apiBaseUrl: settings.apiBaseUrl, @@ -401,7 +392,7 @@ const refineMessage = async (messageId: string) => { model: modelConfig.modelName, messages: [ { role: "system", content: systemPrompt }, - { role: "user", content: `[Source Text]\n${msg.original}\n\n[Current Translation]\n${currentTranslation}\n\n[Suggestions]\n${suggestionsText}` } + { role: "user", content: buildConversationRefinementUserPrompt(msg.original, currentTranslation, selectedSuggestions.map((s: any) => s.text)) } ], stream: settings.enableStreaming }; diff --git a/src/components/TranslationView.vue b/src/components/TranslationView.vue index 830c57c..d3695f8 100644 --- a/src/components/TranslationView.vue +++ b/src/components/TranslationView.vue @@ -9,6 +9,14 @@ import { useLogsStore } from '../stores/logs'; import { useTranslationWorkspaceStore } from '../stores/translation-workspace'; import { cn } from '../lib/utils'; import { useClipboard } from '../composables/useClipboard'; +import { + buildSingleEvaluationSystemPrompt, + buildSingleEvaluationUserPrompt, + buildSingleRefinementSystemPrompt, + buildSingleRefinementUserPrompt, + buildSingleTranslationSystemPrompt, + buildSingleTranslationUserPrompt, +} from '../lib/prompt-builders'; import { executeTranslationRequest, extractAssistantContent, @@ -123,14 +131,15 @@ const evaluateTranslation = async () => { modelName: settings.modelName, }, settings.profiles, settings.evaluationProfileId); - const evaluationSystemPrompt = settings.evaluationPromptTemplate - .replace(/{SOURCE_LANG}/g, sourceLang.value.englishName) - .replace(/{TARGET_LANG}/g, targetLang.value.englishName) - .replace(/{SPEAKER_IDENTITY}/g, settings.speakerIdentity) - .replace(/{TONE_REGISTER}/g, settings.toneRegister) - .replace(/{CONTEXT}/g, context.value || 'None'); + const evaluationSystemPrompt = buildSingleEvaluationSystemPrompt(settings.evaluationPromptTemplate, { + sourceLang: sourceLang.value, + targetLang: targetLang.value, + speakerIdentity: settings.speakerIdentity, + toneRegister: settings.toneRegister, + context: context.value, + }); - const evaluationUserPrompt = `[Source Text]\n${sourceText.value}\n\n[Translated Text]\n${targetText.value}`; + const evaluationUserPrompt = buildSingleEvaluationUserPrompt(sourceText.value, targetText.value); const requestBody: TranslationPayload = { model: modelConfig.modelName, @@ -173,15 +182,15 @@ const refineTranslation = async () => { modelName: settings.modelName, }, settings.profiles, settings.evaluationProfileId); - const refinementSystemPrompt = settings.refinementPromptTemplate - .replace(/{SOURCE_LANG}/g, sourceLang.value.englishName) - .replace(/{TARGET_LANG}/g, targetLang.value.englishName) - .replace(/{SPEAKER_IDENTITY}/g, settings.speakerIdentity) - .replace(/{TONE_REGISTER}/g, settings.toneRegister) - .replace(/{CONTEXT}/g, context.value || 'None'); + const refinementSystemPrompt = buildSingleRefinementSystemPrompt(settings.refinementPromptTemplate, { + sourceLang: sourceLang.value, + targetLang: targetLang.value, + speakerIdentity: settings.speakerIdentity, + toneRegister: settings.toneRegister, + context: context.value, + }); - const formattedSuggestions = selectedTexts.map((text, i) => `${i + 1}. ${text}`).join('\n'); - const refinementUserPrompt = `[Source Text]\n${sourceText.value}\n\n[Current Translation]\n${originalTranslation}\n\n[User Feedback]\n${formattedSuggestions}`; + const refinementUserPrompt = buildSingleRefinementUserPrompt(sourceText.value, originalTranslation, selectedTexts); const requestBody: TranslationPayload = { model: modelConfig.modelName, @@ -229,17 +238,14 @@ const translate = async () => { targetText.value = ''; evaluationResult.value = null; - const systemMessage = settings.systemPromptTemplate - .replace(/{SOURCE_LANG}/g, sourceLang.value.englishName) - .replace(/{SOURCE_CODE}/g, sourceLang.value.code) - .replace(/{TARGET_LANG}/g, targetLang.value.englishName) - .replace(/{TARGET_CODE}/g, targetLang.value.code) - .replace(/{SPEAKER_IDENTITY}/g, settings.speakerIdentity) - .replace(/{TONE_REGISTER}/g, settings.toneRegister); + const systemMessage = buildSingleTranslationSystemPrompt(settings.systemPromptTemplate, { + sourceLang: sourceLang.value, + targetLang: targetLang.value, + speakerIdentity: settings.speakerIdentity, + toneRegister: settings.toneRegister, + }); - const userMessage = context.value - ? `[Context]\n${context.value}\n\n[Text to Translate]\n${sourceText.value}` - : `[Text to Translate]\n${sourceText.value}`; + const userMessage = buildSingleTranslationUserPrompt(sourceText.value, context.value); const requestBody: TranslationPayload = { model: settings.modelName, diff --git a/src/lib/prompt-builders.ts b/src/lib/prompt-builders.ts new file mode 100644 index 0000000..346c84b --- /dev/null +++ b/src/lib/prompt-builders.ts @@ -0,0 +1,103 @@ +import type { Language, Participant } from '../stores/settings'; + +interface SingleTranslationPromptContext { + sourceLang: Language; + targetLang: Language; + speakerIdentity: string; + toneRegister: string; + context: string; +} + +interface ConversationPromptContext { + me: Participant; + partner: Participant; + historyBlock: string; + senderName: string; + fromLang: Language; + toLang: Language; + targetTone: string; +} + +function replaceTokens(template: string, values: Record) { + return Object.entries(values).reduce( + (result, [key, value]) => result.replace(new RegExp(`\\{${key}\\}`, 'g'), value), + template, + ); +} + +export function buildSingleTranslationSystemPrompt( + template: string, + context: Omit, +) { + return replaceTokens(template, { + SOURCE_LANG: context.sourceLang.englishName, + SOURCE_CODE: context.sourceLang.code, + TARGET_LANG: context.targetLang.englishName, + TARGET_CODE: context.targetLang.code, + SPEAKER_IDENTITY: context.speakerIdentity, + TONE_REGISTER: context.toneRegister, + }); +} + +export function buildSingleEvaluationSystemPrompt( + template: string, + context: SingleTranslationPromptContext, +) { + return replaceTokens(template, { + SOURCE_LANG: context.sourceLang.englishName, + TARGET_LANG: context.targetLang.englishName, + SPEAKER_IDENTITY: context.speakerIdentity, + TONE_REGISTER: context.toneRegister, + CONTEXT: context.context || 'None', + }); +} + +export function buildSingleRefinementSystemPrompt( + template: string, + context: SingleTranslationPromptContext, +) { + return buildSingleEvaluationSystemPrompt(template, context); +} + +export function buildSingleTranslationUserPrompt(sourceText: string, context: string) { + return context + ? `[Context]\n${context}\n\n[Text to Translate]\n${sourceText}` + : `[Text to Translate]\n${sourceText}`; +} + +export function buildSingleEvaluationUserPrompt(sourceText: string, targetText: string) { + return `[Source Text]\n${sourceText}\n\n[Translated Text]\n${targetText}`; +} + +export function buildSingleRefinementUserPrompt(sourceText: string, currentTranslation: string, suggestions: string[]) { + const formattedSuggestions = suggestions.map((text, index) => `${index + 1}. ${text}`).join('\n'); + return `[Source Text]\n${sourceText}\n\n[Current Translation]\n${currentTranslation}\n\n[User Feedback]\n${formattedSuggestions}`; +} + +export function buildConversationSystemPrompt(template: string, context: ConversationPromptContext, emptyHistoryFallback: string) { + return replaceTokens(template, { + ME_NAME: context.me.name, + ME_GENDER: context.me.gender, + ME_LANG: context.me.language.englishName, + PART_NAME: context.partner.name, + PART_GENDER: context.partner.gender, + PART_LANG: context.partner.language.englishName, + HISTORY_BLOCK: context.historyBlock || emptyHistoryFallback, + SENDER_NAME: context.senderName, + FROM_LANG: context.fromLang.englishName, + TO_LANG: context.toLang.englishName, + TARGET_TONE: context.targetTone, + }); +} + +export function buildConversationTranslationUserPrompt(text: string) { + return `[Text to Translate]\n${text}`; +} + +export function buildConversationEvaluationUserPrompt(sourceText: string, currentTranslation: string) { + return `[Source Text]\n${sourceText}\n\n[Current Translation]\n${currentTranslation}`; +} + +export function buildConversationRefinementUserPrompt(sourceText: string, currentTranslation: string, suggestions: string[]) { + return `[Source Text]\n${sourceText}\n\n[Current Translation]\n${currentTranslation}\n\n[Suggestions]\n${suggestions.map((text) => `- ${text}`).join('\n')}`; +}