- Added image upload functionality for agents in the backend and frontend.

- Implemented image URL generation based on storage strategy (S3 or local).
- Updated agent creation and update endpoints to handle image files.
- Enhanced frontend components to display agent images with fallbacks.
- New API endpoint to serve images from storage.
- Refactored API client to support FormData for file uploads.
- Improved error handling and logging for image processing.
This commit is contained in:
Siddhant Rai
2025-06-18 18:17:20 +05:30
parent cab1f3787a
commit e35f1d70e4
12 changed files with 385 additions and 146 deletions

View File

@@ -401,9 +401,13 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
<div className="flex items-center gap-2">
<div className="flex w-6 justify-center">
<img
src={agent.image ?? Robot}
src={
agent.image && agent.image.trim() !== ''
? agent.image
: Robot
}
alt="agent-logo"
className="h-6 w-6"
className="h-6 w-6 rounded-full object-contain"
/>
</div>
<p className="overflow-hidden overflow-ellipsis whitespace-nowrap text-sm leading-6 text-eerie-black dark:text-bright-gray">

View File

@@ -83,9 +83,9 @@ export default function AgentCard({
<div className="w-full">
<div className="flex w-full items-center gap-1 px-1">
<img
src={agent.image ?? Robot}
src={agent.image && agent.image.trim() !== '' ? agent.image : Robot}
alt={`${agent.name}`}
className="h-7 w-7 rounded-full"
className="h-7 w-7 rounded-full object-contain"
/>
{agent.status === 'draft' && (
<p className="text-xs text-black opacity-50 dark:text-[#E0E0E0]">

View File

@@ -1,4 +1,4 @@
import React, { useEffect, useRef, useState } from 'react';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useNavigate, useParams } from 'react-router-dom';
@@ -6,6 +6,7 @@ import userService from '../api/services/userService';
import ArrowLeft from '../assets/arrow-left.svg';
import SourceIcon from '../assets/source.svg';
import Dropdown from '../components/Dropdown';
import { FileUpload } from '../components/FileUpload';
import MultiSelectPopup, { OptionType } from '../components/MultiSelectPopup';
import AgentDetailsModal from '../modals/AgentDetailsModal';
import ConfirmationModal from '../modals/ConfirmationModal';
@@ -48,6 +49,7 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
agent_type: '',
status: '',
});
const [imageFile, setImageFile] = useState<File | null>(null);
const [prompts, setPrompts] = useState<
{ name: string; id: string; type: string }[]
>([]);
@@ -106,6 +108,13 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
);
};
const handleUpload = useCallback((files: File[]) => {
if (files && files.length > 0) {
const file = files[0];
setImageFile(file);
}
}, []);
const handleCancel = () => {
if (selectedAgent) dispatch(setSelectedAgent(null));
navigate('/agents');
@@ -118,42 +127,80 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
};
const handleSaveDraft = async () => {
const response =
effectiveMode === 'new'
? await userService.createAgent({ ...agent, status: 'draft' }, token)
: await userService.updateAgent(
agent.id || '',
{ ...agent, status: 'draft' },
token,
);
if (!response.ok) throw new Error('Failed to create agent draft');
const data = await response.json();
if (effectiveMode === 'new') {
setEffectiveMode('draft');
setAgent((prev) => ({ ...prev, id: data.id }));
const formData = new FormData();
formData.append('name', agent.name);
formData.append('description', agent.description);
formData.append('source', agent.source);
formData.append('chunks', agent.chunks);
formData.append('retriever', agent.retriever);
formData.append('prompt_id', agent.prompt_id);
formData.append('agent_type', agent.agent_type);
formData.append('status', 'draft');
if (imageFile) formData.append('image', imageFile);
if (agent.tools && agent.tools.length > 0)
formData.append('tools', JSON.stringify(agent.tools));
else formData.append('tools', '[]');
try {
const response =
effectiveMode === 'new'
? await userService.createAgent(formData, token)
: await userService.updateAgent(agent.id || '', formData, token);
if (!response.ok) throw new Error('Failed to create agent draft');
const data = await response.json();
if (effectiveMode === 'new') {
setEffectiveMode('draft');
setAgent((prev) => ({
...prev,
id: data.id,
image: data.image || prev.image,
}));
}
} catch (error) {
console.error('Error saving draft:', error);
throw new Error('Failed to save draft');
}
};
const handlePublish = async () => {
const response =
effectiveMode === 'new'
? await userService.createAgent(
{ ...agent, status: 'published' },
token,
)
: await userService.updateAgent(
agent.id || '',
{ ...agent, status: 'published' },
token,
);
if (!response.ok) throw new Error('Failed to publish agent');
const data = await response.json();
if (data.id) setAgent((prev) => ({ ...prev, id: data.id }));
if (data.key) setAgent((prev) => ({ ...prev, key: data.key }));
if (effectiveMode === 'new' || effectiveMode === 'draft') {
setEffectiveMode('edit');
setAgent((prev) => ({ ...prev, status: 'published' }));
setAgentDetails('ACTIVE');
const formData = new FormData();
formData.append('name', agent.name);
formData.append('description', agent.description);
formData.append('source', agent.source);
formData.append('chunks', agent.chunks);
formData.append('retriever', agent.retriever);
formData.append('prompt_id', agent.prompt_id);
formData.append('agent_type', agent.agent_type);
formData.append('status', 'published');
if (imageFile) formData.append('image', imageFile);
if (agent.tools && agent.tools.length > 0)
formData.append('tools', JSON.stringify(agent.tools));
else formData.append('tools', '[]');
try {
const response =
effectiveMode === 'new'
? await userService.createAgent(formData, token)
: await userService.updateAgent(agent.id || '', formData, token);
if (!response.ok) throw new Error('Failed to publish agent');
const data = await response.json();
if (data.id) setAgent((prev) => ({ ...prev, id: data.id }));
if (data.key) setAgent((prev) => ({ ...prev, key: data.key }));
if (effectiveMode === 'new' || effectiveMode === 'draft') {
setEffectiveMode('edit');
setAgent((prev) => ({
...prev,
status: 'published',
image: data.image || prev.image,
}));
setAgentDetails('ACTIVE');
}
} catch (error) {
console.error('Error publishing agent:', error);
throw new Error('Failed to publish agent');
}
};
@@ -325,6 +372,21 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
setAgent({ ...agent, description: e.target.value })
}
/>
<div className="mt-3">
<FileUpload
showPreview
className="dark:bg-[#222327]"
onUpload={handleUpload}
onRemove={() => setImageFile(null)}
uploadText={[
{ text: 'Click to upload', colorClass: 'text-[#7D54D1]' },
{
text: ' or drag and drop',
colorClass: 'text-[#525252]',
},
]}
/>
</div>
</div>
<div className="rounded-[30px] bg-[#F6F6F6] px-6 py-3 dark:bg-[#383838] dark:text-[#E0E0E0]">
<h2 className="text-lg font-semibold">Source</h2>

View File

@@ -155,9 +155,13 @@ export default function SharedAgent() {
<div className="relative h-full w-full">
<div className="absolute left-4 top-5 hidden items-center gap-3 sm:flex">
<img
src={sharedAgent.image ?? Robot}
src={
sharedAgent.image && sharedAgent.image.trim() !== ''
? sharedAgent.image
: Robot
}
alt="agent-logo"
className="h-6 w-6"
className="h-6 w-6 rounded-full object-contain"
/>
<h2 className="text-lg font-semibold text-[#212121] dark:text-[#E0E0E0]">
{sharedAgent.name}

View File

@@ -6,7 +6,10 @@ export default function SharedAgentCard({ agent }: { agent: Agent }) {
<div className="flex w-full max-w-[720px] flex-col rounded-3xl border border-dark-gray p-6 shadow-sm dark:border-grey sm:w-fit sm:min-w-[480px]">
<div className="flex items-center gap-3">
<div className="flex h-12 w-12 items-center justify-center overflow-hidden rounded-full p-1">
<img src={Robot} className="h-full w-full object-contain" />
<img
src={agent.image && agent.image.trim() !== '' ? agent.image : Robot}
className="h-full w-full rounded-full object-contain"
/>
</div>
<div className="flex max-h-[92px] w-[80%] flex-col gap-px">
<h2 className="text-base font-semibold text-[#212121] dark:text-[#E0E0E0] sm:text-lg">

View File

@@ -324,17 +324,21 @@ function AgentCard({
iconWidth: 14,
iconHeight: 14,
},
{
icon: agent.pinned ? UnPin : Pin,
label: agent.pinned ? 'Unpin' : 'Pin agent',
onClick: (e: SyntheticEvent) => {
e.stopPropagation();
togglePin();
},
variant: 'primary',
iconWidth: 18,
iconHeight: 18,
},
...(agent.status === 'published'
? [
{
icon: agent.pinned ? UnPin : Pin,
label: agent.pinned ? 'Unpin' : 'Pin agent',
onClick: (e: SyntheticEvent) => {
e.stopPropagation();
togglePin();
},
variant: 'primary' as const,
iconWidth: 18,
iconHeight: 18,
},
]
: []),
{
icon: Trash,
label: 'Delete',
@@ -426,16 +430,16 @@ function AgentCard({
setIsOpen={setIsMenuOpen}
options={menuOptions}
anchorRef={menuRef}
position="top-right"
position="bottom-right"
offset={{ x: 0, y: 0 }}
/>
</div>
<div className="w-full">
<div className="flex w-full items-center gap-1 px-1">
<img
src={agent.image ?? Robot}
src={agent.image && agent.image.trim() !== '' ? agent.image : Robot}
alt={`${agent.name}`}
className="h-7 w-7 rounded-full"
className="h-7 w-7 rounded-full object-contain"
/>
{agent.status === 'draft' && (
<p className="text-xs text-black opacity-50 dark:text-[#E0E0E0]">{`(Draft)`}</p>

View File

@@ -1,16 +1,21 @@
export const baseURL =
import.meta.env.VITE_API_HOST || 'https://docsapi.arc53.com';
const defaultHeaders = {
'Content-Type': 'application/json',
};
const getHeaders = (token: string | null, customHeaders = {}): HeadersInit => {
return {
...defaultHeaders,
const getHeaders = (
token: string | null,
customHeaders = {},
isFormData = false,
): HeadersInit => {
const headers: HeadersInit = {
...(token ? { Authorization: `Bearer ${token}` } : {}),
...customHeaders,
};
if (!isFormData) {
headers['Content-Type'] = 'application/json';
}
return headers;
};
const apiClient = {
@@ -44,6 +49,21 @@ const apiClient = {
return response;
}),
postFormData: (
url: string,
formData: FormData,
token: string | null,
headers = {},
signal?: AbortSignal,
): Promise<Response> => {
return fetch(`${baseURL}${url}`, {
method: 'POST',
headers: getHeaders(token, headers, true),
body: formData,
signal,
});
},
put: (
url: string,
data: any,
@@ -60,6 +80,21 @@ const apiClient = {
return response;
}),
putFormData: (
url: string,
formData: FormData,
token: string | null,
headers = {},
signal?: AbortSignal,
): Promise<Response> => {
return fetch(`${baseURL}${url}`, {
method: 'PUT',
headers: getHeaders(token, headers, true),
body: formData,
signal,
});
},
delete: (
url: string,
token: string | null,

View File

@@ -22,13 +22,13 @@ const userService = {
getAgents: (token: string | null): Promise<any> =>
apiClient.get(endpoints.USER.AGENTS, token),
createAgent: (data: any, token: string | null): Promise<any> =>
apiClient.post(endpoints.USER.CREATE_AGENT, data, token),
apiClient.postFormData(endpoints.USER.CREATE_AGENT, data, token),
updateAgent: (
agent_id: string,
data: any,
token: string | null,
): Promise<any> =>
apiClient.put(endpoints.USER.UPDATE_AGENT(agent_id), data, token),
apiClient.putFormData(endpoints.USER.UPDATE_AGENT(agent_id), data, token),
deleteAgent: (id: string, token: string | null): Promise<any> =>
apiClient.delete(endpoints.USER.DELETE_AGENT(id), token),
getPinnedAgents: (token: string | null): Promise<any> =>

View File

@@ -1,19 +1,19 @@
<svg width="22" height="18" viewBox="0 0 22 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M13.394 1.001H7.982C7.32776 1.00074 6.67989 1.12939 6.07539 1.3796C5.47089 1.62982 4.92162 1.99669 4.45896 2.45926C3.9963 2.92182 3.62932 3.47102 3.37898 4.07547C3.12865 4.67992 2.99987 5.32776 3 5.982V11.394C2.99974 12.0483 3.12842 12.6963 3.3787 13.3008C3.62897 13.9054 3.99593 14.4547 4.45861 14.9174C4.92128 15.3801 5.4706 15.747 6.07516 15.9973C6.67972 16.2476 7.32768 16.3763 7.982 16.376H13.394C14.0483 16.3763 14.6963 16.2476 15.3008 15.9973C15.9054 15.747 16.4547 15.3801 16.9174 14.9174C17.3801 14.4547 17.747 13.9054 17.9973 13.3008C18.2476 12.6963 18.3763 12.0483 18.376 11.394V5.982C18.3763 5.32768 18.2476 4.67972 17.9973 4.07516C17.747 3.4706 17.3801 2.92128 16.9174 2.45861C16.4547 1.99593 15.9054 1.62897 15.3008 1.3787C14.6963 1.12842 14.0483 0.999738 13.394 1V1.001Z" stroke="url(#paint0_linear_8958_15228)" stroke-width="1.5"/>
<path d="M18.606 12.5881H20.225C20.4968 12.5881 20.7576 12.4801 20.9498 12.2879C21.142 12.0956 21.25 11.8349 21.25 11.5631V6.43809C21.25 6.16624 21.142 5.90553 20.9498 5.7133C20.7576 5.52108 20.4968 5.41309 20.225 5.41309H18.605M3.395 12.5881H1.775C1.6404 12.5881 1.50711 12.5616 1.38275 12.5101C1.25839 12.4586 1.1454 12.3831 1.05022 12.2879C0.955035 12.1927 0.879535 12.0797 0.828023 11.9553C0.776512 11.831 0.75 11.6977 0.75 11.5631V6.43809C0.75 6.16624 0.857991 5.90553 1.05022 5.7133C1.24244 5.52108 1.50315 5.41309 1.775 5.41309H3.395" stroke="url(#paint1_linear_8958_15228)" stroke-width="1.5"/>
<path d="M1.76562 5.41323V1.31323M20.2256 5.41323L20.2156 1.31323M7.91562 5.76323V8.46123M14.0656 5.76323V8.46123M8.94062 12.5882H13.0406" stroke="url(#paint2_linear_8958_15228)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14.394 4.001H8.982C8.32776 4.00074 7.67989 4.12939 7.07539 4.3796C6.47089 4.62982 5.92162 4.99669 5.45896 5.45926C4.9963 5.92182 4.62932 6.47102 4.37898 7.07547C4.12865 7.67992 3.99987 8.32776 4 8.982V14.394C3.99974 15.0483 4.12842 15.6963 4.3787 16.3008C4.62897 16.9054 4.99593 17.4547 5.45861 17.9174C5.92128 18.3801 6.4706 18.747 7.07516 18.9973C7.67972 19.2476 8.32768 19.3763 8.982 19.376H14.394C15.0483 19.3763 15.6963 19.2476 16.3008 18.9973C16.9054 18.747 17.4547 18.3801 17.9174 17.9174C18.3801 17.4547 18.747 16.9054 18.9973 16.3008C19.2476 15.6963 19.3763 15.0483 19.376 14.394V8.982C19.3763 8.32768 19.2476 7.67972 18.9973 7.07516C18.747 6.4706 18.3801 5.92128 17.9174 5.45861C17.4547 4.99593 16.9054 4.62897 16.3008 4.3787C15.6963 4.12842 15.0483 3.99974 14.394 4V4.001Z" stroke="url(#paint0_linear_9044_3689)" stroke-width="1.5"/>
<path d="M19.606 15.5881H21.225C21.4968 15.5881 21.7576 15.4801 21.9498 15.2879C22.142 15.0956 22.25 14.8349 22.25 14.5631V9.43809C22.25 9.16624 22.142 8.90553 21.9498 8.7133C21.7576 8.52108 21.4968 8.41309 21.225 8.41309H19.605M4.395 15.5881H2.775C2.6404 15.5881 2.50711 15.5616 2.38275 15.5101C2.25839 15.4586 2.1454 15.3831 2.05022 15.2879C1.95504 15.1927 1.87953 15.0797 1.82802 14.9553C1.77651 14.831 1.75 14.6977 1.75 14.5631V9.43809C1.75 9.16624 1.85799 8.90553 2.05022 8.7133C2.24244 8.52108 2.50315 8.41309 2.775 8.41309H4.395" stroke="url(#paint1_linear_9044_3689)" stroke-width="1.5"/>
<path d="M2.76562 8.41323V4.31323M21.2256 8.41323L21.2156 4.31323M8.91562 8.76323V11.4612M15.0656 8.76323V11.4612M9.94062 15.5882H14.0406" stroke="url(#paint2_linear_9044_3689)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<defs>
<linearGradient id="paint0_linear_8958_15228" x1="10.688" y1="1" x2="10.688" y2="16.376" gradientUnits="userSpaceOnUse">
<linearGradient id="paint0_linear_9044_3689" x1="11.688" y1="4" x2="11.688" y2="19.376" gradientUnits="userSpaceOnUse">
<stop stop-color="#58E2E1"/>
<stop offset="0.524038" stop-color="#657797"/>
<stop offset="1" stop-color="#CC7871"/>
</linearGradient>
<linearGradient id="paint1_linear_8958_15228" x1="11" y1="5.41309" x2="11" y2="12.5881" gradientUnits="userSpaceOnUse">
<linearGradient id="paint1_linear_9044_3689" x1="12" y1="8.41309" x2="12" y2="15.5881" gradientUnits="userSpaceOnUse">
<stop stop-color="#58E2E1"/>
<stop offset="0.524038" stop-color="#657797"/>
<stop offset="1" stop-color="#CC7871"/>
</linearGradient>
<linearGradient id="paint2_linear_8958_15228" x1="10.9956" y1="1.31323" x2="10.9956" y2="12.5882" gradientUnits="userSpaceOnUse">
<linearGradient id="paint2_linear_9044_3689" x1="11.9956" y1="4.31323" x2="11.9956" y2="15.5882" gradientUnits="userSpaceOnUse">
<stop stop-color="#58E2E1"/>
<stop offset="0.524038" stop-color="#657797"/>
<stop offset="1" stop-color="#CC7871"/>

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB