Skip to content

Commit 47739e2

Browse files
committed
update to latest components and ai-sdk v5
1 parent d373d52 commit 47739e2

18 files changed

+2205
-726
lines changed

app/globals.css

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@
128128
body {
129129
@apply bg-background text-foreground;
130130
}
131-
131+
132132
/* Scrollbar styling */
133133
::-webkit-scrollbar {
134134
width: 12px;
Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
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+
useAttachment,
11+
} from "@assistant-ui/react";
12+
import { useShallow } from "zustand/shallow";
13+
import {
14+
Tooltip,
15+
TooltipContent,
16+
TooltipTrigger,
17+
} from "@/components/ui/tooltip";
18+
import {
19+
Dialog,
20+
DialogTitle,
21+
DialogContent,
22+
DialogTrigger,
23+
} from "@/components/ui/dialog";
24+
import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar";
25+
import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button";
26+
import { cn } from "@/lib/utils";
27+
28+
const useFileSrc = (file: File | undefined) => {
29+
const [src, setSrc] = useState<string | undefined>(undefined);
30+
31+
useEffect(() => {
32+
if (!file) {
33+
setSrc(undefined);
34+
return;
35+
}
36+
37+
const objectUrl = URL.createObjectURL(file);
38+
setSrc(objectUrl);
39+
40+
return () => {
41+
URL.revokeObjectURL(objectUrl);
42+
};
43+
}, [file]);
44+
45+
return src;
46+
};
47+
48+
const useAttachmentSrc = () => {
49+
const { file, src } = useAttachment(
50+
useShallow((a): { file?: File; src?: string } => {
51+
if (a.type !== "image") return {};
52+
if (a.file) return { file: a.file };
53+
const src = a.content?.filter((c) => c.type === "image")[0]?.image;
54+
if (!src) return {};
55+
return { src };
56+
}),
57+
);
58+
59+
return useFileSrc(file) ?? src;
60+
};
61+
62+
type AttachmentPreviewProps = {
63+
src: string;
64+
};
65+
66+
const AttachmentPreview: FC<AttachmentPreviewProps> = ({ src }) => {
67+
const [isLoaded, setIsLoaded] = useState(false);
68+
return (
69+
<Image
70+
src={src}
71+
alt="Image Preview"
72+
width={1}
73+
height={1}
74+
className={
75+
isLoaded
76+
? "aui-attachment-preview-image-loaded block h-auto max-h-[80vh] w-auto max-w-full object-contain"
77+
: "aui-attachment-preview-image-loading hidden"
78+
}
79+
onLoadingComplete={() => setIsLoaded(true)}
80+
priority={false}
81+
/>
82+
);
83+
};
84+
85+
const AttachmentPreviewDialog: FC<PropsWithChildren> = ({ children }) => {
86+
const src = useAttachmentSrc();
87+
88+
if (!src) return children;
89+
90+
return (
91+
<Dialog>
92+
<DialogTrigger
93+
className="aui-attachment-preview-trigger cursor-pointer transition-colors hover:bg-accent/50"
94+
asChild
95+
>
96+
{children}
97+
</DialogTrigger>
98+
<DialogContent className="aui-attachment-preview-dialog-content p-2 sm:max-w-3xl [&_svg]:text-background [&>button]:rounded-full [&>button]:bg-foreground/60 [&>button]:p-1 [&>button]:opacity-100 [&>button]:!ring-0 [&>button]:hover:[&_svg]:text-destructive">
99+
<DialogTitle className="aui-sr-only sr-only">Image Attachment Preview</DialogTitle>
100+
<div className="aui-attachment-preview relative mx-auto flex max-h-[80dvh] w-full items-center justify-center overflow-hidden bg-background">
101+
<AttachmentPreview src={src} />
102+
</div>
103+
</DialogContent>
104+
</Dialog>
105+
);
106+
};
107+
108+
const AttachmentThumb: FC = () => {
109+
const isImage = useAttachment((a) => a.type === "image");
110+
const src = useAttachmentSrc();
111+
112+
return (
113+
<Avatar className="aui-attachment-tile-avatar h-full w-full rounded-none">
114+
<AvatarImage
115+
src={src}
116+
alt="Attachment preview"
117+
className="aui-attachment-tile-image object-cover"
118+
/>
119+
<AvatarFallback delayMs={isImage ? 200 : 0}>
120+
<FileText className="aui-attachment-tile-fallback-icon size-8 text-muted-foreground" />
121+
</AvatarFallback>
122+
</Avatar>
123+
);
124+
};
125+
126+
const AttachmentUI: FC = () => {
127+
const isComposer = useAttachment((a) => a.source !== "message");
128+
const isImage = useAttachment((a) => a.type === "image");
129+
const typeLabel = useAttachment((a) => {
130+
const type = a.type;
131+
switch (type) {
132+
case "image":
133+
return "Image";
134+
case "document":
135+
return "Document";
136+
case "file":
137+
return "File";
138+
default:
139+
const _exhaustiveCheck: never = type;
140+
throw new Error(`Unknown attachment type: ${_exhaustiveCheck}`);
141+
}
142+
});
143+
144+
return (
145+
<Tooltip>
146+
<AttachmentPrimitive.Root
147+
className={cn(
148+
"aui-attachment-root relative",
149+
isImage && "aui-attachment-root-composer only:[&>#attachment-tile]:size-24",
150+
)}
151+
>
152+
<AttachmentPreviewDialog>
153+
<TooltipTrigger asChild>
154+
<div
155+
className={cn(
156+
"aui-attachment-tile size-14 cursor-pointer overflow-hidden rounded-[14px] border bg-muted transition-opacity hover:opacity-75",
157+
isComposer && "aui-attachment-tile-composer border-foreground/20",
158+
)}
159+
role="button"
160+
id="attachment-tile"
161+
aria-label={`${typeLabel} attachment`}
162+
>
163+
<AttachmentThumb />
164+
</div>
165+
</TooltipTrigger>
166+
</AttachmentPreviewDialog>
167+
{isComposer && <AttachmentRemove />}
168+
</AttachmentPrimitive.Root>
169+
<TooltipContent side="top">
170+
<AttachmentPrimitive.Name />
171+
</TooltipContent>
172+
</Tooltip>
173+
);
174+
};
175+
176+
const AttachmentRemove: FC = () => {
177+
return (
178+
<AttachmentPrimitive.Remove asChild>
179+
<TooltipIconButton
180+
tooltip="Remove file"
181+
className="aui-attachment-tile-remove absolute top-1.5 right-1.5 size-3.5 rounded-full bg-white text-muted-foreground opacity-100 shadow-sm hover:!bg-white [&_svg]:text-black hover:[&_svg]:text-destructive"
182+
side="top"
183+
>
184+
<XIcon className="aui-attachment-remove-icon size-3 dark:stroke-[2.5px]" />
185+
</TooltipIconButton>
186+
</AttachmentPrimitive.Remove>
187+
);
188+
};
189+
190+
export const UserMessageAttachments: FC = () => {
191+
return (
192+
<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">
193+
<MessagePrimitive.Attachments components={{ Attachment: AttachmentUI }} />
194+
</div>
195+
);
196+
};
197+
198+
export const ComposerAttachments: FC = () => {
199+
return (
200+
<div className="aui-composer-attachments mb-2 flex w-full flex-row items-center gap-2 overflow-x-auto px-1.5 pt-0.5 pb-1 empty:hidden">
201+
<ComposerPrimitive.Attachments
202+
components={{ Attachment: AttachmentUI }}
203+
/>
204+
</div>
205+
);
206+
};
207+
208+
export const ComposerAddAttachment: FC = () => {
209+
return (
210+
<ComposerPrimitive.AddAttachment asChild>
211+
<TooltipIconButton
212+
tooltip="Add Attachment"
213+
side="bottom"
214+
variant="ghost"
215+
size="icon"
216+
className="aui-composer-add-attachment size-[34px] rounded-full p-1 text-xs font-semibold hover:bg-muted-foreground/15 dark:border-muted-foreground/15 dark:hover:bg-muted-foreground/30"
217+
aria-label="Add Attachment"
218+
>
219+
<PlusIcon className="aui-attachment-add-icon size-5 stroke-[1.5px]" />
220+
</TooltipIconButton>
221+
</ComposerPrimitive.AddAttachment>
222+
);
223+
};

0 commit comments

Comments
 (0)