Skip to content

Commit 4090bfd

Browse files
committed
AutiSense v2.1: comprehensive bug fix and polish
- Auth: UserMenu component on all pages, DynamoDB SDK credential fix, OAuth prompt fix - TTS: unified Polly/browser fallback helper, speech page word fetch bug fix, auto-play - Chat: removed gender toggle, fallback mode indicator, mic permission check, expanded fallbacks - Color & Sound: persistent AudioContext, TTS voice cue after tone - Bubble Pop: guaranteed target bubbles, no blank screen, larger target display - Memory: capped at 3x3 grid (4 pairs max) - Detection: elapsed timer mode replacing 1-hour countdown hack - Dashboard: Lucide icons for quick links, 4 recent/default games instead of 12-game grid - BottomNav: Lucide icons, rounded top corners, max-width centered - Nearby Help: proper leaflet npm install, Overpass API integration, live results - UI: increased base font to 17px, Sun/Moon theme toggle icons - Added packages: lucide-react, leaflet, react-leaflet, @types/leaflet
1 parent 7a72051 commit 4090bfd

22 files changed

Lines changed: 980 additions & 384 deletions

File tree

app/api/auth/google/route.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ export async function GET() {
3030
scope: google.scopes,
3131
state,
3232
access_type: "offline",
33-
prompt: "consent",
33+
prompt: "select_account",
3434
});
3535

3636
const authorizationUrl = `${google.authUrl}?${params.toString()}`;

app/api/chat/conversation/route.ts

Lines changed: 39 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -125,39 +125,47 @@ function buildFallbackTurn(
125125
): ConversationResponse {
126126
const prefix = (animalPersonality && PERSONALITY_PREFIX[animalPersonality]) || "";
127127

128-
const fallback: Array<Omit<ConversationResponse, "fallback">> = [
129-
{
130-
text: `${prefix}Hi ${childName}! I'm so happy to talk with you today! Are you ready to play a fun game with me?`,
131-
metadata: { turnType: "greeting", expectsResponse: true, responseRelevance: 0.5, shouldEnd: false, domain: "social" },
132-
},
133-
{
134-
text: `${prefix}Awesome! Let's start with something fun. Can you wave hello to me?`,
135-
metadata: { turnType: "instruction", expectsResponse: true, responseRelevance: 0.5, shouldEnd: false, domain: "motor", action: "wave" },
136-
},
137-
{
138-
text: `${prefix}Great job! Now tell me, what color is the sky?`,
139-
metadata: { turnType: "question", expectsResponse: true, responseRelevance: 0.5, shouldEnd: false, domain: "cognitive" },
140-
},
141-
{
142-
text: `${prefix}You're doing so well! Can you say the word banana for me?`,
143-
metadata: { turnType: "question", expectsResponse: true, responseRelevance: 0.5, shouldEnd: false, domain: "language" },
144-
},
145-
{
146-
text: `${prefix}That's wonderful! Now let's try something silly. Can you touch your nose?`,
147-
metadata: { turnType: "instruction", expectsResponse: true, responseRelevance: 0.5, shouldEnd: false, domain: "motor", action: "touch_nose" },
148-
},
149-
{
150-
text: `${prefix}You're a superstar! What's your favorite animal?`,
151-
metadata: { turnType: "question", expectsResponse: true, responseRelevance: 0.5, shouldEnd: false, domain: "social" },
152-
},
153-
{
154-
text: `${prefix}You did such an amazing job, ${childName}! Thank you so much for talking with me today! You're wonderful!`,
155-
metadata: { turnType: "farewell", expectsResponse: false, responseRelevance: 0.5, shouldEnd: true, domain: "general" },
156-
},
128+
// Multiple greeting/question pools for variety
129+
const greetings = [
130+
`${prefix}Hi ${childName}! I'm so happy to talk with you today! Are you ready to play a fun game with me?`,
131+
`${prefix}Hello ${childName}! It's so nice to meet you! Do you want to have some fun together?`,
132+
`${prefix}Hey there, ${childName}! I've been waiting to play with you! Shall we get started?`,
133+
];
134+
135+
const midQuestions: Array<Omit<ConversationResponse, "fallback">> = [
136+
{ text: `${prefix}Awesome! Let's start with something fun. Can you wave hello to me?`, metadata: { turnType: "instruction", expectsResponse: true, responseRelevance: 0.5, shouldEnd: false, domain: "motor", action: "wave" } },
137+
{ text: `${prefix}Great job! Now tell me, what color is the sky?`, metadata: { turnType: "question", expectsResponse: true, responseRelevance: 0.5, shouldEnd: false, domain: "cognitive" } },
138+
{ text: `${prefix}You're doing so well! Can you say the word butterfly for me?`, metadata: { turnType: "question", expectsResponse: true, responseRelevance: 0.5, shouldEnd: false, domain: "language" } },
139+
{ text: `${prefix}That's wonderful! Can you clap your hands for me?`, metadata: { turnType: "instruction", expectsResponse: true, responseRelevance: 0.5, shouldEnd: false, domain: "motor", action: "clap" } },
140+
{ text: `${prefix}You're a superstar! What's your favorite animal?`, metadata: { turnType: "question", expectsResponse: true, responseRelevance: 0.5, shouldEnd: false, domain: "social" } },
141+
{ text: `${prefix}Amazing! Can you count to three with me? One, two...`, metadata: { turnType: "question", expectsResponse: true, responseRelevance: 0.5, shouldEnd: false, domain: "cognitive" } },
142+
{ text: `${prefix}That's great! Now let's try something silly. Can you touch your nose?`, metadata: { turnType: "instruction", expectsResponse: true, responseRelevance: 0.5, shouldEnd: false, domain: "motor", action: "touch_nose" } },
143+
{ text: `${prefix}So cool! What's your favorite food?`, metadata: { turnType: "question", expectsResponse: true, responseRelevance: 0.5, shouldEnd: false, domain: "social" } },
144+
{ text: `${prefix}Nice! Can you tell me what sound a cat makes?`, metadata: { turnType: "question", expectsResponse: true, responseRelevance: 0.5, shouldEnd: false, domain: "language" } },
145+
{ text: `${prefix}You're doing great! Can you raise your arms up high?`, metadata: { turnType: "instruction", expectsResponse: true, responseRelevance: 0.5, shouldEnd: false, domain: "motor", action: "raise_arms" } },
157146
];
158147

159-
const idx = Math.min(turnNumber, fallback.length - 1);
160-
return { ...fallback[idx], fallback: true };
148+
const farewell: Omit<ConversationResponse, "fallback"> = {
149+
text: `${prefix}You did such an amazing job, ${childName}! Thank you so much for talking with me today! You're wonderful!`,
150+
metadata: { turnType: "farewell", expectsResponse: false, responseRelevance: 0.5, shouldEnd: true, domain: "general" },
151+
};
152+
153+
if (turnNumber === 0) {
154+
const greeting = greetings[Math.floor(Math.random() * greetings.length)];
155+
return {
156+
text: greeting,
157+
metadata: { turnType: "greeting", expectsResponse: true, responseRelevance: 0.5, shouldEnd: false, domain: "social" },
158+
fallback: true,
159+
};
160+
}
161+
162+
if (turnNumber >= 6) {
163+
return { ...farewell, fallback: true };
164+
}
165+
166+
// Pick from mid-questions, cycling through with some randomness
167+
const poolIdx = (turnNumber - 1) % midQuestions.length;
168+
return { ...midQuestions[poolIdx], fallback: true };
161169
}
162170

163171
/* ------------------------------------------------------------------ */

app/api/nearby/route.ts

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
/**
2+
* POST /api/nearby
3+
*
4+
* Queries the Overpass API (OpenStreetMap) for nearby healthcare facilities.
5+
* Returns hospitals, clinics, doctors, and social facilities.
6+
*
7+
* Body: { lat: number, lng: number, radius?: number }
8+
* Response: { results: NearbyResult[], source: "overpass" }
9+
*/
10+
11+
import { NextRequest, NextResponse } from "next/server";
12+
13+
interface NearbyResult {
14+
id: number;
15+
name: string;
16+
lat: number;
17+
lng: number;
18+
type: string;
19+
phone?: string;
20+
website?: string;
21+
}
22+
23+
const OVERPASS_URL = "https://overpass-api.de/api/interpreter";
24+
25+
export async function POST(req: NextRequest) {
26+
let body: { lat: number; lng: number; radius?: number };
27+
28+
try {
29+
body = await req.json();
30+
} catch {
31+
return NextResponse.json({ error: "Invalid JSON body" }, { status: 400 });
32+
}
33+
34+
const { lat, lng, radius = 10000 } = body;
35+
36+
if (typeof lat !== "number" || typeof lng !== "number") {
37+
return NextResponse.json({ error: "Missing lat/lng" }, { status: 400 });
38+
}
39+
40+
// Overpass QL query: hospitals, clinics, doctors, healthcare, social facilities
41+
const query = `
42+
[out:json][timeout:15];
43+
(
44+
node["amenity"~"hospital|clinic|doctors"](around:${radius},${lat},${lng});
45+
node["healthcare"](around:${radius},${lat},${lng});
46+
node["amenity"="social_facility"](around:${radius},${lat},${lng});
47+
way["amenity"~"hospital|clinic|doctors"](around:${radius},${lat},${lng});
48+
way["healthcare"](around:${radius},${lat},${lng});
49+
);
50+
out center body;
51+
`;
52+
53+
try {
54+
const res = await fetch(OVERPASS_URL, {
55+
method: "POST",
56+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
57+
body: `data=${encodeURIComponent(query)}`,
58+
});
59+
60+
if (!res.ok) {
61+
return NextResponse.json(
62+
{ error: "Overpass API error", results: [], source: "overpass" },
63+
{ status: 502 },
64+
);
65+
}
66+
67+
const data = await res.json();
68+
const elements: Array<{
69+
id: number;
70+
type: string;
71+
lat?: number;
72+
lon?: number;
73+
center?: { lat: number; lon: number };
74+
tags?: Record<string, string>;
75+
}> = data.elements || [];
76+
77+
const results: NearbyResult[] = elements
78+
.filter((el) => {
79+
const name = el.tags?.name;
80+
return !!name;
81+
})
82+
.map((el) => {
83+
const elLat = el.lat ?? el.center?.lat ?? 0;
84+
const elLng = el.lon ?? el.center?.lon ?? 0;
85+
const tags = el.tags || {};
86+
const amenity = tags.amenity || tags.healthcare || "facility";
87+
88+
return {
89+
id: el.id,
90+
name: tags.name || "Unknown",
91+
lat: elLat,
92+
lng: elLng,
93+
type: amenity,
94+
...(tags.phone ? { phone: tags.phone } : {}),
95+
...(tags.website ? { website: tags.website } : {}),
96+
};
97+
});
98+
99+
return NextResponse.json(
100+
{ results, source: "overpass" },
101+
{
102+
headers: {
103+
"Cache-Control": "public, max-age=3600, s-maxage=3600",
104+
},
105+
},
106+
);
107+
} catch (err) {
108+
console.error("[Nearby] Overpass query failed:", err);
109+
return NextResponse.json(
110+
{ error: "Failed to query nearby facilities", results: [], source: "overpass" },
111+
{ status: 500 },
112+
);
113+
}
114+
}

app/components/BottomNav.tsx

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,14 @@
22

33
import Link from "next/link";
44
import { usePathname } from "next/navigation";
5+
import { Home, Gamepad2, MessageCircle, BarChart3, MapPin } from "lucide-react";
56

67
const tabs = [
7-
{ href: "/kid-dashboard", label: "Home", icon: "🏠" },
8-
{ href: "/kid-dashboard/games", label: "Games", icon: "🎮" },
9-
{ href: "/kid-dashboard/chat", label: "Chat", icon: "💬" },
10-
{ href: "/kid-dashboard/progress", label: "Progress", icon: "📊" },
11-
{ href: "/kid-dashboard/nearby-help", label: "Help", icon: "🏥" },
8+
{ href: "/kid-dashboard", label: "Home", Icon: Home },
9+
{ href: "/kid-dashboard/games", label: "Games", Icon: Gamepad2 },
10+
{ href: "/kid-dashboard/chat", label: "Chat", Icon: MessageCircle },
11+
{ href: "/kid-dashboard/progress", label: "Progress", Icon: BarChart3 },
12+
{ href: "/kid-dashboard/nearby-help", label: "Help", Icon: MapPin },
1213
];
1314

1415
export default function BottomNav() {
@@ -19,15 +20,18 @@ export default function BottomNav() {
1920
style={{
2021
position: "fixed",
2122
bottom: 0,
22-
left: 0,
23-
right: 0,
23+
left: "50%",
24+
transform: "translateX(-50%)",
2425
zIndex: 100,
2526
background: "var(--card)",
2627
borderTop: "1px solid var(--border)",
28+
borderRadius: "20px 20px 0 0",
2729
display: "flex",
2830
justifyContent: "space-around",
2931
alignItems: "center",
3032
height: 64,
33+
width: "100%",
34+
maxWidth: 600,
3135
paddingBottom: "env(safe-area-inset-bottom, 0px)",
3236
boxShadow: "0 -2px 12px rgba(0,0,0,0.06)",
3337
}}
@@ -49,15 +53,14 @@ export default function BottomNav() {
4953
gap: 2,
5054
textDecoration: "none",
5155
color: isActive ? "var(--sage-600)" : "var(--text-muted)",
52-
fontSize: "1.3rem",
5356
minWidth: 56,
5457
minHeight: 56,
5558
justifyContent: "center",
5659
borderRadius: "var(--r-md)",
5760
transition: "color 200ms var(--ease)",
5861
}}
5962
>
60-
<span>{tab.icon}</span>
63+
<tab.Icon size={22} strokeWidth={isActive ? 2.5 : 2} />
6164
<span
6265
style={{
6366
fontSize: "0.65rem",

app/components/DetectorResultsPanel.tsx

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,36 +12,44 @@ interface Props {
1212
backend: string;
1313
timeLeft: number;
1414
totalTime: number;
15+
mode?: "countdown" | "elapsed";
16+
elapsed?: number;
1517
}
1618

17-
export default function DetectorResultsPanel({ result, isModelLoaded, backend, timeLeft, totalTime }: Props) {
19+
export default function DetectorResultsPanel({ result, isModelLoaded, backend, timeLeft, totalTime, mode = "countdown", elapsed: elapsedSec }: Props) {
1820
const asdRisk = result?.multimodal?.asdRisk ?? 0;
1921
const bodyRisk = result?.multimodal?.bodyRisk ?? 0;
2022
const faceRisk = result?.multimodal?.faceRisk ?? 0;
2123
const confidence = result?.multimodal?.confidence ?? 0;
2224
const riskPct = Math.round(asdRisk * 100);
2325
const riskColor = riskPct >= 70 ? "var(--peach-300)" : riskPct >= 40 ? "#d4a843" : "var(--sage-500)";
24-
const progressPct = ((totalTime - timeLeft) / totalTime) * 100;
26+
const progressPct = mode === "elapsed" ? 0 : ((totalTime - timeLeft) / totalTime) * 100;
27+
const isElapsedMode = mode === "elapsed";
2528

2629
return (
2730
<div style={{ display: "flex", flexDirection: "column", gap: 14 }}>
28-
{/* Timer progress */}
31+
{/* Timer */}
2932
<div className="card" style={{ padding: "16px 20px" }}>
30-
<div style={{ display: "flex", justifyContent: "space-between", marginBottom: 8 }}>
33+
<div style={{ display: "flex", justifyContent: "space-between", marginBottom: isElapsedMode ? 0 : 8 }}>
3134
<span style={{ fontWeight: 700, fontSize: "0.9rem", fontFamily: "'Fredoka',sans-serif" }}>
32-
Screening Progress
35+
{isElapsedMode ? "Elapsed" : "Screening Progress"}
3336
</span>
34-
<span style={{ fontWeight: 700, fontSize: "0.9rem", color: timeLeft <= 30 ? "var(--peach-300)" : "var(--sage-500)" }}>
35-
{Math.floor(timeLeft / 60)}:{String(timeLeft % 60).padStart(2, "0")}
37+
<span style={{ fontWeight: 700, fontSize: "0.9rem", color: "var(--sage-500)" }}>
38+
{isElapsedMode
39+
? `${Math.floor((elapsedSec ?? 0) / 60)}:${String((elapsedSec ?? 0) % 60).padStart(2, "0")}`
40+
: `${Math.floor(timeLeft / 60)}:${String(timeLeft % 60).padStart(2, "0")}`
41+
}
3642
</span>
3743
</div>
38-
<div style={{ height: 8, background: "var(--sage-100)", borderRadius: 4, overflow: "hidden" }}>
39-
<div style={{
40-
height: "100%", width: `${progressPct}%`,
41-
background: "var(--sage-500)", borderRadius: 4,
42-
transition: "width 1s linear",
43-
}} />
44-
</div>
44+
{!isElapsedMode && (
45+
<div style={{ height: 8, background: "var(--sage-100)", borderRadius: 4, overflow: "hidden" }}>
46+
<div style={{
47+
height: "100%", width: `${progressPct}%`,
48+
background: "var(--sage-500)", borderRadius: 4,
49+
transition: "width 1s linear",
50+
}} />
51+
</div>
52+
)}
4553
</div>
4654

4755
{/* ASD Risk Gauge */}

0 commit comments

Comments
 (0)