Skip to content

[Assistant] Delete avatar button instead of reset #725

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 12 commits into from
Jan 24, 2024
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
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