diff --git a/backend/main.py b/backend/main.py index a23c3cc..a943686 100644 --- a/backend/main.py +++ b/backend/main.py @@ -2,6 +2,8 @@ import asyncio import base64 import json import logging +import os +import tempfile import uuid from typing import Optional @@ -14,6 +16,7 @@ from pydantic import BaseModel from geoip import get_ip_location_text from llm import call_ollama, call_vlm_ocr from prompt import build_completion_prompts, prepare_prompt_context +import markitdown logging.basicConfig( level=logging.INFO, @@ -73,6 +76,11 @@ class OCRRequest(BaseModel): language: str = "auto" +class ConvertRequest(BaseModel): + file: str + filename: str = "document.pdf" + + def _preview(text: str, limit: int = 80) -> str: value = (text or "").replace("\n", "\\n") if len(value) <= limit: @@ -243,6 +251,58 @@ async def ocr_image(request: OCRRequest, api_key: str = Security(get_api_key)): return JSONResponse(content={"error": str(e)}, status_code=500) +@app.post("/v1/convert") +async def convert_to_markdown(request: ConvertRequest, api_key: str = Security(get_api_key)): + """将文件转换为Markdown格式""" + request_id = str(uuid.uuid4())[:8] + + try: + logger.info( + "[%s] /v1/convert filename=%s file_base64_chars=%d", + request_id, + request.filename, + len(request.file or ""), + ) + + # 解码Base64文件内容 + file_bytes = base64.b64decode(request.file) + logger.info("[%s] /v1/convert decoded file_bytes=%d", request_id, len(file_bytes)) + + # 获取文件扩展名 + ext = os.path.splitext(request.filename)[1].lower() + + # 创建临时文件 + with tempfile.NamedTemporaryFile(delete=False, suffix=ext) as tmp: + tmp.write(file_bytes) + tmp_path = tmp.name + + try: + # 使用MarkItDown转换为Markdown + md = markitdown.MarkItDown() + result = md.convert(tmp_path) + markdown_text = result.text_content + + logger.info( + "[%s] /v1/convert success text_chars=%d text_preview='%s'", + request_id, + len(markdown_text or ""), + _preview(markdown_text, 120), + ) + + return { + "markdown": markdown_text, + "filename": request.filename + } + finally: + # 清理临时文件 + if os.path.exists(tmp_path): + os.unlink(tmp_path) + + except Exception as e: + logger.exception("[%s] /v1/convert failed: %s", request_id, e) + return JSONResponse(content={"error": str(e)}, status_code=500) + + if __name__ == "__main__": import uvicorn diff --git a/backend/requirements.txt b/backend/requirements.txt index 2a2f806..86035db 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -5,3 +5,8 @@ pydantic python-dotenv httpx geoip2 +markitdown +python-docx +python-pptx +openpyxl +pypdf diff --git a/package-lock.json b/package-lock.json index 72d01ac..a82cccf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -424,7 +424,6 @@ "version": "6.12.1", "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.12.1.tgz", "integrity": "sha512-Fa6xkSiuGKc8XC8Cn96T+TQHYj4ZZ7RdFmXA3i9xe/3hLHfwPZdM+dqfX0Cp0zQklBKhVD8Yzc8LS45rkqcwpQ==", - "peer": true, "dependencies": { "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.23.0", @@ -496,7 +495,6 @@ "version": "6.5.4", "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.4.tgz", "integrity": "sha512-8y7xqG/hpB53l25CIoit9/ngxdfoG+fx+V3SHBrinnhOtLvKHRyAJJuHzkWrR4YXXLX8eXBsejgAAxHUOdW1yw==", - "peer": true, "dependencies": { "@marijn/find-cluster-break": "^1.0.0" } @@ -516,7 +514,6 @@ "version": "6.39.11", "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.39.11.tgz", "integrity": "sha512-bWdeR8gWM87l4DB/kYSF9A+dVackzDb/V56Tq7QVrQ7rn86W0rgZFtlL3g3pem6AeGcb9NQNoy3ao4WpW4h5tQ==", - "peer": true, "dependencies": { "@codemirror/state": "^6.5.0", "crelt": "^1.0.6", @@ -3073,7 +3070,6 @@ "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-11.1.1.tgz", "integrity": "sha512-f0yv5CPKaFxfsPTBzX7vGuim4oIC1/gcS7LUGdBSwl2dU6+FON6LVUksdOo1qJjoUvXNn45urgh8C+0a24pACQ==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@chevrotain/cst-dts-gen": "11.1.1", "@chevrotain/gast": "11.1.1", @@ -3402,7 +3398,6 @@ "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.33.1.tgz", "integrity": "sha512-iJc4TwyANnOGR1OmWhsS9ayRS3s+XQ185FmuHObThD+5AeJCakAAbWv8KimMTt08xCCLNgneQwFp+JRJOr9qGQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10" } @@ -3824,7 +3819,6 @@ "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", "license": "ISC", - "peer": true, "engines": { "node": ">=12" } @@ -6578,7 +6572,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -6794,7 +6787,6 @@ "version": "1.25.4", "resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.25.4.tgz", "integrity": "sha512-PIM7E43PBxKce8OQeezAs9j4TP+5yDpZVbuurd1h5phUxEKIu+G2a+EUZzIC5nS1mJktDJWzbqS23n1tsAf5QA==", - "peer": true, "dependencies": { "orderedmap": "^2.0.0" } @@ -6825,7 +6817,6 @@ "version": "1.4.4", "resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.4.4.tgz", "integrity": "sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw==", - "peer": true, "dependencies": { "prosemirror-model": "^1.0.0", "prosemirror-transform": "^1.0.0", @@ -6856,7 +6847,6 @@ "version": "1.41.5", "resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.41.5.tgz", "integrity": "sha512-UDQbIPnDrjE8tqUBbPmCOZgtd75htE6W3r0JCmY9bL6W1iemDM37MZEKC49d+tdQ0v/CKx4gjxLoLsfkD2NiZA==", - "peer": true, "dependencies": { "prosemirror-model": "^1.20.0", "prosemirror-state": "^1.0.0", @@ -8498,7 +8488,6 @@ "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -8622,7 +8611,6 @@ "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.26.tgz", "integrity": "sha512-SJ/NTccVyAoNUJmkM9KUqPcYlY+u8OVL1X5EW9RIs3ch5H2uERxyyIUI4MRxVCSOiEcupX9xNGde1tL9ZKpimA==", "license": "MIT", - "peer": true, "dependencies": { "@vue/compiler-dom": "3.5.26", "@vue/compiler-sfc": "3.5.26", diff --git a/src/components/MilkdownEditor.vue b/src/components/MilkdownEditor.vue index 0e5e36a..14eeade 100644 --- a/src/components/MilkdownEditor.vue +++ b/src/components/MilkdownEditor.vue @@ -48,27 +48,20 @@ -
- -
- - - -
-
+
- -
- -
@@ -151,19 +135,15 @@ import { onMounted, onUnmounted, ref, computed, watch } from 'vue' import { replaceAll } from '@milkdown/kit/utils' import { Crepe } from '@milkdown/crepe' -import { editorViewCtx } from '@milkdown/kit/core' +import { editorViewCtx, serializerCtx } from '@milkdown/kit/core' import { Selection } from '@milkdown/prose/state' import { undo, redo, undoDepth, redoDepth } from '@milkdown/prose/history' import { copilotPlugin, copilotConfigCtx, copilotGhostMark, setCopilotEnabled, interruptCopilot, COPILOT_PLUGIN_KEY, SIZE_LIMIT, checkSizeLimit, clearGhostSuggestion } from '../plugins/copilotPlugin' -import { mermaidRenderPreview, codeBlockConfig, refreshMermaidPreviews } from '../plugins/mermaidPlugin' +import { mermaidRenderPreview, codeBlockConfig } from '../plugins/mermaidPlugin' import { fetchSuggestion } from '../utils/api.js' import { useSettingsStore } from '../stores/settings' import { OCR_URL } from '../utils/config.js' import { setOcrCache, clearOcrCache, clearAllOcrCache, IMAGE_SIZE_LIMIT, calculateImageHash, getOcrByHash, setOcrByHash } from '../utils/ocrCache.js' -import MarkdownIt from 'markdown-it' -import katex from 'katex' -import 'katex/dist/katex.min.css' -import html2pdf from 'html2pdf.js' const emit = defineEmits(['update:markdown']) const settings = useSettingsStore() @@ -176,10 +156,7 @@ const cameraInputRef = ref(null) const aiEnabled = ref(true) const contentSize = ref(0) const showImageDropdown = ref(false) -const showExportDropdown = ref(false) const showUrlDialog = ref(false) -const showMermaidPreview = ref(false) -const mermaidPreviewSrc = ref('') const imageUrl = ref('') const canUndo = ref(false) const canRedo = ref(false) @@ -201,8 +178,6 @@ const aiButtonLabel = computed(() => { let crepe = null let markdownSyncTimer = null let rootResizeObserver = null -let themeObserver = null -let mermaidResizeTimer = null const objectUrls = new Set() const IMAGE_NODE_TYPES = new Set(['image', 'image-block', 'imageBlock']) const MARKDOWN_EXT_RE = /\.md$/i @@ -417,170 +392,6 @@ const prepareImageFile = async (file) => { return objectUrl } -const closeMermaidPreview = () => { - showMermaidPreview.value = false - mermaidPreviewSrc.value = '' -} - -const openMermaidPreview = (url) => { - if (!url) return - mermaidPreviewSrc.value = url - showMermaidPreview.value = true -} - -const makeMermaidFilename = () => { - const now = new Date() - const pad = (n) => String(n).padStart(2, '0') - const datePart = `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}` - const timePart = `${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}` - return `mermaid-${datePart}-${timePart}.png` -} - -const normalizeMermaidFilename = (filename = '') => { - if (!filename) return makeMermaidFilename() - if (/\.png$/i.test(filename)) return filename - if (/\.[^./\\]+$/.test(filename)) return filename.replace(/\.[^./\\]+$/, '.png') - return `${filename}.png` -} - -const parseSvgSize = (svg) => { - const viewBox = svg.match(/viewBox\s*=\s*["']\s*[-\d.]+\s+[-\d.]+\s+([-\d.]+)\s+([-\d.]+)\s*["']/i) - if (viewBox) { - const width = Number(viewBox[1]) - const height = Number(viewBox[2]) - if (Number.isFinite(width) && Number.isFinite(height) && width > 0 && height > 0) { - return { width, height } - } - } - const widthAttr = svg.match(/width\s*=\s*["']([-\d.]+)(px)?["']/i) - const heightAttr = svg.match(/height\s*=\s*["']([-\d.]+)(px)?["']/i) - const width = widthAttr ? Number(widthAttr[1]) : 960 - const height = heightAttr ? Number(heightAttr[1]) : 540 - return { - width: Number.isFinite(width) && width > 0 ? width : 960, - height: Number.isFinite(height) && height > 0 ? height : 540, - } -} - -const getRasterDpr = (width, height) => { - const rawDpr = typeof window !== 'undefined' ? (window.devicePixelRatio || 1) : 1 - const baseDpr = Math.min(3, Math.max(1, rawDpr)) - const maxCanvasEdge = 4096 - const edgeLimitedDpr = maxCanvasEdge / Math.max(width, height, 1) - return Math.max(1, Math.min(baseDpr, edgeLimitedDpr)) -} - -const decodeSvgDataUrl = (url) => { - const commaIndex = url.indexOf(',') - if (commaIndex === -1) throw new Error('Invalid SVG data URL') - const header = url.slice(0, commaIndex) - const payload = url.slice(commaIndex + 1) - if (/;base64/i.test(header)) { - const binary = atob(payload) - const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0)) - return new TextDecoder().decode(bytes) - } - return decodeURIComponent(payload) -} - -const svgTextToPngDataUrl = async (svgText) => { - const fallback = parseSvgSize(svgText) - let width = fallback.width - let height = fallback.height - - const maxEdge = 2400 - const scale = Math.min(1, maxEdge / Math.max(width, height)) - width = Math.max(1, Math.round(width * scale)) - height = Math.max(1, Math.round(height * scale)) - - const image = new Image() - image.decoding = 'async' - image.crossOrigin = 'anonymous' - - await new Promise((resolve, reject) => { - image.onload = () => resolve() - image.onerror = () => reject(new Error('Failed to load SVG image')) - image.src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgText)}` - }) - - const dpr = getRasterDpr(width, height) - const canvas = document.createElement('canvas') - canvas.width = Math.max(1, Math.round(width * dpr)) - canvas.height = Math.max(1, Math.round(height * dpr)) - const ctx = canvas.getContext('2d') - if (!ctx) throw new Error('Canvas context unavailable') - ctx.setTransform(dpr, 0, 0, dpr, 0, 0) - ctx.imageSmoothingEnabled = true - ctx.imageSmoothingQuality = 'high' - ctx.clearRect(0, 0, width, height) - ctx.drawImage(image, 0, 0, width, height) - - return canvas.toDataURL('image/png') -} - -const ensurePngDownloadUrl = async (url) => { - if (!url) return '' - if (/^data:image\/png/i.test(url)) return url - if (/^data:image\/svg\+xml/i.test(url)) { - const svgText = decodeSvgDataUrl(url) - return await svgTextToPngDataUrl(svgText) - } - return url -} - -const downloadMermaidImage = async (url, filename = '') => { - if (!url) return - let downloadUrl = url - try { - downloadUrl = await ensurePngDownloadUrl(url) - } catch (error) { - alert('PNG export failed for this diagram. Please remove external image resources and try again.') - return - } - const a = document.createElement('a') - a.href = downloadUrl - a.download = normalizeMermaidFilename(filename) - document.body.appendChild(a) - a.click() - a.remove() -} - -const handleMermaidAction = async (event) => { - const target = event.target instanceof Element ? event.target.closest('[data-mermaid-action]') : null - if (!(target instanceof HTMLElement)) return - - const action = target.getAttribute('data-mermaid-action') - if (!action) return - - const block = target.closest('.mermaid-block') - const url = target.getAttribute('data-mermaid-url') || block?.getAttribute('data-mermaid-url') || '' - if (!url) return - - event.preventDefault() - event.stopPropagation() - - if (action === 'zoom') { - openMermaidPreview(url) - return - } - - if (action === 'download') { - const filename = target.getAttribute('data-mermaid-filename') || '' - await downloadMermaidImage(url, filename) - } -} - -const handleMermaidViewportResize = () => { - if (mermaidResizeTimer) { - clearTimeout(mermaidResizeTimer) - mermaidResizeTimer = null - } - mermaidResizeTimer = setTimeout(() => { - mermaidResizeTimer = null - refreshMermaidPreviews() - }, 140) -} - onMounted(async () => { if (!root.value) throw new Error('root.value is null') updateEditorTailSpace() @@ -590,12 +401,10 @@ onMounted(async () => { }) rootResizeObserver.observe(root.value) } - root.value.addEventListener('click', handleMermaidAction, true) - window.addEventListener('resize', handleMermaidViewportResize) crepe = new Crepe({ root: root.value, - defaultValue: '# 欢迎来到LLM-IN-TEXT\n\n一个即时LLM系统\n\n在下开始你的创作...', + defaultValue: '# Welcome to LLM-IN-TEXT\n\nA instant LLM system\n\nStart your creative work below...', features: { [Crepe.Feature.Latex]: true, [Crepe.Feature.ImageBlock]: true, @@ -655,7 +464,6 @@ onMounted(async () => { await crepe.create() - refreshMermaidPreviews() crepe.on((listener) => { listener.updated((ctx, doc) => { @@ -673,20 +481,6 @@ onMounted(async () => { refreshSizeAndLimit(ctx) updateHistoryState(view) }) - - if (typeof MutationObserver !== 'undefined') { - themeObserver = new MutationObserver((mutations) => { - const changed = mutations.some((mutation) => mutation.type === 'attributes' && mutation.attributeName === 'data-theme') - if (changed) { - refreshMermaidPreviews() - } - }) - themeObserver.observe(document.documentElement, { - attributes: true, - attributeFilter: ['data-theme'], - }) - } - scheduleMarkdownSync() }) @@ -714,233 +508,6 @@ const exportMarkdown = async () => { URL.revokeObjectURL(url) } -// 棰勫鐞?LaTeX 鍏紡 -const preprocessLatex = (text) => { - // 澶勭悊 $$...$$ 鍧楃骇鍏紡 - text = text.replace(/\$\$([\s\S]*?)\$\$/g, (match, content) => { - try { - const html = katex.renderToString(content.trim(), { - displayMode: true, - throwOnError: false - }) - return `
${html}
` - } catch (e) { - return `
$$${content}$$
` - } - }) - - // 澶勭悊 $...$ 琛屽唴鍏紡 - text = text.replace(/(? { - try { - const html = katex.renderToString(content.trim(), { - displayMode: false, - throwOnError: false - }) - return `${html}` - } catch (e) { - return `${match}` - } - }) - - return text -} - -// 灏?markdown 杞崲涓?HTML 骞跺鐞?mermaid -const decodeHtmlEntities = (input) => { - if (!input) return '' - const textarea = document.createElement('textarea') - textarea.innerHTML = input - return textarea.value -} - -const collectRenderedMermaidImages = () => { - const imageMap = new Map() - const blocks = document.querySelectorAll('.mermaid-block[data-mermaid-code][data-mermaid-url]') - for (const block of blocks) { - const encodedCode = block.getAttribute('data-mermaid-code') || '' - const imageUrl = block.getAttribute('data-mermaid-url') || '' - if (!encodedCode || !imageUrl) continue - try { - const code = normalizeMermaidCodeForExport(decodeURIComponent(encodedCode)) - if (code) imageMap.set(code, imageUrl) - } catch (error) { - continue - } - } - return imageMap -} - -const normalizeMermaidCodeForExport = (code) => { - return (code || '').replace(/\r\n/g, '\n').trim() -} -const markdownToHtml = async (markdown) => { - const md = new MarkdownIt({ - html: true, - linkify: true, - typographer: true - }) - - // 棰勫鐞?LaTeX - let html = preprocessLatex(markdown) - - // 娓叉煋 markdown - html = md.render(html) - - // Mermaid code block -> rendered image - const mermaidImageMap = collectRenderedMermaidImages() - const template = document.createElement('template') - template.innerHTML = html - const mermaidNodes = template.content.querySelectorAll('pre > code.language-mermaid') - for (const node of mermaidNodes) { - const pre = node.parentElement - if (!(pre instanceof HTMLElement)) continue - const normalizedCode = normalizeMermaidCodeForExport(decodeHtmlEntities(node.textContent || '')) - const imgUrl = mermaidImageMap.get(normalizedCode) - if (!imgUrl) { - pre.remove() - continue - } - const wrapper = document.createElement('div') - wrapper.className = 'mermaid-export' - const image = document.createElement('img') - image.src = imgUrl - image.alt = 'Mermaid diagram' - wrapper.appendChild(image) - pre.replaceWith(wrapper) - } - html = template.innerHTML - - html = html.replace(/
([\s\S]*?)<\/code><\/pre>/g, 
-        '
$2
') - - // Process remote images - html = html.replace(/]*)>/g, (match, src, attrs) => { - return `` - }) - - return html -} - -// 瀵煎嚭涓?DOCX (浣跨敤 HTML 鏍煎紡锛學ord 鍙互鐩存帴鎵撳紑) -const exportToDocx = async () => { - if (!crepe) return - - showExportDropdown.value = false - - try { - const markdown = await crepe.getMarkdown() - const html = await markdownToHtml(markdown) - - // 鍖呰 HTML 涓哄畬鏁存枃妗o紝娣诲姞 Word 鍏煎鐨勫厓鏁版嵁 - const fullHtml = ` - - - - Document - - - - -${html} - -` - - // 浣跨敤 HTML 鏍煎紡淇濆瓨涓?.doc锛學ord 鍙互鐩存帴鎵撳紑 - const blob = new Blob([fullHtml], { type: 'application/msword' }) - const url = URL.createObjectURL(blob) - const a = document.createElement('a') - const now = new Date() - const pad = (n) => String(n).padStart(2, '0') - const datePart = `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}` - const timePart = `${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}` - a.href = url - a.download = `document${datePart}${timePart}.doc` - document.body.appendChild(a) - a.click() - a.remove() - URL.revokeObjectURL(url) - } catch (error) { - console.error('[Export DOCX] Error:', error) - alert('瀵煎嚭 DOCX 澶辫触锛岃閲嶈瘯') - } -} - -// 瀵煎嚭涓?PDF -const exportToPdf = async () => { - if (!crepe) return - - showExportDropdown.value = false - - try { - const markdown = await crepe.getMarkdown() - const html = await markdownToHtml(markdown) - - const fullHtml = ` - - - - Document - - - -${html} - -` - - // 閰嶇疆 PDF 閫夐」 - const options = { - margin: [10, 10, 10, 10], - filename: `document${new Date().toISOString().slice(0, 10)}.pdf`, - image: { type: 'jpeg', quality: 0.98 }, - html2canvas: { scale: 2, useCORS: true, backgroundColor: '#ffffff', windowWidth: 1200 }, - pagebreak: { mode: ['css', 'legacy'] }, - jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' } - } - - // 鐢熸垚 PDF - await html2pdf().set(options).from(fullHtml, 'string').save() - } catch (error) { - console.error('[Export PDF] Error:', error) - alert('瀵煎嚭 PDF 澶辫触锛岃閲嶈瘯') - } -} - const triggerUpload = () => { fileInputRef.value?.click() } @@ -992,10 +559,6 @@ const toggleAI = async () => { }) } -const toggleExportDropdown = () => { - showExportDropdown.value = !showExportDropdown.value -} - const toggleImageDropdown = () => { showImageDropdown.value = !showImageDropdown.value } @@ -1067,21 +630,6 @@ onUnmounted(() => { rootResizeObserver = null } - if (mermaidResizeTimer) { - clearTimeout(mermaidResizeTimer) - mermaidResizeTimer = null - } - - if (themeObserver) { - themeObserver.disconnect() - themeObserver = null - } - - if (root.value) { - root.value.removeEventListener('click', handleMermaidAction, true) - } - window.removeEventListener('resize', handleMermaidViewportResize) - for (const url of Array.from(objectUrls)) { revokeObjectUrl(url) } @@ -1243,40 +791,6 @@ onUnmounted(() => { opacity: 1; } -.export-btn-wrapper { - position: relative; -} - -.export-dropdown { - position: absolute; - bottom: 100%; - right: 0; - margin-bottom: 8px; - background: var(--panel-bg); - border: 1px solid var(--panel-border); - border-radius: 8px; - box-shadow: var(--panel-shadow); - overflow: hidden; - z-index: 10000; - min-width: 160px; -} - -.export-dropdown button { - display: block; - width: 100%; - padding: 10px 16px; - border: none; - background: none; - text-align: left; - cursor: pointer; - font-size: 14px; - color: var(--app-text); -} - -.export-dropdown button:hover { - background: var(--crepe-color-hover); -} - .image-btn-wrapper { position: relative; } @@ -1386,60 +900,6 @@ onUnmounted(() => { filter: brightness(0.92); } -.mermaid-preview-overlay { - position: fixed; - inset: 0; - z-index: 10002; - background: var(--overlay-bg); - display: flex; - align-items: center; - justify-content: center; - padding: 16px; -} - -.mermaid-preview-dialog { - width: min(96vw, 1280px); - max-height: min(92vh, 920px); - background: var(--panel-bg); - border: 1px solid var(--panel-border); - border-radius: 12px; - box-shadow: var(--panel-shadow); - display: flex; - flex-direction: column; - position: relative; -} - -.mermaid-preview-close { - position: absolute; - top: 8px; - right: 8px; - width: 34px; - height: 34px; - border: 1px solid var(--panel-border); - border-radius: 8px; - background: var(--btn-bg); - color: var(--btn-fg); - cursor: pointer; - z-index: 1; -} - -.mermaid-preview-close:hover { - background: var(--btn-hover-bg); - color: var(--btn-hover-fg); - border-color: var(--btn-hover-bg); -} - -.mermaid-preview-scroll { - overflow: auto; - padding: 40px 16px 16px; -} - -.mermaid-preview-scroll img { - display: block; - max-width: none; - margin: 0 auto; -} - .milkdown-editor { --editor-tail-space: calc(100vh - 32px); width: 100%; @@ -1477,14 +937,10 @@ onUnmounted(() => { } .milkdown-editor :deep(.ProseMirror img) { - max-width: 80%; + max-width: 60%; height: auto; } -.milkdown-editor :deep(.ProseMirror .mermaid-image) { - max-width: none !important; -} - .milkdown-editor :deep(.ProseMirror > *:first-child) { margin-top: 0 !important; } @@ -1590,6 +1046,3 @@ onUnmounted(() => { } - - - diff --git a/src/stores/settings.js b/src/stores/settings.js index 727609e..72b0c96 100644 --- a/src/stores/settings.js +++ b/src/stores/settings.js @@ -18,11 +18,9 @@ export const useSettingsStore = defineStore('settings', () => { // 4. Preferences const language = ref('auto') const currency = ref('auto') - // const timezone = ref('auto') // removed // 5. Background const backgroundType = ref('default') // 'default' | 'warm' | 'reading' | 'image' - // const backgroundColor = ref('#ffffff') // removed const backgroundImage = ref('') const backgroundOpacity = ref(0.2) // 0.05 - 0.50 @@ -61,13 +59,10 @@ export const useSettingsStore = defineStore('settings', () => { if (typeof data.privacyMode === 'boolean') privacyMode.value = data.privacyMode if (data.language) language.value = data.language if (data.currency) currency.value = data.currency - // if (data.timezone) timezone.value = data.timezone // timezone legacy ignore if (data.backgroundType) { - // migrate color to default if needed, or mapped if (data.backgroundType === 'color') backgroundType.value = 'default' else backgroundType.value = data.backgroundType } - // if (data.backgroundColor) backgroundColor.value = data.backgroundColor // removed if (data.backgroundImage) backgroundImage.value = data.backgroundImage if (data.backgroundOpacity) backgroundOpacity.value = data.backgroundOpacity } @@ -86,9 +81,7 @@ export const useSettingsStore = defineStore('settings', () => { privacyMode: privacyMode.value, language: language.value, currency: currency.value, - // timezone: timezone.value, // removed backgroundType: backgroundType.value, - // backgroundColor: backgroundColor.value, // removed backgroundImage: backgroundImage.value, backgroundOpacity: backgroundOpacity.value, } @@ -106,9 +99,7 @@ export const useSettingsStore = defineStore('settings', () => { privacyMode.value = false language.value = 'auto' currency.value = 'auto' - // timezone.value = 'auto' // removed backgroundType.value = 'default' - // backgroundColor.value = '#ffffff' // removed backgroundImage.value = '' backgroundOpacity.value = 0.2 saveSettings() @@ -123,9 +114,7 @@ export const useSettingsStore = defineStore('settings', () => { privacyMode, language, currency, - // timezone, // removed backgroundType, - // backgroundColor, // removed backgroundImage, backgroundOpacity, ], @@ -144,9 +133,7 @@ export const useSettingsStore = defineStore('settings', () => { privacyMode, language, currency, - // timezone, // removed backgroundType, - // backgroundColor, // removed backgroundImage, backgroundOpacity, uiLanguage, diff --git a/src/utils/config.js b/src/utils/config.js index d439e94..ff28843 100644 --- a/src/utils/config.js +++ b/src/utils/config.js @@ -1,5 +1,3 @@ -export const DEBUG = import.meta.env.DEV - const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'https://api.imageteach.tech:8002' export const API_URL = import.meta.env.VITE_API_URL || `${API_BASE_URL}/v1/completions`