Skip to content

Commit ce77bda

Browse files
committed
feat: add 5 next-gen capabilities - 3D measurement tools, Groq Whisper STT, Vite bundler, PDF report exporter, and gamified challenges
1. 3D Measurement Tools: Added MeasurementToolManager in src/render/measurementTools.js with raycasting 3D distance ruler & angle protractor overlays. 2. Groq Whisper STT: Added POST /api/voice/transcribe using whisper-large-v3-turbo model. 3. Vite Setup: Added vite.config.js with dev proxy to port 3000 and build configuration. 4. PDF Report Exporter: Added src/ui/reportExporter.js to capture WebGL snapshots, formulas, and transcript into printable PDF reports. 5. Gamified Math Challenges: Added src/core/challengeManager.js and src/ui/challengePanel.js for streak tracking, multipliers, and badge unlocks.
1 parent 60474b4 commit ce77bda

10 files changed

Lines changed: 620 additions & 2 deletions

File tree

data/mindscape.db

0 Bytes
Binary file not shown.

package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
"scripts": {
77
"start": "node server/index.js",
88
"dev": "node --watch server/index.js",
9+
"dev:vite": "vite",
10+
"build": "vite build",
911
"test": "node --test",
1012
"lint": "eslint .",
1113
"lint:fix": "eslint . --fix",
@@ -25,6 +27,7 @@
2527
"devDependencies": {
2628
"@eslint/js": "^9.22.0",
2729
"eslint": "^9.22.0",
28-
"globals": "^16.0.0"
30+
"globals": "^16.0.0",
31+
"vite": "^5.4.0"
2932
}
3033
}

public/index.html

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,24 @@ <h1>AI Spatial Reasoning Tutor</h1>
5454
</button>
5555
<input id="loadSceneInput" type="file" accept="application/json" hidden />
5656
</div>
57+
58+
<div class="tool-cluster" aria-label="3D Measurement & Tools">
59+
<button id="rulerBtn" class="tool-btn" data-tooltip="3D Distance Ruler">
60+
<span class="tool-label">📏 Ruler</span>
61+
</button>
62+
<button id="protractorBtn" class="tool-btn" data-tooltip="3D Angle Protractor">
63+
<span class="tool-label">📐 Angle</span>
64+
</button>
65+
<button id="clearMeasurementsBtn" class="tool-btn" data-tooltip="Clear 3D Measurements">
66+
<span class="tool-label">🧹 Clear 3D</span>
67+
</button>
68+
<button id="exportPdfBtn" class="tool-btn" data-tooltip="Export PDF Report">
69+
<span class="tool-label">📄 Export PDF</span>
70+
</button>
71+
<button id="challengeModeBtn" class="tool-btn" data-tooltip="Toggle Math Challenge Mode">
72+
<span class="tool-label">🏆 Challenges</span>
73+
</button>
74+
</div>
5775
</div>
5876
</header>
5977

server/routes/voice.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,26 @@ export function createVoiceRoute({
4242
}
4343
});
4444

45+
voiceRoute.post("/transcribe", async (c) => {
46+
try {
47+
const { audioBase64 = "", mimeType = "audio/wav" } = await c.req.json();
48+
if (!audioBase64 || typeof audioBase64 !== "string") {
49+
return c.json({ error: "audioBase64 is required" }, 400);
50+
}
51+
52+
const { invokeTranscriptionWithModelFailover } = await import("../services/modelInvoker.js");
53+
const transcript = await invokeTranscriptionWithModelFailover("transcription", {
54+
audioBase64,
55+
mimeType,
56+
});
57+
58+
return c.json({ text: typeof transcript === "string" ? transcript : transcript?.text || "" });
59+
} catch (error) {
60+
console.error("Voice transcribe route error:", error);
61+
return c.json({ error: error.message || "Internal server error" }, 500);
62+
}
63+
});
64+
4565
voiceRoute.post("/session", async (c) => {
4666
try {
4767
return c.json(sessionManager.createSession());

src/core/challengeManager.js

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
/**
2+
* challengeManager.js — Core logic for gamified math challenges, streaks, scores, and badges.
3+
*/
4+
5+
export class ChallengeManager {
6+
constructor() {
7+
this.score = 0;
8+
this.streak = 0;
9+
this.solvedCount = 0;
10+
this.badges = new Set();
11+
this.listeners = new Set();
12+
this.load();
13+
}
14+
15+
load() {
16+
try {
17+
const stored = localStorage.getItem("mindscape_gamification_profile");
18+
if (stored) {
19+
const parsed = JSON.parse(stored);
20+
this.score = parsed.score || 0;
21+
this.streak = parsed.streak || 0;
22+
this.solvedCount = parsed.solvedCount || 0;
23+
this.badges = new Set(parsed.badges || []);
24+
}
25+
} catch (err) {
26+
console.warn("[ChallengeManager] LocalStorage load warning:", err.message);
27+
}
28+
}
29+
30+
save() {
31+
try {
32+
const payload = {
33+
score: this.score,
34+
streak: this.streak,
35+
solvedCount: this.solvedCount,
36+
badges: Array.from(this.badges),
37+
};
38+
localStorage.setItem("mindscape_gamification_profile", JSON.stringify(payload));
39+
} catch (err) {
40+
console.warn("[ChallengeManager] LocalStorage save warning:", err.message);
41+
}
42+
this.notify();
43+
}
44+
45+
onCorrectAnswer(difficulty = "medium") {
46+
const points = difficulty === "hard" ? 150 : difficulty === "easy" ? 50 : 100;
47+
this.streak += 1;
48+
const multiplier = Math.min(3, 1 + Math.floor(this.streak / 3) * 0.5);
49+
const addedPoints = Math.round(points * multiplier);
50+
this.score += addedPoints;
51+
this.solvedCount += 1;
52+
53+
// Evaluate Badge Unlocks
54+
const unlockedNow = [];
55+
if (this.solvedCount >= 1 && !this.badges.has("first_step")) {
56+
this.badges.add("first_step");
57+
unlockedNow.push({ id: "first_step", title: "🌱 First Step", desc: "Solved your first 3D lesson!" });
58+
}
59+
if (this.streak >= 3 && !this.badges.has("streak_3")) {
60+
this.badges.add("streak_3");
61+
unlockedNow.push({ id: "streak_3", title: "🔥 On Fire", desc: "Achieved a 3-lesson streak!" });
62+
}
63+
if (this.solvedCount >= 5 && !this.badges.has("3d_explorer")) {
64+
this.badges.add("3d_explorer");
65+
unlockedNow.push({ id: "3d_explorer", title: "📐 3D Explorer", desc: "Completed 5 spatial geometry challenges!" });
66+
}
67+
if (this.score >= 500 && !this.badges.has("score_500")) {
68+
this.badges.add("score_500");
69+
unlockedNow.push({ id: "score_500", title: "⭐ Math Master", desc: "Earned 500+ challenge points!" });
70+
}
71+
72+
this.save();
73+
return { addedPoints, multiplier, newStreak: this.streak, unlockedNow };
74+
}
75+
76+
onIncorrectAnswer() {
77+
this.streak = 0;
78+
this.save();
79+
}
80+
81+
subscribe(listener) {
82+
this.listeners.add(listener);
83+
listener(this.getState());
84+
return () => this.listeners.delete(listener);
85+
}
86+
87+
notify() {
88+
const state = this.getState();
89+
this.listeners.forEach((fn) => fn(state));
90+
}
91+
92+
getState() {
93+
return {
94+
score: this.score,
95+
streak: this.streak,
96+
solvedCount: this.solvedCount,
97+
badges: Array.from(this.badges),
98+
};
99+
}
100+
}
101+
102+
export const challengeManager = new ChallengeManager();

src/main.js

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ import { bootstrapApp } from "./app.js";
22
import { initTutorController, updateTutorLabels } from "./ui/tutorController.js";
33
import { initDemoMode } from "./ui/demoMode.js";
44
import { initFloatingChat } from "./ui/chatController.js?v=2";
5+
import { MeasurementToolManager } from "./render/measurementTools.js";
6+
import { exportPdfReport } from "./ui/reportExporter.js";
7+
import { initChallengeUI } from "./ui/challengePanel.js";
58

69
const appContext = bootstrapApp();
710
const isDemoMode = new URLSearchParams(window.location.search).get("demo") === "true";
@@ -15,9 +18,71 @@ if (isDemoMode) {
1518
// Initialize Chatbot
1619
initFloatingChat();
1720

18-
// Add label rendering to the animation loop
21+
// Initialize Gamification Banner
22+
initChallengeUI();
23+
24+
// ─── 3D Measurement & Toolbar Wiring ─────────────────────────────────────────
25+
let measurementTools = null;
26+
if (appContext?.world) {
27+
measurementTools = new MeasurementToolManager(appContext.world);
28+
}
29+
30+
const rulerBtn = document.getElementById("rulerBtn");
31+
const protractorBtn = document.getElementById("protractorBtn");
32+
const clearMeasurementsBtn = document.getElementById("clearMeasurementsBtn");
33+
const exportPdfBtn = document.getElementById("exportPdfBtn");
34+
35+
if (rulerBtn) {
36+
rulerBtn.addEventListener("click", () => {
37+
if (!measurementTools && appContext?.world) {
38+
measurementTools = new MeasurementToolManager(appContext.world);
39+
}
40+
const isActive = rulerBtn.classList.contains("active");
41+
rulerBtn.classList.toggle("active", !isActive);
42+
if (protractorBtn) protractorBtn.classList.remove("active");
43+
measurementTools?.setMode(!isActive ? "ruler" : "off");
44+
});
45+
}
46+
47+
if (protractorBtn) {
48+
protractorBtn.addEventListener("click", () => {
49+
if (!measurementTools && appContext?.world) {
50+
measurementTools = new MeasurementToolManager(appContext.world);
51+
}
52+
const isActive = protractorBtn.classList.contains("active");
53+
protractorBtn.classList.toggle("active", !isActive);
54+
if (rulerBtn) rulerBtn.classList.remove("active");
55+
measurementTools?.setMode(!isActive ? "protractor" : "off");
56+
});
57+
}
58+
59+
if (clearMeasurementsBtn) {
60+
clearMeasurementsBtn.addEventListener("click", () => {
61+
measurementTools?.clearAllMeasurements();
62+
if (rulerBtn) rulerBtn.classList.remove("active");
63+
if (protractorBtn) protractorBtn.classList.remove("active");
64+
measurementTools?.setMode("off");
65+
});
66+
}
67+
68+
if (exportPdfBtn) {
69+
exportPdfBtn.addEventListener("click", () => {
70+
const questionInput = document.getElementById("questionInput");
71+
const questionText = questionInput?.value || "Mindscape 3D STEM Learning Lesson";
72+
exportPdfReport({
73+
world: appContext?.world,
74+
questionText,
75+
lessonPlan: null,
76+
history: [],
77+
finalAnswer: null,
78+
});
79+
});
80+
}
81+
82+
// Add label rendering and measurement updates to the animation loop
1983
function tutorRenderLoop() {
2084
updateTutorLabels();
85+
measurementTools?.update();
2186
requestAnimationFrame(tutorRenderLoop);
2287
}
2388
requestAnimationFrame(tutorRenderLoop);

0 commit comments

Comments
 (0)