Skip to content

Commit 1abcd02

Browse files
committed
PEER-236: Refactor partner and ai chat code
1 parent 7c762b7 commit 1abcd02

File tree

5 files changed

+270
-364
lines changed

5 files changed

+270
-364
lines changed
Lines changed: 27 additions & 230 deletions
Original file line numberDiff line numberDiff line change
@@ -1,146 +1,39 @@
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';
32

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';
85

96
// Types for OpenAI API
107
// interface OpenAIMessage {
118
// role: 'user' | 'assistant';
129
// content: string;
1310
// }
1411

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 {
2613
isOpen: boolean;
2714
onClose: () => void;
2815
}
2916

3017
const API_URL = 'https://api.openai.com/v1/chat/completions';
3118
const API_KEY = process.env.OPENAI_API_KEY;
3219

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[]>([]);
10522
const [isLoading, setIsLoading] = useState<boolean>(false);
10623
const [error, setError] = useState<string | null>(null);
10724
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]);
12625

12726
const callOpenAI = async (userMessage: string): Promise<string> => {
12827
if (!API_KEY) {
12928
throw new Error('OpenAI API key is not configured');
13029
}
13130

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+
}));
13935

140-
openAIMessages.push({
141-
role: 'user',
142-
content: userMessage,
143-
});
36+
openAIMessages.push({ role: 'user', content: userMessage });
14437

14538
const response = await fetch(API_URL, {
14639
method: 'POST',
@@ -170,34 +63,16 @@ export const ChatSidebar: React.FC<ChatSidebarProps> = ({ isOpen, onClose }) =>
17063
return data.choices[0].message.content;
17164
};
17265

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;
17568

176-
const userMessage = input.trim();
177-
setInput('');
178-
setError(null);
69+
setMessages((prev) => [...prev, { text: userMessage, isUser: true, timestamp: new Date() }]);
17970
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);
19072

19173
try {
19274
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() }]);
20176
} catch (err) {
20277
setError(
20378
err instanceof Error ? err.message : 'An error occurred while fetching the response'
@@ -207,95 +82,17 @@ export const ChatSidebar: React.FC<ChatSidebarProps> = ({ isOpen, onClose }) =>
20782
}
20883
};
20984

210-
const handleKeyPress = (e: KeyboardEvent<HTMLInputElement>): void => {
211-
if (e.key === 'Enter' && !e.shiftKey) {
212-
e.preventDefault();
213-
handleSend();
214-
}
215-
};
216-
21785
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+
/>
30097
);
30198
};

0 commit comments

Comments
 (0)