@adminide-stack/yantra-mobile 12.0.136 → 12.0.137-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. package/lib/features/apps/AppsCatalog.js +3 -3
  2. package/lib/features/apps/AppsCatalog.js.map +1 -1
  3. package/lib/features/apps/appLabelText.js +9 -0
  4. package/lib/features/apps/appLabelText.js.map +1 -0
  5. package/lib/features/devices/readings.js.map +1 -1
  6. package/lib/features/devices/session.js +2 -22
  7. package/lib/features/devices/session.js.map +1 -1
  8. package/lib/features/dictate/DictateExperience.js +48 -25
  9. package/lib/features/dictate/DictateExperience.js.map +1 -1
  10. package/lib/features/dictate/backgroundRecording.js +15 -0
  11. package/lib/features/dictate/backgroundRecording.js.map +1 -0
  12. package/lib/features/dictate/components/DictationList.js.map +1 -1
  13. package/lib/features/dictate/components/NoteView.js +3 -1
  14. package/lib/features/dictate/components/NoteView.js.map +1 -1
  15. package/lib/features/dictate/components/RecordPanel.js +33 -4
  16. package/lib/features/dictate/components/RecordPanel.js.map +1 -1
  17. package/lib/features/dictate/dictateClient.js +7 -2
  18. package/lib/features/dictate/dictateClient.js.map +1 -1
  19. package/lib/features/dictate/dictationStore.js.map +1 -1
  20. package/lib/features/dictate/useDictationRecorder.js +48 -14
  21. package/lib/features/dictate/useDictationRecorder.js.map +1 -1
  22. package/lib/screens/Devices/index.js +3 -205
  23. package/lib/screens/Devices/index.js.map +1 -1
  24. package/lib/screens/DictateNative/index.js.map +1 -1
  25. package/package.json +4 -4
  26. package/lib/features/devices/bodyComposition.js +0 -116
  27. package/lib/features/devices/bodyComposition.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"RecordPanel.js","sources":["../../../../src/features/dictate/components/RecordPanel.tsx"],"sourcesContent":["/**\n * The microphone: one large button to start, then a timer, a live level,\n * Pause/Resume, Discard and Done. A pause the app made (it left the screen,\n * or the hour ran out) says why, so no audio goes missing unnoticed.\n */\nimport React, { useEffect, useRef, useState } from 'react';\nimport { ActivityIndicator, StyleSheet, Text, View } from 'react-native';\nimport { MaterialCommunityIcons } from '@expo/vector-icons';\nimport { DIM, Tap } from '../../inbox/components/Tap';\nimport { formatDuration } from '../noteText';\nimport type { DictateTheme } from '../dictateTheme';\nimport type { PauseReason, RecorderPhase } from '../useDictationRecorder';\n\nconst BARS = 32;\n\n/** The last few seconds of level, as bars: the clinician can see the phone hears them. */\nfunction LevelBars({ level, active, theme }: { level: number; active: boolean; theme: DictateTheme }) {\n const [history, setHistory] = useState<number[]>(() => Array(BARS).fill(0));\n const levelRef = useRef(level);\n levelRef.current = level;\n useEffect(() => {\n if (!active) return;\n const id = setInterval(() => setHistory((h) => [...h.slice(1), levelRef.current]), 90);\n return () => clearInterval(id);\n }, [active]);\n return (\n <View style={styles.bars} accessibilityLabel=\"Microphone level\">\n {history.map((v, i) => (\n <View\n key={i}\n style={[\n styles.bar,\n {\n height: 6 + Math.round(v * 46),\n backgroundColor: active ? theme.rec : theme.border,\n opacity: active ? 0.35 + 0.65 * (i / BARS) : 1,\n },\n ]}\n />\n ))}\n </View>\n );\n}\n\nexport function StartPanel({\n theme,\n starting,\n onStart,\n}: {\n theme: DictateTheme;\n starting: boolean;\n onStart: () => void;\n}) {\n // One target for the button and its words: \"Start dictating\" reads like the thing to tap.\n return (\n <Tap\n onPress={onStart}\n disabled={starting}\n accessibilityRole=\"button\"\n accessibilityLabel=\"Start dictating\"\n style={styles.start}\n pressedStyle={DIM}\n >\n <View style={[styles.micButton, { backgroundColor: theme.accent, shadowColor: theme.accent }]}>\n {starting ? (\n <ActivityIndicator color={theme.accentInk} />\n ) : (\n <MaterialCommunityIcons name=\"microphone\" size={42} color={theme.accentInk} />\n )}\n </View>\n <Text style={[styles.startLabel, { color: theme.ink }]}>Start dictating</Text>\n <Text style={[styles.startHint, { color: theme.muted }]}>Keep Yantra open while you speak.</Text>\n </Tap>\n );\n}\n\nexport function RecordingPanel({\n theme,\n phase,\n pauseReason,\n elapsedMs,\n level,\n onPause,\n onResume,\n onDone,\n onDiscard,\n}: {\n theme: DictateTheme;\n phase: Exclude<RecorderPhase, 'idle'>;\n pauseReason: PauseReason;\n elapsedMs: number;\n level: number;\n onPause: () => void;\n onResume: () => void;\n onDone: () => void;\n onDiscard: () => void;\n}) {\n const live = phase === 'recording';\n const notice =\n pauseReason === 'background'\n ? 'Paused because Yantra left the screen. Resume to keep dictating.'\n : pauseReason === 'limit'\n ? 'An hour is the most one dictation holds. Tap Done to write the note.'\n : null;\n return (\n <View style={styles.recording}>\n <View style={[styles.status, { backgroundColor: live ? theme.recSoft : theme.chip }]}>\n <View style={[styles.dot, { backgroundColor: live ? theme.rec : theme.faint }]} />\n <Text style={[styles.statusText, { color: live ? theme.rec : theme.muted }]}>{live ? 'Recording' : 'Paused'}</Text>\n </View>\n <Text style={[styles.timer, { color: theme.ink }]} accessibilityLabel={`Elapsed ${formatDuration(elapsedMs)}`}>\n {formatDuration(elapsedMs)}\n </Text>\n <LevelBars level={level} active={live} theme={theme} />\n {notice ? (\n <View style={[styles.notice, { backgroundColor: theme.warnSoft, borderColor: theme.warnBorder }]}>\n <MaterialCommunityIcons name=\"pause-circle-outline\" size={18} color={theme.warnInk} />\n <Text style={[styles.noticeText, { color: theme.warnInk }]}>{notice}</Text>\n </View>\n ) : null}\n <View style={styles.controls}>\n <Tap\n onPress={onDiscard}\n accessibilityRole=\"button\"\n accessibilityLabel=\"Discard this dictation\"\n style={[styles.side, { backgroundColor: theme.chip }]}\n pressedStyle={DIM}\n >\n <MaterialCommunityIcons name=\"trash-can-outline\" size={24} color={theme.muted} />\n </Tap>\n <Tap\n onPress={live ? onPause : onResume}\n disabled={pauseReason === 'limit'}\n accessibilityRole=\"button\"\n accessibilityLabel={live ? 'Pause' : 'Resume'}\n style={[\n styles.main,\n { backgroundColor: live ? theme.rec : theme.accent, opacity: pauseReason === 'limit' ? 0.4 : 1 },\n ]}\n pressedStyle={DIM}\n >\n <MaterialCommunityIcons name={live ? 'pause' : 'microphone'} size={34} color=\"#ffffff\" />\n </Tap>\n <Tap\n onPress={onDone}\n disabled={elapsedMs < 1000}\n accessibilityRole=\"button\"\n accessibilityLabel=\"Done, write the note\"\n style={[styles.side, { backgroundColor: theme.accentSoft, opacity: elapsedMs < 1000 ? 0.4 : 1 }]}\n pressedStyle={DIM}\n >\n <MaterialCommunityIcons name=\"check\" size={26} color={theme.accent} />\n </Tap>\n </View>\n <View style={styles.legend}>\n <Text style={[styles.legendText, { color: theme.faint }]}>Discard</Text>\n <Text style={[styles.legendText, { color: theme.faint }]}>{live ? 'Pause' : 'Resume'}</Text>\n <Text style={[styles.legendText, { color: theme.faint }]}>Done</Text>\n </View>\n </View>\n );\n}\n\nconst styles = StyleSheet.create({\n start: { alignItems: 'center', paddingVertical: 28 },\n micButton: {\n width: 104,\n height: 104,\n borderRadius: 52,\n alignItems: 'center',\n justifyContent: 'center',\n shadowOpacity: 0.28,\n shadowRadius: 16,\n shadowOffset: { width: 0, height: 8 },\n elevation: 6,\n },\n startLabel: { marginTop: 18, fontSize: 17, fontWeight: '600' },\n startHint: { marginTop: 6, fontSize: 13 },\n recording: { alignItems: 'center', paddingTop: 12, paddingBottom: 8 },\n status: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 12, paddingVertical: 6, borderRadius: 999 },\n dot: { width: 8, height: 8, borderRadius: 4, marginRight: 8 },\n statusText: { fontSize: 13, fontWeight: '600' },\n timer: { marginTop: 18, fontSize: 56, fontWeight: '300', fontVariant: ['tabular-nums'], letterSpacing: 1 },\n bars: { marginTop: 18, height: 56, flexDirection: 'row', alignItems: 'center', gap: 3 },\n bar: { width: 4, borderRadius: 2 },\n notice: {\n marginTop: 18,\n marginHorizontal: 8,\n flexDirection: 'row',\n alignItems: 'center',\n gap: 8,\n paddingHorizontal: 12,\n paddingVertical: 10,\n borderRadius: 12,\n borderWidth: StyleSheet.hairlineWidth,\n },\n noticeText: { flex: 1, fontSize: 13, lineHeight: 18 },\n controls: { marginTop: 28, flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: 36 },\n side: { width: 56, height: 56, borderRadius: 28, alignItems: 'center', justifyContent: 'center' },\n main: { width: 80, height: 80, borderRadius: 40, alignItems: 'center', justifyContent: 'center' },\n legend: { marginTop: 8, flexDirection: 'row', justifyContent: 'center', gap: 36 },\n legendText: { width: 68, textAlign: 'center', fontSize: 12 },\n});\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"RecordPanel.js","sources":["../../../../src/features/dictate/components/RecordPanel.tsx"],"sourcesContent":["/**\n * The microphone: one large button to start, then a timer, a live level,\n * Pause/Resume, Discard and Done. A pause the app made (it left the screen,\n * a call took the microphone, or the hour ran out) says why, and so does a\n * microphone that hears nothing, so no audio goes missing unnoticed.\n */\nimport React, { useEffect, useRef, useState } from 'react';\nimport { ActivityIndicator, StyleSheet, Text, View } from 'react-native';\nimport { MaterialCommunityIcons } from '@expo/vector-icons';\nimport { DIM, Tap } from '../../inbox/components/Tap';\nimport { formatDuration } from '../noteText';\nimport type { DictateTheme } from '../dictateTheme';\nimport type { PauseReason, RecorderPhase } from '../useDictationRecorder';\n\nconst BARS = 32;\n\n/** The last few seconds of level, as bars: the clinician can see the phone hears them. */\nfunction LevelBars({ level, active, theme }: { level: number; active: boolean; theme: DictateTheme }) {\n const [history, setHistory] = useState<number[]>(() => Array(BARS).fill(0));\n const levelRef = useRef(level);\n levelRef.current = level;\n useEffect(() => {\n if (!active) return;\n const id = setInterval(() => setHistory((h) => [...h.slice(1), levelRef.current]), 90);\n return () => clearInterval(id);\n }, [active]);\n return (\n <View style={styles.bars} accessibilityLabel=\"Microphone level\">\n {history.map((v, i) => (\n <View\n key={i}\n style={[\n styles.bar,\n {\n height: 6 + Math.round(v * 46),\n backgroundColor: active ? theme.rec : theme.border,\n opacity: active ? 0.35 + 0.65 * (i / BARS) : 1,\n },\n ]}\n />\n ))}\n </View>\n );\n}\n\nexport function StartPanel({\n theme,\n starting,\n recordsInBackground,\n onStart,\n}: {\n theme: DictateTheme;\n starting: boolean;\n /** This app keeps recording with the phone locked or another app open. */\n recordsInBackground: boolean;\n onStart: () => void;\n}) {\n // One target for the button and its words: \"Start dictating\" reads like the thing to tap.\n return (\n <Tap\n onPress={onStart}\n disabled={starting}\n accessibilityRole=\"button\"\n accessibilityLabel=\"Start dictating\"\n style={styles.start}\n pressedStyle={DIM}\n >\n <View style={[styles.micButton, { backgroundColor: theme.accent, shadowColor: theme.accent }]}>\n {starting ? (\n <ActivityIndicator color={theme.accentInk} />\n ) : (\n <MaterialCommunityIcons name=\"microphone\" size={42} color={theme.accentInk} />\n )}\n </View>\n <Text style={[styles.startLabel, { color: theme.ink }]}>Start dictating</Text>\n <Text style={[styles.startHint, { color: theme.muted }]}>\n {recordsInBackground\n ? 'Keeps recording if you lock the phone or switch apps.'\n : 'Keep Yantra open while you speak.'}\n </Text>\n </Tap>\n );\n}\n\ntype Notice = { icon: 'pause-circle-outline' | 'microphone-off'; text: string };\n\n/** Why the recording is not taking down the visit right now, if it is not. */\nfunction recordingNotice(pauseReason: PauseReason, noInput: boolean): Notice | null {\n if (pauseReason === 'background') {\n return {\n icon: 'pause-circle-outline',\n text: 'Paused because Yantra left the screen. Resume to keep dictating.',\n };\n }\n if (pauseReason === 'interrupted') {\n return {\n icon: 'pause-circle-outline',\n text: 'Paused for a call or another app using the microphone. Resume to keep dictating.',\n };\n }\n if (pauseReason === 'limit') {\n return {\n icon: 'pause-circle-outline',\n text: 'An hour is the most one dictation holds. Tap Done to write the note.',\n };\n }\n if (noInput) {\n return {\n icon: 'microphone-off',\n text: 'No sound is reaching the microphone. Check that nothing covers it and no other app is using it.',\n };\n }\n return null;\n}\n\nexport function RecordingPanel({\n theme,\n phase,\n pauseReason,\n elapsedMs,\n level,\n noInput,\n onPause,\n onResume,\n onDone,\n onDiscard,\n}: {\n theme: DictateTheme;\n phase: Exclude<RecorderPhase, 'idle'>;\n pauseReason: PauseReason;\n elapsedMs: number;\n level: number;\n /** The microphone has carried no signal for a few seconds of recording. */\n noInput: boolean;\n onPause: () => void;\n onResume: () => void;\n onDone: () => void;\n onDiscard: () => void;\n}) {\n const live = phase === 'recording';\n const notice = recordingNotice(pauseReason, live && noInput);\n return (\n <View style={styles.recording}>\n <View style={[styles.status, { backgroundColor: live ? theme.recSoft : theme.chip }]}>\n <View style={[styles.dot, { backgroundColor: live ? theme.rec : theme.faint }]} />\n <Text style={[styles.statusText, { color: live ? theme.rec : theme.muted }]}>\n {live ? 'Recording' : 'Paused'}\n </Text>\n </View>\n <Text\n style={[styles.timer, { color: theme.ink }]}\n accessibilityLabel={`Elapsed ${formatDuration(elapsedMs)}`}\n >\n {formatDuration(elapsedMs)}\n </Text>\n <LevelBars level={level} active={live} theme={theme} />\n {notice ? (\n <View style={[styles.notice, { backgroundColor: theme.warnSoft, borderColor: theme.warnBorder }]}>\n <MaterialCommunityIcons name={notice.icon} size={18} color={theme.warnInk} />\n <Text style={[styles.noticeText, { color: theme.warnInk }]}>{notice.text}</Text>\n </View>\n ) : null}\n <View style={styles.controls}>\n <Tap\n onPress={onDiscard}\n accessibilityRole=\"button\"\n accessibilityLabel=\"Discard this dictation\"\n style={[styles.side, { backgroundColor: theme.chip }]}\n pressedStyle={DIM}\n >\n <MaterialCommunityIcons name=\"trash-can-outline\" size={24} color={theme.muted} />\n </Tap>\n <Tap\n onPress={live ? onPause : onResume}\n disabled={pauseReason === 'limit'}\n accessibilityRole=\"button\"\n accessibilityLabel={live ? 'Pause' : 'Resume'}\n style={[\n styles.main,\n {\n backgroundColor: live ? theme.rec : theme.accent,\n opacity: pauseReason === 'limit' ? 0.4 : 1,\n },\n ]}\n pressedStyle={DIM}\n >\n <MaterialCommunityIcons name={live ? 'pause' : 'microphone'} size={34} color=\"#ffffff\" />\n </Tap>\n <Tap\n onPress={onDone}\n disabled={elapsedMs < 1000}\n accessibilityRole=\"button\"\n accessibilityLabel=\"Done, write the note\"\n style={[styles.side, { backgroundColor: theme.accentSoft, opacity: elapsedMs < 1000 ? 0.4 : 1 }]}\n pressedStyle={DIM}\n >\n <MaterialCommunityIcons name=\"check\" size={26} color={theme.accent} />\n </Tap>\n </View>\n <View style={styles.legend}>\n <Text style={[styles.legendText, { color: theme.faint }]}>Discard</Text>\n <Text style={[styles.legendText, { color: theme.faint }]}>{live ? 'Pause' : 'Resume'}</Text>\n <Text style={[styles.legendText, { color: theme.faint }]}>Done</Text>\n </View>\n </View>\n );\n}\n\nconst styles = StyleSheet.create({\n start: { alignItems: 'center', paddingVertical: 28 },\n micButton: {\n width: 104,\n height: 104,\n borderRadius: 52,\n alignItems: 'center',\n justifyContent: 'center',\n shadowOpacity: 0.28,\n shadowRadius: 16,\n shadowOffset: { width: 0, height: 8 },\n elevation: 6,\n },\n startLabel: { marginTop: 18, fontSize: 17, fontWeight: '600' },\n startHint: { marginTop: 6, fontSize: 13 },\n recording: { alignItems: 'center', paddingTop: 12, paddingBottom: 8 },\n status: {\n flexDirection: 'row',\n alignItems: 'center',\n paddingHorizontal: 12,\n paddingVertical: 6,\n borderRadius: 999,\n },\n dot: { width: 8, height: 8, borderRadius: 4, marginRight: 8 },\n statusText: { fontSize: 13, fontWeight: '600' },\n timer: { marginTop: 18, fontSize: 56, fontWeight: '300', fontVariant: ['tabular-nums'], letterSpacing: 1 },\n bars: { marginTop: 18, height: 56, flexDirection: 'row', alignItems: 'center', gap: 3 },\n bar: { width: 4, borderRadius: 2 },\n notice: {\n marginTop: 18,\n marginHorizontal: 8,\n flexDirection: 'row',\n alignItems: 'center',\n gap: 8,\n paddingHorizontal: 12,\n paddingVertical: 10,\n borderRadius: 12,\n borderWidth: StyleSheet.hairlineWidth,\n },\n noticeText: { flex: 1, fontSize: 13, lineHeight: 18 },\n controls: { marginTop: 28, flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: 36 },\n side: { width: 56, height: 56, borderRadius: 28, alignItems: 'center', justifyContent: 'center' },\n main: { width: 80, height: 80, borderRadius: 40, alignItems: 'center', justifyContent: 'center' },\n legend: { marginTop: 8, flexDirection: 'row', justifyContent: 'center', gap: 36 },\n legendText: { width: 68, textAlign: 'center', fontSize: 12 },\n});\n"],"names":[],"mappings":"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"}
@@ -26,7 +26,10 @@ class DictateError extends Error {
26
26
  }
27
27
  const asRecord = (v) => v && typeof v === "object" && !Array.isArray(v) ? v : null;
28
28
  const str = (v) => typeof v === "string" ? v.trim() : "";
29
- const strings = (v) => (Array.isArray(v) ? v : typeof v === "string" && v.trim() ? [v] : []).map(str).filter(Boolean);
29
+ const strings = (v) => {
30
+ if (Array.isArray(v)) return v.map(str).filter(Boolean);
31
+ return str(v) ? [str(v)] : [];
32
+ };
30
33
  const ACTIONS = ["start", "continue", "change", "stop", "mentioned"];
31
34
  function unwrapNote(raw) {
32
35
  var _a;
@@ -72,7 +75,9 @@ function dictateErrorText(err) {
72
75
  if (err instanceof DictateError && err.kind === "empty") return "No speech was heard in the recording. Try again closer to the phone.";
73
76
  if (/network request failed|offline|not signed in/i.test(message)) return "You are offline or signed out. The recording is saved; try again when you are back online.";
74
77
  if (/no speech|has no speech/i.test(message)) return "No speech was heard in the recording. Try again closer to the phone.";
75
- if (/not found in registry|unknown action|CLINICAL_NOTE/i.test(message)) return "Dictate is not available on this workspace yet. The recording is saved.";
78
+ if (/action '[^']*' not found in connector|not found in registry|is not installed|auto-install (failed|did not complete)/i.test(message)) return "Dictate is not available on this workspace yet. The recording is saved.";
79
+ if (/temporarily unavailable|problem on our side|all LLM providers failed|transcription failed|HTTP 5\d\d/i.test(message)) return "Yantra could not write the note right now. This is a problem on our side; the recording is saved, so try again in a few minutes.";
80
+ if (/declined by the content safety rules/i.test(message)) return "The note could not be written from this dictation. The recording is saved.";
76
81
  if (/timed? ?out|abort/i.test(message)) return "Writing the note took too long. The recording is saved; try again.";
77
82
  return "The note could not be written. The recording is saved; try again.";
78
83
  }
@@ -1 +1 @@
1
- {"version":3,"file":"dictateClient.js","sources":["../../../src/features/dictate/dictateClient.ts"],"sourcesContent":["/**\n * Recording -> SOAP note. The recording is uploaded first and handed to the\n * connector as a URL (connector params travel as one CLI argument, capped at\n * 128 KB - audio never rides inline), then YANTRA_MEETINGS_CLINICAL_NOTE\n * transcribes it with a medical speech model and writes the note.\n */\nimport type { RemoteConnectorExec } from '../canvas/videoEditor/remoteMediaClient';\nimport type { ClinicalNote, MedicationAction } from './types';\n\nexport const DICTATE_CONNECTOR = 'yantra-meetings';\nexport const CLINICAL_NOTE_ACTION = 'YANTRA_MEETINGS_CLINICAL_NOTE';\n/** Transcription plus the note runs ~10 s for two minutes of speech; an hour's dictation stays well inside this. */\nexport const NOTE_TIMEOUT_MS = 10 * 60_000;\n\nexport type DictateStage = 'uploading' | 'writing';\n\nexport interface DictateDeps {\n exec: RemoteConnectorExec;\n upload: (localUri: string) => Promise<string>;\n}\n\nexport interface WrittenNote {\n note: ClinicalNote;\n transcript: string;\n model?: string;\n}\n\nexport class DictateError extends Error {\n constructor(\n readonly kind: 'offline' | 'unavailable' | 'empty' | 'failed',\n message: string,\n ) {\n super(message);\n this.name = 'DictateError';\n }\n}\n\ntype Json = Record<string, unknown>;\nconst asRecord = (v: unknown): Json | null => (v && typeof v === 'object' && !Array.isArray(v) ? (v as Json) : null);\nconst str = (v: unknown): string => (typeof v === 'string' ? v.trim() : '');\nconst strings = (v: unknown): string[] =>\n (Array.isArray(v) ? v : typeof v === 'string' && v.trim() ? [v] : []).map(str).filter(Boolean);\nconst ACTIONS: MedicationAction[] = ['start', 'continue', 'change', 'stop', 'mentioned'];\n\n/** The note whatever wraps it ({ data: { note } }, { result: ... }), shaped defensively. */\nexport function unwrapNote(raw: unknown): WrittenNote {\n let layer = asRecord(raw);\n const seen: Json[] = [];\n for (let depth = 0; layer && depth < 6; depth += 1) {\n seen.push(layer);\n const note = asRecord(layer.note);\n if (note) {\n const transcript = asRecord(layer.transcript);\n const meds = Array.isArray(note.medications) ? note.medications : [];\n return {\n note: {\n title: str(note.title) || 'Clinical note',\n subjective: strings(note.subjective),\n objective: strings(note.objective),\n assessment: strings(note.assessment),\n plan: strings(note.plan),\n medications: meds\n .map(asRecord)\n .filter((m): m is Json => !!m && !!str(m.name))\n .map((m) => {\n const action = str(m.action).toLowerCase() as MedicationAction;\n return {\n name: str(m.name),\n dose: str(m.dose),\n route: str(m.route),\n frequency: str(m.frequency),\n action: ACTIONS.includes(action) ? action : 'mentioned',\n };\n }),\n follow_up: str(note.follow_up),\n to_verify: strings(note.to_verify),\n },\n transcript: str(transcript?.text),\n model: str(layer.model) || undefined,\n };\n }\n layer = asRecord(layer.data) ?? asRecord(layer.result);\n }\n const reason = seen\n .flatMap((l) => [l.error, l.message, l.stderr])\n .find((v): v is string => typeof v === 'string' && v.trim().length > 0);\n throw new DictateError('failed', reason ?? 'The note came back empty.');\n}\n\n/** A message a clinician can act on, never a stack trace. */\nexport function dictateErrorText(err: unknown): string {\n const message = err instanceof Error ? err.message : String(err);\n if (err instanceof DictateError && err.kind === 'empty') return 'No speech was heard in the recording. Try again closer to the phone.';\n if (/network request failed|offline|not signed in/i.test(message)) return 'You are offline or signed out. The recording is saved; try again when you are back online.';\n if (/no speech|has no speech/i.test(message)) return 'No speech was heard in the recording. Try again closer to the phone.';\n if (/not found in registry|unknown action|CLINICAL_NOTE/i.test(message)) return 'Dictate is not available on this workspace yet. The recording is saved.';\n if (/timed? ?out|abort/i.test(message)) return 'Writing the note took too long. The recording is saved; try again.';\n return 'The note could not be written. The recording is saved; try again.';\n}\n\nexport async function writeNoteFromRecording(\n audioUri: string,\n deps: DictateDeps,\n opts: { keyterms?: string[]; context?: string } = {},\n onStage: (stage: DictateStage) => void = () => undefined,\n): Promise<WrittenNote> {\n onStage('uploading');\n const inputUrl = await deps.upload(audioUri);\n onStage('writing');\n const raw = await deps.exec(\n DICTATE_CONNECTOR,\n CLINICAL_NOTE_ACTION,\n {\n input_url: inputUrl,\n ...(opts.keyterms?.length ? { keyterms: opts.keyterms } : {}),\n ...(opts.context?.trim() ? { context: opts.context.trim() } : {}),\n },\n { timeoutMs: NOTE_TIMEOUT_MS },\n );\n const written = unwrapNote(raw);\n const n = written.note;\n const empty = !written.transcript && ![n.subjective, n.objective, n.assessment, n.plan].some((s) => s.length);\n if (empty) throw new DictateError('empty', 'The dictation has no speech.');\n return written;\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"dictateClient.js","sources":["../../../src/features/dictate/dictateClient.ts"],"sourcesContent":["/**\n * Recording -> SOAP note. The recording is uploaded first and handed to the\n * connector as a URL (connector params travel as one CLI argument, capped at\n * 128 KB - audio never rides inline), then YANTRA_MEETINGS_CLINICAL_NOTE\n * transcribes it with a medical speech model and writes the note.\n */\nimport type { RemoteConnectorExec } from '../canvas/videoEditor/remoteMediaClient';\nimport type { ClinicalNote, MedicationAction } from './types';\n\nexport const DICTATE_CONNECTOR = 'yantra-meetings';\nexport const CLINICAL_NOTE_ACTION = 'YANTRA_MEETINGS_CLINICAL_NOTE';\n/** Transcription plus the note runs ~10 s for two minutes of speech; an hour's dictation stays well inside this. */\nexport const NOTE_TIMEOUT_MS = 10 * 60_000;\n\nexport type DictateStage = 'uploading' | 'writing';\n\nexport interface DictateDeps {\n exec: RemoteConnectorExec;\n upload: (localUri: string) => Promise<string>;\n}\n\nexport interface WrittenNote {\n note: ClinicalNote;\n transcript: string;\n model?: string;\n}\n\nexport class DictateError extends Error {\n constructor(\n readonly kind: 'offline' | 'unavailable' | 'empty' | 'failed',\n message: string,\n ) {\n super(message);\n this.name = 'DictateError';\n }\n}\n\ntype Json = Record<string, unknown>;\nconst asRecord = (v: unknown): Json | null => (v && typeof v === 'object' && !Array.isArray(v) ? (v as Json) : null);\nconst str = (v: unknown): string => (typeof v === 'string' ? v.trim() : '');\nconst strings = (v: unknown): string[] => {\n if (Array.isArray(v)) return v.map(str).filter(Boolean);\n return str(v) ? [str(v)] : [];\n};\nconst ACTIONS: MedicationAction[] = ['start', 'continue', 'change', 'stop', 'mentioned'];\n\n/** The note whatever wraps it ({ data: { note } }, { result: ... }), shaped defensively. */\nexport function unwrapNote(raw: unknown): WrittenNote {\n let layer = asRecord(raw);\n const seen: Json[] = [];\n for (let depth = 0; layer && depth < 6; depth += 1) {\n seen.push(layer);\n const note = asRecord(layer.note);\n if (note) {\n const transcript = asRecord(layer.transcript);\n const meds = Array.isArray(note.medications) ? note.medications : [];\n return {\n note: {\n title: str(note.title) || 'Clinical note',\n subjective: strings(note.subjective),\n objective: strings(note.objective),\n assessment: strings(note.assessment),\n plan: strings(note.plan),\n medications: meds\n .map(asRecord)\n .filter((m): m is Json => !!m && !!str(m.name))\n .map((m) => {\n const action = str(m.action).toLowerCase() as MedicationAction;\n return {\n name: str(m.name),\n dose: str(m.dose),\n route: str(m.route),\n frequency: str(m.frequency),\n action: ACTIONS.includes(action) ? action : 'mentioned',\n };\n }),\n follow_up: str(note.follow_up),\n to_verify: strings(note.to_verify),\n },\n transcript: str(transcript?.text),\n model: str(layer.model) || undefined,\n };\n }\n layer = asRecord(layer.data) ?? asRecord(layer.result);\n }\n const reason = seen\n .flatMap((l) => [l.error, l.message, l.stderr])\n .find((v): v is string => typeof v === 'string' && v.trim().length > 0);\n throw new DictateError('failed', reason ?? 'The note came back empty.');\n}\n\n/** A message a clinician can act on, never a stack trace. */\nexport function dictateErrorText(err: unknown): string {\n const message = err instanceof Error ? err.message : String(err);\n if (err instanceof DictateError && err.kind === 'empty')\n return 'No speech was heard in the recording. Try again closer to the phone.';\n if (/network request failed|offline|not signed in/i.test(message))\n return 'You are offline or signed out. The recording is saved; try again when you are back online.';\n if (/no speech|has no speech/i.test(message))\n return 'No speech was heard in the recording. Try again closer to the phone.';\n // Every exec failure starts with the action's name (\"YANTRA_MEETINGS_CLINICAL_NOTE failed: ...\"), so\n // only cdecli's own words for a missing connector or action mean Dictate is not set up here.\n if (\n /action '[^']*' not found in connector|not found in registry|is not installed|auto-install (failed|did not complete)/i.test(\n message,\n )\n )\n return 'Dictate is not available on this workspace yet. The recording is saved.';\n // Ours, not theirs: the service behind the note (the connector's neutral \"temporarily unavailable\",\n // or, from an older connector, its raw \"all LLM providers failed\"), or the agent itself failing.\n if (\n /temporarily unavailable|problem on our side|all LLM providers failed|transcription failed|HTTP 5\\d\\d/i.test(\n message,\n )\n )\n return 'Yantra could not write the note right now. This is a problem on our side; the recording is saved, so try again in a few minutes.';\n if (/declined by the content safety rules/i.test(message))\n return 'The note could not be written from this dictation. The recording is saved.';\n if (/timed? ?out|abort/i.test(message)) return 'Writing the note took too long. The recording is saved; try again.';\n return 'The note could not be written. The recording is saved; try again.';\n}\n\nexport async function writeNoteFromRecording(\n audioUri: string,\n deps: DictateDeps,\n opts: { keyterms?: string[]; context?: string } = {},\n onStage: (stage: DictateStage) => void = () => undefined,\n): Promise<WrittenNote> {\n onStage('uploading');\n const inputUrl = await deps.upload(audioUri);\n onStage('writing');\n const raw = await deps.exec(\n DICTATE_CONNECTOR,\n CLINICAL_NOTE_ACTION,\n {\n input_url: inputUrl,\n ...(opts.keyterms?.length ? { keyterms: opts.keyterms } : {}),\n ...(opts.context?.trim() ? { context: opts.context.trim() } : {}),\n },\n { timeoutMs: NOTE_TIMEOUT_MS },\n );\n const written = unwrapNote(raw);\n const n = written.note;\n const empty = !written.transcript && ![n.subjective, n.objective, n.assessment, n.plan].some((s) => s.length);\n if (empty) throw new DictateError('empty', 'The dictation has no speech.');\n return written;\n}\n"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"dictationStore.js","sources":["../../../src/features/dictate/dictationStore.ts"],"sourcesContent":["/**\n * Dictations live on this device only: an index file plus, until its note is\n * written, the recording itself. A recording is moved out of the recorder's\n * cache the moment it stops, so a failed upload or a closed app never loses\n * it, and it is deleted as soon as the note exists - the less clinical audio\n * a phone keeps, the better.\n */\nimport type { DictationRecord } from './types';\n\n/** The slice of expo-file-system (legacy API) this needs; injectable for tests. */\nexport interface DictationFs {\n documentDirectory: string | null;\n getInfoAsync(uri: string): Promise<{ exists: boolean }>;\n makeDirectoryAsync(uri: string, options?: { intermediates?: boolean }): Promise<void>;\n readAsStringAsync(uri: string): Promise<string>;\n writeAsStringAsync(uri: string, contents: string): Promise<void>;\n moveAsync(options: { from: string; to: string }): Promise<void>;\n deleteAsync(uri: string, options?: { idempotent?: boolean }): Promise<void>;\n}\n\nfunction loadFs(): DictationFs {\n // Lazy: the module is native, and a test or a build without it must still load this file.\n return require('expo-file-system') as DictationFs;\n}\n\n/** Keeps the list from growing without bound; the oldest go first. */\nconst MAX_RECORDS = 200;\n\nexport interface DictationStore {\n list(): Promise<DictationRecord[]>;\n /** Moves a finished recording into the store and records it as saved. */\n keepRecording(tempUri: string, durationMs: number): Promise<DictationRecord>;\n update(id: string, patch: Partial<DictationRecord>): Promise<DictationRecord | null>;\n /** The note exists: the recording is no longer needed. */\n dropAudio(id: string): Promise<void>;\n remove(id: string): Promise<void>;\n /** A recording the clinician threw away before it was kept. */\n discardRecording(tempUri: string): Promise<void>;\n}\n\nexport function createDictationStore(fs: DictationFs = loadFs(), now: () => Date = () => new Date()): DictationStore {\n const root = `${fs.documentDirectory ?? ''}dictations/`;\n const indexUri = `${root}index.json`;\n // One write at a time: two quick updates must not race the index file.\n let queue: Promise<unknown> = Promise.resolve();\n const serial = <T>(task: () => Promise<T>): Promise<T> => {\n const next = queue.then(task, task);\n queue = next.catch(() => undefined);\n return next;\n };\n\n const ensureDir = async () => {\n if (!(await fs.getInfoAsync(root)).exists) await fs.makeDirectoryAsync(root, { intermediates: true });\n };\n const read = async (): Promise<DictationRecord[]> => {\n try {\n if (!(await fs.getInfoAsync(indexUri)).exists) return [];\n const parsed = JSON.parse(await fs.readAsStringAsync(indexUri));\n return Array.isArray(parsed) ? (parsed as DictationRecord[]) : [];\n } catch {\n return []; // a damaged index must not lock the clinician out of recording\n }\n };\n const write = async (records: DictationRecord[]) => {\n await ensureDir();\n await fs.writeAsStringAsync(indexUri, JSON.stringify(records.slice(0, MAX_RECORDS)));\n };\n\n return {\n list: () => serial(read),\n keepRecording: (tempUri, durationMs) =>\n serial(async () => {\n await ensureDir();\n const at = now();\n const id = `d${at.getTime().toString(36)}${Math.random().toString(36).slice(2, 6)}`;\n const ext = /\\.[a-z0-9]{2,4}$/i.exec(tempUri)?.[0] ?? '.m4a';\n const audioUri = `${root}${id}${ext}`;\n await fs.moveAsync({ from: tempUri, to: audioUri });\n const record: DictationRecord = { id, createdAt: at.toISOString(), durationMs, status: 'saved', audioUri };\n await write([record, ...(await read())]);\n return record;\n }),\n update: (id, patch) =>\n serial(async () => {\n const records = await read();\n const i = records.findIndex((r) => r.id === id);\n if (i < 0) return null;\n records[i] = { ...records[i], ...patch, id };\n await write(records);\n return records[i];\n }),\n dropAudio: (id) =>\n serial(async () => {\n const records = await read();\n const record = records.find((r) => r.id === id);\n if (!record?.audioUri) return;\n await fs.deleteAsync(record.audioUri, { idempotent: true }).catch(() => undefined);\n delete record.audioUri;\n await write(records);\n }),\n remove: (id) =>\n serial(async () => {\n const records = await read();\n const record = records.find((r) => r.id === id);\n if (record?.audioUri) await fs.deleteAsync(record.audioUri, { idempotent: true }).catch(() => undefined);\n await write(records.filter((r) => r.id !== id));\n }),\n discardRecording: (tempUri) => fs.deleteAsync(tempUri, { idempotent: true }).catch(() => undefined),\n };\n}\n"],"names":["_a"],"mappings":"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"}
1
+ {"version":3,"file":"dictationStore.js","sources":["../../../src/features/dictate/dictationStore.ts"],"sourcesContent":["/**\n * Dictations live on this device only: an index file plus, until its note is\n * written, the recording itself. A recording is moved out of the recorder's\n * cache the moment it stops, so a failed upload or a closed app never loses\n * it, and it is deleted as soon as the note exists - the less clinical audio\n * a phone keeps, the better.\n */\nimport type { DictationRecord } from './types';\n\n/** The slice of expo-file-system (legacy API) this needs; injectable for tests. */\nexport interface DictationFs {\n documentDirectory: string | null;\n getInfoAsync(uri: string): Promise<{ exists: boolean }>;\n makeDirectoryAsync(uri: string, options?: { intermediates?: boolean }): Promise<void>;\n readAsStringAsync(uri: string): Promise<string>;\n writeAsStringAsync(uri: string, contents: string): Promise<void>;\n moveAsync(options: { from: string; to: string }): Promise<void>;\n deleteAsync(uri: string, options?: { idempotent?: boolean }): Promise<void>;\n}\n\nfunction loadFs(): DictationFs {\n // Lazy: the module is native, and a test or a build without it must still load this file.\n // eslint-disable-next-line @typescript-eslint/no-var-requires, global-require\n return require('expo-file-system') as DictationFs;\n}\n\n/** Keeps the list from growing without bound; the oldest go first. */\nconst MAX_RECORDS = 200;\n\nexport interface DictationStore {\n list(): Promise<DictationRecord[]>;\n /** Moves a finished recording into the store and records it as saved. */\n keepRecording(tempUri: string, durationMs: number): Promise<DictationRecord>;\n update(id: string, patch: Partial<DictationRecord>): Promise<DictationRecord | null>;\n /** The note exists: the recording is no longer needed. */\n dropAudio(id: string): Promise<void>;\n remove(id: string): Promise<void>;\n /** A recording the clinician threw away before it was kept. */\n discardRecording(tempUri: string): Promise<void>;\n}\n\nexport function createDictationStore(fs: DictationFs = loadFs(), now: () => Date = () => new Date()): DictationStore {\n const root = `${fs.documentDirectory ?? ''}dictations/`;\n const indexUri = `${root}index.json`;\n // One write at a time: two quick updates must not race the index file.\n let queue: Promise<unknown> = Promise.resolve();\n const serial = <T>(task: () => Promise<T>): Promise<T> => {\n const next = queue.then(task, task);\n queue = next.catch(() => undefined);\n return next;\n };\n\n const ensureDir = async () => {\n if (!(await fs.getInfoAsync(root)).exists) await fs.makeDirectoryAsync(root, { intermediates: true });\n };\n const read = async (): Promise<DictationRecord[]> => {\n try {\n if (!(await fs.getInfoAsync(indexUri)).exists) return [];\n const parsed = JSON.parse(await fs.readAsStringAsync(indexUri));\n return Array.isArray(parsed) ? (parsed as DictationRecord[]) : [];\n } catch {\n return []; // a damaged index must not lock the clinician out of recording\n }\n };\n const write = async (records: DictationRecord[]) => {\n await ensureDir();\n await fs.writeAsStringAsync(indexUri, JSON.stringify(records.slice(0, MAX_RECORDS)));\n };\n\n return {\n list: () => serial(read),\n keepRecording: (tempUri, durationMs) =>\n serial(async () => {\n await ensureDir();\n const at = now();\n const id = `d${at.getTime().toString(36)}${Math.random().toString(36).slice(2, 6)}`;\n const ext = /\\.[a-z0-9]{2,4}$/i.exec(tempUri)?.[0] ?? '.m4a';\n const audioUri = `${root}${id}${ext}`;\n await fs.moveAsync({ from: tempUri, to: audioUri });\n const record: DictationRecord = {\n id,\n createdAt: at.toISOString(),\n durationMs,\n status: 'saved',\n audioUri,\n };\n await write([record, ...(await read())]);\n return record;\n }),\n update: (id, patch) =>\n serial(async () => {\n const records = await read();\n const i = records.findIndex((r) => r.id === id);\n if (i < 0) return null;\n records[i] = { ...records[i], ...patch, id };\n await write(records);\n return records[i];\n }),\n dropAudio: (id) =>\n serial(async () => {\n const records = await read();\n const record = records.find((r) => r.id === id);\n if (!record?.audioUri) return;\n await fs.deleteAsync(record.audioUri, { idempotent: true }).catch(() => undefined);\n delete record.audioUri;\n await write(records);\n }),\n remove: (id) =>\n serial(async () => {\n const records = await read();\n const record = records.find((r) => r.id === id);\n if (record?.audioUri)\n await fs.deleteAsync(record.audioUri, { idempotent: true }).catch(() => undefined);\n await write(records.filter((r) => r.id !== id));\n }),\n discardRecording: (tempUri) => fs.deleteAsync(tempUri, { idempotent: true }).catch(() => undefined),\n };\n}\n"],"names":["_a"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import {useState,useRef,useCallback,useEffect}from'react';import {AppState}from'react-native';import {useAudioRecorder,useAudioRecorderState,setAudioModeAsync,requestRecordingPermissionsAsync,RecordingPresets,AudioQuality,IOSOutputFormat}from'expo-audio';import {deactivateKeepAwake,activateKeepAwakeAsync}from'expo-keep-awake';var __defProp = Object.defineProperty;
1
+ import {useState,useRef,useMemo,useCallback,useEffect}from'react';import {AppState}from'react-native';import {useAudioRecorder,useAudioRecorderState,setAudioModeAsync,requestRecordingPermissionsAsync,setIsAudioActiveAsync,RecordingPresets,AudioQuality,IOSOutputFormat}from'expo-audio';import {deactivateKeepAwake,activateKeepAwakeAsync}from'expo-keep-awake';import {backgroundRecordingNative,BACKGROUND_NOTICE}from'./backgroundRecording.js';var __defProp = Object.defineProperty;
2
2
  var __defProps = Object.defineProperties;
3
3
  var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
4
4
  var __getOwnPropSymbols = Object.getOwnPropertySymbols;
@@ -38,6 +38,11 @@ function levelFromMetering(db) {
38
38
  if (typeof db !== "number" || !Number.isFinite(db)) return 0;
39
39
  return Math.min(1, Math.max(0, (db + 60) / 60));
40
40
  }
41
+ const NO_SIGNAL_DB = -100;
42
+ const NO_INPUT_WARN_MS = 4e3;
43
+ function carriesSignal(db) {
44
+ return typeof db !== "number" || !Number.isFinite(db) || db > NO_SIGNAL_DB;
45
+ }
41
46
  class MicrophoneDeniedError extends Error {
42
47
  constructor() {
43
48
  super("Microphone access was declined. Allow it in Settings to dictate.");
@@ -51,13 +56,18 @@ function useDictationRecorder() {
51
56
  const [pauseReason, setPauseReason] = useState(null);
52
57
  const phaseRef = useRef("idle");
53
58
  phaseRef.current = phase;
59
+ const background = useMemo(() => backgroundRecordingNative(), []);
60
+ const inBackgroundRef = useRef(false);
54
61
  const releaseSession = useCallback(async () => {
62
+ if (inBackgroundRef.current) await (background == null ? void 0 : background.stop().catch(() => void 0));
63
+ inBackgroundRef.current = false;
55
64
  await deactivateKeepAwake(KEEP_AWAKE_TAG).catch(() => void 0);
56
65
  await setAudioModeAsync({
57
66
  allowsRecording: false,
58
- playsInSilentMode: true
67
+ playsInSilentMode: true,
68
+ shouldPlayInBackground: false
59
69
  }).catch(() => void 0);
60
- }, []);
70
+ }, [background]);
61
71
  const pause = useCallback((reason = "user") => {
62
72
  if (phaseRef.current !== "recording") return;
63
73
  recorder.pause();
@@ -67,18 +77,26 @@ function useDictationRecorder() {
67
77
  const start = useCallback(async () => {
68
78
  const permission = await requestRecordingPermissionsAsync();
69
79
  if (!permission.granted) throw new MicrophoneDeniedError();
70
- await setAudioModeAsync({
71
- allowsRecording: true,
72
- playsInSilentMode: true
73
- });
74
- await recorder.prepareToRecordAsync();
75
- recorder.record();
76
- await activateKeepAwakeAsync(KEEP_AWAKE_TAG).catch(() => void 0);
80
+ inBackgroundRef.current = (background == null ? void 0 : background.supported) ? await background.start(BACKGROUND_NOTICE.title, BACKGROUND_NOTICE.text).then(() => true, () => false) : false;
81
+ try {
82
+ await setAudioModeAsync({
83
+ allowsRecording: true,
84
+ playsInSilentMode: true,
85
+ shouldPlayInBackground: inBackgroundRef.current
86
+ });
87
+ await recorder.prepareToRecordAsync();
88
+ recorder.record();
89
+ } catch (err) {
90
+ await releaseSession();
91
+ throw err;
92
+ }
93
+ if (!inBackgroundRef.current) await activateKeepAwakeAsync(KEEP_AWAKE_TAG).catch(() => void 0);
77
94
  setPhase("recording");
78
95
  setPauseReason(null);
79
- }, [recorder]);
80
- const resume = useCallback(() => {
96
+ }, [recorder, background, releaseSession]);
97
+ const resume = useCallback(async () => {
81
98
  if (phaseRef.current !== "paused") return;
99
+ await setIsAudioActiveAsync(true).catch(() => void 0);
82
100
  recorder.record();
83
101
  setPhase("recording");
84
102
  setPauseReason(null);
@@ -97,13 +115,26 @@ function useDictationRecorder() {
97
115
  }, [recorder, releaseSession]);
98
116
  useEffect(() => {
99
117
  const sub = AppState.addEventListener("change", (next) => {
100
- if (next !== "active") pause("background");
118
+ if (next !== "active" && !inBackgroundRef.current) pause("background");
101
119
  });
102
120
  return () => sub.remove();
103
121
  }, [pause]);
122
+ useEffect(() => {
123
+ const sub = background == null ? void 0 : background.addListener("onInterruption", (event) => {
124
+ if (event.state === "began") pause("interrupted");
125
+ });
126
+ return () => sub == null ? void 0 : sub.remove();
127
+ }, [background, pause]);
104
128
  useEffect(() => {
105
129
  if (phase === "recording" && state.durationMillis >= MAX_DICTATION_MS) pause("limit");
106
130
  }, [phase, state.durationMillis, pause]);
131
+ const signalAtRef = useRef(0);
132
+ useEffect(() => {
133
+ if (phase !== "recording" || carriesSignal(state.metering) || state.durationMillis < signalAtRef.current) {
134
+ signalAtRef.current = state.durationMillis;
135
+ }
136
+ }, [phase, state.durationMillis, state.metering]);
137
+ const noInput = phase === "recording" && !carriesSignal(state.metering) && state.durationMillis - signalAtRef.current >= NO_INPUT_WARN_MS;
107
138
  useEffect(() => () => {
108
139
  if (phaseRef.current !== "idle") void recorder.stop().catch(() => void 0);
109
140
  void releaseSession();
@@ -113,9 +144,12 @@ function useDictationRecorder() {
113
144
  pauseReason,
114
145
  elapsedMs: state.durationMillis,
115
146
  level: phase === "recording" ? levelFromMetering(state.metering) : 0,
147
+ noInput,
148
+ /** This binary keeps a dictation recording with the phone locked or another app open. */
149
+ recordsInBackground: (background == null ? void 0 : background.supported) === true,
116
150
  start,
117
151
  pause: () => pause("user"),
118
152
  resume,
119
153
  finish
120
154
  };
121
- }export{DICTATION_RECORDING,MAX_DICTATION_MS,MicrophoneDeniedError,levelFromMetering,useDictationRecorder};//# sourceMappingURL=useDictationRecorder.js.map
155
+ }export{DICTATION_RECORDING,MAX_DICTATION_MS,MicrophoneDeniedError,NO_INPUT_WARN_MS,carriesSignal,levelFromMetering,useDictationRecorder};//# sourceMappingURL=useDictationRecorder.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"useDictationRecorder.js","sources":["../../../src/features/dictate/useDictationRecorder.ts"],"sourcesContent":["/**\n * The dictation recorder: mono AAC at 64 kbps (speech, about 0.5 MB a minute),\n * pause and resume, a live level, and the screen kept awake while a dictation\n * is open.\n *\n * This build has no background audio mode, so iOS stops the microphone when\n * the app leaves the screen. Rather than lose audio silently, the recorder\n * pauses itself when the app goes to the background and says so; the\n * clinician resumes when back. Recording with the screen locked needs a\n * native build with `UIBackgroundModes: audio` (and an Android microphone\n * foreground service).\n */\nimport { useCallback, useEffect, useRef, useState } from 'react';\nimport { AppState } from 'react-native';\nimport {\n AudioQuality,\n IOSOutputFormat,\n RecordingPresets,\n requestRecordingPermissionsAsync,\n setAudioModeAsync,\n useAudioRecorder,\n useAudioRecorderState,\n type RecordingOptions,\n} from 'expo-audio';\nimport { activateKeepAwakeAsync, deactivateKeepAwake } from 'expo-keep-awake';\n\nexport const DICTATION_RECORDING: RecordingOptions = {\n ...RecordingPresets.HIGH_QUALITY,\n extension: '.m4a',\n sampleRate: 44100,\n numberOfChannels: 1,\n bitRate: 64000,\n isMeteringEnabled: true,\n ios: {\n ...RecordingPresets.HIGH_QUALITY.ios,\n outputFormat: IOSOutputFormat.MPEG4AAC,\n audioQuality: AudioQuality.HIGH,\n },\n android: {\n ...RecordingPresets.HIGH_QUALITY.android,\n outputFormat: 'mpeg4',\n audioEncoder: 'aac',\n },\n};\n\n/** One visit is well under this; past it the recording pauses and asks to be finished. */\nexport const MAX_DICTATION_MS = 60 * 60_000;\nconst KEEP_AWAKE_TAG = 'yantra-dictate';\n\nexport type RecorderPhase = 'idle' | 'recording' | 'paused';\nexport type PauseReason = 'user' | 'background' | 'limit' | null;\n\n/** -60 dB (silence) .. 0 dB (loud) mapped to 0..1. */\nexport function levelFromMetering(db: number | undefined): number {\n if (typeof db !== 'number' || !Number.isFinite(db)) return 0;\n return Math.min(1, Math.max(0, (db + 60) / 60));\n}\n\nexport class MicrophoneDeniedError extends Error {\n constructor() {\n super('Microphone access was declined. Allow it in Settings to dictate.');\n this.name = 'MicrophoneDeniedError';\n }\n}\n\nexport function useDictationRecorder() {\n const recorder = useAudioRecorder(DICTATION_RECORDING);\n const state = useAudioRecorderState(recorder, 100);\n const [phase, setPhase] = useState<RecorderPhase>('idle');\n const [pauseReason, setPauseReason] = useState<PauseReason>(null);\n const phaseRef = useRef<RecorderPhase>('idle');\n phaseRef.current = phase;\n\n const releaseSession = useCallback(async () => {\n await deactivateKeepAwake(KEEP_AWAKE_TAG).catch(() => undefined);\n await setAudioModeAsync({ allowsRecording: false, playsInSilentMode: true }).catch(() => undefined);\n }, []);\n\n const pause = useCallback(\n (reason: Exclude<PauseReason, null> = 'user') => {\n if (phaseRef.current !== 'recording') return;\n recorder.pause();\n setPhase('paused');\n setPauseReason(reason);\n },\n [recorder],\n );\n\n const start = useCallback(async () => {\n const permission = await requestRecordingPermissionsAsync();\n if (!permission.granted) throw new MicrophoneDeniedError();\n await setAudioModeAsync({ allowsRecording: true, playsInSilentMode: true });\n await recorder.prepareToRecordAsync();\n recorder.record();\n await activateKeepAwakeAsync(KEEP_AWAKE_TAG).catch(() => undefined);\n setPhase('recording');\n setPauseReason(null);\n }, [recorder]);\n\n const resume = useCallback(() => {\n if (phaseRef.current !== 'paused') return;\n recorder.record();\n setPhase('recording');\n setPauseReason(null);\n }, [recorder]);\n\n /** Ends the recording; resolves to its file (in the recorder's cache - move it before anything else). */\n const finish = useCallback(async (): Promise<{ uri: string; durationMs: number } | null> => {\n if (phaseRef.current === 'idle') return null;\n const durationMs = recorder.getStatus().durationMillis;\n await recorder.stop();\n setPhase('idle');\n setPauseReason(null);\n await releaseSession();\n return recorder.uri ? { uri: recorder.uri, durationMs } : null;\n }, [recorder, releaseSession]);\n\n // The microphone stops when the app leaves the screen: pause, and say so.\n useEffect(() => {\n const sub = AppState.addEventListener('change', (next) => {\n if (next !== 'active') pause('background');\n });\n return () => sub.remove();\n }, [pause]);\n\n useEffect(() => {\n if (phase === 'recording' && state.durationMillis >= MAX_DICTATION_MS) pause('limit');\n }, [phase, state.durationMillis, pause]);\n\n // Leaving the screen mid-dictation stops the microphone and lets the screen sleep.\n useEffect(\n () => () => {\n if (phaseRef.current !== 'idle') void recorder.stop().catch(() => undefined);\n void releaseSession();\n },\n [recorder, releaseSession],\n );\n\n return {\n phase,\n pauseReason,\n elapsedMs: state.durationMillis,\n level: phase === 'recording' ? levelFromMetering(state.metering) : 0,\n start,\n pause: () => pause('user'),\n resume,\n finish,\n };\n}\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;AAgBa,MAAA,mBAAA,GAAwC,aAChD,CAAA,cAAA,CAAA,EAAA,EAAA,gBAAA,CAAiB,YAD+B,CAAA,EAAA;AAAA,EAEnD,SAAW,EAAA,MAAA;AAAA,EACX,UAAY,EAAA,KAAA;AAAA,EACZ,gBAAkB,EAAA,CAAA;AAAA,EAClB,OAAS,EAAA,IAAA;AAAA,EACT,iBAAmB,EAAA,IAAA;AAAA,EACnB,GAAK,EAAA,aAAA,CAAA,cAAA,CAAA,EAAA,EACA,gBAAiB,CAAA,YAAA,CAAa,GAD9B,CAAA,EAAA;AAAA,IAEH,cAAc,eAAgB,CAAA,QAAA;AAAA,IAC9B,cAAc,YAAa,CAAA;AAAA,GAC7B,CAAA;AAAA,EACA,OAAS,EAAA,aAAA,CAAA,cAAA,CAAA,EAAA,EACJ,gBAAiB,CAAA,YAAA,CAAa,OAD1B,CAAA,EAAA;AAAA,IAEP,YAAc,EAAA,OAAA;AAAA,IACd,YAAc,EAAA;AAAA,GAChB;AACF,CAAA;AAGO,MAAM,mBAAmB,EAAK,GAAA;AACrC,MAAM,cAAiB,GAAA,gBAAA;AAKhB,SAAS,kBAAkB,EAAgC,EAAA;AAChE,EAAI,IAAA,OAAO,OAAO,QAAY,IAAA,CAAC,OAAO,QAAS,CAAA,EAAE,GAAU,OAAA,CAAA;AAC3D,EAAO,OAAA,IAAA,CAAK,IAAI,CAAG,EAAA,IAAA,CAAK,IAAI,CAAI,EAAA,CAAA,EAAA,GAAK,EAAM,IAAA,EAAE,CAAC,CAAA;AAChD;AACO,MAAM,8BAA8B,KAAM,CAAA;AAAA,EAC/C,WAAc,GAAA;AACZ,IAAA,KAAA,CAAM,kEAAkE,CAAA;AACxE,IAAA,IAAA,CAAK,IAAO,GAAA,uBAAA;AAAA;AAEhB;AACO,SAAS,oBAAuB,GAAA;AACrC,EAAM,MAAA,QAAA,GAAW,iBAAiB,mBAAmB,CAAA;AACrD,EAAM,MAAA,KAAA,GAAQ,qBAAsB,CAAA,QAAA,EAAU,GAAG,CAAA;AACjD,EAAA,MAAM,CAAC,KAAA,EAAO,QAAQ,CAAA,GAAI,SAAwB,MAAM,CAAA;AACxD,EAAA,MAAM,CAAC,WAAA,EAAa,cAAc,CAAA,GAAI,SAAsB,IAAI,CAAA;AAChE,EAAM,MAAA,QAAA,GAAW,OAAsB,MAAM,CAAA;AAC7C,EAAA,QAAA,CAAS,OAAU,GAAA,KAAA;AACnB,EAAM,MAAA,cAAA,GAAiB,YAAY,YAAY;AAC7C,IAAA,MAAM,mBAAoB,CAAA,cAAc,CAAE,CAAA,KAAA,CAAM,MAAM,MAAS,CAAA;AAC/D,IAAA,MAAM,iBAAkB,CAAA;AAAA,MACtB,eAAiB,EAAA,KAAA;AAAA,MACjB,iBAAmB,EAAA;AAAA,KACpB,CAAA,CAAE,KAAM,CAAA,MAAM,MAAS,CAAA;AAAA,GAC1B,EAAG,EAAE,CAAA;AACL,EAAA,MAAM,KAAQ,GAAA,WAAA,CAAY,CAAC,MAAA,GAAqC,MAAW,KAAA;AACzE,IAAI,IAAA,QAAA,CAAS,YAAY,WAAa,EAAA;AACtC,IAAA,QAAA,CAAS,KAAM,EAAA;AACf,IAAA,QAAA,CAAS,QAAQ,CAAA;AACjB,IAAA,cAAA,CAAe,MAAM,CAAA;AAAA,GACvB,EAAG,CAAC,QAAQ,CAAC,CAAA;AACb,EAAM,MAAA,KAAA,GAAQ,YAAY,YAAY;AACpC,IAAM,MAAA,UAAA,GAAa,MAAM,gCAAiC,EAAA;AAC1D,IAAA,IAAI,CAAC,UAAA,CAAW,OAAS,EAAA,MAAM,IAAI,qBAAsB,EAAA;AACzD,IAAA,MAAM,iBAAkB,CAAA;AAAA,MACtB,eAAiB,EAAA,IAAA;AAAA,MACjB,iBAAmB,EAAA;AAAA,KACpB,CAAA;AACD,IAAA,MAAM,SAAS,oBAAqB,EAAA;AACpC,IAAA,QAAA,CAAS,MAAO,EAAA;AAChB,IAAA,MAAM,sBAAuB,CAAA,cAAc,CAAE,CAAA,KAAA,CAAM,MAAM,MAAS,CAAA;AAClE,IAAA,QAAA,CAAS,WAAW,CAAA;AACpB,IAAA,cAAA,CAAe,IAAI,CAAA;AAAA,GACrB,EAAG,CAAC,QAAQ,CAAC,CAAA;AACb,EAAM,MAAA,MAAA,GAAS,YAAY,MAAM;AAC/B,IAAI,IAAA,QAAA,CAAS,YAAY,QAAU,EAAA;AACnC,IAAA,QAAA,CAAS,MAAO,EAAA;AAChB,IAAA,QAAA,CAAS,WAAW,CAAA;AACpB,IAAA,cAAA,CAAe,IAAI,CAAA;AAAA,GACrB,EAAG,CAAC,QAAQ,CAAC,CAAA;AAGb,EAAM,MAAA,MAAA,GAAS,YAAY,YAGd;AACX,IAAI,IAAA,QAAA,CAAS,OAAY,KAAA,MAAA,EAAe,OAAA,IAAA;AACxC,IAAM,MAAA,UAAA,GAAa,QAAS,CAAA,SAAA,EAAY,CAAA,cAAA;AACxC,IAAA,MAAM,SAAS,IAAK,EAAA;AACpB,IAAA,QAAA,CAAS,MAAM,CAAA;AACf,IAAA,cAAA,CAAe,IAAI,CAAA;AACnB,IAAA,MAAM,cAAe,EAAA;AACrB,IAAA,OAAO,SAAS,GAAM,GAAA;AAAA,MACpB,KAAK,QAAS,CAAA,GAAA;AAAA,MACd;AAAA,KACE,GAAA,IAAA;AAAA,GACH,EAAA,CAAC,QAAU,EAAA,cAAc,CAAC,CAAA;AAG7B,EAAA,SAAA,CAAU,MAAM;AACd,IAAA,MAAM,GAAM,GAAA,QAAA,CAAS,gBAAiB,CAAA,QAAA,EAAU,CAAQ,IAAA,KAAA;AACtD,MAAI,IAAA,IAAA,KAAS,QAAU,EAAA,KAAA,CAAM,YAAY,CAAA;AAAA,KAC1C,CAAA;AACD,IAAO,OAAA,MAAM,IAAI,MAAO,EAAA;AAAA,GAC1B,EAAG,CAAC,KAAK,CAAC,CAAA;AACV,EAAA,SAAA,CAAU,MAAM;AACd,IAAA,IAAI,UAAU,WAAe,IAAA,KAAA,CAAM,cAAkB,IAAA,gBAAA,QAAwB,OAAO,CAAA;AAAA,KACnF,CAAC,KAAA,EAAO,KAAM,CAAA,cAAA,EAAgB,KAAK,CAAC,CAAA;AAGvC,EAAA,SAAA,CAAU,MAAM,MAAM;AACpB,IAAI,IAAA,QAAA,CAAS,YAAY,MAAQ,EAAA,KAAK,SAAS,IAAK,EAAA,CAAE,KAAM,CAAA,MAAM,MAAS,CAAA;AAC3E,IAAA,KAAK,cAAe,EAAA;AAAA,GACnB,EAAA,CAAC,QAAU,EAAA,cAAc,CAAC,CAAA;AAC7B,EAAO,OAAA;AAAA,IACL,KAAA;AAAA,IACA,WAAA;AAAA,IACA,WAAW,KAAM,CAAA,cAAA;AAAA,IACjB,OAAO,KAAU,KAAA,WAAA,GAAc,iBAAkB,CAAA,KAAA,CAAM,QAAQ,CAAI,GAAA,CAAA;AAAA,IACnE,KAAA;AAAA,IACA,KAAA,EAAO,MAAM,KAAA,CAAM,MAAM,CAAA;AAAA,IACzB,MAAA;AAAA,IACA;AAAA,GACF;AACF"}
1
+ {"version":3,"file":"useDictationRecorder.js","sources":["../../../src/features/dictate/useDictationRecorder.ts"],"sourcesContent":["/**\n * The dictation recorder: mono AAC at 64 kbps (speech, about 0.5 MB a minute),\n * pause and resume, and a live level.\n *\n * A binary with the background-recording module keeps recording when Yantra\n * leaves the screen or the phone locks, like a call (backgroundRecording.ts).\n * An older binary cannot: iOS stops the microphone there, so the recorder\n * keeps the screen awake, pauses itself when the app leaves the screen, and\n * says so. Either way, a call or another app taking the microphone pauses the\n * dictation and says why, instead of recording nothing under a running timer.\n */\nimport { useCallback, useEffect, useMemo, useRef, useState } from 'react';\nimport { AppState } from 'react-native';\nimport {\n AudioQuality,\n IOSOutputFormat,\n RecordingPresets,\n requestRecordingPermissionsAsync,\n setAudioModeAsync,\n setIsAudioActiveAsync,\n useAudioRecorder,\n useAudioRecorderState,\n type RecordingOptions,\n} from 'expo-audio';\nimport { activateKeepAwakeAsync, deactivateKeepAwake } from 'expo-keep-awake';\nimport { BACKGROUND_NOTICE, backgroundRecordingNative } from './backgroundRecording';\n\nexport const DICTATION_RECORDING: RecordingOptions = {\n ...RecordingPresets.HIGH_QUALITY,\n extension: '.m4a',\n sampleRate: 44100,\n numberOfChannels: 1,\n bitRate: 64000,\n isMeteringEnabled: true,\n ios: {\n ...RecordingPresets.HIGH_QUALITY.ios,\n outputFormat: IOSOutputFormat.MPEG4AAC,\n audioQuality: AudioQuality.HIGH,\n },\n android: {\n ...RecordingPresets.HIGH_QUALITY.android,\n outputFormat: 'mpeg4',\n audioEncoder: 'aac',\n },\n};\n\n/** One visit is well under this; past it the recording pauses and asks to be finished. */\nexport const MAX_DICTATION_MS = 60 * 60_000;\nconst KEEP_AWAKE_TAG = 'yantra-dictate';\n\nexport type RecorderPhase = 'idle' | 'recording' | 'paused';\nexport type PauseReason = 'user' | 'background' | 'interrupted' | 'limit' | null;\n\n/** -60 dB (silence) .. 0 dB (loud) mapped to 0..1. */\nexport function levelFromMetering(db: number | undefined): number {\n if (typeof db !== 'number' || !Number.isFinite(db)) return 0;\n return Math.min(1, Math.max(0, (db + 60) / 60));\n}\n\n/**\n * Below this the microphone carries no signal at all (iOS reports -120 to\n * -160 dB): it is blocked, taken by another app, or failed to start. A quiet\n * room still reads about -50 dB, so silence between sentences never trips it.\n */\nconst NO_SIGNAL_DB = -100;\n/** How long a running recording may carry no signal before the clinician is told. */\nexport const NO_INPUT_WARN_MS = 4000;\n\n/** False only for a metering reading that shows no signal; no reading at all says nothing either way. */\nexport function carriesSignal(db: number | undefined): boolean {\n return typeof db !== 'number' || !Number.isFinite(db) || db > NO_SIGNAL_DB;\n}\n\nexport class MicrophoneDeniedError extends Error {\n constructor() {\n super('Microphone access was declined. Allow it in Settings to dictate.');\n this.name = 'MicrophoneDeniedError';\n }\n}\n\nexport function useDictationRecorder() {\n const recorder = useAudioRecorder(DICTATION_RECORDING);\n const state = useAudioRecorderState(recorder, 100);\n const [phase, setPhase] = useState<RecorderPhase>('idle');\n const [pauseReason, setPauseReason] = useState<PauseReason>(null);\n const phaseRef = useRef<RecorderPhase>('idle');\n phaseRef.current = phase;\n const background = useMemo(() => backgroundRecordingNative(), []);\n /** This dictation keeps recording when Yantra leaves the screen. */\n const inBackgroundRef = useRef(false);\n\n const releaseSession = useCallback(async () => {\n if (inBackgroundRef.current) await background?.stop().catch(() => undefined);\n inBackgroundRef.current = false;\n await deactivateKeepAwake(KEEP_AWAKE_TAG).catch(() => undefined);\n // The rest of the app's audio (chat voice notes) does not play on in the background.\n await setAudioModeAsync({\n allowsRecording: false,\n playsInSilentMode: true,\n shouldPlayInBackground: false,\n }).catch(() => undefined);\n }, [background]);\n\n const pause = useCallback(\n (reason: Exclude<PauseReason, null> = 'user') => {\n if (phaseRef.current !== 'recording') return;\n recorder.pause();\n setPhase('paused');\n setPauseReason(reason);\n },\n [recorder],\n );\n\n const start = useCallback(async () => {\n const permission = await requestRecordingPermissionsAsync();\n if (!permission.granted) throw new MicrophoneDeniedError();\n // Android's microphone service has to be running before Yantra leaves the screen, and\n // may only start while it is on it: now. A refusal means pausing on leave, as before.\n inBackgroundRef.current = background?.supported\n ? await background.start(BACKGROUND_NOTICE.title, BACKGROUND_NOTICE.text).then(\n () => true,\n () => false,\n )\n : false;\n try {\n await setAudioModeAsync({\n allowsRecording: true,\n playsInSilentMode: true,\n shouldPlayInBackground: inBackgroundRef.current,\n });\n await recorder.prepareToRecordAsync();\n recorder.record();\n } catch (err) {\n await releaseSession();\n throw err;\n }\n // Without background recording the screen must stay on, or the microphone stops.\n if (!inBackgroundRef.current) await activateKeepAwakeAsync(KEEP_AWAKE_TAG).catch(() => undefined);\n setPhase('recording');\n setPauseReason(null);\n }, [recorder, background, releaseSession]);\n\n const resume = useCallback(async () => {\n if (phaseRef.current !== 'paused') return;\n // A call or another app deactivated the audio session; recording needs it back.\n await setIsAudioActiveAsync(true).catch(() => undefined);\n recorder.record();\n setPhase('recording');\n setPauseReason(null);\n }, [recorder]);\n\n /** Ends the recording; resolves to its file (in the recorder's cache - move it before anything else). */\n const finish = useCallback(async (): Promise<{ uri: string; durationMs: number } | null> => {\n if (phaseRef.current === 'idle') return null;\n const durationMs = recorder.getStatus().durationMillis;\n await recorder.stop();\n setPhase('idle');\n setPauseReason(null);\n await releaseSession();\n return recorder.uri ? { uri: recorder.uri, durationMs } : null;\n }, [recorder, releaseSession]);\n\n // Without background recording the microphone stops when the app leaves the screen: pause, and say so.\n useEffect(() => {\n const sub = AppState.addEventListener('change', (next) => {\n if (next !== 'active' && !inBackgroundRef.current) pause('background');\n });\n return () => sub.remove();\n }, [pause]);\n\n // A call or another app took the microphone. Nothing is recorded from here, though\n // expo-audio's timer would keep running: pause, and stay paused until the clinician resumes.\n useEffect(() => {\n const sub = background?.addListener('onInterruption', (event) => {\n if (event.state === 'began') pause('interrupted');\n });\n return () => sub?.remove();\n }, [background, pause]);\n\n useEffect(() => {\n if (phase === 'recording' && state.durationMillis >= MAX_DICTATION_MS) pause('limit');\n }, [phase, state.durationMillis, pause]);\n\n // Recording time at which the microphone last carried a signal. A paused or idle recorder\n // starts the count again, so Resume gets a fresh few seconds; so does a new recording,\n // whose time restarts below the last one's.\n const signalAtRef = useRef(0);\n useEffect(() => {\n if (phase !== 'recording' || carriesSignal(state.metering) || state.durationMillis < signalAtRef.current) {\n signalAtRef.current = state.durationMillis;\n }\n }, [phase, state.durationMillis, state.metering]);\n // Without this a dead microphone records a whole visit of silence, timer running.\n const noInput =\n phase === 'recording' &&\n !carriesSignal(state.metering) &&\n state.durationMillis - signalAtRef.current >= NO_INPUT_WARN_MS;\n\n // Leaving the screen mid-dictation stops the microphone and lets the screen sleep.\n useEffect(\n () => () => {\n if (phaseRef.current !== 'idle') void recorder.stop().catch(() => undefined);\n void releaseSession();\n },\n [recorder, releaseSession],\n );\n\n return {\n phase,\n pauseReason,\n elapsedMs: state.durationMillis,\n level: phase === 'recording' ? levelFromMetering(state.metering) : 0,\n noInput,\n /** This binary keeps a dictation recording with the phone locked or another app open. */\n recordsInBackground: background?.supported === true,\n start,\n pause: () => pause('user'),\n resume,\n finish,\n };\n}\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;AAgBa,MAAA,mBAAA,GAAwC,aAChD,CAAA,cAAA,CAAA,EAAA,EAAA,gBAAA,CAAiB,YAD+B,CAAA,EAAA;AAAA,EAEnD,SAAW,EAAA,MAAA;AAAA,EACX,UAAY,EAAA,KAAA;AAAA,EACZ,gBAAkB,EAAA,CAAA;AAAA,EAClB,OAAS,EAAA,IAAA;AAAA,EACT,iBAAmB,EAAA,IAAA;AAAA,EACnB,GAAK,EAAA,aAAA,CAAA,cAAA,CAAA,EAAA,EACA,gBAAiB,CAAA,YAAA,CAAa,GAD9B,CAAA,EAAA;AAAA,IAEH,cAAc,eAAgB,CAAA,QAAA;AAAA,IAC9B,cAAc,YAAa,CAAA;AAAA,GAC7B,CAAA;AAAA,EACA,OAAS,EAAA,aAAA,CAAA,cAAA,CAAA,EAAA,EACJ,gBAAiB,CAAA,YAAA,CAAa,OAD1B,CAAA,EAAA;AAAA,IAEP,YAAc,EAAA,OAAA;AAAA,IACd,YAAc,EAAA;AAAA,GAChB;AACF,CAAA;AAGO,MAAM,mBAAmB,EAAK,GAAA;AACrC,MAAM,cAAiB,GAAA,gBAAA;AAKhB,SAAS,kBAAkB,EAAgC,EAAA;AAChE,EAAI,IAAA,OAAO,OAAO,QAAY,IAAA,CAAC,OAAO,QAAS,CAAA,EAAE,GAAU,OAAA,CAAA;AAC3D,EAAO,OAAA,IAAA,CAAK,IAAI,CAAG,EAAA,IAAA,CAAK,IAAI,CAAI,EAAA,CAAA,EAAA,GAAK,EAAM,IAAA,EAAE,CAAC,CAAA;AAChD;AAOA,MAAM,YAAe,GAAA,IAAA;AAEd,MAAM,gBAAmB,GAAA;AAGzB,SAAS,cAAc,EAAiC,EAAA;AAC7D,EAAO,OAAA,OAAO,OAAO,QAAY,IAAA,CAAC,OAAO,QAAS,CAAA,EAAE,KAAK,EAAK,GAAA,YAAA;AAChE;AACO,MAAM,8BAA8B,KAAM,CAAA;AAAA,EAC/C,WAAc,GAAA;AACZ,IAAA,KAAA,CAAM,kEAAkE,CAAA;AACxE,IAAA,IAAA,CAAK,IAAO,GAAA,uBAAA;AAAA;AAEhB;AACO,SAAS,oBAAuB,GAAA;AACrC,EAAM,MAAA,QAAA,GAAW,iBAAiB,mBAAmB,CAAA;AACrD,EAAM,MAAA,KAAA,GAAQ,qBAAsB,CAAA,QAAA,EAAU,GAAG,CAAA;AACjD,EAAA,MAAM,CAAC,KAAA,EAAO,QAAQ,CAAA,GAAI,SAAwB,MAAM,CAAA;AACxD,EAAA,MAAM,CAAC,WAAA,EAAa,cAAc,CAAA,GAAI,SAAsB,IAAI,CAAA;AAChE,EAAM,MAAA,QAAA,GAAW,OAAsB,MAAM,CAAA;AAC7C,EAAA,QAAA,CAAS,OAAU,GAAA,KAAA;AACnB,EAAA,MAAM,aAAa,OAAQ,CAAA,MAAM,yBAA0B,EAAA,EAAG,EAAE,CAAA;AAEhE,EAAM,MAAA,eAAA,GAAkB,OAAO,KAAK,CAAA;AACpC,EAAM,MAAA,cAAA,GAAiB,YAAY,YAAY;AAC7C,IAAA,IAAI,gBAAgB,OAAS,EAAA,OAAM,UAAY,IAAA,IAAA,GAAA,MAAA,GAAA,UAAA,CAAA,IAAA,EAAA,CAAO,MAAM,MAAM,MAAA,CAAA,CAAA;AAClE,IAAA,eAAA,CAAgB,OAAU,GAAA,KAAA;AAC1B,IAAA,MAAM,mBAAoB,CAAA,cAAc,CAAE,CAAA,KAAA,CAAM,MAAM,MAAS,CAAA;AAE/D,IAAA,MAAM,iBAAkB,CAAA;AAAA,MACtB,eAAiB,EAAA,KAAA;AAAA,MACjB,iBAAmB,EAAA,IAAA;AAAA,MACnB,sBAAwB,EAAA;AAAA,KACzB,CAAA,CAAE,KAAM,CAAA,MAAM,MAAS,CAAA;AAAA,GAC1B,EAAG,CAAC,UAAU,CAAC,CAAA;AACf,EAAA,MAAM,KAAQ,GAAA,WAAA,CAAY,CAAC,MAAA,GAAqC,MAAW,KAAA;AACzE,IAAI,IAAA,QAAA,CAAS,YAAY,WAAa,EAAA;AACtC,IAAA,QAAA,CAAS,KAAM,EAAA;AACf,IAAA,QAAA,CAAS,QAAQ,CAAA;AACjB,IAAA,cAAA,CAAe,MAAM,CAAA;AAAA,GACvB,EAAG,CAAC,QAAQ,CAAC,CAAA;AACb,EAAM,MAAA,KAAA,GAAQ,YAAY,YAAY;AACpC,IAAM,MAAA,UAAA,GAAa,MAAM,gCAAiC,EAAA;AAC1D,IAAA,IAAI,CAAC,UAAA,CAAW,OAAS,EAAA,MAAM,IAAI,qBAAsB,EAAA;AAGzD,IAAA,eAAA,CAAgB,WAAU,UAAY,IAAA,IAAA,GAAA,MAAA,GAAA,UAAA,CAAA,SAAA,IAAY,MAAM,UAAA,CAAW,MAAM,iBAAkB,CAAA,KAAA,EAAO,iBAAkB,CAAA,IAAI,EAAE,IAAK,CAAA,MAAM,IAAM,EAAA,MAAM,KAAK,CAAI,GAAA,KAAA;AAC1J,IAAI,IAAA;AACF,MAAA,MAAM,iBAAkB,CAAA;AAAA,QACtB,eAAiB,EAAA,IAAA;AAAA,QACjB,iBAAmB,EAAA,IAAA;AAAA,QACnB,wBAAwB,eAAgB,CAAA;AAAA,OACzC,CAAA;AACD,MAAA,MAAM,SAAS,oBAAqB,EAAA;AACpC,MAAA,QAAA,CAAS,MAAO,EAAA;AAAA,aACT,GAAK,EAAA;AACZ,MAAA,MAAM,cAAe,EAAA;AACrB,MAAM,MAAA,GAAA;AAAA;AAGR,IAAI,IAAA,CAAC,gBAAgB,OAAS,EAAA,MAAM,uBAAuB,cAAc,CAAA,CAAE,KAAM,CAAA,MAAM,MAAS,CAAA;AAChG,IAAA,QAAA,CAAS,WAAW,CAAA;AACpB,IAAA,cAAA,CAAe,IAAI,CAAA;AAAA,GAClB,EAAA,CAAC,QAAU,EAAA,UAAA,EAAY,cAAc,CAAC,CAAA;AACzC,EAAM,MAAA,MAAA,GAAS,YAAY,YAAY;AACrC,IAAI,IAAA,QAAA,CAAS,YAAY,QAAU,EAAA;AAEnC,IAAA,MAAM,qBAAsB,CAAA,IAAI,CAAE,CAAA,KAAA,CAAM,MAAM,MAAS,CAAA;AACvD,IAAA,QAAA,CAAS,MAAO,EAAA;AAChB,IAAA,QAAA,CAAS,WAAW,CAAA;AACpB,IAAA,cAAA,CAAe,IAAI,CAAA;AAAA,GACrB,EAAG,CAAC,QAAQ,CAAC,CAAA;AAGb,EAAM,MAAA,MAAA,GAAS,YAAY,YAGd;AACX,IAAI,IAAA,QAAA,CAAS,OAAY,KAAA,MAAA,EAAe,OAAA,IAAA;AACxC,IAAM,MAAA,UAAA,GAAa,QAAS,CAAA,SAAA,EAAY,CAAA,cAAA;AACxC,IAAA,MAAM,SAAS,IAAK,EAAA;AACpB,IAAA,QAAA,CAAS,MAAM,CAAA;AACf,IAAA,cAAA,CAAe,IAAI,CAAA;AACnB,IAAA,MAAM,cAAe,EAAA;AACrB,IAAA,OAAO,SAAS,GAAM,GAAA;AAAA,MACpB,KAAK,QAAS,CAAA,GAAA;AAAA,MACd;AAAA,KACE,GAAA,IAAA;AAAA,GACH,EAAA,CAAC,QAAU,EAAA,cAAc,CAAC,CAAA;AAG7B,EAAA,SAAA,CAAU,MAAM;AACd,IAAA,MAAM,GAAM,GAAA,QAAA,CAAS,gBAAiB,CAAA,QAAA,EAAU,CAAQ,IAAA,KAAA;AACtD,MAAA,IAAI,SAAS,QAAY,IAAA,CAAC,eAAgB,CAAA,OAAA,QAAe,YAAY,CAAA;AAAA,KACtE,CAAA;AACD,IAAO,OAAA,MAAM,IAAI,MAAO,EAAA;AAAA,GAC1B,EAAG,CAAC,KAAK,CAAC,CAAA;AAIV,EAAA,SAAA,CAAU,MAAM;AACd,IAAA,MAAM,GAAM,GAAA,UAAA,IAAA,IAAA,GAAA,MAAA,GAAA,UAAA,CAAY,WAAY,CAAA,gBAAA,EAAkB,CAAS,KAAA,KAAA;AAC7D,MAAA,IAAI,KAAM,CAAA,KAAA,KAAU,OAAS,EAAA,KAAA,CAAM,aAAa,CAAA;AAAA,KAClD,CAAA;AACA,IAAA,OAAO,MAAM,GAAK,IAAA,IAAA,GAAA,MAAA,GAAA,GAAA,CAAA,MAAA,EAAA;AAAA,GACjB,EAAA,CAAC,UAAY,EAAA,KAAK,CAAC,CAAA;AACtB,EAAA,SAAA,CAAU,MAAM;AACd,IAAA,IAAI,UAAU,WAAe,IAAA,KAAA,CAAM,cAAkB,IAAA,gBAAA,QAAwB,OAAO,CAAA;AAAA,KACnF,CAAC,KAAA,EAAO,KAAM,CAAA,cAAA,EAAgB,KAAK,CAAC,CAAA;AAKvC,EAAM,MAAA,WAAA,GAAc,OAAO,CAAC,CAAA;AAC5B,EAAA,SAAA,CAAU,MAAM;AACd,IAAI,IAAA,KAAA,KAAU,eAAe,aAAc,CAAA,KAAA,CAAM,QAAQ,CAAK,IAAA,KAAA,CAAM,cAAiB,GAAA,WAAA,CAAY,OAAS,EAAA;AACxG,MAAA,WAAA,CAAY,UAAU,KAAM,CAAA,cAAA;AAAA;AAC9B,KACC,CAAC,KAAA,EAAO,MAAM,cAAgB,EAAA,KAAA,CAAM,QAAQ,CAAC,CAAA;AAEhD,EAAM,MAAA,OAAA,GAAU,KAAU,KAAA,WAAA,IAAe,CAAC,aAAA,CAAc,KAAM,CAAA,QAAQ,CAAK,IAAA,KAAA,CAAM,cAAiB,GAAA,WAAA,CAAY,OAAW,IAAA,gBAAA;AAGzH,EAAA,SAAA,CAAU,MAAM,MAAM;AACpB,IAAI,IAAA,QAAA,CAAS,YAAY,MAAQ,EAAA,KAAK,SAAS,IAAK,EAAA,CAAE,KAAM,CAAA,MAAM,MAAS,CAAA;AAC3E,IAAA,KAAK,cAAe,EAAA;AAAA,GACnB,EAAA,CAAC,QAAU,EAAA,cAAc,CAAC,CAAA;AAC7B,EAAO,OAAA;AAAA,IACL,KAAA;AAAA,IACA,WAAA;AAAA,IACA,WAAW,KAAM,CAAA,cAAA;AAAA,IACjB,OAAO,KAAU,KAAA,WAAA,GAAc,iBAAkB,CAAA,KAAA,CAAM,QAAQ,CAAI,GAAA,CAAA;AAAA,IACnE,OAAA;AAAA;AAAA,IAEA,mBAAA,EAAA,CAAqB,yCAAY,SAAc,MAAA,IAAA;AAAA,IAC/C,KAAA;AAAA,IACA,KAAA,EAAO,MAAM,KAAA,CAAM,MAAM,CAAA;AAAA,IACzB,MAAA;AAAA,IACA;AAAA,GACF;AACF"}