@@ -3,13 +3,20 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/com
33import { Button } from '@/components/ui/button' ;
44import { Input } from '@/components/ui/input' ;
55import { 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
814interface Message {
915 id : string ;
1016 text : string ;
1117 sender : 'user' | 'ai' ;
1218 timestamp : Date ;
19+ isHtml ?: boolean ;
1320}
1421
1522interface AIChatAssistantProps {
@@ -21,6 +28,7 @@ interface AIChatAssistantProps {
2128 initialMessage ?: string ;
2229 height ?: string ;
2330 showHeader ?: boolean ;
31+ disableCourseGeneration ?: boolean ;
2432}
2533
2634const 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