|
7 | 7 | import { applyAction, enhance } from "$app/forms";
|
8 | 8 | import { base } from "$app/paths";
|
9 | 9 | import CarbonPen from "~icons/carbon/pen";
|
| 10 | + import CarbonUpload from "~icons/carbon/upload"; |
10 | 11 | import { useSettingsStore } from "$lib/stores/settings";
|
11 |
| - import { page } from "$app/stores"; |
12 | 12 | import IconLoading from "./icons/IconLoading.svelte";
|
13 | 13 |
|
14 | 14 | type ActionData = {
|
|
41 | 41 | let inputMessage3 = assistant?.exampleInputs[2] ?? "";
|
42 | 42 | let inputMessage4 = assistant?.exampleInputs[3] ?? "";
|
43 | 43 |
|
| 44 | + function resetErrors() { |
| 45 | + if (form) { |
| 46 | + form.errors = []; |
| 47 | + form.error = false; |
| 48 | + } |
| 49 | + } |
| 50 | +
|
44 | 51 | function onFilesChange(e: Event) {
|
45 | 52 | const inputEl = e.target as HTMLInputElement;
|
46 | 53 | if (inputEl.files?.length) {
|
47 | 54 | files = inputEl.files;
|
| 55 | + resetErrors(); |
| 56 | + deleteExistingAvatar = false; |
48 | 57 | }
|
49 | 58 | }
|
50 | 59 |
|
51 | 60 | function getError(field: string, returnForm: ActionData) {
|
52 | 61 | return returnForm?.errors.find((error) => error.field === field)?.message ?? "";
|
53 | 62 | }
|
54 | 63 |
|
55 |
| - let loading = false; |
| 64 | + let deleteExistingAvatar = false; |
56 | 65 |
|
57 |
| - let generateAvatar = false; |
| 66 | + let loading = false; |
58 | 67 | </script>
|
59 | 68 |
|
60 | 69 | <form
|
|
63 | 72 | enctype="multipart/form-data"
|
64 | 73 | use:enhance={async ({ formData }) => {
|
65 | 74 | loading = true;
|
66 |
| - const avatar = formData.get("avatar"); |
67 |
| - |
68 |
| - if (avatar && typeof avatar !== "string" && avatar.size > 0 && compress) { |
69 |
| - await compress(avatar, { |
| 75 | + if (files?.[0] && files[0].size > 0 && compress) { |
| 76 | + await compress(files[0], { |
70 | 77 | maxWidth: 500,
|
71 | 78 | maxHeight: 500,
|
72 | 79 | quality: 1,
|
|
75 | 82 | });
|
76 | 83 | }
|
77 | 84 |
|
| 85 | + if (deleteExistingAvatar === true) { |
| 86 | + if (assistant?.avatar) { |
| 87 | + // if there is an avatar we explicitly removei t |
| 88 | + formData.set("avatar", "null"); |
| 89 | + } else { |
| 90 | + // else we just remove it from the input |
| 91 | + formData.delete("avatar"); |
| 92 | + } |
| 93 | + } |
| 94 | + |
78 | 95 | return async ({ result }) => {
|
79 | 96 | loading = false;
|
80 | 97 | await applyAction(result);
|
|
93 | 110 | </p>
|
94 | 111 | {/if}
|
95 | 112 |
|
96 |
| - <div class="grid flex-1 grid-cols-2 gap-4 max-sm:grid-cols-1"> |
| 113 | + <div class="mx-1 grid flex-1 grid-cols-2 gap-4 max-sm:grid-cols-1"> |
97 | 114 | <div class="flex flex-col gap-4">
|
98 |
| - <label class="truncate"> |
99 |
| - <span class="mb-1 block text-sm font-semibold">Avatar</span> |
| 115 | + <div> |
| 116 | + <span class="mb-1 block pb-2 text-sm font-semibold">Avatar</span> |
100 | 117 | <input
|
101 | 118 | type="file"
|
102 | 119 | accept="image/*"
|
103 | 120 | name="avatar"
|
104 |
| - class="invisible z-10 block h-0 w-0" |
105 |
| - disabled={generateAvatar} |
| 121 | + id="avatar" |
| 122 | + class="hidden" |
106 | 123 | on:change={onFilesChange}
|
107 | 124 | />
|
108 |
| - {#if (files && files[0]) || assistant?.avatar} |
109 |
| - <div class="group relative h-12 w-12"> |
| 125 | + |
| 126 | + {#if (files && files[0]) || (assistant?.avatar && !deleteExistingAvatar)} |
| 127 | + <div class="group relative mx-auto h-12 w-12"> |
110 | 128 | {#if files && files[0]}
|
111 | 129 | <img
|
112 | 130 | src={URL.createObjectURL(files[0])}
|
113 | 131 | alt="avatar"
|
114 |
| - class="crop mx-auto h-12 w-12 cursor-pointer rounded-full" |
| 132 | + class="crop mx-auto h-12 w-12 cursor-pointer rounded-full object-cover" |
115 | 133 | />
|
116 | 134 | {:else if assistant?.avatar}
|
117 | 135 | <img
|
118 | 136 | src="{base}/settings/assistants/{assistant._id}/avatar?hash={assistant.avatar}"
|
119 | 137 | alt="avatar"
|
120 |
| - class="crop mx-auto h-12 w-12 cursor-pointer rounded-full" |
| 138 | + class="crop mx-auto h-12 w-12 cursor-pointer rounded-full object-cover" |
121 | 139 | />
|
122 | 140 | {/if}
|
123 | 141 |
|
124 |
| - <div |
| 142 | + <label |
| 143 | + for="avatar" |
125 | 144 | class="invisible absolute bottom-0 h-12 w-12 rounded-full bg-black bg-opacity-50 p-1 group-hover:visible hover:visible"
|
126 | 145 | >
|
127 | 146 | <CarbonPen class="mx-auto my-auto h-full cursor-pointer text-center text-white" />
|
128 |
| - </div> |
| 147 | + </label> |
| 148 | + </div> |
| 149 | + <div class="mx-auto w-max pt-1"> |
| 150 | + <button |
| 151 | + type="button" |
| 152 | + on:click|stopPropagation|preventDefault={() => { |
| 153 | + files = null; |
| 154 | + deleteExistingAvatar = true; |
| 155 | + }} |
| 156 | + class="mx-auto w-max text-center text-xs text-gray-600 hover:underline" |
| 157 | + > |
| 158 | + Delete |
| 159 | + </button> |
129 | 160 | </div>
|
130 |
| - <button |
131 |
| - type="button" |
132 |
| - on:click|stopPropagation|preventDefault={() => (files = null)} |
133 |
| - class="mt-1 text-xs text-gray-600 hover:underline" |
134 |
| - > |
135 |
| - Reset |
136 |
| - </button> |
137 | 161 | {:else}
|
138 |
| - <span |
139 |
| - class="text-xs text-gray-500" |
140 |
| - class:hover:underline={!generateAvatar} |
141 |
| - class:cursor-pointer={!generateAvatar}>Click to upload</span |
142 |
| - > |
143 |
| - {/if} |
144 |
| - <p class="text-xs text-red-500">{getError("avatar", form)}</p> |
145 |
| - {#if !files?.[0] && $page.data.avatarGeneration && !assistant?.avatar} |
146 |
| - <label class="text-xs text-gray-500"> |
147 |
| - <input |
148 |
| - type="checkbox" |
149 |
| - name="generateAvatar" |
150 |
| - class="text-xs text-gray-500" |
151 |
| - bind:checked={generateAvatar} |
152 |
| - /> |
153 |
| - Generate avatar from description |
154 |
| - </label> |
| 162 | + <div class="mb-1 flex w-max flex-row gap-4"> |
| 163 | + <label |
| 164 | + for="avatar" |
| 165 | + 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" |
| 166 | + > |
| 167 | + <CarbonUpload class="mr-2 text-xs " /> Upload |
| 168 | + </label> |
| 169 | + </div> |
| 170 | + <p class="text-xs text-red-500">{getError("avatar", form)}</p> |
155 | 171 | {/if}
|
156 |
| - </label> |
| 172 | + </div> |
157 | 173 |
|
158 | 174 | <label>
|
159 | 175 | <span class="mb-1 text-sm font-semibold">Name</span>
|
160 | 176 | <input
|
161 | 177 | name="name"
|
162 |
| - class=" w-full rounded-lg border-2 border-gray-200 bg-gray-100 p-2" |
| 178 | + class="w-full rounded-lg border-2 border-gray-200 bg-gray-100 p-2" |
163 | 179 | placeholder="My awesome model"
|
164 | 180 | value={assistant?.name ?? ""}
|
165 | 181 | />
|
|
228 | 244 |
|
229 | 245 | <label class="flex flex-col">
|
230 | 246 | <span class="mb-1 text-sm font-semibold"> Instructions (system prompt) </span>
|
231 |
| - |
232 | 247 | <textarea
|
233 | 248 | name="preprompt"
|
234 |
| - class="flex-1 rounded-lg border-2 border-gray-200 bg-gray-100 p-2 text-sm" |
| 249 | + class="min-h-[8lh] flex-1 rounded-lg border-2 border-gray-200 bg-gray-100 p-2 text-sm" |
235 | 250 | placeholder="You'll act as..."
|
236 | 251 | value={assistant?.preprompt ?? ""}
|
237 | 252 | />
|
238 |
| - |
239 | 253 | <p class="text-xs text-red-500">{getError("preprompt", form)}</p>
|
240 | 254 | </label>
|
241 | 255 | </div>
|
|
245 | 259 | href={assistant ? `${base}/settings/assistants/${assistant?._id}` : `${base}/settings`}
|
246 | 260 | class="rounded-full bg-gray-200 px-8 py-2 font-semibold text-gray-600">Cancel</a
|
247 | 261 | >
|
248 |
| - |
249 | 262 | <button
|
250 | 263 | type="submit"
|
251 | 264 | disabled={loading}
|
|
0 commit comments