@alia.onl/sdk 10.1.1 → 11.1.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/CHANGELOG.md +67 -0
- package/README.md +24 -0
- package/package.json +4 -6
- package/src/components/AliaAvatar.tsx +66 -0
- package/src/components/AliaChatContent.tsx +236 -112
- package/src/components/AliaChatHeader.tsx +118 -0
- package/src/components/AliaChatMessageList.tsx +197 -260
- package/src/components/AliaChatScreen.tsx +23 -24
- package/src/components/AliaChatSheet.tsx +70 -316
- package/src/components/AliaWelcomeMessage.tsx +47 -50
- package/src/components/AmbientField.tsx +577 -0
- package/src/components/IdentityMark.tsx +10 -2
- package/src/components/Markdown.tsx +116 -40
- package/src/components/PlanPreviewCard.tsx +71 -55
- package/src/components/QuestionCard.tsx +62 -0
- package/src/components/ThinkingBlock.tsx +47 -0
- package/src/components/VoiceModeIcon.tsx +10 -0
- package/src/components/voice/VoiceControls.tsx +22 -73
- package/src/components/voice/VoiceSession.tsx +2 -1
- package/src/hooks/useAliaChat.ts +22 -15
- package/src/hooks/useAmbientWave.ts +35 -5
- package/src/hooks/useSoundEffects.ts +14 -11
- package/src/hooks/useSpeechToText.ts +9 -2
- package/src/hooks/useTTS.ts +163 -156
- package/src/hooks/useVoiceRoom.ts +228 -170
- package/src/index.ts +26 -6
- package/src/lib/catalogue.ts +17 -4
- package/src/lib/chat-stream.ts +25 -7
- package/src/lib/chat-transport.ts +2 -1
- package/src/lib/citations.ts +63 -0
- package/src/lib/content-utils.ts +3 -2
- package/src/lib/image-data-url.ts +15 -0
- package/src/lib/keyboard.tsx +1 -1
- package/src/lib/labels.tsx +81 -0
- package/src/lib/questions.ts +78 -0
- package/src/lib/speech-recognition.native.ts +15 -4
- package/src/lib/speech-recognition.ts +27 -7
- package/src/lib/task-labels.ts +67 -0
- package/src/lib/tool-call-status.ts +28 -0
- package/src/lib/tool-registry.ts +14 -14
- package/src/lib/voice-text.ts +12 -6
- package/src/lib/voice-turn.ts +4 -1
- package/src/lib/work-log-copy.ts +61 -0
- package/src/lib/work-log.ts +378 -0
- package/src/tools.ts +18 -0
- package/src/types.ts +5 -2
- package/src/voice.ts +12 -4
- package/src/components/Reasoning.tsx +0 -152
- package/src/components/ResearchProgressCard.tsx +0 -159
- package/src/components/ThinkingIndicator.tsx +0 -98
- package/src/components/ui/button.tsx +0 -104
- package/src/components/ui/chat-text-input.tsx +0 -184
- package/src/components/ui/dropdown-menu.tsx +0 -2
- package/src/components/ui/dropdown-menu.web.tsx +0 -383
- package/src/components/ui/prompt-input/actions.tsx +0 -28
- package/src/components/ui/prompt-input/add-menu.tsx +0 -83
- package/src/components/ui/prompt-input/attachments.tsx +0 -181
- package/src/components/ui/prompt-input/autocomplete.tsx +0 -168
- package/src/components/ui/prompt-input/context.tsx +0 -71
- package/src/components/ui/prompt-input/mic-button.tsx +0 -53
- package/src/components/ui/prompt-input/prompt-input.tsx +0 -279
- package/src/components/ui/prompt-input/submit-button.tsx +0 -50
- package/src/components/ui/prompt-input/textarea.tsx +0 -80
- package/src/components/ui/text.tsx +0 -29
- package/src/components/voice/AudioWaveVisualizer.tsx +0 -444
- 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
|
-
});
|