feat: 前后端分离架构 — FastAPI SSE后端 + Vue 3前端
将单体 Streamlit 应用拆分为三层架构: - api_server.py: FastAPI SSE 流式后端 (端口 8000) - frontend/: Vue 3 + Vite + Pinia 聊天前端 (端口 5173) - agent/graph.py: 新增 node_start 回调支持 - 更新启动脚本为三服务模式 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,173 @@
|
||||
<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 StreamingMessage from './components/StreamingMessage.vue'
|
||||
import NodeProgress from './components/NodeProgress.vue'
|
||||
import SummaryCard from './components/SummaryCard.vue'
|
||||
import UnifiedInput from './components/UnifiedInput.vue'
|
||||
|
||||
const chat = useChatStore()
|
||||
const session = useSessionStore()
|
||||
|
||||
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(data)
|
||||
},
|
||||
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,
|
||||
retry_count: data.retry_count,
|
||||
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') {
|
||||
if (streamContent) {
|
||||
chat.addMessage({ role: 'assistant', content: streamContent, type: 'consult' })
|
||||
}
|
||||
} else {
|
||||
if (streamContent) {
|
||||
chat.addMessage({ role: 'assistant', content: streamContent, type: 'jrxml' })
|
||||
}
|
||||
}
|
||||
|
||||
// Refresh session sidebar data after a short delay
|
||||
setTimeout(() => session.refreshFromState({}), 500)
|
||||
},
|
||||
onAgentError(data) {
|
||||
chat.setError(data.error)
|
||||
chat.addMessage({ role: 'assistant', content: `执行异常: ${data.error}`, type: 'error' })
|
||||
},
|
||||
})
|
||||
} catch (e: any) {
|
||||
chat.setError(e.message || '网络请求失败')
|
||||
chat.addMessage({ role: 'assistant', content: `请求失败: ${e.message}`, type: 'error' })
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="app-layout">
|
||||
<Sidebar />
|
||||
|
||||
<main class="main-area">
|
||||
<div class="chat-container" ref="chatContainer">
|
||||
<ChatMessages />
|
||||
<StreamingMessage />
|
||||
<NodeProgress />
|
||||
<SummaryCard />
|
||||
</div>
|
||||
|
||||
<UnifiedInput
|
||||
:disabled="chat.streaming"
|
||||
@send="handleSend"
|
||||
/>
|
||||
</main>
|
||||
</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>
|
||||
Reference in New Issue
Block a user