Spaces:
Sleeping
Sleeping
| import React, { useState, useEffect, useRef } from "react"; | |
| import { Send, Bot, User } from "lucide-react"; | |
| import { motion } from "framer-motion"; | |
| import { mbaFaqs } from "../data/mbaData"; | |
| const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || ""; | |
| export default function ICFAIChatbot() { | |
| // Local program/semester/course data | |
| const programs = [ | |
| { id: "mba_online", name: "Online MBA" }, | |
| { id: "bba_online", name: "Online BBA" }, | |
| ]; | |
| const allSemesters = [ | |
| { | |
| id: "sem1", | |
| name: "Semester I", | |
| courses: [ | |
| { id: "mob", name: "Management and Organization Behavior" }, | |
| { id: "ba", name: "Business Analytics" }, | |
| { id: "faf", name: "Foundations of Accounting and Finance" }, | |
| { id: "be", name: "Business Environment" }, | |
| { id: "itm", name: "IT for Managers" }, | |
| ], | |
| }, | |
| { id: "sem2", name: "Semester II", courses: [] }, | |
| { id: "sem3", name: "Semester III", courses: [] }, | |
| { id: "sem4", name: "Semester IV", courses: [] }, | |
| ]; | |
| const faqs = mbaFaqs || []; | |
| // Flow state: program -> semester -> course -> units | |
| const [flowStep, setFlowStep] = useState("program"); | |
| const [selectedProgram, setSelectedProgram] = useState(null); | |
| const [selectedSemester, setSelectedSemester] = useState(null); | |
| const [selectedCourse, setSelectedCourse] = useState(null); | |
| const [openUnit, setOpenUnit] = useState(null); // when a unit is selected, sidebar shows unit content | |
| // Chat state | |
| const [messages, setMessages] = useState([]); | |
| const [inputValue, setInputValue] = useState(""); | |
| const [isTyping, setIsTyping] = useState(false); | |
| const messagesEndRef = useRef(null); | |
| useEffect(() => { | |
| if (messages.length === 0) addBotMessage("Hello! 👋 Welcome — choose a program from the left."); | |
| // eslint-disable-next-line react-hooks/exhaustive-deps | |
| }, []); | |
| const addBotMessage = (text, delay = 300) => { | |
| setIsTyping(true); | |
| setTimeout(() => { | |
| setMessages((m) => [...m, { type: "bot", text }]); | |
| setIsTyping(false); | |
| scrollToBottom(); | |
| }, delay); | |
| }; | |
| const addUserMessage = (text) => { | |
| setMessages((m) => [...m, { type: "user", text }]); | |
| scrollToBottom(); | |
| }; | |
| const scrollToBottom = () => messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); | |
| // Flow handlers | |
| const onSelectProgram = (p) => { | |
| addUserMessage(p.name); | |
| setSelectedProgram(p); | |
| setSelectedSemester(null); | |
| setSelectedCourse(null); | |
| setOpenUnit(null); | |
| setFlowStep("semester"); | |
| addBotMessage(`Great — ${p.name} selected. Choose a semester.`); | |
| }; | |
| const onSelectSemester = (s) => { | |
| addUserMessage(s.name); | |
| setSelectedSemester(s); | |
| setSelectedCourse(null); | |
| setOpenUnit(null); | |
| setFlowStep("course"); | |
| addBotMessage(`You selected ${s.name}. Pick a course.`); | |
| }; | |
| const onSelectCourse = (c) => { | |
| addUserMessage(c.name); | |
| setSelectedCourse(c); | |
| setOpenUnit(null); | |
| if (c.id === "itm") { | |
| setFlowStep("units"); | |
| addBotMessage(`You selected ${c.name}. Choose a unit from the left.`); | |
| } else { | |
| setFlowStep("chat"); | |
| addBotMessage(`You selected ${c.name}. Ask academic questions or open Units if available.`); | |
| } | |
| }; | |
| const onSelectUnit = (n) => setOpenUnit(n); | |
| const onBack = () => { | |
| if (openUnit) { | |
| setOpenUnit(null); | |
| return; | |
| } | |
| if (flowStep === "units") { | |
| setFlowStep("course"); | |
| setSelectedCourse(null); | |
| return; | |
| } | |
| if (flowStep === "course") { | |
| setFlowStep("semester"); | |
| setSelectedSemester(null); | |
| return; | |
| } | |
| if (flowStep === "semester") { | |
| setFlowStep("program"); | |
| setSelectedProgram(null); | |
| return; | |
| } | |
| }; | |
| const onFaqClick = (faq) => { | |
| // show FAQ answer in chat (keeps sidebar focused on FAQs) | |
| addUserMessage(faq.question); | |
| addBotMessage(faq.answer, 400); | |
| }; | |
| const handleSendMessage = async () => { | |
| if (!inputValue.trim()) return; | |
| const q = inputValue.trim(); | |
| addUserMessage(q); | |
| setInputValue(""); | |
| setIsTyping(true); | |
| try { | |
| const fd = new FormData(); | |
| fd.append("user_message", q); | |
| const res = await fetch(`${API_BASE_URL}/digital_icfai_chat`, { method: "POST", body: fd }); | |
| const data = await res.json().catch(() => ({})); | |
| const answer = (data && (data.answer || data.response)) || "I couldn't find an exact answer in the FAQs."; | |
| addBotMessage(answer, 200); | |
| } catch (err) { | |
| addBotMessage("Sorry, knowledge server unreachable. Try again later."); | |
| } finally { | |
| setIsTyping(false); | |
| } | |
| }; | |
| // Sidebar renderer — only one section visible at a time, and unit content expands inside sidebar | |
| const SidebarContent = () => { | |
| if (!flowStep || flowStep === "program") { | |
| return ( | |
| <> | |
| <h3 className="font-bold text-lg">Programs</h3> | |
| <div className="mt-4 space-y-3"> | |
| {programs.map((p) => ( | |
| <button key={p.id} onClick={() => onSelectProgram(p)} className="w-full text-left px-4 py-3 rounded-lg bg-white border border-gray-100 hover:bg-blue-50"> | |
| {p.name} | |
| </button> | |
| ))} | |
| </div> | |
| </> | |
| ); | |
| } | |
| if (flowStep === "semester") { | |
| return ( | |
| <> | |
| <div className="flex items-center justify-between"> | |
| <h3 className="font-bold text-lg">Semesters</h3> | |
| <button onClick={onBack} className="text-sm text-gray-600">Back</button> | |
| </div> | |
| <div className="mt-4 space-y-3"> | |
| {allSemesters.map((s) => ( | |
| <button key={s.id} onClick={() => onSelectSemester(s)} className="w-full text-left px-4 py-3 rounded-lg bg-white border border-gray-100 hover:bg-blue-50"> | |
| {s.name} | |
| </button> | |
| ))} | |
| </div> | |
| </> | |
| ); | |
| } | |
| if (flowStep === "course") { | |
| return ( | |
| <> | |
| <div className="flex items-center justify-between"> | |
| <h3 className="font-bold text-lg">Courses in {selectedSemester?.name}</h3> | |
| <button onClick={onBack} className="text-sm text-gray-600">Back</button> | |
| </div> | |
| <div className="mt-4 space-y-3"> | |
| {(selectedSemester?.courses || []).length > 0 ? ( | |
| selectedSemester.courses.map((c) => ( | |
| <button key={c.id} onClick={() => onSelectCourse(c)} className="w-full text-left px-4 py-3 rounded-lg bg-white border border-gray-100 hover:bg-blue-50"> | |
| {c.name} | |
| </button> | |
| )) | |
| ) : ( | |
| <div className="text-sm text-gray-500">No courses listed for this semester</div> | |
| )} | |
| </div> | |
| </> | |
| ); | |
| } | |
| if (flowStep === "units") { | |
| // If no unit selected, show unit buttons. If unit selected, show its FAQ(s) in the sidebar content area. | |
| if (!openUnit) { | |
| return ( | |
| <> | |
| <div className="flex items-center justify-between"> | |
| <h3 className="font-bold text-lg">ITM Units</h3> | |
| <button onClick={onBack} className="text-sm text-gray-600">Back</button> | |
| </div> | |
| <div className="mt-4 space-y-3"> | |
| {[1,2,3,4,5].map((n) => ( | |
| <button key={n} onClick={() => onSelectUnit(n)} className="w-full text-left px-4 py-3 rounded-lg bg-white border border-gray-100 hover:bg-blue-50"> | |
| Unit {n} | |
| </button> | |
| ))} | |
| </div> | |
| </> | |
| ); | |
| } | |
| // openUnit: render the unit content inside the sidebar (fixed header + scrollable FAQ list) | |
| return ( | |
| <> | |
| <div className="flex items-center justify-between"> | |
| <h3 className="font-bold text-lg">Unit {openUnit}</h3> | |
| <div className="space-x-2"> | |
| <button onClick={() => setOpenUnit(null)} className="text-sm text-gray-600">Back to Units</button> | |
| <button onClick={onBack} className="text-sm text-gray-600">Back</button> | |
| </div> | |
| </div> | |
| <div className="mt-4 flex-1 overflow-auto pr-2"> | |
| {openUnit === 1 ? ( | |
| <div className="space-y-2"> | |
| {faqs.map((q,i) => ( | |
| <button key={i} onClick={() => onFaqClick(q)} className="w-full text-left px-3 py-2 rounded-md bg-white border border-gray-100 hover:bg-gray-50"> | |
| {q.question} | |
| </button> | |
| ))} | |
| </div> | |
| ) : ( | |
| <div> | |
| {faqs[openUnit - 2] ? ( | |
| <button onClick={() => onFaqClick(faqs[openUnit - 2])} className="w-full text-left px-3 py-2 rounded-md bg-white border border-gray-100 hover:bg-gray-50"> | |
| {faqs[openUnit - 2].question} | |
| </button> | |
| ) : ( | |
| <div className="text-xs text-gray-400">No FAQ assigned</div> | |
| )} | |
| </div> | |
| )} | |
| </div> | |
| </> | |
| ); | |
| } | |
| return null; | |
| }; | |
| return ( | |
| <div className="flex w-full h-screen min-w-0 bg-gradient-to-br from-indigo-500 via-violet-500 to-pink-500"> | |
| {/* Sidebar: 600px, vertical layout, header fixed, content scrolls, footer fixed */} | |
| <aside style={{ width: 600, minWidth: 600 }} className="flex-shrink-0 bg-white border border-gray-200 rounded-l-xl p-6 shadow-lg h-screen flex flex-col"> | |
| {/* Sidebar header */} | |
| <div className="flex items-start justify-between mb-3"> | |
| <div> | |
| <h2 className="text-xl font-bold">ICFAI Online Assistant</h2> | |
| <p className="text-xs text-gray-500">Select program → semester → course → unit</p> | |
| </div> | |
| <div> | |
| <button onClick={() => { setFlowStep("program"); setSelectedProgram(null); setSelectedSemester(null); setSelectedCourse(null); setOpenUnit(null); }} className="text-sm text-gray-600">Reset</button> | |
| </div> | |
| </div> | |
| {/* Sidebar content area: this is the scrollable region */} | |
| <div className="flex-1 overflow-auto"> | |
| <SidebarContent /> | |
| </div> | |
| {/* Sidebar footer (fixed) */} | |
| <div className="mt-4"> | |
| <button onClick={() => { setFlowStep("program"); setSelectedProgram(null); setSelectedSemester(null); setSelectedCourse(null); setOpenUnit(null); }} className="w-full bg-blue-600 text-white px-4 py-3 rounded-lg">Main Menu</button> | |
| </div> | |
| </aside> | |
| {/* Right panel: always shows chat */} | |
| <div className="flex-1 flex flex-col h-screen bg-white rounded-r-xl shadow-lg overflow-hidden min-w-0"> | |
| <motion.div className="bg-gradient-to-r from-blue-600 to-blue-700 text-white p-6 flex items-center justify-between"> | |
| <div className="flex items-center gap-4"> | |
| <div className="bg-white p-3 rounded-full"><Bot size={22} className="text-blue-600" /></div> | |
| <div> | |
| <h3 className="font-bold text-lg">ICFAI Online Assistant</h3> | |
| <p className="text-sm text-blue-100">Chat / Answers</p> | |
| </div> | |
| </div> | |
| <div className="flex items-center gap-3"> | |
| <button onClick={onBack} className="text-sm bg-white/10 px-4 py-2 rounded-full">← Go Back</button> | |
| <button onClick={() => { setFlowStep("program"); setSelectedProgram(null); setSelectedSemester(null); setSelectedCourse(null); setOpenUnit(null); }} className="text-sm bg-white/10 px-4 py-2 rounded-full">Main Menu</button> | |
| </div> | |
| </motion.div> | |
| {/* Chat messages */} | |
| <div className="flex-1 overflow-auto p-8 space-y-6" style={{ paddingBottom: 140 }}> | |
| {messages.map((m, i) => ( | |
| <div key={i} className={`flex ${m.type === "user" ? "justify-end" : "justify-start"}`}> | |
| {m.type !== "user" && <div className="bg-blue-600 p-3 rounded-full h-12 w-12 mr-3"><Bot size={18} className="text-white" /></div>} | |
| <div className={`${m.type === "user" ? "bg-blue-600 text-white" : "bg-white text-gray-800"} p-4 rounded-2xl shadow-sm max-w-[85%]`}> | |
| {m.text} | |
| </div> | |
| {m.type === "user" && <div className="bg-gray-300 p-2 rounded-full h-9 w-9 ml-3"><User size={16} className="text-gray-700" /></div>} | |
| </div> | |
| ))} | |
| {isTyping && ( | |
| <div className="flex gap-3"> | |
| <div className="bg-blue-600 p-3 rounded-full h-12 w-12"><Bot size={18} className="text-white" /></div> | |
| <div className="bg-white p-3 rounded-2xl shadow-sm border border-gray-100"> | |
| <div className="flex gap-1"> | |
| <div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce" /> | |
| <div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce" style={{ animationDelay: "150ms" }} /> | |
| <div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce" style={{ animationDelay: "300ms" }} /> | |
| </div> | |
| </div> | |
| </div> | |
| )} | |
| <div ref={messagesEndRef} /> | |
| </div> | |
| {/* Input area */} | |
| <div className="p-6 bg-gray-50 border-t border-gray-200" style={{ position: 'relative', zIndex: 2 }}> | |
| <div className="flex gap-3 items-center"> | |
| <input value={inputValue} onChange={(e) => setInputValue(e.target.value)} placeholder="Ask academic questions about the selected course..." className="flex-1 p-4 border border-gray-300 rounded-2xl focus:outline-none focus:ring-2 focus:ring-blue-600 text-sm" /> | |
| <button onClick={handleSendMessage} disabled={!inputValue.trim()} className="bg-blue-600 text-white p-4 rounded-full disabled:opacity-50"><Send size={20} /></button> | |
| </div> | |
| <p className="text-xs text-gray-400 mt-3 text-center">Powered by ICFAI Online</p> | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| } | |