Skip to content

πŸ–ΌοΈ feat: Avatar GIF Support & Dynamic Extensions #7657

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jun 2, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 20 additions & 5 deletions api/server/controllers/agents/v1.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const {
} = require('~/models/Agent');
const { uploadImageBuffer, filterFile } = require('~/server/services/Files/process');
const { getStrategyFunctions } = require('~/server/services/Files/strategies');
const { resizeAvatar } = require('~/server/services/Files/images/avatar');
const { refreshS3Url } = require('~/server/services/Files/S3/crud');
const { updateAction, getActions } = require('~/models/Action');
const { updateAgentProjects } = require('~/models/Agent');
Expand Down Expand Up @@ -373,12 +374,26 @@ const uploadAgentAvatarHandler = async (req, res) => {
}

const buffer = await fs.readFile(req.file.path);
const image = await uploadImageBuffer({
req,
context: FileContext.avatar,
metadata: { buffer },

const fileStrategy = req.app.locals.fileStrategy;

const resizedBuffer = await resizeAvatar({
userId: req.user.id,
input: buffer,
});

const { processAvatar } = getStrategyFunctions(fileStrategy);
const avatarUrl = await processAvatar({
buffer: resizedBuffer,
userId: req.user.id,
manual: 'false',
});

const image = {
filepath: avatarUrl,
source: fileStrategy,
};

let _avatar;
try {
const agent = await getAgent({ id: agent_id });
Expand All @@ -403,7 +418,7 @@ const uploadAgentAvatarHandler = async (req, res) => {
const data = {
avatar: {
filepath: image.filepath,
source: req.app.locals.fileStrategy,
source: image.source,
},
};

Expand Down
6 changes: 5 additions & 1 deletion api/server/services/Files/Azure/images.js
Original file line number Diff line number Diff line change
Expand Up @@ -98,10 +98,14 @@ async function prepareAzureImageURL(req, file) {
*/
async function processAzureAvatar({ buffer, userId, manual, basePath = 'images', containerName }) {
try {
const metadata = await sharp(buffer).metadata();
const extension = metadata.format === 'gif' ? 'gif' : 'png';
const fileName = `avatar.${extension}`;

const downloadURL = await saveBufferToAzure({
userId,
buffer,
fileName: 'avatar.png',
fileName,
basePath,
containerName,
});
Expand Down
6 changes: 5 additions & 1 deletion api/server/services/Files/Firebase/images.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,10 +88,14 @@ async function prepareImageURL(req, file) {
*/
async function processFirebaseAvatar({ buffer, userId, manual }) {
try {
const metadata = await sharp(buffer).metadata();
const extension = metadata.format === 'gif' ? 'gif' : 'png';
const fileName = `avatar.${extension}`;

const downloadURL = await saveBufferToFirebase({
userId,
buffer,
fileName: 'avatar.png',
fileName,
});

const isManual = manual === 'true';
Expand Down
5 changes: 4 additions & 1 deletion api/server/services/Files/Local/images.js
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,10 @@ async function processLocalAvatar({ buffer, userId, manual }) {
userId,
);

const fileName = `avatar-${new Date().getTime()}.png`;
const metadata = await sharp(buffer).metadata();
const extension = metadata.format === 'gif' ? 'gif' : 'png';

const fileName = `avatar-${new Date().getTime()}.${extension}`;
const urlRoute = `/images/${userId}/${fileName}`;
const avatarPath = path.join(userDir, fileName);

Expand Down
6 changes: 5 additions & 1 deletion api/server/services/Files/S3/images.js
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,11 @@ async function prepareImageURLS3(req, file) {
*/
async function processS3Avatar({ buffer, userId, manual, basePath = defaultBasePath }) {
try {
const downloadURL = await saveBufferToS3({ userId, buffer, fileName: 'avatar.png', basePath });
const metadata = await sharp(buffer).metadata();
const extension = metadata.format === 'gif' ? 'gif' : 'png';
const fileName = `avatar.${extension}`;

const downloadURL = await saveBufferToS3({ userId, buffer, fileName, basePath });
if (manual === 'true') {
await updateUser(userId, { avatar: downloadURL });
}
Expand Down
19 changes: 18 additions & 1 deletion api/server/services/Files/images/avatar.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,25 @@ async function resizeAvatar({ userId, input, desiredFormat = EImageOutputType.PN
throw new Error('Invalid input type. Expected URL, Buffer, or File.');
}

const { width, height } = await sharp(imageBuffer).metadata();
const metadata = await sharp(imageBuffer).metadata();
const { width, height } = metadata;
const minSize = Math.min(width, height);

if (metadata.format === 'gif') {
const resizedBuffer = await sharp(imageBuffer, { animated: true })
.extract({
left: Math.floor((width - minSize) / 2),
top: Math.floor((height - minSize) / 2),
width: minSize,
height: minSize,
})
.resize(250, 250)
.gif()
.toBuffer();

return resizedBuffer;
}

const squaredBuffer = await sharp(imageBuffer)
.extract({
left: Math.floor((width - minSize) / 2),
Expand Down
1 change: 1 addition & 0 deletions client/src/components/SidePanel/Agents/Images.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export const AgentAvatarRender = ({
width="80"
height="80"
style={{ opacity: progress < 1 ? 0.4 : 1 }}
key={url || 'default-key'}
/>
{progress < 1 && (
<div className="absolute inset-0 flex items-center justify-center bg-black/5 text-white">
Expand Down