// SDK 57 : les fonctions module-level (dont deleteAsync) sont dépréciées au // profit des classes File/Directory et lèvent désormais en développement — // import explicite du sous-chemin legacy, comportement inchangé sinon. import * as FileSystem from 'expo-file-system/legacy'; import { router } from 'expo-router'; import { useRef, useState } from 'react'; import { Pressable, ScrollView, Text, TextInput, View } from 'react-native'; import { RecordingPresets, requestRecordingPermissionsAsync, setAudioModeAsync, useAudioRecorder, } from 'expo-audio'; import { SafeAreaView } from 'react-native-safe-area-context'; import type { AssistantAnswer, AssistantExcerpt } from '@siop/shared'; import { useAskAssistant } from '@/api/assistant'; import { useTranscription } from '@/api/exploitation'; import { BoutonTel, EnteteFiche } from '@/composants/ui'; import { useTokens, type Tokens } from '@/theme/tokens'; /** Assistant — maquette « Assistant mobile » (validée 02/08) : même contenu * et mêmes règles que le web (D2 « sourcé ou silencieux », ADR-004 §4 — * le service IA n'est jamais appelé directement, toujours via l'API). * D5 : 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), la transcription REMPLIT le * champ (éditable), aucun envoi automatique. */ interface Echange { question: string; reponse?: AssistantAnswer; erreur?: string; } export default function PageAssistant() { const t = useTokens(); const ask = useAskAssistant(); const transcrire = useTranscription(); const recorder = useAudioRecorder(RecordingPresets.HIGH_QUALITY); const [question, setQuestion] = useState(''); const [echanges, setEchanges] = useState([]); const [enregistrement, setEnregistrement] = useState(false); const saisieRef = useRef(null); const demarrerDictee = async () => { const { granted } = await requestRecordingPermissionsAsync(); if (!granted) return; // iOS refuse recorder.record() sans autorisation explicite de la session // audio (RecordingDisabledException) — même correctif qu'en clôture. await setAudioModeAsync({ allowsRecording: true, playsInSilentMode: true }); await recorder.prepareToRecordAsync(); recorder.record(); setEnregistrement(true); }; const terminerDictee = async () => { await recorder.stop(); await setAudioModeAsync({ allowsRecording: false }); setEnregistrement(false); const uri = recorder.uri; if (!uri) return; transcrire.mutate( { uri, nom: 'question.m4a', mime: 'audio/m4a' }, { onSuccess: (resultat) => setQuestion(resultat.text), // Loi 09-08 (D5) : l'audio ne survit jamais à l'appel, succès ou pas. onSettled: () => void FileSystem.deleteAsync(uri, { idempotent: true }), }, ); }; const envoyer = () => { const q = question.trim(); if (q.length < 3 || ask.isPending) return; setQuestion(''); setEchanges((liste) => [...liste, { question: q }]); ask.mutate( { question: q }, { onSuccess: (reponse) => setEchanges((liste) => liste.map((e, i) => (i === liste.length - 1 ? { ...e, reponse } : e)), ), onError: (erreur) => setEchanges((liste) => liste.map((e, i) => (i === liste.length - 1 ? { ...e, erreur: erreur.message } : e)), ), }, ); }; return ( {echanges.length === 0 ? ( Posez une question sur vos notices, vos historiques d'intervention ou vos procédures — chaque réponse cite ses sources. Quand le corpus ne porte pas la réponse, l'assistant le dit au lieu d'inventer. ) : null} {echanges.map((e, i) => ( {e.question} {e.reponse ? ( saisieRef.current?.focus()} /> ) : e.erreur ? ( {e.erreur} ) : ( Recherche dans le corpus… )} ))} {enregistrement ? ( 🎙 Enregistrement en cours… void terminerDictee()} /> ) : ( void demarrerDictee()} style={{ width: 26, height: 26, borderRadius: 13, backgroundColor: t.primaireDoux, alignItems: 'center', justifyContent: 'center', }} > 🎙 Envoyer )} {transcrire.isError ? ( {transcrire.error.message} ) : null} ); } function Reponse({ reponse, t, surReformuler, }: { reponse: AssistantAnswer; t: Tokens; surReformuler: () => void; }) { if (reponse.mode === 'REFUSAL') { return ( Je ne trouve pas de source fiable dans votre bibliothèque — je préfère ne pas inventer. J'ai cherché dans {reponse.corpus.documents} document {reponse.corpus.documents > 1 ? 's' : ''} indexé {reponse.corpus.documents > 1 ? 's' : ''} et {reponse.corpus.reports} bilan {reponse.corpus.reports > 1 ? 's' : ''} d'intervention : rien d'assez proche de votre question. ); } return ( {reponse.mode === 'GENERATED' && reponse.answer ? ( {reponse.answer} ) : ( Voici ce que portent vos sources — les extraits sont cités tels quels. )} {reponse.excerpts.map((ex, i) => ( ))} ⚠ L'IA propose, vous validez : vérifiez la notice avant d'agir sur l'appareil. ); } function Source({ ex, no, t }: { ex: AssistantExcerpt; no: number; t: Tokens }) { const ouvrir = () => { if (ex.sourceType === 'DOCUMENT' && ex.documentId) router.push(`/bibliotheque/${ex.documentId}`); else if (ex.workOrderId) router.push(`/ot/${ex.workOrderId}`); }; return ( {no} {ex.title} {ex.sourceType === 'DOCUMENT' ? 'Bibliothèque' : 'Historique'} · {ex.locator} « {ex.content} » {ex.sourceType === 'DOCUMENT' ? 'Voir le document' : "Ouvrir l'OT"} ); }