2025-03-23 12:04:33 +00:00
"use client" ;
2025-03-22 13:15:51 +00:00
2025-03-23 12:56:47 +00:00
import React , { useCallback , useRef , useEffect , useState } from "react" ;
2025-03-23 12:04:33 +00:00
import { Button } from "@/components/ui/button" ;
import { Textarea } from "@/components/ui/textarea" ;
2025-03-26 08:58:46 +00:00
import { ResetWarningModal } from "@/components/reset-warning-modal" ;
2025-03-23 13:54:21 +00:00
import {
Loader2 ,
Send ,
RotateCcw ,
Image as ImageIcon ,
X ,
History ,
} from "lucide-react" ;
2025-03-26 10:32:33 +00:00
import { ButtonWithTooltip } from "@/components/button-with-tooltip" ;
2025-03-23 12:04:33 +00:00
import Image from "next/image" ;
2025-03-22 13:15:51 +00:00
2025-03-26 00:30:00 +00:00
import { useDiagram } from "@/contexts/diagram-context" ;
2025-03-26 08:51:21 +00:00
import { HistoryDialog } from "@/components/history-dialog" ;
2025-03-26 00:30:00 +00:00
2025-03-22 13:15:51 +00:00
interface ChatInputProps {
2025-03-23 12:04:33 +00:00
input : string ;
status : "submitted" | "streaming" | "ready" | "error" ;
onSubmit : ( e : React.FormEvent < HTMLFormElement > ) = > void ;
onChange : ( e : React.ChangeEvent < HTMLTextAreaElement > ) = > void ;
setMessages : ( messages : any [ ] ) = > void ;
files? : FileList ;
onFileChange ? : ( files : FileList | undefined ) = > void ;
2025-03-23 13:54:21 +00:00
showHistory? : boolean ;
setShowHistory ? : ( show : boolean ) = > void ;
2025-03-22 13:15:51 +00:00
}
2025-03-23 11:03:25 +00:00
export function ChatInput ( {
input ,
status ,
onSubmit ,
onChange ,
setMessages ,
files ,
2025-03-23 12:04:33 +00:00
onFileChange ,
2025-03-23 13:54:21 +00:00
showHistory = false ,
setShowHistory = ( ) = > { } ,
2025-03-23 11:03:25 +00:00
} : ChatInputProps ) {
2025-03-26 00:30:00 +00:00
const { loadDiagram : onDisplayChart , diagramHistory } = useDiagram ( ) ;
2025-03-23 12:04:33 +00:00
const textareaRef = useRef < HTMLTextAreaElement > ( null ) ;
const fileInputRef = useRef < HTMLInputElement > ( null ) ;
2025-03-23 12:56:47 +00:00
const [ isDragging , setIsDragging ] = useState ( false ) ;
2025-03-23 13:31:26 +00:00
const [ showClearDialog , setShowClearDialog ] = useState ( false ) ;
2025-03-22 13:15:51 +00:00
// Auto-resize textarea based on content
const adjustTextareaHeight = useCallback ( ( ) = > {
2025-03-23 12:04:33 +00:00
const textarea = textareaRef . current ;
2025-03-22 13:15:51 +00:00
if ( textarea ) {
2025-03-23 12:04:33 +00:00
textarea . style . height = "auto" ;
textarea . style . height = ` ${ Math . min ( textarea . scrollHeight , 200 ) } px ` ;
2025-03-22 13:15:51 +00:00
}
2025-03-23 12:04:33 +00:00
} , [ ] ) ;
2025-03-22 13:15:51 +00:00
useEffect ( ( ) = > {
2025-03-23 12:04:33 +00:00
adjustTextareaHeight ( ) ;
} , [ input , adjustTextareaHeight ] ) ;
2025-03-22 13:15:51 +00:00
// Handle keyboard shortcuts
const handleKeyDown = ( e : React.KeyboardEvent ) = > {
if ( ( e . metaKey || e . ctrlKey ) && e . key === "Enter" ) {
2025-03-23 12:04:33 +00:00
e . preventDefault ( ) ;
const form = e . currentTarget . closest ( "form" ) ;
2025-03-22 13:26:14 +00:00
if ( form && input . trim ( ) && status !== "streaming" ) {
2025-03-23 12:04:33 +00:00
form . requestSubmit ( ) ;
2025-03-22 13:15:51 +00:00
}
}
2025-03-23 12:04:33 +00:00
} ;
2025-03-22 13:15:51 +00:00
2025-03-23 11:03:25 +00:00
// Handle file changes
const handleFileChange = ( e : React.ChangeEvent < HTMLInputElement > ) = > {
if ( onFileChange ) {
2025-03-23 12:04:33 +00:00
onFileChange ( e . target . files || undefined ) ;
2025-03-23 11:03:25 +00:00
}
2025-03-23 12:04:33 +00:00
} ;
2025-03-23 11:03:25 +00:00
// Clear file selection
const clearFiles = ( ) = > {
if ( fileInputRef . current ) {
2025-03-23 12:04:33 +00:00
fileInputRef . current . value = "" ;
2025-03-23 11:03:25 +00:00
}
if ( onFileChange ) {
2025-03-23 12:04:33 +00:00
onFileChange ( undefined ) ;
2025-03-23 11:03:25 +00:00
}
2025-03-23 12:04:33 +00:00
} ;
2025-03-23 11:03:25 +00:00
// Trigger file input click
const triggerFileInput = ( ) = > {
2025-03-23 12:04:33 +00:00
fileInputRef . current ? . click ( ) ;
} ;
2025-03-23 11:03:25 +00:00
2025-03-23 12:56:47 +00:00
// Handle drag events
const handleDragOver = ( e : React.DragEvent < HTMLFormElement > ) = > {
e . preventDefault ( ) ;
e . stopPropagation ( ) ;
setIsDragging ( true ) ;
} ;
const handleDragLeave = ( e : React.DragEvent < HTMLFormElement > ) = > {
e . preventDefault ( ) ;
e . stopPropagation ( ) ;
setIsDragging ( false ) ;
} ;
const handleDrop = ( e : React.DragEvent < HTMLFormElement > ) = > {
e . preventDefault ( ) ;
e . stopPropagation ( ) ;
setIsDragging ( false ) ;
if ( status === "streaming" ) return ;
const droppedFiles = e . dataTransfer . files ;
// Only process image files
if ( droppedFiles . length > 0 ) {
const imageFiles = Array . from ( droppedFiles ) . filter ( ( file ) = >
file . type . startsWith ( "image/" )
) ;
if ( imageFiles . length > 0 && onFileChange ) {
// Create a new FileList-like object with only image files
const dt = new DataTransfer ( ) ;
imageFiles . forEach ( ( file ) = > dt . items . add ( file ) ) ;
onFileChange ( dt . files ) ;
}
}
} ;
2025-03-23 13:31:26 +00:00
// Handle clearing conversation and diagram
const handleClear = ( ) = > {
setMessages ( [ ] ) ;
onDisplayChart ( ` <mxfile host="embed.diagrams.net" agent="Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36" version="26.1.1">
< diagram name = "Page-1" id = "NsivuNt5aJDXaP8udwGv" >
< mxGraphModel dx = "394" dy = "700" grid = "1" gridSize = "10" guides = "1" tooltips = "1" connect = "1" arrows = "1" fold = "1" page = "1" pageScale = "1" pageWidth = "850" pageHeight = "1100" math = "0" shadow = "0" >
< root >
< / root >
< / mxGraphModel >
< / diagram >
< / mxfile > ` );
setShowClearDialog ( false ) ;
} ;
2025-03-22 13:15:51 +00:00
return (
2025-03-23 12:56:47 +00:00
< form
onSubmit = { onSubmit }
className = { ` w-full space-y-2 ${
isDragging
? "border-2 border-dashed border-primary p-4 rounded-lg bg-muted/20"
: ""
} ` }
onDragOver = { handleDragOver }
onDragLeave = { handleDragLeave }
onDrop = { handleDrop }
>
2025-03-23 11:03:25 +00:00
{ /* File preview area */ }
{ files && files . length > 0 && (
< div className = "flex flex-wrap gap-2 mt-2 p-2 bg-muted/50 rounded-md" >
{ Array . from ( files ) . map ( ( file , index ) = > (
< div key = { index } className = "relative group" >
< div className = "w-20 h-20 border rounded-md overflow-hidden bg-muted" >
2025-03-23 12:04:33 +00:00
{ file . type . startsWith ( "image/" ) ? (
2025-03-23 11:03:25 +00:00
< Image
src = { URL . createObjectURL ( file ) }
alt = { file . name }
width = { 80 }
height = { 80 }
className = "object-cover w-full h-full"
/ >
) : (
< div className = "flex items-center justify-center h-full text-xs text-center p-1" >
{ file . name }
< / div >
) }
< / div >
< button
type = "button"
onClick = { clearFiles }
2025-03-23 12:56:47 +00:00
className = "absolute -top-2 -right-2 bg-destructive rounded-full p-1 opacity-0 group-hover:opacity-100 transition-opacity"
2025-03-23 11:03:25 +00:00
aria - label = "Remove file"
>
< X className = "h-3 w-3" / >
< / button >
< / div >
) ) }
< / div >
) }
2025-03-22 13:15:51 +00:00
< Textarea
ref = { textareaRef }
value = { input }
onChange = { onChange }
onKeyDown = { handleKeyDown }
placeholder = "Describe what changes you want to make to the diagram... (Press Cmd/Ctrl + Enter to send)"
2025-03-22 13:26:14 +00:00
disabled = { status === "streaming" }
2025-03-22 13:15:51 +00:00
aria - label = "Chat input"
2025-03-23 14:36:21 +00:00
className = "min-h-[80px] resize-none transition-all duration-200 px-1 py-0"
2025-03-22 13:15:51 +00:00
/ >
2025-03-23 11:03:25 +00:00
2025-03-23 12:04:33 +00:00
< div className = "flex items-center gap-2" >
< div className = "mr-auto" >
2025-03-26 10:32:33 +00:00
< ButtonWithTooltip
type = "button"
variant = "ghost"
size = "icon"
onClick = { ( ) = > setShowClearDialog ( true ) }
tooltipContent = "Clear current conversation and diagram"
>
< RotateCcw className = "mr-2 h-4 w-4" / >
< / ButtonWithTooltip >
2025-03-23 13:31:26 +00:00
{ /* Warning Modal */ }
2025-03-26 08:58:46 +00:00
< ResetWarningModal
2025-03-23 13:31:26 +00:00
open = { showClearDialog }
onOpenChange = { setShowClearDialog }
2025-03-26 08:58:46 +00:00
onClear = { handleClear }
/ >
2025-03-23 13:54:21 +00:00
2025-03-26 08:51:21 +00:00
< HistoryDialog
showHistory = { showHistory }
setShowHistory = { setShowHistory }
/ >
2025-03-23 12:04:33 +00:00
< / div >
2025-03-23 11:03:25 +00:00
< div className = "flex gap-2" >
2025-03-23 13:54:21 +00:00
{ /* History Button */ }
2025-03-26 10:32:33 +00:00
< ButtonWithTooltip
type = "button"
variant = "outline"
size = "icon"
onClick = { ( ) = > setShowHistory ( true ) }
disabled = {
status === "streaming" ||
diagramHistory . length === 0
}
title = "Diagram History"
tooltipContent = "View diagram history"
>
< History className = "h-4 w-4" / >
< / ButtonWithTooltip >
2025-03-23 13:54:21 +00:00
2025-03-23 11:03:25 +00:00
< Button
type = "button"
variant = "outline"
size = "icon"
onClick = { triggerFileInput }
disabled = { status === "streaming" }
title = "Upload image"
>
< ImageIcon className = "h-4 w-4" / >
< / Button >
< input
type = "file"
ref = { fileInputRef }
className = "hidden"
onChange = { handleFileChange }
accept = "image/*"
multiple
disabled = { status === "streaming" }
/ >
< / div >
2025-03-22 13:15:51 +00:00
< Button
type = "submit"
2025-03-22 13:26:14 +00:00
disabled = { status === "streaming" || ! input . trim ( ) }
2025-03-22 13:15:51 +00:00
className = "transition-opacity"
2025-03-23 12:04:33 +00:00
aria - label = {
status === "streaming"
? "Sending message..."
: "Send message"
}
2025-03-22 13:15:51 +00:00
>
2025-03-22 13:26:14 +00:00
{ status === "streaming" ? (
2025-03-22 13:15:51 +00:00
< Loader2 className = "mr-2 h-4 w-4 animate-spin" / >
) : (
< Send className = "mr-2 h-4 w-4" / >
) }
Send
< / Button >
< / div >
< / form >
2025-03-23 12:04:33 +00:00
) ;
2025-03-22 13:15:51 +00:00
}