Skip to content

Commit 3f70a9e

Browse files
Merge pull request #61 from AET-DevOps25/60-client-integrate-course-service-with-the-frontend-client
60 client integrate course service with the frontend client
2 parents 9d5aff2 + a45bfbc commit 3f70a9e

File tree

60 files changed

+5922
-1287
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

60 files changed

+5922
-1287
lines changed

client/package-lock.json

Lines changed: 1440 additions & 24 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

client/package.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,16 +58,20 @@
5858
"date-fns": "^3.6.0",
5959
"embla-carousel-react": "^8.3.0",
6060
"input-otp": "^1.2.4",
61+
"jspdf": "^3.0.1",
6162
"lodash": "^4.17.21",
6263
"lucide-react": "^0.462.0",
6364
"next-themes": "^0.3.0",
6465
"react": "^18.3.1",
6566
"react-day-picker": "^8.10.1",
6667
"react-dom": "^18.3.1",
6768
"react-hook-form": "^7.53.0",
69+
"react-icons": "^5.5.0",
70+
"react-markdown": "^10.1.0",
6871
"react-resizable-panels": "^2.1.3",
6972
"react-router-dom": "^6.26.2",
7073
"recharts": "^2.12.7",
74+
"simple-icons": "^15.4.0",
7175
"sonner": "^1.5.0",
7276
"tailwind-merge": "^2.5.2",
7377
"tailwindcss-animate": "^1.0.7",
@@ -79,7 +83,7 @@
7983
},
8084
"devDependencies": {
8185
"@eslint/js": "^9.9.0",
82-
"@tailwindcss/typography": "^0.5.15",
86+
"@tailwindcss/typography": "^0.5.16",
8387
"@types/lodash": "^4.17.17",
8488
"@types/node": "^22.5.5",
8589
"@types/react": "^18.3.3",

client/src/App.tsx

Lines changed: 28 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -10,66 +10,48 @@ import Signup from './pages/Signup';
1010
import Dashboard from './pages/Dashboard';
1111
import NotFound from './pages/NotFound';
1212
import Courses from '@/pages/Courses';
13+
import CourseDetail from '@/pages/CourseDetail';
14+
import LessonPage from '@/pages/LessonPage';
15+
import AiCenter from '@/pages/AiCenter';
16+
//import About from '@/pages/About';
17+
// import PrivacyPolicy from '@/pages/PrivacyPolicy';
18+
// import TermsOfService from '@/pages/TermsOfService';
19+
// import CookiePolicy from '@/pages/CookiePolicy';
1320

1421
import { AuthProvider } from '@/contexts/AuthProvider';
15-
import { RequireAuth, RequireGuest } from '@/components/RouteGuards';
22+
import { RequireAny, RequireAuth, RequireGuest } from '@/components/RouteGuards';
1623
import Profile from '@/pages/Profile';
24+
import ScrollToTop from '@/components/ScrollToTop';
1725

1826
const queryClient = new QueryClient();
1927

2028
const AppRoutes = () => (
2129
<Routes>
22-
{/* PUBLIC */}
23-
<Route path="/" element={<Index />} />
30+
{/* ANY */}
31+
<Route path="/" element={<RequireAny><Index /></RequireAny>}/>
2432

25-
<Route
26-
path="/login"
27-
element={
28-
<RequireGuest>
29-
<Login />
30-
</RequireGuest>
31-
}
32-
/>
33+
<Route path="/courses" element={<RequireAny><Courses /></RequireAny>}/>
3334

34-
<Route
35-
path="/signup"
36-
element={
37-
<RequireGuest>
38-
<Signup />
39-
</RequireGuest>
40-
}
41-
/>
35+
{ /* Uncomment these routes when the components are available */ }
36+
{/* <Route path="/about" element={<About />} />
37+
<Route path="/privacy" element={<PrivacyPolicy />} />
38+
<Route path="/terms" element={<TermsOfService />} />
39+
<Route path="/cookies" element={<CookiePolicy />} /> */}
4240

43-
{/* PROTECTED */}
44-
<Route
45-
path="/dashboard"
46-
element={
47-
<RequireAuth>
48-
<Dashboard />
49-
</RequireAuth>
50-
}
51-
/>
41+
{/* PUBLIC ONLY */}
42+
<Route path="/login" element={<RequireGuest><Login /></RequireGuest>} />
43+
<Route path="/signup" element={<RequireGuest><Signup /></RequireGuest>} />
5244

53-
<Route
54-
path="/courses"
55-
element={
56-
<RequireAuth>
57-
<Courses />
58-
</RequireAuth>
59-
}
60-
/>
45+
{/* PROTECTED */}
46+
<Route path="/dashboard" element={<RequireAuth><Dashboard /></RequireAuth>} />
47+
<Route path="/ai-center" element={<RequireAuth><AiCenter /></RequireAuth>} />
48+
<Route path="/profile" element={<RequireAuth><Profile /></RequireAuth>} />
6149

62-
<Route
63-
path="/profile"
64-
element={
65-
<RequireAuth>
66-
<Profile />
67-
</RequireAuth>
68-
}
69-
/>
50+
<Route path="/courses/:courseId" element={<RequireAuth><CourseDetail /></RequireAuth>} />
51+
<Route path="/courses/:courseId/lessons/:lessonId" element={<RequireAuth><LessonPage /></RequireAuth>} />
7052

7153
{/* CATCH-ALL */}
72-
<Route path="*" element={<NotFound />} />
54+
<Route path="*" element={<RequireAny><NotFound /></RequireAny>} />
7355
</Routes>
7456
);
7557

@@ -79,6 +61,7 @@ const App = () => (
7961
<Toaster />
8062
<Sonner />
8163
<BrowserRouter>
64+
<ScrollToTop />
8265
<AuthProvider>
8366
<AppRoutes />
8467
</AuthProvider>

client/src/assets/react.svg

Lines changed: 0 additions & 1 deletion
This file was deleted.
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
import React, { useState, useEffect, useRef } from 'react';
2+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
3+
import { Button } from '@/components/ui/button';
4+
import { Input } from '@/components/ui/input';
5+
import { Bot, User, Send } from 'lucide-react';
6+
import { getAIChatResponse } from '@/services/aiChat.service';
7+
8+
interface Message {
9+
id: string;
10+
text: string;
11+
sender: 'user' | 'ai';
12+
timestamp: Date;
13+
}
14+
15+
interface AIChatAssistantProps {
16+
className?: string;
17+
userSkills?: string[];
18+
title?: string;
19+
description?: string;
20+
placeholder?: string;
21+
initialMessage?: string;
22+
height?: string;
23+
showHeader?: boolean;
24+
}
25+
26+
const AIChatAssistant: React.FC<AIChatAssistantProps> = ({
27+
className,
28+
userSkills = [],
29+
title = "AI Learning Assistant",
30+
description = "Chat with your AI assistant to get help with your learning journey",
31+
placeholder = "Ask about your courses or what you want to learn next...",
32+
initialMessage = "👋 Hello! I'm your AI learning assistant. I'm here to help you learn and grow.\n\n💡 **Available Commands:**\n• `/help` - Show all available commands\n• `/generate <topic>` - Create a personalized course\n• `/explain <subject>` - Get a quick explanation\n\nAsk me anything about your courses or what you'd like to learn next!",
33+
height = "h-80",
34+
showHeader = true
35+
}) => {
36+
const [messages, setMessages] = useState<Message[]>([
37+
{
38+
id: '1',
39+
text: initialMessage,
40+
sender: 'ai',
41+
timestamp: new Date()
42+
}
43+
]);
44+
const [inputMessage, setInputMessage] = useState('');
45+
const [isTyping, setIsTyping] = useState(false);
46+
const messagesEndRef = useRef<HTMLDivElement>(null);
47+
48+
const scrollToBottom = () => {
49+
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
50+
};
51+
52+
useEffect(() => {
53+
scrollToBottom();
54+
}, [messages]);
55+
56+
const handleSendMessage = async () => {
57+
if (!inputMessage.trim()) return;
58+
59+
const userMessage: Message = {
60+
id: Date.now().toString(),
61+
text: inputMessage,
62+
sender: 'user',
63+
timestamp: new Date()
64+
};
65+
66+
setMessages(prev => [...prev, userMessage]);
67+
setInputMessage('');
68+
setIsTyping(true);
69+
70+
try {
71+
// Use the AI chat service for the response
72+
const aiText = await getAIChatResponse(inputMessage, userSkills);
73+
const aiMessage: Message = {
74+
id: (Date.now() + 1).toString(),
75+
text: aiText,
76+
sender: 'ai',
77+
timestamp: new Date()
78+
};
79+
setMessages(prev => [...prev, aiMessage]);
80+
} catch (error) {
81+
// Fallback response if AI service fails
82+
console.error('AI chat service failed:', error.message);
83+
const aiMessage: Message = {
84+
id: (Date.now() + 1).toString(),
85+
text: "I understand you're asking about that. Let me help you with that. This is a simulated response - in a real implementation, this would connect to your AI service.",
86+
sender: 'ai',
87+
timestamp: new Date()
88+
};
89+
setMessages(prev => [...prev, aiMessage]);
90+
} finally {
91+
setIsTyping(false);
92+
}
93+
};
94+
95+
const handleKeyPress = (e: React.KeyboardEvent) => {
96+
if (e.key === 'Enter' && !e.shiftKey) {
97+
e.preventDefault();
98+
handleSendMessage();
99+
}
100+
};
101+
102+
return (
103+
<Card className={`h-full ${className}`}>
104+
{showHeader && (
105+
<CardHeader>
106+
<CardTitle className="flex items-center space-x-2">
107+
<Bot className="h-5 w-5 text-blue-600" />
108+
<span>{title}</span>
109+
</CardTitle>
110+
<CardDescription>{description}</CardDescription>
111+
</CardHeader>
112+
)}
113+
<CardContent className="p-0">
114+
{/* Messages Container */}
115+
<div className={`${height} overflow-y-auto p-4 space-y-4`}>
116+
{messages.map((message) => (
117+
<div
118+
key={message.id}
119+
className={`flex ${message.sender === 'user' ? 'justify-end' : 'justify-start'}`}
120+
>
121+
<div
122+
className={`flex max-w-xs items-start space-x-2 rounded-lg px-3 py-2 ${
123+
message.sender === 'user'
124+
? 'bg-blue-600 text-white'
125+
: 'bg-gray-100 text-gray-900'
126+
}`}
127+
>
128+
{message.sender === 'ai' && (
129+
<Bot className="h-4 w-4 mt-0.5 text-blue-600" />
130+
)}
131+
<div className="flex-1">
132+
<div
133+
className="text-sm whitespace-pre-wrap"
134+
dangerouslySetInnerHTML={{
135+
__html: message.text
136+
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
137+
.replace(/\n/g, '<br>')
138+
}}
139+
/>
140+
<p className="text-xs opacity-70 mt-1">
141+
{message.timestamp.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
142+
</p>
143+
</div>
144+
{message.sender === 'user' && (
145+
<User className="h-4 w-4 mt-0.5 text-blue-200" />
146+
)}
147+
</div>
148+
</div>
149+
))}
150+
151+
{isTyping && (
152+
<div className="flex justify-start">
153+
<div className="flex max-w-xs items-start space-x-2 rounded-lg bg-gray-100 px-3 py-2">
154+
<Bot className="h-4 w-4 mt-0.5 text-blue-600" />
155+
<div className="flex space-x-1">
156+
<div className="h-2 w-2 animate-bounce rounded-full bg-gray-400"></div>
157+
<div className="h-2 w-2 animate-bounce rounded-full bg-gray-400" style={{ animationDelay: '0.1s' }}></div>
158+
<div className="h-2 w-2 animate-bounce rounded-full bg-gray-400" style={{ animationDelay: '0.2s' }}></div>
159+
</div>
160+
</div>
161+
</div>
162+
)}
163+
164+
<div ref={messagesEndRef} />
165+
</div>
166+
167+
{/* Input Area */}
168+
<div className="border-t p-4">
169+
<div className="flex space-x-2">
170+
<Input
171+
value={inputMessage}
172+
onChange={(e) => setInputMessage(e.target.value)}
173+
onKeyPress={handleKeyPress}
174+
placeholder={placeholder}
175+
className="flex-1"
176+
/>
177+
<Button
178+
onClick={handleSendMessage}
179+
disabled={!inputMessage.trim() || isTyping}
180+
size="sm"
181+
className="px-3"
182+
>
183+
<Send className="h-4 w-4" />
184+
</Button>
185+
</div>
186+
</div>
187+
</CardContent>
188+
</Card>
189+
);
190+
};
191+
192+
export default AIChatAssistant;

0 commit comments

Comments
 (0)