1
- import { Loader2 , Maximize2 , MessageSquare , Minimize2 , Send , X } from 'lucide-react' ;
2
- import React , { ChangeEvent , KeyboardEvent , useEffect , useRef , useState } from 'react' ;
1
+ import React , { useState } from 'react' ;
3
2
4
- import { Alert , AlertDescription } from '@/components/ui/alert' ;
5
- import { Button } from '@/components/ui/button' ;
6
- import { Input } from '@/components/ui/input' ;
7
- import { ScrollArea } from '@/components/ui/scroll-area' ;
3
+ import { ChatLayout } from './chat/chat-layout' ;
4
+ import { ChatMessageType } from './chat/chat-message' ;
8
5
9
6
// Types for OpenAI API
10
7
// interface OpenAIMessage {
11
8
// role: 'user' | 'assistant';
12
9
// content: string;
13
10
// }
14
11
15
- interface Message {
16
- text : string ;
17
- isUser : boolean ;
18
- timestamp : Date ;
19
- }
20
-
21
- interface ChatMessageProps {
22
- message : Message ;
23
- }
24
-
25
- interface ChatSidebarProps {
12
+ interface AIChatProps {
26
13
isOpen : boolean ;
27
14
onClose : ( ) => void ;
28
15
}
29
16
30
17
const API_URL = 'https://api.openai.com/v1/chat/completions' ;
31
18
const API_KEY = process . env . OPENAI_API_KEY ;
32
19
33
- interface Message {
34
- text : string ;
35
- isUser : boolean ;
36
- timestamp : Date ;
37
- isCode ?: boolean ;
38
- }
39
-
40
- interface ChatMessageProps {
41
- message : Message ;
42
- }
43
-
44
- interface ChatSidebarProps {
45
- isOpen : boolean ;
46
- onClose : ( ) => void ;
47
- }
48
-
49
- const CodeBlock : React . FC < { content : string } > = ( { content } ) => (
50
- < div className = 'group relative' >
51
- < pre className = 'my-4 overflow-x-auto rounded-md bg-gray-900 p-4 text-sm text-gray-100' >
52
- < code > { content } </ code >
53
- </ pre >
54
- < button
55
- onClick = { ( ) => navigator . clipboard . writeText ( content ) }
56
- className = 'absolute right-2 top-2 rounded bg-gray-700 px-2 py-1 text-xs text-gray-300 opacity-0 transition-opacity group-hover:opacity-100'
57
- >
58
- Copy
59
- </ button >
60
- </ div >
61
- ) ;
62
-
63
- const ChatMessage : React . FC < ChatMessageProps > = ( { message } ) => {
64
- // Detect if the message contains code (basic detection - can be enhanced)
65
- const hasCode = message . text . includes ( '```' ) ;
66
- const parts = hasCode ? message . text . split ( '```' ) : [ message . text ] ;
67
-
68
- return (
69
- < div className = { `flex ${ message . isUser ? 'justify-end' : 'justify-start' } mb-4` } >
70
- < div
71
- className = { `max-w-[85%] rounded-lg px-4 py-2 text-xs ${
72
- message . isUser ? 'bg-blue-500 text-white' : 'border border-gray-100 bg-gray-50'
73
- } `}
74
- >
75
- { parts . map ( ( part , index ) => {
76
- if ( index % 2 === 1 ) {
77
- // Code block
78
- return < CodeBlock key = { index } content = { part . trim ( ) } /> ;
79
- }
80
-
81
- return (
82
- < div key = { index } >
83
- { part . split ( '\n' ) . map ( ( line , i ) => (
84
- < p key = { i } className = 'whitespace-pre-wrap' >
85
- { line }
86
- </ p >
87
- ) ) }
88
- </ div >
89
- ) ;
90
- } ) }
91
- < div className = 'mt-1 text-xs opacity-60' >
92
- { message . timestamp . toLocaleTimeString ( [ ] , {
93
- hour : '2-digit' ,
94
- minute : '2-digit' ,
95
- } ) }
96
- </ div >
97
- </ div >
98
- </ div >
99
- ) ;
100
- } ;
101
-
102
- export const ChatSidebar : React . FC < ChatSidebarProps > = ( { isOpen, onClose } ) => {
103
- const [ messages , setMessages ] = useState < Message [ ] > ( [ ] ) ;
104
- const [ input , setInput ] = useState < string > ( '' ) ;
20
+ export const AIChat : React . FC < AIChatProps > = ( { isOpen, onClose } ) => {
21
+ const [ messages , setMessages ] = useState < ChatMessageType [ ] > ( [ ] ) ;
105
22
const [ isLoading , setIsLoading ] = useState < boolean > ( false ) ;
106
23
const [ error , setError ] = useState < string | null > ( null ) ;
107
24
const [ isExpanded , setIsExpanded ] = useState < boolean > ( false ) ;
108
- const scrollAreaRef = useRef < HTMLDivElement > ( null ) ;
109
- const inputRef = useRef < HTMLInputElement > ( null ) ;
110
- const messagesEndRef = useRef < HTMLDivElement > ( null ) ;
111
-
112
- const scrollToBottom = ( ) => {
113
- messagesEndRef . current ?. scrollIntoView ( { behavior : 'smooth' } ) ;
114
- } ;
115
-
116
- useEffect ( ( ) => {
117
- if ( isOpen ) {
118
- inputRef . current ?. focus ( ) ;
119
- scrollToBottom ( ) ;
120
- }
121
- } , [ isOpen ] ) ;
122
-
123
- useEffect ( ( ) => {
124
- scrollToBottom ( ) ;
125
- } , [ messages , isLoading ] ) ;
126
25
127
26
const callOpenAI = async ( userMessage : string ) : Promise < string > => {
128
27
if ( ! API_KEY ) {
129
28
throw new Error ( 'OpenAI API key is not configured' ) ;
130
29
}
131
30
132
- // convert the messages array to the format expected by the API
133
- const openAIMessages = messages . map ( ( msg ) => {
134
- return {
135
- role : msg . isUser ? 'user' : 'assistant' ,
136
- content : msg . text ,
137
- } ;
138
- } ) ;
31
+ const openAIMessages = messages . map ( ( msg ) => ( {
32
+ role : msg . isUser ? 'user' : 'assistant' ,
33
+ content : msg . text ,
34
+ } ) ) ;
139
35
140
- openAIMessages . push ( {
141
- role : 'user' ,
142
- content : userMessage ,
143
- } ) ;
36
+ openAIMessages . push ( { role : 'user' , content : userMessage } ) ;
144
37
145
38
const response = await fetch ( API_URL , {
146
39
method : 'POST' ,
@@ -170,34 +63,16 @@ export const ChatSidebar: React.FC<ChatSidebarProps> = ({ isOpen, onClose }) =>
170
63
return data . choices [ 0 ] . message . content ;
171
64
} ;
172
65
173
- const handleSend = async ( ) : Promise < void > => {
174
- if ( ! input . trim ( ) || isLoading ) return ;
66
+ const handleSend = async ( userMessage : string ) : Promise < void > => {
67
+ if ( ! userMessage . trim ( ) || isLoading ) return ;
175
68
176
- const userMessage = input . trim ( ) ;
177
- setInput ( '' ) ;
178
- setError ( null ) ;
69
+ setMessages ( ( prev ) => [ ...prev , { text : userMessage , isUser : true , timestamp : new Date ( ) } ] ) ;
179
70
setIsLoading ( true ) ;
180
-
181
- // Add user message
182
- setMessages ( ( prev ) => [
183
- ...prev ,
184
- {
185
- text : userMessage ,
186
- isUser : true ,
187
- timestamp : new Date ( ) ,
188
- } ,
189
- ] ) ;
71
+ setError ( null ) ;
190
72
191
73
try {
192
74
const response = await callOpenAI ( userMessage ) ;
193
- setMessages ( ( prev ) => [
194
- ...prev ,
195
- {
196
- text : response ,
197
- isUser : false ,
198
- timestamp : new Date ( ) ,
199
- } ,
200
- ] ) ;
75
+ setMessages ( ( prev ) => [ ...prev , { text : response , isUser : false , timestamp : new Date ( ) } ] ) ;
201
76
} catch ( err ) {
202
77
setError (
203
78
err instanceof Error ? err . message : 'An error occurred while fetching the response'
@@ -207,95 +82,17 @@ export const ChatSidebar: React.FC<ChatSidebarProps> = ({ isOpen, onClose }) =>
207
82
}
208
83
} ;
209
84
210
- const handleKeyPress = ( e : KeyboardEvent < HTMLInputElement > ) : void => {
211
- if ( e . key === 'Enter' && ! e . shiftKey ) {
212
- e . preventDefault ( ) ;
213
- handleSend ( ) ;
214
- }
215
- } ;
216
-
217
85
return (
218
- < div
219
- className = { `fixed right-0 top-14 h-[calc(100%-3.5rem)] bg-white shadow-xl transition-all duration-300 ease-in-out${
220
- isOpen ? 'translate-x-0' : 'translate-x-full'
221
- } ${ isExpanded ? 'w-3/4' : 'w-96' } `}
222
- >
223
- < div className = 'flex h-full flex-col' >
224
- < div className = 'flex items-center justify-between border-b bg-white px-4 py-3' >
225
- < div className = 'flex items-center gap-2' >
226
- < MessageSquare className = 'size-5 text-blue-500' />
227
- < h2 className = 'text-base font-semibold' > AI Assistant</ h2 >
228
- </ div >
229
- < div className = 'flex items-center gap-2' >
230
- < Button
231
- variant = 'ghost'
232
- size = 'icon'
233
- onClick = { ( ) => setIsExpanded ( ! isExpanded ) }
234
- className = 'rounded-full hover:bg-gray-100'
235
- >
236
- { isExpanded ? < Minimize2 className = 'size-5' /> : < Maximize2 className = 'size-5' /> }
237
- </ Button >
238
- < Button
239
- variant = 'ghost'
240
- size = 'icon'
241
- onClick = { onClose }
242
- className = 'rounded-full hover:bg-gray-100'
243
- >
244
- < X className = 'size-5' />
245
- </ Button >
246
- </ div >
247
- </ div >
248
-
249
- < ScrollArea className = 'flex-1 overflow-y-auto p-4' ref = { scrollAreaRef } >
250
- { messages . length === 0 && (
251
- < div className = 'flex h-full flex-col items-center justify-center text-gray-500' >
252
- < MessageSquare className = 'mb-4 size-12 opacity-50' />
253
- < p className = 'text-center' > No messages yet. Start a conversation!</ p >
254
- </ div >
255
- ) }
256
- { messages . map ( ( msg , index ) => (
257
- < ChatMessage key = { index } message = { msg } />
258
- ) ) }
259
- { isLoading && (
260
- < div className = 'mb-4 flex justify-start' >
261
- < div className = 'rounded-lg bg-gray-50 px-4 py-2' >
262
- < Loader2 className = 'size-5 animate-spin text-gray-500' />
263
- </ div >
264
- </ div >
265
- ) }
266
- { error && (
267
- < Alert variant = 'destructive' className = 'mb-4' >
268
- < AlertDescription > { error } </ AlertDescription >
269
- </ Alert >
270
- ) }
271
- < div ref = { messagesEndRef } />
272
- </ ScrollArea >
273
-
274
- < div className = 'border-t bg-white p-4' >
275
- < div className = 'flex gap-2' >
276
- < Input
277
- ref = { inputRef }
278
- value = { input }
279
- onChange = { ( e : ChangeEvent < HTMLInputElement > ) => setInput ( e . target . value ) }
280
- placeholder = 'Type your message...'
281
- onKeyPress = { handleKeyPress }
282
- disabled = { isLoading }
283
- className = 'flex-1'
284
- />
285
- < Button
286
- onClick = { handleSend }
287
- disabled = { isLoading || ! input . trim ( ) }
288
- className = 'bg-blue-500 hover:bg-blue-600'
289
- >
290
- { isLoading ? (
291
- < Loader2 className = 'size-4 animate-spin' />
292
- ) : (
293
- < Send className = 'size-4' />
294
- ) }
295
- </ Button >
296
- </ div >
297
- </ div >
298
- </ div >
299
- </ div >
86
+ < ChatLayout
87
+ isOpen = { isOpen }
88
+ onClose = { onClose }
89
+ messages = { messages }
90
+ onSend = { handleSend }
91
+ isLoading = { isLoading }
92
+ error = { error }
93
+ title = 'AI Assistant'
94
+ isExpanded = { isExpanded }
95
+ setIsExpanded = { setIsExpanded }
96
+ />
300
97
) ;
301
98
} ;
0 commit comments