@alia.onl/sdk 1.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.
Files changed (44) hide show
  1. package/package.json +37 -3
  2. package/src/components/AliaChatContent.tsx +313 -0
  3. package/src/components/AliaChatInput.tsx +174 -27
  4. package/src/components/AliaChatMessageList.tsx +220 -34
  5. package/src/components/AliaChatScreen.tsx +132 -0
  6. package/src/components/AliaChatSheet.tsx +36 -49
  7. package/src/components/AliaChatSuggestions.tsx +1 -1
  8. package/src/components/AliaFace.tsx +402 -0
  9. package/src/components/Markdown.tsx +288 -0
  10. package/src/components/Reasoning.tsx +154 -0
  11. package/src/components/ThinkingIndicator.tsx +134 -0
  12. package/src/components/ui/button.tsx +104 -0
  13. package/src/components/ui/chat-text-input.tsx +164 -0
  14. package/src/components/ui/dropdown-menu.tsx +2 -0
  15. package/src/components/ui/dropdown-menu.web.tsx +355 -0
  16. package/src/components/ui/prompt-input/actions.tsx +28 -0
  17. package/src/components/ui/prompt-input/add-menu.tsx +83 -0
  18. package/src/components/ui/prompt-input/attachments.tsx +183 -0
  19. package/src/components/ui/prompt-input/autocomplete.tsx +168 -0
  20. package/src/components/ui/prompt-input/context.tsx +71 -0
  21. package/src/components/ui/prompt-input/mic-button.tsx +53 -0
  22. package/src/components/ui/prompt-input/prompt-input.tsx +276 -0
  23. package/src/components/ui/prompt-input/submit-button.tsx +49 -0
  24. package/src/components/ui/prompt-input/textarea.tsx +64 -0
  25. package/src/components/ui/text.tsx +29 -0
  26. package/src/components/voice/AudioWaveVisualizer.tsx +377 -0
  27. package/src/components/voice/VoiceControls.tsx +180 -0
  28. package/src/components/voice/VoiceOverlay.tsx +43 -0
  29. package/src/hooks/useAliaChat.ts +278 -91
  30. package/src/hooks/useAudioLevelMonitor.ts +142 -0
  31. package/src/hooks/useAudioLevels.ts +51 -0
  32. package/src/hooks/useDocumentPicker.ts +37 -0
  33. package/src/hooks/useImagePicker.ts +38 -0
  34. package/src/hooks/useSoundEffects.ts +128 -0
  35. package/src/hooks/useSpeechToText.ts +182 -0
  36. package/src/hooks/useTTS.ts +233 -0
  37. package/src/hooks/useVoiceRoom.ts +420 -0
  38. package/src/index.ts +71 -6
  39. package/src/lib/keyboard.native.tsx +6 -0
  40. package/src/lib/keyboard.tsx +25 -0
  41. package/src/lib/utils.ts +14 -0
  42. package/src/optional-deps.d.ts +86 -0
  43. package/src/theme.ts +25 -2
  44. package/src/types.ts +63 -1
@@ -0,0 +1,168 @@
1
+ import React, { useState, useEffect, useRef, useMemo, useCallback } from "react";
2
+ import { View, Pressable } from "react-native";
3
+ import { Text } from "../text";
4
+ import { cn } from "../../../lib/utils";
5
+ import { usePromptInput, type Completion } from "./context";
6
+
7
+ export type PromptInputAutocompleteProps = {
8
+ enabled?: boolean;
9
+ position?: "top" | "bottom";
10
+ className?: string;
11
+ };
12
+
13
+ export function PromptInputAutocomplete({
14
+ enabled = true,
15
+ position = "top",
16
+ className,
17
+ }: PromptInputAutocompleteProps) {
18
+ const { value, setValue, setHandleCompletionKey, useSuggestions, onSuggestionUsed } = usePromptInput();
19
+ const [selectedIndex, setSelectedIndex] = useState(-1);
20
+ const selectedIndexRef = useRef(-1);
21
+ const completionsRef = useRef<Completion[]>([]);
22
+ const [debouncedQuery, setDebouncedQuery] = useState('');
23
+
24
+ // If no useSuggestions hook is injected, autocomplete is disabled
25
+ const suggestionsResult = useSuggestions?.(debouncedQuery);
26
+ const apiResults = suggestionsResult?.data;
27
+
28
+ // Debounce the search query (200ms)
29
+ useEffect(() => {
30
+ if (!enabled || !useSuggestions) {
31
+ setDebouncedQuery('');
32
+ return;
33
+ }
34
+ const trimmed = value.trim();
35
+ if (!trimmed || trimmed.length < 2) {
36
+ setDebouncedQuery('');
37
+ return;
38
+ }
39
+ const timer = setTimeout(() => setDebouncedQuery(trimmed), 200);
40
+ return () => clearTimeout(timer);
41
+ }, [value, enabled, useSuggestions]);
42
+
43
+ // Map API results to completions with match highlighting
44
+ const completions = useMemo<Completion[]>(() => {
45
+ const trimmed = value.trim();
46
+ if (!trimmed || trimmed.length < 2 || !apiResults?.length) return [];
47
+
48
+ const lower = trimmed.toLowerCase();
49
+ const results: Completion[] = [];
50
+ const seen = new Set<string>();
51
+
52
+ for (const s of apiResults) {
53
+ if (results.length >= 6) break;
54
+ const textLower = s.text.toLowerCase();
55
+ if (seen.has(textLower)) continue;
56
+ seen.add(textLower);
57
+
58
+ const idx = textLower.indexOf(lower);
59
+ results.push({
60
+ text: s.text,
61
+ matchStart: idx !== -1 ? idx : 0,
62
+ matchEnd: idx !== -1 ? idx + trimmed.length : 0,
63
+ suggestionId: s.suggestionId,
64
+ });
65
+ }
66
+
67
+ return results;
68
+ }, [apiResults, value]);
69
+
70
+ // Keep refs in sync
71
+ useEffect(() => {
72
+ completionsRef.current = completions;
73
+ }, [completions]);
74
+
75
+ // Reset selection when completions change
76
+ useEffect(() => {
77
+ selectedIndexRef.current = -1;
78
+ setSelectedIndex(-1);
79
+ }, [completions]);
80
+
81
+ // Arrow key handler — stable callback using refs
82
+ const handleKey = useCallback((key: string): boolean => {
83
+ const items = completionsRef.current;
84
+ if (items.length === 0) return false;
85
+
86
+ if (key === "ArrowDown") {
87
+ const next = selectedIndexRef.current < items.length - 1
88
+ ? selectedIndexRef.current + 1
89
+ : 0;
90
+ selectedIndexRef.current = next;
91
+ setSelectedIndex(next);
92
+ return true;
93
+ }
94
+
95
+ if (key === "ArrowUp") {
96
+ const next = selectedIndexRef.current > 0
97
+ ? selectedIndexRef.current - 1
98
+ : items.length - 1;
99
+ selectedIndexRef.current = next;
100
+ setSelectedIndex(next);
101
+ return true;
102
+ }
103
+
104
+ if (key === "Enter") {
105
+ if (selectedIndexRef.current < 0) return false;
106
+ const item = items[selectedIndexRef.current];
107
+ if (item.suggestionId) onSuggestionUsed?.(item.suggestionId);
108
+ setValue(item.text);
109
+ return true;
110
+ }
111
+
112
+ if (key === "Escape") {
113
+ if (selectedIndexRef.current < 0) return false;
114
+ selectedIndexRef.current = -1;
115
+ setSelectedIndex(-1);
116
+ return true;
117
+ }
118
+
119
+ return false;
120
+ }, [setValue, onSuggestionUsed]);
121
+
122
+ // Register/unregister the key handler based on completions
123
+ useEffect(() => {
124
+ if (completions.length > 0) {
125
+ (setHandleCompletionKey as any)(() => handleKey);
126
+ } else {
127
+ setHandleCompletionKey(null);
128
+ }
129
+ return () => setHandleCompletionKey(null);
130
+ }, [completions.length, handleKey, setHandleCompletionKey]);
131
+
132
+ // If no suggestions hook or no completions, render nothing
133
+ if (!useSuggestions || completions.length === 0) return null;
134
+
135
+ return (
136
+ <View className={className}>
137
+ <View className={position === "bottom" ? "pt-0.5" : "pb-0.5"}>
138
+ {completions.map((item, index) => (
139
+ <Pressable
140
+ key={item.suggestionId || item.text}
141
+ onPress={() => {
142
+ if (item.suggestionId) {
143
+ onSuggestionUsed?.(item.suggestionId);
144
+ }
145
+ setValue(item.text);
146
+ }}
147
+ className={cn(
148
+ "px-3 py-1.5 rounded-lg active:bg-muted/50",
149
+ index === selectedIndex && "bg-muted"
150
+ )}
151
+ >
152
+ <Text className="text-sm leading-5" numberOfLines={1}>
153
+ <Text className="text-foreground">
154
+ {item.text.slice(0, item.matchStart)}
155
+ </Text>
156
+ <Text className="text-primary font-medium">
157
+ {item.text.slice(item.matchStart, item.matchEnd)}
158
+ </Text>
159
+ <Text className="text-foreground">
160
+ {item.text.slice(item.matchEnd)}
161
+ </Text>
162
+ </Text>
163
+ </Pressable>
164
+ ))}
165
+ </View>
166
+ </View>
167
+ );
168
+ }
@@ -0,0 +1,71 @@
1
+ import React, { createContext, useContext } from "react";
2
+ import type { TextInput as RNTextInput } from "react-native";
3
+
4
+ export interface Attachment {
5
+ id: string;
6
+ uri: string;
7
+ type: "image" | "document";
8
+ name: string;
9
+ size: number;
10
+ mimeType: string;
11
+ isLoading?: boolean;
12
+ }
13
+
14
+ export interface Completion {
15
+ text: string;
16
+ matchStart: number;
17
+ matchEnd: number;
18
+ suggestionId?: string;
19
+ }
20
+
21
+ export type PromptInputContextType = {
22
+ isLoading: boolean;
23
+ value: string;
24
+ setValue: (value: string) => void;
25
+ maxHeight: number;
26
+ onSubmit?: () => void;
27
+ disabled?: boolean;
28
+ textareaRef: React.RefObject<RNTextInput | null>;
29
+ currentHeight: number;
30
+ setCurrentHeight: (height: number) => void;
31
+ isFullscreen: boolean;
32
+ onImagePaste?: (files: File[]) => void;
33
+ attachments: Attachment[];
34
+ addAttachment: (attachment: Attachment) => void;
35
+ removeAttachment: (id: string) => void;
36
+ updateAttachment: (id: string, updates: Partial<Attachment>) => void;
37
+ handleCompletionKey: ((key: string) => boolean) | null;
38
+ setHandleCompletionKey: (fn: ((key: string) => boolean) | null) => void;
39
+ // Injectable autocomplete hooks
40
+ useSuggestions?: (query: string) => { data: Completion[] | undefined; isLoading: boolean };
41
+ onSuggestionUsed?: (suggestionId: string) => void;
42
+ // Injectable error handler (replaces toast)
43
+ onError?: (message: string) => void;
44
+ };
45
+
46
+ export const PromptInputContext = createContext<PromptInputContextType>({
47
+ isLoading: false,
48
+ value: "",
49
+ setValue: () => {},
50
+ maxHeight: 240,
51
+ onSubmit: undefined,
52
+ disabled: false,
53
+ textareaRef: React.createRef<RNTextInput>(),
54
+ currentHeight: 44,
55
+ setCurrentHeight: () => {},
56
+ isFullscreen: false,
57
+ attachments: [],
58
+ addAttachment: () => {},
59
+ removeAttachment: () => {},
60
+ updateAttachment: () => {},
61
+ handleCompletionKey: null,
62
+ setHandleCompletionKey: () => {},
63
+ });
64
+
65
+ export function usePromptInput() {
66
+ return useContext(PromptInputContext);
67
+ }
68
+
69
+ export function useIsFullscreen() {
70
+ return useContext(PromptInputContext).isFullscreen;
71
+ }
@@ -0,0 +1,53 @@
1
+ import React, { useEffect } from "react";
2
+ import { Pressable, ActivityIndicator } from "react-native";
3
+ import { Mic, MicOff } from "lucide-react-native";
4
+ import { cn } from "../../../lib/utils";
5
+ import { useSpeechToText } from "../../../hooks/useSpeechToText";
6
+ import { usePromptInput } from "./context";
7
+
8
+ export type PromptInputMicButtonProps = {
9
+ className?: string;
10
+ apiUrl?: string;
11
+ };
12
+
13
+ export function PromptInputMicButton({ className, apiUrl }: PromptInputMicButtonProps) {
14
+ const { value, setValue, onError } = usePromptInput();
15
+ const stt = useSpeechToText({ apiUrl });
16
+
17
+ useEffect(() => {
18
+ if (stt.error) {
19
+ if (onError) onError(stt.error);
20
+ else console.warn('[PromptInput] STT error:', stt.error);
21
+ }
22
+ }, [stt.error, onError]);
23
+
24
+ const handlePress = async () => {
25
+ if (stt.isRecording) {
26
+ const text = await stt.stopAndTranscribe();
27
+ if (text) {
28
+ setValue(value ? `${value} ${text}` : text);
29
+ }
30
+ } else if (!stt.isTranscribing) {
31
+ stt.startRecording();
32
+ }
33
+ };
34
+
35
+ return (
36
+ <Pressable
37
+ onPress={handlePress}
38
+ disabled={stt.isTranscribing}
39
+ className={cn(
40
+ "h-8 w-8 rounded-full items-center justify-center active:opacity-70",
41
+ className
42
+ )}
43
+ >
44
+ {stt.isTranscribing ? (
45
+ <ActivityIndicator size="small" color="#6366f1" />
46
+ ) : stt.isRecording ? (
47
+ <MicOff size={16} color="#ef4444" />
48
+ ) : (
49
+ <Mic size={16} className="text-muted-foreground" />
50
+ )}
51
+ </Pressable>
52
+ );
53
+ }
@@ -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
+ }