From 7d80a6e2ec9d3adc7335a459da761ce3d486e982 Mon Sep 17 00:00:00 2001 From: "DESKTOP-72TV0V4\\caoxiaozhu" Date: Sun, 22 Mar 2026 13:48:16 +0800 Subject: [PATCH] Add brain and chat workspace views Expand the frontend with brain, graph, and chat workspace updates so the new backend orchestration and memory features have matching screens. These changes also wire the new APIs into routing and add focused view and routing tests. Co-Authored-By: Claude Opus 4.6 --- frontend/.env.example | 2 +- frontend/src/api/conversation.ts | 106 +- frontend/src/api/document.ts | 25 +- frontend/src/api/index.ts | 12 +- frontend/src/api/system.ts | 14 + frontend/src/app/navigation/nav.ts | 2 +- frontend/src/app/router/routes.ts | 5 + .../src/components/brain/GraphProjection.vue | 500 +++++++++ .../components/chat/OrchestrationPanel.vue | 555 ++++++++++ .../components/chat/TelemetrySparkline.vue | 78 ++ frontend/src/pages/app/layout.vue | 16 +- frontend/src/pages/brain/brainEmbed.test.ts | 14 + frontend/src/pages/brain/brainRouting.test.ts | 18 + frontend/src/pages/brain/index.vue | 7 + .../chat/composables/useChatView.test.ts | 168 ++++ .../src/pages/chat/composables/useChatView.ts | 394 +++++++- frontend/src/pages/chat/index.vue | 947 +++++++++++++++--- frontend/src/pages/graph/index.vue | 465 +-------- frontend/src/pages/knowledge/index.vue | 366 ++++++- frontend/src/stores/auth.ts | 52 +- frontend/src/stores/conversation.ts | 7 +- 21 files changed, 3095 insertions(+), 658 deletions(-) create mode 100644 frontend/src/api/system.ts create mode 100644 frontend/src/components/brain/GraphProjection.vue create mode 100644 frontend/src/components/chat/OrchestrationPanel.vue create mode 100644 frontend/src/components/chat/TelemetrySparkline.vue create mode 100644 frontend/src/pages/brain/brainEmbed.test.ts create mode 100644 frontend/src/pages/brain/brainRouting.test.ts create mode 100644 frontend/src/pages/brain/index.vue create mode 100644 frontend/src/pages/chat/composables/useChatView.test.ts diff --git a/frontend/.env.example b/frontend/.env.example index 5934e2e..ecc517b 100644 --- a/frontend/.env.example +++ b/frontend/.env.example @@ -1 +1 @@ -VITE_API_URL=http://localhost:8000 +VITE_API_URL=http://localhost:9528 diff --git a/frontend/src/api/conversation.ts b/frontend/src/api/conversation.ts index 6815826..41acf20 100644 --- a/frontend/src/api/conversation.ts +++ b/frontend/src/api/conversation.ts @@ -1,5 +1,30 @@ import api from './index' +export interface ChatProgressEvent { + stage: 'thinking' | 'planning' | 'tool' | 'responding' + label: string + agent?: string | null + tool_name?: string | null + step?: string | null + steps?: string[] +} + +export interface ChatStreamChunkEvent { + content: string +} + +export interface ChatStreamMetadataEvent { + conversation_id: string + message_id: string +} + +export interface ChatStreamHandlers { + onMetadata?: (payload: ChatStreamMetadataEvent) => void + onProgress?: (payload: ChatProgressEvent) => void + onChunk?: (payload: ChatStreamChunkEvent) => void + onError?: (message: string) => void +} + export interface MessageAttachment { id: string name: string @@ -25,6 +50,23 @@ export interface Conversation { updated_at: string } +function parseSseBlocks(buffer: string) { + const chunks = buffer.split('\n\n') + const rest = chunks.pop() ?? '' + const events = chunks + .map((block) => { + const lines = block.split('\n') + const event = lines.find((line) => line.startsWith('event:'))?.slice(6).trim() || 'message' + const data = lines + .filter((line) => line.startsWith('data:')) + .map((line) => line.slice(5).trim()) + .join('\n') + return { event, data } + }) + .filter((item) => item.data) + return { events, rest } +} + export const conversationApi = { list() { return api.get('/api/conversations') @@ -35,18 +77,78 @@ export const conversationApi = { }, getMessages(conversationId: string) { - return api.get(`/api/conversations/${conversationId}`) + return api.get(`/api/conversations/${conversationId}/messages`) }, delete(conversationId: string) { return api.delete(`/api/conversations/${conversationId}`) }, - chat(message: string, conversationId?: string, fileIds: string[] = []) { + chat(message: string, conversationId?: string, fileIds: string[] = [], modelName?: string) { return api.post('/api/conversations/chat', { message, conversation_id: conversationId, file_ids: fileIds, + model_name: modelName, }) }, + + async chatStream( + message: string, + conversationId?: string, + fileIds: string[] = [], + modelName?: string, + handlers: ChatStreamHandlers = {}, + ) { + const token = localStorage.getItem('access_token') + const response = await fetch(`${import.meta.env.VITE_API_URL}/api/conversations/chat/stream`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + ...(token ? { Authorization: `Bearer ${token}` } : {}), + }, + body: JSON.stringify({ + message, + conversation_id: conversationId, + file_ids: fileIds, + model_name: modelName, + }), + }) + + if (!response.ok || !response.body) { + let messageText = '连接失败。请检查服务状态。' + try { + const payload = await response.json() + messageText = payload?.detail || payload?.error || messageText + } catch { + // ignore parse error + } + throw new Error(messageText) + } + + const reader = response.body.getReader() + const decoder = new TextDecoder('utf-8') + let buffer = '' + + while (true) { + const { done, value } = await reader.read() + if (done) break + buffer += decoder.decode(value, { stream: true }) + const { events, rest } = parseSseBlocks(buffer) + buffer = rest + + for (const item of events) { + const payload = JSON.parse(item.data) + if (item.event === 'metadata') { + handlers.onMetadata?.(payload) + } else if (item.event === 'progress') { + handlers.onProgress?.(payload) + } else if (item.event === 'chunk') { + handlers.onChunk?.(payload) + } else if (item.event === 'error') { + handlers.onError?.(payload.error || '连接失败。请检查服务状态。') + } + } + } + }, } diff --git a/frontend/src/api/document.ts b/frontend/src/api/document.ts index 50bf5a3..4b6ca82 100644 --- a/frontend/src/api/document.ts +++ b/frontend/src/api/document.ts @@ -9,10 +9,26 @@ export interface Document { summary?: string chunk_count: number is_indexed: boolean + ingestion_status?: 'uploaded' | 'parsing' | 'indexing' | 'ready' | 'warning' | 'failed' + ingestion_error?: string | null + indexed_at?: string | null + parser_version?: string | null + index_version?: string | null folder_id?: string | null created_at: string } +export interface DocumentChunk { + id: string + chunk_index: number + content: string + metadata_?: string | null +} + +export interface DocumentChunkUpdate { + content: string +} + export interface SearchResult { chunk_id: string document_id: string @@ -29,6 +45,9 @@ export interface UploadResponse { title: string chunk_count: number status: string + ingestion_status?: 'uploaded' | 'parsing' | 'indexing' | 'ready' | 'warning' | 'failed' + ingestion_error?: string | null + indexed_at?: string | null } export const documentApi = { @@ -54,7 +73,11 @@ export const documentApi = { }, getChunks(id: string) { - return api.get(`/api/documents/${id}/chunks`) + return api.get(`/api/documents/${id}/chunks`) + }, + + updateChunk(documentId: string, chunkId: string, payload: DocumentChunkUpdate) { + return api.put(`/api/documents/${documentId}/chunks/${chunkId}`, payload) }, delete(id: string) { diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts index 1dbe877..0068f1c 100644 --- a/frontend/src/api/index.ts +++ b/frontend/src/api/index.ts @@ -1,7 +1,9 @@ import axios from 'axios' +let redirectingToLogin = false + const api = axios.create({ - baseURL: import.meta.env.VITE_API_URL || 'http://localhost:9527', + baseURL: import.meta.env.VITE_API_URL, timeout: 30000, }) @@ -63,7 +65,13 @@ api.interceptors.response.use( const requestId = error.response?.headers?.['x-request-id'] || metadata?.requestId if (error.response?.status === 401) { localStorage.removeItem('access_token') - window.location.href = '/login' + if (typeof window !== 'undefined') { + window.dispatchEvent(new CustomEvent('jarvis:auth-unauthorized')) + if (!redirectingToLogin && window.location.pathname !== '/login') { + redirectingToLogin = true + window.location.href = '/login' + } + } } debugLog('error', { requestId, diff --git a/frontend/src/api/system.ts b/frontend/src/api/system.ts new file mode 100644 index 0000000..b2ff5e5 --- /dev/null +++ b/frontend/src/api/system.ts @@ -0,0 +1,14 @@ +import api from './index' + +export interface SystemStatus { + cpu_percent: number + memory_percent: number + disk_percent: number + timestamp: string +} + +export const systemApi = { + getStatus() { + return api.get('/api/system/status') + }, +} diff --git a/frontend/src/app/navigation/nav.ts b/frontend/src/app/navigation/nav.ts index fe08c01..bfa3a0c 100644 --- a/frontend/src/app/navigation/nav.ts +++ b/frontend/src/app/navigation/nav.ts @@ -24,7 +24,7 @@ export const navItems: NavItem[] = [ { name: '智能链路', path: '/agents', icon: Bot }, { name: '技能中心', path: '/skills', icon: Star }, { name: '资料中枢', path: '/knowledge', icon: BookOpen }, - { name: '知识大脑', path: '/graph', icon: Network }, + { name: '知识大脑', path: '/brain', icon: Network }, { name: '任务矩阵', path: '/kanban', icon: LayoutGrid }, { name: '任务调度', path: '/todo', icon: CheckSquare }, { name: '信息交易所', path: '/forum', icon: MessageSquare }, diff --git a/frontend/src/app/router/routes.ts b/frontend/src/app/router/routes.ts index 8fb4bbd..e2eadfd 100644 --- a/frontend/src/app/router/routes.ts +++ b/frontend/src/app/router/routes.ts @@ -15,6 +15,11 @@ const appChildren: RouteRecordRaw[] = [ name: 'knowledge', component: () => import('@/pages/knowledge/index.vue'), }, + { + path: 'brain', + name: 'brain', + component: () => import('@/pages/brain/index.vue'), + }, { path: 'graph', name: 'graph', diff --git a/frontend/src/components/brain/GraphProjection.vue b/frontend/src/components/brain/GraphProjection.vue new file mode 100644 index 0000000..7b4dd70 --- /dev/null +++ b/frontend/src/components/brain/GraphProjection.vue @@ -0,0 +1,500 @@ + + + + + diff --git a/frontend/src/components/chat/OrchestrationPanel.vue b/frontend/src/components/chat/OrchestrationPanel.vue new file mode 100644 index 0000000..04e1db6 --- /dev/null +++ b/frontend/src/components/chat/OrchestrationPanel.vue @@ -0,0 +1,555 @@ + + + + + diff --git a/frontend/src/components/chat/TelemetrySparkline.vue b/frontend/src/components/chat/TelemetrySparkline.vue new file mode 100644 index 0000000..9bba954 --- /dev/null +++ b/frontend/src/components/chat/TelemetrySparkline.vue @@ -0,0 +1,78 @@ + + + + + diff --git a/frontend/src/pages/app/layout.vue b/frontend/src/pages/app/layout.vue index bcc4d6f..c6bccf8 100644 --- a/frontend/src/pages/app/layout.vue +++ b/frontend/src/pages/app/layout.vue @@ -1,6 +1,20 @@