import { useSocket } from "@/context/SocketContext" import { TFile } from "@/lib/types" import { X } from "lucide-react" import { nanoid } from "nanoid" import { useEffect, useRef, useState } from "react" import LoadingDots from "../../ui/LoadingDots" import ChatInput from "./ChatInput" import ChatMessage from "./ChatMessage" import ContextTabs from "./ContextTabs" import { handleSend, handleStopGeneration } from "./lib/chatUtils" import { AIChatProps, ContextTab, Message } from "./types" export default function AIChat({ activeFileContent, activeFileName, onClose, editorRef, lastCopiedRangeRef, files, }: AIChatProps) { // Initialize socket and messages const { socket } = useSocket() const [messages, setMessages] = useState([]) // Initialize input and state for generating messages const [input, setInput] = useState("") const [isGenerating, setIsGenerating] = useState(false) // Initialize chat container ref and abort controller ref const chatContainerRef = useRef(null) const abortControllerRef = useRef(null) // Initialize context tabs and state for expanding context const [contextTabs, setContextTabs] = useState([]) const [isContextExpanded, setIsContextExpanded] = useState(false) const [isLoading, setIsLoading] = useState(false) // Initialize textarea ref const textareaRef = useRef(null) // Scroll to bottom of chat when messages change useEffect(() => { scrollToBottom() }, [messages]) // Scroll to bottom of chat when messages change const scrollToBottom = () => { if (chatContainerRef.current) { setTimeout(() => { chatContainerRef.current?.scrollTo({ top: chatContainerRef.current.scrollHeight, behavior: "smooth", }) }, 100) } } // Add context tab to context tabs const addContextTab = ( type: string, name: string, content: string, lineRange?: { start: number; end: number } ) => { const newTab = { id: nanoid(), type: type as "file" | "code" | "image", name, content, lineRange, } setContextTabs((prev) => [...prev, newTab]) } // Remove context tab from context tabs const removeContextTab = (id: string) => { setContextTabs((prev) => prev.filter((tab) => tab.id !== id)) } // Add file to context tabs const handleAddFile = (tab: ContextTab) => { setContextTabs((prev) => [...prev, tab]) } // Format code content to remove starting and ending code block markers if they exist const formatCodeContent = (content: string) => { return content.replace(/^```[\w-]*\n/, "").replace(/\n```$/, "") } // Get combined context from context tabs const getCombinedContext = () => { if (contextTabs.length === 0) return "" return contextTabs .map((tab) => { if (tab.type === "file") { const fileExt = tab.name.split(".").pop() || "txt" const cleanContent = formatCodeContent(tab.content) return `File ${tab.name}:\n\`\`\`${fileExt}\n${cleanContent}\n\`\`\`` } else if (tab.type === "code") { const cleanContent = formatCodeContent(tab.content) return `Code from ${tab.name}:\n\`\`\`typescript\n${cleanContent}\n\`\`\`` } return `${tab.name}:\n${tab.content}` }) .join("\n\n") } // Handle sending message with context const handleSendWithContext = () => { const combinedContext = getCombinedContext() handleSend( input, combinedContext, messages, setMessages, setInput, setIsContextExpanded, setIsGenerating, setIsLoading, abortControllerRef, activeFileContent ) // Clear context tabs after sending setContextTabs([]) } // Set context for the chat const setContext = ( context: string | null, name: string, range?: { start: number; end: number } ) => { if (!context) { setContextTabs([]) return } // Always add a new tab instead of updating existing ones addContextTab("code", name, context, range) } return (
CHAT
{activeFileName}
{messages.map((message, messageIndex) => ( // Render chat message component for each message ))} {isLoading && }
{/* Render context tabs component */} setIsContextExpanded(!isContextExpanded)} files={files} socket={socket} onFileSelect={(file: TFile) => { socket?.emit("getFile", { fileId: file.id }, (response: string) => { const fileExt = file.name.split(".").pop() || "txt" const formattedContent = `\`\`\`${fileExt}\n${response}\n\`\`\`` addContextTab("file", file.name, formattedContent) if (textareaRef.current) { textareaRef.current.focus() } }) }} /> {/* Render chat input component */} handleStopGeneration(abortControllerRef)} onImageUpload={(file) => { const reader = new FileReader() reader.onload = (e) => { if (e.target?.result) { addContextTab("image", file.name, e.target.result as string) } } reader.readAsDataURL(file) }} lastCopiedRangeRef={lastCopiedRangeRef} activeFileName={activeFileName} contextTabs={contextTabs.map((tab) => ({ ...tab, title: tab.id, }))} onRemoveTab={removeContextTab} />
) }