feat: 5-issue fix — OCR image parse bug + Vue frontend feature parity + streaming UX

Fix 1 (CRITICAL): file_parser.py suffix normalization ".jpg", api_server.py Path.suffix
Fix 2: Sidebar version history download, ProcessSection replaces old components
Fix 3: OCR content/position layer structured logging in agent/nodes.py
Fix 4: collapsible process sections with per-section stream routing + auto-fold
Fix 5: agent_complete total_duration_ms, SummaryCard duration display

- backend/file_parser.py: normalize suffix to always include leading dot
- api_server.py: step_index in node_start, total_duration_ms in agent_complete
- agent/nodes.py: _log_ocr_layers() for [内容层]/[位置层]/[合并] logging
- frontend: ProcessSection.vue (NEW), chat.ts sections model, Sidebar versions
- CLAUDE.md: updated component list and v6 changelog
This commit is contained in:
2026-05-21 23:43:21 +08:00
parent 60e2f520ba
commit a364e1de81
9 changed files with 492 additions and 21 deletions
+103 -7
View File
@@ -1,7 +1,7 @@
/** Pinia store — chat messages + streaming state. */
/** Pinia store — chat messages + streaming state with per-section tracking. */
import { defineStore } from 'pinia'
import { ref } from 'vue'
import { ref, computed } from 'vue'
export interface Message {
id: string
@@ -18,6 +18,18 @@ export interface NodeProgress {
status: 'running' | 'done'
}
export interface ProcessSection {
node: string
label: string
stepIndex: number
detail: string
content: string
status: 'running' | 'done'
expanded: boolean
durationMs: number
startTime: number
}
export interface AgentSummary {
intent: string
status: string
@@ -27,18 +39,45 @@ export interface AgentSummary {
retry_count: number
}
export interface UploadedFile {
file_id: string
filename: string
content_type: string
size: number
preview?: string
}
export const useChatStore = defineStore('chat', () => {
const messages = ref<Message[]>([])
const streaming = ref(false)
const lastDurationMs = ref(0)
const streamText = ref('')
const nodes = ref<NodeProgress[]>([])
const sections = ref<ProcessSection[]>([])
const error = ref<string>('')
const ocrResult = ref<any>(null)
const uploadedFiles = ref<UploadedFile[]>([])
const summary = ref<AgentSummary>({
intent: '', status: '', jrxml_length: 0,
error_msg: '', natural_explanation: '', retry_count: 0,
})
const totalDurationMs = computed(() => {
if (sections.value.length === 0) return 0
const last = sections.value[sections.value.length - 1]
return last.status === 'done'
? last.startTime + last.durationMs - sections.value[0].startTime
: Date.now() - sections.value[0].startTime
})
function formatDuration(ms: number): string {
if (ms < 1000) return `${ms}ms`
if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`
const m = Math.floor(ms / 60000)
const s = Math.round((ms % 60000) / 1000)
return `${m}m${s}s`
}
function addMessage(msg: Omit<Message, 'id' | 'timestamp'>) {
messages.value.push({
...msg,
@@ -49,8 +88,10 @@ export const useChatStore = defineStore('chat', () => {
function startStreaming() {
streaming.value = true
lastDurationMs.value = 0
streamText.value = ''
nodes.value = []
sections.value = []
error.value = ''
summary.value = {
intent: '', status: '', jrxml_length: 0,
@@ -60,10 +101,31 @@ export const useChatStore = defineStore('chat', () => {
function appendStreamToken(text: string) {
streamText.value += text
const active = sections.value.find(s => s.status === 'running')
if (active) {
active.content += text
}
}
function addNode(node: { node: string; label: string }) {
nodes.value.push({ ...node, status: 'running' })
function addNode(node: { node: string; label: string; step_index?: number }) {
nodes.value.push({ node: node.node, label: node.label, status: 'running' })
const prev = sections.value.find(s => s.status === 'running')
if (prev) {
prev.status = 'done'
prev.durationMs = Date.now() - prev.startTime
prev.expanded = false
}
sections.value.push({
node: node.node,
label: node.label,
stepIndex: node.step_index || sections.value.length + 1,
detail: '',
content: '',
status: 'running',
expanded: true,
durationMs: 0,
startTime: Date.now(),
})
}
function completeNode(node: { node: string; label: string; detail: string }) {
@@ -72,16 +134,30 @@ export const useChatStore = defineStore('chat', () => {
existing.status = 'done'
existing.detail = node.detail
}
const sec = sections.value.find(s => s.node === node.node && s.status === 'running')
if (sec) {
sec.detail = node.detail
sec.status = 'done'
sec.durationMs = Date.now() - sec.startTime
}
}
function finishStreaming(data?: {
intent?: string; status?: string; jrxml_length?: number
error_msg?: string; natural_explanation?: string; retry_count?: number
ocr_extraction_result?: any
total_duration_ms?: number; ocr_extraction_result?: any
}) {
streaming.value = false
nodes.value.forEach(n => { n.status = 'done' })
sections.value.forEach(s => {
if (s.status === 'running') {
s.status = 'done'
s.durationMs = Date.now() - s.startTime
}
s.expanded = false
})
if (data) {
lastDurationMs.value = data.total_duration_ms || 0
summary.value = {
intent: data.intent || '',
status: data.status || '',
@@ -99,15 +175,33 @@ export const useChatStore = defineStore('chat', () => {
function setError(err: string) {
error.value = err
streaming.value = false
sections.value.forEach(s => { s.status = 'done'; s.expanded = false })
}
function toggleSection(node: string) {
const sec = sections.value.find(s => s.node === node)
if (sec) {
sec.expanded = !sec.expanded
}
}
function addUploadedFile(file: UploadedFile) {
uploadedFiles.value.push(file)
}
function removeUploadedFile(fileId: string) {
uploadedFiles.value = uploadedFiles.value.filter(f => f.file_id !== fileId)
}
function reset() {
messages.value = []
streamText.value = ''
nodes.value = []
sections.value = []
error.value = ''
streaming.value = false
ocrResult.value = null
uploadedFiles.value = []
summary.value = {
intent: '', status: '', jrxml_length: 0,
error_msg: '', natural_explanation: '', retry_count: 0,
@@ -115,8 +209,10 @@ export const useChatStore = defineStore('chat', () => {
}
return {
messages, streaming, streamText, nodes, error, ocrResult, summary,
messages, streaming, lastDurationMs, streamText, nodes, sections, error, ocrResult,
uploadedFiles, summary, totalDurationMs,
addMessage, startStreaming, appendStreamToken, addNode, completeNode,
finishStreaming, setError, reset,
finishStreaming, setError, toggleSection, reset, formatDuration,
addUploadedFile, removeUploadedFile,
}
})