nsarrazin HF staff Mishig commited on
Commit
91ec91f
1 Parent(s): 0b2a549

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 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?.assistant.avatar}"
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.avatar}"
 
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": mime ?? "application/octet-stream",
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
- const dims = sizeof(Buffer.from(await parse.data.avatar.arrayBuffer()));
88
-
89
- if ((dims.height ?? 1000) > 512 || (dims.width ?? 1000) > 512) {
90
- const errors = [{ field: "avatar", message: "Avatar too big" }];
 
 
 
 
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(parse.data.avatar, assistant._id);
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
- const dims = sizeof(Buffer.from(await parse.data.avatar.arrayBuffer()));
78
-
79
- if ((dims.height ?? 1000) > 512 || (dims.width ?? 1000) > 512) {
80
- const errors = [
81
- {
82
- field: "avatar",
83
- message:
84
- "Avatar is too big. Please make sure the size of your avatar is no bigger than 512px by 512px.",
85
- },
86
- ];
87
  return fail(400, { error: true, errors });
88
  }
89
 
90
- hash = await uploadAvatar(parse.data.avatar, newAssistantId);
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({