wu981526092's picture
✨ UPDATE CHAT AND DEVICE COMPONENTS: Refactor for improved layout and functionality
27615c9
raw
history blame
10.2 kB
import * as React from "react";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import { Textarea } from "@/components/ui/textarea";
import { Send, Square, User, Bot } from "lucide-react";
import ReactMarkdown from "react-markdown";
import { AssistantInfo } from "@/types/chat";
export interface ChatProps extends React.HTMLAttributes<HTMLDivElement> {
messages: Array<{
id: string;
role: "user" | "assistant" | "system";
content: string;
createdAt?: Date;
assistantInfo?: AssistantInfo;
}>;
input: string;
handleInputChange: (e: React.ChangeEvent<HTMLTextAreaElement>) => void;
handleSubmit: (e: React.FormEvent<HTMLFormElement>) => void;
isGenerating?: boolean;
stop?: () => void;
}
const Chat = React.forwardRef<HTMLDivElement, ChatProps>(
(
{
className,
messages,
input,
handleInputChange,
handleSubmit,
isGenerating,
stop,
...props
},
ref
) => {
const messagesEndRef = React.useRef<HTMLDivElement>(null);
const messagesContainerRef = React.useRef<HTMLDivElement>(null);
const scrollToBottom = () => {
if (messagesContainerRef.current) {
const element = messagesContainerRef.current;
element.scrollTop = element.scrollHeight;
}
};
React.useEffect(() => {
console.log(
"Chat component - messages updated:",
messages.length,
messages.map((m) => ({
id: m.id,
role: m.role,
content: m.content.slice(0, 50) + "...",
}))
);
// Always scroll to bottom when messages change
const timeoutId = setTimeout(() => {
scrollToBottom();
}, 50);
return () => clearTimeout(timeoutId);
}, [messages]);
return (
<div
className={cn("flex flex-col h-full max-h-screen", className)}
ref={ref}
{...props}
>
{/* Messages */}
<div
ref={messagesContainerRef}
className="flex-1 overflow-y-auto p-4 space-y-4 max-h-96 min-h-0"
>
{messages.length === 0 ? (
<div className="flex items-center justify-center h-full text-muted-foreground">
<p>No messages yet. Start a conversation!</p>
</div>
) : (
messages.map((message, index) => (
<div
key={`${message.id}-${index}`}
className={cn(
"flex gap-3 w-full items-start",
message.role === "user" ? "justify-end" : "justify-start"
)}
>
{/* Avatar for assistant */}
{message.role !== "user" && (
<div className="w-8 h-8 rounded-full bg-primary flex items-center justify-center flex-shrink-0 mt-1">
<Bot className="h-4 w-4 text-primary-foreground" />
</div>
)}
{/* Message content */}
<div
className={cn(
"max-w-[75%] min-w-0 flex flex-col gap-2 rounded-lg px-3 py-2 text-sm break-words",
message.role === "user"
? "bg-primary text-primary-foreground"
: "bg-muted"
)}
>
<div className="text-xs opacity-70 flex items-center gap-2">
<span>
{message.role === "user" ? (
"You"
) : message.assistantInfo ? (
<>
<span className="font-medium">
{message.assistantInfo.name}
</span>
{message.assistantInfo.type !== "default" && (
<span
className={cn(
"inline-block px-1.5 py-0.5 rounded text-[10px] font-medium ml-1",
message.assistantInfo.type === "user" &&
"bg-blue-100 text-blue-700",
message.assistantInfo.type === "template" &&
"bg-gray-100 text-gray-700",
message.assistantInfo.type === "new" &&
"bg-green-100 text-green-700"
)}
>
{message.assistantInfo.type === "user"
? "Mine"
: message.assistantInfo.type === "template"
? "Template"
: "New"}
</span>
)}
{message.assistantInfo.originalTemplate && (
<span className="text-[10px] text-muted-foreground">
(from {message.assistantInfo.originalTemplate})
</span>
)}
</>
) : (
"Assistant"
)}
</span>
<span></span>
<span>#{index + 1}</span>
</div>
<div className="leading-relaxed prose prose-sm dark:prose-invert max-w-none overflow-hidden">
<ReactMarkdown
components={{
// Customize components for better styling
p: ({ children }) => (
<p className="mb-2 last:mb-0 break-words">
{children}
</p>
),
ul: ({ children }) => (
<ul className="mb-2 last:mb-0 list-disc pl-4 break-words">
{children}
</ul>
),
ol: ({ children }) => (
<ol className="mb-2 last:mb-0 list-decimal pl-4 break-words">
{children}
</ol>
),
li: ({ children }) => (
<li className="mb-1 break-words">{children}</li>
),
code: ({ children, className }) => {
const isInline = !className;
return isInline ? (
<code className="bg-muted px-1 py-0.5 rounded text-xs font-mono break-words">
{children}
</code>
) : (
<code className="block bg-muted p-2 rounded text-xs font-mono overflow-x-auto whitespace-pre-wrap break-all">
{children}
</code>
);
},
pre: ({ children }) => (
<div className="mb-2 last:mb-0 overflow-hidden">
{children}
</div>
),
strong: ({ children }) => (
<strong className="font-semibold">{children}</strong>
),
em: ({ children }) => (
<em className="italic">{children}</em>
),
blockquote: ({ children }) => (
<blockquote className="border-l-4 border-muted pl-4 italic mb-2 last:mb-0">
{children}
</blockquote>
),
h1: ({ children }) => (
<h1 className="text-lg font-bold mb-2 last:mb-0">
{children}
</h1>
),
h2: ({ children }) => (
<h2 className="text-base font-bold mb-2 last:mb-0">
{children}
</h2>
),
h3: ({ children }) => (
<h3 className="text-sm font-bold mb-2 last:mb-0">
{children}
</h3>
),
}}
>
{message.content}
</ReactMarkdown>
</div>
</div>
{/* Avatar for user */}
{message.role === "user" && (
<div className="w-8 h-8 rounded-full bg-muted flex items-center justify-center flex-shrink-0 mt-1">
<User className="h-4 w-4" />
</div>
)}
</div>
))
)}
<div ref={messagesEndRef} />
</div>
{/* Input */}
<div className="border-t p-4">
<form onSubmit={handleSubmit} className="flex gap-2">
<Textarea
value={input}
onChange={handleInputChange}
placeholder="Type your message..."
className="min-h-[60px] resize-none"
onKeyDown={(e) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
handleSubmit(e as any);
}
}}
/>
{isGenerating ? (
<Button
type="button"
onClick={stop}
variant="outline"
size="icon"
>
<Square className="h-4 w-4" />
</Button>
) : (
<Button type="submit" disabled={!input.trim()} size="icon">
<Send className="h-4 w-4" />
</Button>
)}
</form>
</div>
</div>
);
}
);
Chat.displayName = "Chat";
export { Chat };