diff --git a/client/src/components/AIChatBox.tsx b/client/src/components/AIChatBox.tsx deleted file mode 100644 index 1c00871..0000000 --- a/client/src/components/AIChatBox.tsx +++ /dev/null @@ -1,335 +0,0 @@ -import { Button } from "@/components/ui/button"; -import { Textarea } from "@/components/ui/textarea"; -import { ScrollArea } from "@/components/ui/scroll-area"; -import { cn } from "@/lib/utils"; -import { Loader2, Send, User, Sparkles } from "lucide-react"; -import { useState, useEffect, useRef } from "react"; -import { Streamdown } from "streamdown"; - -/** - * Message type matching server-side LLM Message interface - */ -export type Message = { - role: "system" | "user" | "assistant"; - content: string; -}; - -export type AIChatBoxProps = { - /** - * Messages array to display in the chat. - * Should match the format used by invokeLLM on the server. - */ - messages: Message[]; - - /** - * Callback when user sends a message. - * Typically you'll call a tRPC mutation here to invoke the LLM. - */ - onSendMessage: (content: string) => void; - - /** - * Whether the AI is currently generating a response - */ - isLoading?: boolean; - - /** - * Placeholder text for the input field - */ - placeholder?: string; - - /** - * Custom className for the container - */ - className?: string; - - /** - * Height of the chat box (default: 600px) - */ - height?: string | number; - - /** - * Empty state message to display when no messages - */ - emptyStateMessage?: string; - - /** - * Suggested prompts to display in empty state - * Click to send directly - */ - suggestedPrompts?: string[]; -}; - -/** - * A ready-to-use AI chat box component that integrates with the LLM system. - * - * Features: - * - Matches server-side Message interface for seamless integration - * - Markdown rendering with Streamdown - * - Auto-scrolls to latest message - * - Loading states - * - Uses global theme colors from index.css - * - * @example - * ```tsx - * const ChatPage = () => { - * const [messages, setMessages] = useState([ - * { role: "system", content: "You are a helpful assistant." } - * ]); - * - * const chatMutation = trpc.ai.chat.useMutation({ - * onSuccess: (response) => { - * // Assuming your tRPC endpoint returns the AI response as a string - * setMessages(prev => [...prev, { - * role: "assistant", - * content: response - * }]); - * }, - * onError: (error) => { - * console.error("Chat error:", error); - * // Optionally show error message to user - * } - * }); - * - * const handleSend = (content: string) => { - * const newMessages = [...messages, { role: "user", content }]; - * setMessages(newMessages); - * chatMutation.mutate({ messages: newMessages }); - * }; - * - * return ( - * - * ); - * }; - * ``` - */ -export function AIChatBox({ - messages, - onSendMessage, - isLoading = false, - placeholder = "Type your message...", - className, - height = "600px", - emptyStateMessage = "Start a conversation with AI", - suggestedPrompts, -}: AIChatBoxProps) { - const [input, setInput] = useState(""); - const scrollAreaRef = useRef(null); - const containerRef = useRef(null); - const inputAreaRef = useRef(null); - const textareaRef = useRef(null); - - // Filter out system messages - const displayMessages = messages.filter((msg) => msg.role !== "system"); - - // Calculate min-height for last assistant message to push user message to top - const [minHeightForLastMessage, setMinHeightForLastMessage] = useState(0); - - useEffect(() => { - if (containerRef.current && inputAreaRef.current) { - const containerHeight = containerRef.current.offsetHeight; - const inputHeight = inputAreaRef.current.offsetHeight; - const scrollAreaHeight = containerHeight - inputHeight; - - // Reserve space for: - // - padding (p-4 = 32px top+bottom) - // - user message: 40px (item height) + 16px (margin-top from space-y-4) = 56px - // Note: margin-bottom is not counted because it naturally pushes the assistant message down - const userMessageReservedHeight = 56; - const calculatedHeight = scrollAreaHeight - 32 - userMessageReservedHeight; - - setMinHeightForLastMessage(Math.max(0, calculatedHeight)); - } - }, []); - - // Scroll to bottom helper function with smooth animation - const scrollToBottom = () => { - const viewport = scrollAreaRef.current?.querySelector( - '[data-radix-scroll-area-viewport]' - ) as HTMLDivElement; - - if (viewport) { - requestAnimationFrame(() => { - viewport.scrollTo({ - top: viewport.scrollHeight, - behavior: 'smooth' - }); - }); - } - }; - - const handleSubmit = (e: React.FormEvent) => { - e.preventDefault(); - const trimmedInput = input.trim(); - if (!trimmedInput || isLoading) return; - - onSendMessage(trimmedInput); - setInput(""); - - // Scroll immediately after sending - scrollToBottom(); - - // Keep focus on input - textareaRef.current?.focus(); - }; - - const handleKeyDown = (e: React.KeyboardEvent) => { - if (e.key === "Enter" && !e.shiftKey) { - e.preventDefault(); - handleSubmit(e); - } - }; - - return ( -
- {/* Messages Area */} -
- {displayMessages.length === 0 ? ( -
-
-
- -

{emptyStateMessage}

-
- - {suggestedPrompts && suggestedPrompts.length > 0 && ( -
- {suggestedPrompts.map((prompt, index) => ( - - ))} -
- )} -
-
- ) : ( - -
- {displayMessages.map((message, index) => { - // Apply min-height to last message only if NOT loading (when loading, the loading indicator gets it) - const isLastMessage = index === displayMessages.length - 1; - const shouldApplyMinHeight = - isLastMessage && !isLoading && minHeightForLastMessage > 0; - - return ( -
- {message.role === "assistant" && ( -
- -
- )} - -
- {message.role === "assistant" ? ( -
- {message.content} -
- ) : ( -

- {message.content} -

- )} -
- - {message.role === "user" && ( -
- -
- )} -
- ); - })} - - {isLoading && ( -
0 - ? { minHeight: `${minHeightForLastMessage}px` } - : undefined - } - > -
- -
-
- -
-
- )} -
-
- )} -
- - {/* Input Area */} -
-