"git@developer.sourcefind.cn:chenpangpang/open-webui.git" did not exist on "ddac5284fe8f5519c567035d95582e5cccd436a3"
Unverified Commit fbdae0f7 authored by Timothy Jaeryang Baek's avatar Timothy Jaeryang Baek Committed by GitHub
Browse files

Merge pull request #216 from ollama-webui/dev

feat: full backend support (including auth/rbac)
parents f417a27d 5abd0ad5
...@@ -18,6 +18,8 @@ services: ...@@ -18,6 +18,8 @@ services:
dockerfile: Dockerfile dockerfile: Dockerfile
image: ollama-webui:latest image: ollama-webui:latest
container_name: ollama-webui container_name: ollama-webui
volumes:
- ollama-webui:/app/backend/data
depends_on: depends_on:
- ollama - ollama
ports: ports:
...@@ -30,3 +32,4 @@ services: ...@@ -30,3 +32,4 @@ services:
volumes: volumes:
ollama: {} ollama: {}
ollama-webui: {}
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/backend/data --name ollama-webui --restart always ollama-webui
docker image prune -f docker image prune -f
\ No newline at end of file
import { WEBUI_API_BASE_URL } from '$lib/constants';
export const getSessionUser = async (token: string) => {
let error = null;
const res = await fetch(`${WEBUI_API_BASE_URL}/auths`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`
}
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.catch((err) => {
console.log(err);
error = err.detail;
return null;
});
if (error) {
throw error;
}
return res;
};
export const userSignIn = async (email: string, password: string) => {
let error = null;
const res = await fetch(`${WEBUI_API_BASE_URL}/auths/signin`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
email: email,
password: password
})
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.catch((err) => {
console.log(err);
error = err.detail;
return null;
});
if (error) {
throw error;
}
return res;
};
export const userSignUp = async (name: string, email: string, password: string) => {
let error = null;
const res = await fetch(`${WEBUI_API_BASE_URL}/auths/signup`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
name: name,
email: email,
password: password
})
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.catch((err) => {
console.log(err);
error = err.detail;
return null;
});
if (error) {
throw error;
}
return res;
};
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 getAllChats = async (token: string) => {
let error = null;
const res = await fetch(`${WEBUI_API_BASE_URL}/chats/all`, {
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;
};
import { WEBUI_API_BASE_URL } from '$lib/constants';
export const getBackendConfig = async () => {
let error = null;
const res = await fetch(`${WEBUI_API_BASE_URL}/`, {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.catch((err) => {
console.log(err);
error = err;
return null;
});
return res;
};
import { WEBUI_API_BASE_URL } from '$lib/constants';
export const createNewModelfile = async (token: string, modelfile: object) => {
let error = null;
const res = await fetch(`${WEBUI_API_BASE_URL}/modelfiles/create`, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
authorization: `Bearer ${token}`
},
body: JSON.stringify({
modelfile: modelfile
})
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.catch((err) => {
error = err.detail;
console.log(err);
return null;
});
if (error) {
throw error;
}
return res;
};
export const getModelfiles = async (token: string = '') => {
let error = null;
const res = await fetch(`${WEBUI_API_BASE_URL}/modelfiles/`, {
method: 'GET',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
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.map((modelfile) => modelfile.modelfile);
};
export const getModelfileByTagName = async (token: string, tagName: string) => {
let error = null;
const res = await fetch(`${WEBUI_API_BASE_URL}/modelfiles/`, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
authorization: `Bearer ${token}`
},
body: JSON.stringify({
tag_name: tagName
})
})
.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.modelfile;
};
export const updateModelfileByTagName = async (
token: string,
tagName: string,
modelfile: object
) => {
let error = null;
const res = await fetch(`${WEBUI_API_BASE_URL}/modelfiles/update`, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
authorization: `Bearer ${token}`
},
body: JSON.stringify({
tag_name: tagName,
modelfile: modelfile
})
})
.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 deleteModelfileByTagName = async (token: string, tagName: string) => {
let error = null;
const res = await fetch(`${WEBUI_API_BASE_URL}/modelfiles/delete`, {
method: 'DELETE',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
authorization: `Bearer ${token}`
},
body: JSON.stringify({
tag_name: tagName
})
})
.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;
};
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((err) => {
console.log(err);
if ('detail' in err) {
error = err.detail;
} else {
error = 'Server connection failed';
}
return null;
});
if (error) {
throw error;
}
return res?.version ?? '';
};
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((err) => {
console.log(err);
if ('detail' in err) {
error = err.detail;
} else {
error = 'Server connection failed';
}
return null;
});
if (error) {
throw error;
}
return res?.models ?? [];
};
export const generateTitle = async (
base_url: string = OLLAMA_API_BASE_URL,
token: string = '',
model: string,
prompt: string
) => {
let error = null;
const res = await fetch(`${base_url}/generate`, {
method: 'POST',
headers: {
'Content-Type': 'text/event-stream',
Authorization: `Bearer ${token}`
},
body: JSON.stringify({
model: model,
prompt: `Generate a brief 3-5 word title for this question, excluding the term 'title.' Then, please reply with only the title: ${prompt}`,
stream: false
})
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.catch((err) => {
console.log(err);
if ('detail' in err) {
error = err.detail;
}
return null;
});
if (error) {
throw error;
}
return res?.response ?? 'New Chat';
};
export const generateChatCompletion = async (
base_url: string = OLLAMA_API_BASE_URL,
token: string = '',
body: object
) => {
let error = null;
const res = await fetch(`${base_url}/chat`, {
method: 'POST',
headers: {
'Content-Type': 'text/event-stream',
Authorization: `Bearer ${token}`
},
body: JSON.stringify(body)
}).catch((err) => {
error = err;
return null;
});
if (error) {
throw error;
}
return res;
};
export const createModel = async (
base_url: string = OLLAMA_API_BASE_URL,
token: string,
tagName: string,
content: string
) => {
let error = null;
const res = await fetch(`${base_url}/create`, {
method: 'POST',
headers: {
'Content-Type': 'text/event-stream',
Authorization: `Bearer ${token}`
},
body: JSON.stringify({
name: tagName,
modelfile: content
})
}).catch((err) => {
error = err;
return null;
});
if (error) {
throw error;
}
return res;
};
export const deleteModel = async (
base_url: string = OLLAMA_API_BASE_URL,
token: string,
tagName: string
) => {
let error = null;
const res = await fetch(`${base_url}/delete`, {
method: 'DELETE',
headers: {
'Content-Type': 'text/event-stream',
Authorization: `Bearer ${token}`
},
body: JSON.stringify({
name: tagName
})
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.then((json) => {
console.log(json);
return true;
})
.catch((err) => {
console.log(err);
error = err.error;
return null;
});
if (error) {
throw error;
}
return res;
};
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((err) => {
console.log(err);
error = `OpenAI: ${err?.error?.message ?? 'Network Problem'}`;
return null;
});
if (error) {
throw error;
}
let models = Array.isArray(res) ? res : res?.data ?? null;
return models
.map((model) => ({ name: model.id, external: true }))
.filter((model) => (base_url.includes('openai') ? model.name.includes('gpt') : true));
};
import { WEBUI_API_BASE_URL } from '$lib/constants';
export const updateUserRole = async (token: string, id: string, role: string) => {
let error = null;
const res = await fetch(`${WEBUI_API_BASE_URL}/users/update/role`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`
},
body: JSON.stringify({
id: id,
role: role
})
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.catch((err) => {
console.log(err);
error = err.detail;
return null;
});
if (error) {
throw error;
}
return res;
};
export const getUsers = async (token: string) => {
let error = null;
const res = await fetch(`${WEBUI_API_BASE_URL}/users`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`
}
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.catch((error) => {
console.log(error);
return null;
});
if (error) {
throw error;
}
return res ? res : [];
};
...@@ -8,11 +8,13 @@ ...@@ -8,11 +8,13 @@
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 { chats, 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';
import { getChatList, updateChatById } from '$lib/apis/chats';
export let chatId = '';
export let sendPrompt: Function; export let sendPrompt: Function;
export let regenerateResponse: Function; export let regenerateResponse: Function;
...@@ -27,14 +29,25 @@ ...@@ -27,14 +29,25 @@
$: if (messages && messages.length > 0 && (messages.at(-1).done ?? false)) { $: if (messages && messages.length > 0 && (messages.at(-1).done ?? false)) {
(async () => { (async () => {
await tick(); await tick();
[...document.querySelectorAll('*')].forEach((node) => {
if (node._tippy) {
node._tippy.destroy();
}
});
console.log('rendering message');
renderLatex(); renderLatex();
hljs.highlightAll(); hljs.highlightAll();
createCopyCodeBlockButton(); createCopyCodeBlockButton();
for (const message of messages) { for (const message of messages) {
if (message.info) { if (message.info) {
console.log(message);
tippy(`#info-${message.id}`, { tippy(`#info-${message.id}`, {
content: `<span class="text-xs">token/s: ${ content: `<span class="text-xs" id="tooltip-${message.id}">token/s: ${
`${ `${
Math.round( Math.round(
((message.info.eval_count ?? 0) / (message.info.eval_duration / 1000000000)) * 100 ((message.info.eval_count ?? 0) / (message.info.eval_duration / 1000000000)) * 100
...@@ -81,7 +94,7 @@ ...@@ -81,7 +94,7 @@
blocks.forEach((block) => { blocks.forEach((block) => {
// only add button if browser supports Clipboard API // only add button if browser supports Clipboard API
if (navigator.clipboard && block.childNodes.length < 2 && block.id !== 'user-message') { if (block.childNodes.length < 2 && block.id !== 'user-message') {
let code = block.querySelector('code'); let code = block.querySelector('code');
code.style.borderTopRightRadius = 0; code.style.borderTopRightRadius = 0;
code.style.borderTopLeftRadius = 0; code.style.borderTopLeftRadius = 0;
...@@ -119,10 +132,6 @@ ...@@ -119,10 +132,6 @@
topBarDiv.appendChild(button); topBarDiv.appendChild(button);
block.prepend(topBarDiv); block.prepend(topBarDiv);
// button.addEventListener('click', async () => {
// await copyCode(block, button);
// });
} }
}); });
...@@ -130,7 +139,7 @@ ...@@ -130,7 +139,7 @@
let code = block.querySelector('code'); let code = block.querySelector('code');
let text = code.innerText; let text = code.innerText;
await navigator.clipboard.writeText(text); await copyToClipboard(text);
// visual feedback that task is completed // visual feedback that task is completed
button.innerText = 'Copied!'; button.innerText = 'Copied!';
...@@ -239,7 +248,7 @@ ...@@ -239,7 +248,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) => {
...@@ -253,6 +262,7 @@ ...@@ -253,6 +262,7 @@
}; };
const rateMessage = async (messageIdx, rating) => { const rateMessage = async (messageIdx, rating) => {
// TODO: Move this function to parent
messages = messages.map((message, idx) => { messages = messages.map((message, idx) => {
if (messageIdx === idx) { if (messageIdx === idx) {
message.rating = rating; message.rating = rating;
...@@ -260,10 +270,12 @@ ...@@ -260,10 +270,12 @@
return message; return message;
}); });
$db.updateChatById(chatId, { await updateChatById(localStorage.token, chatId, {
messages: messages, messages: messages,
history: history history: history
}); });
await chats.set(await getChatList(localStorage.token));
}; };
const showPreviousMessage = async (message) => { const showPreviousMessage = async (message) => {
......
<script lang="ts"> <script lang="ts">
import Modal from '../common/Modal.svelte'; import toast from 'svelte-french-toast';
import fileSaver from 'file-saver';
const { saveAs } = fileSaver;
import { onMount } from 'svelte';
import { config, models, settings, user, chats } from '$lib/stores';
import { splitStream, getGravatarURL } from '$lib/utils';
import { getOllamaVersion } from '$lib/apis/ollama';
import { createNewChat, getAllChats, getChatList } from '$lib/apis/chats';
import { import {
WEB_UI_VERSION, WEB_UI_VERSION,
OLLAMA_API_BASE_URL, OLLAMA_API_BASE_URL,
WEBUI_API_BASE_URL, WEBUI_API_BASE_URL,
WEBUI_BASE_URL WEBUI_BASE_URL
} from '$lib/constants'; } from '$lib/constants';
import toast from 'svelte-french-toast';
import { onMount } from 'svelte';
import { config, info, models, settings, user } from '$lib/stores';
import { splitStream, getGravatarURL } from '$lib/utils';
import Advanced from './Settings/Advanced.svelte'; import Advanced from './Settings/Advanced.svelte';
import { stringify } from 'postcss'; import Modal from '../common/Modal.svelte';
export let show = false; export let show = false;
...@@ -74,11 +79,48 @@ ...@@ -74,11 +79,48 @@
let OPENAI_API_KEY = ''; let OPENAI_API_KEY = '';
let OPENAI_API_BASE_URL = ''; let OPENAI_API_BASE_URL = '';
// Chats
let importFiles;
let showDeleteHistoryConfirm = false;
const importChats = async (_chats) => {
for (const chat of _chats) {
console.log(chat);
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`);
};
$: if (importFiles) {
console.log(importFiles);
let reader = new FileReader();
reader.onload = (event) => {
let chats = JSON.parse(event.target.result);
console.log(chats);
importChats(chats);
};
reader.readAsText(importFiles[0]);
}
// Auth // Auth
let authEnabled = false; let authEnabled = false;
let authType = 'Basic'; let authType = 'Basic';
let authContent = ''; let authContent = '';
// About
let ollamaVersion = '';
const checkOllamaConnection = async () => { const checkOllamaConnection = async () => {
if (API_BASE_URL === '') { if (API_BASE_URL === '') {
API_BASE_URL = OLLAMA_API_BASE_URL; API_BASE_URL = OLLAMA_API_BASE_URL;
...@@ -553,7 +595,7 @@ ...@@ -553,7 +595,7 @@
return models; return models;
}; };
onMount(() => { onMount(async () => {
let settings = JSON.parse(localStorage.getItem('settings') ?? '{}'); let settings = JSON.parse(localStorage.getItem('settings') ?? '{}');
console.log(settings); console.log(settings);
...@@ -586,6 +628,13 @@ ...@@ -586,6 +628,13 @@
authType = settings.authHeader.split(' ')[0]; authType = settings.authHeader.split(' ')[0];
authContent = settings.authHeader.split(' ')[1]; authContent = settings.authHeader.split(' ')[1];
} }
ollamaVersion = await getOllamaVersion(
API_BASE_URL ?? OLLAMA_API_BASE_URL,
localStorage.token
).catch((error) => {
return '';
});
}); });
</script> </script>
...@@ -741,6 +790,32 @@ ...@@ -741,6 +790,32 @@
<div class=" self-center">Add-ons</div> <div class=" self-center">Add-ons</div>
</button> </button>
<button
class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
'chats'
? 'bg-gray-200 dark:bg-gray-700'
: ' hover:bg-gray-300 dark:hover:bg-gray-800'}"
on:click={() => {
selectedTab = 'chats';
}}
>
<div class=" self-center mr-2">
<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="M8 2C4.262 2 1 4.57 1 8c0 1.86.98 3.486 2.455 4.566a3.472 3.472 0 0 1-.469 1.26.75.75 0 0 0 .713 1.14 6.961 6.961 0 0 0 3.06-1.06c.403.062.818.094 1.241.094 3.738 0 7-2.57 7-6s-3.262-6-7-6ZM5 9a1 1 0 1 0 0-2 1 1 0 0 0 0 2Zm7-1a1 1 0 1 1-2 0 1 1 0 0 1 2 0ZM8 9a1 1 0 1 0 0-2 1 1 0 0 0 0 2Z"
clip-rule="evenodd"
/>
</svg>
</div>
<div class=" self-center">Chats</div>
</button>
{#if !$config || ($config && !$config.auth)} {#if !$config || ($config && !$config.auth)}
<button <button
class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab === class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
...@@ -1089,13 +1164,65 @@ ...@@ -1089,13 +1164,65 @@
</div> </div>
<hr class=" dark:border-gray-700" /> <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 <form
on:submit|preventDefault={() => { on:submit|preventDefault={() => {
uploadModelHandler(); uploadModelHandler();
}} }}
> >
<div class=" mb-2 flex w-full justify-between"> <div class=" mb-2 flex w-full justify-between">
<div class=" text-sm font-medium">Upload a GGUF model</div> <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 <button
class="p-1 px-3 text-xs flex rounded transition" class="p-1 px-3 text-xs flex rounded transition"
...@@ -1252,51 +1379,6 @@ ...@@ -1252,51 +1379,6 @@
</div> </div>
{/if} {/if}
</form> </form>
<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>
</div> </div>
</div> </div>
{:else if selectedTab === 'external'} {:else if selectedTab === 'external'}
...@@ -1472,6 +1554,150 @@ ...@@ -1472,6 +1554,150 @@
</button> </button>
</div> </div>
</form> </form>
{:else if selectedTab === 'chats'}
<div class="flex flex-col h-full justify-between space-y-3 text-sm">
<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>
<!-- {#if showDeleteHistoryConfirm}
<div
class="flex justify-between rounded-md items-center py-3 px-3.5 w-full transition"
>
<div class="flex items-center">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-5 h-5 mr-3"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0"
/>
</svg>
<span>Are you sure?</span>
</div>
<div class="flex space-x-1.5 items-center">
<button
class="hover:text-white transition"
on:click={() => {
deleteChatHistory();
showDeleteHistoryConfirm = 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={() => {
showDeleteHistoryConfirm = 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-3 px-3.5 w-full hover:bg-gray-900 transition"
on:click={() => {
showDeleteHistoryConfirm = true;
}}
>
<div class="mr-3">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-5 h-5"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0"
/>
</svg>
</div>
<span>Clear conversations</span>
</button>
{/if} -->
</div>
{:else if selectedTab === 'auth'} {:else if selectedTab === 'auth'}
<form <form
class="flex flex-col h-full justify-between space-y-3 text-sm" class="flex flex-col h-full justify-between space-y-3 text-sm"
...@@ -1607,7 +1833,7 @@ ...@@ -1607,7 +1833,7 @@
<div class=" mb-2.5 text-sm font-medium">Ollama Version</div> <div class=" mb-2.5 text-sm font-medium">Ollama Version</div>
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1 text-xs text-gray-700 dark:text-gray-200"> <div class="flex-1 text-xs text-gray-700 dark:text-gray-200">
{$info?.ollama?.version ?? 'N/A'} {ollamaVersion ?? 'N/A'}
</div> </div>
</div> </div>
</div> </div>
......
<script lang="ts"> <script lang="ts">
import { v4 as uuidv4 } from 'uuid'; import { getChatById } from '$lib/apis/chats';
import { goto } from '$app/navigation';
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 getChatById(localStorage.token, $chatId)).chat;
console.log('share', chat); console.log('share', chat);
toast.success('Redirecting you to OllamaHub');
toast.success('Redirecting you to OllamaHub');
const url = 'https://ollamahub.com'; const url = 'https://ollamahub.com';
// const url = 'http://localhost:5173'; // const url = 'http://localhost:5173';
...@@ -44,12 +43,9 @@ ...@@ -44,12 +43,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
......
...@@ -6,32 +6,28 @@ ...@@ -6,32 +6,28 @@
import { goto, invalidateAll } from '$app/navigation'; import { goto, invalidateAll } from '$app/navigation';
import { page } from '$app/stores'; import { page } from '$app/stores';
import { user, db, chats, showSettings, chatId } from '$lib/stores'; import { user, chats, showSettings, chatId } from '$lib/stores';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { deleteChatById, getChatList, updateChatById } from '$lib/apis/chats';
let show = false; let show = false;
let navElement; let navElement;
let importFileInputElement;
let importFiles;
let title: string = 'Ollama Web UI'; let title: string = 'Ollama Web UI';
let search = ''; let search = '';
let chatDeleteId = null; let chatDeleteId = null;
let chatTitleEditId = null; let chatTitleEditId = null;
let chatTitle = ''; let chatTitle = '';
let showDropdown = false; let showDropdown = false;
let showDeleteHistoryConfirm = false;
onMount(async () => { onMount(async () => {
if (window.innerWidth > 1280) { if (window.innerWidth > 1280) {
show = true; show = true;
} }
await chats.set(await $db.getChats()); await chats.set(await getChatList(localStorage.token));
}); });
const loadChat = async (id) => { const loadChat = async (id) => {
...@@ -39,49 +35,27 @@ ...@@ -39,49 +35,27 @@
}; };
const editChatTitle = async (id, _title) => { const editChatTitle = async (id, _title) => {
await $db.updateChatById(id, { title = _title;
await updateChatById(localStorage.token, id, {
title: _title title: _title
}); });
title = _title; await chats.set(await getChatList(localStorage.token));
}; };
const deleteChat = async (id) => { const deleteChat = async (id) => {
goto('/'); goto('/');
$db.deleteChatById(id);
};
const deleteChatHistory = async () => {
await $db.deleteAllChat();
};
const importChats = async (chatHistory) => {
await $db.addChats(chatHistory);
};
const exportChats = async () => { await deleteChatById(localStorage.token, id);
let blob = new Blob([JSON.stringify(await $db.exportChats())], { type: 'application/json' }); await chats.set(await getChatList(localStorage.token));
saveAs(blob, `chat-export-${Date.now()}.json`);
}; };
$: if (importFiles) {
console.log(importFiles);
let reader = new FileReader();
reader.onload = (event) => {
let chats = JSON.parse(event.target.result);
console.log(chats);
importChats(chats);
};
reader.readAsText(importFiles[0]);
}
</script> </script>
<div <div
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 +65,11 @@ ...@@ -91,8 +65,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">
...@@ -121,39 +98,41 @@ ...@@ -121,39 +98,41 @@
</button> </button>
</div> </div>
<div class="px-2.5 flex justify-center my-1"> {#if $user?.role === 'admin'}
<button <div class="px-2.5 flex justify-center my-1">
class="flex-grow flex space-x-3 rounded-md px-3 py-2 hover:bg-gray-900 transition" <button
on:click={async () => { class="flex-grow flex space-x-3 rounded-md px-3 py-2 hover:bg-gray-900 transition"
goto('/modelfiles'); on:click={async () => {
}} goto('/modelfiles');
> }}
<div class="self-center"> >
<svg <div class="self-center">
xmlns="http://www.w3.org/2000/svg" <svg
fill="none" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24" fill="none"
stroke-width="1.5" viewBox="0 0 24 24"
stroke="currentColor" stroke-width="1.5"
class="w-4 h-4" stroke="currentColor"
> class="w-4 h-4"
<path >
stroke-linecap="round" <path
stroke-linejoin="round" stroke-linecap="round"
d="M13.5 16.875h3.375m0 0h3.375m-3.375 0V13.5m0 3.375v3.375M6 10.5h2.25a2.25 2.25 0 002.25-2.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v2.25A2.25 2.25 0 006 10.5zm0 9.75h2.25A2.25 2.25 0 0010.5 18v-2.25a2.25 2.25 0 00-2.25-2.25H6a2.25 2.25 0 00-2.25 2.25V18A2.25 2.25 0 006 20.25zm9.75-9.75H18a2.25 2.25 0 002.25-2.25V6A2.25 2.25 0 0018 3.75h-2.25A2.25 2.25 0 0013.5 6v2.25a2.25 2.25 0 002.25 2.25z" stroke-linejoin="round"
/> d="M13.5 16.875h3.375m0 0h3.375m-3.375 0V13.5m0 3.375v3.375M6 10.5h2.25a2.25 2.25 0 002.25-2.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v2.25A2.25 2.25 0 006 10.5zm0 9.75h2.25A2.25 2.25 0 0010.5 18v-2.25a2.25 2.25 0 00-2.25-2.25H6a2.25 2.25 0 00-2.25 2.25V18A2.25 2.25 0 006 20.25zm9.75-9.75H18a2.25 2.25 0 002.25-2.25V6A2.25 2.25 0 0018 3.75h-2.25A2.25 2.25 0 0013.5 6v2.25a2.25 2.25 0 002.25 2.25z"
</svg> />
</div> </svg>
</div>
<div class="flex self-center"> <div class="flex self-center">
<div class=" self-center font-medium text-sm">Modelfiles</div> <div class=" self-center font-medium text-sm">Modelfiles</div>
</div> </div>
</button> </button>
</div> </div>
{/if}
<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 +148,7 @@ ...@@ -169,7 +148,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,148 +373,9 @@ ...@@ -394,148 +373,9 @@
</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">
<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"
on:click={() => {
importFileInputElement.click();
// importChats();
}}
>
<div class=" self-center mr-3">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-5 h-5"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m6.75 12l-3-3m0 0l-3 3m3-3v6m-1.5-15H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z"
/>
</svg>
</div>
<div class=" self-center">Import</div>
</button>
<button
class=" flex rounded-md py-3 px-3.5 w-full hover:bg-gray-900 transition"
on:click={() => {
exportChats();
}}
>
<div class=" self-center mr-3">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-5 h-5"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m.75 12l3 3m0 0l3-3m-3 3v-6m-1.5-9H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z"
/>
</svg>
</div>
<div class=" self-center">Export</div>
</button>
</div>
{#if showDeleteHistoryConfirm}
<div class="flex justify-between rounded-md items-center py-3 px-3.5 w-full transition">
<div class="flex items-center">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-5 h-5 mr-3"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0"
/>
</svg>
<span>Are you sure?</span>
</div>
<div class="flex space-x-1.5 items-center">
<button
class="hover:text-white transition"
on:click={() => {
deleteChatHistory();
showDeleteHistoryConfirm = 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={() => {
showDeleteHistoryConfirm = 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-3 px-3.5 w-full hover:bg-gray-900 transition"
on:click={() => {
showDeleteHistoryConfirm = true;
}}
>
<div class="mr-3">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-5 h-5"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0"
/>
</svg>
</div>
<span>Clear conversations</span>
</button>
{/if}
{#if $user !== undefined} {#if $user !== undefined}
<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"
......
import { dev, browser } from '$app/environment'; import { dev, browser } from '$app/environment';
import { PUBLIC_API_BASE_URL } from '$env/static/public'; import { PUBLIC_API_BASE_URL } from '$env/static/public';
export const OLLAMA_API_BASE_URL = export const OLLAMA_API_BASE_URL = dev
PUBLIC_API_BASE_URL === '' ? `http://${location.hostname}:8080/ollama/api`
? browser : PUBLIC_API_BASE_URL === ''
? `http://${location.hostname}:11434/api` ? browser
: `http://localhost:11434/api` ? `http://${location.hostname}:11434/api`
: PUBLIC_API_BASE_URL; : `http://localhost:11434/api`
: PUBLIC_API_BASE_URL;
export const WEBUI_BASE_URL = dev ? `http://${location.hostname}:8080` : ``; export const WEBUI_BASE_URL = dev ? `http://${location.hostname}:8080` : ``;
export const WEBUI_API_BASE_URL = `${WEBUI_BASE_URL}/api/v1`; export const WEBUI_API_BASE_URL = `${WEBUI_BASE_URL}/api/v1`;
export const WEB_UI_VERSION = 'v1.0.0-alpha-static'; export const WEB_UI_VERSION = 'v1.0.0-alpha-static';
export const REQUIRED_OLLAMA_VERSION = '0.1.16';
// Source: https://kit.svelte.dev/docs/modules#$env-static-public // Source: https://kit.svelte.dev/docs/modules#$env-static-public
// This feature, akin to $env/static/private, exclusively incorporates environment variables // This feature, akin to $env/static/private, exclusively incorporates environment variables
// that are prefixed with config.kit.env.publicPrefix (usually set to PUBLIC_). // that are prefixed with config.kit.env.publicPrefix (usually set to PUBLIC_).
......
import { writable } from 'svelte/store'; import { writable } from 'svelte/store';
// Backend // Backend
export const info = writable({});
export const config = writable(undefined); export const config = writable(undefined);
export const user = writable(undefined); export const user = writable(undefined);
......
...@@ -66,9 +66,9 @@ export const getGravatarURL = (email) => { ...@@ -66,9 +66,9 @@ export const getGravatarURL = (email) => {
return `https://www.gravatar.com/avatar/${hash}`; return `https://www.gravatar.com/avatar/${hash}`;
}; };
const copyToClipboard = (text) => { export const copyToClipboard = (text) => {
if (!navigator.clipboard) { if (!navigator.clipboard) {
var textArea = document.createElement('textarea'); const textArea = document.createElement('textarea');
textArea.value = text; textArea.value = text;
// Avoid scrolling to bottom // Avoid scrolling to bottom
...@@ -81,8 +81,8 @@ const copyToClipboard = (text) => { ...@@ -81,8 +81,8 @@ const copyToClipboard = (text) => {
textArea.select(); textArea.select();
try { try {
var successful = document.execCommand('copy'); const successful = document.execCommand('copy');
var msg = successful ? 'successful' : 'unsuccessful'; const msg = successful ? 'successful' : 'unsuccessful';
console.log('Fallback: Copying text command was ' + msg); console.log('Fallback: Copying text command was ' + msg);
} catch (err) { } catch (err) {
console.error('Fallback: Oops, unable to copy', err); console.error('Fallback: Oops, unable to copy', err);
...@@ -100,3 +100,14 @@ const copyToClipboard = (text) => { ...@@ -100,3 +100,14 @@ const copyToClipboard = (text) => {
} }
); );
}; };
export const checkVersion = (required, current) => {
// Returns true when current version is below required
return current === '0.0.0'
? false
: current.localeCompare(required, undefined, {
numeric: true,
sensitivity: 'case',
caseFirst: 'upper'
}) < 0;
};
<script lang="ts"> <script lang="ts">
import { v4 as uuidv4 } from 'uuid'; import toast from 'svelte-french-toast';
import { openDB, deleteDB } from 'idb'; import { openDB, deleteDB } from 'idb';
import { onMount, tick } from 'svelte'; import { onMount, tick } from 'svelte';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { import fileSaver from 'file-saver';
config, const { saveAs } = fileSaver;
info,
user, import { getOllamaModels, getOllamaVersion } from '$lib/apis/ollama';
showSettings, import { getModelfiles } from '$lib/apis/modelfiles';
settings,
models, import { getOpenAIModels } from '$lib/apis/openai';
db,
chats, import { user, showSettings, settings, models, modelfiles } from '$lib/stores';
chatId, import { OLLAMA_API_BASE_URL, REQUIRED_OLLAMA_VERSION, WEBUI_API_BASE_URL } from '$lib/constants';
modelfiles
} from '$lib/stores';
import SettingsModal from '$lib/components/chat/SettingsModal.svelte'; import SettingsModal from '$lib/components/chat/SettingsModal.svelte';
import Sidebar from '$lib/components/layout/Sidebar.svelte'; import Sidebar from '$lib/components/layout/Sidebar.svelte';
import toast from 'svelte-french-toast'; import { checkVersion } from '$lib/utils';
import { OLLAMA_API_BASE_URL, WEBUI_API_BASE_URL } from '$lib/constants';
let requiredOllamaVersion = '0.1.16'; let ollamaVersion = '';
let loaded = false; let loaded = false;
let DB = null;
let localDBChats = [];
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(
headers: { $settings?.API_BASE_URL ?? OLLAMA_API_BASE_URL,
Accept: 'application/json', localStorage.token
'Content-Type': 'application/json', ).catch((error) => {
...($settings.authHeader && { Authorization: $settings.authHeader }), toast.error(error);
...($user && { Authorization: `Bearer ${localStorage.token}` }) return [];
} }))
}) );
.then(async (res) => { // If OpenAI API Key exists
if (!res.ok) throw await res.json(); if ($settings.OPENAI_API_KEY) {
return res.json(); const openAIModels = await getOpenAIModels(
}) $settings.OPENAI_API_BASE_URL ?? 'https://api.openai.com/v1',
.catch((error) => { $settings.OPENAI_API_KEY
).catch((error) => {
console.log(error); console.log(error);
if ('detail' in error) { toast.error(error);
toast.error(error.detail);
} else {
toast.error('Server connection failed');
}
return null; return null;
}); });
console.log(res);
models.push(...(res?.models ?? []));
// If OpenAI API Key exists models.push(...(openAIModels ? [{ name: 'hr' }, ...openAIModels] : []));
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
)
]
: [])
);
} }
return models; return models;
}; };
const getDB = async () => { const setOllamaVersion = async (version: string = '') => {
const DB = await openDB('Chats', 1, { if (version === '') {
upgrade(db) { version = await getOllamaVersion(
const store = db.createObjectStore('chats', { $settings?.API_BASE_URL ?? OLLAMA_API_BASE_URL,
keyPath: 'id', localStorage.token
autoIncrement: true ).catch((error) => {
}); return '';
store.createIndex('timestamp', 'timestamp');
}
});
return {
db: DB,
getChatById: async function (id) {
return await this.db.get('chats', id);
},
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
}));
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);
}
await chats.set(await this.getChats());
},
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,
...updated,
timestamp: Date.now()
});
await chats.set(await this.getChats());
},
deleteChatById: async function (id) {
if ($chatId === id) {
goto('/');
await chatId.set(uuidv4());
}
await this.db.delete('chats', 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());
}
};
};
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); ollamaVersion = version;
return res?.version ?? '0';
};
const setOllamaVersion = async (ollamaVersion) => { console.log(ollamaVersion);
await info.set({ ...$info, ollama: { version: ollamaVersion } }); if (checkVersion(REQUIRED_OLLAMA_VERSION, ollamaVersion)) {
toast.error(`Ollama Version: ${ollamaVersion !== '' ? ollamaVersion : 'Not Detected'}`);
if (
ollamaVersion.localeCompare(requiredOllamaVersion, undefined, {
numeric: true,
sensitivity: 'case',
caseFirst: 'upper'
}) < 0
) {
toast.error(`Ollama Version: ${ollamaVersion}`);
} }
}; };
onMount(async () => { onMount(async () => {
if ($config && $config.auth && $user === undefined) { if ($user === undefined) {
await goto('/auth'); await goto('/auth');
} } else if (['user', 'admin'].includes($user.role)) {
try {
// Check if IndexedDB exists
DB = await openDB('Chats', 1);
if (DB) {
const chats = await DB.getAllFromIndex('chats', 'timestamp');
localDBChats = chats.map((item, idx) => chats[chats.length - 1 - idx]);
await settings.set(JSON.parse(localStorage.getItem('settings') ?? '{}')); if (localDBChats.length === 0) {
await deleteDB('Chats');
}
await models.set(await getModels()); console.log('localdb', localDBChats);
await modelfiles.set(JSON.parse(localStorage.getItem('modelfiles') ?? '[]')); }
console.log(DB);
} catch (error) {
// IndexedDB Not Found
console.log('IDB Not Found');
}
modelfiles.subscribe(async () => { console.log();
await models.set(await getModels()); await settings.set(JSON.parse(localStorage.getItem('settings') ?? '{}'));
}); await modelfiles.set(await getModelfiles(localStorage.token));
console.log($modelfiles);
let _db = await getDB(); modelfiles.subscribe(async () => {
await db.set(_db); // should fetch models
await models.set(await getModels());
});
await setOllamaVersion(await getOllamaVersion()); await setOllamaVersion();
await tick();
}
await tick();
loaded = true; loaded = true;
}); });
</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="fixed w-full h-full flex z-50">
<div
class="absolute w-full h-full backdrop-blur-md bg-white/20 dark:bg-gray-900/50 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-white border border-gray-100 dark:border-none hover:bg-gray-100 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 checkVersion(REQUIRED_OLLAMA_VERSION, ollamaVersion ?? '0')}
<div class="fixed 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 w-full h-full backdrop-blur-md bg-white/20 dark:bg-gray-900/50 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">
...@@ -254,15 +171,16 @@ ...@@ -254,15 +171,16 @@
/>We've detected either a connection hiccup or observed that you're using an older />We've detected either a connection hiccup or observed that you're using an older
version. Ensure you're on the latest Ollama version version. Ensure you're on the latest Ollama version
<br class=" hidden sm:flex" />(version <br class=" hidden sm:flex" />(version
<span class=" dark:text-white font-medium">{requiredOllamaVersion} or higher</span>) <span class=" dark:text-white font-medium">{REQUIRED_OLLAMA_VERSION} or higher</span
or check your connection. >) or check your connection.
</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-white border border-gray-100 dark:border-none hover:bg-gray-100 transition font-medium text-sm"
on:click={async () => { on:click={async () => {
await setOllamaVersion(await getOllamaVersion()); location.href = '/';
// await setOllamaVersion();
}} }}
> >
Check Again Check Again
...@@ -271,7 +189,57 @@ ...@@ -271,7 +189,57 @@
<button <button
class="text-xs text-center w-full mt-2 text-gray-400 underline" class="text-xs text-center w-full mt-2 text-gray-400 underline"
on:click={async () => { on:click={async () => {
await setOllamaVersion(requiredOllamaVersion); await setOllamaVersion(REQUIRED_OLLAMA_VERSION);
}}>Close</button
>
</div>
</div>
</div>
</div>
</div>
{:else if localDBChats.length > 0}
<div class="fixed w-full h-full flex z-50">
<div
class="absolute w-full h-full backdrop-blur-md bg-white/20 dark:bg-gray-900/50 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">
Important Update<br /> Action Required for Chat Log Storage
</div>
<div class=" mt-4 text-center text-sm dark:text-gray-200 w-full">
Saving chat logs directly to your browser's storage is no longer supported. Please
take a moment to download and delete your chat logs by clicking the button below.
Don't worry, you can easily re-import your chat logs to the backend through <span
class="font-semibold dark:text-white">Settings > Chats > Import Chats</span
>. This ensures that your valuable conversations are securely saved to your backend
database. Thank you!
</div>
<div class=" mt-6 mx-auto relative group w-fit">
<button
class="relative z-20 flex px-5 py-2 rounded-full bg-white border border-gray-100 dark:border-none hover:bg-gray-100 transition font-medium text-sm"
on:click={async () => {
let blob = new Blob([JSON.stringify(localDBChats)], {
type: 'application/json'
});
saveAs(blob, `chat-export-${Date.now()}.json`);
const tx = DB.transaction('chats', 'readwrite');
await Promise.all([tx.store.clear(), tx.done]);
await deleteDB('Chats');
localDBChats = [];
}}
>
Download & Delete
</button>
<button
class="text-xs text-center w-full mt-2 text-gray-400 underline"
on:click={async () => {
localDBChats = [];
}}>Close</button }}>Close</button
> >
</div> </div>
...@@ -285,9 +253,7 @@ ...@@ -285,9 +253,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>
......
...@@ -2,23 +2,27 @@ ...@@ -2,23 +2,27 @@
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 { 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 { models, modelfiles, user, settings, db, chats, chatId } from '$lib/stores';
import { OLLAMA_API_BASE_URL } from '$lib/constants';
import { config, models, modelfiles, user, settings, db, chats, chatId } from '$lib/stores'; import { generateChatCompletion, generateTitle } from '$lib/apis/ollama';
import { copyToClipboard, 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'; import { createNewChat, getChatList, updateChatById } from '$lib/apis/chats';
let stopResponseFlag = false; let stopResponseFlag = false;
let autoScroll = true; let autoScroll = true;
let selectedModels = ['']; let selectedModels = [''];
let selectedModelfile = null; let selectedModelfile = null;
$: selectedModelfile = $: selectedModelfile =
selectedModels.length === 1 && selectedModels.length === 1 &&
...@@ -26,10 +30,11 @@ ...@@ -26,10 +30,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,16 +55,8 @@ ...@@ -50,16 +55,8 @@
messages = []; messages = [];
} }
$: if (files) {
console.log(files);
}
onMount(async () => { onMount(async () => {
await chatId.set(uuidv4()); await initNewChat();
chatId.subscribe(async () => {
await initNewChat();
});
}); });
////////////////////////// //////////////////////////
...@@ -67,6 +64,11 @@ ...@@ -67,6 +64,11 @@
////////////////////////// //////////////////////////
const initNewChat = async () => { const initNewChat = async () => {
window.history.replaceState(history.state, '', `/`);
console.log('initNewChat');
await chatId.set('');
console.log($chatId); console.log($chatId);
autoScroll = true; autoScroll = true;
...@@ -82,68 +84,33 @@ ...@@ -82,68 +84,33 @@
: $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
}); });
}; };
const copyToClipboard = (text) => {
if (!navigator.clipboard) {
var textArea = document.createElement('textarea');
textArea.value = text;
// Avoid scrolling to bottom
textArea.style.top = '0';
textArea.style.left = '0';
textArea.style.position = 'fixed';
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
try {
var successful = document.execCommand('copy');
var msg = successful ? 'successful' : 'unsuccessful';
console.log('Fallback: Copying text command was ' + msg);
} catch (err) {
console.error('Fallback: Oops, unable to copy', err);
}
document.body.removeChild(textArea);
return;
}
navigator.clipboard.writeText(text).then(
function () {
console.log('Async: Copying to clipboard was successful!');
},
function (err) {
console.error('Async: Could not copy text: ', err);
}
);
};
////////////////////////// //////////////////////////
// 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);
} }
}) })
); );
await chats.set(await $db.getChats()); await chats.set(await getChatList(localStorage.token));
}; };
const sendPromptOllama = async (model, userPrompt, parentId, _chatId) => { const sendPromptOllama = async (model, userPrompt, parentId, _chatId) => {
console.log('sendPromptOllama'); // Create response message
let responseMessageId = uuidv4(); let responseMessageId = uuidv4();
let responseMessage = { let responseMessage = {
parentId: parentId, parentId: parentId,
...@@ -154,8 +121,11 @@ ...@@ -154,8 +121,11 @@
model: model model: model
}; };
// Add message to history and Set currentId to messageId
history.messages[responseMessageId] = responseMessage; history.messages[responseMessageId] = responseMessage;
history.currentId = responseMessageId; history.currentId = responseMessageId;
// Append messageId to childrenIds of parent message
if (parentId !== null) { if (parentId !== null) {
history.messages[parentId].childrenIds = [ history.messages[parentId].childrenIds = [
...history.messages[parentId].childrenIds, ...history.messages[parentId].childrenIds,
...@@ -163,17 +133,16 @@ ...@@ -163,17 +133,16 @@
]; ];
} }
// Wait until history/message have been updated
await tick(); await tick();
// Scroll down
window.scrollTo({ top: document.body.scrollHeight }); window.scrollTo({ top: document.body.scrollHeight });
const res = await fetch(`${$settings?.API_BASE_URL ?? OLLAMA_API_BASE_URL}/chat`, { const res = await generateChatCompletion(
method: 'POST', $settings?.API_BASE_URL ?? OLLAMA_API_BASE_URL,
headers: { localStorage.token,
'Content-Type': 'text/event-stream', {
...($settings.authHeader && { Authorization: $settings.authHeader }),
...($user && { Authorization: `Bearer ${localStorage.token}` })
},
body: JSON.stringify({
model: model, model: model,
messages: [ messages: [
$settings.system $settings.system
...@@ -195,20 +164,11 @@ ...@@ -195,20 +164,11 @@
}) })
})), })),
options: { options: {
seed: $settings.seed ?? undefined,
temperature: $settings.temperature ?? undefined,
repeat_penalty: $settings.repeat_penalty ?? undefined,
top_k: $settings.top_k ?? undefined,
top_p: $settings.top_p ?? undefined,
num_ctx: $settings.num_ctx ?? undefined,
...($settings.options ?? {}) ...($settings.options ?? {})
}, },
format: $settings.requestFormat ?? undefined format: $settings.requestFormat ?? undefined
}) }
}).catch((err) => { );
console.log(err);
return null;
});
if (res && res.ok) { if (res && res.ok) {
const reader = res.body const reader = res.body
...@@ -297,23 +257,14 @@ ...@@ -297,23 +257,14 @@
if (autoScroll) { if (autoScroll) {
window.scrollTo({ top: document.body.scrollHeight }); window.scrollTo({ top: document.body.scrollHeight });
} }
}
await $db.updateChatById(_chatId, { if ($chatId == _chatId) {
title: title === '' ? 'New Chat' : title, chat = await updateChatById(localStorage.token, _chatId, {
models: selectedModels,
system: $settings.system ?? undefined,
options: {
seed: $settings.seed ?? undefined,
temperature: $settings.temperature ?? undefined,
repeat_penalty: $settings.repeat_penalty ?? undefined,
top_k: $settings.top_k ?? undefined,
top_p: $settings.top_p ?? undefined,
num_ctx: $settings.num_ctx ?? undefined,
...($settings.options ?? {})
},
messages: messages, messages: messages,
history: history history: history
}); });
await chats.set(await getChatList(localStorage.token));
} }
} else { } else {
if (res !== null) { if (res !== null) {
...@@ -339,6 +290,7 @@ ...@@ -339,6 +290,7 @@
stopResponseFlag = false; stopResponseFlag = false;
await tick(); await tick();
if (autoScroll) { if (autoScroll) {
window.scrollTo({ top: document.body.scrollHeight }); window.scrollTo({ top: document.body.scrollHeight });
} }
...@@ -481,23 +433,14 @@ ...@@ -481,23 +433,14 @@
if (autoScroll) { if (autoScroll) {
window.scrollTo({ top: document.body.scrollHeight }); window.scrollTo({ top: document.body.scrollHeight });
} }
}
await $db.updateChatById(_chatId, { if ($chatId == _chatId) {
title: title === '' ? 'New Chat' : title, chat = await updateChatById(localStorage.token, _chatId, {
models: selectedModels,
system: $settings.system ?? undefined,
options: {
seed: $settings.seed ?? undefined,
temperature: $settings.temperature ?? undefined,
repeat_penalty: $settings.repeat_penalty ?? undefined,
top_k: $settings.top_k ?? undefined,
top_p: $settings.top_p ?? undefined,
num_ctx: $settings.num_ctx ?? undefined,
...($settings.options ?? {})
},
messages: messages, messages: messages,
history: history history: history
}); });
await chats.set(await getChatList(localStorage.token));
} }
} else { } else {
if (res !== null) { if (res !== null) {
...@@ -542,16 +485,18 @@ ...@@ -542,16 +485,18 @@
}; };
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');
} else if (messages.length != 0 && messages.at(-1).done != true) { } else if (messages.length != 0 && messages.at(-1).done != true) {
// Response not done
console.log('wait'); console.log('wait');
} else { } else {
// Reset chat message textarea height
document.getElementById('chat-textarea').style.height = ''; document.getElementById('chat-textarea').style.height = '';
// Create user message
let userMessageId = uuidv4(); let userMessageId = uuidv4();
let userMessage = { let userMessage = {
id: userMessageId, id: userMessageId,
...@@ -562,42 +507,43 @@ ...@@ -562,42 +507,43 @@
files: files.length > 0 ? files : undefined files: files.length > 0 ? files : undefined
}; };
// Add message to history and Set currentId to messageId
history.messages[userMessageId] = userMessage;
history.currentId = userMessageId;
// Append messageId to childrenIds of parent message
if (messages.length !== 0) { if (messages.length !== 0) {
history.messages[messages.at(-1).id].childrenIds.push(userMessageId); history.messages[messages.at(-1).id].childrenIds.push(userMessageId);
} }
history.messages[userMessageId] = userMessage; // Wait until history/message have been updated
history.currentId = userMessageId;
await tick(); await tick();
// Create new chat if only one message in messages
if (messages.length == 1) { if (messages.length == 1) {
await $db.createNewChat({ chat = await createNewChat(localStorage.token, {
id: _chatId, id: $chatId,
title: 'New Chat', title: 'New Chat',
models: selectedModels, models: selectedModels,
system: $settings.system ?? undefined, system: $settings.system ?? undefined,
options: { options: {
seed: $settings.seed ?? undefined,
temperature: $settings.temperature ?? undefined,
repeat_penalty: $settings.repeat_penalty ?? undefined,
top_k: $settings.top_k ?? undefined,
top_p: $settings.top_p ?? undefined,
num_ctx: $settings.num_ctx ?? undefined,
...($settings.options ?? {}) ...($settings.options ?? {})
}, },
messages: messages, messages: messages,
history: history history: history,
timestamp: Date.now()
}); });
await chats.set(await getChatList(localStorage.token));
await chatId.set(chat.id);
await tick();
} }
// Reset chat input textarea
prompt = ''; prompt = '';
files = []; files = [];
setTimeout(() => { // Send prompt
window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' }); await sendPrompt(userPrompt, userMessageId);
}, 50);
await sendPrompt(userPrompt, userMessageId, _chatId);
} }
}; };
...@@ -607,9 +553,7 @@ ...@@ -607,9 +553,7 @@
}; };
const regenerateResponse = async () => { const regenerateResponse = async () => {
const _chatId = JSON.parse(JSON.stringify($chatId)); console.log('regenerateResponse');
console.log('regenerateResponse', _chatId);
if (messages.length != 0 && messages.at(-1).done == true) { if (messages.length != 0 && messages.at(-1).done == true) {
messages.splice(messages.length - 1, 1); messages.splice(messages.length - 1, 1);
messages = messages; messages = messages;
...@@ -617,41 +561,21 @@ ...@@ -617,41 +561,21 @@
let userMessage = messages.at(-1); let userMessage = messages.at(-1);
let userPrompt = userMessage.content; let userPrompt = userMessage.content;
await sendPrompt(userPrompt, userMessage.id, _chatId); await sendPrompt(userPrompt, userMessage.id);
} }
}; };
const generateChatTitle = async (_chatId, userPrompt) => { const generateChatTitle = async (_chatId, userPrompt) => {
if ($settings.titleAutoGenerate ?? true) { if ($settings.titleAutoGenerate ?? true) {
console.log('generateChatTitle'); const title = await generateTitle(
$settings?.API_BASE_URL ?? OLLAMA_API_BASE_URL,
const res = await fetch(`${$settings?.API_BASE_URL ?? OLLAMA_API_BASE_URL}/generate`, { localStorage.token,
method: 'POST', selectedModels[0],
headers: { userPrompt
'Content-Type': 'text/event-stream', );
...($settings.authHeader && { Authorization: $settings.authHeader }),
...($user && { Authorization: `Bearer ${localStorage.token}` }) if (title) {
}, await setChatTitle(_chatId, title);
body: JSON.stringify({
model: selectedModels[0],
prompt: `Generate a brief 3-5 word title for this question, excluding the term 'title.' Then, please reply with only the title: ${userPrompt}`,
stream: false
})
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.catch((error) => {
if ('detail' in error) {
toast.error(error.detail);
}
console.log(error);
return null;
});
if (res) {
await setChatTitle(_chatId, res.response === '' ? 'New Chat' : res.response);
} }
} else { } else {
await setChatTitle(_chatId, `${userPrompt}`); await setChatTitle(_chatId, `${userPrompt}`);
...@@ -659,10 +583,12 @@ ...@@ -659,10 +583,12 @@
}; };
const setChatTitle = async (_chatId, _title) => { const setChatTitle = async (_chatId, _title) => {
await $db.updateChatById(_chatId, { title: _title });
if (_chatId === $chatId) { if (_chatId === $chatId) {
title = _title; title = _title;
} }
chat = await updateChatById(localStorage.token, _chatId, { title: _title });
await chats.set(await getChatList(localStorage.token));
}; };
</script> </script>
...@@ -672,7 +598,7 @@ ...@@ -672,7 +598,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 +607,7 @@ ...@@ -681,6 +607,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
......
...@@ -6,62 +6,27 @@ ...@@ -6,62 +6,27 @@
import toast from 'svelte-french-toast'; import toast from 'svelte-french-toast';
import { updateUserRole, getUsers } from '$lib/apis/users';
let loaded = false; let loaded = false;
let users = []; let users = [];
const updateUserRole = async (id, role) => { const updateRoleHandler = async (id, role) => {
const res = await fetch(`${WEBUI_API_BASE_URL}/users/update/role`, { const res = await updateUserRole(localStorage.token, id, role).catch((error) => {
method: 'POST', toast.error(error);
headers: { return null;
'Content-Type': 'application/json', });
Authorization: `Bearer ${localStorage.token}`
},
body: JSON.stringify({
id: id,
role: role
})
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.catch((error) => {
console.log(error);
toast.error(error.detail);
return null;
});
if (res) { if (res) {
await getUsers(); users = await getUsers(localStorage.token);
} }
}; };
const getUsers = async () => {
const res = await fetch(`${WEBUI_API_BASE_URL}/users/`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${localStorage.token}`
}
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.catch((error) => {
console.log(error);
toast.error(error.detail);
return null;
});
users = res ? res : [];
};
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(); users = await getUsers(localStorage.token);
} }
loaded = true; loaded = true;
}); });
...@@ -115,11 +80,11 @@ ...@@ -115,11 +80,11 @@
class=" dark:text-white underline" class=" dark:text-white underline"
on:click={() => { on:click={() => {
if (user.role === 'user') { if (user.role === 'user') {
updateUserRole(user.id, 'admin'); updateRoleHandler(user.id, 'admin');
} else if (user.role === 'pending') { } else if (user.role === 'pending') {
updateUserRole(user.id, 'user'); updateRoleHandler(user.id, 'user');
} else { } else {
updateUserRole(user.id, 'pending'); updateRoleHandler(user.id, 'pending');
} }
}}>{user.role}</button }}>{user.role}</button
> >
......
...@@ -2,17 +2,21 @@ ...@@ -2,17 +2,21 @@
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 { onMount, tick } from 'svelte'; import { onMount, tick } from 'svelte';
import { convertMessagesToHistory, splitStream } from '$lib/utils';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { config, models, modelfiles, user, settings, db, chats, chatId } from '$lib/stores'; import { page } from '$app/stores';
import { models, modelfiles, user, settings, db, chats, chatId } from '$lib/stores';
import { OLLAMA_API_BASE_URL } from '$lib/constants';
import { generateChatCompletion, generateTitle } from '$lib/apis/ollama';
import { copyToClipboard, 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'; import { createNewChat, getChatById, getChatList, updateChatById } from '$lib/apis/chats';
let loaded = false; let loaded = false;
let stopResponseFlag = false; let stopResponseFlag = false;
...@@ -27,6 +31,8 @@ ...@@ -27,6 +31,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 +59,8 @@ ...@@ -53,10 +59,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,94 +74,70 @@ ...@@ -70,94 +74,70 @@
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 getChatById(localStorage.token, $chatId).catch(async (error) => {
await goto('/');
return null;
});
if (chat) { if (chat) {
console.log(chat); const chatContent = chat.chat;
selectedModels = (chat?.models ?? undefined) !== undefined ? chat.models : [chat.model ?? '']; if (chatContent) {
history = console.log(chatContent);
(chat?.history ?? undefined) !== undefined
? chat.history selectedModels =
: convertMessagesToHistory(chat.messages); (chatContent?.models ?? undefined) !== undefined
title = chat.title; ? chatContent.models
: [chatContent.model ?? ''];
let _settings = JSON.parse(localStorage.getItem('settings') ?? '{}'); history =
await settings.set({ (chatContent?.history ?? undefined) !== undefined
..._settings, ? chatContent.history
system: chat.system ?? _settings.system, : convertMessagesToHistory(chatContent.messages);
options: chat.options ?? _settings.options title = chatContent.title;
});
autoScroll = true; let _settings = JSON.parse(localStorage.getItem('settings') ?? '{}');
await settings.set({
await tick(); ..._settings,
if (messages.length > 0) { system: chatContent.system ?? _settings.system,
history.messages[messages.at(-1).id].done = true; options: chatContent.options ?? _settings.options
} });
await tick(); autoScroll = true;
await tick();
return chat; if (messages.length > 0) {
} else { history.messages[messages.at(-1).id].done = true;
return null; }
} await tick();
};
const copyToClipboard = (text) => { return true;
if (!navigator.clipboard) { } else {
var textArea = document.createElement('textarea'); return null;
textArea.value = text;
// Avoid scrolling to bottom
textArea.style.top = '0';
textArea.style.left = '0';
textArea.style.position = 'fixed';
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
try {
var successful = document.execCommand('copy');
var msg = successful ? 'successful' : 'unsuccessful';
console.log('Fallback: Copying text command was ' + msg);
} catch (err) {
console.error('Fallback: Oops, unable to copy', err);
} }
document.body.removeChild(textArea);
return;
} }
navigator.clipboard.writeText(text).then(
function () {
console.log('Async: Copying to clipboard was successful!');
},
function (err) {
console.error('Async: Could not copy text: ', err);
}
);
}; };
////////////////////////// //////////////////////////
// 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);
} }
}) })
); );
await chats.set(await $db.getChats()); await chats.set(await getChatList(localStorage.token));
}; };
const sendPromptOllama = async (model, userPrompt, parentId, _chatId) => { const sendPromptOllama = async (model, userPrompt, parentId, _chatId) => {
console.log('sendPromptOllama'); // Create response message
let responseMessageId = uuidv4(); let responseMessageId = uuidv4();
let responseMessage = { let responseMessage = {
parentId: parentId, parentId: parentId,
...@@ -168,8 +148,11 @@ ...@@ -168,8 +148,11 @@
model: model model: model
}; };
// Add message to history and Set currentId to messageId
history.messages[responseMessageId] = responseMessage; history.messages[responseMessageId] = responseMessage;
history.currentId = responseMessageId; history.currentId = responseMessageId;
// Append messageId to childrenIds of parent message
if (parentId !== null) { if (parentId !== null) {
history.messages[parentId].childrenIds = [ history.messages[parentId].childrenIds = [
...history.messages[parentId].childrenIds, ...history.messages[parentId].childrenIds,
...@@ -177,17 +160,16 @@ ...@@ -177,17 +160,16 @@
]; ];
} }
// Wait until history/message have been updated
await tick(); await tick();
// Scroll down
window.scrollTo({ top: document.body.scrollHeight }); window.scrollTo({ top: document.body.scrollHeight });
const res = await fetch(`${$settings?.API_BASE_URL ?? OLLAMA_API_BASE_URL}/chat`, { const res = await generateChatCompletion(
method: 'POST', $settings?.API_BASE_URL ?? OLLAMA_API_BASE_URL,
headers: { localStorage.token,
'Content-Type': 'text/event-stream', {
...($settings.authHeader && { Authorization: $settings.authHeader }),
...($user && { Authorization: `Bearer ${localStorage.token}` })
},
body: JSON.stringify({
model: model, model: model,
messages: [ messages: [
$settings.system $settings.system
...@@ -209,20 +191,11 @@ ...@@ -209,20 +191,11 @@
}) })
})), })),
options: { options: {
seed: $settings.seed ?? undefined,
temperature: $settings.temperature ?? undefined,
repeat_penalty: $settings.repeat_penalty ?? undefined,
top_k: $settings.top_k ?? undefined,
top_p: $settings.top_p ?? undefined,
num_ctx: $settings.num_ctx ?? undefined,
...($settings.options ?? {}) ...($settings.options ?? {})
}, },
format: $settings.requestFormat ?? undefined format: $settings.requestFormat ?? undefined
}) }
}).catch((err) => { );
console.log(err);
return null;
});
if (res && res.ok) { if (res && res.ok) {
const reader = res.body const reader = res.body
...@@ -311,23 +284,14 @@ ...@@ -311,23 +284,14 @@
if (autoScroll) { if (autoScroll) {
window.scrollTo({ top: document.body.scrollHeight }); window.scrollTo({ top: document.body.scrollHeight });
} }
}
await $db.updateChatById(_chatId, { if ($chatId == _chatId) {
title: title === '' ? 'New Chat' : title, chat = await updateChatById(localStorage.token, _chatId, {
models: selectedModels,
system: $settings.system ?? undefined,
options: {
seed: $settings.seed ?? undefined,
temperature: $settings.temperature ?? undefined,
repeat_penalty: $settings.repeat_penalty ?? undefined,
top_k: $settings.top_k ?? undefined,
top_p: $settings.top_p ?? undefined,
num_ctx: $settings.num_ctx ?? undefined,
...($settings.options ?? {})
},
messages: messages, messages: messages,
history: history history: history
}); });
await chats.set(await getChatList(localStorage.token));
} }
} else { } else {
if (res !== null) { if (res !== null) {
...@@ -353,6 +317,7 @@ ...@@ -353,6 +317,7 @@
stopResponseFlag = false; stopResponseFlag = false;
await tick(); await tick();
if (autoScroll) { if (autoScroll) {
window.scrollTo({ top: document.body.scrollHeight }); window.scrollTo({ top: document.body.scrollHeight });
} }
...@@ -495,23 +460,14 @@ ...@@ -495,23 +460,14 @@
if (autoScroll) { if (autoScroll) {
window.scrollTo({ top: document.body.scrollHeight }); window.scrollTo({ top: document.body.scrollHeight });
} }
}
await $db.updateChatById(_chatId, { if ($chatId == _chatId) {
title: title === '' ? 'New Chat' : title, chat = await updateChatById(localStorage.token, _chatId, {
models: selectedModels,
system: $settings.system ?? undefined,
options: {
seed: $settings.seed ?? undefined,
temperature: $settings.temperature ?? undefined,
repeat_penalty: $settings.repeat_penalty ?? undefined,
top_k: $settings.top_k ?? undefined,
top_p: $settings.top_p ?? undefined,
num_ctx: $settings.num_ctx ?? undefined,
...($settings.options ?? {})
},
messages: messages, messages: messages,
history: history history: history
}); });
await chats.set(await getChatList(localStorage.token));
} }
} else { } else {
if (res !== null) { if (res !== null) {
...@@ -556,16 +512,18 @@ ...@@ -556,16 +512,18 @@
}; };
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');
} else if (messages.length != 0 && messages.at(-1).done != true) { } else if (messages.length != 0 && messages.at(-1).done != true) {
// Response not done
console.log('wait'); console.log('wait');
} else { } else {
// Reset chat message textarea height
document.getElementById('chat-textarea').style.height = ''; document.getElementById('chat-textarea').style.height = '';
// Create user message
let userMessageId = uuidv4(); let userMessageId = uuidv4();
let userMessage = { let userMessage = {
id: userMessageId, id: userMessageId,
...@@ -576,42 +534,43 @@ ...@@ -576,42 +534,43 @@
files: files.length > 0 ? files : undefined files: files.length > 0 ? files : undefined
}; };
// Add message to history and Set currentId to messageId
history.messages[userMessageId] = userMessage;
history.currentId = userMessageId;
// Append messageId to childrenIds of parent message
if (messages.length !== 0) { if (messages.length !== 0) {
history.messages[messages.at(-1).id].childrenIds.push(userMessageId); history.messages[messages.at(-1).id].childrenIds.push(userMessageId);
} }
history.messages[userMessageId] = userMessage; // Wait until history/message have been updated
history.currentId = userMessageId;
await tick(); await tick();
// Create new chat if only one message in messages
if (messages.length == 1) { if (messages.length == 1) {
await $db.createNewChat({ chat = await createNewChat(localStorage.token, {
id: _chatId, id: $chatId,
title: 'New Chat', title: 'New Chat',
models: selectedModels, models: selectedModels,
system: $settings.system ?? undefined, system: $settings.system ?? undefined,
options: { options: {
seed: $settings.seed ?? undefined,
temperature: $settings.temperature ?? undefined,
repeat_penalty: $settings.repeat_penalty ?? undefined,
top_k: $settings.top_k ?? undefined,
top_p: $settings.top_p ?? undefined,
num_ctx: $settings.num_ctx ?? undefined,
...($settings.options ?? {}) ...($settings.options ?? {})
}, },
messages: messages, messages: messages,
history: history history: history,
timestamp: Date.now()
}); });
await chats.set(await getChatList(localStorage.token));
await chatId.set(chat.id);
await tick();
} }
// Reset chat input textarea
prompt = ''; prompt = '';
files = []; files = [];
setTimeout(() => { // Send prompt
window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' }); await sendPrompt(userPrompt, userMessageId);
}, 50);
await sendPrompt(userPrompt, userMessageId, _chatId);
} }
}; };
...@@ -621,9 +580,7 @@ ...@@ -621,9 +580,7 @@
}; };
const regenerateResponse = async () => { const regenerateResponse = async () => {
const _chatId = JSON.parse(JSON.stringify($chatId)); console.log('regenerateResponse');
console.log('regenerateResponse', _chatId);
if (messages.length != 0 && messages.at(-1).done == true) { if (messages.length != 0 && messages.at(-1).done == true) {
messages.splice(messages.length - 1, 1); messages.splice(messages.length - 1, 1);
messages = messages; messages = messages;
...@@ -631,41 +588,21 @@ ...@@ -631,41 +588,21 @@
let userMessage = messages.at(-1); let userMessage = messages.at(-1);
let userPrompt = userMessage.content; let userPrompt = userMessage.content;
await sendPrompt(userPrompt, userMessage.id, _chatId); await sendPrompt(userPrompt, userMessage.id);
} }
}; };
const generateChatTitle = async (_chatId, userPrompt) => { const generateChatTitle = async (_chatId, userPrompt) => {
if ($settings.titleAutoGenerate ?? true) { if ($settings.titleAutoGenerate ?? true) {
console.log('generateChatTitle'); const title = await generateTitle(
$settings?.API_BASE_URL ?? OLLAMA_API_BASE_URL,
const res = await fetch(`${$settings?.API_BASE_URL ?? OLLAMA_API_BASE_URL}/generate`, { localStorage.token,
method: 'POST', selectedModels[0],
headers: { userPrompt
'Content-Type': 'text/event-stream', );
...($settings.authHeader && { Authorization: $settings.authHeader }),
...($user && { Authorization: `Bearer ${localStorage.token}` }) if (title) {
}, await setChatTitle(_chatId, title);
body: JSON.stringify({
model: selectedModels[0],
prompt: `Generate a brief 3-5 word title for this question, excluding the term 'title.' Then, please reply with only the title: ${userPrompt}`,
stream: false
})
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.catch((error) => {
if ('detail' in error) {
toast.error(error.detail);
}
console.log(error);
return null;
});
if (res) {
await setChatTitle(_chatId, res.response === '' ? 'New Chat' : res.response);
} }
} else { } else {
await setChatTitle(_chatId, `${userPrompt}`); await setChatTitle(_chatId, `${userPrompt}`);
...@@ -673,10 +610,12 @@ ...@@ -673,10 +610,12 @@
}; };
const setChatTitle = async (_chatId, _title) => { const setChatTitle = async (_chatId, _title) => {
await $db.updateChatById(_chatId, { title: _title });
if (_chatId === $chatId) { if (_chatId === $chatId) {
title = _title; title = _title;
} }
chat = await updateChatById(localStorage.token, _chatId, { title: _title });
await chats.set(await getChatList(localStorage.token));
}; };
</script> </script>
...@@ -687,7 +626,13 @@ ...@@ -687,7 +626,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 +641,7 @@ ...@@ -696,6 +641,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
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment