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 <noreply@anthropic.com>
This commit is contained in:
@@ -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<Conversation[]>('/api/conversations')
|
||||
@@ -35,18 +77,78 @@ export const conversationApi = {
|
||||
},
|
||||
|
||||
getMessages(conversationId: string) {
|
||||
return api.get<Message[]>(`/api/conversations/${conversationId}`)
|
||||
return api.get<Message[]>(`/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 || '连接失败。请检查服务状态。')
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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<any[]>(`/api/documents/${id}/chunks`)
|
||||
return api.get<DocumentChunk[]>(`/api/documents/${id}/chunks`)
|
||||
},
|
||||
|
||||
updateChunk(documentId: string, chunkId: string, payload: DocumentChunkUpdate) {
|
||||
return api.put<DocumentChunk>(`/api/documents/${documentId}/chunks/${chunkId}`, payload)
|
||||
},
|
||||
|
||||
delete(id: string) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
14
frontend/src/api/system.ts
Normal file
14
frontend/src/api/system.ts
Normal file
@@ -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<SystemStatus>('/api/system/status')
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user