30 Commits

Author SHA1 Message Date
Julian Freeman
352047c123 support history 2026-03-25 13:27:50 -04:00
Julian Freeman
e0dd17bfff support auto-detect tone 2026-03-25 12:45:54 -04:00
Julian Freeman
c5890bd5f2 combine gender and tone with srclang 2026-03-25 12:28:12 -04:00
Julian Freeman
96be7c67f3 improve refine ui2 2026-02-25 14:06:18 -04:00
Julian Freeman
ae1028588b improve refine ui 2026-02-25 13:57:49 -04:00
Julian Freeman
dd06a3159b support refinement 2026-02-25 13:22:39 -04:00
Julian Freeman
f20a9bb851 improve eval prompt 2026-02-25 12:14:33 -04:00
Julian Freeman
8294c12d17 fix eval ui on the bottom 2026-02-25 11:55:55 -04:00
Julian Freeman
55de02f5e3 improve ui and temp 2026-02-25 11:38:18 -04:00
Julian Freeman
c1b3c3b142 improve eval ui 2026-02-25 00:33:34 -04:00
Julian Freeman
a8e14269af import eval display 2026-02-25 00:21:17 -04:00
Julian Freeman
f21366e55f improve evaluation 2026-02-25 00:00:07 -04:00
Julian Freeman
6ff6fd3f25 support evaluation 2026-02-24 23:27:06 -04:00
Julian Freeman
f6bc4152b0 change the left bottom 2026-02-24 22:27:19 -04:00
Julian Freeman
5b493ce767 make light softer 2 2026-02-24 22:23:58 -04:00
Julian Freeman
49b96eeaeb make light softer 2026-02-24 22:07:45 -04:00
Julian Freeman
d259b54f11 not store context 2026-02-24 22:00:56 -04:00
Julian Freeman
484dd17f1c fix ui 2026-02-24 21:49:58 -04:00
Julian Freeman
985cfd933a modify 2026-02-24 16:44:42 -04:00
Julian Freeman
d9f0af53c4 support provide context 2026-02-24 16:36:08 -04:00
Julian Freeman
f183cccd5b change name to ai translate client 2026-02-23 19:57:44 -04:00
Julian Freeman
44c98f1d44 fix ui 2026-02-23 19:17:44 -04:00
Julian Freeman
f7f7556b98 support api profiles 2026-02-23 18:54:59 -04:00
Julian Freeman
f84ee6ced7 support generic openai api 2026-02-23 18:33:13 -04:00
Julian Freeman
8df295cbf5 little modify 2026-02-23 17:24:13 -04:00
Julian Freeman
2c61c19093 add identity and tone 2026-02-23 17:10:46 -04:00
Julian Freeman
fb98ab5472 fix origin problem 2026-02-22 19:58:34 -04:00
Julian Freeman
ce358e5b76 fix ui height 2026-02-22 19:51:40 -04:00
Julian Freeman
48d9c6e240 change name 2026-02-22 18:37:51 -04:00
Julian Freeman
74fe954478 change icon and others 2026-02-22 18:27:50 -04:00
64 changed files with 1422 additions and 234 deletions

View File

@@ -1,7 +1,7 @@
# Tauri + Vue + TypeScript # AI Translate Client
This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more. 使用 AI 模型进行翻译的客户端。使用 Gemini 编写。
## Recommended IDE Setup 更新版本:手动修改 `package.json` 文件中的版本号,然后运行 `pnpm sync-version`
- [VS Code](https://code.visualstudio.com/) + [Vue - Official](https://marketplace.visualstudio.com/items?itemName=Vue.volar) + [Tauri](https://marketplace.visualstudio.com/items?itemName=tauri-apps.tauri-vscode) + [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer) 图标由 Gemini 生成。

BIN
app-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

View File

@@ -4,7 +4,7 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" /> <link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Tauri + Vue + Typescript App</title> <title>AI Translate Client</title>
</head> </head>
<body> <body>

View File

@@ -1,7 +1,7 @@
{ {
"name": "gemmatrans-client", "name": "ai-translate-client",
"private": true, "private": true,
"version": "0.1.2", "version": "0.3.4",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",

View File

@@ -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.
---

191
src-tauri/Cargo.lock generated
View File

@@ -17,6 +17,20 @@ dependencies = [
"memchr", "memchr",
] ]
[[package]]
name = "ai-translate-client"
version = "0.3.4"
dependencies = [
"futures-util",
"reqwest 0.12.28",
"serde",
"serde_json",
"tauri",
"tauri-build",
"tauri-plugin-http",
"tauri-plugin-opener",
]
[[package]] [[package]]
name = "alloc-no-stdlib" name = "alloc-no-stdlib"
version = "2.0.4" version = "2.0.4"
@@ -554,7 +568,7 @@ dependencies = [
"bitflags 2.11.0", "bitflags 2.11.0",
"core-foundation 0.10.1", "core-foundation 0.10.1",
"core-graphics-types", "core-graphics-types",
"foreign-types", "foreign-types 0.5.0",
"libc", "libc",
] ]
@@ -996,6 +1010,15 @@ version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
[[package]]
name = "foreign-types"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
dependencies = [
"foreign-types-shared 0.1.1",
]
[[package]] [[package]]
name = "foreign-types" name = "foreign-types"
version = "0.5.0" version = "0.5.0"
@@ -1003,7 +1026,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965"
dependencies = [ dependencies = [
"foreign-types-macros", "foreign-types-macros",
"foreign-types-shared", "foreign-types-shared 0.3.1",
] ]
[[package]] [[package]]
@@ -1017,6 +1040,12 @@ dependencies = [
"syn 2.0.117", "syn 2.0.117",
] ]
[[package]]
name = "foreign-types-shared"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
[[package]] [[package]]
name = "foreign-types-shared" name = "foreign-types-shared"
version = "0.3.1" version = "0.3.1"
@@ -1234,18 +1263,6 @@ dependencies = [
"x11", "x11",
] ]
[[package]]
name = "gemmatrans-client"
version = "0.1.2"
dependencies = [
"serde",
"serde_json",
"tauri",
"tauri-build",
"tauri-plugin-http",
"tauri-plugin-opener",
]
[[package]] [[package]]
name = "generic-array" name = "generic-array"
version = "0.14.7" version = "0.14.7"
@@ -1609,6 +1626,22 @@ dependencies = [
"webpki-roots", "webpki-roots",
] ]
[[package]]
name = "hyper-tls"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0"
dependencies = [
"bytes",
"http-body-util",
"hyper",
"hyper-util",
"native-tls",
"tokio",
"tokio-native-tls",
"tower-service",
]
[[package]] [[package]]
name = "hyper-util" name = "hyper-util"
version = "0.1.20" version = "0.1.20"
@@ -2156,6 +2189,23 @@ dependencies = [
"windows-sys 0.60.2", "windows-sys 0.60.2",
] ]
[[package]]
name = "native-tls"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e"
dependencies = [
"libc",
"log",
"openssl",
"openssl-probe",
"openssl-sys",
"schannel",
"security-framework",
"security-framework-sys",
"tempfile",
]
[[package]] [[package]]
name = "ndk" name = "ndk"
version = "0.9.0" version = "0.9.0"
@@ -2465,6 +2515,50 @@ dependencies = [
"pathdiff", "pathdiff",
] ]
[[package]]
name = "openssl"
version = "0.10.75"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328"
dependencies = [
"bitflags 2.11.0",
"cfg-if",
"foreign-types 0.3.2",
"libc",
"once_cell",
"openssl-macros",
"openssl-sys",
]
[[package]]
name = "openssl-macros"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.117",
]
[[package]]
name = "openssl-probe"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
[[package]]
name = "openssl-sys"
version = "0.9.111"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321"
dependencies = [
"cc",
"libc",
"pkg-config",
"vcpkg",
]
[[package]] [[package]]
name = "option-ext" name = "option-ext"
version = "0.2.0" version = "0.2.0"
@@ -3150,16 +3244,19 @@ dependencies = [
"cookie_store 0.22.1", "cookie_store 0.22.1",
"encoding_rs", "encoding_rs",
"futures-core", "futures-core",
"futures-util",
"h2", "h2",
"http", "http",
"http-body", "http-body",
"http-body-util", "http-body-util",
"hyper", "hyper",
"hyper-rustls", "hyper-rustls",
"hyper-tls",
"hyper-util", "hyper-util",
"js-sys", "js-sys",
"log", "log",
"mime", "mime",
"native-tls",
"percent-encoding", "percent-encoding",
"pin-project-lite", "pin-project-lite",
"quinn", "quinn",
@@ -3170,13 +3267,16 @@ dependencies = [
"serde_urlencoded", "serde_urlencoded",
"sync_wrapper", "sync_wrapper",
"tokio", "tokio",
"tokio-native-tls",
"tokio-rustls", "tokio-rustls",
"tokio-util",
"tower", "tower",
"tower-http", "tower-http",
"tower-service", "tower-service",
"url", "url",
"wasm-bindgen", "wasm-bindgen",
"wasm-bindgen-futures", "wasm-bindgen-futures",
"wasm-streams 0.4.2",
"web-sys", "web-sys",
"webpki-roots", "webpki-roots",
] ]
@@ -3211,7 +3311,7 @@ dependencies = [
"url", "url",
"wasm-bindgen", "wasm-bindgen",
"wasm-bindgen-futures", "wasm-bindgen-futures",
"wasm-streams", "wasm-streams 0.5.0",
"web-sys", "web-sys",
] ]
@@ -3313,6 +3413,15 @@ dependencies = [
"winapi-util", "winapi-util",
] ]
[[package]]
name = "schannel"
version = "0.1.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1"
dependencies = [
"windows-sys 0.61.2",
]
[[package]] [[package]]
name = "schemars" name = "schemars"
version = "0.8.22" version = "0.8.22"
@@ -3370,6 +3479,29 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "security-framework"
version = "2.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
dependencies = [
"bitflags 2.11.0",
"core-foundation 0.9.4",
"core-foundation-sys",
"libc",
"security-framework-sys",
]
[[package]]
name = "security-framework-sys"
version = "2.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]] [[package]]
name = "selectors" name = "selectors"
version = "0.24.0" version = "0.24.0"
@@ -4316,6 +4448,16 @@ dependencies = [
"syn 2.0.117", "syn 2.0.117",
] ]
[[package]]
name = "tokio-native-tls"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2"
dependencies = [
"native-tls",
"tokio",
]
[[package]] [[package]]
name = "tokio-rustls" name = "tokio-rustls"
version = "0.26.4" version = "0.26.4"
@@ -4676,6 +4818,12 @@ dependencies = [
"wasm-bindgen", "wasm-bindgen",
] ]
[[package]]
name = "vcpkg"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
[[package]] [[package]]
name = "version-compare" name = "version-compare"
version = "0.2.1" version = "0.2.1"
@@ -4838,6 +4986,19 @@ dependencies = [
"wasmparser", "wasmparser",
] ]
[[package]]
name = "wasm-streams"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65"
dependencies = [
"futures-util",
"js-sys",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
]
[[package]] [[package]]
name = "wasm-streams" name = "wasm-streams"
version = "0.5.0" version = "0.5.0"

View File

@@ -1,8 +1,8 @@
[package] [package]
name = "gemmatrans-client" name = "ai-translate-client"
version = "0.1.2" version = "0.3.4"
description = "A Tauri App" description = "A client using AI models to translate"
authors = ["you"] authors = ["Julian"]
edition = "2021" edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@@ -11,7 +11,7 @@ edition = "2021"
# The `_lib` suffix may seem redundant but it is necessary # The `_lib` suffix may seem redundant but it is necessary
# to make the lib name unique and wouldn't conflict with the bin name. # to make the lib name unique and wouldn't conflict with the bin name.
# This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519 # This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519
name = "gemmatrans_client_lib" name = "ai_translate_client_lib"
crate-type = ["staticlib", "cdylib", "rlib"] crate-type = ["staticlib", "cdylib", "rlib"]
[build-dependencies] [build-dependencies]
@@ -23,4 +23,6 @@ tauri-plugin-opener = "2"
tauri-plugin-http = "2" tauri-plugin-http = "2"
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
serde_json = "1" serde_json = "1"
reqwest = { version = "0.12", features = ["json", "stream"] }
futures-util = "0.3"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 974 B

After

Width:  |  Height:  |  Size: 2.1 KiB

BIN
src-tauri/icons/64x64.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 903 B

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.4 KiB

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
<background android:drawable="@color/ic_launcher_background"/>
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 179 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#fff</color>
</resources>

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 85 KiB

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 252 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 997 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View File

@@ -1,7 +1,93 @@
// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/ use tauri::{AppHandle, Emitter};
use serde::{Deserialize, Serialize};
use futures_util::StreamExt;
use reqwest::Client;
#[derive(Serialize, Deserialize, Clone)]
struct Message {
role: String,
content: String,
}
#[derive(Serialize, Deserialize, Clone)]
struct TranslationPayload {
model: String,
messages: Vec<Message>,
stream: bool,
}
#[derive(Deserialize)]
struct OpenAIResponse {
choices: Vec<Choice>,
}
#[derive(Deserialize)]
struct Choice {
message: Option<Message>,
delta: Option<Delta>,
}
#[derive(Deserialize)]
struct Delta {
content: Option<String>,
}
#[tauri::command] #[tauri::command]
fn greet(name: &str) -> String { async fn translate(
format!("Hello, {}! You've been greeted from Rust!", name) app: AppHandle,
api_address: String,
api_key: String,
payload: TranslationPayload,
) -> Result<String, String> {
let client = Client::new();
// Ensure URL doesn't have double slashes if api_address ends with /
let base_url = api_address.trim_end_matches('/');
let url = format!("{}/chat/completions", base_url);
let mut request = client.post(&url).json(&payload);
if !api_key.is_empty() {
request = request.header("Authorization", format!("Bearer {}", api_key));
}
let res = request
.send()
.await
.map_err(|e| e.to_string())?;
if !payload.stream {
let data = res.json::<OpenAIResponse>().await.map_err(|e| e.to_string())?;
return Ok(data.choices.get(0).and_then(|c| c.message.as_ref()).map(|m| m.content.clone()).unwrap_or_default());
}
let mut stream = res.bytes_stream();
let mut full_response = String::new();
while let Some(item) = stream.next().await {
let chunk = item.map_err(|e| e.to_string())?;
let text = String::from_utf8_lossy(&chunk);
for line in text.lines() {
let line = line.trim();
if line.is_empty() { continue; }
if line == "data: [DONE]" { break; }
if let Some(data_str) = line.strip_prefix("data: ") {
if let Ok(json) = serde_json::from_str::<OpenAIResponse>(data_str) {
if let Some(choice) = json.choices.get(0) {
if let Some(delta) = &choice.delta {
if let Some(content) = &delta.content {
full_response.push_str(content);
app.emit("translation-chunk", content).map_err(|e| e.to_string())?;
}
}
}
}
}
}
}
Ok(full_response)
} }
#[cfg_attr(mobile, tauri::mobile_entry_point)] #[cfg_attr(mobile, tauri::mobile_entry_point)]
@@ -9,7 +95,7 @@ pub fn run() {
tauri::Builder::default() tauri::Builder::default()
.plugin(tauri_plugin_opener::init()) .plugin(tauri_plugin_opener::init())
.plugin(tauri_plugin_http::init()) .plugin(tauri_plugin_http::init())
.invoke_handler(tauri::generate_handler![greet]) .invoke_handler(tauri::generate_handler![translate])
.run(tauri::generate_context!()) .run(tauri::generate_context!())
.expect("error while running tauri application"); .expect("error while running tauri application");
} }

View File

@@ -2,5 +2,5 @@
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
fn main() { fn main() {
gemmatrans_client_lib::run() ai_translate_client_lib::run()
} }

View File

@@ -1,8 +1,8 @@
{ {
"$schema": "https://schema.tauri.app/config/2", "$schema": "https://schema.tauri.app/config/2",
"productName": "gemmatrans-client", "productName": "ai-translate-client",
"version": "0.1.2", "version": "0.3.4",
"identifier": "top.volan.gemmatrans-client", "identifier": "top.volan.ai-translate-client",
"build": { "build": {
"beforeDevCommand": "pnpm dev", "beforeDevCommand": "pnpm dev",
"devUrl": "http://localhost:1420", "devUrl": "http://localhost:1420",
@@ -12,7 +12,7 @@
"app": { "app": {
"windows": [ "windows": [
{ {
"title": "gemmatrans-client", "title": "AI 翻译客户端",
"width": 1400, "width": 1400,
"height": 900 "height": 900
} }

File diff suppressed because it is too large Load Diff

View File

@@ -1,48 +1,166 @@
import { ref } from 'vue'; import { ref, computed } from 'vue';
import { defineStore } from 'pinia'; import { defineStore } from 'pinia';
import { useLocalStorage } from '@vueuse/core'; import { useLocalStorage } from '@vueuse/core';
export interface Language { export interface Language {
displayName: string; // UI 显示的中文名,如 "英语(英国)" displayName: string; // UI 显示的中文名,如 "英语英国"
englishName: string; // 文件中的第二列,用于 {SOURCE_LANG} englishName: string; // 文件中的第二列,用于 {SOURCE_LANG}
code: string; // 文件中的第一列,用于 {SOURCE_CODE} code: string; // 文件中的第一列,用于 {SOURCE_CODE}
} }
export const LANGUAGES: Language[] = [ export const LANGUAGES: Language[] = [
{ displayName: '中文(简体)', englishName: 'Simplified Chinese', code: 'zh-Hans' }, { displayName: '中文简体', englishName: 'Simplified Chinese', code: 'zh-Hans' },
{ displayName: '中文(繁体)', englishName: 'Traditional Chinese', code: 'zh-Hant' }, { displayName: '中文繁体', englishName: 'Traditional Chinese', code: 'zh-Hant' },
{ displayName: '英语', englishName: 'English', code: 'en' }, { 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: 'Japanese', code: 'ja' },
{ displayName: '韩语', englishName: 'Korean', code: 'ko' }, { displayName: '韩语', englishName: 'Korean', code: 'ko' },
{ displayName: '法语', englishName: 'French', code: 'fr' }, { displayName: '法语', englishName: 'French', code: 'fr' },
{ displayName: '德语', englishName: 'German', code: 'de' }, { displayName: '德语', englishName: 'German', code: 'de' },
{ displayName: '西班牙语', englishName: 'Spanish', code: 'es' },
{ displayName: '意大利语', englishName: 'Italian', code: 'it' }, { displayName: '意大利语', englishName: 'Italian', code: 'it' },
{ displayName: '俄语', englishName: 'Russian', code: 'ru' }, { displayName: '俄语', englishName: 'Russian', code: 'ru' },
{ displayName: '葡萄牙语', englishName: 'Portuguese', code: 'pt' },
{ displayName: '越南语', englishName: 'Vietnamese', code: 'vi' }, { displayName: '越南语', englishName: 'Vietnamese', code: 'vi' },
{ displayName: '泰语', englishName: 'Thai', code: 'th' }, { displayName: '泰语', englishName: 'Thai', code: 'th' },
{ displayName: '阿拉伯语', englishName: 'Arabic', code: 'ar' }, { 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. 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.
Produce only the {TARGET_LANG} translation, without any additional explanations or commentary. Please translate the following {SOURCE_LANG} text into {TARGET_LANG}:
[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].`;
{TEXT}`; 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 const useSettingsStore = defineStore('settings', () => { export const useSettingsStore = defineStore('settings', () => {
const isDark = useLocalStorage('is-dark', false); const isDark = useLocalStorage('is-dark', false);
const ollamaApiAddress = useLocalStorage('ollama-api-address', 'http://localhost:11434'); const apiBaseUrl = useLocalStorage('api-base-url', 'http://localhost:11434/v1');
const apiKey = useLocalStorage('api-key', '');
const modelName = useLocalStorage('model-name', 'translategemma:12b'); const modelName = useLocalStorage('model-name', 'translategemma:12b');
const profiles = useLocalStorage<ApiProfile[]>('api-profiles', []);
const enableStreaming = useLocalStorage('enable-streaming', true); const enableStreaming = useLocalStorage('enable-streaming', true);
const systemPromptTemplate = useLocalStorage('system-prompt-template', DEFAULT_TEMPLATE); 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 sourceLang = useLocalStorage<Language>('source-lang-v2', LANGUAGES[0]);
const targetLang = useLocalStorage<Language>('target-lang-v2', LANGUAGES[4]); 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<{ timestamp: string; type: 'request' | 'response' | 'error'; content: any }[]>([]); const logs = ref<{ timestamp: string; type: 'request' | 'response' | 'error'; content: any }[]>([]);
const history = useLocalStorage<HistoryItem[]>('translation-history-v1', []);
const addLog = (type: 'request' | 'response' | 'error', content: any) => { const addLog = (type: 'request' | 'response' | 'error', content: any) => {
const now = new Date(); const now = new Date();
@@ -56,15 +174,41 @@ export const useSettingsStore = defineStore('settings', () => {
if (logs.value.length > 20) logs.value.pop(); if (logs.value.length > 20) 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')}`;
history.value.unshift({
...item,
id: crypto.randomUUID(),
timestamp
});
// 限制 100 条
if (history.value.length > 100) {
history.value = history.value.slice(0, 100);
}
};
return { return {
isDark, isDark,
ollamaApiAddress, apiBaseUrl,
apiKey,
modelName, modelName,
profiles,
enableStreaming, enableStreaming,
systemPromptTemplate, systemPromptTemplate,
enableEvaluation,
evaluationPromptTemplate,
evaluationProfileId,
refinementPromptTemplate,
sourceLang, sourceLang,
targetLang, targetLang,
speakerIdentity,
toneRegister,
logs, logs,
addLog history,
addLog,
addHistory
}; };
}); });