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>
|
||||
|
||||
Reference in New Issue
Block a user