@adminide-stack/yantra-mobile 12.0.137 → 12.0.139-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 (96) hide show
  1. package/lib/components/CustomDrawer.js +17 -1
  2. package/lib/components/CustomDrawer.js.map +1 -1
  3. package/lib/compute.js +106 -1
  4. package/lib/compute.js.map +1 -1
  5. package/lib/config/env-config.js +12 -0
  6. package/lib/config/env-config.js.map +1 -1
  7. package/lib/features/chat/ChatTranscript.js +2 -2
  8. package/lib/features/chat/ChatTranscript.js.map +1 -1
  9. package/lib/features/chat/surfaceNotes.js +4 -0
  10. package/lib/features/chat/surfaceNotes.js.map +1 -0
  11. package/lib/features/devices/readings.js.map +1 -1
  12. package/lib/features/devices/session.js +2 -22
  13. package/lib/features/devices/session.js.map +1 -1
  14. package/lib/features/dictate/backgroundRecording.js.map +1 -1
  15. package/lib/features/dictate/useDictationRecorder.js +42 -1
  16. package/lib/features/dictate/useDictationRecorder.js.map +1 -1
  17. package/lib/features/home/useHomeWeather.js +85 -0
  18. package/lib/features/home/useHomeWeather.js.map +1 -0
  19. package/lib/features/home/weather.js +139 -0
  20. package/lib/features/home/weather.js.map +1 -0
  21. package/lib/features/notion/NotionPageView.js +819 -0
  22. package/lib/features/notion/NotionPageView.js.map +1 -0
  23. package/lib/features/notion/NotionSheets.js +452 -0
  24. package/lib/features/notion/NotionSheets.js.map +1 -0
  25. package/lib/features/notion/NotionTrashSheet.js +415 -0
  26. package/lib/features/notion/NotionTrashSheet.js.map +1 -0
  27. package/lib/features/notion/NotionWorkspaceView.js +837 -0
  28. package/lib/features/notion/NotionWorkspaceView.js.map +1 -0
  29. package/lib/features/notion/notionBlocks.js +203 -0
  30. package/lib/features/notion/notionBlocks.js.map +1 -0
  31. package/lib/features/notion/notionDocuments.js +182 -0
  32. package/lib/features/notion/notionDocuments.js.map +1 -0
  33. package/lib/features/notion/notionEditorModel.js +180 -0
  34. package/lib/features/notion/notionEditorModel.js.map +1 -0
  35. package/lib/features/notion/notionTemplates.js +406 -0
  36. package/lib/features/notion/notionTemplates.js.map +1 -0
  37. package/lib/features/notion/notionTheme.js +34 -0
  38. package/lib/features/notion/notionTheme.js.map +1 -0
  39. package/lib/features/notion/notionTrash.js +100 -0
  40. package/lib/features/notion/notionTrash.js.map +1 -0
  41. package/lib/features/notion/notionUi.js +310 -0
  42. package/lib/features/notion/notionUi.js.map +1 -0
  43. package/lib/features/notion/useNotionWorkspace.js +181 -0
  44. package/lib/features/notion/useNotionWorkspace.js.map +1 -0
  45. package/lib/features/webbuilder/buildFragments.js +15 -9
  46. package/lib/features/webbuilder/buildFragments.js.map +1 -1
  47. package/lib/features/webbuilder/buildJob.js +3 -3
  48. package/lib/features/webbuilder/buildJob.js.map +1 -1
  49. package/lib/features/webbuilder/publicSandboxDomain.js +5 -0
  50. package/lib/features/webbuilder/publicSandboxDomain.js.map +1 -0
  51. package/lib/features/webbuilder/sandboxPublicHost.js +74 -0
  52. package/lib/features/webbuilder/sandboxPublicHost.js.map +1 -0
  53. package/lib/features/webbuilder/siteAddress.js +2 -1
  54. package/lib/features/webbuilder/siteAddress.js.map +1 -1
  55. package/lib/features/webbuilder/webbuilderPrompts.js +3 -2
  56. package/lib/features/webbuilder/webbuilderPrompts.js.map +1 -1
  57. package/lib/features/webbuilder/workflow.js +2 -2
  58. package/lib/features/webbuilder/workflow.js.map +1 -1
  59. package/lib/hooks/useChatApi.js +15 -3
  60. package/lib/hooks/useChatApi.js.map +1 -1
  61. package/lib/hooks/usePrerequisiteIds.js +15 -2
  62. package/lib/hooks/usePrerequisiteIds.js.map +1 -1
  63. package/lib/routes.json +107 -0
  64. package/lib/screens/Devices/index.js +3 -205
  65. package/lib/screens/Devices/index.js.map +1 -1
  66. package/lib/screens/Home/HomeScreen.js +20 -5
  67. package/lib/screens/Home/HomeScreen.js.map +1 -1
  68. package/lib/screens/Home/components/HomeHero.js +117 -0
  69. package/lib/screens/Home/components/HomeHero.js.map +1 -0
  70. package/lib/screens/Home/components/HomeSuggestions.js +108 -85
  71. package/lib/screens/Home/components/HomeSuggestions.js.map +1 -1
  72. package/lib/screens/Home/components/HomeWash.js +44 -0
  73. package/lib/screens/Home/components/HomeWash.js.map +1 -0
  74. package/lib/screens/Home/components/homeCatalog.js +125 -0
  75. package/lib/screens/Home/components/homeCatalog.js.map +1 -0
  76. package/lib/screens/Notion/index.js +18 -0
  77. package/lib/screens/Notion/index.js.map +1 -0
  78. package/lib/screens/NotionPage/index.js +19 -0
  79. package/lib/screens/NotionPage/index.js.map +1 -0
  80. package/lib/screens/WebBuilderNative/sandboxProxyApi.js +2 -2
  81. package/lib/screens/WebBuilderNative/sandboxProxyApi.js.map +1 -1
  82. package/lib/screens/WebBuilderNative/useRestoredSandboxes.js +16 -8
  83. package/lib/screens/WebBuilderNative/useRestoredSandboxes.js.map +1 -1
  84. package/lib/screens/WebBuilderNative/useWebBuilderGallery.js +6 -5
  85. package/lib/screens/WebBuilderNative/useWebBuilderGallery.js.map +1 -1
  86. package/lib/screens/WebBuilderNative/useWebBuilderHost.js +5 -4
  87. package/lib/screens/WebBuilderNative/useWebBuilderHost.js.map +1 -1
  88. package/lib/screens/WebBuilderNative/webbuilderTransport.js +4 -3
  89. package/lib/screens/WebBuilderNative/webbuilderTransport.js.map +1 -1
  90. package/lib/services/savedWorkspace.js +44 -0
  91. package/lib/services/savedWorkspace.js.map +1 -0
  92. package/lib/theme/mobileTokens.js +1 -0
  93. package/lib/theme/mobileTokens.js.map +1 -1
  94. package/package.json +4 -4
  95. package/lib/features/devices/bodyComposition.js +0 -116
  96. package/lib/features/devices/bodyComposition.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"ChatTranscript.js","sources":["../../../src/features/chat/ChatTranscript.tsx"],"sourcesContent":["/**\n * Chat thread list that can show attachment previews.\n *\n * `@messenger-box/platform-mobile` PlanModeView maps each row to GiftedChat\n * `{ text }` only, so an image send becomes the gateway caption\n * \"Attached: IMG_0005.jpg\" with no thumbnail. This list keeps streaming\n * markdown for the assistant and draws Manus-style previews on the user side.\n *\n * Rows stay oldest → newest (same as web). A previous `inverted` FlatList\n * reversed that order on device. The composer dock already lifts with the\n * keyboard; this list just fills the leftover space and pins to the latest\n * turn.\n */\nimport React, { memo, useCallback, useEffect, useMemo, useRef, type ReactNode } from 'react';\nimport { FlatList, Platform, Pressable, StyleSheet, Text, View, type StyleProp, type ViewStyle } from 'react-native';\nimport Markdown from 'react-native-markdown-display';\nimport { dismissKeyboard } from '../../utils/keyboardController';\nimport { useKeyboardLiftHeight } from '../../components/KeyboardComposerDock';\nimport type { MessageAttachment } from '../../hooks/useChatStream';\nimport { displayUserMessageText, shouldRenderUserTranscriptTurn } from '../attachments/displayUserMessageText';\nimport { MessageAttachmentPreviews } from '../attachments/MessageAttachmentPreviews';\nimport { ToolTimeline } from './ToolTimeline';\nimport { ChatWidgetCard } from './ChatWidgetCard';\nimport { splitChatWidgets } from './chatWidgets';\nimport { parseToolActivity } from './toolActivity';\nimport { parseAskUser, stripAskUser, displayAskUserAssistantText } from './askUser';\nimport { collapseDuplicateUserRows, groupConsecutiveRoleRows, type RoleGroup } from './transcriptGroups';\nimport { AssistantMarkdown } from './AssistantMarkdown';\nimport { useChatLinkOpener } from '../links/chatLinkOpener';\nimport { autolinkBareUrls } from '../messaging/chatLinks';\nimport { PreResponseStrip } from './PreResponseStrip';\nimport { splitPreResponse } from './preResponse';\n\nexport type TranscriptMessage = {\n id: string;\n role: string;\n content: string;\n displayContent?: string;\n attachments?: MessageAttachment[];\n /** Typed while the gateway was down: shown in place, waiting on the outbox. */\n queued?: boolean;\n /** What the caption under a queued turn says, when it is not waiting on the gateway to connect. */\n queuedNote?: string;\n};\n\ntype TranscriptRow = TranscriptMessage & {\n streaming?: boolean;\n askAnswered?: boolean;\n /** The fast lane's quick answer for this turn, drawn as a strip - never as text. */\n preResponse?: string | null;\n};\ntype TranscriptGroup = RoleGroup<TranscriptRow>;\n\nexport const USER_BUBBLE_BG = '#2563eb';\n\nexport function ChatTranscript({\n messages,\n streamingContent,\n quickReply,\n renderMessageActions,\n listContentStyle,\n listFooter,\n isDark = false,\n}: {\n messages: TranscriptMessage[];\n streamingContent?: string;\n /** The running turn's quick answer (fast lane or brain), already split off the stream. */\n quickReply?: string;\n renderMessageActions?: (message: { id: string; role: string; content: string }) => React.ReactNode;\n listContentStyle?: StyleProp<ViewStyle>;\n listFooter?: ReactNode;\n isDark?: boolean;\n}) {\n const listRef = useRef<FlatList<TranscriptGroup>>(null);\n const stickToBottomRef = useRef(true);\n const isProgrammaticScrollRef = useRef(false);\n const ignoreScrollUntilRef = useRef(0);\n const keyboardHeight = useKeyboardLiftHeight();\n\n const historyRows = useMemo<TranscriptRow[]>(() => {\n const rows: TranscriptRow[] = [];\n messages.forEach((msg, index) => {\n if (msg.role === 'user') {\n if (shouldRenderUserTranscriptTurn(msg.content, msg.attachments)) {\n rows.push({ ...msg, streaming: false });\n }\n return;\n }\n if (msg.role !== 'assistant') {\n rows.push({ ...msg, streaming: false });\n return;\n }\n // A persisted reply carries its quick answer prepended in the gateway's\n // markers; the row keeps the two apart from here on.\n const { preResponse, text: content } = splitPreResponse(msg.content);\n const hasAttachments = (msg.attachments?.length ?? 0) > 0;\n const askAnswered =\n Boolean(parseAskUser(content)) && messages.slice(index + 1).some((m) => m.role === 'user');\n const visible = displayAskUserAssistantText(content, askAnswered).trim();\n const tools = parseToolActivity(stripAskUser(content), false);\n const hasAsk = Boolean(parseAskUser(content));\n if (hasAsk && !visible && tools.steps.length === 0 && !hasAttachments && !preResponse) {\n return;\n }\n if (!hasAsk) {\n const leftover = stripAskUser(content).trim();\n if (!leftover && !hasAttachments && !preResponse) return;\n }\n rows.push({ ...msg, content, preResponse, streaming: false, askAnswered });\n });\n return rows;\n }, [messages]);\n\n const rows = useMemo<TranscriptRow[]>(() => {\n const history = collapseDuplicateUserRows(historyRows);\n // The chat hook hands the quick answer over split off the stream; a raw\n // stream (another host) may still carry the markers, open mid-stream.\n const { preResponse: streamPre, text: rawStream } = splitPreResponse(streamingContent ?? '');\n const livePre = quickReply?.trim() || streamPre;\n const last = history[history.length - 1];\n const streamAskAnswered = last?.role === 'user' && Boolean(parseAskUser(rawStream));\n const streamProse = displayAskUserAssistantText(rawStream, streamAskAnswered).trim();\n const streamTools = parseToolActivity(stripAskUser(rawStream), true);\n if (!streamProse && streamTools.steps.length === 0 && !livePre) return history;\n if (last?.role === 'assistant' && (!livePre || last.preResponse === livePre)) {\n const lastText = last.content.trim();\n const streamText = rawStream.trim();\n if (lastText === streamText || lastText.startsWith(streamText) || streamText.startsWith(lastText)) {\n return history;\n }\n }\n return [\n ...history,\n {\n id: '__streaming__',\n role: 'assistant',\n content: rawStream,\n preResponse: livePre,\n streaming: true,\n askAnswered: streamAskAnswered,\n },\n ];\n }, [historyRows, streamingContent, quickReply]);\n\n const groups = useMemo(() => groupConsecutiveRoleRows(rows), [rows]);\n\n const scrollToLatest = useCallback(\n (animated: boolean) => {\n if (groups.length === 0) return;\n isProgrammaticScrollRef.current = true;\n stickToBottomRef.current = true;\n const list = listRef.current;\n if (!list) return;\n list.scrollToEnd({ animated });\n },\n [groups.length],\n );\n\n useEffect(() => {\n if (groups.length === 0 || !stickToBottomRef.current) return undefined;\n const t = setTimeout(() => scrollToLatest(false), 0);\n return () => clearTimeout(t);\n }, [groups.length, streamingContent?.length, quickReply?.length, scrollToLatest]);\n\n // Spacer and list height update on the same keyboard frame. Scroll only\n // after that shrink, or we stay on the top of a long turn (image 1).\n useEffect(() => {\n if (keyboardHeight <= 0) return undefined;\n stickToBottomRef.current = true;\n ignoreScrollUntilRef.current = Date.now() + 400;\n const frame = requestAnimationFrame(() => scrollToLatest(false));\n const afterLayout = setTimeout(() => scrollToLatest(false), 64);\n const afterKeyboard = setTimeout(() => scrollToLatest(false), 280);\n return () => {\n cancelAnimationFrame(frame);\n clearTimeout(afterLayout);\n clearTimeout(afterKeyboard);\n };\n }, [keyboardHeight, scrollToLatest]);\n\n const handleScroll = useCallback(\n (e: {\n nativeEvent: {\n contentOffset: { y: number };\n contentSize: { height: number };\n layoutMeasurement: { height: number };\n };\n }) => {\n if (Date.now() < ignoreScrollUntilRef.current) {\n stickToBottomRef.current = true;\n return;\n }\n const { contentOffset, contentSize, layoutMeasurement } = e.nativeEvent;\n const nearLatest = contentOffset.y + layoutMeasurement.height >= contentSize.height - 80;\n if (!isProgrammaticScrollRef.current) {\n stickToBottomRef.current = nearLatest;\n } else if (nearLatest) {\n isProgrammaticScrollRef.current = false;\n }\n },\n [],\n );\n\n const handleContentSizeChange = useCallback(() => {\n if (!stickToBottomRef.current) return;\n scrollToLatest(false);\n }, [scrollToLatest]);\n\n const renderItem = useCallback(\n ({ item }: { item: TranscriptGroup }) => (\n <TranscriptGroupView group={item} isDark={isDark} renderMessageActions={renderMessageActions} />\n ),\n [isDark, renderMessageActions],\n );\n\n return (\n <View style={styles.list}>\n <FlatList\n ref={listRef}\n data={groups}\n keyExtractor={(item) => item.id}\n renderItem={renderItem}\n extraData={`${streamingContent ?? ''}:${quickReply ?? ''}:${Boolean(listFooter)}`}\n ListFooterComponent={listFooter ? <>{listFooter}</> : null}\n initialNumToRender={12}\n maxToRenderPerBatch={8}\n updateCellsBatchingPeriod={50}\n windowSize={21}\n scrollEnabled\n nestedScrollEnabled={false}\n directionalLockEnabled\n onScroll={handleScroll}\n onContentSizeChange={handleContentSizeChange}\n onLayout={() => {\n if (stickToBottomRef.current) scrollToLatest(false);\n }}\n scrollEventThrottle={16}\n keyboardShouldPersistTaps=\"handled\"\n keyboardDismissMode={Platform.OS === 'ios' ? 'interactive' : 'on-drag'}\n automaticallyAdjustKeyboardInsets={false}\n automaticallyAdjustContentInsets={false}\n contentInsetAdjustmentBehavior=\"never\"\n contentContainerStyle={[styles.listContent, listContentStyle, !listFooter && styles.listContentFill]}\n style={styles.list}\n />\n </View>\n );\n}\n\nconst TranscriptGroupView = memo(function TranscriptGroupView({\n group,\n isDark,\n renderMessageActions,\n}: {\n group: TranscriptGroup;\n isDark: boolean;\n renderMessageActions?: (message: { id: string; role: string; content: string }) => React.ReactNode;\n}) {\n // A link the person pasted opens like one the assistant sent: in the app or a browser sheet.\n const openLink = useChatLinkOpener();\n const onLinkPress = useCallback(\n (url: string) => {\n openLink(url);\n return false;\n },\n [openLink],\n );\n if (group.role === 'user') {\n return (\n <Pressable style={styles.userCol} onPress={dismissKeyboard} accessible={false}>\n {group.items.map((item, index) => {\n const attachments = item.attachments ?? [];\n const text = displayUserMessageText(item.displayContent ?? item.content, attachments);\n if (!text && attachments.length === 0) return null;\n return (\n <View key={item.id} style={[styles.userTurn, index > 0 && styles.userFollow]}>\n {attachments.length > 0 ? (\n <MessageAttachmentPreviews attachments={attachments} align=\"end\" isDark={isDark} />\n ) : null}\n {text ? (\n <View style={[styles.userBubble, item.queued && styles.userBubbleQueued]}>\n <Markdown style={USER_MARKDOWN} onLinkPress={onLinkPress}>\n {/* Bare URLs the person typed become links, as in the assistant's replies. */}\n {autolinkBareUrls(text)}\n </Markdown>\n </View>\n ) : null}\n {item.queued ? (\n <Text style={[styles.queuedCaption, { color: isDark ? '#94a3b8' : '#64748b' }]}>\n {item.queuedNote ?? 'Queued · sends when the gateway connects'}\n </Text>\n ) : null}\n </View>\n );\n })}\n </Pressable>\n );\n }\n\n const last = group.items[group.items.length - 1];\n\n return (\n <View style={styles.assistantGroup}>\n {group.items.map((item, index) => (\n <AssistantTurnView\n key={item.id}\n item={item}\n isDark={isDark}\n tightTop={index > 0}\n renderMessageActions={item.streaming || item !== last ? undefined : renderMessageActions}\n />\n ))}\n </View>\n );\n});\n\nconst AssistantTurnView = memo(function AssistantTurnView({\n item,\n isDark,\n tightTop,\n renderMessageActions,\n}: {\n item: TranscriptRow;\n isDark: boolean;\n tightTop: boolean;\n renderMessageActions?: (message: { id: string; role: string; content: string }) => React.ReactNode;\n}) {\n const attachments = item.attachments ?? [];\n const stripped = stripAskUser(item.content);\n const toolActivity = parseToolActivity(stripped, !!item.streaming);\n // Widget envelopes become native cards below the text, never raw JSON.\n const { text, widgets } = splitChatWidgets(\n parseToolActivity(displayAskUserAssistantText(item.content, !!item.askAnswered), !!item.streaming).text,\n !!item.streaming,\n );\n\n // The quick answer stays open while it is all there is to read: until the\n // agent's own words start on a live turn, or for good on a reply that never\n // got anything past it. Tool steps alone do not fold it - they are not an answer.\n const preResponseOpen = item.streaming\n ? !text.trim()\n : !text.trim() && toolActivity.steps.length === 0 && widgets.length === 0 && attachments.length === 0;\n\n return (\n <View style={[styles.assistantCol, tightTop && styles.assistantColTight]}>\n {attachments.length > 0 ? (\n <MessageAttachmentPreviews attachments={attachments} align=\"start\" isDark={isDark} />\n ) : null}\n {item.preResponse ? (\n <PreResponseStrip\n text={item.preResponse}\n expanded={preResponseOpen}\n live={!!item.streaming}\n isDark={isDark}\n />\n ) : null}\n {toolActivity.steps.length > 0 ? (\n <ToolTimeline steps={toolActivity.steps} active={!!item.streaming} isDark={isDark} />\n ) : null}\n {text ? <AssistantMarkdown text={text} streaming={!!item.streaming} isDark={isDark} /> : null}\n {widgets.map((widget, i) => (\n <ChatWidgetCard key={`${widget.component}-${i}`} widget={widget} isDark={isDark} />\n ))}\n {renderMessageActions\n ? renderMessageActions({\n id: item.id,\n role: 'assistant',\n // Copy and share take what the user reads, not the wire markup.\n content: text.trim() || item.preResponse || item.content,\n })\n : null}\n </View>\n );\n});\n\nconst USER_MARKDOWN = {\n body: { color: '#ffffff', fontSize: 15, lineHeight: 22 },\n // White and underlined: the bubble is blue, so the usual blue link would vanish.\n link: { color: '#ffffff', textDecorationLine: 'underline' as const },\n paragraph: { marginTop: 4, marginBottom: 4, color: '#ffffff' },\n strong: { fontWeight: '700' as const, color: '#ffffff' },\n em: { color: '#ffffff' },\n text: { color: '#ffffff' },\n bullet_list: { marginVertical: 4 },\n ordered_list: { marginVertical: 4 },\n list_item: { marginVertical: 2, color: '#ffffff' },\n};\n\nconst styles = StyleSheet.create({\n list: {\n flex: 1,\n minHeight: 0,\n },\n listContent: {\n paddingTop: 8,\n paddingBottom: 8,\n paddingHorizontal: 8,\n },\n listContentFill: {\n flexGrow: 1,\n },\n userCol: {\n width: '100%',\n alignItems: 'flex-end',\n marginBottom: 12,\n paddingHorizontal: 4,\n },\n userTurn: {\n maxWidth: '92%',\n alignItems: 'flex-end',\n },\n userFollow: {\n marginTop: 4,\n },\n userBubble: {\n backgroundColor: USER_BUBBLE_BG,\n borderRadius: 16,\n paddingHorizontal: 12,\n paddingVertical: 8,\n },\n // Same bubble, held back: a queued turn reads as pending, not as delivered.\n userBubbleQueued: {\n opacity: 0.55,\n },\n queuedCaption: {\n fontSize: 11,\n fontWeight: '500',\n marginTop: 4,\n marginRight: 2,\n },\n assistantGroup: {\n width: '100%',\n marginBottom: 4,\n },\n assistantCol: {\n width: '100%',\n flexGrow: 0,\n alignItems: 'stretch',\n marginBottom: 8,\n paddingHorizontal: 4,\n },\n assistantColTight: {\n marginTop: -4,\n },\n});\n"],"names":["rows","TranscriptGroupView","AssistantTurnView"],"mappings":"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"}
1
+ {"version":3,"file":"ChatTranscript.js","sources":["../../../src/features/chat/ChatTranscript.tsx"],"sourcesContent":["/**\n * Chat thread list that can show attachment previews.\n *\n * `@messenger-box/platform-mobile` PlanModeView maps each row to GiftedChat\n * `{ text }` only, so an image send becomes the gateway caption\n * \"Attached: IMG_0005.jpg\" with no thumbnail. This list keeps streaming\n * markdown for the assistant and draws Manus-style previews on the user side.\n *\n * Rows stay oldest → newest (same as web). A previous `inverted` FlatList\n * reversed that order on device. The composer dock already lifts with the\n * keyboard; this list just fills the leftover space and pins to the latest\n * turn.\n */\nimport React, { memo, useCallback, useEffect, useMemo, useRef, type ReactNode } from 'react';\nimport { FlatList, Platform, Pressable, StyleSheet, Text, View, type StyleProp, type ViewStyle } from 'react-native';\nimport Markdown from 'react-native-markdown-display';\nimport { dismissKeyboard } from '../../utils/keyboardController';\nimport { useKeyboardLiftHeight } from '../../components/KeyboardComposerDock';\nimport type { MessageAttachment } from '../../hooks/useChatStream';\nimport { displayUserMessageText, shouldRenderUserTranscriptTurn } from '../attachments/displayUserMessageText';\nimport { MessageAttachmentPreviews } from '../attachments/MessageAttachmentPreviews';\nimport { ToolTimeline } from './ToolTimeline';\nimport { ChatWidgetCard } from './ChatWidgetCard';\nimport { splitChatWidgets } from './chatWidgets';\nimport { parseToolActivity } from './toolActivity';\nimport { parseAskUser, stripAskUser, displayAskUserAssistantText } from './askUser';\nimport { stripSurfaceNotes } from './surfaceNotes';\nimport { collapseDuplicateUserRows, groupConsecutiveRoleRows, type RoleGroup } from './transcriptGroups';\nimport { AssistantMarkdown } from './AssistantMarkdown';\nimport { useChatLinkOpener } from '../links/chatLinkOpener';\nimport { autolinkBareUrls } from '../messaging/chatLinks';\nimport { PreResponseStrip } from './PreResponseStrip';\nimport { splitPreResponse } from './preResponse';\n\nexport type TranscriptMessage = {\n id: string;\n role: string;\n content: string;\n displayContent?: string;\n attachments?: MessageAttachment[];\n /** Typed while the gateway was down: shown in place, waiting on the outbox. */\n queued?: boolean;\n /** What the caption under a queued turn says, when it is not waiting on the gateway to connect. */\n queuedNote?: string;\n};\n\ntype TranscriptRow = TranscriptMessage & {\n streaming?: boolean;\n askAnswered?: boolean;\n /** The fast lane's quick answer for this turn, drawn as a strip - never as text. */\n preResponse?: string | null;\n};\ntype TranscriptGroup = RoleGroup<TranscriptRow>;\n\nexport const USER_BUBBLE_BG = '#2563eb';\n\nexport function ChatTranscript({\n messages,\n streamingContent,\n quickReply,\n renderMessageActions,\n listContentStyle,\n listFooter,\n isDark = false,\n}: {\n messages: TranscriptMessage[];\n streamingContent?: string;\n /** The running turn's quick answer (fast lane or brain), already split off the stream. */\n quickReply?: string;\n renderMessageActions?: (message: { id: string; role: string; content: string }) => React.ReactNode;\n listContentStyle?: StyleProp<ViewStyle>;\n listFooter?: ReactNode;\n isDark?: boolean;\n}) {\n const listRef = useRef<FlatList<TranscriptGroup>>(null);\n const stickToBottomRef = useRef(true);\n const isProgrammaticScrollRef = useRef(false);\n const ignoreScrollUntilRef = useRef(0);\n const keyboardHeight = useKeyboardLiftHeight();\n\n const historyRows = useMemo<TranscriptRow[]>(() => {\n const rows: TranscriptRow[] = [];\n messages.forEach((msg, index) => {\n if (msg.role === 'user') {\n if (shouldRenderUserTranscriptTurn(msg.content, msg.attachments)) {\n rows.push({ ...msg, streaming: false });\n }\n return;\n }\n if (msg.role !== 'assistant') {\n rows.push({ ...msg, streaming: false });\n return;\n }\n // A persisted reply carries its quick answer prepended in the gateway's\n // markers; the row keeps the two apart from here on.\n const { preResponse, text: content } = splitPreResponse(msg.content);\n const hasAttachments = (msg.attachments?.length ?? 0) > 0;\n const askAnswered =\n Boolean(parseAskUser(content)) && messages.slice(index + 1).some((m) => m.role === 'user');\n const visible = displayAskUserAssistantText(content, askAnswered).trim();\n const tools = parseToolActivity(stripAskUser(content), false);\n const hasAsk = Boolean(parseAskUser(content));\n if (hasAsk && !visible && tools.steps.length === 0 && !hasAttachments && !preResponse) {\n return;\n }\n if (!hasAsk) {\n const leftover = stripAskUser(content).trim();\n if (!leftover && !hasAttachments && !preResponse) return;\n }\n rows.push({ ...msg, content, preResponse, streaming: false, askAnswered });\n });\n return rows;\n }, [messages]);\n\n const rows = useMemo<TranscriptRow[]>(() => {\n const history = collapseDuplicateUserRows(historyRows);\n // The chat hook hands the quick answer over split off the stream; a raw\n // stream (another host) may still carry the markers, open mid-stream.\n const { preResponse: streamPre, text: rawStream } = splitPreResponse(streamingContent ?? '');\n const livePre = quickReply?.trim() || streamPre;\n const last = history[history.length - 1];\n const streamAskAnswered = last?.role === 'user' && Boolean(parseAskUser(rawStream));\n const streamProse = displayAskUserAssistantText(rawStream, streamAskAnswered).trim();\n const streamTools = parseToolActivity(stripAskUser(rawStream), true);\n if (!streamProse && streamTools.steps.length === 0 && !livePre) return history;\n if (last?.role === 'assistant' && (!livePre || last.preResponse === livePre)) {\n const lastText = last.content.trim();\n const streamText = rawStream.trim();\n if (lastText === streamText || lastText.startsWith(streamText) || streamText.startsWith(lastText)) {\n return history;\n }\n }\n return [\n ...history,\n {\n id: '__streaming__',\n role: 'assistant',\n content: rawStream,\n preResponse: livePre,\n streaming: true,\n askAnswered: streamAskAnswered,\n },\n ];\n }, [historyRows, streamingContent, quickReply]);\n\n const groups = useMemo(() => groupConsecutiveRoleRows(rows), [rows]);\n\n const scrollToLatest = useCallback(\n (animated: boolean) => {\n if (groups.length === 0) return;\n isProgrammaticScrollRef.current = true;\n stickToBottomRef.current = true;\n const list = listRef.current;\n if (!list) return;\n list.scrollToEnd({ animated });\n },\n [groups.length],\n );\n\n useEffect(() => {\n if (groups.length === 0 || !stickToBottomRef.current) return undefined;\n const t = setTimeout(() => scrollToLatest(false), 0);\n return () => clearTimeout(t);\n }, [groups.length, streamingContent?.length, quickReply?.length, scrollToLatest]);\n\n // Spacer and list height update on the same keyboard frame. Scroll only\n // after that shrink, or we stay on the top of a long turn (image 1).\n useEffect(() => {\n if (keyboardHeight <= 0) return undefined;\n stickToBottomRef.current = true;\n ignoreScrollUntilRef.current = Date.now() + 400;\n const frame = requestAnimationFrame(() => scrollToLatest(false));\n const afterLayout = setTimeout(() => scrollToLatest(false), 64);\n const afterKeyboard = setTimeout(() => scrollToLatest(false), 280);\n return () => {\n cancelAnimationFrame(frame);\n clearTimeout(afterLayout);\n clearTimeout(afterKeyboard);\n };\n }, [keyboardHeight, scrollToLatest]);\n\n const handleScroll = useCallback(\n (e: {\n nativeEvent: {\n contentOffset: { y: number };\n contentSize: { height: number };\n layoutMeasurement: { height: number };\n };\n }) => {\n if (Date.now() < ignoreScrollUntilRef.current) {\n stickToBottomRef.current = true;\n return;\n }\n const { contentOffset, contentSize, layoutMeasurement } = e.nativeEvent;\n const nearLatest = contentOffset.y + layoutMeasurement.height >= contentSize.height - 80;\n if (!isProgrammaticScrollRef.current) {\n stickToBottomRef.current = nearLatest;\n } else if (nearLatest) {\n isProgrammaticScrollRef.current = false;\n }\n },\n [],\n );\n\n const handleContentSizeChange = useCallback(() => {\n if (!stickToBottomRef.current) return;\n scrollToLatest(false);\n }, [scrollToLatest]);\n\n const renderItem = useCallback(\n ({ item }: { item: TranscriptGroup }) => (\n <TranscriptGroupView group={item} isDark={isDark} renderMessageActions={renderMessageActions} />\n ),\n [isDark, renderMessageActions],\n );\n\n return (\n <View style={styles.list}>\n <FlatList\n ref={listRef}\n data={groups}\n keyExtractor={(item) => item.id}\n renderItem={renderItem}\n extraData={`${streamingContent ?? ''}:${quickReply ?? ''}:${Boolean(listFooter)}`}\n ListFooterComponent={listFooter ? <>{listFooter}</> : null}\n initialNumToRender={12}\n maxToRenderPerBatch={8}\n updateCellsBatchingPeriod={50}\n windowSize={21}\n scrollEnabled\n nestedScrollEnabled={false}\n directionalLockEnabled\n onScroll={handleScroll}\n onContentSizeChange={handleContentSizeChange}\n onLayout={() => {\n if (stickToBottomRef.current) scrollToLatest(false);\n }}\n scrollEventThrottle={16}\n keyboardShouldPersistTaps=\"handled\"\n keyboardDismissMode={Platform.OS === 'ios' ? 'interactive' : 'on-drag'}\n automaticallyAdjustKeyboardInsets={false}\n automaticallyAdjustContentInsets={false}\n contentInsetAdjustmentBehavior=\"never\"\n contentContainerStyle={[styles.listContent, listContentStyle, !listFooter && styles.listContentFill]}\n style={styles.list}\n />\n </View>\n );\n}\n\nconst TranscriptGroupView = memo(function TranscriptGroupView({\n group,\n isDark,\n renderMessageActions,\n}: {\n group: TranscriptGroup;\n isDark: boolean;\n renderMessageActions?: (message: { id: string; role: string; content: string }) => React.ReactNode;\n}) {\n // A link the person pasted opens like one the assistant sent: in the app or a browser sheet.\n const openLink = useChatLinkOpener();\n const onLinkPress = useCallback(\n (url: string) => {\n openLink(url);\n return false;\n },\n [openLink],\n );\n if (group.role === 'user') {\n return (\n <Pressable style={styles.userCol} onPress={dismissKeyboard} accessible={false}>\n {group.items.map((item, index) => {\n const attachments = item.attachments ?? [];\n const text = displayUserMessageText(item.displayContent ?? item.content, attachments);\n if (!text && attachments.length === 0) return null;\n return (\n <View key={item.id} style={[styles.userTurn, index > 0 && styles.userFollow]}>\n {attachments.length > 0 ? (\n <MessageAttachmentPreviews attachments={attachments} align=\"end\" isDark={isDark} />\n ) : null}\n {text ? (\n <View style={[styles.userBubble, item.queued && styles.userBubbleQueued]}>\n <Markdown style={USER_MARKDOWN} onLinkPress={onLinkPress}>\n {/* Bare URLs the person typed become links, as in the assistant's replies. */}\n {autolinkBareUrls(text)}\n </Markdown>\n </View>\n ) : null}\n {item.queued ? (\n <Text style={[styles.queuedCaption, { color: isDark ? '#94a3b8' : '#64748b' }]}>\n {item.queuedNote ?? 'Queued · sends when the gateway connects'}\n </Text>\n ) : null}\n </View>\n );\n })}\n </Pressable>\n );\n }\n\n const last = group.items[group.items.length - 1];\n\n return (\n <View style={styles.assistantGroup}>\n {group.items.map((item, index) => (\n <AssistantTurnView\n key={item.id}\n item={item}\n isDark={isDark}\n tightTop={index > 0}\n renderMessageActions={item.streaming || item !== last ? undefined : renderMessageActions}\n />\n ))}\n </View>\n );\n});\n\nconst AssistantTurnView = memo(function AssistantTurnView({\n item,\n isDark,\n tightTop,\n renderMessageActions,\n}: {\n item: TranscriptRow;\n isDark: boolean;\n tightTop: boolean;\n renderMessageActions?: (message: { id: string; role: string; content: string }) => React.ReactNode;\n}) {\n const attachments = item.attachments ?? [];\n const stripped = stripAskUser(item.content);\n const toolActivity = parseToolActivity(stripped, !!item.streaming);\n // Widget envelopes become native cards below the text, never raw JSON.\n const { text, widgets } = splitChatWidgets(\n parseToolActivity(\n stripSurfaceNotes(displayAskUserAssistantText(item.content, !!item.askAnswered)),\n !!item.streaming,\n ).text,\n !!item.streaming,\n );\n\n // The quick answer stays open while it is all there is to read: until the\n // agent's own words start on a live turn, or for good on a reply that never\n // got anything past it. Tool steps alone do not fold it - they are not an answer.\n const preResponseOpen = item.streaming\n ? !text.trim()\n : !text.trim() && toolActivity.steps.length === 0 && widgets.length === 0 && attachments.length === 0;\n\n return (\n <View style={[styles.assistantCol, tightTop && styles.assistantColTight]}>\n {attachments.length > 0 ? (\n <MessageAttachmentPreviews attachments={attachments} align=\"start\" isDark={isDark} />\n ) : null}\n {item.preResponse ? (\n <PreResponseStrip\n text={item.preResponse}\n expanded={preResponseOpen}\n live={!!item.streaming}\n isDark={isDark}\n />\n ) : null}\n {toolActivity.steps.length > 0 ? (\n <ToolTimeline steps={toolActivity.steps} active={!!item.streaming} isDark={isDark} />\n ) : null}\n {text ? <AssistantMarkdown text={text} streaming={!!item.streaming} isDark={isDark} /> : null}\n {widgets.map((widget, i) => (\n <ChatWidgetCard key={`${widget.component}-${i}`} widget={widget} isDark={isDark} />\n ))}\n {renderMessageActions\n ? renderMessageActions({\n id: item.id,\n role: 'assistant',\n // Copy and share take what the user reads, not the wire markup.\n content: text.trim() || item.preResponse || item.content,\n })\n : null}\n </View>\n );\n});\n\nconst USER_MARKDOWN = {\n body: { color: '#ffffff', fontSize: 15, lineHeight: 22 },\n // White and underlined: the bubble is blue, so the usual blue link would vanish.\n link: { color: '#ffffff', textDecorationLine: 'underline' as const },\n paragraph: { marginTop: 4, marginBottom: 4, color: '#ffffff' },\n strong: { fontWeight: '700' as const, color: '#ffffff' },\n em: { color: '#ffffff' },\n text: { color: '#ffffff' },\n bullet_list: { marginVertical: 4 },\n ordered_list: { marginVertical: 4 },\n list_item: { marginVertical: 2, color: '#ffffff' },\n};\n\nconst styles = StyleSheet.create({\n list: {\n flex: 1,\n minHeight: 0,\n },\n listContent: {\n paddingTop: 8,\n paddingBottom: 8,\n paddingHorizontal: 8,\n },\n listContentFill: {\n flexGrow: 1,\n },\n userCol: {\n width: '100%',\n alignItems: 'flex-end',\n marginBottom: 12,\n paddingHorizontal: 4,\n },\n userTurn: {\n maxWidth: '92%',\n alignItems: 'flex-end',\n },\n userFollow: {\n marginTop: 4,\n },\n userBubble: {\n backgroundColor: USER_BUBBLE_BG,\n borderRadius: 16,\n paddingHorizontal: 12,\n paddingVertical: 8,\n },\n // Same bubble, held back: a queued turn reads as pending, not as delivered.\n userBubbleQueued: {\n opacity: 0.55,\n },\n queuedCaption: {\n fontSize: 11,\n fontWeight: '500',\n marginTop: 4,\n marginRight: 2,\n },\n assistantGroup: {\n width: '100%',\n marginBottom: 4,\n },\n assistantCol: {\n width: '100%',\n flexGrow: 0,\n alignItems: 'stretch',\n marginBottom: 8,\n paddingHorizontal: 4,\n },\n assistantColTight: {\n marginTop: -4,\n },\n});\n"],"names":["rows","TranscriptGroupView","AssistantTurnView"],"mappings":"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"}
@@ -0,0 +1,4 @@
1
+ function stripSurfaceNotes(text) {
2
+ if (!text || !text.includes("(preview `") && !text.includes("(snapshot `")) return text;
3
+ return text.replace(/(restore point) \(snapshot `[^`]+`\)/gi, "$1").replace(/\s*\(preview `[^`]+`\)/gi, "");
4
+ }export{stripSurfaceNotes};//# sourceMappingURL=surfaceNotes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"surfaceNotes.js","sources":["../../../src/features/chat/surfaceNotes.ts"],"sourcesContent":["/**\n * Markers a surface skill writes for its surface, not for the reader (web Web Builder\n * `assistantBubbleContent`, design-pro 3.11): the sandbox preview marker\n * (preview `https://3000-<id>.e2b.app`) - a link that dies with the sandbox; the site\n * opens in Web Builder, which wakes it - and the restore point's id. Pure.\n */\nexport function stripSurfaceNotes(text: string): string {\n if (!text || (!text.includes('(preview `') && !text.includes('(snapshot `'))) return text;\n return text.replace(/(restore point) \\(snapshot `[^`]+`\\)/gi, '$1').replace(/\\s*\\(preview `[^`]+`\\)/gi, '');\n}\n"],"names":[],"mappings":"AAMO,SAAS,kBAAkB,IAAsB,EAAA;AACtD,EAAA,IAAI,CAAC,IAAA,IAAQ,CAAC,IAAA,CAAK,QAAS,CAAA,YAAY,CAAK,IAAA,CAAC,IAAK,CAAA,QAAA,CAAS,aAAa,CAAA,EAAU,OAAA,IAAA;AACnF,EAAA,OAAO,KAAK,OAAQ,CAAA,wCAAA,EAA0C,IAAI,CAAE,CAAA,OAAA,CAAQ,4BAA4B,EAAE,CAAA;AAC5G"}
@@ -1 +1 @@
1
- {"version":3,"file":"readings.js","sources":["../../../src/features/devices/readings.ts"],"sourcesContent":["/**\n * What a paired Bluetooth device measured, and how it reaches the Fitness\n * backend.\n *\n * The backend keeps ONE day per source and replaces it whole on every upload,\n * so a reading is never sent alone: the app keeps every reading it took (the\n * device store), and each upload rebuilds the full `device` day for the dates\n * that changed. A blood-pressure reading in the evening never wipes the\n * morning's weigh-in.\n *\n * Reading ids are stable (device + what + when): a monitor that sends its\n * whole memory on every connection, or the same reading arriving twice, is\n * stored once.\n */\n\nimport type { BodyComposition } from './bodyComposition';\nimport { sanitizeDay, hasData } from '../health/buildDays';\nimport type { TimedValue } from '../health/model';\nimport type { HealthDay, HealthReading, HealthWorkout, MealRelation } from '../health/types';\nimport type { ZoneClock } from '../health/time';\nimport { zoneMinutes } from '../health/zones';\n\ninterface ReadingBase {\n id: string;\n /** Epoch ms of the measurement. */\n at: number;\n deviceId: string;\n deviceName: string;\n}\n\nexport interface BodyReading extends ReadingBase {\n kind: 'body';\n weightKg?: number;\n bodyFatPct?: number;\n leanMassKg?: number;\n bmi?: number;\n /**\n * Whole-body impedance in ohms, when the scale sends it. A scale that only\n * reports weight and impedance (RENPHO's common protocol) leaves body fat\n * to Yantra's own estimate; one that computes body fat itself (R-A017) also\n * fills bodyFatPct and bmi.\n */\n impedanceOhm?: number;\n /**\n * The full body composition (muscle, bone, water, BMR, metabolic age ...),\n * computed from weight + body fat + the person's height/age/sex when all are\n * known (bodyComposition.ts). Shown on the reading; the backend day keeps\n * the fields it supports.\n */\n composition?: BodyComposition;\n}\n\nexport interface BloodPressureReading extends ReadingBase {\n kind: 'blood-pressure';\n systolic: number;\n diastolic: number;\n pulse?: number;\n /** What the monitor flagged (moved, loose cuff, irregular pulse), in words. */\n problems?: string[];\n}\n\nexport interface TemperatureReading extends ReadingBase {\n kind: 'temperature';\n celsius: number;\n}\n\nexport interface OxygenReading extends ReadingBase {\n kind: 'spo2';\n spo2: number;\n pulse?: number;\n}\n\nexport interface GlucoseReading extends ReadingBase {\n kind: 'glucose';\n mgdl: number;\n meal?: MealRelation;\n}\n\n/** A heart-rate strap worn for a session: the samples, summed up when it ended. */\nexport interface HeartRateSession extends ReadingBase {\n kind: 'heart-rate';\n /** `at` is the start. */\n end: number;\n /** run | walk | cycle | strength | hiit | other, as the person picked it. */\n workoutType: string;\n avgHr: number;\n maxHr: number;\n /** At most one sample every few seconds (see thinSamples); emptied once saved, see compactSession. */\n samples: TimedValue[];\n /** Minutes per heart-rate zone, kept when the samples are let go. */\n zoneMinutes?: number[];\n}\n\n/**\n * A finished exercise a device recorded itself: a jump-rope session or a\n * treadmill walk/run. Becomes one HealthWorkout in the day (readings with no\n * heart-rate samples, unlike HeartRateSession).\n */\nexport interface WorkoutReading extends ReadingBase {\n kind: 'workout';\n /** `at` is the start. */\n end: number;\n /** jump-rope | run | walk | ... lowercased. */\n workoutType: string;\n durationMin: number;\n calories?: number;\n distanceMeters?: number;\n /** Jump rope only: the number of skips. */\n jumps?: number;\n avgHr?: number;\n maxHr?: number;\n}\n\n/** A tape-measure girth reading of one body part. Only the waist has a home in the day. */\nexport interface BodyMeasurementReading extends ReadingBase {\n kind: 'body-measurement';\n /** waist | chest | hip | left_arm | ... as the tape reported it. */\n bodyPart: string;\n cm: number;\n}\n\nexport type DeviceReading =\n | BodyReading\n | BloodPressureReading\n | TemperatureReading\n | OxygenReading\n | GlucoseReading\n | HeartRateSession\n | WorkoutReading\n | BodyMeasurementReading;\n\nconst round1 = (n: number) => Math.round(n * 10) / 10;\n\n/** A stable id: the same measurement from the same device always gets the same one. */\nexport function readingId(deviceId: string, kind: DeviceReading['kind'], at: number, extra?: string | number): string {\n return [deviceId, kind, Math.round(at / 1000), extra].filter((p) => p !== undefined && p !== '').join(':');\n}\n\n/** Keep one heart-rate sample per `everyMs`: enough for zones, few enough to store and save. */\nexport function thinSamples(samples: readonly TimedValue[], everyMs = 5_000): TimedValue[] {\n const out: TimedValue[] = [];\n let last = -Infinity;\n for (const s of [...samples].sort((a, b) => a.t - b.t)) {\n if (s.t - last >= everyMs) {\n out.push(s);\n last = s.t;\n }\n }\n return out;\n}\n\nconst latestOf = <T extends DeviceReading>(list: readonly T[], has: (r: T) => boolean): T | undefined =>\n list.filter(has).reduce<T | undefined>((a, b) => (!a || b.at >= a.at ? b : a), undefined);\n\nexport interface DeviceDaysOptions {\n clock: ZoneClock;\n syncedAt: string;\n /** 208 - 0.7 x age, for heart-rate zones; undefined when the birth date is unknown. */\n maxHr?: number;\n /** Only these dates (the ones a new reading touched); every date when omitted. */\n dates?: ReadonlySet<string>;\n}\n\n/** Every reading -> one `device` HealthDay per local date, cleaned exactly as the backend cleans. */\nexport function deviceHealthDays(\n readings: readonly DeviceReading[],\n opts: DeviceDaysOptions,\n): { days: HealthDay[]; dropped: string[] } {\n const byDate = new Map<string, DeviceReading[]>();\n for (const r of readings) {\n const date = opts.clock.dateOf(r.at);\n if (opts.dates && !opts.dates.has(date)) continue;\n byDate.set(date, [...(byDate.get(date) ?? []), r]);\n }\n const dropped: string[] = [];\n const days: HealthDay[] = [];\n for (const [date, list] of [...byDate.entries()].sort(([a], [b]) => a.localeCompare(b))) {\n const day: HealthDay = { date, source: 'device' };\n const body = list.filter((r): r is BodyReading => r.kind === 'body');\n const weight = latestOf(body, (r) => r.weightKg !== undefined);\n if (weight) day.weightKg = weight.weightKg;\n const fat = latestOf(body, (r) => r.bodyFatPct !== undefined);\n if (fat) day.bodyFatPct = fat.bodyFatPct;\n const lean = latestOf(body, (r) => r.leanMassKg !== undefined);\n if (lean) day.leanMassKg = lean.leanMassKg;\n const bmi = latestOf(body, (r) => r.bmi !== undefined);\n if (bmi) day.bmi = bmi.bmi;\n const temp = latestOf(\n list.filter((r): r is TemperatureReading => r.kind === 'temperature'),\n () => true,\n );\n if (temp) day.bodyTempC = temp.celsius;\n const spo2 = latestOf(\n list.filter((r): r is OxygenReading => r.kind === 'spo2'),\n () => true,\n );\n if (spo2) day.spo2 = spo2.spo2;\n const waist = latestOf(\n list.filter((r): r is BodyMeasurementReading => r.kind === 'body-measurement' && r.bodyPart === 'waist'),\n () => true,\n );\n if (waist) day.waistCm = waist.cm;\n\n const readingsOut: HealthReading[] = [];\n for (const r of list) {\n if (r.kind === 'blood-pressure') {\n readingsOut.push({\n id: r.id,\n at: new Date(r.at).toISOString(),\n kind: 'blood-pressure',\n systolic: r.systolic,\n diastolic: r.diastolic,\n ...(r.pulse !== undefined ? { pulse: r.pulse } : {}),\n device: r.deviceName,\n });\n } else if (r.kind === 'glucose') {\n readingsOut.push({\n id: r.id,\n at: new Date(r.at).toISOString(),\n kind: 'glucose',\n glucoseMgDl: r.mgdl,\n ...(r.meal ? { meal: r.meal } : {}),\n device: r.deviceName,\n });\n }\n }\n if (readingsOut.length) day.readings = readingsOut.sort((a, b) => a.at.localeCompare(b.at));\n\n const workouts: HealthWorkout[] = list\n .filter((r): r is HeartRateSession => r.kind === 'heart-rate')\n .sort((a, b) => a.at - b.at)\n .map((s) => {\n const w: HealthWorkout = {\n id: s.id,\n start: new Date(s.at).toISOString(),\n end: new Date(s.end).toISOString(),\n type: s.workoutType,\n durationMin: round1((s.end - s.at) / 60_000),\n avgHr: s.avgHr,\n maxHr: s.maxHr,\n };\n if (opts.maxHr && s.samples.length) {\n w.zoneMinutes = zoneMinutes(s.samples, { start: s.at, end: s.end }, opts.maxHr);\n } else if (s.zoneMinutes) w.zoneMinutes = s.zoneMinutes;\n return w;\n });\n // Device-recorded workouts (jump rope, treadmill): same HealthWorkout shape, no HR samples.\n for (const s of list.filter((r): r is WorkoutReading => r.kind === 'workout').sort((a, b) => a.at - b.at)) {\n const w: HealthWorkout = {\n id: s.id,\n start: new Date(s.at).toISOString(),\n end: new Date(s.end).toISOString(),\n type: s.workoutType,\n durationMin: round1(s.durationMin),\n ...(s.calories !== undefined ? { calories: s.calories } : {}),\n ...(s.distanceMeters !== undefined ? { distanceMeters: s.distanceMeters } : {}),\n ...(s.avgHr !== undefined ? { avgHr: s.avgHr } : {}),\n ...(s.maxHr !== undefined ? { maxHr: s.maxHr } : {}),\n };\n workouts.push(w);\n }\n if (workouts.length) day.workouts = workouts.sort((a, b) => a.start.localeCompare(b.start));\n\n const clean = sanitizeDay(day, dropped);\n if (hasData(clean)) days.push({ ...clean, syncedAt: opts.syncedAt });\n }\n return { days, dropped };\n}\n\n/** One line for a reading, as the person reads it (\"82.4 kg, body fat 23.1%\"). */\nexport function describeReading(r: DeviceReading, units: 'metric' | 'imperial' = 'metric'): string {\n const kg = (v: number) => (units === 'imperial' ? `${round1(v / 0.45359237)} lb` : `${round1(v)} kg`);\n switch (r.kind) {\n case 'body': {\n const parts: string[] = [];\n if (r.weightKg !== undefined) parts.push(kg(r.weightKg));\n if (r.bodyFatPct !== undefined) parts.push(`body fat ${round1(r.bodyFatPct)}%`);\n if (r.bmi !== undefined) parts.push(`BMI ${round1(r.bmi)}`);\n return parts.join(', ') || 'A measurement with no weight';\n }\n case 'blood-pressure':\n return `${Math.round(r.systolic)}/${Math.round(r.diastolic)} mmHg${r.pulse ? `, pulse ${Math.round(r.pulse)}` : ''}`;\n case 'temperature':\n return units === 'imperial' ? `${round1((r.celsius * 9) / 5 + 32)} °F` : `${round1(r.celsius)} °C`;\n case 'spo2':\n return `Blood oxygen ${Math.round(r.spo2)}%${r.pulse ? `, pulse ${Math.round(r.pulse)}` : ''}`;\n case 'glucose':\n return `${Math.round(r.mgdl)} mg/dL${r.meal ? `, ${r.meal.replace('-', ' ')}` : ''}`;\n case 'heart-rate': {\n const minutes = Math.round((r.end - r.at) / 60_000);\n return `${minutes} min, average ${Math.round(r.avgHr)} bpm, max ${Math.round(r.maxHr)}`;\n }\n case 'workout': {\n const parts: string[] = [`${Math.round(r.durationMin)} min`];\n if (r.jumps !== undefined) parts.unshift(`${r.jumps} jumps`);\n if (r.distanceMeters !== undefined) parts.push(`${round1(r.distanceMeters / 1000)} km`);\n if (r.calories !== undefined) parts.push(`${Math.round(r.calories)} kcal`);\n return parts.join(', ');\n }\n case 'body-measurement': {\n const part = r.bodyPart.replace(/_/g, ' ');\n return units === 'imperial' ? `${part} ${round1(r.cm / 2.54)} in` : `${part} ${round1(r.cm)} cm`;\n }\n default:\n return '';\n }\n}\n\n/**\n * A session's samples are only needed until they are uploaded and saved to\n * the health store; after that the zones are kept and the samples let go, so\n * the phone's store stays small.\n */\nexport function compactSession(s: HeartRateSession, maxHr: number | undefined): HeartRateSession {\n if (!s.samples.length) return s;\n const zones = maxHr ? zoneMinutes(s.samples, { start: s.at, end: s.end }, maxHr) : s.zoneMinutes;\n return { ...s, samples: [], ...(zones ? { zoneMinutes: zones } : {}) };\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"readings.js","sources":["../../../src/features/devices/readings.ts"],"sourcesContent":["/**\n * What a paired Bluetooth device measured, and how it reaches the Fitness\n * backend.\n *\n * The backend keeps ONE day per source and replaces it whole on every upload,\n * so a reading is never sent alone: the app keeps every reading it took (the\n * device store), and each upload rebuilds the full `device` day for the dates\n * that changed. A blood-pressure reading in the evening never wipes the\n * morning's weigh-in.\n *\n * Reading ids are stable (device + what + when): a monitor that sends its\n * whole memory on every connection, or the same reading arriving twice, is\n * stored once.\n */\n\nimport { sanitizeDay, hasData } from '../health/buildDays';\nimport type { TimedValue } from '../health/model';\nimport type { HealthDay, HealthReading, HealthWorkout, MealRelation } from '../health/types';\nimport type { ZoneClock } from '../health/time';\nimport { zoneMinutes } from '../health/zones';\n\ninterface ReadingBase {\n id: string;\n /** Epoch ms of the measurement. */\n at: number;\n deviceId: string;\n deviceName: string;\n}\n\nexport interface BodyReading extends ReadingBase {\n kind: 'body';\n weightKg?: number;\n bodyFatPct?: number;\n leanMassKg?: number;\n bmi?: number;\n /**\n * Whole-body impedance in ohms, when the scale sends it. A scale that only\n * reports weight and impedance (RENPHO's common protocol) leaves body fat\n * to Yantra's own estimate; one that computes body fat itself (R-A017) also\n * fills bodyFatPct and bmi.\n */\n impedanceOhm?: number;\n}\n\nexport interface BloodPressureReading extends ReadingBase {\n kind: 'blood-pressure';\n systolic: number;\n diastolic: number;\n pulse?: number;\n /** What the monitor flagged (moved, loose cuff, irregular pulse), in words. */\n problems?: string[];\n}\n\nexport interface TemperatureReading extends ReadingBase {\n kind: 'temperature';\n celsius: number;\n}\n\nexport interface OxygenReading extends ReadingBase {\n kind: 'spo2';\n spo2: number;\n pulse?: number;\n}\n\nexport interface GlucoseReading extends ReadingBase {\n kind: 'glucose';\n mgdl: number;\n meal?: MealRelation;\n}\n\n/** A heart-rate strap worn for a session: the samples, summed up when it ended. */\nexport interface HeartRateSession extends ReadingBase {\n kind: 'heart-rate';\n /** `at` is the start. */\n end: number;\n /** run | walk | cycle | strength | hiit | other, as the person picked it. */\n workoutType: string;\n avgHr: number;\n maxHr: number;\n /** At most one sample every few seconds (see thinSamples); emptied once saved, see compactSession. */\n samples: TimedValue[];\n /** Minutes per heart-rate zone, kept when the samples are let go. */\n zoneMinutes?: number[];\n}\n\n/**\n * A finished exercise a device recorded itself: a jump-rope session or a\n * treadmill walk/run. Becomes one HealthWorkout in the day (readings with no\n * heart-rate samples, unlike HeartRateSession).\n */\nexport interface WorkoutReading extends ReadingBase {\n kind: 'workout';\n /** `at` is the start. */\n end: number;\n /** jump-rope | run | walk | ... lowercased. */\n workoutType: string;\n durationMin: number;\n calories?: number;\n distanceMeters?: number;\n /** Jump rope only: the number of skips. */\n jumps?: number;\n avgHr?: number;\n maxHr?: number;\n}\n\n/** A tape-measure girth reading of one body part. Only the waist has a home in the day. */\nexport interface BodyMeasurementReading extends ReadingBase {\n kind: 'body-measurement';\n /** waist | chest | hip | left_arm | ... as the tape reported it. */\n bodyPart: string;\n cm: number;\n}\n\nexport type DeviceReading =\n | BodyReading\n | BloodPressureReading\n | TemperatureReading\n | OxygenReading\n | GlucoseReading\n | HeartRateSession\n | WorkoutReading\n | BodyMeasurementReading;\n\nconst round1 = (n: number) => Math.round(n * 10) / 10;\n\n/** A stable id: the same measurement from the same device always gets the same one. */\nexport function readingId(deviceId: string, kind: DeviceReading['kind'], at: number, extra?: string | number): string {\n return [deviceId, kind, Math.round(at / 1000), extra].filter((p) => p !== undefined && p !== '').join(':');\n}\n\n/** Keep one heart-rate sample per `everyMs`: enough for zones, few enough to store and save. */\nexport function thinSamples(samples: readonly TimedValue[], everyMs = 5_000): TimedValue[] {\n const out: TimedValue[] = [];\n let last = -Infinity;\n for (const s of [...samples].sort((a, b) => a.t - b.t)) {\n if (s.t - last >= everyMs) {\n out.push(s);\n last = s.t;\n }\n }\n return out;\n}\n\nconst latestOf = <T extends DeviceReading>(list: readonly T[], has: (r: T) => boolean): T | undefined =>\n list.filter(has).reduce<T | undefined>((a, b) => (!a || b.at >= a.at ? b : a), undefined);\n\nexport interface DeviceDaysOptions {\n clock: ZoneClock;\n syncedAt: string;\n /** 208 - 0.7 x age, for heart-rate zones; undefined when the birth date is unknown. */\n maxHr?: number;\n /** Only these dates (the ones a new reading touched); every date when omitted. */\n dates?: ReadonlySet<string>;\n}\n\n/** Every reading -> one `device` HealthDay per local date, cleaned exactly as the backend cleans. */\nexport function deviceHealthDays(\n readings: readonly DeviceReading[],\n opts: DeviceDaysOptions,\n): { days: HealthDay[]; dropped: string[] } {\n const byDate = new Map<string, DeviceReading[]>();\n for (const r of readings) {\n const date = opts.clock.dateOf(r.at);\n if (opts.dates && !opts.dates.has(date)) continue;\n byDate.set(date, [...(byDate.get(date) ?? []), r]);\n }\n const dropped: string[] = [];\n const days: HealthDay[] = [];\n for (const [date, list] of [...byDate.entries()].sort(([a], [b]) => a.localeCompare(b))) {\n const day: HealthDay = { date, source: 'device' };\n const body = list.filter((r): r is BodyReading => r.kind === 'body');\n const weight = latestOf(body, (r) => r.weightKg !== undefined);\n if (weight) day.weightKg = weight.weightKg;\n const fat = latestOf(body, (r) => r.bodyFatPct !== undefined);\n if (fat) day.bodyFatPct = fat.bodyFatPct;\n const lean = latestOf(body, (r) => r.leanMassKg !== undefined);\n if (lean) day.leanMassKg = lean.leanMassKg;\n const bmi = latestOf(body, (r) => r.bmi !== undefined);\n if (bmi) day.bmi = bmi.bmi;\n const temp = latestOf(\n list.filter((r): r is TemperatureReading => r.kind === 'temperature'),\n () => true,\n );\n if (temp) day.bodyTempC = temp.celsius;\n const spo2 = latestOf(\n list.filter((r): r is OxygenReading => r.kind === 'spo2'),\n () => true,\n );\n if (spo2) day.spo2 = spo2.spo2;\n const waist = latestOf(\n list.filter((r): r is BodyMeasurementReading => r.kind === 'body-measurement' && r.bodyPart === 'waist'),\n () => true,\n );\n if (waist) day.waistCm = waist.cm;\n\n const readingsOut: HealthReading[] = [];\n for (const r of list) {\n if (r.kind === 'blood-pressure') {\n readingsOut.push({\n id: r.id,\n at: new Date(r.at).toISOString(),\n kind: 'blood-pressure',\n systolic: r.systolic,\n diastolic: r.diastolic,\n ...(r.pulse !== undefined ? { pulse: r.pulse } : {}),\n device: r.deviceName,\n });\n } else if (r.kind === 'glucose') {\n readingsOut.push({\n id: r.id,\n at: new Date(r.at).toISOString(),\n kind: 'glucose',\n glucoseMgDl: r.mgdl,\n ...(r.meal ? { meal: r.meal } : {}),\n device: r.deviceName,\n });\n }\n }\n if (readingsOut.length) day.readings = readingsOut.sort((a, b) => a.at.localeCompare(b.at));\n\n const workouts: HealthWorkout[] = list\n .filter((r): r is HeartRateSession => r.kind === 'heart-rate')\n .sort((a, b) => a.at - b.at)\n .map((s) => {\n const w: HealthWorkout = {\n id: s.id,\n start: new Date(s.at).toISOString(),\n end: new Date(s.end).toISOString(),\n type: s.workoutType,\n durationMin: round1((s.end - s.at) / 60_000),\n avgHr: s.avgHr,\n maxHr: s.maxHr,\n };\n if (opts.maxHr && s.samples.length) {\n w.zoneMinutes = zoneMinutes(s.samples, { start: s.at, end: s.end }, opts.maxHr);\n } else if (s.zoneMinutes) w.zoneMinutes = s.zoneMinutes;\n return w;\n });\n // Device-recorded workouts (jump rope, treadmill): same HealthWorkout shape, no HR samples.\n for (const s of list.filter((r): r is WorkoutReading => r.kind === 'workout').sort((a, b) => a.at - b.at)) {\n const w: HealthWorkout = {\n id: s.id,\n start: new Date(s.at).toISOString(),\n end: new Date(s.end).toISOString(),\n type: s.workoutType,\n durationMin: round1(s.durationMin),\n ...(s.calories !== undefined ? { calories: s.calories } : {}),\n ...(s.distanceMeters !== undefined ? { distanceMeters: s.distanceMeters } : {}),\n ...(s.avgHr !== undefined ? { avgHr: s.avgHr } : {}),\n ...(s.maxHr !== undefined ? { maxHr: s.maxHr } : {}),\n };\n workouts.push(w);\n }\n if (workouts.length) day.workouts = workouts.sort((a, b) => a.start.localeCompare(b.start));\n\n const clean = sanitizeDay(day, dropped);\n if (hasData(clean)) days.push({ ...clean, syncedAt: opts.syncedAt });\n }\n return { days, dropped };\n}\n\n/** One line for a reading, as the person reads it (\"82.4 kg, body fat 23.1%\"). */\nexport function describeReading(r: DeviceReading, units: 'metric' | 'imperial' = 'metric'): string {\n const kg = (v: number) => (units === 'imperial' ? `${round1(v / 0.45359237)} lb` : `${round1(v)} kg`);\n switch (r.kind) {\n case 'body': {\n const parts: string[] = [];\n if (r.weightKg !== undefined) parts.push(kg(r.weightKg));\n if (r.bodyFatPct !== undefined) parts.push(`body fat ${round1(r.bodyFatPct)}%`);\n if (r.bmi !== undefined) parts.push(`BMI ${round1(r.bmi)}`);\n return parts.join(', ') || 'A measurement with no weight';\n }\n case 'blood-pressure':\n return `${Math.round(r.systolic)}/${Math.round(r.diastolic)} mmHg${r.pulse ? `, pulse ${Math.round(r.pulse)}` : ''}`;\n case 'temperature':\n return units === 'imperial' ? `${round1((r.celsius * 9) / 5 + 32)} °F` : `${round1(r.celsius)} °C`;\n case 'spo2':\n return `Blood oxygen ${Math.round(r.spo2)}%${r.pulse ? `, pulse ${Math.round(r.pulse)}` : ''}`;\n case 'glucose':\n return `${Math.round(r.mgdl)} mg/dL${r.meal ? `, ${r.meal.replace('-', ' ')}` : ''}`;\n case 'heart-rate': {\n const minutes = Math.round((r.end - r.at) / 60_000);\n return `${minutes} min, average ${Math.round(r.avgHr)} bpm, max ${Math.round(r.maxHr)}`;\n }\n case 'workout': {\n const parts: string[] = [`${Math.round(r.durationMin)} min`];\n if (r.jumps !== undefined) parts.unshift(`${r.jumps} jumps`);\n if (r.distanceMeters !== undefined) parts.push(`${round1(r.distanceMeters / 1000)} km`);\n if (r.calories !== undefined) parts.push(`${Math.round(r.calories)} kcal`);\n return parts.join(', ');\n }\n case 'body-measurement': {\n const part = r.bodyPart.replace(/_/g, ' ');\n return units === 'imperial' ? `${part} ${round1(r.cm / 2.54)} in` : `${part} ${round1(r.cm)} cm`;\n }\n default:\n return '';\n }\n}\n\n/**\n * A session's samples are only needed until they are uploaded and saved to\n * the health store; after that the zones are kept and the samples let go, so\n * the phone's store stays small.\n */\nexport function compactSession(s: HeartRateSession, maxHr: number | undefined): HeartRateSession {\n if (!s.samples.length) return s;\n const zones = maxHr ? zoneMinutes(s.samples, { start: s.at, end: s.end }, maxHr) : s.zoneMinutes;\n return { ...s, samples: [], ...(zones ? { zoneMinutes: zones } : {}) };\n}\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;AA2GA,MAAM,SAAS,CAAC,CAAA,KAAc,KAAK,KAAM,CAAA,CAAA,GAAI,EAAE,CAAI,GAAA,EAAA;AAG5C,SAAS,SAAU,CAAA,QAAA,EAAkB,IAA6B,EAAA,EAAA,EAAY,KAAiC,EAAA;AACpH,EAAA,OAAO,CAAC,QAAU,EAAA,IAAA,EAAM,KAAK,KAAM,CAAA,EAAA,GAAK,GAAI,CAAG,EAAA,KAAK,CAAE,CAAA,MAAA,CAAO,OAAK,CAAM,KAAA,MAAA,IAAa,MAAM,EAAE,CAAA,CAAE,KAAK,GAAG,CAAA;AACzG;AAGgB,SAAA,WAAA,CAAY,OAAgC,EAAA,OAAA,GAAU,GAAqB,EAAA;AACzF,EAAA,MAAM,MAAoB,EAAC;AAC3B,EAAA,IAAI,IAAO,GAAA,CAAA,QAAA;AACX,EAAA,KAAA,MAAW,CAAK,IAAA,CAAC,GAAG,OAAO,CAAE,CAAA,IAAA,CAAK,CAAC,CAAA,EAAG,CAAM,KAAA,CAAA,CAAE,CAAI,GAAA,CAAA,CAAE,CAAC,CAAG,EAAA;AACtD,IAAI,IAAA,CAAA,CAAE,CAAI,GAAA,IAAA,IAAQ,OAAS,EAAA;AACzB,MAAA,GAAA,CAAI,KAAK,CAAC,CAAA;AACV,MAAA,IAAA,GAAO,CAAE,CAAA,CAAA;AAAA;AACX;AAEF,EAAO,OAAA,GAAA;AACT;AACA,MAAM,QAAA,GAAW,CAA2B,IAAoB,EAAA,GAAA,KAA0C,KAAK,MAAO,CAAA,GAAG,EAAE,MAAsB,CAAA,CAAC,GAAG,CAAM,KAAA,CAAC,KAAK,CAAE,CAAA,EAAA,IAAM,EAAE,EAAK,GAAA,CAAA,GAAI,GAAG,MAAS,CAAA;AAWhL,SAAA,gBAAA,CAAiB,UAAoC,IAGnE,EAAA;AA5IF,EAAA,IAAA,EAAA;AA6IE,EAAM,MAAA,MAAA,uBAAa,GAA6B,EAAA;AAChD,EAAA,KAAA,MAAW,KAAK,QAAU,EAAA;AACxB,IAAA,MAAM,IAAO,GAAA,IAAA,CAAK,KAAM,CAAA,MAAA,CAAO,EAAE,EAAE,CAAA;AACnC,IAAA,IAAI,KAAK,KAAS,IAAA,CAAC,KAAK,KAAM,CAAA,GAAA,CAAI,IAAI,CAAG,EAAA;AACzC,IAAA,MAAA,CAAO,GAAI,CAAA,IAAA,EAAM,CAAC,GAAA,CAAI,EAAO,GAAA,MAAA,CAAA,GAAA,CAAI,IAAI,CAAA,KAAf,IAAoB,GAAA,EAAA,GAAA,EAAK,EAAA,CAAC,CAAC,CAAA;AAAA;AAEnD,EAAA,MAAM,UAAoB,EAAC;AAC3B,EAAA,MAAM,OAAoB,EAAC;AAC3B,EAAW,KAAA,MAAA,CAAC,MAAM,IAAI,CAAA,IAAK,CAAC,GAAG,MAAA,CAAO,OAAQ,EAAC,CAAE,CAAA,IAAA,CAAK,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,MAAM,CAAE,CAAA,aAAA,CAAc,CAAC,CAAC,CAAG,EAAA;AACvF,IAAA,MAAM,GAAiB,GAAA;AAAA,MACrB,IAAA;AAAA,MACA,MAAQ,EAAA;AAAA,KACV;AACA,IAAA,MAAM,OAAO,IAAK,CAAA,MAAA,CAAO,CAAC,CAAwB,KAAA,CAAA,CAAE,SAAS,MAAM,CAAA;AACnE,IAAA,MAAM,SAAS,QAAS,CAAA,IAAA,EAAM,CAAK,CAAA,KAAA,CAAA,CAAE,aAAa,MAAS,CAAA;AAC3D,IAAI,IAAA,MAAA,EAAY,GAAA,CAAA,QAAA,GAAW,MAAO,CAAA,QAAA;AAClC,IAAA,MAAM,MAAM,QAAS,CAAA,IAAA,EAAM,CAAK,CAAA,KAAA,CAAA,CAAE,eAAe,MAAS,CAAA;AAC1D,IAAI,IAAA,GAAA,EAAS,GAAA,CAAA,UAAA,GAAa,GAAI,CAAA,UAAA;AAC9B,IAAA,MAAM,OAAO,QAAS,CAAA,IAAA,EAAM,CAAK,CAAA,KAAA,CAAA,CAAE,eAAe,MAAS,CAAA;AAC3D,IAAI,IAAA,IAAA,EAAU,GAAA,CAAA,UAAA,GAAa,IAAK,CAAA,UAAA;AAChC,IAAA,MAAM,MAAM,QAAS,CAAA,IAAA,EAAM,CAAK,CAAA,KAAA,CAAA,CAAE,QAAQ,MAAS,CAAA;AACnD,IAAI,IAAA,GAAA,EAAS,GAAA,CAAA,GAAA,GAAM,GAAI,CAAA,GAAA;AACvB,IAAM,MAAA,IAAA,GAAO,QAAS,CAAA,IAAA,CAAK,MAAO,CAAA,CAAC,CAA+B,KAAA,CAAA,CAAE,IAAS,KAAA,aAAa,CAAG,EAAA,MAAM,IAAI,CAAA;AACvG,IAAI,IAAA,IAAA,EAAU,GAAA,CAAA,SAAA,GAAY,IAAK,CAAA,OAAA;AAC/B,IAAM,MAAA,IAAA,GAAO,QAAS,CAAA,IAAA,CAAK,MAAO,CAAA,CAAC,CAA0B,KAAA,CAAA,CAAE,IAAS,KAAA,MAAM,CAAG,EAAA,MAAM,IAAI,CAAA;AAC3F,IAAI,IAAA,IAAA,EAAU,GAAA,CAAA,IAAA,GAAO,IAAK,CAAA,IAAA;AAC1B,IAAA,MAAM,KAAQ,GAAA,QAAA,CAAS,IAAK,CAAA,MAAA,CAAO,CAAC,CAAmC,KAAA,CAAA,CAAE,IAAS,KAAA,kBAAA,IAAsB,CAAE,CAAA,QAAA,KAAa,OAAO,CAAA,EAAG,MAAM,IAAI,CAAA;AAC3I,IAAI,IAAA,KAAA,EAAW,GAAA,CAAA,OAAA,GAAU,KAAM,CAAA,EAAA;AAC/B,IAAA,MAAM,cAA+B,EAAC;AACtC,IAAA,KAAA,MAAW,KAAK,IAAM,EAAA;AACpB,MAAI,IAAA,CAAA,CAAE,SAAS,gBAAkB,EAAA;AAC/B,QAAA,WAAA,CAAY,IAAK,CAAA,aAAA,CAAA,cAAA,CAAA;AAAA,UACf,IAAI,CAAE,CAAA,EAAA;AAAA,UACN,IAAI,IAAI,IAAA,CAAK,CAAE,CAAA,EAAE,EAAE,WAAY,EAAA;AAAA,UAC/B,IAAM,EAAA,gBAAA;AAAA,UACN,UAAU,CAAE,CAAA,QAAA;AAAA,UACZ,WAAW,CAAE,CAAA;AAAA,SACT,EAAA,CAAA,CAAE,UAAU,MAAY,GAAA;AAAA,UAC1B,OAAO,CAAE,CAAA;AAAA,SACX,GAAI,EARW,CAAA,EAAA;AAAA,UASf,QAAQ,CAAE,CAAA;AAAA,SACX,CAAA,CAAA;AAAA,OACH,MAAA,IAAW,CAAE,CAAA,IAAA,KAAS,SAAW,EAAA;AAC/B,QAAA,WAAA,CAAY,IAAK,CAAA,aAAA,CAAA,cAAA,CAAA;AAAA,UACf,IAAI,CAAE,CAAA,EAAA;AAAA,UACN,IAAI,IAAI,IAAA,CAAK,CAAE,CAAA,EAAE,EAAE,WAAY,EAAA;AAAA,UAC/B,IAAM,EAAA,SAAA;AAAA,UACN,aAAa,CAAE,CAAA;AAAA,SAAA,EACX,EAAE,IAAO,GAAA;AAAA,UACX,MAAM,CAAE,CAAA;AAAA,SACV,GAAI,EAPW,CAAA,EAAA;AAAA,UAQf,QAAQ,CAAE,CAAA;AAAA,SACX,CAAA,CAAA;AAAA;AACH;AAEF,IAAA,IAAI,WAAY,CAAA,MAAA,EAAY,GAAA,CAAA,QAAA,GAAW,YAAY,IAAK,CAAA,CAAC,CAAG,EAAA,CAAA,KAAM,CAAE,CAAA,EAAA,CAAG,aAAc,CAAA,CAAA,CAAE,EAAE,CAAC,CAAA;AAC1F,IAAA,MAAM,WAA4B,IAAK,CAAA,MAAA,CAAO,CAAC,CAA6B,KAAA,CAAA,CAAE,SAAS,YAAY,CAAA,CAAE,KAAK,CAAC,CAAA,EAAG,MAAM,CAAE,CAAA,EAAA,GAAK,EAAE,EAAE,CAAA,CAAE,IAAI,CAAK,CAAA,KAAA;AACxI,MAAA,MAAM,CAAmB,GAAA;AAAA,QACvB,IAAI,CAAE,CAAA,EAAA;AAAA,QACN,OAAO,IAAI,IAAA,CAAK,CAAE,CAAA,EAAE,EAAE,WAAY,EAAA;AAAA,QAClC,KAAK,IAAI,IAAA,CAAK,CAAE,CAAA,GAAG,EAAE,WAAY,EAAA;AAAA,QACjC,MAAM,CAAE,CAAA,WAAA;AAAA,QACR,aAAa,MAAQ,CAAA,CAAA,CAAA,CAAE,GAAM,GAAA,CAAA,CAAE,MAAM,GAAM,CAAA;AAAA,QAC3C,OAAO,CAAE,CAAA,KAAA;AAAA,QACT,OAAO,CAAE,CAAA;AAAA,OACX;AACA,MAAA,IAAI,IAAK,CAAA,KAAA,IAAS,CAAE,CAAA,OAAA,CAAQ,MAAQ,EAAA;AAClC,QAAE,CAAA,CAAA,WAAA,GAAc,WAAY,CAAA,CAAA,CAAE,OAAS,EAAA;AAAA,UACrC,OAAO,CAAE,CAAA,EAAA;AAAA,UACT,KAAK,CAAE,CAAA;AAAA,SACT,EAAG,KAAK,KAAK,CAAA;AAAA,OACJ,MAAA,IAAA,CAAA,CAAE,WAAa,EAAA,CAAA,CAAE,cAAc,CAAE,CAAA,WAAA;AAC5C,MAAO,OAAA,CAAA;AAAA,KACR,CAAA;AAED,IAAA,KAAA,MAAW,KAAK,IAAK,CAAA,MAAA,CAAO,CAAC,CAAA,KAA2B,EAAE,IAAS,KAAA,SAAS,CAAE,CAAA,IAAA,CAAK,CAAC,CAAG,EAAA,CAAA,KAAM,EAAE,EAAK,GAAA,CAAA,CAAE,EAAE,CAAG,EAAA;AACzG,MAAA,MAAM,CAAmB,GAAA,cAAA,CAAA,cAAA,CAAA,cAAA,CAAA,cAAA,CAAA;AAAA,QACvB,IAAI,CAAE,CAAA,EAAA;AAAA,QACN,OAAO,IAAI,IAAA,CAAK,CAAE,CAAA,EAAE,EAAE,WAAY,EAAA;AAAA,QAClC,KAAK,IAAI,IAAA,CAAK,CAAE,CAAA,GAAG,EAAE,WAAY,EAAA;AAAA,QACjC,MAAM,CAAE,CAAA,WAAA;AAAA,QACR,WAAA,EAAa,MAAO,CAAA,CAAA,CAAE,WAAW;AAAA,OAC7B,EAAA,CAAA,CAAE,aAAa,MAAY,GAAA;AAAA,QAC7B,UAAU,CAAE,CAAA;AAAA,OACV,GAAA,EACA,CAAA,EAAA,CAAA,CAAE,mBAAmB,MAAY,GAAA;AAAA,QACnC,gBAAgB,CAAE,CAAA;AAAA,OAChB,GAAA,EACA,CAAA,EAAA,CAAA,CAAE,UAAU,MAAY,GAAA;AAAA,QAC1B,OAAO,CAAE,CAAA;AAAA,OACP,GAAA,EACA,CAAA,EAAA,CAAA,CAAE,UAAU,MAAY,GAAA;AAAA,QAC1B,OAAO,CAAE,CAAA;AAAA,UACP,EAAC,CAAA;AAEP,MAAA,QAAA,CAAS,KAAK,CAAC,CAAA;AAAA;AAEjB,IAAA,IAAI,QAAS,CAAA,MAAA,EAAY,GAAA,CAAA,QAAA,GAAW,SAAS,IAAK,CAAA,CAAC,CAAG,EAAA,CAAA,KAAM,CAAE,CAAA,KAAA,CAAM,aAAc,CAAA,CAAA,CAAE,KAAK,CAAC,CAAA;AAC1F,IAAM,MAAA,KAAA,GAAQ,WAAY,CAAA,GAAA,EAAK,OAAO,CAAA;AACtC,IAAA,IAAI,QAAQ,KAAK,CAAA,EAAQ,IAAA,CAAA,IAAA,CAAK,iCACzB,KADyB,CAAA,EAAA;AAAA,MAE5B,UAAU,IAAK,CAAA;AAAA,KAChB,CAAA,CAAA;AAAA;AAEH,EAAO,OAAA;AAAA,IACL,IAAA;AAAA,IACA;AAAA,GACF;AACF;AAGgB,SAAA,eAAA,CAAgB,CAAkB,EAAA,KAAA,GAA+B,QAAkB,EAAA;AACjG,EAAA,MAAM,EAAK,GAAA,CAAC,CAAc,KAAA,KAAA,KAAU,aAAa,CAAG,EAAA,MAAA,CAAO,CAAI,GAAA,UAAU,CAAC,CAAA,GAAA,CAAA,GAAQ,CAAG,EAAA,MAAA,CAAO,CAAC,CAAC,CAAA,GAAA,CAAA;AAC9F,EAAA,QAAQ,EAAE,IAAM;AAAA,IACd,KAAK,MACH,EAAA;AACE,MAAA,MAAM,QAAkB,EAAC;AACzB,MAAI,IAAA,CAAA,CAAE,aAAa,MAAW,EAAA,KAAA,CAAM,KAAK,EAAG,CAAA,CAAA,CAAE,QAAQ,CAAC,CAAA;AACvD,MAAI,IAAA,CAAA,CAAE,UAAe,KAAA,MAAA,EAAiB,KAAA,CAAA,IAAA,CAAK,YAAY,MAAO,CAAA,CAAA,CAAE,UAAU,CAAC,CAAG,CAAA,CAAA,CAAA;AAC9E,MAAI,IAAA,CAAA,CAAE,GAAQ,KAAA,MAAA,EAAiB,KAAA,CAAA,IAAA,CAAK,OAAO,MAAO,CAAA,CAAA,CAAE,GAAG,CAAC,CAAE,CAAA,CAAA;AAC1D,MAAO,OAAA,KAAA,CAAM,IAAK,CAAA,IAAI,CAAK,IAAA,8BAAA;AAAA;AAC7B,IACF,KAAK,gBAAA;AACH,MAAO,OAAA,CAAA,EAAG,KAAK,KAAM,CAAA,CAAA,CAAE,QAAQ,CAAC,CAAA,CAAA,EAAI,IAAK,CAAA,KAAA,CAAM,CAAE,CAAA,SAAS,CAAC,CAAQ,KAAA,EAAA,CAAA,CAAE,QAAQ,CAAW,QAAA,EAAA,IAAA,CAAK,MAAM,CAAE,CAAA,KAAK,CAAC,CAAA,CAAA,GAAK,EAAE,CAAA,CAAA;AAAA,IACpH,KAAK,aAAA;AACH,MAAA,OAAO,KAAU,KAAA,UAAA,GAAa,CAAG,EAAA,MAAA,CAAO,EAAE,OAAU,GAAA,CAAA,GAAI,CAAI,GAAA,EAAE,CAAC,CAAQ,MAAA,CAAA,GAAA,CAAA,EAAG,MAAO,CAAA,CAAA,CAAE,OAAO,CAAC,CAAA,MAAA,CAAA;AAAA,IAC7F,KAAK,MAAA;AACH,MAAA,OAAO,gBAAgB,IAAK,CAAA,KAAA,CAAM,CAAE,CAAA,IAAI,CAAC,CAAI,CAAA,EAAA,CAAA,CAAE,KAAQ,GAAA,CAAA,QAAA,EAAW,KAAK,KAAM,CAAA,CAAA,CAAE,KAAK,CAAC,KAAK,EAAE,CAAA,CAAA;AAAA,IAC9F,KAAK,SAAA;AACH,MAAA,OAAO,GAAG,IAAK,CAAA,KAAA,CAAM,CAAE,CAAA,IAAI,CAAC,CAAS,MAAA,EAAA,CAAA,CAAE,IAAO,GAAA,CAAA,EAAA,EAAK,EAAE,IAAK,CAAA,OAAA,CAAQ,KAAK,GAAG,CAAC,KAAK,EAAE,CAAA,CAAA;AAAA,IACpF,KAAK,YACH,EAAA;AACE,MAAA,MAAM,UAAU,IAAK,CAAA,KAAA,CAAA,CAAO,EAAE,GAAM,GAAA,CAAA,CAAE,MAAM,GAAM,CAAA;AAClD,MAAA,OAAO,CAAG,EAAA,OAAO,CAAiB,cAAA,EAAA,IAAA,CAAK,KAAM,CAAA,CAAA,CAAE,KAAK,CAAC,CAAa,UAAA,EAAA,IAAA,CAAK,KAAM,CAAA,CAAA,CAAE,KAAK,CAAC,CAAA,CAAA;AAAA;AACvF,IACF,KAAK,SACH,EAAA;AACE,MAAM,MAAA,KAAA,GAAkB,CAAC,CAAG,EAAA,IAAA,CAAK,MAAM,CAAE,CAAA,WAAW,CAAC,CAAM,IAAA,CAAA,CAAA;AAC3D,MAAI,IAAA,CAAA,CAAE,UAAU,MAAW,EAAA,KAAA,CAAM,QAAQ,CAAG,EAAA,CAAA,CAAE,KAAK,CAAQ,MAAA,CAAA,CAAA;AAC3D,MAAI,IAAA,CAAA,CAAE,cAAmB,KAAA,MAAA,EAAiB,KAAA,CAAA,IAAA,CAAK,CAAG,EAAA,MAAA,CAAO,CAAE,CAAA,cAAA,GAAiB,GAAI,CAAC,CAAK,GAAA,CAAA,CAAA;AACtF,MAAI,IAAA,CAAA,CAAE,QAAa,KAAA,MAAA,EAAiB,KAAA,CAAA,IAAA,CAAK,CAAG,EAAA,IAAA,CAAK,KAAM,CAAA,CAAA,CAAE,QAAQ,CAAC,CAAO,KAAA,CAAA,CAAA;AACzE,MAAO,OAAA,KAAA,CAAM,KAAK,IAAI,CAAA;AAAA;AACxB,IACF,KAAK,kBACH,EAAA;AACE,MAAA,MAAM,IAAO,GAAA,CAAA,CAAE,QAAS,CAAA,OAAA,CAAQ,MAAM,GAAG,CAAA;AACzC,MAAA,OAAO,UAAU,UAAa,GAAA,CAAA,EAAG,IAAI,CAAA,CAAA,EAAI,OAAO,CAAE,CAAA,EAAA,GAAK,IAAI,CAAC,QAAQ,CAAG,EAAA,IAAI,IAAI,MAAO,CAAA,CAAA,CAAE,EAAE,CAAC,CAAA,GAAA,CAAA;AAAA;AAC7F,IACF;AACE,MAAO,OAAA,EAAA;AAAA;AAEb;AAOgB,SAAA,cAAA,CAAe,GAAqB,KAA6C,EAAA;AAC/F,EAAA,IAAI,CAAC,CAAA,CAAE,OAAQ,CAAA,MAAA,EAAe,OAAA,CAAA;AAC9B,EAAA,MAAM,KAAQ,GAAA,KAAA,GAAQ,WAAY,CAAA,CAAA,CAAE,OAAS,EAAA;AAAA,IAC3C,OAAO,CAAE,CAAA,EAAA;AAAA,IACT,KAAK,CAAE,CAAA;AAAA,GACT,EAAG,KAAK,CAAA,GAAI,CAAE,CAAA,WAAA;AACd,EAAA,OAAO,gDACF,CADE,CAAA,EAAA;AAAA,IAEL,SAAS;AAAC,GAAA,CAAA,EACN,KAAQ,GAAA;AAAA,IACV,WAAa,EAAA;AAAA,MACX,EAAC,CAAA;AAET"}
@@ -1,4 +1,4 @@
1
- import {deviceKind}from'./catalog.js';import {describeBleError}from'./link.js';import {thinSamples,readingId}from'./readings.js';import'./brands/frames.js';import'./brands/uuids.js';import'./gatt/uuids.js';import {detectBrand}from'./brands/detect.js';import {startDriver}from'./drivers.js';import {computeBodyComposition}from'./bodyComposition.js';var __defProp = Object.defineProperty;
1
+ import {deviceKind}from'./catalog.js';import {describeBleError}from'./link.js';import {thinSamples,readingId}from'./readings.js';import'./brands/frames.js';import'./brands/uuids.js';import'./gatt/uuids.js';import {detectBrand}from'./brands/detect.js';import {startDriver}from'./drivers.js';var __defProp = Object.defineProperty;
2
2
  var __defProps = Object.defineProperties;
3
3
  var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
4
4
  var __getOwnPropSymbols = Object.getOwnPropertySymbols;
@@ -24,26 +24,6 @@ function ageFrom(birthDate, now) {
24
24
  const t = Date.parse(`${birthDate}T00:00:00Z`);
25
25
  return Number.isFinite(t) ? Math.floor((now - t) / (365.25 * 864e5)) : void 0;
26
26
  }
27
- function enrichWithComposition(reading, facts) {
28
- var _a, _b, _c;
29
- if (reading.kind !== "body" || reading.weightKg === void 0 || reading.bodyFatPct === void 0) {
30
- return reading;
31
- }
32
- if (!facts.heightCm || facts.age === void 0) return reading;
33
- const composition = computeBodyComposition({
34
- weightKg: reading.weightKg,
35
- bodyFatPct: reading.bodyFatPct,
36
- heightCm: facts.heightCm,
37
- age: facts.age,
38
- sex: (_a = facts.sex) != null ? _a : "unspecified",
39
- impedanceOhm: reading.impedanceOhm
40
- });
41
- return __spreadProps(__spreadValues({}, reading), {
42
- bmi: (_b = reading.bmi) != null ? _b : composition.bmi,
43
- leanMassKg: (_c = reading.leanMassKg) != null ? _c : composition.leanMassKg,
44
- composition
45
- });
46
- }
47
27
  class DeviceSession {
48
28
  constructor(deps) {
49
29
  this.deps = deps;
@@ -207,7 +187,7 @@ class DeviceSession {
207
187
  } : void 0,
208
188
  scaleUser: paired == null ? void 0 : paired.scaleUser,
209
189
  saveScaleUser: (user) => store.saveScaleUser(link.id, user),
210
- onReading: (r) => void this.keep([enrichWithComposition(r, facts)]),
190
+ onReading: (r) => void this.keep([r]),
211
191
  onLive: (v) => this.onLive(v),
212
192
  onNotice: (m) => this.set({
213
193
  notices: [...this.snapshot.notices, m]
@@ -1 +1 @@
1
- {"version":3,"file":"session.js","sources":["../../../src/features/devices/session.ts"],"sourcesContent":["/**\n * One \"take a reading\" from start to end, for the screen: is Bluetooth on and\n * allowed, find the device (or the one already paired), connect, run its\n * driver, keep what it measures, and end cleanly when it goes to sleep (a\n * scale switches off seconds after a weighing; that is the normal end, not an\n * error).\n *\n * Heart-rate straps and continuous oximeters stream: the person starts and\n * stops a session (heart rate) or saves the value they see (oxygen).\n *\n * Plain class with injected transport and store, like the other stores: the\n * screen subscribes with useSyncExternalStore, tests drive it with fakes.\n */\nimport type { TimedValue } from '../health/model';\nimport { deviceKind, type DeviceKindId } from './catalog';\nimport { describeBleError, type BleTransport, type ConnectedLink, type FoundDevice, type RadioState } from './link';\nimport { detectBrand } from './brands';\nimport type { DeviceStore } from './deviceStore';\nimport { startDriver, type DriverHandle, type LiveValue, type PersonFacts } from './drivers';\nimport { computeBodyComposition } from './bodyComposition';\nimport { readingId, thinSamples, type DeviceReading, type HeartRateSession } from './readings';\n\nexport type SessionState =\n | { step: 'starting' }\n | { step: 'unavailable'; reason: string }\n | { step: 'radio'; radio: Exclude<RadioState, 'ready'> }\n | { step: 'scanning'; found: FoundDevice[]; hint?: string }\n | { step: 'connecting'; device: FoundDevice }\n | { step: 'connected'; device: { id: string; name: string } }\n | { step: 'ended'; device: { id: string; name: string } }\n | { step: 'error'; message: string };\n\nexport interface SessionSnapshot {\n state: SessionState;\n kind: DeviceKindId;\n /** What this session measured, newest first. */\n readings: DeviceReading[];\n live?: LiveValue;\n recording?: { startedAt: number; workoutType: string; avgHr?: number; maxHr?: number };\n notices: string[];\n}\n\nexport interface SessionDeps {\n transport: BleTransport | null;\n store: Pick<DeviceStore, 'pair' | 'addReadings' | 'saveScaleUser' | 'person' | 'getSnapshot'>;\n kind: DeviceKindId;\n /** Reconnect to this paired device instead of offering a list. */\n deviceId?: string;\n now?: () => number;\n}\n\n/** Scanning this long without finding anything: tell the person how to wake it. */\nexport const SCAN_HINT_MS = 20_000;\n\nfunction ageFrom(birthDate: string | undefined, now: number): number | undefined {\n if (!birthDate) return undefined;\n const t = Date.parse(`${birthDate}T00:00:00Z`);\n return Number.isFinite(t) ? Math.floor((now - t) / (365.25 * 86_400_000)) : undefined;\n}\n\n/**\n * Fill in the full body composition for a body reading once the scale has given\n * weight and body fat and the person's height/age/sex are known. Other readings\n * pass through unchanged.\n */\nfunction enrichWithComposition(reading: DeviceReading, facts: PersonFacts): DeviceReading {\n if (reading.kind !== 'body' || reading.weightKg === undefined || reading.bodyFatPct === undefined) {\n return reading;\n }\n if (!facts.heightCm || facts.age === undefined) return reading;\n const composition = computeBodyComposition({\n weightKg: reading.weightKg,\n bodyFatPct: reading.bodyFatPct,\n heightCm: facts.heightCm,\n age: facts.age,\n sex: facts.sex ?? 'unspecified',\n impedanceOhm: reading.impedanceOhm,\n });\n return {\n ...reading,\n bmi: reading.bmi ?? composition.bmi,\n leanMassKg: reading.leanMassKg ?? composition.leanMassKg,\n composition,\n };\n}\n\nexport class DeviceSession {\n private snapshot: SessionSnapshot;\n\n private listeners = new Set<() => void>();\n\n private stopScan: (() => void) | null = null;\n\n private link: ConnectedLink | null = null;\n\n private driver: DriverHandle | null = null;\n\n private hintTimer: ReturnType<typeof setTimeout> | null = null;\n\n private hr: { samples: TimedValue[]; sum: number; count: number; max: number } | null = null;\n\n private closed = false;\n\n constructor(private readonly deps: SessionDeps) {\n this.snapshot = { state: { step: 'starting' }, kind: deps.kind, readings: [], notices: [] };\n }\n\n subscribe = (listener: () => void) => {\n this.listeners.add(listener);\n return () => {\n this.listeners.delete(listener);\n };\n };\n\n getSnapshot = (): SessionSnapshot => this.snapshot;\n\n private now(): number {\n return this.deps.now ? this.deps.now() : Date.now();\n }\n\n private set(patch: Partial<SessionSnapshot>) {\n if (this.closed) return;\n this.snapshot = { ...this.snapshot, ...patch };\n this.listeners.forEach((l) => l());\n }\n\n async start(): Promise<void> {\n const { transport } = this.deps;\n if (!transport) {\n this.set({\n state: { step: 'unavailable', reason: 'Bluetooth devices need the latest version of the Yantra app.' },\n });\n return;\n }\n if (!(await transport.requestPermissions())) {\n this.set({ state: { step: 'radio', radio: 'unauthorized' } });\n return;\n }\n const radio = await transport.radio();\n if (radio !== 'ready' && radio !== 'unknown') {\n this.set({ state: { step: 'radio', radio } });\n return;\n }\n this.scan();\n }\n\n /** Back to looking for the device (after the radio came on, or to measure again). */\n scan(): void {\n const { transport } = this.deps;\n if (!transport || this.closed) return;\n this.stopScan?.();\n this.set({ state: { step: 'scanning', found: [] } });\n this.hintTimer = setTimeout(() => {\n const s = this.snapshot.state;\n if (s.step === 'scanning' && !s.found.length) {\n this.set({ state: { ...s, hint: deviceKind(this.deps.kind).steps[0] } });\n }\n }, SCAN_HINT_MS);\n const kind = this.deps.kind;\n this.stopScan = transport.scan(\n {\n services: deviceKind(kind).services,\n // Brand devices do not advertise a standard service: recognise this kind of them.\n match: (adv) => {\n const brand = detectBrand(adv);\n return brand && brand.kind === kind ? brand : undefined;\n },\n },\n (found) => {\n const s = this.snapshot.state;\n if (s.step !== 'scanning') return;\n if (this.deps.deviceId && found.id === this.deps.deviceId) {\n void this.pick(found);\n return;\n }\n if (this.deps.deviceId) return;\n this.set({\n state: {\n ...s,\n hint: undefined,\n found: [...s.found, found].sort((a, b) => (b.rssi ?? -999) - (a.rssi ?? -999)),\n },\n });\n },\n (message) => this.set({ state: { step: 'error', message } }),\n );\n }\n\n async pick(found: FoundDevice): Promise<void> {\n const { transport, store } = this.deps;\n if (!transport) return;\n this.clearScan();\n this.set({ state: { step: 'connecting', device: found } });\n try {\n const link = await transport.connect(found.id, found.name, () => this.onDisconnect());\n this.link = link;\n const paired = store.getSnapshot().devices.find((d) => d.id === link.id);\n const person = await store.person();\n const facts: PersonFacts = {\n age: ageFrom(person?.birthDate, this.now()),\n sex: person?.sex,\n heightCm: person?.heightCm,\n birthDate: person?.birthDate,\n weightKg: paired?.lastWeightKg,\n };\n this.driver = await startDriver(this.deps.kind, link, {\n now: () => this.now(),\n person: facts,\n identity: found.protocol\n ? {\n protocol: found.protocol,\n mac: found.mac,\n deviceType: found.deviceType,\n model: found.model,\n kind: found.kind,\n }\n : undefined,\n scaleUser: paired?.scaleUser,\n saveScaleUser: (user) => store.saveScaleUser(link.id, user),\n onReading: (r) => void this.keep([enrichWithComposition(r, facts)]),\n onLive: (v) => this.onLive(v),\n onNotice: (m) => this.set({ notices: [...this.snapshot.notices, m] }),\n });\n await store.pair({\n id: link.id,\n name: link.name,\n kind: this.deps.kind,\n ...(found.protocol ? { protocol: found.protocol } : {}),\n });\n this.set({ state: { step: 'connected', device: { id: link.id, name: link.name } } });\n } catch (err) {\n await this.link?.close();\n this.link = null;\n this.set({ state: { step: 'error', message: describeErrorOrOwn(err) } });\n }\n }\n\n private async keep(readings: DeviceReading[]): Promise<void> {\n const fresh = await this.deps.store.addReadings(readings);\n if (fresh.length) this.set({ readings: [...fresh.reverse(), ...this.snapshot.readings] });\n }\n\n private onLive(v: LiveValue) {\n this.set({ live: { ...this.snapshot.live, ...v } });\n if (this.hr && v.bpm) {\n const t = this.now();\n this.hr.samples.push({ t, value: v.bpm });\n this.hr.sum += v.bpm;\n this.hr.count += 1;\n this.hr.max = Math.max(this.hr.max, v.bpm);\n const rec = this.snapshot.recording;\n if (rec)\n this.set({ recording: { ...rec, avgHr: Math.round(this.hr.sum / this.hr.count), maxHr: this.hr.max } });\n }\n }\n\n /** Heart rate: start recording a session of the given kind (run, walk, cycle, strength ...). */\n startRecording(workoutType: string): void {\n this.hr = { samples: [], sum: 0, count: 0, max: 0 };\n this.set({ recording: { startedAt: this.now(), workoutType } });\n }\n\n /** Heart rate: end the session and keep it. Nothing is kept for a session with no heart rate. */\n async stopRecording(): Promise<HeartRateSession | null> {\n const rec = this.snapshot.recording;\n const hr = this.hr;\n this.hr = null;\n this.set({ recording: undefined });\n const device = this.link;\n if (!rec || !hr || !hr.count || !device) return null;\n const end = this.now();\n const session: HeartRateSession = {\n kind: 'heart-rate',\n id: readingId(device.id, 'heart-rate', rec.startedAt),\n at: rec.startedAt,\n end,\n deviceId: device.id,\n deviceName: device.name,\n workoutType: rec.workoutType,\n avgHr: Math.round(hr.sum / hr.count),\n maxHr: hr.max,\n samples: thinSamples(hr.samples),\n };\n await this.keep([session]);\n return session;\n }\n\n /** Continuous oximeter: keep the value on screen as a reading. */\n async saveLive(): Promise<void> {\n const live = this.snapshot.live;\n const device = this.link;\n if (!device || live?.spo2 === undefined) return;\n const at = this.now();\n await this.keep([\n {\n kind: 'spo2',\n id: readingId(device.id, 'spo2', at, live.spo2),\n at,\n deviceId: device.id,\n deviceName: device.name,\n spo2: live.spo2,\n ...(live.pulse !== undefined ? { pulse: live.pulse } : {}),\n },\n ]);\n }\n\n private onDisconnect() {\n this.driver?.stop();\n this.driver = null;\n const link = this.link;\n this.link = null;\n if (this.hr) void this.stopRecording();\n const s = this.snapshot.state;\n if (s.step === 'connected' && link) {\n // A scale or cuff switches off after measuring: the normal end.\n this.set({ state: { step: 'ended', device: { id: link.id, name: link.name } }, live: undefined });\n }\n }\n\n private clearScan() {\n this.stopScan?.();\n this.stopScan = null;\n if (this.hintTimer) clearTimeout(this.hintTimer);\n this.hintTimer = null;\n }\n\n /** Leave: finish what was being assembled, stop scanning, disconnect. */\n async close(): Promise<void> {\n if (this.hr) await this.stopRecording();\n this.clearScan();\n this.driver?.stop();\n this.driver = null;\n const link = this.link;\n this.link = null;\n this.closed = true;\n await link?.close();\n }\n}\n\nfunction describeErrorOrOwn(err: unknown): string {\n // The drivers' own messages are already in plain words (\"does not send weight the standard way\").\n if (err instanceof Error && /standard way|does not know how/.test(err.message)) return err.message;\n return describeBleError(err);\n}\n"],"names":["_a"],"mappings":"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"}
1
+ {"version":3,"file":"session.js","sources":["../../../src/features/devices/session.ts"],"sourcesContent":["/**\n * One \"take a reading\" from start to end, for the screen: is Bluetooth on and\n * allowed, find the device (or the one already paired), connect, run its\n * driver, keep what it measures, and end cleanly when it goes to sleep (a\n * scale switches off seconds after a weighing; that is the normal end, not an\n * error).\n *\n * Heart-rate straps and continuous oximeters stream: the person starts and\n * stops a session (heart rate) or saves the value they see (oxygen).\n *\n * Plain class with injected transport and store, like the other stores: the\n * screen subscribes with useSyncExternalStore, tests drive it with fakes.\n */\nimport type { TimedValue } from '../health/model';\nimport { deviceKind, type DeviceKindId } from './catalog';\nimport { describeBleError, type BleTransport, type ConnectedLink, type FoundDevice, type RadioState } from './link';\nimport { detectBrand } from './brands';\nimport type { DeviceStore } from './deviceStore';\nimport { startDriver, type DriverHandle, type LiveValue, type PersonFacts } from './drivers';\nimport { readingId, thinSamples, type DeviceReading, type HeartRateSession } from './readings';\n\nexport type SessionState =\n | { step: 'starting' }\n | { step: 'unavailable'; reason: string }\n | { step: 'radio'; radio: Exclude<RadioState, 'ready'> }\n | { step: 'scanning'; found: FoundDevice[]; hint?: string }\n | { step: 'connecting'; device: FoundDevice }\n | { step: 'connected'; device: { id: string; name: string } }\n | { step: 'ended'; device: { id: string; name: string } }\n | { step: 'error'; message: string };\n\nexport interface SessionSnapshot {\n state: SessionState;\n kind: DeviceKindId;\n /** What this session measured, newest first. */\n readings: DeviceReading[];\n live?: LiveValue;\n recording?: { startedAt: number; workoutType: string; avgHr?: number; maxHr?: number };\n notices: string[];\n}\n\nexport interface SessionDeps {\n transport: BleTransport | null;\n store: Pick<DeviceStore, 'pair' | 'addReadings' | 'saveScaleUser' | 'person' | 'getSnapshot'>;\n kind: DeviceKindId;\n /** Reconnect to this paired device instead of offering a list. */\n deviceId?: string;\n now?: () => number;\n}\n\n/** Scanning this long without finding anything: tell the person how to wake it. */\nexport const SCAN_HINT_MS = 20_000;\n\nfunction ageFrom(birthDate: string | undefined, now: number): number | undefined {\n if (!birthDate) return undefined;\n const t = Date.parse(`${birthDate}T00:00:00Z`);\n return Number.isFinite(t) ? Math.floor((now - t) / (365.25 * 86_400_000)) : undefined;\n}\n\nexport class DeviceSession {\n private snapshot: SessionSnapshot;\n\n private listeners = new Set<() => void>();\n\n private stopScan: (() => void) | null = null;\n\n private link: ConnectedLink | null = null;\n\n private driver: DriverHandle | null = null;\n\n private hintTimer: ReturnType<typeof setTimeout> | null = null;\n\n private hr: { samples: TimedValue[]; sum: number; count: number; max: number } | null = null;\n\n private closed = false;\n\n constructor(private readonly deps: SessionDeps) {\n this.snapshot = { state: { step: 'starting' }, kind: deps.kind, readings: [], notices: [] };\n }\n\n subscribe = (listener: () => void) => {\n this.listeners.add(listener);\n return () => {\n this.listeners.delete(listener);\n };\n };\n\n getSnapshot = (): SessionSnapshot => this.snapshot;\n\n private now(): number {\n return this.deps.now ? this.deps.now() : Date.now();\n }\n\n private set(patch: Partial<SessionSnapshot>) {\n if (this.closed) return;\n this.snapshot = { ...this.snapshot, ...patch };\n this.listeners.forEach((l) => l());\n }\n\n async start(): Promise<void> {\n const { transport } = this.deps;\n if (!transport) {\n this.set({\n state: { step: 'unavailable', reason: 'Bluetooth devices need the latest version of the Yantra app.' },\n });\n return;\n }\n if (!(await transport.requestPermissions())) {\n this.set({ state: { step: 'radio', radio: 'unauthorized' } });\n return;\n }\n const radio = await transport.radio();\n if (radio !== 'ready' && radio !== 'unknown') {\n this.set({ state: { step: 'radio', radio } });\n return;\n }\n this.scan();\n }\n\n /** Back to looking for the device (after the radio came on, or to measure again). */\n scan(): void {\n const { transport } = this.deps;\n if (!transport || this.closed) return;\n this.stopScan?.();\n this.set({ state: { step: 'scanning', found: [] } });\n this.hintTimer = setTimeout(() => {\n const s = this.snapshot.state;\n if (s.step === 'scanning' && !s.found.length) {\n this.set({ state: { ...s, hint: deviceKind(this.deps.kind).steps[0] } });\n }\n }, SCAN_HINT_MS);\n const kind = this.deps.kind;\n this.stopScan = transport.scan(\n {\n services: deviceKind(kind).services,\n // Brand devices do not advertise a standard service: recognise this kind of them.\n match: (adv) => {\n const brand = detectBrand(adv);\n return brand && brand.kind === kind ? brand : undefined;\n },\n },\n (found) => {\n const s = this.snapshot.state;\n if (s.step !== 'scanning') return;\n if (this.deps.deviceId && found.id === this.deps.deviceId) {\n void this.pick(found);\n return;\n }\n if (this.deps.deviceId) return;\n this.set({\n state: {\n ...s,\n hint: undefined,\n found: [...s.found, found].sort((a, b) => (b.rssi ?? -999) - (a.rssi ?? -999)),\n },\n });\n },\n (message) => this.set({ state: { step: 'error', message } }),\n );\n }\n\n async pick(found: FoundDevice): Promise<void> {\n const { transport, store } = this.deps;\n if (!transport) return;\n this.clearScan();\n this.set({ state: { step: 'connecting', device: found } });\n try {\n const link = await transport.connect(found.id, found.name, () => this.onDisconnect());\n this.link = link;\n const paired = store.getSnapshot().devices.find((d) => d.id === link.id);\n const person = await store.person();\n const facts: PersonFacts = {\n age: ageFrom(person?.birthDate, this.now()),\n sex: person?.sex,\n heightCm: person?.heightCm,\n birthDate: person?.birthDate,\n weightKg: paired?.lastWeightKg,\n };\n this.driver = await startDriver(this.deps.kind, link, {\n now: () => this.now(),\n person: facts,\n identity: found.protocol\n ? {\n protocol: found.protocol,\n mac: found.mac,\n deviceType: found.deviceType,\n model: found.model,\n kind: found.kind,\n }\n : undefined,\n scaleUser: paired?.scaleUser,\n saveScaleUser: (user) => store.saveScaleUser(link.id, user),\n onReading: (r) => void this.keep([r]),\n onLive: (v) => this.onLive(v),\n onNotice: (m) => this.set({ notices: [...this.snapshot.notices, m] }),\n });\n await store.pair({\n id: link.id,\n name: link.name,\n kind: this.deps.kind,\n ...(found.protocol ? { protocol: found.protocol } : {}),\n });\n this.set({ state: { step: 'connected', device: { id: link.id, name: link.name } } });\n } catch (err) {\n await this.link?.close();\n this.link = null;\n this.set({ state: { step: 'error', message: describeErrorOrOwn(err) } });\n }\n }\n\n private async keep(readings: DeviceReading[]): Promise<void> {\n const fresh = await this.deps.store.addReadings(readings);\n if (fresh.length) this.set({ readings: [...fresh.reverse(), ...this.snapshot.readings] });\n }\n\n private onLive(v: LiveValue) {\n this.set({ live: { ...this.snapshot.live, ...v } });\n if (this.hr && v.bpm) {\n const t = this.now();\n this.hr.samples.push({ t, value: v.bpm });\n this.hr.sum += v.bpm;\n this.hr.count += 1;\n this.hr.max = Math.max(this.hr.max, v.bpm);\n const rec = this.snapshot.recording;\n if (rec)\n this.set({ recording: { ...rec, avgHr: Math.round(this.hr.sum / this.hr.count), maxHr: this.hr.max } });\n }\n }\n\n /** Heart rate: start recording a session of the given kind (run, walk, cycle, strength ...). */\n startRecording(workoutType: string): void {\n this.hr = { samples: [], sum: 0, count: 0, max: 0 };\n this.set({ recording: { startedAt: this.now(), workoutType } });\n }\n\n /** Heart rate: end the session and keep it. Nothing is kept for a session with no heart rate. */\n async stopRecording(): Promise<HeartRateSession | null> {\n const rec = this.snapshot.recording;\n const hr = this.hr;\n this.hr = null;\n this.set({ recording: undefined });\n const device = this.link;\n if (!rec || !hr || !hr.count || !device) return null;\n const end = this.now();\n const session: HeartRateSession = {\n kind: 'heart-rate',\n id: readingId(device.id, 'heart-rate', rec.startedAt),\n at: rec.startedAt,\n end,\n deviceId: device.id,\n deviceName: device.name,\n workoutType: rec.workoutType,\n avgHr: Math.round(hr.sum / hr.count),\n maxHr: hr.max,\n samples: thinSamples(hr.samples),\n };\n await this.keep([session]);\n return session;\n }\n\n /** Continuous oximeter: keep the value on screen as a reading. */\n async saveLive(): Promise<void> {\n const live = this.snapshot.live;\n const device = this.link;\n if (!device || live?.spo2 === undefined) return;\n const at = this.now();\n await this.keep([\n {\n kind: 'spo2',\n id: readingId(device.id, 'spo2', at, live.spo2),\n at,\n deviceId: device.id,\n deviceName: device.name,\n spo2: live.spo2,\n ...(live.pulse !== undefined ? { pulse: live.pulse } : {}),\n },\n ]);\n }\n\n private onDisconnect() {\n this.driver?.stop();\n this.driver = null;\n const link = this.link;\n this.link = null;\n if (this.hr) void this.stopRecording();\n const s = this.snapshot.state;\n if (s.step === 'connected' && link) {\n // A scale or cuff switches off after measuring: the normal end.\n this.set({ state: { step: 'ended', device: { id: link.id, name: link.name } }, live: undefined });\n }\n }\n\n private clearScan() {\n this.stopScan?.();\n this.stopScan = null;\n if (this.hintTimer) clearTimeout(this.hintTimer);\n this.hintTimer = null;\n }\n\n /** Leave: finish what was being assembled, stop scanning, disconnect. */\n async close(): Promise<void> {\n if (this.hr) await this.stopRecording();\n this.clearScan();\n this.driver?.stop();\n this.driver = null;\n const link = this.link;\n this.link = null;\n this.closed = true;\n await link?.close();\n }\n}\n\nfunction describeErrorOrOwn(err: unknown): string {\n // The drivers' own messages are already in plain words (\"does not send weight the standard way\").\n if (err instanceof Error && /standard way|does not know how/.test(err.message)) return err.message;\n return describeBleError(err);\n}\n"],"names":["_a"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"backgroundRecording.js","sources":["../../../src/features/dictate/backgroundRecording.ts"],"sourcesContent":["/**\n * The app's background-recording module (portable-devices/mobile/modules/\n * yantra-background-recording), which keeps a dictation going when Yantra\n * leaves the screen, the way a call does:\n * - iOS: the binary declares the `audio` background mode; iOS shows its own\n * recording indicator. The module also reports interruptions (a phone call,\n * another app recording), which expo-audio does not.\n * - Android: a microphone foreground service with an ongoing notification,\n * without which Android gives a backgrounded app only silence.\n *\n * Optional on purpose: an over-the-air JS update can reach a binary built\n * before the module existed. That reads as \"not supported\", and Dictate\n * pauses when it leaves the screen, as before.\n */\nimport { requireOptionalNativeModule } from 'expo-modules-core';\n\nexport type InterruptionEvent = { state: 'began' } | { state: 'ended'; shouldResume: boolean };\n\nexport interface BackgroundRecordingNative {\n /** The binary may keep recording in the background. */\n readonly supported: boolean;\n /** Android starts the microphone service (title and text show in its notification); iOS has nothing to start. */\n start(title: string, text: string): Promise<void>;\n stop(): Promise<void>;\n addListener(event: 'onInterruption', listener: (event: InterruptionEvent) => void): { remove(): void };\n}\n\nlet cached: BackgroundRecordingNative | null | undefined;\n\nexport function backgroundRecordingNative(): BackgroundRecordingNative | null {\n if (cached === undefined) {\n try {\n cached = requireOptionalNativeModule<BackgroundRecordingNative>('YantraBackgroundRecording');\n } catch {\n cached = null;\n }\n }\n return cached;\n}\n\n/** What the Android notification says while a dictation records in the background. */\nexport const BACKGROUND_NOTICE = { title: 'Dictating', text: 'Yantra is recording your dictation. Tap to return.' };\n"],"names":[],"mappings":"4DA+BA,IAAI,MAAA;AACG,SAAS,yBAA8D,GAAA;AAC5E,EAAA,IAAI,WAAW,MAAW,EAAA;AACxB,IAAI,IAAA;AACF,MAAA,MAAA,GAAS,4BAAuD,2BAA2B,CAAA;AAAA,KACrF,CAAA,OAAA,CAAA,EAAA;AACN,MAAS,MAAA,GAAA,IAAA;AAAA;AACX;AAEF,EAAO,OAAA,MAAA;AACT;AAGO,MAAM,iBAAoB,GAAA;AAAA,EAC/B,KAAO,EAAA,WAAA;AAAA,EACP,IAAM,EAAA;AACR"}
1
+ {"version":3,"file":"backgroundRecording.js","sources":["../../../src/features/dictate/backgroundRecording.ts"],"sourcesContent":["/**\n * The app's background-recording module (portable-devices/mobile/modules/\n * yantra-background-recording), which keeps a dictation going when Yantra\n * leaves the screen, the way a call does:\n * - iOS: the binary declares the `audio` background mode, and the recording\n * shows as a Live Activity on the lock screen and in the Dynamic Island. The\n * module also reports interruptions (a phone call, another app recording),\n * which expo-audio does not.\n * - Android: a microphone foreground service with an ongoing notification,\n * without which Android gives a backgrounded app only silence.\n *\n * Optional on purpose: an over-the-air JS update can reach a binary built\n * before the module existed. That reads as \"not supported\", and Dictate\n * pauses when it leaves the screen, as before.\n */\nimport { requireOptionalNativeModule } from 'expo-modules-core';\n\nexport type InterruptionEvent = { state: 'began' } | { state: 'ended'; shouldResume: boolean };\n\n/** What the Live Activity shows, sent on every change. */\nexport interface BackgroundActivityState {\n title: string;\n detail: string;\n recording: boolean;\n /** The recorder's own time, which stops while paused. */\n recordedMs: number;\n noSound: boolean;\n}\n\nexport interface BackgroundRecordingNative {\n /** The binary may keep recording in the background. */\n readonly supported: boolean;\n /** Android starts the microphone service, iOS the Live Activity; both show the title and text. */\n start(title: string, text: string): Promise<void>;\n /** iOS: the Live Activity follows the recording. Missing on Android and on binaries built before it. */\n update?(state: BackgroundActivityState): Promise<void>;\n stop(): Promise<void>;\n addListener(event: 'onInterruption', listener: (event: InterruptionEvent) => void): { remove(): void };\n}\n\nlet cached: BackgroundRecordingNative | null | undefined;\n\nexport function backgroundRecordingNative(): BackgroundRecordingNative | null {\n if (cached === undefined) {\n try {\n cached = requireOptionalNativeModule<BackgroundRecordingNative>('YantraBackgroundRecording');\n } catch {\n cached = null;\n }\n }\n return cached;\n}\n\n/** What the notification (Android) or the Live Activity (iOS) says while a dictation records in the background. */\nexport const BACKGROUND_NOTICE = { title: 'Dictating', text: 'Yantra is recording your dictation. Tap to return.' };\n"],"names":[],"mappings":"4DA4CA,IAAI,MAAA;AACG,SAAS,yBAA8D,GAAA;AAC5E,EAAA,IAAI,WAAW,MAAW,EAAA;AACxB,IAAI,IAAA;AACF,MAAA,MAAA,GAAS,4BAAuD,2BAA2B,CAAA;AAAA,KACrF,CAAA,OAAA,CAAA,EAAA;AACN,MAAS,MAAA,GAAA,IAAA;AAAA;AACX;AAEF,EAAO,OAAA,MAAA;AACT;AAGO,MAAM,iBAAoB,GAAA;AAAA,EAC/B,KAAO,EAAA,WAAA;AAAA,EACP,IAAM,EAAA;AACR"}
@@ -43,6 +43,34 @@ const NO_INPUT_WARN_MS = 4e3;
43
43
  function carriesSignal(db) {
44
44
  return typeof db !== "number" || !Number.isFinite(db) || db > NO_SIGNAL_DB;
45
45
  }
46
+ function backgroundActivityText(phase, pauseReason, noInput) {
47
+ if (phase === "idle") return null;
48
+ if (phase === "recording") {
49
+ return noInput ? {
50
+ title: BACKGROUND_NOTICE.title,
51
+ detail: "No sound is reaching the microphone. Open Yantra to check."
52
+ } : {
53
+ title: BACKGROUND_NOTICE.title,
54
+ detail: BACKGROUND_NOTICE.text
55
+ };
56
+ }
57
+ if (pauseReason === "interrupted") {
58
+ return {
59
+ title: "Paused for a call",
60
+ detail: "A call or another app took the microphone. Open Yantra to resume."
61
+ };
62
+ }
63
+ if (pauseReason === "limit") {
64
+ return {
65
+ title: "Paused at one hour",
66
+ detail: "An hour is the most one dictation holds. Open Yantra to write the note."
67
+ };
68
+ }
69
+ return {
70
+ title: "Paused",
71
+ detail: "Open Yantra to resume or finish the dictation."
72
+ };
73
+ }
46
74
  class MicrophoneDeniedError extends Error {
47
75
  constructor() {
48
76
  super("Microphone access was declined. Allow it in Settings to dictate.");
@@ -135,6 +163,19 @@ function useDictationRecorder() {
135
163
  }
136
164
  }, [phase, state.durationMillis, state.metering]);
137
165
  const noInput = phase === "recording" && !carriesSignal(state.metering) && state.durationMillis - signalAtRef.current >= NO_INPUT_WARN_MS;
166
+ const activity = backgroundActivityText(phase, pauseReason, noInput);
167
+ const activityTitle = activity == null ? void 0 : activity.title;
168
+ const activityDetail = activity == null ? void 0 : activity.detail;
169
+ useEffect(() => {
170
+ if (!activityTitle || !activityDetail || !inBackgroundRef.current || !(background == null ? void 0 : background.update)) return;
171
+ void background.update({
172
+ title: activityTitle,
173
+ detail: activityDetail,
174
+ recording: phase === "recording",
175
+ recordedMs: recorder.getStatus().durationMillis,
176
+ noSound: noInput
177
+ }).catch(() => void 0);
178
+ }, [activityTitle, activityDetail, phase, noInput, background, recorder]);
138
179
  useEffect(() => () => {
139
180
  if (phaseRef.current !== "idle") void recorder.stop().catch(() => void 0);
140
181
  void releaseSession();
@@ -152,4 +193,4 @@ function useDictationRecorder() {
152
193
  resume,
153
194
  finish
154
195
  };
155
- }export{DICTATION_RECORDING,MAX_DICTATION_MS,MicrophoneDeniedError,NO_INPUT_WARN_MS,carriesSignal,levelFromMetering,useDictationRecorder};//# sourceMappingURL=useDictationRecorder.js.map
196
+ }export{DICTATION_RECORDING,MAX_DICTATION_MS,MicrophoneDeniedError,NO_INPUT_WARN_MS,backgroundActivityText,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, 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":"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"}
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, and on iOS shows it on the\n * lock screen and in the Dynamic Island meanwhile (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\n/** The Live Activity's words for where the dictation stands; nothing once it is done. */\nexport function backgroundActivityText(\n phase: RecorderPhase,\n pauseReason: PauseReason,\n noInput: boolean,\n): { title: string; detail: string } | null {\n if (phase === 'idle') return null;\n if (phase === 'recording') {\n return noInput\n ? { title: BACKGROUND_NOTICE.title, detail: 'No sound is reaching the microphone. Open Yantra to check.' }\n : { title: BACKGROUND_NOTICE.title, detail: BACKGROUND_NOTICE.text };\n }\n if (pauseReason === 'interrupted') {\n return {\n title: 'Paused for a call',\n detail: 'A call or another app took the microphone. Open Yantra to resume.',\n };\n }\n if (pauseReason === 'limit') {\n return {\n title: 'Paused at one hour',\n detail: 'An hour is the most one dictation holds. Open Yantra to write the note.',\n };\n }\n return { title: 'Paused', detail: 'Open Yantra to resume or finish the dictation.' };\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 // The lock screen and the Dynamic Island follow the dictation while it records in the background (iOS).\n const activity = backgroundActivityText(phase, pauseReason, noInput);\n const activityTitle = activity?.title;\n const activityDetail = activity?.detail;\n useEffect(() => {\n if (!activityTitle || !activityDetail || !inBackgroundRef.current || !background?.update) return;\n void background\n .update({\n title: activityTitle,\n detail: activityDetail,\n recording: phase === 'recording',\n recordedMs: recorder.getStatus().durationMillis,\n noSound: noInput,\n })\n .catch(() => undefined);\n }, [activityTitle, activityDetail, phase, noInput, background, recorder]);\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":"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"}