---
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 `