support refinement
This commit is contained in:
165
src/App.vue
165
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,38 @@ 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 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 +243,7 @@ const evaluateTranslation = async () => {
|
|||||||
|
|
||||||
isEvaluating.value = true;
|
isEvaluating.value = true;
|
||||||
evaluationResult.value = null;
|
evaluationResult.value = null;
|
||||||
|
selectedSuggestionIds.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;
|
||||||
@@ -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 () => {
|
const translate = async () => {
|
||||||
if (!sourceText.value.trim() || isTranslating.value) return;
|
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
|
<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 || 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"
|
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" />
|
||||||
@@ -640,24 +730,62 @@ 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-2 pt-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 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 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>
|
||||||
</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
|
<button
|
||||||
@click="evaluateTranslation"
|
@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"
|
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" />
|
<Loader2 v-if="isEvaluating" class="w-4 h-4 animate-spin" />
|
||||||
@@ -909,6 +1037,21 @@ const translate = async () => {
|
|||||||
</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.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>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</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.
|
- **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.
|
- **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").
|
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
|
# 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,
|
"score": number,
|
||||||
"analysis": "string",
|
"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 {
|
export interface ApiProfile {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
@@ -107,6 +125,7 @@ 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]);
|
||||||
@@ -140,6 +159,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