@@ -8,6 +8,8 @@ import { ChatSettings } from '../chat-settings'
88import { ChatPicker } from '../chat-picker'
99import { LLMModel , LLMModelConfig } from '@/lib/models'
1010import { TemplateId , Templates } from '@/lib/templates'
11+ import { getMatchingCommands , isSlashCommand , extractCommand , SlashCommand } from '@/lib/slash-commands'
12+ import { SlashCommandMenu } from '../slash-command-menu'
1113
1214const cn = ( ...classes : ( string | undefined | null | false ) [ ] ) => classes . filter ( Boolean ) . join ( " " ) ;
1315
@@ -101,7 +103,7 @@ const DialogContent = React.forwardRef<
101103 { ...props }
102104 >
103105 { children }
104- < DialogPrimitive . Close className = "absolute right-4 top-4 z-10 rounded-full bg-muted/80 p-2 hover:bg-muted transition-all" >
106+ < DialogPrimitive . Close className = "absolute right-4 top-4 z-10 rounded-full bg-muted/80 p-2 hover:bg-primary/10 dark:hover:bg- muted transition-all" >
105107 < X className = "h-5 w-5 text-muted-foreground hover:text-primary" />
106108 < span className = "sr-only" > Close</ span >
107109 </ DialogPrimitive . Close >
@@ -131,8 +133,8 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
131133 ( { className, variant = "default" , size = "default" , ...props } , ref ) => {
132134 const variantClasses = {
133135 default : "bg-primary hover:bg-primary/80 text-primary-foreground" ,
134- outline : "border bg-transparent hover:bg-muted" ,
135- ghost : "bg-transparent hover:bg-muted" ,
136+ outline : "border bg-transparent hover:bg-primary/5 dark:hover:bg- muted" ,
137+ ghost : "bg-transparent hover:bg-primary/5 dark:hover:bg- muted" ,
136138 } ;
137139 const sizeClasses = {
138140 default : "h-10 px-4 py-2" ,
@@ -351,12 +353,24 @@ PromptInput.displayName = "PromptInput";
351353interface PromptInputTextareaProps {
352354 disableAutosize ?: boolean ;
353355 placeholder ?: string ;
356+ showSlashCommands ?: boolean ;
357+ matchingCommandsCount ?: number ;
358+ selectedCommandIndex ?: number ;
359+ onCommandNavigate ?: ( direction : 'up' | 'down' ) => void ;
360+ onCommandSelect ?: ( ) => void ;
361+ onCommandCancel ?: ( ) => void ;
354362}
355363const PromptInputTextarea : React . FC < PromptInputTextareaProps & React . ComponentProps < typeof Textarea > > = ( {
356364 className,
357365 onKeyDown,
358366 disableAutosize = false ,
359367 placeholder,
368+ showSlashCommands = false ,
369+ matchingCommandsCount = 0 ,
370+ selectedCommandIndex = 0 ,
371+ onCommandNavigate,
372+ onCommandSelect,
373+ onCommandCancel,
360374 ...props
361375} ) => {
362376 const { value, setValue, maxHeight, onSubmit, disabled } = usePromptInput ( ) ;
@@ -372,6 +386,29 @@ const PromptInputTextarea: React.FC<PromptInputTextareaProps & React.ComponentPr
372386 } , [ value , maxHeight , disableAutosize ] ) ;
373387
374388 const handleKeyDown = ( e : React . KeyboardEvent < HTMLTextAreaElement > ) => {
389+ if ( showSlashCommands ) {
390+ if ( e . key === 'ArrowDown' ) {
391+ e . preventDefault ( ) ;
392+ onCommandNavigate ?.( 'down' ) ;
393+ return ;
394+ }
395+ if ( e . key === 'ArrowUp' ) {
396+ e . preventDefault ( ) ;
397+ onCommandNavigate ?.( 'up' ) ;
398+ return ;
399+ }
400+ if ( e . key === 'Enter' && ! e . shiftKey ) {
401+ e . preventDefault ( ) ;
402+ onCommandSelect ?.( ) ;
403+ return ;
404+ }
405+ if ( e . key === 'Escape' ) {
406+ e . preventDefault ( ) ;
407+ onCommandCancel ?.( ) ;
408+ return ;
409+ }
410+ }
411+
375412 if ( e . key === "Enter" && ! e . shiftKey ) {
376413 e . preventDefault ( ) ;
377414 onSubmit ?.( ) ;
@@ -475,6 +512,9 @@ export const PromptInputBox = React.forwardRef((props: PromptInputBoxProps, ref:
475512 const [ showCanvas , setShowCanvas ] = React . useState ( false ) ;
476513 const uploadInputRef = React . useRef < HTMLInputElement > ( null ) ;
477514 const promptBoxRef = React . useRef < HTMLDivElement > ( null ) ;
515+ const [ showSlashCommands , setShowSlashCommands ] = React . useState ( false ) ;
516+ const [ matchingCommands , setMatchingCommands ] = React . useState < SlashCommand [ ] > ( [ ] ) ;
517+ const [ selectedCommandIndex , setSelectedCommandIndex ] = React . useState ( 0 ) ;
478518
479519 const handleToggleChange = ( value : string ) => {
480520 if ( value === "search" ) {
@@ -562,9 +602,56 @@ export const PromptInputBox = React.forwardRef((props: PromptInputBoxProps, ref:
562602 setInput ( "" ) ;
563603 setFiles ( [ ] ) ;
564604 setFilePreviews ( { } ) ;
605+ setShowSlashCommands ( false ) ;
606+ setMatchingCommands ( [ ] ) ;
607+ setSelectedCommandIndex ( 0 ) ;
608+ }
609+ } ;
610+
611+ const handleCommandSelect = ( command : SlashCommand ) => {
612+ setInput ( command . command + ' ' ) ;
613+ setShowSlashCommands ( false ) ;
614+ setMatchingCommands ( [ ] ) ;
615+ setSelectedCommandIndex ( 0 ) ;
616+ } ;
617+
618+ const handleCommandNavigate = ( direction : 'up' | 'down' ) => {
619+ if ( matchingCommands . length === 0 ) return ;
620+
621+ setSelectedCommandIndex ( prevIndex => {
622+ if ( direction === 'down' ) {
623+ return ( prevIndex + 1 ) % matchingCommands . length ;
624+ } else {
625+ return prevIndex === 0 ? matchingCommands . length - 1 : prevIndex - 1 ;
626+ }
627+ } ) ;
628+ } ;
629+
630+ const handleSelectCurrentCommand = ( ) => {
631+ if ( matchingCommands . length > 0 ) {
632+ handleCommandSelect ( matchingCommands [ selectedCommandIndex ] ) ;
565633 }
566634 } ;
567635
636+ const handleCancelCommands = ( ) => {
637+ setShowSlashCommands ( false ) ;
638+ setMatchingCommands ( [ ] ) ;
639+ setSelectedCommandIndex ( 0 ) ;
640+ } ;
641+
642+ React . useEffect ( ( ) => {
643+ if ( isSlashCommand ( input ) ) {
644+ const commands = getMatchingCommands ( input ) ;
645+ setMatchingCommands ( commands ) ;
646+ setShowSlashCommands ( commands . length > 0 ) ;
647+ setSelectedCommandIndex ( 0 ) ;
648+ } else {
649+ setShowSlashCommands ( false ) ;
650+ setMatchingCommands ( [ ] ) ;
651+ setSelectedCommandIndex ( 0 ) ;
652+ }
653+ } , [ input ] ) ;
654+
568655 const handleStartRecording = ( ) => console . log ( "Started recording" ) ;
569656
570657 const handleStopRecording = ( duration : number ) => {
@@ -645,7 +732,7 @@ export const PromptInputBox = React.forwardRef((props: PromptInputBoxProps, ref:
645732
646733 < div
647734 className = { cn (
648- "transition-all duration-300" ,
735+ "transition-all duration-300 relative " ,
649736 isRecording ? "h-0 overflow-hidden opacity-0" : "opacity-100"
650737 ) }
651738 >
@@ -660,7 +747,21 @@ export const PromptInputBox = React.forwardRef((props: PromptInputBoxProps, ref:
660747 : placeholder
661748 }
662749 className = "text-base"
750+ showSlashCommands = { showSlashCommands }
751+ matchingCommandsCount = { matchingCommands . length }
752+ selectedCommandIndex = { selectedCommandIndex }
753+ onCommandNavigate = { handleCommandNavigate }
754+ onCommandSelect = { handleSelectCurrentCommand }
755+ onCommandCancel = { handleCancelCommands }
663756 />
757+ { showSlashCommands && (
758+ < SlashCommandMenu
759+ commands = { matchingCommands }
760+ selectedIndex = { selectedCommandIndex }
761+ onSelect = { handleCommandSelect }
762+ position = { { top : 0 , left : 0 } }
763+ />
764+ ) }
664765 </ div >
665766
666767 { isRecording && (
@@ -681,7 +782,7 @@ export const PromptInputBox = React.forwardRef((props: PromptInputBoxProps, ref:
681782 < PromptInputAction tooltip = "Upload image" >
682783 < button
683784 onClick = { ( ) => uploadInputRef . current ?. click ( ) }
684- className = "flex h-8 w-8 text-muted-foreground cursor-pointer items-center justify-center rounded-full transition-colors hover:bg-muted/30 hover:text-primary"
785+ className = "flex h-8 w-8 text-muted-foreground cursor-pointer items-center justify-center rounded-full transition-colors hover:bg-primary/5 dark:hover:bg- muted/30 hover:text-primary"
685786 disabled = { isRecording }
686787 >
687788 < Paperclip className = "h-5 w-5 transition-colors" />
@@ -824,10 +925,10 @@ export const PromptInputBox = React.forwardRef((props: PromptInputBoxProps, ref:
824925 className = { cn (
825926 "h-8 w-8 rounded-full transition-all duration-200" ,
826927 isRecording
827- ? "bg-transparent hover:bg-muted/30 text-red-500 hover:text-red-400"
928+ ? "bg-transparent hover:bg-red-50 dark:hover:bg- muted/30 text-red-500 hover:text-red-400"
828929 : hasContent
829930 ? "bg-primary hover:bg-primary/80 text-primary-foreground"
830- : "bg-transparent hover:bg-muted/30 text-muted-foreground hover:text-primary"
931+ : "bg-transparent hover:bg-primary/5 dark:hover:bg- muted/30 text-muted-foreground hover:text-primary"
831932 ) }
832933 onClick = { ( ) => {
833934 if ( isRecording ) setIsRecording ( false ) ;
0 commit comments