ソースを参照

refac: settings

Timothy J. Baek 1 年間 前
コミット
cd9f0135f6

+ 63 - 0
src/lib/components/chat/Settings/About.svelte

@@ -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>

+ 249 - 0
src/lib/components/chat/Settings/AddOns.svelte

@@ -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>

+ 91 - 529
src/lib/components/chat/Settings/Advanced.svelte

@@ -1,8 +1,15 @@
 <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
 		seed: 0,
-		stop: '',
 		temperature: '',
 		repeat_penalty: '',
 		repeat_last_n: '',
@@ -11,546 +18,101 @@
 		mirostat_tau: '',
 		top_k: '',
 		top_p: '',
+		stop: '',
 		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>
+	const toggleRequestFormat = async () => {
+		if (requestFormat === '') {
+			requestFormat = 'json';
+		} else {
+			requestFormat = '';
+		}
 
-	<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>
+		saveSettings({ requestFormat: requestFormat !== '' ? requestFormat : undefined });
+	};
 
-	<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>
+	onMount(() => {
+		let settings = JSON.parse(localStorage.getItem('settings') ?? '{}');
 
-			<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>
+		requestFormat = settings.requestFormat ?? '';
 
-		{#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>
+		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>
 
-		{#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 class="flex flex-col h-full justify-between text-sm">
+	<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-80">
+		<div class=" text-sm font-medium">Parameters</div>
+
+		<AdvancedParams bind:options />
+		<hr class=" dark:border-gray-700" />
+
+		<div>
+			<div class=" py-1 flex w-full justify-between">
+				<div class=" self-center text-sm font-medium">Request Mode</div>
+
+				<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>
-		{/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 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({
+					options: {
+						seed: (options.seed !== 0 ? options.seed : undefined) ?? undefined,
+						stop: options.stop !== '' ? options.stop.split(',').filter((e) => e) : undefined,
+						temperature: options.temperature !== '' ? options.temperature : undefined,
+						repeat_penalty: options.repeat_penalty !== '' ? options.repeat_penalty : undefined,
+						repeat_last_n: options.repeat_last_n !== '' ? options.repeat_last_n : undefined,
+						mirostat: options.mirostat !== '' ? options.mirostat : undefined,
+						mirostat_eta: options.mirostat_eta !== '' ? options.mirostat_eta : undefined,
+						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
+					}
+				});
+
+				dispatch('save');
+			}}
+		>
+			Save
+		</button>
 	</div>
 </div>

+ 556 - 0
src/lib/components/chat/Settings/Advanced/AdvancedParams.svelte

@@ -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>

+ 353 - 0
src/lib/components/chat/Settings/Chats.svelte

@@ -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>

+ 86 - 0
src/lib/components/chat/Settings/External.svelte

@@ -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>

+ 224 - 0
src/lib/components/chat/Settings/General.svelte

@@ -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>

+ 118 - 0
src/lib/components/chat/Settings/Interface.svelte

@@ -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>

+ 596 - 0
src/lib/components/chat/Settings/Models.svelte

@@ -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>

ファイルの差分が大きいため隠しています
+ 38 - 1741
src/lib/components/chat/SettingsModal.svelte


+ 2 - 2
src/routes/(app)/modelfiles/create/+page.svelte

@@ -4,7 +4,7 @@
 	import { goto } from '$app/navigation';
 	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 { onMount, tick } from 'svelte';
 	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>
-									<Advanced bind:options />
+									<AdvancedParams bind:options />
 								</div>
 							</div>
 						{/if}

+ 1 - 1
src/routes/(app)/modelfiles/edit/+page.svelte

@@ -12,7 +12,7 @@
 	import { createModel } from '$lib/apis/ollama';
 	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;
 

この差分においてかなりの量のファイルが変更されているため、一部のファイルを表示していません