@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,383 +0,0 @@
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 type { LucideIcon } from "lucide-react-native";
8
- // Per-icon subpaths, never the `lucide-react-native` barrel: the barrel
9
- // statically re-exports all ~1500 icons, and Metro has no tree shaking, so a
10
- // single named import pulls ~1.3 MB of unused icon modules into every consumer
11
- // bundle.
12
- import Star from "lucide-react-native/icons/star";
13
- import Pencil from "lucide-react-native/icons/pencil";
14
- import Trash2 from "lucide-react-native/icons/trash-2";
15
- import Share2 from "lucide-react-native/icons/share-2";
16
- import Download from "lucide-react-native/icons/download";
17
- import Settings from "lucide-react-native/icons/settings";
18
- import CircleQuestionMark from "lucide-react-native/icons/circle-question-mark";
19
- import Image from "lucide-react-native/icons/image";
20
- import FileText from "lucide-react-native/icons/file-text";
21
- import Search from "lucide-react-native/icons/search";
22
- import ShoppingBag from "lucide-react-native/icons/shopping-bag";
23
- import Ellipsis from "lucide-react-native/icons/ellipsis";
24
- import ExternalLink from "lucide-react-native/icons/external-link";
25
- import BookOpen from "lucide-react-native/icons/book-open";
26
- import Globe from "lucide-react-native/icons/globe";
27
- import PenTool from "lucide-react-native/icons/pen-tool";
28
- import Sparkles from "lucide-react-native/icons/sparkles";
29
- import User from "lucide-react-native/icons/user";
30
- import CreditCard from "lucide-react-native/icons/credit-card";
31
- import Bell from "lucide-react-native/icons/bell";
32
- import LogOut from "lucide-react-native/icons/log-out";
33
- import Folder from "lucide-react-native/icons/folder";
34
- import Check from "lucide-react-native/icons/check";
35
- import Brain from "lucide-react-native/icons/brain";
36
- import Ghost from "lucide-react-native/icons/ghost";
37
- import Bot from "lucide-react-native/icons/bot";
38
- import Bookmark from "lucide-react-native/icons/bookmark";
39
- import TriangleAlert from "lucide-react-native/icons/triangle-alert";
40
- import Pin from "lucide-react-native/icons/pin";
41
-
42
- // Map iOS SF Symbol names to Lucide icons for web rendering
43
- const SF_SYMBOL_MAP: Record<string, LucideIcon> = {
44
- "star": Star, "star.fill": Star, "pencil": Pencil, "trash": Trash2,
45
- "square.and.arrow.up": Share2, "arrow.down.doc": Download,
46
- "gearshape": Settings, "questionmark.circle": CircleQuestionMark,
47
- "photo": Image, "doc": FileText, "magnifyingglass": Search,
48
- "bag": ShoppingBag, "ellipsis": Ellipsis, "link": ExternalLink,
49
- "book": BookOpen, "globe": Globe, "pencil.tip": PenTool,
50
- "sparkle": Sparkles, "person.circle": User, "creditcard": CreditCard,
51
- "bell": Bell, "rectangle.portrait.and.arrow.right": LogOut,
52
- "folder": Folder, "checkmark": Check, "brain": Brain,
53
- "eye.slash": Ghost, "cpu": Bot,
54
- "bookmark": Bookmark, "bookmark.fill": Bookmark,
55
- "exclamationmark.triangle": TriangleAlert,
56
- "pin": Pin, "pin.fill": Pin,
57
- };
58
-
59
-
60
- // Singleton registry: only one dropdown menu open at a time.
61
- const openMenus = new Set<() => void>();
62
-
63
- function DropdownMenu({
64
- open: controlledOpen,
65
- onOpenChange,
66
- defaultOpen,
67
- ...props
68
- }: React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Root>) {
69
- const [internalOpen, setInternalOpen] = React.useState(defaultOpen ?? false);
70
- const open = controlledOpen ?? internalOpen;
71
-
72
- const onOpenChangeRef = React.useRef(onOpenChange);
73
- onOpenChangeRef.current = onOpenChange;
74
-
75
- const closeRef = React.useRef<() => void>(undefined);
76
- if (!closeRef.current) {
77
- closeRef.current = () => {
78
- setInternalOpen(false);
79
- onOpenChangeRef.current?.(false);
80
- };
81
- }
82
-
83
- React.useEffect(() => {
84
- const cb = closeRef.current!;
85
- openMenus.add(cb);
86
- return () => { openMenus.delete(cb); };
87
- }, []);
88
-
89
- const handleOpenChange = (next: boolean) => {
90
- if (next) {
91
- openMenus.forEach((cb) => {
92
- if (cb !== closeRef.current) cb();
93
- });
94
- }
95
- setInternalOpen(next);
96
- onOpenChange?.(next);
97
- };
98
-
99
- return (
100
- <DropdownMenuPrimitive.Root
101
- modal={false}
102
- open={open}
103
- onOpenChange={handleOpenChange}
104
- {...props}
105
- />
106
- );
107
- }
108
-
109
- const DropdownMenuTrigger = React.forwardRef<
110
- React.ElementRef<typeof DropdownMenuPrimitive.Trigger>,
111
- React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Trigger>
112
- >((props, ref) => (
113
- <DropdownMenuPrimitive.Trigger ref={ref} asChild {...props} />
114
- ));
115
- DropdownMenuTrigger.displayName = "DropdownMenuTrigger";
116
-
117
- const DropdownMenuSub = DropdownMenuPrimitive.Sub;
118
-
119
- const DropdownMenuGroup = React.forwardRef<
120
- React.ElementRef<typeof DropdownMenuPrimitive.Group>,
121
- React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Group> & {
122
- horizontal?: boolean;
123
- }
124
- >(({ className, horizontal, children, ...props }, ref) => (
125
- <DropdownMenuPrimitive.Group
126
- ref={ref}
127
- className={cn(
128
- horizontal && "flex flex-row space-x-2 justify-between",
129
- className
130
- )}
131
- {...props}
132
- >
133
- {children}
134
- </DropdownMenuPrimitive.Group>
135
- ));
136
- DropdownMenuGroup.displayName = DropdownMenuPrimitive.Group.displayName;
137
-
138
- const DropdownMenuSubTrigger = React.forwardRef<
139
- React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
140
- React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
141
- inset?: boolean;
142
- }
143
- >(({ className, inset, children, ...props }, ref) => (
144
- <DropdownMenuPrimitive.SubTrigger
145
- ref={ref}
146
- className={cn(
147
- "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",
148
- inset && "pl-8",
149
- className
150
- )}
151
- {...props}
152
- >
153
- {children}
154
- <ChevronRightIcon className="ml-auto h-4 w-4" />
155
- </DropdownMenuPrimitive.SubTrigger>
156
- ));
157
- DropdownMenuSubTrigger.displayName =
158
- DropdownMenuPrimitive.SubTrigger.displayName;
159
-
160
- const DropdownMenuSubContent = React.forwardRef<
161
- React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
162
- React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
163
- >(({ className, style, ...props }, ref) => (
164
- <DropdownMenuPrimitive.Portal>
165
- <DropdownMenuPrimitive.SubContent
166
- ref={ref}
167
- className={cn(
168
- "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",
169
- className
170
- )}
171
- style={{
172
- boxShadow: '0 4px 6px -1px rgba(0,0,0,0.1), 0 2px 4px -2px rgba(0,0,0,0.1)',
173
- border: '1px solid var(--border)',
174
- ...style,
175
- }}
176
- {...props}
177
- />
178
- </DropdownMenuPrimitive.Portal>
179
- ));
180
- DropdownMenuSubContent.displayName =
181
- DropdownMenuPrimitive.SubContent.displayName;
182
-
183
- const DropdownMenuContent = React.forwardRef<
184
- React.ElementRef<typeof DropdownMenuPrimitive.Content>,
185
- React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
186
- >(({ className, sideOffset = 4, align = "start", style, ...props }, ref) => (
187
- <DropdownMenuPrimitive.Portal>
188
- <DropdownMenuPrimitive.Content
189
- ref={ref}
190
- sideOffset={sideOffset}
191
- align={align}
192
- className={cn(
193
- "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",
194
- "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",
195
- className
196
- )}
197
- style={{
198
- boxShadow: '0 4px 6px -1px rgba(0,0,0,0.1), 0 2px 4px -2px rgba(0,0,0,0.1)',
199
- border: '1px solid var(--border)',
200
- ...style,
201
- }}
202
- {...props}
203
- />
204
- </DropdownMenuPrimitive.Portal>
205
- ));
206
- DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
207
-
208
- const DropdownMenuItem = React.forwardRef<
209
- React.ElementRef<typeof DropdownMenuPrimitive.Item>,
210
- React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
211
- inset?: boolean;
212
- destructive?: boolean;
213
- shouldDismissMenuOnSelect?: boolean;
214
- }
215
- >(
216
- (
217
- {
218
- className,
219
- inset,
220
- destructive,
221
- shouldDismissMenuOnSelect,
222
- onSelect,
223
- ...props
224
- },
225
- ref
226
- ) => (
227
- <DropdownMenuPrimitive.Item
228
- ref={ref}
229
- onSelect={(e: Event) => {
230
- onSelect?.(e);
231
- if (shouldDismissMenuOnSelect === false) {
232
- e.preventDefault();
233
- }
234
- }}
235
- className={cn(
236
- "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",
237
- inset && "pl-8",
238
- destructive &&
239
- "text-destructive focus:text-destructive focus:bg-destructive/10 dark:focus:bg-destructive/20 [&_svg]:!text-destructive",
240
- className
241
- )}
242
- {...props}
243
- />
244
- )
245
- );
246
- DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
247
-
248
- const DropdownMenuCheckboxItem = React.forwardRef<
249
- React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
250
- Omit<
251
- React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>,
252
- "onSelect" | "checked"
253
- > & {
254
- value: "mixed" | "on" | "off" | boolean;
255
- onValueChange?: (
256
- state: "mixed" | "on" | "off",
257
- prevState: "mixed" | "on" | "off"
258
- ) => void;
259
- shouldDismissMenuOnSelect?: boolean;
260
- }
261
- >(
262
- (
263
- {
264
- className,
265
- value,
266
- children,
267
- shouldDismissMenuOnSelect,
268
- onValueChange,
269
- ...props
270
- },
271
- ref
272
- ) => (
273
- <DropdownMenuPrimitive.CheckboxItem
274
- ref={ref}
275
- onSelect={(e: Event) => {
276
- const current =
277
- value === true ? "on" : value === false ? "off" : value;
278
- const next = current === "on" ? "off" : "on";
279
- onValueChange?.(next, current);
280
- if (shouldDismissMenuOnSelect === false) {
281
- e.preventDefault();
282
- }
283
- }}
284
- className={cn(
285
- "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",
286
- className
287
- )}
288
- checked={typeof value === "boolean" ? value : value !== "off"}
289
- {...props}
290
- >
291
- <span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
292
- <DropdownMenuPrimitive.ItemIndicator>
293
- <CheckIcon className="h-4 w-4" />
294
- </DropdownMenuPrimitive.ItemIndicator>
295
- </span>
296
- {children}
297
- </DropdownMenuPrimitive.CheckboxItem>
298
- )
299
- );
300
- DropdownMenuCheckboxItem.displayName =
301
- DropdownMenuPrimitive.CheckboxItem.displayName;
302
-
303
- const DropdownMenuLabel = React.forwardRef<
304
- React.ElementRef<typeof DropdownMenuPrimitive.Label>,
305
- React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
306
- inset?: boolean;
307
- }
308
- >(({ className, inset, ...props }, ref) => (
309
- <DropdownMenuPrimitive.Label
310
- ref={ref}
311
- className={cn(
312
- "text-muted-foreground px-1.5 py-1 text-xs font-medium",
313
- inset && "pl-8",
314
- className
315
- )}
316
- {...props}
317
- />
318
- ));
319
- DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
320
-
321
- const DropdownMenuSeparator = React.forwardRef<
322
- React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
323
- React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
324
- >(({ className, ...props }, ref) => (
325
- <DropdownMenuPrimitive.Separator
326
- ref={ref}
327
- className={cn("-mx-1 my-1 h-px bg-border", className)}
328
- {...props}
329
- />
330
- ));
331
- DropdownMenuSeparator.displayName =
332
- DropdownMenuPrimitive.Separator.displayName;
333
-
334
- export const ItemIcon = React.forwardRef<
335
- HTMLSpanElement,
336
- React.HTMLProps<HTMLSpanElement> & { ios?: { name?: string; [key: string]: unknown }; androidIconName?: string }
337
- >(({ className, ios, androidIconName, children, ...props }, ref) => {
338
- const IconComponent = ios?.name ? SF_SYMBOL_MAP[ios.name] : null;
339
- return (
340
- <span ref={ref} className={cn("inline-flex h-4 w-4 items-center justify-center shrink-0 text-muted-foreground", className)} {...props}>
341
- {IconComponent ? <IconComponent size={16} /> : children}
342
- </span>
343
- );
344
- });
345
- ItemIcon.displayName = "ItemIcon";
346
-
347
- export const ItemTitle = React.forwardRef<
348
- HTMLSpanElement,
349
- React.HTMLProps<HTMLSpanElement>
350
- >((props, ref) => {
351
- return <span ref={ref} {...props} />;
352
- });
353
- ItemTitle.displayName = "ItemTitle";
354
-
355
- export const ItemSubtitle = React.forwardRef<
356
- HTMLSpanElement,
357
- React.HTMLProps<HTMLSpanElement>
358
- >(({ className, ...props }, ref) => {
359
- return (
360
- <span
361
- ref={ref}
362
- className={cn("block text-xs text-muted-foreground mt-0.5", className)}
363
- {...props}
364
- />
365
- );
366
- });
367
- ItemSubtitle.displayName = "ItemSubtitle";
368
-
369
- export { ItemImage, ItemIndicator, Arrow } from "zeego/dropdown-menu";
370
-
371
- export {
372
- DropdownMenu as Root,
373
- DropdownMenuTrigger as Trigger,
374
- DropdownMenuContent as Content,
375
- DropdownMenuItem as Item,
376
- DropdownMenuCheckboxItem as CheckboxItem,
377
- DropdownMenuLabel as Label,
378
- DropdownMenuSeparator as Separator,
379
- DropdownMenuGroup as Group,
380
- DropdownMenuSub as Sub,
381
- DropdownMenuSubContent as SubContent,
382
- DropdownMenuSubTrigger as SubTrigger,
383
- };
@@ -1,28 +0,0 @@
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
- }
@@ -1,83 +0,0 @@
1
- import React from "react";
2
- import Plus from "lucide-react-native/icons/plus";
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
- }
@@ -1,181 +0,0 @@
1
- import React from "react";
2
- import { View, Pressable, ScrollView, ActivityIndicator } from "react-native";
3
- import { Image } from "expo-image";
4
- import FileText from "lucide-react-native/icons/file-text";
5
- import FileSpreadsheet from "lucide-react-native/icons/file-spreadsheet";
6
- import FileCode from "lucide-react-native/icons/file-code";
7
- import FileArchive from "lucide-react-native/icons/file-archive";
8
- import FileHeadphone from "lucide-react-native/icons/file-headphone";
9
- import File from "lucide-react-native/icons/file";
10
- import X from "lucide-react-native/icons/x";
11
- import { Text } from "../text";
12
- import { formatFileSize } from "../../../lib/utils";
13
- import { usePromptInput, type Attachment } from "./context";
14
-
15
- function getDocumentIcon(mimeType: string, name: string) {
16
- const ext = name.split(".").pop()?.toLowerCase() || "";
17
-
18
- if (mimeType === "application/pdf" || ext === "pdf")
19
- return { Icon: FileText, color: "#EF4444", bgColor: "#EF444418" };
20
- if (mimeType.includes("word") || ["doc", "docx"].includes(ext))
21
- return { Icon: FileText, color: "#3B82F6", bgColor: "#3B82F618" };
22
- if (
23
- mimeType.includes("spreadsheet") ||
24
- mimeType.includes("excel") ||
25
- ["xls", "xlsx", "csv"].includes(ext)
26
- )
27
- return {
28
- Icon: FileSpreadsheet,
29
- color: "#22C55E",
30
- bgColor: "#22C55E18",
31
- };
32
- if (
33
- [
34
- "js", "ts", "tsx", "jsx", "py", "rb", "go", "rs", "java", "c", "cpp",
35
- "h", "json", "xml", "yaml", "yml", "html", "css", "scss", "sh", "sql",
36
- ].includes(ext)
37
- )
38
- return { Icon: FileCode, color: "#8B5CF6", bgColor: "#8B5CF618" };
39
- if (
40
- mimeType.includes("zip") ||
41
- mimeType.includes("archive") ||
42
- ["zip", "rar", "tar", "gz", "7z"].includes(ext)
43
- )
44
- return { Icon: FileArchive, color: "#EAB308", bgColor: "#EAB30818" };
45
- if (
46
- mimeType.startsWith("audio/") ||
47
- ["mp3", "wav", "ogg", "flac", "aac"].includes(ext)
48
- )
49
- return { Icon: FileHeadphone, color: "#EC4899", bgColor: "#EC489918" };
50
- if (
51
- mimeType === "text/plain" ||
52
- ["txt", "md", "rtf"].includes(ext)
53
- )
54
- return { Icon: FileText, color: "#6B7280", bgColor: "#6B728018" };
55
- return { Icon: File, color: "#9CA3AF", bgColor: "#9CA3AF18" };
56
- }
57
-
58
- function truncateFilename(name: string, maxLength = 20): string {
59
- if (name.length <= maxLength) return name;
60
- const lastDot = name.lastIndexOf(".");
61
- if (lastDot < 0) return name.slice(0, maxLength - 3) + "...";
62
- const ext = name.slice(lastDot);
63
- const base = name.slice(0, lastDot);
64
- const available = maxLength - ext.length - 3;
65
- if (available <= 0) return name.slice(0, maxLength - 3) + "...";
66
- return base.slice(0, available) + "..." + ext;
67
- }
68
-
69
- function AttachmentItem({
70
- attachment,
71
- onRemove,
72
- }: {
73
- attachment: Attachment;
74
- onRemove: () => void;
75
- }) {
76
- if (attachment.type === "image") {
77
- return (
78
- <View
79
- className="relative rounded-2xl overflow-hidden bg-muted border border-border"
80
- style={{ width: 120, height: 120 }}
81
- >
82
- {!attachment.isLoading && attachment.uri ? (
83
- <Image
84
- source={{ uri: attachment.uri }}
85
- className="w-full h-full"
86
- contentFit="cover"
87
- />
88
- ) : (
89
- <View className="absolute inset-0 items-center justify-center bg-muted">
90
- <ActivityIndicator size="small" />
91
- </View>
92
- )}
93
- {attachment.name && !attachment.isLoading && (
94
- <View
95
- className="absolute bottom-0 left-0 right-0 px-2 py-1"
96
- style={{ backgroundColor: "rgba(0,0,0,0.5)" }}
97
- >
98
- <Text className="text-[11px] text-white" numberOfLines={1}>
99
- {truncateFilename(attachment.name)}
100
- </Text>
101
- </View>
102
- )}
103
- <Pressable
104
- onPress={onRemove}
105
- className="absolute top-1.5 right-1.5 w-6 h-6 rounded-full items-center justify-center active:opacity-70"
106
- style={{ backgroundColor: "rgba(0,0,0,0.6)" }}
107
- accessibilityRole="button"
108
- accessibilityLabel={`Remove ${attachment.name || "image"}`}
109
- >
110
- <X size={14} color="white" />
111
- </Pressable>
112
- </View>
113
- );
114
- }
115
-
116
- const { Icon, color, bgColor } = getDocumentIcon(
117
- attachment.mimeType,
118
- attachment.name
119
- );
120
-
121
- return (
122
- <View
123
- className="relative rounded-2xl border border-border overflow-hidden bg-muted/30"
124
- style={{ width: 180, height: 80 }}
125
- >
126
- <View className="flex-1 flex-row items-center px-3 gap-3">
127
- <View
128
- className="w-10 h-10 rounded-xl items-center justify-center"
129
- style={{ backgroundColor: bgColor }}
130
- >
131
- <Icon size={20} color={color} />
132
- </View>
133
- <View className="flex-1 pr-4">
134
- <Text
135
- className="text-xs font-medium text-foreground"
136
- numberOfLines={1}
137
- >
138
- {truncateFilename(attachment.name)}
139
- </Text>
140
- {attachment.size > 0 && (
141
- <Text className="text-[11px] text-muted-foreground mt-0.5">
142
- {formatFileSize(attachment.size)}
143
- </Text>
144
- )}
145
- </View>
146
- </View>
147
- <Pressable
148
- onPress={onRemove}
149
- 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"
150
- accessibilityRole="button"
151
- accessibilityLabel={`Remove ${attachment.name || "document"}`}
152
- >
153
- <X size={12} className="text-foreground" />
154
- </Pressable>
155
- </View>
156
- );
157
- }
158
-
159
- export function PromptInputAttachments() {
160
- const { attachments, removeAttachment } = usePromptInput();
161
-
162
- if (attachments.length === 0) return null;
163
-
164
- return (
165
- <ScrollView
166
- horizontal
167
- showsHorizontalScrollIndicator={false}
168
- className="mb-2 pt-2 flex-none"
169
- contentContainerClassName="gap-2.5 px-3"
170
- >
171
- {attachments.map((attachment) => (
172
- <View key={attachment.id} className="relative">
173
- <AttachmentItem
174
- attachment={attachment}
175
- onRemove={() => removeAttachment(attachment.id)}
176
- />
177
- </View>
178
- ))}
179
- </ScrollView>
180
- );
181
- }