From 0810a2648f18649f80102e04c45f26a0f1788efc Mon Sep 17 00:00:00 2001 From: "Timothy J. Baek" Date: Tue, 26 Dec 2023 03:28:30 -0800 Subject: [PATCH] feat: multi-user chat history support --- .dockerignore | 2 +- run.sh | 2 +- src/lib/apis/chats/index.ts | 162 ++++++++++++ src/lib/apis/index.ts | 35 +++ src/lib/apis/ollama/index.ts | 71 ++++++ src/lib/components/chat/Messages.svelte | 5 +- src/lib/components/layout/Navbar.svelte | 10 +- src/lib/components/layout/Sidebar.svelte | 21 +- src/routes/(app)/+layout.svelte | 235 ++++++++---------- src/routes/(app)/+page.svelte | 63 ++--- src/routes/(app)/c/[id]/+page.svelte | 83 ++++--- .../(app)/modelfiles/create/+page.svelte | 1 - src/routes/admin/+page.svelte | 4 +- src/routes/auth/+page.svelte | 2 +- src/routes/error/+page.svelte | 15 +- 15 files changed, 495 insertions(+), 216 deletions(-) create mode 100644 src/lib/apis/chats/index.ts create mode 100644 src/lib/apis/index.ts create mode 100644 src/lib/apis/ollama/index.ts diff --git a/.dockerignore b/.dockerignore index 0221b085..419f53fb 100644 --- a/.dockerignore +++ b/.dockerignore @@ -12,5 +12,5 @@ __pycache__ _old uploads .ipynb_checkpoints -*.db +**/*.db _test \ No newline at end of file diff --git a/run.sh b/run.sh index 584c7f64..e2fae795 100644 --- a/run.sh +++ b/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 \ No newline at end of file diff --git a/src/lib/apis/chats/index.ts b/src/lib/apis/chats/index.ts new file mode 100644 index 00000000..9c421816 --- /dev/null +++ b/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; +}; diff --git a/src/lib/apis/index.ts b/src/lib/apis/index.ts new file mode 100644 index 00000000..6b6f9631 --- /dev/null +++ b/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)); +}; diff --git a/src/lib/apis/ollama/index.ts b/src/lib/apis/ollama/index.ts new file mode 100644 index 00000000..67adcdf6 --- /dev/null +++ b/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 ?? []; +}; diff --git a/src/lib/components/chat/Messages.svelte b/src/lib/components/chat/Messages.svelte index 543ce6a5..072ade46 100644 --- a/src/lib/components/chat/Messages.svelte +++ b/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) => { diff --git a/src/lib/components/layout/Navbar.svelte b/src/lib/components/layout/Navbar.svelte index bcd66ee9..219801bf 100644 --- a/src/lib/components/layout/Navbar.svelte +++ b/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 @@
+ + +
+
+ + + + {:else if ($info?.ollama?.version ?? '0').localeCompare( requiredOllamaVersion, undefined, { numeric: true, sensitivity: 'case', caseFirst: 'upper' } ) < 0}
- -
diff --git a/src/routes/(app)/+page.svelte b/src/routes/(app)/+page.svelte index 0d1055f9..e7e8a03c 100644 --- a/src/routes/(app)/+page.svelte +++ b/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 @@ }} /> - 0} /> + 0} {initNewChat} />
@@ -681,6 +687,7 @@
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} - 0} /> + 0} + initNewChat={() => { + goto('/'); + }} + />
@@ -696,6 +722,7 @@
{ - 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(); diff --git a/src/routes/auth/+page.svelte b/src/routes/auth/+page.svelte index 9ec0b16f..d77ee503 100644 --- a/src/routes/auth/+page.svelte +++ b/src/routes/auth/+page.svelte @@ -105,7 +105,7 @@
-
+
-
+
Ollama WebUI Backend Required
- Oops! It seems like your Ollama WebUI needs a little attention. - describe troubleshooting/installation, help @ discord - - + Oops! You're using an unsupported method (frontend only). + Please access the WebUI from the backend. See readme.md for instructions or join our Discord + for help. +