Ver Fonte

style(chat): 等待指示加上绕身彩带并去掉等待时间

Samien há 1 mês atrás
pai
commit
65f3663

+ 0 - 8
src/client/components/common/TypingIndicator.vue

@@ -4,7 +4,6 @@ import AgentAvatar from './AgentAvatar.vue'
 defineProps<{
   avatarName: string
   avatar?: string
-  elapsedLabel: string
 }>()
 </script>
 
@@ -32,7 +31,6 @@ defineProps<{
         :style="{ '--dot-index': index - 1 }"
       />
     </span>
-    <small class="thinking-indicator__elapsed">{{ elapsedLabel }}</small>
   </div>
 </template>
 
@@ -61,12 +59,6 @@ defineProps<{
   animation-delay: calc(var(--dot-index) * 0.15s);
 }
 
-.thinking-indicator__elapsed {
-  color: var(--text-muted);
-  font-size: 10px;
-  font-variant-numeric: tabular-nums;
-}
-
 @keyframes thinking-dot-bounce {
   0%, 55%, 100% { transform: translateY(0); }
   28% { transform: translateY(-5px); }

+ 1 - 0
src/client/components/common/maus/cursor-engine.ts

@@ -238,6 +238,7 @@ export const EFFECTS: EffectsByState = {
   laughing: { confetti: { count: 7, period: 2400, life: 1000, origin: 70, spread: 38 } },
 
   // Work in flight — ribbons circling the body.
+  working: { trails: { count: 3, period: 2200, radius: 126 } },
   orbit: { trails: { count: 3, period: 2600, radius: 128 } },
   radar: { trails: { count: 2, period: 2000, radius: 132 } },
   progress: { trails: { count: 3, period: 1800, radius: 124 } },

+ 0 - 21
src/client/components/messages/MessageTimeline.vue

@@ -76,7 +76,6 @@ const pinnedToBottom = ref(true)
 const showJump = ref(false)
 const copiedMessageId = ref('')
 const copyFailedMessageId = ref('')
-const thinkingElapsedMs = ref(0)
 const timelineRows = computed(() => buildMessageTimelineRows(props.messages))
 const showThinkingIndicator = computed(() => props.thinking)
 const thinkingAgent = computed(() =>
@@ -89,8 +88,6 @@ const thinkingAvatar = computed(() => {
   return profile ? props.agentAvatars[profile] || '' : ''
 })
 let copyResetTimer: number | undefined
-let thinkingTimer: number | undefined
-let thinkingStartedAt = 0
 
 const formatTime = formatMessageTime
 
@@ -202,11 +199,6 @@ function deliveryLabel(status?: UiMessage['status']): string {
   } as Partial<Record<NonNullable<UiMessage['status']>, string>>)[status || 'settled'] || ''
 }
 
-function formatThinkingElapsed(value: number): string {
-  const seconds = Math.max(0, Math.floor(value / 1000))
-  return `${seconds} 秒`
-}
-
 watch(() => props.messages.length, async () => {
   if (!pinnedToBottom.value) return
   await nextTick()
@@ -219,21 +211,9 @@ watch(() => props.messages.at(-1)?.content, async () => {
   scrollToBottom('auto')
 })
 
-watch(showThinkingIndicator, visible => {
-  if (thinkingTimer !== undefined) window.clearInterval(thinkingTimer)
-  thinkingTimer = undefined
-  if (!visible) {
-    thinkingElapsedMs.value = 0
-    return
-  }
-  thinkingStartedAt = Date.now()
-  thinkingTimer = window.setInterval(() => { thinkingElapsedMs.value = Date.now() - thinkingStartedAt }, 250)
-}, { immediate: true })
-
 onMounted(() => nextTick(() => scrollToBottom('auto')))
 onBeforeUnmount(() => {
   if (copyResetTimer !== undefined) window.clearTimeout(copyResetTimer)
-  if (thinkingTimer !== undefined) window.clearInterval(thinkingTimer)
 })
 
 defineExpose({ scrollToMessage, scrollToAnchor, scrollToBottom, isFollowingBottom: () => pinnedToBottom.value })
@@ -396,7 +376,6 @@ defineExpose({ scrollToMessage, scrollToAnchor, scrollToBottom, isFollowingBotto
           v-if="showThinkingIndicator"
           :avatar-name="thinkingAvatarName"
           :avatar="thinkingAvatar"
-          :elapsed-label="formatThinkingElapsed(thinkingElapsedMs)"
         />
 
         <InteractionCard

+ 0 - 21
src/client/components/workspace/WorkspaceMessageTimeline.vue

@@ -81,7 +81,6 @@ const pinnedToBottom = ref(true)
 const showJump = ref(false)
 const copiedMessageId = ref('')
 const copyFailedMessageId = ref('')
-const thinkingElapsedMs = ref(0)
 // Like OpenMausBot, mount the tail first and explicitly expand older content.
 const windowStart = ref(0)
 const windowSize = 120
@@ -145,8 +144,6 @@ const thinkingAvatar = computed(() => {
   return profile ? props.agentAvatars[profile] || '' : ''
 })
 let copyResetTimer: number | undefined
-let thinkingTimer: number | undefined
-let thinkingStartedAt = 0
 
 const formatTime = formatMessageTime
 
@@ -265,11 +262,6 @@ function deliveryLabel(status?: UiMessage['status']): string {
   } as Partial<Record<NonNullable<UiMessage['status']>, string>>)[status || 'settled'] || ''
 }
 
-function formatThinkingElapsed(value: number): string {
-  const seconds = Math.max(0, Math.floor(value / 1000))
-  return `${seconds} 秒`
-}
-
 watch(() => props.messages.length, async () => {
   if (!pinnedToBottom.value) return
   await nextTick()
@@ -282,17 +274,6 @@ watch(() => props.messages.at(-1)?.content, async () => {
   scheduleFollow()
 })
 
-watch(showThinkingIndicator, visible => {
-  if (thinkingTimer !== undefined) window.clearInterval(thinkingTimer)
-  thinkingTimer = undefined
-  if (!visible) {
-    thinkingElapsedMs.value = 0
-    return
-  }
-  thinkingStartedAt = Date.now()
-  thinkingTimer = window.setInterval(() => { thinkingElapsedMs.value = Date.now() - thinkingStartedAt }, 250)
-}, { immediate: true })
-
 onMounted(() => nextTick(() => {
   scheduleFollow()
   transcriptObserver = new ResizeObserver(scheduleFollow)
@@ -307,7 +288,6 @@ onBeforeUnmount(() => {
   transcriptObserver?.disconnect()
   if (followFrame !== undefined) cancelAnimationFrame(followFrame)
   if (copyResetTimer !== undefined) window.clearTimeout(copyResetTimer)
-  if (thinkingTimer !== undefined) window.clearInterval(thinkingTimer)
 })
 
 defineExpose({ scrollToMessage, scrollToAnchor, scrollToBottom, isFollowingBottom: () => pinnedToBottom.value })
@@ -483,7 +463,6 @@ defineExpose({ scrollToMessage, scrollToAnchor, scrollToBottom, isFollowingBotto
           v-if="showThinkingIndicator"
           :avatar-name="thinkingAvatarName"
           :avatar="thinkingAvatar"
-          :elapsed-label="formatThinkingElapsed(thinkingElapsedMs)"
         />
 
         <WorkspaceApprovalCard v-if="interaction?.kind === 'approval'" :prompt="interaction.prompt" :agent-name="approvalAgentName" :busy="interactionBusy" :error="interactionError" @choose="emit('approvalChoice', $event)" />

+ 4 - 13
tests/client/typingIndicator.test.ts

@@ -57,9 +57,9 @@ function mountTimeline(component: Component, messages: UiMessage[], thinking: bo
 }
 
 describe('TypingIndicator', () => {
-  it('renders a compact avatar, three dots, elapsed time and status semantics', () => {
+  it('renders a compact avatar, three dots and status semantics', () => {
     const wrapper = mount(TypingIndicator, {
-      props: { avatarName: '丫头', avatar: '', elapsedLabel: '3 秒' },
+      props: { avatarName: '丫头', avatar: '' },
     })
 
     expect(wrapper.attributes('role')).toBe('status')
@@ -68,7 +68,7 @@ describe('TypingIndicator', () => {
     expect(wrapper.attributes('data-testid')).toBe('chat-run-thinking-dots')
     expect(wrapper.get('.agent-avatar').attributes('style')).toContain('width: 20px')
     expect(wrapper.findAll('.thinking-indicator__dot')).toHaveLength(3)
-    expect(wrapper.text()).toContain('3 秒')
+    expect(wrapper.text()).not.toMatch(/\d+ 秒/)
     wrapper.unmount()
   })
 })
@@ -101,7 +101,7 @@ describe('message timeline typing state', () => {
     expect(indicator.attributes('role')).toBe('status')
     expect(indicator.attributes('aria-label')).toBe('机器人正在输入')
     expect(wrapper.findAll('.thinking-indicator__dot')).toHaveLength(3)
-    expect(wrapper.text()).toContain('0 秒')
+    expect(wrapper.text()).not.toMatch(/\d+ 秒/)
     const avatar = wrapper.get('[data-testid="chat-run-thinking-dots"] .agent-avatar-stub')
     expect(avatar.attributes('data-name')).toBe('流式助手')
     expect(avatar.attributes('data-avatar')).toBe('avatar-streaming')
@@ -138,14 +138,5 @@ describe('message timeline typing state', () => {
     fallback.unmount()
   })
 
-  it('counts elapsed time while thinking remains active', async () => {
-    vi.useFakeTimers()
-    const wrapper = mountTimeline(MessageTimeline, [user, streamingAssistant()], true)
 
-    expect(wrapper.text()).toContain('0 秒')
-    vi.advanceTimersByTime(1_100)
-    await nextTick()
-    expect(wrapper.text()).toContain('1 秒')
-    wrapper.unmount()
-  })
 })