Spaces:
Running
Running
<script lang="ts"> | |
import type { readAndCompressImage } from "browser-image-resizer"; | |
import type { Model } from "$lib/types/Model"; | |
import type { Assistant } from "$lib/types/Assistant"; | |
import { onMount } from "svelte"; | |
import { applyAction, enhance } from "$app/forms"; | |
import { page } from "$app/stores"; | |
import { base } from "$app/paths"; | |
import CarbonPen from "~icons/carbon/pen"; | |
import CarbonUpload from "~icons/carbon/upload"; | |
import CarbonHelpFilled from "~icons/carbon/help"; | |
import CarbonSettingsAdjust from "~icons/carbon/settings-adjust"; | |
import { useSettingsStore } from "$lib/stores/settings"; | |
import { isHuggingChat } from "$lib/utils/isHuggingChat"; | |
import IconInternet from "./icons/IconInternet.svelte"; | |
import TokensCounter from "./TokensCounter.svelte"; | |
import HoverTooltip from "./HoverTooltip.svelte"; | |
type ActionData = { | |
error: boolean; | |
errors: { | |
field: string | number; | |
message: string; | |
}[]; | |
} | null; | |
type AssistantFront = Omit<Assistant, "_id" | "createdById"> & { _id: string }; | |
export let form: ActionData; | |
export let assistant: AssistantFront | undefined = undefined; | |
export let models: Model[] = []; | |
let files: FileList | null = null; | |
const settings = useSettingsStore(); | |
let modelId = ""; | |
let systemPrompt = assistant?.preprompt ?? ""; | |
let dynamicPrompt = assistant?.dynamicPrompt ?? false; | |
let showModelSettings = Object.values(assistant?.generateSettings ?? {}).some((v) => !!v); | |
let compress: typeof readAndCompressImage | null = null; | |
onMount(async () => { | |
const module = await import("browser-image-resizer"); | |
compress = module.readAndCompressImage; | |
if (assistant) { | |
modelId = assistant.modelId; | |
} else { | |
modelId = models.find((model) => model.id === $settings.activeModel)?.id ?? models[0].id; | |
} | |
}); | |
let inputMessage1 = assistant?.exampleInputs[0] ?? ""; | |
let inputMessage2 = assistant?.exampleInputs[1] ?? ""; | |
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 && inputEl.files[0].size > 0) { | |
if (!inputEl.files[0].type.includes("image")) { | |
inputEl.files = null; | |
files = null; | |
form = { error: true, errors: [{ field: "avatar", message: "Only images are allowed" }] }; | |
return; | |
} | |
files = inputEl.files; | |
resetErrors(); | |
deleteExistingAvatar = false; | |
} | |
} | |
function getError(field: string, returnForm: ActionData) { | |
return returnForm?.errors.find((error) => error.field === field)?.message ?? ""; | |
} | |
let deleteExistingAvatar = false; | |
let loading = false; | |
let ragMode: false | "links" | "domains" | "all" = assistant?.rag?.allowAllDomains | |
? "all" | |
: assistant?.rag?.allowedLinks?.length ?? 0 > 0 | |
? "links" | |
: (assistant?.rag?.allowedDomains?.length ?? 0) > 0 | |
? "domains" | |
: false; | |
const regex = /{{\s?url=(.+?)\s?}}/g; | |
$: templateVariables = [...systemPrompt.matchAll(regex)].map((match) => match[1]); | |
$: selectedModel = models.find((m) => m.id === modelId); | |
</script> | |
<form | |
method="POST" | |
class="relative flex h-full flex-col overflow-y-auto p-4 md:p-8" | |
enctype="multipart/form-data" | |
use:enhance={async ({ formData }) => { | |
loading = true; | |
if (files?.[0] && files[0].size > 0 && compress) { | |
await compress(files[0], { | |
maxWidth: 500, | |
maxHeight: 500, | |
quality: 1, | |
}).then((resizedImage) => { | |
formData.set("avatar", resizedImage); | |
}); | |
} | |
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"); | |
} | |
} else { | |
if (files === null) { | |
formData.delete("avatar"); | |
} | |
} | |
formData.delete("ragMode"); | |
if (ragMode === false || !$page.data.enableAssistantsRAG) { | |
formData.set("ragAllowAll", "false"); | |
formData.set("ragLinkList", ""); | |
formData.set("ragDomainList", ""); | |
} else if (ragMode === "all") { | |
formData.set("ragAllowAll", "true"); | |
formData.set("ragLinkList", ""); | |
formData.set("ragDomainList", ""); | |
} else if (ragMode === "links") { | |
formData.set("ragAllowAll", "false"); | |
formData.set("ragDomainList", ""); | |
} else if (ragMode === "domains") { | |
formData.set("ragAllowAll", "false"); | |
formData.set("ragLinkList", ""); | |
} | |
return async ({ result }) => { | |
loading = false; | |
await applyAction(result); | |
}; | |
}} | |
> | |
{#if assistant} | |
<h2 class="text-xl font-semibold"> | |
Edit Assistant: {assistant?.name ?? "assistant"} | |
</h2> | |
<p class="mb-6 text-sm text-gray-500"> | |
Modifying an existing assistant will propagate the changes to all users. | |
</p> | |
{:else} | |
<h2 class="text-xl font-semibold">Create new assistant</h2> | |
<p class="mb-6 text-sm text-gray-500"> | |
Create and share your own AI Assistant. All assistants are <span | |
class="rounded-full border px-2 py-0.5 leading-none">public</span | |
> | |
</p> | |
{/if} | |
<div class="grid h-full w-full flex-1 grid-cols-2 gap-6 text-sm max-sm:grid-cols-1"> | |
<div class="col-span-1 flex flex-col gap-4"> | |
<div> | |
<div class="mb-1 block pb-2 text-sm font-semibold">Avatar</div> | |
<input | |
type="file" | |
accept="image/*" | |
name="avatar" | |
id="avatar" | |
class="hidden" | |
on:change={onFilesChange} | |
/> | |
{#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 object-cover" | |
/> | |
{:else if assistant?.avatar} | |
<img | |
src="{base}/settings/assistants/{assistant._id}/avatar.jpg?hash={assistant.avatar}" | |
alt="avatar" | |
class="crop mx-auto h-12 w-12 cursor-pointer rounded-full object-cover" | |
/> | |
{/if} | |
<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" /> | |
</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> | |
{:else} | |
<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> | |
{/if} | |
<p class="text-xs text-red-500">{getError("avatar", form)}</p> | |
</div> | |
<label> | |
<div class="mb-1 font-semibold">Name</div> | |
<input | |
name="name" | |
class="w-full rounded-lg border-2 border-gray-200 bg-gray-100 p-2" | |
placeholder="Assistant Name" | |
value={assistant?.name ?? ""} | |
/> | |
<p class="text-xs text-red-500">{getError("name", form)}</p> | |
</label> | |
<label> | |
<div class="mb-1 font-semibold">Description</div> | |
<textarea | |
name="description" | |
class="h-15 w-full rounded-lg border-2 border-gray-200 bg-gray-100 p-2" | |
placeholder="He knows everything about python" | |
value={assistant?.description ?? ""} | |
/> | |
<p class="text-xs text-red-500">{getError("description", form)}</p> | |
</label> | |
<label> | |
<div class="mb-1 font-semibold">Model</div> | |
<div class="flex gap-2"> | |
<select | |
name="modelId" | |
class="w-full rounded-lg border-2 border-gray-200 bg-gray-100 p-2" | |
bind:value={modelId} | |
> | |
{#each models.filter((model) => !model.unlisted) as model} | |
<option value={model.id}>{model.displayName}</option> | |
{/each} | |
<p class="text-xs text-red-500">{getError("modelId", form)}</p> | |
</select> | |
<button | |
type="button" | |
class="flex aspect-square items-center gap-2 whitespace-nowrap rounded-lg border px-3 {showModelSettings | |
? 'border-blue-500/20 bg-blue-50 text-blue-600' | |
: ''}" | |
on:click={() => (showModelSettings = !showModelSettings)} | |
><CarbonSettingsAdjust class="text-xs" /></button | |
> | |
</div> | |
<div | |
class="mt-2 rounded-lg border border-blue-500/20 bg-blue-500/5 px-2 py-0.5" | |
class:hidden={!showModelSettings} | |
> | |
<p class="text-xs text-red-500">{getError("inputMessage1", form)}</p> | |
<div class="my-2 grid grid-cols-1 gap-2.5 sm:grid-cols-2 sm:grid-rows-2"> | |
<label for="temperature" class="flex justify-between"> | |
<span class="m-1 ml-0 flex items-center gap-1.5 whitespace-nowrap text-sm"> | |
Temperature | |
<HoverTooltip | |
label="Temperature: Controls creativity, higher values allow more variety." | |
> | |
<CarbonHelpFilled | |
class="inline text-xxs text-gray-500 group-hover/tooltip:text-blue-600" | |
/> | |
</HoverTooltip> | |
</span> | |
<input | |
type="number" | |
name="temperature" | |
min="0.1" | |
max="2" | |
step="0.1" | |
class="w-20 rounded-lg border-2 border-gray-200 bg-gray-100 px-2 py-1" | |
placeholder={selectedModel?.parameters?.temperature?.toString() ?? "1"} | |
value={assistant?.generateSettings?.temperature ?? ""} | |
/> | |
</label> | |
<label for="top_p" class="flex justify-between"> | |
<span class="m-1 ml-0 flex items-center gap-1.5 whitespace-nowrap text-sm"> | |
Top P | |
<HoverTooltip | |
label="Top P: Sets word choice boundaries, lower values tighten focus." | |
> | |
<CarbonHelpFilled | |
class="inline text-xxs text-gray-500 group-hover/tooltip:text-blue-600" | |
/> | |
</HoverTooltip> | |
</span> | |
<input | |
type="number" | |
name="top_p" | |
class="w-20 rounded-lg border-2 border-gray-200 bg-gray-100 px-2 py-1" | |
min="0.05" | |
max="1" | |
step="0.05" | |
placeholder={selectedModel?.parameters?.top_p?.toString() ?? "1"} | |
value={assistant?.generateSettings?.top_p ?? ""} | |
/> | |
</label> | |
<label for="repetition_penalty" class="flex justify-between"> | |
<span class="m-1 ml-0 flex items-center gap-1.5 whitespace-nowrap text-sm"> | |
Repetition penalty | |
<HoverTooltip | |
label="Repetition penalty: Prevents reuse, higher values decrease repetition." | |
> | |
<CarbonHelpFilled | |
class="inline text-xxs text-gray-500 group-hover/tooltip:text-blue-600" | |
/> | |
</HoverTooltip> | |
</span> | |
<input | |
type="number" | |
name="repetition_penalty" | |
min="0.1" | |
max="2" | |
class="w-20 rounded-lg border-2 border-gray-200 bg-gray-100 px-2 py-1" | |
placeholder={selectedModel?.parameters?.repetition_penalty?.toString() ?? "1.0"} | |
value={assistant?.generateSettings?.repetition_penalty ?? ""} | |
/> | |
</label> | |
<label for="top_k" class="flex justify-between"> | |
<span class="m-1 ml-0 flex items-center gap-1.5 whitespace-nowrap text-sm"> | |
Top K <HoverTooltip | |
label="Top K: Restricts word options, lower values for predictability." | |
> | |
<CarbonHelpFilled | |
class="inline text-xxs text-gray-500 group-hover/tooltip:text-blue-600" | |
/> | |
</HoverTooltip> | |
</span> | |
<input | |
type="number" | |
name="top_k" | |
min="5" | |
max="100" | |
step="5" | |
class="w-20 rounded-lg border-2 border-gray-200 bg-gray-100 px-2 py-1" | |
placeholder={selectedModel?.parameters?.top_k?.toString() ?? "50"} | |
value={assistant?.generateSettings?.top_k ?? ""} | |
/> | |
</label> | |
</div> | |
</div> | |
</label> | |
<label> | |
<div class="mb-1 font-semibold">User start messages</div> | |
<div class="grid gap-1.5 text-sm md:grid-cols-2"> | |
<input | |
name="exampleInput1" | |
placeholder="Start Message 1" | |
bind:value={inputMessage1} | |
class="w-full rounded-lg border-2 border-gray-200 bg-gray-100 p-2" | |
/> | |
<input | |
name="exampleInput2" | |
placeholder="Start Message 2" | |
bind:value={inputMessage2} | |
class="w-full rounded-lg border-2 border-gray-200 bg-gray-100 p-2" | |
/> | |
<input | |
name="exampleInput3" | |
placeholder="Start Message 3" | |
bind:value={inputMessage3} | |
class="w-full rounded-lg border-2 border-gray-200 bg-gray-100 p-2" | |
/> | |
<input | |
name="exampleInput4" | |
placeholder="Start Message 4" | |
bind:value={inputMessage4} | |
class="w-full rounded-lg border-2 border-gray-200 bg-gray-100 p-2" | |
/> | |
</div> | |
<p class="text-xs text-red-500">{getError("inputMessage1", form)}</p> | |
</label> | |
{#if $page.data.enableAssistantsRAG} | |
<div class="mb-4 flex flex-col flex-nowrap"> | |
<span class="mt-2 text-smd font-semibold" | |
>Internet access | |
<IconInternet classNames="inline text-sm text-blue-600" /> | |
<span class="ml-1 rounded bg-gray-100 px-1 py-0.5 text-xxs font-normal text-gray-600" | |
>Experimental</span | |
> | |
{#if isHuggingChat} | |
<a | |
href="https://huggingface.co/spaces/huggingchat/chat-ui/discussions/385" | |
target="_blank" | |
class="ml-0.5 rounded bg-gray-100 px-1 py-0.5 text-xxs font-normal text-gray-700 underline decoration-gray-400" | |
>Give feedback</a | |
> | |
{/if} | |
</span> | |
<label class="mt-1"> | |
<input | |
checked={!ragMode} | |
on:change={() => (ragMode = false)} | |
type="radio" | |
name="ragMode" | |
value={false} | |
/> | |
<span class="my-2 text-sm" class:font-semibold={!ragMode}> Default </span> | |
{#if !ragMode} | |
<span class="block text-xs text-gray-500"> | |
Assistant will not use internet to do information retrieval and will respond faster. | |
Recommended for most Assistants. | |
</span> | |
{/if} | |
</label> | |
<label class="mt-1"> | |
<input | |
checked={ragMode === "all"} | |
on:change={() => (ragMode = "all")} | |
type="radio" | |
name="ragMode" | |
value={"all"} | |
/> | |
<span class="my-2 text-sm" class:font-semibold={ragMode === "all"}> Web search </span> | |
{#if ragMode === "all"} | |
<span class="block text-xs text-gray-500"> | |
Assistant will do a web search on each user request to find information. | |
</span> | |
{/if} | |
</label> | |
<label class="mt-1"> | |
<input | |
checked={ragMode === "domains"} | |
on:change={() => (ragMode = "domains")} | |
type="radio" | |
name="ragMode" | |
value={false} | |
/> | |
<span class="my-2 text-sm" class:font-semibold={ragMode === "domains"}> | |
Domains search | |
</span> | |
</label> | |
{#if ragMode === "domains"} | |
<span class="mb-2 text-xs text-gray-500"> | |
Specify domains and URLs that the application can search, separated by commas. | |
</span> | |
<input | |
name="ragDomainList" | |
class="w-full rounded-lg border-2 border-gray-200 bg-gray-100 p-2" | |
placeholder="wikipedia.org,bbc.com" | |
value={assistant?.rag?.allowedDomains?.join(",") ?? ""} | |
/> | |
<p class="text-xs text-red-500">{getError("ragDomainList", form)}</p> | |
{/if} | |
<label class="mt-1"> | |
<input | |
checked={ragMode === "links"} | |
on:change={() => (ragMode = "links")} | |
type="radio" | |
name="ragMode" | |
value={false} | |
/> | |
<span class="my-2 text-sm" class:font-semibold={ragMode === "links"}> | |
Specific Links | |
</span> | |
</label> | |
{#if ragMode === "links"} | |
<span class="mb-2 text-xs text-gray-500"> | |
Specify a maximum of 10 direct URLs that the Assistant will access. HTML & Plain Text | |
only, separated by commas | |
</span> | |
<input | |
name="ragLinkList" | |
class="w-full rounded-lg border-2 border-gray-200 bg-gray-100 p-2" | |
placeholder="https://raw.githubusercontent.com/huggingface/chat-ui/main/README.md" | |
value={assistant?.rag?.allowedLinks.join(",") ?? ""} | |
/> | |
<p class="text-xs text-red-500">{getError("ragLinkList", form)}</p> | |
{/if} | |
<!-- divider --> | |
<div class="my-3 ml-0 mr-6 w-full border border-gray-200" /> | |
<label class="text-sm has-[:checked]:font-semibold"> | |
<input type="checkbox" name="dynamicPrompt" bind:checked={dynamicPrompt} /> | |
Dynamic Prompt | |
<p class="mb-2 text-xs font-normal text-gray-500"> | |
Allow the use of template variables {"{{url=https://example.com/path}}"} | |
to insert dynamic content into your prompt by making GET requests to specified URLs on | |
each inference. | |
</p> | |
</label> | |
</div> | |
{/if} | |
</div> | |
<div class="relative col-span-1 flex h-full flex-col"> | |
<div class="mb-1 flex justify-between text-sm"> | |
<span class="font-semibold"> Instructions (System Prompt) </span> | |
{#if dynamicPrompt && templateVariables.length} | |
<div class="relative"> | |
<button | |
type="button" | |
class="peer rounded bg-blue-500/20 px-1 text-xs text-blue-600 focus:bg-blue-500/30 focus:text-blue-800 sm:text-sm" | |
> | |
{templateVariables.length} template variable{templateVariables.length > 1 ? "s" : ""} | |
</button> | |
<div | |
class="invisible absolute right-0 top-6 z-10 rounded-lg border bg-white p-2 text-xs shadow-lg peer-focus:visible hover:visible sm:w-96" | |
> | |
Will performs a GET request and injects the response into the prompt. Works better | |
with plain text, csv or json content. | |
{#each templateVariables as match} | |
<a href={match} target="_blank" class="text-gray-500 underline decoration-gray-300" | |
>{match}</a | |
> | |
{/each} | |
</div> | |
</div> | |
{/if} | |
</div> | |
<div class="relative mb-20 flex h-full flex-col gap-2"> | |
<textarea | |
name="preprompt" | |
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..." | |
bind:value={systemPrompt} | |
/> | |
{#if modelId} | |
{@const model = models.find((_model) => _model.id === modelId)} | |
{#if model?.tokenizer && systemPrompt} | |
<TokensCounter | |
classNames="absolute bottom-4 right-4" | |
prompt={systemPrompt} | |
modelTokenizer={model.tokenizer} | |
truncate={model?.parameters?.truncate} | |
/> | |
{/if} | |
{/if} | |
<p class="text-xs text-red-500">{getError("preprompt", form)}</p> | |
</div> | |
<div class="absolute bottom-6 flex w-full justify-end gap-2 md:right-0 md:w-fit"> | |
<a | |
href={assistant ? `${base}/settings/assistants/${assistant?._id}` : `${base}/settings`} | |
class="flex items-center justify-center rounded-full bg-gray-200 px-5 py-2 font-semibold text-gray-600" | |
> | |
Cancel | |
</a> | |
<button | |
type="submit" | |
disabled={loading} | |
aria-disabled={loading} | |
class="flex items-center justify-center rounded-full bg-black px-8 py-2 font-semibold" | |
class:bg-gray-200={loading} | |
class:text-gray-600={loading} | |
class:text-white={!loading} | |
> | |
{assistant ? "Save" : "Create"} | |
</button> | |
</div> | |
</div> | |
</div> | |
</form> | |