浏览代码

feat: multi-user chat history support

Timothy J. Baek 1 年之前
父节点
当前提交
0810a2648f

+ 1 - 1
.dockerignore

@@ -12,5 +12,5 @@ __pycache__
 _old
 uploads
 .ipynb_checkpoints
-*.db
+**/*.db
 _test

+ 1 - 1
run.sh

@@ -1,5 +1,5 @@
 docker stop ollama-webui || true
 docker rm ollama-webui || true
 docker build -t ollama-webui .
-docker run -d -p 3000:8080 --add-host=host.docker.internal:host-gateway --name ollama-webui --restart always ollama-webui
+docker run -d -p 3000:8080 --add-host=host.docker.internal:host-gateway -v ollama-webui:/app --name ollama-webui --restart always ollama-webui
 docker image prune -f

+ 162 - 0
src/lib/apis/chats/index.ts

@@ -0,0 +1,162 @@
+import { WEBUI_API_BASE_URL } from '$lib/constants';
+
+export const createNewChat = async (token: string, chat: object) => {
+	let error = null;
+
+	const res = await fetch(`${WEBUI_API_BASE_URL}/chats/new`, {
+		method: 'POST',
+		headers: {
+			Accept: 'application/json',
+			'Content-Type': 'application/json',
+			authorization: `Bearer ${token}`
+		},
+		body: JSON.stringify({
+			chat: chat
+		})
+	})
+		.then(async (res) => {
+			if (!res.ok) throw await res.json();
+			return res.json();
+		})
+		.catch((err) => {
+			error = err;
+			console.log(err);
+			return null;
+		});
+
+	if (error) {
+		throw error;
+	}
+
+	return res;
+};
+
+export const getChatlist = async (token: string = '') => {
+	let error = null;
+
+	const res = await fetch(`${WEBUI_API_BASE_URL}/chats/`, {
+		method: 'GET',
+		headers: {
+			Accept: 'application/json',
+			'Content-Type': 'application/json',
+			...(token && { authorization: `Bearer ${token}` })
+		}
+	})
+		.then(async (res) => {
+			if (!res.ok) throw await res.json();
+			return res.json();
+		})
+		.then((json) => {
+			return json;
+		})
+		.catch((err) => {
+			error = err;
+			console.log(err);
+			return null;
+		});
+
+	if (error) {
+		throw error;
+	}
+
+	return res;
+};
+
+export const getChatById = async (token: string, id: string) => {
+	let error = null;
+
+	const res = await fetch(`${WEBUI_API_BASE_URL}/chats/${id}`, {
+		method: 'GET',
+		headers: {
+			Accept: 'application/json',
+			'Content-Type': 'application/json',
+			...(token && { authorization: `Bearer ${token}` })
+		}
+	})
+		.then(async (res) => {
+			if (!res.ok) throw await res.json();
+			return res.json();
+		})
+		.then((json) => {
+			return json;
+		})
+		.catch((err) => {
+			error = err;
+
+			console.log(err);
+			return null;
+		});
+
+	if (error) {
+		throw error;
+	}
+
+	return res;
+};
+
+export const updateChatById = async (token: string, id: string, chat: object) => {
+	let error = null;
+
+	const res = await fetch(`${WEBUI_API_BASE_URL}/chats/${id}`, {
+		method: 'POST',
+		headers: {
+			Accept: 'application/json',
+			'Content-Type': 'application/json',
+			...(token && { authorization: `Bearer ${token}` })
+		},
+		body: JSON.stringify({
+			chat: chat
+		})
+	})
+		.then(async (res) => {
+			if (!res.ok) throw await res.json();
+			return res.json();
+		})
+		.then((json) => {
+			return json;
+		})
+		.catch((err) => {
+			error = err;
+
+			console.log(err);
+			return null;
+		});
+
+	if (error) {
+		throw error;
+	}
+
+	return res;
+};
+
+export const deleteChatById = async (token: string, id: string) => {
+	let error = null;
+
+	const res = await fetch(`${WEBUI_API_BASE_URL}/chats/${id}`, {
+		method: 'DELETE',
+		headers: {
+			Accept: 'application/json',
+			'Content-Type': 'application/json',
+			...(token && { authorization: `Bearer ${token}` })
+		}
+	})
+		.then(async (res) => {
+			if (!res.ok) throw await res.json();
+			return res.json();
+		})
+		.then((json) => {
+			return json;
+		})
+		.catch((err) => {
+			error = err;
+
+			console.log(err);
+			return null;
+		});
+
+	if (error) {
+		throw error;
+	}
+
+	return res;
+};

+ 35 - 0
src/lib/apis/index.ts

@@ -0,0 +1,35 @@
+export const getOpenAIModels = async (
+	base_url: string = 'https://api.openai.com/v1',
+	api_key: string = ''
+) => {
+	let error = null;
+
+	const res = await fetch(`${base_url}/models`, {
+		method: 'GET',
+		headers: {
+			'Content-Type': 'application/json',
+			Authorization: `Bearer ${api_key}`
+		}
+	})
+		.then(async (res) => {
+			if (!res.ok) throw await res.json();
+			return res.json();
+		})
+		.catch((error) => {
+			console.log(error);
+			error = `OpenAI: ${error?.error?.message ?? 'Network Problem'}`;
+			return null;
+		});
+
+	if (error) {
+		throw error;
+	}
+
+	let models = Array.isArray(res) ? res : res?.data ?? null;
+
+	console.log(models);
+
+	return models
+		.map((model) => ({ name: model.id, external: true }))
+		.filter((model) => (base_url.includes('openai') ? model.name.includes('gpt') : true));
+};

+ 71 - 0
src/lib/apis/ollama/index.ts

@@ -0,0 +1,71 @@
+import { OLLAMA_API_BASE_URL } from '$lib/constants';
+
+export const getOllamaVersion = async (
+	base_url: string = OLLAMA_API_BASE_URL,
+	token: string = ''
+) => {
+	let error = null;
+
+	const res = await fetch(`${base_url}/version`, {
+		method: 'GET',
+		headers: {
+			Accept: 'application/json',
+			'Content-Type': 'application/json',
+			...(token && { authorization: `Bearer ${token}` })
+		}
+	})
+		.then(async (res) => {
+			if (!res.ok) throw await res.json();
+			return res.json();
+		})
+		.catch((error) => {
+			console.log(error);
+			if ('detail' in error) {
+				error = error.detail;
+			} else {
+				error = 'Server connection failed';
+			}
+			return null;
+		});
+
+	if (error) {
+		throw error;
+	}
+
+	return res?.version ?? '0';
+};
+
+export const getOllamaModels = async (
+	base_url: string = OLLAMA_API_BASE_URL,
+	token: string = ''
+) => {
+	let error = null;
+
+	const res = await fetch(`${base_url}/tags`, {
+		method: 'GET',
+		headers: {
+			Accept: 'application/json',
+			'Content-Type': 'application/json',
+			...(token && { authorization: `Bearer ${token}` })
+		}
+	})
+		.then(async (res) => {
+			if (!res.ok) throw await res.json();
+			return res.json();
+		})
+		.catch((error) => {
+			console.log(error);
+			if ('detail' in error) {
+				error = error.detail;
+			} else {
+				error = 'Server connection failed';
+			}
+			return null;
+		});
+
+	if (error) {
+		throw error;
+	}
+
+	return res?.models ?? [];
+};

+ 3 - 2
src/lib/components/chat/Messages.svelte

@@ -8,11 +8,12 @@
 	import auto_render from 'katex/dist/contrib/auto-render.mjs';
 	import 'katex/dist/katex.min.css';
 
-	import { chatId, config, db, modelfiles, settings, user } from '$lib/stores';
+	import { config, db, modelfiles, settings, user } from '$lib/stores';
 	import { tick } from 'svelte';
 
 	import toast from 'svelte-french-toast';
 
+	export let chatId = '';
 	export let sendPrompt: Function;
 	export let regenerateResponse: Function;
 
@@ -239,7 +240,7 @@
 		history.currentId = userMessageId;
 
 		await tick();
-		await sendPrompt(userPrompt, userMessageId, $chatId);
+		await sendPrompt(userPrompt, userMessageId, chatId);
 	};
 
 	const confirmEditResponseMessage = async (messageId) => {

+ 4 - 6
src/lib/components/layout/Navbar.svelte

@@ -5,11 +5,12 @@
 	import { chatId, db, modelfiles } from '$lib/stores';
 	import toast from 'svelte-french-toast';
 
+	export let initNewChat: Function;
 	export let title: string = 'Ollama Web UI';
 	export let shareEnabled: boolean = false;
 
 	const shareChat = async () => {
-		const chat = await $db.getChatById($chatId);
+		const chat = (await $db.getChatById($chatId)).chat;
 		console.log('share', chat);
 		toast.success('Redirecting you to OllamaHub');
 
@@ -44,12 +45,9 @@
 		<div class="flex w-full max-w-full">
 			<div class="pr-2 self-center">
 				<button
+					id="new-chat-button"
 					class=" cursor-pointer p-1 flex dark:hover:bg-gray-700 rounded-lg transition"
-					on:click={async () => {
-						console.log('newChat');
-						goto('/');
-						await chatId.set(uuidv4());
-					}}
+					on:click={initNewChat}
 				>
 					<div class=" m-auto self-center">
 						<svg

+ 12 - 9
src/lib/components/layout/Sidebar.svelte

@@ -55,7 +55,7 @@
 	};
 
 	const importChats = async (chatHistory) => {
-		await $db.addChats(chatHistory);
+		await $db.importChats(chatHistory);
 	};
 
 	const exportChats = async () => {
@@ -81,7 +81,7 @@
 	bind:this={navElement}
 	class="h-screen {show
 		? ''
-		: '-translate-x-[260px]'}  w-[260px] fixed top-0 left-0 z-40 transition bg-[#0a0a0a] text-gray-200 shadow-2xl text-sm
+		: '-translate-x-[260px]'}  w-[260px] fixed top-0 left-0 z-40 transition bg-black text-gray-200 shadow-2xl text-sm
         "
 >
 	<div class="py-2.5 my-auto flex flex-col justify-between h-screen">
@@ -91,8 +91,11 @@
 				on:click={async () => {
 					goto('/');
 
-					await chatId.set(uuidv4());
-					// createNewChat();
+					const newChatButton = document.getElementById('new-chat-button');
+
+					if (newChatButton) {
+						newChatButton.click();
+					}
 				}}
 			>
 				<div class="flex self-center">
@@ -153,7 +156,7 @@
 
 		<div class="px-2.5 mt-1 mb-2 flex justify-center space-x-2">
 			<div class="flex w-full">
-				<div class="self-center pl-3 py-2 rounded-l bg-gray-900">
+				<div class="self-center pl-3 py-2 rounded-l bg-gray-950">
 					<svg
 						xmlns="http://www.w3.org/2000/svg"
 						viewBox="0 0 20 20"
@@ -169,7 +172,7 @@
 				</div>
 
 				<input
-					class="w-full rounded-r py-1.5 pl-2.5 pr-4 text-sm text-gray-300 bg-gray-900 outline-none"
+					class="w-full rounded-r py-1.5 pl-2.5 pr-4 text-sm text-gray-300 bg-gray-950 outline-none"
 					placeholder="Search"
 					bind:value={search}
 				/>
@@ -394,10 +397,10 @@
 		</div>
 
 		<div class="px-2.5">
-			<hr class=" border-gray-800 mb-2 w-full" />
+			<hr class=" border-gray-900 mb-1 w-full" />
 
 			<div class="flex flex-col">
-				<div class="flex">
+				<!-- <div class="flex">
 					<input bind:this={importFileInputElement} bind:files={importFiles} type="file" hidden />
 					<button
 						class=" flex rounded-md py-3 px-3.5 w-full hover:bg-gray-900 transition"
@@ -534,7 +537,7 @@
 						</div>
 						<span>Clear conversations</span>
 					</button>
-				{/if}
+				{/if} -->
 
 				{#if $user !== undefined}
 					<button

+ 104 - 129
src/routes/(app)/+layout.svelte

@@ -21,77 +21,37 @@
 	import Sidebar from '$lib/components/layout/Sidebar.svelte';
 	import toast from 'svelte-french-toast';
 	import { OLLAMA_API_BASE_URL, WEBUI_API_BASE_URL } from '$lib/constants';
+	import { getOllamaModels, getOllamaVersion } from '$lib/apis/ollama';
+	import { getOpenAIModels } from '$lib/apis';
+	import {
+		createNewChat,
+		deleteChatById,
+		getChatById,
+		getChatlist,
+		updateChatById
+	} from '$lib/apis/chats';
 
 	let requiredOllamaVersion = '0.1.16';
 	let loaded = false;
 
 	const getModels = async () => {
 		let models = [];
-		const res = await fetch(`${$settings?.API_BASE_URL ?? OLLAMA_API_BASE_URL}/tags`, {
-			method: 'GET',
-			headers: {
-				Accept: 'application/json',
-				'Content-Type': 'application/json',
-				...($settings.authHeader && { Authorization: $settings.authHeader }),
-				...($user && { Authorization: `Bearer ${localStorage.token}` })
-			}
-		})
-			.then(async (res) => {
-				if (!res.ok) throw await res.json();
-				return res.json();
-			})
-			.catch((error) => {
+		models.push(
+			...(await getOllamaModels($settings?.API_BASE_URL ?? OLLAMA_API_BASE_URL, localStorage.token))
+		);
+		// If OpenAI API Key exists
+		if ($settings.OPENAI_API_KEY) {
+			const openAIModels = await getOpenAIModels(
+				$settings.OPENAI_API_BASE_URL ?? 'https://api.openai.com/v1',
+				$settings.OPENAI_API_KEY
+			).catch((error) => {
 				console.log(error);
-				if ('detail' in error) {
-					toast.error(error.detail);
-				} else {
-					toast.error('Server connection failed');
-				}
+				toast.error(error);
 				return null;
 			});
-		console.log(res);
-		models.push(...(res?.models ?? []));
-
-		// If OpenAI API Key exists
-		if ($settings.OPENAI_API_KEY) {
-			// Validate OPENAI_API_KEY
-
-			const API_BASE_URL = $settings.OPENAI_API_BASE_URL ?? 'https://api.openai.com/v1';
-			const openaiModelRes = await fetch(`${API_BASE_URL}/models`, {
-				method: 'GET',
-				headers: {
-					'Content-Type': 'application/json',
-					Authorization: `Bearer ${$settings.OPENAI_API_KEY}`
-				}
-			})
-				.then(async (res) => {
-					if (!res.ok) throw await res.json();
-					return res.json();
-				})
-				.catch((error) => {
-					console.log(error);
-					toast.error(`OpenAI: ${error?.error?.message ?? 'Network Problem'}`);
-					return null;
-				});
 
-			const openAIModels = Array.isArray(openaiModelRes)
-				? openaiModelRes
-				: openaiModelRes?.data ?? null;
-
-			models.push(
-				...(openAIModels
-					? [
-							{ name: 'hr' },
-							...openAIModels
-								.map((model) => ({ name: model.id, external: true }))
-								.filter((model) =>
-									API_BASE_URL.includes('openai') ? model.name.includes('gpt') : true
-								)
-					  ]
-					: [])
-			);
+			models.push(...(openAIModels ? [{ name: 'hr' }, ...openAIModels] : []));
 		}
-
 		return models;
 	};
 
@@ -109,135 +69,152 @@
 		return {
 			db: DB,
 			getChatById: async function (id) {
-				return await this.db.get('chats', id);
+				const chat = await getChatById(localStorage.token, id);
+				return chat;
 			},
 			getChats: async function () {
-				let chats = await this.db.getAllFromIndex('chats', 'timestamp');
-				chats = chats.map((item, idx) => ({
-					title: chats[chats.length - 1 - idx].title,
-					id: chats[chats.length - 1 - idx].id
-				}));
+				const chats = await getChatlist(localStorage.token);
 				return chats;
 			},
-			exportChats: async function () {
-				let chats = await this.db.getAllFromIndex('chats', 'timestamp');
-				chats = chats.map((item, idx) => chats[chats.length - 1 - idx]);
-				return chats;
-			},
-			addChats: async function (_chats) {
-				for (const chat of _chats) {
-					console.log(chat);
-					await this.addChat(chat);
-				}
+			createNewChat: async function (_chat) {
+				const chat = await createNewChat(localStorage.token, { ..._chat, timestamp: Date.now() });
+				console.log(chat);
 				await chats.set(await this.getChats());
+
+				return chat;
 			},
+
 			addChat: async function (chat) {
 				await this.db.put('chats', {
 					...chat
 				});
 			},
-			createNewChat: async function (chat) {
-				await this.addChat({ ...chat, timestamp: Date.now() });
-				await chats.set(await this.getChats());
-			},
-			updateChatById: async function (id, updated) {
-				const chat = await this.getChatById(id);
 
-				await this.db.put('chats', {
-					...chat,
+			updateChatById: async function (id, updated) {
+				const chat = await updateChatById(localStorage.token, id, {
 					...updated,
 					timestamp: Date.now()
 				});
-
 				await chats.set(await this.getChats());
+				return chat;
 			},
 			deleteChatById: async function (id) {
 				if ($chatId === id) {
 					goto('/');
 					await chatId.set(uuidv4());
 				}
-				await this.db.delete('chats', id);
+
+				await deleteChatById(localStorage.token, id);
 				await chats.set(await this.getChats());
 			},
+
 			deleteAllChat: async function () {
 				const tx = this.db.transaction('chats', 'readwrite');
 				await Promise.all([tx.store.clear(), tx.done]);
-
+				await chats.set(await this.getChats());
+			},
+			exportChats: async function () {
+				let chats = await this.db.getAllFromIndex('chats', 'timestamp');
+				chats = chats.map((item, idx) => chats[chats.length - 1 - idx]);
+				return chats;
+			},
+			importChats: async function (_chats) {
+				for (const chat of _chats) {
+					console.log(chat);
+					await this.addChat(chat);
+				}
 				await chats.set(await this.getChats());
 			}
 		};
 	};
 
-	const getOllamaVersion = async () => {
-		const res = await fetch(`${$settings?.API_BASE_URL ?? OLLAMA_API_BASE_URL}/version`, {
-			method: 'GET',
-			headers: {
-				Accept: 'application/json',
-				'Content-Type': 'application/json',
-				...($settings.authHeader && { Authorization: $settings.authHeader }),
-				...($user && { Authorization: `Bearer ${localStorage.token}` })
-			}
-		})
-			.then(async (res) => {
-				if (!res.ok) throw await res.json();
-				return res.json();
-			})
-			.catch((error) => {
-				console.log(error);
-				if ('detail' in error) {
-					toast.error(error.detail);
-				} else {
-					toast.error('Server connection failed');
-				}
-				return null;
-			});
-
-		console.log(res);
-
-		return res?.version ?? '0';
-	};
+	const setOllamaVersion = async () => {
+		const version = await getOllamaVersion(
+			$settings?.API_BASE_URL ?? OLLAMA_API_BASE_URL,
+			localStorage.token
+		).catch((error) => {
+			toast.error(error);
+			return '0';
+		});
 
-	const setOllamaVersion = async (ollamaVersion) => {
-		await info.set({ ...$info, ollama: { version: ollamaVersion } });
+		await info.set({ ...$info, ollama: { version: version } });
 
 		if (
-			ollamaVersion.localeCompare(requiredOllamaVersion, undefined, {
+			version.localeCompare(requiredOllamaVersion, undefined, {
 				numeric: true,
 				sensitivity: 'case',
 				caseFirst: 'upper'
 			}) < 0
 		) {
-			toast.error(`Ollama Version: ${ollamaVersion}`);
+			toast.error(`Ollama Version: ${version}`);
 		}
 	};
 
 	onMount(async () => {
-		if ($config && $config.auth && $user === undefined) {
+		if ($config && $user === undefined) {
 			await goto('/auth');
 		}
 
 		await settings.set(JSON.parse(localStorage.getItem('settings') ?? '{}'));
-
 		await models.set(await getModels());
+
 		await modelfiles.set(JSON.parse(localStorage.getItem('modelfiles') ?? '[]'));
 
-		modelfiles.subscribe(async () => {
-			await models.set(await getModels());
-		});
+		modelfiles.subscribe(async () => {});
 
 		let _db = await getDB();
 		await db.set(_db);
-
-		await setOllamaVersion(await getOllamaVersion());
+		await setOllamaVersion();
 
 		await tick();
 		loaded = true;
 	});
+
+	let child;
 </script>
 
 {#if loaded}
 	<div class="app relative">
-		{#if ($info?.ollama?.version ?? '0').localeCompare( requiredOllamaVersion, undefined, { numeric: true, sensitivity: 'case', caseFirst: 'upper' } ) < 0}
+		{#if !['user', 'admin'].includes($user.role)}
+			<div class="absolute w-full h-full flex z-50">
+				<div
+					class="absolute rounded-xl w-full h-full backdrop-blur bg-gray-900/60 flex justify-center"
+				>
+					<div class="m-auto pb-44 flex flex-col justify-center">
+						<div class="max-w-md">
+							<div class="text-center dark:text-white text-2xl font-medium z-50">
+								Account Activation Pending<br /> Contact Admin for WebUI Access
+							</div>
+
+							<div class=" mt-4 text-center text-sm dark:text-gray-200 w-full">
+								Your account status is currently pending activation. To access the WebUI, please
+								reach out to the administrator. Admins can manage user statuses from the Admin
+								Panel.
+							</div>
+
+							<div class=" mt-6 mx-auto relative group w-fit">
+								<button
+									class="relative z-20 flex px-5 py-2 rounded-full bg-gray-100 hover:bg-gray-200 transition font-medium text-sm"
+									on:click={async () => {
+										location.href = '/';
+									}}
+								>
+									Check Again
+								</button>
+
+								<button
+									class="text-xs text-center w-full mt-2 text-gray-400 underline"
+									on:click={async () => {
+										localStorage.removeItem('token');
+										location.href = '/auth';
+									}}>Sign Out</button
+								>
+							</div>
+						</div>
+					</div>
+				</div>
+			</div>
+		{:else if ($info?.ollama?.version ?? '0').localeCompare( requiredOllamaVersion, undefined, { numeric: true, sensitivity: 'case', caseFirst: 'upper' } ) < 0}
 			<div class="absolute w-full h-full flex z-50">
 				<div
 					class="absolute rounded-xl w-full h-full backdrop-blur bg-gray-900/60 flex justify-center"
@@ -285,9 +262,7 @@
 			class=" text-gray-700 dark:text-gray-100 bg-white dark:bg-gray-800 min-h-screen overflow-auto flex flex-row"
 		>
 			<Sidebar />
-
 			<SettingsModal bind:show={$showSettings} />
-
 			<slot />
 		</div>
 	</div>

+ 35 - 28
src/routes/(app)/+page.svelte

@@ -2,18 +2,18 @@
 	import { v4 as uuidv4 } from 'uuid';
 	import toast from 'svelte-french-toast';
 
-	import { OLLAMA_API_BASE_URL } from '$lib/constants';
-	import { onMount, tick } from 'svelte';
-	import { splitStream } from '$lib/utils';
+	import { onDestroy, onMount, tick } from 'svelte';
 	import { goto } from '$app/navigation';
+	import { page } from '$app/stores';
 
 	import { config, models, modelfiles, user, settings, db, chats, chatId } from '$lib/stores';
+	import { OLLAMA_API_BASE_URL } from '$lib/constants';
+	import { splitStream } from '$lib/utils';
 
 	import MessageInput from '$lib/components/chat/MessageInput.svelte';
 	import Messages from '$lib/components/chat/Messages.svelte';
 	import ModelSelector from '$lib/components/chat/ModelSelector.svelte';
 	import Navbar from '$lib/components/layout/Navbar.svelte';
-	import { page } from '$app/stores';
 
 	let stopResponseFlag = false;
 	let autoScroll = true;
@@ -26,10 +26,11 @@
 			? $modelfiles.filter((modelfile) => modelfile.tagName === selectedModels[0])[0]
 			: null;
 
+	let chat = null;
+
 	let title = '';
 	let prompt = '';
 	let files = [];
-
 	let messages = [];
 	let history = {
 		messages: {},
@@ -50,16 +51,8 @@
 		messages = [];
 	}
 
-	$: if (files) {
-		console.log(files);
-	}
-
 	onMount(async () => {
-		await chatId.set(uuidv4());
-
-		chatId.subscribe(async () => {
-			await initNewChat();
-		});
+		await initNewChat();
 	});
 
 	//////////////////////////
@@ -67,6 +60,9 @@
 	//////////////////////////
 
 	const initNewChat = async () => {
+		console.log('initNewChat');
+
+		await chatId.set('');
 		console.log($chatId);
 
 		autoScroll = true;
@@ -82,7 +78,6 @@
 			: $settings.models ?? [''];
 
 		let _settings = JSON.parse(localStorage.getItem('settings') ?? '{}');
-		console.log(_settings);
 		settings.set({
 			..._settings
 		});
@@ -127,14 +122,15 @@
 	// Ollama functions
 	//////////////////////////
 
-	const sendPrompt = async (userPrompt, parentId, _chatId) => {
+	const sendPrompt = async (prompt, parentId) => {
+		const _chatId = JSON.parse(JSON.stringify($chatId));
 		await Promise.all(
 			selectedModels.map(async (model) => {
 				console.log(model);
 				if ($models.filter((m) => m.name === model)[0].external) {
-					await sendPromptOpenAI(model, userPrompt, parentId, _chatId);
+					await sendPromptOpenAI(model, prompt, parentId, _chatId);
 				} else {
-					await sendPromptOllama(model, userPrompt, parentId, _chatId);
+					await sendPromptOllama(model, prompt, parentId, _chatId);
 				}
 			})
 		);
@@ -297,8 +293,11 @@
 				if (autoScroll) {
 					window.scrollTo({ top: document.body.scrollHeight });
 				}
+			}
 
-				await $db.updateChatById(_chatId, {
+			if ($chatId == _chatId) {
+				chat = await $db.updateChatById(_chatId, {
+					...chat.chat,
 					title: title === '' ? 'New Chat' : title,
 					models: selectedModels,
 					system: $settings.system ?? undefined,
@@ -481,8 +480,11 @@
 						if (autoScroll) {
 							window.scrollTo({ top: document.body.scrollHeight });
 						}
+					}
 
-						await $db.updateChatById(_chatId, {
+					if ($chatId == _chatId) {
+						chat = await $db.updateChatById(_chatId, {
+							...chat.chat,
 							title: title === '' ? 'New Chat' : title,
 							models: selectedModels,
 							system: $settings.system ?? undefined,
@@ -542,8 +544,7 @@
 	};
 
 	const submitPrompt = async (userPrompt) => {
-		const _chatId = JSON.parse(JSON.stringify($chatId));
-		console.log('submitPrompt', _chatId);
+		console.log('submitPrompt', $chatId);
 
 		if (selectedModels.includes('')) {
 			toast.error('Model not selected');
@@ -570,9 +571,10 @@
 			history.currentId = userMessageId;
 
 			await tick();
+
 			if (messages.length == 1) {
-				await $db.createNewChat({
-					id: _chatId,
+				chat = await $db.createNewChat({
+					id: $chatId,
 					title: 'New Chat',
 					models: selectedModels,
 					system: $settings.system ?? undefined,
@@ -588,6 +590,11 @@
 					messages: messages,
 					history: history
 				});
+
+				console.log(chat);
+
+				await chatId.set(chat.id);
+				await tick();
 			}
 
 			prompt = '';
@@ -597,7 +604,7 @@
 				window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' });
 			}, 50);
 
-			await sendPrompt(userPrompt, userMessageId, _chatId);
+			await sendPrompt(userPrompt, userMessageId);
 		}
 	};
 
@@ -629,7 +636,6 @@
 				method: 'POST',
 				headers: {
 					'Content-Type': 'text/event-stream',
-					...($settings.authHeader && { Authorization: $settings.authHeader }),
 					...($user && { Authorization: `Bearer ${localStorage.token}` })
 				},
 				body: JSON.stringify({
@@ -659,7 +665,7 @@
 	};
 
 	const setChatTitle = async (_chatId, _title) => {
-		await $db.updateChatById(_chatId, { title: _title });
+		chat = await $db.updateChatById(_chatId, { ...chat.chat, title: _title });
 		if (_chatId === $chatId) {
 			title = _title;
 		}
@@ -672,7 +678,7 @@
 	}}
 />
 
-<Navbar {title} shareEnabled={messages.length > 0} />
+<Navbar {title} shareEnabled={messages.length > 0} {initNewChat} />
 <div class="min-h-screen w-full flex justify-center">
 	<div class=" py-2.5 flex flex-col justify-between w-full">
 		<div class="max-w-2xl mx-auto w-full px-3 md:px-0 mt-10">
@@ -681,6 +687,7 @@
 
 		<div class=" h-full mt-10 mb-32 w-full flex flex-col">
 			<Messages
+				chatId={$chatId}
 				{selectedModels}
 				{selectedModelfile}
 				bind:history

+ 55 - 28
src/routes/(app)/c/[id]/+page.svelte

@@ -27,6 +27,8 @@
 			? $modelfiles.filter((modelfile) => modelfile.tagName === selectedModels[0])[0]
 			: null;
 
+	let chat = null;
+
 	let title = '';
 	let prompt = '';
 	let files = [];
@@ -53,10 +55,8 @@
 
 	$: if ($page.params.id) {
 		(async () => {
-			let chat = await loadChat();
-
-			await tick();
-			if (chat) {
+			if (await loadChat()) {
+				await tick();
 				loaded = true;
 			} else {
 				await goto('/');
@@ -70,33 +70,38 @@
 
 	const loadChat = async () => {
 		await chatId.set($page.params.id);
-		const chat = await $db.getChatById($chatId);
+		chat = await $db.getChatById($chatId);
 
-		if (chat) {
-			console.log(chat);
+		const chatContent = chat.chat;
 
-			selectedModels = (chat?.models ?? undefined) !== undefined ? chat.models : [chat.model ?? ''];
+		if (chatContent) {
+			console.log(chatContent);
+
+			selectedModels =
+				(chatContent?.models ?? undefined) !== undefined
+					? chatContent.models
+					: [chatContent.model ?? ''];
 			history =
-				(chat?.history ?? undefined) !== undefined
-					? chat.history
-					: convertMessagesToHistory(chat.messages);
-			title = chat.title;
+				(chatContent?.history ?? undefined) !== undefined
+					? chatContent.history
+					: convertMessagesToHistory(chatContent.messages);
+			title = chatContent.title;
 
 			let _settings = JSON.parse(localStorage.getItem('settings') ?? '{}');
 			await settings.set({
 				..._settings,
-				system: chat.system ?? _settings.system,
-				options: chat.options ?? _settings.options
+				system: chatContent.system ?? _settings.system,
+				options: chatContent.options ?? _settings.options
 			});
 			autoScroll = true;
-
 			await tick();
+
 			if (messages.length > 0) {
 				history.messages[messages.at(-1).id].done = true;
 			}
 			await tick();
 
-			return chat;
+			return true;
 		} else {
 			return null;
 		}
@@ -141,14 +146,15 @@
 	// Ollama functions
 	//////////////////////////
 
-	const sendPrompt = async (userPrompt, parentId, _chatId) => {
+	const sendPrompt = async (prompt, parentId) => {
+		const _chatId = JSON.parse(JSON.stringify($chatId));
 		await Promise.all(
 			selectedModels.map(async (model) => {
 				console.log(model);
 				if ($models.filter((m) => m.name === model)[0].external) {
-					await sendPromptOpenAI(model, userPrompt, parentId, _chatId);
+					await sendPromptOpenAI(model, prompt, parentId, _chatId);
 				} else {
-					await sendPromptOllama(model, userPrompt, parentId, _chatId);
+					await sendPromptOllama(model, prompt, parentId, _chatId);
 				}
 			})
 		);
@@ -311,8 +317,11 @@
 				if (autoScroll) {
 					window.scrollTo({ top: document.body.scrollHeight });
 				}
+			}
 
-				await $db.updateChatById(_chatId, {
+			if ($chatId == _chatId) {
+				chat = await $db.updateChatById(_chatId, {
+					...chat.chat,
 					title: title === '' ? 'New Chat' : title,
 					models: selectedModels,
 					system: $settings.system ?? undefined,
@@ -495,8 +504,11 @@
 						if (autoScroll) {
 							window.scrollTo({ top: document.body.scrollHeight });
 						}
+					}
 
-						await $db.updateChatById(_chatId, {
+					if ($chatId == _chatId) {
+						chat = await $db.updateChatById(_chatId, {
+							...chat.chat,
 							title: title === '' ? 'New Chat' : title,
 							models: selectedModels,
 							system: $settings.system ?? undefined,
@@ -556,8 +568,7 @@
 	};
 
 	const submitPrompt = async (userPrompt) => {
-		const _chatId = JSON.parse(JSON.stringify($chatId));
-		console.log('submitPrompt', _chatId);
+		console.log('submitPrompt', $chatId);
 
 		if (selectedModels.includes('')) {
 			toast.error('Model not selected');
@@ -584,9 +595,10 @@
 			history.currentId = userMessageId;
 
 			await tick();
+
 			if (messages.length == 1) {
-				await $db.createNewChat({
-					id: _chatId,
+				chat = await $db.createNewChat({
+					id: $chatId,
 					title: 'New Chat',
 					models: selectedModels,
 					system: $settings.system ?? undefined,
@@ -602,6 +614,11 @@
 					messages: messages,
 					history: history
 				});
+
+				console.log(chat);
+
+				await chatId.set(chat.id);
+				await tick();
 			}
 
 			prompt = '';
@@ -611,7 +628,7 @@
 				window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' });
 			}, 50);
 
-			await sendPrompt(userPrompt, userMessageId, _chatId);
+			await sendPrompt(userPrompt, userMessageId);
 		}
 	};
 
@@ -673,7 +690,10 @@
 	};
 
 	const setChatTitle = async (_chatId, _title) => {
-		await $db.updateChatById(_chatId, { title: _title });
+		chat = await $db.updateChatById(_chatId, {
+			...chat.chat,
+			title: _title
+		});
 		if (_chatId === $chatId) {
 			title = _title;
 		}
@@ -687,7 +707,13 @@
 />
 
 {#if loaded}
-	<Navbar {title} shareEnabled={messages.length > 0} />
+	<Navbar
+		{title}
+		shareEnabled={messages.length > 0}
+		initNewChat={() => {
+			goto('/');
+		}}
+	/>
 	<div class="min-h-screen w-full flex justify-center">
 		<div class=" py-2.5 flex flex-col justify-between w-full">
 			<div class="max-w-2xl mx-auto w-full px-3 md:px-0 mt-10">
@@ -696,6 +722,7 @@
 
 			<div class=" h-full mt-10 mb-32 w-full flex flex-col">
 				<Messages
+					chatId={$chatId}
 					{selectedModels}
 					{selectedModelfile}
 					bind:history

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

@@ -132,7 +132,6 @@ SYSTEM """${system}"""`.replace(/^\s*\n/gm, '');
 				method: 'POST',
 				headers: {
 					'Content-Type': 'text/event-stream',
-					...($settings.authHeader && { Authorization: $settings.authHeader }),
 					...($user && { Authorization: `Bearer ${localStorage.token}` })
 				},
 				body: JSON.stringify({

+ 2 - 2
src/routes/admin/+page.svelte

@@ -37,7 +37,7 @@
 	};
 
 	const getUsers = async () => {
-		const res = await fetch(`${WEBUI_API_BASE_URL}/users/`, {
+		const res = await fetch(`${WEBUI_API_BASE_URL}/users`, {
 			method: 'GET',
 			headers: {
 				'Content-Type': 'application/json',
@@ -58,7 +58,7 @@
 	};
 
 	onMount(async () => {
-		if ($config === null || !$config.auth || ($config.auth && $user && $user.role !== 'admin')) {
+		if ($user?.role !== 'admin') {
 			await goto('/');
 		} else {
 			await getUsers();

+ 1 - 1
src/routes/auth/+page.svelte

@@ -105,7 +105,7 @@
 			</div>
 		</div>
 
-		<div class="w-full max-w-xl px-10 md:px-16 bg-white min-h-screen w-full flex flex-col">
+		<div class="w-full max-w-xl px-10 md:px-16 bg-white min-h-screen flex flex-col">
 			<div class=" my-auto pb-10 w-full">
 				<form
 					class=" flex flex-col justify-center"

+ 8 - 7
src/routes/error/+page.svelte

@@ -16,23 +16,24 @@
 
 {#if loaded}
 	<div class="absolute w-full h-full flex z-50">
-		<div class="absolute rounded-xl w-full h-full backdrop-blur bg-gray-900/5 flex justify-center">
+		<div class="absolute rounded-xl w-full h-full backdrop-blur flex justify-center">
 			<div class="m-auto pb-44 flex flex-col justify-center">
 				<div class="max-w-md">
 					<div class="text-center text-2xl font-medium z-50">Ollama WebUI Backend Required</div>
 
 					<div class=" mt-4 text-center text-sm w-full">
-						Oops! It seems like your Ollama WebUI needs a little attention. <br
-							class=" hidden sm:flex"
-						/>
-						describe troubleshooting/installation, help @ discord
-
-						<!-- TODO: update text -->
+						Oops! You're using an unsupported method (frontend only).<br class=" hidden sm:flex" />
+						Please access the WebUI from the backend. See readme.md for instructions or join our Discord
+						for help.
+						<!-- TODO: update links -->
 					</div>
 
 					<div class=" mt-6 mx-auto relative group w-fit">
 						<button
 							class="relative z-20 flex px-5 py-2 rounded-full bg-gray-100 hover:bg-gray-200 transition font-medium text-sm"
+							on:click={() => {
+								location.href = '/';
+							}}
 						>
 							Check Again
 						</button>