@alia.onl/sdk 10.1.1 → 11.1.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 (66) hide show
  1. package/CHANGELOG.md +67 -0
  2. package/README.md +24 -0
  3. package/package.json +4 -6
  4. package/src/components/AliaAvatar.tsx +66 -0
  5. package/src/components/AliaChatContent.tsx +236 -112
  6. package/src/components/AliaChatHeader.tsx +118 -0
  7. package/src/components/AliaChatMessageList.tsx +197 -260
  8. package/src/components/AliaChatScreen.tsx +23 -24
  9. package/src/components/AliaChatSheet.tsx +70 -316
  10. package/src/components/AliaWelcomeMessage.tsx +47 -50
  11. package/src/components/AmbientField.tsx +577 -0
  12. package/src/components/IdentityMark.tsx +10 -2
  13. package/src/components/Markdown.tsx +116 -40
  14. package/src/components/PlanPreviewCard.tsx +71 -55
  15. package/src/components/QuestionCard.tsx +62 -0
  16. package/src/components/ThinkingBlock.tsx +47 -0
  17. package/src/components/VoiceModeIcon.tsx +10 -0
  18. package/src/components/voice/VoiceControls.tsx +22 -73
  19. package/src/components/voice/VoiceSession.tsx +2 -1
  20. package/src/hooks/useAliaChat.ts +22 -15
  21. package/src/hooks/useAmbientWave.ts +35 -5
  22. package/src/hooks/useSoundEffects.ts +14 -11
  23. package/src/hooks/useSpeechToText.ts +9 -2
  24. package/src/hooks/useTTS.ts +163 -156
  25. package/src/hooks/useVoiceRoom.ts +228 -170
  26. package/src/index.ts +26 -6
  27. package/src/lib/catalogue.ts +17 -4
  28. package/src/lib/chat-stream.ts +25 -7
  29. package/src/lib/chat-transport.ts +2 -1
  30. package/src/lib/citations.ts +63 -0
  31. package/src/lib/content-utils.ts +3 -2
  32. package/src/lib/image-data-url.ts +15 -0
  33. package/src/lib/keyboard.tsx +1 -1
  34. package/src/lib/labels.tsx +81 -0
  35. package/src/lib/questions.ts +78 -0
  36. package/src/lib/speech-recognition.native.ts +15 -4
  37. package/src/lib/speech-recognition.ts +27 -7
  38. package/src/lib/task-labels.ts +67 -0
  39. package/src/lib/tool-call-status.ts +28 -0
  40. package/src/lib/tool-registry.ts +14 -14
  41. package/src/lib/voice-text.ts +12 -6
  42. package/src/lib/voice-turn.ts +4 -1
  43. package/src/lib/work-log-copy.ts +61 -0
  44. package/src/lib/work-log.ts +378 -0
  45. package/src/tools.ts +18 -0
  46. package/src/types.ts +5 -2
  47. package/src/voice.ts +12 -4
  48. package/src/components/Reasoning.tsx +0 -152
  49. package/src/components/ResearchProgressCard.tsx +0 -159
  50. package/src/components/ThinkingIndicator.tsx +0 -98
  51. package/src/components/ui/button.tsx +0 -104
  52. package/src/components/ui/chat-text-input.tsx +0 -184
  53. package/src/components/ui/dropdown-menu.tsx +0 -2
  54. package/src/components/ui/dropdown-menu.web.tsx +0 -383
  55. package/src/components/ui/prompt-input/actions.tsx +0 -28
  56. package/src/components/ui/prompt-input/add-menu.tsx +0 -83
  57. package/src/components/ui/prompt-input/attachments.tsx +0 -181
  58. package/src/components/ui/prompt-input/autocomplete.tsx +0 -168
  59. package/src/components/ui/prompt-input/context.tsx +0 -71
  60. package/src/components/ui/prompt-input/mic-button.tsx +0 -53
  61. package/src/components/ui/prompt-input/prompt-input.tsx +0 -279
  62. package/src/components/ui/prompt-input/submit-button.tsx +0 -50
  63. package/src/components/ui/prompt-input/textarea.tsx +0 -80
  64. package/src/components/ui/text.tsx +0 -29
  65. package/src/components/voice/AudioWaveVisualizer.tsx +0 -444
  66. package/src/components/voice/VoiceOverlay.tsx +0 -75
@@ -1,11 +1,15 @@
1
1
  import React, { useMemo, type ReactNode } from 'react';
2
2
  import { View, Text, Platform, Linking, useColorScheme } from 'react-native';
3
3
  import Markdown, { MarkdownIt, type ASTNode } from 'react-native-markdown-display';
4
+ import { CodeBlock } from '@oxy.so/bloom/code';
4
5
  import type { AliaColors } from '../theme';
5
6
 
6
7
  // Hardcoded fallback colors for standalone SDK usage (when no color override is passed).
7
8
  // The main app always passes resolved colors via the `colors` prop.
8
- const FALLBACK_LIGHT: Pick<AliaColors, 'text' | 'muted' | 'border' | 'primary' | 'mutedForeground'> = {
9
+ const FALLBACK_LIGHT: Pick<
10
+ AliaColors,
11
+ 'text' | 'muted' | 'border' | 'primary' | 'mutedForeground'
12
+ > = {
9
13
  text: '#11181C',
10
14
  muted: '#F4F4F5',
11
15
  border: '#E5E5EA',
@@ -28,32 +32,37 @@ function createRules(colors: AliaColors, renderCodeBlock?: RenderCodeBlock) {
28
32
  const { text: textColor, muted, border, primary, mutedForeground } = colors;
29
33
 
30
34
  /**
31
- * Fenced and indented code, drawn by the host when it brings a code card of
32
- * its own (the app passes Bloom's `CodeBlock`). This package has no Bloom
33
- * dependency, so it cannot name one itself; without the prop the library's
34
- * default `fence` / `code_block` rendering, styled below, stays.
35
+ * Fenced and indented code: Bloom's `CodeBlock` unless the host brings a
36
+ * code card of its own.
35
37
  */
36
- const codeRules = renderCodeBlock === undefined
37
- ? {}
38
- : {
39
- fence: (node: ASTNode) => (
40
- <View key={node.key} style={{ marginVertical: 8 }}>
41
- {renderCodeBlock(node.content.replace(/\n$/, ''), codeLanguage(node))}
42
- </View>
43
- ),
44
- code_block: (node: ASTNode) => (
45
- <View key={node.key} style={{ marginVertical: 8 }}>
46
- {renderCodeBlock(node.content.replace(/\n$/, ''), undefined)}
47
- </View>
48
- ),
49
- };
38
+ const drawCodeBlock: RenderCodeBlock =
39
+ renderCodeBlock ?? ((code, language) => <CodeBlock code={code} language={language} />);
40
+ const codeRules = {
41
+ fence: (node: ASTNode) => (
42
+ <View key={node.key} style={{ marginVertical: 8 }}>
43
+ {drawCodeBlock(node.content.replace(/\n$/, ''), codeLanguage(node))}
44
+ </View>
45
+ ),
46
+ code_block: (node: ASTNode) => (
47
+ <View key={node.key} style={{ marginVertical: 8 }}>
48
+ {drawCodeBlock(node.content.replace(/\n$/, ''), undefined)}
49
+ </View>
50
+ ),
51
+ };
50
52
 
51
53
  return {
52
54
  ...codeRules,
53
55
  heading1: (node: ASTNode, children: ReactNode[]) => (
54
56
  <Text
55
57
  key={node.key}
56
- style={{ marginBottom: 8, marginTop: 12, fontSize: 18, fontWeight: '600', lineHeight: 24, color: textColor }}
58
+ style={{
59
+ marginBottom: 8,
60
+ marginTop: 12,
61
+ fontSize: 18,
62
+ fontWeight: '600',
63
+ lineHeight: 24,
64
+ color: textColor,
65
+ }}
57
66
  >
58
67
  {children}
59
68
  </Text>
@@ -61,7 +70,13 @@ function createRules(colors: AliaColors, renderCodeBlock?: RenderCodeBlock) {
61
70
  heading2: (node: ASTNode, children: ReactNode[]) => (
62
71
  <Text
63
72
  key={node.key}
64
- style={{ marginBottom: 8, marginTop: 12, ...HEADING_TEXT, fontWeight: '600', color: textColor }}
73
+ style={{
74
+ marginBottom: 8,
75
+ marginTop: 12,
76
+ ...HEADING_TEXT,
77
+ fontWeight: '600',
78
+ color: textColor,
79
+ }}
65
80
  >
66
81
  {children}
67
82
  </Text>
@@ -69,7 +84,13 @@ function createRules(colors: AliaColors, renderCodeBlock?: RenderCodeBlock) {
69
84
  heading3: (node: ASTNode, children: ReactNode[]) => (
70
85
  <Text
71
86
  key={node.key}
72
- style={{ marginBottom: 6, marginTop: 8, ...HEADING_TEXT, fontWeight: '600', color: textColor }}
87
+ style={{
88
+ marginBottom: 6,
89
+ marginTop: 8,
90
+ ...HEADING_TEXT,
91
+ fontWeight: '600',
92
+ color: textColor,
93
+ }}
73
94
  >
74
95
  {children}
75
96
  </Text>
@@ -77,7 +98,13 @@ function createRules(colors: AliaColors, renderCodeBlock?: RenderCodeBlock) {
77
98
  heading4: (node: ASTNode, children: ReactNode[]) => (
78
99
  <Text
79
100
  key={node.key}
80
- style={{ marginBottom: 6, marginTop: 8, ...HEADING_TEXT, fontWeight: '500', color: textColor }}
101
+ style={{
102
+ marginBottom: 6,
103
+ marginTop: 8,
104
+ ...HEADING_TEXT,
105
+ fontWeight: '500',
106
+ color: textColor,
107
+ }}
81
108
  >
82
109
  {children}
83
110
  </Text>
@@ -85,7 +112,13 @@ function createRules(colors: AliaColors, renderCodeBlock?: RenderCodeBlock) {
85
112
  heading5: (node: ASTNode, children: ReactNode[]) => (
86
113
  <Text
87
114
  key={node.key}
88
- style={{ marginBottom: 4, marginTop: 8, ...HEADING_TEXT, fontWeight: '500', color: textColor }}
115
+ style={{
116
+ marginBottom: 4,
117
+ marginTop: 8,
118
+ ...HEADING_TEXT,
119
+ fontWeight: '500',
120
+ color: textColor,
121
+ }}
89
122
  >
90
123
  {children}
91
124
  </Text>
@@ -93,7 +126,13 @@ function createRules(colors: AliaColors, renderCodeBlock?: RenderCodeBlock) {
93
126
  heading6: (node: ASTNode, children: ReactNode[]) => (
94
127
  <Text
95
128
  key={node.key}
96
- style={{ marginBottom: 4, marginTop: 8, ...HEADING_TEXT, fontWeight: '500', color: textColor }}
129
+ style={{
130
+ marginBottom: 4,
131
+ marginTop: 8,
132
+ ...HEADING_TEXT,
133
+ fontWeight: '500',
134
+ color: textColor,
135
+ }}
97
136
  >
98
137
  {children}
99
138
  </Text>
@@ -138,19 +177,27 @@ function createRules(colors: AliaColors, renderCodeBlock?: RenderCodeBlock) {
138
177
 
139
178
  return (
140
179
  <View key={node.key} style={{ flexDirection: 'row', paddingVertical: 2, paddingLeft: 16 }}>
141
- <Text style={{ marginRight: 8, minWidth: 14, ...BODY_TEXT, color: mutedForeground }}>{bullet}</Text>
180
+ <Text style={{ marginRight: 8, minWidth: 14, ...BODY_TEXT, color: mutedForeground }}>
181
+ {bullet}
182
+ </Text>
142
183
  <Text style={{ flex: 1, ...BODY_TEXT, color: textColor }}>{children}</Text>
143
184
  </View>
144
185
  );
145
186
  },
146
187
  ordered_list: (node: ASTNode, children: ReactNode[]) => (
147
- <View key={node.key} style={{ marginVertical: 8 }}>{children}</View>
188
+ <View key={node.key} style={{ marginVertical: 8 }}>
189
+ {children}
190
+ </View>
148
191
  ),
149
192
  unordered_list: (node: ASTNode, children: ReactNode[]) => (
150
- <View key={node.key} style={{ marginVertical: 8 }}>{children}</View>
193
+ <View key={node.key} style={{ marginVertical: 8 }}>
194
+ {children}
195
+ </View>
151
196
  ),
152
197
  strong: (node: ASTNode, children: ReactNode[]) => (
153
- <Text key={node.key} style={{ fontWeight: '600', ...BODY_TEXT, color: textColor }}>{children}</Text>
198
+ <Text key={node.key} style={{ fontWeight: '600', ...BODY_TEXT, color: textColor }}>
199
+ {children}
200
+ </Text>
154
201
  ),
155
202
  link: (node: ASTNode, children: ReactNode[]) => (
156
203
  <Text
@@ -164,7 +211,9 @@ function createRules(colors: AliaColors, renderCodeBlock?: RenderCodeBlock) {
164
211
  </Text>
165
212
  ),
166
213
  paragraph: (node: ASTNode, children: ReactNode[]) => (
167
- <Text key={node.key} style={{ marginBottom: 8, ...BODY_TEXT, color: textColor }}>{children}</Text>
214
+ <Text key={node.key} style={{ marginBottom: 8, ...BODY_TEXT, color: textColor }}>
215
+ {children}
216
+ </Text>
168
217
  ),
169
218
  blockquote: (node: ASTNode, children: ReactNode[]) => (
170
219
  <View
@@ -194,7 +243,11 @@ function createRules(colors: AliaColors, renderCodeBlock?: RenderCodeBlock) {
194
243
  />
195
244
  ),
196
245
  body: (node: ASTNode, children: ReactNode[]) => {
197
- return <View key={node.key} style={{ marginBottom: -8 }}>{children}</View>;
246
+ return (
247
+ <View key={node.key} style={{ marginBottom: -8 }}>
248
+ {children}
249
+ </View>
250
+ );
198
251
  },
199
252
  };
200
253
  }
@@ -215,9 +268,8 @@ function createStyles(colors: AliaColors, fontFamily: string | undefined) {
215
268
  // in any Expo consumer, and this renderer cannot inherit a face.
216
269
  //
217
270
  // `fontFamily` therefore comes from the caller, which is the only party
218
- // that both knows the family and is allowed to name it: this package has no
219
- // `@oxy.so/bloom` dependency, and react-native-markdown-display takes RN
220
- // style objects, so there is no className path to NativeWind's token.
271
+ // that both knows the family and is allowed to name it: react-native-markdown-display
272
+ // takes RN style objects, so there is no className path to NativeWind's token.
221
273
  body: { ...BODY_TEXT, color: textColor, fontFamily },
222
274
  text: { ...BODY_TEXT, color: textColor, fontFamily },
223
275
  paragraph: { marginTop: 0, marginBottom: 8 },
@@ -255,7 +307,13 @@ function createStyles(colors: AliaColors, fontFamily: string | undefined) {
255
307
  borderRadius: 6,
256
308
  padding: 12,
257
309
  },
258
- table: { borderWidth: 1, borderColor: border, borderRadius: 12, marginVertical: 8, overflow: 'hidden' as const },
310
+ table: {
311
+ borderWidth: 1,
312
+ borderColor: border,
313
+ borderRadius: 12,
314
+ marginVertical: 8,
315
+ overflow: 'hidden' as const,
316
+ },
259
317
  thead: { backgroundColor: muted },
260
318
  th: { flex: 1, padding: 8, fontWeight: '600' as const, ...BODY_TEXT, color: textColor },
261
319
  td: { flex: 1, padding: 8, ...BODY_TEXT, color: textColor },
@@ -289,8 +347,8 @@ export interface AliaMarkdownProps {
289
347
  * Omitted, body text inherits: correct on web, where Bloom's
290
348
  * `--bloom-font-sans` reaches it through the cascade. On native nothing is
291
349
  * inherited (see `createStyles`), so a native caller that wants Bloom's face
292
- * has to pass it — this package has no `@oxy.so/bloom` dependency and so
293
- * cannot name it itself.
350
+ * has to pass it — this renderer takes RN style objects, so it cannot
351
+ * read Bloom's token itself.
294
352
  */
295
353
  fontFamily?: string;
296
354
  content: string;
@@ -412,13 +470,31 @@ function ParsedMarkdown({ content, rules, styles }: MarkdownBlockProps) {
412
470
 
413
471
  const MarkdownBlock = React.memo(ParsedMarkdown);
414
472
 
415
- export function AliaMarkdown({ content, colors: colorOverrides, fontFamily, renderCodeBlock }: AliaMarkdownProps) {
473
+ export function AliaMarkdown({
474
+ content,
475
+ colors: colorOverrides,
476
+ fontFamily,
477
+ renderCodeBlock,
478
+ }: AliaMarkdownProps) {
416
479
  const scheme = useColorScheme();
417
480
  const fallback = scheme === 'dark' ? FALLBACK_DARK : FALLBACK_LIGHT;
418
481
  const colors = { ...fallback, ...colorOverrides } as AliaColors;
419
482
 
420
- const customRules = useMemo(() => createRules(colors, renderCodeBlock), [colors.text, colors.muted, colors.border, colors.primary, colors.mutedForeground, renderCodeBlock]);
421
- const markdownStyles = useMemo(() => createStyles(colors, fontFamily), [colors.text, colors.muted, colors.border, colors.primary, colors.mutedForeground, fontFamily]);
483
+ const customRules = useMemo(
484
+ () => createRules(colors, renderCodeBlock),
485
+ [
486
+ colors.text,
487
+ colors.muted,
488
+ colors.border,
489
+ colors.primary,
490
+ colors.mutedForeground,
491
+ renderCodeBlock,
492
+ ],
493
+ );
494
+ const markdownStyles = useMemo(
495
+ () => createStyles(colors, fontFamily),
496
+ [colors.text, colors.muted, colors.border, colors.primary, colors.mutedForeground, fontFamily],
497
+ );
422
498
  const blocks = useMemo(() => splitMarkdownBlocks(content), [content]);
423
499
 
424
500
  if (blocks.length === 1) {
@@ -1,11 +1,13 @@
1
- import { View, Pressable } from 'react-native';
2
- import { Text } from './ui/text';
3
- import { Button } from './ui/button';
4
- import Check from 'lucide-react-native/icons/check';
5
- import X from 'lucide-react-native/icons/x';
6
- import ListChecks from 'lucide-react-native/icons/list-checks';
1
+ import { Button } from '@oxy.so/bloom/button';
2
+ import { Card, CardBody, CardFooter, CardHeader, CardTitle } from '@oxy.so/bloom/card';
3
+ import { RiCheckLine, RiCloseLine, RiListCheck3 } from '@oxy.so/bloom/icons';
4
+ import { Item } from '@oxy.so/bloom/item';
5
+ import { useTheme } from '@oxy.so/bloom/theme';
6
+ import { Text } from '@oxy.so/bloom/typography';
7
+ import { View } from 'react-native';
7
8
  import Animated from 'react-native-reanimated';
8
9
  import { planPreviewEnter } from '../animations';
10
+ import { useLabels } from '../lib/labels';
9
11
  import type { PlanStep } from '../types';
10
12
 
11
13
  interface PlanPreviewCardProps {
@@ -16,64 +18,78 @@ interface PlanPreviewCardProps {
16
18
  rejected?: boolean;
17
19
  }
18
20
 
19
- export function PlanPreviewCard({ steps, onApprove, onReject, approved, rejected }: PlanPreviewCardProps) {
20
- if (approved) {
21
- return (
22
- <View className="flex-row items-center gap-1.5 py-1">
23
- <Check size={12} className="text-green-500" />
24
- <Text className="text-xs text-muted-foreground">Plan approved</Text>
25
- </View>
26
- );
27
- }
21
+ /** The plan Alia proposes, until the person approves or cancels it; then one line saying which. */
22
+ export function PlanPreviewCard({
23
+ steps,
24
+ onApprove,
25
+ onReject,
26
+ approved,
27
+ rejected,
28
+ }: PlanPreviewCardProps) {
29
+ const labels = useLabels();
30
+ const { colors } = useTheme();
28
31
 
29
- if (rejected) {
32
+ if (approved || rejected) {
33
+ const Icon = approved ? RiCheckLine : RiCloseLine;
30
34
  return (
31
35
  <View className="flex-row items-center gap-1.5 py-1">
32
- <X size={12} className="text-red-500" />
33
- <Text className="text-xs text-muted-foreground">Plan cancelled</Text>
36
+ <Icon width={14} height={14} fill={approved ? colors.success : colors.error} />
37
+ <Text className="text-xs text-text-secondary">
38
+ {approved ? labels.planApproved : labels.planCancelled}
39
+ </Text>
34
40
  </View>
35
41
  );
36
42
  }
37
43
 
38
44
  return (
39
- <Animated.View entering={planPreviewEnter}>
40
- <View className="border border-border rounded-xl bg-card p-4 gap-3 my-2">
41
- <View className="flex-row items-center gap-2">
42
- <ListChecks size={16} className="text-foreground" />
43
- <Text className="text-sm font-semibold text-foreground">Here's my plan</Text>
44
- </View>
45
-
46
- <View className="gap-2">
47
- {steps.map((step, i) => (
48
- <View key={i} className="flex-row items-start gap-2.5">
49
- <View className="w-5 h-5 rounded-full bg-muted items-center justify-center mt-0.5">
50
- <Text className="text-[10px] font-bold text-muted-foreground">{i + 1}</Text>
51
- </View>
52
- <View className="flex-1">
53
- <Text className="text-[13px] font-medium text-foreground">{step.action}</Text>
54
- {step.description ? (
55
- <Text className="text-xs text-muted-foreground">{step.description}</Text>
56
- ) : null}
57
- </View>
58
- </View>
45
+ <Animated.View entering={planPreviewEnter} className="my-2">
46
+ <Card appearance="outline">
47
+ <CardHeader>
48
+ <View className="flex-row items-center gap-2">
49
+ <RiListCheck3 width={16} height={16} fill={colors.icon} />
50
+ <CardTitle>{labels.planTitle}</CardTitle>
51
+ </View>
52
+ </CardHeader>
53
+ <CardBody>
54
+ {steps.map((step, index) => (
55
+ <Item
56
+ // A plan has no ids and its steps never reorder: the position is the identity.
57
+ // biome-ignore lint/suspicious/noArrayIndexKey: see above
58
+ key={index}
59
+ density="compact"
60
+ leading={
61
+ <Text className="w-5 text-center text-xs font-bold text-text-secondary">
62
+ {index + 1}
63
+ </Text>
64
+ }
65
+ title={step.action}
66
+ subtitle={step.description || undefined}
67
+ />
59
68
  ))}
60
- </View>
61
-
62
- <View className="flex-row gap-2 pt-1">
63
- <Button onPress={onApprove} size="sm" className="flex-1">
64
- <View className="flex-row items-center gap-1.5">
65
- <Check size={14} className="text-primary-foreground" />
66
- <Text className="text-sm font-medium text-primary-foreground">Approve</Text>
67
- </View>
68
- </Button>
69
- <Pressable
70
- onPress={onReject}
71
- className="flex-1 items-center justify-center py-2 rounded-lg active:opacity-70"
72
- >
73
- <Text className="text-sm text-muted-foreground">Cancel</Text>
74
- </Pressable>
75
- </View>
76
- </View>
69
+ </CardBody>
70
+ <CardFooter>
71
+ <View className="flex-1 flex-row gap-2">
72
+ <Button
73
+ tone="action"
74
+ size="sm"
75
+ className="flex-1"
76
+ leadingIcon={RiCheckLine}
77
+ onPress={onApprove}
78
+ >
79
+ {labels.planApprove}
80
+ </Button>
81
+ <Button
82
+ tone="neutral"
83
+ appearance="subtle"
84
+ size="sm"
85
+ className="flex-1"
86
+ onPress={onReject}
87
+ >
88
+ {labels.planCancel}
89
+ </Button>
90
+ </View>
91
+ </CardFooter>
92
+ </Card>
77
93
  </Animated.View>
78
94
  );
79
95
  }
@@ -0,0 +1,62 @@
1
+ import { Questionnaire, type QuestionnaireAnswers } from '@oxy.so/bloom/questionnaire';
2
+ import { Muted } from '@oxy.so/bloom/typography';
3
+ import { View } from 'react-native';
4
+ import { type AskedQuestion, formatAnswers } from '../lib/questions';
5
+
6
+ interface QuestionCardProps {
7
+ questions: AskedQuestion[];
8
+ /** Sends the answers as the person's next message. Absent: the card is read-only. */
9
+ onSubmit?: (message: string) => void;
10
+ }
11
+
12
+ /** Ids are positions: the questions are the model's, and carry none. */
13
+ const idOf = (index: number) => `q${index}`;
14
+
15
+ /**
16
+ * The questions Alia asked: Bloom's `Questionnaire` while the person can still answer, and
17
+ * the questions as plain lines once they have (the answer is their message, right below).
18
+ * Typing a reply in the composer answers just as well, which is why there is no dismiss.
19
+ */
20
+ export function QuestionCard({ questions, onSubmit }: QuestionCardProps) {
21
+ if (onSubmit === undefined) {
22
+ return (
23
+ <View className="my-2 gap-1">
24
+ {questions.map((q, i) => (
25
+ // The model's questions never reorder: the position is the identity.
26
+ // biome-ignore lint/suspicious/noArrayIndexKey: see above
27
+ <Muted key={i}>{q.question}</Muted>
28
+ ))}
29
+ </View>
30
+ );
31
+ }
32
+
33
+ const complete = (answers: QuestionnaireAnswers) =>
34
+ onSubmit(
35
+ formatAnswers(
36
+ questions,
37
+ questions.map((_, i) => ({
38
+ picked: (answers[idOf(i)]?.values ?? []).map(Number),
39
+ other: answers[idOf(i)]?.other,
40
+ })),
41
+ ),
42
+ );
43
+
44
+ return (
45
+ <View className="my-2">
46
+ <Questionnaire
47
+ questions={questions.map((q, i) => ({
48
+ id: idOf(i),
49
+ question: q.question,
50
+ select: q.multiSelect ? 'multiple' : 'single',
51
+ other: true,
52
+ options: q.options.map((option, index) => ({
53
+ value: String(index),
54
+ label: option.label,
55
+ description: option.description,
56
+ })),
57
+ }))}
58
+ onComplete={complete}
59
+ />
60
+ </View>
61
+ );
62
+ }
@@ -0,0 +1,47 @@
1
+ import { AgentThinking } from '@oxy.so/bloom/agent-thinking';
2
+ import { useEffect, useRef, useState } from 'react';
3
+ import { View } from 'react-native';
4
+ import { useLabels } from '../lib/labels';
5
+ import { AliaMarkdown } from './Markdown';
6
+
7
+ interface ThinkingBlockProps {
8
+ /** The model's reasoning, as Markdown. */
9
+ text: string;
10
+ /** Still reasoning: the block opens, and closes again when this turns false. */
11
+ streaming: boolean;
12
+ }
13
+
14
+ /**
15
+ * A turn's reasoning: Bloom's `AgentThinking` as a disclosure (live while it
16
+ * streams, "Thought for N seconds" once done), with the text set as Markdown.
17
+ * Open and close, the indicator and the chevron are Bloom's; this only counts
18
+ * how long the stream ran, because the message carries no duration.
19
+ */
20
+ export function ThinkingBlock({ text, streaming }: ThinkingBlockProps) {
21
+ const labels = useLabels();
22
+ const startedAt = useRef<number | null>(null);
23
+ const [seconds, setSeconds] = useState<number>();
24
+
25
+ useEffect(() => {
26
+ if (streaming) {
27
+ startedAt.current = Date.now();
28
+ } else if (startedAt.current !== null) {
29
+ setSeconds(Math.max(1, Math.round((Date.now() - startedAt.current) / 1000)));
30
+ startedAt.current = null;
31
+ }
32
+ }, [streaming]);
33
+
34
+ const finishedLabel = seconds ? labels.thoughtFor(seconds) : labels.reasoning;
35
+
36
+ return (
37
+ <AgentThinking
38
+ variant="stars"
39
+ thinking={streaming}
40
+ label={streaming ? undefined : finishedLabel}
41
+ >
42
+ <View style={{ opacity: streaming ? 0.8 : 1 }}>
43
+ <AliaMarkdown content={text} />
44
+ </View>
45
+ </AgentThinking>
46
+ );
47
+ }
@@ -0,0 +1,10 @@
1
+ import Entypo from '@expo/vector-icons/Entypo';
2
+ import type { BloomIconComponent } from '@oxy.so/bloom/icons';
3
+
4
+ /**
5
+ * The glyph of Alia's voice button: Entypo's `sound`. Remix has no equivalent, so it is
6
+ * handed Bloom's icon contract (`width` / `fill`) for a Bloom `Button` to size and colour.
7
+ */
8
+ export const VoiceModeIcon: BloomIconComponent = ({ width = 18, fill }) => (
9
+ <Entypo name="sound" size={width} color={fill} />
10
+ );