bd5bfbac2d
Root cause: LLM receiving full 34k-char JRXML would regenerate from scratch
instead of modifying coordinates in-place, shrinking output to ~3k chars.
Solution (programmatic node control, not prompt engineering):
- New agent/jrxml_windower.py: decompose JRXML into header (never sent to
LLM) + individual bands. Split bands >4000 chars at element boundaries.
Reassemble with element count validation (>10% change = rollback).
- Rewrite refine_layout: per-band windowed LLM processing (~2-4k chars
each). LLM cannot "reimagine" the entire report.
- Rewrite map_fields: 100% programmatic regex $F{field_N} -> real name
replacement. Zero LLM calls, zero content loss.
- _sanitize_field_name: non-ASCII chars escaped to _uXXXX_ format for
valid JRXML identifiers.
- Tests: 48 new unit tests (windower 28 + map_fields 20). All passing.
Full suite 385 tests, zero regressions.
197 lines
5.5 KiB
Vue
197 lines
5.5 KiB
Vue
<script setup lang="ts">
|
|
import { watch, nextTick, ref } from 'vue'
|
|
import { useChatStore } from './stores/chat'
|
|
import { useSessionStore } from './stores/session'
|
|
import { api } from './api/client'
|
|
import Sidebar from './components/Sidebar.vue'
|
|
import ChatMessages from './components/ChatMessages.vue'
|
|
import ProcessSection from './components/ProcessSection.vue'
|
|
import SummaryCard from './components/SummaryCard.vue'
|
|
import UnifiedInput from './components/UnifiedInput.vue'
|
|
import KbSelector from './components/KbSelector.vue'
|
|
import KbManager from './components/KbManager.vue'
|
|
import { useKbStore } from './stores/kb'
|
|
|
|
const chat = useChatStore()
|
|
const session = useSessionStore()
|
|
const kb = useKbStore()
|
|
|
|
function handleKbChange(kbId: string) {
|
|
if (session.currentId) {
|
|
kb.bindKbToSession(session.currentId, kbId)
|
|
}
|
|
}
|
|
|
|
const chatContainer = ref<HTMLElement | null>(null)
|
|
|
|
async function scrollToBottom() {
|
|
await nextTick()
|
|
if (chatContainer.value) {
|
|
chatContainer.value.scrollTop = chatContainer.value.scrollHeight
|
|
}
|
|
}
|
|
|
|
watch(
|
|
() => [chat.messages.length, chat.streamText],
|
|
() => scrollToBottom(),
|
|
{ flush: 'post' }
|
|
)
|
|
|
|
async function handleSend(text: string, files: File[]) {
|
|
if (!session.currentId) {
|
|
const sid = await session.createSession()
|
|
await session.switchSession(sid)
|
|
}
|
|
|
|
// Upload files first
|
|
const remoteIds: string[] = []
|
|
for (const f of files) {
|
|
try {
|
|
const info = await api.uploadFile(f, session.currentId)
|
|
remoteIds.push(info.file_id)
|
|
} catch (e) {
|
|
console.error('文件上传失败:', e)
|
|
chat.setError('文件上传失败')
|
|
return
|
|
}
|
|
}
|
|
|
|
chat.addMessage({ role: 'user', content: text || '[附加文件]' })
|
|
scrollToBottom()
|
|
|
|
chat.startStreaming()
|
|
|
|
try {
|
|
await api.chat(session.currentId, text, remoteIds, {
|
|
onNodeStart(data) {
|
|
chat.addNode({ node: data.node, label: data.label, step_index: data.step_index })
|
|
},
|
|
onNodeComplete(data) {
|
|
chat.completeNode(data)
|
|
},
|
|
onStreamToken(data) {
|
|
chat.appendStreamToken(data.text)
|
|
scrollToBottom()
|
|
},
|
|
onAgentComplete(data) {
|
|
chat.finishStreaming({
|
|
intent: data.intent,
|
|
status: data.status,
|
|
jrxml_length: data.jrxml_length,
|
|
error_msg: data.error_msg,
|
|
natural_explanation: data.natural_explanation,
|
|
consult_answer: data.consult_answer,
|
|
retry_count: data.retry_count,
|
|
total_duration_ms: data.total_duration_ms,
|
|
ocr_extraction_result: data.ocr_extraction_result,
|
|
})
|
|
|
|
const streamContent = chat.streamText
|
|
if (data.status === 'pass') {
|
|
if (streamContent) {
|
|
chat.addMessage({ role: 'assistant', content: streamContent, type: 'jrxml' })
|
|
}
|
|
chat.addMessage({ role: 'assistant', content: 'JRXML 生成成功!可从侧边栏下载。', type: 'success' })
|
|
} else if (data.status && data.status !== 'pass') {
|
|
chat.addMessage({
|
|
role: 'assistant',
|
|
content: `经过 ${data.retry_count} 次重试后失败。\n\n错误: ${data.error_msg}${data.natural_explanation ? '\n\n原因: ' + data.natural_explanation : ''}`,
|
|
type: 'error',
|
|
})
|
|
} else if (data.intent === 'consult_question') {
|
|
// 咨询回答:优先用 streamContent,其次用 consult_answer
|
|
const answerText = streamContent || data.consult_answer || ''
|
|
if (answerText) {
|
|
chat.addMessage({ role: 'assistant', content: answerText, type: 'consult' })
|
|
} else {
|
|
chat.addMessage({ role: 'assistant', content: '咨询已完成,但未获取到回答内容。', type: 'error' })
|
|
}
|
|
} else {
|
|
if (streamContent) {
|
|
chat.addMessage({ role: 'assistant', content: streamContent, type: 'jrxml' })
|
|
}
|
|
}
|
|
|
|
// Refresh session sidebar data after a short delay
|
|
setTimeout(() => session.refreshFromApi(), 500)
|
|
},
|
|
onAgentError(data) {
|
|
chat.setError(data.error)
|
|
chat.addMessage({ role: 'assistant', content: `执行异常: ${data.error}`, type: 'error' })
|
|
setTimeout(() => session.refreshFromApi(), 500)
|
|
},
|
|
})
|
|
} catch (e: any) {
|
|
chat.setError(e.message || '网络请求失败')
|
|
chat.addMessage({ role: 'assistant', content: `请求失败: ${e.message}`, type: 'error' })
|
|
chat.finishStreaming({ status: '' })
|
|
} finally {
|
|
if (chat.streaming) {
|
|
chat.finishStreaming({ status: '' })
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<div class="app-layout">
|
|
<Sidebar @quickAction="(text) => handleSend(text, [])" />
|
|
|
|
<main class="main-area">
|
|
<KbSelector @change="handleKbChange" />
|
|
<div class="chat-container" ref="chatContainer">
|
|
<ChatMessages />
|
|
<ProcessSection />
|
|
<SummaryCard />
|
|
</div>
|
|
|
|
<UnifiedInput
|
|
:disabled="chat.streaming"
|
|
@send="handleSend"
|
|
/>
|
|
</main>
|
|
|
|
<KbManager />
|
|
</div>
|
|
</template>
|
|
|
|
<style>
|
|
* {
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
body {
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
|
background: #11111b;
|
|
color: #cdd6f4;
|
|
}
|
|
|
|
#app {
|
|
width: 100vw;
|
|
height: 100vh;
|
|
}
|
|
|
|
.app-layout {
|
|
display: flex;
|
|
width: 100%;
|
|
height: 100%;
|
|
}
|
|
|
|
.main-area {
|
|
flex: 1;
|
|
display: flex;
|
|
flex-direction: column;
|
|
height: 100vh;
|
|
min-width: 0;
|
|
}
|
|
|
|
.chat-container {
|
|
flex: 1;
|
|
overflow-y: auto;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
</style>
|