import React, {useState} from 'react'; import {Linking, Pressable, ScrollView, StyleSheet, Text, View} from 'react-native'; import {useSafeAreaInsets} from 'react-native-safe-area-context'; import {colors, radii, spacing} from '../../../theme'; import {PrimaryButton} from '../components/PrimaryButton'; import type { MeasurementState, PPGProcessingResult, SignalQualityIssue, } from '../measurement/measurementTypes'; interface PPGFailureScreenProps { state: MeasurementState; message: string; result?: PPGProcessingResult | null; showOpenSettings?: boolean; onRetry: () => void; onBack: () => void; } const ISSUE_LABELS: Record = { contactInsufficient: 'contact caméra insuffisant', contactInterrupted: 'contact interrompu', durationInsufficient: 'durée utile insuffisante', droppedFrames: 'images perdues', timestampJitter: 'cadence caméra irrégulière', longGap: 'interruption trop longue', saturation: 'image trop lumineuse / saturée', motionArtifacts: 'mouvements détectés', lowPulsatileAmplitude: 'pulsations trop faibles', lowSignalToNoiseRatio: 'signal trop bruité', tooFewPeaks: 'trop peu de battements détectés', tooManyRejectedPeaks: 'trop de battements rejetés', tooManyRejectedIntervals: 'intervalles trop irréguliers', methodDisagreement: 'estimations du rythme incohérentes', channelInstability: 'couleur du signal instable', nonFiniteProcessingResult: 'calcul du signal impossible', }; const percent = (value: number): string => `${Math.round(value * 100)} %`; function qualityLabel(result: PPGProcessingResult): string { switch (result.quality.level) { case 'good': return 'bonne'; case 'medium': return 'moyenne'; case 'poor': return 'faible'; default: return 'invalide'; } } export function PPGFailureScreen({ state, message, result = null, showOpenSettings = false, onRetry, onBack, }: PPGFailureScreenProps): React.JSX.Element { const thermal = state === 'thermalStop'; const insets = useSafeAreaInsets(); const [showDiagnostic, setShowDiagnostic] = useState(false); return ( {thermal ? '°' : '!'} {thermal ? 'L’iPhone doit refroidir' : 'Mesure non exploitable'} {message} {!thermal && result !== null ? ( setShowDiagnostic(value => !value)} style={styles.diagnosticToggle}> Détails techniques {showDiagnostic ? '−' : '+'} ) : null} {!thermal && result !== null && showDiagnostic ? ( Diagnostic de cette mesure Qualité : {percent(result.quality.score)} ({qualityLabel(result)}) Signal utile : {result.quality.usableDurationSeconds.toFixed(1)} s · mouvement : {percent(result.quality.motionArtifactRatio)} Saturation : {percent(result.quality.saturationRatio)} · pertes :{' '} {percent(result.quality.droppedFrameRatio)} SNR :{' '} {result.quality.signalToNoiseRatio === null ? 'indisponible' : result.quality.signalToNoiseRatio.toFixed(1)}{' '} · battements : {result.bpm.validPeakCount} Trames : {result.rawSampleCount} · rééchantillonnées :{' '} {result.resampledSampleCount} {result.quality.issues.length > 0 ? ( Cause(s) :{' '} {result.quality.issues.map(issue => ISSUE_LABELS[issue]).join(', ')} ) : null} ) : null} Pour le prochain essai Posez le doigt doucement, couvrez entièrement la caméra choisie et gardez la main immobile. Avec la caméra arrière, couvrez aussi le flash. {showOpenSettings ? ( Linking.openSettings().catch(() => undefined)} /> ) : null} ); } const styles = StyleSheet.create({ scroll: { flex: 1, backgroundColor: colors.background, }, container: { flexGrow: 1, padding: spacing.lg, justifyContent: 'center', backgroundColor: colors.background, }, icon: { width: 60, height: 60, borderRadius: 30, alignItems: 'center', justifyContent: 'center', borderWidth: 1, borderColor: colors.poor, backgroundColor: colors.accentSoft, }, iconText: { color: colors.poor, fontSize: 27, fontWeight: '900', }, title: { color: colors.text, fontSize: 32, lineHeight: 37, fontWeight: '800', marginTop: spacing.lg, }, message: { color: colors.muted, fontSize: 15, lineHeight: 22, marginTop: spacing.sm, }, diagnosticCard: { padding: spacing.md, borderRadius: radii.md, backgroundColor: colors.surface, borderWidth: 1, borderColor: colors.accent, marginTop: spacing.sm, }, diagnosticToggle: { minHeight: 54, marginTop: spacing.lg, paddingHorizontal: spacing.md, borderRadius: radii.md, backgroundColor: colors.surface, borderWidth: 1, borderColor: colors.border, flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', }, diagnosticChevron: { color: colors.accent, fontSize: 25, fontWeight: '300', }, diagnosticLine: { color: colors.text, fontSize: 14, lineHeight: 21, marginTop: spacing.xs, }, issueText: { color: colors.poor, fontSize: 14, lineHeight: 20, fontWeight: '700', marginTop: spacing.sm, }, tipCard: { padding: spacing.md, borderRadius: radii.md, backgroundColor: colors.surface, borderWidth: 1, borderColor: colors.border, marginVertical: spacing.lg, }, tipTitle: { color: colors.text, fontWeight: '700', }, tipText: { color: colors.muted, lineHeight: 20, marginTop: spacing.xs, }, });