support refinement

This commit is contained in:
Julian Freeman
2026-02-25 13:22:39 -04:00
parent f20a9bb851
commit dd06a3159b
2 changed files with 177 additions and 14 deletions

View File

@@ -26,6 +26,7 @@ import {
LANGUAGES,
DEFAULT_TEMPLATE,
DEFAULT_EVALUATION_TEMPLATE,
DEFAULT_REFINEMENT_TEMPLATE,
SPEAKER_IDENTITY_OPTIONS,
TONE_REGISTER_OPTIONS,
type ApiProfile
@@ -138,20 +139,38 @@ const targetText = ref('');
const isTranslating = ref(false);
const showCopyFeedback = ref(false);
interface Suggestion {
id: number;
text: string;
importance: number;
}
interface EvaluationResult {
score: number;
analysis: string;
improvements?: string;
suggestions?: Suggestion[];
}
const evaluationResult = ref<EvaluationResult | null>(null);
const isEvaluating = ref(false);
const isRefining = ref(false);
const selectedSuggestionIds = 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;
onMounted(async () => {
unlisten = await listen<string>('translation-chunk', (event) => {
if (isTranslating.value) {
if (isTranslating.value || isRefining.value) {
targetText.value += event.payload;
}
});
@@ -224,6 +243,7 @@ const evaluateTranslation = async () => {
isEvaluating.value = true;
evaluationResult.value = null;
selectedSuggestionIds.value = [];
// Determine which API config to use for evaluation
let apiBaseUrl = settings.apiBaseUrl;
@@ -282,6 +302,76 @@ 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;
}
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 () => {
if (!sourceText.value.trim() || isTranslating.value) return;
@@ -458,7 +548,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
@click="translate"
:disabled="isTranslating || isEvaluating || !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"
>
<Loader2 v-if="isTranslating" class="w-4 h-4 animate-spin" />
@@ -640,24 +730,62 @@ const translate = async () => {
</p>
</div>
<div v-if="evaluationResult.improvements" class="space-y-2 pt-2">
<div v-if="evaluationResult.suggestions && evaluationResult.suggestions.length > 0" 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>
<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 class="space-y-2">
<div
v-for="sug in evaluationResult.suggestions"
: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 class="p-4 border-t dark:border-slate-800 bg-slate-50/30 dark:bg-transparent flex justify-end shrink-0">
<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
@click="evaluateTranslation"
:disabled="isEvaluating || isTranslating || !targetText.trim()"
:disabled="isEvaluating || isTranslating || isRefining || !targetText.trim()"
class="bg-slate-200 enabled:hover:bg-slate-300 dark:bg-slate-800 dark:enabled: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" />
@@ -909,6 +1037,21 @@ const translate = async () => {
</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>
</section>
</div>

View File

@@ -77,16 +77,34 @@ Only penalize and provide improvements if the translation meets one of these cri
- **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. **Improvements**: Provide suggestions in Simplified Chinese. If the score is 95+, this field can be an empty string "".
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 "improvements" MUST be in Simplified Chinese:
Respond ONLY in JSON format. The "analysis" and "text" within "suggestions" MUST be in Simplified Chinese:
{
"score": number,
"analysis": "string",
"improvements": "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]
- Source Language: {SOURCE_LANG}
- Target Language: {TARGET_LANG}
- Speaker Identity: {SPEAKER_IDENTITY}
- Intended Tone/Register: {TONE_REGISTER}
- Context: {CONTEXT}
[Instructions]
1. Carefully review the [User Feedback] and apply the requested improvements to the [Current Translation].
2. Ensure that the refined translation remains semantically identical to the [Source Text].
3. Maintain the [Speaker Identity] and [Intended Tone/Register] as specified.
4. If a piece of feedback contradicts the [Source Text], prioritize accuracy and provide a balanced refinement.
5. Produce ONLY the refined {TARGET_LANG} translation, without any additional explanations, notes, or commentary.`;
export interface ApiProfile {
id: string;
name: string;
@@ -107,6 +125,7 @@ export const useSettingsStore = defineStore('settings', () => {
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 refinementPromptTemplate = useLocalStorage('refinement-prompt-template', DEFAULT_REFINEMENT_TEMPLATE);
// 存储整个对象以保持一致性
const sourceLang = useLocalStorage<Language>('source-lang-v2', LANGUAGES[0]);
@@ -140,6 +159,7 @@ export const useSettingsStore = defineStore('settings', () => {
enableEvaluation,
evaluationPromptTemplate,
evaluationProfileId,
refinementPromptTemplate,
sourceLang,
targetLang,
speakerIdentity,