feat(思维导图助手):思维导图助手增加智能整理总结-分批次整理
parent
fd69d47eca
commit
8dbfeddfe4
|
|
@ -388,8 +388,117 @@ DEFAULT_MINDMAP_ORGANIZE_PROMPT = """你是文档结构整理助手。请基于
|
||||||
8. 节点标题尽量简短,正文说明使用短句列表。
|
8. 节点标题尽量简短,正文说明使用短句列表。
|
||||||
9. 只输出 Markdown,不要输出解释、代码块围栏或额外说明。"""
|
9. 只输出 Markdown,不要输出解释、代码块围栏或额外说明。"""
|
||||||
|
|
||||||
|
MINDMAP_MERGE_PROMPT = """你是 Markdown 思维导图结构校对助手。下面是多个分块已经整理总结后的局部 Markdown 大纲。
|
||||||
|
|
||||||
def _call_mindmap_llm(markdown: str, mode: str = "smart", custom_prompt: Optional[str] = None, task_id: Optional[str] = None) -> str:
|
任务:
|
||||||
|
1. 合并这些局部大纲为一份完整 Markdown。
|
||||||
|
2. 只检查和调整标题层级结构、顺序、重复标题和父子关系。
|
||||||
|
3. 不要重新总结正文,不要扩写内容,不要新增原文没有的信息。
|
||||||
|
4. 保留各局部大纲中的源语言:英文保持英文,中文保持中文,多语言分别保留。
|
||||||
|
5. 最大层级不超过 4 层。
|
||||||
|
6. 每个父节点下最多 8 个子节点,必要时只合并相近标题。
|
||||||
|
7. 只输出 Markdown,不要输出解释、代码块围栏或额外说明。"""
|
||||||
|
|
||||||
|
|
||||||
|
def _estimate_tokens(text: str) -> int:
|
||||||
|
ascii_chars = 0
|
||||||
|
non_ascii_chars = 0
|
||||||
|
for ch in text:
|
||||||
|
if ch.isspace():
|
||||||
|
continue
|
||||||
|
if ord(ch) < 128:
|
||||||
|
ascii_chars += 1
|
||||||
|
else:
|
||||||
|
non_ascii_chars += 1
|
||||||
|
return max(1, int(ascii_chars / 4) + int(non_ascii_chars * 1.5))
|
||||||
|
|
||||||
|
|
||||||
|
def _get_mindmap_context_budget(prompt: str, reserve_output_tokens: int = 4096) -> tuple[int, int]:
|
||||||
|
max_context_tokens = int(os.getenv("MINDMAP_LLM_MAX_CONTEXT_TOKENS", "32768"))
|
||||||
|
prompt_tokens = _estimate_tokens(prompt)
|
||||||
|
safety_tokens = int(os.getenv("MINDMAP_LLM_SAFETY_TOKENS", "1024"))
|
||||||
|
input_budget_tokens = max(2048, max_context_tokens - prompt_tokens - reserve_output_tokens - safety_tokens)
|
||||||
|
logger.info(
|
||||||
|
"Mindmap context budget max_context_tokens={} prompt_tokens={} reserve_output_tokens={} safety_tokens={} input_budget_tokens={}",
|
||||||
|
max_context_tokens, prompt_tokens, reserve_output_tokens, safety_tokens, input_budget_tokens
|
||||||
|
)
|
||||||
|
return max_context_tokens, input_budget_tokens
|
||||||
|
|
||||||
|
|
||||||
|
def _split_markdown_blocks(markdown: str) -> list[str]:
|
||||||
|
lines = markdown.splitlines()
|
||||||
|
blocks: list[str] = []
|
||||||
|
current: list[str] = []
|
||||||
|
|
||||||
|
heading_pattern = re.compile(r"^#{1,6}\s+")
|
||||||
|
for line in lines:
|
||||||
|
if heading_pattern.match(line) and current:
|
||||||
|
blocks.append("\n".join(current).strip())
|
||||||
|
current = [line]
|
||||||
|
else:
|
||||||
|
current.append(line)
|
||||||
|
|
||||||
|
if current:
|
||||||
|
blocks.append("\n".join(current).strip())
|
||||||
|
|
||||||
|
return [block for block in blocks if block]
|
||||||
|
|
||||||
|
|
||||||
|
def _split_large_block(block: str, max_tokens: int) -> list[str]:
|
||||||
|
paragraphs = re.split(r"\n{2,}", block)
|
||||||
|
chunks: list[str] = []
|
||||||
|
current: list[str] = []
|
||||||
|
current_tokens = 0
|
||||||
|
|
||||||
|
for paragraph in paragraphs:
|
||||||
|
paragraph = paragraph.strip()
|
||||||
|
if not paragraph:
|
||||||
|
continue
|
||||||
|
paragraph_tokens = _estimate_tokens(paragraph)
|
||||||
|
if current and current_tokens + paragraph_tokens > max_tokens:
|
||||||
|
chunks.append("\n\n".join(current))
|
||||||
|
current = [paragraph]
|
||||||
|
current_tokens = paragraph_tokens
|
||||||
|
else:
|
||||||
|
current.append(paragraph)
|
||||||
|
current_tokens += paragraph_tokens
|
||||||
|
|
||||||
|
if current:
|
||||||
|
chunks.append("\n\n".join(current))
|
||||||
|
return chunks or [block]
|
||||||
|
|
||||||
|
|
||||||
|
def _chunk_markdown_by_headings(markdown: str, max_tokens: int) -> list[str]:
|
||||||
|
blocks = _split_markdown_blocks(markdown)
|
||||||
|
chunks: list[str] = []
|
||||||
|
current: list[str] = []
|
||||||
|
current_tokens = 0
|
||||||
|
|
||||||
|
for block in blocks:
|
||||||
|
block_tokens = _estimate_tokens(block)
|
||||||
|
if block_tokens > max_tokens:
|
||||||
|
if current:
|
||||||
|
chunks.append("\n\n".join(current))
|
||||||
|
current = []
|
||||||
|
current_tokens = 0
|
||||||
|
chunks.extend(_split_large_block(block, max_tokens))
|
||||||
|
continue
|
||||||
|
|
||||||
|
if current and current_tokens + block_tokens > max_tokens:
|
||||||
|
chunks.append("\n\n".join(current))
|
||||||
|
current = [block]
|
||||||
|
current_tokens = block_tokens
|
||||||
|
else:
|
||||||
|
current.append(block)
|
||||||
|
current_tokens += block_tokens
|
||||||
|
|
||||||
|
if current:
|
||||||
|
chunks.append("\n\n".join(current))
|
||||||
|
|
||||||
|
return chunks or [markdown]
|
||||||
|
|
||||||
|
|
||||||
|
def _call_mindmap_llm(markdown: str, mode: str = "smart", custom_prompt: Optional[str] = None, task_id: Optional[str] = None, request_role: str = "organize") -> str:
|
||||||
base_url = os.getenv("MINDMAP_LLM_BASE_URL", "").rstrip("/")
|
base_url = os.getenv("MINDMAP_LLM_BASE_URL", "").rstrip("/")
|
||||||
model = os.getenv("MINDMAP_LLM_MODEL", "gemma-4-26B")
|
model = os.getenv("MINDMAP_LLM_MODEL", "gemma-4-26B")
|
||||||
api_key = os.getenv("MINDMAP_LLM_API_KEY", "")
|
api_key = os.getenv("MINDMAP_LLM_API_KEY", "")
|
||||||
|
|
@ -399,10 +508,6 @@ def _call_mindmap_llm(markdown: str, mode: str = "smart", custom_prompt: Optiona
|
||||||
raise RuntimeError("未配置智能整理模型服务,请设置 MINDMAP_LLM_BASE_URL")
|
raise RuntimeError("未配置智能整理模型服务,请设置 MINDMAP_LLM_BASE_URL")
|
||||||
|
|
||||||
compact_markdown = markdown.strip()
|
compact_markdown = markdown.strip()
|
||||||
max_chars = int(os.getenv("MINDMAP_LLM_MAX_INPUT_CHARS", "30000"))
|
|
||||||
if len(compact_markdown) > max_chars:
|
|
||||||
compact_markdown = compact_markdown[:max_chars] + "\n\n...(后续内容已截断)"
|
|
||||||
|
|
||||||
prompt_template = (custom_prompt or "").strip() or DEFAULT_MINDMAP_ORGANIZE_PROMPT
|
prompt_template = (custom_prompt or "").strip() or DEFAULT_MINDMAP_ORGANIZE_PROMPT
|
||||||
prompt = f"""{prompt_template}
|
prompt = f"""{prompt_template}
|
||||||
|
|
||||||
|
|
@ -410,8 +515,8 @@ def _call_mindmap_llm(markdown: str, mode: str = "smart", custom_prompt: Optiona
|
||||||
{compact_markdown}
|
{compact_markdown}
|
||||||
"""
|
"""
|
||||||
logger.info(
|
logger.info(
|
||||||
"Mindmap LLM request start task_id={} model={} base_url={} mode={} input_chars={} prompt_chars={}",
|
"Mindmap LLM request start task_id={} role={} model={} base_url={} mode={} input_chars={} input_tokens_est={} prompt_chars={}",
|
||||||
task_id or "-", model, base_url, mode, len(compact_markdown), len(prompt_template)
|
task_id or "-", request_role, model, base_url, mode, len(compact_markdown), _estimate_tokens(compact_markdown), len(prompt_template)
|
||||||
)
|
)
|
||||||
|
|
||||||
payload = {
|
payload = {
|
||||||
|
|
@ -444,12 +549,60 @@ def _call_mindmap_llm(markdown: str, mode: str = "smart", custom_prompt: Optiona
|
||||||
if not organized:
|
if not organized:
|
||||||
raise RuntimeError("智能整理模型未返回有效内容")
|
raise RuntimeError("智能整理模型未返回有效内容")
|
||||||
logger.info(
|
logger.info(
|
||||||
"Mindmap LLM request completed task_id={} output_chars={}",
|
"Mindmap LLM request completed task_id={} role={} output_chars={} output_tokens_est={}",
|
||||||
task_id or "-", len(organized)
|
task_id or "-", request_role, len(organized), _estimate_tokens(organized)
|
||||||
)
|
)
|
||||||
return organized
|
return organized
|
||||||
|
|
||||||
|
|
||||||
|
def _organize_mindmap_markdown(markdown: str, mode: str, custom_prompt: Optional[str], task_id: str) -> str:
|
||||||
|
prompt_template = (custom_prompt or "").strip() or DEFAULT_MINDMAP_ORGANIZE_PROMPT
|
||||||
|
_, input_budget_tokens = _get_mindmap_context_budget(prompt_template)
|
||||||
|
source_tokens = _estimate_tokens(markdown)
|
||||||
|
logger.info(
|
||||||
|
"Mindmap organize strategy task_id={} source_chars={} source_tokens_est={} input_budget_tokens={}",
|
||||||
|
task_id, len(markdown), source_tokens, input_budget_tokens
|
||||||
|
)
|
||||||
|
|
||||||
|
if source_tokens <= input_budget_tokens:
|
||||||
|
_update_task_progress(task_id, 35, "调用智能整理模型")
|
||||||
|
return _call_mindmap_llm(markdown, mode, prompt_template, task_id, "single")
|
||||||
|
|
||||||
|
chunks = _chunk_markdown_by_headings(markdown, input_budget_tokens)
|
||||||
|
logger.info("Mindmap large input split task_id={} chunks={}", task_id, len(chunks))
|
||||||
|
partial_results: list[str] = []
|
||||||
|
for index, chunk in enumerate(chunks, start=1):
|
||||||
|
progress = 20 + int(index / max(len(chunks), 1) * 55)
|
||||||
|
_update_task_progress(task_id, progress, f"智能整理分块 {index}/{len(chunks)}")
|
||||||
|
logger.info(
|
||||||
|
"Mindmap chunk organize task_id={} chunk={}/{} chars={} tokens_est={}",
|
||||||
|
task_id, index, len(chunks), len(chunk), _estimate_tokens(chunk)
|
||||||
|
)
|
||||||
|
partial = _call_mindmap_llm(chunk, mode, prompt_template, task_id, f"chunk-{index}")
|
||||||
|
partial_results.append(partial)
|
||||||
|
|
||||||
|
merged_input = "\n\n".join(
|
||||||
|
f"<!-- chunk {index} -->\n{partial}"
|
||||||
|
for index, partial in enumerate(partial_results, start=1)
|
||||||
|
)
|
||||||
|
_, merge_budget_tokens = _get_mindmap_context_budget(MINDMAP_MERGE_PROMPT)
|
||||||
|
merge_tokens = _estimate_tokens(merged_input)
|
||||||
|
if merge_tokens > merge_budget_tokens:
|
||||||
|
logger.warning(
|
||||||
|
"Mindmap merged outline still exceeds context task_id={} tokens_est={} budget={} chunks={}",
|
||||||
|
task_id, merge_tokens, merge_budget_tokens, len(partial_results)
|
||||||
|
)
|
||||||
|
merge_chunks = _chunk_markdown_by_headings(merged_input, merge_budget_tokens)
|
||||||
|
merged_round: list[str] = []
|
||||||
|
for index, chunk in enumerate(merge_chunks, start=1):
|
||||||
|
_update_task_progress(task_id, 78 + int(index / max(len(merge_chunks), 1) * 10), f"合并局部大纲 {index}/{len(merge_chunks)}")
|
||||||
|
merged_round.append(_call_mindmap_llm(chunk, mode, MINDMAP_MERGE_PROMPT, task_id, f"merge-round-{index}"))
|
||||||
|
merged_input = "\n\n".join(merged_round)
|
||||||
|
|
||||||
|
_update_task_progress(task_id, 90, "全局整理标题结构")
|
||||||
|
return _call_mindmap_llm(merged_input, mode, MINDMAP_MERGE_PROMPT, task_id, "merge")
|
||||||
|
|
||||||
|
|
||||||
async def _run_mindmap_organize_task(task_id: str, markdown: str, mode: str, prompt: Optional[str]):
|
async def _run_mindmap_organize_task(task_id: str, markdown: str, mode: str, prompt: Optional[str]):
|
||||||
try:
|
try:
|
||||||
_store_task_progress(task_id, {
|
_store_task_progress(task_id, {
|
||||||
|
|
@ -460,8 +613,7 @@ async def _run_mindmap_organize_task(task_id: str, markdown: str, mode: str, pro
|
||||||
"file_names": "",
|
"file_names": "",
|
||||||
"result_md": None,
|
"result_md": None,
|
||||||
})
|
})
|
||||||
_update_task_progress(task_id, 35, "调用智能整理模型")
|
organized = await asyncio.to_thread(_organize_mindmap_markdown, markdown, mode, prompt, task_id)
|
||||||
organized = await asyncio.to_thread(_call_mindmap_llm, markdown, mode, prompt, task_id)
|
|
||||||
state = _get_task_progress(task_id) or {}
|
state = _get_task_progress(task_id) or {}
|
||||||
state.update({
|
state.update({
|
||||||
"progress": 100,
|
"progress": 100,
|
||||||
|
|
|
||||||
|
|
@ -14,29 +14,6 @@
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item label="导出分辨率" class="form-item">
|
|
||||||
<el-select v-model="exportResolutionPreset" style="width: 100%" class="select">
|
|
||||||
<el-option
|
|
||||||
v-for="option in exportResolutionOptions"
|
|
||||||
:key="option.value"
|
|
||||||
:label="option.label"
|
|
||||||
:value="option.value"
|
|
||||||
/>
|
|
||||||
</el-select>
|
|
||||||
<el-input-number
|
|
||||||
v-if="exportResolutionPreset === 'custom'"
|
|
||||||
v-model="draftConfig.exportResolution"
|
|
||||||
:min="720"
|
|
||||||
:max="16384"
|
|
||||||
:step="100"
|
|
||||||
controls-position="right"
|
|
||||||
class="export-resolution-input"
|
|
||||||
/>
|
|
||||||
<div class="form-item-description">
|
|
||||||
PNG/JPEG 导出长边上限;默认 8K,导出文件会按该最大长边生效。
|
|
||||||
</div>
|
|
||||||
</el-form-item>
|
|
||||||
|
|
||||||
<!-- 解析后端 -->
|
<!-- 解析后端 -->
|
||||||
<el-form-item :label="$t('config.backend')" class="form-item">
|
<el-form-item :label="$t('config.backend')" class="form-item">
|
||||||
<el-select
|
<el-select
|
||||||
|
|
@ -73,25 +50,6 @@
|
||||||
</div>
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<div class="divider"></div>
|
|
||||||
|
|
||||||
<div class="section-title">思维导图智能整理</div>
|
|
||||||
|
|
||||||
<el-form-item label="智能整理提示词" class="form-item">
|
|
||||||
<el-input
|
|
||||||
v-model="draftConfig.mindmapPrompt"
|
|
||||||
type="textarea"
|
|
||||||
:rows="10"
|
|
||||||
resize="vertical"
|
|
||||||
class="input"
|
|
||||||
/>
|
|
||||||
<div class="form-item-description">
|
|
||||||
切换到“智能整理”时发送给后端大模型;默认保留标题结构、总结段落要点,并保持原文语言。
|
|
||||||
</div>
|
|
||||||
</el-form-item>
|
|
||||||
|
|
||||||
<div class="divider"></div>
|
|
||||||
|
|
||||||
<!-- 识别选项 -->
|
<!-- 识别选项 -->
|
||||||
<div class="section-title">{{ $t('config.recognitionOptions') }}</div>
|
<div class="section-title">{{ $t('config.recognitionOptions') }}</div>
|
||||||
|
|
||||||
|
|
@ -144,6 +102,46 @@
|
||||||
</div>
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<div class="divider"></div>
|
||||||
|
|
||||||
|
<div class="section-title">导出与智能整理</div>
|
||||||
|
|
||||||
|
<el-form-item label="导出分辨率" class="form-item">
|
||||||
|
<el-select v-model="exportResolutionPreset" style="width: 100%" class="select">
|
||||||
|
<el-option
|
||||||
|
v-for="option in exportResolutionOptions"
|
||||||
|
:key="option.value"
|
||||||
|
:label="option.label"
|
||||||
|
:value="option.value"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
<el-input-number
|
||||||
|
v-if="exportResolutionPreset === 'custom'"
|
||||||
|
v-model="draftConfig.exportResolution"
|
||||||
|
:min="720"
|
||||||
|
:max="16384"
|
||||||
|
:step="100"
|
||||||
|
controls-position="right"
|
||||||
|
class="export-resolution-input"
|
||||||
|
/>
|
||||||
|
<div class="form-item-description">
|
||||||
|
PNG/JPEG 导出长边上限;默认 8K,导出文件会按该最大长边生效。
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="智能整理提示词" class="form-item">
|
||||||
|
<el-input
|
||||||
|
v-model="draftConfig.mindmapPrompt"
|
||||||
|
type="textarea"
|
||||||
|
:rows="10"
|
||||||
|
resize="vertical"
|
||||||
|
class="input"
|
||||||
|
/>
|
||||||
|
<div class="form-item-description">
|
||||||
|
切换到“智能整理”时发送给后端大模型;默认保留标题结构、总结段落要点,并保持原文语言。
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
<div class="config-actions">
|
<div class="config-actions">
|
||||||
<el-button @click="resetDraft">重置</el-button>
|
<el-button @click="resetDraft">重置</el-button>
|
||||||
<el-button type="primary" @click="confirmConfig">确认</el-button>
|
<el-button type="primary" @click="confirmConfig">确认</el-button>
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@
|
||||||
type="primary"
|
type="primary"
|
||||||
size="small"
|
size="small"
|
||||||
class="action-button primary"
|
class="action-button primary"
|
||||||
|
:disabled="!content"
|
||||||
>
|
>
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<el-icon><Download /></el-icon>
|
<el-icon><Download /></el-icon>
|
||||||
|
|
@ -456,6 +457,10 @@ const downloadMindMap = async (format: ExportFormat = 'svg') => {
|
||||||
|
|
||||||
// 处理下载命令
|
// 处理下载命令
|
||||||
const handleDownload = (command: string) => {
|
const handleDownload = (command: string) => {
|
||||||
|
if (!props.content) {
|
||||||
|
ElMessage.warning('暂无思维导图内容可下载')
|
||||||
|
return
|
||||||
|
}
|
||||||
if (command === 'svg' || command === 'png' || command === 'jpeg') {
|
if (command === 'svg' || command === 'png' || command === 'jpeg') {
|
||||||
downloadMindMap(command)
|
downloadMindMap(command)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -129,6 +129,8 @@ export function useDocumentProcessor() {
|
||||||
const validFiles: File[] = []
|
const validFiles: File[] = []
|
||||||
const file = files[0]
|
const file = files[0]
|
||||||
uploadedFiles.value = []
|
uploadedFiles.value = []
|
||||||
|
results.value = null
|
||||||
|
error.value = null
|
||||||
if (files.length > 1) {
|
if (files.length > 1) {
|
||||||
error.value = '一次只能选择一个文件,已使用第一个文件'
|
error.value = '一次只能选择一个文件,已使用第一个文件'
|
||||||
}
|
}
|
||||||
|
|
@ -138,16 +140,17 @@ export function useDocumentProcessor() {
|
||||||
|
|
||||||
console.log('Processing file:', fileName, 'type:', fileType)
|
console.log('Processing file:', fileName, 'type:', fileType)
|
||||||
|
|
||||||
// 验证文件类型 - 支持 PDF、Word 和后端可转换为 PDF 的图片格式
|
// 验证文件类型 - 支持 Markdown、PDF、Word 和后端可转换为 PDF 的图片格式
|
||||||
const supportedImageExtensions = ['.png', '.jpg', '.jpeg', '.webp', '.bmp', '.tiff', '.gif', '.jp2']
|
const supportedImageExtensions = ['.png', '.jpg', '.jpeg', '.webp', '.bmp', '.tiff', '.gif', '.jp2']
|
||||||
const isImage = fileType.startsWith('image/') && supportedImageExtensions.some((ext) => fileName.endsWith(ext))
|
const isImage = fileType.startsWith('image/') && supportedImageExtensions.some((ext) => fileName.endsWith(ext))
|
||||||
const isPdf = fileType === 'application/pdf'
|
const isPdf = fileType === 'application/pdf'
|
||||||
const isWord = fileName.endsWith('.docx') || fileName.endsWith('.doc') || fileType === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
|
const isWord = fileName.endsWith('.docx') || fileName.endsWith('.doc') || fileType === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
|
||||||
|
const isMarkdown = fileName.endsWith('.md') || fileType === 'text/markdown'
|
||||||
|
|
||||||
console.log('File type checks - isImage:', isImage, 'isPdf:', isPdf, 'isWord:', isWord)
|
console.log('File type checks - isImage:', isImage, 'isPdf:', isPdf, 'isWord:', isWord, 'isMarkdown:', isMarkdown)
|
||||||
|
|
||||||
if (!isImage && !isPdf && !isWord) {
|
if (!isImage && !isPdf && !isWord && !isMarkdown) {
|
||||||
error.value = '不支持的文件类型,请选择 PDF、Word、JPG、PNG、WEBP、BMP、TIFF、GIF 或 JP2 文件'
|
error.value = '不支持的文件类型,请选择 MD、PDF、Word、JPG、PNG、WEBP、BMP、TIFF、GIF 或 JP2 文件'
|
||||||
console.log('Unsupported file type:', fileName, fileType)
|
console.log('Unsupported file type:', fileName, fileType)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -159,8 +162,26 @@ export function useDocumentProcessor() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果是 Word 文档,转换为 PDF
|
if (isMarkdown) {
|
||||||
if (isWord) {
|
try {
|
||||||
|
isUploading.value = true
|
||||||
|
const markdown = await file.text()
|
||||||
|
validFiles.push(file)
|
||||||
|
results.value = {
|
||||||
|
markdown,
|
||||||
|
source: markdown,
|
||||||
|
mindmap: buildMindmapMarkdown(markdown)
|
||||||
|
}
|
||||||
|
error.value = null
|
||||||
|
console.log('Markdown file loaded directly:', fileName, markdown.length)
|
||||||
|
} catch (err) {
|
||||||
|
error.value = 'Markdown 文件读取失败: ' + (err as Error).message
|
||||||
|
console.error('Markdown read failed:', (err as Error).message)
|
||||||
|
return
|
||||||
|
} finally {
|
||||||
|
isUploading.value = false
|
||||||
|
}
|
||||||
|
} else if (isWord) {
|
||||||
try {
|
try {
|
||||||
isUploading.value = true
|
isUploading.value = true
|
||||||
error.value = '正在将 Word 文档转换为 PDF...'
|
error.value = '正在将 Word 文档转换为 PDF...'
|
||||||
|
|
@ -242,6 +263,14 @@ export function useDocumentProcessor() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const firstFileName = uploadedFiles.value[0]?.name.toLowerCase() || ''
|
||||||
|
if (firstFileName.endsWith('.md')) {
|
||||||
|
if (!results.value) {
|
||||||
|
error.value = 'Markdown 文件内容为空,请重新上传'
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
isProcessing.value = true
|
isProcessing.value = true
|
||||||
error.value = null
|
error.value = null
|
||||||
processingStage.value = '准备提交解析任务'
|
processingStage.value = '准备提交解析任务'
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@
|
||||||
<el-icon class="upload-icon"><Folder /></el-icon>
|
<el-icon class="upload-icon"><Folder /></el-icon>
|
||||||
<div class="upload-copy">
|
<div class="upload-copy">
|
||||||
<div class="upload-text">文件导入</div>
|
<div class="upload-text">文件导入</div>
|
||||||
<div class="upload-hint">支持 PDF、Word、JPG、PNG、WEBP、BMP、TIFF、GIF、JP2 单个文件</div>
|
<div class="upload-hint">支持 MD、PDF、Word、JPG、PNG、BMP、TIFF 等格式文件</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -201,7 +201,7 @@
|
||||||
<input
|
<input
|
||||||
ref="fileInput"
|
ref="fileInput"
|
||||||
type="file"
|
type="file"
|
||||||
accept=".pdf,.doc,.docx,.png,.jpg,.jpeg,.webp,.bmp,.tiff,.gif,.jp2"
|
accept=".md,.pdf,.doc,.docx,.png,.jpg,.jpeg,.webp,.bmp,.tiff,.gif,.jp2"
|
||||||
style="position: absolute; width: 0; height: 0; overflow: hidden;"
|
style="position: absolute; width: 0; height: 0; overflow: hidden;"
|
||||||
@change="handleFileInputChange"
|
@change="handleFileInputChange"
|
||||||
/>
|
/>
|
||||||
|
|
@ -618,7 +618,9 @@ const handleFileInputChange = async (event: Event) => {
|
||||||
if (uploadedFiles.value.length > 0) {
|
if (uploadedFiles.value.length > 0) {
|
||||||
resetSmartOrganizeState()
|
resetSmartOrganizeState()
|
||||||
isUploadAreaCollapsed.value = true
|
isUploadAreaCollapsed.value = true
|
||||||
initializeManualResult()
|
if (!results.value) {
|
||||||
|
initializeManualResult()
|
||||||
|
}
|
||||||
markdownRenderMode.value = 'markdown'
|
markdownRenderMode.value = 'markdown'
|
||||||
sourceViewMode.value = 'markdown'
|
sourceViewMode.value = 'markdown'
|
||||||
activeTab.value = 'source'
|
activeTab.value = 'source'
|
||||||
|
|
@ -635,7 +637,9 @@ const handleDrop = async (event: DragEvent) => {
|
||||||
if (uploadedFiles.value.length > 0) {
|
if (uploadedFiles.value.length > 0) {
|
||||||
resetSmartOrganizeState()
|
resetSmartOrganizeState()
|
||||||
isUploadAreaCollapsed.value = true
|
isUploadAreaCollapsed.value = true
|
||||||
initializeManualResult()
|
if (!results.value) {
|
||||||
|
initializeManualResult()
|
||||||
|
}
|
||||||
markdownRenderMode.value = 'markdown'
|
markdownRenderMode.value = 'markdown'
|
||||||
sourceViewMode.value = 'markdown'
|
sourceViewMode.value = 'markdown'
|
||||||
activeTab.value = 'source'
|
activeTab.value = 'source'
|
||||||
|
|
@ -733,14 +737,16 @@ onUnmounted(() => {
|
||||||
background-color: #FFFFFF;
|
background-color: #FFFFFF;
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
min-height: 36px;
|
height: 52px;
|
||||||
|
min-height: 52px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
.drag-upload-area.collapsed {
|
.drag-upload-area.collapsed {
|
||||||
padding: 8px 16px;
|
padding: 8px 16px;
|
||||||
min-height: 36px;
|
min-height: 52px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.drag-upload-area:hover,
|
.drag-upload-area:hover,
|
||||||
|
|
@ -782,6 +788,7 @@ onUnmounted(() => {
|
||||||
|
|
||||||
.uploaded-files {
|
.uploaded-files {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -795,6 +802,7 @@ onUnmounted(() => {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue