diff --git a/dashboard/src/components/EmptyState.vue b/dashboard/src/components/EmptyState.vue deleted file mode 100644 index 84421e9..0000000 --- a/dashboard/src/components/EmptyState.vue +++ /dev/null @@ -1,79 +0,0 @@ - - - - - diff --git a/dashboard/src/components/ErrorMessage.vue b/dashboard/src/components/ErrorMessage.vue deleted file mode 100644 index 7c92623..0000000 --- a/dashboard/src/components/ErrorMessage.vue +++ /dev/null @@ -1,103 +0,0 @@ - - - - - diff --git a/dashboard/src/index.js b/dashboard/src/index.js index d4d18e0..eed2331 100644 --- a/dashboard/src/index.js +++ b/dashboard/src/index.js @@ -1,3 +1,5 @@ // 导出组件 -export { default as ErrorMessage } from './components/ErrorMessage.vue' -export { default as EmptyState } from './components/EmptyState.vue' +export { default as AppHeader } from './components/AppHeader.vue' +export { default as ProcessBlock } from './components/ProcessBlock.vue' +export { default as MessageBubble } from './components/MessageBubble.vue' +export { default as MessageNav } from './components/MessageNav.vue' diff --git a/dashboard/src/utils/useConversations.js b/dashboard/src/utils/useConversations.js new file mode 100644 index 0000000..9dcf410 --- /dev/null +++ b/dashboard/src/utils/useConversations.js @@ -0,0 +1,261 @@ +import { ref, computed, watch } from 'vue' +import { conversationsAPI, messagesAPI, toolsAPI, providersAPI } from './api.js' +import { streamManager } from './streamManager.js' +import { useStreamStore } from './streamStore.js' + +// 对话管理 Composable +export function useConversations() { + const streamStore = useStreamStore() + + // 状态 + const list = ref([]) + const providers = ref([]) + const page = ref(1) + const pageSize = 20 + const total = ref(0) + const loading = ref(true) + const error = ref('') + + const selectedId = ref(null) + const selectedConv = ref(null) + const convMessages = ref([]) + const loadingMessages = ref(false) + const enabledTools = ref([]) + + // 计算属性 + const totalPages = computed(() => Math.ceil(total.value / pageSize)) + + const currentStreamState = computed(() => { + return streamStore.getStreamState(selectedId.value) + }) + + const sending = computed(() => { + const state = streamStore.getStreamState(selectedId.value) + return state && state.status === 'streaming' + }) + + // 检查指定会话是否有活跃流 + const hasActiveStream = (convId) => { + return streamStore.hasActiveStream(convId) + } + + // 加载启用的工具列表 + const loadEnabledTools = async () => { + try { + const res = await toolsAPI.list() + if (res.success) { + const tools = res.data?.tools || [] + enabledTools.value = tools.map(t => t.function?.name || t.name) + } + } catch (e) { + console.error('Failed to load tools:', e) + } + } + + // 加载会话列表 + const fetchData = async () => { + loading.value = true + error.value = '' + try { + const [convRes, provRes, toolsRes] = await Promise.allSettled([ + conversationsAPI.list({ page: page.value, page_size: pageSize }), + providersAPI.list(), + toolsAPI.list() + ]) + if (convRes.status === 'fulfilled' && convRes.value.success) { + list.value = convRes.value.data?.items || [] + total.value = convRes.value.data?.total || 0 + // 默认选中第一个会话 + if (list.value.length > 0 && !selectedId.value) { + selectConv(list.value[0]) + } + } + if (provRes.status === 'fulfilled' && provRes.value.success) { + providers.value = provRes.value.data?.providers || [] + } + if (toolsRes.status === 'fulfilled' && toolsRes.value.success) { + enabledTools.value = (toolsRes.value.data?.tools || []).map(t => t.function?.name || t.name) + } + } catch (e) { + error.value = e.message + } + finally { + loading.value = false + } + } + + // 选择会话 + const selectConv = async (c) => { + selectedId.value = c.id + selectedConv.value = c + await fetchConvMessages(c.id) + setupStreamWatch() + } + + // 获取会话消息 + const fetchConvMessages = async (convId) => { + loadingMessages.value = true + convMessages.value = [] + try { + const res = await messagesAPI.list(convId) + if (res.success) { + convMessages.value = res.data?.messages || [] + } + } catch (e) { + console.error('获取消息失败:', e) + } finally { + loadingMessages.value = false + } + } + + // 发送消息 + const sendMessage = async (content) => { + if (!content.trim() || !selectedConv.value || sending.value) return + + const trimmedContent = content.trim() + + // 添加用户消息到列表 + const userMsgId = 'user-' + Date.now() + const userMsg = { + id: userMsgId, + role: 'user', + content: trimmedContent, + created_at: new Date().toISOString() + } + convMessages.value.push(userMsg) + + // 如果还没有标题或标题为默认标题,使用第一条消息作为标题 + const currentTitle = selectedConv.value?.title + const isDefaultTitle = !currentTitle || currentTitle === 'New Conversation' || currentTitle.trim() === '' + if (isDefaultTitle) { + const title = trimmedContent.slice(0, 30) + (trimmedContent.length > 30 ? '...' : '') + selectedConv.value.title = title + // 更新列表中的标题 + const conv = list.value.find(c => c.id === selectedConv.value.id) + if (conv) conv.title = title + // 调用 API 保存 + await conversationsAPI.update(selectedConv.value.id, { title }) + } + + // 使用 StreamManager 发送流式请求 + await streamManager.startStream( + selectedConv.value.id, + { + conversation_id: selectedConv.value.id, + content: trimmedContent, + enabled_tools: enabledTools.value + }, + userMsgId + ) + } + + // 创建会话 + const createConv = async (form) => { + const res = await conversationsAPI.create(form) + if (res.success && res.data?.id) { + await fetchData() + const newConv = list.value.find(c => c.id === res.data.id) + if (newConv) { + await selectConv(newConv) + } + return res.data + } + throw new Error(res.message) + } + + // 删除会话 + const deleteConv = async (c) => { + if (hasActiveStream(c.id)) { + streamManager.cancelStream(c.id) + } + await conversationsAPI.delete(c.id) + if (selectedId.value === c.id) { + selectedId.value = null + selectedConv.value = null + } + await fetchData() + } + + // 更新会话标题 + const updateConvTitle = async (c, newTitle) => { + c.title = newTitle + // 更新列表中的标题 + const conv = list.value.find(item => item.id === c.id) + if (conv) conv.title = newTitle + // 调用 API 保存 + await conversationsAPI.update(c.id, { title: newTitle }) + } + + // 设置流状态监听 + let unwatchStream = null + const setupStreamWatch = () => { + if (unwatchStream) { + unwatchStream() + } + + unwatchStream = watch( + () => streamStore.getStreamState(selectedId.value), + (state) => { + if (!state) return + + if (state.status === 'done') { + const completedMessage = { + id: state.id, + role: 'assistant', + process_steps: state.process_steps, + token_count: state.token_count, + usage: state.usage, + created_at: new Date().toISOString() + } + convMessages.value.push(completedMessage) + streamStore.clearStream(selectedId.value) + } else if (state.status === 'error') { + console.error('Stream error:', state.error) + streamStore.clearStream(selectedId.value) + } + }, + { deep: true } + ) + } + + // 初始化 + const init = async () => { + await fetchData() + } + + // 清理 + const cleanup = () => { + if (unwatchStream) { + unwatchStream() + } + } + + return { + // 状态 + list, + providers, + page, + totalPages, + loading, + error, + selectedId, + selectedConv, + convMessages, + loadingMessages, + sending, + currentStreamState, + + // 方法 + hasActiveStream, + fetchData, + selectConv, + fetchConvMessages, + sendMessage, + createConv, + deleteConv, + updateConvTitle, + loadEnabledTools, + init, + cleanup + } +} diff --git a/dashboard/src/views/ConversationView.vue b/dashboard/src/views/ConversationView.vue index cfbb5de..79fa3d8 100644 --- a/dashboard/src/views/ConversationView.vue +++ b/dashboard/src/views/ConversationView.vue @@ -99,18 +99,18 @@
-
@@ -137,7 +137,7 @@ @@ -149,7 +149,7 @@
@@ -166,93 +166,38 @@ @@ -633,7 +430,7 @@ onUnmounted(() => { .modal h2 { margin: 0 0 1.25rem; font-size: 1.1rem; color: var(--text-primary); } .form-group { margin-bottom: 1rem; } .form-group label { display: block; margin-bottom: 0.5rem; font-weight: 500; font-size: 0.9rem; color: var(--text-primary); } -.form-group input, .form-group select { width: 100%; padding: 0.65rem; border: 1px solid var(--border-input); border-radius: 8px; background: var(--bg-input); box-sizing: border-box; font-size: 0.9rem; color: var(--text-primary); } +.form-group input { width: 100%; padding: 0.65rem; border: 1px solid var(--border-input); border-radius: 8px; background: var(--bg-input); box-sizing: border-box; color: var(--text-primary); font-size: 0.9rem; } .modal-actions { display: flex; justify-content: flex-end; gap: 0.75rem; margin-top: 1.25rem; }