diff --git a/.env.local b/.env.local
index 8212b8ab9b..e32ef1222a 100644
--- a/.env.local
+++ b/.env.local
@@ -22,3 +22,5 @@ MATCHING_DB_HOST_MGMT_PORT=3001
FRONTEND_SERVICE_NAME=frontend
FRONTEND_PORT=3000
+OPENAI_API_KEY=PUT_YOUR_OPENAI_API_KEY_HERE
+
diff --git a/frontend/.env.docker b/frontend/.env.docker
index bc248fff11..a34a07f4d0 100644
--- a/frontend/.env.docker
+++ b/frontend/.env.docker
@@ -4,4 +4,5 @@ VITE_USER_SERVICE=http://host.docker.internal:9001
VITE_QUESTION_SERVICE=http://host.docker.internal:9002
VITE_COLLAB_SERVICE=http://host.docker.internal:9003
VITE_MATCHING_SERVICE=http://host.docker.internal:9004
-FRONTEND_PORT=3000
\ No newline at end of file
+FRONTEND_PORT=3000
+OPENAI_API_KEY=PUT_YOUR_OPENAI_API_KEY_HERE
\ No newline at end of file
diff --git a/frontend/.env.local b/frontend/.env.local
index e6d1d250f2..fab7b5d425 100644
--- a/frontend/.env.local
+++ b/frontend/.env.local
@@ -4,3 +4,4 @@ VITE_USER_SERVICE=http://localhost:9001
VITE_QUESTION_SERVICE=http://localhost:9002
VITE_COLLAB_SERVICE=http://localhost:9003
VITE_MATCHING_SERVICE=http://localhost:9004
+OPENAI_API_KEY=PUT_YOUR_OPENAI_API_KEY_HERE
diff --git a/frontend/README.md b/frontend/README.md
index b8cca666ad..b23c312879 100644
--- a/frontend/README.md
+++ b/frontend/README.md
@@ -2,21 +2,22 @@
## Running with Docker (Standalone)
-1. Run this command to build:
+1. Enter your OPEN AI Api Key in the .env.docker file.
+2. Run this command to build:
```sh
docker build \
--build-arg FRONTEND_PORT=3000 \
-t frontend-app -f frontend.Dockerfile .
```
-2. Run this command, from the root folder:
+3. Run this command, from the root folder:
```sh
make db-up
```
-3. Run the necessary migrate and seed commands, if you haven't yet.
+4. Run the necessary migrate and seed commands, if you haven't yet.
-4. Run this command to expose the container:
+5. Run this command to expose the container:
```sh
docker run -p 3000:3000 --env-file ./.env.docker frontend-app
```
diff --git a/frontend/src/components/ui/chat-sidebar.tsx b/frontend/src/components/ui/chat-sidebar.tsx
new file mode 100644
index 0000000000..3bbb27b281
--- /dev/null
+++ b/frontend/src/components/ui/chat-sidebar.tsx
@@ -0,0 +1,320 @@
+import { Loader2, Maximize2, MessageSquare, Minimize2, Send, X } from 'lucide-react';
+import React, { ChangeEvent, KeyboardEvent, useEffect, useRef, useState } from 'react';
+
+import { Alert, AlertDescription } from '@/components/ui/alert';
+import { Button } from '@/components/ui/button';
+import { Input } from '@/components/ui/input';
+import { ScrollArea } from '@/components/ui/scroll-area';
+
+// Types for OpenAI API
+// interface OpenAIMessage {
+// role: 'user' | 'assistant';
+// content: string;
+// }
+
+interface Message {
+ text: string;
+ isUser: boolean;
+ timestamp: Date;
+}
+
+interface ChatMessageProps {
+ message: Message;
+}
+
+interface ChatSidebarProps {
+ isOpen: boolean;
+ onClose: () => void;
+}
+
+const API_URL = 'https://api.openai.com/v1/chat/completions';
+const API_KEY = process.env.OPENAI_API_KEY;
+
+interface Message {
+ text: string;
+ isUser: boolean;
+ timestamp: Date;
+ isCode?: boolean;
+}
+
+interface ChatMessageProps {
+ message: Message;
+}
+
+interface ChatSidebarProps {
+ isOpen: boolean;
+ onClose: () => void;
+}
+
+const CodeBlock: React.FC<{ content: string }> = ({ content }) => (
+
+
+ {content}
+
+
+
+);
+
+const ChatMessage: React.FC = ({ message }) => {
+ // Detect if the message contains code (basic detection - can be enhanced)
+ const hasCode = message.text.includes('```');
+ const parts = hasCode ? message.text.split('```') : [message.text];
+
+ return (
+
+
+ {parts.map((part, index) => {
+ if (index % 2 === 1) {
+ // Code block
+ return
;
+ }
+
+ return (
+
+ {part.split('\n').map((line, i) => (
+
+ {line}
+
+ ))}
+
+ );
+ })}
+
+ {message.timestamp.toLocaleTimeString([], {
+ hour: '2-digit',
+ minute: '2-digit',
+ })}
+
+
+
+ );
+};
+
+const ChatSidebar: React.FC = ({ isOpen, onClose }) => {
+ const [messages, setMessages] = useState([]);
+ const [input, setInput] = useState('');
+ const [isLoading, setIsLoading] = useState(false);
+ const [error, setError] = useState(null);
+ const [isExpanded, setIsExpanded] = useState(false);
+ const scrollAreaRef = useRef(null);
+ const inputRef = useRef(null);
+ const messagesEndRef = useRef(null);
+
+ const scrollToBottom = () => {
+ messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
+ };
+
+ useEffect(() => {
+ if (isOpen) {
+ inputRef.current?.focus();
+ scrollToBottom();
+ }
+ }, [isOpen]);
+
+ useEffect(() => {
+ scrollToBottom();
+ }, [messages, isLoading]);
+
+ const callOpenAI = async (userMessage: string): Promise => {
+ if (!API_KEY) {
+ throw new Error('OpenAI API key is not configured');
+ }
+
+ // convert the messages array to the format expected by the API
+ const openAIMessages = messages.map((msg) => {
+ return {
+ role: msg.isUser ? 'user' : 'assistant',
+ content: msg.text,
+ };
+ });
+
+ openAIMessages.push({
+ role: 'user',
+ content: userMessage,
+ });
+
+ const response = await fetch(API_URL, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: `Bearer ${API_KEY}`,
+ },
+ body: JSON.stringify({
+ model: 'gpt-3.5-turbo',
+ messages: [
+ {
+ role: 'system',
+ content: 'You are a helpful coding assistant. Provide concise, accurate answers.',
+ },
+ ...openAIMessages,
+ ],
+ temperature: 0.7,
+ max_tokens: 500,
+ }),
+ });
+
+ if (!response.ok) {
+ throw new Error(`API error: ${response.statusText}`);
+ }
+
+ const data = await response.json();
+ return data.choices[0].message.content;
+ };
+
+ const handleSend = async (): Promise => {
+ if (!input.trim() || isLoading) return;
+
+ const userMessage = input.trim();
+ setInput('');
+ setError(null);
+ setIsLoading(true);
+
+ // Add user message
+ setMessages((prev) => [
+ ...prev,
+ {
+ text: userMessage,
+ isUser: true,
+ timestamp: new Date(),
+ },
+ ]);
+
+ try {
+ const response = await callOpenAI(userMessage);
+ setMessages((prev) => [
+ ...prev,
+ {
+ text: response,
+ isUser: false,
+ timestamp: new Date(),
+ },
+ ]);
+ } catch (err) {
+ setError(
+ err instanceof Error ? err.message : 'An error occurred while fetching the response'
+ );
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ const handleKeyPress = (e: KeyboardEvent): void => {
+ if (e.key === 'Enter' && !e.shiftKey) {
+ e.preventDefault();
+ handleSend();
+ }
+ };
+
+ return (
+
+
+
+
+
+
AI Assistant
+
+
+
+
+
+
+
+
+ {messages.length === 0 && (
+
+
+
No messages yet. Start a conversation!
+
+ )}
+ {messages.map((msg, index) => (
+
+ ))}
+ {isLoading && (
+
+ )}
+ {error && (
+
+ {error}
+
+ )}
+
+
+
+
+
+ ) => setInput(e.target.value)}
+ placeholder='Type your message...'
+ onKeyPress={handleKeyPress}
+ disabled={isLoading}
+ className='flex-1'
+ />
+
+
+
+
+
+ );
+};
+
+const FloatingChatButton: React.FC = () => {
+ const [isSidebarOpen, setIsSidebarOpen] = useState(false);
+
+ return (
+ <>
+
+
+ setIsSidebarOpen(false)} />
+ >
+ );
+};
+
+export default FloatingChatButton;
diff --git a/frontend/src/routes/questions/details/main.tsx b/frontend/src/routes/questions/details/main.tsx
index f2ce2d0fbb..e9eb2ec587 100644
--- a/frontend/src/routes/questions/details/main.tsx
+++ b/frontend/src/routes/questions/details/main.tsx
@@ -5,6 +5,7 @@ import { LoaderFunctionArgs, useLoaderData } from 'react-router-dom';
import { WithNavBanner } from '@/components/blocks/authed';
import { QuestionDetails } from '@/components/blocks/questions/details';
import { Card } from '@/components/ui/card';
+import FloatingChatButton from '@/components/ui/chat-sidebar';
import { useCrumbs } from '@/lib/hooks/use-crumbs';
import { usePageTitle } from '@/lib/hooks/use-page-title';
import { questionDetailsQuery } from '@/lib/queries/question-details';
@@ -37,6 +38,7 @@ export const QuestionDetailsPage = () => {
+
);
};