Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c1b3c3b142 | ||
|
|
a8e14269af | ||
|
|
f21366e55f | ||
|
|
6ff6fd3f25 |
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "ai-translate-client",
|
"name": "ai-translate-client",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.3.1",
|
"version": "0.3.2",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
2
src-tauri/Cargo.lock
generated
2
src-tauri/Cargo.lock
generated
@@ -19,7 +19,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ai-translate-client"
|
name = "ai-translate-client"
|
||||||
version = "0.3.1"
|
version = "0.3.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"reqwest 0.12.28",
|
"reqwest 0.12.28",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "ai-translate-client"
|
name = "ai-translate-client"
|
||||||
version = "0.3.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"
|
||||||
|
|||||||
@@ -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.3.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",
|
||||||
|
|||||||
254
src/App.vue
254
src/App.vue
@@ -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];
|
||||||
@@ -134,6 +138,15 @@ 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 () => {
|
||||||
@@ -176,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 };
|
||||||
@@ -185,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 () => {
|
||||||
@@ -199,11 +219,75 @@ 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 systemMessage = settings.systemPromptTemplate
|
const systemMessage = settings.systemPromptTemplate
|
||||||
.replace(/{SOURCE_LANG}/g, sourceLang.value.englishName)
|
.replace(/{SOURCE_LANG}/g, sourceLang.value.englishName)
|
||||||
@@ -240,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);
|
||||||
@@ -520,6 +609,61 @@ 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>
|
||||||
@@ -647,26 +791,124 @@ 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-slate-200/20 dark:bg-slate-900 rounded-xl shadow-sm/5 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}']" :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>
|
||||||
|
|||||||
@@ -51,6 +51,37 @@ export const DEFAULT_TEMPLATE = `You are a professional {SOURCE_LANG} ({SOURCE_C
|
|||||||
3. Produce ONLY the {TARGET_LANG} translation, without any additional explanations, notes, or commentary.
|
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].`;
|
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].`;
|
||||||
|
|
||||||
|
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}
|
||||||
|
|
||||||
|
[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;
|
||||||
name: string;
|
name: string;
|
||||||
@@ -68,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]);
|
||||||
@@ -97,6 +132,9 @@ export const useSettingsStore = defineStore('settings', () => {
|
|||||||
profiles,
|
profiles,
|
||||||
enableStreaming,
|
enableStreaming,
|
||||||
systemPromptTemplate,
|
systemPromptTemplate,
|
||||||
|
enableEvaluation,
|
||||||
|
evaluationPromptTemplate,
|
||||||
|
evaluationProfileId,
|
||||||
sourceLang,
|
sourceLang,
|
||||||
targetLang,
|
targetLang,
|
||||||
speakerIdentity,
|
speakerIdentity,
|
||||||
|
|||||||
Reference in New Issue
Block a user