feat(r6): R6.5 — Assistant mobile (chat sourcé + dictée)

Maquette dédiée validée (docs/02-design/maquettes/maquette-mobile-assistant.html,
3 écrans) — la seule sous-étape de R6 à avoir eu son propre tour de
design-first plutôt qu'une exécution directe de la maquette R6 initiale,
un chat sourcé étant un patron d'écran neuf. D1 un échange à la fois ; D2
"Voir le document" → métadonnées seules (R6.3), "Ouvrir l'OT" → fiche R4 ;
D3 refus chiffré + avertissement permanents, identiques au web ; D4
Assistant rejoint Pilotage pour tout rôle avec WORK_ORDERS.view ; D5
(ajoutée en revue) — la question peut être tapée OU dictée, même pipeline
que la dictée déjà livrée en clôture (ADR-004 §5, faster-whisper local,
audio jamais persisté), la transcription remplit le champ, jamais d'envoi
automatique.

api/assistant.ts (useAskAssistant, 503 géré comme le web). Écran Assistant :
chat un échange à la fois, citations numérotées, sources avec extrait
exact, refus honnête chiffré + Reformuler. Micro dans la barre de saisie :
réutilise telle quelle la mécanique de cloture.tsx (useAudioRecorder,
setAudioModeAsync, useTranscription, purge quoi qu'il arrive) — pipeline
déjà recetté sur iPhone physique (01/08), aucun nouveau bug à découvrir.

Fiche document (bibliotheque/[id].tsx, nouveau) — la liste R6.3 y mène
aussi désormais. Menu : Assistant route réellement — le Menu R6 n'a plus
d'entrée "à venir" dans les 4 groupes (Catégories exceptée, admin, hors
périmètre mobile).

R6 fonctionnellement complet (Exploitation/Parc/Ressources/Pilotage,
Assistant inclus, pour tous les rôles) — reste une recette complète avant
de considérer R6 close au sens R0→R5.

Typecheck propre, 17 tests Jest, lint 5/5 paquets. Contrat non touché.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
pr-daaif
2026-08-02 12:18:25 +01:00
parent 700bd4e7ff
commit ac8b92e9c5
8 changed files with 857 additions and 5 deletions

View File

@@ -0,0 +1,26 @@
import { useMutation } from '@tanstack/react-query';
import type { AssistantAsk } from '@siop/shared';
import { api } from './client';
/** Hook R6.5 — l'assistant passe par l'API NestJS (le service IA n'est
* jamais appelé depuis l'app, ADR-004 §4). Le 503 est un état ATTENDU du
* contrat (service éteint ou pas encore déployé) — même message que le web. */
async function unwrap<T>(res: { data?: T; error?: unknown; response: Response }): Promise<T> {
if (res.response.status === 503) {
throw new Error('Assistant indisponible pour le moment — réessayez dans un instant.');
}
if (res.error || res.data === undefined) {
const message =
(res.error as { message?: string } | undefined)?.message ??
`Le serveur a répondu ${res.response.status}`;
throw new Error(Array.isArray(message) ? message.join(' — ') : message);
}
return res.data;
}
export function useAskAssistant() {
return useMutation({
mutationFn: async (body: AssistantAsk) => unwrap(await api.POST('/assistant/ask', { body })),
});
}