From 5fbeab1bd82eaa66310efcc31b8f3df1ca00e6d1 Mon Sep 17 00:00:00 2001 From: lavanyagarg112 Date: Fri, 11 Jul 2025 12:36:55 +0800 Subject: [PATCH 01/11] Update quiz question number bug --- components/organisation/courses/ModuleDetail.tsx | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/components/organisation/courses/ModuleDetail.tsx b/components/organisation/courses/ModuleDetail.tsx index 3dd02bc..ac80230 100644 --- a/components/organisation/courses/ModuleDetail.tsx +++ b/components/organisation/courses/ModuleDetail.tsx @@ -329,9 +329,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 +370,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) => (
From 0e107c8aaca1424c5169f385d890c8ffcb901a70 Mon Sep 17 00:00:00 2001 From: lavanyagarg112 Date: Fri, 11 Jul 2025 12:52:27 +0800 Subject: [PATCH 03/11] Update navigation in course page --- app/courses/[courseId]/layout.tsx | 17 +++---------- components/navigation/CourseBreadCrumb.tsx | 29 ++++++++++++++++++++++ 2 files changed, 33 insertions(+), 13 deletions(-) create mode 100644 components/navigation/CourseBreadCrumb.tsx 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/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} + +
+ ); +} From 8d9fad39930c7bbcccf438f87adead3ba4862756 Mon Sep 17 00:00:00 2001 From: lavanyagarg112 Date: Fri, 11 Jul 2025 13:01:33 +0800 Subject: [PATCH 04/11] remove channel recommendations and refresh --- components/roadmap/RoadmapEditor.tsx | 235 +++++++++++++++------------ 1 file changed, 131 insertions(+), 104 deletions(-) diff --git a/components/roadmap/RoadmapEditor.tsx b/components/roadmap/RoadmapEditor.tsx index 86dc052..70961e7 100644 --- a/components/roadmap/RoadmapEditor.tsx +++ b/components/roadmap/RoadmapEditor.tsx @@ -14,16 +14,17 @@ export default function RoadmapEditor({ roadmap, onBack, onUpdate }: Props) { const [roadmapName, setRoadmapName] = useState(roadmap.name); const [items, setItems] = useState([]); const [availableModules, setAvailableModules] = useState([]); - const [channelRecommendations, setChannelRecommendations] = useState<{[key: string]: Module[]}>({}); + // const [channelRecommendations, setChannelRecommendations] = useState<{[key: string]: Module[]}>({}); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); const [showModuleSelector, setShowModuleSelector] = useState(false); - const [showChannelRecommendations, setShowChannelRecommendations] = useState(false); + // const [showChannelRecommendations, setShowChannelRecommendations] = + // useState(false); useEffect(() => { fetchRoadmapItems(); fetchAvailableModules(); - fetchChannelRecommendations(); + // fetchChannelRecommendations(); }, [roadmap.id]); const fetchRoadmapItems = async () => { @@ -78,44 +79,44 @@ export default function RoadmapEditor({ roadmap, onBack, onUpdate }: Props) { } }; - const fetchChannelRecommendations = async () => { - try { - const response = await fetch("/api/materials/by-user-skills", { - credentials: "include", - }); - - if (response.ok) { - const data = await response.json(); - const modules = data.materials || []; - - // Group modules by channel - const groupedByChannel: {[key: string]: Module[]} = {}; - - modules.forEach((module: Module) => { - if (module.channel) { - const channelName = module.channel.name; - if (!groupedByChannel[channelName]) { - groupedByChannel[channelName] = []; - } - groupedByChannel[channelName].push(module); - } - }); - - // Sort channels by number of modules and limit to top 3 modules per channel - const sortedChannels: {[key: string]: Module[]} = {}; - Object.entries(groupedByChannel) - .sort(([,a], [,b]) => b.length - a.length) - .forEach(([channel, modules]) => { - sortedChannels[channel] = modules.slice(0, 3); - }); - - setChannelRecommendations(sortedChannels); - } - } catch (error) { - console.error("Failed to fetch channel recommendations:", error); - setChannelRecommendations({}); - } - }; + // const fetchChannelRecommendations = async () => { + // try { + // const response = await fetch("/api/materials/by-user-skills", { + // credentials: "include", + // }); + + // if (response.ok) { + // const data = await response.json(); + // const modules = data.materials || []; + + // // Group modules by channel + // const groupedByChannel: { [key: string]: Module[] } = {}; + + // modules.forEach((module: Module) => { + // if (module.channel) { + // const channelName = module.channel.name; + // if (!groupedByChannel[channelName]) { + // groupedByChannel[channelName] = []; + // } + // groupedByChannel[channelName].push(module); + // } + // }); + + // // Sort channels by number of modules and limit to top 3 modules per channel + // const sortedChannels: { [key: string]: Module[] } = {}; + // Object.entries(groupedByChannel) + // .sort(([, a], [, b]) => b.length - a.length) + // .forEach(([channel, modules]) => { + // sortedChannels[channel] = modules.slice(0, 3); + // }); + + // setChannelRecommendations(sortedChannels); + // } + // } catch (error) { + // console.error("Failed to fetch channel recommendations:", error); + // setChannelRecommendations({}); + // } + // }; const updateRoadmapName = async () => { if (roadmapName.trim() === roadmap.name) return; @@ -239,7 +240,7 @@ export default function RoadmapEditor({ roadmap, onBack, onUpdate }: Props) {

Learning Modules

- + */}
- {item.course_name} • {item.module_type} + + {item.course_name} • {item.module_type} + {(item.channel || item.level) && (
{item.channel && ( @@ -445,7 +448,9 @@ export default function RoadmapEditor({ roadmap, onBack, onUpdate }: Props) { )}
- {module.course_name} • {module.module_type} + + {module.course_name} • {module.module_type} + {(module.channel || module.level) && (
{module.channel && ( @@ -469,8 +474,9 @@ export default function RoadmapEditor({ roadmap, onBack, onUpdate }: Props) { {module.skills && (

Skills:{" "} - {module.skills.filter((skill) => skill).join(", ") || - "None"} + {module.skills + .filter((skill) => skill) + .join(", ") || "None"}

)}
@@ -489,11 +495,13 @@ export default function RoadmapEditor({ roadmap, onBack, onUpdate }: Props) {
)} - {showChannelRecommendations && ( + {/* {showChannelRecommendations && (
-

Channel-Based Recommendations

+

+ Channel-Based Recommendations +

{Object.keys(channelRecommendations).length === 0 ? ( -

No channel-based recommendations available.

+

+ No channel-based recommendations available. +

) : (
- {Object.entries(channelRecommendations).map(([channelName, modules]) => ( -
-

- - {channelName} - - ({modules.length} modules) -

-
- {modules - .filter(module => !items.some(item => item.module_id === module.id)) - .map(module => ( -
-
-
-
{module.module_title}
- {module.level && ( - - {module.level.name} - - )} - {module.matching_skills && module.matching_skills > 0 && ( - - {module.matching_skills} skills match - + {Object.entries(channelRecommendations).map( + ([channelName, modules]) => ( +
+

+ + {channelName} + + + ({modules.length} modules) + +

+
+ {modules + .filter( + (module) => + !items.some( + (item) => item.module_id === module.id + ) + ) + .map((module) => ( +
+
+
+
+ {module.module_title} +
+ {module.level && ( + + {module.level.name} + + )} + {module.matching_skills && + module.matching_skills > 0 && ( + + {module.matching_skills} skills match + + )} +
+

+ {module.course_name} • {module.module_type} +

+ {module.description && ( +

+ {module.description} +

)}
-

- {module.course_name} • {module.module_type} -

- {module.description && ( -

- {module.description} -

- )} +
- -
- )) - } - {modules.filter(module => !items.some(item => item.module_id === module.id)).length === 0 && ( -

All modules from this channel are already in your roadmap.

- )} + ))} + {modules.filter( + (module) => + !items.some((item) => item.module_id === module.id) + ).length === 0 && ( +

+ All modules from this channel are already in your + roadmap. +

+ )} +
-
- ))} + ) + )}
)}
- )} + )} */}
); } From ec1e010f8566d1e9c22c07e11eb9dbcff404cdd9 Mon Sep 17 00:00:00 2001 From: lavanyagarg112 Date: Fri, 11 Jul 2025 13:03:59 +0800 Subject: [PATCH 05/11] Update spacing --- components/roadmap/RoadmapEditor.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/roadmap/RoadmapEditor.tsx b/components/roadmap/RoadmapEditor.tsx index 70961e7..ffb2b61 100644 --- a/components/roadmap/RoadmapEditor.tsx +++ b/components/roadmap/RoadmapEditor.tsx @@ -296,7 +296,7 @@ export default function RoadmapEditor({ roadmap, onBack, onUpdate }: Props) { #{index + 1} -
+

{item.module_title}

{isAccessible ? ( From 8f3485140b9acc572c6d44afc6bad6166afb8b3e Mon Sep 17 00:00:00 2001 From: lavanyagarg112 Date: Fri, 11 Jul 2025 13:09:56 +0800 Subject: [PATCH 06/11] Update message of no new roadmap on frontend --- components/roadmap/RoadmapList.tsx | 55 +++++++++++++++++++++++++----- 1 file changed, 46 insertions(+), 9 deletions(-) diff --git a/components/roadmap/RoadmapList.tsx b/components/roadmap/RoadmapList.tsx index 56dfd0b..b5b2246 100644 --- a/components/roadmap/RoadmapList.tsx +++ b/components/roadmap/RoadmapList.tsx @@ -1,5 +1,3 @@ -"use client"; - import { useState } from "react"; import { Roadmap } from "./types"; @@ -8,15 +6,22 @@ interface Props { onSelect: (roadmap: Roadmap) => void; onDelete: (roadmapId: number) => void; onCreateNew: () => void; - onAutoGenerate: () => void; + onAutoGenerate: () => Promise<{ roadmap?: Roadmap; modulesAdded?: number }>; } -export default function RoadmapList({ roadmaps, onSelect, onDelete, onCreateNew, onAutoGenerate }: Props) { +export default function RoadmapList({ + roadmaps, + onSelect, + onDelete, + onCreateNew, + onAutoGenerate, +}: Props) { const [deletingId, setDeletingId] = useState(null); + const [autoGenMessage, setAutoGenMessage] = useState(null); const handleDelete = async (roadmapId: number) => { if (!confirm("Are you sure you want to delete this roadmap?")) return; - + setDeletingId(roadmapId); try { await onDelete(roadmapId); @@ -25,13 +30,39 @@ export default function RoadmapList({ roadmaps, onSelect, onDelete, onCreateNew, } }; + const handleAutoGenerate = async () => { + setAutoGenMessage(null); + try { + const result = await onAutoGenerate(); + if ( + !result || + (result.modulesAdded !== undefined && result.modulesAdded === 0) + ) { + setAutoGenMessage( + "No new modules to recommend. You can create it manually if you want." + ); + } + } catch (err: any) { + const msg = err?.message || "Failed to auto-generate roadmap."; + if (msg.toLowerCase().includes("same set of modules")) { + setAutoGenMessage( + "No new modules to recommend. You can create it manually if you want." + ); + } else { + setAutoGenMessage(msg); + } + } + }; + return (
-

My Learning Roadmaps

+

+ My Learning Roadmaps +

+ {autoGenMessage && ( +
+ {autoGenMessage} +
+ )} + {roadmaps.length === 0 ? (

You don't have any roadmaps yet.

); -} \ No newline at end of file +} From a5fb00e5c4ea6e8fab4e07ef4f9bceb4182f5dda Mon Sep 17 00:00:00 2001 From: lavanyagarg112 Date: Fri, 11 Jul 2025 13:10:31 +0800 Subject: [PATCH 07/11] Add dismiss for notification --- components/roadmap/RoadmapList.tsx | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/components/roadmap/RoadmapList.tsx b/components/roadmap/RoadmapList.tsx index b5b2246..82ad113 100644 --- a/components/roadmap/RoadmapList.tsx +++ b/components/roadmap/RoadmapList.tsx @@ -77,8 +77,15 @@ export default function RoadmapList({
{autoGenMessage && ( -
- {autoGenMessage} +
+ {autoGenMessage} +
)} From a8fa3c37fdb47a445d03bbb1f243633f86b9a3f3 Mon Sep 17 00:00:00 2001 From: lavanyagarg112 Date: Fri, 11 Jul 2025 13:16:22 +0800 Subject: [PATCH 08/11] Modify types --- components/roadmap/RoadmapList.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/components/roadmap/RoadmapList.tsx b/components/roadmap/RoadmapList.tsx index 82ad113..b762df9 100644 --- a/components/roadmap/RoadmapList.tsx +++ b/components/roadmap/RoadmapList.tsx @@ -6,7 +6,10 @@ interface Props { onSelect: (roadmap: Roadmap) => void; onDelete: (roadmapId: number) => void; onCreateNew: () => void; - onAutoGenerate: () => Promise<{ roadmap?: Roadmap; modulesAdded?: number }>; + onAutoGenerate: () => Promise<{ + roadmap?: Roadmap; + modulesAdded?: number; + } | void>; } export default function RoadmapList({ From ecba750d27cb0b7e0e994436da1e92dca70e160c Mon Sep 17 00:00:00 2001 From: lavanyagarg112 Date: Fri, 11 Jul 2025 13:19:44 +0800 Subject: [PATCH 09/11] Enhance front page --- app/page.tsx | 48 +++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 41 insertions(+), 7 deletions(-) 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! +

); From 3978db161111fc379f970f5818241795260a5c2e Mon Sep 17 00:00:00 2001 From: lavanyagarg112 Date: Fri, 11 Jul 2025 13:42:32 +0800 Subject: [PATCH 10/11] Add ai enabled flag --- .../[courseId]/modules/[moduleId]/page.tsx | 8 +- app/roadmap/page.tsx | 7 ++ components/SideNav.tsx | 75 ++++++----- components/extra | 119 ++++++++++++++++++ components/organisation/OrgNav.tsx | 75 ++++++----- .../organisation/courses/ModuleDetail.tsx | 20 ++- .../organisation/settings/OrgSettings.tsx | 13 +- components/roadmap/RoadmapList.tsx | 30 +++-- context/AuthContext.tsx | 1 + 9 files changed, 258 insertions(+), 90 deletions(-) create mode 100644 components/extra 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/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 (
- + {isAiEnabled && ( + + )}
); diff --git a/components/organisation/settings/OrgSettings.tsx b/components/organisation/settings/OrgSettings.tsx index 6079f43..c199a08 100644 --- a/components/organisation/settings/OrgSettings.tsx +++ b/components/organisation/settings/OrgSettings.tsx @@ -72,11 +72,18 @@ export default function OrgSettings() { }), }); if (!res.ok) throw new Error("Failed to save settings"); - setName(name); - setDescription(description); - setAiEnabled(aiEnabled); + // setName(name); + // setDescription(description); + // setAiEnabled(aiEnabled); setError(null); setMessage("Settings saved successfully"); + setInitialData({ + id: organisationId, + organisation_name: name, + description, + ai_enabled: aiEnabled, + }); + window.location.reload(); } catch (err: any) { setError(err.message || "Failed to save settings"); setMessage(null); diff --git a/components/roadmap/RoadmapList.tsx b/components/roadmap/RoadmapList.tsx index b762df9..26b0f9a 100644 --- a/components/roadmap/RoadmapList.tsx +++ b/components/roadmap/RoadmapList.tsx @@ -10,6 +10,7 @@ interface Props { roadmap?: Roadmap; modulesAdded?: number; } | void>; + isAiEnabled: boolean; } export default function RoadmapList({ @@ -18,6 +19,7 @@ export default function RoadmapList({ onDelete, onCreateNew, onAutoGenerate, + isAiEnabled, }: Props) { const [deletingId, setDeletingId] = useState(null); const [autoGenMessage, setAutoGenMessage] = useState(null); @@ -64,12 +66,14 @@ export default function RoadmapList({ My Learning Roadmaps
- + {isAiEnabled && ( + + )} + {isAiEnabled && ( + + )} +
+ {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}
}