Voice command improvements: - Auto-start listening when voice dialog opens (removes extra tap/click) - Added Activity option to unknown intent dialog - Users can now speak immediately after clicking the mic button Desktop navigation: - Added Home icon button in top bar/header for quick navigation to main page - Positioned between app title and user avatar 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
224 lines
6.6 KiB
JavaScript
Executable File
224 lines
6.6 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
|
|
|
/**
|
|
* End-to-End Voice Command Test
|
|
* Tests the full voice flow: classify + create activity in database
|
|
*/
|
|
|
|
const API_URL = process.env.API_URL || 'http://localhost:3020';
|
|
|
|
// ANSI color codes
|
|
const colors = {
|
|
reset: '\x1b[0m',
|
|
red: '\x1b[31m',
|
|
green: '\x1b[32m',
|
|
yellow: '\x1b[33m',
|
|
blue: '\x1b[34m',
|
|
cyan: '\x1b[36m',
|
|
};
|
|
|
|
// Test credentials
|
|
const TEST_USER = {
|
|
email: 'andrei@cloudz.ro',
|
|
password: 'Test1234!',
|
|
};
|
|
|
|
// Test commands
|
|
const commands = [
|
|
'Change wet diaper',
|
|
'Baby ate 150ml formula',
|
|
'Baby slept for 1 hour',
|
|
'Alice ate 3 pcs of broccoli at 11:00 AM',
|
|
];
|
|
|
|
let accessToken = null;
|
|
let childId = null;
|
|
|
|
async function login() {
|
|
console.log(`${colors.blue}Logging in...${colors.reset}`);
|
|
const response = await fetch(`${API_URL}/api/v1/auth/login`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(TEST_USER),
|
|
});
|
|
|
|
const data = await response.json();
|
|
if (!response.ok || !data.success) {
|
|
throw new Error(`Login failed: ${data.message || 'Unknown error'}`);
|
|
}
|
|
|
|
accessToken = data.data.accessToken;
|
|
console.log(`${colors.green}✓ Logged in successfully${colors.reset}\n`);
|
|
}
|
|
|
|
async function getChild() {
|
|
console.log(`${colors.blue}Fetching children...${colors.reset}`);
|
|
const response = await fetch(`${API_URL}/api/v1/children`, {
|
|
headers: { 'Authorization': `Bearer ${accessToken}` },
|
|
});
|
|
|
|
const data = await response.json();
|
|
if (!response.ok || !data.success || data.data.length === 0) {
|
|
throw new Error('No children found');
|
|
}
|
|
|
|
childId = data.data[0].id;
|
|
console.log(`${colors.green}✓ Found child: ${data.data[0].name} (${childId})${colors.reset}\n`);
|
|
}
|
|
|
|
async function getActivitiesCount(type = null) {
|
|
let url = `${API_URL}/api/v1/activities?childId=${childId}&limit=1000`;
|
|
if (type) url += `&type=${type}`;
|
|
|
|
const response = await fetch(url, {
|
|
headers: { 'Authorization': `Bearer ${accessToken}` },
|
|
});
|
|
|
|
const data = await response.json();
|
|
return data.success ? data.data.length : 0;
|
|
}
|
|
|
|
async function classifyAndCreateActivity(text) {
|
|
console.log(`${colors.yellow}Processing: "${text}"${colors.reset}`);
|
|
|
|
// Step 1: Classify the command
|
|
const classifyResponse = await fetch(`${API_URL}/api/v1/voice/transcribe`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': `Bearer ${accessToken}`,
|
|
},
|
|
body: JSON.stringify({
|
|
text,
|
|
language: 'en',
|
|
childName: 'Alice',
|
|
}),
|
|
});
|
|
|
|
const classifyData = await classifyResponse.json();
|
|
|
|
if (!classifyResponse.ok || !classifyData.success) {
|
|
console.log(`${colors.red}✗ Classification failed${colors.reset}`);
|
|
console.log(JSON.stringify(classifyData, null, 2));
|
|
return false;
|
|
}
|
|
|
|
const { type, details, timestamp, confidence } = classifyData.classification;
|
|
|
|
console.log(` Type: ${type} (confidence: ${confidence})`);
|
|
console.log(` Details: ${JSON.stringify(details)}`);
|
|
|
|
if (type === 'unknown' || confidence < 0.3) {
|
|
console.log(`${colors.red}✗ Low confidence or unknown type${colors.reset}\n`);
|
|
return false;
|
|
}
|
|
|
|
// Step 2: Create the activity
|
|
const activityData = {
|
|
type,
|
|
timestamp: timestamp || new Date().toISOString(),
|
|
data: details || {},
|
|
notes: details?.notes || undefined,
|
|
};
|
|
|
|
const createResponse = await fetch(`${API_URL}/api/v1/activities`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': `Bearer ${accessToken}`,
|
|
},
|
|
body: JSON.stringify({
|
|
childId,
|
|
...activityData,
|
|
}),
|
|
});
|
|
|
|
const createData = await createResponse.json();
|
|
|
|
if (!createResponse.ok || !createData.success) {
|
|
console.log(`${colors.red}✗ Failed to create activity${colors.reset}`);
|
|
console.log(JSON.stringify(createData, null, 2));
|
|
return false;
|
|
}
|
|
|
|
console.log(`${colors.green}✓ Activity created: ${createData.data.id}${colors.reset}\n`);
|
|
return true;
|
|
}
|
|
|
|
async function runTests() {
|
|
console.log(`${colors.blue}========================================${colors.reset}`);
|
|
console.log(`${colors.blue}Voice E2E Test Suite${colors.reset}`);
|
|
console.log(`${colors.blue}========================================${colors.reset}\n`);
|
|
|
|
try {
|
|
// Login and get child
|
|
await login();
|
|
await getChild();
|
|
|
|
// Get initial counts
|
|
const initialCounts = {
|
|
total: await getActivitiesCount(),
|
|
diaper: await getActivitiesCount('diaper'),
|
|
feeding: await getActivitiesCount('feeding'),
|
|
sleep: await getActivitiesCount('sleep'),
|
|
};
|
|
|
|
console.log(`${colors.cyan}Initial activity counts:${colors.reset}`);
|
|
console.log(` Total: ${initialCounts.total}`);
|
|
console.log(` Diapers: ${initialCounts.diaper}`);
|
|
console.log(` Feedings: ${initialCounts.feeding}`);
|
|
console.log(` Sleep: ${initialCounts.sleep}\n`);
|
|
|
|
// Run tests
|
|
let passed = 0;
|
|
let failed = 0;
|
|
|
|
for (const command of commands) {
|
|
const result = await classifyAndCreateActivity(command);
|
|
if (result) {
|
|
passed++;
|
|
} else {
|
|
failed++;
|
|
}
|
|
}
|
|
|
|
// Get final counts
|
|
const finalCounts = {
|
|
total: await getActivitiesCount(),
|
|
diaper: await getActivitiesCount('diaper'),
|
|
feeding: await getActivitiesCount('feeding'),
|
|
sleep: await getActivitiesCount('sleep'),
|
|
};
|
|
|
|
console.log(`${colors.cyan}Final activity counts:${colors.reset}`);
|
|
console.log(` Total: ${finalCounts.total} (+${finalCounts.total - initialCounts.total})`);
|
|
console.log(` Diapers: ${finalCounts.diaper} (+${finalCounts.diaper - initialCounts.diaper})`);
|
|
console.log(` Feedings: ${finalCounts.feeding} (+${finalCounts.feeding - initialCounts.feeding})`);
|
|
console.log(` Sleep: ${finalCounts.sleep} (+${finalCounts.sleep - initialCounts.sleep})\n`);
|
|
|
|
// Summary
|
|
console.log(`${colors.blue}========================================${colors.reset}`);
|
|
console.log(`${colors.blue}Test Summary${colors.reset}`);
|
|
console.log(`${colors.blue}========================================${colors.reset}`);
|
|
console.log(`Total: ${commands.length}`);
|
|
console.log(`${colors.green}Passed: ${passed}${colors.reset}`);
|
|
console.log(`${colors.red}Failed: ${failed}${colors.reset}`);
|
|
console.log('');
|
|
|
|
if (failed === 0) {
|
|
console.log(`${colors.green}All tests passed! Activities saved to database. 🎉${colors.reset}`);
|
|
process.exit(0);
|
|
} else {
|
|
console.log(`${colors.red}Some tests failed. Check the output above.${colors.reset}`);
|
|
process.exit(1);
|
|
}
|
|
} catch (error) {
|
|
console.error(`${colors.red}Fatal error: ${error.message}${colors.reset}`);
|
|
console.error(error.stack);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
// Run tests
|
|
runTests();
|