Files
aragrp/components/playground/agent-thread.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

386 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import {
BarChart3,
Check,
ChevronDown,
FileText,
GitBranch,
Loader2,
Megaphone,
Sparkles,
Target,
} from "lucide-react";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
import { useState } from "react";
import {
AGENT_AUDIENCE_TAGS,
AGENT_PROGRESS_STAGES,
} from "./playground-content";
import {
agentEase,
agentPanelTransition,
agentSoftSpring,
agentStaggerFast,
agentThreadItem,
} from "./agent-motion";
import { cn } from "@/lib/utils";
import { type } from "@/lib/typography";
import type { ChatMessage } from "./types";
interface AgentThreadProps {
messages: ChatMessage[];
isTyping?: boolean;
bottomRef?: React.RefObject<HTMLDivElement | null>;
}
function ProgressCard() {
const activeStage = 2;
const reduceMotion = useReducedMotion();
return (
<motion.div
className="rounded-2xl border bg-card p-4 shadow-sm"
variants={agentThreadItem}
initial="hidden"
animate="visible"
>
<div className="flex items-center justify-between gap-2">
<h3 className={type["card-title"]}>تولید سناریو آغاز شد</h3>
<span className={cn(type.caption, "text-muted-foreground")}>۵ دقیقه</span>
</div>
<div className="mt-4 flex gap-1">
{AGENT_PROGRESS_STAGES.map((stage, index) => {
const isComplete = index < activeStage;
const isActive = index === activeStage;
return (
<div key={stage} className="flex-1 space-y-1.5">
<div className="h-1.5 overflow-hidden rounded-full bg-muted">
<motion.div
className="h-full rounded-full bg-primary"
initial={{ scaleX: 0 }}
animate={{
scaleX: isComplete || isActive ? 1 : 0,
opacity: isComplete || isActive ? 1 : 0,
}}
style={{ transformOrigin: "right center" }}
transition={{
duration: reduceMotion ? 0 : 0.55,
delay: reduceMotion ? 0 : index * 0.1,
ease: agentEase,
}}
/>
</div>
<motion.p
className={cn(
type.caption,
"text-center text-[10px] uppercase tracking-wide",
isComplete || isActive
? "text-primary"
: "text-muted-foreground"
)}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: reduceMotion ? 0 : 0.15 + index * 0.06 }}
>
{stage}
</motion.p>
</div>
);
})}
</div>
</motion.div>
);
}
function TaskRow({
title,
status,
expanded,
onToggle,
children,
index = 0,
}: {
title: string;
status: "done" | "active" | "pending";
expanded?: boolean;
onToggle?: () => void;
children?: React.ReactNode;
index?: number;
}) {
const reduceMotion = useReducedMotion();
return (
<motion.div
className="overflow-hidden rounded-xl border bg-card"
variants={agentThreadItem}
initial="hidden"
animate="visible"
transition={{ ...agentPanelTransition, delay: reduceMotion ? 0 : 0.08 + index * 0.07 }}
layout
>
<button
type="button"
onClick={onToggle}
className="flex w-full items-center gap-3 px-4 py-3 text-start"
>
<motion.span
className={cn(
"flex size-5 shrink-0 items-center justify-center rounded-full",
status === "done" && "bg-primary/15 text-primary",
status === "active" && "bg-primary text-primary-foreground",
status === "pending" && "border text-muted-foreground"
)}
initial={{ scale: 0.6, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
transition={agentSoftSpring}
>
{status === "done" ? (
<Check className="size-3" aria-hidden />
) : status === "active" ? (
<Loader2 className="size-3 animate-spin" aria-hidden />
) : (
<span className="size-1.5 rounded-full bg-muted-foreground/50" />
)}
</motion.span>
<span className={cn("flex-1", type["body-sm"], status === "pending" && "text-muted-foreground")}>
{title}
</span>
{onToggle && (
<motion.span
animate={{ rotate: expanded ? 180 : 0 }}
transition={{ duration: 0.28, ease: agentEase }}
>
<ChevronDown
className="size-4 shrink-0 text-muted-foreground"
aria-hidden
/>
</motion.span>
)}
</button>
<AnimatePresence initial={false}>
{expanded && children && (
<motion.div
key="content"
initial={{ height: 0, opacity: 0 }}
animate={{ height: "auto", opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{ duration: reduceMotion ? 0 : 0.32, ease: agentEase }}
className="overflow-hidden"
>
<div className="border-t px-4 pb-4 pt-3">{children}</div>
</motion.div>
)}
</AnimatePresence>
</motion.div>
);
}
function AudienceTags() {
const allTags = [
...AGENT_AUDIENCE_TAGS.companyType,
...AGENT_AUDIENCE_TAGS.stage,
...AGENT_AUDIENCE_TAGS.region,
];
const reduceMotion = useReducedMotion();
return (
<div className="flex flex-wrap gap-1.5">
{allTags.map((tag, index) => (
<motion.span
key={tag}
className={cn(
"rounded-md border bg-muted/50 px-2.5 py-1 text-foreground",
type.caption
)}
initial={{ opacity: 0, scale: 0.85, y: 6 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
transition={{
...agentSoftSpring,
delay: reduceMotion ? 0 : index * 0.05,
}}
>
{tag}
</motion.span>
))}
</div>
);
}
function AssistantBubble({ content }: { content: string }) {
return (
<motion.div
className="rounded-2xl border bg-card p-4 shadow-sm"
initial={{ opacity: 0, y: 16 }}
animate={{ opacity: 1, y: 0 }}
transition={agentPanelTransition}
>
<p className={cn(type.chat, "whitespace-pre-wrap break-words text-card-foreground")}>
{content}
</p>
</motion.div>
);
}
export function AgentThread({
messages,
isTyping = false,
bottomRef,
}: AgentThreadProps) {
const [expandedTask, setExpandedTask] = useState(true);
const lastUserMessage = [...messages].reverse().find((m) => m.role === "user");
const assistantMessages = messages.filter((m) => m.role === "assistant");
const lastAssistant = assistantMessages[assistantMessages.length - 1];
return (
<div className="min-h-0 flex-1 overflow-y-auto">
<motion.div
className="flex flex-col gap-4 p-4 sm:p-5"
variants={agentStaggerFast}
initial="hidden"
animate="visible"
>
<AnimatePresence mode="popLayout">
{lastUserMessage && (
<motion.div
key="user-prompt"
className="rounded-2xl bg-muted/50 px-4 py-3.5"
layout
initial={{ opacity: 0, y: -12, scale: 0.98 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: -8, scale: 0.98 }}
transition={agentPanelTransition}
>
<p className={cn(type["body-sm"], "whitespace-pre-wrap break-words text-foreground")}>
{lastUserMessage.content}
</p>
</motion.div>
)}
</AnimatePresence>
<AnimatePresence mode="popLayout">
{(isTyping || assistantMessages.length > 0) && (
<motion.div
key="agent-workflow"
className="space-y-4"
layout
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.35, ease: agentEase }}
>
<AnimatePresence>
{isTyping && (
<motion.div
key="progress"
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: "auto" }}
exit={{ opacity: 0, height: 0 }}
transition={{ duration: 0.38, ease: agentEase }}
>
<ProgressCard />
</motion.div>
)}
</AnimatePresence>
<div className="space-y-2">
<TaskRow title="تحلیل نیازمندی‌ها" status="done" index={0} />
<TaskRow
title="کشف حساب‌ها و سیگنال‌ها"
status={isTyping ? "active" : "done"}
expanded={expandedTask}
onToggle={() => setExpandedTask((v) => !v)}
index={1}
>
<AudienceTags />
</TaskRow>
<TaskRow
title="ساخت پیام‌های شخصی‌سازی‌شده"
status={isTyping ? "pending" : "done"}
index={2}
/>
<TaskRow
title="تنظیم دنباله ارتباطی"
status="pending"
index={3}
/>
</div>
<AnimatePresence>
{!isTyping && lastAssistant && (
<motion.div
key="assistant"
initial={{ opacity: 0, y: 14 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 8 }}
transition={agentPanelTransition}
>
<AssistantBubble content={lastAssistant.content} />
</motion.div>
)}
</AnimatePresence>
<AnimatePresence>
{!isTyping && assistantMessages.length > 0 && (
<motion.div
key="ready-banner"
className="flex items-center justify-between rounded-xl bg-muted/40 px-4 py-2.5"
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0 }}
transition={{ ...agentPanelTransition, delay: 0.1 }}
>
<span className={cn(type["body-sm"], "text-muted-foreground")}>
سناریوی شما آماده است!
</span>
<motion.button
type="button"
className={cn(
"rounded-lg bg-foreground px-3 py-1.5 text-background",
type.caption
)}
whileHover={{ scale: 1.03 }}
whileTap={{ scale: 0.97 }}
>
پیش‌نمایش
</motion.button>
</motion.div>
)}
</AnimatePresence>
</motion.div>
)}
</AnimatePresence>
<div ref={bottomRef} className="h-px shrink-0" aria-hidden />
</motion.div>
</div>
);
}
export function FeatureCardIcon({
icon,
className,
}: {
icon: (typeof import("./playground-content").AGENT_FEATURE_CARDS)[number]["icon"];
className?: string;
}) {
const iconClass = cn("size-4", className);
switch (icon) {
case "megaphone":
return <Megaphone className={iconClass} aria-hidden />;
case "document":
return <FileText className={iconClass} aria-hidden />;
case "sparkles":
return <Sparkles className={iconClass} aria-hidden />;
case "chart":
return <BarChart3 className={iconClass} aria-hidden />;
case "target":
return <Target className={iconClass} aria-hidden />;
case "steps":
return <GitBranch className={iconClass} aria-hidden />;
}
}