@alia.onl/sdk 10.1.1 → 11.0.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.
- package/CHANGELOG.md +53 -0
- package/README.md +24 -0
- package/package.json +4 -6
- package/src/components/AliaAvatar.tsx +66 -0
- package/src/components/AliaChatContent.tsx +235 -112
- package/src/components/AliaChatHeader.tsx +118 -0
- package/src/components/AliaChatMessageList.tsx +171 -260
- package/src/components/AliaChatScreen.tsx +23 -24
- package/src/components/AliaChatSheet.tsx +70 -316
- package/src/components/AliaWelcomeMessage.tsx +47 -50
- package/src/components/AmbientField.tsx +577 -0
- package/src/components/IdentityMark.tsx +10 -2
- package/src/components/Markdown.tsx +116 -40
- package/src/components/PlanPreviewCard.tsx +71 -55
- package/src/components/ThinkingBlock.tsx +47 -0
- package/src/components/VoiceModeIcon.tsx +10 -0
- package/src/components/voice/VoiceControls.tsx +22 -73
- package/src/components/voice/VoiceSession.tsx +2 -1
- package/src/hooks/useAliaChat.ts +22 -15
- package/src/hooks/useAmbientWave.ts +35 -5
- package/src/hooks/useSoundEffects.ts +14 -11
- package/src/hooks/useSpeechToText.ts +9 -2
- package/src/hooks/useTTS.ts +163 -156
- package/src/hooks/useVoiceRoom.ts +228 -170
- package/src/index.ts +18 -6
- package/src/lib/catalogue.ts +17 -4
- package/src/lib/chat-stream.ts +25 -7
- package/src/lib/chat-transport.ts +2 -1
- package/src/lib/citations.ts +63 -0
- package/src/lib/content-utils.ts +3 -2
- package/src/lib/image-data-url.ts +15 -0
- package/src/lib/keyboard.tsx +1 -1
- package/src/lib/labels.tsx +81 -0
- package/src/lib/speech-recognition.native.ts +15 -4
- package/src/lib/speech-recognition.ts +27 -7
- package/src/lib/task-labels.ts +67 -0
- package/src/lib/tool-call-status.ts +28 -0
- package/src/lib/tool-registry.ts +14 -14
- package/src/lib/voice-text.ts +12 -6
- package/src/lib/voice-turn.ts +4 -1
- package/src/lib/work-log-copy.ts +61 -0
- package/src/lib/work-log.ts +378 -0
- package/src/tools.ts +11 -0
- package/src/types.ts +5 -2
- package/src/voice.ts +12 -4
- package/src/components/Reasoning.tsx +0 -152
- package/src/components/ResearchProgressCard.tsx +0 -159
- package/src/components/ThinkingIndicator.tsx +0 -98
- package/src/components/ui/button.tsx +0 -104
- package/src/components/ui/chat-text-input.tsx +0 -184
- package/src/components/ui/dropdown-menu.tsx +0 -2
- package/src/components/ui/dropdown-menu.web.tsx +0 -383
- package/src/components/ui/prompt-input/actions.tsx +0 -28
- package/src/components/ui/prompt-input/add-menu.tsx +0 -83
- package/src/components/ui/prompt-input/attachments.tsx +0 -181
- package/src/components/ui/prompt-input/autocomplete.tsx +0 -168
- package/src/components/ui/prompt-input/context.tsx +0 -71
- package/src/components/ui/prompt-input/mic-button.tsx +0 -53
- package/src/components/ui/prompt-input/prompt-input.tsx +0 -279
- package/src/components/ui/prompt-input/submit-button.tsx +0 -50
- package/src/components/ui/prompt-input/textarea.tsx +0 -80
- package/src/components/ui/text.tsx +0 -29
- package/src/components/voice/AudioWaveVisualizer.tsx +0 -444
- 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<
|
|
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
|
|
32
|
-
*
|
|
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
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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={{
|
|
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={{
|
|
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={{
|
|
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={{
|
|
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={{
|
|
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={{
|
|
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 }}>
|
|
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 }}>
|
|
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 }}>
|
|
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 }}>
|
|
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 }}>
|
|
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
|
|
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:
|
|
219
|
-
//
|
|
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: {
|
|
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
|
|
293
|
-
*
|
|
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({
|
|
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(
|
|
421
|
-
|
|
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 {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
import
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
<
|
|
33
|
-
<Text className="text-xs 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
|
-
<
|
|
41
|
-
<
|
|
42
|
-
<
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
</
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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,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
|
+
);
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Bottom voice control bar that replaces the composer when voice mode is active.
|
|
3
|
-
* Contains status text and mute/end
|
|
3
|
+
* Contains status text and Bloom's mute/end call controls.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import {
|
|
7
|
-
import
|
|
8
|
-
import
|
|
9
|
-
import
|
|
6
|
+
import { CallControls } from '@oxy.so/bloom/call-ui';
|
|
7
|
+
import { Loading } from '@oxy.so/bloom/loading';
|
|
8
|
+
import { Text } from '@oxy.so/bloom/typography';
|
|
9
|
+
import { View } from 'react-native';
|
|
10
10
|
import type { RoomState, AgentState } from '../../types';
|
|
11
11
|
|
|
12
12
|
/** Every word the bar shows or announces. An app in another language passes its own. */
|
|
@@ -40,8 +40,6 @@ export interface VoiceControlsProps {
|
|
|
40
40
|
isMuted: boolean;
|
|
41
41
|
onToggleMute: () => void;
|
|
42
42
|
onEnd: () => void;
|
|
43
|
-
/** Override theme primary color (resolved hex/hsl value, not CSS var) */
|
|
44
|
-
primaryColor?: string;
|
|
45
43
|
/** Overrides for any of {@link VOICE_CONTROLS_LABELS}. */
|
|
46
44
|
labels?: Partial<VoiceControlsLabels>;
|
|
47
45
|
}
|
|
@@ -68,84 +66,35 @@ export function VoiceControls({
|
|
|
68
66
|
isMuted,
|
|
69
67
|
onToggleMute,
|
|
70
68
|
onEnd,
|
|
71
|
-
primaryColor,
|
|
72
69
|
labels: labelOverrides,
|
|
73
70
|
}: VoiceControlsProps) {
|
|
74
71
|
const labels = { ...VOICE_CONTROLS_LABELS, ...labelOverrides };
|
|
75
72
|
const statusText = getStatusText(roomState, agentState, isMuted, labels);
|
|
76
73
|
|
|
77
74
|
return (
|
|
78
|
-
<View
|
|
75
|
+
<View className="items-center gap-4 pb-6 pt-4">
|
|
79
76
|
{statusText ? (
|
|
80
|
-
<Text
|
|
77
|
+
<Text
|
|
78
|
+
className="text-lg font-medium text-text-primary"
|
|
79
|
+
accessibilityLiveRegion="polite"
|
|
80
|
+
aria-live="polite"
|
|
81
|
+
>
|
|
81
82
|
{statusText}
|
|
82
83
|
</Text>
|
|
83
84
|
) : null}
|
|
84
85
|
|
|
85
|
-
{roomState === 'connected'
|
|
86
|
-
<
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
style={[styles.button, isMuted ? { backgroundColor: '#ef4444' } : undefined]}
|
|
96
|
-
>
|
|
97
|
-
{isMuted ? (
|
|
98
|
-
<MicOff size={24} color="white" />
|
|
99
|
-
) : (
|
|
100
|
-
<Mic size={24} color="white" />
|
|
101
|
-
)}
|
|
102
|
-
</Pressable>
|
|
103
|
-
<Text className="text-xs text-muted-foreground">
|
|
104
|
-
{isMuted ? labels.unmute : labels.mute}
|
|
105
|
-
</Text>
|
|
106
|
-
</View>
|
|
107
|
-
|
|
108
|
-
<View style={styles.buttonWrapper}>
|
|
109
|
-
<Pressable
|
|
110
|
-
onPress={onEnd}
|
|
111
|
-
accessibilityRole="button"
|
|
112
|
-
accessibilityLabel={labels.end}
|
|
113
|
-
style={[styles.button, { backgroundColor: '#ef4444' }]}
|
|
114
|
-
>
|
|
115
|
-
<PhoneOff size={24} color="white" />
|
|
116
|
-
</Pressable>
|
|
117
|
-
<Text className="text-xs text-muted-foreground">{labels.end}</Text>
|
|
118
|
-
</View>
|
|
119
|
-
</View>
|
|
120
|
-
)}
|
|
86
|
+
{roomState === 'connected' ? (
|
|
87
|
+
<CallControls
|
|
88
|
+
muted={isMuted}
|
|
89
|
+
onMutedChange={onToggleMute}
|
|
90
|
+
onEndCall={onEnd}
|
|
91
|
+
// TODO(bloom): `CallControls` draws its captions for a dark call stage, so on the
|
|
92
|
+
// chat's light surface they vanish. The buttons keep their accessible names.
|
|
93
|
+
labels={{ mute: labels.mute, unmute: labels.unmute, endCall: labels.end }}
|
|
94
|
+
/>
|
|
95
|
+
) : null}
|
|
121
96
|
|
|
122
|
-
{roomState === 'connecting'
|
|
123
|
-
<ActivityIndicator size="large" className="text-muted-foreground" />
|
|
124
|
-
)}
|
|
97
|
+
{roomState === 'connecting' ? <Loading accessibilityLabel={labels.connecting} /> : null}
|
|
125
98
|
</View>
|
|
126
99
|
);
|
|
127
100
|
}
|
|
128
|
-
|
|
129
|
-
const styles = StyleSheet.create({
|
|
130
|
-
container: {
|
|
131
|
-
alignItems: 'center',
|
|
132
|
-
paddingBottom: 24,
|
|
133
|
-
paddingTop: 16,
|
|
134
|
-
},
|
|
135
|
-
buttonRow: {
|
|
136
|
-
flexDirection: 'row',
|
|
137
|
-
alignItems: 'center',
|
|
138
|
-
gap: 32,
|
|
139
|
-
},
|
|
140
|
-
buttonWrapper: {
|
|
141
|
-
alignItems: 'center',
|
|
142
|
-
gap: 8,
|
|
143
|
-
},
|
|
144
|
-
button: {
|
|
145
|
-
width: 56,
|
|
146
|
-
height: 56,
|
|
147
|
-
borderRadius: 28,
|
|
148
|
-
alignItems: 'center',
|
|
149
|
-
justifyContent: 'center',
|
|
150
|
-
},
|
|
151
|
-
});
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
import { useEffect, useRef } from 'react';
|
|
13
13
|
import { useVoiceRoom } from '../../hooks/useVoiceRoom';
|
|
14
|
+
import { VOICE_ERROR_MESSAGES } from '../../lib/speech-messages';
|
|
14
15
|
import { useAudioLevelMonitor } from '../../hooks/useAudioLevelMonitor';
|
|
15
16
|
import { useAudioLevels } from '../../hooks/useAudioLevels';
|
|
16
17
|
import { VoiceControls } from './VoiceControls';
|
|
@@ -57,7 +58,7 @@ export function VoiceSession({ apiUrl, onStateChange, onEnd }: VoiceSessionProps
|
|
|
57
58
|
useEffect(() => {
|
|
58
59
|
if (roomState === 'connected') reachedRoomRef.current = true;
|
|
59
60
|
if (error || roomState === 'error') {
|
|
60
|
-
onEnd();
|
|
61
|
+
onEnd(error ?? VOICE_ERROR_MESSAGES['turn-failed']);
|
|
61
62
|
return;
|
|
62
63
|
}
|
|
63
64
|
if (roomState === 'disconnected' && reachedRoomRef.current) onEnd();
|