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:
168
frontend/src/pages/chat/composables/useChatView.test.ts
Normal file
168
frontend/src/pages/chat/composables/useChatView.test.ts
Normal file
@@ -0,0 +1,168 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { nextTick } from 'vue'
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
chatStream: vi.fn(),
|
||||
list: vi.fn(),
|
||||
getMessages: vi.fn(),
|
||||
deleteConversation: vi.fn(),
|
||||
settingsGet: vi.fn(),
|
||||
upload: vi.fn(),
|
||||
systemStatusGet: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/api/conversation', () => ({
|
||||
conversationApi: {
|
||||
chatStream: mocks.chatStream,
|
||||
list: mocks.list,
|
||||
getMessages: mocks.getMessages,
|
||||
delete: mocks.deleteConversation,
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@/api/settings', () => ({
|
||||
settingsApi: {
|
||||
get: mocks.settingsGet,
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@/api/system', () => ({
|
||||
systemApi: {
|
||||
getStatus: mocks.systemStatusGet,
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@/api/document', () => ({
|
||||
documentApi: {
|
||||
upload: mocks.upload,
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/auth', () => ({
|
||||
useAuthStore: () => ({
|
||||
ensureAuthReady: vi.fn().mockResolvedValue(undefined),
|
||||
isAuthenticated: true,
|
||||
user: { id: 'user-1', email: 'test@example.com' },
|
||||
}),
|
||||
}))
|
||||
|
||||
import { useChatView } from './useChatView'
|
||||
|
||||
describe('useChatView orchestration state', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia())
|
||||
vi.useFakeTimers()
|
||||
vi.clearAllMocks()
|
||||
mocks.list.mockResolvedValue({ data: [] })
|
||||
mocks.getMessages.mockResolvedValue({ data: [] })
|
||||
mocks.deleteConversation.mockResolvedValue(undefined)
|
||||
mocks.settingsGet.mockResolvedValue({
|
||||
data: {
|
||||
llm_config: {
|
||||
chat: [
|
||||
{
|
||||
name: 'Jarvis Chat',
|
||||
provider: 'openai',
|
||||
model: 'gpt-test',
|
||||
base_url: '',
|
||||
api_key: '',
|
||||
enabled: true,
|
||||
},
|
||||
],
|
||||
vlm: [],
|
||||
},
|
||||
},
|
||||
})
|
||||
mocks.upload.mockResolvedValue({ data: { id: 'file-1' } })
|
||||
mocks.systemStatusGet.mockResolvedValue({
|
||||
data: {
|
||||
cpu_percent: 21,
|
||||
memory_percent: 48,
|
||||
disk_percent: 63,
|
||||
timestamp: '2026-03-22T02:40:00Z',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('derives telemetry state from real system status and session events without persisting it to message history', async () => {
|
||||
mocks.chatStream.mockImplementation(async (_message, _conversationId, _fileIds, _modelName, handlers) => {
|
||||
handlers.onMetadata?.({ conversation_id: 'conv-1', message_id: 'msg-1' })
|
||||
handlers.onProgress?.({
|
||||
stage: 'tool',
|
||||
label: 'Jarvis 正在调用工具',
|
||||
agent: 'executor',
|
||||
tool_name: 'search_knowledge',
|
||||
step: '调用工具 search_knowledge',
|
||||
steps: ['理解问题', '检索知识'],
|
||||
})
|
||||
handlers.onChunk?.({ content: '最终回复' })
|
||||
})
|
||||
|
||||
const view = useChatView()
|
||||
await nextTick()
|
||||
await Promise.resolve()
|
||||
|
||||
expect(mocks.systemStatusGet).toHaveBeenCalledTimes(1)
|
||||
expect(view.systemTelemetry.value.cpu.current).toBe(21)
|
||||
expect(view.systemTelemetry.value.memory.current).toBe(48)
|
||||
expect(view.systemTelemetry.value.disk.current).toBe(63)
|
||||
expect(view.systemTelemetry.value.cpu.series.at(-1)).toBe(21)
|
||||
|
||||
view.inputMessage.value = '测试问题'
|
||||
const promise = view.sendMessage()
|
||||
await Promise.resolve()
|
||||
|
||||
expect(view.sessionTelemetry.value.eventsCount).toBe(2)
|
||||
expect(view.sessionTelemetry.value.toolCount).toBe(1)
|
||||
expect(view.sessionTelemetry.value.agentCount).toBe(1)
|
||||
expect(view.sessionTelemetry.value.activitySeries.some((point) => point > 0)).toBe(true)
|
||||
expect(view.store.messages).toHaveLength(1)
|
||||
|
||||
await promise
|
||||
|
||||
expect(view.store.messages).toHaveLength(2)
|
||||
expect(view.store.messages.every((message) => !('activitySeries' in message))).toBe(true)
|
||||
})
|
||||
|
||||
it('keeps the orchestration panel persistent and confines thinking state to the side panel', async () => {
|
||||
mocks.chatStream.mockImplementation(async (_message, _conversationId, _fileIds, _modelName, handlers) => {
|
||||
handlers.onMetadata?.({ conversation_id: 'conv-1', message_id: 'msg-1' })
|
||||
handlers.onProgress?.({
|
||||
stage: 'planning',
|
||||
label: 'Jarvis 正在拆解步骤',
|
||||
agent: 'planner',
|
||||
tool_name: null,
|
||||
step: '正在分配任务',
|
||||
steps: ['理解问题', '分配 planner'],
|
||||
})
|
||||
handlers.onChunk?.({ content: '最终回复' })
|
||||
})
|
||||
|
||||
const view = useChatView()
|
||||
view.inputMessage.value = '测试问题'
|
||||
|
||||
const promise = view.sendMessage()
|
||||
await Promise.resolve()
|
||||
|
||||
expect(view.store.messages).toHaveLength(1)
|
||||
expect(view.store.messages[0].role).toBe('user')
|
||||
expect(view.isTyping.value).toBe(true)
|
||||
expect(view.orchestrationPanelVisible.value).toBe(true)
|
||||
expect(view.orchestrationStatus.value).toBe('active')
|
||||
expect(view.activeAgent.value).toBe('planner')
|
||||
expect(view.visitedAgents.value).toContain('planner')
|
||||
expect(view.orchestrationEventFeed.value.map((item) => item.label)).toContain('正在分配任务')
|
||||
|
||||
await promise
|
||||
|
||||
expect(view.store.messages).toHaveLength(2)
|
||||
expect(view.store.messages[1].role).toBe('assistant')
|
||||
expect(view.store.messages[1].content).toBe('最终回复')
|
||||
expect(view.orchestrationPanelVisible.value).toBe(true)
|
||||
expect(view.orchestrationStatus.value).toBe('complete')
|
||||
expect(view.orchestrationEventFeed.value.at(-1)?.label).toBe('响应已生成')
|
||||
expect(view.activeAgent.value).toBe('planner')
|
||||
expect(view.store.messages).toHaveLength(2)
|
||||
})
|
||||
})
|
||||
@@ -1,7 +1,14 @@
|
||||
import { nextTick, onMounted, ref } from 'vue'
|
||||
import { computed, nextTick, onMounted, onUnmounted, ref } from 'vue'
|
||||
import { useConversationStore } from '@/stores/conversation'
|
||||
import { conversationApi, type Message } from '@/api/conversation'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import {
|
||||
conversationApi,
|
||||
type ChatProgressEvent,
|
||||
type Message,
|
||||
} from '@/api/conversation'
|
||||
import { documentApi } from '@/api/document'
|
||||
import { settingsApi, type LLMModelConfig } from '@/api/settings'
|
||||
import { systemApi } from '@/api/system'
|
||||
|
||||
export interface SelectedFile {
|
||||
id: string
|
||||
@@ -14,8 +21,51 @@ interface MessageWithAttachments extends Message {
|
||||
attachments?: SelectedFile[]
|
||||
}
|
||||
|
||||
interface ThinkingState {
|
||||
stage: ChatProgressEvent['stage']
|
||||
label: string
|
||||
agent?: string | null
|
||||
toolName?: string | null
|
||||
step?: string | null
|
||||
steps: string[]
|
||||
}
|
||||
|
||||
interface OrchestrationEventItem {
|
||||
id: string
|
||||
label: string
|
||||
kind: 'info' | 'tool' | 'success' | 'error'
|
||||
}
|
||||
|
||||
interface OrchestrationInsight {
|
||||
statusTitle: string
|
||||
systemSummary: string
|
||||
jarvisNote: string
|
||||
}
|
||||
|
||||
interface TelemetryMetricState {
|
||||
current: number | null
|
||||
series: number[]
|
||||
online: boolean
|
||||
}
|
||||
|
||||
interface SystemTelemetryState {
|
||||
cpu: TelemetryMetricState
|
||||
memory: TelemetryMetricState
|
||||
disk: TelemetryMetricState
|
||||
}
|
||||
|
||||
interface SessionTelemetryState {
|
||||
activitySeries: number[]
|
||||
eventsCount: number
|
||||
toolCount: number
|
||||
agentCount: number
|
||||
}
|
||||
|
||||
type OrchestrationStatus = 'idle' | 'active' | 'complete' | 'error'
|
||||
|
||||
export function useChatView() {
|
||||
const store = useConversationStore()
|
||||
const auth = useAuthStore()
|
||||
const inputMessage = ref('')
|
||||
const isSending = ref(false)
|
||||
const chatContainer = ref<HTMLElement>()
|
||||
@@ -24,18 +74,222 @@ export function useChatView() {
|
||||
const fileInputRef = ref<HTMLInputElement>()
|
||||
const showEmojiPicker = ref(false)
|
||||
const selectedFiles = ref<SelectedFile[]>([])
|
||||
const chatModels = ref<LLMModelConfig[]>([])
|
||||
const selectedModelName = ref('')
|
||||
const isLoadingModels = ref(false)
|
||||
const conversationsError = ref('')
|
||||
const currentSelectionRequestId = ref(0)
|
||||
const thinkingState = ref<ThinkingState | null>(null)
|
||||
const orchestrationPanelVisible = ref(true)
|
||||
const orchestrationStatus = ref<OrchestrationStatus>('idle')
|
||||
const orchestrationInsight = ref<OrchestrationInsight>({
|
||||
statusTitle: 'STANDBY',
|
||||
systemSummary: '等待请求接入',
|
||||
jarvisNote: '系统在线。希望下一项任务别太无聊。',
|
||||
})
|
||||
const activeAgent = ref<string | null>(null)
|
||||
const visitedAgents = ref<string[]>([])
|
||||
const orchestrationEventFeed = ref<OrchestrationEventItem[]>([])
|
||||
const systemTelemetry = ref<SystemTelemetryState>({
|
||||
cpu: { current: null, series: [], online: false },
|
||||
memory: { current: null, series: [], online: false },
|
||||
disk: { current: null, series: [], online: false },
|
||||
})
|
||||
const sessionTelemetry = ref<SessionTelemetryState>({
|
||||
activitySeries: [],
|
||||
eventsCount: 0,
|
||||
toolCount: 0,
|
||||
agentCount: 0,
|
||||
})
|
||||
const selectedModel = computed(() => chatModels.value.find((model) => model.name === selectedModelName.value) ?? null)
|
||||
let systemTelemetryTimer: ReturnType<typeof setInterval> | null = null
|
||||
let sessionTelemetryTimer: ReturnType<typeof setInterval> | null = null
|
||||
|
||||
function resetOrchestrationState() {
|
||||
orchestrationPanelVisible.value = true
|
||||
orchestrationStatus.value = 'idle'
|
||||
orchestrationInsight.value = {
|
||||
statusTitle: 'STANDBY',
|
||||
systemSummary: '等待请求接入',
|
||||
jarvisNote: '系统在线。希望下一项任务别太无聊。',
|
||||
}
|
||||
activeAgent.value = null
|
||||
visitedAgents.value = []
|
||||
orchestrationEventFeed.value = []
|
||||
sessionTelemetry.value = {
|
||||
activitySeries: [],
|
||||
eventsCount: 0,
|
||||
toolCount: 0,
|
||||
agentCount: 0,
|
||||
}
|
||||
}
|
||||
|
||||
function appendTelemetryPoint(series: number[], value: number, limit = 24) {
|
||||
return [...series, value].slice(-limit)
|
||||
}
|
||||
|
||||
function markSessionActivity(value: number, options?: { tool?: boolean; agent?: string | null }) {
|
||||
sessionTelemetry.value = {
|
||||
activitySeries: appendTelemetryPoint(sessionTelemetry.value.activitySeries, value),
|
||||
eventsCount: sessionTelemetry.value.eventsCount + 1,
|
||||
toolCount: sessionTelemetry.value.toolCount + (options?.tool ? 1 : 0),
|
||||
agentCount: options?.agent && !visitedAgents.value.includes(options.agent)
|
||||
? sessionTelemetry.value.agentCount + 1
|
||||
: sessionTelemetry.value.agentCount,
|
||||
}
|
||||
}
|
||||
|
||||
function updateSystemTelemetry(metric: keyof SystemTelemetryState, value: number | null, online: boolean) {
|
||||
const current = systemTelemetry.value[metric]
|
||||
systemTelemetry.value = {
|
||||
...systemTelemetry.value,
|
||||
[metric]: {
|
||||
current: value,
|
||||
online,
|
||||
series: value === null ? current.series : appendTelemetryPoint(current.series, value),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
async function loadSystemStatus() {
|
||||
try {
|
||||
const response = await systemApi.getStatus()
|
||||
updateSystemTelemetry('cpu', response.data.cpu_percent, true)
|
||||
updateSystemTelemetry('memory', response.data.memory_percent, true)
|
||||
updateSystemTelemetry('disk', response.data.disk_percent, true)
|
||||
} catch (error) {
|
||||
console.error('加载系统状态失败:', error)
|
||||
updateSystemTelemetry('cpu', systemTelemetry.value.cpu.current, false)
|
||||
updateSystemTelemetry('memory', systemTelemetry.value.memory.current, false)
|
||||
updateSystemTelemetry('disk', systemTelemetry.value.disk.current, false)
|
||||
}
|
||||
}
|
||||
|
||||
function startSystemTelemetryPolling() {
|
||||
if (systemTelemetryTimer) clearInterval(systemTelemetryTimer)
|
||||
systemTelemetryTimer = setInterval(() => {
|
||||
void loadSystemStatus()
|
||||
}, 2000)
|
||||
}
|
||||
|
||||
function startSessionTelemetryDecay() {
|
||||
if (sessionTelemetryTimer) clearInterval(sessionTelemetryTimer)
|
||||
sessionTelemetryTimer = setInterval(() => {
|
||||
const lastValue = sessionTelemetry.value.activitySeries.at(-1) ?? 0
|
||||
const nextValue = Math.max(0, Math.round(lastValue * 0.72) - 2)
|
||||
sessionTelemetry.value = {
|
||||
...sessionTelemetry.value,
|
||||
activitySeries: appendTelemetryPoint(sessionTelemetry.value.activitySeries, nextValue),
|
||||
}
|
||||
}, 1200)
|
||||
}
|
||||
|
||||
function pushOrchestrationEvent(label: string, kind: OrchestrationEventItem['kind']) {
|
||||
const normalized = label.trim()
|
||||
if (!normalized) return
|
||||
if (orchestrationEventFeed.value.at(-1)?.label === normalized) return
|
||||
orchestrationEventFeed.value = [
|
||||
...orchestrationEventFeed.value,
|
||||
{
|
||||
id: `${Date.now()}-${orchestrationEventFeed.value.length}`,
|
||||
label: normalized,
|
||||
kind,
|
||||
},
|
||||
].slice(-5)
|
||||
}
|
||||
|
||||
function buildOrchestrationInsight(payload: ChatProgressEvent): OrchestrationInsight {
|
||||
if (payload.stage === 'thinking') {
|
||||
return {
|
||||
statusTitle: 'ANALYSIS',
|
||||
systemSummary: payload.agent ? '正在解析请求并评估处理路径' : '正在解析请求意图',
|
||||
jarvisNote: payload.steps?.length
|
||||
? '这件事比表面复杂一点,我宁可先把结构看清。'
|
||||
: '这个请求不算棘手,先让我确认范围。',
|
||||
}
|
||||
}
|
||||
|
||||
if (payload.stage === 'planning') {
|
||||
return {
|
||||
statusTitle: 'ROUTING',
|
||||
systemSummary: payload.agent === 'planner' ? '已路由至 planner,正在拆解任务' : '正在规划执行链路',
|
||||
jarvisNote: payload.steps?.length
|
||||
? '问题有几层关系,按顺序拆开会体面很多。'
|
||||
: '这一步需要一点秩序感。',
|
||||
}
|
||||
}
|
||||
|
||||
if (payload.stage === 'tool') {
|
||||
return {
|
||||
statusTitle: 'EXECUTION',
|
||||
systemSummary: payload.tool_name ? `正在调用工具 · ${payload.tool_name}` : '正在执行操作',
|
||||
jarvisNote: payload.tool_name
|
||||
? '工具链已接通。希望它今天愿意配合。'
|
||||
: '执行阶段开始了,接下来看看链路表现。',
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
statusTitle: 'SYNTHESIS',
|
||||
systemSummary: payload.agent === 'analyst' ? 'analyst 正在整理结果' : '结果已收集,正在整理回答',
|
||||
jarvisNote: '信息已经够用了,我来把它变得清晰一点。',
|
||||
}
|
||||
}
|
||||
|
||||
function applyProgressToOrchestration(payload: ChatProgressEvent) {
|
||||
orchestrationPanelVisible.value = true
|
||||
orchestrationStatus.value = 'active'
|
||||
orchestrationInsight.value = buildOrchestrationInsight(payload)
|
||||
const isNewAgent = Boolean(payload.agent && !visitedAgents.value.includes(payload.agent))
|
||||
activeAgent.value = payload.agent || null
|
||||
markSessionActivity(payload.tool_name ? 92 : 68, {
|
||||
tool: Boolean(payload.tool_name),
|
||||
agent: isNewAgent ? payload.agent || null : null,
|
||||
})
|
||||
if (payload.agent && isNewAgent) {
|
||||
visitedAgents.value = [...visitedAgents.value, payload.agent]
|
||||
}
|
||||
|
||||
if (payload.step) {
|
||||
pushOrchestrationEvent(payload.step, payload.tool_name ? 'tool' : 'info')
|
||||
} else if (payload.tool_name) {
|
||||
pushOrchestrationEvent(`调用工具 · ${payload.tool_name}`, 'tool')
|
||||
} else {
|
||||
pushOrchestrationEvent(payload.label, 'info')
|
||||
}
|
||||
}
|
||||
|
||||
function finalizeOrchestration(status: Exclude<OrchestrationStatus, 'idle' | 'active'>, finalLabel: string) {
|
||||
orchestrationPanelVisible.value = true
|
||||
orchestrationStatus.value = status
|
||||
orchestrationInsight.value = status === 'error'
|
||||
? {
|
||||
statusTitle: 'ERROR',
|
||||
systemSummary: '执行中断,等待进一步处理',
|
||||
jarvisNote: '结果不理想,不过问题已经开始显形。',
|
||||
}
|
||||
: {
|
||||
statusTitle: 'COMPLETE',
|
||||
systemSummary: '执行完成,结果已生成',
|
||||
jarvisNote: '很好,问题已经收束。',
|
||||
}
|
||||
pushOrchestrationEvent(finalLabel, status === 'error' ? 'error' : 'success')
|
||||
}
|
||||
|
||||
async function sendMessage() {
|
||||
if (!inputMessage.value.trim() || isSending.value) return
|
||||
|
||||
resetOrchestrationState()
|
||||
isSending.value = true
|
||||
isTyping.value = true
|
||||
const text = inputMessage.value.trim()
|
||||
const attachments = [...selectedFiles.value]
|
||||
const tempMessageId = `temp-${Date.now()}`
|
||||
const previousConversationId = store.currentConversationId
|
||||
inputMessage.value = ''
|
||||
|
||||
store.addMessage({
|
||||
id: `temp-${Date.now()}`,
|
||||
id: tempMessageId,
|
||||
role: 'user',
|
||||
content: text,
|
||||
created_at: new Date().toISOString(),
|
||||
@@ -45,28 +299,74 @@ export function useChatView() {
|
||||
await nextTick()
|
||||
scrollToBottom()
|
||||
|
||||
let finalConversationId = previousConversationId
|
||||
let finalMessageId = ''
|
||||
let finalContent = ''
|
||||
let streamError = ''
|
||||
|
||||
try {
|
||||
const response = await conversationApi.chat(text, store.currentConversationId || undefined, attachments.map((file) => file.id))
|
||||
await conversationApi.chatStream(
|
||||
text,
|
||||
previousConversationId ?? undefined,
|
||||
attachments.map((file) => file.id),
|
||||
selectedModelName.value || undefined,
|
||||
{
|
||||
onMetadata(payload) {
|
||||
finalConversationId = payload.conversation_id
|
||||
finalMessageId = payload.message_id
|
||||
if (previousConversationId === null) {
|
||||
store.setCurrentConversation(payload.conversation_id)
|
||||
}
|
||||
},
|
||||
onProgress(payload) {
|
||||
thinkingState.value = {
|
||||
stage: payload.stage,
|
||||
label: payload.label,
|
||||
agent: payload.agent,
|
||||
toolName: payload.tool_name,
|
||||
step: payload.step,
|
||||
steps: payload.steps || [],
|
||||
}
|
||||
applyProgressToOrchestration(payload)
|
||||
},
|
||||
onChunk(payload) {
|
||||
finalContent += payload.content
|
||||
markSessionActivity(54)
|
||||
},
|
||||
onError(message) {
|
||||
streamError = message
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
if (streamError) {
|
||||
throw new Error(streamError)
|
||||
}
|
||||
|
||||
selectedFiles.value = []
|
||||
isTyping.value = false
|
||||
store.addMessage({
|
||||
id: response.data.message_id,
|
||||
id: finalMessageId || `assistant-${Date.now()}`,
|
||||
role: 'assistant',
|
||||
content: response.data.content,
|
||||
model: response.data.agent_name,
|
||||
content: finalContent || '抱歉,我暂时没有生成可用回复。',
|
||||
model: selectedModelName.value || selectedModel.value?.name || 'jarvis',
|
||||
created_at: new Date().toISOString(),
|
||||
})
|
||||
if (!store.currentConversationId) {
|
||||
store.setCurrentConversation(response.data.conversation_id)
|
||||
await loadConversations()
|
||||
if (finalConversationId && previousConversationId === null) {
|
||||
store.setCurrentConversation(finalConversationId)
|
||||
}
|
||||
} catch (error) {
|
||||
finalizeOrchestration('complete', '响应已生成')
|
||||
await loadConversations()
|
||||
} catch (error: any) {
|
||||
isTyping.value = false
|
||||
console.error('发送失败:', error)
|
||||
const content = error?.message || '抱歉,连接失败。请检查服务状态。'
|
||||
finalizeOrchestration('error', content)
|
||||
store.removeMessage(tempMessageId)
|
||||
store.addMessage({
|
||||
id: `err-${Date.now()}`,
|
||||
role: 'assistant',
|
||||
content: '抱歉,连接失败。请检查服务状态。',
|
||||
content,
|
||||
created_at: new Date().toISOString(),
|
||||
})
|
||||
}
|
||||
@@ -77,29 +377,65 @@ export function useChatView() {
|
||||
}
|
||||
|
||||
async function loadConversations() {
|
||||
conversationsError.value = ''
|
||||
try {
|
||||
const response = await conversationApi.list()
|
||||
store.setConversations(response.data)
|
||||
} catch (error) {
|
||||
conversationsError.value = '加载会话失败,请稍后重试'
|
||||
console.error('加载对话列表失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
async function loadChatModels() {
|
||||
isLoadingModels.value = true
|
||||
try {
|
||||
const response = await settingsApi.get()
|
||||
const chatModelsList = (response.data.llm_config?.chat || []).filter((model) => model.enabled)
|
||||
const vlmModels = (response.data.llm_config?.vlm || []).filter((model) => model.enabled)
|
||||
// 合并 chat 和 vlm 模型
|
||||
chatModels.value = [...chatModelsList, ...vlmModels]
|
||||
if (!selectedModelName.value || !chatModels.value.some((model) => model.name === selectedModelName.value)) {
|
||||
selectedModelName.value = chatModels.value[0]?.name || ''
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载聊天模型失败:', error)
|
||||
chatModels.value = []
|
||||
selectedModelName.value = ''
|
||||
} finally {
|
||||
isLoadingModels.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function selectConversation(id: string) {
|
||||
resetOrchestrationState()
|
||||
thinkingState.value = null
|
||||
isTyping.value = false
|
||||
const requestId = currentSelectionRequestId.value + 1
|
||||
currentSelectionRequestId.value = requestId
|
||||
store.setCurrentConversation(id)
|
||||
store.setMessages([])
|
||||
try {
|
||||
const response = await conversationApi.getMessages(id)
|
||||
if (currentSelectionRequestId.value !== requestId || store.currentConversationId !== id) {
|
||||
return
|
||||
}
|
||||
store.setMessages(response.data)
|
||||
await nextTick()
|
||||
scrollToBottom()
|
||||
} catch (error) {
|
||||
if (currentSelectionRequestId.value === requestId) {
|
||||
store.setMessages([])
|
||||
}
|
||||
console.error('加载消息失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
function newConversation() {
|
||||
store.setCurrentConversation('')
|
||||
resetOrchestrationState()
|
||||
thinkingState.value = null
|
||||
isTyping.value = false
|
||||
store.setCurrentConversation(null)
|
||||
store.setMessages([])
|
||||
inputRef.value?.focus()
|
||||
}
|
||||
@@ -110,7 +446,7 @@ export function useChatView() {
|
||||
await conversationApi.delete(id)
|
||||
store.removeConversation(id)
|
||||
if (store.currentConversationId === id) {
|
||||
store.setCurrentConversation('')
|
||||
store.setCurrentConversation(null)
|
||||
store.setMessages([])
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -181,11 +517,23 @@ export function useChatView() {
|
||||
fileInputRef.value?.click()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadConversations()
|
||||
void loadSystemStatus()
|
||||
startSystemTelemetryPolling()
|
||||
startSessionTelemetryDecay()
|
||||
|
||||
onMounted(async () => {
|
||||
await auth.ensureAuthReady()
|
||||
if (auth.isAuthenticated && auth.user) {
|
||||
await Promise.all([loadConversations(), loadChatModels(), loadSystemStatus()])
|
||||
}
|
||||
inputRef.value?.focus()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (systemTelemetryTimer) clearInterval(systemTelemetryTimer)
|
||||
if (sessionTelemetryTimer) clearInterval(sessionTelemetryTimer)
|
||||
})
|
||||
|
||||
return {
|
||||
store,
|
||||
inputMessage,
|
||||
@@ -196,6 +544,20 @@ export function useChatView() {
|
||||
fileInputRef,
|
||||
showEmojiPicker,
|
||||
selectedFiles,
|
||||
chatModels,
|
||||
selectedModelName,
|
||||
selectedModel,
|
||||
isLoadingModels,
|
||||
conversationsError,
|
||||
thinkingState,
|
||||
orchestrationPanelVisible,
|
||||
orchestrationStatus,
|
||||
orchestrationInsight,
|
||||
activeAgent,
|
||||
visitedAgents,
|
||||
orchestrationEventFeed,
|
||||
systemTelemetry,
|
||||
sessionTelemetry,
|
||||
sendMessage,
|
||||
selectConversation,
|
||||
newConversation,
|
||||
|
||||
Reference in New Issue
Block a user