feat: 增加加载动画, 修复已知问题

This commit is contained in:
ViperEkura 2026-03-25 17:26:39 +08:00
parent 3fd308d6b6
commit e9ed4a8b39
3 changed files with 92 additions and 18 deletions

View File

@ -119,7 +119,7 @@ async function createConversation() {
// -- Select conversation -- // -- Select conversation --
async function selectConversation(id) { async function selectConversation(id) {
// //
if (currentConvId.value && streaming.value) { if (currentConvId.value && streaming.value) {
streamStates.set(currentConvId.value, { streamStates.set(currentConvId.value, {
streaming: true, streaming: true,
@ -127,11 +127,11 @@ async function selectConversation(id) {
streamThinking: streamThinking.value, streamThinking: streamThinking.value,
streamToolCalls: [...streamToolCalls.value], streamToolCalls: [...streamToolCalls.value],
streamProcessSteps: [...streamProcessSteps.value], streamProcessSteps: [...streamProcessSteps.value],
messages: [...messages.value], //
}) })
} }
currentConvId.value = id currentConvId.value = id
messages.value = []
nextMsgCursor.value = null nextMsgCursor.value = null
hasMoreMessages.value = false hasMoreMessages.value = false
@ -143,16 +143,21 @@ async function selectConversation(id) {
streamThinking.value = savedState.streamThinking streamThinking.value = savedState.streamThinking
streamToolCalls.value = savedState.streamToolCalls streamToolCalls.value = savedState.streamToolCalls
streamProcessSteps.value = savedState.streamProcessSteps streamProcessSteps.value = savedState.streamProcessSteps
messages.value = savedState.messages || [] //
} else { } else {
streaming.value = false streaming.value = false
streamContent.value = '' streamContent.value = ''
streamThinking.value = '' streamThinking.value = ''
streamToolCalls.value = [] streamToolCalls.value = []
streamProcessSteps.value = [] streamProcessSteps.value = []
messages.value = []
} }
//
if (!streaming.value) {
await loadMessages(true) await loadMessages(true)
} }
}
// -- Load messages -- // -- Load messages --
async function loadMessages(reset = true) { async function loadMessages(reset = true) {
@ -277,8 +282,7 @@ async function sendMessage(content) {
if (currentConvId.value === convId) { if (currentConvId.value === convId) {
streaming.value = false streaming.value = false
currentStreamPromise = null currentStreamPromise = null
// Replace temp message and add assistant message from server //
messages.value = messages.value.filter(m => m.id !== userMsg.id)
messages.value.push({ messages.value.push({
id: data.message_id, id: data.message_id,
conversation_id: convId, conversation_id: convId,

View File

@ -46,7 +46,7 @@
@delete="$emit('deleteMessage', msg.id)" @delete="$emit('deleteMessage', msg.id)"
/> />
<div v-if="streaming" class="message-bubble assistant"> <div v-if="streaming" class="message-bubble assistant streaming">
<div class="avatar">claw</div> <div class="avatar">claw</div>
<div class="message-body"> <div class="message-body">
<ProcessBlock <ProcessBlock
@ -56,6 +56,12 @@
:streaming="streaming" :streaming="streaming"
/> />
<div class="message-content streaming-content" v-html="renderedStreamContent || '<span class=\'placeholder\'>...</span>'"></div> <div class="message-content streaming-content" v-html="renderedStreamContent || '<span class=\'placeholder\'>...</span>'"></div>
<div class="streaming-indicator">
<svg class="spinner" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 12a9 9 0 1 1-6.219-8.56"/>
</svg>
<span>正在生成...</span>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -330,6 +336,26 @@ defineExpose({ scrollToBottom })
word-break: break-word; 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) { .streaming-content :deep(p) {
margin: 0 0 8px; margin: 0 0 8px;
} }

View File

@ -1,5 +1,17 @@
<template> <template>
<div class="process-block"> <div class="process-block" :class="{ 'is-streaming': streaming }">
<!-- 流式加载状态 -->
<div v-if="streaming && processItems.length === 0" class="streaming-placeholder">
<div class="streaming-icon">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"/>
</svg>
</div>
<span class="streaming-text">正在思考中<span class="dots">...</span></span>
</div>
<!-- 正常内容 -->
<template v-else>
<button class="process-toggle" @click="toggleAll"> <button class="process-toggle" @click="toggleAll">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"/> <path d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"/>
@ -56,6 +68,7 @@
</div> </div>
</div> </div>
</div> </div>
</template>
</div> </div>
</template> </template>
@ -267,6 +280,37 @@ watch(() => props.streaming, (streaming) => {
max-width: 100%; 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 { .process-toggle {
width: 100%; width: 100%;
padding: 10px 12px; padding: 10px 12px;
@ -406,7 +450,7 @@ watch(() => props.streaming, (streaming) => {
background: var(--bg-hover); background: var(--bg-hover);
} }
.process-item.loading .process-icon { .process-item.loading.tool_call .process-icon {
animation: spin 1s linear infinite; animation: spin 1s linear infinite;
} }