feat(web): modernize chat UI, fix account navigation

- Chat: avatars, cleaner bubbles, improved spacing, typing dots
- Input: rounded design, auto-expand textarea, arrow button, slash commands
- Account list: fix navigation with useParams reactivity
- Bot/User role labels in messages

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-07 04:14:16 +08:00
parent e850613972
commit 5104bbc18a
3 changed files with 102 additions and 88 deletions

View File

@@ -1,6 +1,6 @@
import { cn } from '@/lib/utils';
import { renderMarkdown } from '@/lib/markdown';
import { RefreshCw, ArrowRight, Quote } from 'lucide-react';
import { RefreshCw, ArrowRight, Quote, Bot, User } from 'lucide-react';
import type { Message } from '@/types';
interface Props {
@@ -15,61 +15,57 @@ export function ChatMessage({ message, onRegenerate, onContinue, onQuote }: Prop
const isEmpty = !message.content;
return (
<div className={cn('mb-4 flex group', isUser ? 'justify-end' : 'justify-start')}>
<div className="max-w-[80%]">
<div
className={cn(
'rounded-xl px-4 py-2.5',
isUser
? 'bg-indigo-600 text-white'
: 'bg-white text-zinc-700 border border-zinc-200 shadow-sm'
)}
>
<div className={cn('flex gap-3 mb-6', isUser ? 'flex-row-reverse' : 'flex-row')}>
{/* Avatar */}
<div className={cn(
'w-8 h-8 rounded-full flex items-center justify-center flex-shrink-0',
isUser ? 'bg-indigo-600 text-white' : 'bg-zinc-100 text-zinc-500'
)}>
{isUser ? <User size={15} /> : <Bot size={15} />}
</div>
{/* Content */}
<div className={cn('flex-1 max-w-[75%]', isUser && 'flex flex-col items-end')}>
{/* Role label */}
<div className={cn('text-[10px] font-medium mb-1 px-1', isUser ? 'text-right text-indigo-500' : 'text-zinc-400')}>
{isUser ? '你' : '美图 Agent'}
</div>
{/* Bubble */}
<div className={cn(
'rounded-2xl px-4 py-3 text-sm leading-relaxed',
isUser
? 'bg-indigo-600 text-white rounded-tr-md'
: 'bg-white text-zinc-700 border border-zinc-200/60 shadow-sm rounded-tl-md'
)}>
{isEmpty ? (
<span className="text-zinc-400 italic text-xs">...</span>
<div className="flex items-center gap-1 text-zinc-400">
<span className="inline-block w-1.5 h-1.5 bg-zinc-300 rounded-full animate-pulse" />
<span className="inline-block w-1.5 h-1.5 bg-zinc-300 rounded-full animate-pulse delay-100" />
<span className="inline-block w-1.5 h-1.5 bg-zinc-300 rounded-full animate-pulse delay-200" />
</div>
) : isUser ? (
<p className="text-sm leading-relaxed whitespace-pre-wrap">{message.content}</p>
<p className="whitespace-pre-wrap">{message.content}</p>
) : (
<div
className="markdown-body text-sm leading-relaxed"
dangerouslySetInnerHTML={{ __html: renderMarkdown(message.content) }}
/>
<div className="markdown-body" dangerouslySetInnerHTML={{ __html: renderMarkdown(message.content) }} />
)}
</div>
{/* Action buttons - only for assistant messages */}
{/* Actions - only for assistant messages */}
{!isUser && !isEmpty && (
<div className="flex gap-1 mt-1 opacity-0 group-hover:opacity-100 transition-opacity">
{onQuote && (
<button
onClick={() => onQuote(message.content)}
className="flex items-center gap-0.5 px-1.5 py-0.5 rounded text-[10px] text-zinc-400 hover:text-zinc-600 hover:bg-zinc-100 transition-colors"
title="引用"
>
<Quote size={10} />
</button>
)}
{onRegenerate && (
<button
onClick={() => onRegenerate(message.id)}
className="flex items-center gap-0.5 px-1.5 py-0.5 rounded text-[10px] text-zinc-400 hover:text-zinc-600 hover:bg-zinc-100 transition-colors"
title="重新生成"
>
<RefreshCw size={10} />
</button>
)}
{onContinue && (
<button
onClick={() => onContinue(message.id)}
className="flex items-center gap-0.5 px-1.5 py-0.5 rounded text-[10px] text-zinc-400 hover:text-zinc-600 hover:bg-zinc-100 transition-colors"
title="继续"
>
<ArrowRight size={10} />
</button>
)}
<div className="flex gap-0.5 mt-1 opacity-0 hover:opacity-100 transition-opacity px-1">
<button onClick={() => onQuote?.(message.content)}
className="flex items-center gap-1 px-2 py-0.5 rounded text-[10px] text-zinc-400 hover:text-zinc-600 hover:bg-zinc-100 transition-colors">
<Quote size={10} />
</button>
<button onClick={() => onRegenerate?.(message.id)}
className="flex items-center gap-1 px-2 py-0.5 rounded text-[10px] text-zinc-400 hover:text-zinc-600 hover:bg-zinc-100 transition-colors">
<RefreshCw size={10} />
</button>
<button onClick={() => onContinue?.(message.id)}
className="flex items-center gap-1 px-2 py-0.5 rounded text-[10px] text-zinc-400 hover:text-zinc-600 hover:bg-zinc-100 transition-colors">
<ArrowRight size={10} />
</button>
</div>
)}
</div>