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