348 lines
15 KiB
TypeScript
348 lines
15 KiB
TypeScript
import { ref, computed } from 'vue';
|
|
import { defineStore } from 'pinia';
|
|
import { useLocalStorage } from '@vueuse/core';
|
|
|
|
export interface Language {
|
|
displayName: string; // UI 显示的中文名,如 "英语(英国)"
|
|
englishName: string; // 文件中的第二列,用于 {SOURCE_LANG}
|
|
code: string; // 文件中的第一列,用于 {SOURCE_CODE}
|
|
}
|
|
|
|
export const LANGUAGES: Language[] = [
|
|
{ displayName: '中文(简体)', englishName: 'Simplified Chinese', code: 'zh-Hans' },
|
|
{ displayName: '中文(繁体)', englishName: 'Traditional Chinese', code: 'zh-Hant' },
|
|
{ displayName: '英语(美国)', englishName: 'American English', code: 'en-US' },
|
|
{ displayName: '英语(英国)', englishName: 'British English', code: 'en-GB' },
|
|
{ displayName: '西班牙语', englishName: 'Spanish', code: 'es' },
|
|
{ displayName: '葡萄牙语', englishName: 'Portuguese', code: 'pt' },
|
|
{ displayName: '日语', englishName: 'Japanese', code: 'ja' },
|
|
{ displayName: '韩语', englishName: 'Korean', code: 'ko' },
|
|
{ displayName: '法语', englishName: 'French', code: 'fr' },
|
|
{ displayName: '德语', englishName: 'German', code: 'de' },
|
|
{ displayName: '意大利语', englishName: 'Italian', code: 'it' },
|
|
{ displayName: '俄语', englishName: 'Russian', code: 'ru' },
|
|
{ displayName: '越南语', englishName: 'Vietnamese', code: 'vi' },
|
|
{ displayName: '泰语', englishName: 'Thai', code: 'th' },
|
|
{ displayName: '阿拉伯语', englishName: 'Arabic', code: 'ar' },
|
|
];
|
|
|
|
export const SPEAKER_IDENTITY_OPTIONS = [
|
|
{ label: '男性', value: 'Male' },
|
|
{ label: '女性', value: 'Female' },
|
|
{ label: '中性', value: 'Gender-neutral' },
|
|
];
|
|
|
|
export const TONE_REGISTER_OPTIONS = [
|
|
{ label: '自动识别', value: 'Auto-detect', description: '分析并保持原文的语气、情绪和礼貌程度' },
|
|
{ label: '正式专业', value: 'Formal & Professional', description: '商务邮件、法律合同、官方报告' },
|
|
{ label: '礼貌客气', value: 'Polite & Respectful', description: '与长辈、客户或初次见面的人交流' },
|
|
{ label: '礼貌随和', value: 'Polite & Conversational', description: '得体但不刻板的日常对话' },
|
|
{ label: '中性标准', value: 'Neutral & Standard', description: '维基百科、说明书、客观的新闻报道' },
|
|
{ label: '非正式', value: 'Casual & Informal', description: '朋友聊天、社交媒体、非正式简讯' },
|
|
{ label: '亲切友好', value: 'Warm & Friendly', description: '社区信函、给朋友的建议、温馨提示' },
|
|
{ label: '严谨权威', value: 'Strict & Authoritative', description: '警示标志、强制规定、上级指令' },
|
|
{ label: '热情生动', value: 'Enthusiastic & Vivid', description: '广告文案、旅游推荐、博主推文' },
|
|
];
|
|
|
|
export const DEFAULT_TEMPLATE = `You are a professional {SOURCE_LANG} ({SOURCE_CODE}) to {TARGET_LANG} ({TARGET_CODE}) translator. Your goal is to accurately convey the meaning and nuances of the original {SOURCE_LANG} text while adhering to {TARGET_LANG} grammar, vocabulary, and cultural sensitivities.
|
|
|
|
[Constraints]
|
|
1. Speaker Identity: {SPEAKER_IDENTITY}. Ensure all grammatical agreements and self-referential terms in {TARGET_LANG} reflect this.
|
|
2. Tone & Register: {TONE_REGISTER}. (If set to 'Auto-detect', analyze the tone, formality, and emotional nuance of the source text and faithfully replicate it. Do not neutralize strong emotions or unique styles.)
|
|
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].`;
|
|
|
|
export const DEFAULT_EVALUATION_TEMPLATE = `# Role
|
|
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]
|
|
- 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;
|
|
apiBaseUrl: string;
|
|
apiKey: string;
|
|
modelName: string;
|
|
}
|
|
|
|
export interface HistoryItem {
|
|
id: string;
|
|
timestamp: string;
|
|
sourceLang: Language;
|
|
targetLang: Language;
|
|
sourceText: string;
|
|
targetText: string;
|
|
context: string;
|
|
speakerIdentity: string;
|
|
toneRegister: string;
|
|
modelName: string;
|
|
}
|
|
|
|
export interface Participant {
|
|
name: string;
|
|
gender: string;
|
|
language: Language;
|
|
tone: string;
|
|
}
|
|
|
|
export interface ChatMessage {
|
|
id: string;
|
|
sender: 'me' | 'partner';
|
|
original: string;
|
|
translated: string;
|
|
timestamp: string;
|
|
}
|
|
|
|
export interface ChatSession {
|
|
id: string;
|
|
title: string;
|
|
me: Participant;
|
|
partner: Participant;
|
|
messages: ChatMessage[];
|
|
lastActivity: string;
|
|
}
|
|
|
|
export const CONVERSATION_SYSTEM_PROMPT_TEMPLATE = `You are a professional real-time conversation translator.
|
|
Current Context:
|
|
- Role A (Me): {ME_NAME}, Gender: {ME_GENDER}, Language: {ME_LANG}.
|
|
- Role B (Partner): {PART_NAME}, Gender: {PART_GENDER}, Language: {PART_LANG}.
|
|
|
|
[Conversation History]
|
|
{HISTORY_BLOCK}
|
|
|
|
[Current Task]
|
|
Translate the incoming text from {FROM_LANG} to {TO_LANG}.
|
|
|
|
[Constraints]
|
|
1. Contextual Awareness: Use the [Conversation History] to resolve pronouns (it, that, etc.) and maintain consistency.
|
|
2. Tone & Register:
|
|
- If translating for 'Me', strictly use the tone: {MY_TONE}.
|
|
- If translating for 'Partner', auto-detect and preserve their original tone/emotion.
|
|
3. Natural Flow: Keep the translation concise and natural for a chat environment. Avoid "translationese".
|
|
4. Strictly avoid over-translation: Do not add extra information not present in the source text.
|
|
5. Output ONLY the translated text, no explanations.`;
|
|
|
|
export const useSettingsStore = defineStore('settings', () => {
|
|
const isDark = useLocalStorage('is-dark', false);
|
|
const apiBaseUrl = useLocalStorage('api-base-url', 'http://localhost:11434/v1');
|
|
const apiKey = useLocalStorage('api-key', '');
|
|
const modelName = useLocalStorage('model-name', 'translategemma:12b');
|
|
const profiles = useLocalStorage<ApiProfile[]>('api-profiles', []);
|
|
const enableStreaming = useLocalStorage('enable-streaming', true);
|
|
const systemPromptTemplate = useLocalStorage('system-prompt-template', DEFAULT_TEMPLATE);
|
|
|
|
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]);
|
|
const targetLang = useLocalStorage<Language>('target-lang-v2', LANGUAGES[4]);
|
|
|
|
// 按源语言分别存储身份和语气,实现基于语言自动切换
|
|
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[0].value,
|
|
set: (val) => { toneRegisterMap.value[sourceLang.value.code] = val; }
|
|
});
|
|
|
|
const logs = ref<{ id: string; timestamp: string; type: 'request' | 'response' | 'error'; content: any; curl?: string }[]>([]);
|
|
const history = useLocalStorage<HistoryItem[]>('translation-history-v1', []);
|
|
|
|
// 对话模式状态
|
|
const chatSessions = useLocalStorage<ChatSession[]>('chat-sessions-v1', []);
|
|
const activeSessionId = useLocalStorage<string | null>('active-session-id-v1', null);
|
|
|
|
const addLog = (type: 'request' | 'response' | 'error', content: any, curl?: string) => {
|
|
const now = new Date();
|
|
const timestamp = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')} ${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}:${String(now.getSeconds()).padStart(2, '0')}`;
|
|
|
|
logs.value.unshift({
|
|
id: crypto.randomUUID(),
|
|
timestamp,
|
|
type,
|
|
content,
|
|
curl
|
|
});
|
|
if (logs.value.length > 50) logs.value.pop(); // 稍微增加日志保留数量
|
|
};
|
|
|
|
const addHistory = (item: Omit<HistoryItem, 'id' | 'timestamp'>) => {
|
|
const now = new Date();
|
|
const timestamp = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')} ${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}:${String(now.getSeconds()).padStart(2, '0')}`;
|
|
const id = crypto.randomUUID();
|
|
|
|
history.value.unshift({
|
|
...item,
|
|
id,
|
|
timestamp
|
|
});
|
|
|
|
// 限制 100 条
|
|
if (history.value.length > 100) {
|
|
history.value = history.value.slice(0, 100);
|
|
}
|
|
|
|
return id;
|
|
};
|
|
|
|
const updateHistoryItem = (id: string, updates: Partial<HistoryItem>) => {
|
|
const index = history.value.findIndex(h => h.id === id);
|
|
if (index !== -1) {
|
|
history.value[index] = { ...history.value[index], ...updates };
|
|
}
|
|
};
|
|
|
|
// 对话模式方法
|
|
const createSession = (me: Participant, partner: Participant) => {
|
|
const id = crypto.randomUUID();
|
|
const now = new Date();
|
|
const timestamp = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')} ${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}:${String(now.getSeconds()).padStart(2, '0')}`;
|
|
|
|
const newSession: ChatSession = {
|
|
id,
|
|
title: `${partner.name} 的对话`,
|
|
me,
|
|
partner,
|
|
messages: [],
|
|
lastActivity: timestamp
|
|
};
|
|
|
|
chatSessions.value.unshift(newSession);
|
|
activeSessionId.value = id;
|
|
return id;
|
|
};
|
|
|
|
const deleteSession = (id: string) => {
|
|
chatSessions.value = chatSessions.value.filter(s => s.id !== id);
|
|
if (activeSessionId.value === id) {
|
|
activeSessionId.value = chatSessions.value[0]?.id || null;
|
|
}
|
|
};
|
|
|
|
const addMessageToSession = (sessionId: string, sender: 'me' | 'partner', original: string, translated: string = '') => {
|
|
const session = chatSessions.value.find(s => s.id === sessionId);
|
|
if (session) {
|
|
const now = new Date();
|
|
const timestamp = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')} ${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}:${String(now.getSeconds()).padStart(2, '0')}`;
|
|
|
|
const newMessage: ChatMessage = {
|
|
id: crypto.randomUUID(),
|
|
sender,
|
|
original,
|
|
translated,
|
|
timestamp
|
|
};
|
|
|
|
session.messages.push(newMessage);
|
|
session.lastActivity = timestamp;
|
|
|
|
// 将活跃会话移至顶部
|
|
const index = chatSessions.value.findIndex(s => s.id === sessionId);
|
|
if (index > 0) {
|
|
const [s] = chatSessions.value.splice(index, 1);
|
|
chatSessions.value.unshift(s);
|
|
}
|
|
|
|
return newMessage.id;
|
|
}
|
|
return null;
|
|
};
|
|
|
|
const updateChatMessage = (sessionId: string, messageId: string, updates: Partial<ChatMessage>) => {
|
|
const session = chatSessions.value.find(s => s.id === sessionId);
|
|
if (session) {
|
|
const message = session.messages.find(m => m.id === messageId);
|
|
if (message) {
|
|
Object.assign(message, updates);
|
|
}
|
|
}
|
|
};
|
|
|
|
return {
|
|
isDark,
|
|
apiBaseUrl,
|
|
apiKey,
|
|
modelName,
|
|
profiles,
|
|
enableStreaming,
|
|
systemPromptTemplate,
|
|
enableEvaluation,
|
|
evaluationPromptTemplate,
|
|
evaluationProfileId,
|
|
refinementPromptTemplate,
|
|
sourceLang,
|
|
targetLang,
|
|
speakerIdentity,
|
|
toneRegister,
|
|
logs,
|
|
history,
|
|
chatSessions,
|
|
activeSessionId,
|
|
addLog,
|
|
addHistory,
|
|
updateHistoryItem,
|
|
createSession,
|
|
deleteSession,
|
|
addMessageToSession,
|
|
updateChatMessage
|
|
};
|
|
});
|