bb6cc6e241
- lib/java/: Java renderer (JrxmlRenderer) using JasperReports 6.21.0 - JrxmlDebug for diagnostics, JrxmlGen for format reference - download_jars.sh for one-time dependency setup - agent/nodes.py: _render_jrxml_to_png() and _compute_pixel_similarity() - Pixel comparison integrates into validate node (SSIM < 0.4 fails) - Pixel fidelity context injected into correct_jrxml for targeted fixes - tests/test_pixel_comparison.py: 15 unit tests (render, SSIM, integration) - .gitignore: exclude lib/java/*.jar, lib/java/*.class, tmp/ - CLAUDE.md: v11 changelog documenting the rendering pipeline - All non-LLM tests pass (97/97)
220 lines
6.0 KiB
TypeScript
220 lines
6.0 KiB
TypeScript
/** 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<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,
|
|
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,
|
|
}
|
|
})
|