@alia.onl/sdk 2.0.0 → 3.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/package.json +25 -3
- package/src/components/AliaChatContent.tsx +356 -0
- package/src/components/AliaChatInput.tsx +173 -26
- package/src/components/AliaChatMessageList.tsx +459 -168
- package/src/components/AliaChatScreen.tsx +140 -0
- package/src/components/AliaChatSheet.tsx +41 -44
- package/src/components/AliaWelcomeMessage.tsx +75 -0
- package/src/components/Markdown.tsx +58 -114
- package/src/components/PlanPreviewCard.tsx +76 -0
- package/src/components/ResearchProgressCard.tsx +159 -0
- 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 +18 -8
- package/src/hooks/useDocumentPicker.ts +37 -0
- package/src/hooks/useImagePicker.ts +38 -0
- package/src/hooks/useSpeechToText.ts +25 -9
- package/src/index.ts +41 -3
- package/src/lib/content-utils.ts +23 -0
- package/src/lib/keyboard.native.tsx +6 -0
- package/src/lib/keyboard.tsx +25 -0
- package/src/lib/tool-registry.ts +55 -0
- package/src/lib/utils.ts +14 -0
- package/src/optional-deps.d.ts +17 -0
- package/src/types.ts +27 -10
- package/src/components/AliaChatSuggestions.tsx +0 -70
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { View, Pressable, ScrollView, ActivityIndicator } from "react-native";
|
|
3
|
+
import { Image } from "expo-image";
|
|
4
|
+
import {
|
|
5
|
+
FileText,
|
|
6
|
+
FileSpreadsheet,
|
|
7
|
+
FileCode,
|
|
8
|
+
FileArchive,
|
|
9
|
+
FileAudio,
|
|
10
|
+
File,
|
|
11
|
+
X,
|
|
12
|
+
} from "lucide-react-native";
|
|
13
|
+
import { Text } from "../text";
|
|
14
|
+
import { formatFileSize } from "../../../lib/utils";
|
|
15
|
+
import { usePromptInput, type Attachment } from "./context";
|
|
16
|
+
|
|
17
|
+
function getDocumentIcon(mimeType: string, name: string) {
|
|
18
|
+
const ext = name.split(".").pop()?.toLowerCase() || "";
|
|
19
|
+
|
|
20
|
+
if (mimeType === "application/pdf" || ext === "pdf")
|
|
21
|
+
return { Icon: FileText, color: "#EF4444", bgColor: "#EF444418" };
|
|
22
|
+
if (mimeType.includes("word") || ["doc", "docx"].includes(ext))
|
|
23
|
+
return { Icon: FileText, color: "#3B82F6", bgColor: "#3B82F618" };
|
|
24
|
+
if (
|
|
25
|
+
mimeType.includes("spreadsheet") ||
|
|
26
|
+
mimeType.includes("excel") ||
|
|
27
|
+
["xls", "xlsx", "csv"].includes(ext)
|
|
28
|
+
)
|
|
29
|
+
return {
|
|
30
|
+
Icon: FileSpreadsheet,
|
|
31
|
+
color: "#22C55E",
|
|
32
|
+
bgColor: "#22C55E18",
|
|
33
|
+
};
|
|
34
|
+
if (
|
|
35
|
+
[
|
|
36
|
+
"js", "ts", "tsx", "jsx", "py", "rb", "go", "rs", "java", "c", "cpp",
|
|
37
|
+
"h", "json", "xml", "yaml", "yml", "html", "css", "scss", "sh", "sql",
|
|
38
|
+
].includes(ext)
|
|
39
|
+
)
|
|
40
|
+
return { Icon: FileCode, color: "#8B5CF6", bgColor: "#8B5CF618" };
|
|
41
|
+
if (
|
|
42
|
+
mimeType.includes("zip") ||
|
|
43
|
+
mimeType.includes("archive") ||
|
|
44
|
+
["zip", "rar", "tar", "gz", "7z"].includes(ext)
|
|
45
|
+
)
|
|
46
|
+
return { Icon: FileArchive, color: "#EAB308", bgColor: "#EAB30818" };
|
|
47
|
+
if (
|
|
48
|
+
mimeType.startsWith("audio/") ||
|
|
49
|
+
["mp3", "wav", "ogg", "flac", "aac"].includes(ext)
|
|
50
|
+
)
|
|
51
|
+
return { Icon: FileAudio, color: "#EC4899", bgColor: "#EC489918" };
|
|
52
|
+
if (
|
|
53
|
+
mimeType === "text/plain" ||
|
|
54
|
+
["txt", "md", "rtf"].includes(ext)
|
|
55
|
+
)
|
|
56
|
+
return { Icon: FileText, color: "#6B7280", bgColor: "#6B728018" };
|
|
57
|
+
return { Icon: File, color: "#9CA3AF", bgColor: "#9CA3AF18" };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function truncateFilename(name: string, maxLength = 20): string {
|
|
61
|
+
if (name.length <= maxLength) return name;
|
|
62
|
+
const lastDot = name.lastIndexOf(".");
|
|
63
|
+
if (lastDot < 0) return name.slice(0, maxLength - 3) + "...";
|
|
64
|
+
const ext = name.slice(lastDot);
|
|
65
|
+
const base = name.slice(0, lastDot);
|
|
66
|
+
const available = maxLength - ext.length - 3;
|
|
67
|
+
if (available <= 0) return name.slice(0, maxLength - 3) + "...";
|
|
68
|
+
return base.slice(0, available) + "..." + ext;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function AttachmentItem({
|
|
72
|
+
attachment,
|
|
73
|
+
onRemove,
|
|
74
|
+
}: {
|
|
75
|
+
attachment: Attachment;
|
|
76
|
+
onRemove: () => void;
|
|
77
|
+
}) {
|
|
78
|
+
if (attachment.type === "image") {
|
|
79
|
+
return (
|
|
80
|
+
<View
|
|
81
|
+
className="relative rounded-2xl overflow-hidden bg-muted border border-border"
|
|
82
|
+
style={{ width: 120, height: 120 }}
|
|
83
|
+
>
|
|
84
|
+
{!attachment.isLoading && attachment.uri ? (
|
|
85
|
+
<Image
|
|
86
|
+
source={{ uri: attachment.uri }}
|
|
87
|
+
className="w-full h-full"
|
|
88
|
+
contentFit="cover"
|
|
89
|
+
/>
|
|
90
|
+
) : (
|
|
91
|
+
<View className="absolute inset-0 items-center justify-center bg-muted">
|
|
92
|
+
<ActivityIndicator size="small" />
|
|
93
|
+
</View>
|
|
94
|
+
)}
|
|
95
|
+
{attachment.name && !attachment.isLoading && (
|
|
96
|
+
<View
|
|
97
|
+
className="absolute bottom-0 left-0 right-0 px-2 py-1"
|
|
98
|
+
style={{ backgroundColor: "rgba(0,0,0,0.5)" }}
|
|
99
|
+
>
|
|
100
|
+
<Text className="text-[11px] text-white" numberOfLines={1}>
|
|
101
|
+
{truncateFilename(attachment.name)}
|
|
102
|
+
</Text>
|
|
103
|
+
</View>
|
|
104
|
+
)}
|
|
105
|
+
<Pressable
|
|
106
|
+
onPress={onRemove}
|
|
107
|
+
className="absolute top-1.5 right-1.5 w-6 h-6 rounded-full items-center justify-center active:opacity-70"
|
|
108
|
+
style={{ backgroundColor: "rgba(0,0,0,0.6)" }}
|
|
109
|
+
accessibilityRole="button"
|
|
110
|
+
accessibilityLabel={`Remove ${attachment.name || "image"}`}
|
|
111
|
+
>
|
|
112
|
+
<X size={14} color="white" />
|
|
113
|
+
</Pressable>
|
|
114
|
+
</View>
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const { Icon, color, bgColor } = getDocumentIcon(
|
|
119
|
+
attachment.mimeType,
|
|
120
|
+
attachment.name
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
return (
|
|
124
|
+
<View
|
|
125
|
+
className="relative rounded-2xl border border-border overflow-hidden bg-muted/30"
|
|
126
|
+
style={{ width: 180, height: 80 }}
|
|
127
|
+
>
|
|
128
|
+
<View className="flex-1 flex-row items-center px-3 gap-3">
|
|
129
|
+
<View
|
|
130
|
+
className="w-10 h-10 rounded-xl items-center justify-center"
|
|
131
|
+
style={{ backgroundColor: bgColor }}
|
|
132
|
+
>
|
|
133
|
+
<Icon size={20} color={color} />
|
|
134
|
+
</View>
|
|
135
|
+
<View className="flex-1 pr-4">
|
|
136
|
+
<Text
|
|
137
|
+
className="text-xs font-medium text-foreground"
|
|
138
|
+
numberOfLines={1}
|
|
139
|
+
>
|
|
140
|
+
{truncateFilename(attachment.name)}
|
|
141
|
+
</Text>
|
|
142
|
+
{attachment.size > 0 && (
|
|
143
|
+
<Text className="text-[11px] text-muted-foreground mt-0.5">
|
|
144
|
+
{formatFileSize(attachment.size)}
|
|
145
|
+
</Text>
|
|
146
|
+
)}
|
|
147
|
+
</View>
|
|
148
|
+
</View>
|
|
149
|
+
<Pressable
|
|
150
|
+
onPress={onRemove}
|
|
151
|
+
className="absolute top-1.5 right-1.5 w-5 h-5 rounded-full bg-muted items-center justify-center active:opacity-70 border border-border"
|
|
152
|
+
accessibilityRole="button"
|
|
153
|
+
accessibilityLabel={`Remove ${attachment.name || "document"}`}
|
|
154
|
+
>
|
|
155
|
+
<X size={12} className="text-foreground" />
|
|
156
|
+
</Pressable>
|
|
157
|
+
</View>
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function PromptInputAttachments() {
|
|
162
|
+
const { attachments, removeAttachment } = usePromptInput();
|
|
163
|
+
|
|
164
|
+
if (attachments.length === 0) return null;
|
|
165
|
+
|
|
166
|
+
return (
|
|
167
|
+
<ScrollView
|
|
168
|
+
horizontal
|
|
169
|
+
showsHorizontalScrollIndicator={false}
|
|
170
|
+
className="mb-2 pt-2 flex-none"
|
|
171
|
+
contentContainerClassName="gap-2.5 px-3"
|
|
172
|
+
>
|
|
173
|
+
{attachments.map((attachment) => (
|
|
174
|
+
<View key={attachment.id} className="relative">
|
|
175
|
+
<AttachmentItem
|
|
176
|
+
attachment={attachment}
|
|
177
|
+
onRemove={() => removeAttachment(attachment.id)}
|
|
178
|
+
/>
|
|
179
|
+
</View>
|
|
180
|
+
))}
|
|
181
|
+
</ScrollView>
|
|
182
|
+
);
|
|
183
|
+
}
|