@alia.onl/sdk 3.1.0 → 3.1.2
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/package.json +47 -16
- package/src/animations.ts +6 -0
- package/src/components/AliaChatMessageList.tsx +2 -3
- package/src/components/AliaChatScreen.tsx +8 -43
- package/src/components/AliaChatSheet.tsx +8 -26
- package/src/components/AliaFace.tsx +108 -58
- package/src/components/Markdown.tsx +59 -39
- package/src/components/PlanPreviewCard.tsx +3 -2
- package/src/components/Reasoning.tsx +1 -3
- package/src/components/ResearchProgressCard.tsx +16 -17
- package/src/components/ThinkingIndicator.tsx +6 -21
- package/src/components/ui/dropdown-menu.web.tsx +2 -2
- package/src/components/voice/AudioWaveVisualizer.tsx +72 -22
- package/src/components/voice/VoiceControls.tsx +25 -42
- package/src/components/voice/VoiceOverlay.tsx +5 -1
- package/src/hooks/useVoiceRoom.ts +24 -8
- package/src/index.ts +2 -2
- package/src/theme.ts +2 -41
- package/src/components/AliaChatInput.tsx +0 -294
|
@@ -1,14 +1,33 @@
|
|
|
1
1
|
import React, { useMemo } from 'react';
|
|
2
|
-
import { View, Text, Platform, Linking } from 'react-native';
|
|
2
|
+
import { View, Text, Platform, Linking, useColorScheme } from 'react-native';
|
|
3
3
|
import Markdown from 'react-native-markdown-display';
|
|
4
|
-
import {
|
|
4
|
+
import type { AliaColors } from '../theme';
|
|
5
|
+
|
|
6
|
+
// Hardcoded fallback colors for standalone SDK usage (when no color override is passed).
|
|
7
|
+
// The main app always passes resolved colors via the `colors` prop.
|
|
8
|
+
const FALLBACK_LIGHT: Pick<AliaColors, 'text' | 'muted' | 'border' | 'primary' | 'mutedForeground'> = {
|
|
9
|
+
text: '#11181C',
|
|
10
|
+
muted: '#F4F4F5',
|
|
11
|
+
border: '#E5E5EA',
|
|
12
|
+
primary: '#7C3AED',
|
|
13
|
+
mutedForeground: '#71717A',
|
|
14
|
+
};
|
|
15
|
+
const FALLBACK_DARK: typeof FALLBACK_LIGHT = {
|
|
16
|
+
text: '#ECEDEE',
|
|
17
|
+
muted: '#27272A',
|
|
18
|
+
border: '#2C2C2E',
|
|
19
|
+
primary: '#A78BFA',
|
|
20
|
+
mutedForeground: '#A1A1AA',
|
|
21
|
+
};
|
|
5
22
|
|
|
6
23
|
const BODY_TEXT = { fontSize: 16, lineHeight: 28 } as const;
|
|
7
24
|
const HEADING_TEXT = { fontSize: 16, lineHeight: 22 } as const;
|
|
8
25
|
const SANS_FONT = Platform.select({ ios: 'Inter', android: 'Inter', default: 'Inter, sans-serif' })!;
|
|
9
26
|
const MONO_FONT = Platform.select({ ios: 'Courier', android: 'monospace', default: 'monospace' })!;
|
|
10
27
|
|
|
11
|
-
function createRules(
|
|
28
|
+
function createRules(colors: AliaColors) {
|
|
29
|
+
const { text: textColor, muted, border, primary, mutedForeground } = colors;
|
|
30
|
+
|
|
12
31
|
return {
|
|
13
32
|
heading1: (node: any, children: any) => (
|
|
14
33
|
<Text
|
|
@@ -66,9 +85,9 @@ function createRules(isDark: boolean, textColor: string) {
|
|
|
66
85
|
marginVertical: 8,
|
|
67
86
|
overflow: 'hidden',
|
|
68
87
|
borderRadius: 6,
|
|
69
|
-
backgroundColor:
|
|
88
|
+
backgroundColor: muted,
|
|
70
89
|
borderWidth: 1,
|
|
71
|
-
borderColor:
|
|
90
|
+
borderColor: border,
|
|
72
91
|
padding: 12,
|
|
73
92
|
}}
|
|
74
93
|
>
|
|
@@ -78,7 +97,7 @@ function createRules(isDark: boolean, textColor: string) {
|
|
|
78
97
|
<Text
|
|
79
98
|
key={node.key}
|
|
80
99
|
style={{
|
|
81
|
-
backgroundColor:
|
|
100
|
+
backgroundColor: muted,
|
|
82
101
|
paddingHorizontal: 4,
|
|
83
102
|
paddingVertical: 1,
|
|
84
103
|
fontSize: 13,
|
|
@@ -98,7 +117,7 @@ function createRules(isDark: boolean, textColor: string) {
|
|
|
98
117
|
|
|
99
118
|
return (
|
|
100
119
|
<View key={node.key} style={{ flexDirection: 'row', paddingVertical: 2, paddingLeft: 16 }}>
|
|
101
|
-
<Text style={{ marginRight: 8, minWidth: 14, ...BODY_TEXT, color:
|
|
120
|
+
<Text style={{ marginRight: 8, minWidth: 14, ...BODY_TEXT, color: mutedForeground }}>{bullet}</Text>
|
|
102
121
|
<Text style={{ flex: 1, ...BODY_TEXT, color: textColor }}>{children}</Text>
|
|
103
122
|
</View>
|
|
104
123
|
);
|
|
@@ -115,7 +134,7 @@ function createRules(isDark: boolean, textColor: string) {
|
|
|
115
134
|
link: (node: any, children: any) => (
|
|
116
135
|
<Text
|
|
117
136
|
key={node.key}
|
|
118
|
-
style={{ color:
|
|
137
|
+
style={{ color: primary, textDecorationLine: 'underline', ...BODY_TEXT }}
|
|
119
138
|
onPress={() => {
|
|
120
139
|
if (node.attributes?.href) Linking.openURL(node.attributes.href);
|
|
121
140
|
}}
|
|
@@ -132,8 +151,8 @@ function createRules(isDark: boolean, textColor: string) {
|
|
|
132
151
|
style={{
|
|
133
152
|
marginVertical: 8,
|
|
134
153
|
borderLeftWidth: 4,
|
|
135
|
-
borderLeftColor:
|
|
136
|
-
backgroundColor:
|
|
154
|
+
borderLeftColor: border,
|
|
155
|
+
backgroundColor: muted,
|
|
137
156
|
paddingHorizontal: 12,
|
|
138
157
|
paddingVertical: 4,
|
|
139
158
|
borderTopRightRadius: 6,
|
|
@@ -149,20 +168,19 @@ function createRules(isDark: boolean, textColor: string) {
|
|
|
149
168
|
style={{
|
|
150
169
|
marginVertical: 16,
|
|
151
170
|
height: 1,
|
|
152
|
-
backgroundColor:
|
|
171
|
+
backgroundColor: border,
|
|
153
172
|
}}
|
|
154
173
|
/>
|
|
155
174
|
),
|
|
156
|
-
text: (node: any) => {
|
|
157
|
-
return node.content;
|
|
158
|
-
},
|
|
159
175
|
body: (node: any, children: any) => {
|
|
160
|
-
return <View key={node.key}>{children}</View>;
|
|
176
|
+
return <View key={node.key} style={{ marginBottom: -8 }}>{children}</View>;
|
|
161
177
|
},
|
|
162
178
|
};
|
|
163
179
|
}
|
|
164
180
|
|
|
165
|
-
function createStyles(
|
|
181
|
+
function createStyles(colors: AliaColors) {
|
|
182
|
+
const { text: textColor, muted, border, primary, mutedForeground } = colors;
|
|
183
|
+
|
|
166
184
|
return {
|
|
167
185
|
body: { ...BODY_TEXT, color: textColor, fontFamily: SANS_FONT },
|
|
168
186
|
text: { ...BODY_TEXT, color: textColor, fontFamily: SANS_FONT },
|
|
@@ -170,14 +188,14 @@ function createStyles(isDark: boolean, textColor: string, borderColor: string, m
|
|
|
170
188
|
strong: { fontWeight: '600' as const },
|
|
171
189
|
em: { fontStyle: 'italic' as const },
|
|
172
190
|
s: { textDecorationLine: 'line-through' as const },
|
|
173
|
-
bullet_list_icon: { marginLeft: 0, marginRight: 8, ...BODY_TEXT },
|
|
174
|
-
ordered_list_icon: { marginLeft: 0, marginRight: 8, ...BODY_TEXT },
|
|
191
|
+
bullet_list_icon: { marginLeft: 0, marginRight: 8, ...BODY_TEXT, color: mutedForeground },
|
|
192
|
+
ordered_list_icon: { marginLeft: 0, marginRight: 8, ...BODY_TEXT, color: mutedForeground },
|
|
175
193
|
bullet_list_content: { flex: 1 },
|
|
176
194
|
ordered_list_content: { flex: 1 },
|
|
177
195
|
code_inline: {
|
|
178
196
|
fontSize: 13,
|
|
179
197
|
fontFamily: MONO_FONT,
|
|
180
|
-
backgroundColor:
|
|
198
|
+
backgroundColor: muted,
|
|
181
199
|
borderWidth: 0,
|
|
182
200
|
borderRadius: 3,
|
|
183
201
|
paddingHorizontal: 4,
|
|
@@ -186,28 +204,28 @@ function createStyles(isDark: boolean, textColor: string, borderColor: string, m
|
|
|
186
204
|
code_block: {
|
|
187
205
|
fontSize: 13,
|
|
188
206
|
fontFamily: MONO_FONT,
|
|
189
|
-
backgroundColor:
|
|
207
|
+
backgroundColor: muted,
|
|
190
208
|
borderWidth: 1,
|
|
191
|
-
borderColor:
|
|
209
|
+
borderColor: border,
|
|
192
210
|
borderRadius: 6,
|
|
193
211
|
padding: 12,
|
|
194
212
|
},
|
|
195
213
|
fence: {
|
|
196
214
|
fontSize: 13,
|
|
197
215
|
fontFamily: MONO_FONT,
|
|
198
|
-
backgroundColor:
|
|
216
|
+
backgroundColor: muted,
|
|
199
217
|
borderWidth: 1,
|
|
200
|
-
borderColor:
|
|
218
|
+
borderColor: border,
|
|
201
219
|
borderRadius: 6,
|
|
202
220
|
padding: 12,
|
|
203
221
|
},
|
|
204
|
-
table: { borderWidth: 1, borderColor, borderRadius:
|
|
205
|
-
thead: { backgroundColor:
|
|
206
|
-
th: { flex: 1, padding: 8, fontWeight: '600' as const, ...BODY_TEXT },
|
|
207
|
-
td: { flex: 1, padding: 8, ...BODY_TEXT },
|
|
208
|
-
tr: { borderBottomWidth: 1, borderColor, flexDirection: 'row' as const },
|
|
209
|
-
link: { textDecorationLine: 'underline' as const, color:
|
|
210
|
-
hr: { backgroundColor:
|
|
222
|
+
table: { borderWidth: 1, borderColor: border, borderRadius: 12, marginVertical: 8, overflow: 'hidden' as const },
|
|
223
|
+
thead: { backgroundColor: muted },
|
|
224
|
+
th: { flex: 1, padding: 8, fontWeight: '600' as const, ...BODY_TEXT, color: textColor },
|
|
225
|
+
td: { flex: 1, padding: 8, ...BODY_TEXT, color: textColor },
|
|
226
|
+
tr: { borderBottomWidth: 1, borderColor: border, flexDirection: 'row' as const },
|
|
227
|
+
link: { textDecorationLine: 'underline' as const, color: primary, ...BODY_TEXT },
|
|
228
|
+
hr: { backgroundColor: border, height: 1, marginVertical: 16 },
|
|
211
229
|
heading1: { fontSize: 18, fontWeight: '600' as const },
|
|
212
230
|
heading2: { ...HEADING_TEXT, fontWeight: '600' as const },
|
|
213
231
|
heading3: { ...HEADING_TEXT, fontWeight: '600' as const },
|
|
@@ -217,16 +235,18 @@ function createStyles(isDark: boolean, textColor: string, borderColor: string, m
|
|
|
217
235
|
};
|
|
218
236
|
}
|
|
219
237
|
|
|
220
|
-
export
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
238
|
+
export interface AliaMarkdownProps {
|
|
239
|
+
content: string;
|
|
240
|
+
colors?: Partial<AliaColors>;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export function AliaMarkdown({ content, colors: colorOverrides }: AliaMarkdownProps) {
|
|
244
|
+
const scheme = useColorScheme();
|
|
245
|
+
const fallback = scheme === 'dark' ? FALLBACK_DARK : FALLBACK_LIGHT;
|
|
246
|
+
const colors = { ...fallback, ...colorOverrides } as AliaColors;
|
|
224
247
|
|
|
225
|
-
const customRules = useMemo(() => createRules(
|
|
226
|
-
const markdownStyles = useMemo(
|
|
227
|
-
() => createStyles(isDark, textColor, colors.border, colors.muted),
|
|
228
|
-
[isDark, colors.border, colors.muted],
|
|
229
|
-
);
|
|
248
|
+
const customRules = useMemo(() => createRules(colors), [colors.text, colors.muted, colors.border, colors.primary, colors.mutedForeground]);
|
|
249
|
+
const markdownStyles = useMemo(() => createStyles(colors), [colors.text, colors.muted, colors.border, colors.primary, colors.mutedForeground]);
|
|
230
250
|
|
|
231
251
|
return <Markdown rules={customRules} style={markdownStyles}>{content}</Markdown>;
|
|
232
252
|
}
|
|
@@ -2,7 +2,8 @@ import { View, Pressable } from 'react-native';
|
|
|
2
2
|
import { Text } from './ui/text';
|
|
3
3
|
import { Button } from './ui/button';
|
|
4
4
|
import { Check, X, ListChecks } from 'lucide-react-native';
|
|
5
|
-
import Animated
|
|
5
|
+
import Animated from 'react-native-reanimated';
|
|
6
|
+
import { planPreviewEnter } from '../animations';
|
|
6
7
|
import type { PlanStep } from '../types';
|
|
7
8
|
|
|
8
9
|
interface PlanPreviewCardProps {
|
|
@@ -33,7 +34,7 @@ export function PlanPreviewCard({ steps, onApprove, onReject, approved, rejected
|
|
|
33
34
|
}
|
|
34
35
|
|
|
35
36
|
return (
|
|
36
|
-
<Animated.View entering={
|
|
37
|
+
<Animated.View entering={planPreviewEnter}>
|
|
37
38
|
<View className="border border-border rounded-xl bg-card p-4 gap-3 my-2">
|
|
38
39
|
<View className="flex-row items-center gap-2">
|
|
39
40
|
<ListChecks size={16} className="text-foreground" />
|
|
@@ -8,7 +8,7 @@ import Animated, {
|
|
|
8
8
|
withSequence,
|
|
9
9
|
cancelAnimation,
|
|
10
10
|
} from 'react-native-reanimated';
|
|
11
|
-
|
|
11
|
+
|
|
12
12
|
|
|
13
13
|
// Context
|
|
14
14
|
type ReasoningContextType = {
|
|
@@ -80,7 +80,6 @@ export function Reasoning({ isStreaming = false, defaultOpen = true, duration: e
|
|
|
80
80
|
// Trigger — shows "Thinking for Xs..." label with pulsing icon
|
|
81
81
|
export function ReasoningTrigger({ onPress }: ReasoningTriggerProps) {
|
|
82
82
|
const { isStreaming, isOpen, setIsOpen, duration } = useReasoning();
|
|
83
|
-
const colors = useAliaColors();
|
|
84
83
|
const pulseOpacity = useSharedValue(1);
|
|
85
84
|
|
|
86
85
|
useEffect(() => {
|
|
@@ -114,7 +113,6 @@ export function ReasoningTrigger({ onPress }: ReasoningTriggerProps) {
|
|
|
114
113
|
// Content — renders thinking text (collapsible)
|
|
115
114
|
export function ReasoningContent({ children }: ReasoningContentProps) {
|
|
116
115
|
const { isOpen, isStreaming } = useReasoning();
|
|
117
|
-
const colors = useAliaColors();
|
|
118
116
|
|
|
119
117
|
if (!isOpen) return null;
|
|
120
118
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useState } from 'react';
|
|
2
2
|
import { View, Pressable } from 'react-native';
|
|
3
3
|
import { Text } from './ui/text';
|
|
4
|
+
import { cn } from '../lib/utils';
|
|
4
5
|
import {
|
|
5
6
|
Search,
|
|
6
7
|
BookOpen,
|
|
@@ -10,7 +11,6 @@ import {
|
|
|
10
11
|
ExternalLink,
|
|
11
12
|
Loader2,
|
|
12
13
|
} from 'lucide-react-native';
|
|
13
|
-
import { useAliaColors } from '../theme';
|
|
14
14
|
import type { ResearchProgress } from '../types';
|
|
15
15
|
|
|
16
16
|
interface ResearchProgressCardProps {
|
|
@@ -30,7 +30,6 @@ const PHASE_LABELS: Record<string, string> = {
|
|
|
30
30
|
const PHASE_ORDER = ['decomposing', 'searching', 'synthesizing', 'follow_up', 'finalizing', 'complete'];
|
|
31
31
|
|
|
32
32
|
export function ResearchProgressCard({ progress }: ResearchProgressCardProps) {
|
|
33
|
-
const colors = useAliaColors();
|
|
34
33
|
const [showSources, setShowSources] = useState(false);
|
|
35
34
|
const [showQuestions, setShowQuestions] = useState(true);
|
|
36
35
|
|
|
@@ -43,9 +42,9 @@ export function ResearchProgressCard({ progress }: ResearchProgressCardProps) {
|
|
|
43
42
|
<View className="flex-row items-center gap-2">
|
|
44
43
|
<View className="w-8 h-8 rounded-full bg-primary/10 items-center justify-center">
|
|
45
44
|
{isComplete ? (
|
|
46
|
-
<CheckCircle size={16}
|
|
45
|
+
<CheckCircle size={16} className="text-green-500" />
|
|
47
46
|
) : (
|
|
48
|
-
<Search size={16}
|
|
47
|
+
<Search size={16} className="text-primary" />
|
|
49
48
|
)}
|
|
50
49
|
</View>
|
|
51
50
|
<View className="flex-1">
|
|
@@ -70,12 +69,12 @@ export function ResearchProgressCard({ progress }: ResearchProgressCardProps) {
|
|
|
70
69
|
{PHASE_ORDER.map((phase, i) => (
|
|
71
70
|
<View
|
|
72
71
|
key={phase}
|
|
73
|
-
className=
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
? (isComplete ? '
|
|
77
|
-
:
|
|
78
|
-
}
|
|
72
|
+
className={cn(
|
|
73
|
+
'flex-1 h-1 rounded-full',
|
|
74
|
+
i <= phaseIndex
|
|
75
|
+
? (isComplete ? 'bg-green-500' : 'bg-primary')
|
|
76
|
+
: 'bg-muted',
|
|
77
|
+
)}
|
|
79
78
|
/>
|
|
80
79
|
))}
|
|
81
80
|
</View>
|
|
@@ -87,14 +86,14 @@ export function ResearchProgressCard({ progress }: ResearchProgressCardProps) {
|
|
|
87
86
|
onPress={() => setShowQuestions(!showQuestions)}
|
|
88
87
|
className="flex-row items-center gap-1 mb-1"
|
|
89
88
|
>
|
|
90
|
-
<BookOpen size={12}
|
|
89
|
+
<BookOpen size={12} className="text-muted-foreground" />
|
|
91
90
|
<Text className="text-xs font-medium text-muted-foreground">
|
|
92
91
|
Research angles ({progress.subQuestions.length})
|
|
93
92
|
</Text>
|
|
94
93
|
{showQuestions ? (
|
|
95
|
-
<ChevronUp size={12}
|
|
94
|
+
<ChevronUp size={12} className="text-muted-foreground" />
|
|
96
95
|
) : (
|
|
97
|
-
<ChevronDown size={12}
|
|
96
|
+
<ChevronDown size={12} className="text-muted-foreground" />
|
|
98
97
|
)}
|
|
99
98
|
</Pressable>
|
|
100
99
|
{showQuestions && (
|
|
@@ -112,7 +111,7 @@ export function ResearchProgressCard({ progress }: ResearchProgressCardProps) {
|
|
|
112
111
|
{/* Current search query indicator */}
|
|
113
112
|
{progress.currentQuery && !isComplete && (
|
|
114
113
|
<View className="flex-row items-center gap-2 bg-muted/50 rounded-lg px-3 py-1.5">
|
|
115
|
-
<Loader2 size={12}
|
|
114
|
+
<Loader2 size={12} className="text-muted-foreground" />
|
|
116
115
|
<Text className="text-xs text-muted-foreground flex-1" numberOfLines={1}>
|
|
117
116
|
{progress.currentQuery}
|
|
118
117
|
</Text>
|
|
@@ -126,14 +125,14 @@ export function ResearchProgressCard({ progress }: ResearchProgressCardProps) {
|
|
|
126
125
|
onPress={() => setShowSources(!showSources)}
|
|
127
126
|
className="flex-row items-center gap-1"
|
|
128
127
|
>
|
|
129
|
-
<ExternalLink size={12}
|
|
128
|
+
<ExternalLink size={12} className="text-muted-foreground" />
|
|
130
129
|
<Text className="text-xs font-medium text-muted-foreground">
|
|
131
130
|
Sources ({progress.sources.length})
|
|
132
131
|
</Text>
|
|
133
132
|
{showSources ? (
|
|
134
|
-
<ChevronUp size={12}
|
|
133
|
+
<ChevronUp size={12} className="text-muted-foreground" />
|
|
135
134
|
) : (
|
|
136
|
-
<ChevronDown size={12}
|
|
135
|
+
<ChevronDown size={12} className="text-muted-foreground" />
|
|
137
136
|
)}
|
|
138
137
|
</Pressable>
|
|
139
138
|
{showSources && (
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React, { useState, useEffect } from "react";
|
|
2
|
-
import { View, Text
|
|
2
|
+
import { View, Text } from "react-native";
|
|
3
3
|
import Animated, {
|
|
4
4
|
useSharedValue,
|
|
5
5
|
useAnimatedStyle,
|
|
@@ -8,7 +8,6 @@ import Animated, {
|
|
|
8
8
|
withSequence,
|
|
9
9
|
Easing,
|
|
10
10
|
} from "react-native-reanimated";
|
|
11
|
-
import { useAliaColors } from "../theme";
|
|
12
11
|
|
|
13
12
|
const thinkingPhrases = [
|
|
14
13
|
"Thinking...",
|
|
@@ -33,7 +32,6 @@ const workingPhrases = [
|
|
|
33
32
|
];
|
|
34
33
|
|
|
35
34
|
export function ThinkingIndicator({ isWorking = false, statusText }: { isWorking?: boolean; statusText?: string }) {
|
|
36
|
-
const colors = useAliaColors();
|
|
37
35
|
const phrases = isWorking ? workingPhrases : thinkingPhrases;
|
|
38
36
|
const [phraseIndex, setPhraseIndex] = useState(() =>
|
|
39
37
|
Math.floor(Math.random() * phrases.length)
|
|
@@ -104,31 +102,18 @@ export function ThinkingIndicator({ isWorking = false, statusText }: { isWorking
|
|
|
104
102
|
const shownText = statusText || displayText;
|
|
105
103
|
|
|
106
104
|
return (
|
|
107
|
-
<View
|
|
105
|
+
<View className="flex-row items-center gap-2 py-2">
|
|
108
106
|
<Animated.View style={spinStyle}>
|
|
109
|
-
<Text
|
|
107
|
+
<Text className="text-base text-muted-foreground">✱</Text>
|
|
110
108
|
</Animated.View>
|
|
111
|
-
<View
|
|
112
|
-
<Text
|
|
109
|
+
<View className="flex-row items-center">
|
|
110
|
+
<Text className="text-base text-muted-foreground">{shownText}</Text>
|
|
113
111
|
{(statusText || isTyping) && (
|
|
114
112
|
<Animated.View style={cursorStyle}>
|
|
115
|
-
<Text
|
|
113
|
+
<Text className="text-base text-muted-foreground">|</Text>
|
|
116
114
|
</Animated.View>
|
|
117
115
|
)}
|
|
118
116
|
</View>
|
|
119
117
|
</View>
|
|
120
118
|
);
|
|
121
119
|
}
|
|
122
|
-
|
|
123
|
-
const styles = StyleSheet.create({
|
|
124
|
-
container: {
|
|
125
|
-
flexDirection: 'row',
|
|
126
|
-
alignItems: 'center',
|
|
127
|
-
gap: 8,
|
|
128
|
-
paddingVertical: 8,
|
|
129
|
-
},
|
|
130
|
-
textRow: {
|
|
131
|
-
flexDirection: 'row',
|
|
132
|
-
alignItems: 'center',
|
|
133
|
-
},
|
|
134
|
-
});
|
|
@@ -142,7 +142,7 @@ const DropdownMenuSubContent = React.forwardRef<
|
|
|
142
142
|
)}
|
|
143
143
|
style={{
|
|
144
144
|
boxShadow: '0 4px 6px -1px rgba(0,0,0,0.1), 0 2px 4px -2px rgba(0,0,0,0.1)',
|
|
145
|
-
border: '1px solid
|
|
145
|
+
border: '1px solid var(--border)',
|
|
146
146
|
...style as any,
|
|
147
147
|
}}
|
|
148
148
|
{...props}
|
|
@@ -168,7 +168,7 @@ const DropdownMenuContent = React.forwardRef<
|
|
|
168
168
|
)}
|
|
169
169
|
style={{
|
|
170
170
|
boxShadow: '0 4px 6px -1px rgba(0,0,0,0.1), 0 2px 4px -2px rgba(0,0,0,0.1)',
|
|
171
|
-
border: '1px solid
|
|
171
|
+
border: '1px solid var(--border)',
|
|
172
172
|
...style as any,
|
|
173
173
|
}}
|
|
174
174
|
{...props}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import React, { useEffect } from 'react';
|
|
2
|
-
import { View, Platform, useWindowDimensions } from 'react-native';
|
|
1
|
+
import React, { useEffect, useMemo } from 'react';
|
|
2
|
+
import { View, Platform, useWindowDimensions, useColorScheme } from 'react-native';
|
|
3
3
|
import Svg, { Path } from 'react-native-svg';
|
|
4
4
|
import Animated, {
|
|
5
5
|
useSharedValue,
|
|
@@ -15,6 +15,13 @@ import type { AgentState } from '../../types';
|
|
|
15
15
|
|
|
16
16
|
const AnimatedPath = Animated.createAnimatedComponent(Path);
|
|
17
17
|
|
|
18
|
+
/** Cross-platform blur: CSS string on web, RN array syntax on native */
|
|
19
|
+
function blurStyle(radius: number) {
|
|
20
|
+
return Platform.OS === 'web'
|
|
21
|
+
? ({ filter: `blur(${radius}px)`, WebkitFilter: `blur(${radius}px)` } as any)
|
|
22
|
+
: ({ filter: [{ blur: radius }] } as any);
|
|
23
|
+
}
|
|
24
|
+
|
|
18
25
|
const GLOW_HEIGHT = 350;
|
|
19
26
|
const NUM_POINTS = 8;
|
|
20
27
|
|
|
@@ -167,20 +174,79 @@ const BLOB_COLORS: Record<AgentState, string[]> = {
|
|
|
167
174
|
speaking: ['#1e2870', '#6366f1', '#9333ea', '#0ea5e9'],
|
|
168
175
|
};
|
|
169
176
|
|
|
177
|
+
// Derive wave & blob palettes from a hex color
|
|
178
|
+
function hexToRgb(hex: string): [number, number, number] {
|
|
179
|
+
const h = hex.replace('#', '');
|
|
180
|
+
return [parseInt(h.slice(0, 2), 16), parseInt(h.slice(2, 4), 16), parseInt(h.slice(4, 6), 16)];
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function darken(rgb: [number, number, number], factor: number): [number, number, number] {
|
|
184
|
+
return [Math.round(rgb[0] * factor), Math.round(rgb[1] * factor), Math.round(rgb[2] * factor)];
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function lighten(rgb: [number, number, number], factor: number): [number, number, number] {
|
|
188
|
+
return [
|
|
189
|
+
Math.min(255, Math.round(rgb[0] + (255 - rgb[0]) * factor)),
|
|
190
|
+
Math.min(255, Math.round(rgb[1] + (255 - rgb[1]) * factor)),
|
|
191
|
+
Math.min(255, Math.round(rgb[2] + (255 - rgb[2]) * factor)),
|
|
192
|
+
];
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function deriveWavePalette(hex: string, isDark: boolean): { waves: string[]; blobs: string[] } {
|
|
196
|
+
const rgb = hexToRgb(hex);
|
|
197
|
+
// Dark mode: lighter/brighter waves visible against dark bg
|
|
198
|
+
// Light mode: darker waves visible against light bg
|
|
199
|
+
const layer0 = isDark ? darken(rgb, 0.55) : darken(rgb, 0.3);
|
|
200
|
+
const layer1 = isDark ? lighten(rgb, 0.15) : darken(rgb, 0.5);
|
|
201
|
+
const layer2 = isDark ? lighten(rgb, 0.4) : darken(rgb, 0.15);
|
|
202
|
+
const layer3 = isDark ? lighten(rgb, 0.6) : rgb;
|
|
203
|
+
return {
|
|
204
|
+
waves: [
|
|
205
|
+
`rgba(${layer0[0]}, ${layer0[1]}, ${layer0[2]}, 0.8)`,
|
|
206
|
+
`rgba(${layer1[0]}, ${layer1[1]}, ${layer1[2]}, 0.45)`,
|
|
207
|
+
`rgba(${layer2[0]}, ${layer2[1]}, ${layer2[2]}, 0.35)`,
|
|
208
|
+
`rgba(${layer3[0]}, ${layer3[1]}, ${layer3[2]}, 0.25)`,
|
|
209
|
+
],
|
|
210
|
+
blobs: [
|
|
211
|
+
`rgb(${layer0[0]}, ${layer0[1]}, ${layer0[2]})`,
|
|
212
|
+
`rgb(${layer1[0]}, ${layer1[1]}, ${layer1[2]})`,
|
|
213
|
+
`rgb(${layer2[0]}, ${layer2[1]}, ${layer2[2]})`,
|
|
214
|
+
`rgb(${layer3[0]}, ${layer3[1]}, ${layer3[2]})`,
|
|
215
|
+
],
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
170
219
|
interface AudioWaveVisualizerProps {
|
|
171
220
|
waveAmplitude: SharedValue<number>;
|
|
172
221
|
agentState: AgentState;
|
|
173
222
|
isConnected: boolean;
|
|
223
|
+
/** Theme primary color hex — derives idle/listening wave palette */
|
|
224
|
+
primaryColor?: string;
|
|
225
|
+
/** Whether the app is in dark mode (affects wave brightness) */
|
|
226
|
+
isDarkMode?: boolean;
|
|
174
227
|
}
|
|
175
228
|
|
|
176
229
|
export function AudioWaveVisualizer({
|
|
177
230
|
waveAmplitude,
|
|
178
231
|
agentState = 'idle',
|
|
179
232
|
isConnected,
|
|
233
|
+
primaryColor,
|
|
234
|
+
isDarkMode,
|
|
180
235
|
}: AudioWaveVisualizerProps) {
|
|
181
236
|
const { width: screenWidth } = useWindowDimensions();
|
|
237
|
+
const scheme = useColorScheme();
|
|
238
|
+
const effectiveIsDark = isDarkMode ?? (scheme === 'dark');
|
|
182
239
|
const state = WAVE_COLORS[agentState] ? agentState : 'idle';
|
|
183
240
|
|
|
241
|
+
// Use theme-derived palette for idle/listening, keep hardcoded for thinking/speaking
|
|
242
|
+
const themed = useMemo(
|
|
243
|
+
() => (primaryColor ? deriveWavePalette(primaryColor, effectiveIsDark) : null),
|
|
244
|
+
[primaryColor, effectiveIsDark],
|
|
245
|
+
);
|
|
246
|
+
const isThemed = (state === 'idle' || state === 'listening') && themed != null;
|
|
247
|
+
const waveColors = isThemed ? themed.waves : WAVE_COLORS[state];
|
|
248
|
+
const blobColors = isThemed ? themed.blobs : BLOB_COLORS[state];
|
|
249
|
+
|
|
184
250
|
return (
|
|
185
251
|
<View
|
|
186
252
|
style={{
|
|
@@ -194,7 +260,7 @@ export function AudioWaveVisualizer({
|
|
|
194
260
|
<GlowBlob
|
|
195
261
|
key={`blob-${i}`}
|
|
196
262
|
blob={blob}
|
|
197
|
-
color={
|
|
263
|
+
color={blobColors[i]}
|
|
198
264
|
waveAmplitude={waveAmplitude}
|
|
199
265
|
isConnected={isConnected}
|
|
200
266
|
screenWidth={screenWidth}
|
|
@@ -206,7 +272,7 @@ export function AudioWaveVisualizer({
|
|
|
206
272
|
<OceanWave
|
|
207
273
|
key={`wave-${i}`}
|
|
208
274
|
layer={layer}
|
|
209
|
-
color={
|
|
275
|
+
color={waveColors[i]}
|
|
210
276
|
waveAmplitude={waveAmplitude}
|
|
211
277
|
isConnected={isConnected}
|
|
212
278
|
screenWidth={screenWidth}
|
|
@@ -279,14 +345,6 @@ function OceanWave({
|
|
|
279
345
|
return { d };
|
|
280
346
|
});
|
|
281
347
|
|
|
282
|
-
const webBlurStyle =
|
|
283
|
-
Platform.OS === 'web'
|
|
284
|
-
? ({
|
|
285
|
-
filter: `blur(${layer.blur}px)`,
|
|
286
|
-
WebkitFilter: `blur(${layer.blur}px)`,
|
|
287
|
-
} as any)
|
|
288
|
-
: {};
|
|
289
|
-
|
|
290
348
|
return (
|
|
291
349
|
<View
|
|
292
350
|
style={{
|
|
@@ -296,7 +354,7 @@ function OceanWave({
|
|
|
296
354
|
right: 0,
|
|
297
355
|
bottom: 0,
|
|
298
356
|
opacity: isConnected ? 1 : 0,
|
|
299
|
-
...
|
|
357
|
+
...blurStyle(layer.blur),
|
|
300
358
|
pointerEvents: 'none',
|
|
301
359
|
}}
|
|
302
360
|
>
|
|
@@ -365,13 +423,5 @@ function GlowBlob({
|
|
|
365
423
|
};
|
|
366
424
|
});
|
|
367
425
|
|
|
368
|
-
|
|
369
|
-
Platform.OS === 'web'
|
|
370
|
-
? ({
|
|
371
|
-
filter: `blur(${blob.blur}px)`,
|
|
372
|
-
WebkitFilter: `blur(${blob.blur}px)`,
|
|
373
|
-
} as any)
|
|
374
|
-
: {};
|
|
375
|
-
|
|
376
|
-
return <Animated.View style={[animatedStyle, webBlurStyle]} />;
|
|
426
|
+
return <Animated.View style={[animatedStyle, blurStyle(blob.blur)]} />;
|
|
377
427
|
}
|