Files
arrweek/skill/lambda/index.js

1198 lines
43 KiB
JavaScript

import Alexa from 'ask-sdk-core';
const BACKEND_API_URL = process.env.BACKEND_API_URL || 'http://localhost:4050';
const FALLBACK_HTTPS_IMAGE =
'https://images.unsplash.com/photo-1507525428034-b723cf961d3e?auto=format&fit=crop&w=1000&q=80';
/**
* Récupère les informations du profil vocal reconnu par Alexa (Skill Personalization / Voice ID)
*/
async function getPersonInfo(handlerInput) {
const system = handlerInput.requestEnvelope?.context?.System;
const person = system?.person;
const personId = person?.personId || null;
let personGivenName = null;
if (personId && system?.apiEndpoint && system?.apiAccessToken) {
try {
const res = await fetch(`${system.apiEndpoint}/v2/persons/~current/profile/givenName`, {
headers: {
Authorization: `Bearer ${system.apiAccessToken}`,
Accept: 'application/json'
}
});
if (res.ok) {
const nameData = await res.json();
if (typeof nameData === 'string') {
personGivenName = nameData;
}
}
} catch {
// Si la permission Given Name n'est pas cochée, on utilise personId mémorisé dans SQLite
}
}
if (personId) {
console.log(`🗣️ [ALEXA VOICE ID] Locuteur détecté: personId=${personId}, givenName=${personGivenName || 'non fourni'}`);
}
return { personId, personGivenName };
}
/**
* Appelle le moteur de la Banque des Coquillages (dossier back/)
*/
async function callBackendVoiceEngine(intent, slots = {}, handlerInput = null) {
const publicBaseUrl = handlerInput?.requestEnvelope?._publicBaseUrl || '';
const personInfo = handlerInput ? await getPersonInfo(handlerInput) : { personId: null, personGivenName: null };
const response = await fetch(`${BACKEND_API_URL}/api/alexa/voice-intent`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
intent,
slots,
publicBaseUrl,
personId: personInfo.personId,
personGivenName: personInfo.personGivenName
})
});
if (!response.ok) {
throw new Error(`Erreur API Backend: ${response.status}`);
}
return response.json();
}
function getSlotValue(handlerInput, slotName) {
const slot = Alexa.getSlot(handlerInput.requestEnvelope, slotName);
if (!slot) return null;
const resolution = slot.resolutions?.resolutionsPerAuthority?.[0];
if (resolution?.status?.code === 'ER_SUCCESS_MATCH') {
return resolution.values[0].value.name;
}
return slot.value || null;
}
/**
* Nettoie le texte vocal pour éviter que les caractères spéciaux XML (&, <, >)
* ne fassent planter le lecteur SSML (<speak>...</speak>) d'Amazon Alexa
*/
function sanitizeSpeech(text = '') {
return String(text)
.replace(/&/g, ' et ')
.replace(/</g, ' ')
.replace(/>/g, ' ')
.replace(/\s+/g, ' ')
.trim();
}
function supportsAPL(handlerInput) {
try {
const supportedInterfaces = Alexa.getSupportedInterfaces(handlerInput.requestEnvelope);
return Boolean(supportedInterfaces && supportedInterfaces['Alexa.Presentation.APL']);
} catch {
return false;
}
}
function ensureHttpsImage(url, publicBaseUrl = '') {
if (typeof url !== 'string' || !url) return FALLBACK_HTTPS_IMAGE;
if (url.startsWith('https://')) return url;
if (publicBaseUrl && url.startsWith('http://localhost:4050')) {
return url.replace('http://localhost:4050', publicBaseUrl);
}
return FALLBACK_HTTPS_IMAGE;
}
/**
* Construit l'écran APL interactif (100% tactile avec TouchWrapper + SendEvent) :
* - Mode DASHBOARD : Enfants (Hailey, Violette, Andreas) + Banque + Carrousel d'Activités cliquables
* - Mode ACTIVITY_DETAIL : Détails complets d'une activité + qui a cotisé + boutons tactiles [+2 🐚]
* - Mode CHILD_DETAIL : Détails du compte d'un enfant + bouton tactile [Bloquer 5 🐚]
*/
function buildInteractiveAPLDocument(data, publicBaseUrl) {
const children = Array.isArray(data.children) ? data.children : [];
const activities = Array.isArray(data.activities) ? data.activities : [];
const rate = data.settings?.interest_rate_percent ?? 20;
const viewMode = data.viewMode || 'DASHBOARD';
// ==================== VUE 1 : DÉTAIL D'UNE ACTIVITÉ (CLIC OU VOIX) ====================
if (viewMode === 'ACTIVITY_DETAIL' && data.selectedActivity) {
const act = data.selectedActivity;
const actImage = ensureHttpsImage(act.image_url, publicBaseUrl);
const contribs = Array.isArray(act.contributions) ? act.contributions : [];
const contribSummary =
contribs.length > 0
? contribs.map((c) => `${c.avatar} ${c.child_name}: ${c.total_shells} 🐚`).join(' • ')
: 'Aucun coquillage déposé pour le moment';
return {
type: 'APL',
version: '1.8',
mainTemplate: {
parameters: ['payload'],
items: [
{
type: 'Frame',
width: '100vw',
height: '100vh',
backgroundColor: '#0B1120',
item: {
type: 'Container',
width: '100vw',
height: '100vh',
paddingLeft: '28dp',
paddingRight: '28dp',
paddingTop: '20dp',
paddingBottom: '20dp',
items: [
// Barre du haut avec bouton Retour cliquable
{
type: 'Container',
direction: 'row',
justifyContent: 'spaceBetween',
alignItems: 'center',
width: '100%',
paddingBottom: '14dp',
items: [
{
type: 'TouchWrapper',
onPress: [{ type: 'SendEvent', arguments: ['VIEW_DASHBOARD'] }],
item: {
type: 'Frame',
backgroundColor: '#1E293B',
borderRadius: '12dp',
borderWidth: '1dp',
borderColor: '#38BDF8',
paddingLeft: '16dp',
paddingRight: '16dp',
paddingTop: '8dp',
paddingBottom: '8dp',
item: {
type: 'Text',
text: '⬅ Retour à la Banque',
color: '#38BDF8',
fontSize: '18dp',
fontWeight: '700'
}
}
},
{
type: 'Text',
text: `🐚 DÉTAIL DE L'ACTIVITÉ • OBJECTIF ${act.cost_shells} COQUILLAGES`,
color: '#FBBF24',
fontSize: '18dp',
fontWeight: '700'
}
]
},
// Zone centrale : Photo à gauche + Infos & Progression à droite
{
type: 'Container',
direction: 'row',
width: '100%',
height: '62%',
items: [
{
type: 'Image',
source: actImage,
scale: 'best-fill',
width: '44%',
height: '100%',
borderRadius: '18dp'
},
{
type: 'Container',
width: '56%',
height: '100%',
paddingLeft: '24dp',
justifyContent: 'center',
items: [
{
type: 'Text',
text: `${act.emoji} ${act.title}`,
color: '#FFFFFF',
fontSize: '32dp',
fontWeight: '800',
maxLines: 2
},
{
type: 'Text',
text: act.description || 'Activité commune de la famille',
color: '#CBD5E1',
fontSize: '18dp',
paddingTop: '6dp',
maxLines: 2
},
{
type: 'Frame',
backgroundColor: '#1E293B',
borderRadius: '14dp',
paddingLeft: '16dp',
paddingRight: '16dp',
paddingTop: '12dp',
paddingBottom: '12dp',
item: {
type: 'Container',
items: [
{
type: 'Text',
text: `Pot commun : ${act.current_shells} / ${act.cost_shells} 🐚 (${act.progress_percent}%)`,
color: '#FBBF24',
fontSize: '22dp',
fontWeight: '700'
},
{
type: 'Text',
text:
act.remaining_shells === 0
? '🎉 Activité débloquée !'
: `Il manque encore ${act.remaining_shells} coquillages pour la débloquer`,
color: '#38BDF8',
fontSize: '17dp',
paddingTop: '4dp'
},
{
type: 'Text',
text: contribSummary,
color: '#E2E8F0',
fontSize: '16dp',
paddingTop: '8dp',
maxLines: 2
}
]
}
}
]
}
]
},
// Zone du bas : Boutons tactiles pour déclarer l'ajout de coquillages (verrouillés après validation !)
{
type: 'Container',
direction: 'row',
justifyContent: 'spaceBetween',
alignItems: 'center',
width: '100%',
paddingTop: '14dp',
items: children.map((child) => {
const isLocked = Array.isArray(act.locked_child_ids) && act.locked_child_ids.includes(child.id);
if (isLocked) {
return {
type: 'Frame',
width: '31%',
backgroundColor: '#1E293B',
borderRadius: '14dp',
borderWidth: '1dp',
borderColor: '#475569',
paddingTop: '12dp',
paddingBottom: '12dp',
paddingLeft: '10dp',
paddingRight: '10dp',
item: {
type: 'Text',
text: `🔒 ${child.name} : En attente`,
color: '#94A3B8',
fontSize: '18dp',
fontWeight: '700',
textAlign: 'center'
}
};
}
return {
type: 'TouchWrapper',
width: '31%',
onPress: [
{
type: 'SendEvent',
arguments: ['TOUCH_CONTRIBUTE', String(act.id), String(child.id)]
}
],
item: {
type: 'Frame',
backgroundColor: '#0284C7',
borderRadius: '14dp',
paddingTop: '12dp',
paddingBottom: '12dp',
paddingLeft: '10dp',
paddingRight: '10dp',
item: {
type: 'Text',
text: `${child.avatar} ${child.name} : Ajouter 🐚`,
color: '#FFFFFF',
fontSize: '18dp',
fontWeight: '700',
textAlign: 'center'
}
}
};
})
}
]
}
}
]
}
};
}
// ==================== VUE 2 : DÉTAIL DU COMPTE D'UN ENFANT ====================
if (viewMode === 'CHILD_DETAIL' && data.selectedChild) {
const c = data.selectedChild;
return {
type: 'APL',
version: '1.8',
mainTemplate: {
parameters: ['payload'],
items: [
{
type: 'Frame',
width: '100vw',
height: '100vh',
backgroundColor: '#0B1120',
item: {
type: 'Container',
width: '100vw',
height: '100vh',
paddingLeft: '32dp',
paddingRight: '32dp',
paddingTop: '24dp',
paddingBottom: '24dp',
items: [
{
type: 'Container',
direction: 'row',
justifyContent: 'spaceBetween',
alignItems: 'center',
width: '100%',
paddingBottom: '20dp',
items: [
{
type: 'TouchWrapper',
onPress: [{ type: 'SendEvent', arguments: ['VIEW_DASHBOARD'] }],
item: {
type: 'Frame',
backgroundColor: '#1E293B',
borderRadius: '12dp',
borderWidth: '1dp',
borderColor: '#38BDF8',
paddingLeft: '16dp',
paddingRight: '16dp',
paddingTop: '8dp',
paddingBottom: '8dp',
item: {
type: 'Text',
text: '⬅ Retour à la Banque',
color: '#38BDF8',
fontSize: '18dp',
fontWeight: '700'
}
}
},
{
type: 'Text',
text: `${c.avatar} COMPTE BANCAIRE DE ${c.name.toUpperCase()}`,
color: '#FBBF24',
fontSize: '24dp',
fontWeight: '800'
}
]
},
// 3 grandes cartes pour l'enfant : Disponible, Livret Bloqué (+Intérêts), Tirelire Euros
{
type: 'Container',
direction: 'row',
justifyContent: 'spaceBetween',
width: '100%',
height: '55%',
items: [
{
type: 'Frame',
width: '31%',
height: '100%',
backgroundColor: '#1E293B',
borderRadius: '18dp',
borderWidth: '2dp',
borderColor: '#FBBF24',
paddingLeft: '18dp',
paddingRight: '18dp',
paddingTop: '18dp',
item: {
type: 'Container',
items: [
{
type: 'Text',
text: '🐚 DISPONIBLES',
color: '#FBBF24',
fontSize: '18dp',
fontWeight: '700'
},
{
type: 'Text',
text: `${c.available_shells} 🐚`,
color: '#FFFFFF',
fontSize: '44dp',
fontWeight: '800',
paddingTop: '12dp'
},
{
type: 'Text',
text: 'Prêts pour les activités ou euros',
color: '#94A3B8',
fontSize: '15dp',
paddingTop: '8dp'
}
]
}
},
{
type: 'Frame',
width: '31%',
height: '100%',
backgroundColor: '#1E293B',
borderRadius: '18dp',
borderWidth: '2dp',
borderColor: '#38BDF8',
paddingLeft: '18dp',
paddingRight: '18dp',
paddingTop: '18dp',
item: {
type: 'Container',
items: [
{
type: 'Text',
text: `🔒 LIVRET BLOQUÉ (+${rate}%)`,
color: '#38BDF8',
fontSize: '18dp',
fontWeight: '700'
},
{
type: 'Text',
text: `${c.locked_shells} 🔒`,
color: '#FFFFFF',
fontSize: '44dp',
fontWeight: '800',
paddingTop: '12dp'
},
{
type: 'Text',
text: `✨ +${c.projected_interest_shells} 🐚 gratuits fin de mois !`,
color: '#34D399',
fontSize: '16dp',
fontWeight: '700',
paddingTop: '8dp'
}
]
}
},
{
type: 'Frame',
width: '31%',
height: '100%',
backgroundColor: '#1E293B',
borderRadius: '18dp',
borderWidth: '2dp',
borderColor: '#34D399',
paddingLeft: '18dp',
paddingRight: '18dp',
paddingTop: '18dp',
item: {
type: 'Container',
items: [
{
type: 'Text',
text: '💶 TIRELIRE EUROS',
color: '#34D399',
fontSize: '18dp',
fontWeight: '700'
},
{
type: 'Text',
text: `${Number(c.euros_balance).toFixed(2)} €`,
color: '#FFFFFF',
fontSize: '44dp',
fontWeight: '800',
paddingTop: '12dp'
},
{
type: 'Text',
text: `Reste ${c.remaining_shells_to_convert} 🐚 convertibles ce mois`,
color: '#94A3B8',
fontSize: '15dp',
paddingTop: '8dp'
}
]
}
}
]
},
// Boutons d'action tactiles pour cet enfant
{
type: 'Container',
direction: 'row',
justifyContent: 'spaceBetween',
width: '100%',
paddingTop: '18dp',
items: [
{
type: 'TouchWrapper',
width: '48%',
onPress: [{ type: 'SendEvent', arguments: ['TOUCH_LOCK', String(c.id), '4'] }],
item: {
type: 'Frame',
backgroundColor: '#0284C7',
borderRadius: '14dp',
paddingTop: '14dp',
paddingBottom: '14dp',
item: {
type: 'Text',
text: `🔒 Demander à bloquer 4 coquillages (+${rate}%/mois)`,
color: '#FFFFFF',
fontSize: '19dp',
fontWeight: '700',
textAlign: 'center'
}
}
},
{
type: 'TouchWrapper',
width: '48%',
onPress: [{ type: 'SendEvent', arguments: ['TOUCH_LOCK', String(c.id), '8'] }],
item: {
type: 'Frame',
backgroundColor: '#059669',
borderRadius: '14dp',
paddingTop: '14dp',
paddingBottom: '14dp',
item: {
type: 'Text',
text: `🔒 Demander à bloquer 8 coquillages (+${rate}%/mois)`,
color: '#FFFFFF',
fontSize: '19dp',
fontWeight: '700',
textAlign: 'center'
}
}
}
]
}
]
}
}
]
}
};
}
// ==================== VUE 3 : DASHBOARD COMPLET (BANQUE + 3 ENFANTS + ACTIVITÉS CLIQUABLES) ====================
return {
type: 'APL',
version: '1.8',
mainTemplate: {
parameters: ['payload'],
items: [
{
type: 'Frame',
width: '100vw',
height: '100vh',
backgroundColor: '#0B1120',
item: {
type: 'Container',
width: '100vw',
height: '100vh',
paddingLeft: '24dp',
paddingRight: '24dp',
paddingTop: '16dp',
paddingBottom: '16dp',
items: [
// 1. En-tête de la Banque
{
type: 'Container',
direction: 'row',
justifyContent: 'spaceBetween',
alignItems: 'center',
width: '100%',
paddingBottom: '10dp',
items: [
{
type: 'Text',
text: '🐚 LA BANQUE DES COQUILLAGES',
color: '#FBBF24',
fontSize: '22dp',
fontWeight: '800'
},
{
type: 'Text',
text: `📈 Livret Épargne : +${rate}% / mois • Touchez un enfant ou une activité`,
color: '#38BDF8',
fontSize: '16dp',
fontWeight: '700'
}
]
},
// 2. Les 3 Cartes Enfants Cliquables (Hailey, Violette, Andreas)
{
type: 'Container',
direction: 'row',
justifyContent: 'spaceBetween',
width: '100%',
height: '28%',
items: children.map((c) => ({
type: 'TouchWrapper',
width: '32%',
height: '100%',
onPress: [{ type: 'SendEvent', arguments: ['VIEW_CHILD', String(c.id)] }],
item: {
type: 'Frame',
width: '100%',
height: '100%',
backgroundColor: '#1E293B',
borderRadius: '14dp',
borderWidth: '1dp',
borderColor: '#334155',
paddingLeft: '14dp',
paddingRight: '14dp',
paddingTop: '10dp',
paddingBottom: '10dp',
item: {
type: 'Container',
justifyContent: 'spaceBetween',
items: [
{
type: 'Container',
direction: 'row',
justifyContent: 'spaceBetween',
alignItems: 'center',
items: [
{
type: 'Text',
text: `${c.avatar} ${c.name}`,
color: '#FFFFFF',
fontSize: '21dp',
fontWeight: '800'
},
{
type: 'Text',
text: `${Number(c.euros_balance).toFixed(2)} €`,
color: '#34D399',
fontSize: '17dp',
fontWeight: '700'
}
]
},
{
type: 'Text',
text: `${c.available_shells} 🐚 dispo • ${c.locked_shells} 🔒 bloqués`,
color: '#FBBF24',
fontSize: '16dp',
fontWeight: '700'
},
{
type: 'Text',
text: `Intérêts fin de mois : +${c.projected_interest_shells} 🐚 gratuits`,
color: '#38BDF8',
fontSize: '13dp'
}
]
}
}
}))
},
// 3. Titre section Activités
{
type: 'Text',
text: '🎡 ACTIVITÉS COMMUNES DU MOIS (Touchez une photo pour voir les détails et cotiser)',
color: '#E2E8F0',
fontSize: '16dp',
fontWeight: '700',
paddingTop: '10dp',
paddingBottom: '8dp'
},
// 4. Carrousel Horizontal des Activités Illustrées Cliquables + Carte Proposer une activité
{
type: 'Sequence',
scrollDirection: 'horizontal',
width: '100%',
height: '52%',
items: [
...activities.map((act) => ({
type: 'TouchWrapper',
width: '300dp',
height: '100%',
paddingRight: '16dp',
onPress: [{ type: 'SendEvent', arguments: ['VIEW_ACTIVITY', String(act.id)] }],
item: {
type: 'Frame',
width: '284dp',
height: '100%',
backgroundColor: '#1E293B',
borderRadius: '16dp',
borderWidth: '1dp',
borderColor: '#475569',
item: {
type: 'Container',
width: '100%',
height: '100%',
items: [
{
type: 'Image',
source: ensureHttpsImage(act.image_url, publicBaseUrl),
scale: 'best-fill',
width: '100%',
height: '58%',
borderRadius: '16dp'
},
{
type: 'Container',
paddingLeft: '12dp',
paddingRight: '12dp',
paddingTop: '8dp',
items: [
{
type: 'Text',
text: `${act.emoji} ${act.title}`,
color: '#FFFFFF',
fontSize: '19dp',
fontWeight: '800',
maxLines: 1
},
{
type: 'Text',
text: `Pot : ${act.current_shells} / ${act.cost_shells} 🐚 (${act.progress_percent}%)`,
color: '#FBBF24',
fontSize: '16dp',
fontWeight: '700',
paddingTop: '2dp',
maxLines: 1
},
{
type: 'Text',
text:
act.remaining_shells === 0
? '🎉 Débloquée ! Toucher pour ouvrir'
: `Manque ${act.remaining_shells} 🐚 • Toucher pour détails`,
color: '#38BDF8',
fontSize: '13dp',
paddingTop: '2dp',
maxLines: 1
}
]
}
]
}
}
})),
{
type: 'TouchWrapper',
width: '260dp',
height: '100%',
paddingRight: '16dp',
onPress: [{ type: 'SendEvent', arguments: ['TOUCH_PROPOSE_ACTIVITY'] }],
item: {
type: 'Frame',
width: '244dp',
height: '100%',
backgroundColor: '#1E293B',
borderRadius: '16dp',
borderWidth: '2dp',
borderColor: '#F59E0B',
item: {
type: 'Container',
width: '100%',
height: '100%',
justifyContent: 'center',
alignItems: 'center',
paddingLeft: '16dp',
paddingRight: '16dp',
items: [
{
type: 'Text',
text: '💡',
fontSize: '44dp'
},
{
type: 'Text',
text: 'Proposer une activité',
color: '#FBBF24',
fontSize: '18dp',
fontWeight: '800',
paddingTop: '8dp',
textAlign: 'center'
},
{
type: 'Text',
text: 'Toucher ou dire :\n« Je propose l\'activité... »',
color: '#38BDF8',
fontSize: '13dp',
paddingTop: '6dp',
textAlign: 'center'
}
]
}
}
}
]
}
]
}
}
]
}
};
}
/**
* Construit la réponse vocale + StandardCard + Écran interactif APL
*/
function buildVisualResponse(handlerInput, data, repromptText) {
const publicBaseUrl = handlerInput.requestEnvelope?._publicBaseUrl || '';
const cleanSpeech = sanitizeSpeech(data.speech);
const builder = handlerInput.responseBuilder.speak(cleanSpeech);
if (repromptText) {
builder.reprompt(sanitizeSpeech(repromptText));
}
const aplSupported = supportsAPL(handlerInput);
if (data.card) {
const safeImageUrl = ensureHttpsImage(data.card.imageUrl, publicBaseUrl);
const cardTitle = String(data.card.title || 'La Banque des Coquillages');
const cardSubtitle = String(data.card.subtitle || '');
const cardText = String(data.card.text || '');
const cardBody = `${cardSubtitle ? cardSubtitle + '\n' : ''}${cardText}`.trim();
builder.withStandardCard(cardTitle, cardBody, safeImageUrl, safeImageUrl);
}
if (aplSupported) {
const aplDocument = buildInteractiveAPLDocument(data, publicBaseUrl);
builder.addDirective({
type: 'Alexa.Presentation.APL.RenderDocument',
token: 'banqueCoquillagesVisualToken',
document: aplDocument,
datasources: {
payload: {}
}
});
}
return builder.getResponse();
}
const LaunchRequestHandler = {
canHandle(handlerInput) {
const reqType = Alexa.getRequestType(handlerInput.requestEnvelope);
if (reqType === 'LaunchRequest') return true;
if (reqType === 'IntentRequest') {
const intentName = Alexa.getIntentName(handlerInput.requestEnvelope);
return intentName === 'OpenBankIntent' || intentName === 'AMAZON.NavigateHomeIntent';
}
return false;
},
async handle(handlerInput) {
const data = await callBackendVoiceEngine('LaunchRequest', {}, handlerInput);
return buildVisualResponse(
handlerInput,
data,
'Tu peux toucher un prénom ou une activité sur l\'écran, ou me demander combien tu as de coquillages.'
);
}
};
const IdentifySpeakerIntentHandler = {
canHandle(handlerInput) {
return (
Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest' &&
Alexa.getIntentName(handlerInput.requestEnvelope) === 'IdentifySpeakerIntent'
);
},
async handle(handlerInput) {
const ChildName = getSlotValue(handlerInput, 'ChildName');
const data = await callBackendVoiceEngine('IdentifySpeakerIntent', { ChildName }, handlerInput);
return buildVisualResponse(
handlerInput,
data,
'Que veux-tu faire maintenant avec tes coquillages ?'
);
}
};
const GetActivityDetailIntentHandler = {
canHandle(handlerInput) {
return (
Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest' &&
Alexa.getIntentName(handlerInput.requestEnvelope) === 'GetActivityDetailIntent'
);
},
async handle(handlerInput) {
const ActivityName = getSlotValue(handlerInput, 'ActivityName');
const data = await callBackendVoiceEngine('GetActivityDetailIntent', { ActivityName }, handlerInput);
return buildVisualResponse(
handlerInput,
data,
'Veux-tu mettre des coquillages sur cette activité ou revenir à la banque ?'
);
}
};
/**
* Gère tous les clics / touches sur l'écran Alexa Echo Show (TouchWrapper -> SendEvent)
*/
const APLUserEventHandler = {
canHandle(handlerInput) {
return Alexa.getRequestType(handlerInput.requestEnvelope) === 'Alexa.Presentation.APL.UserEvent';
},
async handle(handlerInput) {
const args = handlerInput.requestEnvelope.request.arguments || [];
console.log('👆 [ALEXA TOUCH EVENT] Clic sur l\'écran:', args);
const data = await callBackendVoiceEngine('APLTouchEvent', { arguments: args }, handlerInput);
return buildVisualResponse(
handlerInput,
data,
'Que souhaites-tu faire maintenant ?'
);
}
};
const GetBalanceIntentHandler = {
canHandle(handlerInput) {
return (
Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest' &&
Alexa.getIntentName(handlerInput.requestEnvelope) === 'GetBalanceIntent'
);
},
async handle(handlerInput) {
const ChildName = getSlotValue(handlerInput, 'ChildName');
const data = await callBackendVoiceEngine('GetBalanceIntent', { ChildName }, handlerInput);
return buildVisualResponse(
handlerInput,
data,
'Veux-tu consulter les activités ou placer des coquillages ?'
);
}
};
const ListActivitiesIntentHandler = {
canHandle(handlerInput) {
return (
Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest' &&
Alexa.getIntentName(handlerInput.requestEnvelope) === 'ListActivitiesIntent'
);
},
async handle(handlerInput) {
const ShellCount = getSlotValue(handlerInput, 'ShellCount');
const data = await callBackendVoiceEngine('ListActivitiesIntent', { ShellCount }, handlerInput);
return buildVisualResponse(
handlerInput,
data,
'Touche une activité sur l\'écran ou dis-moi son nom pour voir ses détails !'
);
}
};
const DepositShellsIntentHandler = {
canHandle(handlerInput) {
return (
Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest' &&
Alexa.getIntentName(handlerInput.requestEnvelope) === 'DepositShellsIntent'
);
},
async handle(handlerInput) {
const ChildName = getSlotValue(handlerInput, 'ChildName');
const ShellCount = getSlotValue(handlerInput, 'ShellCount');
const AccountType = getSlotValue(handlerInput, 'AccountType') || 'bloqué';
const data = await callBackendVoiceEngine(
'DepositShellsIntent',
{
ChildName,
ShellCount,
AccountType
},
handlerInput
);
return buildVisualResponse(handlerInput, data, 'Autre chose ?');
}
};
const ContributeActivityIntentHandler = {
canHandle(handlerInput) {
return (
Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest' &&
Alexa.getIntentName(handlerInput.requestEnvelope) === 'ContributeActivityIntent'
);
},
async handle(handlerInput) {
const ChildName = getSlotValue(handlerInput, 'ChildName');
const ShellCount = getSlotValue(handlerInput, 'ShellCount');
const ActivityName = getSlotValue(handlerInput, 'ActivityName');
const data = await callBackendVoiceEngine(
'ContributeActivityIntent',
{
ChildName,
ShellCount,
ActivityName
},
handlerInput
);
return buildVisualResponse(handlerInput, data, 'Une autre opération ?');
}
};
const ConvertEurosIntentHandler = {
canHandle(handlerInput) {
return (
Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest' &&
Alexa.getIntentName(handlerInput.requestEnvelope) === 'ConvertEurosIntent'
);
},
async handle(handlerInput) {
const ChildName = getSlotValue(handlerInput, 'ChildName');
const ShellCount = getSlotValue(handlerInput, 'ShellCount');
const data = await callBackendVoiceEngine(
'ConvertEurosIntent',
{
ChildName,
ShellCount
},
handlerInput
);
return buildVisualResponse(handlerInput, data, 'Autre chose ?');
}
};
const SimulateInterestIntentHandler = {
canHandle(handlerInput) {
return (
Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest' &&
Alexa.getIntentName(handlerInput.requestEnvelope) === 'SimulateInterestIntent'
);
},
async handle(handlerInput) {
const ShellCount = getSlotValue(handlerInput, 'ShellCount') || '10';
const data = await callBackendVoiceEngine('SimulateInterestIntent', { ShellCount }, handlerInput);
return buildVisualResponse(
handlerInput,
data,
'Veux-tu bloquer tes coquillages maintenant ?'
);
}
};
const ProposeActivityIntentHandler = {
canHandle(handlerInput) {
return (
Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest' &&
Alexa.getIntentName(handlerInput.requestEnvelope) === 'ProposeActivityIntent'
);
},
async handle(handlerInput) {
const ChildName = getSlotValue(handlerInput, 'ChildName');
const ActivityTitle = getSlotValue(handlerInput, 'ActivityTitle');
const data = await callBackendVoiceEngine(
'ProposeActivityIntent',
{
ChildName,
ActivityTitle
},
handlerInput
);
return buildVisualResponse(
handlerInput,
data,
'Veux-tu voir les activités en cours ou poser une autre question ?'
);
}
};
const HelpIntentHandler = {
canHandle(handlerInput) {
return (
Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest' &&
Alexa.getIntentName(handlerInput.requestEnvelope) === 'AMAZON.HelpIntent'
);
},
handle(handlerInput) {
const speechText =
'Dans la banque des coquillages, Hailey, Violette et Andreas peuvent toucher l\'écran ou utiliser la voix pour voir leur solde, afficher les détails d\'une activité, cotiser pour une activité commune, convertir des coquillages en euros, ou les bloquer pour gagner des intérêts !';
return handlerInput.responseBuilder.speak(speechText).reprompt(speechText).getResponse();
}
};
const CancelAndStopIntentHandler = {
canHandle(handlerInput) {
return (
Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest' &&
(Alexa.getIntentName(handlerInput.requestEnvelope) === 'AMAZON.CancelIntent' ||
Alexa.getIntentName(handlerInput.requestEnvelope) === 'AMAZON.StopIntent')
);
},
handle(handlerInput) {
return handlerInput.responseBuilder
.speak('À bientôt dans la banque des coquillages ! Continuez à bien récolter vos coquillages Hailey, Violette et Andreas !')
.getResponse();
}
};
const SessionEndedRequestHandler = {
canHandle(handlerInput) {
return Alexa.getRequestType(handlerInput.requestEnvelope) === 'SessionEndedRequest';
},
handle(handlerInput) {
const error = handlerInput.requestEnvelope?.request?.error;
if (error) {
console.error('⚠️ Alexa SessionEnded Error:', JSON.stringify(error));
}
return handlerInput.responseBuilder.getResponse();
}
};
const ErrorHandler = {
canHandle() {
return true;
},
handle(handlerInput, error) {
console.error(`Erreur Skill Alexa: ${error.message}`);
return handlerInput.responseBuilder
.speak('Oups, je n\'ai pas réussi à joindre le serveur de la banque des coquillages. Vérifie que le serveur est bien allumé !')
.getResponse();
}
};
const skillBuilder = Alexa.SkillBuilders.custom()
.addRequestHandlers(
LaunchRequestHandler,
IdentifySpeakerIntentHandler,
GetActivityDetailIntentHandler,
APLUserEventHandler,
GetBalanceIntentHandler,
ListActivitiesIntentHandler,
DepositShellsIntentHandler,
ContributeActivityIntentHandler,
ConvertEurosIntentHandler,
SimulateInterestIntentHandler,
ProposeActivityIntentHandler,
HelpIntentHandler,
CancelAndStopIntentHandler,
SessionEndedRequestHandler
)
.addErrorHandlers(ErrorHandler);
export const skill = skillBuilder.create();
export const handler = skillBuilder.lambda();