diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 2659896..a00be48 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -119,7 +119,7 @@ async function createConversation() { // -- Select conversation -- async function selectConversation(id) { - // 保存当前对话的流式状态(如果有) + // 保存当前对话的流式状态和消息列表(如果有) if (currentConvId.value && streaming.value) { streamStates.set(currentConvId.value, { streaming: true, @@ -127,11 +127,11 @@ async function selectConversation(id) { streamThinking: streamThinking.value, streamToolCalls: [...streamToolCalls.value], streamProcessSteps: [...streamProcessSteps.value], + messages: [...messages.value], // 保存消息列表(包括临时用户消息) }) } currentConvId.value = id - messages.value = [] nextMsgCursor.value = null hasMoreMessages.value = false @@ -143,15 +143,20 @@ async function selectConversation(id) { streamThinking.value = savedState.streamThinking streamToolCalls.value = savedState.streamToolCalls streamProcessSteps.value = savedState.streamProcessSteps + messages.value = savedState.messages || [] // 恢复消息列表 } else { streaming.value = false streamContent.value = '' streamThinking.value = '' streamToolCalls.value = [] streamProcessSteps.value = [] + messages.value = [] } - await loadMessages(true) + // 如果不是正在流式传输,从服务器加载消息 + if (!streaming.value) { + await loadMessages(true) + } } // -- Load messages -- @@ -277,8 +282,7 @@ async function sendMessage(content) { if (currentConvId.value === convId) { streaming.value = false currentStreamPromise = null - // Replace temp message and add assistant message from server - messages.value = messages.value.filter(m => m.id !== userMsg.id) + // 添加助手消息(保留临时用户消息) messages.value.push({ id: data.message_id, conversation_id: convId, diff --git a/frontend/src/components/ChatView.vue b/frontend/src/components/ChatView.vue index e3ba793..d7f9cb4 100644 --- a/frontend/src/components/ChatView.vue +++ b/frontend/src/components/ChatView.vue @@ -46,7 +46,7 @@ @delete="$emit('deleteMessage', msg.id)" /> -
+
claw
+
+ + + + 正在生成... +
@@ -330,6 +336,26 @@ defineExpose({ scrollToBottom }) word-break: break-word; } +.streaming-indicator { + display: flex; + align-items: center; + gap: 8px; + margin-top: 12px; + padding-top: 12px; + border-top: 1px solid var(--border-light); + font-size: 12px; + color: var(--text-tertiary); +} + +.spinner { + animation: spin 1s linear infinite; +} + +@keyframes spin { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } +} + .streaming-content :deep(p) { margin: 0 0 8px; } diff --git a/frontend/src/components/ProcessBlock.vue b/frontend/src/components/ProcessBlock.vue index c8ea801..a84c0ce 100644 --- a/frontend/src/components/ProcessBlock.vue +++ b/frontend/src/components/ProcessBlock.vue @@ -1,15 +1,27 @@ @@ -267,6 +280,37 @@ watch(() => props.streaming, (streaming) => { max-width: 100%; } +.streaming-placeholder { + padding: 16px 20px; + display: flex; + align-items: center; + gap: 12px; + background: var(--bg-hover); +} + +.streaming-icon { + width: 28px; + height: 28px; + border-radius: 6px; + background: #fef3c7; + color: #f59e0b; + display: flex; + align-items: center; + justify-content: center; + animation: spin 2s linear infinite; +} + +.streaming-text { + font-size: 14px; + color: var(--text-secondary); + font-weight: 500; +} + +.streaming-text .dots { + display: inline-block; + animation: pulse 1s ease-in-out infinite; +} + .process-toggle { width: 100%; padding: 10px 12px; @@ -406,7 +450,7 @@ watch(() => props.streaming, (streaming) => { background: var(--bg-hover); } -.process-item.loading .process-icon { +.process-item.loading.tool_call .process-icon { animation: spin 1s linear infinite; }