feat: 增加加载动画, 修复已知问题
This commit is contained in:
parent
3fd308d6b6
commit
e9ed4a8b39
|
|
@ -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,15 +143,20 @@ 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 = []
|
||||||
}
|
}
|
||||||
|
|
||||||
await loadMessages(true)
|
// 如果不是正在流式传输,从服务器加载消息
|
||||||
|
if (!streaming.value) {
|
||||||
|
await loadMessages(true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// -- Load messages --
|
// -- Load messages --
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,27 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="process-block">
|
<div class="process-block" :class="{ 'is-streaming': streaming }">
|
||||||
<button class="process-toggle" @click="toggleAll">
|
<!-- 流式加载状态 -->
|
||||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<div v-if="streaming && processItems.length === 0" class="streaming-placeholder">
|
||||||
<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"/>
|
<div class="streaming-icon">
|
||||||
</svg>
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
<span>思考与工具调用过程</span>
|
<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"/>
|
||||||
<span class="process-count">{{ processItems.length }} 步</span>
|
</svg>
|
||||||
<svg class="arrow" :class="{ open: allExpanded }" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
</div>
|
||||||
<polyline points="6 9 12 15 18 9"></polyline>
|
<span class="streaming-text">正在思考中<span class="dots">...</span></span>
|
||||||
</svg>
|
</div>
|
||||||
</button>
|
|
||||||
|
<!-- 正常内容 -->
|
||||||
|
<template v-else>
|
||||||
|
<button class="process-toggle" @click="toggleAll">
|
||||||
|
<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"/>
|
||||||
|
</svg>
|
||||||
|
<span>思考与工具调用过程</span>
|
||||||
|
<span class="process-count">{{ processItems.length }} 步</span>
|
||||||
|
<svg class="arrow" :class="{ open: allExpanded }" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<polyline points="6 9 12 15 18 9"></polyline>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
<div v-if="allExpanded" class="process-list">
|
<div v-if="allExpanded" class="process-list">
|
||||||
<div v-for="item in processItems" :key="item.key" class="process-item" :class="[item.type, { loading: item.loading }]">
|
<div v-for="item in processItems" :key="item.key" class="process-item" :class="[item.type, { loading: item.loading }]">
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue