"use client"; import { useState, useRef, useEffect } from "react"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, } from "@/components/ui/dialog"; import { Textarea } from "@/components/ui/textarea"; import { cn } from "@/lib/utils"; import { Send, Bot, User, AlertCircle } from "lucide-react"; import { MarkdownRenderer } from "./markdown-renderer"; import { betterFetch } from "@better-fetch/fetch"; import { atom } from "jotai"; interface Message { id: string; role: "user" | "assistant"; content: string; timestamp: Date; isStreaming?: boolean; } export const aiChatModalAtom = atom(false); interface AIChatModalProps { isOpen: boolean; onClose: () => void; } export function AIChatModal({ isOpen, onClose }: AIChatModalProps) { const [messages, setMessages] = useState([]); const [input, setInput] = useState(""); const [isLoading, setIsLoading] = useState(false); const [apiError, setApiError] = useState(null); const [sessionId, setSessionId] = useState(null); const [externalUserId] = useState( () => `better-auth-user-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, ); const messagesEndRef = useRef(null); const abortControllerRef = useRef(null); const scrollToBottom = () => { messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); }; useEffect(() => { scrollToBottom(); }, [messages]); useEffect(() => { return () => { if (abortControllerRef.current) { abortControllerRef.current.abort(); } }; }, []); useEffect(() => { if (!isOpen) { setSessionId(null); setMessages([]); setInput(""); setApiError(null); } }, [isOpen]); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (!input.trim() || isLoading) return; const userMessage: Message = { id: Date.now().toString(), role: "user", content: input.trim(), timestamp: new Date(), }; setMessages((prev) => [...prev, userMessage]); setInput(""); setIsLoading(true); setApiError(null); const thinkingMessage: Message = { id: `thinking-${Date.now()}`, role: "assistant", content: "", timestamp: new Date(), isStreaming: false, }; setMessages((prev) => [...prev, thinkingMessage]); abortControllerRef.current = new AbortController(); try { const payload = { question: userMessage.content, stream: false, // Use non-streaming to get session_id session_id: sessionId, // Use existing session_id if available external_user_id: externalUserId, // Use consistent external_user_id for consistency on getting the context right fetch_existing: false, }; const { data, error } = await betterFetch<{ content?: string; answer?: string; response?: string; session_id?: string; }>("/api/ai-chat", { method: "POST", headers: { "content-type": "application/json", }, body: JSON.stringify(payload), signal: abortControllerRef.current.signal, }); if (error) { console.error("API Error Response:", error); throw new Error(`HTTP ${error.status}: ${error.message}`); } if (data.session_id) { setSessionId(data.session_id); } let answer = ""; if (data.content) { answer = data.content; } else if (data.answer) { answer = data.answer; } else if (data.response) { answer = data.response; } else if (typeof data === "string") { answer = data; } else { console.error("Unexpected response format:", data); throw new Error("Unexpected response format from API"); } await simulateStreamingEffect(answer, thinkingMessage.id); } catch (error) { if (error instanceof Error && error.name === "AbortError") { console.log("Request was aborted"); return; } console.error("Error calling AI API:", error); setMessages((prev) => prev.map((msg) => msg.id.startsWith("thinking-") ? { id: (Date.now() + 1).toString(), role: "assistant" as const, content: `I encountered an error while processing your request. Please try again.`, timestamp: new Date(), isStreaming: false, } : msg, ), ); if (error instanceof Error) { setApiError(error.message); } } finally { setIsLoading(false); abortControllerRef.current = null; } }; const simulateStreamingEffect = async ( fullContent: string, thinkingMessageId: string, ) => { const assistantMessageId = (Date.now() + 1).toString(); let displayedContent = ""; setMessages((prev) => prev.map((msg) => msg.id === thinkingMessageId ? { id: assistantMessageId, role: "assistant" as const, content: "", timestamp: new Date(), isStreaming: true, } : msg, ), ); const words = fullContent.split(" "); for (let i = 0; i < words.length; i++) { displayedContent += (i > 0 ? " " : "") + words[i]; setMessages((prev) => prev.map((msg) => msg.id === assistantMessageId ? { ...msg, content: displayedContent } : msg, ), ); const delay = Math.random() * 50 + 20; await new Promise((resolve) => setTimeout(resolve, delay)); } setMessages((prev) => prev.map((msg) => msg.id === assistantMessageId ? { ...msg, isStreaming: false } : msg, ), ); }; return ( Ask AI About Better Auth Ask questions about Better-Auth and get AI-powered answers {apiError && (
API Error: Something went wrong. Please try again.
)}
{messages.length === 0 ? (

Ask About Better Auth

I'm here to help you with Better Auth questions, setup guides, and implementation tips. Ask me anything!

Try asking:

{[ "How do I set up SSO with Google?", "How to integrate Better Auth with NextJs?", "How to setup Two Factor Authentication?", ].map((question, index) => ( ))}
) : ( messages.map((message) => (
{message.role === "assistant" && (
)}
{message.role === "assistant" ? (
{message.id.startsWith("thinking-") ? (
Thinking...
) : ( <> {message.isStreaming && (
)} )}
) : (

{message.content}

)}
{message.role === "user" && (
)}
)) )}