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 () => { + + +