@alia.onl/sdk 2.0.0 → 3.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/package.json +25 -3
- package/src/components/AliaChatContent.tsx +313 -0
- package/src/components/AliaChatInput.tsx +173 -26
- package/src/components/AliaChatMessageList.tsx +136 -3
- package/src/components/AliaChatScreen.tsx +132 -0
- package/src/components/AliaChatSheet.tsx +31 -40
- package/src/components/ui/button.tsx +104 -0
- package/src/components/ui/chat-text-input.tsx +164 -0
- package/src/components/ui/dropdown-menu.tsx +2 -0
- package/src/components/ui/dropdown-menu.web.tsx +355 -0
- package/src/components/ui/prompt-input/actions.tsx +28 -0
- package/src/components/ui/prompt-input/add-menu.tsx +83 -0
- package/src/components/ui/prompt-input/attachments.tsx +183 -0
- package/src/components/ui/prompt-input/autocomplete.tsx +168 -0
- package/src/components/ui/prompt-input/context.tsx +71 -0
- package/src/components/ui/prompt-input/mic-button.tsx +53 -0
- package/src/components/ui/prompt-input/prompt-input.tsx +276 -0
- package/src/components/ui/prompt-input/submit-button.tsx +49 -0
- package/src/components/ui/prompt-input/textarea.tsx +64 -0
- package/src/components/ui/text.tsx +29 -0
- package/src/hooks/useAliaChat.ts +16 -7
- package/src/hooks/useDocumentPicker.ts +37 -0
- package/src/hooks/useImagePicker.ts +38 -0
- package/src/hooks/useSpeechToText.ts +4 -2
- package/src/index.ts +33 -2
- package/src/lib/keyboard.native.tsx +6 -0
- package/src/lib/keyboard.tsx +25 -0
- package/src/lib/utils.ts +14 -0
- package/src/optional-deps.d.ts +17 -0
- package/src/types.ts +7 -3
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import React, { useRef, useEffect } from 'react';
|
|
1
|
+
import React, { useRef, useEffect, useState, useCallback } from 'react';
|
|
2
2
|
import {
|
|
3
3
|
View,
|
|
4
4
|
Text,
|
|
5
|
+
Pressable,
|
|
5
6
|
StyleSheet,
|
|
7
|
+
Platform,
|
|
6
8
|
} from 'react-native';
|
|
7
9
|
import { BlurView } from 'expo-blur';
|
|
8
10
|
import Animated, {
|
|
@@ -25,10 +27,15 @@ import type { ChatMessage, ToolInvocation } from '../types';
|
|
|
25
27
|
|
|
26
28
|
const ENTER_ANIMATION = FadeInUp.duration(200);
|
|
27
29
|
|
|
28
|
-
|
|
30
|
+
type PlaybackState = 'idle' | 'loading' | 'playing' | 'paused' | 'error';
|
|
31
|
+
|
|
32
|
+
export interface AliaChatMessageListProps {
|
|
29
33
|
messages: ChatMessage[];
|
|
30
34
|
isStreaming: boolean;
|
|
31
35
|
scrollOffsetY: SharedValue<number>;
|
|
36
|
+
onReadAloud?: (messageId: string, text: string) => void;
|
|
37
|
+
ttsActiveMessageId?: string | null;
|
|
38
|
+
ttsPlaybackState?: PlaybackState;
|
|
32
39
|
}
|
|
33
40
|
|
|
34
41
|
// ── Tool Bullet ──
|
|
@@ -36,7 +43,6 @@ interface AliaChatMessageListProps {
|
|
|
36
43
|
function ToolBullet({ invocation }: { invocation: ToolInvocation }) {
|
|
37
44
|
const colors = useAliaColors();
|
|
38
45
|
|
|
39
|
-
// Clean up tool name for display: "oxy_inbox__searchEmails" → "Searching emails"
|
|
40
46
|
const displayName = invocation.toolName
|
|
41
47
|
.replace(/^oxy_\w+__/, '')
|
|
42
48
|
.replace(/([A-Z])/g, ' $1')
|
|
@@ -99,14 +105,107 @@ function UserBubble({ content }: { content: string }) {
|
|
|
99
105
|
);
|
|
100
106
|
}
|
|
101
107
|
|
|
108
|
+
// ── Message Actions ──
|
|
109
|
+
|
|
110
|
+
function MessageActions({
|
|
111
|
+
message,
|
|
112
|
+
onReadAloud,
|
|
113
|
+
ttsActiveMessageId,
|
|
114
|
+
ttsPlaybackState,
|
|
115
|
+
}: {
|
|
116
|
+
message: ChatMessage;
|
|
117
|
+
onReadAloud?: (messageId: string, text: string) => void;
|
|
118
|
+
ttsActiveMessageId?: string | null;
|
|
119
|
+
ttsPlaybackState?: PlaybackState;
|
|
120
|
+
}) {
|
|
121
|
+
const colors = useAliaColors();
|
|
122
|
+
const [copied, setCopied] = useState(false);
|
|
123
|
+
|
|
124
|
+
const handleCopy = useCallback(async () => {
|
|
125
|
+
try {
|
|
126
|
+
if (Platform.OS === 'web') {
|
|
127
|
+
await navigator.clipboard.writeText(message.content);
|
|
128
|
+
} else {
|
|
129
|
+
const Clipboard = await import('expo-clipboard');
|
|
130
|
+
await Clipboard.setStringAsync(message.content);
|
|
131
|
+
}
|
|
132
|
+
setCopied(true);
|
|
133
|
+
setTimeout(() => setCopied(false), 2000);
|
|
134
|
+
} catch {}
|
|
135
|
+
}, [message.content]);
|
|
136
|
+
|
|
137
|
+
const isThisPlaying = ttsActiveMessageId === message.id && ttsPlaybackState === 'playing';
|
|
138
|
+
const isThisLoading = ttsActiveMessageId === message.id && ttsPlaybackState === 'loading';
|
|
139
|
+
|
|
140
|
+
if (!message.content) return null;
|
|
141
|
+
|
|
142
|
+
return (
|
|
143
|
+
<View style={styles.actionsRow}>
|
|
144
|
+
{/* Copy */}
|
|
145
|
+
<Pressable onPress={handleCopy} style={styles.actionBtn} hitSlop={8}>
|
|
146
|
+
{copied ? (
|
|
147
|
+
<Text style={[styles.actionIcon, { color: colors.tint }]}>{'\u2713'}</Text>
|
|
148
|
+
) : (
|
|
149
|
+
<CopyIcon color={colors.secondaryText} />
|
|
150
|
+
)}
|
|
151
|
+
</Pressable>
|
|
152
|
+
|
|
153
|
+
{/* Read aloud */}
|
|
154
|
+
{onReadAloud && (
|
|
155
|
+
<Pressable
|
|
156
|
+
onPress={() => onReadAloud(message.id, message.content)}
|
|
157
|
+
style={styles.actionBtn}
|
|
158
|
+
hitSlop={8}
|
|
159
|
+
>
|
|
160
|
+
{isThisLoading ? (
|
|
161
|
+
<Text style={[styles.actionIcon, { color: colors.secondaryText }]}>{'\u2026'}</Text>
|
|
162
|
+
) : (
|
|
163
|
+
<VolumeIcon color={isThisPlaying ? colors.tint : colors.secondaryText} />
|
|
164
|
+
)}
|
|
165
|
+
</Pressable>
|
|
166
|
+
)}
|
|
167
|
+
</View>
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function CopyIcon({ color }: { color: string }) {
|
|
172
|
+
if (Platform.OS === 'web') {
|
|
173
|
+
return (
|
|
174
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke={color} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
175
|
+
<rect x="9" y="9" width="13" height="13" rx="2" ry="2" />
|
|
176
|
+
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
|
|
177
|
+
</svg>
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
return <Text style={[styles.actionIcon, { color }]}>{'\u2398'}</Text>;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function VolumeIcon({ color }: { color: string }) {
|
|
184
|
+
if (Platform.OS === 'web') {
|
|
185
|
+
return (
|
|
186
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke={color} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
187
|
+
<path d="M11 5L6 9H2v6h4l5 4V5z" />
|
|
188
|
+
<path d="M15.54 8.46a5 5 0 0 1 0 7.07" />
|
|
189
|
+
</svg>
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
return <Text style={[styles.actionIcon, { color }]}>{'\u{1F50A}'}</Text>;
|
|
193
|
+
}
|
|
194
|
+
|
|
102
195
|
// ── Assistant Message ──
|
|
103
196
|
|
|
104
197
|
function AssistantMessage({
|
|
105
198
|
message,
|
|
106
199
|
isStreamingThis,
|
|
200
|
+
onReadAloud,
|
|
201
|
+
ttsActiveMessageId,
|
|
202
|
+
ttsPlaybackState,
|
|
107
203
|
}: {
|
|
108
204
|
message: ChatMessage;
|
|
109
205
|
isStreamingThis: boolean;
|
|
206
|
+
onReadAloud?: (messageId: string, text: string) => void;
|
|
207
|
+
ttsActiveMessageId?: string | null;
|
|
208
|
+
ttsPlaybackState?: PlaybackState;
|
|
110
209
|
}) {
|
|
111
210
|
const colors = useAliaColors();
|
|
112
211
|
const hasContent = !!message.content;
|
|
@@ -143,6 +242,16 @@ function AssistantMessage({
|
|
|
143
242
|
{isStreamingThis && hasContent && (
|
|
144
243
|
<Text style={[styles.cursor, { color: colors.secondaryText }]}>{'\u2758'}</Text>
|
|
145
244
|
)}
|
|
245
|
+
|
|
246
|
+
{/* Message actions (copy, read aloud) — shown when not streaming */}
|
|
247
|
+
{!isStreamingThis && hasContent && (
|
|
248
|
+
<MessageActions
|
|
249
|
+
message={message}
|
|
250
|
+
onReadAloud={onReadAloud}
|
|
251
|
+
ttsActiveMessageId={ttsActiveMessageId}
|
|
252
|
+
ttsPlaybackState={ttsPlaybackState}
|
|
253
|
+
/>
|
|
254
|
+
)}
|
|
146
255
|
</Animated.View>
|
|
147
256
|
);
|
|
148
257
|
}
|
|
@@ -153,6 +262,9 @@ export function AliaChatMessageList({
|
|
|
153
262
|
messages,
|
|
154
263
|
isStreaming,
|
|
155
264
|
scrollOffsetY,
|
|
265
|
+
onReadAloud,
|
|
266
|
+
ttsActiveMessageId,
|
|
267
|
+
ttsPlaybackState,
|
|
156
268
|
}: AliaChatMessageListProps) {
|
|
157
269
|
const scrollRef = useRef<Animated.ScrollView>(null);
|
|
158
270
|
|
|
@@ -196,6 +308,9 @@ export function AliaChatMessageList({
|
|
|
196
308
|
key={msg.id}
|
|
197
309
|
message={msg}
|
|
198
310
|
isStreamingThis={isStreamingThis}
|
|
311
|
+
onReadAloud={onReadAloud}
|
|
312
|
+
ttsActiveMessageId={ttsActiveMessageId}
|
|
313
|
+
ttsPlaybackState={ttsPlaybackState}
|
|
199
314
|
/>
|
|
200
315
|
);
|
|
201
316
|
})}
|
|
@@ -261,4 +376,22 @@ const styles = StyleSheet.create({
|
|
|
261
376
|
toolLabel: {
|
|
262
377
|
fontSize: 13,
|
|
263
378
|
},
|
|
379
|
+
|
|
380
|
+
// Message actions
|
|
381
|
+
actionsRow: {
|
|
382
|
+
flexDirection: 'row',
|
|
383
|
+
alignItems: 'center',
|
|
384
|
+
gap: 12,
|
|
385
|
+
marginTop: 6,
|
|
386
|
+
opacity: 0.7,
|
|
387
|
+
},
|
|
388
|
+
actionBtn: {
|
|
389
|
+
width: 28,
|
|
390
|
+
height: 28,
|
|
391
|
+
alignItems: 'center',
|
|
392
|
+
justifyContent: 'center',
|
|
393
|
+
},
|
|
394
|
+
actionIcon: {
|
|
395
|
+
fontSize: 14,
|
|
396
|
+
},
|
|
264
397
|
});
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Full-screen Alia chat component for use as a route/screen in Oxy ecosystem apps.
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* ```tsx
|
|
6
|
+
* import { AliaChatScreen } from '@alia.onl/sdk';
|
|
7
|
+
*
|
|
8
|
+
* <AliaChatScreen
|
|
9
|
+
* clientContext="Notes app — user is viewing note #42"
|
|
10
|
+
* suggestions={[{ label: 'Summarize', prompt: 'Summarize this note' }]}
|
|
11
|
+
* headerLeft={<BackButton />}
|
|
12
|
+
* />
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import React, { useRef, useState, useEffect, useCallback } from 'react';
|
|
17
|
+
import { View, Text, TouchableOpacity, StyleSheet } from 'react-native';
|
|
18
|
+
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
19
|
+
import { AliaChatContent, type AliaChatContentRef } from './AliaChatContent';
|
|
20
|
+
import { AliaFace, type AliaExpression } from './AliaFace';
|
|
21
|
+
import { useAliaColors } from '../theme';
|
|
22
|
+
import type { AliaChatSuggestion } from '../types';
|
|
23
|
+
|
|
24
|
+
export interface AliaChatScreenProps {
|
|
25
|
+
/** App context injected into system prompt */
|
|
26
|
+
clientContext?: string;
|
|
27
|
+
/** Quick action suggestions shown when chat is empty */
|
|
28
|
+
suggestions?: AliaChatSuggestion[];
|
|
29
|
+
/** Alia model (default: 'alia-v1') */
|
|
30
|
+
model?: string;
|
|
31
|
+
/** API URL override */
|
|
32
|
+
apiUrl?: string;
|
|
33
|
+
/** Optional header left action (e.g., back button) */
|
|
34
|
+
headerLeft?: React.ReactNode;
|
|
35
|
+
/** Optional header right action (e.g., settings button) */
|
|
36
|
+
headerRight?: React.ReactNode;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function AliaChatScreen({
|
|
40
|
+
clientContext,
|
|
41
|
+
suggestions = [],
|
|
42
|
+
model,
|
|
43
|
+
apiUrl,
|
|
44
|
+
headerLeft,
|
|
45
|
+
headerRight,
|
|
46
|
+
}: AliaChatScreenProps) {
|
|
47
|
+
const colors = useAliaColors();
|
|
48
|
+
const insets = useSafeAreaInsets();
|
|
49
|
+
const contentRef = useRef<AliaChatContentRef>(null);
|
|
50
|
+
|
|
51
|
+
const [faceExpression, setFaceExpression] = useState<AliaExpression>('Idle A');
|
|
52
|
+
const [hasMessages, setHasMessages] = useState(false);
|
|
53
|
+
|
|
54
|
+
// Poll content ref for header state
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
const interval = setInterval(() => {
|
|
57
|
+
if (contentRef.current) {
|
|
58
|
+
setFaceExpression(contentRef.current.faceExpression);
|
|
59
|
+
setHasMessages(contentRef.current.messages.length > 0);
|
|
60
|
+
}
|
|
61
|
+
}, 200);
|
|
62
|
+
return () => clearInterval(interval);
|
|
63
|
+
}, []);
|
|
64
|
+
|
|
65
|
+
const handleClear = useCallback(() => {
|
|
66
|
+
contentRef.current?.clear();
|
|
67
|
+
}, []);
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<View style={[styles.container, { backgroundColor: colors.background, paddingTop: insets.top }]}>
|
|
71
|
+
{/* Header */}
|
|
72
|
+
<View style={styles.header}>
|
|
73
|
+
<View style={styles.headerLeft}>
|
|
74
|
+
{headerLeft}
|
|
75
|
+
<AliaFace size={28} expression={faceExpression} />
|
|
76
|
+
<Text style={[styles.headerTitle, { color: colors.text }]}>Alia</Text>
|
|
77
|
+
</View>
|
|
78
|
+
<View style={styles.headerRight}>
|
|
79
|
+
{hasMessages && (
|
|
80
|
+
<TouchableOpacity onPress={handleClear} style={styles.clearButton}>
|
|
81
|
+
<Text style={[styles.clearText, { color: colors.secondaryText }]}>Clear</Text>
|
|
82
|
+
</TouchableOpacity>
|
|
83
|
+
)}
|
|
84
|
+
{headerRight}
|
|
85
|
+
</View>
|
|
86
|
+
</View>
|
|
87
|
+
|
|
88
|
+
{/* Chat content */}
|
|
89
|
+
<AliaChatContent
|
|
90
|
+
ref={contentRef}
|
|
91
|
+
clientContext={clientContext}
|
|
92
|
+
suggestions={suggestions}
|
|
93
|
+
model={model}
|
|
94
|
+
apiUrl={apiUrl}
|
|
95
|
+
/>
|
|
96
|
+
</View>
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const styles = StyleSheet.create({
|
|
101
|
+
container: {
|
|
102
|
+
flex: 1,
|
|
103
|
+
},
|
|
104
|
+
header: {
|
|
105
|
+
flexDirection: 'row',
|
|
106
|
+
alignItems: 'center',
|
|
107
|
+
justifyContent: 'space-between',
|
|
108
|
+
paddingHorizontal: 16,
|
|
109
|
+
paddingVertical: 12,
|
|
110
|
+
},
|
|
111
|
+
headerLeft: {
|
|
112
|
+
flexDirection: 'row',
|
|
113
|
+
alignItems: 'center',
|
|
114
|
+
gap: 10,
|
|
115
|
+
},
|
|
116
|
+
headerTitle: {
|
|
117
|
+
fontSize: 18,
|
|
118
|
+
fontWeight: '600',
|
|
119
|
+
},
|
|
120
|
+
headerRight: {
|
|
121
|
+
flexDirection: 'row',
|
|
122
|
+
alignItems: 'center',
|
|
123
|
+
gap: 8,
|
|
124
|
+
},
|
|
125
|
+
clearButton: {
|
|
126
|
+
paddingHorizontal: 10,
|
|
127
|
+
paddingVertical: 6,
|
|
128
|
+
},
|
|
129
|
+
clearText: {
|
|
130
|
+
fontSize: 14,
|
|
131
|
+
},
|
|
132
|
+
});
|
|
@@ -27,10 +27,7 @@ import Animated, {
|
|
|
27
27
|
withTiming,
|
|
28
28
|
} from 'react-native-reanimated';
|
|
29
29
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
30
|
-
import {
|
|
31
|
-
import { AliaChatMessageList } from './AliaChatMessageList';
|
|
32
|
-
import { AliaChatInput } from './AliaChatInput';
|
|
33
|
-
import { AliaChatSuggestions } from './AliaChatSuggestions';
|
|
30
|
+
import { AliaChatContent, type AliaChatContentRef } from './AliaChatContent';
|
|
34
31
|
import { AliaFace, type AliaExpression } from './AliaFace';
|
|
35
32
|
import { useAliaColors } from '../theme';
|
|
36
33
|
import type { AliaChatSuggestion } from '../types';
|
|
@@ -65,15 +62,28 @@ export const AliaChatSheet = forwardRef<AliaChatSheetRef, AliaChatSheetProps>(
|
|
|
65
62
|
const isDark = colors.isDark;
|
|
66
63
|
const insets = useSafeAreaInsets();
|
|
67
64
|
|
|
68
|
-
//
|
|
69
|
-
const
|
|
70
|
-
const
|
|
65
|
+
// Content ref for reading state
|
|
66
|
+
const contentRef = useRef<AliaChatContentRef>(null);
|
|
67
|
+
const [faceExpression, setFaceExpression] = useState<AliaExpression>('Idle A');
|
|
68
|
+
const [hasMessages, setHasMessages] = useState(false);
|
|
71
69
|
|
|
72
70
|
// Sheet visibility
|
|
73
71
|
const [rendered, setRendered] = useState(false);
|
|
74
72
|
const hasClosedRef = useRef(false);
|
|
75
73
|
const closeTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
76
74
|
|
|
75
|
+
// Poll content ref for header state (lightweight, only when rendered)
|
|
76
|
+
useEffect(() => {
|
|
77
|
+
if (!rendered) return;
|
|
78
|
+
const interval = setInterval(() => {
|
|
79
|
+
if (contentRef.current) {
|
|
80
|
+
setFaceExpression(contentRef.current.faceExpression);
|
|
81
|
+
setHasMessages(contentRef.current.messages.length > 0);
|
|
82
|
+
}
|
|
83
|
+
}, 200);
|
|
84
|
+
return () => clearInterval(interval);
|
|
85
|
+
}, [rendered]);
|
|
86
|
+
|
|
77
87
|
// Reanimated shared values
|
|
78
88
|
const translateY = useSharedValue(SCREEN_HEIGHT);
|
|
79
89
|
const backdropOpacity = useSharedValue(0);
|
|
@@ -200,16 +210,9 @@ export const AliaChatSheet = forwardRef<AliaChatSheetRef, AliaChatSheetProps>(
|
|
|
200
210
|
[insets.top],
|
|
201
211
|
);
|
|
202
212
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
const lastMsg = messages[messages.length - 1];
|
|
207
|
-
if (lastMsg?.role === 'assistant' && lastMsg.toolInvocations?.some(t => t.state === 'call')) return 'Searching A';
|
|
208
|
-
if (lastMsg?.thinking) return 'Thinking';
|
|
209
|
-
return 'Writing E';
|
|
210
|
-
}, [messages, isStreaming]);
|
|
211
|
-
|
|
212
|
-
const showSuggestions = messages.length === 0 && suggestions.length > 0;
|
|
213
|
+
const handleClear = useCallback(() => {
|
|
214
|
+
contentRef.current?.clear();
|
|
215
|
+
}, []);
|
|
213
216
|
|
|
214
217
|
if (!rendered) return null;
|
|
215
218
|
|
|
@@ -256,8 +259,8 @@ export const AliaChatSheet = forwardRef<AliaChatSheetRef, AliaChatSheetProps>(
|
|
|
256
259
|
</Text>
|
|
257
260
|
</View>
|
|
258
261
|
<View style={styles.headerRight}>
|
|
259
|
-
{
|
|
260
|
-
<TouchableOpacity onPress={
|
|
262
|
+
{hasMessages && (
|
|
263
|
+
<TouchableOpacity onPress={handleClear} style={styles.clearButton}>
|
|
261
264
|
<Text
|
|
262
265
|
style={[
|
|
263
266
|
styles.clearText,
|
|
@@ -286,26 +289,17 @@ export const AliaChatSheet = forwardRef<AliaChatSheetRef, AliaChatSheetProps>(
|
|
|
286
289
|
</View>
|
|
287
290
|
</View>
|
|
288
291
|
|
|
289
|
-
{/*
|
|
292
|
+
{/* Chat content */}
|
|
290
293
|
<GestureDetector gesture={nativeGesture}>
|
|
291
|
-
<
|
|
292
|
-
{
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
messages={messages}
|
|
300
|
-
isStreaming={isStreaming}
|
|
301
|
-
scrollOffsetY={scrollOffsetY}
|
|
302
|
-
/>
|
|
303
|
-
)}
|
|
304
|
-
</View>
|
|
294
|
+
<AliaChatContent
|
|
295
|
+
ref={contentRef}
|
|
296
|
+
clientContext={clientContext}
|
|
297
|
+
suggestions={suggestions}
|
|
298
|
+
model={model}
|
|
299
|
+
apiUrl={apiUrl}
|
|
300
|
+
scrollOffsetY={scrollOffsetY}
|
|
301
|
+
/>
|
|
305
302
|
</GestureDetector>
|
|
306
|
-
|
|
307
|
-
{/* Input */}
|
|
308
|
-
<AliaChatInput onSend={send} isStreaming={isStreaming} />
|
|
309
303
|
</Animated.View>
|
|
310
304
|
</GestureDetector>
|
|
311
305
|
</GestureHandlerRootView>
|
|
@@ -384,7 +378,4 @@ const styles = StyleSheet.create({
|
|
|
384
378
|
justifyContent: 'center',
|
|
385
379
|
borderRadius: 16,
|
|
386
380
|
},
|
|
387
|
-
chatArea: {
|
|
388
|
-
flex: 1,
|
|
389
|
-
},
|
|
390
381
|
});
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import { Pressable, ActivityIndicator } from "react-native";
|
|
4
|
+
import { TextClassContext } from "./text";
|
|
5
|
+
import { cn } from "../../lib/utils";
|
|
6
|
+
|
|
7
|
+
const buttonVariants = cva(
|
|
8
|
+
"group flex items-center justify-center rounded-xl web:ring-offset-background web:transition-colors web:focus-visible:outline-none web:focus-visible:ring-2 web:focus-visible:ring-ring web:focus-visible:ring-offset-2",
|
|
9
|
+
{
|
|
10
|
+
variants: {
|
|
11
|
+
variant: {
|
|
12
|
+
default: "bg-primary web:hover:opacity-90 active:opacity-90",
|
|
13
|
+
destructive: "bg-destructive web:hover:opacity-90 active:opacity-90",
|
|
14
|
+
outline:
|
|
15
|
+
"border border-input bg-background web:hover:bg-accent web:hover:text-accent-foreground active:bg-accent",
|
|
16
|
+
secondary: "bg-secondary web:hover:opacity-80 active:opacity-80",
|
|
17
|
+
ghost:
|
|
18
|
+
"web:hover:bg-accent web:hover:text-accent-foreground active:bg-accent",
|
|
19
|
+
link: "web:underline-offset-4 web:hover:underline web:focus:underline ",
|
|
20
|
+
pill: "text-primary-foreground rounded-full bg-black web:hover:opacity-90 active:opacity-90",
|
|
21
|
+
},
|
|
22
|
+
size: {
|
|
23
|
+
default: "h-9 px-3.5 py-2 native:h-11 native:px-4",
|
|
24
|
+
sm: "h-8 rounded-lg px-2.5",
|
|
25
|
+
lg: "h-10 rounded-xl px-6 native:h-12",
|
|
26
|
+
icon: "h-9 w-9",
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
defaultVariants: {
|
|
30
|
+
variant: "default",
|
|
31
|
+
size: "default",
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
const buttonTextVariants = cva(
|
|
37
|
+
"web:whitespace-nowrap text-sm native:text-base font-medium text-foreground web:transition-colors",
|
|
38
|
+
{
|
|
39
|
+
variants: {
|
|
40
|
+
variant: {
|
|
41
|
+
default: "text-primary-foreground",
|
|
42
|
+
destructive: "text-destructive-foreground",
|
|
43
|
+
outline: "group-active:text-accent-foreground",
|
|
44
|
+
secondary:
|
|
45
|
+
"text-secondary-foreground group-active:text-secondary-foreground",
|
|
46
|
+
ghost: "group-active:text-accent-foreground",
|
|
47
|
+
link: "text-primary group-active:underline",
|
|
48
|
+
pill: "text-primary-foreground",
|
|
49
|
+
},
|
|
50
|
+
size: {
|
|
51
|
+
default: "",
|
|
52
|
+
sm: "",
|
|
53
|
+
lg: "native:text-lg",
|
|
54
|
+
icon: "",
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
defaultVariants: {
|
|
58
|
+
variant: "default",
|
|
59
|
+
size: "default",
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
type ButtonProps = React.ComponentPropsWithoutRef<typeof Pressable> &
|
|
65
|
+
VariantProps<typeof buttonVariants> & {
|
|
66
|
+
isLoading?: boolean;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const Button = React.forwardRef<
|
|
70
|
+
React.ElementRef<typeof Pressable>,
|
|
71
|
+
ButtonProps
|
|
72
|
+
>(({ className, variant, size, isLoading, children, ...props }, ref) => {
|
|
73
|
+
return (
|
|
74
|
+
<TextClassContext.Provider
|
|
75
|
+
value={cn(
|
|
76
|
+
props.disabled && "web:pointer-events-none",
|
|
77
|
+
buttonTextVariants({ variant, size }),
|
|
78
|
+
)}
|
|
79
|
+
>
|
|
80
|
+
<Pressable
|
|
81
|
+
className={cn(
|
|
82
|
+
props.disabled && "opacity-50 web:pointer-events-none",
|
|
83
|
+
isLoading && "web:pointer-events-none",
|
|
84
|
+
buttonVariants({ variant, size, className }),
|
|
85
|
+
)}
|
|
86
|
+
ref={ref}
|
|
87
|
+
role="button"
|
|
88
|
+
{...props}
|
|
89
|
+
>
|
|
90
|
+
{isLoading ? (
|
|
91
|
+
<ActivityIndicator
|
|
92
|
+
color={variant === "outline" ? "black" : "white"}
|
|
93
|
+
/>
|
|
94
|
+
) : (
|
|
95
|
+
children
|
|
96
|
+
)}
|
|
97
|
+
</Pressable>
|
|
98
|
+
</TextClassContext.Provider>
|
|
99
|
+
);
|
|
100
|
+
});
|
|
101
|
+
Button.displayName = "Button";
|
|
102
|
+
|
|
103
|
+
export { Button, buttonTextVariants, buttonVariants };
|
|
104
|
+
export type { ButtonProps };
|