@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
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import React, { useCallback, useEffect, useRef, useState } from "react";
|
|
2
|
+
import {
|
|
3
|
+
View,
|
|
4
|
+
Pressable,
|
|
5
|
+
type TextInput as RNTextInput,
|
|
6
|
+
} from "react-native";
|
|
7
|
+
import { KeyboardAvoidingView } from "../../../lib/keyboard";
|
|
8
|
+
import { Maximize2, Minimize2 } from "lucide-react-native";
|
|
9
|
+
import { cn } from "../../../lib/utils";
|
|
10
|
+
import { PromptInputContext, type Attachment, type Completion } from "./context";
|
|
11
|
+
import { PromptInputTextarea } from "./textarea";
|
|
12
|
+
import { PromptInputActions } from "./actions";
|
|
13
|
+
import { PromptInputMicButton } from "./mic-button";
|
|
14
|
+
import { PromptInputAutocomplete } from "./autocomplete";
|
|
15
|
+
import { PromptInputAttachments } from "./attachments";
|
|
16
|
+
import { PromptInputSubmitButton } from "./submit-button";
|
|
17
|
+
import { PromptInputAddMenu } from "./add-menu";
|
|
18
|
+
|
|
19
|
+
export type PromptInputProps = {
|
|
20
|
+
isLoading?: boolean;
|
|
21
|
+
value?: string;
|
|
22
|
+
onValueChange?: (value: string) => void;
|
|
23
|
+
maxHeight?: number;
|
|
24
|
+
onSubmit?: () => void;
|
|
25
|
+
children?: React.ReactNode;
|
|
26
|
+
className?: string;
|
|
27
|
+
disabled?: boolean;
|
|
28
|
+
onImagePaste?: (files: File[]) => void;
|
|
29
|
+
// Simple mode props (when no children)
|
|
30
|
+
placeholder?: string;
|
|
31
|
+
autocomplete?: boolean;
|
|
32
|
+
autocompletePosition?: "top" | "bottom";
|
|
33
|
+
// Shows the add menu as a standalone button to the left of the input box
|
|
34
|
+
leadingAddMenu?: boolean;
|
|
35
|
+
// Custom left-side actions (replaces default add menu in the actions bar)
|
|
36
|
+
actionsLeft?: React.ReactNode;
|
|
37
|
+
// Submit button props
|
|
38
|
+
onStop?: () => void;
|
|
39
|
+
emptyAction?: React.ReactNode;
|
|
40
|
+
// Controlled attachments (optional — uses internal state if omitted)
|
|
41
|
+
attachments?: Attachment[];
|
|
42
|
+
onAddAttachment?: (attachment: Attachment) => void;
|
|
43
|
+
onRemoveAttachment?: (id: string) => void;
|
|
44
|
+
onUpdateAttachment?: (id: string, updates: Partial<Attachment>) => void;
|
|
45
|
+
// Injectable autocomplete hooks
|
|
46
|
+
useSuggestions?: (query: string) => { data: Completion[] | undefined; isLoading: boolean };
|
|
47
|
+
onSuggestionUsed?: (suggestionId: string) => void;
|
|
48
|
+
// Injectable error handler (replaces toast)
|
|
49
|
+
onError?: (message: string) => void;
|
|
50
|
+
// Mic button API URL
|
|
51
|
+
apiUrl?: string;
|
|
52
|
+
} & Omit<React.ComponentProps<typeof View>, "children">;
|
|
53
|
+
|
|
54
|
+
export function PromptInput({
|
|
55
|
+
className,
|
|
56
|
+
isLoading = false,
|
|
57
|
+
maxHeight = 240,
|
|
58
|
+
value,
|
|
59
|
+
onValueChange,
|
|
60
|
+
onSubmit,
|
|
61
|
+
children,
|
|
62
|
+
disabled = false,
|
|
63
|
+
onImagePaste,
|
|
64
|
+
placeholder,
|
|
65
|
+
autocomplete = false,
|
|
66
|
+
autocompletePosition = "top",
|
|
67
|
+
leadingAddMenu = false,
|
|
68
|
+
actionsLeft,
|
|
69
|
+
onStop,
|
|
70
|
+
emptyAction,
|
|
71
|
+
attachments: controlledAttachments,
|
|
72
|
+
onAddAttachment,
|
|
73
|
+
onRemoveAttachment,
|
|
74
|
+
onUpdateAttachment,
|
|
75
|
+
useSuggestions,
|
|
76
|
+
onSuggestionUsed,
|
|
77
|
+
onError,
|
|
78
|
+
apiUrl,
|
|
79
|
+
...props
|
|
80
|
+
}: PromptInputProps) {
|
|
81
|
+
const [internalValue, setInternalValue] = useState(value || "");
|
|
82
|
+
const [currentHeight, setCurrentHeight] = useState(44);
|
|
83
|
+
const [showFullscreen, setShowFullscreen] = useState(false);
|
|
84
|
+
const [handleCompletionKey, setHandleCompletionKey] = useState<((key: string) => boolean) | null>(null);
|
|
85
|
+
const textareaRef = useRef<RNTextInput>(null);
|
|
86
|
+
|
|
87
|
+
// Internal attachment state (used when no controlled props)
|
|
88
|
+
const [internalAttachments, setInternalAttachments] = useState<Attachment[]>(
|
|
89
|
+
[]
|
|
90
|
+
);
|
|
91
|
+
const attachments = controlledAttachments ?? internalAttachments;
|
|
92
|
+
|
|
93
|
+
const addAttachment = useCallback(
|
|
94
|
+
(a: Attachment) => {
|
|
95
|
+
if (onAddAttachment) {
|
|
96
|
+
onAddAttachment(a);
|
|
97
|
+
} else {
|
|
98
|
+
setInternalAttachments((prev) => [...prev, a]);
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
[onAddAttachment]
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
const removeAttachment = useCallback(
|
|
105
|
+
(id: string) => {
|
|
106
|
+
if (onRemoveAttachment) {
|
|
107
|
+
onRemoveAttachment(id);
|
|
108
|
+
} else {
|
|
109
|
+
setInternalAttachments((prev) => prev.filter((a) => a.id !== id));
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
[onRemoveAttachment]
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
const updateAttachment = useCallback(
|
|
116
|
+
(id: string, updates: Partial<Attachment>) => {
|
|
117
|
+
if (onUpdateAttachment) {
|
|
118
|
+
onUpdateAttachment(id, updates);
|
|
119
|
+
} else {
|
|
120
|
+
setInternalAttachments((prev) =>
|
|
121
|
+
prev.map((a) => (a.id === id ? { ...a, ...updates } : a))
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
[onUpdateAttachment]
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
const handleChange = (newValue: string) => {
|
|
129
|
+
setInternalValue(newValue);
|
|
130
|
+
onValueChange?.(newValue);
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const handleSubmit = () => {
|
|
134
|
+
onSubmit?.();
|
|
135
|
+
if (showFullscreen) setShowFullscreen(false);
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
useEffect(() => {
|
|
139
|
+
if (!showFullscreen) setCurrentHeight(44);
|
|
140
|
+
}, [showFullscreen]);
|
|
141
|
+
|
|
142
|
+
const showExpandIcon = currentHeight > 100;
|
|
143
|
+
const isSimpleMode = !children;
|
|
144
|
+
|
|
145
|
+
const contextValue = {
|
|
146
|
+
isLoading,
|
|
147
|
+
value: value ?? internalValue,
|
|
148
|
+
setValue: onValueChange ?? handleChange,
|
|
149
|
+
maxHeight,
|
|
150
|
+
onSubmit: handleSubmit,
|
|
151
|
+
disabled,
|
|
152
|
+
textareaRef,
|
|
153
|
+
currentHeight,
|
|
154
|
+
setCurrentHeight,
|
|
155
|
+
isFullscreen: showFullscreen,
|
|
156
|
+
onImagePaste,
|
|
157
|
+
attachments,
|
|
158
|
+
addAttachment,
|
|
159
|
+
removeAttachment,
|
|
160
|
+
updateAttachment,
|
|
161
|
+
handleCompletionKey,
|
|
162
|
+
setHandleCompletionKey,
|
|
163
|
+
useSuggestions,
|
|
164
|
+
onSuggestionUsed,
|
|
165
|
+
onError,
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const content = isSimpleMode ? (
|
|
169
|
+
<>
|
|
170
|
+
<PromptInputAttachments />
|
|
171
|
+
<PromptInputTextarea
|
|
172
|
+
placeholder={placeholder}
|
|
173
|
+
className="min-h-[44px] text-base py-3"
|
|
174
|
+
/>
|
|
175
|
+
<PromptInputActions className="flex-row items-center justify-between gap-2 mt-2 mb-1 px-3">
|
|
176
|
+
<View className="flex-row items-center gap-1.5">
|
|
177
|
+
{actionsLeft ?? <PromptInputAddMenu />}
|
|
178
|
+
</View>
|
|
179
|
+
<View className="flex-row items-center gap-1.5">
|
|
180
|
+
<PromptInputMicButton apiUrl={apiUrl} />
|
|
181
|
+
<PromptInputSubmitButton
|
|
182
|
+
isLoading={isLoading}
|
|
183
|
+
onStop={onStop}
|
|
184
|
+
emptyAction={emptyAction}
|
|
185
|
+
/>
|
|
186
|
+
</View>
|
|
187
|
+
</PromptInputActions>
|
|
188
|
+
</>
|
|
189
|
+
) : (
|
|
190
|
+
children
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
const inputBox = (
|
|
194
|
+
<Pressable
|
|
195
|
+
onPress={() => {
|
|
196
|
+
if (!disabled) textareaRef.current?.focus();
|
|
197
|
+
}}
|
|
198
|
+
disabled={disabled}
|
|
199
|
+
>
|
|
200
|
+
<View
|
|
201
|
+
className={cn(
|
|
202
|
+
"rounded-[24px] border border-border bg-background relative overflow-hidden",
|
|
203
|
+
disabled && "opacity-60",
|
|
204
|
+
className
|
|
205
|
+
)}
|
|
206
|
+
{...props}
|
|
207
|
+
>
|
|
208
|
+
{showExpandIcon && !disabled && (
|
|
209
|
+
<Pressable
|
|
210
|
+
onPress={() => setShowFullscreen(true)}
|
|
211
|
+
className="absolute top-2 right-2 z-10 bg-background rounded-full p-1.5 border border-border active:opacity-70"
|
|
212
|
+
>
|
|
213
|
+
<Maximize2 size={16} className="text-muted-foreground" />
|
|
214
|
+
</Pressable>
|
|
215
|
+
)}
|
|
216
|
+
{content}
|
|
217
|
+
</View>
|
|
218
|
+
</Pressable>
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
return (
|
|
222
|
+
<PromptInputContext.Provider value={contextValue}>
|
|
223
|
+
{autocomplete && autocompletePosition === "top" && !leadingAddMenu && (
|
|
224
|
+
<PromptInputAutocomplete position="top" />
|
|
225
|
+
)}
|
|
226
|
+
|
|
227
|
+
<KeyboardAvoidingView behavior="padding">
|
|
228
|
+
{leadingAddMenu ? (
|
|
229
|
+
<View className="flex-row items-end gap-2">
|
|
230
|
+
<PromptInputAddMenu
|
|
231
|
+
iconSize={20}
|
|
232
|
+
className="h-10 w-10 rounded-full border"
|
|
233
|
+
/>
|
|
234
|
+
<View className="flex-1">
|
|
235
|
+
{autocomplete && autocompletePosition === "top" && (
|
|
236
|
+
<PromptInputAutocomplete position="top" />
|
|
237
|
+
)}
|
|
238
|
+
{inputBox}
|
|
239
|
+
{autocomplete && autocompletePosition === "bottom" && (
|
|
240
|
+
<PromptInputAutocomplete position="bottom" />
|
|
241
|
+
)}
|
|
242
|
+
</View>
|
|
243
|
+
</View>
|
|
244
|
+
) : (
|
|
245
|
+
inputBox
|
|
246
|
+
)}
|
|
247
|
+
</KeyboardAvoidingView>
|
|
248
|
+
|
|
249
|
+
{autocomplete && autocompletePosition === "bottom" && !leadingAddMenu && (
|
|
250
|
+
<PromptInputAutocomplete position="bottom" />
|
|
251
|
+
)}
|
|
252
|
+
|
|
253
|
+
{showFullscreen && (
|
|
254
|
+
<View
|
|
255
|
+
style={{
|
|
256
|
+
position: "fixed" as any,
|
|
257
|
+
top: 0,
|
|
258
|
+
left: 0,
|
|
259
|
+
right: 0,
|
|
260
|
+
bottom: 0,
|
|
261
|
+
zIndex: 9998,
|
|
262
|
+
}}
|
|
263
|
+
className="bg-background"
|
|
264
|
+
>
|
|
265
|
+
<Pressable
|
|
266
|
+
onPress={() => setShowFullscreen(false)}
|
|
267
|
+
className="absolute top-4 right-4 z-50 p-2 active:opacity-70 bg-background/80 rounded-full"
|
|
268
|
+
>
|
|
269
|
+
<Minimize2 size={20} className="text-foreground" />
|
|
270
|
+
</Pressable>
|
|
271
|
+
<View className="flex-1 flex-col">{content}</View>
|
|
272
|
+
</View>
|
|
273
|
+
)}
|
|
274
|
+
</PromptInputContext.Provider>
|
|
275
|
+
);
|
|
276
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { ArrowUp, Square } from "lucide-react-native";
|
|
3
|
+
import { cn } from "../../../lib/utils";
|
|
4
|
+
import { Button } from "../button";
|
|
5
|
+
import { usePromptInput } from "./context";
|
|
6
|
+
|
|
7
|
+
export type PromptInputSubmitButtonProps = {
|
|
8
|
+
isLoading?: boolean;
|
|
9
|
+
onStop?: () => void;
|
|
10
|
+
emptyAction?: React.ReactNode;
|
|
11
|
+
className?: string;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export function PromptInputSubmitButton({
|
|
15
|
+
isLoading,
|
|
16
|
+
onStop,
|
|
17
|
+
emptyAction,
|
|
18
|
+
className,
|
|
19
|
+
}: PromptInputSubmitButtonProps) {
|
|
20
|
+
const { onSubmit, value, attachments } = usePromptInput();
|
|
21
|
+
const hasContent = value.trim() || attachments.length > 0;
|
|
22
|
+
|
|
23
|
+
if (isLoading && onStop) {
|
|
24
|
+
return (
|
|
25
|
+
<Button
|
|
26
|
+
size="icon"
|
|
27
|
+
onPress={onStop}
|
|
28
|
+
className={cn("h-8 w-8 rounded-full", className)}
|
|
29
|
+
>
|
|
30
|
+
<Square size={12} color="white" className="fill-current" />
|
|
31
|
+
</Button>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (!hasContent && emptyAction) {
|
|
36
|
+
return <>{emptyAction}</>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<Button
|
|
41
|
+
size="icon"
|
|
42
|
+
onPress={onSubmit}
|
|
43
|
+
disabled={!hasContent}
|
|
44
|
+
className={cn("h-8 w-8 rounded-full", className)}
|
|
45
|
+
>
|
|
46
|
+
<ArrowUp size={16} color="white" />
|
|
47
|
+
</Button>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { View } from "react-native";
|
|
3
|
+
import { cn } from "../../../lib/utils";
|
|
4
|
+
import { ChatTextInput } from "../chat-text-input";
|
|
5
|
+
import { usePromptInput } from "./context";
|
|
6
|
+
|
|
7
|
+
export type PromptInputTextareaProps = {
|
|
8
|
+
placeholder?: string;
|
|
9
|
+
className?: string;
|
|
10
|
+
} & React.ComponentProps<typeof ChatTextInput>;
|
|
11
|
+
|
|
12
|
+
export function PromptInputTextarea({
|
|
13
|
+
className,
|
|
14
|
+
placeholder,
|
|
15
|
+
style,
|
|
16
|
+
...props
|
|
17
|
+
}: PromptInputTextareaProps) {
|
|
18
|
+
const {
|
|
19
|
+
value,
|
|
20
|
+
setValue,
|
|
21
|
+
onSubmit,
|
|
22
|
+
disabled,
|
|
23
|
+
textareaRef,
|
|
24
|
+
setCurrentHeight,
|
|
25
|
+
isFullscreen,
|
|
26
|
+
maxHeight,
|
|
27
|
+
onImagePaste,
|
|
28
|
+
handleCompletionKey,
|
|
29
|
+
} = usePromptInput();
|
|
30
|
+
|
|
31
|
+
const textInput = (
|
|
32
|
+
<ChatTextInput
|
|
33
|
+
ref={textareaRef}
|
|
34
|
+
value={value}
|
|
35
|
+
onChangeText={setValue}
|
|
36
|
+
onSubmitEditing={onSubmit}
|
|
37
|
+
onEnterPress={onSubmit}
|
|
38
|
+
onHeightChange={setCurrentHeight}
|
|
39
|
+
onCompletionKey={handleCompletionKey ?? undefined}
|
|
40
|
+
disableEnterToSubmit={isFullscreen}
|
|
41
|
+
disableAutoHeight={isFullscreen}
|
|
42
|
+
maxHeight={isFullscreen ? 10000 : maxHeight}
|
|
43
|
+
onImagePaste={onImagePaste}
|
|
44
|
+
fillContainer={isFullscreen}
|
|
45
|
+
className={cn(
|
|
46
|
+
"w-full border-0 bg-transparent text-foreground shadow-none",
|
|
47
|
+
isFullscreen ? "px-4 pt-4" : "min-h-[44px] py-3",
|
|
48
|
+
className
|
|
49
|
+
)}
|
|
50
|
+
style={[style, isFullscreen && { paddingBottom: 100 }]}
|
|
51
|
+
placeholder={placeholder}
|
|
52
|
+
multiline
|
|
53
|
+
editable={!disabled}
|
|
54
|
+
noFocus={true}
|
|
55
|
+
{...props}
|
|
56
|
+
/>
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
if (isFullscreen) {
|
|
60
|
+
return <View style={{ flex: 1 }}>{textInput}</View>;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return textInput;
|
|
64
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { Text as RNText, type TextProps as RNTextProps } from "react-native";
|
|
3
|
+
import { cn } from "../../lib/utils";
|
|
4
|
+
|
|
5
|
+
const TextClassContext = React.createContext<string | undefined>(undefined);
|
|
6
|
+
|
|
7
|
+
export type TextProps = RNTextProps & {
|
|
8
|
+
className?: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const Text = React.forwardRef<RNText, TextProps>(
|
|
12
|
+
({ className, ...props }, ref) => {
|
|
13
|
+
const textClass = React.useContext(TextClassContext);
|
|
14
|
+
return (
|
|
15
|
+
<RNText
|
|
16
|
+
className={cn(
|
|
17
|
+
"text-base leading-7 text-foreground web:select-text font-sans",
|
|
18
|
+
textClass,
|
|
19
|
+
className
|
|
20
|
+
)}
|
|
21
|
+
ref={ref}
|
|
22
|
+
{...props}
|
|
23
|
+
/>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
);
|
|
27
|
+
Text.displayName = "Text";
|
|
28
|
+
|
|
29
|
+
export { Text, TextClassContext };
|
package/src/hooks/useAliaChat.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useState, useRef, useCallback, useEffect } from 'react';
|
|
1
|
+
import React, { useState, useRef, useCallback, useEffect } from 'react';
|
|
2
2
|
import { useOxy } from '@oxyhq/services';
|
|
3
3
|
import type { ChatMessage, ToolInvocation } from '../types';
|
|
4
4
|
|
|
@@ -17,8 +17,10 @@ export interface UseAliaChatOptions {
|
|
|
17
17
|
|
|
18
18
|
export interface UseAliaChatReturn {
|
|
19
19
|
messages: ChatMessage[];
|
|
20
|
+
setMessages: React.Dispatch<React.SetStateAction<ChatMessage[]>>;
|
|
20
21
|
send: (text: string) => void;
|
|
21
22
|
isStreaming: boolean;
|
|
23
|
+
stop: () => void;
|
|
22
24
|
clear: () => void;
|
|
23
25
|
error: string | null;
|
|
24
26
|
}
|
|
@@ -312,8 +314,6 @@ export function useAliaChat(options: UseAliaChatOptions = {}): UseAliaChatReturn
|
|
|
312
314
|
updateAssistant(() => ({
|
|
313
315
|
pendingPlan: {
|
|
314
316
|
planId: parsed.planId,
|
|
315
|
-
intent: parsed.intent,
|
|
316
|
-
confidence: parsed.confidence,
|
|
317
317
|
steps: parsed.steps || [],
|
|
318
318
|
approved: false,
|
|
319
319
|
rejected: false,
|
|
@@ -431,18 +431,27 @@ export function useAliaChat(options: UseAliaChatOptions = {}): UseAliaChatReturn
|
|
|
431
431
|
[isStreaming, getToken, apiUrl, model, clientContext, scheduleFlush, flushPendingUpdates, updateAssistant, applyToolResult],
|
|
432
432
|
);
|
|
433
433
|
|
|
434
|
-
const
|
|
434
|
+
const stop = useCallback(() => {
|
|
435
435
|
if (abortRef.current) {
|
|
436
436
|
abortRef.current.abort();
|
|
437
437
|
abortRef.current = null;
|
|
438
438
|
}
|
|
439
|
-
|
|
439
|
+
flushPendingUpdates();
|
|
440
|
+
if (flushTimerRef.current) {
|
|
441
|
+
clearTimeout(flushTimerRef.current);
|
|
442
|
+
flushTimerRef.current = null;
|
|
443
|
+
}
|
|
440
444
|
setIsStreaming(false);
|
|
445
|
+
}, [flushPendingUpdates]);
|
|
446
|
+
|
|
447
|
+
const clear = useCallback(() => {
|
|
448
|
+
stop();
|
|
449
|
+
setMessages([]);
|
|
441
450
|
setError(null);
|
|
442
451
|
pendingContentRef.current = '';
|
|
443
452
|
pendingReasoningRef.current = '';
|
|
444
453
|
toolInvocationsRef.current = [];
|
|
445
|
-
}, []);
|
|
454
|
+
}, [stop]);
|
|
446
455
|
|
|
447
|
-
return { messages, send, isStreaming, clear, error };
|
|
456
|
+
return { messages, setMessages, send, isStreaming, stop, clear, error };
|
|
448
457
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import * as DocumentPicker from 'expo-document-picker';
|
|
2
|
+
|
|
3
|
+
export type DocumentPickerResult = {
|
|
4
|
+
uri: string;
|
|
5
|
+
name: string;
|
|
6
|
+
size: number;
|
|
7
|
+
mimeType: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
type UseDocumentPickerResult = {
|
|
11
|
+
pickDocument: () => Promise<DocumentPickerResult[] | undefined>;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export function useDocumentPicker(): UseDocumentPickerResult {
|
|
15
|
+
const pickDocument = async (): Promise<DocumentPickerResult[] | undefined> => {
|
|
16
|
+
try {
|
|
17
|
+
const result = await DocumentPicker.getDocumentAsync({
|
|
18
|
+
type: '*/*',
|
|
19
|
+
multiple: true,
|
|
20
|
+
copyToCacheDirectory: true,
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
if (result.canceled === false && result.assets && result.assets.length > 0) {
|
|
24
|
+
return result.assets.map((asset: any) => ({
|
|
25
|
+
uri: asset.uri,
|
|
26
|
+
name: asset.name,
|
|
27
|
+
size: asset.size || 0,
|
|
28
|
+
mimeType: asset.mimeType || 'application/octet-stream',
|
|
29
|
+
}));
|
|
30
|
+
}
|
|
31
|
+
} catch (error) {
|
|
32
|
+
console.warn('Failed to pick document:', error);
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
return { pickDocument };
|
|
37
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import * as ImagePicker from 'expo-image-picker';
|
|
2
|
+
|
|
3
|
+
export type ImagePickerAsset = {
|
|
4
|
+
uri: string;
|
|
5
|
+
name: string;
|
|
6
|
+
size: number;
|
|
7
|
+
mimeType: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
type ImagePickerResult = {
|
|
11
|
+
pickImage: () => Promise<ImagePickerAsset[] | undefined>;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export function useImagePicker(): ImagePickerResult {
|
|
15
|
+
const pickImage = async (): Promise<ImagePickerAsset[] | undefined> => {
|
|
16
|
+
try {
|
|
17
|
+
const result = await ImagePicker.launchImageLibraryAsync({
|
|
18
|
+
allowsMultipleSelection: true,
|
|
19
|
+
mediaTypes: ImagePicker.MediaTypeOptions.Images,
|
|
20
|
+
aspect: [4, 3],
|
|
21
|
+
quality: 1,
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
if (!result.canceled && result.assets.length > 0) {
|
|
25
|
+
return result.assets.map((asset: any) => ({
|
|
26
|
+
uri: asset.uri,
|
|
27
|
+
name: asset.fileName || `image-${Date.now()}.jpg`,
|
|
28
|
+
size: asset.fileSize || 0,
|
|
29
|
+
mimeType: asset.mimeType || 'image/jpeg',
|
|
30
|
+
}));
|
|
31
|
+
}
|
|
32
|
+
} catch (error) {
|
|
33
|
+
console.warn('Failed to pick image:', error);
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
return { pickImage };
|
|
38
|
+
}
|
|
@@ -27,7 +27,7 @@ interface STTStoreState {
|
|
|
27
27
|
setMetering: (v: number) => void;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
const useSTTStore = create<STTStoreState>((set) => ({
|
|
30
|
+
export const useSTTStore = create<STTStoreState>((set) => ({
|
|
31
31
|
isRecording: false,
|
|
32
32
|
metering: 0,
|
|
33
33
|
setRecording: (isRecording) => set({ isRecording }),
|
|
@@ -119,6 +119,8 @@ export function useSpeechToText(options: UseSTTOptions = {}) {
|
|
|
119
119
|
// Read audio file as base64
|
|
120
120
|
const response = await fetch(uri);
|
|
121
121
|
const blob = await response.blob();
|
|
122
|
+
// Detect actual MIME type (web records webm, native records m4a)
|
|
123
|
+
const detectedFormat = blob.type?.split(';')[0] || 'audio/m4a';
|
|
122
124
|
const base64 = await new Promise<string>((resolve, reject) => {
|
|
123
125
|
const reader = new FileReader();
|
|
124
126
|
reader.onload = () => {
|
|
@@ -139,7 +141,7 @@ export function useSpeechToText(options: UseSTTOptions = {}) {
|
|
|
139
141
|
},
|
|
140
142
|
body: JSON.stringify({
|
|
141
143
|
audio: base64,
|
|
142
|
-
format:
|
|
144
|
+
format: detectedFormat,
|
|
143
145
|
}),
|
|
144
146
|
});
|
|
145
147
|
|
package/src/index.ts
CHANGED
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
// @alia.onl/sdk — Alia AI Chat SDK
|
|
3
3
|
// Reusable chat UI, voice, and streaming components for Oxy apps
|
|
4
4
|
|
|
5
|
-
// ── Main
|
|
5
|
+
// ── Main components ──
|
|
6
6
|
export { AliaChatSheet } from './components/AliaChatSheet';
|
|
7
7
|
export type { AliaChatSheetProps, AliaChatSheetRef } from './components/AliaChatSheet';
|
|
8
|
+
export { AliaChatScreen } from './components/AliaChatScreen';
|
|
9
|
+
export type { AliaChatScreenProps } from './components/AliaChatScreen';
|
|
8
10
|
|
|
9
11
|
// ── Chat UI components ──
|
|
10
12
|
export { AliaFace } from './components/AliaFace';
|
|
@@ -28,7 +30,7 @@ export { useAudioLevelMonitor } from './hooks/useAudioLevelMonitor';
|
|
|
28
30
|
export { useAudioLevels } from './hooks/useAudioLevels';
|
|
29
31
|
export { useTTS } from './hooks/useTTS';
|
|
30
32
|
export type { UseTTSOptions } from './hooks/useTTS';
|
|
31
|
-
export { useSpeechToText } from './hooks/useSpeechToText';
|
|
33
|
+
export { useSpeechToText, useSTTStore } from './hooks/useSpeechToText';
|
|
32
34
|
export type { UseSTTOptions } from './hooks/useSpeechToText';
|
|
33
35
|
export { useSoundEffects, useVoiceSoundEffects } from './hooks/useSoundEffects';
|
|
34
36
|
export type { SoundName, SoundSources } from './hooks/useSoundEffects';
|
|
@@ -46,6 +48,35 @@ export type {
|
|
|
46
48
|
VoiceToolInvocation,
|
|
47
49
|
} from './types';
|
|
48
50
|
|
|
51
|
+
// ── UI Primitives (NativeWind) ──
|
|
52
|
+
export { Button, buttonVariants, buttonTextVariants } from './components/ui/button';
|
|
53
|
+
export type { ButtonProps } from './components/ui/button';
|
|
54
|
+
export { Text, TextClassContext } from './components/ui/text';
|
|
55
|
+
export { ChatTextInput } from './components/ui/chat-text-input';
|
|
56
|
+
export { cn, formatFileSize } from './lib/utils';
|
|
57
|
+
|
|
58
|
+
// ── PromptInput ──
|
|
59
|
+
export { PromptInput } from './components/ui/prompt-input/prompt-input';
|
|
60
|
+
export type { PromptInputProps } from './components/ui/prompt-input/prompt-input';
|
|
61
|
+
export { usePromptInput, useIsFullscreen, PromptInputContext } from './components/ui/prompt-input/context';
|
|
62
|
+
export type { Attachment, Completion, PromptInputContextType } from './components/ui/prompt-input/context';
|
|
63
|
+
export { PromptInputTextarea } from './components/ui/prompt-input/textarea';
|
|
64
|
+
export { PromptInputActions } from './components/ui/prompt-input/actions';
|
|
65
|
+
export { PromptInputSubmitButton } from './components/ui/prompt-input/submit-button';
|
|
66
|
+
export { PromptInputMicButton } from './components/ui/prompt-input/mic-button';
|
|
67
|
+
export { PromptInputAddMenu } from './components/ui/prompt-input/add-menu';
|
|
68
|
+
export { PromptInputAttachments } from './components/ui/prompt-input/attachments';
|
|
69
|
+
export { PromptInputAutocomplete } from './components/ui/prompt-input/autocomplete';
|
|
70
|
+
|
|
71
|
+
// ── Picker hooks ──
|
|
72
|
+
export { useImagePicker } from './hooks/useImagePicker';
|
|
73
|
+
export type { ImagePickerAsset } from './hooks/useImagePicker';
|
|
74
|
+
export { useDocumentPicker } from './hooks/useDocumentPicker';
|
|
75
|
+
export type { DocumentPickerResult } from './hooks/useDocumentPicker';
|
|
76
|
+
|
|
77
|
+
// ── Keyboard ──
|
|
78
|
+
export { KeyboardAwareScrollView, KeyboardAvoidingView, KeyboardProvider } from './lib/keyboard';
|
|
79
|
+
|
|
49
80
|
// ── Theme ──
|
|
50
81
|
export { useAliaColors } from './theme';
|
|
51
82
|
export type { AliaColors } from './theme';
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Web: re-export React Native built-in components as keyboard-controller substitutes
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { ScrollView, type ScrollViewProps, KeyboardAvoidingView } from 'react-native';
|
|
4
|
+
|
|
5
|
+
// Accept native-only props so shared components don't cause TS errors
|
|
6
|
+
type KeyboardAwareScrollViewProps = ScrollViewProps & {
|
|
7
|
+
bottomOffset?: number;
|
|
8
|
+
disableScrollOnKeyboardHide?: boolean;
|
|
9
|
+
enabled?: boolean;
|
|
10
|
+
extraKeyboardSpace?: number;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const KeyboardAwareScrollView = React.forwardRef<ScrollView, KeyboardAwareScrollViewProps>(
|
|
14
|
+
({ bottomOffset, disableScrollOnKeyboardHide, enabled, extraKeyboardSpace, ...props }, ref) => (
|
|
15
|
+
<ScrollView ref={ref} {...props} />
|
|
16
|
+
)
|
|
17
|
+
);
|
|
18
|
+
KeyboardAwareScrollView.displayName = 'KeyboardAwareScrollView';
|
|
19
|
+
|
|
20
|
+
export { KeyboardAwareScrollView, KeyboardAvoidingView };
|
|
21
|
+
|
|
22
|
+
// No-op provider on web — keyboard-controller is native-only
|
|
23
|
+
export function KeyboardProvider({ children }: { children: React.ReactNode }) {
|
|
24
|
+
return <>{children}</>;
|
|
25
|
+
}
|
package/src/lib/utils.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { clsx, type ClassValue } from 'clsx';
|
|
2
|
+
import { twMerge } from 'tailwind-merge';
|
|
3
|
+
|
|
4
|
+
export function cn(...inputs: ClassValue[]) {
|
|
5
|
+
return twMerge(clsx(inputs));
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function formatFileSize(bytes: number): string {
|
|
9
|
+
if (bytes === 0) return '0 B';
|
|
10
|
+
const k = 1024;
|
|
11
|
+
const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
12
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
13
|
+
return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + ' ' + sizes[i];
|
|
14
|
+
}
|