@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,164 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import {
|
|
3
|
+
TextInput,
|
|
4
|
+
Platform,
|
|
5
|
+
View,
|
|
6
|
+
type NativeSyntheticEvent,
|
|
7
|
+
type TextInputKeyPressEventData,
|
|
8
|
+
type NativeSyntheticEvent as RNSyntheticEvent,
|
|
9
|
+
type TextInputContentSizeChangeEventData,
|
|
10
|
+
} from "react-native";
|
|
11
|
+
import { cn } from "../../lib/utils";
|
|
12
|
+
|
|
13
|
+
type ChatTextInputProps = React.ComponentPropsWithoutRef<typeof TextInput> & {
|
|
14
|
+
noFocus?: boolean;
|
|
15
|
+
onEnterPress?: () => void;
|
|
16
|
+
onCompletionKey?: (key: string) => boolean;
|
|
17
|
+
maxHeight?: number;
|
|
18
|
+
minHeight?: number;
|
|
19
|
+
onHeightChange?: (height: number) => void;
|
|
20
|
+
disableEnterToSubmit?: boolean;
|
|
21
|
+
disableAutoHeight?: boolean;
|
|
22
|
+
onImagePaste?: (files: File[]) => void;
|
|
23
|
+
fillContainer?: boolean;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const ChatTextInput = React.forwardRef<TextInput, ChatTextInputProps>(
|
|
27
|
+
({
|
|
28
|
+
className,
|
|
29
|
+
noFocus = false,
|
|
30
|
+
onEnterPress,
|
|
31
|
+
onCompletionKey,
|
|
32
|
+
onKeyPress,
|
|
33
|
+
maxHeight = 200,
|
|
34
|
+
minHeight = 44,
|
|
35
|
+
onContentSizeChange,
|
|
36
|
+
onHeightChange,
|
|
37
|
+
style,
|
|
38
|
+
disableEnterToSubmit = false,
|
|
39
|
+
disableAutoHeight = false,
|
|
40
|
+
onImagePaste,
|
|
41
|
+
fillContainer = false,
|
|
42
|
+
...props
|
|
43
|
+
}, ref) => {
|
|
44
|
+
const inputRef = React.useRef<TextInput>(null);
|
|
45
|
+
const wrapperRef = React.useRef<View>(null);
|
|
46
|
+
|
|
47
|
+
React.useImperativeHandle(ref, () => inputRef.current as TextInput);
|
|
48
|
+
|
|
49
|
+
// Attach paste event listener (web only)
|
|
50
|
+
React.useEffect(() => {
|
|
51
|
+
if (Platform.OS !== 'web' || !onImagePaste) return;
|
|
52
|
+
|
|
53
|
+
const handlePaste = (e: Event) => {
|
|
54
|
+
const clipboardEvent = e as ClipboardEvent;
|
|
55
|
+
|
|
56
|
+
// @ts-ignore - web-specific API
|
|
57
|
+
const activeElement = document.activeElement;
|
|
58
|
+
// @ts-ignore - web-specific API
|
|
59
|
+
const wrapper = wrapperRef.current;
|
|
60
|
+
|
|
61
|
+
// @ts-ignore - web-specific API
|
|
62
|
+
const isContained = wrapper && wrapper.contains(activeElement);
|
|
63
|
+
|
|
64
|
+
if (!isContained) return;
|
|
65
|
+
|
|
66
|
+
const items = clipboardEvent.clipboardData?.items;
|
|
67
|
+
if (!items) return;
|
|
68
|
+
|
|
69
|
+
const imageFiles: File[] = [];
|
|
70
|
+
for (let i = 0; i < items.length; i++) {
|
|
71
|
+
const item = items[i];
|
|
72
|
+
if (item.type.indexOf('image') !== -1) {
|
|
73
|
+
const file = item.getAsFile();
|
|
74
|
+
if (file) {
|
|
75
|
+
imageFiles.push(file);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (imageFiles.length > 0) {
|
|
81
|
+
clipboardEvent.preventDefault();
|
|
82
|
+
onImagePaste(imageFiles);
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
// @ts-ignore - web-specific API
|
|
87
|
+
document.addEventListener('paste', handlePaste);
|
|
88
|
+
|
|
89
|
+
return () => {
|
|
90
|
+
// @ts-ignore - web-specific API
|
|
91
|
+
document.removeEventListener('paste', handlePaste);
|
|
92
|
+
};
|
|
93
|
+
}, [onImagePaste]);
|
|
94
|
+
|
|
95
|
+
const handleKeyPress = (
|
|
96
|
+
e: NativeSyntheticEvent<TextInputKeyPressEventData>
|
|
97
|
+
) => {
|
|
98
|
+
onKeyPress?.(e);
|
|
99
|
+
|
|
100
|
+
const key = e.nativeEvent.key;
|
|
101
|
+
|
|
102
|
+
if (onCompletionKey && (key === "ArrowUp" || key === "ArrowDown" || key === "Enter" || key === "Escape")) {
|
|
103
|
+
if (onCompletionKey(key)) {
|
|
104
|
+
e.preventDefault();
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (key === "Enter" && !disableEnterToSubmit) {
|
|
110
|
+
// @ts-ignore - shiftKey exists on web
|
|
111
|
+
if (Platform.OS !== 'web' || !e.nativeEvent.shiftKey) {
|
|
112
|
+
e.preventDefault();
|
|
113
|
+
onEnterPress?.();
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const handleContentSizeChange = (
|
|
119
|
+
e: RNSyntheticEvent<TextInputContentSizeChangeEventData>
|
|
120
|
+
) => {
|
|
121
|
+
onContentSizeChange?.(e);
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
return (
|
|
125
|
+
<View
|
|
126
|
+
ref={wrapperRef}
|
|
127
|
+
style={{ width: '100%', ...(fillContainer && { flex: 1 }) }}
|
|
128
|
+
onLayout={(e) => onHeightChange?.(e.nativeEvent.layout.height)}
|
|
129
|
+
>
|
|
130
|
+
<TextInput
|
|
131
|
+
ref={inputRef}
|
|
132
|
+
className={cn(
|
|
133
|
+
"native:text-md native:leading-[1.25] rounded-xl border border-input bg-background px-3.5 text-base text-foreground file:border-0 file:bg-transparent file:font-medium placeholder:text-muted-foreground web:flex web:w-full web:py-2 lg:text-sm",
|
|
134
|
+
"web:ring-offset-background web:focus-visible:outline-none web:focus-visible:ring-2 web:focus-visible:ring-ring web:focus-visible:ring-offset-2",
|
|
135
|
+
!fillContainer && !props.multiline && "h-9",
|
|
136
|
+
fillContainer && "h-full",
|
|
137
|
+
noFocus && "web:focus-visible:ring-0 web:focus-visible:ring-offset-0",
|
|
138
|
+
props.editable === false && "opacity-50 web:cursor-not-allowed",
|
|
139
|
+
className
|
|
140
|
+
)}
|
|
141
|
+
placeholderClassName={cn("text-muted-foreground", props.placeholderClassName)}
|
|
142
|
+
onKeyPress={handleKeyPress}
|
|
143
|
+
onContentSizeChange={handleContentSizeChange}
|
|
144
|
+
scrollEnabled={fillContainer || props.multiline}
|
|
145
|
+
style={[
|
|
146
|
+
style,
|
|
147
|
+
!fillContainer && props.multiline && !disableAutoHeight && {
|
|
148
|
+
minHeight,
|
|
149
|
+
maxHeight,
|
|
150
|
+
overflow: 'auto' as any,
|
|
151
|
+
...(Platform.OS === 'web' ? { fieldSizing: 'content' } : {}),
|
|
152
|
+
},
|
|
153
|
+
fillContainer && { flex: 1, height: '100%' },
|
|
154
|
+
]}
|
|
155
|
+
{...props}
|
|
156
|
+
/>
|
|
157
|
+
</View>
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
ChatTextInput.displayName = "ChatTextInput";
|
|
163
|
+
|
|
164
|
+
export { ChatTextInput };
|
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
// Web dropdown menu using Radix UI with Tailwind styling.
|
|
2
|
+
// Provides the same zeego-compatible API as the native dropdown-menu.tsx.
|
|
3
|
+
import { cn } from "../../lib/utils";
|
|
4
|
+
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
|
|
5
|
+
import { CheckIcon, ChevronRightIcon } from "@radix-ui/react-icons";
|
|
6
|
+
import * as React from "react";
|
|
7
|
+
import {
|
|
8
|
+
Star, Pencil, Trash2, Share2, Download, Settings, HelpCircle,
|
|
9
|
+
Image, FileText, Search, ShoppingBag, MoreHorizontal, ExternalLink,
|
|
10
|
+
BookOpen, Globe, PenTool, Sparkles, User, CreditCard, Bell, LogOut,
|
|
11
|
+
Folder, Check, Brain, Ghost, Bot, Bookmark, AlertTriangle, Pin,
|
|
12
|
+
} from "lucide-react-native";
|
|
13
|
+
|
|
14
|
+
// Map iOS SF Symbol names to Lucide icons for web rendering
|
|
15
|
+
const SF_SYMBOL_MAP: Record<string, React.ComponentType<any>> = {
|
|
16
|
+
"star": Star, "star.fill": Star, "pencil": Pencil, "trash": Trash2,
|
|
17
|
+
"square.and.arrow.up": Share2, "arrow.down.doc": Download,
|
|
18
|
+
"gearshape": Settings, "questionmark.circle": HelpCircle,
|
|
19
|
+
"photo": Image, "doc": FileText, "magnifyingglass": Search,
|
|
20
|
+
"bag": ShoppingBag, "ellipsis": MoreHorizontal, "link": ExternalLink,
|
|
21
|
+
"book": BookOpen, "globe": Globe, "pencil.tip": PenTool,
|
|
22
|
+
"sparkle": Sparkles, "person.circle": User, "creditcard": CreditCard,
|
|
23
|
+
"bell": Bell, "rectangle.portrait.and.arrow.right": LogOut,
|
|
24
|
+
"folder": Folder, "checkmark": Check, "brain": Brain,
|
|
25
|
+
"eye.slash": Ghost, "cpu": Bot,
|
|
26
|
+
"bookmark": Bookmark, "bookmark.fill": Bookmark,
|
|
27
|
+
"exclamationmark.triangle": AlertTriangle,
|
|
28
|
+
"pin": Pin, "pin.fill": Pin,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
// Singleton registry: only one dropdown menu open at a time.
|
|
33
|
+
const openMenus = new Set<() => void>();
|
|
34
|
+
|
|
35
|
+
function DropdownMenu({
|
|
36
|
+
open: controlledOpen,
|
|
37
|
+
onOpenChange,
|
|
38
|
+
defaultOpen,
|
|
39
|
+
...props
|
|
40
|
+
}: React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Root>) {
|
|
41
|
+
const [internalOpen, setInternalOpen] = React.useState(defaultOpen ?? false);
|
|
42
|
+
const open = controlledOpen ?? internalOpen;
|
|
43
|
+
|
|
44
|
+
const onOpenChangeRef = React.useRef(onOpenChange);
|
|
45
|
+
onOpenChangeRef.current = onOpenChange;
|
|
46
|
+
|
|
47
|
+
const closeRef = React.useRef<() => void>(undefined);
|
|
48
|
+
if (!closeRef.current) {
|
|
49
|
+
closeRef.current = () => {
|
|
50
|
+
setInternalOpen(false);
|
|
51
|
+
onOpenChangeRef.current?.(false);
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
React.useEffect(() => {
|
|
56
|
+
const cb = closeRef.current!;
|
|
57
|
+
openMenus.add(cb);
|
|
58
|
+
return () => { openMenus.delete(cb); };
|
|
59
|
+
}, []);
|
|
60
|
+
|
|
61
|
+
const handleOpenChange = (next: boolean) => {
|
|
62
|
+
if (next) {
|
|
63
|
+
openMenus.forEach((cb) => {
|
|
64
|
+
if (cb !== closeRef.current) cb();
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
setInternalOpen(next);
|
|
68
|
+
onOpenChange?.(next);
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<DropdownMenuPrimitive.Root
|
|
73
|
+
modal={false}
|
|
74
|
+
open={open}
|
|
75
|
+
onOpenChange={handleOpenChange}
|
|
76
|
+
{...props}
|
|
77
|
+
/>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const DropdownMenuTrigger = React.forwardRef<
|
|
82
|
+
React.ElementRef<typeof DropdownMenuPrimitive.Trigger>,
|
|
83
|
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Trigger>
|
|
84
|
+
>((props, ref) => (
|
|
85
|
+
<DropdownMenuPrimitive.Trigger ref={ref} asChild {...props} />
|
|
86
|
+
));
|
|
87
|
+
DropdownMenuTrigger.displayName = "DropdownMenuTrigger";
|
|
88
|
+
|
|
89
|
+
const DropdownMenuSub = DropdownMenuPrimitive.Sub;
|
|
90
|
+
|
|
91
|
+
const DropdownMenuGroup = React.forwardRef<
|
|
92
|
+
React.ElementRef<typeof DropdownMenuPrimitive.Group>,
|
|
93
|
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Group> & {
|
|
94
|
+
horizontal?: boolean;
|
|
95
|
+
}
|
|
96
|
+
>(({ className, horizontal, children, ...props }, ref) => (
|
|
97
|
+
<DropdownMenuPrimitive.Group
|
|
98
|
+
ref={ref}
|
|
99
|
+
className={cn(
|
|
100
|
+
horizontal && "flex flex-row space-x-2 justify-between",
|
|
101
|
+
className
|
|
102
|
+
)}
|
|
103
|
+
{...props}
|
|
104
|
+
>
|
|
105
|
+
{children}
|
|
106
|
+
</DropdownMenuPrimitive.Group>
|
|
107
|
+
));
|
|
108
|
+
DropdownMenuGroup.displayName = DropdownMenuPrimitive.Group.displayName;
|
|
109
|
+
|
|
110
|
+
const DropdownMenuSubTrigger = React.forwardRef<
|
|
111
|
+
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
|
|
112
|
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
|
|
113
|
+
inset?: boolean;
|
|
114
|
+
}
|
|
115
|
+
>(({ className, inset, children, ...props }, ref) => (
|
|
116
|
+
<DropdownMenuPrimitive.SubTrigger
|
|
117
|
+
ref={ref}
|
|
118
|
+
className={cn(
|
|
119
|
+
"flex cursor-default select-none items-center gap-1.5 rounded-md px-1.5 py-1 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground [&:focus_svg:not([class*='text-'])]:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
120
|
+
inset && "pl-8",
|
|
121
|
+
className
|
|
122
|
+
)}
|
|
123
|
+
{...props}
|
|
124
|
+
>
|
|
125
|
+
{children}
|
|
126
|
+
<ChevronRightIcon className="ml-auto h-4 w-4" />
|
|
127
|
+
</DropdownMenuPrimitive.SubTrigger>
|
|
128
|
+
));
|
|
129
|
+
DropdownMenuSubTrigger.displayName =
|
|
130
|
+
DropdownMenuPrimitive.SubTrigger.displayName;
|
|
131
|
+
|
|
132
|
+
const DropdownMenuSubContent = React.forwardRef<
|
|
133
|
+
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
|
|
134
|
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
|
|
135
|
+
>(({ className, style, ...props }, ref) => (
|
|
136
|
+
<DropdownMenuPrimitive.Portal>
|
|
137
|
+
<DropdownMenuPrimitive.SubContent
|
|
138
|
+
ref={ref}
|
|
139
|
+
className={cn(
|
|
140
|
+
"z-[9999] min-w-[96px] rounded-md bg-popover p-1 text-popover-foreground duration-100 origin-[var(--radix-dropdown-menu-content-transform-origin)] overflow-hidden data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
|
141
|
+
className
|
|
142
|
+
)}
|
|
143
|
+
style={{
|
|
144
|
+
boxShadow: '0 4px 6px -1px rgba(0,0,0,0.1), 0 2px 4px -2px rgba(0,0,0,0.1)',
|
|
145
|
+
border: '1px solid hsl(var(--border))',
|
|
146
|
+
...style as any,
|
|
147
|
+
}}
|
|
148
|
+
{...props}
|
|
149
|
+
/>
|
|
150
|
+
</DropdownMenuPrimitive.Portal>
|
|
151
|
+
));
|
|
152
|
+
DropdownMenuSubContent.displayName =
|
|
153
|
+
DropdownMenuPrimitive.SubContent.displayName;
|
|
154
|
+
|
|
155
|
+
const DropdownMenuContent = React.forwardRef<
|
|
156
|
+
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
|
|
157
|
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
|
|
158
|
+
>(({ className, sideOffset = 4, align = "start", style, ...props }, ref) => (
|
|
159
|
+
<DropdownMenuPrimitive.Portal>
|
|
160
|
+
<DropdownMenuPrimitive.Content
|
|
161
|
+
ref={ref}
|
|
162
|
+
sideOffset={sideOffset}
|
|
163
|
+
align={align}
|
|
164
|
+
className={cn(
|
|
165
|
+
"z-[9999] min-w-[8rem] rounded-lg bg-popover p-1 text-popover-foreground duration-100 max-h-[var(--radix-dropdown-menu-content-available-height)] origin-[var(--radix-dropdown-menu-content-transform-origin)] overflow-x-hidden overflow-y-auto data-[state=closed]:overflow-hidden",
|
|
166
|
+
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
|
167
|
+
className
|
|
168
|
+
)}
|
|
169
|
+
style={{
|
|
170
|
+
boxShadow: '0 4px 6px -1px rgba(0,0,0,0.1), 0 2px 4px -2px rgba(0,0,0,0.1)',
|
|
171
|
+
border: '1px solid hsl(var(--border))',
|
|
172
|
+
...style as any,
|
|
173
|
+
}}
|
|
174
|
+
{...props}
|
|
175
|
+
/>
|
|
176
|
+
</DropdownMenuPrimitive.Portal>
|
|
177
|
+
));
|
|
178
|
+
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
|
|
179
|
+
|
|
180
|
+
const DropdownMenuItem = React.forwardRef<
|
|
181
|
+
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
|
|
182
|
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
|
|
183
|
+
inset?: boolean;
|
|
184
|
+
destructive?: boolean;
|
|
185
|
+
shouldDismissMenuOnSelect?: boolean;
|
|
186
|
+
}
|
|
187
|
+
>(
|
|
188
|
+
(
|
|
189
|
+
{
|
|
190
|
+
className,
|
|
191
|
+
inset,
|
|
192
|
+
destructive,
|
|
193
|
+
shouldDismissMenuOnSelect,
|
|
194
|
+
onSelect,
|
|
195
|
+
...props
|
|
196
|
+
},
|
|
197
|
+
ref
|
|
198
|
+
) => (
|
|
199
|
+
<DropdownMenuPrimitive.Item
|
|
200
|
+
ref={ref}
|
|
201
|
+
onSelect={(e: any) => {
|
|
202
|
+
onSelect?.(e);
|
|
203
|
+
if (shouldDismissMenuOnSelect === false) {
|
|
204
|
+
e.preventDefault();
|
|
205
|
+
}
|
|
206
|
+
}}
|
|
207
|
+
className={cn(
|
|
208
|
+
"relative flex flex-row cursor-default select-none items-center gap-1.5 rounded-md px-1.5 py-1 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg:not([class*='text-'])]:text-muted-foreground [&:focus_svg:not([class*='text-'])]:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
209
|
+
inset && "pl-8",
|
|
210
|
+
destructive &&
|
|
211
|
+
"text-destructive focus:text-destructive focus:bg-destructive/10 dark:focus:bg-destructive/20 [&_svg]:!text-destructive",
|
|
212
|
+
className
|
|
213
|
+
)}
|
|
214
|
+
{...props}
|
|
215
|
+
/>
|
|
216
|
+
)
|
|
217
|
+
);
|
|
218
|
+
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
|
|
219
|
+
|
|
220
|
+
const DropdownMenuCheckboxItem = React.forwardRef<
|
|
221
|
+
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
|
|
222
|
+
Omit<
|
|
223
|
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>,
|
|
224
|
+
"onSelect" | "checked"
|
|
225
|
+
> & {
|
|
226
|
+
value: "mixed" | "on" | "off" | boolean;
|
|
227
|
+
onValueChange?: (
|
|
228
|
+
state: "mixed" | "on" | "off",
|
|
229
|
+
prevState: "mixed" | "on" | "off"
|
|
230
|
+
) => void;
|
|
231
|
+
shouldDismissMenuOnSelect?: boolean;
|
|
232
|
+
}
|
|
233
|
+
>(
|
|
234
|
+
(
|
|
235
|
+
{
|
|
236
|
+
className,
|
|
237
|
+
value,
|
|
238
|
+
children,
|
|
239
|
+
shouldDismissMenuOnSelect,
|
|
240
|
+
onValueChange,
|
|
241
|
+
...props
|
|
242
|
+
},
|
|
243
|
+
ref
|
|
244
|
+
) => (
|
|
245
|
+
<DropdownMenuPrimitive.CheckboxItem
|
|
246
|
+
ref={ref}
|
|
247
|
+
onSelect={(e: any) => {
|
|
248
|
+
const current =
|
|
249
|
+
value === true ? "on" : value === false ? "off" : value;
|
|
250
|
+
const next = current === "on" ? "off" : "on";
|
|
251
|
+
onValueChange?.(next, current);
|
|
252
|
+
if (shouldDismissMenuOnSelect === false) {
|
|
253
|
+
e.preventDefault();
|
|
254
|
+
}
|
|
255
|
+
}}
|
|
256
|
+
className={cn(
|
|
257
|
+
"relative flex cursor-default select-none items-center gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground [&:focus_svg:not([class*='text-'])]:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
258
|
+
className
|
|
259
|
+
)}
|
|
260
|
+
checked={typeof value === "boolean" ? value : value !== "off"}
|
|
261
|
+
{...props}
|
|
262
|
+
>
|
|
263
|
+
<span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
|
|
264
|
+
<DropdownMenuPrimitive.ItemIndicator>
|
|
265
|
+
<CheckIcon className="h-4 w-4" />
|
|
266
|
+
</DropdownMenuPrimitive.ItemIndicator>
|
|
267
|
+
</span>
|
|
268
|
+
{children}
|
|
269
|
+
</DropdownMenuPrimitive.CheckboxItem>
|
|
270
|
+
)
|
|
271
|
+
);
|
|
272
|
+
DropdownMenuCheckboxItem.displayName =
|
|
273
|
+
DropdownMenuPrimitive.CheckboxItem.displayName;
|
|
274
|
+
|
|
275
|
+
const DropdownMenuLabel = React.forwardRef<
|
|
276
|
+
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
|
|
277
|
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
|
|
278
|
+
inset?: boolean;
|
|
279
|
+
}
|
|
280
|
+
>(({ className, inset, ...props }, ref) => (
|
|
281
|
+
<DropdownMenuPrimitive.Label
|
|
282
|
+
ref={ref}
|
|
283
|
+
className={cn(
|
|
284
|
+
"text-muted-foreground px-1.5 py-1 text-xs font-medium",
|
|
285
|
+
inset && "pl-8",
|
|
286
|
+
className
|
|
287
|
+
)}
|
|
288
|
+
{...props}
|
|
289
|
+
/>
|
|
290
|
+
));
|
|
291
|
+
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
|
|
292
|
+
|
|
293
|
+
const DropdownMenuSeparator = React.forwardRef<
|
|
294
|
+
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
|
|
295
|
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
|
|
296
|
+
>(({ className, ...props }, ref) => (
|
|
297
|
+
<DropdownMenuPrimitive.Separator
|
|
298
|
+
ref={ref}
|
|
299
|
+
className={cn("-mx-1 my-1 h-px bg-border", className)}
|
|
300
|
+
{...props}
|
|
301
|
+
/>
|
|
302
|
+
));
|
|
303
|
+
DropdownMenuSeparator.displayName =
|
|
304
|
+
DropdownMenuPrimitive.Separator.displayName;
|
|
305
|
+
|
|
306
|
+
export const ItemIcon = React.forwardRef<
|
|
307
|
+
HTMLSpanElement,
|
|
308
|
+
React.HTMLProps<HTMLSpanElement> & { ios?: { name?: string; [key: string]: any }; androidIconName?: string }
|
|
309
|
+
>(({ className, ios, androidIconName, children, ...props }, ref) => {
|
|
310
|
+
const IconComponent = ios?.name ? SF_SYMBOL_MAP[ios.name] : null;
|
|
311
|
+
return (
|
|
312
|
+
<span ref={ref} className={cn("inline-flex h-4 w-4 items-center justify-center shrink-0 text-muted-foreground", className)} {...props}>
|
|
313
|
+
{IconComponent ? <IconComponent size={16} /> : children}
|
|
314
|
+
</span>
|
|
315
|
+
);
|
|
316
|
+
});
|
|
317
|
+
ItemIcon.displayName = "ItemIcon";
|
|
318
|
+
|
|
319
|
+
export const ItemTitle = React.forwardRef<
|
|
320
|
+
HTMLSpanElement,
|
|
321
|
+
React.HTMLProps<HTMLSpanElement>
|
|
322
|
+
>((props, ref) => {
|
|
323
|
+
return <span ref={ref} {...props} />;
|
|
324
|
+
});
|
|
325
|
+
ItemTitle.displayName = "ItemTitle";
|
|
326
|
+
|
|
327
|
+
export const ItemSubtitle = React.forwardRef<
|
|
328
|
+
HTMLSpanElement,
|
|
329
|
+
React.HTMLProps<HTMLSpanElement>
|
|
330
|
+
>(({ className, ...props }, ref) => {
|
|
331
|
+
return (
|
|
332
|
+
<span
|
|
333
|
+
ref={ref}
|
|
334
|
+
className={cn("block text-xs text-muted-foreground mt-0.5", className)}
|
|
335
|
+
{...props}
|
|
336
|
+
/>
|
|
337
|
+
);
|
|
338
|
+
});
|
|
339
|
+
ItemSubtitle.displayName = "ItemSubtitle";
|
|
340
|
+
|
|
341
|
+
export { ItemImage, ItemIndicator, Arrow } from "zeego/dropdown-menu";
|
|
342
|
+
|
|
343
|
+
export {
|
|
344
|
+
DropdownMenu as Root,
|
|
345
|
+
DropdownMenuTrigger as Trigger,
|
|
346
|
+
DropdownMenuContent as Content,
|
|
347
|
+
DropdownMenuItem as Item,
|
|
348
|
+
DropdownMenuCheckboxItem as CheckboxItem,
|
|
349
|
+
DropdownMenuLabel as Label,
|
|
350
|
+
DropdownMenuSeparator as Separator,
|
|
351
|
+
DropdownMenuGroup as Group,
|
|
352
|
+
DropdownMenuSub as Sub,
|
|
353
|
+
DropdownMenuSubContent as SubContent,
|
|
354
|
+
DropdownMenuSubTrigger as SubTrigger,
|
|
355
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { View } from "react-native";
|
|
3
|
+
import { cn } from "../../../lib/utils";
|
|
4
|
+
import { usePromptInput } from "./context";
|
|
5
|
+
|
|
6
|
+
export type PromptInputActionsProps = React.ComponentProps<typeof View>;
|
|
7
|
+
|
|
8
|
+
export function PromptInputActions({
|
|
9
|
+
children,
|
|
10
|
+
className,
|
|
11
|
+
...props
|
|
12
|
+
}: PromptInputActionsProps) {
|
|
13
|
+
const { isFullscreen } = usePromptInput();
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<View
|
|
17
|
+
className={cn(
|
|
18
|
+
"flex-row items-center gap-2",
|
|
19
|
+
isFullscreen &&
|
|
20
|
+
"absolute bottom-4 left-4 right-4 max-w-2xl mx-auto rounded-full border border-border bg-background px-4 py-3",
|
|
21
|
+
className
|
|
22
|
+
)}
|
|
23
|
+
{...props}
|
|
24
|
+
>
|
|
25
|
+
{children}
|
|
26
|
+
</View>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Plus } from "lucide-react-native";
|
|
3
|
+
import { cn } from "../../../lib/utils";
|
|
4
|
+
import { Button } from "../button";
|
|
5
|
+
import * as DropdownMenu from "../dropdown-menu";
|
|
6
|
+
import { useImagePicker } from "../../../hooks/useImagePicker";
|
|
7
|
+
import { useDocumentPicker } from "../../../hooks/useDocumentPicker";
|
|
8
|
+
import { usePromptInput } from "./context";
|
|
9
|
+
|
|
10
|
+
export type PromptInputAddMenuProps = {
|
|
11
|
+
className?: string;
|
|
12
|
+
iconSize?: number;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export function PromptInputAddMenu({ className, iconSize = 16 }: PromptInputAddMenuProps) {
|
|
16
|
+
const { addAttachment } = usePromptInput();
|
|
17
|
+
const { pickImage } = useImagePicker();
|
|
18
|
+
const { pickDocument } = useDocumentPicker();
|
|
19
|
+
|
|
20
|
+
const handleAddPhotos = async () => {
|
|
21
|
+
try {
|
|
22
|
+
const assets = await pickImage();
|
|
23
|
+
if (assets && assets.length > 0) {
|
|
24
|
+
assets.forEach((asset) => {
|
|
25
|
+
addAttachment({
|
|
26
|
+
id: `img-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
|
27
|
+
uri: asset.uri,
|
|
28
|
+
type: "image",
|
|
29
|
+
name: asset.name,
|
|
30
|
+
size: asset.size,
|
|
31
|
+
mimeType: asset.mimeType,
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
} catch (err) {
|
|
36
|
+
console.error("Error picking images:", err);
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const handleAddDocument = async () => {
|
|
41
|
+
try {
|
|
42
|
+
const docs = await pickDocument();
|
|
43
|
+
if (docs && docs.length > 0) {
|
|
44
|
+
docs.forEach((doc) => {
|
|
45
|
+
addAttachment({
|
|
46
|
+
id: `doc-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
|
47
|
+
uri: doc.uri,
|
|
48
|
+
type: "document",
|
|
49
|
+
name: doc.name,
|
|
50
|
+
size: doc.size,
|
|
51
|
+
mimeType: doc.mimeType,
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
} catch (err) {
|
|
56
|
+
console.error("Error picking documents:", err);
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<DropdownMenu.Root>
|
|
62
|
+
<DropdownMenu.Trigger>
|
|
63
|
+
<Button
|
|
64
|
+
variant="outline"
|
|
65
|
+
size="icon"
|
|
66
|
+
className={cn("h-8 w-8 rounded-full border-0", className)}
|
|
67
|
+
>
|
|
68
|
+
<Plus size={iconSize} className="text-muted-foreground" />
|
|
69
|
+
</Button>
|
|
70
|
+
</DropdownMenu.Trigger>
|
|
71
|
+
<DropdownMenu.Content side="top" align="start">
|
|
72
|
+
<DropdownMenu.Item key="photos" onSelect={handleAddPhotos}>
|
|
73
|
+
<DropdownMenu.ItemIcon ios={{ name: "photo" }} />
|
|
74
|
+
<DropdownMenu.ItemTitle>Add photos</DropdownMenu.ItemTitle>
|
|
75
|
+
</DropdownMenu.Item>
|
|
76
|
+
<DropdownMenu.Item key="document" onSelect={handleAddDocument}>
|
|
77
|
+
<DropdownMenu.ItemIcon ios={{ name: "doc" }} />
|
|
78
|
+
<DropdownMenu.ItemTitle>Add document</DropdownMenu.ItemTitle>
|
|
79
|
+
</DropdownMenu.Item>
|
|
80
|
+
</DropdownMenu.Content>
|
|
81
|
+
</DropdownMenu.Root>
|
|
82
|
+
);
|
|
83
|
+
}
|