mirror of
https://github.com/multica-ai/multica.git
synced 2026-06-17 03:38:32 +02:00
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:
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user