Spaces:
Runtime error
Runtime error
Convert all assistants avatar to jpeg server-side (#762)
Browse files* Convert all assistants to jpeg server side, and rename endpoint appropriately
* Improve avatar validation/error display
* preserve aspect ratio on resize
* Update src/lib/components/chat/ChatMessages.svelte
Co-authored-by: Mishig <mishig.davaadorj@coloradocollege.edu>
---------
Co-authored-by: Mishig <mishig.davaadorj@coloradocollege.edu>
- src/lib/components/AssistantSettings.svelte +14 -3
- src/lib/components/NavConversationItem.svelte +1 -1
- src/lib/components/chat/AssistantIntroduction.svelte +1 -1
- src/lib/components/chat/ChatMessages.svelte +2 -2
- src/routes/assistant/[assistantId]/+page.svelte +2 -1
- src/routes/assistants/+page.svelte +1 -1
- src/routes/settings/+layout.svelte +1 -1
- src/routes/settings/assistants/[assistantId]/+page.svelte +1 -1
- src/routes/settings/assistants/[assistantId]/{avatar → avatar.jpg}/+server.ts +1 -5
- src/routes/settings/assistants/[assistantId]/edit/+page.server.ts +11 -6
- src/routes/settings/assistants/new/+page.server.ts +10 -12
src/lib/components/AssistantSettings.svelte
CHANGED
|
@@ -50,7 +50,14 @@
|
|
| 50 |
|
| 51 |
function onFilesChange(e: Event) {
|
| 52 |
const inputEl = e.target as HTMLInputElement;
|
| 53 |
-
if (inputEl.files?.length) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
files = inputEl.files;
|
| 55 |
resetErrors();
|
| 56 |
deleteExistingAvatar = false;
|
|
@@ -90,6 +97,10 @@
|
|
| 90 |
// else we just remove it from the input
|
| 91 |
formData.delete("avatar");
|
| 92 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 93 |
}
|
| 94 |
|
| 95 |
return async ({ result }) => {
|
|
@@ -135,7 +146,7 @@
|
|
| 135 |
/>
|
| 136 |
{:else if assistant?.avatar}
|
| 137 |
<img
|
| 138 |
-
src="{base}/settings/assistants/{assistant._id}/avatar?hash={assistant.avatar}"
|
| 139 |
alt="avatar"
|
| 140 |
class="crop mx-auto h-12 w-12 cursor-pointer rounded-full object-cover"
|
| 141 |
/>
|
|
@@ -169,8 +180,8 @@
|
|
| 169 |
<CarbonUpload class="mr-2 text-xs " /> Upload
|
| 170 |
</label>
|
| 171 |
</div>
|
| 172 |
-
<p class="text-xs text-red-500">{getError("avatar", form)}</p>
|
| 173 |
{/if}
|
|
|
|
| 174 |
</div>
|
| 175 |
|
| 176 |
<label>
|
|
|
|
| 50 |
|
| 51 |
function onFilesChange(e: Event) {
|
| 52 |
const inputEl = e.target as HTMLInputElement;
|
| 53 |
+
if (inputEl.files?.length && inputEl.files[0].size > 0) {
|
| 54 |
+
if (!inputEl.files[0].type.includes("image")) {
|
| 55 |
+
inputEl.files = null;
|
| 56 |
+
files = null;
|
| 57 |
+
|
| 58 |
+
form = { error: true, errors: [{ field: "avatar", message: "Only images are allowed" }] };
|
| 59 |
+
return;
|
| 60 |
+
}
|
| 61 |
files = inputEl.files;
|
| 62 |
resetErrors();
|
| 63 |
deleteExistingAvatar = false;
|
|
|
|
| 97 |
// else we just remove it from the input
|
| 98 |
formData.delete("avatar");
|
| 99 |
}
|
| 100 |
+
} else {
|
| 101 |
+
if (files === null) {
|
| 102 |
+
formData.delete("avatar");
|
| 103 |
+
}
|
| 104 |
}
|
| 105 |
|
| 106 |
return async ({ result }) => {
|
|
|
|
| 146 |
/>
|
| 147 |
{:else if assistant?.avatar}
|
| 148 |
<img
|
| 149 |
+
src="{base}/settings/assistants/{assistant._id}/avatar.jpg?hash={assistant.avatar}"
|
| 150 |
alt="avatar"
|
| 151 |
class="crop mx-auto h-12 w-12 cursor-pointer rounded-full object-cover"
|
| 152 |
/>
|
|
|
|
| 180 |
<CarbonUpload class="mr-2 text-xs " /> Upload
|
| 181 |
</label>
|
| 182 |
</div>
|
|
|
|
| 183 |
{/if}
|
| 184 |
+
<p class="text-xs text-red-500">{getError("avatar", form)}</p>
|
| 185 |
</div>
|
| 186 |
|
| 187 |
<label>
|
src/lib/components/NavConversationItem.svelte
CHANGED
|
@@ -36,7 +36,7 @@
|
|
| 36 |
{/if}
|
| 37 |
{#if conv.avatarHash}
|
| 38 |
<img
|
| 39 |
-
src="{base}/settings/assistants/{conv.assistantId}/avatar?hash={conv.avatarHash}"
|
| 40 |
alt="Assistant avatar"
|
| 41 |
class="mr-1.5 inline size-4 flex-none rounded-full object-cover"
|
| 42 |
/>
|
|
|
|
| 36 |
{/if}
|
| 37 |
{#if conv.avatarHash}
|
| 38 |
<img
|
| 39 |
+
src="{base}/settings/assistants/{conv.assistantId}/avatar.jpg?hash={conv.avatarHash}"
|
| 40 |
alt="Assistant avatar"
|
| 41 |
class="mr-1.5 inline size-4 flex-none rounded-full object-cover"
|
| 42 |
/>
|
src/lib/components/chat/AssistantIntroduction.svelte
CHANGED
|
@@ -21,7 +21,7 @@
|
|
| 21 |
>
|
| 22 |
{#if assistant.avatar}
|
| 23 |
<img
|
| 24 |
-
src={`${base}/settings/assistants/${assistant._id.toString()}/avatar?hash=${
|
| 25 |
assistant.avatar
|
| 26 |
}`}
|
| 27 |
alt="avatar"
|
|
|
|
| 21 |
>
|
| 22 |
{#if assistant.avatar}
|
| 23 |
<img
|
| 24 |
+
src={`${base}/settings/assistants/${assistant._id.toString()}/avatar.jpg?hash=${
|
| 25 |
assistant.avatar
|
| 26 |
}`}
|
| 27 |
alt="avatar"
|
src/lib/components/chat/ChatMessages.svelte
CHANGED
|
@@ -54,8 +54,8 @@
|
|
| 54 |
>
|
| 55 |
{#if $page.data?.assistant.avatar}
|
| 56 |
<img
|
| 57 |
-
src="{base}/settings/assistants/{$page.data?.assistant._id.toString()}/avatar?hash=${$page
|
| 58 |
-
.data
|
| 59 |
alt="Avatar"
|
| 60 |
class="size-5 rounded-full object-cover"
|
| 61 |
/>
|
|
|
|
| 54 |
>
|
| 55 |
{#if $page.data?.assistant.avatar}
|
| 56 |
<img
|
| 57 |
+
src="{base}/settings/assistants/{$page.data?.assistant._id.toString()}/avatar.jpg?hash=${$page
|
| 58 |
+
.data.assistant.avatar}"
|
| 59 |
alt="Avatar"
|
| 60 |
class="size-5 rounded-full object-cover"
|
| 61 |
/>
|
src/routes/assistant/[assistantId]/+page.svelte
CHANGED
|
@@ -50,7 +50,8 @@
|
|
| 50 |
{#if data.assistant.avatar}
|
| 51 |
<img
|
| 52 |
class="size-16 flex-none rounded-full object-cover sm:size-24"
|
| 53 |
-
src="{base}/settings/assistants/{data.assistant._id}/avatar?hash={data.assistant
|
|
|
|
| 54 |
alt="avatar"
|
| 55 |
/>
|
| 56 |
{:else}
|
|
|
|
| 50 |
{#if data.assistant.avatar}
|
| 51 |
<img
|
| 52 |
class="size-16 flex-none rounded-full object-cover sm:size-24"
|
| 53 |
+
src="{base}/settings/assistants/{data.assistant._id}/avatar.jpg?hash={data.assistant
|
| 54 |
+
.avatar}"
|
| 55 |
alt="avatar"
|
| 56 |
/>
|
| 57 |
{:else}
|
src/routes/assistants/+page.svelte
CHANGED
|
@@ -79,7 +79,7 @@
|
|
| 79 |
>
|
| 80 |
{#if assistant.avatar}
|
| 81 |
<img
|
| 82 |
-
src="{base}/settings/assistants/{assistant._id}/avatar"
|
| 83 |
alt="Avatar"
|
| 84 |
class="mb-2 aspect-square size-12 flex-none rounded-full object-cover sm:mb-6 sm:size-20"
|
| 85 |
/>
|
|
|
|
| 79 |
>
|
| 80 |
{#if assistant.avatar}
|
| 81 |
<img
|
| 82 |
+
src="{base}/settings/assistants/{assistant._id}/avatar.jpg"
|
| 83 |
alt="Avatar"
|
| 84 |
class="mb-2 aspect-square size-12 flex-none rounded-full object-cover sm:mb-6 sm:size-20"
|
| 85 |
/>
|
src/routes/settings/+layout.svelte
CHANGED
|
@@ -101,7 +101,7 @@
|
|
| 101 |
>
|
| 102 |
{#if assistant.avatar}
|
| 103 |
<img
|
| 104 |
-
src="{base}/settings/assistants/{assistant._id.toString()}/avatar?hash={assistant.avatar}"
|
| 105 |
alt="Avatar"
|
| 106 |
class="h-6 w-6 rounded-full object-cover"
|
| 107 |
/>
|
|
|
|
| 101 |
>
|
| 102 |
{#if assistant.avatar}
|
| 103 |
<img
|
| 104 |
+
src="{base}/settings/assistants/{assistant._id.toString()}/avatar.jpg?hash={assistant.avatar}"
|
| 105 |
alt="Avatar"
|
| 106 |
class="h-6 w-6 rounded-full object-cover"
|
| 107 |
/>
|
src/routes/settings/assistants/[assistantId]/+page.svelte
CHANGED
|
@@ -31,7 +31,7 @@
|
|
| 31 |
{#if assistant?.avatar}
|
| 32 |
<!-- crop image if not square -->
|
| 33 |
<img
|
| 34 |
-
src={`${base}/settings/assistants/${assistant?._id}/avatar?hash=${assistant?.avatar}`}
|
| 35 |
alt="Avatar"
|
| 36 |
class="size-16 flex-none rounded-full object-cover sm:size-24"
|
| 37 |
/>
|
|
|
|
| 31 |
{#if assistant?.avatar}
|
| 32 |
<!-- crop image if not square -->
|
| 33 |
<img
|
| 34 |
+
src={`${base}/settings/assistants/${assistant?._id}/avatar.jpg?hash=${assistant?.avatar}`}
|
| 35 |
alt="Avatar"
|
| 36 |
class="size-16 flex-none rounded-full object-cover sm:size-24"
|
| 37 |
/>
|
src/routes/settings/assistants/[assistantId]/{avatar → avatar.jpg}/+server.ts
RENAMED
|
@@ -17,11 +17,7 @@ export const GET: RequestHandler = async ({ params }) => {
|
|
| 17 |
|
| 18 |
const fileId = collections.bucket.find({ filename: assistant._id.toString() });
|
| 19 |
|
| 20 |
-
let mime = "";
|
| 21 |
-
|
| 22 |
const content = await fileId.next().then(async (file) => {
|
| 23 |
-
mime = file?.metadata?.mime;
|
| 24 |
-
|
| 25 |
if (!file?._id) {
|
| 26 |
throw error(404, "Avatar not found");
|
| 27 |
}
|
|
@@ -40,7 +36,7 @@ export const GET: RequestHandler = async ({ params }) => {
|
|
| 40 |
|
| 41 |
return new Response(content, {
|
| 42 |
headers: {
|
| 43 |
-
"Content-Type":
|
| 44 |
},
|
| 45 |
});
|
| 46 |
};
|
|
|
|
| 17 |
|
| 18 |
const fileId = collections.bucket.find({ filename: assistant._id.toString() });
|
| 19 |
|
|
|
|
|
|
|
| 20 |
const content = await fileId.next().then(async (file) => {
|
|
|
|
|
|
|
| 21 |
if (!file?._id) {
|
| 22 |
throw error(404, "Avatar not found");
|
| 23 |
}
|
|
|
|
| 36 |
|
| 37 |
return new Response(content, {
|
| 38 |
headers: {
|
| 39 |
+
"Content-Type": "image/jpeg",
|
| 40 |
},
|
| 41 |
});
|
| 42 |
};
|
src/routes/settings/assistants/[assistantId]/edit/+page.server.ts
CHANGED
|
@@ -5,9 +5,10 @@ import { fail, type Actions, redirect } from "@sveltejs/kit";
|
|
| 5 |
import { ObjectId } from "mongodb";
|
| 6 |
|
| 7 |
import { z } from "zod";
|
| 8 |
-
import sizeof from "image-size";
|
| 9 |
import { sha256 } from "$lib/utils/sha256";
|
| 10 |
|
|
|
|
|
|
|
| 11 |
const newAsssistantSchema = z.object({
|
| 12 |
name: z.string().min(1),
|
| 13 |
modelId: z.string().min(1),
|
|
@@ -84,10 +85,14 @@ export const actions: Actions = {
|
|
| 84 |
|
| 85 |
let hash;
|
| 86 |
if (parse.data.avatar && parse.data.avatar !== "null" && parse.data.avatar.size > 0) {
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 91 |
return fail(400, { error: true, errors });
|
| 92 |
}
|
| 93 |
|
|
@@ -100,7 +105,7 @@ export const actions: Actions = {
|
|
| 100 |
fileId = await fileCursor.next();
|
| 101 |
}
|
| 102 |
|
| 103 |
-
hash = await uploadAvatar(
|
| 104 |
} else if (deleteAvatar) {
|
| 105 |
// delete the avatar
|
| 106 |
const fileCursor = collections.bucket.find({ filename: assistant._id.toString() });
|
|
|
|
| 5 |
import { ObjectId } from "mongodb";
|
| 6 |
|
| 7 |
import { z } from "zod";
|
|
|
|
| 8 |
import { sha256 } from "$lib/utils/sha256";
|
| 9 |
|
| 10 |
+
import sharp from "sharp";
|
| 11 |
+
|
| 12 |
const newAsssistantSchema = z.object({
|
| 13 |
name: z.string().min(1),
|
| 14 |
modelId: z.string().min(1),
|
|
|
|
| 85 |
|
| 86 |
let hash;
|
| 87 |
if (parse.data.avatar && parse.data.avatar !== "null" && parse.data.avatar.size > 0) {
|
| 88 |
+
let image;
|
| 89 |
+
try {
|
| 90 |
+
image = await sharp(await parse.data.avatar.arrayBuffer())
|
| 91 |
+
.resize(512, 512, { fit: "inside" })
|
| 92 |
+
.jpeg({ quality: 80 })
|
| 93 |
+
.toBuffer();
|
| 94 |
+
} catch (e) {
|
| 95 |
+
const errors = [{ field: "avatar", message: (e as Error).message }];
|
| 96 |
return fail(400, { error: true, errors });
|
| 97 |
}
|
| 98 |
|
|
|
|
| 105 |
fileId = await fileCursor.next();
|
| 106 |
}
|
| 107 |
|
| 108 |
+
hash = await uploadAvatar(new File([image], "avatar.jpg"), assistant._id);
|
| 109 |
} else if (deleteAvatar) {
|
| 110 |
// delete the avatar
|
| 111 |
const fileCursor = collections.bucket.find({ filename: assistant._id.toString() });
|
src/routes/settings/assistants/new/+page.server.ts
CHANGED
|
@@ -5,8 +5,8 @@ import { fail, type Actions, redirect } from "@sveltejs/kit";
|
|
| 5 |
import { ObjectId } from "mongodb";
|
| 6 |
|
| 7 |
import { z } from "zod";
|
| 8 |
-
import sizeof from "image-size";
|
| 9 |
import { sha256 } from "$lib/utils/sha256";
|
|
|
|
| 10 |
|
| 11 |
const newAsssistantSchema = z.object({
|
| 12 |
name: z.string().min(1),
|
|
@@ -74,20 +74,18 @@ export const actions: Actions = {
|
|
| 74 |
|
| 75 |
let hash;
|
| 76 |
if (parse.data.avatar && parse.data.avatar.size > 0) {
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
{
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
},
|
| 86 |
-
];
|
| 87 |
return fail(400, { error: true, errors });
|
| 88 |
}
|
| 89 |
|
| 90 |
-
hash = await uploadAvatar(
|
| 91 |
}
|
| 92 |
|
| 93 |
const { insertedId } = await collections.assistants.insertOne({
|
|
|
|
| 5 |
import { ObjectId } from "mongodb";
|
| 6 |
|
| 7 |
import { z } from "zod";
|
|
|
|
| 8 |
import { sha256 } from "$lib/utils/sha256";
|
| 9 |
+
import sharp from "sharp";
|
| 10 |
|
| 11 |
const newAsssistantSchema = z.object({
|
| 12 |
name: z.string().min(1),
|
|
|
|
| 74 |
|
| 75 |
let hash;
|
| 76 |
if (parse.data.avatar && parse.data.avatar.size > 0) {
|
| 77 |
+
let image;
|
| 78 |
+
try {
|
| 79 |
+
image = await sharp(await parse.data.avatar.arrayBuffer())
|
| 80 |
+
.resize(512, 512, { fit: "inside" })
|
| 81 |
+
.jpeg({ quality: 80 })
|
| 82 |
+
.toBuffer();
|
| 83 |
+
} catch (e) {
|
| 84 |
+
const errors = [{ field: "avatar", message: (e as Error).message }];
|
|
|
|
|
|
|
| 85 |
return fail(400, { error: true, errors });
|
| 86 |
}
|
| 87 |
|
| 88 |
+
hash = await uploadAvatar(new File([image], "avatar.jpg"), newAssistantId);
|
| 89 |
}
|
| 90 |
|
| 91 |
const { insertedId } = await collections.assistants.insertOne({
|