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
This commit is contained in:
2026-02-19 10:22:27 +08:00
parent d2b64ad5d6
commit aa6133e3ed
24 changed files with 1291 additions and 222 deletions
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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')
+33 -9
View File
@@ -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 ""
+29 -3
View File
@@ -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) <OCR:description>.