@alia.onl/sdk 10.1.1 → 11.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/README.md +24 -0
  3. package/package.json +4 -6
  4. package/src/components/AliaAvatar.tsx +66 -0
  5. package/src/components/AliaChatContent.tsx +235 -112
  6. package/src/components/AliaChatHeader.tsx +118 -0
  7. package/src/components/AliaChatMessageList.tsx +171 -260
  8. package/src/components/AliaChatScreen.tsx +23 -24
  9. package/src/components/AliaChatSheet.tsx +70 -316
  10. package/src/components/AliaWelcomeMessage.tsx +47 -50
  11. package/src/components/AmbientField.tsx +577 -0
  12. package/src/components/IdentityMark.tsx +10 -2
  13. package/src/components/Markdown.tsx +116 -40
  14. package/src/components/PlanPreviewCard.tsx +71 -55
  15. package/src/components/ThinkingBlock.tsx +47 -0
  16. package/src/components/VoiceModeIcon.tsx +10 -0
  17. package/src/components/voice/VoiceControls.tsx +22 -73
  18. package/src/components/voice/VoiceSession.tsx +2 -1
  19. package/src/hooks/useAliaChat.ts +22 -15
  20. package/src/hooks/useAmbientWave.ts +35 -5
  21. package/src/hooks/useSoundEffects.ts +14 -11
  22. package/src/hooks/useSpeechToText.ts +9 -2
  23. package/src/hooks/useTTS.ts +163 -156
  24. package/src/hooks/useVoiceRoom.ts +228 -170
  25. package/src/index.ts +18 -6
  26. package/src/lib/catalogue.ts +17 -4
  27. package/src/lib/chat-stream.ts +25 -7
  28. package/src/lib/chat-transport.ts +2 -1
  29. package/src/lib/citations.ts +63 -0
  30. package/src/lib/content-utils.ts +3 -2
  31. package/src/lib/image-data-url.ts +15 -0
  32. package/src/lib/keyboard.tsx +1 -1
  33. package/src/lib/labels.tsx +81 -0
  34. package/src/lib/speech-recognition.native.ts +15 -4
  35. package/src/lib/speech-recognition.ts +27 -7
  36. package/src/lib/task-labels.ts +67 -0
  37. package/src/lib/tool-call-status.ts +28 -0
  38. package/src/lib/tool-registry.ts +14 -14
  39. package/src/lib/voice-text.ts +12 -6
  40. package/src/lib/voice-turn.ts +4 -1
  41. package/src/lib/work-log-copy.ts +61 -0
  42. package/src/lib/work-log.ts +378 -0
  43. package/src/tools.ts +11 -0
  44. package/src/types.ts +5 -2
  45. package/src/voice.ts +12 -4
  46. package/src/components/Reasoning.tsx +0 -152
  47. package/src/components/ResearchProgressCard.tsx +0 -159
  48. package/src/components/ThinkingIndicator.tsx +0 -98
  49. package/src/components/ui/button.tsx +0 -104
  50. package/src/components/ui/chat-text-input.tsx +0 -184
  51. package/src/components/ui/dropdown-menu.tsx +0 -2
  52. package/src/components/ui/dropdown-menu.web.tsx +0 -383
  53. package/src/components/ui/prompt-input/actions.tsx +0 -28
  54. package/src/components/ui/prompt-input/add-menu.tsx +0 -83
  55. package/src/components/ui/prompt-input/attachments.tsx +0 -181
  56. package/src/components/ui/prompt-input/autocomplete.tsx +0 -168
  57. package/src/components/ui/prompt-input/context.tsx +0 -71
  58. package/src/components/ui/prompt-input/mic-button.tsx +0 -53
  59. package/src/components/ui/prompt-input/prompt-input.tsx +0 -279
  60. package/src/components/ui/prompt-input/submit-button.tsx +0 -50
  61. package/src/components/ui/prompt-input/textarea.tsx +0 -80
  62. package/src/components/ui/text.tsx +0 -29
  63. package/src/components/voice/AudioWaveVisualizer.tsx +0 -444
  64. package/src/components/voice/VoiceOverlay.tsx +0 -75
@@ -1,168 +0,0 @@
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(() => 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
- }
@@ -1,71 +0,0 @@
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: React.Dispatch<React.SetStateAction<((key: string) => boolean) | null>>;
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
- }
@@ -1,53 +0,0 @@
1
- import React, { useEffect } from "react";
2
- import { Pressable, ActivityIndicator } from "react-native";
3
- import Mic from "lucide-react-native/icons/mic";
4
- import MicOff from "lucide-react-native/icons/mic-off";
5
- import { cn } from "../../../lib/utils";
6
- import { useSpeechToText } from "../../../hooks/useSpeechToText";
7
- import { usePromptInput } from "./context";
8
-
9
- export type PromptInputMicButtonProps = {
10
- className?: string;
11
- };
12
-
13
- export function PromptInputMicButton({ className }: PromptInputMicButtonProps) {
14
- const { value, setValue, onError } = usePromptInput();
15
- const stt = useSpeechToText();
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
- }
@@ -1,279 +0,0 @@
1
- import React, { useCallback, useEffect, useRef, useState } from "react";
2
- import {
3
- View,
4
- Pressable,
5
- type TextInput as RNTextInput,
6
- type ViewStyle,
7
- } from "react-native";
8
- import { KeyboardAvoidingView } from "../../../lib/keyboard";
9
- import Maximize2 from "lucide-react-native/icons/maximize-2";
10
- import Minimize2 from "lucide-react-native/icons/minimize-2";
11
- import { cn } from "../../../lib/utils";
12
- import { PromptInputContext, type Attachment, type Completion } from "./context";
13
- import { PromptInputTextarea } from "./textarea";
14
- import { PromptInputActions } from "./actions";
15
- import { PromptInputMicButton } from "./mic-button";
16
- import { PromptInputAutocomplete } from "./autocomplete";
17
- import { PromptInputAttachments } from "./attachments";
18
- import { PromptInputSubmitButton } from "./submit-button";
19
- import { PromptInputAddMenu } from "./add-menu";
20
-
21
- export type PromptInputProps = {
22
- isLoading?: boolean;
23
- value?: string;
24
- onValueChange?: (value: string) => void;
25
- maxHeight?: number;
26
- onSubmit?: () => void;
27
- children?: React.ReactNode;
28
- className?: string;
29
- disabled?: boolean;
30
- onImagePaste?: (files: File[]) => void;
31
- // Simple mode props (when no children)
32
- placeholder?: string;
33
- autocomplete?: boolean;
34
- autocompletePosition?: "top" | "bottom";
35
- // Shows the add menu as a standalone button to the left of the input box
36
- leadingAddMenu?: boolean;
37
- // Custom left-side actions (replaces default add menu in the actions bar)
38
- actionsLeft?: React.ReactNode;
39
- // Submit button props
40
- onStop?: () => void;
41
- emptyAction?: React.ReactNode;
42
- // Controlled attachments (optional — uses internal state if omitted)
43
- attachments?: Attachment[];
44
- onAddAttachment?: (attachment: Attachment) => void;
45
- onRemoveAttachment?: (id: string) => void;
46
- onUpdateAttachment?: (id: string, updates: Partial<Attachment>) => void;
47
- // Injectable autocomplete hooks
48
- useSuggestions?: (query: string) => { data: Completion[] | undefined; isLoading: boolean };
49
- onSuggestionUsed?: (suggestionId: string) => void;
50
- // Injectable error handler (replaces toast)
51
- onError?: (message: string) => void;
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
- ...props
79
- }: PromptInputProps) {
80
- const [internalValue, setInternalValue] = useState(value || "");
81
- const [currentHeight, setCurrentHeight] = useState(44);
82
- const [showFullscreen, setShowFullscreen] = useState(false);
83
- const [handleCompletionKey, setHandleCompletionKey] = useState<((key: string) => boolean) | null>(null);
84
- const textareaRef = useRef<RNTextInput>(null);
85
-
86
- // Internal attachment state (used when no controlled props)
87
- const [internalAttachments, setInternalAttachments] = useState<Attachment[]>(
88
- []
89
- );
90
- const attachments = controlledAttachments ?? internalAttachments;
91
-
92
- const addAttachment = useCallback(
93
- (a: Attachment) => {
94
- if (onAddAttachment) {
95
- onAddAttachment(a);
96
- } else {
97
- setInternalAttachments((prev) => [...prev, a]);
98
- }
99
- },
100
- [onAddAttachment]
101
- );
102
-
103
- const removeAttachment = useCallback(
104
- (id: string) => {
105
- if (onRemoveAttachment) {
106
- onRemoveAttachment(id);
107
- } else {
108
- setInternalAttachments((prev) => prev.filter((a) => a.id !== id));
109
- }
110
- },
111
- [onRemoveAttachment]
112
- );
113
-
114
- const updateAttachment = useCallback(
115
- (id: string, updates: Partial<Attachment>) => {
116
- if (onUpdateAttachment) {
117
- onUpdateAttachment(id, updates);
118
- } else {
119
- setInternalAttachments((prev) =>
120
- prev.map((a) => (a.id === id ? { ...a, ...updates } : a))
121
- );
122
- }
123
- },
124
- [onUpdateAttachment]
125
- );
126
-
127
- const handleChange = (newValue: string) => {
128
- setInternalValue(newValue);
129
- onValueChange?.(newValue);
130
- };
131
-
132
- const handleSubmit = () => {
133
- onSubmit?.();
134
- // Clicking the send button (as opposed to pressing Enter) shifts DOM focus
135
- // to the button on web — restore it so the user can keep typing.
136
- textareaRef.current?.focus();
137
- if (showFullscreen) setShowFullscreen(false);
138
- };
139
-
140
- useEffect(() => {
141
- if (!showFullscreen) setCurrentHeight(44);
142
- }, [showFullscreen]);
143
-
144
- const showExpandIcon = currentHeight > 100;
145
- const isSimpleMode = !children;
146
-
147
- const contextValue = {
148
- isLoading,
149
- value: value ?? internalValue,
150
- setValue: onValueChange ?? handleChange,
151
- maxHeight,
152
- onSubmit: handleSubmit,
153
- disabled,
154
- textareaRef,
155
- currentHeight,
156
- setCurrentHeight,
157
- isFullscreen: showFullscreen,
158
- onImagePaste,
159
- attachments,
160
- addAttachment,
161
- removeAttachment,
162
- updateAttachment,
163
- handleCompletionKey,
164
- setHandleCompletionKey,
165
- useSuggestions,
166
- onSuggestionUsed,
167
- onError,
168
- };
169
-
170
- const content = isSimpleMode ? (
171
- <>
172
- <PromptInputAttachments />
173
- <PromptInputTextarea
174
- placeholder={placeholder}
175
- className="min-h-[44px] text-base py-3"
176
- />
177
- <PromptInputActions className="flex-row items-center justify-between gap-2 mt-2 mb-1 px-3">
178
- <View className="flex-row items-center gap-1.5">
179
- {actionsLeft ?? <PromptInputAddMenu />}
180
- </View>
181
- <View className="flex-row items-center gap-1.5">
182
- <PromptInputMicButton />
183
- <PromptInputSubmitButton
184
- isLoading={isLoading}
185
- onStop={onStop}
186
- emptyAction={emptyAction}
187
- />
188
- </View>
189
- </PromptInputActions>
190
- </>
191
- ) : (
192
- children
193
- );
194
-
195
- const inputBox = (
196
- <Pressable
197
- onPress={() => {
198
- if (!disabled) textareaRef.current?.focus();
199
- }}
200
- disabled={disabled}
201
- >
202
- <View
203
- className={cn(
204
- "rounded-[24px] border border-border bg-background relative overflow-hidden",
205
- disabled && "opacity-60",
206
- className
207
- )}
208
- {...props}
209
- >
210
- {showExpandIcon && !disabled && (
211
- <Pressable
212
- onPress={() => setShowFullscreen(true)}
213
- className="absolute top-2 right-2 z-10 bg-background rounded-full p-1.5 border border-border active:opacity-70"
214
- >
215
- <Maximize2 size={16} className="text-muted-foreground" />
216
- </Pressable>
217
- )}
218
- {content}
219
- </View>
220
- </Pressable>
221
- );
222
-
223
- return (
224
- <PromptInputContext.Provider value={contextValue}>
225
- {autocomplete && autocompletePosition === "top" && !leadingAddMenu && (
226
- <PromptInputAutocomplete position="top" />
227
- )}
228
-
229
- <KeyboardAvoidingView behavior="padding">
230
- {leadingAddMenu ? (
231
- <View className="flex-row items-end gap-2">
232
- <PromptInputAddMenu
233
- iconSize={20}
234
- className="h-10 w-10 rounded-full border"
235
- />
236
- <View className="flex-1">
237
- {autocomplete && autocompletePosition === "top" && (
238
- <PromptInputAutocomplete position="top" />
239
- )}
240
- {inputBox}
241
- {autocomplete && autocompletePosition === "bottom" && (
242
- <PromptInputAutocomplete position="bottom" />
243
- )}
244
- </View>
245
- </View>
246
- ) : (
247
- inputBox
248
- )}
249
- </KeyboardAvoidingView>
250
-
251
- {autocomplete && autocompletePosition === "bottom" && !leadingAddMenu && (
252
- <PromptInputAutocomplete position="bottom" />
253
- )}
254
-
255
- {showFullscreen && (
256
- <View
257
- style={{
258
- // `position: 'fixed'` is a web-only value (NativeWind) not in RN's ViewStyle union.
259
- position: "fixed" as unknown as ViewStyle["position"],
260
- top: 0,
261
- left: 0,
262
- right: 0,
263
- bottom: 0,
264
- zIndex: 9998,
265
- }}
266
- className="bg-background"
267
- >
268
- <Pressable
269
- onPress={() => setShowFullscreen(false)}
270
- className="absolute top-4 right-4 z-50 p-2 active:opacity-70 bg-background/80 rounded-full"
271
- >
272
- <Minimize2 size={20} className="text-foreground" />
273
- </Pressable>
274
- <View className="flex-1 flex-col">{content}</View>
275
- </View>
276
- )}
277
- </PromptInputContext.Provider>
278
- );
279
- }
@@ -1,50 +0,0 @@
1
- import React from "react";
2
- import ArrowUp from "lucide-react-native/icons/arrow-up";
3
- import Square from "lucide-react-native/icons/square";
4
- import { cn } from "../../../lib/utils";
5
- import { Button } from "../button";
6
- import { usePromptInput } from "./context";
7
-
8
- export type PromptInputSubmitButtonProps = {
9
- isLoading?: boolean;
10
- onStop?: () => void;
11
- emptyAction?: React.ReactNode;
12
- className?: string;
13
- };
14
-
15
- export function PromptInputSubmitButton({
16
- isLoading,
17
- onStop,
18
- emptyAction,
19
- className,
20
- }: PromptInputSubmitButtonProps) {
21
- const { onSubmit, value, attachments } = usePromptInput();
22
- const hasContent = value.trim() || attachments.length > 0;
23
-
24
- if (isLoading && onStop) {
25
- return (
26
- <Button
27
- size="icon"
28
- onPress={onStop}
29
- className={cn("h-8 w-8 rounded-full", className)}
30
- >
31
- <Square size={12} color="white" className="fill-current" />
32
- </Button>
33
- );
34
- }
35
-
36
- if (!hasContent && emptyAction) {
37
- return <>{emptyAction}</>;
38
- }
39
-
40
- return (
41
- <Button
42
- size="icon"
43
- onPress={onSubmit}
44
- disabled={!hasContent}
45
- className={cn("h-8 w-8 rounded-full", className)}
46
- >
47
- <ArrowUp size={16} color="white" />
48
- </Button>
49
- );
50
- }
@@ -1,80 +0,0 @@
1
- import React from "react";
2
- import { StyleSheet, 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 inputStyle = React.useMemo(
32
- () => [style, styles.borderless, isFullscreen && styles.fullscreen],
33
- [isFullscreen, style],
34
- );
35
-
36
- const textInput = (
37
- <ChatTextInput
38
- ref={textareaRef}
39
- value={value}
40
- onChangeText={setValue}
41
- onSubmitEditing={onSubmit}
42
- onEnterPress={onSubmit}
43
- onHeightChange={setCurrentHeight}
44
- onCompletionKey={handleCompletionKey ?? undefined}
45
- disableEnterToSubmit={isFullscreen}
46
- disableAutoHeight={isFullscreen}
47
- maxHeight={isFullscreen ? 10000 : maxHeight}
48
- onImagePaste={onImagePaste}
49
- fillContainer={isFullscreen}
50
- unstyled
51
- className={cn(
52
- "w-full border-0 bg-transparent text-foreground web:shadow-none",
53
- isFullscreen ? "px-4 pt-4" : "min-h-[44px] py-3",
54
- className
55
- )}
56
- style={inputStyle}
57
- underlineColorAndroid="transparent"
58
- placeholder={placeholder}
59
- multiline
60
- editable={!disabled}
61
- noFocus={true}
62
- {...props}
63
- />
64
- );
65
-
66
- if (isFullscreen) {
67
- return <View style={{ flex: 1 }}>{textInput}</View>;
68
- }
69
-
70
- return textInput;
71
- }
72
-
73
- const styles = StyleSheet.create({
74
- borderless: {
75
- borderWidth: 0,
76
- },
77
- fullscreen: {
78
- paddingBottom: 100,
79
- },
80
- });