Spaces:
Running
Running
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 <[email protected]>
---------
Co-authored-by: Mishig <[email protected]>
- 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({
|