"use client"; import { getDefaultModel, type ModelID } from "@/lib/models"; import { Message, useChat } from "@ai-sdk/react"; import { useState, useEffect, useMemo, useCallback } from "react"; import { Textarea } from "./textarea"; import { ProjectOverview } from "./project-overview"; import { Messages } from "./messages"; import { toast } from "sonner"; import { useRouter, useParams } from "next/navigation"; import { getUserId } from "@/lib/user-id"; import { useLocalStorage } from "@/lib/hooks/use-local-storage"; import { useQuery, useQueryClient } from "@tanstack/react-query"; import { convertToUIMessages } from "@/lib/chat-store"; import { type Message as DBMessage } from "@/lib/db/schema"; import { nanoid } from "nanoid"; import { useMCP } from "@/lib/context/mcp-context"; // Type for chat data from DB interface ChatData { id: string; messages: DBMessage[]; createdAt: string; updatedAt: string; } export default function Chat() { const router = useRouter(); const params = useParams(); const chatId = params?.id as string | undefined; const queryClient = useQueryClient(); const [selectedModel, setSelectedModel] = useLocalStorage("selectedModel", ""); const [userId, setUserId] = useState(''); const [generatedChatId, setGeneratedChatId] = useState(''); // Get MCP server data from context const { mcpServersForApi } = useMCP(); // Initialize userId useEffect(() => { setUserId(getUserId()); }, []); useEffect(() => { const fetchDefaultModel = async () => { const defaultModel = await getDefaultModel(); if (!selectedModel) { setSelectedModel(defaultModel); } }; fetchDefaultModel(); }, [selectedModel, setSelectedModel]); // Generate a chat ID if needed useEffect(() => { if (!chatId) { setGeneratedChatId(nanoid()); } }, [chatId]); // Use React Query to fetch chat history const { data: chatData, isLoading: isLoadingChat, error } = useQuery({ queryKey: ['chat', chatId, userId] as const, queryFn: async ({ queryKey }) => { const [_, chatId, userId] = queryKey; if (!chatId || !userId) return null; const response = await fetch(`/api/chats/${chatId}`, { headers: { 'x-user-id': userId } }); if (!response.ok) { // For 404, return empty chat data instead of throwing if (response.status === 404) { return { id: chatId, messages: [], createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() }; } throw new Error('Failed to load chat'); } return response.json() as Promise; }, enabled: !!chatId && !!userId, retry: 1, staleTime: 1000 * 60 * 5, // 5 minutes refetchOnWindowFocus: false }); // Handle query errors useEffect(() => { if (error) { console.error('Error loading chat history:', error); toast.error('Failed to load chat history'); } }, [error]); // Prepare initial messages from query data const initialMessages = useMemo(() => { if (!chatData || !chatData.messages || chatData.messages.length === 0) { return []; } // Convert DB messages to UI format, then ensure it matches the Message type from @ai-sdk/react const uiMessages = convertToUIMessages(chatData.messages); return uiMessages.map(msg => ({ id: msg.id, role: msg.role as Message['role'], // Ensure role is properly typed content: msg.content, parts: msg.parts, } as Message)); }, [chatData]); const { messages, input, handleInputChange, handleSubmit, status, stop } = useChat({ id: chatId || generatedChatId, // Use generated ID if no chatId in URL initialMessages, maxSteps: 20, body: { selectedModel, mcpServers: mcpServersForApi, chatId: chatId || generatedChatId, // Use generated ID if no chatId in URL userId, }, experimental_throttle: 75, onFinish: () => { // Invalidate the chats query to refresh the sidebar if (userId) { queryClient.invalidateQueries({ queryKey: ['chats', userId] }); } }, onError: (error) => { toast.error( error.message.length > 0 ? error.message : "An error occurred, please try again later.", { position: "top-center", richColors: true }, ); }, }); // Custom submit handler const handleFormSubmit = useCallback((e: React.FormEvent) => { e.preventDefault(); if (!chatId && generatedChatId && input.trim()) { // If this is a new conversation, redirect to the chat page with the generated ID const effectiveChatId = generatedChatId; // Submit the form handleSubmit(e); // Redirect to the chat page with the generated ID router.push(`/chat/${effectiveChatId}`); } else { // Normal submission for existing chats handleSubmit(e); } }, [chatId, generatedChatId, input, handleSubmit, router]); // Save messages whenever they change (with debouncing) useEffect(() => { const effectiveChatId = chatId || generatedChatId; if (!userId || !effectiveChatId || messages.length === 0) { return; } // Debounce message saving to avoid too many requests const timeoutId = setTimeout(async () => { try { const response = await fetch(`/api/chats/${effectiveChatId}/messages`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-user-id': userId, }, body: JSON.stringify({ messages }), }); if (!response.ok) { console.error('Failed to save messages:', await response.text()); } } catch (error) { console.error('Failed to save messages:', error); } }, 1000); // Save after 1 second of no changes return () => clearTimeout(timeoutId); }, [messages, chatId, generatedChatId, userId]); const isLoading = status === "streaming" || status === "submitted" || isLoadingChat; return (
{messages.length === 0 && !isLoadingChat ? (