feat: multi-user chat history support

This commit is contained in:
Timothy J. Baek 2023-12-26 03:28:30 -08:00
parent 1274bd986b
commit 0810a2648f
15 changed files with 495 additions and 216 deletions

View file

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

2
run.sh
View file

@ -1,5 +1,5 @@
docker stop ollama-webui || true docker stop ollama-webui || true
docker rm ollama-webui || true docker rm ollama-webui || true
docker build -t ollama-webui . 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 docker image prune -f

162
src/lib/apis/chats/index.ts Normal file
View file

@ -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
src/lib/apis/index.ts Normal file
View file

@ -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));
};

View file

@ -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 ?? [];
};

View file

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

View file

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

View file

@ -55,7 +55,7 @@
}; };
const importChats = async (chatHistory) => { const importChats = async (chatHistory) => {
await $db.addChats(chatHistory); await $db.importChats(chatHistory);
}; };
const exportChats = async () => { const exportChats = async () => {
@ -81,7 +81,7 @@
bind:this={navElement} bind:this={navElement}
class="h-screen {show 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"> <div class="py-2.5 my-auto flex flex-col justify-between h-screen">
@ -91,8 +91,11 @@
on:click={async () => { on:click={async () => {
goto('/'); goto('/');
await chatId.set(uuidv4()); const newChatButton = document.getElementById('new-chat-button');
// createNewChat();
if (newChatButton) {
newChatButton.click();
}
}} }}
> >
<div class="flex self-center"> <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="px-2.5 mt-1 mb-2 flex justify-center space-x-2">
<div class="flex w-full"> <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 <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20" viewBox="0 0 20 20"
@ -169,7 +172,7 @@
</div> </div>
<input <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" placeholder="Search"
bind:value={search} bind:value={search}
/> />
@ -394,10 +397,10 @@
</div> </div>
<div class="px-2.5"> <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 flex-col">
<div class="flex"> <!-- <div class="flex">
<input bind:this={importFileInputElement} bind:files={importFiles} type="file" hidden /> <input bind:this={importFileInputElement} bind:files={importFiles} type="file" hidden />
<button <button
class=" flex rounded-md py-3 px-3.5 w-full hover:bg-gray-900 transition" class=" flex rounded-md py-3 px-3.5 w-full hover:bg-gray-900 transition"
@ -534,7 +537,7 @@
</div> </div>
<span>Clear conversations</span> <span>Clear conversations</span>
</button> </button>
{/if} {/if} -->
{#if $user !== undefined} {#if $user !== undefined}
<button <button

View file

@ -21,77 +21,37 @@
import Sidebar from '$lib/components/layout/Sidebar.svelte'; import Sidebar from '$lib/components/layout/Sidebar.svelte';
import toast from 'svelte-french-toast'; import toast from 'svelte-french-toast';
import { OLLAMA_API_BASE_URL, WEBUI_API_BASE_URL } from '$lib/constants'; 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 requiredOllamaVersion = '0.1.16';
let loaded = false; let loaded = false;
const getModels = async () => { const getModels = async () => {
let models = []; let models = [];
const res = await fetch(`${$settings?.API_BASE_URL ?? OLLAMA_API_BASE_URL}/tags`, { models.push(
method: 'GET', ...(await getOllamaModels($settings?.API_BASE_URL ?? OLLAMA_API_BASE_URL, localStorage.token))
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);
models.push(...(res?.models ?? []));
// If OpenAI API Key exists // If OpenAI API Key exists
if ($settings.OPENAI_API_KEY) { if ($settings.OPENAI_API_KEY) {
// Validate OPENAI_API_KEY const openAIModels = await getOpenAIModels(
$settings.OPENAI_API_BASE_URL ?? 'https://api.openai.com/v1',
const API_BASE_URL = $settings.OPENAI_API_BASE_URL ?? 'https://api.openai.com/v1'; $settings.OPENAI_API_KEY
const openaiModelRes = await fetch(`${API_BASE_URL}/models`, { ).catch((error) => {
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); console.log(error);
toast.error(`OpenAI: ${error?.error?.message ?? 'Network Problem'}`); toast.error(error);
return null; return null;
}); });
const openAIModels = Array.isArray(openaiModelRes) models.push(...(openAIModels ? [{ name: 'hr' }, ...openAIModels] : []));
? 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
)
]
: [])
);
} }
return models; return models;
}; };
@ -109,135 +69,152 @@
return { return {
db: DB, db: DB,
getChatById: async function (id) { getChatById: async function (id) {
return await this.db.get('chats', id); const chat = await getChatById(localStorage.token, id);
return chat;
}, },
getChats: async function () { getChats: async function () {
let chats = await this.db.getAllFromIndex('chats', 'timestamp'); const chats = await getChatlist(localStorage.token);
chats = chats.map((item, idx) => ({
title: chats[chats.length - 1 - idx].title,
id: chats[chats.length - 1 - idx].id
}));
return chats; return chats;
}, },
exportChats: async function () { createNewChat: async function (_chat) {
let chats = await this.db.getAllFromIndex('chats', 'timestamp'); const chat = await createNewChat(localStorage.token, { ..._chat, timestamp: Date.now() });
chats = chats.map((item, idx) => chats[chats.length - 1 - idx]);
return chats;
},
addChats: async function (_chats) {
for (const chat of _chats) {
console.log(chat); console.log(chat);
await this.addChat(chat);
}
await chats.set(await this.getChats()); await chats.set(await this.getChats());
return chat;
}, },
addChat: async function (chat) { addChat: async function (chat) {
await this.db.put('chats', { await this.db.put('chats', {
...chat ...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', { updateChatById: async function (id, updated) {
...chat, const chat = await updateChatById(localStorage.token, id, {
...updated, ...updated,
timestamp: Date.now() timestamp: Date.now()
}); });
await chats.set(await this.getChats()); await chats.set(await this.getChats());
return chat;
}, },
deleteChatById: async function (id) { deleteChatById: async function (id) {
if ($chatId === id) { if ($chatId === id) {
goto('/'); goto('/');
await chatId.set(uuidv4()); await chatId.set(uuidv4());
} }
await this.db.delete('chats', id);
await deleteChatById(localStorage.token, id);
await chats.set(await this.getChats()); await chats.set(await this.getChats());
}, },
deleteAllChat: async function () { deleteAllChat: async function () {
const tx = this.db.transaction('chats', 'readwrite'); const tx = this.db.transaction('chats', 'readwrite');
await Promise.all([tx.store.clear(), tx.done]); 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()); await chats.set(await this.getChats());
} }
}; };
}; };
const getOllamaVersion = async () => { const setOllamaVersion = async () => {
const res = await fetch(`${$settings?.API_BASE_URL ?? OLLAMA_API_BASE_URL}/version`, { const version = await getOllamaVersion(
method: 'GET', $settings?.API_BASE_URL ?? OLLAMA_API_BASE_URL,
headers: { localStorage.token
Accept: 'application/json', ).catch((error) => {
'Content-Type': 'application/json', toast.error(error);
...($settings.authHeader && { Authorization: $settings.authHeader }), return '0';
...($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); await info.set({ ...$info, ollama: { version: version } });
return res?.version ?? '0';
};
const setOllamaVersion = async (ollamaVersion) => {
await info.set({ ...$info, ollama: { version: ollamaVersion } });
if ( if (
ollamaVersion.localeCompare(requiredOllamaVersion, undefined, { version.localeCompare(requiredOllamaVersion, undefined, {
numeric: true, numeric: true,
sensitivity: 'case', sensitivity: 'case',
caseFirst: 'upper' caseFirst: 'upper'
}) < 0 }) < 0
) { ) {
toast.error(`Ollama Version: ${ollamaVersion}`); toast.error(`Ollama Version: ${version}`);
} }
}; };
onMount(async () => { onMount(async () => {
if ($config && $config.auth && $user === undefined) { if ($config && $user === undefined) {
await goto('/auth'); await goto('/auth');
} }
await settings.set(JSON.parse(localStorage.getItem('settings') ?? '{}')); await settings.set(JSON.parse(localStorage.getItem('settings') ?? '{}'));
await models.set(await getModels()); await models.set(await getModels());
await modelfiles.set(JSON.parse(localStorage.getItem('modelfiles') ?? '[]')); await modelfiles.set(JSON.parse(localStorage.getItem('modelfiles') ?? '[]'));
modelfiles.subscribe(async () => { modelfiles.subscribe(async () => {});
await models.set(await getModels());
});
let _db = await getDB(); let _db = await getDB();
await db.set(_db); await db.set(_db);
await setOllamaVersion();
await setOllamaVersion(await getOllamaVersion());
await tick(); await tick();
loaded = true; loaded = true;
}); });
let child;
</script> </script>
{#if loaded} {#if loaded}
<div class="app relative"> <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 w-full h-full flex z-50">
<div <div
class="absolute rounded-xl w-full h-full backdrop-blur bg-gray-900/60 flex justify-center" 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" class=" text-gray-700 dark:text-gray-100 bg-white dark:bg-gray-800 min-h-screen overflow-auto flex flex-row"
> >
<Sidebar /> <Sidebar />
<SettingsModal bind:show={$showSettings} /> <SettingsModal bind:show={$showSettings} />
<slot /> <slot />
</div> </div>
</div> </div>

View file

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

View file

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

View file

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

View file

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

View file

@ -105,7 +105,7 @@
</div> </div>
</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"> <div class=" my-auto pb-10 w-full">
<form <form
class=" flex flex-col justify-center" class=" flex flex-col justify-center"

View file

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