1
1
"use client" ;
2
2
// componnets/VisionContainer.tsx
3
+ import React , {
4
+ useState ,
5
+ useRef ,
6
+ useCallback ,
7
+ KeyboardEvent ,
8
+ FormEvent ,
9
+ } from "react" ;
3
10
import { MarkdownViewer } from "@/components/MarkdownViewer" ;
4
11
import { Card } from "@/components/ui/card" ;
5
- import { useState } from "react" ;
6
12
import { Loader , Send } from "lucide-react" ;
7
13
8
14
import { useControlContext } from "@/providers/ControlContext" ;
@@ -16,79 +22,131 @@ export const VisionContainer = () => {
16
22
const [ prompt , setPrompt ] = useState < string > ( "" ) ;
17
23
const [ userQuestion , setUserQuestion ] = useState < string > ( "" ) ;
18
24
const [ loading , setLoading ] = useState < boolean > ( false ) ;
25
+ const textareaRef = useRef < HTMLTextAreaElement > ( null ) ;
19
26
20
- const isFormSubmittable = ( ) => {
27
+ const isFormSubmittable = useCallback ( ( ) => {
21
28
return (
22
29
prompt . trim ( ) !== "" &&
23
30
mediaDataList . some (
24
31
( media ) => media !== null && media . data !== "" && media . mimeType !== ""
25
32
)
26
33
) ;
27
- } ;
34
+ } , [ prompt , mediaDataList ] ) ;
28
35
29
- const handleSubmitForm = async ( e : React . FormEvent < HTMLFormElement > ) => {
30
- e . preventDefault ( ) ;
36
+ const handleSubmitForm = useCallback (
37
+ async ( e : React . FormEvent < HTMLFormElement > ) => {
38
+ e . preventDefault ( ) ;
31
39
32
- if ( ! isFormSubmittable ( ) ) return ;
33
- setLoading ( true ) ;
34
- setUserQuestion ( prompt ) ;
35
- setResult ( "" ) ;
36
- setPrompt ( "" ) ;
40
+ if ( ! isFormSubmittable ( ) ) return ;
41
+ setLoading ( true ) ;
42
+ setUserQuestion ( prompt ) ;
43
+ setResult ( "" ) ;
44
+ setPrompt ( "" ) ;
45
+ resetTextareaHeight ( ) ;
37
46
38
- // Filter out any invalid image data
39
- const validMediaData = mediaDataList . filter (
40
- ( data ) => data . data !== "" && data . mimeType !== ""
41
- ) ;
47
+ // Filter out any invalid image data
48
+ const validMediaData = mediaDataList . filter (
49
+ ( data ) => data . data !== "" && data . mimeType !== ""
50
+ ) ;
42
51
43
- // If there are no valid images and the prompt is empty, do not proceed
44
- if ( validMediaData . length === 0 ) return ;
52
+ // If there are no valid images and the prompt is empty, do not proceed
53
+ if ( validMediaData . length === 0 ) return ;
45
54
46
- const mediaBase64 = validMediaData . map ( ( data ) =>
47
- data . data . replace ( / ^ d a t a : ( i m a g e | v i d e o ) \/ \w + ; b a s e 6 4 , / , "" )
48
- ) ;
49
- const mediaTypes = validMediaData . map ( ( data ) => data . mimeType ) ;
50
-
51
- // console.log(mediaBase64.length, mediaTypes);
52
-
53
- const body = JSON . stringify ( {
54
- message : prompt ,
55
- media : mediaBase64 ,
56
- media_types : mediaTypes ,
57
- general_settings : generalSettings ,
58
- safety_settings : safetySettings ,
59
- } ) ;
60
-
61
- try {
62
- const response = await fetch ( `/api/gemini-vision` , {
63
- method : "POST" ,
64
- body,
65
- headers : {
66
- "Content-Type" : "application/json" ,
67
- } ,
55
+ const mediaBase64 = validMediaData . map ( ( data ) =>
56
+ data . data . replace ( / ^ d a t a : ( i m a g e | v i d e o ) \/ \w + ; b a s e 6 4 , / , "" )
57
+ ) ;
58
+ const mediaTypes = validMediaData . map ( ( data ) => data . mimeType ) ;
59
+
60
+ // console.log(mediaBase64.length, mediaTypes);
61
+
62
+ const body = JSON . stringify ( {
63
+ message : prompt ,
64
+ media : mediaBase64 ,
65
+ media_types : mediaTypes ,
66
+ general_settings : generalSettings ,
67
+ safety_settings : safetySettings ,
68
68
} ) ;
69
69
70
- if ( ! response . ok ) {
71
- throw new Error ( `HTTP error! status: ${ response . status } ` ) ;
72
- }
70
+ try {
71
+ const response = await fetch ( `/api/gemini-vision` , {
72
+ method : "POST" ,
73
+ body,
74
+ headers : {
75
+ "Content-Type" : "application/json" ,
76
+ } ,
77
+ } ) ;
73
78
74
- const reader = response . body ?. getReader ( ) ;
75
- if ( reader ) {
76
- let accumulator = "" ;
77
- while ( true ) {
78
- const { done, value } = await reader . read ( ) ;
79
- if ( done ) break ;
80
- const text = new TextDecoder ( ) . decode ( value ) ;
81
- accumulator += text ;
82
- setResult ( accumulator ) ;
79
+ if ( ! response . ok ) {
80
+ throw new Error ( `HTTP error! status: ${ response . status } ` ) ;
81
+ }
82
+
83
+ const reader = response . body ?. getReader ( ) ;
84
+ if ( reader ) {
85
+ let accumulator = "" ;
86
+ while ( true ) {
87
+ const { done, value } = await reader . read ( ) ;
88
+ if ( done ) break ;
89
+ const text = new TextDecoder ( ) . decode ( value ) ;
90
+ accumulator += text ;
91
+ setResult ( accumulator ) ;
92
+ }
93
+ setLoading ( false ) ;
94
+ }
95
+ } catch ( error ) {
96
+ if ( error instanceof Error ) {
97
+ setResult ( `Error: ${ error . message } ` ) ;
98
+ setLoading ( false ) ;
83
99
}
84
- setLoading ( false ) ;
85
- }
86
- } catch ( error ) {
87
- if ( error instanceof Error ) {
88
- setResult ( `Error: ${ error . message } ` ) ;
89
- setLoading ( false ) ;
90
100
}
101
+ // };
102
+ } ,
103
+ [ generalSettings , safetySettings , mediaDataList , prompt ]
104
+ ) ;
105
+
106
+ const resetTextareaHeight = useCallback ( ( ) => {
107
+ if ( textareaRef . current ) {
108
+ textareaRef . current . style . height = "2.5rem" ;
91
109
}
110
+ } , [ ] ) ;
111
+
112
+ const handleKeyPress = useCallback (
113
+ ( event : KeyboardEvent < HTMLTextAreaElement > ) => {
114
+ if ( event . key === "Enter" && ! event . ctrlKey && ! event . shiftKey ) {
115
+ event . preventDefault ( ) ;
116
+ if ( isFormSubmittable ( ) ) {
117
+ setLoading ( true ) ;
118
+ handleSubmitForm ( event as unknown as FormEvent < HTMLFormElement > ) ;
119
+ }
120
+ } else if ( event . key === "Enter" ) {
121
+ // Allow for Ctrl+Enter or Shift+Enter to insert new lines
122
+ event . preventDefault ( ) ;
123
+ const textarea = event . currentTarget ;
124
+ const cursorPosition = textarea . selectionStart ;
125
+ textarea . value =
126
+ textarea . value . slice ( 0 , cursorPosition ) +
127
+ "\n" +
128
+ textarea . value . slice ( cursorPosition ) ;
129
+ setPrompt ( textarea . value ) ;
130
+ textarea . selectionStart = cursorPosition + 1 ;
131
+ textarea . selectionEnd = cursorPosition + 1 ;
132
+ adjustTextareaHeight ( textarea ) ;
133
+ }
134
+ } ,
135
+ [ setPrompt , isFormSubmittable , handleSubmitForm ]
136
+ ) ;
137
+
138
+ const handleTextAreaInput = useCallback (
139
+ ( event : React . FormEvent < HTMLTextAreaElement > ) => {
140
+ const target = event . currentTarget ;
141
+ setPrompt ( target . value ) ;
142
+ adjustTextareaHeight ( target ) ;
143
+ } ,
144
+ [ setPrompt ]
145
+ ) ;
146
+
147
+ const adjustTextareaHeight = ( target : HTMLTextAreaElement ) => {
148
+ target . style . height = "auto" ;
149
+ target . style . height = `${ target . scrollHeight } px` ;
92
150
} ;
93
151
94
152
return (
@@ -113,11 +171,14 @@ export const VisionContainer = () => {
113
171
onSubmit = { handleSubmitForm }
114
172
className = "flex gap-4 pt-4 border-t border-primary/70 p-2"
115
173
>
116
- < input
117
- type = "text"
174
+ < textarea
175
+ ref = { textareaRef }
118
176
value = { prompt }
177
+ onInput = { handleTextAreaInput }
119
178
onChange = { ( e ) => setPrompt ( e . target . value ) }
120
- className = "flex-1 p-2 rounded"
179
+ onKeyDown = { handleKeyPress }
180
+ rows = { 1 }
181
+ className = "flex-1 p-2 resize-none overflow-hidden min-h-8 rounded"
121
182
placeholder = "Ask a question about the images"
122
183
/>
123
184
< div className = "m-auto" >
@@ -133,17 +194,6 @@ export const VisionContainer = () => {
133
194
) }
134
195
</ Button >
135
196
</ div >
136
- { /* <button
137
- type="submit"
138
- className="ml-2 p-2 rounded-full border bg-blue-500 hover:bg-blue-600 text-white"
139
- disabled={!isFormSubmittable() || loading}
140
- >
141
- {loading ? (
142
- <Loader className="animate-spin" />
143
- ) : (
144
- <Send className="m-auto" />
145
- )}
146
- </button> */ }
147
197
</ form >
148
198
</ Card >
149
199
</ div >
0 commit comments