--- title: "Une page web minimale, servie par l'API" --- # Une page web minimale, servie par l'API > [!NOTE] > Dans cette leçon, vous allez écrire la page de discussion d'Alti : **un seul fichier HTML** de 100 lignes, sans framework ni outil de build, servi par FastAPI, qui affiche la réponse au fur et à mesure et propose 👍 / 👎 après chaque réponse. ## Une page, trois rôles ```mermaid flowchart LR F["Formulaire : question"] -->|"fetch POST /chat"| A["API"] A -->|"flux de texte"| L["Lecteur : getReader()"] L -->|"textContent += morceau"| B["Bulle du bot"] B --> V["Boutons 👍 / 👎"] V -->|"fetch POST /avis"| A ``` La page est servie par l'API elle-même (`GET /` renvoie [index.html](../files/index.html)) : même adresse, donc pas de problème de **CORS** (la règle du navigateur qui bloque, par défaut, les appels vers un autre domaine). ```python title="files/app.py (extrait)" @app.get("/") def page(): return FileResponse(Path(__file__).parent / "index.html") ``` ## Lire un flux en JavaScript `fetch` renvoie une réponse dont le corps (`body`) est un **flux lisible** (`ReadableStream`). On le lit morceau par morceau avec un lecteur, et on décode les octets en texte avec `TextDecoder` : ```javascript title="files/index.html (extrait)" const rep = await fetch("/chat", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ session, message }) }); const lecteur = rep.body.getReader(); const decodeur = new TextDecoder(); bot.textContent = ""; for (;;) { // affiche chaque morceau dès qu'il arrive const { done, value } = await lecteur.read(); if (done) break; bot.textContent += decodeur.decode(value, { stream: true }); } boutonsAvis(bot, rep.headers.get("X-Echange-Id")); ``` - `{ stream: true }` évite de couper un caractère accentué (plusieurs octets en UTF-8) entre deux morceaux. - `session` vaut `crypto.randomUUID()` : une conversation par onglet, sans cookie ni compte. > [!WARNING] > On écrit avec `textContent`, **jamais** avec `innerHTML`. Si le modèle (ou un utilisateur malin, par injection de prompt) produit ``, `innerHTML` l'exécuterait dans la page : c'est une faille **XSS** (*cross-site scripting*). `textContent` l'affiche comme du texte. | Choix | Ici | Quand changer | | --- | --- | --- | | HTML + JS sans framework | 1 fichier, rien à compiler | interface riche (historique, pièces jointes, comptes) | | Texte brut, pas de Markdown | sûr et simple | si vous voulez des listes : une bibliothèque Markdown avec nettoyage du HTML | | Session en mémoire du navigateur | rien à stocker | reprise de conversation sur un autre appareil | ## Accessibilité de base - `aria-live="polite"` sur le fil de discussion : un lecteur d'écran annonce les nouvelles réponses. - Un `