2024-11-04 14:21:13 -05:00
import { Send , StopCircle , Image as ImageIcon , Paperclip } from "lucide-react"
2024-10-21 13:57:45 -06:00
import { Button } from "../../ui/button"
2024-11-04 14:21:13 -05:00
import { useEffect } from "react"
2024-10-29 01:37:46 -04:00
import { TFile , TFolder } from "@/lib/types"
2024-11-04 14:21:13 -05:00
import { ALLOWED_FILE_TYPES } from "./types"
import { looksLikeCode } from "./lib/chatUtils"
import { 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-11-04 14:21:13 -05:00
2024-10-29 01:37:46 -04:00
// Auto-resize textarea as content changes
useEffect ( ( ) = > {
if ( textareaRef . current ) {
textareaRef . current . style . height = 'auto'
textareaRef . current . style . height = textareaRef . current . scrollHeight + 'px'
}
} , [ 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 )
}
} else if ( e . key === "Backspace" && input === "" && contextTabs . length > 0 ) {
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
const items = Array . from ( e . clipboardData . items ) ;
for ( const item of items ) {
if ( item . type . startsWith ( 'image/' ) ) {
e . preventDefault ( ) ;
const file = item . getAsFile ( ) ;
if ( ! file ) continue ;
try {
2024-11-04 14:21:13 -05:00
// Convert image to base64 string for context tab title and timestamp
2024-10-29 01:37:46 -04:00
const reader = new FileReader ( ) ;
reader . onload = ( ) = > {
const base64String = reader . result as string ;
addContextTab (
"image" ,
2024-11-04 14:21:13 -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
) ;
} ;
reader . readAsDataURL ( file ) ;
} catch ( error ) {
console . error ( 'Error processing pasted image:' , error ) ;
}
return ;
}
}
2024-11-04 14:21:13 -05:00
// Get text from clipboard
2024-10-29 01:37:46 -04:00
const text = e . clipboardData . getData ( 'text' ) ;
// If text doesn't contain newlines or doesn't look like code, let it paste normally
if ( ! text || ! text . includes ( '\n' ) || ! looksLikeCode ( text ) ) {
return ;
}
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-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 ,
{ start : currentSelection.startLineNumber , end : currentSelection.endLineNumber }
) ;
return ;
}
// If we have stored line range from a copy operation in the editor
if ( lastCopiedRangeRef . current ) {
const range = lastCopiedRangeRef . current ;
addContextTab (
"code" ,
` ${ activeFileName } ( ${ range . startLine } - ${ range . endLine } ) ` ,
text ,
{ start : range.startLine , end : range.endLine }
) ;
return ;
}
// For code pasted from outside the editor
addContextTab (
"code" ,
` Pasted Code (1- ${ lines . length } ) ` ,
text ,
{ start : 1 , end : lines.length }
) ;
} ;
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 = ( ) = > {
const input = document . createElement ( 'input' )
input . type = 'file'
input . accept = 'image/*'
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 = ( ) = > {
const input = document . createElement ( 'input' )
input . type = 'file'
input . accept = '.txt,.md,.csv,.json,.js,.ts,.html,.css,.pdf'
input . onchange = ( e ) = > {
const file = ( e . target as HTMLInputElement ) . files ? . [ 0 ]
if ( file ) {
if ( ! ( file . type in ALLOWED_FILE_TYPES ) ) {
alert ( 'Unsupported file type. Please upload text, code, or PDF files.' )
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 */ }
< Button
variant = "ghost"
size = "sm"
2024-10-29 01:37:46 -04: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
}
2024-10-29 01:37:46 -04:00