Merge pull request #650 from ollama-webui/settings-refac

refac: settings
This commit is contained in:
Timothy Jaeryang Baek 2024-02-05 01:59:52 -08:00 committed by GitHub
commit 3e1c7e4e06
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 2381 additions and 2325 deletions

View file

@ -0,0 +1,63 @@
<script lang="ts">
import { getOllamaVersion } from '$lib/apis/ollama';
import { WEB_UI_VERSION } from '$lib/constants';
import { config } from '$lib/stores';
import { onMount } from 'svelte';
let ollamaVersion = '';
onMount(async () => {
ollamaVersion = await getOllamaVersion(localStorage.token).catch((error) => {
return '';
});
});
</script>
<div class="flex flex-col h-full justify-between space-y-3 text-sm mb-6">
<div class=" space-y-3">
<div>
<div class=" mb-2.5 text-sm font-medium">Ollama Web UI Version</div>
<div class="flex w-full">
<div class="flex-1 text-xs text-gray-700 dark:text-gray-200">
{$config && $config.version ? $config.version : WEB_UI_VERSION}
</div>
</div>
</div>
<hr class=" dark:border-gray-700" />
<div>
<div class=" mb-2.5 text-sm font-medium">Ollama Version</div>
<div class="flex w-full">
<div class="flex-1 text-xs text-gray-700 dark:text-gray-200">
{ollamaVersion ?? 'N/A'}
</div>
</div>
</div>
<hr class=" dark:border-gray-700" />
<div class="flex space-x-1">
<a href="https://discord.gg/5rJgQTnV4s" target="_blank">
<img
alt="Discord"
src="https://img.shields.io/badge/Discord-Ollama_Web_UI-blue?logo=discord&logoColor=white"
/>
</a>
<a href="https://github.com/ollama-webui/ollama-webui" target="_blank">
<img
alt="Github Repo"
src="https://img.shields.io/github/stars/ollama-webui/ollama-webui?style=social&label=Star us on Github"
/>
</a>
</div>
<div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
Created by <a
class=" text-gray-500 dark:text-gray-300 font-medium"
href="https://github.com/tjbck"
target="_blank">Timothy J. Baek</a
>
</div>
</div>
</div>

View file

@ -0,0 +1,249 @@
<script lang="ts">
import toast from 'svelte-french-toast';
import { createEventDispatcher, onMount } from 'svelte';
import { models, voices } from '$lib/stores';
const dispatch = createEventDispatcher();
export let saveSettings: Function;
// Addons
let titleAutoGenerate = true;
let speechAutoSend = false;
let responseAutoCopy = false;
let gravatarEmail = '';
let titleAutoGenerateModel = '';
// Voice
let speakVoice = '';
const toggleSpeechAutoSend = async () => {
speechAutoSend = !speechAutoSend;
saveSettings({ speechAutoSend: speechAutoSend });
};
const toggleTitleAutoGenerate = async () => {
titleAutoGenerate = !titleAutoGenerate;
saveSettings({ titleAutoGenerate: titleAutoGenerate });
};
const toggleResponseAutoCopy = async () => {
const permission = await navigator.clipboard
.readText()
.then(() => {
return 'granted';
})
.catch(() => {
return '';
});
console.log(permission);
if (permission === 'granted') {
responseAutoCopy = !responseAutoCopy;
saveSettings({ responseAutoCopy: responseAutoCopy });
} else {
toast.error(
'Clipboard write permission denied. Please check your browser settings to grant the necessary access.'
);
}
};
onMount(async () => {
let settings = JSON.parse(localStorage.getItem('settings') ?? '{}');
titleAutoGenerate = settings.titleAutoGenerate ?? true;
speechAutoSend = settings.speechAutoSend ?? false;
responseAutoCopy = settings.responseAutoCopy ?? false;
titleAutoGenerateModel = settings.titleAutoGenerateModel ?? '';
gravatarEmail = settings.gravatarEmail ?? '';
speakVoice = settings.speakVoice ?? '';
const getVoicesLoop = setInterval(async () => {
const _voices = await speechSynthesis.getVoices();
await voices.set(_voices);
// do your loop
if (_voices.length > 0) {
clearInterval(getVoicesLoop);
}
}, 100);
});
</script>
<form
class="flex flex-col h-full justify-between space-y-3 text-sm"
on:submit|preventDefault={() => {
saveSettings({
speakVoice: speakVoice !== '' ? speakVoice : undefined
});
dispatch('save');
}}
>
<div class=" space-y-3">
<div>
<div class=" mb-1 text-sm font-medium">WebUI Add-ons</div>
<div>
<div class=" py-0.5 flex w-full justify-between">
<div class=" self-center text-xs font-medium">Title Auto-Generation</div>
<button
class="p-1 px-3 text-xs flex rounded transition"
on:click={() => {
toggleTitleAutoGenerate();
}}
type="button"
>
{#if titleAutoGenerate === true}
<span class="ml-2 self-center">On</span>
{:else}
<span class="ml-2 self-center">Off</span>
{/if}
</button>
</div>
</div>
<div>
<div class=" py-0.5 flex w-full justify-between">
<div class=" self-center text-xs font-medium">Voice Input Auto-Send</div>
<button
class="p-1 px-3 text-xs flex rounded transition"
on:click={() => {
toggleSpeechAutoSend();
}}
type="button"
>
{#if speechAutoSend === true}
<span class="ml-2 self-center">On</span>
{:else}
<span class="ml-2 self-center">Off</span>
{/if}
</button>
</div>
</div>
<div>
<div class=" py-0.5 flex w-full justify-between">
<div class=" self-center text-xs font-medium">Response AutoCopy to Clipboard</div>
<button
class="p-1 px-3 text-xs flex rounded transition"
on:click={() => {
toggleResponseAutoCopy();
}}
type="button"
>
{#if responseAutoCopy === true}
<span class="ml-2 self-center">On</span>
{:else}
<span class="ml-2 self-center">Off</span>
{/if}
</button>
</div>
</div>
</div>
<hr class=" dark:border-gray-700" />
<div>
<div class=" mb-2.5 text-sm font-medium">Set Title Auto-Generation Model</div>
<div class="flex w-full">
<div class="flex-1 mr-2">
<select
class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none"
bind:value={titleAutoGenerateModel}
placeholder="Select a model"
>
<option value="" selected>Current Model</option>
{#each $models.filter((m) => m.size != null) as model}
<option value={model.name} class="bg-gray-100 dark:bg-gray-700"
>{model.name + ' (' + (model.size / 1024 ** 3).toFixed(1) + ' GB)'}</option
>
{/each}
</select>
</div>
<button
class="px-3 bg-gray-200 hover:bg-gray-300 dark:bg-gray-700 dark:hover:bg-gray-800 dark:text-gray-100 rounded transition"
on:click={() => {
saveSettings({
titleAutoGenerateModel:
titleAutoGenerateModel !== '' ? titleAutoGenerateModel : undefined
});
}}
type="button"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-3.5 h-3.5"
>
<path
fill-rule="evenodd"
d="M13.836 2.477a.75.75 0 0 1 .75.75v3.182a.75.75 0 0 1-.75.75h-3.182a.75.75 0 0 1 0-1.5h1.37l-.84-.841a4.5 4.5 0 0 0-7.08.932.75.75 0 0 1-1.3-.75 6 6 0 0 1 9.44-1.242l.842.84V3.227a.75.75 0 0 1 .75-.75Zm-.911 7.5A.75.75 0 0 1 13.199 11a6 6 0 0 1-9.44 1.241l-.84-.84v1.371a.75.75 0 0 1-1.5 0V9.591a.75.75 0 0 1 .75-.75H5.35a.75.75 0 0 1 0 1.5H3.98l.841.841a4.5 4.5 0 0 0 7.08-.932.75.75 0 0 1 1.025-.273Z"
clip-rule="evenodd"
/>
</svg>
</button>
</div>
</div>
<hr class=" dark:border-gray-700" />
<div class=" space-y-3">
<div>
<div class=" mb-2.5 text-sm font-medium">Set Default Voice</div>
<div class="flex w-full">
<div class="flex-1">
<select
class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none"
bind:value={speakVoice}
placeholder="Select a voice"
>
<option value="" selected>Default</option>
{#each $voices.filter((v) => v.localService === true) as voice}
<option value={voice.name} class="bg-gray-100 dark:bg-gray-700">{voice.name}</option
>
{/each}
</select>
</div>
</div>
</div>
</div>
<!--
<div>
<div class=" mb-2.5 text-sm font-medium">
Gravatar Email <span class=" text-gray-400 text-sm">(optional)</span>
</div>
<div class="flex w-full">
<div class="flex-1">
<input
class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none"
placeholder="Enter Your Email"
bind:value={gravatarEmail}
autocomplete="off"
type="email"
/>
</div>
</div>
<div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
Changes user profile image to match your <a
class=" text-gray-500 dark:text-gray-300 font-medium"
href="https://gravatar.com/"
target="_blank">Gravatar.</a
>
</div>
</div> -->
</div>
<div class="flex justify-end pt-3 text-sm font-medium">
<button
class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded"
type="submit"
>
Save
</button>
</div>
</form>

View file

@ -1,8 +1,15 @@
<script lang="ts"> <script lang="ts">
export let options = { import { createEventDispatcher, onMount } from 'svelte';
const dispatch = createEventDispatcher();
import AdvancedParams from './Advanced/AdvancedParams.svelte';
export let saveSettings: Function;
// Advanced
let requestFormat = '';
let options = {
// Advanced // Advanced
seed: 0, seed: 0,
stop: '',
temperature: '', temperature: '',
repeat_penalty: '', repeat_penalty: '',
repeat_last_n: '', repeat_last_n: '',
@ -11,546 +18,101 @@
mirostat_tau: '', mirostat_tau: '',
top_k: '', top_k: '',
top_p: '', top_p: '',
stop: '',
tfs_z: '', tfs_z: '',
num_ctx: '', num_ctx: '',
num_predict: '' num_predict: ''
}; };
const toggleRequestFormat = async () => {
if (requestFormat === '') {
requestFormat = 'json';
} else {
requestFormat = '';
}
saveSettings({ requestFormat: requestFormat !== '' ? requestFormat : undefined });
};
onMount(() => {
let settings = JSON.parse(localStorage.getItem('settings') ?? '{}');
requestFormat = settings.requestFormat ?? '';
options.seed = settings.seed ?? 0;
options.temperature = settings.temperature ?? '';
options.repeat_penalty = settings.repeat_penalty ?? '';
options.top_k = settings.top_k ?? '';
options.top_p = settings.top_p ?? '';
options.num_ctx = settings.num_ctx ?? '';
options = { ...options, ...settings.options };
options.stop = (settings?.options?.stop ?? []).join(',');
});
</script> </script>
<div class=" space-y-3 text-xs"> <div class="flex flex-col h-full justify-between text-sm">
<div> <div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-80">
<div class=" py-0.5 flex w-full justify-between"> <div class=" text-sm font-medium">Parameters</div>
<div class=" w-20 text-xs font-medium self-center">Seed</div>
<div class=" flex-1 self-center"> <AdvancedParams bind:options />
<input <hr class=" dark:border-gray-700" />
class="w-full rounded py-1.5 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none border border-gray-100 dark:border-gray-600"
type="number" <div>
placeholder="Enter Seed" <div class=" py-1 flex w-full justify-between">
bind:value={options.seed} <div class=" self-center text-sm font-medium">Request Mode</div>
autocomplete="off"
min="0" <button
/> class="p-1 px-3 text-xs flex rounded transition"
on:click={() => {
toggleRequestFormat();
}}
>
{#if requestFormat === ''}
<span class="ml-2 self-center"> Default </span>
{:else if requestFormat === 'json'}
<!-- <svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-4 h-4 self-center"
>
<path
d="M10 2a.75.75 0 01.75.75v1.5a.75.75 0 01-1.5 0v-1.5A.75.75 0 0110 2zM10 15a.75.75 0 01.75.75v1.5a.75.75 0 01-1.5 0v-1.5A.75.75 0 0110 15zM10 7a3 3 0 100 6 3 3 0 000-6zM15.657 5.404a.75.75 0 10-1.06-1.06l-1.061 1.06a.75.75 0 001.06 1.06l1.06-1.06zM6.464 14.596a.75.75 0 10-1.06-1.06l-1.06 1.06a.75.75 0 001.06 1.06l1.06-1.06zM18 10a.75.75 0 01-.75.75h-1.5a.75.75 0 010-1.5h1.5A.75.75 0 0118 10zM5 10a.75.75 0 01-.75.75h-1.5a.75.75 0 010-1.5h1.5A.75.75 0 015 10zM14.596 15.657a.75.75 0 001.06-1.06l-1.06-1.061a.75.75 0 10-1.06 1.06l1.06 1.06zM5.404 6.464a.75.75 0 001.06-1.06l-1.06-1.06a.75.75 0 10-1.061 1.06l1.06 1.06z"
/>
</svg> -->
<span class="ml-2 self-center"> JSON </span>
{/if}
</button>
</div> </div>
</div> </div>
</div> </div>
<div> <div class="flex justify-end pt-3 text-sm font-medium">
<div class=" py-0.5 flex w-full justify-between"> <button
<div class=" w-20 text-xs font-medium self-center">Stop Sequence</div> class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded"
<div class=" flex-1 self-center"> on:click={() => {
<input saveSettings({
class="w-full rounded py-1.5 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none border border-gray-100 dark:border-gray-600" options: {
type="text" seed: (options.seed !== 0 ? options.seed : undefined) ?? undefined,
placeholder="Enter Stop Sequence" stop: options.stop !== '' ? options.stop.split(',').filter((e) => e) : undefined,
bind:value={options.stop} temperature: options.temperature !== '' ? options.temperature : undefined,
autocomplete="off" repeat_penalty: options.repeat_penalty !== '' ? options.repeat_penalty : undefined,
/> repeat_last_n: options.repeat_last_n !== '' ? options.repeat_last_n : undefined,
</div> mirostat: options.mirostat !== '' ? options.mirostat : undefined,
</div> mirostat_eta: options.mirostat_eta !== '' ? options.mirostat_eta : undefined,
</div> mirostat_tau: options.mirostat_tau !== '' ? options.mirostat_tau : undefined,
top_k: options.top_k !== '' ? options.top_k : undefined,
top_p: options.top_p !== '' ? options.top_p : undefined,
tfs_z: options.tfs_z !== '' ? options.tfs_z : undefined,
num_ctx: options.num_ctx !== '' ? options.num_ctx : undefined,
num_predict: options.num_predict !== '' ? options.num_predict : undefined
}
});
<div class=" py-0.5 w-full justify-between"> dispatch('save');
<div class="flex w-full justify-between"> }}
<div class=" self-center text-xs font-medium">Temperature</div> >
Save
<button </button>
class="p-1 px-3 text-xs flex rounded transition"
type="button"
on:click={() => {
options.temperature = options.temperature === '' ? 0.8 : '';
}}
>
{#if options.temperature === ''}
<span class="ml-2 self-center"> Default </span>
{:else}
<span class="ml-2 self-center"> Custom </span>
{/if}
</button>
</div>
{#if options.temperature !== ''}
<div class="flex mt-0.5 space-x-2">
<div class=" flex-1">
<input
id="steps-range"
type="range"
min="0"
max="1"
step="0.05"
bind:value={options.temperature}
class="w-full h-2 rounded-lg appearance-none cursor-pointer dark:bg-gray-700"
/>
</div>
<div>
<input
bind:value={options.temperature}
type="number"
class=" bg-transparent text-center w-14"
min="0"
max="1"
step="0.05"
/>
</div>
</div>
{/if}
</div>
<div class=" py-0.5 w-full justify-between">
<div class="flex w-full justify-between">
<div class=" self-center text-xs font-medium">Mirostat</div>
<button
class="p-1 px-3 text-xs flex rounded transition"
type="button"
on:click={() => {
options.mirostat = options.mirostat === '' ? 0 : '';
}}
>
{#if options.mirostat === ''}
<span class="ml-2 self-center"> Default </span>
{:else}
<span class="ml-2 self-center"> Custom </span>
{/if}
</button>
</div>
{#if options.mirostat !== ''}
<div class="flex mt-0.5 space-x-2">
<div class=" flex-1">
<input
id="steps-range"
type="range"
min="0"
max="2"
step="1"
bind:value={options.mirostat}
class="w-full h-2 rounded-lg appearance-none cursor-pointer dark:bg-gray-700"
/>
</div>
<div>
<input
bind:value={options.mirostat}
type="number"
class=" bg-transparent text-center w-14"
min="0"
max="2"
step="1"
/>
</div>
</div>
{/if}
</div>
<div class=" py-0.5 w-full justify-between">
<div class="flex w-full justify-between">
<div class=" self-center text-xs font-medium">Mirostat Eta</div>
<button
class="p-1 px-3 text-xs flex rounded transition"
type="button"
on:click={() => {
options.mirostat_eta = options.mirostat_eta === '' ? 0.1 : '';
}}
>
{#if options.mirostat_eta === ''}
<span class="ml-2 self-center"> Default </span>
{:else}
<span class="ml-2 self-center"> Custom </span>
{/if}
</button>
</div>
{#if options.mirostat_eta !== ''}
<div class="flex mt-0.5 space-x-2">
<div class=" flex-1">
<input
id="steps-range"
type="range"
min="0"
max="1"
step="0.05"
bind:value={options.mirostat_eta}
class="w-full h-2 rounded-lg appearance-none cursor-pointer dark:bg-gray-700"
/>
</div>
<div>
<input
bind:value={options.mirostat_eta}
type="number"
class=" bg-transparent text-center w-14"
min="0"
max="1"
step="0.05"
/>
</div>
</div>
{/if}
</div>
<div class=" py-0.5 w-full justify-between">
<div class="flex w-full justify-between">
<div class=" self-center text-xs font-medium">Mirostat Tau</div>
<button
class="p-1 px-3 text-xs flex rounded transition"
type="button"
on:click={() => {
options.mirostat_tau = options.mirostat_tau === '' ? 5.0 : '';
}}
>
{#if options.mirostat_tau === ''}
<span class="ml-2 self-center"> Default </span>
{:else}
<span class="ml-2 self-center"> Custom </span>
{/if}
</button>
</div>
{#if options.mirostat_tau !== ''}
<div class="flex mt-0.5 space-x-2">
<div class=" flex-1">
<input
id="steps-range"
type="range"
min="0"
max="10"
step="0.5"
bind:value={options.mirostat_tau}
class="w-full h-2 rounded-lg appearance-none cursor-pointer dark:bg-gray-700"
/>
</div>
<div>
<input
bind:value={options.mirostat_tau}
type="number"
class=" bg-transparent text-center w-14"
min="0"
max="10"
step="0.5"
/>
</div>
</div>
{/if}
</div>
<div class=" py-0.5 w-full justify-between">
<div class="flex w-full justify-between">
<div class=" self-center text-xs font-medium">Top K</div>
<button
class="p-1 px-3 text-xs flex rounded transition"
type="button"
on:click={() => {
options.top_k = options.top_k === '' ? 40 : '';
}}
>
{#if options.top_k === ''}
<span class="ml-2 self-center"> Default </span>
{:else}
<span class="ml-2 self-center"> Custom </span>
{/if}
</button>
</div>
{#if options.top_k !== ''}
<div class="flex mt-0.5 space-x-2">
<div class=" flex-1">
<input
id="steps-range"
type="range"
min="0"
max="100"
step="0.5"
bind:value={options.top_k}
class="w-full h-2 rounded-lg appearance-none cursor-pointer dark:bg-gray-700"
/>
</div>
<div>
<input
bind:value={options.top_k}
type="number"
class=" bg-transparent text-center w-14"
min="0"
max="100"
step="0.5"
/>
</div>
</div>
{/if}
</div>
<div class=" py-0.5 w-full justify-between">
<div class="flex w-full justify-between">
<div class=" self-center text-xs font-medium">Top P</div>
<button
class="p-1 px-3 text-xs flex rounded transition"
type="button"
on:click={() => {
options.top_p = options.top_p === '' ? 0.9 : '';
}}
>
{#if options.top_p === ''}
<span class="ml-2 self-center"> Default </span>
{:else}
<span class="ml-2 self-center"> Custom </span>
{/if}
</button>
</div>
{#if options.top_p !== ''}
<div class="flex mt-0.5 space-x-2">
<div class=" flex-1">
<input
id="steps-range"
type="range"
min="0"
max="1"
step="0.05"
bind:value={options.top_p}
class="w-full h-2 rounded-lg appearance-none cursor-pointer dark:bg-gray-700"
/>
</div>
<div>
<input
bind:value={options.top_p}
type="number"
class=" bg-transparent text-center w-14"
min="0"
max="1"
step="0.05"
/>
</div>
</div>
{/if}
</div>
<div class=" py-0.5 w-full justify-between">
<div class="flex w-full justify-between">
<div class=" self-center text-xs font-medium">Repeat Penalty</div>
<button
class="p-1 px-3 text-xs flex rounded transition"
type="button"
on:click={() => {
options.repeat_penalty = options.repeat_penalty === '' ? 1.1 : '';
}}
>
{#if options.repeat_penalty === ''}
<span class="ml-2 self-center"> Default </span>
{:else}
<span class="ml-2 self-center"> Custom </span>
{/if}
</button>
</div>
{#if options.repeat_penalty !== ''}
<div class="flex mt-0.5 space-x-2">
<div class=" flex-1">
<input
id="steps-range"
type="range"
min="0"
max="2"
step="0.05"
bind:value={options.repeat_penalty}
class="w-full h-2 rounded-lg appearance-none cursor-pointer dark:bg-gray-700"
/>
</div>
<div>
<input
bind:value={options.repeat_penalty}
type="number"
class=" bg-transparent text-center w-14"
min="0"
max="2"
step="0.05"
/>
</div>
</div>
{/if}
</div>
<div class=" py-0.5 w-full justify-between">
<div class="flex w-full justify-between">
<div class=" self-center text-xs font-medium">Repeat Last N</div>
<button
class="p-1 px-3 text-xs flex rounded transition"
type="button"
on:click={() => {
options.repeat_last_n = options.repeat_last_n === '' ? 64 : '';
}}
>
{#if options.repeat_last_n === ''}
<span class="ml-2 self-center"> Default </span>
{:else}
<span class="ml-2 self-center"> Custom </span>
{/if}
</button>
</div>
{#if options.repeat_last_n !== ''}
<div class="flex mt-0.5 space-x-2">
<div class=" flex-1">
<input
id="steps-range"
type="range"
min="-1"
max="128"
step="1"
bind:value={options.repeat_last_n}
class="w-full h-2 rounded-lg appearance-none cursor-pointer dark:bg-gray-700"
/>
</div>
<div>
<input
bind:value={options.repeat_last_n}
type="number"
class=" bg-transparent text-center w-14"
min="-1"
max="128"
step="1"
/>
</div>
</div>
{/if}
</div>
<div class=" py-0.5 w-full justify-between">
<div class="flex w-full justify-between">
<div class=" self-center text-xs font-medium">Tfs Z</div>
<button
class="p-1 px-3 text-xs flex rounded transition"
type="button"
on:click={() => {
options.tfs_z = options.tfs_z === '' ? 1 : '';
}}
>
{#if options.tfs_z === ''}
<span class="ml-2 self-center"> Default </span>
{:else}
<span class="ml-2 self-center"> Custom </span>
{/if}
</button>
</div>
{#if options.tfs_z !== ''}
<div class="flex mt-0.5 space-x-2">
<div class=" flex-1">
<input
id="steps-range"
type="range"
min="0"
max="2"
step="0.05"
bind:value={options.tfs_z}
class="w-full h-2 rounded-lg appearance-none cursor-pointer dark:bg-gray-700"
/>
</div>
<div>
<input
bind:value={options.tfs_z}
type="number"
class=" bg-transparent text-center w-14"
min="0"
max="2"
step="0.05"
/>
</div>
</div>
{/if}
</div>
<div class=" py-0.5 w-full justify-between">
<div class="flex w-full justify-between">
<div class=" self-center text-xs font-medium">Context Length</div>
<button
class="p-1 px-3 text-xs flex rounded transition"
type="button"
on:click={() => {
options.num_ctx = options.num_ctx === '' ? 2048 : '';
}}
>
{#if options.num_ctx === ''}
<span class="ml-2 self-center"> Default </span>
{:else}
<span class="ml-2 self-center"> Custom </span>
{/if}
</button>
</div>
{#if options.num_ctx !== ''}
<div class="flex mt-0.5 space-x-2">
<div class=" flex-1">
<input
id="steps-range"
type="range"
min="1"
max="16000"
step="1"
bind:value={options.num_ctx}
class="w-full h-2 rounded-lg appearance-none cursor-pointer dark:bg-gray-700"
/>
</div>
<div class="">
<input
bind:value={options.num_ctx}
type="number"
class=" bg-transparent text-center w-14"
min="1"
max="16000"
step="1"
/>
</div>
</div>
{/if}
</div>
<div class=" py-0.5 w-full justify-between">
<div class="flex w-full justify-between">
<div class=" self-center text-xs font-medium">Max Tokens</div>
<button
class="p-1 px-3 text-xs flex rounded transition"
type="button"
on:click={() => {
options.num_predict = options.num_predict === '' ? 128 : '';
}}
>
{#if options.num_predict === ''}
<span class="ml-2 self-center"> Default </span>
{:else}
<span class="ml-2 self-center"> Custom </span>
{/if}
</button>
</div>
{#if options.num_predict !== ''}
<div class="flex mt-0.5 space-x-2">
<div class=" flex-1">
<input
id="steps-range"
type="range"
min="-2"
max="16000"
step="1"
bind:value={options.num_predict}
class="w-full h-2 rounded-lg appearance-none cursor-pointer dark:bg-gray-700"
/>
</div>
<div class="">
<input
bind:value={options.num_predict}
type="number"
class=" bg-transparent text-center w-14"
min="-2"
max="16000"
step="1"
/>
</div>
</div>
{/if}
</div> </div>
</div> </div>

View file

@ -0,0 +1,556 @@
<script lang="ts">
export let options = {
// Advanced
seed: 0,
stop: '',
temperature: '',
repeat_penalty: '',
repeat_last_n: '',
mirostat: '',
mirostat_eta: '',
mirostat_tau: '',
top_k: '',
top_p: '',
tfs_z: '',
num_ctx: '',
num_predict: ''
};
</script>
<div class=" space-y-3 text-xs">
<div>
<div class=" py-0.5 flex w-full justify-between">
<div class=" w-20 text-xs font-medium self-center">Seed</div>
<div class=" flex-1 self-center">
<input
class="w-full rounded py-1.5 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none border border-gray-100 dark:border-gray-600"
type="number"
placeholder="Enter Seed"
bind:value={options.seed}
autocomplete="off"
min="0"
/>
</div>
</div>
</div>
<div>
<div class=" py-0.5 flex w-full justify-between">
<div class=" w-20 text-xs font-medium self-center">Stop Sequence</div>
<div class=" flex-1 self-center">
<input
class="w-full rounded py-1.5 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none border border-gray-100 dark:border-gray-600"
type="text"
placeholder="Enter Stop Sequence"
bind:value={options.stop}
autocomplete="off"
/>
</div>
</div>
</div>
<div class=" py-0.5 w-full justify-between">
<div class="flex w-full justify-between">
<div class=" self-center text-xs font-medium">Temperature</div>
<button
class="p-1 px-3 text-xs flex rounded transition"
type="button"
on:click={() => {
options.temperature = options.temperature === '' ? 0.8 : '';
}}
>
{#if options.temperature === ''}
<span class="ml-2 self-center"> Default </span>
{:else}
<span class="ml-2 self-center"> Custom </span>
{/if}
</button>
</div>
{#if options.temperature !== ''}
<div class="flex mt-0.5 space-x-2">
<div class=" flex-1">
<input
id="steps-range"
type="range"
min="0"
max="1"
step="0.05"
bind:value={options.temperature}
class="w-full h-2 rounded-lg appearance-none cursor-pointer dark:bg-gray-700"
/>
</div>
<div>
<input
bind:value={options.temperature}
type="number"
class=" bg-transparent text-center w-14"
min="0"
max="1"
step="0.05"
/>
</div>
</div>
{/if}
</div>
<div class=" py-0.5 w-full justify-between">
<div class="flex w-full justify-between">
<div class=" self-center text-xs font-medium">Mirostat</div>
<button
class="p-1 px-3 text-xs flex rounded transition"
type="button"
on:click={() => {
options.mirostat = options.mirostat === '' ? 0 : '';
}}
>
{#if options.mirostat === ''}
<span class="ml-2 self-center"> Default </span>
{:else}
<span class="ml-2 self-center"> Custom </span>
{/if}
</button>
</div>
{#if options.mirostat !== ''}
<div class="flex mt-0.5 space-x-2">
<div class=" flex-1">
<input
id="steps-range"
type="range"
min="0"
max="2"
step="1"
bind:value={options.mirostat}
class="w-full h-2 rounded-lg appearance-none cursor-pointer dark:bg-gray-700"
/>
</div>
<div>
<input
bind:value={options.mirostat}
type="number"
class=" bg-transparent text-center w-14"
min="0"
max="2"
step="1"
/>
</div>
</div>
{/if}
</div>
<div class=" py-0.5 w-full justify-between">
<div class="flex w-full justify-between">
<div class=" self-center text-xs font-medium">Mirostat Eta</div>
<button
class="p-1 px-3 text-xs flex rounded transition"
type="button"
on:click={() => {
options.mirostat_eta = options.mirostat_eta === '' ? 0.1 : '';
}}
>
{#if options.mirostat_eta === ''}
<span class="ml-2 self-center"> Default </span>
{:else}
<span class="ml-2 self-center"> Custom </span>
{/if}
</button>
</div>
{#if options.mirostat_eta !== ''}
<div class="flex mt-0.5 space-x-2">
<div class=" flex-1">
<input
id="steps-range"
type="range"
min="0"
max="1"
step="0.05"
bind:value={options.mirostat_eta}
class="w-full h-2 rounded-lg appearance-none cursor-pointer dark:bg-gray-700"
/>
</div>
<div>
<input
bind:value={options.mirostat_eta}
type="number"
class=" bg-transparent text-center w-14"
min="0"
max="1"
step="0.05"
/>
</div>
</div>
{/if}
</div>
<div class=" py-0.5 w-full justify-between">
<div class="flex w-full justify-between">
<div class=" self-center text-xs font-medium">Mirostat Tau</div>
<button
class="p-1 px-3 text-xs flex rounded transition"
type="button"
on:click={() => {
options.mirostat_tau = options.mirostat_tau === '' ? 5.0 : '';
}}
>
{#if options.mirostat_tau === ''}
<span class="ml-2 self-center"> Default </span>
{:else}
<span class="ml-2 self-center"> Custom </span>
{/if}
</button>
</div>
{#if options.mirostat_tau !== ''}
<div class="flex mt-0.5 space-x-2">
<div class=" flex-1">
<input
id="steps-range"
type="range"
min="0"
max="10"
step="0.5"
bind:value={options.mirostat_tau}
class="w-full h-2 rounded-lg appearance-none cursor-pointer dark:bg-gray-700"
/>
</div>
<div>
<input
bind:value={options.mirostat_tau}
type="number"
class=" bg-transparent text-center w-14"
min="0"
max="10"
step="0.5"
/>
</div>
</div>
{/if}
</div>
<div class=" py-0.5 w-full justify-between">
<div class="flex w-full justify-between">
<div class=" self-center text-xs font-medium">Top K</div>
<button
class="p-1 px-3 text-xs flex rounded transition"
type="button"
on:click={() => {
options.top_k = options.top_k === '' ? 40 : '';
}}
>
{#if options.top_k === ''}
<span class="ml-2 self-center"> Default </span>
{:else}
<span class="ml-2 self-center"> Custom </span>
{/if}
</button>
</div>
{#if options.top_k !== ''}
<div class="flex mt-0.5 space-x-2">
<div class=" flex-1">
<input
id="steps-range"
type="range"
min="0"
max="100"
step="0.5"
bind:value={options.top_k}
class="w-full h-2 rounded-lg appearance-none cursor-pointer dark:bg-gray-700"
/>
</div>
<div>
<input
bind:value={options.top_k}
type="number"
class=" bg-transparent text-center w-14"
min="0"
max="100"
step="0.5"
/>
</div>
</div>
{/if}
</div>
<div class=" py-0.5 w-full justify-between">
<div class="flex w-full justify-between">
<div class=" self-center text-xs font-medium">Top P</div>
<button
class="p-1 px-3 text-xs flex rounded transition"
type="button"
on:click={() => {
options.top_p = options.top_p === '' ? 0.9 : '';
}}
>
{#if options.top_p === ''}
<span class="ml-2 self-center"> Default </span>
{:else}
<span class="ml-2 self-center"> Custom </span>
{/if}
</button>
</div>
{#if options.top_p !== ''}
<div class="flex mt-0.5 space-x-2">
<div class=" flex-1">
<input
id="steps-range"
type="range"
min="0"
max="1"
step="0.05"
bind:value={options.top_p}
class="w-full h-2 rounded-lg appearance-none cursor-pointer dark:bg-gray-700"
/>
</div>
<div>
<input
bind:value={options.top_p}
type="number"
class=" bg-transparent text-center w-14"
min="0"
max="1"
step="0.05"
/>
</div>
</div>
{/if}
</div>
<div class=" py-0.5 w-full justify-between">
<div class="flex w-full justify-between">
<div class=" self-center text-xs font-medium">Repeat Penalty</div>
<button
class="p-1 px-3 text-xs flex rounded transition"
type="button"
on:click={() => {
options.repeat_penalty = options.repeat_penalty === '' ? 1.1 : '';
}}
>
{#if options.repeat_penalty === ''}
<span class="ml-2 self-center"> Default </span>
{:else}
<span class="ml-2 self-center"> Custom </span>
{/if}
</button>
</div>
{#if options.repeat_penalty !== ''}
<div class="flex mt-0.5 space-x-2">
<div class=" flex-1">
<input
id="steps-range"
type="range"
min="0"
max="2"
step="0.05"
bind:value={options.repeat_penalty}
class="w-full h-2 rounded-lg appearance-none cursor-pointer dark:bg-gray-700"
/>
</div>
<div>
<input
bind:value={options.repeat_penalty}
type="number"
class=" bg-transparent text-center w-14"
min="0"
max="2"
step="0.05"
/>
</div>
</div>
{/if}
</div>
<div class=" py-0.5 w-full justify-between">
<div class="flex w-full justify-between">
<div class=" self-center text-xs font-medium">Repeat Last N</div>
<button
class="p-1 px-3 text-xs flex rounded transition"
type="button"
on:click={() => {
options.repeat_last_n = options.repeat_last_n === '' ? 64 : '';
}}
>
{#if options.repeat_last_n === ''}
<span class="ml-2 self-center"> Default </span>
{:else}
<span class="ml-2 self-center"> Custom </span>
{/if}
</button>
</div>
{#if options.repeat_last_n !== ''}
<div class="flex mt-0.5 space-x-2">
<div class=" flex-1">
<input
id="steps-range"
type="range"
min="-1"
max="128"
step="1"
bind:value={options.repeat_last_n}
class="w-full h-2 rounded-lg appearance-none cursor-pointer dark:bg-gray-700"
/>
</div>
<div>
<input
bind:value={options.repeat_last_n}
type="number"
class=" bg-transparent text-center w-14"
min="-1"
max="128"
step="1"
/>
</div>
</div>
{/if}
</div>
<div class=" py-0.5 w-full justify-between">
<div class="flex w-full justify-between">
<div class=" self-center text-xs font-medium">Tfs Z</div>
<button
class="p-1 px-3 text-xs flex rounded transition"
type="button"
on:click={() => {
options.tfs_z = options.tfs_z === '' ? 1 : '';
}}
>
{#if options.tfs_z === ''}
<span class="ml-2 self-center"> Default </span>
{:else}
<span class="ml-2 self-center"> Custom </span>
{/if}
</button>
</div>
{#if options.tfs_z !== ''}
<div class="flex mt-0.5 space-x-2">
<div class=" flex-1">
<input
id="steps-range"
type="range"
min="0"
max="2"
step="0.05"
bind:value={options.tfs_z}
class="w-full h-2 rounded-lg appearance-none cursor-pointer dark:bg-gray-700"
/>
</div>
<div>
<input
bind:value={options.tfs_z}
type="number"
class=" bg-transparent text-center w-14"
min="0"
max="2"
step="0.05"
/>
</div>
</div>
{/if}
</div>
<div class=" py-0.5 w-full justify-between">
<div class="flex w-full justify-between">
<div class=" self-center text-xs font-medium">Context Length</div>
<button
class="p-1 px-3 text-xs flex rounded transition"
type="button"
on:click={() => {
options.num_ctx = options.num_ctx === '' ? 2048 : '';
}}
>
{#if options.num_ctx === ''}
<span class="ml-2 self-center"> Default </span>
{:else}
<span class="ml-2 self-center"> Custom </span>
{/if}
</button>
</div>
{#if options.num_ctx !== ''}
<div class="flex mt-0.5 space-x-2">
<div class=" flex-1">
<input
id="steps-range"
type="range"
min="1"
max="16000"
step="1"
bind:value={options.num_ctx}
class="w-full h-2 rounded-lg appearance-none cursor-pointer dark:bg-gray-700"
/>
</div>
<div class="">
<input
bind:value={options.num_ctx}
type="number"
class=" bg-transparent text-center w-14"
min="1"
max="16000"
step="1"
/>
</div>
</div>
{/if}
</div>
<div class=" py-0.5 w-full justify-between">
<div class="flex w-full justify-between">
<div class=" self-center text-xs font-medium">Max Tokens</div>
<button
class="p-1 px-3 text-xs flex rounded transition"
type="button"
on:click={() => {
options.num_predict = options.num_predict === '' ? 128 : '';
}}
>
{#if options.num_predict === ''}
<span class="ml-2 self-center"> Default </span>
{:else}
<span class="ml-2 self-center"> Custom </span>
{/if}
</button>
</div>
{#if options.num_predict !== ''}
<div class="flex mt-0.5 space-x-2">
<div class=" flex-1">
<input
id="steps-range"
type="range"
min="-2"
max="16000"
step="1"
bind:value={options.num_predict}
class="w-full h-2 rounded-lg appearance-none cursor-pointer dark:bg-gray-700"
/>
</div>
<div class="">
<input
bind:value={options.num_predict}
type="number"
class=" bg-transparent text-center w-14"
min="-2"
max="16000"
step="1"
/>
</div>
</div>
{/if}
</div>
</div>

View file

@ -0,0 +1,353 @@
<script lang="ts">
import fileSaver from 'file-saver';
const { saveAs } = fileSaver;
import { resetVectorDB } from '$lib/apis/rag';
import { chats, user } from '$lib/stores';
import {
createNewChat,
deleteAllChats,
getAllChats,
getAllUserChats,
getChatList
} from '$lib/apis/chats';
import { getImportOrigin, convertOpenAIChats } from '$lib/utils';
import { onMount } from 'svelte';
import { goto } from '$app/navigation';
import toast from 'svelte-french-toast';
export let saveSettings: Function;
// Chats
let saveChatHistory = true;
let importFiles;
let showDeleteConfirm = false;
$: if (importFiles) {
console.log(importFiles);
let reader = new FileReader();
reader.onload = (event) => {
let chats = JSON.parse(event.target.result);
console.log(chats);
if (getImportOrigin(chats) == 'openai') {
try {
chats = convertOpenAIChats(chats);
} catch (error) {
console.log('Unable to import chats:', error);
}
}
importChats(chats);
};
if (importFiles.length > 0) {
reader.readAsText(importFiles[0]);
}
}
const importChats = async (_chats) => {
for (const chat of _chats) {
console.log(chat);
if (chat.chat) {
await createNewChat(localStorage.token, chat.chat);
} else {
await createNewChat(localStorage.token, chat);
}
}
await chats.set(await getChatList(localStorage.token));
};
const exportChats = async () => {
let blob = new Blob([JSON.stringify(await getAllChats(localStorage.token))], {
type: 'application/json'
});
saveAs(blob, `chat-export-${Date.now()}.json`);
};
const exportAllUserChats = async () => {
let blob = new Blob([JSON.stringify(await getAllUserChats(localStorage.token))], {
type: 'application/json'
});
saveAs(blob, `all-chats-export-${Date.now()}.json`);
};
const deleteChats = async () => {
await goto('/');
await deleteAllChats(localStorage.token);
await chats.set(await getChatList(localStorage.token));
};
const toggleSaveChatHistory = async () => {
saveChatHistory = !saveChatHistory;
console.log(saveChatHistory);
if (saveChatHistory === false) {
await goto('/');
}
saveSettings({ saveChatHistory: saveChatHistory });
};
onMount(async () => {
let settings = JSON.parse(localStorage.getItem('settings') ?? '{}');
saveChatHistory = settings.saveChatHistory ?? true;
});
</script>
<div class="flex flex-col h-full justify-between space-y-3 text-sm">
<div class=" space-y-2">
<div
class="flex flex-col justify-between rounded-md items-center py-2 px-3.5 w-full transition"
>
<div class="flex w-full justify-between">
<div class=" self-center text-sm font-medium">Chat History</div>
<button
class="p-1 px-3 text-xs flex rounded transition"
type="button"
on:click={() => {
toggleSaveChatHistory();
}}
>
{#if saveChatHistory === true}
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
>
<path d="M8 9.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Z" />
<path
fill-rule="evenodd"
d="M1.38 8.28a.87.87 0 0 1 0-.566 7.003 7.003 0 0 1 13.238.006.87.87 0 0 1 0 .566A7.003 7.003 0 0 1 1.379 8.28ZM11 8a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z"
clip-rule="evenodd"
/>
</svg>
<span class="ml-2 self-center"> On </span>
{:else}
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
>
<path
fill-rule="evenodd"
d="M3.28 2.22a.75.75 0 0 0-1.06 1.06l10.5 10.5a.75.75 0 1 0 1.06-1.06l-1.322-1.323a7.012 7.012 0 0 0 2.16-3.11.87.87 0 0 0 0-.567A7.003 7.003 0 0 0 4.82 3.76l-1.54-1.54Zm3.196 3.195 1.135 1.136A1.502 1.502 0 0 1 9.45 8.389l1.136 1.135a3 3 0 0 0-4.109-4.109Z"
clip-rule="evenodd"
/>
<path
d="m7.812 10.994 1.816 1.816A7.003 7.003 0 0 1 1.38 8.28a.87.87 0 0 1 0-.566 6.985 6.985 0 0 1 1.113-2.039l2.513 2.513a3 3 0 0 0 2.806 2.806Z"
/>
</svg>
<span class="ml-2 self-center">Off</span>
{/if}
</button>
</div>
<div class="text-xs text-left w-full font-medium mt-0.5">
This setting does not sync across browsers or devices.
</div>
</div>
<hr class=" dark:border-gray-700" />
<div class="flex flex-col">
<input id="chat-import-input" bind:files={importFiles} type="file" accept=".json" hidden />
<button
class=" flex rounded-md py-2 px-3.5 w-full hover:bg-gray-200 dark:hover:bg-gray-800 transition"
on:click={() => {
document.getElementById('chat-import-input').click();
}}
>
<div class=" self-center mr-3">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
>
<path
fill-rule="evenodd"
d="M4 2a1.5 1.5 0 0 0-1.5 1.5v9A1.5 1.5 0 0 0 4 14h8a1.5 1.5 0 0 0 1.5-1.5V6.621a1.5 1.5 0 0 0-.44-1.06L9.94 2.439A1.5 1.5 0 0 0 8.878 2H4Zm4 9.5a.75.75 0 0 1-.75-.75V8.06l-.72.72a.75.75 0 0 1-1.06-1.06l2-2a.75.75 0 0 1 1.06 0l2 2a.75.75 0 1 1-1.06 1.06l-.72-.72v2.69a.75.75 0 0 1-.75.75Z"
clip-rule="evenodd"
/>
</svg>
</div>
<div class=" self-center text-sm font-medium">Import Chats</div>
</button>
<button
class=" flex rounded-md py-2 px-3.5 w-full hover:bg-gray-200 dark:hover:bg-gray-800 transition"
on:click={() => {
exportChats();
}}
>
<div class=" self-center mr-3">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
>
<path
fill-rule="evenodd"
d="M4 2a1.5 1.5 0 0 0-1.5 1.5v9A1.5 1.5 0 0 0 4 14h8a1.5 1.5 0 0 0 1.5-1.5V6.621a1.5 1.5 0 0 0-.44-1.06L9.94 2.439A1.5 1.5 0 0 0 8.878 2H4Zm4 3.5a.75.75 0 0 1 .75.75v2.69l.72-.72a.75.75 0 1 1 1.06 1.06l-2 2a.75.75 0 0 1-1.06 0l-2-2a.75.75 0 0 1 1.06-1.06l.72.72V6.25A.75.75 0 0 1 8 5.5Z"
clip-rule="evenodd"
/>
</svg>
</div>
<div class=" self-center text-sm font-medium">Export Chats</div>
</button>
</div>
<hr class=" dark:border-gray-700" />
{#if showDeleteConfirm}
<div class="flex justify-between rounded-md items-center py-2 px-3.5 w-full transition">
<div class="flex items-center space-x-3">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
>
<path d="M2 3a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v1a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V3Z" />
<path
fill-rule="evenodd"
d="M13 6H3v6a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V6ZM5.72 7.47a.75.75 0 0 1 1.06 0L8 8.69l1.22-1.22a.75.75 0 1 1 1.06 1.06L9.06 9.75l1.22 1.22a.75.75 0 1 1-1.06 1.06L8 10.81l-1.22 1.22a.75.75 0 0 1-1.06-1.06l1.22-1.22-1.22-1.22a.75.75 0 0 1 0-1.06Z"
clip-rule="evenodd"
/>
</svg>
<span>Are you sure?</span>
</div>
<div class="flex space-x-1.5 items-center">
<button
class="hover:text-white transition"
on:click={() => {
deleteChats();
showDeleteConfirm = false;
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-4 h-4"
>
<path
fill-rule="evenodd"
d="M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z"
clip-rule="evenodd"
/>
</svg>
</button>
<button
class="hover:text-white transition"
on:click={() => {
showDeleteConfirm = false;
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-4 h-4"
>
<path
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
/>
</svg>
</button>
</div>
</div>
{:else}
<button
class=" flex rounded-md py-2 px-3.5 w-full hover:bg-gray-200 dark:hover:bg-gray-800 transition"
on:click={() => {
showDeleteConfirm = true;
}}
>
<div class=" self-center mr-3">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
>
<path
fill-rule="evenodd"
d="M4 2a1.5 1.5 0 0 0-1.5 1.5v9A1.5 1.5 0 0 0 4 14h8a1.5 1.5 0 0 0 1.5-1.5V6.621a1.5 1.5 0 0 0-.44-1.06L9.94 2.439A1.5 1.5 0 0 0 8.878 2H4Zm7 7a.75.75 0 0 1-.75.75h-4.5a.75.75 0 0 1 0-1.5h4.5A.75.75 0 0 1 11 9Z"
clip-rule="evenodd"
/>
</svg>
</div>
<div class=" self-center text-sm font-medium">Delete Chats</div>
</button>
{/if}
{#if $user?.role === 'admin'}
<hr class=" dark:border-gray-700" />
<button
class=" flex rounded-md py-2 px-3.5 w-full hover:bg-gray-200 dark:hover:bg-gray-800 transition"
on:click={() => {
exportAllUserChats();
}}
>
<div class=" self-center mr-3">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
>
<path d="M2 3a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v1a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V3Z" />
<path
fill-rule="evenodd"
d="M13 6H3v6a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V6ZM8.75 7.75a.75.75 0 0 0-1.5 0v2.69L6.03 9.22a.75.75 0 0 0-1.06 1.06l2.5 2.5a.75.75 0 0 0 1.06 0l2.5-2.5a.75.75 0 1 0-1.06-1.06l-1.22 1.22V7.75Z"
clip-rule="evenodd"
/>
</svg>
</div>
<div class=" self-center text-sm font-medium">Export All Chats (All Users)</div>
</button>
<hr class=" dark:border-gray-700" />
<button
class=" flex rounded-md py-2 px-3.5 w-full hover:bg-gray-200 dark:hover:bg-gray-800 transition"
on:click={() => {
const res = resetVectorDB(localStorage.token).catch((error) => {
toast.error(error);
return null;
});
if (res) {
toast.success('Success');
}
}}
>
<div class=" self-center mr-3">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
>
<path
fill-rule="evenodd"
d="M3.5 2A1.5 1.5 0 0 0 2 3.5v9A1.5 1.5 0 0 0 3.5 14h9a1.5 1.5 0 0 0 1.5-1.5v-7A1.5 1.5 0 0 0 12.5 4H9.621a1.5 1.5 0 0 1-1.06-.44L7.439 2.44A1.5 1.5 0 0 0 6.38 2H3.5Zm6.75 7.75a.75.75 0 0 0 0-1.5h-4.5a.75.75 0 0 0 0 1.5h4.5Z"
clip-rule="evenodd"
/>
</svg>
</div>
<div class=" self-center text-sm font-medium">Reset Vector Storage</div>
</button>
{/if}
</div>
</div>

View file

@ -0,0 +1,86 @@
<script lang="ts">
import { getOpenAIKey, getOpenAIUrl, updateOpenAIKey, updateOpenAIUrl } from '$lib/apis/openai';
import { models, user } from '$lib/stores';
import { createEventDispatcher, onMount } from 'svelte';
const dispatch = createEventDispatcher();
export let getModels: Function;
// External
let OPENAI_API_KEY = '';
let OPENAI_API_BASE_URL = '';
const updateOpenAIHandler = async () => {
OPENAI_API_BASE_URL = await updateOpenAIUrl(localStorage.token, OPENAI_API_BASE_URL);
OPENAI_API_KEY = await updateOpenAIKey(localStorage.token, OPENAI_API_KEY);
await models.set(await getModels());
};
onMount(async () => {
if ($user.role === 'admin') {
OPENAI_API_BASE_URL = await getOpenAIUrl(localStorage.token);
OPENAI_API_KEY = await getOpenAIKey(localStorage.token);
}
});
</script>
<form
class="flex flex-col h-full justify-between space-y-3 text-sm"
on:submit|preventDefault={() => {
updateOpenAIHandler();
dispatch('save');
// saveSettings({
// OPENAI_API_KEY: OPENAI_API_KEY !== '' ? OPENAI_API_KEY : undefined,
// OPENAI_API_BASE_URL: OPENAI_API_BASE_URL !== '' ? OPENAI_API_BASE_URL : undefined
// });
}}
>
<div class=" space-y-3">
<div>
<div class=" mb-2.5 text-sm font-medium">OpenAI API Key</div>
<div class="flex w-full">
<div class="flex-1">
<input
class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none"
placeholder="Enter OpenAI API Key"
bind:value={OPENAI_API_KEY}
autocomplete="off"
/>
</div>
</div>
<div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
Adds optional support for online models.
</div>
</div>
<hr class=" dark:border-gray-700" />
<div>
<div class=" mb-2.5 text-sm font-medium">OpenAI API Base URL</div>
<div class="flex w-full">
<div class="flex-1">
<input
class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none"
placeholder="Enter OpenAI API Key"
bind:value={OPENAI_API_BASE_URL}
autocomplete="off"
/>
</div>
</div>
<div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
WebUI will make requests to <span class=" text-gray-200">'{OPENAI_API_BASE_URL}/chat'</span>
</div>
</div>
</div>
<div class="flex justify-end pt-3 text-sm font-medium">
<button
class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded"
type="submit"
>
Save
</button>
</div>
</form>

View file

@ -0,0 +1,224 @@
<script lang="ts">
import toast from 'svelte-french-toast';
import { createEventDispatcher, onMount } from 'svelte';
const dispatch = createEventDispatcher();
import { getOllamaAPIUrl, updateOllamaAPIUrl } from '$lib/apis/ollama';
import { models, user } from '$lib/stores';
export let saveSettings: Function;
export let getModels: Function;
// General
let API_BASE_URL = '';
let themes = ['dark', 'light', 'rose-pine dark', 'rose-pine-dawn light'];
let theme = 'dark';
let notificationEnabled = false;
let system = '';
const toggleTheme = async () => {
if (theme === 'dark') {
theme = 'light';
} else {
theme = 'dark';
}
localStorage.theme = theme;
document.documentElement.classList.remove(theme === 'dark' ? 'light' : 'dark');
document.documentElement.classList.add(theme);
};
const toggleNotification = async () => {
const permission = await Notification.requestPermission();
if (permission === 'granted') {
notificationEnabled = !notificationEnabled;
saveSettings({ notificationEnabled: notificationEnabled });
} else {
toast.error(
'Response notifications cannot be activated as the website permissions have been denied. Please visit your browser settings to grant the necessary access.'
);
}
};
const updateOllamaAPIUrlHandler = async () => {
API_BASE_URL = await updateOllamaAPIUrl(localStorage.token, API_BASE_URL);
const _models = await getModels('ollama');
if (_models.length > 0) {
toast.success('Server connection verified');
await models.set(_models);
}
};
onMount(async () => {
if ($user.role === 'admin') {
API_BASE_URL = await getOllamaAPIUrl(localStorage.token);
}
let settings = JSON.parse(localStorage.getItem('settings') ?? '{}');
theme = localStorage.theme ?? 'dark';
notificationEnabled = settings.notificationEnabled ?? false;
system = settings.system ?? '';
});
</script>
<div class="flex flex-col space-y-3">
<div>
<div class=" mb-1 text-sm font-medium">WebUI Settings</div>
<div class=" py-0.5 flex w-full justify-between">
<div class=" self-center text-xs font-medium">Theme</div>
<div class="flex items-center relative">
<div class=" absolute right-16">
{#if theme === 'dark'}
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-4 h-4"
>
<path
fill-rule="evenodd"
d="M7.455 2.004a.75.75 0 01.26.77 7 7 0 009.958 7.967.75.75 0 011.067.853A8.5 8.5 0 116.647 1.921a.75.75 0 01.808.083z"
clip-rule="evenodd"
/>
</svg>
{:else if theme === 'light'}
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-4 h-4 self-center"
>
<path
d="M10 2a.75.75 0 01.75.75v1.5a.75.75 0 01-1.5 0v-1.5A.75.75 0 0110 2zM10 15a.75.75 0 01.75.75v1.5a.75.75 0 01-1.5 0v-1.5A.75.75 0 0110 15zM10 7a3 3 0 100 6 3 3 0 000-6zM15.657 5.404a.75.75 0 10-1.06-1.06l-1.061 1.06a.75.75 0 001.06 1.06l1.06-1.06zM6.464 14.596a.75.75 0 10-1.06-1.06l-1.06 1.06a.75.75 0 001.06 1.06l1.06-1.06zM18 10a.75.75 0 01-.75.75h-1.5a.75.75 0 010-1.5h1.5A.75.75 0 0118 10zM5 10a.75.75 0 01-.75.75h-1.5a.75.75 0 010-1.5h1.5A.75.75 0 015 10zM14.596 15.657a.75.75 0 001.06-1.06l-1.06-1.061a.75.75 0 10-1.06 1.06l1.06 1.06zM5.404 6.464a.75.75 0 001.06-1.06l-1.06-1.06a.75.75 0 10-1.061 1.06l1.06 1.06z"
/>
</svg>
{/if}
</div>
<select
class="w-fit pr-8 rounded py-2 px-2 text-xs bg-transparent outline-none text-right"
bind:value={theme}
placeholder="Select a theme"
on:change={(e) => {
localStorage.theme = theme;
themes
.filter((e) => e !== theme)
.forEach((e) => {
e.split(' ').forEach((e) => {
document.documentElement.classList.remove(e);
});
});
theme.split(' ').forEach((e) => {
document.documentElement.classList.add(e);
});
console.log(theme);
}}
>
<option value="dark">Dark</option>
<option value="light">Light</option>
<option value="rose-pine dark">Rosé Pine</option>
<option value="rose-pine-dawn light">Rosé Pine Dawn</option>
</select>
</div>
</div>
<div>
<div class=" py-0.5 flex w-full justify-between">
<div class=" self-center text-xs font-medium">Notification</div>
<button
class="p-1 px-3 text-xs flex rounded transition"
on:click={() => {
toggleNotification();
}}
type="button"
>
{#if notificationEnabled === true}
<span class="ml-2 self-center">On</span>
{:else}
<span class="ml-2 self-center">Off</span>
{/if}
</button>
</div>
</div>
</div>
{#if $user.role === 'admin'}
<hr class=" dark:border-gray-700" />
<div>
<div class=" mb-2.5 text-sm font-medium">Ollama API URL</div>
<div class="flex w-full">
<div class="flex-1 mr-2">
<input
class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none"
placeholder="Enter URL (e.g. http://localhost:11434/api)"
bind:value={API_BASE_URL}
/>
</div>
<button
class="px-3 bg-gray-200 hover:bg-gray-300 dark:bg-gray-600 dark:hover:bg-gray-700 rounded transition"
on:click={() => {
updateOllamaAPIUrlHandler();
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-4 h-4"
>
<path
fill-rule="evenodd"
d="M15.312 11.424a5.5 5.5 0 01-9.201 2.466l-.312-.311h2.433a.75.75 0 000-1.5H3.989a.75.75 0 00-.75.75v4.242a.75.75 0 001.5 0v-2.43l.31.31a7 7 0 0011.712-3.138.75.75 0 00-1.449-.39zm1.23-3.723a.75.75 0 00.219-.53V2.929a.75.75 0 00-1.5 0V5.36l-.31-.31A7 7 0 003.239 8.188a.75.75 0 101.448.389A5.5 5.5 0 0113.89 6.11l.311.31h-2.432a.75.75 0 000 1.5h4.243a.75.75 0 00.53-.219z"
clip-rule="evenodd"
/>
</svg>
</button>
</div>
<div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
Trouble accessing Ollama?
<a
class=" text-gray-300 font-medium"
href="https://github.com/ollama-webui/ollama-webui#troubleshooting"
target="_blank"
>
Click here for help.
</a>
</div>
</div>
{/if}
<hr class=" dark:border-gray-700" />
<div>
<div class=" mb-2.5 text-sm font-medium">System Prompt</div>
<textarea
bind:value={system}
class="w-full rounded p-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none resize-none"
rows="4"
/>
</div>
<div class="flex justify-end pt-3 text-sm font-medium">
<button
class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded"
on:click={() => {
saveSettings({
system: system !== '' ? system : undefined
});
dispatch('save');
}}
>
Save
</button>
</div>
</div>

View file

@ -0,0 +1,118 @@
<script lang="ts">
import { getBackendConfig } from '$lib/apis';
import { setDefaultPromptSuggestions } from '$lib/apis/configs';
import { config, user } from '$lib/stores';
import { createEventDispatcher, onMount } from 'svelte';
const dispatch = createEventDispatcher();
// Interface
let promptSuggestions = [];
const updateInterfaceHandler = async () => {
promptSuggestions = await setDefaultPromptSuggestions(localStorage.token, promptSuggestions);
await config.set(await getBackendConfig());
};
onMount(async () => {
if ($user.role === 'admin') {
promptSuggestions = $config?.default_prompt_suggestions;
}
});
</script>
<form
class="flex flex-col h-full justify-between space-y-3 text-sm"
on:submit|preventDefault={() => {
updateInterfaceHandler();
dispatch('save');
}}
>
<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-80">
<div class="flex w-full justify-between mb-2">
<div class=" self-center text-sm font-semibold">Default Prompt Suggestions</div>
<button
class="p-1 px-3 text-xs flex rounded transition"
type="button"
on:click={() => {
if (promptSuggestions.length === 0 || promptSuggestions.at(-1).content !== '') {
promptSuggestions = [...promptSuggestions, { content: '', title: ['', ''] }];
}
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-4 h-4"
>
<path
d="M10.75 4.75a.75.75 0 00-1.5 0v4.5h-4.5a.75.75 0 000 1.5h4.5v4.5a.75.75 0 001.5 0v-4.5h4.5a.75.75 0 000-1.5h-4.5v-4.5z"
/>
</svg>
</button>
</div>
<div class="flex flex-col space-y-1">
{#each promptSuggestions as prompt, promptIdx}
<div class=" flex border dark:border-gray-600 rounded-lg">
<div class="flex flex-col flex-1">
<div class="flex border-b dark:border-gray-600 w-full">
<input
class="px-3 py-1.5 text-xs w-full bg-transparent outline-none border-r dark:border-gray-600"
placeholder="Title (e.g. Tell me a fun fact)"
bind:value={prompt.title[0]}
/>
<input
class="px-3 py-1.5 text-xs w-full bg-transparent outline-none border-r dark:border-gray-600"
placeholder="Subtitle (e.g. about the Roman Empire)"
bind:value={prompt.title[1]}
/>
</div>
<input
class="px-3 py-1.5 text-xs w-full bg-transparent outline-none border-r dark:border-gray-600"
placeholder="Prompt (e.g. Tell me a fun fact about the Roman Empire)"
bind:value={prompt.content}
/>
</div>
<button
class="px-2"
type="button"
on:click={() => {
promptSuggestions.splice(promptIdx, 1);
promptSuggestions = promptSuggestions;
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-4 h-4"
>
<path
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
/>
</svg>
</button>
</div>
{/each}
</div>
{#if promptSuggestions.length > 0}
<div class="text-xs text-left w-full mt-2">
Adjusting these settings will apply changes universally to all users.
</div>
{/if}
</div>
<div class="flex justify-end pt-3 text-sm font-medium">
<button
class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded"
type="submit"
>
Save
</button>
</div>
</form>

View file

@ -0,0 +1,596 @@
<script lang="ts">
import queue from 'async/queue';
import toast from 'svelte-french-toast';
import { createModel, deleteModel, pullModel } from '$lib/apis/ollama';
import { WEBUI_API_BASE_URL } from '$lib/constants';
import { models, user } from '$lib/stores';
import { splitStream } from '$lib/utils';
export let getModels: Function;
// Models
const MAX_PARALLEL_DOWNLOADS = 3;
const modelDownloadQueue = queue(
(task: { modelName: string }, cb) =>
pullModelHandlerProcessor({ modelName: task.modelName, callback: cb }),
MAX_PARALLEL_DOWNLOADS
);
let modelDownloadStatus: Record<string, any> = {};
let modelTransferring = false;
let modelTag = '';
let digest = '';
let pullProgress = null;
let modelUploadMode = 'file';
let modelInputFile = '';
let modelFileUrl = '';
let modelFileContent = `TEMPLATE """{{ .System }}\nUSER: {{ .Prompt }}\nASSSISTANT: """\nPARAMETER num_ctx 4096\nPARAMETER stop "</s>"\nPARAMETER stop "USER:"\nPARAMETER stop "ASSSISTANT:"`;
let modelFileDigest = '';
let uploadProgress = null;
let deleteModelTag = '';
const pullModelHandler = async () => {
const sanitizedModelTag = modelTag.trim();
if (modelDownloadStatus[sanitizedModelTag]) {
toast.error(`Model '${sanitizedModelTag}' is already in queue for downloading.`);
return;
}
if (Object.keys(modelDownloadStatus).length === 3) {
toast.error('Maximum of 3 models can be downloaded simultaneously. Please try again later.');
return;
}
modelTransferring = true;
modelDownloadQueue.push(
{ modelName: sanitizedModelTag },
async (data: { modelName: string; success: boolean; error?: Error }) => {
const { modelName } = data;
// Remove the downloaded model
delete modelDownloadStatus[modelName];
console.log(data);
if (!data.success) {
toast.error(data.error);
} else {
toast.success(`Model '${modelName}' has been successfully downloaded.`);
const notification = new Notification(`Ollama`, {
body: `Model '${modelName}' has been successfully downloaded.`,
icon: '/favicon.png'
});
models.set(await getModels());
}
}
);
modelTag = '';
modelTransferring = false;
};
const uploadModelHandler = async () => {
modelTransferring = true;
uploadProgress = 0;
let uploaded = false;
let fileResponse = null;
let name = '';
if (modelUploadMode === 'file') {
const file = modelInputFile[0];
const formData = new FormData();
formData.append('file', file);
fileResponse = await fetch(`${WEBUI_API_BASE_URL}/utils/upload`, {
method: 'POST',
headers: {
...($user && { Authorization: `Bearer ${localStorage.token}` })
},
body: formData
}).catch((error) => {
console.log(error);
return null;
});
} else {
fileResponse = await fetch(`${WEBUI_API_BASE_URL}/utils/download?url=${modelFileUrl}`, {
method: 'GET',
headers: {
...($user && { Authorization: `Bearer ${localStorage.token}` })
}
}).catch((error) => {
console.log(error);
return null;
});
}
if (fileResponse && fileResponse.ok) {
const reader = fileResponse.body
.pipeThrough(new TextDecoderStream())
.pipeThrough(splitStream('\n'))
.getReader();
while (true) {
const { value, done } = await reader.read();
if (done) break;
try {
let lines = value.split('\n');
for (const line of lines) {
if (line !== '') {
let data = JSON.parse(line.replace(/^data: /, ''));
if (data.progress) {
uploadProgress = data.progress;
}
if (data.error) {
throw data.error;
}
if (data.done) {
modelFileDigest = data.blob;
name = data.name;
uploaded = true;
}
}
}
} catch (error) {
console.log(error);
}
}
}
if (uploaded) {
const res = await createModel(
localStorage.token,
`${name}:latest`,
`FROM @${modelFileDigest}\n${modelFileContent}`
);
if (res && res.ok) {
const reader = res.body
.pipeThrough(new TextDecoderStream())
.pipeThrough(splitStream('\n'))
.getReader();
while (true) {
const { value, done } = await reader.read();
if (done) break;
try {
let lines = value.split('\n');
for (const line of lines) {
if (line !== '') {
console.log(line);
let data = JSON.parse(line);
console.log(data);
if (data.error) {
throw data.error;
}
if (data.detail) {
throw data.detail;
}
if (data.status) {
if (
!data.digest &&
!data.status.includes('writing') &&
!data.status.includes('sha256')
) {
toast.success(data.status);
} else {
if (data.digest) {
digest = data.digest;
if (data.completed) {
pullProgress = Math.round((data.completed / data.total) * 1000) / 10;
} else {
pullProgress = 100;
}
}
}
}
}
}
} catch (error) {
console.log(error);
toast.error(error);
}
}
}
}
modelFileUrl = '';
modelInputFile = '';
modelTransferring = false;
uploadProgress = null;
models.set(await getModels());
};
const deleteModelHandler = async () => {
const res = await deleteModel(localStorage.token, deleteModelTag).catch((error) => {
toast.error(error);
});
if (res) {
toast.success(`Deleted ${deleteModelTag}`);
}
deleteModelTag = '';
models.set(await getModels());
};
const pullModelHandlerProcessor = async (opts: { modelName: string; callback: Function }) => {
const res = await pullModel(localStorage.token, opts.modelName).catch((error) => {
opts.callback({ success: false, error, modelName: opts.modelName });
return null;
});
if (res) {
const reader = res.body
.pipeThrough(new TextDecoderStream())
.pipeThrough(splitStream('\n'))
.getReader();
while (true) {
try {
const { value, done } = await reader.read();
if (done) break;
let lines = value.split('\n');
for (const line of lines) {
if (line !== '') {
let data = JSON.parse(line);
if (data.error) {
throw data.error;
}
if (data.detail) {
throw data.detail;
}
if (data.status) {
if (data.digest) {
let downloadProgress = 0;
if (data.completed) {
downloadProgress = Math.round((data.completed / data.total) * 1000) / 10;
} else {
downloadProgress = 100;
}
modelDownloadStatus[opts.modelName] = {
pullProgress: downloadProgress,
digest: data.digest
};
} else {
toast.success(data.status);
}
}
}
}
} catch (error) {
console.log(error);
if (typeof error !== 'string') {
error = error.message;
}
opts.callback({ success: false, error, modelName: opts.modelName });
}
}
opts.callback({ success: true, modelName: opts.modelName });
}
};
</script>
<div class="flex flex-col h-full justify-between text-sm">
<div class=" space-y-3 pr-1.5 overflow-y-scroll h-80">
<div>
<div class=" mb-2.5 text-sm font-medium">Pull a model from Ollama.ai</div>
<div class="flex w-full">
<div class="flex-1 mr-2">
<input
class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none"
placeholder="Enter model tag (e.g. mistral:7b)"
bind:value={modelTag}
/>
</div>
<button
class="px-3 text-gray-100 bg-emerald-600 hover:bg-emerald-700 disabled:bg-gray-700 disabled:cursor-not-allowed rounded transition"
on:click={() => {
pullModelHandler();
}}
disabled={modelTransferring}
>
{#if modelTransferring}
<div class="self-center">
<svg
class=" w-4 h-4"
viewBox="0 0 24 24"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
><style>
.spinner_ajPY {
transform-origin: center;
animation: spinner_AtaB 0.75s infinite linear;
}
@keyframes spinner_AtaB {
100% {
transform: rotate(360deg);
}
}
</style><path
d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
opacity=".25"
/><path
d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
class="spinner_ajPY"
/></svg
>
</div>
{:else}
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
>
<path
d="M8.75 2.75a.75.75 0 0 0-1.5 0v5.69L5.03 6.22a.75.75 0 0 0-1.06 1.06l3.5 3.5a.75.75 0 0 0 1.06 0l3.5-3.5a.75.75 0 0 0-1.06-1.06L8.75 8.44V2.75Z"
/>
<path
d="M3.5 9.75a.75.75 0 0 0-1.5 0v1.5A2.75 2.75 0 0 0 4.75 14h6.5A2.75 2.75 0 0 0 14 11.25v-1.5a.75.75 0 0 0-1.5 0v1.5c0 .69-.56 1.25-1.25 1.25h-6.5c-.69 0-1.25-.56-1.25-1.25v-1.5Z"
/>
</svg>
{/if}
</button>
</div>
<div class="mt-2 mb-1 text-xs text-gray-400 dark:text-gray-500">
To access the available model names for downloading, <a
class=" text-gray-500 dark:text-gray-300 font-medium"
href="https://ollama.ai/library"
target="_blank">click here.</a
>
</div>
{#if Object.keys(modelDownloadStatus).length > 0}
{#each Object.keys(modelDownloadStatus) as model}
<div class="flex flex-col">
<div class="font-medium mb-1">{model}</div>
<div class="">
<div
class="dark:bg-gray-600 bg-gray-500 text-xs font-medium text-gray-100 text-center p-0.5 leading-none rounded-full"
style="width: {Math.max(15, modelDownloadStatus[model].pullProgress ?? 0)}%"
>
{modelDownloadStatus[model].pullProgress ?? 0}%
</div>
<div class="mt-1 text-xs dark:text-gray-500" style="font-size: 0.5rem;">
{modelDownloadStatus[model].digest}
</div>
</div>
</div>
{/each}
{/if}
</div>
<hr class=" dark:border-gray-700" />
<div>
<div class=" mb-2.5 text-sm font-medium">Delete a model</div>
<div class="flex w-full">
<div class="flex-1 mr-2">
<select
class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none"
bind:value={deleteModelTag}
placeholder="Select a model"
>
{#if !deleteModelTag}
<option value="" disabled selected>Select a model</option>
{/if}
{#each $models.filter((m) => m.size != null) as model}
<option value={model.name} class="bg-gray-100 dark:bg-gray-700"
>{model.name + ' (' + (model.size / 1024 ** 3).toFixed(1) + ' GB)'}</option
>
{/each}
</select>
</div>
<button
class="px-3 bg-red-700 hover:bg-red-800 text-gray-100 rounded transition"
on:click={() => {
deleteModelHandler();
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
>
<path
fill-rule="evenodd"
d="M5 3.25V4H2.75a.75.75 0 0 0 0 1.5h.3l.815 8.15A1.5 1.5 0 0 0 5.357 15h5.285a1.5 1.5 0 0 0 1.493-1.35l.815-8.15h.3a.75.75 0 0 0 0-1.5H11v-.75A2.25 2.25 0 0 0 8.75 1h-1.5A2.25 2.25 0 0 0 5 3.25Zm2.25-.75a.75.75 0 0 0-.75.75V4h3v-.75a.75.75 0 0 0-.75-.75h-1.5ZM6.05 6a.75.75 0 0 1 .787.713l.275 5.5a.75.75 0 0 1-1.498.075l-.275-5.5A.75.75 0 0 1 6.05 6Zm3.9 0a.75.75 0 0 1 .712.787l-.275 5.5a.75.75 0 0 1-1.498-.075l.275-5.5a.75.75 0 0 1 .786-.711Z"
clip-rule="evenodd"
/>
</svg>
</button>
</div>
</div>
<hr class=" dark:border-gray-700" />
<form
on:submit|preventDefault={() => {
uploadModelHandler();
}}
>
<div class=" mb-2 flex w-full justify-between">
<div class=" text-sm font-medium">
Upload a GGUF model <a
class=" text-xs font-medium text-gray-500 underline"
href="https://github.com/jmorganca/ollama/blob/main/README.md#import-from-gguf"
target="_blank">(Experimental)</a
>
</div>
<button
class="p-1 px-3 text-xs flex rounded transition"
on:click={() => {
if (modelUploadMode === 'file') {
modelUploadMode = 'url';
} else {
modelUploadMode = 'file';
}
}}
type="button"
>
{#if modelUploadMode === 'file'}
<span class="ml-2 self-center">File Mode</span>
{:else}
<span class="ml-2 self-center">URL Mode</span>
{/if}
</button>
</div>
<div class="flex w-full mb-1.5">
<div class="flex flex-col w-full">
{#if modelUploadMode === 'file'}
<div class="flex-1 {modelInputFile && modelInputFile.length > 0 ? 'mr-2' : ''}">
<input
id="model-upload-input"
type="file"
bind:files={modelInputFile}
on:change={() => {
console.log(modelInputFile);
}}
accept=".gguf"
required
hidden
/>
<button
type="button"
class="w-full rounded text-left py-2 px-4 dark:text-gray-300 dark:bg-gray-800"
on:click={() => {
document.getElementById('model-upload-input').click();
}}
>
{#if modelInputFile && modelInputFile.length > 0}
{modelInputFile[0].name}
{:else}
Click here to select
{/if}
</button>
</div>
{:else}
<div class="flex-1 {modelFileUrl !== '' ? 'mr-2' : ''}">
<input
class="w-full rounded text-left py-2 px-4 dark:text-gray-300 dark:bg-gray-800 outline-none {modelFileUrl !==
''
? 'mr-2'
: ''}"
type="url"
required
bind:value={modelFileUrl}
placeholder="Type HuggingFace Resolve (Download) URL"
/>
</div>
{/if}
</div>
{#if (modelUploadMode === 'file' && modelInputFile && modelInputFile.length > 0) || (modelUploadMode === 'url' && modelFileUrl !== '')}
<button
class="px-3 text-gray-100 bg-emerald-600 hover:bg-emerald-700 disabled:bg-gray-700 disabled:cursor-not-allowed rounded transition"
type="submit"
disabled={modelTransferring}
>
{#if modelTransferring}
<div class="self-center">
<svg
class=" w-4 h-4"
viewBox="0 0 24 24"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
><style>
.spinner_ajPY {
transform-origin: center;
animation: spinner_AtaB 0.75s infinite linear;
}
@keyframes spinner_AtaB {
100% {
transform: rotate(360deg);
}
}
</style><path
d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
opacity=".25"
/><path
d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
class="spinner_ajPY"
/></svg
>
</div>
{:else}
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
>
<path
d="M7.25 10.25a.75.75 0 0 0 1.5 0V4.56l2.22 2.22a.75.75 0 1 0 1.06-1.06l-3.5-3.5a.75.75 0 0 0-1.06 0l-3.5 3.5a.75.75 0 0 0 1.06 1.06l2.22-2.22v5.69Z"
/>
<path
d="M3.5 9.75a.75.75 0 0 0-1.5 0v1.5A2.75 2.75 0 0 0 4.75 14h6.5A2.75 2.75 0 0 0 14 11.25v-1.5a.75.75 0 0 0-1.5 0v1.5c0 .69-.56 1.25-1.25 1.25h-6.5c-.69 0-1.25-.56-1.25-1.25v-1.5Z"
/>
</svg>
{/if}
</button>
{/if}
</div>
{#if (modelUploadMode === 'file' && modelInputFile && modelInputFile.length > 0) || (modelUploadMode === 'url' && modelFileUrl !== '')}
<div>
<div>
<div class=" my-2.5 text-sm font-medium">Modelfile Content</div>
<textarea
bind:value={modelFileContent}
class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none resize-none"
rows="6"
/>
</div>
</div>
{/if}
<div class=" mt-1 text-xs text-gray-400 dark:text-gray-500">
To access the GGUF models available for downloading, <a
class=" text-gray-500 dark:text-gray-300 font-medium"
href="https://huggingface.co/models?search=gguf"
target="_blank">click here.</a
>
</div>
{#if uploadProgress !== null}
<div class="mt-2">
<div class=" mb-2 text-xs">Upload Progress</div>
<div class="w-full rounded-full dark:bg-gray-800">
<div
class="dark:bg-gray-600 bg-gray-500 text-xs font-medium text-gray-100 text-center p-0.5 leading-none rounded-full"
style="width: {Math.max(15, uploadProgress ?? 0)}%"
>
{uploadProgress ?? 0}%
</div>
</div>
<div class="mt-1 text-xs dark:text-gray-500" style="font-size: 0.5rem;">
{modelFileDigest}
</div>
</div>
{/if}
</form>
</div>
</div>

File diff suppressed because it is too large Load diff

View file

@ -4,7 +4,7 @@
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { settings, user, config, modelfiles, models } from '$lib/stores'; import { settings, user, config, modelfiles, models } from '$lib/stores';
import Advanced from '$lib/components/chat/Settings/Advanced.svelte'; import AdvancedParams from '$lib/components/chat/Settings/Advanced/AdvancedParams.svelte';
import { splitStream } from '$lib/utils'; import { splitStream } from '$lib/utils';
import { onMount, tick } from 'svelte'; import { onMount, tick } from 'svelte';
import { createModel } from '$lib/apis/ollama'; import { createModel } from '$lib/apis/ollama';
@ -552,7 +552,7 @@ SYSTEM """${system}"""`.replace(/^\s*\n/gm, '');
<div class=" text-xs font-semibold mb-2">Parameters</div> <div class=" text-xs font-semibold mb-2">Parameters</div>
<div> <div>
<Advanced bind:options /> <AdvancedParams bind:options />
</div> </div>
</div> </div>
{/if} {/if}

View file

@ -12,7 +12,7 @@
import { createModel } from '$lib/apis/ollama'; import { createModel } from '$lib/apis/ollama';
import { getModelfiles, updateModelfileByTagName } from '$lib/apis/modelfiles'; import { getModelfiles, updateModelfileByTagName } from '$lib/apis/modelfiles';
import Advanced from '$lib/components/chat/Settings/Advanced.svelte'; import AdvancedParams from '$lib/components/chat/Settings/Advanced/AdvancedParams.svelte';
let loading = false; let loading = false;