Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9904b9bd78 | |||
| 7ed199aaf1 |
@@ -43,4 +43,5 @@ env/
|
|||||||
|
|
||||||
# IDE directories
|
# IDE directories
|
||||||
.kilocode/
|
.kilocode/
|
||||||
|
.kilo/
|
||||||
.codex/
|
.codex/
|
||||||
+2
-2
@@ -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。
|
||||||
|
|||||||
+7
-19
@@ -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,7 +181,6 @@ 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()
|
||||||
@@ -204,23 +198,14 @@ 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)
|
||||||
@@ -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)
|
||||||
|
def _register_tts_asr_routes():
|
||||||
from tts_asr import register_tts_asr_routes
|
from tts_asr import register_tts_asr_routes
|
||||||
register_tts_asr_routes(app)
|
register_tts_asr_routes(app)
|
||||||
|
|
||||||
|
_register_tts_asr_routes()
|
||||||
|
|
||||||
|
|||||||
+256
-197
@@ -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)
|
||||||
@@ -330,82 +331,103 @@ def build_inline_system_prompt(language_id: str = "markdown") -> str:
|
|||||||
|
|
||||||
Return only the insertion text that should be placed between PREFIX and SUFFIX.
|
Return only the insertion text that should be placed between PREFIX and SUFFIX.
|
||||||
|
|
||||||
Hard constraints you must follow:
|
CORE PRINCIPLE: Output insertion text only. No explanations, no meta labels, no wrapper quotes.
|
||||||
1) Output-only contract:
|
|
||||||
- Output insertion text only.
|
|
||||||
- No explanations, no meta labels, no wrapper quotes around the whole answer.
|
|
||||||
|
|
||||||
2) Strict math formatting (KaTeX):
|
PRIORITY 1: CONTEXT AWARENESS (Read these flags from user prompt)
|
||||||
- If you output any math expression, it must be strict KaTeX-compatible math.
|
- CURSOR_IN_FENCED_CODE_BLOCK: Are you inside a code fence?
|
||||||
- Every formula must be wrapped with either $...$ (inline) or $$...$$ (block).
|
- CURSOR_FENCE_LANGUAGE: What language is the current fence?
|
||||||
- Never output bare formulas without $ or $$ wrappers.
|
- PREFIX_ENDS_WITH_NEWLINE: Does prefix end with newline?
|
||||||
- Exception: If CURSOR_IN_FENCED_CODE_BLOCK=true and CURSOR_FENCE_LANGUAGE is latex/tex/katex,
|
- SUFFIX_STARTS_WITH_NEWLINE: Does suffix start with newline?
|
||||||
output raw LaTeX without $ or $$ wrappers.
|
- MERMAID_CONTEXT: Is this a Mermaid diagram context?
|
||||||
|
|
||||||
3) Strict code formatting:
|
PRIORITY 2: SPECIALIZED CONTENT RULES
|
||||||
- Read CURSOR_IN_FENCED_CODE_BLOCK from the user prompt.
|
|
||||||
- If CURSOR_IN_FENCED_CODE_BLOCK=true:
|
2.1 Code Block Handling:
|
||||||
- You are already inside a fenced code block.
|
If CURSOR_IN_FENCED_CODE_BLOCK=true:
|
||||||
- Never output triple backticks.
|
- You are inside a code fence
|
||||||
- Output code lines only.
|
- Output code lines ONLY (no triple backticks)
|
||||||
- If CURSOR_IN_FENCED_CODE_BLOCK=false:
|
- Use single \\n for code line separation
|
||||||
- Any code output must be in a fenced code block with a language tag:
|
|
||||||
|
If CURSOR_IN_FENCED_CODE_BLOCK=false and code needed:
|
||||||
|
- Wrap code in fenced block with language tag:
|
||||||
```{{language}}
|
```{{language}}
|
||||||
...
|
code here
|
||||||
```
|
```
|
||||||
- Do not output code snippets as inline backticks.
|
- Never use inline backticks for code snippets
|
||||||
- Choose the language tag from context (no default fallback tag instruction).
|
|
||||||
|
|
||||||
4) Mermaid-specific completion rules:
|
2.2 Math Formatting (KaTeX):
|
||||||
- Read CURSOR_FENCE_LANGUAGE and MERMAID_CONTEXT from the user prompt.
|
- Inline math: wrap with $...$
|
||||||
- If CURSOR_FENCE_LANGUAGE=mermaid:
|
- Block math: wrap with $$...$$
|
||||||
- Output Mermaid statements only.
|
- Never output bare formulas
|
||||||
- Never output triple backticks.
|
- Exception: inside latex/tex/katex fence, output raw LaTeX
|
||||||
- Never output prose explanations.
|
|
||||||
- If CURSOR_IN_FENCED_CODE_BLOCK=false and MERMAID_CONTEXT=true:
|
2.3 Mermaid Diagrams:
|
||||||
- Output a complete Mermaid fenced block:
|
If CURSOR_FENCE_LANGUAGE=mermaid:
|
||||||
|
- Output Mermaid syntax ONLY
|
||||||
|
- No backticks, no explanations
|
||||||
|
|
||||||
|
If MERMAID_CONTEXT=true and outside fence:
|
||||||
|
- Output complete fenced block:
|
||||||
```mermaid
|
```mermaid
|
||||||
...
|
diagram syntax
|
||||||
```
|
```
|
||||||
- Keep Mermaid syntax valid and concise.
|
|
||||||
- Never mix Mermaid code and explanatory narration in one output.
|
|
||||||
|
|
||||||
5) Boundary newline repair:
|
PRIORITY 3: MARKDOWN STRUCTURE
|
||||||
- Read PREFIX_ENDS_WITH_NEWLINE and SUFFIX_STARTS_WITH_NEWLINE from the user prompt.
|
|
||||||
- Carefully reason about whether OUTPUT should start or end with a newline.
|
|
||||||
- If PREFIX lacks a required boundary newline, add it at OUTPUT start.
|
|
||||||
- If SUFFIX lacks a required boundary newline, add it at OUTPUT end.
|
|
||||||
- Ensure PREFIX + OUTPUT + SUFFIX is structurally natural.
|
|
||||||
|
|
||||||
6) Context stitching:
|
3.1 Newline Semantics:
|
||||||
- Do not repeat text that already appears at the start of SUFFIX.
|
- Single \\n: soft break (same paragraph, renders as space or <br>)
|
||||||
- Preserve nearby language, tone, punctuation, indentation, and markdown structure.
|
- Double \\n\\n: hard break (new paragraph/block)
|
||||||
- Continue existing structures naturally (lists, tables, block quotes, headings).
|
- Use \\n\\n for: new paragraphs, before headings, starting lists/tables
|
||||||
|
- Use \\n for: continuation within blocks (list items, table cells)
|
||||||
|
- Exception: inside code blocks, use \\n freely for code lines
|
||||||
|
|
||||||
7) OCR safety:
|
3.2 Boundary Management:
|
||||||
- PREFIX may include hidden OCR metadata tags like <OCR:...>.
|
Check PREFIX_ENDS_WITH_NEWLINE and SUFFIX_STARTS_WITH_NEWLINE:
|
||||||
- Never output any OCR tag.
|
- If PREFIX lacks needed newline: start OUTPUT with \\n
|
||||||
- Never output OCR tag fragments such as <OCR:...>."""
|
- If SUFFIX lacks needed newline: end OUTPUT with \\n
|
||||||
|
- Common cases requiring leading \\n:
|
||||||
|
* Starting a list after "Steps:"
|
||||||
|
* Creating new paragraph after text
|
||||||
|
* Adding heading after paragraph
|
||||||
|
- Common cases requiring trailing \\n:
|
||||||
|
* Before new heading
|
||||||
|
* End of section
|
||||||
|
|
||||||
|
3.3 Context Stitching:
|
||||||
|
- Never repeat text from SUFFIX beginning
|
||||||
|
- Match PREFIX tone, style, indentation
|
||||||
|
- Continue structures: lists, tables, quotes, headings
|
||||||
|
|
||||||
|
PRIORITY 4: HIDDEN CONTEXT
|
||||||
|
- OCR metadata like <OCR:...> is hidden context
|
||||||
|
- Never copy OCR tags to output
|
||||||
|
- Use OCR content as semantic hint only
|
||||||
|
"""
|
||||||
|
|
||||||
if language_guidance:
|
if language_guidance:
|
||||||
system_prompt = f"{system_prompt.rstrip()}\n{language_guidance.strip()}"
|
system_prompt = f"{system_prompt.rstrip()}\\n{language_guidance.strip()}"
|
||||||
|
|
||||||
return system_prompt.strip()
|
return system_prompt.strip()
|
||||||
|
|
||||||
|
|
||||||
INLINE_EXAMPLES = """[EX01] Prose continuation
|
INLINE_EXAMPLES = """=== CATEGORY A: PROSE CONTINUATION ===
|
||||||
|
|
||||||
|
[EX01] Simple prose continuation
|
||||||
<PREFIX>The quick brown fox </PREFIX>
|
<PREFIX>The quick brown fox </PREFIX>
|
||||||
<SUFFIX>jumps over the lazy dog.</SUFFIX>
|
<SUFFIX>jumps over the lazy dog.</SUFFIX>
|
||||||
Expected OUTPUT:
|
Expected OUTPUT:
|
||||||
moved quietly and then
|
moved quietly and then
|
||||||
|
|
||||||
[EX02] Avoid repeating suffix beginning
|
[EX02] Avoid repeating suffix
|
||||||
<PREFIX>Our launch plan starts with </PREFIX>
|
<PREFIX>Our launch plan starts with </PREFIX>
|
||||||
<SUFFIX>phase one, followed by phase two.</SUFFIX>
|
<SUFFIX>phase one, followed by phase two.</SUFFIX>
|
||||||
Expected OUTPUT:
|
Expected OUTPUT:
|
||||||
careful internal testing before
|
careful internal testing before
|
||||||
|
WRONG: phase one starts with (repeats suffix)
|
||||||
|
|
||||||
[EX03] Continue markdown checklist
|
=== CATEGORY B: MARKDOWN STRUCTURES ===
|
||||||
|
|
||||||
|
[EX03] Continue checklist
|
||||||
<PREFIX>## TODO
|
<PREFIX>## TODO
|
||||||
- [ ] Buy milk
|
- [ ] Buy milk
|
||||||
- [ ] </PREFIX>
|
- [ ] </PREFIX>
|
||||||
@@ -413,41 +435,7 @@ careful internal testing before
|
|||||||
Expected OUTPUT:
|
Expected OUTPUT:
|
||||||
Write release notes and share draft with team
|
Write release notes and share draft with team
|
||||||
|
|
||||||
[EX04] Cursor outside code block, code must use fenced block
|
[EX04] Start list after header (PREFIX lacks newline)
|
||||||
CURSOR_IN_FENCED_CODE_BLOCK=false
|
|
||||||
<PREFIX>Parse this JSON payload in Python:</PREFIX>
|
|
||||||
<SUFFIX></SUFFIX>
|
|
||||||
Expected OUTPUT:
|
|
||||||
```python
|
|
||||||
import json
|
|
||||||
data = json.loads(payload)
|
|
||||||
```
|
|
||||||
|
|
||||||
[EX05] Cursor inside fenced code block, do not output fences
|
|
||||||
CURSOR_IN_FENCED_CODE_BLOCK=true
|
|
||||||
<PREFIX>```python
|
|
||||||
def add(a, b):
|
|
||||||
return </PREFIX>
|
|
||||||
<SUFFIX>
|
|
||||||
```</SUFFIX>
|
|
||||||
Expected OUTPUT:
|
|
||||||
a + b
|
|
||||||
|
|
||||||
[EX06] Inline math must use $...$
|
|
||||||
<PREFIX>The derivative of x^2 is </PREFIX>
|
|
||||||
<SUFFIX>.</SUFFIX>
|
|
||||||
Expected OUTPUT:
|
|
||||||
$2x$
|
|
||||||
|
|
||||||
[EX07] Block math must use $$...$$
|
|
||||||
<PREFIX>We can write the Gaussian integral as:</PREFIX>
|
|
||||||
<SUFFIX></SUFFIX>
|
|
||||||
Expected OUTPUT:
|
|
||||||
$$
|
|
||||||
\\int_{-\\infty}^{\\infty} e^{-x^2}\\,dx = \\sqrt{\\pi}
|
|
||||||
$$
|
|
||||||
|
|
||||||
[EX08] Prefix misses boundary newline; add newline at output start
|
|
||||||
PREFIX_ENDS_WITH_NEWLINE=false
|
PREFIX_ENDS_WITH_NEWLINE=false
|
||||||
<PREFIX>Deployment steps:</PREFIX>
|
<PREFIX>Deployment steps:</PREFIX>
|
||||||
<SUFFIX></SUFFIX>
|
<SUFFIX></SUFFIX>
|
||||||
@@ -456,21 +444,7 @@ Expected OUTPUT:
|
|||||||
- Build artifact
|
- Build artifact
|
||||||
- Deploy service
|
- Deploy service
|
||||||
|
|
||||||
[EX09] Suffix misses boundary newline; add newline at output end
|
[EX05] Continue table row
|
||||||
SUFFIX_STARTS_WITH_NEWLINE=false
|
|
||||||
<PREFIX>Summary paragraph complete.</PREFIX>
|
|
||||||
<SUFFIX>## Next Section</SUFFIX>
|
|
||||||
Expected OUTPUT:
|
|
||||||
|
|
||||||
|
|
||||||
[EX10] OCR metadata exists but must never be emitted
|
|
||||||
<PREFIX> <OCR:equation y = mx + b>
|
|
||||||
The relationship is </PREFIX>
|
|
||||||
<SUFFIX>.</SUFFIX>
|
|
||||||
Expected OUTPUT:
|
|
||||||
$y = mx + b$
|
|
||||||
|
|
||||||
[EX11] Continue markdown table with correct row shape
|
|
||||||
<PREFIX>| Name | Score |
|
<PREFIX>| Name | Score |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| Alice | 92 |
|
| Alice | 92 |
|
||||||
@@ -479,20 +453,80 @@ $y = mx + b$
|
|||||||
Expected OUTPUT:
|
Expected OUTPUT:
|
||||||
88 |
|
88 |
|
||||||
|
|
||||||
[EX12] Mixed text + math + code in one insertion
|
[EX06] Start new paragraph
|
||||||
CURSOR_IN_FENCED_CODE_BLOCK=false
|
<PREFIX>First paragraph ends.</PREFIX>
|
||||||
<PREFIX>Use the area formula and provide a tiny JS helper.</PREFIX>
|
|
||||||
<SUFFIX></SUFFIX>
|
<SUFFIX></SUFFIX>
|
||||||
Expected OUTPUT:
|
Expected OUTPUT:
|
||||||
The area is $A = \\pi r^2$.
|
|
||||||
|
|
||||||
```javascript
|
Second paragraph starts.
|
||||||
const area = (r) => Math.PI * r * r;
|
WRONG: Second paragraph starts. (missing leading \\n\\n)
|
||||||
|
|
||||||
|
[EX07] Add newline before heading
|
||||||
|
PREFIX_ENDS_WITH_NEWLINE=false
|
||||||
|
<PREFIX>End of previous section.</PREFIX>
|
||||||
|
<SUFFIX>## Next Heading</SUFFIX>
|
||||||
|
Expected OUTPUT:
|
||||||
|
|
||||||
|
WRONG: (would join with heading without separation)
|
||||||
|
|
||||||
|
=== CATEGORY C: CODE BLOCKS ===
|
||||||
|
|
||||||
|
[EX08] Outside fence: wrap code in fence
|
||||||
|
CURSOR_IN_FENCED_CODE_BLOCK=false
|
||||||
|
<PREFIX>Parse this JSON payload in Python:</PREFIX>
|
||||||
|
<SUFFIX></SUFFIX>
|
||||||
|
Expected OUTPUT:
|
||||||
|
```python
|
||||||
|
import json
|
||||||
|
data = json.loads(payload)
|
||||||
```
|
```
|
||||||
|
WRONG: import json\\ndata = json.loads(payload) (no fence)
|
||||||
|
|
||||||
[EX13] Cursor inside mermaid fence: no backticks, mermaid lines only
|
[EX09] Inside fence: output code only
|
||||||
CURSOR_IN_FENCED_CODE_BLOCK=true
|
CURSOR_IN_FENCED_CODE_BLOCK=true
|
||||||
|
<PREFIX>```python
|
||||||
|
def add(a, b):
|
||||||
|
return </PREFIX>
|
||||||
|
<SUFFIX>
|
||||||
|
```</SUFFIX>
|
||||||
|
Expected OUTPUT:
|
||||||
|
a + b
|
||||||
|
WRONG: ```python\\nreturn a + b\\n``` (duplicate fences)
|
||||||
|
|
||||||
|
[EX10] Code inside fence uses single newline
|
||||||
|
CURSOR_IN_FENCED_CODE_BLOCK=true
|
||||||
|
<PREFIX>```python
|
||||||
|
def hello():</PREFIX>
|
||||||
|
<SUFFIX>
|
||||||
|
```</SUFFIX>
|
||||||
|
Expected OUTPUT:
|
||||||
|
print("Hello")
|
||||||
|
return True
|
||||||
|
(Note: single \\n between code lines, no markdown rules)
|
||||||
|
|
||||||
|
=== CATEGORY D: MATH ===
|
||||||
|
|
||||||
|
[EX11] Inline math
|
||||||
|
<PREFIX>The derivative of x^2 is </PREFIX>
|
||||||
|
<SUFFIX>.</SUFFIX>
|
||||||
|
Expected OUTPUT:
|
||||||
|
$2x$
|
||||||
|
WRONG: 2x (bare formula)
|
||||||
|
|
||||||
|
[EX12] Block math
|
||||||
|
<PREFIX>We can write the Gaussian integral as:</PREFIX>
|
||||||
|
<SUFFIX></SUFFIX>
|
||||||
|
Expected OUTPUT:
|
||||||
|
$$
|
||||||
|
\\int_{-\\infty}^{\\infty} e^{-x^2}\\,dx = \\sqrt{\\pi}
|
||||||
|
$$
|
||||||
|
WRONG: \\int... (bare formula without $$)
|
||||||
|
|
||||||
|
=== CATEGORY E: MERMAID ===
|
||||||
|
|
||||||
|
[EX13] Inside mermaid fence
|
||||||
CURSOR_FENCE_LANGUAGE=mermaid
|
CURSOR_FENCE_LANGUAGE=mermaid
|
||||||
|
CURSOR_IN_FENCED_CODE_BLOCK=true
|
||||||
<PREFIX>```mermaid
|
<PREFIX>```mermaid
|
||||||
flowchart TD
|
flowchart TD
|
||||||
A[Start] --> </PREFIX>
|
A[Start] --> </PREFIX>
|
||||||
@@ -501,8 +535,9 @@ A[Start] --> </PREFIX>
|
|||||||
Expected OUTPUT:
|
Expected OUTPUT:
|
||||||
B{Valid?}
|
B{Valid?}
|
||||||
B -->|Yes| C[Done]
|
B -->|Yes| C[Done]
|
||||||
|
WRONG: ```mermaid\\nB{Valid?}... (duplicate fence)
|
||||||
|
|
||||||
[EX14] Mermaid context outside fence: return full mermaid block
|
[EX14] Outside fence with mermaid context
|
||||||
CURSOR_IN_FENCED_CODE_BLOCK=false
|
CURSOR_IN_FENCED_CODE_BLOCK=false
|
||||||
MERMAID_CONTEXT=true
|
MERMAID_CONTEXT=true
|
||||||
<PREFIX>Please provide a simple release pipeline diagram.</PREFIX>
|
<PREFIX>Please provide a simple release pipeline diagram.</PREFIX>
|
||||||
@@ -511,7 +546,17 @@ Expected OUTPUT:
|
|||||||
```mermaid
|
```mermaid
|
||||||
flowchart LR
|
flowchart LR
|
||||||
Build --> Test --> Deploy
|
Build --> Test --> Deploy
|
||||||
```"""
|
```
|
||||||
|
|
||||||
|
=== CATEGORY F: OCR METADATA ===
|
||||||
|
|
||||||
|
[EX15] Use OCR as context, never output
|
||||||
|
<PREFIX> <OCR:equation y = mx + b>
|
||||||
|
The relationship is </PREFIX>
|
||||||
|
<SUFFIX>.</SUFFIX>
|
||||||
|
Expected OUTPUT:
|
||||||
|
$y = mx + b$
|
||||||
|
WRONG: <OCR:equation y = mx + b> (OCR tag in output)"""
|
||||||
|
|
||||||
|
|
||||||
def build_completion_prompts(
|
def build_completion_prompts(
|
||||||
@@ -520,7 +565,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)
|
||||||
@@ -551,35 +596,49 @@ def build_completion_prompts(
|
|||||||
preferences_instruction = f"\nUser Preferences:\n{preferences_instruction}"
|
preferences_instruction = f"\nUser Preferences:\n{preferences_instruction}"
|
||||||
|
|
||||||
user_prompt = f"""Current time: {current_time}{location_info}{preferences_instruction}
|
user_prompt = f"""Current time: {current_time}{location_info}{preferences_instruction}
|
||||||
Reasoning hint: {thinking_level}
|
Reasoning level: {thinking_level}
|
||||||
Editor language id: {safe_language_id}
|
Editor language: {safe_language_id}
|
||||||
|
|
||||||
Completion state flags:
|
=== STATE FLAGS ===
|
||||||
- CURSOR_IN_FENCED_CODE_BLOCK: {"true" if cursor_in_fenced_code_block else "false"}
|
- CURSOR_IN_FENCED_CODE_BLOCK: {"true" if cursor_in_fenced_code_block else "false"}
|
||||||
- CURSOR_FENCE_LANGUAGE: {cursor_fence_language}
|
- CURSOR_FENCE_LANGUAGE: {cursor_fence_language}
|
||||||
- MERMAID_CONTEXT: {"true" if mermaid_context else "false"}
|
- MERMAID_CONTEXT: {"true" if mermaid_context else "false"}
|
||||||
- PREFIX_ENDS_WITH_NEWLINE: {"true" if prefix_ends_with_newline else "false"}
|
- PREFIX_ENDS_WITH_NEWLINE: {"true" if prefix_ends_with_newline else "false"}
|
||||||
- SUFFIX_STARTS_WITH_NEWLINE: {"true" if suffix_starts_with_newline else "false"}
|
- SUFFIX_STARTS_WITH_NEWLINE: {"true" if suffix_starts_with_newline else "false"}
|
||||||
|
|
||||||
Task:
|
=== TASK ===
|
||||||
- Produce the best insertion text at the cursor between PREFIX and SUFFIX.
|
Produce the best insertion text between PREFIX and SUFFIX.
|
||||||
- Keep insertion meaningful and non-empty.
|
Requirements:
|
||||||
- Keep insertion concise unless structure requires more content.
|
- Non-empty and meaningful
|
||||||
|
- Concise unless structure needs more
|
||||||
|
- Follows markdown rules in system prompt
|
||||||
|
|
||||||
Context notes:
|
=== BOUNDARY DECISION GUIDE ===
|
||||||
- PREFIX may include OCR metadata after image markdown, e.g.  <OCR:description>.
|
|
||||||
- OCR metadata is hidden context and must never be copied into output.
|
|
||||||
- Preserve local style and formatting.
|
|
||||||
|
|
||||||
Decision policy:
|
Step 1: Check PREFIX_ENDS_WITH_NEWLINE
|
||||||
- Prioritize seamless join: PREFIX + OUTPUT + SUFFIX must read naturally.
|
If false, ask: "Does output need to start on a new line?"
|
||||||
- Do not repeat SUFFIX-leading text.
|
- YES if PREFIX ends with: ":", "steps:", "items:", heading text, or complete sentence before heading
|
||||||
- If uncertain, prefer a complete short phrase/sentence with clear meaning.
|
- If YES: start output with \\n
|
||||||
|
|
||||||
Comprehensive examples:
|
Step 2: Check SUFFIX_STARTS_WITH_NEWLINE
|
||||||
|
If false, ask: "Does output need to end with a newline?"
|
||||||
|
- YES if SUFFIX starts with: heading (##), new paragraph, or list marker
|
||||||
|
- If YES: end output with \\n
|
||||||
|
|
||||||
|
Step 3: Choose newline type
|
||||||
|
- Use \\n\\n for: new paragraphs, before headings, starting lists
|
||||||
|
- Use \\n for: continuing within blocks, list items, table cells
|
||||||
|
- Exception: inside code fences, use \\n freely
|
||||||
|
|
||||||
|
=== CONTEXT NOTES ===
|
||||||
|
- OCR metadata (e.g., <OCR:description>) is hidden context, never copy to output
|
||||||
|
- Match PREFIX tone, style, and indentation
|
||||||
|
- Do not repeat text from SUFFIX beginning
|
||||||
|
|
||||||
|
=== EXAMPLES BY CATEGORY ===
|
||||||
{INLINE_EXAMPLES}
|
{INLINE_EXAMPLES}
|
||||||
|
|
||||||
Now produce the insertion.
|
=== NOW COMPLETE THE TASK ===
|
||||||
|
|
||||||
<PREFIX>
|
<PREFIX>
|
||||||
{recent_prefix}
|
{recent_prefix}
|
||||||
@@ -601,7 +660,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
@@ -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
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,7 +46,7 @@
|
|||||||
</svg>
|
</svg>
|
||||||
<span class="btn-tooltip">{{ t('uploadFile') }}</span>
|
<span class="btn-tooltip">{{ t('uploadFile') }}</span>
|
||||||
</button>
|
</button>
|
||||||
<input type="file" ref="uploadFileInputRef" @change="handleUploadFile" accept=".txt,.docx,.pptx,.pdf,text/plain,application/pdf,application/vnd.openxmlformats-officedocument.wordprocessingml.document,application/vnd.openxmlformats-officedocument.presentationml.presentation" style="display:none">
|
<input type="file" ref="uploadFileInputRef" @change="handleUploadFile" accept=".txt,.json,.toml,.yaml,.yml,.docx,.pptx,.pdf,text/plain,application/json,text/yaml,text/x-yaml,application/x-yaml,application/pdf,application/vnd.openxmlformats-officedocument.wordprocessingml.document,application/vnd.openxmlformats-officedocument.presentationml.presentation" multiple style="display:none">
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -132,8 +132,33 @@
|
|||||||
<span class="btn-tooltip">{{ aiButtonLabel }}</span>
|
<span class="btn-tooltip">{{ aiButtonLabel }}</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div class="size-indicator" :class="{ 'over-limit': isOverLimit }" aria-live="polite">
|
<div
|
||||||
|
class="size-indicator"
|
||||||
|
:class="{ 'over-limit': isOverLimit }"
|
||||||
|
@mouseenter="showSizeTooltip = true"
|
||||||
|
@mouseleave="showSizeTooltip = false"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="warning-icon"
|
||||||
|
:class="{ 'warning-icon--visible': isOverLimit }"
|
||||||
|
width="12"
|
||||||
|
height="12"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
|
<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z" />
|
||||||
|
<line x1="12" y1="9" x2="12" y2="13" />
|
||||||
|
<line x1="12" y1="17" x2="12.01" y2="17" />
|
||||||
|
</svg>
|
||||||
{{ sizeInKB }} KB
|
{{ sizeInKB }} KB
|
||||||
|
<Transition name="tooltip-fade">
|
||||||
|
<div v-if="showSizeTooltip && isOverLimit" class="size-tooltip">
|
||||||
|
<strong>文档超过32KB限制</strong>
|
||||||
|
<span>AI补全功能已暂停,建议精简内容或分段处理</span>
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -153,6 +178,17 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-if="uploadProgress" class="upload-progress-overlay">
|
||||||
|
<div class="upload-progress-dialog">
|
||||||
|
<div class="spinner"></div>
|
||||||
|
<p>{{ t('uploading') || '正在上传文件' }}</p>
|
||||||
|
<p class="progress-text">
|
||||||
|
{{ uploadProgress.current }} / {{ uploadProgress.total }}
|
||||||
|
</p>
|
||||||
|
<p class="filename">{{ uploadProgress.filename }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
@@ -170,7 +206,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()
|
||||||
@@ -187,10 +223,12 @@ const contentSize = ref(0)
|
|||||||
const showImageDropdown = ref(false)
|
const showImageDropdown = ref(false)
|
||||||
const showExportDropdown = ref(false)
|
const showExportDropdown = ref(false)
|
||||||
const showUrlDialog = ref(false)
|
const showUrlDialog = ref(false)
|
||||||
|
const showSizeTooltip = ref(false)
|
||||||
const imageUrl = ref('')
|
const imageUrl = ref('')
|
||||||
const canUndo = ref(false)
|
const canUndo = ref(false)
|
||||||
const canRedo = ref(false)
|
const canRedo = ref(false)
|
||||||
const isDocUploadDisabled = ref(false)
|
const isDocUploadDisabled = ref(false)
|
||||||
|
const uploadProgress = ref(null)
|
||||||
const isOverLimit = computed(() => contentSize.value > SIZE_LIMIT)
|
const isOverLimit = computed(() => contentSize.value > SIZE_LIMIT)
|
||||||
const sizeInKB = computed(() => Math.floor(contentSize.value / 1024))
|
const sizeInKB = computed(() => Math.floor(contentSize.value / 1024))
|
||||||
const undoLabel = computed(() => t('undo') || 'Undo')
|
const undoLabel = computed(() => t('undo') || 'Undo')
|
||||||
@@ -220,8 +258,8 @@ const IMAGE_NODE_TYPES = new Set(['image', 'image-block', 'imageBlock'])
|
|||||||
const MARKDOWN_EXT_RE = /\.md$/i
|
const MARKDOWN_EXT_RE = /\.md$/i
|
||||||
const IMAGE_EXT_RE = /\.(png|jpe?g|gif|webp|bmp|svg|heic|heif|avif)$/i
|
const IMAGE_EXT_RE = /\.(png|jpe?g|gif|webp|bmp|svg|heic|heif|avif)$/i
|
||||||
const CONVERT_EXT_RE = /\.(docx|pptx|pdf)$/i
|
const CONVERT_EXT_RE = /\.(docx|pptx|pdf)$/i
|
||||||
const TEXT_EXT_RE = /\.txt$/i
|
const TEXT_EXT_RE = /\.(txt|json|toml|ya?ml)$/i
|
||||||
const TEXT_MIME_TYPES = new Set(['text/plain'])
|
const TEXT_MIME_TYPES = new Set(['text/plain', 'application/json', 'text/yaml', 'text/x-yaml', 'application/x-yaml'])
|
||||||
const CONVERT_MIME_TYPES = new Set([
|
const CONVERT_MIME_TYPES = new Set([
|
||||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||||
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
||||||
@@ -887,8 +925,81 @@ 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 insertEmptyParagraph = () => {
|
||||||
|
if (!crepe) return
|
||||||
|
crepe.editor.action((ctx) => {
|
||||||
|
const view = ctx.get(editorViewCtx)
|
||||||
|
const { state } = view
|
||||||
|
const { from, to } = state.selection
|
||||||
|
const tr = state.tr.insertText('\n\n', from, to)
|
||||||
|
const nextPos = from + 2
|
||||||
|
tr.setSelection(Selection.near(tr.doc.resolve(nextPos), 1))
|
||||||
|
view.dispatch(tr)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const insertMultipleDocBlocks = (blocks) => {
|
||||||
|
if (!crepe || blocks.length === 0) return
|
||||||
|
|
||||||
|
crepe.editor.action((ctx) => {
|
||||||
|
const view = ctx.get(editorViewCtx)
|
||||||
|
let tr = view.state.tr
|
||||||
|
const docBlockType = view.state.schema.nodes[DOC_BLOCK_NODE_TYPE]
|
||||||
|
if (!docBlockType) return
|
||||||
|
|
||||||
|
let currentPos = tr.selection.from
|
||||||
|
|
||||||
|
blocks.forEach((block, index) => {
|
||||||
|
const maxPos = tr.doc.content.size
|
||||||
|
|
||||||
|
if (index > 0) {
|
||||||
|
const insertPos = Math.min(currentPos, maxPos)
|
||||||
|
tr = tr.insertText('\n', insertPos, insertPos)
|
||||||
|
currentPos = insertPos + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
const blockNode = docBlockType.create({
|
||||||
|
docType: block.docType,
|
||||||
|
docName: block.docName,
|
||||||
|
uploadTime: block.uploadTime,
|
||||||
|
content: block.content,
|
||||||
|
collapsed: Boolean(block.collapsed),
|
||||||
|
})
|
||||||
|
|
||||||
|
const insertBlockPos = Math.min(currentPos, tr.doc.content.size)
|
||||||
|
tr = tr.replaceRangeWith(insertBlockPos, insertBlockPos, blockNode)
|
||||||
|
currentPos = insertBlockPos + blockNode.nodeSize
|
||||||
|
})
|
||||||
|
|
||||||
|
const finalPos = Math.min(currentPos, tr.doc.content.size)
|
||||||
|
if (finalPos >= 0 && finalPos <= tr.doc.content.size) {
|
||||||
|
tr.setSelection(Selection.near(tr.doc.resolve(finalPos), 1))
|
||||||
|
}
|
||||||
|
view.dispatch(tr.scrollIntoView())
|
||||||
|
view.focus()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const triggerFileUpload = () => {
|
const triggerFileUpload = () => {
|
||||||
@@ -898,20 +1009,51 @@ const triggerFileUpload = () => {
|
|||||||
|
|
||||||
const handleUploadFile = async (event) => {
|
const handleUploadFile = async (event) => {
|
||||||
const input = event.target
|
const input = event.target
|
||||||
const file = input.files?.[0]
|
const files = Array.from(input.files || [])
|
||||||
if (!file) return
|
if (files.length === 0) return
|
||||||
|
|
||||||
try {
|
const BATCH_LIMIT = 10
|
||||||
|
const MAX_FILE_SIZE = 50 * 1024 * 1024
|
||||||
|
|
||||||
|
if (files.length > BATCH_LIMIT) {
|
||||||
|
alert(t('uploadBatchLimit') || `一次最多上传${BATCH_LIMIT}个文件`)
|
||||||
|
input.value = ''
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
if (file.size > MAX_FILE_SIZE) {
|
||||||
|
alert(t('uploadSizeLimit') || `${file.name} 超过${MAX_FILE_SIZE / 1024 / 1024}MB限制`)
|
||||||
|
input.value = ''
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
if (!isSupportedDocFile(file)) {
|
if (!isSupportedDocFile(file)) {
|
||||||
alert(t('uploadDocTypeWarning') || '仅支持 txt、docx、pptx、pdf 格式的文档')
|
alert(t('uploadDocTypeWarning') || '仅支持 txt、docx、pptx、pdf 格式的文档')
|
||||||
|
input.value = ''
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (isDocUploadDisabled.value || !crepe) {
|
if (isDocUploadDisabled.value || !crepe) {
|
||||||
alert(t('uploadDocInBlockWarning') || '当前光标位置不能插入文件')
|
alert(t('uploadDocInBlockWarning') || '当前光标位置不能插入文件')
|
||||||
|
input.value = ''
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const total = files.length
|
||||||
|
uploadProgress.value = { current: 0, total, filename: '' }
|
||||||
|
|
||||||
|
const results = []
|
||||||
|
const errors = []
|
||||||
|
|
||||||
|
for (let index = 0; index < files.length; index++) {
|
||||||
|
const file = files[index]
|
||||||
|
uploadProgress.value = { current: index + 1, total, filename: file.name }
|
||||||
|
|
||||||
|
try {
|
||||||
const docType = getDocTypeFromFilename(file.name)
|
const docType = getDocTypeFromFilename(file.name)
|
||||||
let content = ''
|
let content = ''
|
||||||
|
|
||||||
@@ -920,30 +1062,49 @@ const handleUploadFile = async (event) => {
|
|||||||
} else if (isConvertibleFile(file)) {
|
} else if (isConvertibleFile(file)) {
|
||||||
content = await convertFileToMarkdown(file)
|
content = await convertFileToMarkdown(file)
|
||||||
} else {
|
} else {
|
||||||
alert(t('uploadDocTypeWarning') || '仅支持 txt、docx、pptx、pdf 格式的文档')
|
throw new Error('不支持的文件类型')
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!content) {
|
if (!content) {
|
||||||
throw new Error('文档解析结果为空')
|
throw new Error('文档解析结果为空')
|
||||||
}
|
}
|
||||||
|
|
||||||
clearCurrentGhost()
|
results.push({
|
||||||
insertDocBlockAtCursor({
|
|
||||||
docType,
|
docType,
|
||||||
docName: file.name || `document.${docType}`,
|
docName: file.name || `document.${docType}`,
|
||||||
uploadTime: new Date().toISOString(),
|
|
||||||
collapsed: false,
|
|
||||||
content,
|
content,
|
||||||
|
index,
|
||||||
})
|
})
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const message = e instanceof Error ? e.message : ''
|
const message = e instanceof Error ? e.message : ''
|
||||||
warnConvertError(message)
|
errors.push({ filename: file.name, message })
|
||||||
} finally {
|
|
||||||
input.value = ''
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
uploadProgress.value = null
|
||||||
|
clearCurrentGhost()
|
||||||
|
|
||||||
|
results.sort((a, b) => a.index - b.index)
|
||||||
|
|
||||||
|
const blocksToInsert = results.map(({ docType, docName, content }) => ({
|
||||||
|
docType,
|
||||||
|
docName,
|
||||||
|
content,
|
||||||
|
uploadTime: new Date().toISOString(),
|
||||||
|
collapsed: false,
|
||||||
|
}))
|
||||||
|
|
||||||
|
insertMultipleDocBlocks(blocksToInsert)
|
||||||
|
|
||||||
|
if (errors.length > 0) {
|
||||||
|
const failCount = errors.length
|
||||||
|
const errorMsgs = errors.map(e => `${e.filename}: ${e.message}`).join('\n')
|
||||||
|
alert(`上传失败 ${failCount} 个文件:\n\n${errorMsgs}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
input.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
const handleImageUpload = async (event) => {
|
const handleImageUpload = async (event) => {
|
||||||
const input = event.target
|
const input = event.target
|
||||||
const file = input.files?.[0]
|
const file = input.files?.[0]
|
||||||
@@ -1109,14 +1270,81 @@ onUnmounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.size-indicator {
|
.size-indicator {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 4px 10px;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
color: var(--muted-text);
|
color: var(--muted-text);
|
||||||
text-align: center;
|
border-radius: 12px;
|
||||||
margin-top: 4px;
|
transition: all 0.3s ease;
|
||||||
|
cursor: default;
|
||||||
}
|
}
|
||||||
|
|
||||||
.size-indicator.over-limit {
|
.size-indicator.over-limit {
|
||||||
color: var(--danger-text);
|
color: var(--danger-text);
|
||||||
|
background: rgba(220, 38, 38, 0.08);
|
||||||
|
animation: pulse-warning 2s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warning-icon {
|
||||||
|
flex-shrink: 0;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warning-icon--visible {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse-warning {
|
||||||
|
0%, 100% {
|
||||||
|
opacity: 1;
|
||||||
|
background: rgba(220, 38, 38, 0.08);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 0.75;
|
||||||
|
background: rgba(220, 38, 38, 0.12);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.size-tooltip {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 100%;
|
||||||
|
right: 0;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: var(--tooltip-bg);
|
||||||
|
color: var(--tooltip-fg);
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
white-space: nowrap;
|
||||||
|
box-shadow: var(--panel-shadow);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.size-tooltip strong {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.size-tooltip span {
|
||||||
|
opacity: 0.85;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip-fade-enter-active,
|
||||||
|
.tooltip-fade-leave-active {
|
||||||
|
transition: opacity 0.2s ease, transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip-fade-enter-from,
|
||||||
|
.tooltip-fade-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(4px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-btn {
|
.action-btn {
|
||||||
@@ -1435,6 +1663,55 @@ onUnmounted(() => {
|
|||||||
.copilot-ghost-block code {
|
.copilot-ghost-block code {
|
||||||
background-color: var(--ghost-code-bg);
|
background-color: var(--ghost-code-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.upload-progress-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 9999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-progress-dialog {
|
||||||
|
background: var(--editor-bg, white);
|
||||||
|
padding: 24px 32px;
|
||||||
|
border-radius: 8px;
|
||||||
|
text-align: center;
|
||||||
|
max-width: 400px;
|
||||||
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.spinner {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
margin: 0 auto 16px;
|
||||||
|
border: 3px solid #f3f3f3;
|
||||||
|
border-top: 3px solid #3498db;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
0% { transform: rotate(0deg); }
|
||||||
|
100% { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-text {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin: 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filename {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -743,7 +743,12 @@ export function interruptCopilot(view: EditorView): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function checkSizeLimit(view: EditorView): { size: number; overLimit: boolean } {
|
export function checkSizeLimit(view: EditorView): { size: number; overLimit: boolean } {
|
||||||
const size = view.state.doc.content.size
|
let size = view.state.doc.content.size
|
||||||
|
view.state.doc.descendants((node) => {
|
||||||
|
if (node.type.name === 'doc_block' && node.attrs.content) {
|
||||||
|
size += String(node.attrs.content).length
|
||||||
|
}
|
||||||
|
})
|
||||||
return { size, overLimit: size > SIZE_LIMIT }
|
return { size, overLimit: size > SIZE_LIMIT }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+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'
|
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
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -9,6 +9,9 @@ const HEADER_SEPARATOR = '\n---\n'
|
|||||||
export function normalizeDocType(value = '') {
|
export function normalizeDocType(value = '') {
|
||||||
const lower = String(value || '').trim().toLowerCase()
|
const lower = String(value || '').trim().toLowerCase()
|
||||||
if (lower === 'txt' || lower === 'text' || lower === 'plain') return 'txt'
|
if (lower === 'txt' || lower === 'text' || lower === 'plain') return 'txt'
|
||||||
|
if (lower === 'json') return 'json'
|
||||||
|
if (lower === 'toml') return 'toml'
|
||||||
|
if (lower === 'yaml' || lower === 'yml') return 'yaml'
|
||||||
if (lower === 'doc' || lower === 'docx' || lower === 'word') return 'docx'
|
if (lower === 'doc' || lower === 'docx' || lower === 'word') return 'docx'
|
||||||
if (lower === 'ppt' || lower === 'pptx' || lower === 'powerpoint') return 'pptx'
|
if (lower === 'ppt' || lower === 'pptx' || lower === 'powerpoint') return 'pptx'
|
||||||
if (lower === 'pdf') return 'pdf'
|
if (lower === 'pdf') return 'pdf'
|
||||||
@@ -20,6 +23,9 @@ export function getDocTypeFromFilename(name = '') {
|
|||||||
if (lower.endsWith('.docx')) return 'docx'
|
if (lower.endsWith('.docx')) return 'docx'
|
||||||
if (lower.endsWith('.pptx')) return 'pptx'
|
if (lower.endsWith('.pptx')) return 'pptx'
|
||||||
if (lower.endsWith('.pdf')) return 'pdf'
|
if (lower.endsWith('.pdf')) return 'pdf'
|
||||||
|
if (lower.endsWith('.json')) return 'json'
|
||||||
|
if (lower.endsWith('.toml')) return 'toml'
|
||||||
|
if (lower.endsWith('.yaml') || lower.endsWith('.yml')) return 'yaml'
|
||||||
return 'txt'
|
return 'txt'
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,10 +35,18 @@ export function isSupportedDocFile(file) {
|
|||||||
const type = String(file.type || '').toLowerCase()
|
const type = String(file.type || '').toLowerCase()
|
||||||
return (
|
return (
|
||||||
name.endsWith('.txt') ||
|
name.endsWith('.txt') ||
|
||||||
|
name.endsWith('.json') ||
|
||||||
|
name.endsWith('.toml') ||
|
||||||
|
name.endsWith('.yaml') ||
|
||||||
|
name.endsWith('.yml') ||
|
||||||
name.endsWith('.docx') ||
|
name.endsWith('.docx') ||
|
||||||
name.endsWith('.pptx') ||
|
name.endsWith('.pptx') ||
|
||||||
name.endsWith('.pdf') ||
|
name.endsWith('.pdf') ||
|
||||||
type === 'text/plain' ||
|
type === 'text/plain' ||
|
||||||
|
type === 'application/json' ||
|
||||||
|
type === 'text/yaml' ||
|
||||||
|
type === 'text/x-yaml' ||
|
||||||
|
type === 'application/x-yaml' ||
|
||||||
type === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' ||
|
type === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' ||
|
||||||
type === 'application/vnd.openxmlformats-officedocument.presentationml.presentation' ||
|
type === 'application/vnd.openxmlformats-officedocument.presentationml.presentation' ||
|
||||||
type === 'application/pdf'
|
type === 'application/pdf'
|
||||||
|
|||||||
+11
-2
@@ -36,7 +36,7 @@ export const translations = {
|
|||||||
uploadImg: 'Upload Image',
|
uploadImg: 'Upload Image',
|
||||||
uploadFile: 'Upload File',
|
uploadFile: 'Upload File',
|
||||||
uploadDoc: 'Upload Document',
|
uploadDoc: 'Upload Document',
|
||||||
uploadDocTypeWarning: 'Only txt, docx, pptx, pdf formats are supported.',
|
uploadDocTypeWarning: 'Only txt, json, toml, yaml, docx, pptx, pdf formats are supported.',
|
||||||
uploadDocSizeWarning: 'File size cannot exceed 10MB.',
|
uploadDocSizeWarning: 'File size cannot exceed 10MB.',
|
||||||
uploadDocInBlockWarning: 'Cannot insert document inside an existing document block. Please move cursor outside.',
|
uploadDocInBlockWarning: 'Cannot insert document inside an existing document block. Please move cursor outside.',
|
||||||
uploadDocError: 'Document conversion failed:',
|
uploadDocError: 'Document conversion failed:',
|
||||||
@@ -44,6 +44,9 @@ export const translations = {
|
|||||||
uploadMdTypeWarning: 'Only Markdown (.md) files and image files are supported.',
|
uploadMdTypeWarning: 'Only Markdown (.md) files and image files are supported.',
|
||||||
uploadFileError: 'File upload failed.',
|
uploadFileError: 'File upload failed.',
|
||||||
uploadConvertError: 'File conversion failed.',
|
uploadConvertError: 'File conversion failed.',
|
||||||
|
uploadBatchLimit: 'Maximum 10 files at once',
|
||||||
|
uploadSizeLimit: 'File exceeds 50MB limit',
|
||||||
|
uploading: 'Uploading files...',
|
||||||
enableAI: 'Enable AI',
|
enableAI: 'Enable AI',
|
||||||
disableAI: 'Disable AI',
|
disableAI: 'Disable AI',
|
||||||
insertUrl: 'Insert Image from URL',
|
insertUrl: 'Insert Image from URL',
|
||||||
@@ -90,7 +93,7 @@ export const translations = {
|
|||||||
uploadImg: '上传图片',
|
uploadImg: '上传图片',
|
||||||
uploadFile: '上传文件',
|
uploadFile: '上传文件',
|
||||||
uploadDoc: '上传文档',
|
uploadDoc: '上传文档',
|
||||||
uploadDocTypeWarning: '仅支持 txt、docx、pptx、pdf 格式的文档',
|
uploadDocTypeWarning: '仅支持 txt、json、toml、yaml、docx、pptx、pdf 格式的文档',
|
||||||
uploadDocSizeWarning: '文件大小不能超过 10MB',
|
uploadDocSizeWarning: '文件大小不能超过 10MB',
|
||||||
uploadDocInBlockWarning: '无法在现有文档块内插入新文档,请将光标移到文档外部',
|
uploadDocInBlockWarning: '无法在现有文档块内插入新文档,请将光标移到文档外部',
|
||||||
uploadDocError: '文档转换失败:',
|
uploadDocError: '文档转换失败:',
|
||||||
@@ -98,6 +101,9 @@ export const translations = {
|
|||||||
uploadMdTypeWarning: '仅支持 Markdown(.md)和图片文件。',
|
uploadMdTypeWarning: '仅支持 Markdown(.md)和图片文件。',
|
||||||
uploadFileError: '文件上传失败',
|
uploadFileError: '文件上传失败',
|
||||||
uploadConvertError: '文件转换失败',
|
uploadConvertError: '文件转换失败',
|
||||||
|
uploadBatchLimit: '一次最多上传10个文件',
|
||||||
|
uploadSizeLimit: '文件超过50MB限制',
|
||||||
|
uploading: '正在上传文件...',
|
||||||
enableAI: '启用 AI',
|
enableAI: '启用 AI',
|
||||||
disableAI: '禁用 AI',
|
disableAI: '禁用 AI',
|
||||||
insertUrl: '通过 URL 插入图片',
|
insertUrl: '通过 URL 插入图片',
|
||||||
@@ -147,6 +153,9 @@ export const translations = {
|
|||||||
uploadMdTypeWarning: 'Only Markdown (.md) files and image files are supported.',
|
uploadMdTypeWarning: 'Only Markdown (.md) files and image files are supported.',
|
||||||
uploadFileError: 'File upload failed.',
|
uploadFileError: 'File upload failed.',
|
||||||
uploadConvertError: 'File conversion failed.',
|
uploadConvertError: 'File conversion failed.',
|
||||||
|
uploadBatchLimit: 'Maximum 10 files at once',
|
||||||
|
uploadSizeLimit: 'File exceeds 50MB limit',
|
||||||
|
uploading: 'Uploading files...',
|
||||||
enableAI: 'AIを有効化',
|
enableAI: 'AIを有効化',
|
||||||
disableAI: 'AIを無効化',
|
disableAI: 'AIを無効化',
|
||||||
insertUrl: 'URLから画像を挿入',
|
insertUrl: 'URLから画像を挿入',
|
||||||
|
|||||||
Reference in New Issue
Block a user