/** Pinia store — chat messages + streaming state with per-section tracking. */ import { defineStore } from 'pinia' import { ref, computed } from 'vue' export interface Message { id: string role: 'user' | 'assistant' content: string type?: 'text' | 'jrxml' | 'error' | 'success' | 'consult' timestamp: string } export interface NodeProgress { node: string label: string detail?: string 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 jrxml_length: number error_msg: string natural_explanation: string 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([]) const streaming = ref(false) const lastDurationMs = ref(0) const streamText = ref('') const nodes = ref([]) const sections = ref([]) const error = ref('') const ocrResult = ref(null) const uploadedFiles = ref([]) const summary = ref({ 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) { messages.value.push({ ...msg, id: crypto.randomUUID(), timestamp: new Date().toISOString(), }) } function startStreaming() { streaming.value = true lastDurationMs.value = 0 streamText.value = '' nodes.value = [] sections.value = [] error.value = '' summary.value = { intent: '', status: '', jrxml_length: 0, error_msg: '', natural_explanation: '', retry_count: 0, } } 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; 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 }) { const existing = nodes.value.find(n => n.node === node.node) if (existing) { 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; consult_answer?: string; retry_count?: number 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 || '', jrxml_length: data.jrxml_length || 0, error_msg: data.error_msg || '', natural_explanation: data.natural_explanation || '', consult_answer: data.consult_answer || '', retry_count: data.retry_count || 0, } if (data.ocr_extraction_result) { ocrResult.value = data.ocr_extraction_result } } } 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, } } return { messages, streaming, lastDurationMs, streamText, nodes, sections, error, ocrResult, uploadedFiles, summary, totalDurationMs, addMessage, startStreaming, appendStreamToken, addNode, completeNode, finishStreaming, setError, toggleSection, reset, formatDuration, addUploadedFile, removeUploadedFile, } })