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>
257 lines
7.6 KiB
TypeScript
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>
|
|
);
|
|
}
|