Files
aragrp/components/playground/chat-panel.tsx
T
alirez.brandCursor a47ec493c5 feat: add version history drawer with release notes
Introduce a footer-triggered Version History drawer with categorized release notes, hardcoded release data, and semver tracking at v0.3.0. Also ships blog blockquote/table styling, site-wide typography, home page image updates, header refinements, and related bug fixes.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-02 02:46:20 +03:30

257 lines
7.6 KiB
TypeScript

"use client";
import {
Braces,
ChevronLeft,
MoreVertical,
Pencil,
Settings2,
Share2,
Wand2,
} from "lucide-react";
import { AnimatePresence, motion } from "motion/react";
import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
import { AgentPreviewPanel } from "./agent-preview-panel";
import { AgentThread } from "./agent-thread";
import { ChatComposer } from "./chat-composer";
import { ChatEmptyState } from "./chat-empty-state";
import {
agentFadeIn,
agentLayoutTransition,
} from "./agent-motion";
import {
AGENT_BREADCRUMB,
AGENT_WORKSPACE_TITLE,
} from "./playground-content";
import type { ChatMessage } from "./types";
import type { ToolsTab } from "./playground-tools-sheet";
import { cn } from "@/lib/utils";
import { type } from "@/lib/typography";
interface ChatPanelProps {
messages: ChatMessage[];
inputValue: string;
onInputChange: (value: string) => void;
onSubmit: () => void;
onStarterSelect: (id: string, prompt: string) => void;
onOpenTools: (tab: ToolsTab) => void;
isSending?: boolean;
isTyping?: boolean;
bottomRef?: React.RefObject<HTMLDivElement | null>;
className?: string;
}
function AgentHeader({
onOpenTools,
}: {
onOpenTools: (tab: ToolsTab) => void;
}) {
return (
<div className="shrink-0 border-b bg-card">
<div className="flex items-center gap-1.5 px-4 py-2 sm:px-5">
<span className={cn(type.caption, "text-muted-foreground")}>
{AGENT_BREADCRUMB.parent}
</span>
<ChevronLeft className="size-3 rotate-180 text-muted-foreground/60" aria-hidden />
<span className={cn(type.caption, "font-medium text-foreground")}>
{AGENT_BREADCRUMB.current}
</span>
</div>
<div className="flex items-center justify-between gap-3 border-t bg-muted/20 px-4 py-2.5 sm:px-5">
<h2 className={cn("min-w-0 truncate", type["card-title"])}>
{AGENT_WORKSPACE_TITLE}
</h2>
<div className="flex shrink-0 items-center gap-0.5">
<Button
type="button"
variant="ghost"
size="icon-sm"
className="text-muted-foreground"
aria-label="ویرایش"
>
<Pencil className="size-3.5" aria-hidden />
</Button>
<Button
type="button"
variant="ghost"
size="icon-sm"
className="text-muted-foreground"
aria-label="اشتراک‌گذاری"
>
<Share2 className="size-3.5" aria-hidden />
</Button>
<Button
type="button"
variant="ghost"
size="icon-sm"
className="text-muted-foreground"
aria-label="گزینه‌های بیشتر"
>
<MoreVertical className="size-3.5" aria-hidden />
</Button>
<div className="mx-1 hidden h-4 w-px bg-border sm:block" aria-hidden />
<Button
type="button"
variant="ghost"
size="sm"
className="hidden h-8 gap-1.5 px-2 sm:inline-flex"
onClick={() => onOpenTools("scenarios")}
aria-label="سناریوها"
>
<Wand2 className="size-3.5 shrink-0" aria-hidden />
<span className={type.caption}>سناریوها</span>
</Button>
<Button
type="button"
variant="ghost"
size="icon-sm"
className="sm:hidden"
onClick={() => onOpenTools("scenarios")}
aria-label="سناریوها"
>
<Wand2 className="size-3.5" aria-hidden />
</Button>
<Button
type="button"
variant="ghost"
size="sm"
className="hidden h-8 gap-1.5 px-2 sm:inline-flex"
onClick={() => onOpenTools("settings")}
aria-label="تنظیمات"
>
<Settings2 className="size-3.5 shrink-0" aria-hidden />
<span className={type.caption}>تنظیمات</span>
</Button>
<Button
type="button"
variant="ghost"
size="icon-sm"
className="sm:hidden"
onClick={() => onOpenTools("settings")}
aria-label="تنظیمات"
>
<Settings2 className="size-3.5" aria-hidden />
</Button>
<Button
type="button"
variant="ghost"
size="sm"
className="hidden h-8 gap-1.5 px-2 sm:inline-flex"
onClick={() => onOpenTools("output")}
aria-label="خروجی"
>
<Braces className="size-3.5 shrink-0" aria-hidden />
<span className={type.caption}>خروجی</span>
</Button>
<Button
type="button"
variant="ghost"
size="icon-sm"
className="sm:hidden"
onClick={() => onOpenTools("output")}
aria-label="خروجی"
>
<Braces className="size-3.5" aria-hidden />
</Button>
</div>
</div>
</div>
);
}
export function ChatPanel({
messages,
inputValue,
onInputChange,
onSubmit,
onStarterSelect,
onOpenTools,
isSending = false,
isTyping = false,
bottomRef,
className,
}: ChatPanelProps) {
const showEmptyState = messages.length === 0 && !isTyping;
const showPreview = messages.length > 0 || isTyping;
return (
<Card
className={cn(
"flex min-h-[min(70dvh,42rem)] flex-col gap-0 overflow-hidden border-border/80 bg-muted/30 py-0 shadow-sm md:min-h-[min(75dvh,48rem)]",
className
)}
>
<AgentHeader onOpenTools={onOpenTools} />
<motion.div
layout
className="flex min-h-0 flex-1 flex-col lg:flex-row"
transition={agentLayoutTransition}
>
<motion.div
layout
className="flex min-h-0 min-w-0 flex-1 flex-col bg-background/60"
transition={agentLayoutTransition}
>
<div className="relative min-h-0 flex-1">
<AnimatePresence mode="sync">
{showEmptyState ? (
<motion.div
key="empty"
className="absolute inset-0 flex flex-col"
variants={agentFadeIn}
initial="hidden"
animate="visible"
exit="exit"
>
<ChatEmptyState
onSelectStarter={onStarterSelect}
disabled={isSending}
className="flex-1"
/>
</motion.div>
) : (
<motion.div
key="thread"
className="absolute inset-0 flex flex-col"
variants={agentFadeIn}
initial="hidden"
animate="visible"
exit="exit"
>
<AgentThread
messages={messages}
isTyping={isTyping}
bottomRef={bottomRef}
/>
</motion.div>
)}
</AnimatePresence>
</div>
<motion.div layout transition={agentLayoutTransition}>
<ChatComposer
value={inputValue}
onChange={onInputChange}
onSubmit={onSubmit}
disabled={false}
isSending={isSending}
/>
</motion.div>
</motion.div>
<AnimatePresence>
{showPreview && (
<AgentPreviewPanel key="preview" className="hidden lg:flex" />
)}
</AnimatePresence>
</motion.div>
</Card>
);
}