@aomi-labs/widget-lib 1.0.0 → 1.1.1

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/dist/accordion.json +18 -0
  2. package/dist/alert.json +17 -0
  3. package/dist/aomi-frame.json +24 -0
  4. package/dist/assistant-thread-list.json +22 -0
  5. package/dist/assistant-thread.json +27 -0
  6. package/dist/assistant-threadlist-sidebar.json +20 -0
  7. package/dist/assistant-tool-fallback.json +20 -0
  8. package/dist/avatar.json +17 -0
  9. package/dist/badge.json +17 -0
  10. package/dist/breadcrumb.json +17 -0
  11. package/dist/button.json +18 -0
  12. package/dist/card.json +15 -0
  13. package/dist/collapsible.json +17 -0
  14. package/dist/command.json +21 -0
  15. package/dist/dialog.json +18 -0
  16. package/dist/drawer.json +17 -0
  17. package/dist/input.json +15 -0
  18. package/dist/label.json +15 -0
  19. package/dist/notification.json +20 -0
  20. package/dist/popover.json +17 -0
  21. package/dist/registry.json +429 -0
  22. package/dist/separator.json +17 -0
  23. package/dist/sheet.json +18 -0
  24. package/dist/sidebar.json +18 -0
  25. package/dist/skeleton.json +15 -0
  26. package/dist/sonner.json +17 -0
  27. package/dist/tooltip.json +17 -0
  28. package/package.json +27 -85
  29. package/src/components/aomi-frame.tsx +221 -0
  30. package/src/components/assistant-ui/attachment.tsx +235 -0
  31. package/src/components/assistant-ui/markdown-text.tsx +228 -0
  32. package/src/components/assistant-ui/thread-list.tsx +106 -0
  33. package/src/components/assistant-ui/thread.tsx +476 -0
  34. package/src/components/assistant-ui/threadlist-sidebar.tsx +66 -0
  35. package/src/components/assistant-ui/tool-fallback.tsx +48 -0
  36. package/src/components/assistant-ui/tooltip-icon-button.tsx +42 -0
  37. package/src/components/control-bar/api-key-input.tsx +122 -0
  38. package/src/components/control-bar/index.tsx +58 -0
  39. package/src/components/control-bar/model-select.tsx +120 -0
  40. package/src/components/control-bar/namespace-select.tsx +117 -0
  41. package/src/components/control-bar/wallet-connect.tsx +75 -0
  42. package/src/components/test/ThreadContextTest.tsx +204 -0
  43. package/src/components/tools/example-tool/ExampleTool.tsx +102 -0
  44. package/src/components/ui/accordion.tsx +58 -0
  45. package/src/components/ui/alert.tsx +62 -0
  46. package/src/components/ui/avatar.tsx +53 -0
  47. package/src/components/ui/badge.tsx +37 -0
  48. package/src/components/ui/breadcrumb.tsx +109 -0
  49. package/src/components/ui/button.tsx +59 -0
  50. package/src/components/ui/card.tsx +86 -0
  51. package/src/components/ui/collapsible.tsx +12 -0
  52. package/src/components/ui/command.tsx +156 -0
  53. package/src/components/ui/dialog.tsx +143 -0
  54. package/src/components/ui/drawer.tsx +118 -0
  55. package/src/components/ui/input.tsx +21 -0
  56. package/src/components/ui/label.tsx +20 -0
  57. package/src/components/ui/notification.tsx +57 -0
  58. package/src/components/ui/popover.tsx +33 -0
  59. package/src/components/ui/separator.tsx +28 -0
  60. package/src/components/ui/sheet.tsx +139 -0
  61. package/src/components/ui/sidebar.tsx +827 -0
  62. package/src/components/ui/skeleton.tsx +15 -0
  63. package/src/components/ui/sonner.tsx +29 -0
  64. package/src/components/ui/tooltip.tsx +61 -0
  65. package/src/hooks/use-mobile.ts +21 -0
  66. package/src/index.ts +26 -0
  67. package/src/registry.ts +218 -0
  68. package/{dist/styles.css → src/themes/default.css} +21 -3
  69. package/src/themes/tokens.config.ts +39 -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
package/package.json CHANGED
@@ -1,108 +1,50 @@
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.1",
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"
8
+ ".": "./src/index.ts",
9
+ "./aomi-frame": "./src/components/aomi-frame.tsx",
10
+ "./aomi-frame-collapsible": "./src/components/aomi-frame-collapsible.tsx",
11
+ "./control-bar": "./src/components/control-bar/index.tsx",
12
+ "./components/ui/*": "./src/components/ui/*.tsx",
13
+ "./components/assistant-ui/*": "./src/components/assistant-ui/*.tsx",
14
+ "./components/control-bar/*": "./src/components/control-bar/*.tsx",
15
+ "./hooks/*": "./src/hooks/*.ts",
16
+ "./styles.css": "./src/themes/default.css",
17
+ "./themes/*.css": "./src/themes/*.css"
21
18
  },
22
19
  "files": [
20
+ "src",
23
21
  "dist"
24
22
  ],
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"
50
- },
51
23
  "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
24
  "@assistant-ui/react": "^0.11.28",
59
- "@assistant-ui/react-ai-sdk": "^1.1.5",
60
25
  "@assistant-ui/react-markdown": "^0.11.1",
61
- "@eslint/eslintrc": "^3",
26
+ "@radix-ui/react-accordion": "^1.2.3",
62
27
  "@radix-ui/react-avatar": "^1.1.10",
28
+ "@radix-ui/react-collapsible": "^1.1.12",
63
29
  "@radix-ui/react-dialog": "^1.1.15",
30
+ "@radix-ui/react-popover": "^1.1.4",
64
31
  "@radix-ui/react-separator": "^1.1.7",
65
32
  "@radix-ui/react-slot": "^1.2.3",
66
33
  "@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",
34
+ "class-variance-authority": "^0.7.1",
35
+ "cmdk": "^1.0.0",
78
36
  "lucide-react": "^0.545.0",
79
37
  "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
38
  "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"
39
+ "sonner": "^1.7.4",
40
+ "vaul": "^1.1.2",
41
+ "zustand": "^5.0.8",
42
+ "@aomi-labs/react": "0.2.1"
43
+ },
44
+ "devDependencies": {
45
+ "tsx": "^4.21.0"
96
46
  },
97
47
  "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 ."
48
+ "build": "tsx scripts/build-registry.js"
107
49
  }
108
50
  }
@@ -0,0 +1,221 @@
1
+ "use client";
2
+
3
+ import {
4
+ type CSSProperties,
5
+ type ReactNode,
6
+ type FC,
7
+ createContext,
8
+ useContext,
9
+ } from "react";
10
+ import { AomiRuntimeProvider, cn, useAomiRuntime } from "@aomi-labs/react";
11
+ import { Thread } from "@/components/assistant-ui/thread";
12
+ import { ThreadListSidebar } from "@/components/assistant-ui/threadlist-sidebar";
13
+ import { NotificationToaster } from "@/components/ui/notification";
14
+ import {
15
+ SidebarInset,
16
+ SidebarProvider,
17
+ SidebarTrigger,
18
+ } from "@/components/ui/sidebar";
19
+ import { Separator } from "@/components/ui/separator";
20
+ import {
21
+ Breadcrumb,
22
+ BreadcrumbItem,
23
+ BreadcrumbList,
24
+ } from "@/components/ui/breadcrumb";
25
+ import { ControlBar, type ControlBarProps } from "@/components/control-bar";
26
+
27
+ // =============================================================================
28
+ // Composer Control Context - signals Thread to show inline controls
29
+ // =============================================================================
30
+
31
+ const ComposerControlContext = createContext<boolean>(false);
32
+
33
+ export const useComposerControl = () => useContext(ComposerControlContext);
34
+
35
+ // =============================================================================
36
+ // Types
37
+ // =============================================================================
38
+
39
+ type RootProps = {
40
+ children?: ReactNode;
41
+ width?: CSSProperties["width"];
42
+ height?: CSSProperties["height"];
43
+ className?: string;
44
+ style?: CSSProperties;
45
+ /** Position of the wallet button in the sidebar */
46
+ walletPosition?: "header" | "footer" | null;
47
+ /** Backend URL for the Aomi runtime */
48
+ backendUrl?: string;
49
+ };
50
+
51
+ type HeaderProps = {
52
+ children?: ReactNode;
53
+ /** Show the control bar in the header */
54
+ withControl?: boolean;
55
+ /** Props to pass to the ControlBar when withControl is true */
56
+ controlBarProps?: Omit<ControlBarProps, "children">;
57
+ className?: string;
58
+ };
59
+
60
+ type ComposerProps = {
61
+ children?: ReactNode;
62
+ /** Show inline controls in the composer input area */
63
+ withControl?: boolean;
64
+ className?: string;
65
+ };
66
+
67
+ type FrameControlBarProps = ControlBarProps;
68
+
69
+ // =============================================================================
70
+ // Compound Components
71
+ // =============================================================================
72
+
73
+ /**
74
+ * Root component - provides all context and layout container
75
+ */
76
+ const Root: FC<RootProps> = ({
77
+ children,
78
+ width = "100%",
79
+ height = "80vh",
80
+ className,
81
+ style,
82
+ walletPosition = "footer",
83
+ backendUrl,
84
+ }) => {
85
+ const resolvedBackendUrl =
86
+ backendUrl ??
87
+ process.env.NEXT_PUBLIC_BACKEND_URL ??
88
+ "http://localhost:8080";
89
+ const frameStyle: CSSProperties = { width, height, ...style };
90
+
91
+ return (
92
+ <AomiRuntimeProvider backendUrl={resolvedBackendUrl}>
93
+ <SidebarProvider>
94
+ <div
95
+ className={cn(
96
+ "rounded-4xl flex h-full w-full overflow-hidden bg-white shadow-2xl dark:bg-neutral-950",
97
+ className,
98
+ )}
99
+ style={frameStyle}
100
+ >
101
+ <ThreadListSidebar walletPosition={walletPosition} />
102
+ <SidebarInset className="relative flex flex-col">
103
+ {children}
104
+ </SidebarInset>
105
+ <NotificationToaster />
106
+ </div>
107
+ </SidebarProvider>
108
+ </AomiRuntimeProvider>
109
+ );
110
+ };
111
+
112
+ /**
113
+ * Header component - renders the header with optional control bar
114
+ */
115
+ const Header: FC<HeaderProps> = ({
116
+ children,
117
+ withControl,
118
+ controlBarProps,
119
+ className,
120
+ }) => {
121
+ const { currentThreadId, getThreadMetadata } = useAomiRuntime();
122
+ const currentTitle = getThreadMetadata(currentThreadId)?.title ?? "New Chat";
123
+
124
+ return (
125
+ <header
126
+ className={cn(
127
+ "mt-1 flex h-14 shrink-0 items-center gap-2 px-3",
128
+ className,
129
+ )}
130
+ >
131
+ <SidebarTrigger />
132
+ <Separator orientation="vertical" className="mr-2 h-4" />
133
+ <Breadcrumb>
134
+ <BreadcrumbList>
135
+ <BreadcrumbItem className="hidden md:block">
136
+ {currentTitle}
137
+ </BreadcrumbItem>
138
+ </BreadcrumbList>
139
+ </Breadcrumb>
140
+ <div className="ml-auto flex items-center gap-2">
141
+ {withControl && <ControlBar {...controlBarProps} />}
142
+ {children}
143
+ </div>
144
+ </header>
145
+ );
146
+ };
147
+
148
+ /**
149
+ * Composer component - renders the thread with optional inline controls
150
+ * When withControl={true}, controls appear inline in the composer input area
151
+ */
152
+ const Composer: FC<ComposerProps> = ({
153
+ children,
154
+ withControl = false,
155
+ className,
156
+ }) => {
157
+ const { currentThreadId, threadViewKey } = useAomiRuntime();
158
+
159
+ return (
160
+ <ComposerControlContext.Provider value={withControl}>
161
+ <div className={cn("flex flex-1 flex-col overflow-hidden", className)}>
162
+ <Thread key={`${currentThreadId}-${threadViewKey}`} />
163
+ {children}
164
+ </div>
165
+ </ComposerControlContext.Provider>
166
+ );
167
+ };
168
+
169
+ /**
170
+ * ControlBar component - wrapper for the control bar with frame styling
171
+ */
172
+ const FrameControlBar: FC<FrameControlBarProps> = (props) => {
173
+ return <ControlBar {...props} />;
174
+ };
175
+
176
+ // =============================================================================
177
+ // Default Layout Component (Simple API)
178
+ // =============================================================================
179
+
180
+ type DefaultLayoutProps = Omit<RootProps, "children">;
181
+
182
+ /**
183
+ * Default layout - controls are inline in the composer input area
184
+ * Usage: <AomiFrame /> or <AomiFrame walletPosition="header" />
185
+ */
186
+ const DefaultLayout: FC<DefaultLayoutProps> = ({
187
+ walletPosition = "footer",
188
+ ...props
189
+ }) => {
190
+ // Hide wallet in ControlBar when it's shown in sidebar
191
+ const hideWalletInControlBar = walletPosition !== null;
192
+
193
+ return (
194
+ <Root walletPosition={walletPosition} {...props}>
195
+ <Header
196
+ withControl
197
+ controlBarProps={{ hideWallet: hideWalletInControlBar }}
198
+ />
199
+ <Composer />
200
+ </Root>
201
+ );
202
+ };
203
+
204
+ // =============================================================================
205
+ // Export Compound Component
206
+ // =============================================================================
207
+
208
+ export const AomiFrame = Object.assign(DefaultLayout, {
209
+ Root,
210
+ Header,
211
+ Composer,
212
+ ControlBar: FrameControlBar,
213
+ });
214
+
215
+ // Re-export types for consumers
216
+ export type {
217
+ RootProps as AomiFrameRootProps,
218
+ HeaderProps as AomiFrameHeaderProps,
219
+ ComposerProps as AomiFrameComposerProps,
220
+ FrameControlBarProps as AomiFrameControlBarProps,
221
+ };
@@ -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
+ };