Skip to content

Commit 8f54f41

Browse files
committed
feat(react-core): add v2 CopilotChatAttachmentQueue component
1 parent f6c15cd commit 8f54f41

1 file changed

Lines changed: 142 additions & 0 deletions

File tree

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
import React from "react";
2+
import type { Attachment } from "@copilotkit/shared";
3+
import { formatFileSize } from "@copilotkit/shared";
4+
import { cn } from "../../lib/utils";
5+
6+
interface CopilotChatAttachmentQueueProps {
7+
attachments: Attachment[];
8+
onRemoveAttachment: (id: string) => void;
9+
className?: string;
10+
}
11+
12+
export const CopilotChatAttachmentQueue: React.FC<
13+
CopilotChatAttachmentQueueProps
14+
> = ({ attachments, onRemoveAttachment, className }) => {
15+
if (attachments.length === 0) return null;
16+
17+
return (
18+
<div
19+
className={cn(
20+
"cpk:flex cpk:flex-wrap cpk:gap-2 cpk:p-2",
21+
className,
22+
)}
23+
>
24+
{attachments.map((attachment) => (
25+
<div
26+
key={attachment.id}
27+
className={cn(
28+
"cpk:relative cpk:inline-flex cpk:rounded-lg cpk:overflow-hidden cpk:border cpk:border-border",
29+
attachment.type === "image" || attachment.type === "video"
30+
? "cpk:w-[72px] cpk:h-[72px]"
31+
: attachment.type === "audio"
32+
? "cpk:min-w-[200px] cpk:max-w-[280px] cpk:flex-col cpk:p-1"
33+
: "cpk:p-2 cpk:px-3 cpk:max-w-[200px]",
34+
)}
35+
>
36+
{attachment.status === "uploading" && (
37+
<div className="cpk:absolute cpk:inset-0 cpk:flex cpk:items-center cpk:justify-center cpk:bg-black/40 cpk:z-10">
38+
<div className="cpk:w-5 cpk:h-5 cpk:border-2 cpk:border-white cpk:border-t-transparent cpk:rounded-full cpk:animate-spin" />
39+
</div>
40+
)}
41+
<AttachmentPreview attachment={attachment} />
42+
<button
43+
onClick={() => onRemoveAttachment(attachment.id)}
44+
className="cpk:absolute cpk:top-1 cpk:right-1 cpk:bg-black/60 cpk:text-white cpk:border-none cpk:rounded-full cpk:w-5 cpk:h-5 cpk:flex cpk:items-center cpk:justify-center cpk:cursor-pointer cpk:text-[10px] cpk:z-20"
45+
aria-label="Remove attachment"
46+
>
47+
48+
</button>
49+
</div>
50+
))}
51+
</div>
52+
);
53+
};
54+
55+
function AttachmentPreview({ attachment }: { attachment: Attachment }) {
56+
if (attachment.status === "uploading") {
57+
return <div className="cpk:w-full cpk:h-full" />;
58+
}
59+
60+
const src = getSourceUrl(attachment);
61+
62+
switch (attachment.type) {
63+
case "image":
64+
return (
65+
<img
66+
src={src}
67+
alt={attachment.filename || "Image attachment"}
68+
className="cpk:w-full cpk:h-full cpk:object-cover"
69+
/>
70+
);
71+
72+
case "audio":
73+
return (
74+
<div className="cpk:flex cpk:flex-col cpk:gap-1 cpk:w-full">
75+
<audio
76+
src={src}
77+
controls
78+
preload="metadata"
79+
className="cpk:w-full cpk:h-8"
80+
/>
81+
{attachment.filename && (
82+
<span className="cpk:text-xs cpk:font-medium cpk:overflow-hidden cpk:text-ellipsis cpk:whitespace-nowrap">
83+
{attachment.filename}
84+
</span>
85+
)}
86+
</div>
87+
);
88+
89+
case "video":
90+
return attachment.thumbnail ? (
91+
<img
92+
src={attachment.thumbnail}
93+
alt={attachment.filename || "Video thumbnail"}
94+
className="cpk:w-full cpk:h-full cpk:object-cover"
95+
/>
96+
) : (
97+
<video
98+
src={src}
99+
preload="metadata"
100+
muted
101+
className="cpk:w-full cpk:h-full cpk:object-cover"
102+
/>
103+
);
104+
105+
case "document":
106+
return (
107+
<div className="cpk:flex cpk:items-center cpk:gap-2">
108+
<div className="cpk:w-8 cpk:h-8 cpk:rounded-md cpk:bg-primary cpk:text-primary-foreground cpk:flex cpk:items-center cpk:justify-center cpk:text-[10px] cpk:font-semibold cpk:shrink-0">
109+
{getDocumentIcon(attachment.source.mimeType ?? "")}
110+
</div>
111+
<div className="cpk:flex cpk:flex-col cpk:min-w-0">
112+
<span className="cpk:text-xs cpk:font-medium cpk:overflow-hidden cpk:text-ellipsis cpk:whitespace-nowrap">
113+
{attachment.filename || "Document"}
114+
</span>
115+
{attachment.size != null && (
116+
<span className="cpk:text-[11px] cpk:text-muted-foreground">
117+
{formatFileSize(attachment.size)}
118+
</span>
119+
)}
120+
</div>
121+
</div>
122+
);
123+
}
124+
}
125+
126+
function getSourceUrl(attachment: Attachment): string {
127+
if (attachment.source.type === "url") {
128+
return attachment.source.value;
129+
}
130+
const mimeType = attachment.source.mimeType;
131+
return `data:${mimeType};base64,${attachment.source.value}`;
132+
}
133+
134+
function getDocumentIcon(mimeType: string): string {
135+
if (mimeType.includes("pdf")) return "PDF";
136+
if (mimeType.includes("word") || mimeType.includes("document")) return "DOC";
137+
if (mimeType.includes("sheet") || mimeType.includes("excel")) return "XLS";
138+
if (mimeType.includes("presentation") || mimeType.includes("powerpoint"))
139+
return "PPT";
140+
if (mimeType.includes("text/")) return "TXT";
141+
return "FILE";
142+
}

0 commit comments

Comments
 (0)