Skip to content

Commit c9bbbfc

Browse files
feat: enhance AIChatAssistant with course generation confirmation, improved markdown rendering, and URL analysis functionality
1 parent b3758e7 commit c9bbbfc

File tree

3 files changed

+236
-148
lines changed

3 files changed

+236
-148
lines changed

client/src/components/AIChatAssistant.tsx

Lines changed: 165 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,20 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/com
33
import { Button } from '@/components/ui/button';
44
import { Input } from '@/components/ui/input';
55
import { Bot, User, Send } from 'lucide-react';
6-
import { getAIChatResponse } from '@/services/aiChat.service';
6+
import { getAIChatResponse, confirmCourse } from '@/services/aiChat.service';
7+
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
8+
import CoursePreview from '@/components/CoursePreview';
9+
import ReactMarkdown from 'react-markdown';
10+
import remarkGfm from 'remark-gfm';
11+
import rehypeHighlight from 'rehype-highlight';
12+
import { useAuth } from '@/hooks/useAuth';
713

814
interface Message {
915
id: string;
1016
text: string;
1117
sender: 'user' | 'ai';
1218
timestamp: Date;
19+
isHtml?: boolean;
1320
}
1421

1522
interface AIChatAssistantProps {
@@ -21,6 +28,7 @@ interface AIChatAssistantProps {
2128
initialMessage?: string;
2229
height?: string;
2330
showHeader?: boolean;
31+
disableCourseGeneration?: boolean;
2432
}
2533

2634
const AIChatAssistant: React.FC<AIChatAssistantProps> = ({
@@ -29,9 +37,10 @@ const AIChatAssistant: React.FC<AIChatAssistantProps> = ({
2937
title = "AI Learning Assistant",
3038
description = "Chat with your AI assistant to get help with your learning journey",
3139
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!",
40+
initialMessage = "👋 Hello! I'm your AI learning assistant. I'm here to help you learn and grow.\n\n💡 **Available Commands:**\n\n• `/help` - Show all available commands\n\n• `/generate <topic>` - Create a personalized course\n\n • `/explain <subject>` - Get a quick explanation\n\nAsk me anything about your courses or what you'd like to learn next!",
3341
height = "h-80",
34-
showHeader = true
42+
showHeader = true,
43+
disableCourseGeneration = false
3544
}) => {
3645
const [messages, setMessages] = useState<Message[]>([
3746
{
@@ -44,6 +53,12 @@ const AIChatAssistant: React.FC<AIChatAssistantProps> = ({
4453
const [inputMessage, setInputMessage] = useState('');
4554
const [isTyping, setIsTyping] = useState(false);
4655
const messagesEndRef = useRef<HTMLDivElement>(null);
56+
const [isCoursePreviewOpen, setIsCoursePreviewOpen] = useState(false);
57+
const { user } = useAuth();
58+
const userId = user?.id || '';
59+
const [coursePreview, setCoursePreview] = useState<any | null>(null);
60+
const [lastCoursePrompt, setLastCoursePrompt] = useState<string | null>(null);
61+
const [actionLoading, setActionLoading] = useState(false);
4762

4863
const scrollToBottom = () => {
4964
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
@@ -53,40 +68,64 @@ const AIChatAssistant: React.FC<AIChatAssistantProps> = ({
5368
scrollToBottom();
5469
}, [messages]);
5570

56-
const handleSendMessage = async () => {
57-
if (!inputMessage.trim()) return;
71+
const handleSendMessage = async (customPrompt?: string) => {
72+
if (!(customPrompt ?? inputMessage).trim()) return;
5873

5974
const userMessage: Message = {
6075
id: Date.now().toString(),
61-
text: inputMessage,
76+
text: customPrompt ?? inputMessage,
6277
sender: 'user',
6378
timestamp: new Date()
6479
};
6580

6681
setMessages(prev => [...prev, userMessage]);
6782
setInputMessage('');
6883
setIsTyping(true);
84+
setCoursePreview(null);
6985

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,
86+
// Check if this is a course generation command
87+
const isCourseGeneration = (customPrompt ?? inputMessage).toLowerCase().includes('/generate') ||
88+
(customPrompt ?? inputMessage).toLowerCase().includes('generate') ||
89+
(customPrompt ?? inputMessage).toLowerCase().includes('create');
90+
91+
// Show astro-themed waiting message for course generation
92+
if (isCourseGeneration && !disableCourseGeneration) {
93+
setMessages(prev => [...prev, {
94+
id: (Date.now() + 0.5).toString(),
95+
text: `🚀 **Launching Course Generation...**\n\n✨ I'm crafting your personalized learning journey through the cosmos of knowledge! This might take a moment as I:\n\n• 🌟 Analyze your skills\n\n• 🪐 Navigate through my knowledge base\n\n• ⭐ Structure the perfect learning path for you\n\n**Please hold on while I work my AI magic!** 🔮\n\n*This process typically takes 30-120 seconds...*`,
7696
sender: 'ai',
7797
timestamp: new Date()
78-
};
79-
setMessages(prev => [...prev, aiMessage]);
98+
}]);
99+
}
100+
101+
try {
102+
const aiText = await getAIChatResponse(userId, customPrompt ?? userMessage.text, userSkills, disableCourseGeneration);
103+
104+
if (typeof aiText === 'object' && aiText && aiText.title && aiText.description) {
105+
setCoursePreview(aiText);
106+
setLastCoursePrompt(customPrompt ?? userMessage.text);
107+
setMessages(prev => [...prev, {
108+
id: (Date.now() + 1).toString(),
109+
text: 'A course has been generated. Please review and confirm.',
110+
sender: 'ai',
111+
timestamp: new Date()
112+
}]);
113+
} else {
114+
setMessages(prev => [...prev, {
115+
id: (Date.now() + 1).toString(),
116+
text: typeof aiText === 'string' ? aiText : JSON.stringify(aiText),
117+
sender: 'ai',
118+
timestamp: new Date()
119+
}]);
120+
}
80121
} catch (error) {
81-
// Fallback response if AI service fails
82-
console.error('AI chat service failed:', error.message);
83-
const aiMessage: Message = {
122+
console.error('AI chat service failed:', error);
123+
setMessages(prev => [...prev, {
84124
id: (Date.now() + 1).toString(),
85125
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.",
86126
sender: 'ai',
87127
timestamp: new Date()
88-
};
89-
setMessages(prev => [...prev, aiMessage]);
128+
}]);
90129
} finally {
91130
setIsTyping(false);
92131
}
@@ -99,6 +138,60 @@ const AIChatAssistant: React.FC<AIChatAssistantProps> = ({
99138
}
100139
};
101140

141+
const handleCourseAction = async (action: 'confirm' | 'regenerate' | 'abort') => {
142+
if (!coursePreview) return;
143+
setActionLoading(true);
144+
if (action === 'confirm') {
145+
try {
146+
const result = await confirmCourse(userId);
147+
let messageText: string;
148+
if (typeof result === 'object' && result && result.id) {
149+
messageText = `🎉 Course created successfully! [Go to course](/courses/${result.id})\n\nGood luck on your learning journey!`;
150+
} else {
151+
// Handle case where backend returns simple confirmation string
152+
const resultString = typeof result === 'string' ? result : 'Course confirmed.';
153+
if (resultString.toLowerCase().includes('confirmed') || resultString.toLowerCase().includes('success')) {
154+
messageText = `🎉 Course created successfully!\n\nGood luck on your learning journey! You can find your new course in your dashboard.`;
155+
} else {
156+
messageText = resultString;
157+
}
158+
}
159+
setMessages(prev => [...prev, {
160+
id: (Date.now() + 1).toString(),
161+
text: messageText,
162+
sender: 'ai',
163+
timestamp: new Date()
164+
}]);
165+
setCoursePreview(null);
166+
setLastCoursePrompt(null);
167+
} catch {
168+
setMessages(prev => [...prev, {
169+
id: (Date.now() + 1).toString(),
170+
text: '❌ Failed to confirm course generation. Please try again.',
171+
sender: 'ai',
172+
timestamp: new Date()
173+
}]);
174+
} finally {
175+
setActionLoading(false);
176+
}
177+
} else if (action === 'regenerate') {
178+
if (lastCoursePrompt) {
179+
await handleSendMessage(lastCoursePrompt);
180+
}
181+
setActionLoading(false);
182+
} else if (action === 'abort') {
183+
setCoursePreview(null);
184+
setLastCoursePrompt(null);
185+
setActionLoading(false);
186+
setMessages(prev => [...prev, {
187+
id: (Date.now() + 1).toString(),
188+
text: 'Course generation aborted.',
189+
sender: 'ai',
190+
timestamp: new Date()
191+
}]);
192+
}
193+
};
194+
102195
return (
103196
<Card className={`h-full ${className}`}>
104197
{showHeader && (
@@ -119,24 +212,33 @@ const AIChatAssistant: React.FC<AIChatAssistantProps> = ({
119212
className={`flex ${message.sender === 'user' ? 'justify-end' : 'justify-start'}`}
120213
>
121214
<div
122-
className={`flex max-w-xs items-start space-x-2 rounded-lg px-3 py-2 ${
123-
message.sender === 'user'
215+
className={`
216+
flex max-w-[80vw] break-words overflow-x-auto
217+
items-start space-x-2 rounded-lg px-3 py-2
218+
${message.sender === 'user'
124219
? 'bg-blue-600 text-white'
125-
: 'bg-gray-100 text-gray-900'
126-
}`}
220+
: 'bg-gray-100 text-gray-900'}
221+
`}
127222
>
128223
{message.sender === 'ai' && (
129224
<Bot className="h-4 w-4 mt-0.5 text-blue-600" />
130225
)}
131226
<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-
/>
227+
{message.isHtml ? (
228+
<div
229+
className="text-sm whitespace-pre-wrap"
230+
dangerouslySetInnerHTML={{ __html: message.text }}
231+
/>
232+
) : (
233+
<div className={`prose prose-sm max-w-none ${message.sender === 'user' ? 'prose-invert' : ''}`}>
234+
<ReactMarkdown
235+
remarkPlugins={[remarkGfm]}
236+
rehypePlugins={[rehypeHighlight]}
237+
>
238+
{message.text}
239+
</ReactMarkdown>
240+
</div>
241+
)}
140242
<p className="text-xs opacity-70 mt-1">
141243
{message.timestamp.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
142244
</p>
@@ -163,7 +265,24 @@ const AIChatAssistant: React.FC<AIChatAssistantProps> = ({
163265

164266
<div ref={messagesEndRef} />
165267
</div>
166-
268+
{/* Course Preview Modal (inline, not dialog) */}
269+
{coursePreview && (
270+
<div className="w-full max-w-2xl mx-auto mt-6 mb-2 p-4 border border-blue-200 rounded-lg bg-blue-50 shadow">
271+
<CoursePreview course={coursePreview} />
272+
<div className="flex gap-4 mt-6 justify-end">
273+
<Button onClick={() => handleCourseAction('confirm')} disabled={actionLoading} className="bg-green-600 text-white">
274+
{actionLoading ? 'Confirming...' : 'Confirm'}
275+
</Button>
276+
<Button onClick={() => handleCourseAction('regenerate')} disabled={actionLoading} className="bg-yellow-500 text-white">
277+
{actionLoading ? 'Regenerating...' : 'Regenerate'}
278+
</Button>
279+
<Button onClick={() => handleCourseAction('abort')} disabled={actionLoading} className="bg-red-500 text-white">
280+
Abort
281+
</Button>
282+
</div>
283+
{actionLoading && <div className="text-xs text-gray-500 mt-2">This may take a while. Please wait...</div>}
284+
</div>
285+
)}
167286
{/* Input Area */}
168287
<div className="border-t p-4">
169288
<div className="flex space-x-2">
@@ -173,10 +292,11 @@ const AIChatAssistant: React.FC<AIChatAssistantProps> = ({
173292
onKeyPress={handleKeyPress}
174293
placeholder={placeholder}
175294
className="flex-1"
295+
disabled={isTyping || !!coursePreview}
176296
/>
177297
<Button
178-
onClick={handleSendMessage}
179-
disabled={!inputMessage.trim() || isTyping}
298+
onClick={() => handleSendMessage()}
299+
disabled={!inputMessage.trim() || isTyping || !!coursePreview}
180300
size="sm"
181301
className="px-3"
182302
>
@@ -185,6 +305,17 @@ const AIChatAssistant: React.FC<AIChatAssistantProps> = ({
185305
</div>
186306
</div>
187307
</CardContent>
308+
{/* Course Preview Modal (stub) */}
309+
<Dialog open={isCoursePreviewOpen} onOpenChange={setIsCoursePreviewOpen}>
310+
<DialogContent className="max-w-2xl">
311+
<DialogHeader>
312+
<DialogTitle>Course Preview</DialogTitle>
313+
</DialogHeader>
314+
<div className="p-4">
315+
<CoursePreview course={null} />
316+
</div>
317+
</DialogContent>
318+
</Dialog>
188319
</Card>
189320
);
190321
};

0 commit comments

Comments
 (0)