1
1
'use client' ;
2
-
3
- import React , { useEffect , useRef } from 'react' ;
2
+ import React , { useEffect , useRef , useState } from 'react' ;
4
3
import { motion , AnimatePresence } from 'framer-motion' ;
5
4
import TextareaAutosize from 'react-textarea-autosize' ;
6
- import { Send , X } from 'lucide-react' ;
7
- import useChatStore from '@/hooks/useChatStore' ;
8
- import { Button } from '../ui/button' ;
5
+ import { PaperclipIcon , Send , X } from 'lucide-react' ;
6
+ import { cn } from '@/lib/utils' ;
9
7
import { ChatProps } from './chat-panel' ;
8
+ import Image from 'next/image' ;
9
+ import {
10
+ Tooltip ,
11
+ TooltipContent ,
12
+ TooltipProvider ,
13
+ TooltipTrigger ,
14
+ } from '@/components/ui/tooltip' ;
10
15
11
16
export default function ChatBottombar ( {
12
17
messages,
13
18
input,
14
19
handleInputChange,
15
20
handleSubmit,
16
- stop,
17
21
formRef,
18
22
setInput,
19
23
} : ChatProps ) {
20
- const [ isMobile , setIsMobile ] = React . useState ( false ) ;
24
+ const [ isMobile , setIsMobile ] = useState ( false ) ;
25
+ const [ isFocused , setIsFocused ] = useState ( false ) ;
26
+ const [ attachments , setAttachments ] = useState < File [ ] > ( [ ] ) ;
21
27
const inputRef = useRef < HTMLTextAreaElement > ( null ) ;
22
- const base64Images = useChatStore ( ( state ) => state . base64Images ) ;
23
- const setBase64Images = useChatStore ( ( state ) => state . setBase64Images ) ;
24
- const env = process . env . NODE_ENV ;
28
+ const fileInputRef = useRef < HTMLInputElement > ( null ) ;
25
29
26
30
useEffect ( ( ) => {
27
31
const checkScreenWidth = ( ) => {
28
32
setIsMobile ( window . innerWidth <= 768 ) ;
29
33
} ;
30
-
31
34
// Initial check
32
35
checkScreenWidth ( ) ;
33
-
34
36
// Event listener for screen width changes
35
37
window . addEventListener ( 'resize' , checkScreenWidth ) ;
36
-
37
38
// Cleanup the event listener on component unmount
38
39
return ( ) => {
39
40
window . removeEventListener ( 'resize' , checkScreenWidth ) ;
@@ -43,53 +44,200 @@ export default function ChatBottombar({
43
44
const handleKeyPress = ( e : React . KeyboardEvent < HTMLTextAreaElement > ) => {
44
45
if ( e . key === 'Enter' && ! e . shiftKey ) {
45
46
e . preventDefault ( ) ;
46
- handleSubmit ( e as unknown as React . FormEvent < HTMLFormElement > ) ;
47
+ submitWithAttachments ( e as unknown as React . FormEvent < HTMLFormElement > ) ;
48
+ }
49
+ } ;
50
+
51
+ const handleFileChange = ( e : React . ChangeEvent < HTMLInputElement > ) => {
52
+ if ( e . target . files && e . target . files . length > 0 ) {
53
+ const fileArray = Array . from ( e . target . files ) ;
54
+ setAttachments ( ( prev ) => [ ...prev , ...fileArray ] ) ;
47
55
}
48
56
} ;
49
57
58
+ const handleFileSelect = ( ) => {
59
+ fileInputRef . current ?. click ( ) ;
60
+ } ;
61
+
62
+ const removeAttachment = ( index : number ) => {
63
+ setAttachments ( ( prev ) => prev . filter ( ( _ , i ) => i !== index ) ) ;
64
+ } ;
65
+
66
+ const submitWithAttachments = ( e : React . FormEvent < HTMLFormElement > ) => {
67
+ // Here you would normally handle attachments with your form submission
68
+ // For this example, we'll just clear them after submission
69
+ handleSubmit ( e ) ;
70
+ setAttachments ( [ ] ) ;
71
+ } ;
72
+
50
73
useEffect ( ( ) => {
51
74
if ( inputRef . current ) {
52
75
inputRef . current . focus ( ) ;
53
76
}
54
77
} , [ ] ) ;
55
78
56
79
return (
57
- < div className = "px-4 pb-4 pt-2" >
80
+ < div className = "px-4 pb-4 pt-2 bg-white dark:bg-[#151718] " >
58
81
< motion . div
59
- initial = { { y : 20 , opacity : 0 } }
82
+ initial = { { y : 10 , opacity : 0 } }
60
83
animate = { { y : 0 , opacity : 1 } }
61
- transition = { { duration : 0.3 } }
62
- className = "rounded-xl border bg-background shadow-sm"
84
+ transition = { { duration : 0.2 } }
85
+ className = { cn (
86
+ 'relative border shadow-sm rounded-lg overflow-hidden' ,
87
+ isFocused
88
+ ? 'ring-1 ring-blue-500 border-blue-500'
89
+ : 'border-gray-200 hover:border-gray-300 dark:border-zinc-700 dark:hover:border-zinc-600' ,
90
+ 'bg-white dark:bg-[#1e1e1e]'
91
+ ) }
63
92
>
64
- { /* Image attachments removed */ }
93
+ { /* Attachments preview */ }
94
+ < AnimatePresence >
95
+ { attachments . length > 0 && (
96
+ < motion . div
97
+ initial = { { height : 0 , opacity : 0 } }
98
+ animate = { { height : 'auto' , opacity : 1 } }
99
+ exit = { { height : 0 , opacity : 0 } }
100
+ className = "flex flex-wrap gap-2 p-2 border-b border-gray-100 dark:border-zinc-800"
101
+ >
102
+ { attachments . map ( ( file , index ) => (
103
+ < motion . div
104
+ key = { `${ file . name } -${ index } ` }
105
+ initial = { { scale : 0.8 , opacity : 0 } }
106
+ animate = { { scale : 1 , opacity : 1 } }
107
+ exit = { { scale : 0.8 , opacity : 0 } }
108
+ className = "relative group"
109
+ >
110
+ < div className = "w-16 h-16 rounded border overflow-hidden bg-gray-50 dark:bg-zinc-800" >
111
+ { file . type . startsWith ( 'image/' ) ? (
112
+ < div className = "relative w-full h-full" >
113
+ < Image
114
+ src = { URL . createObjectURL ( file ) }
115
+ alt = { file . name }
116
+ fill
117
+ className = "object-cover"
118
+ />
119
+ </ div >
120
+ ) : (
121
+ < div className = "w-full h-full flex items-center justify-center text-xs text-gray-500 dark:text-zinc-400 p-1 overflow-hidden" >
122
+ { file . name }
123
+ </ div >
124
+ ) }
125
+ </ div >
126
+ < button
127
+ type = "button"
128
+ onClick = { ( ) => removeAttachment ( index ) }
129
+ className = "absolute -top-1 -right-1 size-5 bg-red-500 rounded-full flex items-center justify-center text-white opacity-0 group-hover:opacity-100 transition-opacity"
130
+ >
131
+ < X className = "size-3" />
132
+ </ button >
133
+ </ motion . div >
134
+ ) ) }
135
+ </ motion . div >
136
+ ) }
137
+ </ AnimatePresence >
65
138
66
139
< form
67
140
ref = { formRef }
68
- onSubmit = { handleSubmit }
69
- className = "flex items-end w-full"
141
+ onSubmit = { submitWithAttachments }
142
+ className = "flex items-center w-full"
70
143
>
144
+ { /* Hidden file input */ }
145
+ < input
146
+ type = "file"
147
+ ref = { fileInputRef }
148
+ onChange = { handleFileChange }
149
+ className = "hidden"
150
+ multiple
151
+ />
152
+
153
+ { /* Left icons with tooltips */ }
154
+ < div className = "flex items-center ml-2" >
155
+ < TooltipProvider >
156
+ < Tooltip >
157
+ < TooltipTrigger asChild >
158
+ < button
159
+ type = "button"
160
+ className = "p-1.5 text-gray-400 dark:text-zinc-400 rounded-md cursor-not-allowed opacity-50"
161
+ aria-label = "Attach file (not available)"
162
+ disabled
163
+ >
164
+ < PaperclipIcon className = "h-4 w-4" />
165
+ </ button >
166
+ </ TooltipTrigger >
167
+ < TooltipContent side = "top" >
168
+ < p > Feature not available yet</ p >
169
+ </ TooltipContent >
170
+ </ Tooltip >
171
+ </ TooltipProvider >
172
+
173
+ < TooltipProvider >
174
+ < Tooltip >
175
+ < TooltipTrigger asChild >
176
+ < button
177
+ type = "button"
178
+ className = "p-1.5 text-gray-400 dark:text-zinc-400 rounded-md cursor-not-allowed opacity-50"
179
+ aria-label = "Record (not available)"
180
+ disabled
181
+ >
182
+ < svg
183
+ xmlns = "http://www.w3.org/2000/svg"
184
+ className = "h-4 w-4"
185
+ viewBox = "0 0 24 24"
186
+ fill = "none"
187
+ stroke = "currentColor"
188
+ strokeWidth = "2"
189
+ strokeLinecap = "round"
190
+ strokeLinejoin = "round"
191
+ >
192
+ < circle cx = "12" cy = "12" r = "10" > </ circle >
193
+ < circle cx = "12" cy = "12" r = "4" > </ circle >
194
+ </ svg >
195
+ </ button >
196
+ </ TooltipTrigger >
197
+ < TooltipContent side = "top" >
198
+ < p > Feature not available yet</ p >
199
+ </ TooltipContent >
200
+ </ Tooltip >
201
+ </ TooltipProvider >
202
+ </ div >
203
+
204
+ { /* Text input */ }
71
205
< div className = "relative flex-1 flex items-center" >
72
206
< TextareaAutosize
73
207
autoComplete = "off"
74
208
value = { input }
75
209
ref = { inputRef }
76
210
onKeyDown = { handleKeyPress }
77
211
onChange = { handleInputChange }
212
+ onFocus = { ( ) => setIsFocused ( true ) }
213
+ onBlur = { ( ) => setIsFocused ( false ) }
78
214
name = "message"
79
- placeholder = "Message CodeFox ..."
80
- className = "resize-none px-4 py-3 w-full focus:outline-none bg-transparent text-sm placeholder:text-muted-foreground "
215
+ placeholder = "Message Agent ..."
216
+ className = "resize-none px-2 py-2.5 w-full focus:outline-none bg-transparent text-gray-800 dark:text-zinc-200 text- sm placeholder:text-gray-400 dark:placeholder:text-zinc-400 "
81
217
maxRows = { 5 }
82
218
/>
83
219
</ div >
84
220
85
- < Button
86
- type = "submit"
87
- size = "icon"
88
- className = "h-9 w-9 rounded-lg mr-2 mb-1 bg-primary hover:bg-primary/90 text-primary-foreground"
89
- disabled = { ! input . trim ( ) }
90
- >
91
- < Send className = "h-5 w-5" />
92
- </ Button >
221
+ { /* Right side - feedback & send */ }
222
+ < div className = "flex items-center mr-2 gap-2" >
223
+ < div className = "text-sm text-gray-400 dark:text-zinc-400" >
224
+ < span > Have feedback?</ span >
225
+ </ div >
226
+
227
+ < button
228
+ type = "submit"
229
+ className = { cn (
230
+ 'h-7 w-7 rounded-md flex items-center justify-center' ,
231
+ input . trim ( ) || attachments . length > 0
232
+ ? 'bg-gray-100 hover:bg-gray-200 text-gray-700 dark:bg-zinc-700 dark:hover:bg-zinc-600 dark:text-zinc-200'
233
+ : 'bg-gray-50 text-gray-300 cursor-not-allowed dark:bg-zinc-800 dark:text-zinc-500'
234
+ ) }
235
+ disabled = { ! input . trim ( ) && attachments . length === 0 }
236
+ aria-label = "Send message"
237
+ >
238
+ < Send className = "h-3.5 w-3.5" />
239
+ </ button >
240
+ </ div >
93
241
</ form >
94
242
</ motion . div >
95
243
</ div >
0 commit comments