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