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 = () => {
+ ); };