From aa6133e3ed4a512b20802b9d8da5fa69d36cbc14 Mon Sep 17 00:00:00 2001 From: ydy0615 Date: Thu, 19 Feb 2026 10:22:27 +0800 Subject: [PATCH] feat: add privacy mode, thinking levels, PWA support, and i18n - Add privacy mode to hide IP and user preferences from AI requests - Add model thinking levels (low/medium/high) for context analysis depth - Add PWA support with service worker, manifest, and app icons - Add SettingsPanel for user preferences (theme, background, language) - Add i18n translations for en/zh/ja/ko/de/fr - Add Pinia store for centralized settings management - Update backend to support user preferences and thinking levels - Update config to use absolute API URLs --- .env.example | 5 +- backend/.env.example | 2 +- backend/llm.py | 2 +- backend/main.py | 42 ++- backend/prompt.py | 32 +- index.html | 9 +- public/icons/icon-180.png | Bin 0 -> 4061 bytes public/icons/icon-192.png | Bin 0 -> 4378 bytes public/icons/icon-192.svg | 14 + public/icons/icon-512.png | Bin 0 -> 14866 bytes public/icons/icon-512.svg | 14 + public/icons/icon-maskable.svg | 14 + public/manifest.webmanifest | 50 +++ public/sw.js | 79 +++++ src/App.vue | 205 +++---------- src/components/MilkdownEditor.vue | 57 ++-- src/components/SettingsPanel.vue | 490 ++++++++++++++++++++++++++++++ src/main.js | 13 +- src/plugins/copilotPlugin.ts | 8 +- src/stores/settings.js | 156 ++++++++++ src/style.css | 32 +- src/utils/api.js | 32 +- src/utils/config.js | 9 +- src/utils/i18n.js | 248 +++++++++++++++ 24 files changed, 1291 insertions(+), 222 deletions(-) create mode 100644 public/icons/icon-180.png create mode 100644 public/icons/icon-192.png create mode 100644 public/icons/icon-192.svg create mode 100644 public/icons/icon-512.png create mode 100644 public/icons/icon-512.svg create mode 100644 public/icons/icon-maskable.svg create mode 100644 public/manifest.webmanifest create mode 100644 public/sw.js create mode 100644 src/components/SettingsPanel.vue create mode 100644 src/stores/settings.js create mode 100644 src/utils/i18n.js diff --git a/.env.example b/.env.example index 2516f76..86c7142 100644 --- a/.env.example +++ b/.env.example @@ -1,2 +1,3 @@ -VITE_API_URL=/v1/completions -VITE_OCR_URL=/v1/ocr +VITE_API_BASE_URL=http://149.104.29.239:8001 +VITE_API_URL=http://149.104.29.239:8001/v1/completions +VITE_OCR_URL=http://149.104.29.239:8001/v1/ocr diff --git a/backend/.env.example b/backend/.env.example index c74dfba..aec1400 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -1,4 +1,4 @@ OPENAI_API_KEY=ollama OLLAMA_BASE_URL=http://192.168.0.120:11434/v1/ -OLLAMA_MODEL=gpt-oss:120b +OLLAMA_MODEL=gpt-oss:20b VLM_MODEL=qwen3-vl:30b diff --git a/backend/llm.py b/backend/llm.py index 38b8e5d..4f88721 100644 --- a/backend/llm.py +++ b/backend/llm.py @@ -7,7 +7,7 @@ from dotenv import load_dotenv load_dotenv() -OLLAMA_MODEL = os.getenv('OLLAMA_MODEL', 'gpt-oss:120b') +OLLAMA_MODEL = os.getenv('OLLAMA_MODEL', 'gpt-oss:20b') OLLAMA_HOST = os.getenv('OLLAMA_HOST', 'http://192.168.0.120:11434') VLM_MODEL = os.getenv('VLM_MODEL', 'qwen3-vl:30b') diff --git a/backend/main.py b/backend/main.py index 67a8433..fc6f399 100644 --- a/backend/main.py +++ b/backend/main.py @@ -27,10 +27,20 @@ app.add_middleware( allow_headers=["*"], ) +from typing import Optional + +class UserPreferences(BaseModel): + language: str = 'auto' + currency: str = 'auto' + timezone: str = 'auto' + class CompletionRequest(BaseModel): prefix: str suffix: str languageId: str = 'markdown' + model_thinking: str = 'low' + privacy_mode: bool = False + user_preferences: Optional[UserPreferences] = None class OCRRequest(BaseModel): image: str @@ -50,26 +60,40 @@ def get_client_ip(request: Request) -> str: @app.post("/v1/completions") async def create_completion(request: Request, req: CompletionRequest): request_id = str(uuid.uuid4())[:8] - client_ip = get_client_ip(request) - # 查询 IP 归属地 - location = get_ip_location_text(client_ip) - if location: - logger.info("[%s] client_location=%s", request_id, location) + + client_ip = "hidden" + location = "" + + if not req.privacy_mode: + client_ip = get_client_ip(request) + # 查询 IP 归属地 + location = get_ip_location_text(client_ip) + if location: + logger.info("[%s] client_location=%s", request_id, location) + try: logger.info( - "[%s] /v1/completions client_ip=%s prefix_chars=%d suffix_chars=%d lang=%s prefix_tail='%s' suffix_head='%s'", + "[%s] /v1/completions client_ip=%s prefix_chars=%d suffix_chars=%d lang=%s thinking=%s privacy=%s", request_id, client_ip, len(req.prefix or ""), len(req.suffix or ""), req.languageId, - _preview((req.prefix or "")[-120:]), - _preview((req.suffix or "")[:120]), + req.model_thinking, + req.privacy_mode ) llm_prefix, llm_suffix = prepare_prompt_context(req.prefix or "", req.suffix or "") logger.info("[%s] llm_input_prefix=%r", request_id, llm_prefix) logger.info("[%s] llm_input_suffix=%r", request_id, llm_suffix) - prompt = build_prompt(req.prefix, req.suffix, req.languageId, location=location) + + prompt = build_prompt( + req.prefix, + req.suffix, + req.languageId, + location=location, + thinking_level=req.model_thinking, + preferences=req.user_preferences + ) result = await call_ollama(prompt, tag=f"{request_id}-primary", temperature=0.7) content = result["content"] or "" diff --git a/backend/prompt.py b/backend/prompt.py index 12ec50a..4acfeff 100644 --- a/backend/prompt.py +++ b/backend/prompt.py @@ -29,20 +29,46 @@ def prepare_prompt_context(prefix: str, suffix: str) -> Tuple[str, str]: return _prepare_context(prefix, suffix) -def build_prompt(prefix: str, suffix: str, language_id: str = "markdown", location: str = "") -> str: +def build_prompt( + prefix: str, + suffix: str, + language_id: str = "markdown", + location: str = "", + thinking_level: str = "low", + preferences: object = None +) -> str: safe_language_id = _sanitize_language_id(language_id) recent_prefix, recent_suffix = _prepare_context(prefix, suffix) current_time = _get_current_datetime() location_info = f"\nUser location: {location}" if location else "" + + thinking_instruction = "" + if thinking_level == "medium": + thinking_instruction = "\n- Briefly analyze the context before suggesting." + elif thinking_level == "high": + thinking_instruction = "\n- Deeply analyze the context, structure, and intent before suggesting. Think step-by-step." - prompt = f"""Current time: {current_time}{location_info} + pref_info = [] + if preferences: + if preferences.language and preferences.language != 'auto': + pref_info.append(f"Preferred language: {preferences.language}") + if preferences.currency and preferences.currency != 'auto': + pref_info.append(f"Preferred currency: {preferences.currency}") + if preferences.timezone and preferences.timezone != 'auto': + pref_info.append(f"User timezone: {preferences.timezone}") + + preferences_instruction = "\n".join(pref_info) + if preferences_instruction: + preferences_instruction = f"\nUser Preferences:\n{preferences_instruction}" + + prompt = f"""Current time: {current_time}{location_info}{preferences_instruction} You are an inline completion engine for a {safe_language_id} editor with ghost-text suggestions. Your job: - Return ONLY the text that should be inserted at the cursor between PREFIX and SUFFIX. - Prefer a meaningful, non-empty insertion with moderate length. -- Avoid overly short outputs with little information value. +- Avoid overly short outputs with little information value.{thinking_instruction} Important context: - PREFIX may contain OCR metadata inline after images, e.g. ![alt](url) . diff --git a/index.html b/index.html index cba95be..22f5d36 100644 --- a/index.html +++ b/index.html @@ -2,7 +2,14 @@ - + + + + + + + + llm-in-text diff --git a/public/icons/icon-180.png b/public/icons/icon-180.png new file mode 100644 index 0000000000000000000000000000000000000000..8fd6bd809144bc499bef9867e54e800eba3d9774 GIT binary patch literal 4061 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 literal 0 HcmV?d00001 diff --git a/public/icons/icon-192.svg b/public/icons/icon-192.svg new file mode 100644 index 0000000..592200d --- /dev/null +++ b/public/icons/icon-192.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/public/icons/icon-512.png b/public/icons/icon-512.png new file mode 100644 index 0000000000000000000000000000000000000000..183e65e162e05c7a37b38464bc84b081b27c4ced GIT binary patch literal 14866 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 + + + + + + + + + + + + + diff --git a/public/icons/icon-maskable.svg b/public/icons/icon-maskable.svg new file mode 100644 index 0000000..c66103b --- /dev/null +++ b/public/icons/icon-maskable.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/public/manifest.webmanifest b/public/manifest.webmanifest new file mode 100644 index 0000000..877ee94 --- /dev/null +++ b/public/manifest.webmanifest @@ -0,0 +1,50 @@ +{ + "id": "/?source=pwa", + "name": "LLM in Text", + "short_name": "LLMText", + "description": "AI-assisted Markdown editor with real-time completion.", + "lang": "zh-CN", + "start_url": "/", + "scope": "/", + "display": "standalone", + "display_override": [ + "window-controls-overlay", + "standalone", + "minimal-ui" + ], + "orientation": "any", + "background_color": "#f8fafc", + "theme_color": "#0f172a", + "categories": [ + "productivity", + "utilities" + ], + "icons": [ + { + "src": "/icons/icon-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "any" + }, + { + "src": "/icons/icon-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "any" + }, + { + "src": "/icons/icon-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ], + "shortcuts": [ + { + "name": "New Draft", + "short_name": "Draft", + "description": "Open the editor to start writing immediately.", + "url": "/" + } + ] +} diff --git a/public/sw.js b/public/sw.js new file mode 100644 index 0000000..d120bb1 --- /dev/null +++ b/public/sw.js @@ -0,0 +1,79 @@ +const CACHE_NAME = 'llm-in-text-v1'; +const APP_SHELL_ASSETS = [ + '/', + '/index.html', + '/manifest.webmanifest', + '/icons/icon-180.png', + '/icons/icon-192.png', + '/icons/icon-512.png' +]; + +self.addEventListener('install', (event) => { + event.waitUntil( + caches.open(CACHE_NAME).then((cache) => cache.addAll(APP_SHELL_ASSETS)) + ); + self.skipWaiting(); +}); + +self.addEventListener('activate', (event) => { + event.waitUntil( + caches.keys().then((keys) => + Promise.all( + keys + .filter((key) => key !== CACHE_NAME) + .map((key) => caches.delete(key)) + ) + ) + ); + self.clients.claim(); +}); + +async function staleWhileRevalidate(request) { + const cache = await caches.open(CACHE_NAME); + const cached = await cache.match(request); + const networkPromise = fetch(request) + .then((response) => { + if (response.ok) { + cache.put(request, response.clone()); + } + return response; + }) + .catch(() => cached); + + return cached || networkPromise; +} + +self.addEventListener('fetch', (event) => { + const { request } = event; + + if (request.method !== 'GET') return; + + const url = new URL(request.url); + if (url.origin !== self.location.origin) return; + + if (url.pathname.startsWith('/v1/')) return; + + if (request.mode === 'navigate') { + event.respondWith( + fetch(request) + .then((response) => { + if (response.ok) { + const copy = response.clone(); + caches.open(CACHE_NAME).then((cache) => cache.put(request, copy)); + } + return response; + }) + .catch(async () => { + const cachedPage = await caches.match(request); + if (cachedPage) return cachedPage; + return caches.match('/index.html'); + }) + ); + return; + } + + const cacheableDestinations = ['script', 'style', 'font', 'image', 'worker']; + if (cacheableDestinations.includes(request.destination)) { + event.respondWith(staleWhileRevalidate(request)); + } +}); diff --git a/src/App.vue b/src/App.vue index 06e644b..fbaaf0b 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,54 +1,63 @@ @@ -59,128 +68,14 @@ function onChange(markdownValue) { height: 100%; background: var(--app-bg); color: var(--app-text); + transition: background 0.3s, color 0.3s; + isolation: isolate; /* Create new stacking context */ } -.theme-toggle { - position: fixed; - top: 16px; - right: 20px; - z-index: 11000; - border: none; - padding: 0; - background: none; - cursor: pointer; -} - -.theme-toggle__track { +.editor-container { position: relative; - width: 72px; - height: 36px; - display: block; - border-radius: 999px; - border: 2px solid var(--panel-border); - background: linear-gradient(135deg, var(--toggle-bg-start), var(--toggle-bg-end)); - box-shadow: var(--panel-shadow), inset 0 2px 4px rgba(255, 255, 255, 0.1), inset 0 -2px 4px rgba(0, 0, 0, 0.1); - overflow: hidden; - transition: background 400ms ease, border-color 400ms ease, box-shadow 400ms ease; + z-index: 1; + height: 100%; } -.theme-toggle__track::before { - content: ''; - position: absolute; - inset: 0; - background: linear-gradient(180deg, rgba(255,255,255,0.15) 0%, transparent 50%, rgba(0,0,0,0.1) 100%); - border-radius: 999px; - pointer-events: none; - transition: opacity 400ms ease; -} - -.theme-toggle__thumb { - position: absolute; - top: 2px; - left: 2px; - width: 28px; - height: 28px; - border-radius: 50%; - background: linear-gradient(135deg, var(--toggle-thumb-bg) 0%, var(--toggle-thumb-bg) 100%); - box-shadow: 0 2px 6px rgba(0, 0, 0, 0.25), 0 0 0 1px rgba(0, 0, 0, 0.05), inset 0 1px 0 rgba(255, 255, 255, 0.4); - transition: transform 400ms cubic-bezier(0.34, 1.56, 0.64, 1); - z-index: 2; -} - -.theme-toggle__sun, -.theme-toggle__moon { - position: absolute; - top: 50%; - width: 18px; - height: 18px; - transform: translateY(-50%); - transition: opacity 300ms ease, transform 400ms cubic-bezier(0.34, 1.56, 0.64, 1); -} - -.theme-toggle__sun { - left: 10px; - color: var(--toggle-sun); - opacity: 1; - transform: translateY(-50%) scale(1); -} - -.theme-toggle__sun svg { - width: 18px; - height: 18px; - filter: drop-shadow(0 0 3px var(--toggle-sun)); -} - -.theme-toggle__moon { - right: 10px; - color: var(--toggle-moon); - opacity: 0.5; - transform: translateY(-50%) scale(0.85); -} - -.theme-toggle__moon svg { - width: 18px; - height: 18px; -} - -.theme-toggle.is-dark .theme-toggle__thumb { - transform: translateX(36px); - background: linear-gradient(135deg, var(--toggle-thumb-bg) 0%, #c8d4ec 100%); -} - -.theme-toggle.is-dark .theme-toggle__sun { - opacity: 0.5; - transform: translateY(-50%) scale(0.85); -} - -.theme-toggle.is-dark .theme-toggle__moon { - opacity: 1; - transform: translateY(-50%) scale(1); - color: #c8d4ec; -} - -.theme-toggle.is-dark .theme-toggle__moon svg { - filter: drop-shadow(0 0 4px rgba(200, 212, 236, 0.6)); -} - -.theme-toggle:focus-visible { - outline: 2px solid var(--focus-ring); - outline-offset: 3px; - border-radius: 999px; -} - -@media (max-width: 640px) { - .theme-toggle { - top: 12px; - right: 12px; - } -} - -@media (prefers-reduced-motion: reduce) { - .theme-toggle__thumb, - .theme-toggle__sun, - .theme-toggle__moon { - transition: none; - } -} diff --git a/src/components/MilkdownEditor.vue b/src/components/MilkdownEditor.vue index 567e516..cf8fa37 100644 --- a/src/components/MilkdownEditor.vue +++ b/src/components/MilkdownEditor.vue @@ -6,8 +6,8 @@
- - + +
@@ -84,16 +84,16 @@
-

通过 URL 插入图片

+

{{ t('insertUrl') }}

- - + +
@@ -101,17 +101,20 @@ + + + + diff --git a/src/main.js b/src/main.js index 8a0a19c..9cf6c02 100644 --- a/src/main.js +++ b/src/main.js @@ -2,7 +2,18 @@ import { createApp } from 'vue' import App from './App.vue' import './style.css' +import { createPinia } from 'pinia' import '@milkdown/crepe/theme/common/style.css' import '@milkdown/crepe/theme/frame.css' -createApp(App).mount('#app') +const app = createApp(App) +app.use(createPinia()) +app.mount('#app') + +if (import.meta.env.PROD && 'serviceWorker' in navigator) { + window.addEventListener('load', () => { + navigator.serviceWorker.register('/sw.js').catch((error) => { + console.error('Service worker registration failed:', error) + }) + }) +} diff --git a/src/plugins/copilotPlugin.ts b/src/plugins/copilotPlugin.ts index 419ac49..a0ee43b 100644 --- a/src/plugins/copilotPlugin.ts +++ b/src/plugins/copilotPlugin.ts @@ -51,11 +51,11 @@ export const copilotGhostMark = $markSchema('copilot_ghost', () => ({ toDOM: () => ['span', { 'data-copilot-ghost': '', class: 'copilot-ghost-text' }, 0], parseMarkdown: { match: () => false, - runner: () => {} + runner: () => { } }, toMarkdown: { match: (mark) => mark.type.name === 'copilot_ghost', - runner: () => {} + runner: () => { } } })) @@ -331,7 +331,7 @@ function scheduleFetch(view: EditorView, runtime: CopilotRuntime, pos: number) { const doc = view.state.doc const schema = view.state.schema const baseSize = doc.content.size - + const serializer = runtime.ctx.get(serializerCtx) let prefixMarkdown = '' let suffixMarkdown = '' @@ -356,7 +356,7 @@ function scheduleFetch(view: EditorView, runtime: CopilotRuntime, pos: number) { const totalTextLen = (prefixMarkdown + suffixMarkdown).length const ocrContextLen = requestPrefix.length - prefixMarkdown.length const totalWithOcr = totalTextLen + ocrContextLen - + const overLimit = totalWithOcr > SIZE_LIMIT if (overLimit) { diff --git a/src/stores/settings.js b/src/stores/settings.js new file mode 100644 index 0000000..727609e --- /dev/null +++ b/src/stores/settings.js @@ -0,0 +1,156 @@ +import { defineStore } from 'pinia' +import { ref, watch, computed } from 'vue' +import { translations } from '../utils/i18n' + +export const useSettingsStore = defineStore('settings', () => { + // --- State --- + + // 1. Theme (handled partly by useTheme, but we keep a ref here for the UI) + const theme = ref('system') // 'light' | 'dark' | 'system' + + // 2. Model Behavior + const modelThinking = ref('low') // 'low' | 'medium' | 'high' + const debounceMs = ref(1000) // 1000 - 5000 + + // 3. Privacy + const privacyMode = ref(false) + + // 4. Preferences + const language = ref('auto') + const currency = ref('auto') + // const timezone = ref('auto') // removed + + // 5. Background + const backgroundType = ref('default') // 'default' | 'warm' | 'reading' | 'image' + // const backgroundColor = ref('#ffffff') // removed + const backgroundImage = ref('') + const backgroundOpacity = ref(0.2) // 0.05 - 0.50 + + // --- Getters --- + const uiLanguage = computed(() => { + if (language.value !== 'auto') { + return language.value + } + const sysLang = (navigator.language || navigator.userLanguage || 'en').split('-')[0] + const supported = ['zh', 'en', 'ja', 'ko', 'de', 'fr'] + return supported.includes(sysLang) ? sysLang : 'en' + }) + + const detectedTimezone = computed(() => { + return Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC' + }) + + // We can't easily detect currency by IP on the frontend without an external API. + // We will let the backend handle 'auto' currency if needed, or stick to auto label. + + const t = computed(() => { + return translations[uiLanguage.value] || translations['en'] + }) + + // --- Actions/Logic --- + + // Load from localStorage + const loadSettings = () => { + try { + const stored = localStorage.getItem('llm-in-text-settings') + if (stored) { + const data = JSON.parse(stored) + if (data.theme) theme.value = data.theme + if (data.modelThinking) modelThinking.value = data.modelThinking + if (data.debounceMs) debounceMs.value = data.debounceMs + if (typeof data.privacyMode === 'boolean') privacyMode.value = data.privacyMode + if (data.language) language.value = data.language + if (data.currency) currency.value = data.currency + // if (data.timezone) timezone.value = data.timezone // timezone legacy ignore + if (data.backgroundType) { + // migrate color to default if needed, or mapped + if (data.backgroundType === 'color') backgroundType.value = 'default' + else backgroundType.value = data.backgroundType + } + // if (data.backgroundColor) backgroundColor.value = data.backgroundColor // removed + if (data.backgroundImage) backgroundImage.value = data.backgroundImage + if (data.backgroundOpacity) backgroundOpacity.value = data.backgroundOpacity + } + } catch (e) { + console.error('Failed to load settings', e) + } + } + + // Save to localStorage + const saveSettings = () => { + try { + const data = { + theme: theme.value, + modelThinking: modelThinking.value, + debounceMs: debounceMs.value, + privacyMode: privacyMode.value, + language: language.value, + currency: currency.value, + // timezone: timezone.value, // removed + backgroundType: backgroundType.value, + // backgroundColor: backgroundColor.value, // removed + backgroundImage: backgroundImage.value, + backgroundOpacity: backgroundOpacity.value, + } + localStorage.setItem('llm-in-text-settings', JSON.stringify(data)) + } catch (e) { + console.error('Failed to save settings', e) + } + } + + // Reset to defaults + const resetSettings = () => { + theme.value = 'system' + modelThinking.value = 'low' + debounceMs.value = 1000 + privacyMode.value = false + language.value = 'auto' + currency.value = 'auto' + // timezone.value = 'auto' // removed + backgroundType.value = 'default' + // backgroundColor.value = '#ffffff' // removed + backgroundImage.value = '' + backgroundOpacity.value = 0.2 + saveSettings() + } + + // Auto-save watchers + watch( + [ + theme, + modelThinking, + debounceMs, + privacyMode, + language, + currency, + // timezone, // removed + backgroundType, + // backgroundColor, // removed + backgroundImage, + backgroundOpacity, + ], + () => { + saveSettings() + } + ) + + // Initialize + loadSettings() + + return { + theme, + modelThinking, + debounceMs, + privacyMode, + language, + currency, + // timezone, // removed + backgroundType, + // backgroundColor, // removed + backgroundImage, + backgroundOpacity, + uiLanguage, + t, + resetSettings + } +}) diff --git a/src/style.css b/src/style.css index 196f292..90864c1 100644 --- a/src/style.css +++ b/src/style.css @@ -13,14 +13,14 @@ color-scheme: light; --app-bg: #f4f6fb; --app-text: #1f2937; - --panel-bg: #ffffff; + --panel-bg: rgba(255, 255, 255, 0.5); --panel-border: #d7deea; --panel-shadow: 0 8px 24px rgba(16, 24, 40, 0.12); - --btn-bg: #ffffff; + --btn-bg: rgba(255, 255, 255, 0.5); --btn-fg: #5b6470; --btn-hover-bg: #4a90d9; --btn-hover-fg: #ffffff; - --btn-disabled-bg: #cfd5df; + --btn-disabled-bg: rgba(207, 213, 223, 0.5); --btn-disabled-fg: #8a92a0; --overlay-bg: rgba(15, 23, 42, 0.3); --tooltip-bg: #111827; @@ -30,8 +30,8 @@ --scrollbar-thumb: #d4dae4; --scrollbar-thumb-hover: #bbc4d2; --focus-ring: #3b82f6; - --toggle-bg-start: #fff8dd; - --toggle-bg-end: #f2f4ff; + --toggle-bg-start: rgba(255, 248, 221, 0.5); + --toggle-bg-end: rgba(242, 244, 255, 0.5); --toggle-thumb-bg: #ffffff; --toggle-sun: #f59e0b; --toggle-moon: #475569; @@ -61,14 +61,14 @@ color-scheme: dark; --app-bg: #0f1117; --app-text: #e5e7eb; - --panel-bg: #1a1e27; + --panel-bg: rgba(26, 30, 39, 0.5); --panel-border: #2f3644; --panel-shadow: 0 10px 26px rgba(0, 0, 0, 0.5); - --btn-bg: #222834; + --btn-bg: rgba(34, 40, 52, 0.5); --btn-fg: #d2d8e4; --btn-hover-bg: #6ea8ff; --btn-hover-fg: #0d1117; - --btn-disabled-bg: #2e3441; + --btn-disabled-bg: rgba(46, 52, 65, 0.5); --btn-disabled-fg: #7a8498; --overlay-bg: rgba(2, 6, 23, 0.65); --tooltip-bg: #f8fafc; @@ -78,8 +78,8 @@ --scrollbar-thumb: #40485a; --scrollbar-thumb-hover: #5f6980; --focus-ring: #60a5fa; - --toggle-bg-start: #2d3140; - --toggle-bg-end: #1f2430; + --toggle-bg-start: rgba(45, 49, 64, 0.5); + --toggle-bg-end: rgba(31, 36, 48, 0.5); --toggle-thumb-bg: #dbe3f2; --toggle-sun: #fbbf24; --toggle-moon: #e2e8f0; @@ -106,10 +106,10 @@ } :root[data-theme='light'] .milkdown { - --crepe-color-background: #ffffff; + --crepe-color-background: transparent; --crepe-color-on-background: #000000; - --crepe-color-surface: #f7f7f7; - --crepe-color-surface-low: #ededed; + --crepe-color-surface: rgba(247, 247, 247, 0.5); + --crepe-color-surface-low: rgba(237, 237, 237, 0.5); --crepe-color-on-surface: #1c1c1c; --crepe-color-on-surface-variant: #4d4d4d; --crepe-color-outline: #a8a8a8; @@ -126,10 +126,10 @@ } :root[data-theme='dark'] .milkdown { - --crepe-color-background: #1a1a1a; + --crepe-color-background: transparent; --crepe-color-on-background: #e6e6e6; - --crepe-color-surface: #121212; - --crepe-color-surface-low: #1c1c1c; + --crepe-color-surface: rgba(18, 18, 18, 0.5); + --crepe-color-surface-low: rgba(28, 28, 28, 0.5); --crepe-color-on-surface: #d1d1d1; --crepe-color-on-surface-variant: #a9a9a9; --crepe-color-outline: #757575; diff --git a/src/utils/api.js b/src/utils/api.js index 29712c6..ecf62c6 100644 --- a/src/utils/api.js +++ b/src/utils/api.js @@ -16,16 +16,36 @@ async function getClientIP() { } } +import { useSettingsStore } from '../stores/settings' + export async function fetchSuggestion(prefix, suffix, signal, apiUrl = API_URL) { try { + const settings = useSettingsStore() const clientIP = await getClientIP() const headers = { 'Content-Type': 'application/json' } - if (clientIP) headers['X-Client-IP'] = clientIP - + + // Only send IP if privacy mode is OFF + if (clientIP && !settings.privacyMode) { + headers['X-Client-IP'] = clientIP + } + + const body = { + prefix, + suffix, + languageId: 'markdown', + model_thinking: settings.modelThinking, + privacy_mode: settings.privacyMode, + user_preferences: { + language: settings.language, + currency: settings.currency, + timezone: settings.detectedTimezone + } + } + const res = await fetch(apiUrl, { method: 'POST', headers, - body: JSON.stringify({ prefix, suffix, languageId: 'markdown' }), + body: JSON.stringify(body), signal }) @@ -45,10 +65,10 @@ export async function fetchSuggestion(prefix, suffix, signal, apiUrl = API_URL) const { done, value } = await reader.read() if (done) break buffer += new TextDecoder().decode(value) - + const lines = buffer.split('\n') buffer = lines.pop() || '' - + for (const line of lines) { if (!line.startsWith('data: ')) continue const jsonStr = line.slice(6).trim() @@ -64,7 +84,7 @@ export async function fetchSuggestion(prefix, suffix, signal, apiUrl = API_URL) } } } - + return text } catch (e) { if (e.name === 'AbortError') { diff --git a/src/utils/config.js b/src/utils/config.js index cc817e5..0e7612a 100644 --- a/src/utils/config.js +++ b/src/utils/config.js @@ -1,3 +1,6 @@ -export const DEBUG = import.meta.env.DEV -export const API_URL = import.meta.env.VITE_API_URL || '/v1/completions' -export const OCR_URL = import.meta.env.VITE_OCR_URL || '/v1/ocr' +export const DEBUG = import.meta.env.DEV + +const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://149.104.29.239:8001' + +export const API_URL = import.meta.env.VITE_API_URL || `${API_BASE_URL}/v1/completions` +export const OCR_URL = import.meta.env.VITE_OCR_URL || `${API_BASE_URL}/v1/ocr` diff --git a/src/utils/i18n.js b/src/utils/i18n.js new file mode 100644 index 0000000..f350efe --- /dev/null +++ b/src/utils/i18n.js @@ -0,0 +1,248 @@ +export const translations = { + en: { + settings: 'Settings', + close: 'Close', + appearance: 'Appearance', + theme: 'Theme', + light: 'Light', + dark: 'Dark', + system: 'System', + background: 'Background', + default: 'Default', + warm: 'Warm', + reading: 'Reading Light', + image: 'Custom Image', + opacity: 'Opacity', + modelIntelligence: 'Model Intelligence', + thinkingLevel: 'Thinking Level', + low: 'Low', + medium: 'Medium', + high: 'High', + lowDesc: 'Direct completion (Fastest)', + mediumDesc: 'Brief analysis before suggesting', + highDesc: 'Deep, step-by-step analysis (Slowest)', + debounceTime: 'Debounce Time', + privacyPreferences: 'Privacy & Preferences', + privacyMode: 'Privacy Mode', + privacyDesc: 'Prevent sending IP and preferences to the AI', + language: 'Language', + auto: 'Auto Detect', + currency: 'Currency', + about: 'About Us', + importMd: 'Import Markdown', + exportMd: 'Export Markdown', + uploadImg: 'Upload Image', + enableAI: 'Enable AI', + disableAI: 'Disable AI', + insertUrl: 'Insert Image from URL', + insert: 'Insert', + cancel: 'Cancel', + imgTooLarge: 'Image too large', + docTooLarge: 'Document too large, AI disabled' + }, + zh: { + settings: '设置', + close: '关闭', + appearance: '外观', + theme: '主题', + light: '浅色', + dark: '深色', + system: '跟随系统', + background: '背景', + default: '默认', + warm: '暖色调', + reading: '读书灯', + image: '自定义图片', + opacity: '透明度', + modelIntelligence: '模型智能', + thinkingLevel: '思考程度', + low: '低', + medium: '中', + high: '高', + lowDesc: '直接补全(最快)', + mediumDesc: '简要分析上下文后建议', + highDesc: '深度逐步分析(最慢但质量最高)', + debounceTime: '防抖时间', + privacyPreferences: '隐私与偏好', + privacyMode: '隐私模式', + privacyDesc: '不向 AI 发送 IP 地址和偏好设置', + language: '语言', + auto: '自动检测', + currency: '货币', + about: '关于我们', + importMd: '导入 Markdown', + exportMd: '导出 Markdown', + uploadImg: '上传图片', + enableAI: '启用 AI', + disableAI: '禁用 AI', + insertUrl: '通过 URL 插入图片', + insert: '插入', + cancel: '取消', + imgTooLarge: '图片过大', + docTooLarge: '文档过大,AI已禁用' + }, + ja: { + settings: '設定', + close: '閉じる', + appearance: '外観', + theme: 'テーマ', + light: 'ライト', + dark: 'ダーク', + system: 'システム', + background: '背景', + default: 'デフォルト', + warm: '暖色', + reading: '読書灯', + image: 'カスタム画像', + opacity: '不透明度', + modelIntelligence: 'モデル知能', + thinkingLevel: '思考レベル', + low: '低', + medium: '中', + high: '高', + lowDesc: '直接補完(最速)', + mediumDesc: '提案前に文脈を簡単に分析', + highDesc: '深く段階的に分析(遅いが最高品質)', + debounceTime: 'デバウンス時間', + privacyPreferences: 'プライバシーと設定', + privacyMode: 'プライバシーモード', + privacyDesc: 'AIにIPアドレスと設定を送信しない', + language: '言語', + auto: '自動検出', + currency: '通貨', + about: '私たちについて', + importMd: 'Markdownをインポート', + exportMd: 'Markdownをエクスポート', + uploadImg: '画像をアップロード', + enableAI: 'AIを有効化', + disableAI: 'AIを無効化', + insertUrl: 'URLから画像を挿入', + insert: '挿入', + cancel: 'キャンセル', + imgTooLarge: '画像が大きすぎます', + docTooLarge: 'ドキュメントが大きすぎます、AI無効' + }, + ko: { + settings: '설정', + close: '닫기', + appearance: '외관', + theme: '테마', + light: '라이트', + dark: '다크', + system: '시스템', + background: '배경', + default: '기본', + warm: '따뜻한 색', + reading: '독서등', + image: '사용자 지정 이미지', + opacity: '불투명도', + modelIntelligence: '모델 지능', + thinkingLevel: '사고 수준', + low: '낮음', + medium: '중간', + high: '높음', + lowDesc: '직접 완성 (가장 빠름)', + mediumDesc: '제안 전 문맥 간단 분석', + highDesc: '심층 단계별 분석 (가장 느리지만 최고 품질)', + debounceTime: '디바운스 시간', + privacyPreferences: '개인정보 및 환경설정', + privacyMode: '개인정보 모드', + privacyDesc: 'AI에 IP 주소 및 설정 전송 안 함', + language: '언어', + auto: '자동 감지', + currency: '통화', + about: '회사 소개', + importMd: 'Markdown 가져오기', + exportMd: 'Markdown 내보내기', + uploadImg: '이미지 업로드', + enableAI: 'AI 활성화', + disableAI: 'AI 비활성화', + insertUrl: 'URL로 이미지 삽입', + insert: '삽입', + cancel: '취소', + imgTooLarge: '이미지가 너무 큽니다', + docTooLarge: '문서가 너무 큽니다, AI 비활성화됨' + }, + de: { + settings: 'Einstellungen', + close: 'Schließen', + appearance: 'Aussehen', + theme: 'Thema', + light: 'Hell', + dark: 'Dunkel', + system: 'System', + background: 'Hintergrund', + default: 'Standard', + warm: 'Warm', + reading: 'Leselicht', + image: 'Eigenes Bild', + opacity: 'Deckkraft', + modelIntelligence: 'Modell-Intelligenz', + thinkingLevel: 'Denkniveau', + low: 'Niedrig', + medium: 'Mittel', + high: 'Hoch', + lowDesc: 'Direkte Vervollständigung (Am schnellsten)', + mediumDesc: 'Kurze Analyse vor Vorschlag', + highDesc: 'Tiefe schrittweise Analyse (Langsam, aber höchste Qualität)', + debounceTime: 'Entprellzeit', + privacyPreferences: 'Datenschutz & Einstellungen', + privacyMode: 'Datenschutzmodus', + privacyDesc: 'Sende keine IP und Einstellungen an KI', + language: 'Sprache', + auto: 'Automatisch', + currency: 'Währung', + about: 'Über uns', + importMd: 'Markdown importieren', + exportMd: 'Markdown exportieren', + uploadImg: 'Bild hochladen', + enableAI: 'KI aktivieren', + disableAI: 'KI deaktivieren', + insertUrl: 'Bild per URL einfügen', + insert: 'Einfügen', + cancel: 'Abbrechen', + imgTooLarge: 'Bild zu groß', + docTooLarge: 'Dokument zu groß, KI deaktiviert' + }, + fr: { + settings: 'Paramètres', + close: 'Fermer', + appearance: 'Apparence', + theme: 'Thème', + light: 'Clair', + dark: 'Sombre', + system: 'Système', + background: 'Arrière-plan', + default: 'Défaut', + warm: 'Chaud', + reading: 'Lampe de lecture', + image: 'Image personnalisée', + opacity: 'Opacité', + modelIntelligence: 'Intelligence du modèle', + thinkingLevel: 'Niveau de réflexion', + low: 'Bas', + medium: 'Moyen', + high: 'Haut', + lowDesc: 'Complétion directe (Le plus rapide)', + mediumDesc: 'Analyse brève avant suggestion', + highDesc: 'Analyse approfondie étape par étape (Le plus lent)', + debounceTime: 'Temps de rebond', + privacyPreferences: 'Confidentialité et préférences', + privacyMode: 'Mode confidentialité', + privacyDesc: 'Ne pas envoyer IP et préférences à l\'IA', + language: 'Langue', + auto: 'Détection auto', + currency: 'Devise', + about: 'À propos de nous', + importMd: 'Importer Markdown', + exportMd: 'Exporter Markdown', + uploadImg: 'Télécharger image', + enableAI: 'Activer IA', + disableAI: 'Désactiver IA', + insertUrl: 'Insérer image via URL', + insert: 'Insérer', + cancel: 'Annuler', + imgTooLarge: 'Image trop grande', + docTooLarge: 'Document trop grand, IA désactivée' + } +}