2024-11-17 12:35:56 -05:00
|
|
|
import { useSocket } from "@/context/SocketContext"
|
|
|
|
import { TFile } from "@/lib/types"
|
2024-11-23 21:55:44 -05:00
|
|
|
import { X, ChevronDown } from "lucide-react"
|
2024-11-17 12:35:56 -05:00
|
|
|
import { nanoid } from "nanoid"
|
2024-10-21 13:57:45 -06:00
|
|
|
import { useEffect, useRef, useState } from "react"
|
|
|
|
import LoadingDots from "../../ui/LoadingDots"
|
|
|
|
import ChatInput from "./ChatInput"
|
|
|
|
import ChatMessage from "./ChatMessage"
|
2024-10-29 01:37:46 -04:00
|
|
|
import ContextTabs from "./ContextTabs"
|
2024-10-21 13:57:45 -06:00
|
|
|
import { handleSend, handleStopGeneration } from "./lib/chatUtils"
|
2024-11-17 12:35:56 -05:00
|
|
|
import { AIChatProps, ContextTab, Message } from "./types"
|
2024-10-29 01:37:46 -04:00
|
|
|
|
2024-10-21 13:57:45 -06:00
|
|
|
export default function AIChat({
|
|
|
|
activeFileContent,
|
|
|
|
activeFileName,
|
|
|
|
onClose,
|
2024-10-29 01:37:46 -04:00
|
|
|
editorRef,
|
|
|
|
lastCopiedRangeRef,
|
|
|
|
files,
|
2024-11-24 00:22:10 -05:00
|
|
|
templateType,
|
2024-10-29 01:37:46 -04:00
|
|
|
}: AIChatProps) {
|
2024-11-04 14:21:13 -05:00
|
|
|
// Initialize socket and messages
|
2024-10-29 01:37:46 -04:00
|
|
|
const { socket } = useSocket()
|
2024-10-21 13:57:45 -06:00
|
|
|
const [messages, setMessages] = useState<Message[]>([])
|
2024-11-04 14:21:13 -05:00
|
|
|
|
|
|
|
// Initialize input and state for generating messages
|
2024-10-21 13:57:45 -06:00
|
|
|
const [input, setInput] = useState("")
|
|
|
|
const [isGenerating, setIsGenerating] = useState(false)
|
2024-11-04 14:21:13 -05:00
|
|
|
|
|
|
|
// Initialize chat container ref and abort controller ref
|
2024-10-21 13:57:45 -06:00
|
|
|
const chatContainerRef = useRef<HTMLDivElement>(null)
|
|
|
|
const abortControllerRef = useRef<AbortController | null>(null)
|
2024-11-04 14:21:13 -05:00
|
|
|
|
|
|
|
// Initialize context tabs and state for expanding context
|
2024-10-29 01:37:46 -04:00
|
|
|
const [contextTabs, setContextTabs] = useState<ContextTab[]>([])
|
2024-10-21 13:57:45 -06:00
|
|
|
const [isContextExpanded, setIsContextExpanded] = useState(false)
|
|
|
|
const [isLoading, setIsLoading] = useState(false)
|
2024-11-04 14:21:13 -05:00
|
|
|
|
|
|
|
// Initialize textarea ref
|
2024-10-29 01:37:46 -04:00
|
|
|
const textareaRef = useRef<HTMLTextAreaElement>(null)
|
2024-10-14 22:34:26 -04:00
|
|
|
|
2024-11-23 21:55:44 -05:00
|
|
|
// state variables for auto scroll and scroll button
|
|
|
|
const [autoScroll, setAutoScroll] = useState(true)
|
|
|
|
const [showScrollButton, setShowScrollButton] = useState(false)
|
|
|
|
|
|
|
|
// scroll to bottom of chat when messages change
|
2024-10-14 22:34:26 -04:00
|
|
|
useEffect(() => {
|
2024-11-23 21:55:44 -05:00
|
|
|
if (autoScroll) {
|
|
|
|
scrollToBottom()
|
2024-10-14 22:34:26 -04:00
|
|
|
}
|
2024-11-23 21:55:44 -05:00
|
|
|
}, [messages, autoScroll])
|
|
|
|
|
|
|
|
// scroll to bottom of chat when messages change
|
|
|
|
const scrollToBottom = (force: boolean = false) => {
|
|
|
|
if (!chatContainerRef.current || (!autoScroll && !force)) return
|
|
|
|
|
|
|
|
chatContainerRef.current.scrollTo({
|
|
|
|
top: chatContainerRef.current.scrollHeight,
|
|
|
|
behavior: force ? "smooth" : "auto",
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
// function to handle scroll events
|
|
|
|
const handleScroll = () => {
|
|
|
|
if (!chatContainerRef.current) return
|
|
|
|
|
|
|
|
const { scrollTop, scrollHeight, clientHeight } = chatContainerRef.current
|
|
|
|
const isAtBottom = Math.abs(scrollHeight - scrollTop - clientHeight) < 50
|
|
|
|
|
|
|
|
setAutoScroll(isAtBottom)
|
|
|
|
setShowScrollButton(!isAtBottom)
|
2024-10-21 13:57:45 -06:00
|
|
|
}
|
2024-10-14 22:34:26 -04:00
|
|
|
|
2024-11-23 21:55:44 -05:00
|
|
|
// scroll event listener
|
|
|
|
useEffect(() => {
|
|
|
|
const container = chatContainerRef.current
|
|
|
|
if (container) {
|
|
|
|
container.addEventListener('scroll', handleScroll)
|
|
|
|
return () => container.removeEventListener('scroll', handleScroll)
|
|
|
|
}
|
|
|
|
}, [])
|
|
|
|
|
2024-11-04 14:21:13 -05:00
|
|
|
// Add context tab to context tabs
|
2024-11-17 12:35:56 -05:00
|
|
|
const addContextTab = (
|
|
|
|
type: string,
|
|
|
|
name: string,
|
|
|
|
content: string,
|
|
|
|
lineRange?: { start: number; end: number }
|
|
|
|
) => {
|
2024-10-29 01:37:46 -04:00
|
|
|
const newTab = {
|
|
|
|
id: nanoid(),
|
|
|
|
type: type as "file" | "code" | "image",
|
|
|
|
name,
|
|
|
|
content,
|
2024-11-17 12:35:56 -05:00
|
|
|
lineRange,
|
2024-10-29 01:37:46 -04:00
|
|
|
}
|
2024-11-17 12:35:56 -05:00
|
|
|
setContextTabs((prev) => [...prev, newTab])
|
2024-10-29 01:37:46 -04:00
|
|
|
}
|
|
|
|
|
2024-11-04 14:21:13 -05:00
|
|
|
// Remove context tab from context tabs
|
2024-10-29 01:37:46 -04:00
|
|
|
const removeContextTab = (id: string) => {
|
2024-11-17 12:35:56 -05:00
|
|
|
setContextTabs((prev) => prev.filter((tab) => tab.id !== id))
|
2024-10-29 01:37:46 -04:00
|
|
|
}
|
|
|
|
|
2024-11-04 14:21:13 -05:00
|
|
|
// Add file to context tabs
|
|
|
|
const handleAddFile = (tab: ContextTab) => {
|
2024-11-17 12:35:56 -05:00
|
|
|
setContextTabs((prev) => [...prev, tab])
|
2024-10-29 01:37:46 -04:00
|
|
|
}
|
|
|
|
|
2024-11-04 14:21:13 -05:00
|
|
|
// Format code content to remove starting and ending code block markers if they exist
|
2024-10-29 01:37:46 -04:00
|
|
|
const formatCodeContent = (content: string) => {
|
2024-11-17 12:35:56 -05:00
|
|
|
return content.replace(/^```[\w-]*\n/, "").replace(/\n```$/, "")
|
2024-10-29 01:37:46 -04:00
|
|
|
}
|
|
|
|
|
2024-11-04 14:21:13 -05:00
|
|
|
// Get combined context from context tabs
|
2024-10-29 01:37:46 -04:00
|
|
|
const getCombinedContext = () => {
|
2024-11-17 12:35:56 -05:00
|
|
|
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")
|
2024-10-29 01:37:46 -04:00
|
|
|
}
|
|
|
|
|
2024-11-04 14:21:13 -05:00
|
|
|
// Handle sending message with context
|
2024-10-29 01:37:46 -04:00
|
|
|
const handleSendWithContext = () => {
|
|
|
|
const combinedContext = getCombinedContext()
|
|
|
|
handleSend(
|
|
|
|
input,
|
|
|
|
combinedContext,
|
|
|
|
messages,
|
|
|
|
setMessages,
|
|
|
|
setInput,
|
|
|
|
setIsContextExpanded,
|
|
|
|
setIsGenerating,
|
|
|
|
setIsLoading,
|
|
|
|
abortControllerRef,
|
2024-11-24 00:22:10 -05:00
|
|
|
activeFileContent,
|
|
|
|
false,
|
|
|
|
templateType
|
2024-10-29 01:37:46 -04:00
|
|
|
)
|
|
|
|
// Clear context tabs after sending
|
|
|
|
setContextTabs([])
|
|
|
|
}
|
|
|
|
|
2024-11-04 14:21:13 -05:00
|
|
|
// Set context for the chat
|
|
|
|
const setContext = (
|
2024-11-17 12:35:56 -05:00
|
|
|
context: string | null,
|
|
|
|
name: string,
|
|
|
|
range?: { start: number; end: number }
|
2024-11-04 14:21:13 -05:00
|
|
|
) => {
|
2024-10-29 01:37:46 -04:00
|
|
|
if (!context) {
|
|
|
|
setContextTabs([])
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2024-11-04 14:21:13 -05:00
|
|
|
// Always add a new tab instead of updating existing ones
|
2024-11-17 12:35:56 -05:00
|
|
|
addContextTab("code", name, context, range)
|
2024-10-29 01:37:46 -04:00
|
|
|
}
|
|
|
|
|
2024-10-14 22:34:26 -04:00
|
|
|
return (
|
|
|
|
<div className="flex flex-col h-screen w-full">
|
2024-10-14 23:01:25 -04:00
|
|
|
<div className="flex justify-between items-center p-2 border-b">
|
|
|
|
<span className="text-muted-foreground/50 font-medium">CHAT</span>
|
2024-10-20 23:23:04 -04:00
|
|
|
<div className="flex items-center h-full">
|
2024-10-21 13:57:45 -06:00
|
|
|
<span className="text-muted-foreground/50 font-medium">
|
|
|
|
{activeFileName}
|
|
|
|
</span>
|
2024-10-20 23:23:04 -04:00
|
|
|
<div className="mx-2 h-full w-px bg-muted-foreground/20"></div>
|
|
|
|
<button
|
|
|
|
onClick={onClose}
|
|
|
|
className="text-muted-foreground/50 hover:text-muted-foreground focus:outline-none"
|
|
|
|
aria-label="Close AI Chat"
|
|
|
|
>
|
|
|
|
<X size={18} />
|
|
|
|
</button>
|
|
|
|
</div>
|
2024-10-14 23:01:25 -04:00
|
|
|
</div>
|
2024-10-21 13:57:45 -06:00
|
|
|
<div
|
|
|
|
ref={chatContainerRef}
|
2024-11-23 21:55:44 -05:00
|
|
|
className="flex-grow overflow-y-auto p-4 space-y-4 relative"
|
2024-10-21 13:57:45 -06:00
|
|
|
>
|
2024-10-14 22:34:26 -04:00
|
|
|
{messages.map((message, messageIndex) => (
|
2024-11-17 12:35:56 -05:00
|
|
|
// Render chat message component for each message
|
2024-10-21 13:57:45 -06:00
|
|
|
<ChatMessage
|
|
|
|
key={messageIndex}
|
|
|
|
message={message}
|
2024-10-14 22:34:26 -04:00
|
|
|
setContext={setContext}
|
|
|
|
setIsContextExpanded={setIsContextExpanded}
|
2024-10-29 01:37:46 -04:00
|
|
|
socket={socket}
|
2024-10-14 22:34:26 -04:00
|
|
|
/>
|
|
|
|
))}
|
|
|
|
{isLoading && <LoadingDots />}
|
2024-11-23 21:55:44 -05:00
|
|
|
|
|
|
|
{/* Add scroll to bottom button */}
|
|
|
|
{showScrollButton && (
|
|
|
|
<button
|
|
|
|
onClick={() => scrollToBottom(true)}
|
|
|
|
className="fixed bottom-36 right-6 bg-primary text-primary-foreground rounded-md border border-primary p-0.5 shadow-lg hover:bg-primary/90 transition-all"
|
|
|
|
aria-label="Scroll to bottom"
|
|
|
|
>
|
|
|
|
<ChevronDown className="h-5 w-5" />
|
|
|
|
</button>
|
|
|
|
)}
|
2024-10-14 22:34:26 -04:00
|
|
|
</div>
|
|
|
|
<div className="p-4 border-t mb-14">
|
2024-11-04 14:21:13 -05:00
|
|
|
{/* Render context tabs component */}
|
2024-10-29 01:37:46 -04:00
|
|
|
<ContextTabs
|
|
|
|
activeFileName={activeFileName}
|
|
|
|
onAddFile={handleAddFile}
|
|
|
|
contextTabs={contextTabs}
|
|
|
|
onRemoveTab={removeContextTab}
|
|
|
|
isExpanded={isContextExpanded}
|
|
|
|
onToggleExpand={() => setIsContextExpanded(!isContextExpanded)}
|
|
|
|
files={files}
|
|
|
|
socket={socket}
|
|
|
|
onFileSelect={(file: TFile) => {
|
|
|
|
socket?.emit("getFile", { fileId: file.id }, (response: string) => {
|
2024-11-17 12:35:56 -05:00
|
|
|
const fileExt = file.name.split(".").pop() || "txt"
|
2024-10-29 01:37:46 -04:00
|
|
|
const formattedContent = `\`\`\`${fileExt}\n${response}\n\`\`\``
|
2024-11-17 12:35:56 -05:00
|
|
|
addContextTab("file", file.name, formattedContent)
|
2024-10-29 01:37:46 -04:00
|
|
|
if (textareaRef.current) {
|
|
|
|
textareaRef.current.focus()
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}}
|
2024-10-14 22:34:26 -04:00
|
|
|
/>
|
2024-11-04 14:21:13 -05:00
|
|
|
{/* Render chat input component */}
|
2024-10-21 13:57:45 -06:00
|
|
|
<ChatInput
|
2024-10-29 01:37:46 -04:00
|
|
|
textareaRef={textareaRef}
|
|
|
|
addContextTab={addContextTab}
|
|
|
|
editorRef={editorRef}
|
2024-10-14 22:34:26 -04:00
|
|
|
input={input}
|
|
|
|
setInput={setInput}
|
|
|
|
isGenerating={isGenerating}
|
2024-10-29 01:37:46 -04:00
|
|
|
handleSend={handleSendWithContext}
|
2024-10-14 22:34:26 -04:00
|
|
|
handleStopGeneration={() => handleStopGeneration(abortControllerRef)}
|
2024-10-29 01:37:46 -04:00
|
|
|
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}
|
2024-11-17 12:35:56 -05:00
|
|
|
contextTabs={contextTabs.map((tab) => ({
|
2024-10-29 01:37:46 -04:00
|
|
|
...tab,
|
2024-11-17 12:35:56 -05:00
|
|
|
title: tab.id,
|
2024-10-29 01:37:46 -04:00
|
|
|
}))}
|
|
|
|
onRemoveTab={removeContextTab}
|
2024-10-14 22:34:26 -04:00
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
</div>
|
2024-10-21 13:57:45 -06:00
|
|
|
)
|
2024-10-14 22:34:26 -04:00
|
|
|
}
|