Tutoriel : Construisez Votre Première Application
Dans ce tutoriel, vous allez construire un mini-programme Compteur de Mots qui lit l'historique de chat actif, compte les mots par rôle (utilisateur vs assistant) et affiche les statistiques. À la fin, vous saurez comment créer, tester, empaqueter et publier un mini-programme.
Ce que vous apprendrez :
- Créer un
manifest.json - Écrire le HTML de l'application avec le SDK
window.ais - Utiliser
ais.chat.getHistory()pour lire les messages - Utiliser
ais.storagepour persister les données entre sessions - Tester localement avec le sideloading
- Empaqueter en bundle
.ais - Publier sur le registre communautaire
Temps requis : Environ 15 minutes.
Étape 1 : Créer le Manifeste
Chaque mini-programme a besoin d'un manifest.json qui décrit l'application, ses permissions et son point d'entrée. Créez un nouveau répertoire et ajoutez ce fichier :
mkdir word-counter
cd word-counter
Créez manifest.json :
{
"name": "word-counter",
"version": "1.0.0",
"abi": 1,
"type": "mini-program",
"title": "Word Counter",
"description": "Count words in your chat history by role",
"author": { "name": "Your Name" },
"entry": "index.html",
"base_url": "https://localhost:8080/",
"permissions": ["storage", "chat:read", "ui:toast"],
"keywords": ["statistics", "utility"]
}
Champs clés :
| Champ | Valeur | Pourquoi |
|---|---|---|
name | word-counter | Identifiant unique (minuscules, tirets uniquement) |
abi | 1 | Requis — correspond à l'ABI actuelle de la plateforme |
type | mini-program | Indique à la plateforme qu'il s'agit d'une app iframe sandboxée |
entry | index.html | Le fichier HTML à charger |
base_url | https://localhost:8080/ | Où les ressources sont hébergées (à mettre à jour pour la production) |
permissions | ["storage", "chat:read", "ui:toast"] | Nous devons lire le chat et sauvegarder les stats |
La permission storage est toujours accordée automatiquement, mais c'est une bonne pratique de la lister explicitement pour que les utilisateurs sachent que votre application stocke des données.
Étape 2 : Créer le HTML
Créez index.html dans le même répertoire. C'est votre application entière — HTML, CSS et JavaScript dans un seul fichier.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family:
system-ui,
-apple-system,
sans-serif;
padding: 20px;
color: #e0e0e0;
background: #1a1a2e;
min-height: 100vh;
}
h1 {
font-size: 22px;
margin-bottom: 16px;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
gap: 12px;
margin-bottom: 20px;
}
.stat-card {
background: #16213e;
border: 1px solid #333;
border-radius: 8px;
padding: 16px;
text-align: center;
}
.stat-value {
font-size: 32px;
font-weight: 700;
color: #58a6ff;
font-variant-numeric: tabular-nums;
}
.stat-label {
font-size: 14px;
color: #888;
margin-top: 4px;
}
.actions {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
button {
min-height: 48px;
padding: 10px 20px;
font-size: 16px;
border: 1px solid #444;
border-radius: 6px;
background: #2a2a4e;
color: #e0e0e0;
cursor: pointer;
}
button:hover {
background: #3a3a5e;
}
button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.btn-primary {
background: #1a73e8;
border-color: #1a73e8;
}
.btn-primary:hover {
background: #1557b0;
}
.btn-close {
background: none;
border-color: #666;
color: #888;
}
#last-updated {
margin-top: 16px;
font-size: 14px;
color: #666;
}
</style>
</head>
<body>
<h1>Compteur de Mots</h1>
<div class="stats-grid">
<div class="stat-card">
<div class="stat-value" id="total-words">--</div>
<div class="stat-label">Total Mots</div>
</div>
<div class="stat-card">
<div class="stat-value" id="user-words">--</div>
<div class="stat-label">Vos Mots</div>
</div>
<div class="stat-card">
<div class="stat-value" id="ai-words">--</div>
<div class="stat-label">Mots IA</div>
</div>
<div class="stat-card">
<div class="stat-value" id="msg-count">--</div>
<div class="stat-label">Messages</div>
</div>
</div>
<div class="actions">
<button class="btn-primary" id="analyze">Analyser le Chat</button>
<button class="btn-close" id="close">Fermer</button>
</div>
<div id="last-updated"></div>
<script>
// Helper: count words in a string
function countWords(text) {
if (!text || typeof text !== "string") return 0;
return text.trim().split(/\s+/).filter(Boolean).length;
}
// Format numbers with commas (1234 -> "1,234")
function fmt(n) {
return n.toLocaleString();
}
ais.ready(async function () {
// Set the panel title
ais.ui.setTitle("Compteur de Mots");
// Try to restore last saved stats
var saved = await ais.storage.get("last-stats");
if (saved) {
showStats(saved);
}
// Analyze button
document
.getElementById("analyze")
.addEventListener("click", async function () {
this.disabled = true;
this.textContent = "Analyse...";
try {
// Fetch up to 500 messages from chat history
var messages = await ais.chat.getHistory(500);
var userWords = 0;
var aiWords = 0;
var msgCount = messages.length;
for (var i = 0; i < messages.length; i++) {
var msg = messages[i];
var words = countWords(msg.content);
if (msg.role === "user") {
userWords += words;
} else if (msg.role === "assistant") {
aiWords += words;
}
}
var stats = {
total: userWords + aiWords,
user: userWords,
ai: aiWords,
messages: msgCount,
timestamp: Date.now(),
};
showStats(stats);
// Save stats for next time
await ais.storage.set("last-stats", stats);
ais.ui.toast(msgCount + " messages analysés !");
} catch (err) {
ais.ui.toast("Erreur : " + err.message);
}
this.disabled = false;
this.textContent = "Analyser le Chat";
});
// Close button
document.getElementById("close").addEventListener("click", function () {
ais.close();
});
});
function showStats(stats) {
document.getElementById("total-words").textContent = fmt(stats.total);
document.getElementById("user-words").textContent = fmt(stats.user);
document.getElementById("ai-words").textContent = fmt(stats.ai);
document.getElementById("msg-count").textContent = fmt(stats.messages);
if (stats.timestamp) {
var date = new Date(stats.timestamp);
document.getElementById("last-updated").textContent =
"Dernière analyse : " + date.toLocaleString();
}
}
</script>
</body>
</html>
Ce que fait ce code
ais.ready()— Attend que le pont de la plateforme soit connecté avant d'exécuter toute logique.ais.storage.get('last-stats')— Restaure les statistiques précédemment sauvegardées pour que l'utilisateur voie les données immédiatement au lancement.ais.chat.getHistory(500)— Récupère jusqu'à 500 messages de la conversation active.- Comptage de mots — Itère à travers les messages, divisant le contenu sur les espaces et comptabilisant par rôle.
ais.storage.set('last-stats', stats)— Persiste les résultats pour la prochaine fois.ais.ui.toast()— Affiche une notification lorsque l'analyse est terminée.ais.close()— Retourne à la vue de chat lorsque l'utilisateur clique sur Fermer.
Étape 3 : Tester Localement
Vous avez besoin d'un serveur HTTP local pour servir le manifeste et le fichier HTML. Utilisez l'outil de votre choix :
# Python 3
cd word-counter
python3 -m http.server 8080
# ou Node.js
npx serve -p 8080
# ou PHP
php -S localhost:8080
Maintenant installez l'application dans la plateforme :
- Ouvrez aiscouncil.net et connectez-vous
- Cliquez sur l'icône Apps dans la barre latérale gauche
- Dans la section Sideload, collez :
http://localhost:8080/manifest.json - Cliquez sur Install
- Passez en revue les permissions (storage, chat:read, ui:toast) et cliquez sur Allow
- Cliquez sur Open sur la carte de l'application installée
Pour la boucle de développement la plus rapide, utilisez HTML Upload au lieu du sideloading par URL. Uploadez votre index.html directement — pas de serveur nécessaire. La plateforme crée un manifeste synthétique automatiquement. Vous pouvez désinstaller et ré-uploader à chaque modification.
Dépannage
| Problème | Solution |
|---|---|
| "Failed to fetch manifest" | Assurez-vous que votre serveur local fonctionne et sert des en-têtes CORS. Essayez python3 -m http.server 8080 qui sert des réponses CORS-safe. |
| L'application affiche une page blanche | Vérifiez la console du navigateur pour les erreurs. Le problème le plus courant est d'appeler les méthodes ais.* avant ais.ready(). |
| "PermissionDenied: chat:read" | Votre manifeste n'inclut pas chat:read dans le tableau des permissions. Mettez à jour le manifeste et réinstallez. |
| L'application ne se met pas à jour après les changements de code | Désinstallez d'abord l'application (cliquez sur le bouton X sur la carte), puis réinstallez. Le HTML d'entrée est mis en cache au moment de l'installation. |
Étape 4 : Ajouter de la Finition
Ajoutons une fonctionnalité : le comptage de mots en temps réel à mesure que de nouveaux messages arrivent.
Ajoutez ce code à l'intérieur du callback ais.ready(), après le gestionnaire du bouton de fermeture :
// Subscribe to new messages for real-time counting
ais.chat.onMessage(function (msg) {
// Re-read saved stats and add the new message's words
ais.storage.get("last-stats").then(function (stats) {
if (!stats) return;
var words = countWords(msg.content);
if (msg.role === "user") stats.user += words;
else if (msg.role === "assistant") stats.ai += words;
stats.total = stats.user + stats.ai;
stats.messages++;
stats.timestamp = Date.now();
showStats(stats);
ais.storage.set("last-stats", stats);
});
});
Maintenant les compteurs se mettent à jour en direct à mesure que l'utilisateur discute sans avoir besoin de cliquer sur "Analyser" à nouveau.
Étape 5 : Empaqueter en Bundle .ais
Un bundle .ais est une archive ZIP contenant votre manifeste et tous les fichiers de l'application. La plateforme extrait le ZIP, lit le manifeste et intègre toutes les ressources (CSS, JS, images) dans le HTML d'entrée.
Pour une application mono-fichier comme la nôtre, le bundle est simple :
cd word-counter
zip -r ../word-counter.ais manifest.json index.html
Cela crée word-counter.ais dans le répertoire parent.
Tester le bundle
- Dans la plateforme, allez dans Apps et cliquez sur Upload App
- Sélectionnez
word-counter.ais - Passez en revue les permissions et approuvez
- L'application s'installe depuis le bundle avec toutes les ressources intégrées
Les bundles sont autonomes. L'utilisateur n'a pas besoin d'un accès réseau vers le base_url original — tout est intégré au moment de l'installation. Cela rend les bundles idéaux pour la distribution hors-ligne et le partage.
Bundles multi-fichiers
Si votre application a des fichiers CSS, JavaScript ou images séparés, incluez-les tous dans le ZIP :
zip -r ../word-counter.ais manifest.json index.html style.css app.js icon.png
La plateforme intègre automatiquement :
<link rel="stylesheet" href="style.css">devient<style>...</style><script src="app.js"></script>devient<script>...</script><img src="icon.png">devient<img src="data:image/png;base64,...">
Étape 6 : Publier sur le Registre
Une fois votre application prête pour les autres, publiez-la sur le registre communautaire.
1. Hébergez vos fichiers
Uploadez votre manifeste et HTML d'entrée sur un CDN public. GitHub Pages est gratuit et simple :
# Dans votre repo GitHub (ex., github.com/yourname/word-counter)
# Poussez manifest.json et index.html sur la branche main
# Activez GitHub Pages dans les paramètres du repo (source: branche main, racine)
Vos fichiers seront disponibles à :
https://yourname.github.io/word-counter/manifest.jsonhttps://yourname.github.io/word-counter/index.html
Mettez à jour base_url dans votre manifeste pour correspondre :
"base_url": "https://yourname.github.io/word-counter/"
2. Forkez le repo aiscouncil
Allez sur github.com/nicholasgasior/bcz et cliquez sur Fork.
3. Ajoutez votre entrée de package
Éditez registry/packages.json et ajoutez une entrée au tableau packages :
{
"name": "word-counter",
"type": "mini-program",
"version": "1.0.0",
"manifest": "https://yourname.github.io/word-counter/manifest.json",
"tier": "community",
"category": "utilities",
"description": "Count words in your chat history by role",
"icon": "https://yourname.github.io/word-counter/icon.png",
"added": "2026-02-19",
"price": 0,
"currency": "USD",
"seller": null
}
4. Validez
Exécutez le script de validation pour vérifier votre entrée :
python3 registry/validate.py packages
Si la validation passe, vous êtes prêt à soumettre.
5. Soumettez une pull request
Poussez vos changements vers votre fork et créez une PR contre le repo principal. Si la validation automatisée passe, la PR peut être fusionnée et votre application apparaîtra dans la section App Store de la plateforme.
Voir Publication sur le Registre pour tous les détails sur la tarification, la configuration vendeur et les niveaux de vérification.
Conseils et Meilleures Pratiques
Design
- Thème sombre par défaut — La plupart des utilisateurs de la plateforme utilisent le mode sombre. Concevez pour des fonds sombres (
#1a1a2eou similaire) avec du texte clair (#e0e0e0). - Cibles tactiles minimum 48px — Les boutons et éléments interactifs doivent faire au moins 48px de hauteur pour l'accessibilité et l'interaction VLM-friendly.
- Taille de police minimum 14px — Tout le texte doit faire au moins 14px pour la lisibilité.
- Layout responsive — La largeur du panneau des applications varie. Utilisez CSS Grid ou Flexbox avec
auto-fitpour s'adapter.
Performance
- Cachez les résultats dans le stockage — Utilisez
ais.storagepour sauvegarder les résultats calculés. Restaurez-les au lancement pour que l'utilisateur voie les données immédiatement. - Limitez les requêtes d'historique de chat —
ais.chat.getHistory(500)est généralement suffisant. Évitez de demander un historique illimité. - Pas de polling — Utilisez
ais.chat.onMessage()pour les mises à jour en temps réel au lieu d'appelergetHistoryde façon répétée.
Sécurité
- Demandez des permissions minimales — Listez uniquement les permissions que votre application utilise réellement. Moins de permissions signifie plus d'utilisateurs feront confiance et installeront votre application.
- Validez toutes les entrées — Les données de
ais.chat.getHistory()contiennent du contenu généré par l'utilisateur. Assainissez avant d'insérer dans le DOM. - Ne stockez pas de données sensibles —
ais.storagen'est pas chiffré. Ne stockez jamais de mots de passe, tokens ou clés API (sauf si votre application asecrets:syncet gère explicitement le transfert d'identifiants).
Compatibilité
- Vérifiez
ais.platform.abi— Si votre application dépend de fonctionnalités SDK spécifiques, vérifiez la version ABI et affichez un message utile si la plateforme est plus ancienne. - Enveloppez les appels SDK dans try/catch — Les erreurs de permission et les différences de version de plateforme peuvent causer des rejets. Gérez-les avec élégance.
- Testez avec l'exemple hello-world — La plateforme est livrée avec un mini-programme exemple que vous pouvez utiliser comme référence.