AskAIWidget.tsx111 lines · main
| 1 | import { ChangeEvent, KeyboardEvent, useCallback, useEffect, useRef } from 'react' |
| 2 | import { Button, ExpandingTextArea } from 'ui' |
| 3 | |
| 4 | interface AskAIWidgetProps { |
| 5 | value: string |
| 6 | onChange: (value: string) => void |
| 7 | onSubmit: (prompt: string) => void |
| 8 | onAccept?: () => void |
| 9 | onReject?: () => void |
| 10 | onCancel?: () => void |
| 11 | isDiffVisible: boolean |
| 12 | isLoading?: boolean |
| 13 | } |
| 14 | |
| 15 | export const AskAIWidget = ({ |
| 16 | value, |
| 17 | onChange, |
| 18 | onSubmit, |
| 19 | onAccept, |
| 20 | onReject, |
| 21 | onCancel, |
| 22 | isDiffVisible, |
| 23 | isLoading = false, |
| 24 | }: AskAIWidgetProps) => { |
| 25 | const textAreaRef = useRef<HTMLTextAreaElement>(null) |
| 26 | |
| 27 | useEffect(() => { |
| 28 | setTimeout(() => { |
| 29 | textAreaRef.current?.focus() |
| 30 | textAreaRef.current?.setSelectionRange(value.length, value.length) |
| 31 | }, 100) |
| 32 | }, []) |
| 33 | |
| 34 | const handleSubmit = useCallback(() => { |
| 35 | if (value.trim() && !isLoading) { |
| 36 | onSubmit(value) |
| 37 | } |
| 38 | }, [value, isLoading, onSubmit]) |
| 39 | |
| 40 | const handleChange = useCallback( |
| 41 | (e: ChangeEvent<HTMLTextAreaElement>) => { |
| 42 | onChange(e.target.value) |
| 43 | }, |
| 44 | [onChange] |
| 45 | ) |
| 46 | |
| 47 | const handleKeyDown = useCallback( |
| 48 | (e: KeyboardEvent<HTMLTextAreaElement>) => { |
| 49 | if (e.key === 'Enter' && !e.shiftKey && !e.metaKey && !e.ctrlKey) { |
| 50 | e.preventDefault() |
| 51 | handleSubmit() |
| 52 | } |
| 53 | }, |
| 54 | [handleSubmit] |
| 55 | ) |
| 56 | |
| 57 | return ( |
| 58 | <div className="overflow-hidden rounded-md p-0 bg-popover border border-foreground/20 focus-within:border-foreground/30 shadow-xl text-sm max-w-xl"> |
| 59 | <ExpandingTextArea |
| 60 | ref={textAreaRef} |
| 61 | className="bg-transparent border-0 outline-0 ring-0 ring-offset-0 focus:outline-0 focus:ring-0 focus:ring-offset-0 focus-visible:outline-0 focus-visible:ring-0 focus-visible:ring-offset-0 focus-within:outline-0 focus-within:ring-0 focus-within:ring-offset-0 shadow-none rounded-none gap-4 text-xs md:text-xs py-2 pl-3 leading-[20px]!" |
| 62 | placeholder={isDiffVisible ? 'Make an edit...' : 'Edit via the Assistant...'} |
| 63 | value={value} |
| 64 | onChange={handleChange} |
| 65 | onKeyDown={handleKeyDown} |
| 66 | disabled={isLoading} |
| 67 | /> |
| 68 | {isDiffVisible ? ( |
| 69 | <div className="flex justify-start p-0 border-t"> |
| 70 | <Button |
| 71 | type="text" |
| 72 | onClick={onAccept} |
| 73 | className="text-xs h-auto py-1 rounded-none px-3 border-r-border" |
| 74 | disabled={isLoading} |
| 75 | > |
| 76 | Accept <span className="text-xs text-foreground-light">⌘ + Enter</span> |
| 77 | </Button> |
| 78 | <Button |
| 79 | onClick={onReject} |
| 80 | type="text" |
| 81 | className="text-xs h-auto py-1 rounded-none px-3 border-r-border" |
| 82 | disabled={isLoading} |
| 83 | > |
| 84 | Reject <span className="text-xs text-foreground-light">Esc</span> |
| 85 | </Button> |
| 86 | </div> |
| 87 | ) : ( |
| 88 | <div className="flex justify-start p-0 border-t"> |
| 89 | <Button |
| 90 | type="text" |
| 91 | onClick={handleSubmit} |
| 92 | loading={isLoading} |
| 93 | className="text-xs h-auto py-1 rounded-none px-3 border-r-border" |
| 94 | disabled={isLoading} |
| 95 | > |
| 96 | {isLoading ? 'Generating...' : 'Generate'}{' '} |
| 97 | {!isLoading && <span className="text-xs text-foreground-light">Enter</span>} |
| 98 | </Button> |
| 99 | <Button |
| 100 | onClick={onCancel} |
| 101 | type="text" |
| 102 | className="text-xs h-auto py-1 rounded-none px-3 border-r-border" |
| 103 | disabled={isLoading} |
| 104 | > |
| 105 | Cancel <span className="text-xs text-foreground-light">Esc</span> |
| 106 | </Button> |
| 107 | </div> |
| 108 | )} |
| 109 | </div> |
| 110 | ) |
| 111 | } |