Skip to content

Commit

Permalink
[Assistant] Delete avatar button instead of reset (#725)
Browse files Browse the repository at this point in the history
* Add rate-limited image generating endpoint

* Add generate avatar button

* add little padding for firefox focus ring

* format

* fix upload image bug

* Fix uploads, replace reset by delete

* left-align buttons

* rm avatar generation feature

* final changes to delete feature

* sys prompt min height

* padding

* Add object-cover everywhere

---------

Co-authored-by: Victor Mustar <[email protected]>
  • Loading branch information
nsarrazin and gary149 authored Jan 24, 2024
1 parent cf00c70 commit f3114ae
Show file tree
Hide file tree
Showing 8 changed files with 77 additions and 142 deletions.
4 changes: 1 addition & 3 deletions .env
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,4 @@ EXPOSE_API=true
# PUBLIC_APP_DATA_SHARING=1
# PUBLIC_APP_DISCLAIMER=1

ENABLE_ASSISTANTS=false #set to true to enable assistants feature
ASSISTANTS_GENERATE_AVATAR=true #requires an hf token, uses the model description and name to generate an avatar using a text to image model
TEXT_TO_IMAGE_MODEL="runwayml/stable-diffusion-v1-5"
ENABLE_ASSISTANTS=false #set to true to enable assistants feature
109 changes: 61 additions & 48 deletions src/lib/components/AssistantSettings.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
import { applyAction, enhance } from "$app/forms";
import { base } from "$app/paths";
import CarbonPen from "~icons/carbon/pen";
import CarbonUpload from "~icons/carbon/upload";
import { useSettingsStore } from "$lib/stores/settings";
import { page } from "$app/stores";
import IconLoading from "./icons/IconLoading.svelte";
type ActionData = {
Expand Down Expand Up @@ -41,20 +41,29 @@
let inputMessage3 = assistant?.exampleInputs[2] ?? "";
let inputMessage4 = assistant?.exampleInputs[3] ?? "";
function resetErrors() {
if (form) {
form.errors = [];
form.error = false;
}
}
function onFilesChange(e: Event) {
const inputEl = e.target as HTMLInputElement;
if (inputEl.files?.length) {
files = inputEl.files;
resetErrors();
deleteExistingAvatar = false;
}
}
function getError(field: string, returnForm: ActionData) {
return returnForm?.errors.find((error) => error.field === field)?.message ?? "";
}
let loading = false;
let deleteExistingAvatar = false;
let generateAvatar = false;
let loading = false;
</script>

<form
Expand All @@ -63,10 +72,8 @@
enctype="multipart/form-data"
use:enhance={async ({ formData }) => {
loading = true;
const avatar = formData.get("avatar");

if (avatar && typeof avatar !== "string" && avatar.size > 0 && compress) {
await compress(avatar, {
if (files?.[0] && files[0].size > 0 && compress) {
await compress(files[0], {
maxWidth: 500,
maxHeight: 500,
quality: 1,
Expand All @@ -75,6 +82,16 @@
});
}

if (deleteExistingAvatar === true) {
if (assistant?.avatar) {
// if there is an avatar we explicitly removei t
formData.set("avatar", "null");
} else {
// else we just remove it from the input
formData.delete("avatar");
}
}

return async ({ result }) => {
loading = false;
await applyAction(result);
Expand All @@ -93,73 +110,72 @@
</p>
{/if}

<div class="grid flex-1 grid-cols-2 gap-4 max-sm:grid-cols-1">
<div class="mx-1 grid flex-1 grid-cols-2 gap-4 max-sm:grid-cols-1">
<div class="flex flex-col gap-4">
<label class="truncate">
<span class="mb-1 block text-sm font-semibold">Avatar</span>
<div>
<span class="mb-1 block pb-2 text-sm font-semibold">Avatar</span>
<input
type="file"
accept="image/*"
name="avatar"
class="invisible z-10 block h-0 w-0"
disabled={generateAvatar}
id="avatar"
class="hidden"
on:change={onFilesChange}
/>
{#if (files && files[0]) || assistant?.avatar}
<div class="group relative h-12 w-12">

{#if (files && files[0]) || (assistant?.avatar && !deleteExistingAvatar)}
<div class="group relative mx-auto h-12 w-12">
{#if files && files[0]}
<img
src={URL.createObjectURL(files[0])}
alt="avatar"
class="crop mx-auto h-12 w-12 cursor-pointer rounded-full"
class="crop mx-auto h-12 w-12 cursor-pointer rounded-full object-cover"
/>
{:else if assistant?.avatar}
<img
src="{base}/settings/assistants/{assistant._id}/avatar?hash={assistant.avatar}"
alt="avatar"
class="crop mx-auto h-12 w-12 cursor-pointer rounded-full"
class="crop mx-auto h-12 w-12 cursor-pointer rounded-full object-cover"
/>
{/if}

<div
<label
for="avatar"
class="invisible absolute bottom-0 h-12 w-12 rounded-full bg-black bg-opacity-50 p-1 group-hover:visible hover:visible"
>
<CarbonPen class="mx-auto my-auto h-full cursor-pointer text-center text-white" />
</div>
</label>
</div>
<div class="mx-auto w-max pt-1">
<button
type="button"
on:click|stopPropagation|preventDefault={() => {
files = null;
deleteExistingAvatar = true;
}}
class="mx-auto w-max text-center text-xs text-gray-600 hover:underline"
>
Delete
</button>
</div>
<button
type="button"
on:click|stopPropagation|preventDefault={() => (files = null)}
class="mt-1 text-xs text-gray-600 hover:underline"
>
Reset
</button>
{:else}
<span
class="text-xs text-gray-500"
class:hover:underline={!generateAvatar}
class:cursor-pointer={!generateAvatar}>Click to upload</span
>
{/if}
<p class="text-xs text-red-500">{getError("avatar", form)}</p>
{#if !files?.[0] && $page.data.avatarGeneration && !assistant?.avatar}
<label class="text-xs text-gray-500">
<input
type="checkbox"
name="generateAvatar"
class="text-xs text-gray-500"
bind:checked={generateAvatar}
/>
Generate avatar from description
</label>
<div class="mb-1 flex w-max flex-row gap-4">
<label
for="avatar"
class="btn flex h-8 rounded-lg border bg-white px-3 py-1 text-gray-500 shadow-sm transition-all hover:bg-gray-100"
>
<CarbonUpload class="mr-2 text-xs " /> Upload
</label>
</div>
<p class="text-xs text-red-500">{getError("avatar", form)}</p>
{/if}
</label>
</div>

<label>
<span class="mb-1 text-sm font-semibold">Name</span>
<input
name="name"
class=" w-full rounded-lg border-2 border-gray-200 bg-gray-100 p-2"
class="w-full rounded-lg border-2 border-gray-200 bg-gray-100 p-2"
placeholder="My awesome model"
value={assistant?.name ?? ""}
/>
Expand Down Expand Up @@ -228,14 +244,12 @@

<label class="flex flex-col">
<span class="mb-1 text-sm font-semibold"> Instructions (system prompt) </span>

<textarea
name="preprompt"
class="flex-1 rounded-lg border-2 border-gray-200 bg-gray-100 p-2 text-sm"
class="min-h-[8lh] flex-1 rounded-lg border-2 border-gray-200 bg-gray-100 p-2 text-sm"
placeholder="You'll act as..."
value={assistant?.preprompt ?? ""}
/>

<p class="text-xs text-red-500">{getError("preprompt", form)}</p>
</label>
</div>
Expand All @@ -245,7 +259,6 @@
href={assistant ? `${base}/settings/assistants/${assistant?._id}` : `${base}/settings`}
class="rounded-full bg-gray-200 px-8 py-2 font-semibold text-gray-600">Cancel</a
>

<button
type="submit"
disabled={loading}
Expand Down
2 changes: 1 addition & 1 deletion src/lib/components/NavConversationItem.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
<img
src="{base}/settings/assistants/{conv.assistantId}/avatar?hash={conv.avatarHash}"
alt="Assistant avatar"
class="mr-1.5 inline size-4 rounded-full"
class="mr-1.5 inline size-4 rounded-full object-cover"
/>
{conv.title.replace(/\p{Emoji}/gu, "")}
{:else if conv.assistantId}
Expand Down
24 changes: 0 additions & 24 deletions src/lib/utils/generateAvatar.ts

This file was deleted.

3 changes: 0 additions & 3 deletions src/routes/+layout.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,6 @@ import {
YDC_API_KEY,
USE_LOCAL_WEBSEARCH,
ENABLE_ASSISTANTS,
ASSISTANTS_GENERATE_AVATAR,
TEXT_TO_IMAGE_MODEL,
} from "$env/static/private";
import { ObjectId } from "mongodb";
import type { ConvSidebar } from "$lib/types/ConvSidebar";
Expand Down Expand Up @@ -163,7 +161,6 @@ export const load: LayoutServerLoad = async ({ locals, depends }) => {
email: locals.user.email,
},
assistant,
avatarGeneration: ASSISTANTS_GENERATE_AVATAR === "true" && !!TEXT_TO_IMAGE_MODEL,
enableAssistants,
loginRequired,
loginEnabled: requiresUser,
Expand Down
2 changes: 1 addition & 1 deletion src/routes/settings/+layout.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@
Application Settings
</a>
</div>
<div class="col-span-1 overflow-y-auto max-md:pt-6 md:col-span-2">
<div class="col-span-1 overflow-y-auto px-4 max-md:-mx-4 max-md:pt-6 md:col-span-2">
<slot />
</div>

Expand Down
45 changes: 13 additions & 32 deletions src/routes/settings/assistants/[assistantId]/edit/+page.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,6 @@ import { ObjectId } from "mongodb";
import { z } from "zod";
import sizeof from "image-size";
import { sha256 } from "$lib/utils/sha256";
import { ASSISTANTS_GENERATE_AVATAR, HF_TOKEN } from "$env/static/private";
import { generateAvatar } from "$lib/utils/generateAvatar";
import { timeout } from "$lib/utils/timeout";

const newAsssistantSchema = z.object({
name: z.string().min(1),
Expand All @@ -20,11 +17,7 @@ const newAsssistantSchema = z.object({
exampleInput2: z.string().optional(),
exampleInput3: z.string().optional(),
exampleInput4: z.string().optional(),
avatar: z.instanceof(File).optional(),
generateAvatar: z
.literal("on")
.optional()
.transform((el) => !!el),
avatar: z.union([z.instanceof(File), z.literal("null")]).optional(),
});

const uploadAvatar = async (avatar: File, assistantId: ObjectId): Promise<string> => {
Expand Down Expand Up @@ -87,8 +80,10 @@ export const actions: Actions = {
parse?.data?.exampleInput4 ?? "",
].filter((input) => !!input);

const deleteAvatar = parse.data.avatar === "null";

let hash;
if (parse.data.avatar && parse.data.avatar.size > 0) {
if (parse.data.avatar && parse.data.avatar !== "null" && parse.data.avatar.size > 0) {
const dims = sizeof(Buffer.from(await parse.data.avatar.arrayBuffer()));

if ((dims.height ?? 1000) > 512 || (dims.width ?? 1000) > 512) {
Expand All @@ -106,28 +101,14 @@ export const actions: Actions = {
}

hash = await uploadAvatar(parse.data.avatar, assistant._id);
} else if (
ASSISTANTS_GENERATE_AVATAR === "true" &&
HF_TOKEN !== "" &&
parse.data.generateAvatar
) {
try {
const avatar = await timeout(
generateAvatar(parse.data.description, parse.data.name),
30000
);

hash = await uploadAvatar(avatar, assistant._id);
} catch (err) {
return fail(400, {
error: true,
errors: [
{
field: "avatar",
message: "Avatar generation failed. Try again or disable the feature.",
},
],
});
} else if (deleteAvatar) {
// delete the avatar
const fileCursor = collections.bucket.find({ filename: assistant._id.toString() });

let fileId = await fileCursor.next();
while (fileId) {
await collections.bucket.delete(fileId._id);
fileId = await fileCursor.next();
}
}

Expand All @@ -140,7 +121,7 @@ export const actions: Actions = {
createdByName: locals.user?.username ?? locals.user?.name,
...parse.data,
exampleInputs,
avatar: hash ?? assistant.avatar,
avatar: deleteAvatar ? undefined : hash ?? assistant.avatar,
createdAt: new Date(),
updatedAt: new Date(),
}
Expand Down
Loading

0 comments on commit f3114ae

Please sign in to comment.