2024-10-29 01:37:46 -04:00
import { TFile , TFolder } from "@/lib/types"
2024-11-17 12:35:56 -05:00
import { Image as ImageIcon , Paperclip , Send , StopCircle } from "lucide-react"
import { useEffect } from "react"
import { Button } from "../../ui/button"
2024-11-04 14:21:13 -05:00
import { looksLikeCode } from "./lib/chatUtils"
2024-11-17 12:35:56 -05:00
import { ALLOWED_FILE_TYPES , ChatInputProps } from "./types"
2024-10-14 22:34:26 -04:00
2024-10-21 13:57:45 -06:00
export default function ChatInput ( {
input ,
setInput ,
isGenerating ,
handleSend ,
handleStopGeneration ,
2024-10-29 01:37:46 -04:00
onImageUpload ,
addContextTab ,
activeFileName ,
editorRef ,
lastCopiedRangeRef ,
contextTabs ,
onRemoveTab ,
textareaRef ,
2024-10-21 13:57:45 -06:00
} : ChatInputProps ) {
2024-10-29 01:37:46 -04:00
// Auto-resize textarea as content changes
useEffect ( ( ) = > {
if ( textareaRef . current ) {
2024-11-17 12:35:56 -05:00
textareaRef . current . style . height = "auto"
textareaRef . current . style . height = textareaRef . current . scrollHeight + "px"
2024-10-29 01:37:46 -04:00
}
} , [ input ] )
2024-11-04 14:21:13 -05:00
// Handle keyboard events for sending messages
2024-10-29 01:37:46 -04:00
const handleKeyDown = ( e : React.KeyboardEvent ) = > {
if ( e . key === "Enter" ) {
if ( e . ctrlKey ) {
e . preventDefault ( )
handleSend ( true ) // Send with full context
} else if ( ! e . shiftKey && ! isGenerating ) {
e . preventDefault ( )
handleSend ( false )
}
2024-11-17 12:35:56 -05:00
} else if (
e . key === "Backspace" &&
input === "" &&
contextTabs . length > 0
) {
2024-10-29 01:37:46 -04:00
e . preventDefault ( )
// Remove the last context tab
const lastTab = contextTabs [ contextTabs . length - 1 ]
onRemoveTab ( lastTab . id )
}
}
2024-11-04 14:21:13 -05:00
// Handle paste events for image and code
2024-10-29 01:37:46 -04:00
const handlePaste = async ( e : React.ClipboardEvent ) = > {
// Handle image paste
2024-11-17 12:35:56 -05:00
const items = Array . from ( e . clipboardData . items )
2024-10-29 01:37:46 -04:00
for ( const item of items ) {
2024-11-17 12:35:56 -05:00
if ( item . type . startsWith ( "image/" ) ) {
e . preventDefault ( )
const file = item . getAsFile ( )
if ( ! file ) continue
2024-10-29 01:37:46 -04:00
try {
2024-11-04 14:21:13 -05:00
// Convert image to base64 string for context tab title and timestamp
2024-11-17 12:35:56 -05:00
const reader = new FileReader ( )
2024-10-29 01:37:46 -04:00
reader . onload = ( ) = > {
2024-11-17 12:35:56 -05:00
const base64String = reader . result as string
2024-10-29 01:37:46 -04:00
addContextTab (
"image" ,
2024-11-17 12:35:56 -05:00
` Image ${ new Date ( )
. toLocaleTimeString ( "en-US" , {
hour12 : true ,
hour : "2-digit" ,
minute : "2-digit" ,
} )
. replace ( /(\d{2}):(\d{2})/ , "$1:$2" ) } ` ,
2024-10-29 01:37:46 -04:00
base64String
2024-11-17 12:35:56 -05:00
)
}
reader . readAsDataURL ( file )
2024-10-29 01:37:46 -04:00
} catch ( error ) {
2024-11-17 12:35:56 -05:00
console . error ( "Error processing pasted image:" , error )
2024-10-29 01:37:46 -04:00
}
2024-11-17 12:35:56 -05:00
return
2024-10-29 01:37:46 -04:00
}
}
2024-11-17 12:35:56 -05:00
// Get text from clipboard
const text = e . clipboardData . getData ( "text" )
2024-10-29 01:37:46 -04:00
// If text doesn't contain newlines or doesn't look like code, let it paste normally
2024-11-17 12:35:56 -05:00
if ( ! text || ! text . includes ( "\n" ) || ! looksLikeCode ( text ) ) {
return
2024-10-29 01:37:46 -04:00
}
2024-11-17 12:35:56 -05:00
e . preventDefault ( )
const editor = editorRef . current
const currentSelection = editor ? . getSelection ( )
const lines = text . split ( "\n" )
2024-11-04 14:21:13 -05:00
// TODO: FIX THIS: even when i paste the outside code, it shows the active file name,it works when no tabs are open, just does not work when the tab is open
2024-11-17 12:35:56 -05:00
2024-10-29 01:37:46 -04:00
// If selection exists in editor, use file name and line numbers
if ( currentSelection && ! currentSelection . isEmpty ( ) ) {
addContextTab (
"code" ,
` ${ activeFileName } ( ${ currentSelection . startLineNumber } - ${ currentSelection . endLineNumber } ) ` ,
text ,
2024-11-17 12:35:56 -05:00
{
start : currentSelection.startLineNumber ,
end : currentSelection.endLineNumber ,
}
)
return
2024-10-29 01:37:46 -04:00
}
2024-11-17 12:35:56 -05:00
2024-10-29 01:37:46 -04:00
// If we have stored line range from a copy operation in the editor
if ( lastCopiedRangeRef . current ) {
2024-11-17 12:35:56 -05:00
const range = lastCopiedRangeRef . current
2024-10-29 01:37:46 -04:00
addContextTab (
"code" ,
` ${ activeFileName } ( ${ range . startLine } - ${ range . endLine } ) ` ,
text ,
{ start : range.startLine , end : range.endLine }
2024-11-17 12:35:56 -05:00
)
return
2024-10-29 01:37:46 -04:00
}
2024-11-17 12:35:56 -05:00
2024-10-29 01:37:46 -04:00
// For code pasted from outside the editor
2024-11-17 12:35:56 -05:00
addContextTab ( "code" , ` Pasted Code (1- ${ lines . length } ) ` , text , {
start : 1 ,
end : lines.length ,
} )
}
2024-10-29 01:37:46 -04:00
2024-11-04 14:21:13 -05:00
// Handle image upload from local machine via input
2024-10-29 01:37:46 -04:00
const handleImageUpload = ( ) = > {
2024-11-17 12:35:56 -05:00
const input = document . createElement ( "input" )
input . type = "file"
input . accept = "image/*"
2024-10-29 01:37:46 -04:00
input . onchange = ( e ) = > {
const file = ( e . target as HTMLInputElement ) . files ? . [ 0 ]
if ( file ) onImageUpload ( file )
}
input . click ( )
}
2024-11-04 14:21:13 -05:00
// Helper function to flatten the file tree
2024-10-29 01:37:46 -04:00
const getAllFiles = ( items : ( TFile | TFolder ) [ ] ) : TFile [ ] = > {
return items . reduce ( ( acc : TFile [ ] , item ) = > {
if ( item . type === "file" ) {
acc . push ( item )
} else {
acc . push ( . . . getAllFiles ( item . children ) )
}
return acc
} , [ ] )
}
2024-11-04 14:21:13 -05:00
// Handle file upload from local machine via input
const handleFileUpload = ( ) = > {
2024-11-17 12:35:56 -05:00
const input = document . createElement ( "input" )
input . type = "file"
input . accept = ".txt,.md,.csv,.json,.js,.ts,.html,.css,.pdf"
2024-11-04 14:21:13 -05:00
input . onchange = ( e ) = > {
const file = ( e . target as HTMLInputElement ) . files ? . [ 0 ]
if ( file ) {
if ( ! ( file . type in ALLOWED_FILE_TYPES ) ) {
2024-11-17 12:35:56 -05:00
alert (
"Unsupported file type. Please upload text, code, or PDF files."
)
2024-11-04 14:21:13 -05:00
return
}
const reader = new FileReader ( )
reader . onload = ( ) = > {
addContextTab ( "file" , file . name , reader . result as string )
}
reader . readAsText ( file )
}
}
input . click ( )
}
2024-10-14 22:34:26 -04:00
return (
2024-10-29 01:37:46 -04:00
< div className = "space-y-2" >
< div className = "flex space-x-2 min-w-0" >
< textarea
ref = { textareaRef }
value = { input }
onChange = { ( e ) = > setInput ( e . target . value ) }
onKeyDown = { handleKeyDown }
onPaste = { handlePaste }
className = "flex-grow p-2 border rounded-lg min-w-0 bg-input resize-none overflow-hidden"
placeholder = "Type your message..."
2024-10-21 13:57:45 -06:00
disabled = { isGenerating }
2024-10-29 01:37:46 -04:00
rows = { 1 }
/ >
2024-11-04 14:21:13 -05:00
{ /* Render stop generation button */ }
2024-10-29 01:37:46 -04:00
{ isGenerating ? (
< Button
onClick = { handleStopGeneration }
variant = "destructive"
size = "icon"
className = "h-10 w-10"
>
< StopCircle className = "w-4 h-4" / >
< / Button >
) : (
< Button
onClick = { ( ) = > handleSend ( false ) }
disabled = { isGenerating }
size = "icon"
className = "h-10 w-10"
>
< Send className = "w-4 h-4" / >
< / Button >
) }
< / div >
2024-11-04 14:21:13 -05:00
< div className = "flex items-center justify-end gap-2" >
{ /* Render file upload button */ }
< Button
variant = "ghost"
size = "sm"
className = "h-6 px-2 sm:px-3"
onClick = { handleFileUpload }
>
< Paperclip className = "h-3 w-3 sm:mr-1" / >
< span className = "hidden sm:inline" > File < / span >
< / Button >
{ /* Render image upload button */ }
2024-11-17 12:35:56 -05:00
< Button
variant = "ghost"
2024-11-04 14:21:13 -05:00
size = "sm"
2024-11-17 12:35:56 -05:00
className = "h-6 px-2 sm:px-3"
onClick = { handleImageUpload }
>
< ImageIcon className = "h-3 w-3 sm:mr-1" / >
2024-11-04 14:21:13 -05:00
< span className = "hidden sm:inline" > Image < / span >
< / Button >
2024-10-29 01:37:46 -04:00
< / div >
2024-10-14 22:34:26 -04:00
< / div >
2024-10-21 13:57:45 -06:00
)
2024-10-14 22:34:26 -04:00
}