Skip to content

Commit 4f9fbba

Browse files
iamfaranraheeliftikhar5
authored andcommitted
add file attachments components
1 parent 68b2802 commit 4f9fbba

File tree

6 files changed

+726
-1
lines changed

6 files changed

+726
-1
lines changed

client/packages/lowcoder/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@
3333
"@jsonforms/core": "^3.5.1",
3434
"@lottiefiles/dotlottie-react": "^0.13.0",
3535
"@manaflair/redux-batch": "^1.0.0",
36+
"@radix-ui/react-avatar": "^1.1.10",
37+
"@radix-ui/react-dialog": "^1.1.14",
3638
"@radix-ui/react-slot": "^1.2.3",
3739
"@radix-ui/react-tooltip": "^1.2.7",
3840
"@rjsf/antd": "^5.24.9",

client/packages/lowcoder/src/comps/comps/chatComp/components/assistant-ui/thread.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {
2525
import { Spin, Flex } from "antd";
2626
import { LoadingOutlined } from "@ant-design/icons";
2727
import styled from "styled-components";
28+
import { ComposerAddAttachment, ComposerAttachments } from "../ui/attachment";
2829
const SimpleANTDLoader = () => {
2930
const antIcon = <LoadingOutlined style={{ fontSize: 24 }} spin />;
3031

@@ -150,6 +151,8 @@ import {
150151
const Composer: FC<{ placeholder?: string }> = ({ placeholder = trans("chat.composerPlaceholder") }) => {
151152
return (
152153
<ComposerPrimitive.Root className="aui-composer-root">
154+
<ComposerAttachments />
155+
<ComposerAddAttachment />
153156
<ComposerPrimitive.Input
154157
rows={1}
155158
autoFocus
Lines changed: 346 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,346 @@
1+
"use client";
2+
3+
import { PropsWithChildren, useEffect, useState, type FC } from "react";
4+
import { CircleXIcon, FileIcon, PaperclipIcon } from "lucide-react";
5+
import {
6+
AttachmentPrimitive,
7+
ComposerPrimitive,
8+
MessagePrimitive,
9+
useAttachment,
10+
} from "@assistant-ui/react";
11+
import styled from "styled-components";
12+
import {
13+
Tooltip,
14+
TooltipContent,
15+
TooltipTrigger,
16+
} from "./tooltip";
17+
import {
18+
Dialog,
19+
DialogTitle,
20+
DialogTrigger,
21+
DialogOverlay,
22+
DialogPortal,
23+
DialogContent,
24+
} from "./dialog";
25+
import { Avatar, AvatarImage, AvatarFallback } from "./avatar";
26+
import { TooltipIconButton } from "../assistant-ui/tooltip-icon-button";
27+
28+
// ============================================================================
29+
// STYLED COMPONENTS
30+
// ============================================================================
31+
32+
const StyledDialogTrigger = styled(DialogTrigger)`
33+
cursor: pointer;
34+
transition: background-color 0.2s;
35+
padding: 2px;
36+
border-radius: 4px;
37+
38+
&:hover {
39+
background-color: rgba(0, 0, 0, 0.05);
40+
}
41+
`;
42+
43+
const StyledAvatar = styled(Avatar)`
44+
background-color: #f1f5f9;
45+
display: flex;
46+
width: 40px;
47+
height: 40px;
48+
align-items: center;
49+
justify-content: center;
50+
border-radius: 8px;
51+
border: 1px solid #e2e8f0;
52+
font-size: 14px;
53+
`;
54+
55+
const AttachmentContainer = styled.div`
56+
display: flex;
57+
height: 48px;
58+
width: 160px;
59+
align-items: center;
60+
justify-content: center;
61+
gap: 8px;
62+
border-radius: 8px;
63+
border: 1px solid #e2e8f0;
64+
padding: 4px;
65+
`;
66+
67+
const AttachmentTextContainer = styled.div`
68+
flex-grow: 1;
69+
flex-basis: 0;
70+
overflow: hidden;
71+
`;
72+
73+
const AttachmentName = styled.p`
74+
color: #64748b;
75+
font-size: 12px;
76+
font-weight: bold;
77+
overflow: hidden;
78+
text-overflow: ellipsis;
79+
white-space: nowrap;
80+
word-break: break-all;
81+
margin: 0;
82+
line-height: 16px;
83+
`;
84+
85+
const AttachmentType = styled.p`
86+
color: #64748b;
87+
font-size: 12px;
88+
margin: 0;
89+
line-height: 16px;
90+
`;
91+
92+
const AttachmentRoot = styled(AttachmentPrimitive.Root)`
93+
position: relative;
94+
margin-top: 12px;
95+
`;
96+
97+
const StyledTooltipIconButton = styled(TooltipIconButton)`
98+
color: #64748b;
99+
position: absolute;
100+
right: -12px;
101+
top: -12px;
102+
width: 24px;
103+
height: 24px;
104+
105+
& svg {
106+
background-color: white;
107+
width: 16px;
108+
height: 16px;
109+
border-radius: 50%;
110+
}
111+
`;
112+
113+
const UserAttachmentsContainer = styled.div`
114+
display: flex;
115+
width: 100%;
116+
flex-direction: row;
117+
gap: 12px;
118+
grid-column: 1 / -1;
119+
grid-row-start: 1;
120+
justify-content: flex-end;
121+
`;
122+
123+
const ComposerAttachmentsContainer = styled.div`
124+
display: flex;
125+
width: 100%;
126+
flex-direction: row;
127+
gap: 12px;
128+
overflow-x: auto;
129+
`;
130+
131+
const StyledComposerButton = styled(TooltipIconButton)`
132+
margin: 10px 0;
133+
width: 32px;
134+
height: 32px;
135+
padding: 8px;
136+
transition: opacity 0.2s ease-in;
137+
`;
138+
139+
const ScreenReaderOnly = styled.span`
140+
position: absolute;
141+
left: -10000px;
142+
width: 1px;
143+
height: 1px;
144+
overflow: hidden;
145+
`;
146+
147+
// ============================================================================
148+
// UTILITY HOOKS
149+
// ============================================================================
150+
151+
// Simple replacement for useShallow (removes zustand dependency)
152+
const useShallow = <T,>(selector: (state: any) => T): ((state: any) => T) => selector;
153+
154+
const useFileSrc = (file: File | undefined) => {
155+
const [src, setSrc] = useState<string | undefined>(undefined);
156+
157+
useEffect(() => {
158+
if (!file) {
159+
setSrc(undefined);
160+
return;
161+
}
162+
163+
const objectUrl = URL.createObjectURL(file);
164+
setSrc(objectUrl);
165+
166+
return () => {
167+
URL.revokeObjectURL(objectUrl);
168+
};
169+
}, [file]);
170+
171+
return src;
172+
};
173+
174+
const useAttachmentSrc = () => {
175+
const { file, src } = useAttachment(
176+
useShallow((a): { file?: File; src?: string } => {
177+
if (a.type !== "image") return {};
178+
if (a.file) return { file: a.file };
179+
const src = a.content?.filter((c: any) => c.type === "image")[0]?.image;
180+
if (!src) return {};
181+
return { src };
182+
})
183+
);
184+
185+
return useFileSrc(file) ?? src;
186+
};
187+
188+
// ============================================================================
189+
// ATTACHMENT COMPONENTS
190+
// ============================================================================
191+
192+
type AttachmentPreviewProps = {
193+
src: string;
194+
};
195+
196+
const AttachmentPreview: FC<AttachmentPreviewProps> = ({ src }) => {
197+
const [isLoaded, setIsLoaded] = useState(false);
198+
199+
return (
200+
<img
201+
src={src}
202+
style={{
203+
width: "auto",
204+
height: "auto",
205+
maxWidth: "75dvh",
206+
maxHeight: "75dvh",
207+
display: isLoaded ? "block" : "none",
208+
overflow: "clip",
209+
}}
210+
onLoad={() => setIsLoaded(true)}
211+
alt="Preview"
212+
/>
213+
);
214+
};
215+
216+
const AttachmentPreviewDialog: FC<PropsWithChildren> = ({ children }) => {
217+
const src = useAttachmentSrc();
218+
219+
if (!src) return <>{children}</>;
220+
221+
return (
222+
<Dialog>
223+
<StyledDialogTrigger asChild>
224+
{children}
225+
</StyledDialogTrigger>
226+
<AttachmentDialogContent>
227+
<DialogTitle>
228+
<ScreenReaderOnly>Image Attachment Preview</ScreenReaderOnly>
229+
</DialogTitle>
230+
<AttachmentPreview src={src} />
231+
</AttachmentDialogContent>
232+
</Dialog>
233+
);
234+
};
235+
236+
const AttachmentThumb: FC = () => {
237+
const isImage = useAttachment((a) => a.type === "image");
238+
const src = useAttachmentSrc();
239+
return (
240+
<StyledAvatar>
241+
<AvatarFallback delayMs={isImage ? 200 : 0}>
242+
<FileIcon />
243+
</AvatarFallback>
244+
<AvatarImage src={src} />
245+
</StyledAvatar>
246+
);
247+
};
248+
249+
const AttachmentUI: FC = () => {
250+
const canRemove = useAttachment((a) => a.source !== "message");
251+
const typeLabel = useAttachment((a) => {
252+
const type = a.type;
253+
switch (type) {
254+
case "image":
255+
return "Image";
256+
case "document":
257+
return "Document";
258+
case "file":
259+
return "File";
260+
default:
261+
const _exhaustiveCheck: never = type;
262+
throw new Error(`Unknown attachment type: ${_exhaustiveCheck}`);
263+
}
264+
});
265+
266+
return (
267+
<Tooltip>
268+
<AttachmentRoot>
269+
<AttachmentPreviewDialog>
270+
<TooltipTrigger asChild>
271+
<AttachmentContainer>
272+
<AttachmentThumb />
273+
<AttachmentTextContainer>
274+
<AttachmentName>
275+
<AttachmentPrimitive.Name />
276+
</AttachmentName>
277+
<AttachmentType>{typeLabel}</AttachmentType>
278+
</AttachmentTextContainer>
279+
</AttachmentContainer>
280+
</TooltipTrigger>
281+
</AttachmentPreviewDialog>
282+
{canRemove && <AttachmentRemove />}
283+
</AttachmentRoot>
284+
<TooltipContent side="top">
285+
<AttachmentPrimitive.Name />
286+
</TooltipContent>
287+
</Tooltip>
288+
);
289+
};
290+
291+
const AttachmentRemove: FC = () => {
292+
return (
293+
<AttachmentPrimitive.Remove asChild>
294+
<StyledTooltipIconButton
295+
tooltip="Remove file"
296+
side="top"
297+
>
298+
<CircleXIcon />
299+
</StyledTooltipIconButton>
300+
</AttachmentPrimitive.Remove>
301+
);
302+
};
303+
304+
// ============================================================================
305+
// EXPORTED COMPONENTS
306+
// ============================================================================
307+
308+
export const UserMessageAttachments: FC = () => {
309+
return (
310+
<UserAttachmentsContainer>
311+
<MessagePrimitive.Attachments components={{ Attachment: AttachmentUI }} />
312+
</UserAttachmentsContainer>
313+
);
314+
};
315+
316+
export const ComposerAttachments: FC = () => {
317+
return (
318+
<ComposerAttachmentsContainer>
319+
<ComposerPrimitive.Attachments
320+
components={{ Attachment: AttachmentUI }}
321+
/>
322+
</ComposerAttachmentsContainer>
323+
);
324+
};
325+
326+
export const ComposerAddAttachment: FC = () => {
327+
return (
328+
<ComposerPrimitive.AddAttachment asChild>
329+
<StyledComposerButton
330+
tooltip="Add Attachment"
331+
variant="ghost"
332+
>
333+
<PaperclipIcon />
334+
</StyledComposerButton>
335+
</ComposerPrimitive.AddAttachment>
336+
);
337+
};
338+
339+
const AttachmentDialogContent: FC<PropsWithChildren> = ({ children }) => (
340+
<DialogPortal>
341+
<DialogOverlay />
342+
<DialogContent className="aui-dialog-content">
343+
{children}
344+
</DialogContent>
345+
</DialogPortal>
346+
);

0 commit comments

Comments
 (0)