modified: backend/llm.py
modified: src/components/MilkdownEditor.vue modified: src/utils/config.js modified: src/utils/i18n.js
This commit is contained in:
+2
-2
@@ -16,8 +16,8 @@ VLM_MODEL = os.getenv('VLM_MODEL', 'qwen3-vl:30b')
|
|||||||
|
|
||||||
# Timeouts in seconds (10 minutes for large model loading)
|
# Timeouts in seconds (10 minutes for large model loading)
|
||||||
COMPLETION_TIMEOUT = 600
|
COMPLETION_TIMEOUT = 600
|
||||||
OCR_TIMEOUT = 120
|
OCR_TIMEOUT = 600
|
||||||
CONVERT_TIMEOUT = 60
|
CONVERT_TIMEOUT = 600
|
||||||
|
|
||||||
client = ollama.AsyncClient(host=OLLAMA_HOST)
|
client = ollama.AsyncClient(host=OLLAMA_HOST)
|
||||||
logger = logging.getLogger("llm")
|
logger = logging.getLogger("llm")
|
||||||
|
|||||||
+148
-222
@@ -36,24 +36,27 @@
|
|||||||
type="button"
|
type="button"
|
||||||
class="action-btn"
|
class="action-btn"
|
||||||
:class="{ 'force-disabled': isDocUploadDisabled }"
|
:class="{ 'force-disabled': isDocUploadDisabled }"
|
||||||
:aria-label="t('uploadFile')"
|
:aria-label="t('upload')"
|
||||||
:title="docUploadButtonTitle"
|
:title="uploadButtonTitle"
|
||||||
@click="triggerFileUpload"
|
@click="triggerUpload"
|
||||||
>
|
>
|
||||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg width="20" height="20" 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"/>
|
<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"/>
|
<polyline points="14 2 14 8 20 8"/>
|
||||||
|
<rect x="7" y="11" width="8" height="6" rx="1" stroke-width="1.5"/>
|
||||||
|
<circle cx="9" cy="13" r="0.8" fill="currentColor"/>
|
||||||
|
<path d="M7 16l2-2 2 2" stroke-width="1.5"/>
|
||||||
</svg>
|
</svg>
|
||||||
<span class="btn-tooltip">{{ t('uploadFile') }}</span>
|
<span class="btn-tooltip">{{ t('upload') }}</span>
|
||||||
</button>
|
</button>
|
||||||
<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">
|
<input type="file" ref="uploadInputRef" @change="handleUpload" :accept="acceptAll" multiple style="display:none">
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="action-btn"
|
class="action-btn"
|
||||||
:aria-label="t('importMd')"
|
:aria-label="t('importMd')"
|
||||||
:title="t('importMd')"
|
:title="t('importMd')"
|
||||||
@click="triggerUpload"
|
@click="triggerImportMd"
|
||||||
>
|
>
|
||||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
|
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
|
||||||
@@ -62,7 +65,7 @@
|
|||||||
</svg>
|
</svg>
|
||||||
<span class="btn-tooltip">{{ t('importMd') }}</span>
|
<span class="btn-tooltip">{{ t('importMd') }}</span>
|
||||||
</button>
|
</button>
|
||||||
<input type="file" ref="fileInputRef" @change="handleFileUpload" accept=".md,text/markdown,text/x-markdown" style="display:none">
|
<input type="file" ref="mdInputRef" @change="handleImportMd" accept=".md,text/markdown,text/x-markdown" style="display:none">
|
||||||
|
|
||||||
<div class="export-btn-wrapper">
|
<div class="export-btn-wrapper">
|
||||||
<button
|
<button
|
||||||
@@ -87,29 +90,6 @@
|
|||||||
<button type="button" @click="() => { exportPdf(); showExportDropdown = false; }">{{ t('exportPdf') }}</button>
|
<button type="button" @click="() => { exportPdf(); showExportDropdown = false; }">{{ t('exportPdf') }}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="image-btn-wrapper">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="action-btn"
|
|
||||||
:aria-label="t('uploadImg')"
|
|
||||||
:title="t('uploadImg')"
|
|
||||||
@click="toggleImageDropdown"
|
|
||||||
>
|
|
||||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
||||||
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"/>
|
|
||||||
<circle cx="8.5" cy="8.5" r="1.5"/>
|
|
||||||
<polyline points="21 15 16 10 5 21"/>
|
|
||||||
</svg>
|
|
||||||
<span class="btn-tooltip">{{ t('uploadImg') }}</span>
|
|
||||||
</button>
|
|
||||||
<div v-if="showImageDropdown" class="image-dropdown">
|
|
||||||
<button v-if="supportsCameraCapture" type="button" @click="triggerCameraCapture">{{ cameraUploadLabel }}</button>
|
|
||||||
<button type="button" @click="triggerImageUpload">{{ t('uploadImg') }}</button>
|
|
||||||
<button type="button" @click="showUrlDialog = true; showImageDropdown = false">{{ t('insertUrl') }}</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<input type="file" ref="imageInputRef" @change="handleImageUpload" accept="image/*" style="display:none">
|
|
||||||
<input type="file" ref="cameraInputRef" @change="handleImageUpload" accept="image/*" capture="environment" style="display:none">
|
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -161,22 +141,6 @@
|
|||||||
</Transition>
|
</Transition>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="showUrlDialog" class="url-dialog-overlay" @click.self="showUrlDialog = false">
|
|
||||||
<div class="url-dialog">
|
|
||||||
<h3>{{ t('insertUrl') }}</h3>
|
|
||||||
<input
|
|
||||||
v-model="imageUrl"
|
|
||||||
type="url"
|
|
||||||
placeholder="https://..."
|
|
||||||
@keyup.enter="insertImageFromUrl"
|
|
||||||
/>
|
|
||||||
<div class="url-dialog-buttons">
|
|
||||||
<button type="button" class="dialog-btn primary" @click="insertImageFromUrl">{{ t('insert') }}</button>
|
|
||||||
<button type="button" class="dialog-btn" @click="showUrlDialog = false; imageUrl = ''">{{ t('cancel') }}</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="uploadProgress" class="upload-progress-overlay">
|
<div v-if="uploadProgress" class="upload-progress-overlay">
|
||||||
@@ -214,17 +178,12 @@ const t = (key) => settings.t[key]
|
|||||||
const initialMarkdown = computed(() => settings.initialMarkdown)
|
const initialMarkdown = computed(() => settings.initialMarkdown)
|
||||||
|
|
||||||
const root = ref(null)
|
const root = ref(null)
|
||||||
const fileInputRef = ref(null)
|
const uploadInputRef = ref(null)
|
||||||
const uploadFileInputRef = ref(null)
|
const mdInputRef = ref(null)
|
||||||
const imageInputRef = ref(null)
|
|
||||||
const cameraInputRef = ref(null)
|
|
||||||
const aiEnabled = ref(true)
|
const aiEnabled = ref(true)
|
||||||
const contentSize = ref(0)
|
const contentSize = ref(0)
|
||||||
const showImageDropdown = ref(false)
|
|
||||||
const showExportDropdown = ref(false)
|
const showExportDropdown = ref(false)
|
||||||
const showUrlDialog = ref(false)
|
|
||||||
const showSizeTooltip = ref(false)
|
const showSizeTooltip = ref(false)
|
||||||
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)
|
||||||
@@ -233,20 +192,28 @@ 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')
|
||||||
const redoLabel = computed(() => t('redo') || 'Redo')
|
const redoLabel = computed(() => t('redo') || 'Redo')
|
||||||
const cameraUploadLabel = computed(() => t('cameraUpload') || 'Use Camera')
|
|
||||||
const API_KEY = 'your-secret-key-here'
|
const API_KEY = 'your-secret-key-here'
|
||||||
const supportsCameraCapture = computed(() => {
|
|
||||||
if (typeof navigator === 'undefined') return false
|
|
||||||
const ua = navigator.userAgent || ''
|
|
||||||
return /Android|iPhone|iPad|iPod|Mobile/i.test(ua)
|
|
||||||
})
|
|
||||||
const aiButtonLabel = computed(() => {
|
const aiButtonLabel = computed(() => {
|
||||||
if (isOverLimit.value) return t('docTooLarge')
|
if (isOverLimit.value) return t('docTooLarge')
|
||||||
return aiEnabled.value ? t('disableAI') : t('enableAI')
|
return aiEnabled.value ? t('disableAI') : t('enableAI')
|
||||||
})
|
})
|
||||||
const docUploadButtonTitle = computed(() => {
|
const uploadButtonTitle = computed(() => {
|
||||||
if (isDocUploadDisabled.value) return t('uploadDocInBlockWarning') || '当前光标位置不能插入文件'
|
if (isDocUploadDisabled.value) return t('uploadDocInBlockWarning') || '当前光标位置不能插入文件'
|
||||||
return t('uploadFile')
|
return t('upload')
|
||||||
|
})
|
||||||
|
const acceptAll = computed(() => {
|
||||||
|
const types = [
|
||||||
|
'.txt', '.json', '.toml', '.yaml', '.yml',
|
||||||
|
'.docx', '.pptx', '.pdf',
|
||||||
|
'.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp', '.svg', '.heic', '.heif', '.avif',
|
||||||
|
'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',
|
||||||
|
'image/*'
|
||||||
|
]
|
||||||
|
return types.join(',')
|
||||||
})
|
})
|
||||||
|
|
||||||
let crepe = null
|
let crepe = null
|
||||||
@@ -644,10 +611,8 @@ onMounted(async () => {
|
|||||||
if (!e.target.closest('.export-btn-wrapper')) {
|
if (!e.target.closest('.export-btn-wrapper')) {
|
||||||
showExportDropdown.value = false
|
showExportDropdown.value = false
|
||||||
}
|
}
|
||||||
if (!e.target.closest('.image-btn-wrapper')) {
|
})
|
||||||
showImageDropdown.value = false
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!root.value) throw new Error('root.value is null')
|
if (!root.value) throw new Error('root.value is null')
|
||||||
|
|
||||||
@@ -817,26 +782,132 @@ const exportPdf = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const triggerUpload = () => {
|
const triggerUpload = () => {
|
||||||
fileInputRef.value?.click()
|
if (isDocUploadDisabled.value) return
|
||||||
|
uploadInputRef.value?.click()
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleFileUpload = async (event) => {
|
const handleUpload = 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
|
||||||
|
|
||||||
if (isImageFile(file)) {
|
const BATCH_LIMIT = 10
|
||||||
const objectUrl = await prepareImageFile(file)
|
const MAX_FILE_SIZE = 50 * 1024 * 1024
|
||||||
if (objectUrl) {
|
|
||||||
clearCurrentGhost()
|
if (files.length > BATCH_LIMIT) {
|
||||||
insertImageAtCursor(objectUrl)
|
alert(t('uploadBatchLimit') || `一次最多上传${BATCH_LIMIT}个文件`)
|
||||||
}
|
|
||||||
input.value = ''
|
input.value = ''
|
||||||
return
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const imageFiles = files.filter(isImageFile)
|
||||||
|
const docFiles = files.filter(f => !isImageFile(f))
|
||||||
|
|
||||||
|
if (docFiles.length > 0 && isDocUploadDisabled.value) {
|
||||||
|
alert(t('uploadDocInBlockWarning') || '当前光标位置不能插入文件')
|
||||||
|
input.value = ''
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
const isSupported = isImageFile(file) || isSupportedDocFile(file)
|
||||||
|
if (!isSupported) {
|
||||||
|
alert(t('uploadFileTypeWarning') || '不支持的文件类型')
|
||||||
|
input.value = ''
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
clearCurrentGhost()
|
||||||
|
|
||||||
|
for (const file of imageFiles) {
|
||||||
|
const objectUrl = await prepareImageFile(file)
|
||||||
|
if (objectUrl) {
|
||||||
|
insertImageAtCursor(objectUrl)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (docFiles.length > 0) {
|
||||||
|
uploadProgress.value = { current: 0, total: docFiles.length, filename: '' }
|
||||||
|
|
||||||
|
const results = []
|
||||||
|
const errors = []
|
||||||
|
|
||||||
|
for (let index = 0; index < docFiles.length; index++) {
|
||||||
|
const file = docFiles[index]
|
||||||
|
uploadProgress.value = { current: index + 1, total: docFiles.length, filename: file.name }
|
||||||
|
|
||||||
|
try {
|
||||||
|
const docType = getDocTypeFromFilename(file.name)
|
||||||
|
let content = ''
|
||||||
|
|
||||||
|
if (isTextFile(file)) {
|
||||||
|
content = await file.text()
|
||||||
|
} else if (isConvertibleFile(file)) {
|
||||||
|
content = await convertFileToMarkdown(file)
|
||||||
|
} else {
|
||||||
|
throw new Error('不支持的文件类型')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!content) {
|
||||||
|
throw new Error('文档解析结果为空')
|
||||||
|
}
|
||||||
|
|
||||||
|
results.push({
|
||||||
|
docType,
|
||||||
|
docName: file.name || `document.${docType}`,
|
||||||
|
content,
|
||||||
|
index,
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
const message = e instanceof Error ? e.message : ''
|
||||||
|
errors.push({ filename: file.name, message })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
uploadProgress.value = null
|
||||||
|
|
||||||
|
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 triggerImportMd = () => {
|
||||||
|
mdInputRef.value?.click()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleImportMd = async (event) => {
|
||||||
|
const input = event.target
|
||||||
|
const file = input.files?.[0]
|
||||||
|
if (!file) return
|
||||||
|
|
||||||
if (!isMarkdownFile(file)) {
|
if (!isMarkdownFile(file)) {
|
||||||
warnUnsupportedUploadType()
|
alert(t('uploadMdTypeWarning') || '仅支持 Markdown(.md)文件')
|
||||||
input.value = ''
|
input.value = ''
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -847,7 +918,7 @@ const handleFileUpload = async (event) => {
|
|||||||
crepe.editor.action(replaceAll(transformSpecialDocBlocksToLegacy(text)))
|
crepe.editor.action(replaceAll(transformSpecialDocBlocksToLegacy(text)))
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// File upload error, ignore
|
// ignore
|
||||||
}
|
}
|
||||||
|
|
||||||
input.value = ''
|
input.value = ''
|
||||||
@@ -867,24 +938,8 @@ const toggleAI = async () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const toggleImageDropdown = () => {
|
|
||||||
showImageDropdown.value = !showImageDropdown.value
|
|
||||||
showExportDropdown.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
const toggleExportDropdown = () => {
|
const toggleExportDropdown = () => {
|
||||||
showExportDropdown.value = !showExportDropdown.value
|
showExportDropdown.value = !showExportDropdown.value
|
||||||
showImageDropdown.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
const triggerImageUpload = () => {
|
|
||||||
showImageDropdown.value = false
|
|
||||||
imageInputRef.value?.click()
|
|
||||||
}
|
|
||||||
|
|
||||||
const triggerCameraCapture = () => {
|
|
||||||
showImageDropdown.value = false
|
|
||||||
cameraInputRef.value?.click()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const insertImageAtCursor = (src) => {
|
const insertImageAtCursor = (src) => {
|
||||||
@@ -990,135 +1045,6 @@ const insertMultipleDocBlocks = (blocks) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const triggerFileUpload = () => {
|
|
||||||
if (isDocUploadDisabled.value) return
|
|
||||||
uploadFileInputRef.value?.click()
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleUploadFile = async (event) => {
|
|
||||||
const input = event.target
|
|
||||||
const files = Array.from(input.files || [])
|
|
||||||
if (files.length === 0) return
|
|
||||||
|
|
||||||
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)) {
|
|
||||||
alert(t('uploadDocTypeWarning') || '仅支持 txt、docx、pptx、pdf 格式的文档')
|
|
||||||
input.value = ''
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isDocUploadDisabled.value || !crepe) {
|
|
||||||
alert(t('uploadDocInBlockWarning') || '当前光标位置不能插入文件')
|
|
||||||
input.value = ''
|
|
||||||
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)
|
|
||||||
let content = ''
|
|
||||||
|
|
||||||
if (isTextFile(file)) {
|
|
||||||
content = await file.text()
|
|
||||||
} else if (isConvertibleFile(file)) {
|
|
||||||
content = await convertFileToMarkdown(file)
|
|
||||||
} else {
|
|
||||||
throw new Error('不支持的文件类型')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!content) {
|
|
||||||
throw new Error('文档解析结果为空')
|
|
||||||
}
|
|
||||||
|
|
||||||
results.push({
|
|
||||||
docType,
|
|
||||||
docName: file.name || `document.${docType}`,
|
|
||||||
content,
|
|
||||||
index,
|
|
||||||
})
|
|
||||||
} catch (e) {
|
|
||||||
const message = e instanceof Error ? e.message : ''
|
|
||||||
errors.push({ filename: file.name, message })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 input = event.target
|
|
||||||
const file = input.files?.[0]
|
|
||||||
if (!file) return
|
|
||||||
|
|
||||||
const objectUrl = await prepareImageFile(file)
|
|
||||||
if (!objectUrl) {
|
|
||||||
input.value = ''
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
clearCurrentGhost()
|
|
||||||
insertImageAtCursor(objectUrl)
|
|
||||||
|
|
||||||
input.value = ''
|
|
||||||
}
|
|
||||||
|
|
||||||
const insertImageFromUrl = () => {
|
|
||||||
const url = imageUrl.value.trim()
|
|
||||||
if (!url) return
|
|
||||||
|
|
||||||
insertImageAtCursor(url)
|
|
||||||
imageUrl.value = ''
|
|
||||||
showUrlDialog.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
if (markdownSyncTimer) {
|
if (markdownSyncTimer) {
|
||||||
clearTimeout(markdownSyncTimer)
|
clearTimeout(markdownSyncTimer)
|
||||||
|
|||||||
+2
-1
@@ -1,6 +1,7 @@
|
|||||||
export const DEBUG = import.meta.env.DEV
|
export const DEBUG = import.meta.env.DEV
|
||||||
|
|
||||||
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'https://api.imageteach.tech:8002'
|
const DEFAULT_API_BASE_URL = import.meta.env.DEV ? '' : 'https://api.imageteach.tech:8002'
|
||||||
|
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || DEFAULT_API_BASE_URL
|
||||||
|
|
||||||
export const API_URL = import.meta.env.VITE_API_URL || `${API_BASE_URL}/v1/completions`
|
export const API_URL = import.meta.env.VITE_API_URL || `${API_BASE_URL}/v1/completions`
|
||||||
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`
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ export const translations = {
|
|||||||
exportMd: 'Export Markdown',
|
exportMd: 'Export Markdown',
|
||||||
exportDocx: 'Export DOCX',
|
exportDocx: 'Export DOCX',
|
||||||
exportPdf: 'Export PDF',
|
exportPdf: 'Export PDF',
|
||||||
|
upload: 'Upload',
|
||||||
uploadImg: 'Upload Image',
|
uploadImg: 'Upload Image',
|
||||||
uploadFile: 'Upload File',
|
uploadFile: 'Upload File',
|
||||||
uploadDoc: 'Upload Document',
|
uploadDoc: 'Upload Document',
|
||||||
@@ -90,6 +91,7 @@ export const translations = {
|
|||||||
exportMd: '导出 Markdown',
|
exportMd: '导出 Markdown',
|
||||||
exportDocx: '导出 DOCX',
|
exportDocx: '导出 DOCX',
|
||||||
exportPdf: '导出 PDF',
|
exportPdf: '导出 PDF',
|
||||||
|
upload: '上传',
|
||||||
uploadImg: '上传图片',
|
uploadImg: '上传图片',
|
||||||
uploadFile: '上传文件',
|
uploadFile: '上传文件',
|
||||||
uploadDoc: '上传文档',
|
uploadDoc: '上传文档',
|
||||||
@@ -147,6 +149,7 @@ export const translations = {
|
|||||||
exportMd: 'Markdownをエクスポート',
|
exportMd: 'Markdownをエクスポート',
|
||||||
exportDocx: 'DOCXをエクスポート',
|
exportDocx: 'DOCXをエクスポート',
|
||||||
exportPdf: 'PDFをエクスポート',
|
exportPdf: 'PDFをエクスポート',
|
||||||
|
upload: 'アップロード',
|
||||||
uploadImg: '画像をアップロード',
|
uploadImg: '画像をアップロード',
|
||||||
uploadFile: 'Upload File',
|
uploadFile: 'Upload File',
|
||||||
uploadFileTypeWarning: 'Unsupported file type. Supported: doc/docx/ppt/pptx/pdf/zip, images, txt/json.',
|
uploadFileTypeWarning: 'Unsupported file type. Supported: doc/docx/ppt/pptx/pdf/zip, images, txt/json.',
|
||||||
@@ -199,6 +202,7 @@ export const translations = {
|
|||||||
exportMd: 'Markdown 내보내기',
|
exportMd: 'Markdown 내보내기',
|
||||||
exportDocx: 'DOCX 내보내기',
|
exportDocx: 'DOCX 내보내기',
|
||||||
exportPdf: 'PDF 내보내기',
|
exportPdf: 'PDF 내보내기',
|
||||||
|
upload: '업로드',
|
||||||
uploadImg: '이미지 업로드',
|
uploadImg: '이미지 업로드',
|
||||||
uploadFile: 'Upload File',
|
uploadFile: 'Upload File',
|
||||||
uploadFileTypeWarning: 'Unsupported file type. Supported: doc/docx/ppt/pptx/pdf/zip, images, txt/json.',
|
uploadFileTypeWarning: 'Unsupported file type. Supported: doc/docx/ppt/pptx/pdf/zip, images, txt/json.',
|
||||||
@@ -248,6 +252,7 @@ export const translations = {
|
|||||||
exportMd: 'Markdown exportieren',
|
exportMd: 'Markdown exportieren',
|
||||||
exportDocx: 'DOCX exportieren',
|
exportDocx: 'DOCX exportieren',
|
||||||
exportPdf: 'PDF exportieren',
|
exportPdf: 'PDF exportieren',
|
||||||
|
upload: 'Hochladen',
|
||||||
uploadImg: 'Bild hochladen',
|
uploadImg: 'Bild hochladen',
|
||||||
uploadFile: 'Upload File',
|
uploadFile: 'Upload File',
|
||||||
uploadFileTypeWarning: 'Unsupported file type. Supported: doc/docx/ppt/pptx/pdf/zip, images, txt/json.',
|
uploadFileTypeWarning: 'Unsupported file type. Supported: doc/docx/ppt/pptx/pdf/zip, images, txt/json.',
|
||||||
@@ -297,6 +302,7 @@ export const translations = {
|
|||||||
exportMd: 'Exporter Markdown',
|
exportMd: 'Exporter Markdown',
|
||||||
exportDocx: 'Exporter DOCX',
|
exportDocx: 'Exporter DOCX',
|
||||||
exportPdf: 'Exporter PDF',
|
exportPdf: 'Exporter PDF',
|
||||||
|
upload: 'Télécharger',
|
||||||
uploadImg: 'Télécharger image',
|
uploadImg: 'Télécharger image',
|
||||||
uploadFile: 'Upload File',
|
uploadFile: 'Upload File',
|
||||||
uploadFileTypeWarning: 'Unsupported file type. Supported: doc/docx/ppt/pptx/pdf/zip, images, txt/json.',
|
uploadFileTypeWarning: 'Unsupported file type. Supported: doc/docx/ppt/pptx/pdf/zip, images, txt/json.',
|
||||||
|
|||||||
Reference in New Issue
Block a user