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 @@