support refinement
This commit is contained in:
165
src/App.vue
165
src/App.vue
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user