mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-18 13:23:33 +08:00
fix: streamline AssistantMessage, ThinkingBlock, and StreamingIndicator — remove avatar, compact layout
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
001994d0bd
commit
d25be78f0c
@ -20,27 +20,17 @@ export function AssistantMessage({ content, isStreaming }: Props) {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="group mb-5 flex gap-3">
|
||||
{/* Avatar */}
|
||||
<div className="flex-shrink-0 w-7 h-7 rounded-full bg-[var(--color-brand)] flex items-center justify-center mt-0.5">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 2L2 7l10 5 10-5-10-5z" fill="white" opacity="0.9"/>
|
||||
<path d="M2 17l10 5 10-5" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" opacity="0.7"/>
|
||||
<path d="M2 12l10 5 10-5" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" opacity="0.85"/>
|
||||
</svg>
|
||||
</div>
|
||||
{/* Content */}
|
||||
<div className="min-w-0 flex-1 text-sm text-[var(--color-text-primary)]">
|
||||
{!isStreaming && content.trim() && (
|
||||
<div className="mb-2 flex justify-end">
|
||||
<button
|
||||
onClick={handleCopy}
|
||||
className="rounded-lg border border-[var(--color-border)] px-2.5 py-1 text-[11px] text-[var(--color-text-tertiary)] opacity-0 transition-opacity hover:text-[var(--color-text-primary)] group-hover:opacity-100"
|
||||
>
|
||||
{copied ? 'Copied' : 'Copy'}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<div className="group relative mb-3 ml-10">
|
||||
{/* Copy button — absolute positioned, no reserved space */}
|
||||
{!isStreaming && content.trim() && (
|
||||
<button
|
||||
onClick={handleCopy}
|
||||
className="absolute -right-1 -top-1 rounded-md border border-[var(--color-border)]/60 bg-[var(--color-surface)] px-2 py-0.5 text-[10px] text-[var(--color-text-tertiary)] opacity-0 shadow-sm transition-opacity hover:text-[var(--color-text-primary)] group-hover:opacity-100"
|
||||
>
|
||||
{copied ? 'Copied' : 'Copy'}
|
||||
</button>
|
||||
)}
|
||||
<div className="text-sm text-[var(--color-text-primary)]">
|
||||
<MarkdownRenderer content={content} />
|
||||
{isStreaming && (
|
||||
<span className="inline-block w-0.5 h-4 bg-[var(--color-brand)] animate-shimmer ml-0.5 align-text-bottom" />
|
||||
|
||||
@ -1,26 +1,18 @@
|
||||
import { useChatStore } from '../../stores/chatStore'
|
||||
|
||||
export function StreamingIndicator() {
|
||||
const { chatState, elapsedSeconds, tokenUsage } = useChatStore()
|
||||
const { chatState } = useChatStore()
|
||||
|
||||
const verb = chatState === 'thinking'
|
||||
? 'Thinking'
|
||||
: chatState === 'tool_executing'
|
||||
? 'Executing'
|
||||
: 'Crafting'
|
||||
? 'Running'
|
||||
: 'Working'
|
||||
|
||||
return (
|
||||
<div className="mb-4 ml-10 flex w-fit items-center gap-2 rounded-full border border-[var(--color-border)]/60 bg-[var(--color-surface-container-low)] px-3 py-1.5">
|
||||
<span className="text-[var(--color-brand)] animate-shimmer text-sm">✦</span>
|
||||
<span className="text-sm font-medium text-[var(--color-brand)]">{verb}...</span>
|
||||
<span className="text-xs text-[var(--color-text-tertiary)]">
|
||||
{elapsedSeconds}s
|
||||
</span>
|
||||
{tokenUsage.output_tokens > 0 && (
|
||||
<span className="text-xs text-[var(--color-text-tertiary)]">
|
||||
· ↓ {tokenUsage.output_tokens} tokens
|
||||
</span>
|
||||
)}
|
||||
<div className="mb-2 ml-10 flex w-fit items-center gap-2 rounded-full border border-[var(--color-border)]/40 bg-[var(--color-surface-container-low)] px-3 py-1">
|
||||
<span className="text-[var(--color-brand)] animate-shimmer text-xs">✦</span>
|
||||
<span className="text-xs font-medium text-[var(--color-text-secondary)]">{verb}...</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -2,58 +2,53 @@ import { useState, useEffect, useRef } from 'react'
|
||||
|
||||
export function ThinkingBlock({ content, isActive = false }: { content: string; isActive?: boolean }) {
|
||||
const [expanded, setExpanded] = useState(false)
|
||||
|
||||
// Auto-scroll to bottom of content area when expanded and content is still growing
|
||||
const contentRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (expanded && isActive && contentRef.current) {
|
||||
contentRef.current.scrollTop = contentRef.current.scrollHeight
|
||||
}
|
||||
}, [content, expanded, isActive])
|
||||
|
||||
const showCursor = isActive && expanded
|
||||
const inlinePreview = content.replace(/\s+/g, ' ').trim()
|
||||
const collapsedText = inlinePreview.slice(0, 140)
|
||||
const hasOverflow = inlinePreview.length > collapsedText.length
|
||||
// Preview: take first meaningful line, not first 140 chars
|
||||
const lines = content.split('\n').filter((l) => l.trim())
|
||||
const firstLine = lines[0]?.replace(/\s+/g, ' ').trim() || ''
|
||||
const preview = firstLine.length > 80 ? firstLine.slice(0, 80) + '...' : firstLine
|
||||
|
||||
return (
|
||||
<div className="mb-2 ml-10">
|
||||
<div className="mb-1 ml-10">
|
||||
<style>{thinkingStyles}</style>
|
||||
<button
|
||||
onClick={() => setExpanded((value) => !value)}
|
||||
className="flex w-full items-center gap-2 rounded-lg px-1 py-1 text-left text-[12px] text-[var(--color-text-tertiary)] transition-colors hover:text-[var(--color-text-secondary)]"
|
||||
onClick={() => setExpanded((v) => !v)}
|
||||
className="flex w-full items-center gap-1.5 rounded-md px-1 py-0.5 text-left text-[12px] text-[var(--color-text-tertiary)] transition-colors hover:text-[var(--color-text-secondary)]"
|
||||
>
|
||||
<span className="flex h-4 w-4 items-center justify-center rounded-full bg-[var(--color-surface-container-high)] text-[8px] text-[var(--color-outline)]">
|
||||
<span className="text-[10px] text-[var(--color-outline)]">
|
||||
{expanded ? '\u25BE' : '\u25B8'}
|
||||
</span>
|
||||
<span className="shrink-0 font-medium italic">
|
||||
Thinking
|
||||
{isActive && <span className="thinking-dots" />}
|
||||
</span>
|
||||
{!expanded && (
|
||||
<span className="min-w-0 flex-1 truncate font-[var(--font-mono)] text-[11px] leading-[1.3] text-[var(--color-text-tertiary)]">
|
||||
{collapsedText}
|
||||
{hasOverflow ? '…' : ''}
|
||||
{isActive && !expanded ? <span className="thinking-inline-cursor" /> : null}
|
||||
{!expanded && preview && (
|
||||
<span className="min-w-0 flex-1 truncate font-[var(--font-mono)] text-[11px] text-[var(--color-text-tertiary)]">
|
||||
{preview}
|
||||
{isActive && <span className="thinking-inline-cursor" />}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
{expanded && (
|
||||
<div className="mt-1 rounded-2xl border border-[var(--color-border)]/50 bg-[var(--color-surface-container-lowest)] px-3 py-3">
|
||||
<div
|
||||
ref={contentRef}
|
||||
className="max-h-[220px] overflow-y-auto whitespace-pre-wrap rounded-xl bg-[var(--color-surface)] font-[var(--font-mono)] text-[11px] leading-[1.45] text-[var(--color-text-secondary)]"
|
||||
>
|
||||
{content}
|
||||
{showCursor && <span className="thinking-cursor" />}
|
||||
</div>
|
||||
<div
|
||||
ref={contentRef}
|
||||
className="mt-1 max-h-[300px] overflow-y-auto rounded-lg border border-[var(--color-border)]/40 bg-[var(--color-surface-container-lowest)] p-2.5 font-[var(--font-mono)] text-[11px] leading-[1.35] text-[var(--color-text-secondary)] whitespace-pre-wrap break-words"
|
||||
>
|
||||
{content}
|
||||
{isActive && expanded && <span className="thinking-cursor" />}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/** Shared keyframe styles injected once per ThinkingBlock mount */
|
||||
const thinkingStyles = `
|
||||
@keyframes thinking-cursor-blink {
|
||||
0%, 100% { opacity: 1; }
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user