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>
108 lines
3.6 KiB
TypeScript
108 lines
3.6 KiB
TypeScript
"use client";
|
|
|
|
import { Bot, User } from "lucide-react";
|
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
|
import { cn } from "@/lib/utils";
|
|
import { type, typeSpacing } from "@/lib/typography";
|
|
import type { ChatMessage } from "./types";
|
|
|
|
interface ChatMessageListProps {
|
|
messages: ChatMessage[];
|
|
isTyping?: boolean;
|
|
bottomRef?: React.RefObject<HTMLDivElement | null>;
|
|
}
|
|
|
|
function formatTime(date: Date): string {
|
|
return date.toLocaleTimeString("fa-IR", {
|
|
hour: "2-digit",
|
|
minute: "2-digit",
|
|
});
|
|
}
|
|
|
|
function TypingIndicator() {
|
|
return (
|
|
<div className="flex justify-end">
|
|
<div className="flex max-w-[85%] items-center gap-2 rounded-2xl rounded-br-md border bg-card px-4 py-3 shadow-sm sm:max-w-[75%]">
|
|
<div className="flex size-7 shrink-0 items-center justify-center rounded-full bg-primary/10 text-primary">
|
|
<Bot className="size-4" aria-hidden />
|
|
</div>
|
|
<div className="flex items-center gap-1 py-1" aria-label="در حال نوشتن">
|
|
<span className="size-1.5 animate-bounce rounded-full bg-muted-foreground/60 [animation-delay:0ms]" />
|
|
<span className="size-1.5 animate-bounce rounded-full bg-muted-foreground/60 [animation-delay:150ms]" />
|
|
<span className="size-1.5 animate-bounce rounded-full bg-muted-foreground/60 [animation-delay:300ms]" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function ChatMessageList({
|
|
messages,
|
|
isTyping = false,
|
|
bottomRef,
|
|
}: ChatMessageListProps) {
|
|
return (
|
|
<ScrollArea className="min-h-0 flex-1 pe-1">
|
|
<div className="flex flex-col gap-4 p-4 sm:p-5">
|
|
{messages.map((message) => {
|
|
const isUser = message.role === "user";
|
|
|
|
return (
|
|
<div
|
|
key={message.id}
|
|
className={cn("flex", isUser ? "justify-start" : "justify-end")}
|
|
>
|
|
<div
|
|
className={cn(
|
|
"flex max-w-[92%] gap-2.5 sm:max-w-[80%]",
|
|
isUser ? "flex-row" : "flex-row-reverse"
|
|
)}
|
|
>
|
|
<div
|
|
className={cn(
|
|
"flex size-7 shrink-0 items-center justify-center rounded-full sm:size-8",
|
|
isUser
|
|
? "bg-primary/10 text-primary"
|
|
: "bg-muted text-muted-foreground"
|
|
)}
|
|
>
|
|
{isUser ? (
|
|
<User className="size-4" aria-hidden />
|
|
) : (
|
|
<Bot className="size-4" aria-hidden />
|
|
)}
|
|
</div>
|
|
|
|
<div className={cn("min-w-0", typeSpacing.chatMeta)}>
|
|
<div
|
|
className={cn(
|
|
"rounded-2xl px-4 py-3 shadow-sm",
|
|
type.chat,
|
|
isUser
|
|
? "rounded-tr-md bg-primary text-primary-foreground"
|
|
: "rounded-bl-md border bg-card text-card-foreground"
|
|
)}
|
|
>
|
|
<p className="whitespace-pre-wrap break-words">{message.content}</p>
|
|
</div>
|
|
<p
|
|
className={cn(
|
|
type.caption,
|
|
isUser ? "text-start" : "text-end"
|
|
)}
|
|
>
|
|
{isUser ? "شما" : "دستیار"} · {formatTime(message.timestamp)}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
|
|
{isTyping && <TypingIndicator />}
|
|
<div ref={bottomRef} className="h-px shrink-0" aria-hidden />
|
|
</div>
|
|
</ScrollArea>
|
|
);
|
|
}
|