Compare commits
7 Commits
v0.3.3
...
c5890bd5f2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c5890bd5f2 | ||
|
|
96be7c67f3 | ||
|
|
ae1028588b | ||
|
|
dd06a3159b | ||
|
|
f20a9bb851 | ||
|
|
8294c12d17 | ||
|
|
55de02f5e3 |
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "ai-translate-client",
|
"name": "ai-translate-client",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.3.2",
|
"version": "0.3.3",
|
||||||
"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.2"
|
version = "0.3.3"
|
||||||
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.2"
|
version = "0.3.3"
|
||||||
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.2",
|
"version": "0.3.3",
|
||||||
"identifier": "top.volan.ai-translate-client",
|
"identifier": "top.volan.ai-translate-client",
|
||||||
"build": {
|
"build": {
|
||||||
"beforeDevCommand": "pnpm dev",
|
"beforeDevCommand": "pnpm dev",
|
||||||
|
|||||||
226
src/App.vue
226
src/App.vue
@@ -26,6 +26,7 @@ import {
|
|||||||
LANGUAGES,
|
LANGUAGES,
|
||||||
DEFAULT_TEMPLATE,
|
DEFAULT_TEMPLATE,
|
||||||
DEFAULT_EVALUATION_TEMPLATE,
|
DEFAULT_EVALUATION_TEMPLATE,
|
||||||
|
DEFAULT_REFINEMENT_TEMPLATE,
|
||||||
SPEAKER_IDENTITY_OPTIONS,
|
SPEAKER_IDENTITY_OPTIONS,
|
||||||
TONE_REGISTER_OPTIONS,
|
TONE_REGISTER_OPTIONS,
|
||||||
type ApiProfile
|
type ApiProfile
|
||||||
@@ -138,20 +139,39 @@ const targetText = ref('');
|
|||||||
const isTranslating = ref(false);
|
const isTranslating = ref(false);
|
||||||
const showCopyFeedback = ref(false);
|
const showCopyFeedback = ref(false);
|
||||||
|
|
||||||
|
interface Suggestion {
|
||||||
|
id: number;
|
||||||
|
text: string;
|
||||||
|
importance: number;
|
||||||
|
}
|
||||||
|
|
||||||
interface EvaluationResult {
|
interface EvaluationResult {
|
||||||
score: number;
|
score: number;
|
||||||
analysis: string;
|
analysis: string;
|
||||||
improvements?: string;
|
suggestions?: Suggestion[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const evaluationResult = ref<EvaluationResult | null>(null);
|
const evaluationResult = ref<EvaluationResult | null>(null);
|
||||||
const isEvaluating = ref(false);
|
const isEvaluating = ref(false);
|
||||||
|
const isRefining = ref(false);
|
||||||
|
const selectedSuggestionIds = ref<number[]>([]);
|
||||||
|
const appliedSuggestionIds = ref<number[]>([]);
|
||||||
|
|
||||||
|
const toggleSuggestion = (id: number) => {
|
||||||
|
if (!selectedSuggestionIds.value) selectedSuggestionIds.value = [];
|
||||||
|
const index = selectedSuggestionIds.value.indexOf(id);
|
||||||
|
if (index > -1) {
|
||||||
|
selectedSuggestionIds.value.splice(index, 1);
|
||||||
|
} else {
|
||||||
|
selectedSuggestionIds.value.push(id);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let unlisten: (() => void) | null = null;
|
let unlisten: (() => void) | null = null;
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
unlisten = await listen<string>('translation-chunk', (event) => {
|
unlisten = await listen<string>('translation-chunk', (event) => {
|
||||||
if (isTranslating.value) {
|
if (isTranslating.value || isRefining.value) {
|
||||||
targetText.value += event.payload;
|
targetText.value += event.payload;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -224,6 +244,8 @@ const evaluateTranslation = async () => {
|
|||||||
|
|
||||||
isEvaluating.value = true;
|
isEvaluating.value = true;
|
||||||
evaluationResult.value = null;
|
evaluationResult.value = null;
|
||||||
|
selectedSuggestionIds.value = [];
|
||||||
|
appliedSuggestionIds.value = [];
|
||||||
|
|
||||||
// Determine which API config to use for evaluation
|
// Determine which API config to use for evaluation
|
||||||
let apiBaseUrl = settings.apiBaseUrl;
|
let apiBaseUrl = settings.apiBaseUrl;
|
||||||
@@ -239,20 +261,20 @@ const evaluateTranslation = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const evaluationPrompt = settings.evaluationPromptTemplate
|
const evaluationSystemPrompt = settings.evaluationPromptTemplate
|
||||||
.replace(/{SOURCE_LANG}/g, sourceLang.value.englishName)
|
.replace(/{SOURCE_LANG}/g, sourceLang.value.englishName)
|
||||||
.replace(/{TARGET_LANG}/g, targetLang.value.englishName)
|
.replace(/{TARGET_LANG}/g, targetLang.value.englishName)
|
||||||
.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(/{CONTEXT}/g, context.value || 'None')
|
.replace(/{CONTEXT}/g, context.value || 'None');
|
||||||
.replace(/{SOURCE_TEXT}/g, sourceText.value)
|
|
||||||
.replace(/{TRANSLATED_TEXT}/g, targetText.value);
|
const evaluationUserPrompt = `[Source Text]\n${sourceText.value}\n\n[Translated Text]\n${targetText.value}`;
|
||||||
|
|
||||||
const requestBody = {
|
const requestBody = {
|
||||||
model: modelName,
|
model: modelName,
|
||||||
messages: [
|
messages: [
|
||||||
{ role: "system", content: "You are a professional translation auditor. You must respond in valid JSON format." },
|
{ role: "system", content: evaluationSystemPrompt },
|
||||||
{ role: "user", content: evaluationPrompt }
|
{ role: "user", content: evaluationUserPrompt }
|
||||||
],
|
],
|
||||||
stream: false // Non-streaming for evaluation to parse JSON
|
stream: false // Non-streaming for evaluation to parse JSON
|
||||||
};
|
};
|
||||||
@@ -282,6 +304,83 @@ const evaluateTranslation = async () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const refineTranslation = async () => {
|
||||||
|
if (!targetText.value || isRefining.value) return;
|
||||||
|
|
||||||
|
const selectedTexts = evaluationResult.value?.suggestions
|
||||||
|
?.filter(s => selectedSuggestionIds.value?.includes(s.id))
|
||||||
|
.map(s => s.text);
|
||||||
|
|
||||||
|
if (!selectedTexts || selectedTexts.length === 0) return;
|
||||||
|
|
||||||
|
isRefining.value = true;
|
||||||
|
const originalTranslation = targetText.value;
|
||||||
|
targetText.value = '';
|
||||||
|
// Keep evaluationResult and selectedSuggestionIds to allow the user to see what was changed
|
||||||
|
|
||||||
|
// Determine which API config to use (same as audit)
|
||||||
|
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 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 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 requestBody = {
|
||||||
|
model: modelName,
|
||||||
|
messages: [
|
||||||
|
{ role: "system", content: refinementSystemPrompt },
|
||||||
|
{ role: "user", content: refinementUserPrompt }
|
||||||
|
],
|
||||||
|
stream: settings.enableStreaming
|
||||||
|
};
|
||||||
|
|
||||||
|
settings.addLog('request', { type: 'refinement', ...requestBody });
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await invoke<string>('translate', {
|
||||||
|
apiAddress: apiBaseUrl,
|
||||||
|
apiKey: apiKey,
|
||||||
|
payload: requestBody
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!settings.enableStreaming) {
|
||||||
|
targetText.value = response;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Track applied suggestions and clear selection
|
||||||
|
if (evaluationResult.value?.suggestions) {
|
||||||
|
appliedSuggestionIds.value.push(...selectedSuggestionIds.value);
|
||||||
|
selectedSuggestionIds.value = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
settings.addLog('response', 'Refinement completed');
|
||||||
|
} catch (err: any) {
|
||||||
|
const errorMsg = String(err);
|
||||||
|
settings.addLog('error', errorMsg);
|
||||||
|
targetText.value = `Error: ${errorMsg}`;
|
||||||
|
} finally {
|
||||||
|
isRefining.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const translate = async () => {
|
const translate = async () => {
|
||||||
if (!sourceText.value.trim() || isTranslating.value) return;
|
if (!sourceText.value.trim() || isTranslating.value) return;
|
||||||
|
|
||||||
@@ -324,11 +423,6 @@ 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);
|
||||||
@@ -336,6 +430,11 @@ const translate = async () => {
|
|||||||
} finally {
|
} finally {
|
||||||
isTranslating.value = false;
|
isTranslating.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Trigger evaluation if enabled
|
||||||
|
if (settings.enableEvaluation) {
|
||||||
|
await evaluateTranslation();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -458,7 +557,7 @@ const translate = async () => {
|
|||||||
|
|
||||||
<div class="p-4 border-t dark:border-slate-800 bg-slate-50/30 dark:bg-transparent flex justify-end shrink-0"> <button
|
<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 || isEvaluating || isRefining || !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"
|
||||||
>
|
>
|
||||||
<Loader2 v-if="isTranslating" class="w-4 h-4 animate-spin" />
|
<Loader2 v-if="isTranslating" class="w-4 h-4 animate-spin" />
|
||||||
@@ -609,9 +708,10 @@ 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>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Evaluation Results -->
|
<!-- 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 v-if="isEvaluating || evaluationResult" class="px-6 py-4 bg-slate-200/20 dark:bg-slate-800/20 border-t border-dashed dark:border-slate-800 space-y-4 animate-in fade-in slide-in-from-bottom-2 duration-500 overflow-y-auto max-h-80 shrink-0">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<div :class="cn(
|
<div :class="cn(
|
||||||
@@ -639,26 +739,83 @@ const translate = async () => {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="evaluationResult.improvements" class="space-y-2 pt-2">
|
<div v-if="evaluationResult.suggestions && evaluationResult.suggestions.length > 0" class="space-y-4 pt-2">
|
||||||
|
<!-- Pending Suggestions -->
|
||||||
|
<div v-if="evaluationResult.suggestions.some(s => !appliedSuggestionIds.includes(s.id))" class="space-y-2">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<div class="w-2 h-2 rounded-full bg-blue-500"></div>
|
<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>
|
<h3 class="text-xs font-bold text-slate-400 uppercase tracking-widest">修改建议</h3>
|
||||||
</div>
|
</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">
|
<div class="space-y-2">
|
||||||
<p class="text-xs text-slate-600 dark:text-slate-300 leading-relaxed">
|
<div
|
||||||
{{ evaluationResult.improvements }}
|
v-for="sug in evaluationResult.suggestions.filter(s => !appliedSuggestionIds.includes(s.id))"
|
||||||
</p>
|
:key="sug.id"
|
||||||
|
@click="toggleSuggestion(sug.id)"
|
||||||
|
:class="cn(
|
||||||
|
'flex items-start gap-3 p-3 rounded-xl border transition-all cursor-pointer group',
|
||||||
|
selectedSuggestionIds?.includes(sug.id)
|
||||||
|
? 'bg-blue-50 dark:bg-blue-900/20 border-blue-200 dark:border-blue-800'
|
||||||
|
: 'bg-white dark:bg-slate-800/40 border-slate-100 dark:border-slate-800 hover:border-slate-300 dark:hover:border-slate-700'
|
||||||
|
)"
|
||||||
|
>
|
||||||
|
<div :class="cn(
|
||||||
|
'w-4 h-4 rounded border mt-0.5 shrink-0 flex items-center justify-center transition-colors',
|
||||||
|
selectedSuggestionIds?.includes(sug.id) ? 'bg-blue-600 border-blue-600' : 'border-slate-300 dark:border-slate-600'
|
||||||
|
)">
|
||||||
|
<Check v-if="selectedSuggestionIds?.includes(sug.id)" class="w-3.5 h-3.5 text-white" stroke-width="4" />
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 space-y-1.5 min-w-0">
|
||||||
|
<p class="text-xs text-slate-700 dark:text-slate-200 leading-normal">{{ sug.text }}</p>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<div class="flex-1 h-1 bg-slate-100 dark:bg-slate-700 rounded-full overflow-hidden">
|
||||||
|
<div
|
||||||
|
class="h-full rounded-full transition-all duration-1000"
|
||||||
|
:class="sug.importance >= 80 ? 'bg-red-500' : sug.importance >= 40 ? 'bg-amber-500' : 'bg-blue-500'"
|
||||||
|
:style="{ width: `${sug.importance}%` }"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
<span class="text-[9px] font-bold opacity-40 uppercase tracking-tighter w-8 shrink-0">{{ sug.importance }}%</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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">
|
<!-- Applied Suggestions -->
|
||||||
|
<div v-if="appliedSuggestionIds.length > 0" class="space-y-2 border-t dark:border-slate-800 pt-4">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<div class="w-2 h-2 rounded-full bg-slate-400"></div>
|
||||||
|
<h3 class="text-xs font-bold text-slate-400 uppercase tracking-widest">已润色</h3>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<div
|
||||||
|
v-for="sug in evaluationResult.suggestions.filter(s => appliedSuggestionIds.includes(s.id))"
|
||||||
|
:key="'applied-' + sug.id"
|
||||||
|
class="p-3 rounded-xl border border-slate-100 dark:border-slate-800/60 bg-white/30 dark:bg-slate-800/20 opacity-70"
|
||||||
|
>
|
||||||
|
<p class="text-xs text-slate-500 dark:text-slate-400 leading-normal">{{ sug.text }}</p>
|
||||||
|
</div>
|
||||||
|
</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 gap-2 shrink-0">
|
||||||
|
<button
|
||||||
|
@click="refineTranslation"
|
||||||
|
v-if="evaluationResult && evaluationResult.suggestions && evaluationResult.suggestions.length > 0"
|
||||||
|
:disabled="isRefining || isEvaluating || isTranslating || selectedSuggestionIds.length === 0"
|
||||||
|
class="bg-blue-600 enabled: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"
|
||||||
|
>
|
||||||
|
<Loader2 v-if="isRefining" class="w-4 h-4 animate-spin" />
|
||||||
|
<Save v-else class="w-4 h-4" />
|
||||||
|
{{ isRefining ? '正在润色...' : '润色' }}
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
@click="evaluateTranslation"
|
@click="evaluateTranslation"
|
||||||
:disabled="isEvaluating || isTranslating || !targetText.trim()"
|
:disabled="isEvaluating || isTranslating || isRefining || !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"
|
class="bg-blue-600 enabled: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"
|
||||||
>
|
>
|
||||||
<Loader2 v-if="isEvaluating" class="w-4 h-4 animate-spin" />
|
<Loader2 v-if="isEvaluating" class="w-4 h-4 animate-spin" />
|
||||||
<Check v-else class="w-4 h-4" />
|
<Check v-else class="w-4 h-4" />
|
||||||
@@ -881,7 +1038,7 @@ const translate = async () => {
|
|||||||
<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="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
|
||||||
@@ -896,7 +1053,7 @@ const translate = async () => {
|
|||||||
|
|
||||||
<div class="space-y-2 border-t dark:border-slate-800 pt-6">
|
<div class="space-y-2 border-t dark:border-slate-800 pt-6">
|
||||||
<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.evaluationPromptTemplate = DEFAULT_EVALUATION_TEMPLATE" class="text-xs text-blue-600 dark:text-blue-400 hover:underline">恢复默认值</button>
|
<button @click="settings.evaluationPromptTemplate = DEFAULT_EVALUATION_TEMPLATE" class="text-xs text-blue-600 dark:text-blue-400 hover:underline">恢复默认值</button>
|
||||||
</div>
|
</div>
|
||||||
<textarea
|
<textarea
|
||||||
@@ -905,7 +1062,22 @@ const translate = async () => {
|
|||||||
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}', '{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>
|
<span v-for="tag in ['{SOURCE_LANG}', '{TARGET_LANG}', '{SPEAKER_IDENTITY}', '{TONE_REGISTER}', '{CONTEXT}']" :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 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.refinementPromptTemplate = DEFAULT_REFINEMENT_TEMPLATE" class="text-xs text-blue-600 dark:text-blue-400 hover:underline">恢复默认值</button>
|
||||||
|
</div>
|
||||||
|
<textarea
|
||||||
|
v-model="settings.refinementPromptTemplate"
|
||||||
|
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}']" :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>
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ref } from 'vue';
|
import { ref, computed } from 'vue';
|
||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
import { useLocalStorage } from '@vueuse/core';
|
import { useLocalStorage } from '@vueuse/core';
|
||||||
|
|
||||||
@@ -51,8 +51,45 @@ 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}.
|
export const DEFAULT_EVALUATION_TEMPLATE = `# Role
|
||||||
Your task is to critically evaluate the accuracy and quality of a translation.
|
You are an **Objective Translation Auditor**. Your task is to evaluate translation quality based on accuracy, grammar, and fundamental readability. Avoid pedantic nitpicking over synonyms, but do point out issues that hinder professional quality.
|
||||||
|
|
||||||
|
# Context Info
|
||||||
|
- **Source Language**: {SOURCE_LANG}
|
||||||
|
- **Target Language**: {TARGET_LANG}
|
||||||
|
- **Speaker Identity**: {SPEAKER_IDENTITY}
|
||||||
|
- **Intended Tone/Register**: {TONE_REGISTER}
|
||||||
|
- **Context**: {CONTEXT}
|
||||||
|
|
||||||
|
# Audit Criteria
|
||||||
|
Only penalize and provide improvements if the translation meets one of these criteria:
|
||||||
|
1. **Semantic Error**: Objective misalignment with the source meaning or complete hallucinations.
|
||||||
|
2. **Grammatical Error**: Clear violations of target language grammar or syntax rules.
|
||||||
|
3. **Tone Failure**: A tone that is the opposite or significantly different from the [Intended Tone/Register].
|
||||||
|
4. **Poor Readability**: The phrasing is so stiff, unnatural, or convoluted that it hinders smooth comprehension (e.g., obvious "translationese" or broken logic).
|
||||||
|
|
||||||
|
**Note**: Do NOT penalize if the translation is simply "not the most elegant" or if there's a subjective preference for a different synonym. If it's natural enough for a native speaker to understand without effort, it's acceptable.
|
||||||
|
|
||||||
|
# Instructions
|
||||||
|
1. **Evaluation**: Compare the [Source Text] and [Translated Text] based on the [Audit Criteria].
|
||||||
|
2. **Scoring Strategy**:
|
||||||
|
- **90-100**: Accurate, grammatically sound, and flows naturally.
|
||||||
|
- **75-89**: Accurate meaning, but suffers from "stiff" phrasing or minor flow issues that need adjustment.
|
||||||
|
- **Below 75**: Contains semantic errors, severe grammar issues, or tone mismatches.
|
||||||
|
3. **Analysis**: Provide a concise explanation in Simplified Chinese. Focus on *why* the error matters (e.g., "meaning is reversed" or "too awkward to read").
|
||||||
|
4. **Suggestions**: Provide a list of specific, actionable suggestions. For each, assign an "importance" from 0 to 100 (0 = unnecessary/optional, 100 = critical error).
|
||||||
|
|
||||||
|
# Output Format
|
||||||
|
Respond ONLY in JSON format. The "analysis" and "text" within "suggestions" MUST be in Simplified Chinese:
|
||||||
|
{
|
||||||
|
"score": number,
|
||||||
|
"analysis": "string",
|
||||||
|
"suggestions": [
|
||||||
|
{ "id": 1, "text": "suggestion text", "importance": number }
|
||||||
|
]
|
||||||
|
}`;
|
||||||
|
|
||||||
|
export const DEFAULT_REFINEMENT_TEMPLATE = `You are a senior translation editor. Your task is to refine the [Current Translation] based on specific [User Feedback], while strictly maintaining the original meaning of the [Source Text] and adhering to the established context.
|
||||||
|
|
||||||
[Context Info]
|
[Context Info]
|
||||||
- Source Language: {SOURCE_LANG}
|
- Source Language: {SOURCE_LANG}
|
||||||
@@ -61,26 +98,12 @@ Your task is to critically evaluate the accuracy and quality of a translation.
|
|||||||
- Intended Tone/Register: {TONE_REGISTER}
|
- Intended Tone/Register: {TONE_REGISTER}
|
||||||
- Context: {CONTEXT}
|
- Context: {CONTEXT}
|
||||||
|
|
||||||
[Input]
|
|
||||||
- Source Text: {SOURCE_TEXT}
|
|
||||||
- Translated Text: {TRANSLATED_TEXT}
|
|
||||||
|
|
||||||
[Instructions]
|
[Instructions]
|
||||||
1. Compare the [Source Text] and [Translated Text] meticulously.
|
1. Carefully review the [User Feedback] and apply the requested improvements to the [Current Translation].
|
||||||
2. Check if the translation respects the [Context Info].
|
2. Ensure that the refined translation remains semantically identical to the [Source Text].
|
||||||
3. Assign an "Accuracy Score" from 0 to 100.
|
3. Maintain the [Speaker Identity] and [Intended Tone/Register] as specified.
|
||||||
- Give 0 if there are fatal semantic errors, complete hallucinations, or if the meaning is reversed.
|
4. If a piece of feedback contradicts the [Source Text], prioritize accuracy and provide a balanced refinement.
|
||||||
- Deduct points for minor inaccuracies, unnatural phrasing, or tone mismatches.
|
5. Produce ONLY the refined {TARGET_LANG} translation, without any additional explanations, notes, or commentary.`;
|
||||||
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;
|
||||||
@@ -102,13 +125,25 @@ export const useSettingsStore = defineStore('settings', () => {
|
|||||||
const enableEvaluation = useLocalStorage('enable-evaluation', true);
|
const enableEvaluation = useLocalStorage('enable-evaluation', true);
|
||||||
const evaluationPromptTemplate = useLocalStorage('evaluation-prompt-template', DEFAULT_EVALUATION_TEMPLATE);
|
const evaluationPromptTemplate = useLocalStorage('evaluation-prompt-template', DEFAULT_EVALUATION_TEMPLATE);
|
||||||
const evaluationProfileId = useLocalStorage<string | null>('evaluation-profile-id', null);
|
const evaluationProfileId = useLocalStorage<string | null>('evaluation-profile-id', null);
|
||||||
|
const refinementPromptTemplate = useLocalStorage('refinement-prompt-template', DEFAULT_REFINEMENT_TEMPLATE);
|
||||||
|
|
||||||
// 存储整个对象以保持一致性
|
// 存储整个对象以保持一致性
|
||||||
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]);
|
||||||
|
|
||||||
const speakerIdentity = useLocalStorage('speaker-identity', SPEAKER_IDENTITY_OPTIONS[0].value);
|
// 按源语言分别存储身份和语气,实现基于语言自动切换
|
||||||
const toneRegister = useLocalStorage('tone-register', TONE_REGISTER_OPTIONS[2].value);
|
const speakerIdentityMap = useLocalStorage<Record<string, string>>('speaker-identity-map', {});
|
||||||
|
const toneRegisterMap = useLocalStorage<Record<string, string>>('tone-register-map', {});
|
||||||
|
|
||||||
|
const speakerIdentity = computed({
|
||||||
|
get: () => speakerIdentityMap.value[sourceLang.value.code] || SPEAKER_IDENTITY_OPTIONS[0].value,
|
||||||
|
set: (val) => { speakerIdentityMap.value[sourceLang.value.code] = val; }
|
||||||
|
});
|
||||||
|
|
||||||
|
const toneRegister = computed({
|
||||||
|
get: () => toneRegisterMap.value[sourceLang.value.code] || TONE_REGISTER_OPTIONS[2].value,
|
||||||
|
set: (val) => { toneRegisterMap.value[sourceLang.value.code] = val; }
|
||||||
|
});
|
||||||
|
|
||||||
const logs = ref<{ timestamp: string; type: 'request' | 'response' | 'error'; content: any }[]>([]);
|
const logs = ref<{ timestamp: string; type: 'request' | 'response' | 'error'; content: any }[]>([]);
|
||||||
|
|
||||||
@@ -135,6 +170,7 @@ export const useSettingsStore = defineStore('settings', () => {
|
|||||||
enableEvaluation,
|
enableEvaluation,
|
||||||
evaluationPromptTemplate,
|
evaluationPromptTemplate,
|
||||||
evaluationProfileId,
|
evaluationProfileId,
|
||||||
|
refinementPromptTemplate,
|
||||||
sourceLang,
|
sourceLang,
|
||||||
targetLang,
|
targetLang,
|
||||||
speakerIdentity,
|
speakerIdentity,
|
||||||
|
|||||||
Reference in New Issue
Block a user