refactor(chat): polish chat UI with design system tokens and components

- Replace raw <button> with <Button variant="ghost" size="icon-sm"> in header and history
- Add aria-expanded:bg-accent to agent selector trigger for open state
- Add max-h-60, w-auto max-w-56, truncate to agent dropdown
- Switch FAB to bg-card, chat window to bg-sidebar
- Switch user message bubble from bg-primary to bg-muted, drop text-primary-foreground
- Reduce user bubble max-w from 85% to 80%
- Remove agent avatar from AI messages, make AI content w-full
- Strip arbitrary text-[10px] from AvatarFallback
- Remove manual icon size overrides inside Button components

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Naiyuan Qing
2026-04-13 16:57:48 +08:00
parent 6097f7392e
commit 64ed0806ff
5 changed files with 60 additions and 57 deletions

View File

@@ -18,7 +18,7 @@ export function ChatFab() {
<Tooltip>
<TooltipTrigger
onClick={toggle}
className="fixed bottom-4 right-4 z-50 flex size-10 cursor-pointer items-center justify-center rounded-full border bg-background text-muted-foreground shadow-sm transition-transform hover:scale-110 hover:text-accent-foreground active:scale-95"
className="fixed bottom-4 right-4 z-50 flex size-10 cursor-pointer items-center justify-center rounded-full ring-1 ring-foreground/10 bg-card text-muted-foreground shadow-sm transition-transform hover:scale-110 hover:text-accent-foreground active:scale-95"
>
<MessageCircle className="size-5" />
</TooltipTrigger>

View File

@@ -25,7 +25,7 @@ export function ChatInput({ onSend, onStop, isRunning, disabled }: ChatInputProp
return (
<div className="p-2 pt-0">
<div className="relative flex min-h-16 max-h-40 flex-col rounded-lg bg-card pb-8 ring-1 ring-border">
<div className="relative flex min-h-16 max-h-40 flex-col rounded-lg bg-card pb-8 border-1 border-border transition-colors focus-within:border-brand">
<div className="flex-1 min-h-0 overflow-y-auto px-3 py-2">
<ContentEditor
ref={editorRef}

View File

@@ -45,20 +45,12 @@ export function ChatMessageList({
))}
{/* Live streaming timeline */}
{hasTimeline && (
<div className="flex items-start gap-3">
<AgentAvatar agent={agent} />
<div className="min-w-0 flex-1 space-y-1.5">
<TimelineView items={timelineItems} />
</div>
<div className="w-full space-y-1.5">
<TimelineView items={timelineItems} />
</div>
)}
{isWaiting && !hasTimeline && (
<div className="flex items-start gap-3">
<AgentAvatar agent={agent} />
<div className="flex items-center pt-1">
<Loader2 className="size-4 animate-spin text-muted-foreground" />
</div>
</div>
<Loader2 className="size-4 animate-spin text-muted-foreground" />
)}
<div ref={bottomRef} />
</div>
@@ -77,7 +69,7 @@ function MessageBubble({
if (message.role === "user") {
return (
<div className="flex justify-end">
<div className="rounded-2xl bg-primary px-3.5 py-2 text-sm text-primary-foreground max-w-[85%] whitespace-pre-wrap break-words">
<div className="rounded-2xl bg-muted px-3.5 py-2 text-sm max-w-[80%] whitespace-pre-wrap break-words">
{message.content}
</div>
</div>
@@ -116,17 +108,14 @@ function AssistantMessage({
);
return (
<div className="flex items-start gap-3">
<AgentAvatar agent={agent} />
<div className="min-w-0 flex-1 space-y-1.5">
{timeline.length > 0 ? (
<TimelineView items={timeline} />
) : (
<div className="text-sm leading-relaxed prose prose-sm dark:prose-invert max-w-none">
<Markdown>{message.content}</Markdown>
</div>
)}
</div>
<div className="w-full space-y-1.5">
{timeline.length > 0 ? (
<TimelineView items={timeline} />
) : (
<div className="text-sm leading-relaxed prose prose-sm dark:prose-invert max-w-none">
<Markdown>{message.content}</Markdown>
</div>
)}
</div>
);
}

View File

@@ -2,6 +2,7 @@
import { useQuery } from "@tanstack/react-query";
import { ArrowLeft, MessageSquare, Archive, Trash2 } from "lucide-react";
import { Button } from "@multica/ui/components/ui/button";
import { Avatar, AvatarFallback, AvatarImage } from "@multica/ui/components/ui/avatar";
import { Bot } from "lucide-react";
import { useWorkspaceId } from "@multica/core/hooks";
@@ -47,12 +48,14 @@ export function ChatSessionHistory() {
<div className="flex flex-1 flex-col overflow-hidden">
{/* Header */}
<div className="flex items-center gap-2 border-b px-4 py-2.5">
<button
<Button
variant="ghost"
size="icon-sm"
className="text-muted-foreground"
onClick={() => setShowHistory(false)}
className="flex size-7 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground"
>
<ArrowLeft className="size-3.5" />
</button>
<ArrowLeft />
</Button>
<span className="text-sm font-medium">Chat History</span>
</div>
@@ -151,7 +154,7 @@ function SessionItem({
>
<Avatar className="size-6 shrink-0 mt-0.5">
{agent?.avatar_url && <AvatarImage src={agent.avatar_url} />}
<AvatarFallback className="bg-purple-100 text-purple-700 text-[10px]">
<AvatarFallback className="bg-purple-100 text-purple-700">
<Bot className="size-3" />
</AvatarFallback>
</Avatar>
@@ -174,13 +177,15 @@ function SessionItem({
</div>
</div>
{onArchive && (
<button
<Button
variant="ghost"
size="icon-sm"
className="invisible group-hover:visible text-muted-foreground hover:text-destructive shrink-0 mt-0.5"
onClick={onArchive}
title="Archive"
className="invisible group-hover:visible flex size-6 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-accent hover:text-destructive shrink-0 mt-0.5"
>
<Trash2 className="size-3" />
</button>
<Trash2 />
</Button>
)}
</button>
);

View File

@@ -4,6 +4,7 @@ import { useCallback, useEffect, useRef } from "react";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { Minus, Maximize2, Minimize2, Send, ChevronDown, Bot, Plus, History } from "lucide-react";
import { Avatar, AvatarFallback, AvatarImage } from "@multica/ui/components/ui/avatar";
import { Button } from "@multica/ui/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
@@ -226,8 +227,8 @@ export function ChatWindow() {
const hasMessages = messages.length > 0 || timelineItems.length > 0;
const containerClass = isFullscreen
? "fixed inset-y-0 right-0 z-50 flex flex-col w-[50%] border-l bg-background shadow-2xl"
: "fixed bottom-4 right-4 z-50 flex flex-col w-[420px] h-[600px] rounded-xl border bg-background shadow-2xl overflow-hidden";
? "fixed top-4 right-4 bottom-4 z-50 flex flex-col w-[50%] rounded-xl ring-1 ring-foreground/10 bg-sidebar shadow-2xl overflow-hidden"
: "fixed bottom-4 right-4 z-50 flex flex-col w-[420px] h-[600px] rounded-xl ring-1 ring-foreground/10 bg-sidebar shadow-2xl overflow-hidden";
return (
<div className={containerClass}>
@@ -240,38 +241,46 @@ export function ChatWindow() {
onSelect={handleSelectAgent}
/>
<div className="flex items-center gap-0.5">
<button
<Button
variant="ghost"
size="icon-sm"
className="text-muted-foreground"
onClick={() => setShowHistory(true)}
title="Chat history"
className="flex size-7 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground"
>
<History className="size-3.5" />
</button>
<button
<History />
</Button>
<Button
variant="ghost"
size="icon-sm"
className="text-muted-foreground"
onClick={() => {
setActiveSession(null);
clearTimeline();
setPendingTask(null);
}}
title="New chat"
className="flex size-7 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground"
>
<Plus className="size-3.5" />
</button>
<button
<Plus />
</Button>
<Button
variant="ghost"
size="icon-sm"
className="text-muted-foreground"
onClick={toggleFullscreen}
title={isFullscreen ? "Exit fullscreen" : "Fullscreen"}
className="flex size-7 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground"
>
{isFullscreen ? <Minimize2 className="size-3.5" /> : <Maximize2 className="size-3.5" />}
</button>
<button
{isFullscreen ? <Minimize2 /> : <Maximize2 />}
</Button>
<Button
variant="ghost"
size="icon-sm"
className="text-muted-foreground"
onClick={() => setOpen(false)}
title="Minimize"
className="flex size-7 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground"
>
<Minus className="size-3.5" />
</button>
<Minus />
</Button>
</div>
</div>
)}
@@ -329,20 +338,20 @@ function AgentSelector({
return (
<DropdownMenu>
<DropdownMenuTrigger className="flex items-center gap-2 rounded-md px-1.5 py-1 -ml-1.5 transition-colors hover:bg-accent">
<DropdownMenuTrigger className="flex items-center gap-2 rounded-md px-1.5 py-1 -ml-1.5 transition-colors hover:bg-accent aria-expanded:bg-accent">
<AgentAvatarSmall agent={activeAgent} />
<span className="text-sm font-medium">{activeAgent.name}</span>
<ChevronDown className="size-3 text-muted-foreground" />
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
<DropdownMenuContent align="start" className="max-h-60 w-auto max-w-56">
{agents.map((agent) => (
<DropdownMenuItem
key={agent.id}
onClick={() => onSelect(agent)}
className="flex items-center gap-2"
className="flex min-w-0 items-center gap-2"
>
<AgentAvatarSmall agent={agent} />
<span>{agent.name}</span>
<span className="truncate">{agent.name}</span>
</DropdownMenuItem>
))}
</DropdownMenuContent>
@@ -354,7 +363,7 @@ function AgentAvatarSmall({ agent }: { agent: Agent }) {
return (
<Avatar className="size-5">
{agent.avatar_url && <AvatarImage src={agent.avatar_url} />}
<AvatarFallback className="bg-purple-100 text-purple-700 text-[10px]">
<AvatarFallback className="bg-purple-100 text-purple-700">
<Bot className="size-3" />
</AvatarFallback>
</Avatar>