@aomi-labs/widget-lib 1.0.0 → 1.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.
Files changed (76) hide show
  1. package/SHADCN-FETCH-GUIDE.md +105 -0
  2. package/components.json +10 -0
  3. package/dist/accordion.json +18 -0
  4. package/dist/alert.json +17 -0
  5. package/dist/aomi-frame.json +24 -0
  6. package/dist/assistant-thread-list.json +22 -0
  7. package/dist/assistant-thread.json +27 -0
  8. package/dist/assistant-threadlist-collapsible.json +23 -0
  9. package/dist/assistant-threadlist-sidebar.json +20 -0
  10. package/dist/assistant-tool-fallback.json +20 -0
  11. package/dist/avatar.json +17 -0
  12. package/dist/badge.json +17 -0
  13. package/dist/breadcrumb.json +17 -0
  14. package/dist/button.json +18 -0
  15. package/dist/card.json +15 -0
  16. package/dist/collapsible.json +17 -0
  17. package/dist/command.json +21 -0
  18. package/dist/dialog.json +18 -0
  19. package/dist/drawer.json +17 -0
  20. package/dist/input.json +15 -0
  21. package/dist/label.json +15 -0
  22. package/dist/notification.json +20 -0
  23. package/dist/popover.json +17 -0
  24. package/dist/registry.json +429 -0
  25. package/dist/separator.json +17 -0
  26. package/dist/sheet.json +18 -0
  27. package/dist/sidebar.json +18 -0
  28. package/dist/skeleton.json +15 -0
  29. package/dist/sonner.json +17 -0
  30. package/dist/tooltip.json +17 -0
  31. package/package.json +24 -88
  32. package/scripts/build-registry.js +74 -0
  33. package/src/components/aomi-frame.tsx +128 -0
  34. package/src/components/assistant-ui/attachment.tsx +235 -0
  35. package/src/components/assistant-ui/markdown-text.tsx +228 -0
  36. package/src/components/assistant-ui/thread-list.tsx +106 -0
  37. package/src/components/assistant-ui/thread.tsx +457 -0
  38. package/src/components/assistant-ui/threadlist-sidebar.tsx +71 -0
  39. package/src/components/assistant-ui/tool-fallback.tsx +48 -0
  40. package/src/components/assistant-ui/tooltip-icon-button.tsx +42 -0
  41. package/src/components/test/ThreadContextTest.tsx +204 -0
  42. package/src/components/tools/example-tool/ExampleTool.tsx +102 -0
  43. package/src/components/ui/accordion.tsx +58 -0
  44. package/src/components/ui/alert.tsx +62 -0
  45. package/src/components/ui/avatar.tsx +53 -0
  46. package/src/components/ui/badge.tsx +37 -0
  47. package/src/components/ui/breadcrumb.tsx +109 -0
  48. package/src/components/ui/button.tsx +59 -0
  49. package/src/components/ui/card.tsx +86 -0
  50. package/src/components/ui/collapsible.tsx +12 -0
  51. package/src/components/ui/command.tsx +156 -0
  52. package/src/components/ui/dialog.tsx +143 -0
  53. package/src/components/ui/drawer.tsx +118 -0
  54. package/src/components/ui/input.tsx +21 -0
  55. package/src/components/ui/label.tsx +20 -0
  56. package/src/components/ui/notification.tsx +57 -0
  57. package/src/components/ui/popover.tsx +33 -0
  58. package/src/components/ui/separator.tsx +28 -0
  59. package/src/components/ui/sheet.tsx +139 -0
  60. package/src/components/ui/sidebar.tsx +820 -0
  61. package/src/components/ui/skeleton.tsx +15 -0
  62. package/src/components/ui/sonner.tsx +29 -0
  63. package/src/components/ui/tooltip.tsx +61 -0
  64. package/src/hooks/use-mobile.ts +21 -0
  65. package/src/index.ts +26 -0
  66. package/src/registry.ts +218 -0
  67. package/{dist/styles.css → src/themes/default.css} +21 -3
  68. package/src/themes/tokens.config.ts +39 -0
  69. package/tsconfig.json +19 -0
  70. package/README.md +0 -41
  71. package/dist/index.cjs +0 -3780
  72. package/dist/index.cjs.map +0 -1
  73. package/dist/index.d.cts +0 -302
  74. package/dist/index.d.ts +0 -302
  75. package/dist/index.js +0 -3696
  76. package/dist/index.js.map +0 -1
@@ -0,0 +1,17 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "name": "tooltip",
4
+ "type": "registry:component",
5
+ "description": "A popup that displays information related to an element when the element receives keyboard focus or the mouse hovers over it.",
6
+ "files": [
7
+ {
8
+ "type": "registry:component",
9
+ "path": "components/ui/tooltip.tsx",
10
+ "content": "\"use client\";\n\nimport * as React from \"react\";\nimport * as TooltipPrimitive from \"@radix-ui/react-tooltip\";\n\nimport { cn } from \"@aomi-labs/react\";\n\nfunction TooltipProvider({\n delayDuration = 0,\n ...props\n}: React.ComponentProps<typeof TooltipPrimitive.Provider>) {\n return (\n <TooltipPrimitive.Provider\n data-slot=\"tooltip-provider\"\n delayDuration={delayDuration}\n {...props}\n />\n );\n}\n\nfunction Tooltip({\n ...props\n}: React.ComponentProps<typeof TooltipPrimitive.Root>) {\n return (\n <TooltipProvider>\n <TooltipPrimitive.Root data-slot=\"tooltip\" {...props} />\n </TooltipProvider>\n );\n}\n\nfunction TooltipTrigger({\n ...props\n}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {\n return <TooltipPrimitive.Trigger data-slot=\"tooltip-trigger\" {...props} />;\n}\n\nfunction TooltipContent({\n className,\n sideOffset = 0,\n children,\n ...props\n}: React.ComponentProps<typeof TooltipPrimitive.Content>) {\n return (\n <TooltipPrimitive.Portal>\n <TooltipPrimitive.Content\n data-slot=\"tooltip-content\"\n sideOffset={sideOffset}\n className={cn(\n \"origin-(--radix-tooltip-content-transform-origin) animate-in bg-primary text-primary-foreground fade-in-0 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 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 z-50 w-fit text-balance rounded-md px-3 py-1.5 text-xs\",\n className,\n )}\n {...props}\n >\n {children}\n <TooltipPrimitive.Arrow className=\"bg-primary fill-primary z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]\" />\n </TooltipPrimitive.Content>\n </TooltipPrimitive.Portal>\n );\n}\n\nexport { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };\n"
11
+ }
12
+ ],
13
+ "dependencies": [
14
+ "@radix-ui/react-tooltip"
15
+ ],
16
+ "registryDependencies": []
17
+ }
package/package.json CHANGED
@@ -1,108 +1,44 @@
1
1
  {
2
2
  "name": "@aomi-labs/widget-lib",
3
- "version": "1.0.0",
4
- "description": "AI-powered assistant UI widget library",
3
+ "version": "1.1.0",
4
+ "description": "Shadcn registry for the Aomi widget.",
5
5
  "type": "module",
6
- "main": "./dist/index.cjs",
7
- "module": "./dist/index.js",
8
- "types": "./dist/index.d.ts",
6
+ "main": "./src/index.ts",
9
7
  "exports": {
10
- ".": {
11
- "import": {
12
- "types": "./dist/index.d.ts",
13
- "default": "./dist/index.js"
14
- },
15
- "require": {
16
- "types": "./dist/index.d.cts",
17
- "default": "./dist/index.cjs"
18
- }
19
- },
20
- "./styles.css": "./dist/styles.css"
21
- },
22
- "files": [
23
- "dist"
24
- ],
25
- "prettier": {
26
- "plugins": [
27
- "prettier-plugin-tailwindcss"
28
- ],
29
- "tailwindStylesheet": "app/globals.css"
30
- },
31
- "peerDependencies": {
32
- "@assistant-ui/react": "^0.11.0",
33
- "@assistant-ui/react-markdown": "^0.11.0",
34
- "@radix-ui/react-avatar": "^1.0.0",
35
- "@radix-ui/react-dialog": "^1.0.0",
36
- "@radix-ui/react-separator": "^1.0.0",
37
- "@radix-ui/react-slot": "^1.0.0",
38
- "@radix-ui/react-tooltip": "^1.0.0",
39
- "@tanstack/react-query": "^5.0.0",
40
- "framer-motion": "^12.0.0",
41
- "lucide-react": "^0.500.0",
42
- "motion": "^12.0.0",
43
- "next": ">=14.0.0",
44
- "react": "^18.0.0 || ^19.0.0",
45
- "react-dom": "^18.0.0 || ^19.0.0",
46
- "react-shiki": "^0.9.0",
47
- "remark-gfm": "^4.0.0",
48
- "tailwindcss": "^4.0.0",
49
- "zustand": "^5.0.0"
8
+ ".": "./src/index.ts",
9
+ "./aomi-frame": "./src/components/aomi-frame.tsx",
10
+ "./aomi-frame-collapsible": "./src/components/aomi-frame-collapsible.tsx",
11
+ "./components/ui/*": "./src/components/ui/*.tsx",
12
+ "./components/assistant-ui/*": "./src/components/assistant-ui/*.tsx",
13
+ "./hooks/*": "./src/hooks/*.ts",
14
+ "./styles.css": "./src/themes/default.css",
15
+ "./themes/*.css": "./src/themes/*.css"
50
16
  },
51
17
  "dependencies": {
52
- "class-variance-authority": "^0.7.1",
53
- "clsx": "^2.1.1",
54
- "tailwind-merge": "^3.3.1"
55
- },
56
- "devDependencies": {
57
- "@ai-sdk/openai": "^2.0.46",
58
18
  "@assistant-ui/react": "^0.11.28",
59
- "@assistant-ui/react-ai-sdk": "^1.1.5",
60
19
  "@assistant-ui/react-markdown": "^0.11.1",
61
- "@eslint/eslintrc": "^3",
20
+ "@radix-ui/react-accordion": "^1.2.3",
62
21
  "@radix-ui/react-avatar": "^1.1.10",
22
+ "@radix-ui/react-collapsible": "^1.1.12",
63
23
  "@radix-ui/react-dialog": "^1.1.15",
24
+ "@radix-ui/react-popover": "^1.1.4",
64
25
  "@radix-ui/react-separator": "^1.1.7",
65
26
  "@radix-ui/react-slot": "^1.2.3",
66
27
  "@radix-ui/react-tooltip": "^1.2.8",
67
- "@reown/appkit": "^1.8.14",
68
- "@reown/appkit-adapter-wagmi": "^1.8.14",
69
- "@tailwindcss/postcss": "^4",
70
- "@tanstack/react-query": "^5.90.10",
71
- "@types/node": "^24",
72
- "@types/react": "^19",
73
- "@types/react-dom": "^19",
74
- "ai": "^5.0.65",
75
- "eslint": "^9",
76
- "eslint-config-next": "15.5.4",
77
- "framer-motion": "^12.23.22",
28
+ "class-variance-authority": "^0.7.1",
29
+ "cmdk": "^1.0.0",
78
30
  "lucide-react": "^0.545.0",
79
31
  "motion": "^12.23.22",
80
- "next": "15.5.7",
81
- "prettier": "^3.6.2",
82
- "prettier-plugin-tailwindcss": "^0.6.14",
83
- "@typescript-eslint/eslint-plugin": "^8.18.0",
84
- "@typescript-eslint/parser": "^8.18.0",
85
- "react": "^19.2.0",
86
- "react-dom": "^19.2.0",
87
- "react-shiki": "^0.9.0",
88
32
  "remark-gfm": "^4.0.1",
89
- "tailwindcss": "^4",
90
- "tsup": "^8.5.1",
91
- "tw-animate-css": "^1.4.0",
92
- "typescript": "^5",
93
- "viem": "^2.40.3",
94
- "wagmi": "^2.19.5",
95
- "zustand": "^5.0.8"
33
+ "sonner": "^1.7.4",
34
+ "vaul": "^1.1.2",
35
+ "zustand": "^5.0.8",
36
+ "@aomi-labs/react": "0.2.0"
37
+ },
38
+ "devDependencies": {
39
+ "tsx": "^4.21.0"
96
40
  },
97
41
  "scripts": {
98
- "dev": "next dev --turbopack",
99
- "dev:example:live": "pnpm run build:lib -- --watch & pnpm --filter example dev",
100
- "build": "next build",
101
- "build:lib": "tsup",
102
- "vercel-build": "pnpm run build:lib && pnpm --filter example build",
103
- "start": "next start",
104
- "lint": "eslint .",
105
- "prettier": "prettier --check .",
106
- "prettier:fix": "prettier --write ."
42
+ "build": "tsx scripts/build-registry.js"
107
43
  }
108
44
  }
@@ -0,0 +1,74 @@
1
+ import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ import { registry } from "../src/registry.js";
6
+
7
+ const REGISTRY_NAME = "aomi";
8
+ const REGISTRY_HOMEPAGE = "https://r.aomi.dev";
9
+
10
+ const baseDir = path.dirname(fileURLToPath(import.meta.url));
11
+ const distDir = path.resolve(baseDir, "../dist");
12
+ const srcDir = path.resolve(baseDir, "../src");
13
+
14
+ function buildComponent(entry) {
15
+ const filePath = path.join(srcDir, entry.file);
16
+ const content = readFileSync(filePath, "utf8");
17
+
18
+ const payload = {
19
+ $schema: "https://ui.shadcn.com/schema/registry-item.json",
20
+ name: entry.name,
21
+ type: "registry:component",
22
+ description: entry.description,
23
+ files: [
24
+ {
25
+ type: "registry:component",
26
+ path: entry.file,
27
+ content,
28
+ },
29
+ ],
30
+ dependencies: entry.dependencies ?? [],
31
+ registryDependencies: entry.registryDependencies ?? [],
32
+ };
33
+
34
+ const outPath = path.join(distDir, `${entry.name}.json`);
35
+ writeFileSync(outPath, JSON.stringify(payload, null, 2));
36
+
37
+ // Return item for registry.json (without content)
38
+ return {
39
+ name: entry.name,
40
+ type: "registry:component",
41
+ description: entry.description,
42
+ files: [
43
+ {
44
+ type: "registry:component",
45
+ path: entry.file,
46
+ },
47
+ ],
48
+ dependencies: entry.dependencies ?? [],
49
+ registryDependencies: entry.registryDependencies ?? [],
50
+ };
51
+ }
52
+
53
+ function main() {
54
+ mkdirSync(distDir, { recursive: true });
55
+
56
+ const items = registry.map(buildComponent);
57
+
58
+ // Generate registry.json index
59
+ const registryIndex = {
60
+ $schema: "https://ui.shadcn.com/schema/registry.json",
61
+ name: REGISTRY_NAME,
62
+ homepage: REGISTRY_HOMEPAGE,
63
+ items,
64
+ };
65
+
66
+ writeFileSync(
67
+ path.join(distDir, "registry.json"),
68
+ JSON.stringify(registryIndex, null, 2),
69
+ );
70
+
71
+ console.log(`Wrote ${items.length} component files + registry.json to dist/`);
72
+ }
73
+
74
+ main();
@@ -0,0 +1,128 @@
1
+ "use client";
2
+
3
+ import { type CSSProperties, type ReactNode } from "react";
4
+ import {
5
+ AomiRuntimeProvider,
6
+ cn,
7
+ useAomiRuntime,
8
+ type UserConfig,
9
+ } from "@aomi-labs/react";
10
+ import { Thread } from "@/components/assistant-ui/thread";
11
+ import { ThreadListSidebar } from "@/components/assistant-ui/threadlist-sidebar";
12
+ import {
13
+ SidebarInset,
14
+ SidebarProvider,
15
+ SidebarTrigger,
16
+ } from "@/components/ui/sidebar";
17
+ import { Separator } from "@/components/ui/separator";
18
+ import {
19
+ Breadcrumb,
20
+ BreadcrumbItem,
21
+ BreadcrumbList,
22
+ BreadcrumbSeparator,
23
+ } from "@/components/ui/breadcrumb";
24
+
25
+ // =============================================================================
26
+ // Types
27
+ // =============================================================================
28
+
29
+ type AomiFrameProps = {
30
+ width?: CSSProperties["width"];
31
+ height?: CSSProperties["height"];
32
+ className?: string;
33
+ style?: CSSProperties;
34
+ /** Render prop for wallet footer - receives user state and setter from UserContext */
35
+ walletFooter?: (props: UserConfig) => ReactNode;
36
+ /** Additional content to render inside the frame */
37
+ children?: ReactNode;
38
+ };
39
+
40
+ // =============================================================================
41
+ // AomiFrame Component
42
+ // =============================================================================
43
+
44
+ export const AomiFrame = ({
45
+ width = "100%",
46
+ height = "80vh",
47
+ className,
48
+ style,
49
+ walletFooter,
50
+ children,
51
+ }: AomiFrameProps) => {
52
+ const backendUrl =
53
+ process.env.NEXT_PUBLIC_BACKEND_URL ?? "http://localhost:8080";
54
+
55
+ return (
56
+ <AomiRuntimeProvider backendUrl={backendUrl}>
57
+ <AomiFrameShell
58
+ width={width}
59
+ height={height}
60
+ className={className}
61
+ style={style}
62
+ walletFooter={walletFooter}
63
+ >
64
+ {children}
65
+ </AomiFrameShell>
66
+ </AomiRuntimeProvider>
67
+ );
68
+ };
69
+
70
+ // =============================================================================
71
+ // Internal Shell Component (uses hooks from providers)
72
+ // =============================================================================
73
+
74
+ type AomiFrameShellProps = {
75
+ width: CSSProperties["width"];
76
+ height: CSSProperties["height"];
77
+ className?: string;
78
+ style?: CSSProperties;
79
+ walletFooter?: (props: UserConfig) => ReactNode;
80
+ children?: ReactNode;
81
+ };
82
+
83
+ const AomiFrameShell = ({
84
+ width,
85
+ height,
86
+ className,
87
+ style,
88
+ walletFooter,
89
+ children,
90
+ }: AomiFrameShellProps) => {
91
+ const { user, setUser, currentThreadId, threadViewKey, getThreadMetadata } =
92
+ useAomiRuntime();
93
+ const currentTitle = getThreadMetadata(currentThreadId)?.title ?? "New Chat";
94
+
95
+ const frameStyle: CSSProperties = { width, height, ...style };
96
+
97
+ return (
98
+ <SidebarProvider>
99
+ {children}
100
+ <div
101
+ className={cn(
102
+ "flex h-full w-full overflow-hidden rounded-2xl bg-white shadow-2xl dark:bg-neutral-950",
103
+ className,
104
+ )}
105
+ style={frameStyle}
106
+ >
107
+ <ThreadListSidebar footer={walletFooter?.({ user, setUser })} />
108
+ <SidebarInset className="relative">
109
+ <header className="mt-1 flex h-14 shrink-0 items-center gap-2 border-b px-3">
110
+ <SidebarTrigger />
111
+ <Separator orientation="vertical" className="mr-2 h-4" />
112
+ <Breadcrumb>
113
+ <BreadcrumbList>
114
+ <BreadcrumbItem className="hidden md:block">
115
+ {currentTitle}
116
+ </BreadcrumbItem>
117
+ <BreadcrumbSeparator className="hidden md:block" />
118
+ </BreadcrumbList>
119
+ </Breadcrumb>
120
+ </header>
121
+ <div className="flex-1 overflow-hidden">
122
+ <Thread key={`${currentThreadId}-${threadViewKey}`} />
123
+ </div>
124
+ </SidebarInset>
125
+ </div>
126
+ </SidebarProvider>
127
+ );
128
+ };
@@ -0,0 +1,235 @@
1
+ "use client";
2
+
3
+ import { PropsWithChildren, useEffect, useState, type FC } from "react";
4
+ import Image from "next/image";
5
+ import { XIcon, PlusIcon, FileText } from "lucide-react";
6
+ import {
7
+ AttachmentPrimitive,
8
+ ComposerPrimitive,
9
+ MessagePrimitive,
10
+ useAssistantState,
11
+ useAssistantApi,
12
+ } from "@assistant-ui/react";
13
+ import { useShallow } from "zustand/shallow";
14
+ import {
15
+ Tooltip,
16
+ TooltipContent,
17
+ TooltipTrigger,
18
+ } from "@/components/ui/tooltip";
19
+ import {
20
+ Dialog,
21
+ DialogTitle,
22
+ DialogContent,
23
+ DialogTrigger,
24
+ } from "@/components/ui/dialog";
25
+ import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar";
26
+ import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button";
27
+ import { cn } from "@aomi-labs/react";
28
+
29
+ const useFileSrc = (file: File | undefined) => {
30
+ const [src, setSrc] = useState<string | undefined>(undefined);
31
+
32
+ useEffect(() => {
33
+ if (!file) {
34
+ setSrc(undefined);
35
+ return;
36
+ }
37
+
38
+ const objectUrl = URL.createObjectURL(file);
39
+ setSrc(objectUrl);
40
+
41
+ return () => {
42
+ URL.revokeObjectURL(objectUrl);
43
+ };
44
+ }, [file]);
45
+
46
+ return src;
47
+ };
48
+
49
+ const useAttachmentSrc = () => {
50
+ const { file, src } = useAssistantState(
51
+ useShallow(({ attachment }): { file?: File; src?: string } => {
52
+ if (attachment.type !== "image") return {};
53
+ if (attachment.file) return { file: attachment.file };
54
+ const src = attachment.content?.filter((c) => c.type === "image")[0]
55
+ ?.image;
56
+ if (!src) return {};
57
+ return { src };
58
+ }),
59
+ );
60
+
61
+ return useFileSrc(file) ?? src;
62
+ };
63
+
64
+ type AttachmentPreviewProps = {
65
+ src: string;
66
+ };
67
+
68
+ const AttachmentPreview: FC<AttachmentPreviewProps> = ({ src }) => {
69
+ const [isLoaded, setIsLoaded] = useState(false);
70
+ return (
71
+ <Image
72
+ src={src}
73
+ alt="Image Preview"
74
+ width={1}
75
+ height={1}
76
+ className={
77
+ isLoaded
78
+ ? "aui-attachment-preview-image-loaded block h-auto max-h-[80vh] w-auto max-w-full object-contain"
79
+ : "aui-attachment-preview-image-loading hidden"
80
+ }
81
+ onLoadingComplete={() => setIsLoaded(true)}
82
+ priority={false}
83
+ />
84
+ );
85
+ };
86
+
87
+ const AttachmentPreviewDialog: FC<PropsWithChildren> = ({ children }) => {
88
+ const src = useAttachmentSrc();
89
+
90
+ if (!src) return children;
91
+
92
+ return (
93
+ <Dialog>
94
+ <DialogTrigger
95
+ className="aui-attachment-preview-trigger hover:bg-accent/50 cursor-pointer transition-colors"
96
+ asChild
97
+ >
98
+ {children}
99
+ </DialogTrigger>
100
+ <DialogContent className="aui-attachment-preview-dialog-content [&_svg]:text-background [&>button]:bg-foreground/60 [&>button]:hover:[&_svg]:text-destructive p-2 sm:max-w-3xl [&>button]:rounded-full [&>button]:p-1 [&>button]:opacity-100 [&>button]:!ring-0">
101
+ <DialogTitle className="aui-sr-only sr-only">
102
+ Image Attachment Preview
103
+ </DialogTitle>
104
+ <div className="aui-attachment-preview bg-background relative mx-auto flex max-h-[80dvh] w-full items-center justify-center overflow-hidden">
105
+ <AttachmentPreview src={src} />
106
+ </div>
107
+ </DialogContent>
108
+ </Dialog>
109
+ );
110
+ };
111
+
112
+ const AttachmentThumb: FC = () => {
113
+ const isImage = useAssistantState(
114
+ ({ attachment }) => attachment.type === "image",
115
+ );
116
+ const src = useAttachmentSrc();
117
+
118
+ return (
119
+ <Avatar className="aui-attachment-tile-avatar h-full w-full rounded-none">
120
+ <AvatarImage
121
+ src={src}
122
+ alt="Attachment preview"
123
+ className="aui-attachment-tile-image object-cover"
124
+ />
125
+ <AvatarFallback delayMs={isImage ? 200 : 0}>
126
+ <FileText className="aui-attachment-tile-fallback-icon text-muted-foreground size-8" />
127
+ </AvatarFallback>
128
+ </Avatar>
129
+ );
130
+ };
131
+
132
+ const AttachmentUI: FC = () => {
133
+ const api = useAssistantApi();
134
+ const isComposer = api.attachment.source === "composer";
135
+
136
+ const isImage = useAssistantState(
137
+ ({ attachment }) => attachment.type === "image",
138
+ );
139
+ const typeLabel = useAssistantState(({ attachment }) => {
140
+ const type = attachment.type;
141
+ switch (type) {
142
+ case "image":
143
+ return "Image";
144
+ case "document":
145
+ return "Document";
146
+ case "file":
147
+ return "File";
148
+ default:
149
+ const _exhaustiveCheck: never = type;
150
+ throw new Error(`Unknown attachment type: ${_exhaustiveCheck}`);
151
+ }
152
+ });
153
+
154
+ return (
155
+ <Tooltip>
156
+ <AttachmentPrimitive.Root
157
+ className={cn(
158
+ "aui-attachment-root relative",
159
+ isImage &&
160
+ "aui-attachment-root-composer only:[&>#attachment-tile]:size-24",
161
+ )}
162
+ >
163
+ <AttachmentPreviewDialog>
164
+ <TooltipTrigger asChild>
165
+ <div
166
+ className={cn(
167
+ "aui-attachment-tile bg-muted size-14 cursor-pointer overflow-hidden rounded-[14px] border transition-opacity hover:opacity-75",
168
+ isComposer &&
169
+ "aui-attachment-tile-composer border-foreground/20",
170
+ )}
171
+ role="button"
172
+ id="attachment-tile"
173
+ aria-label={`${typeLabel} attachment`}
174
+ >
175
+ <AttachmentThumb />
176
+ </div>
177
+ </TooltipTrigger>
178
+ </AttachmentPreviewDialog>
179
+ {isComposer && <AttachmentRemove />}
180
+ </AttachmentPrimitive.Root>
181
+ <TooltipContent side="top">
182
+ <AttachmentPrimitive.Name />
183
+ </TooltipContent>
184
+ </Tooltip>
185
+ );
186
+ };
187
+
188
+ const AttachmentRemove: FC = () => {
189
+ return (
190
+ <AttachmentPrimitive.Remove asChild>
191
+ <TooltipIconButton
192
+ tooltip="Remove file"
193
+ className="aui-attachment-tile-remove text-muted-foreground hover:[&_svg]:text-destructive absolute right-1.5 top-1.5 size-3.5 rounded-full bg-white opacity-100 shadow-sm hover:!bg-white [&_svg]:text-black"
194
+ side="top"
195
+ >
196
+ <XIcon className="aui-attachment-remove-icon size-3 dark:stroke-[2.5px]" />
197
+ </TooltipIconButton>
198
+ </AttachmentPrimitive.Remove>
199
+ );
200
+ };
201
+
202
+ export const UserMessageAttachments: FC = () => {
203
+ return (
204
+ <div className="aui-user-message-attachments-end col-span-full col-start-1 row-start-1 flex w-full flex-row justify-end gap-2">
205
+ <MessagePrimitive.Attachments components={{ Attachment: AttachmentUI }} />
206
+ </div>
207
+ );
208
+ };
209
+
210
+ export const ComposerAttachments: FC = () => {
211
+ return (
212
+ <div className="aui-composer-attachments mb-2 flex w-full flex-row items-center gap-2 overflow-x-auto px-1.5 pb-1 pt-0.5 empty:hidden">
213
+ <ComposerPrimitive.Attachments
214
+ components={{ Attachment: AttachmentUI }}
215
+ />
216
+ </div>
217
+ );
218
+ };
219
+
220
+ export const ComposerAddAttachment: FC = () => {
221
+ return (
222
+ <ComposerPrimitive.AddAttachment asChild>
223
+ <TooltipIconButton
224
+ tooltip="Add Attachment"
225
+ side="bottom"
226
+ variant="ghost"
227
+ size="icon"
228
+ className="aui-composer-add-attachment hover:bg-muted-foreground/15 dark:border-muted-foreground/15 dark:hover:bg-muted-foreground/30 size-[34px] rounded-full p-1 text-xs font-semibold"
229
+ aria-label="Add Attachment"
230
+ >
231
+ <PlusIcon className="aui-attachment-add-icon size-5 stroke-[1.5px]" />
232
+ </TooltipIconButton>
233
+ </ComposerPrimitive.AddAttachment>
234
+ );
235
+ };