11 Commits

Author SHA1 Message Date
Julian Freeman
c1b3c3b142 improve eval ui 2026-02-25 00:33:34 -04:00
Julian Freeman
a8e14269af import eval display 2026-02-25 00:21:17 -04:00
Julian Freeman
f21366e55f improve evaluation 2026-02-25 00:00:07 -04:00
Julian Freeman
6ff6fd3f25 support evaluation 2026-02-24 23:27:06 -04:00
Julian Freeman
f6bc4152b0 change the left bottom 2026-02-24 22:27:19 -04:00
Julian Freeman
5b493ce767 make light softer 2 2026-02-24 22:23:58 -04:00
Julian Freeman
49b96eeaeb make light softer 2026-02-24 22:07:45 -04:00
Julian Freeman
d259b54f11 not store context 2026-02-24 22:00:56 -04:00
Julian Freeman
484dd17f1c fix ui 2026-02-24 21:49:58 -04:00
Julian Freeman
985cfd933a modify 2026-02-24 16:44:42 -04:00
Julian Freeman
d9f0af53c4 support provide context 2026-02-24 16:36:08 -04:00
6 changed files with 351 additions and 46 deletions

View File

@@ -1,7 +1,7 @@
{ {
"name": "ai-translate-client", "name": "ai-translate-client",
"private": true, "private": true,
"version": "0.3.0", "version": "0.3.2",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",

2
src-tauri/Cargo.lock generated
View File

@@ -19,7 +19,7 @@ dependencies = [
[[package]] [[package]]
name = "ai-translate-client" name = "ai-translate-client"
version = "0.2.1" version = "0.3.2"
dependencies = [ dependencies = [
"futures-util", "futures-util",
"reqwest 0.12.28", "reqwest 0.12.28",

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "ai-translate-client" name = "ai-translate-client"
version = "0.2.1" version = "0.3.2"
description = "A client using AI models to translate" description = "A client using AI models to translate"
authors = ["Julian"] authors = ["Julian"]
edition = "2021" edition = "2021"

View File

@@ -1,7 +1,7 @@
{ {
"$schema": "https://schema.tauri.app/config/2", "$schema": "https://schema.tauri.app/config/2",
"productName": "ai-translate-client", "productName": "ai-translate-client",
"version": "0.2.1", "version": "0.3.2",
"identifier": "top.volan.ai-translate-client", "identifier": "top.volan.ai-translate-client",
"build": { "build": {
"beforeDevCommand": "pnpm dev", "beforeDevCommand": "pnpm dev",

View File

@@ -25,6 +25,7 @@ import {
useSettingsStore, useSettingsStore,
LANGUAGES, LANGUAGES,
DEFAULT_TEMPLATE, DEFAULT_TEMPLATE,
DEFAULT_EVALUATION_TEMPLATE,
SPEAKER_IDENTITY_OPTIONS, SPEAKER_IDENTITY_OPTIONS,
TONE_REGISTER_OPTIONS, TONE_REGISTER_OPTIONS,
type ApiProfile type ApiProfile
@@ -89,20 +90,23 @@ const sourceDropdownOpen = ref(false);
const targetDropdownOpen = ref(false); const targetDropdownOpen = ref(false);
const speakerDropdownOpen = ref(false); const speakerDropdownOpen = ref(false);
const toneDropdownOpen = ref(false); const toneDropdownOpen = ref(false);
const evaluationProfileDropdownOpen = ref(false);
const closeAllDropdowns = () => { const closeAllDropdowns = () => {
sourceDropdownOpen.value = false; sourceDropdownOpen.value = false;
targetDropdownOpen.value = false; targetDropdownOpen.value = false;
speakerDropdownOpen.value = false; speakerDropdownOpen.value = false;
toneDropdownOpen.value = false; toneDropdownOpen.value = false;
evaluationProfileDropdownOpen.value = false;
}; };
const toggleDropdown = (type: 'source' | 'target' | 'speaker' | 'tone') => { const toggleDropdown = (type: 'source' | 'target' | 'speaker' | 'tone' | 'evaluationProfile') => {
const states = { const states = {
source: sourceDropdownOpen, source: sourceDropdownOpen,
target: targetDropdownOpen, target: targetDropdownOpen,
speaker: speakerDropdownOpen, speaker: speakerDropdownOpen,
tone: toneDropdownOpen tone: toneDropdownOpen,
evaluationProfile: evaluationProfileDropdownOpen
}; };
const targetState = states[type]; const targetState = states[type];
@@ -129,10 +133,20 @@ onUnmounted(() => {
// Translation State // Translation State
const sourceText = ref(''); const sourceText = ref('');
const context = ref('');
const targetText = ref(''); const targetText = ref('');
const isTranslating = ref(false); const isTranslating = ref(false);
const showCopyFeedback = ref(false); const showCopyFeedback = ref(false);
interface EvaluationResult {
score: number;
analysis: string;
improvements?: string;
}
const evaluationResult = ref<EvaluationResult | null>(null);
const isEvaluating = ref(false);
let unlisten: (() => void) | null = null; let unlisten: (() => void) | null = null;
onMounted(async () => { onMounted(async () => {
@@ -175,6 +189,12 @@ const currentToneLabel = computed(() => {
return TONE_REGISTER_OPTIONS.find(opt => opt.value === settings.toneRegister)?.label || '正式专业'; return TONE_REGISTER_OPTIONS.find(opt => opt.value === settings.toneRegister)?.label || '正式专业';
}); });
const currentEvaluationProfileLabel = computed(() => {
if (!settings.evaluationProfileId) return '使用主翻译配置(默认)';
const profile = settings.profiles.find(p => p.id === settings.evaluationProfileId);
return profile ? `${profile.name}${profile.modelName}` : '使用主翻译配置(默认)';
});
const swapLanguages = () => { const swapLanguages = () => {
const temp = { ...settings.sourceLang }; const temp = { ...settings.sourceLang };
settings.sourceLang = { ...settings.targetLang }; settings.sourceLang = { ...settings.targetLang };
@@ -184,6 +204,7 @@ const swapLanguages = () => {
const clearSource = () => { const clearSource = () => {
sourceText.value = ''; sourceText.value = '';
targetText.value = ''; targetText.value = '';
evaluationResult.value = null;
}; };
const copyTarget = async () => { const copyTarget = async () => {
@@ -198,25 +219,93 @@ const copyTarget = async () => {
} }
}; };
const evaluateTranslation = async () => {
if (!targetText.value) return;
isEvaluating.value = true;
evaluationResult.value = null;
// Determine which API config to use for evaluation
let apiBaseUrl = settings.apiBaseUrl;
let apiKey = settings.apiKey;
let modelName = settings.modelName;
if (settings.evaluationProfileId) {
const profile = settings.profiles.find(p => p.id === settings.evaluationProfileId);
if (profile) {
apiBaseUrl = profile.apiBaseUrl;
apiKey = profile.apiKey;
modelName = profile.modelName;
}
}
const evaluationPrompt = 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')
.replace(/{SOURCE_TEXT}/g, sourceText.value)
.replace(/{TRANSLATED_TEXT}/g, targetText.value);
const requestBody = {
model: modelName,
messages: [
{ role: "system", content: "You are a professional translation auditor. You must respond in valid JSON format." },
{ role: "user", content: evaluationPrompt }
],
stream: false // Non-streaming for evaluation to parse JSON
};
settings.addLog('request', { type: 'evaluation', ...requestBody });
try {
const response = await invoke<string>('translate', {
apiAddress: apiBaseUrl,
apiKey: apiKey,
payload: requestBody
});
try {
// Try to extract JSON if the model wrapped it in code blocks
const jsonStr = response.replace(/```json\s?|\s?```/g, '').trim();
evaluationResult.value = JSON.parse(jsonStr);
settings.addLog('response', { type: 'evaluation', content: evaluationResult.value });
} catch (parseErr) {
console.error('Failed to parse evaluation result:', response);
settings.addLog('error', `Evaluation parsing error: ${response}`);
}
} catch (err: any) {
settings.addLog('error', `Evaluation error: ${String(err)}`);
} finally {
isEvaluating.value = false;
}
};
const translate = async () => { const translate = async () => {
if (!sourceText.value.trim() || isTranslating.value) return; if (!sourceText.value.trim() || isTranslating.value) return;
isTranslating.value = true; isTranslating.value = true;
targetText.value = ''; targetText.value = '';
evaluationResult.value = null;
const prompt = settings.systemPromptTemplate const systemMessage = settings.systemPromptTemplate
.replace(/{SOURCE_LANG}/g, sourceLang.value.englishName) .replace(/{SOURCE_LANG}/g, sourceLang.value.englishName)
.replace(/{SOURCE_CODE}/g, sourceLang.value.code) .replace(/{SOURCE_CODE}/g, sourceLang.value.code)
.replace(/{TARGET_LANG}/g, targetLang.value.englishName) .replace(/{TARGET_LANG}/g, targetLang.value.englishName)
.replace(/{TARGET_CODE}/g, targetLang.value.code) .replace(/{TARGET_CODE}/g, targetLang.value.code)
.replace(/{SPEAKER_IDENTITY}/g, settings.speakerIdentity) .replace(/{SPEAKER_IDENTITY}/g, settings.speakerIdentity)
.replace(/{TONE_REGISTER}/g, settings.toneRegister) .replace(/{TONE_REGISTER}/g, settings.toneRegister);
.replace(/{TEXT}/g, sourceText.value);
const userMessage = context.value
? `[Context]\n${context.value}\n\n[Text to Translate]\n${sourceText.value}`
: `[Text to Translate]\n${sourceText.value}`;
const requestBody = { const requestBody = {
model: settings.modelName, model: settings.modelName,
messages: [ messages: [
{ role: "user", content: prompt } { role: "system", content: systemMessage },
{ role: "user", content: userMessage }
], ],
stream: settings.enableStreaming stream: settings.enableStreaming
}; };
@@ -235,6 +324,11 @@ const translate = async () => {
targetText.value = response; targetText.value = response;
} }
settings.addLog('response', 'Translation completed'); settings.addLog('response', 'Translation completed');
// Trigger evaluation if enabled
if (settings.enableEvaluation) {
await evaluateTranslation();
}
} catch (err: any) { } catch (err: any) {
const errorMsg = String(err); const errorMsg = String(err);
settings.addLog('error', errorMsg); settings.addLog('error', errorMsg);
@@ -246,9 +340,9 @@ const translate = async () => {
</script> </script>
<template> <template>
<div class="h-screen bg-slate-50 dark:bg-slate-950 text-slate-900 dark:text-slate-100 font-sans selection:bg-blue-100 dark:selection:bg-blue-900 flex flex-col overflow-hidden"> <div class="h-screen bg-slate-100/50 dark:bg-slate-950 text-slate-900 dark:text-slate-100 font-sans selection:bg-blue-100 dark:selection:bg-blue-900 flex flex-col overflow-hidden">
<!-- Header --> <!-- Header -->
<header class="h-14 border-b dark:border-slate-800 bg-white dark:bg-slate-900 flex items-center justify-between px-6 shrink-0 sticky top-0 z-10 shadow-sm"> <header class="h-14 border-b dark:border-slate-800 bg-slate-50 dark:bg-slate-900 flex items-center justify-between px-6 shrink-0 sticky top-0 z-10 shadow-sm/5">
<div class="flex items-center gap-2 cursor-pointer group" @click="view = 'translate'"> <div class="flex items-center gap-2 cursor-pointer group" @click="view = 'translate'">
<Languages class="w-6 h-6 text-blue-600 group-hover:scale-110 transition-transform" /> <Languages class="w-6 h-6 text-blue-600 group-hover:scale-110 transition-transform" />
<h1 class="font-semibold text-lg tracking-tight">AI 翻译</h1> <h1 class="font-semibold text-lg tracking-tight">AI 翻译</h1>
@@ -256,7 +350,7 @@ const translate = async () => {
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<button <button
@click="toggleTheme" @click="toggleTheme"
class="p-2 rounded-full transition-colors hover:bg-slate-100 dark:hover:bg-slate-800 text-slate-600 dark:text-slate-300" class="p-2 rounded-full transition-colors hover:bg-slate-200/50 dark:hover:bg-slate-800 text-slate-600 dark:text-slate-300"
:title="settings.isDark ? '切换亮色主题' : '切换暗色主题'" :title="settings.isDark ? '切换亮色主题' : '切换暗色主题'"
> >
<Sun v-if="settings.isDark" class="w-5 h-5" /> <Sun v-if="settings.isDark" class="w-5 h-5" />
@@ -264,15 +358,15 @@ const translate = async () => {
</button> </button>
<button <button
@click="view = 'settings'" @click="view = 'settings'"
:class="cn('p-2 rounded-full transition-colors', view === 'settings' ? 'bg-blue-50 text-blue-600 dark:bg-blue-900/30 dark:text-blue-400' : 'hover:bg-slate-100 dark:hover:bg-slate-800 text-slate-600 dark:text-slate-300')" :class="cn('p-2 rounded-full transition-colors', view === 'settings' ? 'bg-blue-50 text-blue-600 dark:bg-blue-900/30 dark:text-blue-400' : 'hover:bg-slate-200/50 dark:hover:bg-slate-800 text-slate-600 dark:text-slate-300')"
title="设置" title="设置"
> >
<Settings class="w-5 h-5" /> <Settings class="w-5 h-5" />
</button> </button>
<button <button
@click="view = 'logs'" @click="view = 'logs'"
:class="cn('p-2 rounded-full transition-colors', view === 'logs' ? 'bg-blue-50 text-blue-600 dark:bg-blue-900/30 dark:text-blue-400' : 'hover:bg-slate-100 dark:hover:bg-slate-800 text-slate-600 dark:text-slate-300')" :class="cn('p-2 rounded-full transition-colors', view === 'logs' ? 'bg-blue-50 text-blue-600 dark:bg-blue-900/30 dark:text-blue-400' : 'hover:bg-slate-200/50 dark:hover:bg-slate-800 text-slate-600 dark:text-slate-300')"
title="调试日志" title="日志"
> >
<FileText class="w-5 h-5" /> <FileText class="w-5 h-5" />
</button> </button>
@@ -281,10 +375,10 @@ const translate = async () => {
<main class="flex-1 flex overflow-hidden min-h-0 relative"> <main class="flex-1 flex overflow-hidden min-h-0 relative">
<!-- Translation View --> <!-- Translation View -->
<div v-if="view === 'translate'" class="flex-1 flex flex-col md:flex-row divide-y md:divide-y-0 md:divide-x dark:divide-slate-800 bg-white dark:bg-slate-900 overflow-hidden h-full"> <div v-if="view === 'translate'" class="flex-1 flex flex-col md:flex-row divide-y md:divide-y-0 md:divide-x dark:divide-slate-800 bg-white/50 dark:bg-slate-900 overflow-hidden h-full">
<!-- Source Pane --> <!-- Source Pane -->
<div class="flex-1 flex flex-col min-h-0 relative h-full"> <div class="flex-1 flex flex-col min-h-0 relative h-full">
<div class="flex items-center gap-3 px-6 py-3 border-b dark:border-slate-800 bg-slate-50/50 dark:bg-slate-800/30 relative z-40 shrink-0"> <div class="flex items-center gap-3 px-6 py-3 border-b dark:border-slate-800 bg-slate-100/40 dark:bg-slate-800/30 relative z-40 shrink-0">
<!-- Custom Source Dropdown --> <!-- Custom Source Dropdown -->
<div class="relative lang-dropdown min-w-30"> <div class="relative lang-dropdown min-w-30">
<button <button
@@ -338,8 +432,31 @@ const translate = async () => {
placeholder="请输入待翻译内容..." placeholder="请输入待翻译内容..."
class="flex-1 p-6 resize-none outline-none text-lg leading-relaxed placeholder:text-slate-300 dark:placeholder:text-slate-600 bg-transparent min-h-0" class="flex-1 p-6 resize-none outline-none text-lg leading-relaxed placeholder:text-slate-300 dark:placeholder:text-slate-600 bg-transparent min-h-0"
></textarea> ></textarea>
<div class="p-4 border-t dark:border-slate-800 flex justify-end shrink-0">
<!-- Context Input Area -->
<div class="px-6 py-3 bg-slate-200/20 dark:bg-slate-800/20 border-t border-dashed dark:border-slate-800 group/context relative">
<div class="flex items-center justify-between mb-1.5 h-5">
<div class="flex items-center gap-1.5">
<FileText class="w-4 h-4 text-slate-400" />
<span class="text-[12px] font-bold text-slate-400 uppercase tracking-widest">情景背景 (可选)</span>
</div>
<button <button
v-if="context"
@click="context = ''"
class="p-1 hover:bg-slate-200 dark:hover:bg-slate-700 rounded opacity-0 group-hover/context:opacity-100 transition-opacity"
title="清空背景"
>
<Plus class="w-3 h-3 rotate-45 text-slate-400" />
</button>
</div>
<textarea
v-model="context"
placeholder="在此输入背景信息,有助于提升翻译准确度..."
class="w-full bg-transparent border-none outline-none text-sm text-slate-500 dark:text-slate-400 resize-none h-14 leading-normal placeholder:italic placeholder:text-slate-300 dark:placeholder:text-slate-600"
></textarea>
</div>
<div class="p-4 border-t dark:border-slate-800 bg-slate-50/30 dark:bg-transparent flex justify-end shrink-0"> <button
@click="translate" @click="translate"
:disabled="isTranslating || !sourceText.trim()" :disabled="isTranslating || !sourceText.trim()"
class="bg-blue-600 hover:bg-blue-700 disabled:bg-blue-300 dark:disabled:bg-blue-900/40 text-white px-6 py-2.5 rounded-lg font-medium transition-all flex items-center gap-2 shadow-sm" class="bg-blue-600 hover:bg-blue-700 disabled:bg-blue-300 dark:disabled:bg-blue-900/40 text-white px-6 py-2.5 rounded-lg font-medium transition-all flex items-center gap-2 shadow-sm"
@@ -352,8 +469,8 @@ const translate = async () => {
</div> </div>
<!-- Target Pane --> <!-- Target Pane -->
<div class="flex-1 flex flex-col min-h-0 bg-slate-50/30 dark:bg-slate-900/50 relative h-full"> <div class="flex-1 flex flex-col min-h-0 bg-slate-100/20 dark:bg-slate-900/50 relative h-full">
<div class="flex items-center gap-3 px-6 py-3 border-b dark:border-slate-800 bg-slate-50/50 dark:bg-slate-800/30 relative z-40 shrink-0"> <div class="flex items-center gap-3 px-6 py-3 border-b dark:border-slate-800 bg-slate-100/40 dark:bg-slate-800/30 relative z-40 shrink-0">
<!-- Custom Target Dropdown --> <!-- Custom Target Dropdown -->
<div class="relative lang-dropdown min-w-30"> <div class="relative lang-dropdown min-w-30">
<button <button
@@ -492,23 +609,78 @@ const translate = async () => {
{{ targetText }} {{ targetText }}
</template> </template>
<span v-else class="text-slate-300 dark:text-slate-600 italic">翻译结果将在此显示...</span> <span v-else class="text-slate-300 dark:text-slate-600 italic">翻译结果将在此显示...</span>
<!-- Evaluation Results -->
<div v-if="isEvaluating || evaluationResult" class="mt-8 pt-6 border-t dark:border-slate-800 space-y-4 animate-in fade-in slide-in-from-bottom-2 duration-500">
<div class="flex items-center justify-between">
<div class="flex items-center gap-2">
<div :class="cn(
'w-2 h-2 rounded-full',
isEvaluating ? 'bg-blue-400 animate-pulse' : (evaluationResult?.score && evaluationResult.score >= 80 ? 'bg-green-500' : evaluationResult?.score && evaluationResult.score >= 60 ? 'bg-amber-500' : 'bg-red-500')
)"></div>
<h3 class="text-xs font-bold text-slate-400 uppercase tracking-widest">质量审计</h3>
</div>
<div v-if="evaluationResult" :class="cn(
'text-lg font-black font-mono',
evaluationResult.score >= 80 ? 'text-green-600' : evaluationResult.score >= 60 ? 'text-amber-600' : 'text-red-600'
)">
{{ evaluationResult.score }} <span class="text-[10px] font-normal opacity-50">/ 100</span>
</div>
<div v-else-if="isEvaluating" class="flex items-center gap-1.5 text-xs text-blue-500 font-medium">
<Loader2 class="w-3 h-3 animate-spin" />
正在审计...
</div>
</div>
<div v-if="evaluationResult" class="space-y-3">
<div class="bg-slate-50 dark:bg-slate-800/40 p-3 rounded-lg border border-slate-100 dark:border-slate-800/60">
<p class="text-xs text-slate-600 dark:text-slate-300 leading-relaxed">
{{ evaluationResult.analysis }}
</p>
</div>
<div v-if="evaluationResult.improvements" class="space-y-2 pt-2">
<div class="flex items-center gap-2">
<div class="w-2 h-2 rounded-full bg-blue-500"></div>
<h3 class="text-xs font-bold text-slate-400 uppercase tracking-widest">建议优化</h3>
</div>
<div class="bg-blue-50/50 dark:bg-blue-900/10 p-3 rounded-lg border border-blue-100/50 dark:border-blue-900/20">
<p class="text-xs text-slate-600 dark:text-slate-300 leading-relaxed">
{{ evaluationResult.improvements }}
</p>
</div>
</div>
</div>
</div>
</div>
<div class="p-4 border-t dark:border-slate-800 bg-slate-50/30 dark:bg-transparent flex justify-end shrink-0">
<button
@click="evaluateTranslation"
:disabled="isEvaluating || isTranslating || !targetText.trim()"
class="bg-slate-200 hover:bg-slate-300 dark:bg-slate-800 dark:hover:bg-slate-700 disabled:opacity-50 text-slate-700 dark:text-slate-200 px-6 py-2.5 rounded-lg font-medium transition-all flex items-center gap-2 shadow-sm border border-slate-300/50 dark:border-slate-700/50"
>
<Loader2 v-if="isEvaluating" class="w-4 h-4 animate-spin" />
<Check v-else class="w-4 h-4" />
{{ isEvaluating ? '正在审计...' : '审计' }}
</button>
</div> </div>
</div> </div>
</div> </div>
<!-- Settings View --> <!-- Settings View -->
<div v-else-if="view === 'settings'" class="flex-1 overflow-y-auto bg-slate-50 dark:bg-slate-950 p-6 md:p-10 min-h-0"> <div v-else-if="view === 'settings'" class="flex-1 overflow-y-auto bg-slate-100/50 dark:bg-slate-950 p-6 md:p-10 min-h-0">
<div class="max-w-2xl mx-auto space-y-8"> <div class="max-w-2xl mx-auto space-y-8">
<section> <section>
<h2 class="text-sm font-semibold text-slate-500 dark:text-slate-400 uppercase tracking-wider mb-4">API 配置预设</h2> <h2 class="text-sm font-semibold text-slate-500 dark:text-slate-400 uppercase tracking-wider mb-4">API 配置预设</h2>
<div class="bg-white dark:bg-slate-900 rounded-xl shadow-sm border dark:border-slate-800 p-1.5 space-y-1"> <div class="bg-white/60 dark:bg-slate-900 rounded-xl shadow-sm border dark:border-slate-800 p-1.5 space-y-1">
<div v-if="settings.profiles.length === 0" class="p-8 text-center text-sm text-slate-400 dark:text-slate-600 italic"> <div v-if="settings.profiles.length === 0" class="p-8 text-center text-sm text-slate-400 dark:text-slate-600 italic">
暂无预设配置 暂无预设配置
</div> </div>
<div <div
v-for="profile in settings.profiles" v-for="profile in settings.profiles"
:key="profile.id" :key="profile.id"
class="p-3 flex items-center justify-between group hover:bg-slate-50 dark:hover:bg-slate-800/50 transition-colors rounded-lg" class="p-3 flex items-center justify-between group hover:bg-slate-100/50 dark:hover:bg-slate-800/50 transition-colors rounded-lg"
> >
<div class="flex flex-col gap-0.5 min-w-0"> <div class="flex flex-col gap-0.5 min-w-0">
<span class="text-sm font-semibold text-slate-700 dark:text-slate-200 truncate">{{ profile.name }}</span> <span class="text-sm font-semibold text-slate-700 dark:text-slate-200 truncate">{{ profile.name }}</span>
@@ -573,7 +745,7 @@ const translate = async () => {
</button> </button>
</div> </div>
</div> </div>
<div class="bg-white dark:bg-slate-900 rounded-xl shadow-sm border dark:border-slate-800 p-6 space-y-4"> <div class="bg-slate-200/20 dark:bg-slate-900 rounded-xl shadow-sm/5 border dark:border-slate-800 p-6 space-y-4">
<div class="space-y-2"> <div class="space-y-2">
<label class="text-sm font-medium text-slate-700 dark:text-slate-300">API Base URL</label> <label class="text-sm font-medium text-slate-700 dark:text-slate-300">API Base URL</label>
<input <input
@@ -619,36 +791,134 @@ const translate = async () => {
)"></div> )"></div>
</button> </button>
</div> </div>
<div class="flex items-center justify-between">
<div>
<label class="text-sm font-medium text-slate-700 dark:text-slate-300">自动质量审计</label>
<p class="text-xs text-slate-500 dark:text-slate-500">翻译完成后自动评估准确度</p>
</div>
<button
@click="settings.enableEvaluation = !settings.enableEvaluation"
:class="cn(
'w-12 h-6 rounded-full transition-colors relative',
settings.enableEvaluation ? 'bg-blue-600' : 'bg-slate-300 dark:bg-slate-700'
)"
>
<div :class="cn(
'absolute top-1 left-1 w-4 h-4 bg-white rounded-full transition-transform',
settings.enableEvaluation ? 'translate-x-6' : 'translate-x-0'
)"></div>
</button>
</div>
<div class="space-y-3 pt-4 border-t border-dashed dark:border-slate-800 animate-in fade-in slide-in-from-top-2 duration-300">
<div class="flex items-center gap-2 mb-1">
<Settings class="w-3.5 h-3.5 text-blue-500" />
<label class="text-sm font-semibold text-slate-700 dark:text-slate-300">审计模型</label>
</div>
<!-- Custom Evaluation Profile Dropdown -->
<div class="relative lang-dropdown">
<button
@click.stop="toggleDropdown('evaluationProfile')"
class="flex items-center justify-between w-full px-4 py-2.5 border dark:border-slate-700 rounded-xl bg-slate-50/50 dark:bg-slate-800/30 hover:bg-slate-100/50 dark:hover:bg-slate-800/50 transition-all text-sm text-slate-700 dark:text-slate-200 group"
>
<span class="truncate">{{ currentEvaluationProfileLabel }}</span>
<ChevronDown :class="cn('w-4 h-4 text-slate-400 transition-transform duration-200 group-hover:text-blue-500', evaluationProfileDropdownOpen && 'rotate-180')" />
</button>
<transition
enter-active-class="transition duration-100 ease-out"
enter-from-class="transform scale-95 opacity-0"
enter-to-class="transform scale-100 opacity-100"
leave-active-class="transition duration-75 ease-in"
leave-from-class="transform scale-100 opacity-100"
leave-to-class="transform scale-95 opacity-0"
>
<div
v-if="evaluationProfileDropdownOpen"
class="absolute left-0 mt-2 w-full max-h-60 overflow-y-auto bg-white dark:bg-slate-800 rounded-xl shadow-xl border border-slate-200 dark:border-slate-700 z-50 py-2 flex flex-col custom-scrollbar"
>
<button
@click="settings.evaluationProfileId = null; evaluationProfileDropdownOpen = false"
:class="cn(
'px-4 py-2.5 text-sm text-left transition-colors flex items-center justify-between',
settings.evaluationProfileId === null ? 'bg-blue-50 text-blue-600 dark:bg-blue-900/30 dark:text-blue-400 font-bold' : 'text-slate-500 hover:bg-slate-50 dark:hover:bg-slate-700/50'
)"
>
使用主翻译配置默认
<Check v-if="settings.evaluationProfileId === null" class="w-3.5 h-3.5" />
</button>
<div class="h-px bg-slate-100 dark:bg-slate-700 my-1 mx-2"></div>
<button
v-for="profile in settings.profiles"
:key="profile.id"
@click="settings.evaluationProfileId = profile.id; evaluationProfileDropdownOpen = false"
:class="cn(
'px-4 py-2.5 text-sm text-left transition-colors flex items-center justify-between',
settings.evaluationProfileId === profile.id ? 'bg-blue-50 text-blue-600 dark:bg-blue-900/30 dark:text-blue-400 font-bold' : 'text-slate-600 dark:text-slate-300 hover:bg-slate-50 dark:hover:bg-slate-700/50'
)"
>
<div class="flex flex-col min-w-0">
<span class="truncate">{{ profile.name }}</span>
<span class="text-[10px] opacity-60 font-mono">{{ profile.modelName }}</span>
</div>
<Check v-if="settings.evaluationProfileId === profile.id" class="w-3.5 h-3.5 shrink-0" />
</button>
</div>
</transition>
</div>
<p class="text-[11px] text-slate-500 dark:text-slate-500 pl-1">
提示建议为审计选择更强大的模型以获得更精准的反馈
</p>
</div>
</div> </div>
</section> </section>
<section> <section>
<h2 class="text-sm font-semibold text-slate-500 dark:text-slate-400 uppercase tracking-wider mb-4">提示词工程</h2> <h2 class="text-sm font-semibold text-slate-500 dark:text-slate-400 uppercase tracking-wider mb-4">提示词工程</h2>
<div class="bg-white dark:bg-slate-900 rounded-xl shadow-sm border dark:border-slate-800 p-6"> <div class="bg-slate-200/20 dark:bg-slate-900 rounded-xl shadow-sm/5 border dark:border-slate-800 p-6 space-y-6">
<div class="space-y-2"> <div class="space-y-2">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<label class="text-sm font-medium text-slate-700 dark:text-slate-300">系统提示词模板</label> <label class="text-sm font-medium text-slate-700 dark:text-slate-300">系统提示词模板 (翻译)</label>
<button @click="settings.systemPromptTemplate = DEFAULT_TEMPLATE" class="text-xs text-blue-600 dark:text-blue-400 hover:underline">恢复默认值</button> <button @click="settings.systemPromptTemplate = DEFAULT_TEMPLATE" class="text-xs text-blue-600 dark:text-blue-400 hover:underline">恢复默认值</button>
</div> </div>
<textarea <textarea
v-model="settings.systemPromptTemplate" v-model="settings.systemPromptTemplate"
rows="9" rows="6"
class="w-full px-4 py-3 border dark:border-slate-700 rounded-lg bg-transparent focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 outline-none transition-all font-mono text-xs leading-relaxed text-slate-900 dark:text-slate-100" class="w-full px-4 py-3 border dark:border-slate-700 rounded-lg bg-transparent focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 outline-none transition-all font-mono text-xs leading-relaxed text-slate-900 dark:text-slate-100"
></textarea> ></textarea>
<div class="flex flex-wrap gap-2 mt-2"> <div class="flex flex-wrap gap-2 mt-2">
<span v-for="tag in ['{SOURCE_LANG}', '{SOURCE_CODE}', '{TARGET_LANG}', '{TARGET_CODE}', '{SPEAKER_IDENTITY}', '{TONE_REGISTER}', '{TEXT}']" :key="tag" class="px-2 py-1 bg-slate-100 dark:bg-slate-800 text-[10px] font-mono rounded border dark:border-slate-700 text-slate-600 dark:text-slate-400">{{ tag }}</span> <span v-for="tag in ['{SOURCE_CODE}', '{TARGET_CODE}', '{SOURCE_LANG}', '{TARGET_LANG}', '{SPEAKER_IDENTITY}', '{TONE_REGISTER}']" :key="tag" class="px-2 py-1 bg-slate-100 dark:bg-slate-800 text-[10px] font-mono rounded border dark:border-slate-700 text-slate-600 dark:text-slate-400">{{ tag }}</span>
</div> </div>
</div> </div>
<div class="space-y-2 border-t dark:border-slate-800 pt-6">
<div class="flex items-center justify-between">
<label class="text-sm font-medium text-slate-700 dark:text-slate-300">审计提示词模板 (评估)</label>
<button @click="settings.evaluationPromptTemplate = DEFAULT_EVALUATION_TEMPLATE" class="text-xs text-blue-600 dark:text-blue-400 hover:underline">恢复默认值</button>
</div>
<textarea
v-model="settings.evaluationPromptTemplate"
rows="8"
class="w-full px-4 py-3 border dark:border-slate-700 rounded-lg bg-transparent focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 outline-none transition-all font-mono text-xs leading-relaxed text-slate-900 dark:text-slate-100"
></textarea>
<div class="flex flex-wrap gap-2 mt-2">
<span v-for="tag in ['{SOURCE_LANG}', '{TARGET_LANG}', '{SPEAKER_IDENTITY}', '{TONE_REGISTER}', '{CONTEXT}', '{SOURCE_TEXT}', '{TRANSLATED_TEXT}']" :key="tag" class="px-2 py-1 bg-slate-100 dark:bg-slate-800 text-[10px] font-mono rounded border dark:border-slate-700 text-slate-600 dark:text-slate-400">{{ tag }}</span>
</div>
</div>
</div> </div>
</section> </section>
</div> </div>
</div> </div>
<!-- Logs View --> <!-- Logs View -->
<div v-else-if="view === 'logs'" class="flex-1 overflow-y-auto bg-slate-50 dark:bg-slate-950 p-6 md:p-10 min-h-0"> <div v-else-if="view === 'logs'" class="flex-1 overflow-y-auto bg-slate-100/50 dark:bg-slate-950 p-6 md:p-10 min-h-0">
<div class="max-w-3xl mx-auto space-y-6"> <div class="max-w-3xl mx-auto space-y-6">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<h2 class="text-sm font-semibold text-slate-500 dark:text-slate-400 uppercase tracking-wider">调试日志</h2> <h2 class="text-sm font-semibold text-slate-500 dark:text-slate-400 uppercase tracking-wider">日志</h2>
<button <button
@click="settings.logs = []" @click="settings.logs = []"
class="text-xs text-red-600 dark:text-red-400 hover:underline flex items-center gap-1" class="text-xs text-red-600 dark:text-red-400 hover:underline flex items-center gap-1"
@@ -657,7 +927,7 @@ const translate = async () => {
清空 清空
</button> </button>
</div> </div>
<div class="bg-white dark:bg-slate-900 rounded-xl shadow-sm border dark:border-slate-800 p-4 space-y-4"> <div class="bg-slate-200/20 dark:bg-slate-900 rounded-xl shadow-sm/5 border dark:border-slate-800 p-4 space-y-4">
<div v-if="settings.logs.length === 0" class="text-sm text-slate-400 dark:text-slate-500 text-center py-10 italic"> <div v-if="settings.logs.length === 0" class="text-sm text-slate-400 dark:text-slate-500 text-center py-10 italic">
暂无日志记录请尝试进行翻译 暂无日志记录请尝试进行翻译
</div> </div>
@@ -677,7 +947,7 @@ const translate = async () => {
)" )"
>{{ log.type === 'request' ? '请求' : log.type === 'response' ? '响应' : '错误' }}</span> >{{ log.type === 'request' ? '请求' : log.type === 'response' ? '响应' : '错误' }}</span>
</div> </div>
<pre class="bg-slate-50 dark:bg-slate-800/50 p-3 rounded-lg overflow-x-auto text-slate-600 dark:text-slate-300 max-h-56 leading-relaxed shadow-inner border border-slate-100 dark:border-slate-700">{{ typeof log.content === 'object' ? JSON.stringify(log.content, null, 2) : log.content }}</pre> <pre class="bg-slate-50 dark:bg-slate-800/50 p-3 rounded-lg overflow-x-auto text-slate-600 dark:text-slate-300 max-h-48 leading-relaxed shadow-inner border border-slate-100 dark:border-slate-700">{{ typeof log.content === 'object' ? JSON.stringify(log.content, null, 2) : log.content }}</pre>
</div> </div>
</div> </div>
</div> </div>
@@ -685,9 +955,9 @@ const translate = async () => {
</main> </main>
<!-- Footer --> <!-- Footer -->
<footer class="h-8 bg-slate-100 dark:bg-slate-900 border-t dark:border-slate-800 flex items-center px-4 justify-between shrink-0"> <footer class="h-8 bg-slate-200/40 dark:bg-slate-900 border-t dark:border-slate-800 flex items-center px-4 justify-between shrink-0">
<div class="text-[10px] text-slate-400 dark:text-slate-500"> <div class="text-[10px] text-slate-400 dark:text-slate-500">
{{ settings.apiBaseUrl }} {{ settings.modelName }}
</div> </div>
<div class="text-[10px] text-slate-400 dark:text-slate-500"> <div class="text-[10px] text-slate-400 dark:text-slate-500">
Client v{{ pkg.version }} Client v{{ pkg.version }}

View File

@@ -45,14 +45,42 @@ export const TONE_REGISTER_OPTIONS = [
export const DEFAULT_TEMPLATE = `You are a professional {SOURCE_LANG} ({SOURCE_CODE}) to {TARGET_LANG} ({TARGET_CODE}) translator. Your goal is to accurately convey the meaning and nuances of the original {SOURCE_LANG} text while adhering to {TARGET_LANG} grammar, vocabulary, and cultural sensitivities. export const DEFAULT_TEMPLATE = `You are a professional {SOURCE_LANG} ({SOURCE_CODE}) to {TARGET_LANG} ({TARGET_CODE}) translator. Your goal is to accurately convey the meaning and nuances of the original {SOURCE_LANG} text while adhering to {TARGET_LANG} grammar, vocabulary, and cultural sensitivities.
Translation Context & Style Constraints: [Constraints]
Speaker Identity: {SPEAKER_IDENTITY}. Ensure all grammatical agreements and self-referential terms in {TARGET_LANG} reflect this. 1. Speaker Identity: {SPEAKER_IDENTITY}. Ensure all grammatical agreements and self-referential terms in {TARGET_LANG} reflect this.
Tone & Register: {TONE_REGISTER}. 2. Tone & Register: {TONE_REGISTER}.
3. Produce ONLY the {TARGET_LANG} translation, without any additional explanations, notes, or commentary.
4. If [Context] is provided, use it strictly to disambiguate polysemous words. DO NOT add any factual information or descriptive details from the [Context] that are not present in the [Text to Translate].`;
Produce only the {TARGET_LANG} translation, without any additional explanations or commentary. Please translate the following {SOURCE_LANG} text into {TARGET_LANG}: export const DEFAULT_EVALUATION_TEMPLATE = `You are an expert translation auditor proficient in {SOURCE_LANG} and {TARGET_LANG}.
Your task is to critically evaluate the accuracy and quality of a translation.
[Context Info]
- Source Language: {SOURCE_LANG}
- Target Language: {TARGET_LANG}
- Speaker Identity: {SPEAKER_IDENTITY}
- Intended Tone/Register: {TONE_REGISTER}
- Context: {CONTEXT}
{TEXT}`; [Input]
- Source Text: {SOURCE_TEXT}
- Translated Text: {TRANSLATED_TEXT}
[Instructions]
1. Compare the [Source Text] and [Translated Text] meticulously.
2. Check if the translation respects the [Context Info].
3. Assign an "Accuracy Score" from 0 to 100.
- Give 0 if there are fatal semantic errors, complete hallucinations, or if the meaning is reversed.
- Deduct points for minor inaccuracies, unnatural phrasing, or tone mismatches.
4. Provide a concise "Analysis" of why you gave that score.
5. (Optional) Provide "Improvements" for a more accurate/natural translation.
[Output Format]
You MUST respond in JSON format with the following keys. The values for "analysis" and "improvements" MUST be written in Simplified Chinese (简体中文), except when quoting the source or target text:
{
"score": number,
"analysis": "string",
"improvements": "string"
}`;
export interface ApiProfile { export interface ApiProfile {
id: string; id: string;
@@ -71,6 +99,10 @@ export const useSettingsStore = defineStore('settings', () => {
const enableStreaming = useLocalStorage('enable-streaming', true); const enableStreaming = useLocalStorage('enable-streaming', true);
const systemPromptTemplate = useLocalStorage('system-prompt-template', DEFAULT_TEMPLATE); const systemPromptTemplate = useLocalStorage('system-prompt-template', DEFAULT_TEMPLATE);
const enableEvaluation = useLocalStorage('enable-evaluation', true);
const evaluationPromptTemplate = useLocalStorage('evaluation-prompt-template', DEFAULT_EVALUATION_TEMPLATE);
const evaluationProfileId = useLocalStorage<string | null>('evaluation-profile-id', null);
// 存储整个对象以保持一致性 // 存储整个对象以保持一致性
const sourceLang = useLocalStorage<Language>('source-lang-v2', LANGUAGES[0]); const sourceLang = useLocalStorage<Language>('source-lang-v2', LANGUAGES[0]);
const targetLang = useLocalStorage<Language>('target-lang-v2', LANGUAGES[4]); const targetLang = useLocalStorage<Language>('target-lang-v2', LANGUAGES[4]);
@@ -100,6 +132,9 @@ export const useSettingsStore = defineStore('settings', () => {
profiles, profiles,
enableStreaming, enableStreaming,
systemPromptTemplate, systemPromptTemplate,
enableEvaluation,
evaluationPromptTemplate,
evaluationProfileId,
sourceLang, sourceLang,
targetLang, targetLang,
speakerIdentity, speakerIdentity,