2026-05-21 20:04:27 +08:00
|
|
|
<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'
|
2026-05-21 23:43:21 +08:00
|
|
|
import ProcessSection from './components/ProcessSection.vue'
|
2026-05-21 20:04:27 +08:00
|
|
|
import SummaryCard from './components/SummaryCard.vue'
|
|
|
|
|
import UnifiedInput from './components/UnifiedInput.vue'
|
2026-05-24 08:55:38 +08:00
|
|
|
import KbSelector from './components/KbSelector.vue'
|
|
|
|
|
import KbManager from './components/KbManager.vue'
|
|
|
|
|
import { useKbStore } from './stores/kb'
|
2026-05-21 20:04:27 +08:00
|
|
|
|
|
|
|
|
const chat = useChatStore()
|
|
|
|
|
const session = useSessionStore()
|
2026-05-24 08:55:38 +08:00
|
|
|
const kb = useKbStore()
|
|
|
|
|
|
|
|
|
|
function handleKbChange(kbId: string) {
|
|
|
|
|
if (session.currentId) {
|
|
|
|
|
kb.bindKbToSession(session.currentId, kbId)
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-05-21 20:04:27 +08:00
|
|
|
|
|
|
|
|
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) {
|
2026-05-21 23:43:21 +08:00
|
|
|
chat.addNode({ node: data.node, label: data.label, step_index: data.step_index })
|
2026-05-21 20:04:27 +08:00
|
|
|
},
|
|
|
|
|
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,
|
2026-05-23 15:09:55 +08:00
|
|
|
consult_answer: data.consult_answer,
|
2026-05-21 20:04:27 +08:00
|
|
|
retry_count: data.retry_count,
|
2026-05-21 23:43:21 +08:00
|
|
|
total_duration_ms: data.total_duration_ms,
|
2026-05-21 20:04:27 +08:00
|
|
|
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') {
|
2026-05-23 15:09:55 +08:00
|
|
|
// 咨询回答:优先用 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' })
|
2026-05-21 20:04:27 +08:00
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
if (streamContent) {
|
|
|
|
|
chat.addMessage({ role: 'assistant', content: streamContent, type: 'jrxml' })
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Refresh session sidebar data after a short delay
|
2026-05-22 11:13:25 +08:00
|
|
|
setTimeout(() => session.refreshFromApi(), 500)
|
2026-05-21 20:04:27 +08:00
|
|
|
},
|
|
|
|
|
onAgentError(data) {
|
|
|
|
|
chat.setError(data.error)
|
|
|
|
|
chat.addMessage({ role: 'assistant', content: `执行异常: ${data.error}`, type: 'error' })
|
2026-05-22 11:13:25 +08:00
|
|
|
setTimeout(() => session.refreshFromApi(), 500)
|
2026-05-21 20:04:27 +08:00
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
} catch (e: any) {
|
|
|
|
|
chat.setError(e.message || '网络请求失败')
|
|
|
|
|
chat.addMessage({ role: 'assistant', content: `请求失败: ${e.message}`, type: 'error' })
|
2026-05-23 09:08:53 +08:00
|
|
|
chat.finishStreaming({ status: '' })
|
|
|
|
|
} finally {
|
|
|
|
|
if (chat.streaming) {
|
|
|
|
|
chat.finishStreaming({ status: '' })
|
|
|
|
|
}
|
2026-05-21 20:04:27 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<template>
|
|
|
|
|
<div class="app-layout">
|
2026-05-21 23:54:57 +08:00
|
|
|
<Sidebar @quickAction="(text) => handleSend(text, [])" />
|
2026-05-21 20:04:27 +08:00
|
|
|
|
|
|
|
|
<main class="main-area">
|
2026-05-24 08:55:38 +08:00
|
|
|
<KbSelector @change="handleKbChange" />
|
2026-05-21 20:04:27 +08:00
|
|
|
<div class="chat-container" ref="chatContainer">
|
|
|
|
|
<ChatMessages />
|
2026-05-21 23:43:21 +08:00
|
|
|
<ProcessSection />
|
2026-05-21 20:04:27 +08:00
|
|
|
<SummaryCard />
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<UnifiedInput
|
|
|
|
|
:disabled="chat.streaming"
|
|
|
|
|
@send="handleSend"
|
|
|
|
|
/>
|
|
|
|
|
</main>
|
2026-05-24 08:55:38 +08:00
|
|
|
|
|
|
|
|
<KbManager />
|
2026-05-21 20:04:27 +08:00
|
|
|
</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>
|