style: 简化文档卡片样式,优化布局间距

This commit is contained in:
2026-04-05 10:16:16 +08:00
parent 9ff51ac2f3
commit 7ed199aaf1
10 changed files with 260 additions and 298 deletions
+1
View File
@@ -43,4 +43,5 @@ env/
# IDE directories # IDE directories
.kilocode/ .kilocode/
.kilo/
.codex/ .codex/
+2 -2
View File
@@ -63,10 +63,10 @@ def _extract_message(response) -> tuple[str, str]:
async def call_ollama( async def call_ollama(
prompt: str, prompt: str,
*, *,
system_prompt: str = None, system_prompt: str | None = None,
tag: str = "default", tag: str = "default",
temperature: float = 0.7, temperature: float = 0.7,
thinking: str = None, thinking: str | None = None,
) -> dict: ) -> dict:
""" """
调用 Ollama API 并返回 content 和 thinking。 调用 Ollama API 并返回 content 和 thinking。
+16 -28
View File
@@ -1,6 +1,5 @@
import asyncio import asyncio
import base64 import base64
import json
import logging import logging
import os import os
import re import re
@@ -12,7 +11,7 @@ from typing import Optional
from fastapi import FastAPI, HTTPException, Request, Security, File, UploadFile from fastapi import FastAPI, HTTPException, Request, Security, File, UploadFile
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse, StreamingResponse, Response from fastapi.responses import JSONResponse, Response
from fastapi.security import APIKeyHeader from fastapi.security import APIKeyHeader
from pydantic import BaseModel from pydantic import BaseModel
@@ -125,10 +124,6 @@ def _sanitize_converted_markdown(text: str) -> str:
return value.strip() return value.strip()
def _sse_payload(payload: dict) -> str:
return f"data: {json.dumps(payload)}\n\n"
def get_client_ip(request: Request) -> str: def get_client_ip(request: Request) -> str:
if request.client: if request.client:
return request.headers.get("X-Client-IP") or request.client.host return request.headers.get("X-Client-IP") or request.client.host
@@ -186,11 +181,10 @@ async def create_completion(request: Request, req: CompletionRequest, api_key: s
) )
) )
async with ACTIVE_COMPLETIONS_LOCK: existing = ACTIVE_COMPLETIONS.get(request_id)
existing = ACTIVE_COMPLETIONS.get(request_id) if existing and not existing.done():
if existing and not existing.done(): existing.cancel()
existing.cancel() ACTIVE_COMPLETIONS[request_id] = inference_task
ACTIVE_COMPLETIONS[request_id] = inference_task
result = await inference_task result = await inference_task
content = result["content"] or "" content = result["content"] or ""
@@ -204,26 +198,17 @@ async def create_completion(request: Request, req: CompletionRequest, api_key: s
_preview(content, 120), _preview(content, 120),
) )
async def generate(): return JSONResponse(content={"content": content, "request_id": request_id})
yield _sse_payload({"content": content})
yield _sse_payload({"done": True})
return StreamingResponse(generate(), media_type="text/event-stream")
except asyncio.CancelledError: except asyncio.CancelledError:
logger.info("[%s] /v1/completions cancelled request_id=%s", request_tag, request_id) logger.info("[%s] /v1/completions cancelled request_id=%s", request_tag, request_id)
return JSONResponse(content={"cancelled": True, "request_id": request_id}, status_code=499)
async def cancelled():
yield _sse_payload({"cancelled": True, "request_id": request_id, "done": True})
return StreamingResponse(cancelled(), media_type="text/event-stream")
except Exception as e: except Exception as e:
logger.exception("[%s] /v1/completions failed request_id=%s: %s", request_tag, request_id, e) logger.exception("[%s] /v1/completions failed request_id=%s: %s", request_tag, request_id, e)
return JSONResponse(content={"error": str(e)}, status_code=500) return JSONResponse(content={"error": str(e)}, status_code=500)
finally: finally:
async with ACTIVE_COMPLETIONS_LOCK: active = ACTIVE_COMPLETIONS.get(request_id)
active = ACTIVE_COMPLETIONS.get(request_id) if active is not None and active is inference_task:
if active is not None and active is inference_task: ACTIVE_COMPLETIONS.pop(request_id, None)
ACTIVE_COMPLETIONS.pop(request_id, None)
@app.post("/v1/completions/cancel") @app.post("/v1/completions/cancel")
@@ -399,7 +384,10 @@ if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=8001) uvicorn.run(app, host="0.0.0.0", port=8001)
# TTS and ASR routes # TTS and ASR routes (lazy loaded to avoid heavy import on startup)
from tts_asr import register_tts_asr_routes def _register_tts_asr_routes():
register_tts_asr_routes(app) from tts_asr import register_tts_asr_routes
register_tts_asr_routes(app)
_register_tts_asr_routes()
+72 -71
View File
@@ -1,6 +1,13 @@
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
import re import re
from typing import Tuple from typing import Protocol, Tuple, runtime_checkable
@runtime_checkable
class UserPreferences(Protocol):
language: str
currency: str
timezone: str
def _get_current_datetime(timezone_pref: str = "auto") -> str: def _get_current_datetime(timezone_pref: str = "auto") -> str:
@@ -214,113 +221,107 @@ def _canonical_language_id(language_id: str) -> str:
return LANGUAGE_SYNONYMS.get(safe, safe) return LANGUAGE_SYNONYMS.get(safe, safe)
def _language_guidance(language_id: str) -> str: _JS_LANGS = {"javascript", "typescript"}
canonical = _canonical_language_id(language_id) _CODE_LANGS = {"python", "go", "rust", "java", "kotlin", "swift", "ruby", "php", "lua", "c", "cpp", "csharp", "r", "matlab", "dart"}
if canonical == "markdown":
return "" _LANG_GUIDANCE = {
if canonical == "mermaid": "mermaid": """
return """
Language-specific guidance (mermaid): Language-specific guidance (mermaid):
- Output valid Mermaid syntax only. - Output valid Mermaid syntax only.
- Prefer concise, syntactically correct diagram statements. - Prefer concise, syntactically correct diagram statements.
- Avoid prose unless the user prompt explicitly requires it.""" - Avoid prose unless the user prompt explicitly requires it.""",
if canonical == "latex": "latex": """
return """
Language-specific guidance (latex): Language-specific guidance (latex):
- Output LaTeX math content only when completing LaTeX. - Output LaTeX math content only when completing LaTeX.
- If CURSOR_IN_FENCED_CODE_BLOCK=true and CURSOR_FENCE_LANGUAGE is latex/tex/katex: - If CURSOR_IN_FENCED_CODE_BLOCK=true and CURSOR_FENCE_LANGUAGE is latex/tex/katex:
- Output raw LaTeX lines only. - Output raw LaTeX lines only.
- Do not wrap with $ or $$.""" - Do not wrap with $ or $$.""",
if canonical == "json": "json": """
return """
Language-specific guidance (json): Language-specific guidance (json):
- Output strict JSON only (no comments, no trailing commas). - Output strict JSON only (no comments, no trailing commas).
- Ensure valid quotes and braces.""" - Ensure valid quotes and braces.""",
if canonical == "yaml": "yaml": """
return """
Language-specific guidance (yaml): Language-specific guidance (yaml):
- Output valid YAML only. - Output valid YAML only.
- Use consistent indentation and avoid tabs.""" - Use consistent indentation and avoid tabs.""",
if canonical == "toml": "toml": """
return """
Language-specific guidance (toml): Language-specific guidance (toml):
- Output valid TOML only. - Output valid TOML only.
- Keep key types consistent.""" - Keep key types consistent.""",
if canonical == "ini": "ini": """
return """
Language-specific guidance (ini): Language-specific guidance (ini):
- Output valid INI only. - Output valid INI only.
- Keep section headers and key=value pairs consistent.""" - Keep section headers and key=value pairs consistent.""",
if canonical == "sql": "sql": """
return """
Language-specific guidance (sql): Language-specific guidance (sql):
- Output a single, valid SQL statement unless context requires multiple. - Output a single, valid SQL statement unless context requires multiple.
- Prefer ANSI SQL when dialect is unclear.""" - Prefer ANSI SQL when dialect is unclear.""",
if canonical == "bash": "bash": """
return """
Language-specific guidance (bash): Language-specific guidance (bash):
- Output POSIX-compatible shell when possible. - Output POSIX-compatible shell when possible.
- Avoid interactive prompts or destructive commands unless requested.""" - Avoid interactive prompts or destructive commands unless requested.""",
if canonical == "powershell": "powershell": """
return """
Language-specific guidance (powershell): Language-specific guidance (powershell):
- Output valid PowerShell commands. - Output valid PowerShell commands.
- Avoid destructive commands unless explicitly requested.""" - Avoid destructive commands unless explicitly requested.""",
if canonical == "html": "html": """
return """
Language-specific guidance (html): Language-specific guidance (html):
- Output valid HTML only. - Output valid HTML only.
- Keep markup minimal and well-formed.""" - Keep markup minimal and well-formed.""",
if canonical == "css": "css": """
return """
Language-specific guidance (css): Language-specific guidance (css):
- Output valid CSS only. - Output valid CSS only.
- Use concise, readable selectors.""" - Use concise, readable selectors.""",
if canonical == "diff": "diff": """
return """
Language-specific guidance (diff): Language-specific guidance (diff):
- Output a unified diff only. - Output a unified diff only.
- Ensure @@ hunk headers and +/- lines are consistent.""" - Ensure @@ hunk headers and +/- lines are consistent.""",
if canonical == "regex": "regex": """
return """
Language-specific guidance (regex): Language-specific guidance (regex):
- Output the regex pattern only. - Output the regex pattern only.
- Avoid delimiters unless explicitly requested.""" - Avoid delimiters unless explicitly requested.""",
if canonical in {"javascript", "typescript"}: "text": """
return f"""
Language-specific guidance ({canonical}):
- Output valid {canonical} code.
- Prefer modern syntax and avoid prose unless comments are needed."""
if canonical in {"python", "go", "rust", "java", "kotlin", "swift", "ruby", "php", "lua", "c", "cpp", "csharp", "r", "matlab", "dart"}:
return f"""
Language-specific guidance ({canonical}):
- Output valid {canonical} code.
- Avoid prose unless context clearly expects comments or docstrings."""
if canonical == "text":
return """
Language-specific guidance (text): Language-specific guidance (text):
- Output plain text only. - Output plain text only.
- Avoid markdown formatting unless explicitly asked.""" - Avoid markdown formatting unless explicitly asked.""",
if canonical == "xml": "xml": """
return """
Language-specific guidance (xml): Language-specific guidance (xml):
- Output well-formed XML only. - Output well-formed XML only.
- Ensure matching tags and proper escaping.""" - Ensure matching tags and proper escaping.""",
if canonical == "dockerfile": "dockerfile": """
return """
Language-specific guidance (dockerfile): Language-specific guidance (dockerfile):
- Output valid Dockerfile instructions only. - Output valid Dockerfile instructions only.
- Keep layers minimal and ordered logically.""" - Keep layers minimal and ordered logically.""",
if canonical == "makefile": "makefile": """
return """
Language-specific guidance (makefile): Language-specific guidance (makefile):
- Output valid Makefile syntax only. - Output valid Makefile syntax only.
- Use tabs for recipe lines.""" - Use tabs for recipe lines.""",
return f""" }
Language-specific guidance ({canonical}):
- Output valid {canonical} code. _GENERIC_CODE = """
Language-specific guidance ({lang}):
- Output valid {lang} code.
- Avoid prose unless context clearly expects comments or docstrings.""" - Avoid prose unless context clearly expects comments or docstrings."""
_JS_CODE = """
Language-specific guidance ({lang}):
- Output valid {lang} code.
- Prefer modern syntax and avoid prose unless comments are needed."""
def _language_guidance(language_id: str) -> str:
canonical = _canonical_language_id(language_id)
if canonical == "markdown":
return ""
guidance = _LANG_GUIDANCE.get(canonical)
if guidance:
return guidance
if canonical in _JS_LANGS:
return _JS_CODE.format(lang=canonical)
if canonical in _CODE_LANGS:
return _GENERIC_CODE.format(lang=canonical)
return _GENERIC_CODE.format(lang=canonical)
def build_inline_system_prompt(language_id: str = "markdown") -> str: def build_inline_system_prompt(language_id: str = "markdown") -> str:
safe_language_id = _canonical_language_id(language_id) safe_language_id = _canonical_language_id(language_id)
@@ -520,7 +521,7 @@ def build_completion_prompts(
language_id: str = "markdown", language_id: str = "markdown",
location: str = "", location: str = "",
thinking_level: str = "low", thinking_level: str = "low",
preferences: object = None, preferences: UserPreferences | None = None,
) -> Tuple[str, str]: ) -> Tuple[str, str]:
safe_language_id = _canonical_language_id(language_id) safe_language_id = _canonical_language_id(language_id)
recent_prefix, recent_suffix = _prepare_context(prefix, suffix) recent_prefix, recent_suffix = _prepare_context(prefix, suffix)
@@ -601,7 +602,7 @@ def build_prompt(
language_id: str = "markdown", language_id: str = "markdown",
location: str = "", location: str = "",
thinking_level: str = "low", thinking_level: str = "low",
preferences: object = None, preferences: UserPreferences | None = None,
) -> str: ) -> str:
""" """
Backward-compatible helper. Returns only the user prompt body. Backward-compatible helper. Returns only the user prompt body.
+2 -1
View File
@@ -197,8 +197,9 @@ class ASRResponse(BaseModel):
def get_api_key(api_key: str): def get_api_key(api_key: str):
from backend.main import API_KEY import main
API_KEY = main.API_KEY
if api_key != API_KEY: if api_key != API_KEY:
raise HTTPException(status_code=403, detail="API Key 无效") raise HTTPException(status_code=403, detail="API Key 无效")
return api_key return api_key
+113 -131
View File
@@ -1,38 +1,13 @@
<template> <template>
<div class="doc-block-crepe" :class="{ collapsed: collapsedState }"> <section class="doc-card" :class="{ 'is-collapsed': collapsedState }">
<div class="doc-header"> <header class="doc-card__header">
<div class="doc-accent"></div> <div class="doc-card__badge">{{ typeLabel }}</div>
<div class="doc-icon"> <div class="doc-card__meta">
<svg v-if="docType === 'pdf'" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <div class="doc-card__name">{{ docName }}</div>
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/> <div class="doc-card__time">{{ displayTime }}</div>
<polyline points="14 2 14 8 20 8"/>
<path d="M8 13h5"/>
<path d="M8 17h8"/>
</svg>
<svg v-else-if="docType === 'docx'" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
<polyline points="14 2 14 8 20 8"/>
<path d="m8 13 2 4 2-4 2 4 2-4"/>
</svg>
<svg v-else-if="docType === 'pptx'" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="4" width="18" height="12" rx="2"/>
<path d="M8 20h8"/>
<path d="M12 16v4"/>
<path d="M9 8h3a2 2 0 0 1 0 4H9z"/>
</svg>
<svg v-else width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
<polyline points="14 2 14 8 20 8"/>
<path d="M8 13h8"/>
<path d="M8 17h5"/>
</svg>
</div> </div>
<div class="doc-meta"> <div class="doc-card__actions">
<div class="doc-name">{{ docName }}</div> <button type="button" class="doc-card__btn" :title="collapsedState ? '展开文件' : '折叠文件'" @click="toggleCollapse">
<div class="doc-subline">{{ typeLabel }} · {{ displayTime }}</div>
</div>
<div class="doc-actions">
<button type="button" class="action-btn" :title="collapsedState ? '展开文件' : '折叠文件'" @click="toggleCollapse">
<svg v-if="collapsedState" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <svg v-if="collapsedState" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="9 18 15 12 9 6"/> <polyline points="9 18 15 12 9 6"/>
</svg> </svg>
@@ -40,7 +15,7 @@
<polyline points="6 9 12 15 18 9"/> <polyline points="6 9 12 15 18 9"/>
</svg> </svg>
</button> </button>
<button type="button" class="action-btn action-btn-danger" title="删除文件" @click="props.onDelete?.()"> <button type="button" class="doc-card__btn doc-card__btn--danger" title="删除文件" @click="props.onDelete?.()">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M3 6h18"/> <path d="M3 6h18"/>
<path d="M8 6V4h8v2"/> <path d="M8 6V4h8v2"/>
@@ -50,11 +25,11 @@
</svg> </svg>
</button> </button>
</div> </div>
</header>
<div v-show="!collapsedState" class="doc-card__body">
<div ref="editorRoot" class="doc-card__editor"></div>
</div> </div>
<div v-show="!collapsedState" class="doc-editor"> </section>
<div ref="editorRoot" class="inner-crepe"></div>
</div>
</div>
</template> </template>
<script setup> <script setup>
@@ -82,14 +57,7 @@ const collapsedState = ref(Boolean(props.collapsed))
const currentContent = ref(props.content || '') const currentContent = ref(props.content || '')
let crepe = null let crepe = null
let syncTimer = null let syncTimer = null
let applyingExternalContent = false let syncingExternal = false
const displayTime = computed(() => {
if (!props.uploadTime) return '刚上传'
const date = new Date(props.uploadTime)
if (Number.isNaN(date.getTime())) return '刚上传'
return date.toLocaleString('zh-CN', { hour12: false })
})
const typeLabel = computed(() => { const typeLabel = computed(() => {
if (props.docType === 'docx') return 'DOCX' if (props.docType === 'docx') return 'DOCX'
@@ -98,6 +66,13 @@ const typeLabel = computed(() => {
return 'TXT' return 'TXT'
}) })
const displayTime = computed(() => {
if (!props.uploadTime) return '刚上传'
const date = new Date(props.uploadTime)
if (Number.isNaN(date.getTime())) return '刚上传'
return date.toLocaleString('zh-CN', { hour12: false })
})
const toggleCollapse = () => { const toggleCollapse = () => {
collapsedState.value = !collapsedState.value collapsedState.value = !collapsedState.value
props.onUpdateCollapsed?.(collapsedState.value) props.onUpdateCollapsed?.(collapsedState.value)
@@ -107,7 +82,7 @@ const syncContent = () => {
if (!crepe) return if (!crepe) return
if (syncTimer) clearTimeout(syncTimer) if (syncTimer) clearTimeout(syncTimer)
syncTimer = setTimeout(async () => { syncTimer = setTimeout(async () => {
if (!crepe || applyingExternalContent) return if (!crepe || syncingExternal) return
const markdown = await crepe.getMarkdown() const markdown = await crepe.getMarkdown()
currentContent.value = markdown currentContent.value = markdown
props.onUpdateContent?.(markdown) props.onUpdateContent?.(markdown)
@@ -115,22 +90,23 @@ const syncContent = () => {
} }
const syncExternalContent = async (nextValue) => { const syncExternalContent = async (nextValue) => {
const value = nextValue || ''
if (!crepe) { if (!crepe) {
currentContent.value = nextValue || '' currentContent.value = value
return return
} }
if ((nextValue || '') === currentContent.value) return if (value === currentContent.value) return
applyingExternalContent = true syncingExternal = true
try { try {
crepe.editor.action(replaceAll(nextValue || '')) crepe.editor.action(replaceAll(value))
currentContent.value = nextValue || '' currentContent.value = value
} finally { } finally {
applyingExternalContent = false syncingExternal = false
} }
} }
watch(() => props.content, (nextValue) => { watch(() => props.content, (nextValue) => {
void syncExternalContent(nextValue || '') void syncExternalContent(nextValue)
}) })
watch(() => props.collapsed, (nextValue) => { watch(() => props.collapsed, (nextValue) => {
@@ -169,7 +145,6 @@ onMounted(async () => {
crepe.editor.use(copilotConfigCtx) crepe.editor.use(copilotConfigCtx)
crepe.editor.use(copilotGhostMark) crepe.editor.use(copilotGhostMark)
crepe.editor.use(copilotPlugin) crepe.editor.use(copilotPlugin)
await crepe.create() await crepe.create()
crepe.on((listener) => { crepe.on((listener) => {
@@ -201,129 +176,136 @@ onUnmounted(() => {
</script> </script>
<style scoped> <style scoped>
.doc-block-crepe { .doc-card {
position: relative; width: 100%;
margin: 14px 0; max-width: 100%;
border: 1px solid color-mix(in srgb, var(--panel-border) 72%, transparent); margin: 8px 0;
border-radius: 18px; border-radius: 12px;
border: 1px solid rgba(59, 130, 246, 0.12);
background: rgba(255, 255, 255, 0.78);
box-shadow: 0 2px 8px rgba(59, 130, 246, 0.06), 0 1px 3px rgba(0, 0, 0, 0.04);
overflow: hidden; overflow: hidden;
background: linear-gradient(180deg, color-mix(in srgb, var(--panel-bg) 82%, transparent) 0%, color-mix(in srgb, var(--crepe-color-surface-low) 88%, transparent) 100%); backdrop-filter: blur(10px);
box-shadow: 0 18px 38px rgba(15, 23, 42, 0.08), inset 0 1px 0 rgba(255, 255, 255, 0.08); position: relative;
backdrop-filter: blur(14px);
} }
.doc-header { .doc-card__header {
display: grid; display: grid;
grid-template-columns: 4px 24px minmax(0, 1fr) auto; grid-template-columns: auto minmax(0, 1fr) auto;
gap: 10px;
align-items: center; align-items: center;
gap: 12px; padding: 8px 12px;
padding: 12px 14px; border-bottom: 1px solid rgba(59, 130, 246, 0.1);
background: linear-gradient(135deg, color-mix(in srgb, var(--btn-bg) 76%, transparent) 0%, color-mix(in srgb, var(--crepe-color-surface) 78%, transparent) 100%); background: rgba(255, 255, 255, 0.6);
border-bottom: 1px solid color-mix(in srgb, var(--panel-border) 76%, transparent);
} }
.doc-accent { .doc-card__badge {
width: 4px; min-width: 48px;
height: 36px; padding: 4px 10px;
border-radius: 999px; border-radius: 999px;
background: linear-gradient(180deg, #4f8cff 0%, #7dc1ff 100%); background: linear-gradient(135deg, #3b82f6 0%, #60a5fa 100%);
box-shadow: 0 0 18px rgba(79, 140, 255, 0.32); color: #fff;
font-size: 10px;
font-weight: 600;
letter-spacing: 0.08em;
text-align: center;
box-shadow: 0 2px 6px rgba(59, 130, 246, 0.2);
} }
.doc-icon { .doc-card__meta {
display: flex;
align-items: center;
justify-content: center;
color: var(--btn-fg);
}
.doc-meta {
min-width: 0; min-width: 0;
} }
.doc-name { .doc-card__name {
font-size: 14px; color: #1e293b;
font-size: 13px;
font-weight: 600; font-weight: 600;
color: var(--app-text);
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
} }
.doc-subline { .doc-card__time {
margin-top: 2px; margin-top: 2px;
font-size: 11px; color: #64748b;
color: var(--muted-text); font-size: 10px;
} }
.doc-actions { .doc-card__actions {
display: flex; display: flex;
align-items: center; gap: 4px;
gap: 6px;
} }
.action-btn { .doc-card__btn {
width: 30px; width: 26px;
height: 30px; height: 26px;
border: 1px solid color-mix(in srgb, var(--panel-border) 72%, transparent); border: 1px solid rgba(59, 130, 246, 0.12);
border-radius: 10px; border-radius: 8px;
background: color-mix(in srgb, var(--btn-bg) 84%, transparent); background: rgba(255, 255, 255, 0.5);
color: var(--btn-fg); color: #64748b;
cursor: pointer;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
transition: transform 0.14s ease, background-color 0.14s ease, border-color 0.14s ease; cursor: pointer;
transition: all 0.15s ease;
} }
.action-btn:hover { .doc-card__btn:hover {
transform: translateY(-1px); background: rgba(59, 130, 246, 0.1);
background: var(--btn-hover-bg); border-color: rgba(59, 130, 246, 0.25);
border-color: var(--btn-hover-bg); color: #3b82f6;
color: var(--btn-hover-fg);
} }
.action-btn-danger:hover { .doc-card__btn--danger:hover {
background: rgba(220, 38, 38, 0.12); background: rgba(239, 68, 68, 0.1);
border-color: rgba(220, 38, 38, 0.22); border-color: rgba(239, 68, 68, 0.2);
color: #dc2626; color: #ef4444;
} }
.doc-editor { .doc-card__body {
padding: 10px 12px 12px; padding: 8px 10px;
background: rgba(248, 250, 252, 0.5);
} }
.inner-crepe { .doc-card__editor {
border-radius: 14px; min-height: 48px;
border-radius: 8px;
border: 1px solid rgba(59, 130, 246, 0.08);
background: rgba(255, 255, 255, 0.6);
overflow: hidden; overflow: hidden;
background: color-mix(in srgb, var(--crepe-color-background) 78%, transparent);
border: 1px solid color-mix(in srgb, var(--panel-border) 68%, transparent);
} }
.inner-crepe :deep(.milkdown) { .doc-card__editor :deep(.milkdown) {
background: transparent !important; background: transparent !important;
} }
.inner-crepe :deep(.milkdown__main), .doc-card__editor :deep(.milkdown__main),
.inner-crepe :deep(.milkdown__editor) { .doc-card__editor :deep(.milkdown__editor) {
margin: 0 !important; margin: 0 !important;
padding: 0 !important; padding: 0 !important;
} }
.inner-crepe :deep(.ProseMirror) { .doc-card__editor :deep(.ProseMirror) {
min-height: 92px; min-height: 80px;
padding: 10px 12px 14px !important; padding: 10px 12px 12px !important;
font-size: 14px !important; font-size: 13px !important;
line-height: 1.7; line-height: 1.6;
} }
.inner-crepe :deep(.ProseMirror h1), .doc-card__editor :deep(.ProseMirror > *:last-child) {
.inner-crepe :deep(.ProseMirror h2), margin-bottom: 0;
.inner-crepe :deep(.ProseMirror h3), }
.inner-crepe :deep(.ProseMirror p),
.inner-crepe :deep(.ProseMirror li), .doc-card__editor :deep(.ProseMirror p:first-child) {
.inner-crepe :deep(.ProseMirror blockquote), margin-top: 0;
.inner-crepe :deep(.ProseMirror code) { }
font-size: inherit;
.doc-card__editor :deep(.milkdown__toolbar),
.doc-card__editor :deep(.milkdown__menu),
.doc-card__editor :deep(.milkdown__statusbar),
.doc-card__editor :deep(.milkdown-slate-toolbar),
.doc-card__editor :deep(.milkdown-bubble-menu) {
display: none !important;
} }
</style> </style>
+27 -29
View File
@@ -107,78 +107,76 @@ const downloadDoc = () => {
<style scoped> <style scoped>
.doc-block { .doc-block {
margin: 12px 0; margin: 8px 0;
border-radius: 8px; border-radius: 10px;
overflow: hidden; overflow: hidden;
background: var(--crepe-color-surface-low); background: rgba(255, 255, 255, 0.72);
border: 1px solid var(--panel-border); backdrop-filter: blur(12px);
border: 1px solid rgba(59, 130, 246, 0.15);
box-shadow: 0 2px 8px rgba(59, 130, 246, 0.06), 0 1px 2px rgba(0, 0, 0, 0.04);
} }
.doc-block.collapsed .doc-content { .doc-block.collapsed .doc-content {
display: none; display: none;
} }
/* 深色条 */
.doc-header { .doc-header {
display: flex; display: flex;
align-items: center; align-items: center;
padding: 10px 12px; padding: 6px 10px;
background: var(--crepe-color-surface); background: rgba(255, 255, 255, 0.85);
border-bottom: 1px solid var(--panel-border); border-bottom: 1px solid rgba(59, 130, 246, 0.12);
gap: 10px; gap: 8px;
} }
/* 文件类型icon */
.doc-icon { .doc-icon {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
color: var(--crepe-color-primary); color: #3b82f6;
} }
/* 文件名 */
.doc-name { .doc-name {
flex: 1; flex: 1;
font-size: 14px; font-size: 13px;
font-weight: 500; font-weight: 500;
color: var(--crepe-color-on-surface); color: #1e293b;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
} }
/* 操作按钮 */
.doc-actions { .doc-actions {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 4px; gap: 2px;
} }
.action-btn { .action-btn {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
width: 28px; width: 24px;
height: 28px; height: 24px;
padding: 0; padding: 0;
border: none; border: none;
background: transparent; background: transparent;
color: var(--crepe-color-on-surface-variant); color: #64748b;
cursor: pointer; cursor: pointer;
border-radius: 4px; border-radius: 6px;
opacity: 0.7; opacity: 0.75;
} }
.action-btn:hover { .action-btn:hover {
background: var(--crepe-color-hover); background: rgba(59, 130, 246, 0.1);
color: #3b82f6;
opacity: 1; opacity: 1;
} }
/* 浅色块:文档内容 */
.doc-content { .doc-content {
padding: 12px; padding: 8px 10px;
background: var(--crepe-color-surface-low); background: rgba(248, 250, 252, 0.6);
max-height: 400px; max-height: 240px;
overflow-y: auto; overflow-y: auto;
} }
@@ -186,9 +184,9 @@ const downloadDoc = () => {
margin: 0; margin: 0;
padding: 0; padding: 0;
font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Mono', monospace; font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Mono', monospace;
font-size: 13px; font-size: 12px;
line-height: 1.6; line-height: 1.5;
color: var(--crepe-color-on-surface); color: #334155;
white-space: pre-wrap; white-space: pre-wrap;
word-break: break-word; word-break: break-word;
} }
+21 -3
View File
@@ -170,7 +170,7 @@ import { useSettingsStore } from '../stores/settings'
import { OCR_URL, EXPORT_PDF_URL } from '../utils/config.js' import { OCR_URL, EXPORT_PDF_URL } from '../utils/config.js'
import { convertFileToMarkdown } from '../utils/convert.js' import { convertFileToMarkdown } from '../utils/convert.js'
import { setOcrCache, clearOcrCache, clearAllOcrCache, IMAGE_SIZE_LIMIT, calculateImageHash, getOcrByHash, setOcrByHash } from '../utils/ocrCache.js' import { setOcrCache, clearOcrCache, clearAllOcrCache, IMAGE_SIZE_LIMIT, calculateImageHash, getOcrByHash, setOcrByHash } from '../utils/ocrCache.js'
import { DOC_BLOCK_NODE_TYPE, buildLegacyDocBlock, getDocTypeFromFilename, isSupportedDocFile, transformDocBlockMarkdownForClipboard, transformLegacyDocBlocksForExport, transformSpecialDocBlocksToLegacy } from '../utils/docBlock.js' import { DOC_BLOCK_NODE_TYPE, getDocTypeFromFilename, isSupportedDocFile, transformDocBlockMarkdownForClipboard, transformLegacyDocBlocksForExport, transformSpecialDocBlocksToLegacy } from '../utils/docBlock.js'
const emit = defineEmits(['update:markdown']) const emit = defineEmits(['update:markdown'])
const settings = useSettingsStore() const settings = useSettingsStore()
@@ -887,8 +887,26 @@ const insertMarkdownAtCursor = (markdown) => {
const insertDocBlockAtCursor = (attrs) => { const insertDocBlockAtCursor = (attrs) => {
if (!crepe) return if (!crepe) return
const markdown = buildLegacyDocBlock(attrs) crepe.editor.action((ctx) => {
insertMarkdownAtCursor(`\n${markdown}\n`) const view = ctx.get(editorViewCtx)
const { state } = view
const { from, to } = state.selection
const docBlockType = state.schema.nodes[DOC_BLOCK_NODE_TYPE]
if (!docBlockType) return
const blockNode = docBlockType.create({
docType: attrs.docType,
docName: attrs.docName,
uploadTime: attrs.uploadTime,
content: attrs.content,
collapsed: Boolean(attrs.collapsed),
})
const tr = state.tr.replaceRangeWith(from, to, blockNode)
const nextPos = Math.min(from + blockNode.nodeSize, tr.doc.content.size)
tr.setSelection(Selection.near(tr.doc.resolve(nextPos), 1))
view.dispatch(tr.scrollIntoView())
view.focus()
})
} }
const triggerFileUpload = () => { const triggerFileUpload = () => {
+5 -33
View File
@@ -1,4 +1,4 @@
import { API_URL } from './config.js' import { API_URL, API_KEY } from './config.js'
import { useSettingsStore } from '../stores/settings' import { useSettingsStore } from '../stores/settings'
function generateRequestId() { function generateRequestId() {
@@ -30,6 +30,7 @@ async function sendCancelRequest(cancelUrl, requestId, reason) {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'X-API-Key': API_KEY,
}, },
body: JSON.stringify({ body: JSON.stringify({
request_id: requestId, request_id: requestId,
@@ -73,6 +74,7 @@ export async function fetchSuggestion(prefix, suffix, languageId, signal, apiUrl
const headers = { const headers = {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'X-Request-Id': requestId, 'X-Request-Id': requestId,
'X-API-Key': API_KEY,
} }
const body = { const body = {
@@ -100,38 +102,8 @@ export async function fetchSuggestion(prefix, suffix, languageId, signal, apiUrl
throw new Error(`HTTP ${res.status}: ${errorText}`) throw new Error(`HTTP ${res.status}: ${errorText}`)
} }
const reader = res.body?.getReader() const data = await res.json()
if (!reader) { return data.content || ''
throw new Error('No reader available')
}
let text = ''
let buffer = ''
while (true) {
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()
if (!jsonStr) continue
try {
const data = JSON.parse(jsonStr)
if (data.content) {
text += data.content
}
if (data.done || data.error) break
} catch (e) {
// skip invalid lines
}
}
}
return text
} catch (e) { } catch (e) {
if (e.name === 'AbortError') { if (e.name === 'AbortError') {
// ignore abort // ignore abort
+1
View File
@@ -6,3 +6,4 @@ export const API_URL = import.meta.env.VITE_API_URL || `${API_BASE_URL}/v1/compl
export const OCR_URL = import.meta.env.VITE_OCR_URL || `${API_BASE_URL}/v1/ocr` export const OCR_URL = import.meta.env.VITE_OCR_URL || `${API_BASE_URL}/v1/ocr`
export const CONVERT_URL = import.meta.env.VITE_CONVERT_URL || `${API_BASE_URL}/v1/convert` export const CONVERT_URL = import.meta.env.VITE_CONVERT_URL || `${API_BASE_URL}/v1/convert`
export const EXPORT_PDF_URL = import.meta.env.VITE_EXPORT_PDF_URL || '/v1/export/pdf' export const EXPORT_PDF_URL = import.meta.env.VITE_EXPORT_PDF_URL || '/v1/export/pdf'
export const API_KEY = import.meta.env.VITE_API_KEY || 'your-secret-key-here'