diff --git a/app/courses/[courseId]/layout.tsx b/app/courses/[courseId]/layout.tsx index c1d7f8a..905a9d5 100644 --- a/app/courses/[courseId]/layout.tsx +++ b/app/courses/[courseId]/layout.tsx @@ -1,6 +1,7 @@ import { ReactNode } from "react"; import Link from "next/link"; import { getAuthUser } from "@/lib/auth"; +import CourseBreadcrumb from "@/components/navigation/CourseBreadCrumb"; export default async function CourseLayout({ children, @@ -13,30 +14,20 @@ export default async function CourseLayout({ const isAdmin = user?.organisation?.role === "admin"; const { courseId } = await params; - const response = { - id: courseId, - name: "Sample Course", - }; return (
- + {isAdmin && (
Edit Course Details Add new module diff --git a/app/courses/[courseId]/modules/[moduleId]/page.tsx b/app/courses/[courseId]/modules/[moduleId]/page.tsx index 5c7337c..1b9ea3d 100644 --- a/app/courses/[courseId]/modules/[moduleId]/page.tsx +++ b/app/courses/[courseId]/modules/[moduleId]/page.tsx @@ -10,10 +10,16 @@ export default async function ModulePage({ const user = await getAuthUser(); const isAdmin = user?.organisation?.role === "admin"; const { courseId, moduleId } = await params; + const isAiEnabled = user?.organisation?.ai_enabled; return (
- +
); } diff --git a/app/page.tsx b/app/page.tsx index 068fa2b..5cc3666 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -7,20 +7,54 @@ export default async function Home() { if (user) { redirect("/dashboard"); } + return (
-
-

SKILLSTACK

-
- +
+

SKILLSTACK

+

+ A corporate learning platform to organize courses for your team, + complete with AI chatbot support. +

+
+ Get Started
-
-
-
+
+
+

Course Management

+

+ Create, organize, and assign courses tailored to your + organization’s needs. +

+
+
+

Employee Learning

+

+ Empower employees with structured learning paths and progress + tracking. +

+
+
+

AI Chatbot Support

+

+ Get instant help and guidance through our built-in AI chatbot + assistant. +

+
+ +

+ More features coming soon! +

); diff --git a/app/roadmap/page.tsx b/app/roadmap/page.tsx index 235de26..7ffd4fa 100644 --- a/app/roadmap/page.tsx +++ b/app/roadmap/page.tsx @@ -12,6 +12,12 @@ export default function RoadmapPage() { const [selectedRoadmap, setSelectedRoadmap] = useState(null); const [loading, setLoading] = useState(true); + let isAiEnabled = user?.organisation?.ai_enabled; + + if (isAiEnabled === undefined) { + isAiEnabled = false; + } + if (!user || !user.hasCompletedOnboarding) { return null; } @@ -137,6 +143,7 @@ export default function RoadmapPage() { onDelete={deleteRoadmap} onCreateNew={createNewRoadmap} onAutoGenerate={autoGenerateRoadmap} + isAiEnabled={isAiEnabled} /> ); } diff --git a/components/SideNav.tsx b/components/SideNav.tsx index dea1ddb..8bccf94 100644 --- a/components/SideNav.tsx +++ b/components/SideNav.tsx @@ -43,6 +43,8 @@ export default function SideNav() { const avatarUrl = `https://avatar.iran.liara.run/username?username=${firstName}+${lastName}&background=f4d9b2&color=FF9800`; const [imgSrc, setImgSrc] = useState(avatarUrl); + const isAiEnabled = user.organisation?.ai_enabled; + return (
diff --git a/components/chatbot/ChatHistory.tsx b/components/chatbot/ChatHistory.tsx index 23c40e5..14ac191 100644 --- a/components/chatbot/ChatHistory.tsx +++ b/components/chatbot/ChatHistory.tsx @@ -52,35 +52,68 @@ function groupLogsByModule(logs: ChatLog[]) { export default function ChatHistoryPage() { const [logs, setLogs] = useState([]); const [openPanels, setOpenPanels] = useState>({}); + const [clearError, setClearError] = useState(null); useEffect(() => { - const fetchLogs = async () => { - try { - const res = await fetch("/api/chatbot/history", { - credentials: "include", - }); - const data = await res.json(); - if (data.success && Array.isArray(data.logs)) { - setLogs(data.logs); - if (data.logs.length > 0) { - const mostRecent = data.logs[0]; - setOpenPanels({ - [`${mostRecent.course_id}:${mostRecent.module_id}`]: true, - }); - } + fetchHistory(); + }, []); + + async function fetchHistory() { + try { + const res = await fetch("/api/chatbot/history", { + credentials: "include", + }); + const data = await res.json(); + if (data.success && Array.isArray(data.logs)) { + setLogs(data.logs); + if (data.logs.length > 0) { + const mostRecent = data.logs[0]; + setOpenPanels({ + [`${mostRecent.course_id}:${mostRecent.module_id}`]: true, + }); } - } catch (err) { - // Ignore errors in loading logs } - }; - fetchLogs(); - }, []); + } catch { + // ignore load errors + } + } + + async function doDelete(url: string, body?: object) { + setClearError(null); + try { + const res = await fetch(url, { + method: "DELETE", + credentials: "include", + headers: { "Content-Type": "application/json" }, + body: body ? JSON.stringify(body) : undefined, + }); + if (!res.ok) throw new Error("Failed to clear logs"); + await fetchHistory(); + } catch (err: any) { + setClearError(err.message || "Failed to clear logs"); + } + } const grouped = groupLogsByModule(logs); return (
-

Your Chatbot Q&A History

+
+

Your Chatbot Q&A History

+ +
+ {clearError && ( +
{clearError}
+ )} + {logs.length === 0 ? (
No questions asked yet!
) : ( @@ -90,49 +123,69 @@ export default function ChatHistoryPage() { const isOpen = openPanels[panelKey] || false; return (
- {/* Panel header */} - + + + +
{isOpen && (
- {mod.logs.map((log, i) => ( + {mod.logs.map((log) => (
{formatDate(log.created_at)} diff --git a/components/chatbot/ModuleChatBot.tsx b/components/chatbot/ModuleChatBot.tsx index 5904854..a9cdae1 100644 --- a/components/chatbot/ModuleChatBot.tsx +++ b/components/chatbot/ModuleChatBot.tsx @@ -16,11 +16,11 @@ export default function ModuleChatbot({ isEnrolled, }: ModuleChatbotProps) { const [question, setQuestion] = useState(""); - const [chat, setChat] = useState< - { type: "user" | "assistant"; content: string }[] - >([]); + const [chat, setChat] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); + const [clearing, setClearing] = useState(false); + const [clearError, setClearError] = useState(null); if (!isEnrolled) { return null; @@ -46,12 +46,12 @@ export default function ModuleChatbot({ }); setChat(logMessages); } - } catch (err) { - // Ignore errors in loading logs + } catch { + // Ignore load errors } }; fetchLogs(); - }, []); + }, [courseId, moduleId]); const sendQuestion = async (e: React.FormEvent) => { e.preventDefault(); @@ -70,7 +70,7 @@ export default function ModuleChatbot({ }); const data = await res.json(); - if (data?.success && data.answer) { + if (data.success && data.answer) { setChat((prev) => [ ...prev, { type: "assistant", content: data.answer }, @@ -78,17 +78,47 @@ export default function ModuleChatbot({ } else { setError(data.message || "Something went wrong."); } - } catch (err: any) { + } catch { setError("Failed to get response. Please try again."); } setLoading(false); setQuestion(""); }; + const clearHistory = async () => { + if (!confirm("Clear all chat history for this module?")) return; + setClearError(null); + setClearing(true); + try { + const res = await fetch("/api/chatbot/module-log", { + method: "DELETE", + credentials: "include", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ moduleId }), + }); + if (!res.ok) throw new Error("Failed to clear history"); + setChat([]); + } catch (err: any) { + setClearError(err.message || "Could not clear history."); + } + setClearing(false); + }; + return (
-

Module Assistant

-
Note: Bot does not have access to course materials.
+
+

Module Assistant

+ +
+
+ Note: Bot does not have access to course materials. +
{chat.length === 0 && (
@@ -115,6 +145,9 @@ export default function ModuleChatbot({
Assistant is typing…
)}
+ {clearError && ( +
{clearError}
+ )} {error &&
{error}
}
+
+ + SkillStack Logo + + SKILLSTACK + + + + +
+ +
+ Your avatar setImgSrc("/avatar-placeholder.jpg")} + /> + +
+ + ); +} diff --git a/components/navigation/CourseBreadCrumb.tsx b/components/navigation/CourseBreadCrumb.tsx new file mode 100644 index 0000000..f051958 --- /dev/null +++ b/components/navigation/CourseBreadCrumb.tsx @@ -0,0 +1,29 @@ +"use client"; + +import { usePathname } from "next/navigation"; +import Link from "next/link"; + +export default function CourseBreadcrumb({ courseId }: { courseId: string }) { + const pathname = usePathname(); + + if (!pathname) return null; + + let linkHref = "/courses"; + let label = "Go to All Courses"; + + if (pathname.includes(`/courses/${courseId}/modules/`)) { + linkHref = `/courses/${courseId}`; + label = "← Back to Modules"; + } else if (pathname.includes(`/courses/${courseId}`)) { + linkHref = "/courses"; + label = "← Back to Courses"; + } + + return ( +
+ + {label} + +
+ ); +} diff --git a/components/organisation/OrgNav.tsx b/components/organisation/OrgNav.tsx index fb8490a..f2fb059 100644 --- a/components/organisation/OrgNav.tsx +++ b/components/organisation/OrgNav.tsx @@ -44,6 +44,8 @@ export default function OrgNav() { const avatarUrl = `https://avatar.iran.liara.run/username?username=${firstName}+${lastName}&background=f4d9b2&color=FF9800`; const [imgSrc, setImgSrc] = useState(avatarUrl); + const isAiEnabled = user.organisation?.ai_enabled; + return (
- + {isAiEnabled && ( + + )}
); @@ -329,9 +337,9 @@ export default function ModuleDetail({ courseId, moduleId, isAdmin }: Props) { return (

{data.title} — Results

- {results.map((r) => ( + {results.map((r, index) => (
-

Question {r.questionId}

+

Question {index + 1}

Your answers:  {r.selectedOptions.map((o) => o.text).join(", ")} @@ -370,14 +378,16 @@ export default function ModuleDetail({ courseId, moduleId, isAdmin }: Props) { answers will be lost

)} - {quiz?.questions.map((q) => ( + {quiz?.questions.map((q, index) => (
-

{q.question_text}

+

+ {index + 1}. {q.question_text} +

{q.options.map((opt) => (