diff --git a/spec/prd.md b/spec/prd.md deleted file mode 100644 index 165143e..0000000 --- a/spec/prd.md +++ /dev/null @@ -1,85 +0,0 @@ -# PRD: GemmaTrans Desktop Client - -## 1. Project Overview -**GemmaTrans** is a lightweight, high-performance desktop translation client built with **Tauri** and **Vue 3**. It is designed to act as a modern frontend for the **TranslateGemma** model running on a local or remote **Ollama** instance (including over private networks like Tailscale). The application prioritizes speed, privacy, and a professional "Google Translate" aesthetic. - ---- - -## 2. Technical Stack -* **Core Framework:** [Tauri](https://tauri.app/) (Rust backend for system-level API access). -* **Frontend Framework:** [Vue 3](https://vuejs.org/) (Composition API). -* **Styling:** [Tailwind CSS](https://tailwindcss.com/) (Minimalist, modern utility-first CSS). -* **Package Manager:** `pnpm`. -* **State Management:** [Pinia](https://pinia.vuejs.org/) (To persist user settings and prompt templates). -* **Icons:** [Lucide Vue Next](https://lucide.dev/). - ---- - -## 3. User Interface (UI) Requirements -The UI should be **Modern, Minimalist, and Functional**, mimicking the clean layout of Google Translate. - -### 3.1 Main Translation Interface -* **Split-Pane Layout:** - * **Left (Source):** Input text area with a header for language selection. - * **Right (Target):** Read-only text area for the translation result, with a header for the target language selection. -* **Language Selection:** Dropdowns supporting both **Full Name** (e.g., English) and **ISO Code** (e.g., en). -* **Actions:** * "Translate" button (with loading state). - * "Copy" button on the target pane. - * "Clear" button on the source pane. - -### 3.2 Settings View -* **API Configuration:** A text input for the `Ollama API Address` (e.g., `http://100.x.y.z:11434`). -* **Output Control:** A toggle switch for `Enable Streaming` (renders text as it is generated). -* **Prompt Engineering:** A large text area for the `System Prompt Template`. - ---- - -## 4. Functional Requirements - -### 4.1 Prompt Engineering Engine -The application must dynamically inject variables into the user-provided template before sending the request to the Ollama API. - -**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. -> Produce only the {TARGET_LANG} translation, without any additional explanations or commentary. Please translate the following {SOURCE_LANG} text into {TARGET_LANG}: -> -> {TEXT} - -### 4.2 Template Variables -The engine must support and correctly replace the following tokens: -* `{SOURCE_LANG}`: Full name of the input language (e.g., "Chinese"). -* `{SOURCE_CODE}`: Short code of the input language (e.g., "zh-Hans"). -* `{TARGET_LANG}`: Full name of the output language (e.g., "English"). -* `{TARGET_CODE}`: Short code of the output language (e.g., "en"). -* `{TEXT}`: The actual content provided by the user. - -### 4.3 Ollama Integration -* Support for the `/api/generate` endpoint. -* Ability to handle both `stream: true` (progressive UI updates) and `stream: false` (batch updates). -* Support for connecting to any valid URL provided in Settings, ensuring compatibility with Tailscale IPs. - ---- - -## 5. Technical Implementation Details - -### 5.1 Language Mapping (Default Configuration) -The application will provide a default list of languages: -| Language | Code | -| :--- | :--- | -| English | en | -| Chinese | zh-Hans | -| Japanese | ja | -| Spanish | es | -| French | fr | -| German | de | - -### 5.2 Persistence -* All settings (API Address, Stream Toggle, and Custom Prompt Template) must be stored in the user's local application data folder to ensure they are remembered across restarts. - ---- - -## 6. Success Metrics -* **Connectivity:** Seamless communication with an Ollama instance over a Tailscale network. -* **UX:** The "Streaming" mode should feel responsive with no UI lag during high-token-per-second generation. - ---- diff --git a/src/App.vue b/src/App.vue index 8307439..3ae2245 100644 --- a/src/App.vue +++ b/src/App.vue @@ -12,11 +12,19 @@ import { FileText, ChevronDown, Sun, - Moon + Moon, + User, + Type } from 'lucide-vue-next'; import { invoke } from '@tauri-apps/api/core'; import { listen } from '@tauri-apps/api/event'; -import { useSettingsStore, LANGUAGES, DEFAULT_TEMPLATE } from './stores/settings'; +import { + useSettingsStore, + LANGUAGES, + DEFAULT_TEMPLATE, + SPEAKER_IDENTITY_OPTIONS, + TONE_REGISTER_OPTIONS +} from './stores/settings'; import pkg from '../package.json'; import { clsx, type ClassValue } from 'clsx'; import { twMerge } from 'tailwind-merge'; @@ -45,20 +53,29 @@ const view = ref<'translate' | 'settings' | 'logs'>('translate'); // Dropdown State const sourceDropdownOpen = ref(false); const targetDropdownOpen = ref(false); +const speakerDropdownOpen = ref(false); +const toneDropdownOpen = ref(false); const closeAllDropdowns = () => { sourceDropdownOpen.value = false; targetDropdownOpen.value = false; + speakerDropdownOpen.value = false; + toneDropdownOpen.value = false; }; -const toggleDropdown = (type: 'source' | 'target') => { - if (type === 'source') { - sourceDropdownOpen.value = !sourceDropdownOpen.value; - targetDropdownOpen.value = false; - } else { - targetDropdownOpen.value = !targetDropdownOpen.value; - sourceDropdownOpen.value = false; - } +const toggleDropdown = (type: 'source' | 'target' | 'speaker' | 'tone') => { + const states = { + source: sourceDropdownOpen, + target: targetDropdownOpen, + speaker: speakerDropdownOpen, + tone: toneDropdownOpen + }; + + const targetState = states[type]; + const currentValue = targetState.value; + + closeAllDropdowns(); + targetState.value = !currentValue; }; const handleGlobalClick = (e: MouseEvent) => { @@ -116,6 +133,14 @@ const targetLangCode = computed({ const sourceLang = computed(() => settings.sourceLang); const targetLang = computed(() => settings.targetLang); +const currentSpeakerLabel = computed(() => { + return SPEAKER_IDENTITY_OPTIONS.find(opt => opt.value === settings.speakerIdentity)?.label || '男性'; +}); + +const currentToneLabel = computed(() => { + return TONE_REGISTER_OPTIONS.find(opt => opt.value === settings.toneRegister)?.label || '正式专业'; +}); + const swapLanguages = () => { const temp = { ...settings.sourceLang }; settings.sourceLang = { ...settings.targetLang }; @@ -150,6 +175,8 @@ const translate = async () => { .replace(/{SOURCE_CODE}/g, sourceLang.value.code) .replace(/{TARGET_LANG}/g, targetLang.value.englishName) .replace(/{TARGET_CODE}/g, targetLang.value.code) + .replace(/{SPEAKER_IDENTITY}/g, settings.speakerIdentity) + .replace(/{TONE_REGISTER}/g, settings.toneRegister) .replace(/{TEXT}/g, sourceText.value); const requestBody = { @@ -328,6 +355,94 @@ const translate = async () => { + + +
+ + + +
+ +
+
+
+ + +
+ + + +
+ +
+
+
+
diff --git a/src/stores/settings.ts b/src/stores/settings.ts index 485f1db..1a05073 100644 --- a/src/stores/settings.ts +++ b/src/stores/settings.ts @@ -25,7 +25,29 @@ export const LANGUAGES: Language[] = [ { 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: '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. + +Translation Context & Style Constraints: +Speaker Identity: {SPEAKER_IDENTITY}. Ensure all grammatical agreements and self-referential terms in {TARGET_LANG} reflect this. +Tone & Register: {TONE_REGISTER}. + Produce only the {TARGET_LANG} translation, without any additional explanations or commentary. Please translate the following {SOURCE_LANG} text into {TARGET_LANG}: @@ -42,6 +64,9 @@ export const useSettingsStore = defineStore('settings', () => { const sourceLang = useLocalStorage('source-lang-v2', LANGUAGES[0]); const targetLang = useLocalStorage('target-lang-v2', LANGUAGES[4]); + const speakerIdentity = useLocalStorage('speaker-identity', SPEAKER_IDENTITY_OPTIONS[0].value); + const toneRegister = useLocalStorage('tone-register', TONE_REGISTER_OPTIONS[0].value); + const logs = ref<{ timestamp: string; type: 'request' | 'response' | 'error'; content: any }[]>([]); const addLog = (type: 'request' | 'response' | 'error', content: any) => { @@ -64,6 +89,8 @@ export const useSettingsStore = defineStore('settings', () => { systemPromptTemplate, sourceLang, targetLang, + speakerIdentity, + toneRegister, logs, addLog };