Unverified Commit 7e061d19 authored by perfectra1n's avatar perfectra1n Committed by GitHub
Browse files

Merge branch 'open-webui:main' into feature-external-db-reconnect

parents 81b24169 9e4dd4b8
{ {
"name": "open-webui", "name": "open-webui",
"version": "0.3.4", "version": "0.3.5",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "open-webui", "name": "open-webui",
"version": "0.3.4", "version": "0.3.5",
"dependencies": { "dependencies": {
"@codemirror/lang-javascript": "^6.2.2", "@codemirror/lang-javascript": "^6.2.2",
"@codemirror/lang-python": "^6.1.6", "@codemirror/lang-python": "^6.1.6",
......
{ {
"name": "open-webui", "name": "open-webui",
"version": "0.3.4", "version": "0.3.5",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "npm run pyodide:fetch && vite dev --host", "dev": "npm run pyodide:fetch && vite dev --host",
......
...@@ -28,6 +28,10 @@ math { ...@@ -28,6 +28,10 @@ math {
@apply rounded-lg; @apply rounded-lg;
} }
.markdown a {
@apply underline;
}
ol > li { ol > li {
counter-increment: list-number; counter-increment: list-number;
display: block; display: block;
......
...@@ -205,6 +205,54 @@ export const generateTitle = async ( ...@@ -205,6 +205,54 @@ export const generateTitle = async (
return res?.choices[0]?.message?.content.replace(/["']/g, '') ?? 'New Chat'; return res?.choices[0]?.message?.content.replace(/["']/g, '') ?? 'New Chat';
}; };
export const generateEmoji = async (
token: string = '',
model: string,
prompt: string,
chat_id?: string
) => {
let error = null;
const res = await fetch(`${WEBUI_BASE_URL}/api/task/emoji/completions`, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`
},
body: JSON.stringify({
model: model,
prompt: prompt,
...(chat_id && { chat_id: chat_id })
})
})
.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;
}
const response = res?.choices[0]?.message?.content.replace(/["']/g, '') ?? null;
if (response) {
if (/\p{Extended_Pictographic}/u.test(response)) {
return response.match(/\p{Extended_Pictographic}/gu)[0];
}
}
return null;
};
export const generateSearchQuery = async ( export const generateSearchQuery = async (
token: string = '', token: string = '',
model: string, model: string,
......
...@@ -59,6 +59,37 @@ export const addNewMemory = async (token: string, content: string) => { ...@@ -59,6 +59,37 @@ export const addNewMemory = async (token: string, content: string) => {
return res; return res;
}; };
export const updateMemoryById = async (token: string, id: string, content: string) => {
let error = null;
const res = await fetch(`${WEBUI_API_BASE_URL}/memories/${id}/update`, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
authorization: `Bearer ${token}`
},
body: JSON.stringify({
content: content
})
})
.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 queryMemory = async (token: string, content: string) => { export const queryMemory = async (token: string, content: string) => {
let error = null; let error = null;
......
import { WEBUI_API_BASE_URL } from '$lib/constants'; import { WEBUI_API_BASE_URL } from '$lib/constants';
import { getUserPosition } from '$lib/utils';
export const getUserPermissions = async (token: string) => { export const getUserPermissions = async (token: string) => {
let error = null; let error = null;
...@@ -198,6 +199,75 @@ export const getUserById = async (token: string, userId: string) => { ...@@ -198,6 +199,75 @@ export const getUserById = async (token: string, userId: string) => {
return res; return res;
}; };
export const getUserInfo = async (token: string) => {
let error = null;
const res = await fetch(`${WEBUI_API_BASE_URL}/users/user/info`, {
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 updateUserInfo = async (token: string, info: object) => {
let error = null;
const res = await fetch(`${WEBUI_API_BASE_URL}/users/user/info/update`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`
},
body: JSON.stringify({
...info
})
})
.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 getAndUpdateUserLocation = async (token: string) => {
const location = await getUserPosition().catch((err) => {
throw err;
});
if (location) {
await updateUserInfo(token, { location: location });
return location;
} else {
throw new Error('Failed to get user location');
}
};
export const deleteUserById = async (token: string, userId: string) => { export const deleteUserById = async (token: string, userId: string) => {
let error = null; let error = null;
......
...@@ -44,6 +44,8 @@ ...@@ -44,6 +44,8 @@
let ENABLE_OLLAMA_API = null; let ENABLE_OLLAMA_API = null;
const verifyOpenAIHandler = async (idx) => { const verifyOpenAIHandler = async (idx) => {
OPENAI_API_BASE_URLS = OPENAI_API_BASE_URLS.map((url) => url.replace(/\/$/, ''));
OPENAI_API_BASE_URLS = await updateOpenAIUrls(localStorage.token, OPENAI_API_BASE_URLS); OPENAI_API_BASE_URLS = await updateOpenAIUrls(localStorage.token, OPENAI_API_BASE_URLS);
OPENAI_API_KEYS = await updateOpenAIKeys(localStorage.token, OPENAI_API_KEYS); OPENAI_API_KEYS = await updateOpenAIKeys(localStorage.token, OPENAI_API_KEYS);
...@@ -63,6 +65,10 @@ ...@@ -63,6 +65,10 @@
}; };
const verifyOllamaHandler = async (idx) => { const verifyOllamaHandler = async (idx) => {
OLLAMA_BASE_URLS = OLLAMA_BASE_URLS.filter((url) => url !== '').map((url) =>
url.replace(/\/$/, '')
);
OLLAMA_BASE_URLS = await updateOllamaUrls(localStorage.token, OLLAMA_BASE_URLS); OLLAMA_BASE_URLS = await updateOllamaUrls(localStorage.token, OLLAMA_BASE_URLS);
const res = await getOllamaVersion(localStorage.token, idx).catch((error) => { const res = await getOllamaVersion(localStorage.token, idx).catch((error) => {
...@@ -78,6 +84,8 @@ ...@@ -78,6 +84,8 @@
}; };
const updateOpenAIHandler = async () => { const updateOpenAIHandler = async () => {
OPENAI_API_BASE_URLS = OPENAI_API_BASE_URLS.map((url) => url.replace(/\/$/, ''));
// Check if API KEYS length is same than API URLS length // Check if API KEYS length is same than API URLS length
if (OPENAI_API_KEYS.length !== OPENAI_API_BASE_URLS.length) { if (OPENAI_API_KEYS.length !== OPENAI_API_BASE_URLS.length) {
// if there are more keys than urls, remove the extra keys // if there are more keys than urls, remove the extra keys
...@@ -100,7 +108,10 @@ ...@@ -100,7 +108,10 @@
}; };
const updateOllamaUrlsHandler = async () => { const updateOllamaUrlsHandler = async () => {
OLLAMA_BASE_URLS = OLLAMA_BASE_URLS.filter((url) => url !== ''); OLLAMA_BASE_URLS = OLLAMA_BASE_URLS.filter((url) => url !== '').map((url) =>
url.replace(/\/$/, '')
);
console.log(OLLAMA_BASE_URLS); console.log(OLLAMA_BASE_URLS);
if (OLLAMA_BASE_URLS.length === 0) { if (OLLAMA_BASE_URLS.length === 0) {
......
...@@ -13,6 +13,8 @@ ...@@ -13,6 +13,8 @@
getRAGConfig, getRAGConfig,
updateRAGConfig updateRAGConfig
} from '$lib/apis/rag'; } from '$lib/apis/rag';
import ResetUploadDirConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
import ResetVectorDBConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
import { documents, models } from '$lib/stores'; import { documents, models } from '$lib/stores';
import { onMount, getContext } from 'svelte'; import { onMount, getContext } from 'svelte';
...@@ -213,6 +215,34 @@ ...@@ -213,6 +215,34 @@
}); });
</script> </script>
<ResetUploadDirConfirmDialog
bind:show={showResetUploadDirConfirm}
on:confirm={() => {
const res = resetUploadDir(localStorage.token).catch((error) => {
toast.error(error);
return null;
});
if (res) {
toast.success($i18n.t('Success'));
}
}}
/>
<ResetVectorDBConfirmDialog
bind:show={showResetConfirm}
on:confirm={() => {
const res = resetVectorDB(localStorage.token).catch((error) => {
toast.error(error);
return null;
});
if (res) {
toast.success($i18n.t('Success'));
}
}}
/>
<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"
on:submit|preventDefault={() => { on:submit|preventDefault={() => {
...@@ -640,78 +670,6 @@ ...@@ -640,78 +670,6 @@
<hr class=" dark:border-gray-850" /> <hr class=" dark:border-gray-850" />
<div> <div>
{#if showResetUploadDirConfirm}
<div class="flex justify-between rounded-md items-center py-2 px-3.5 w-full transition">
<div class="flex items-center space-x-3">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
class="size-4"
>
<path
fill-rule="evenodd"
d="M5.625 1.5H9a3.75 3.75 0 0 1 3.75 3.75v1.875c0 1.036.84 1.875 1.875 1.875H16.5a3.75 3.75 0 0 1 3.75 3.75v7.875c0 1.035-.84 1.875-1.875 1.875H5.625a1.875 1.875 0 0 1-1.875-1.875V3.375c0-1.036.84-1.875 1.875-1.875ZM9.75 14.25a.75.75 0 0 0 0 1.5H15a.75.75 0 0 0 0-1.5H9.75Z"
clip-rule="evenodd"
/>
<path
d="M14.25 5.25a5.23 5.23 0 0 0-1.279-3.434 9.768 9.768 0 0 1 6.963 6.963A5.23 5.23 0 0 0 16.5 7.5h-1.875a.375.375 0 0 1-.375-.375V5.25Z"
/>
</svg>
<span>{$i18n.t('Are you sure?')}</span>
</div>
<div class="flex space-x-1.5 items-center">
<button
class="hover:text-white transition"
on:click={() => {
const res = resetUploadDir(localStorage.token).catch((error) => {
toast.error(error);
return null;
});
if (res) {
toast.success($i18n.t('Success'));
}
showResetUploadDirConfirm = false;
}}
type="button"
>
<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"
type="button"
on:click={() => {
showResetUploadDirConfirm = 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 <button
class=" flex rounded-xl py-2 px-3.5 w-full hover:bg-gray-200 dark:hover:bg-gray-800 transition" class=" flex rounded-xl py-2 px-3.5 w-full hover:bg-gray-200 dark:hover:bg-gray-800 transition"
on:click={() => { on:click={() => {
...@@ -738,77 +696,7 @@ ...@@ -738,77 +696,7 @@
</div> </div>
<div class=" self-center text-sm font-medium">{$i18n.t('Reset Upload Directory')}</div> <div class=" self-center text-sm font-medium">{$i18n.t('Reset Upload Directory')}</div>
</button> </button>
{/if}
{#if showResetConfirm}
<div class="flex justify-between rounded-md items-center py-2 px-3.5 w-full transition">
<div class="flex items-center space-x-3">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
>
<path
fill-rule="evenodd"
d="M3.5 2A1.5 1.5 0 0 0 2 3.5v9A1.5 1.5 0 0 0 3.5 14h9a1.5 1.5 0 0 0 1.5-1.5v-7A1.5 1.5 0 0 0 12.5 4H9.621a1.5 1.5 0 0 1-1.06-.44L7.439 2.44A1.5 1.5 0 0 0 6.38 2H3.5Zm6.75 7.75a.75.75 0 0 0 0-1.5h-4.5a.75.75 0 0 0 0 1.5h4.5Z"
clip-rule="evenodd"
/>
</svg>
<span>{$i18n.t('Are you sure?')}</span>
</div>
<div class="flex space-x-1.5 items-center">
<button
class="hover:text-white transition"
on:click={() => {
const res = resetVectorDB(localStorage.token).catch((error) => {
toast.error(error);
return null;
});
if (res) {
toast.success($i18n.t('Success'));
}
showResetConfirm = false;
}}
type="button"
>
<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={() => {
showResetConfirm = false;
}}
type="button"
>
<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 <button
class=" flex rounded-xl py-2 px-3.5 w-full hover:bg-gray-200 dark:hover:bg-gray-800 transition" class=" flex rounded-xl py-2 px-3.5 w-full hover:bg-gray-200 dark:hover:bg-gray-800 transition"
on:click={() => { on:click={() => {
...@@ -832,7 +720,6 @@ ...@@ -832,7 +720,6 @@
</div> </div>
<div class=" self-center text-sm font-medium">{$i18n.t('Reset Vector Storage')}</div> <div class=" self-center text-sm font-medium">{$i18n.t('Reset Vector Storage')}</div>
</button> </button>
{/if}
</div> </div>
</div> </div>
<div class="flex justify-end pt-3 text-sm font-medium"> <div class="flex justify-end pt-3 text-sm font-medium">
......
<script lang="ts"> <script lang="ts">
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';
import { onMount, getContext } from 'svelte';
import { WEBUI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants';
import { WEBUI_NAME, models, MODEL_DOWNLOAD_POOL, user, config } from '$lib/stores';
import { splitStream } from '$lib/utils';
import { import {
createModel, createModel,
...@@ -11,15 +16,11 @@ ...@@ -11,15 +16,11 @@
uploadModel, uploadModel,
getOllamaConfig getOllamaConfig
} from '$lib/apis/ollama'; } from '$lib/apis/ollama';
import { getModels as _getModels } from '$lib/apis';
import { WEBUI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants';
import { WEBUI_NAME, models, MODEL_DOWNLOAD_POOL, user, config } from '$lib/stores';
import { splitStream } from '$lib/utils';
import { onMount, getContext } from 'svelte';
import Tooltip from '$lib/components/common/Tooltip.svelte'; import Tooltip from '$lib/components/common/Tooltip.svelte';
import Spinner from '$lib/components/common/Spinner.svelte'; import Spinner from '$lib/components/common/Spinner.svelte';
import { getModels as _getModels } from '$lib/apis'; import ModelDeleteConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
const i18n = getContext('i18n'); const i18n = getContext('i18n');
...@@ -29,6 +30,8 @@ ...@@ -29,6 +30,8 @@
let modelUploadInputElement: HTMLInputElement; let modelUploadInputElement: HTMLInputElement;
let showModelDeleteConfirm = false;
// Models // Models
let ollamaEnabled = null; let ollamaEnabled = null;
...@@ -549,6 +552,13 @@ ...@@ -549,6 +552,13 @@
}); });
</script> </script>
<ModelDeleteConfirmDialog
bind:show={showModelDeleteConfirm}
on:confirm={() => {
deleteModelHandler();
}}
/>
<div class="flex flex-col h-full justify-between text-sm"> <div class="flex flex-col h-full justify-between text-sm">
<div class=" space-y-3 overflow-y-scroll scrollbar-hidden h-full"> <div class=" space-y-3 overflow-y-scroll scrollbar-hidden h-full">
{#if ollamaEnabled} {#if ollamaEnabled}
...@@ -763,7 +773,7 @@ ...@@ -763,7 +773,7 @@
<button <button
class="px-2.5 bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition" class="px-2.5 bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition"
on:click={() => { on:click={() => {
deleteModelHandler(); showModelDeleteConfirm = true;
}} }}
> >
<svg <svg
......
...@@ -18,7 +18,8 @@ ...@@ -18,7 +18,8 @@
'serpstack', 'serpstack',
'serper', 'serper',
'serply', 'serply',
'duckduckgo' 'duckduckgo',
'tavily'
]; ];
let youtubeLanguage = 'en'; let youtubeLanguage = 'en';
...@@ -214,6 +215,24 @@ ...@@ -214,6 +215,24 @@
</div> </div>
</div> </div>
</div> </div>
{:else if webConfig.search.engine === 'tavily'}
<div>
<div class=" self-center text-xs font-medium mb-1">
{$i18n.t('Tavily API Key')}
</div>
<div class="flex w-full">
<div class="flex-1">
<input
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
type="text"
placeholder={$i18n.t('Enter Tavily API Key')}
bind:value={webConfig.search.tavily_api_key}
autocomplete="off"
/>
</div>
</div>
</div>
{/if} {/if}
</div> </div>
{/if} {/if}
......
...@@ -31,6 +31,17 @@ ...@@ -31,6 +31,17 @@
} }
})(); })();
} }
let sortKey = 'updated_at'; // default sort key
let sortOrder = 'desc'; // default sort order
function setSortKey(key) {
if (sortKey === key) {
sortOrder = sortOrder === 'asc' ? 'desc' : 'asc';
} else {
sortKey = key;
sortOrder = 'asc';
}
}
</script> </script>
<Modal size="lg" bind:show> <Modal size="lg" bind:show>
...@@ -69,18 +80,56 @@ ...@@ -69,18 +80,56 @@
class="text-xs text-gray-700 uppercase bg-transparent dark:text-gray-200 border-b-2 dark:border-gray-800" class="text-xs text-gray-700 uppercase bg-transparent dark:text-gray-200 border-b-2 dark:border-gray-800"
> >
<tr> <tr>
<th scope="col" class="px-3 py-2"> {$i18n.t('Name')} </th> <th
<th scope="col" class="px-3 py-2 hidden md:flex"> {$i18n.t('Created at')} </th> scope="col"
class="px-3 py-2 cursor-pointer select-none"
on:click={() => setSortKey('title')}
>
{$i18n.t('Title')}
{#if sortKey === 'title'}
{sortOrder === 'asc' ? '▲' : '▼'}
{:else}
<span class="invisible">▲</span>
{/if}
</th>
<th
scope="col"
class="px-3 py-2 cursor-pointer select-none"
on:click={() => setSortKey('created_at')}
>
{$i18n.t('Created at')}
{#if sortKey === 'created_at'}
{sortOrder === 'asc' ? '▲' : '▼'}
{:else}
<span class="invisible">▲</span>
{/if}
</th>
<th
scope="col"
class="px-3 py-2 hidden md:flex cursor-pointer select-none"
on:click={() => setSortKey('updated_at')}
>
{$i18n.t('Updated at')}
{#if sortKey === 'updated_at'}
{sortOrder === 'asc' ? '▲' : '▼'}
{:else}
<span class="invisible">▲</span>
{/if}
</th>
<th scope="col" class="px-3 py-2 text-right" /> <th scope="col" class="px-3 py-2 text-right" />
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{#each chats as chat, idx} {#each chats.sort((a, b) => {
if (a[sortKey] < b[sortKey]) return sortOrder === 'asc' ? -1 : 1;
if (a[sortKey] > b[sortKey]) return sortOrder === 'asc' ? 1 : -1;
return 0;
}) as chat, idx}
<tr <tr
class="bg-transparent {idx !== chats.length - 1 && class="bg-transparent {idx !== chats.length - 1 &&
'border-b'} dark:bg-gray-900 dark:border-gray-850 text-xs" 'border-b'} dark:bg-gray-900 dark:border-gray-850 text-xs"
> >
<td class="px-3 py-1 w-2/3"> <td class="px-3 py-1">
<a href="/s/{chat.id}" target="_blank"> <a href="/s/{chat.id}" target="_blank">
<div class=" underline line-clamp-1"> <div class=" underline line-clamp-1">
{chat.title} {chat.title}
...@@ -88,11 +137,16 @@ ...@@ -88,11 +137,16 @@
</a> </a>
</td> </td>
<td class=" px-3 py-1 hidden md:flex h-[2.5rem]"> <td class=" px-3 py-1 h-[2.5rem]">
<div class="my-auto"> <div class="my-auto">
{dayjs(chat.created_at * 1000).format($i18n.t('MMMM DD, YYYY HH:mm'))} {dayjs(chat.created_at * 1000).format($i18n.t('MMMM DD, YYYY HH:mm'))}
</div> </div>
</td> </td>
<td class=" px-3 py-1 hidden md:flex h-[2.5rem]">
<div class="my-auto">
{dayjs(chat.updated_at * 1000).format($i18n.t('MMMM DD, YYYY HH:mm'))}
</div>
</td>
<td class="px-3 py-1 text-right"> <td class="px-3 py-1 text-right">
<div class="flex justify-end w-full"> <div class="flex justify-end w-full">
......
...@@ -30,6 +30,8 @@ ...@@ -30,6 +30,8 @@
import { import {
convertMessagesToHistory, convertMessagesToHistory,
copyToClipboard, copyToClipboard,
extractSentencesForAudio,
getUserPosition,
promptTemplate, promptTemplate,
splitStream splitStream
} from '$lib/utils'; } from '$lib/utils';
...@@ -49,7 +51,7 @@ ...@@ -49,7 +51,7 @@
import { runWebSearch } from '$lib/apis/rag'; import { runWebSearch } from '$lib/apis/rag';
import { createOpenAITextStream } from '$lib/apis/streaming'; import { createOpenAITextStream } from '$lib/apis/streaming';
import { queryMemory } from '$lib/apis/memories'; import { queryMemory } from '$lib/apis/memories';
import { getUserSettings } from '$lib/apis/users'; import { getAndUpdateUserLocation, getUserSettings } from '$lib/apis/users';
import { chatCompleted, generateTitle, generateSearchQuery } from '$lib/apis'; import { chatCompleted, generateTitle, generateSearchQuery } from '$lib/apis';
import Banner from '../common/Banner.svelte'; import Banner from '../common/Banner.svelte';
...@@ -64,6 +66,8 @@ ...@@ -64,6 +66,8 @@
export let chatIdProp = ''; export let chatIdProp = '';
let loaded = false; let loaded = false;
const eventTarget = new EventTarget();
let stopResponseFlag = false; let stopResponseFlag = false;
let autoScroll = true; let autoScroll = true;
let processing = ''; let processing = '';
...@@ -108,7 +112,8 @@ ...@@ -108,7 +112,8 @@
$: if (chatIdProp) { $: if (chatIdProp) {
(async () => { (async () => {
if (await loadChat()) { console.log(chatIdProp);
if (chatIdProp && (await loadChat())) {
await tick(); await tick();
loaded = true; loaded = true;
...@@ -123,7 +128,11 @@ ...@@ -123,7 +128,11 @@
onMount(async () => { onMount(async () => {
if (!$chatId) { if (!$chatId) {
chatId.subscribe(async (value) => {
if (!value) {
await initNewChat(); await initNewChat();
}
});
} else { } else {
if (!($settings.saveChatHistory ?? true)) { if (!($settings.saveChatHistory ?? true)) {
await goto('/'); await goto('/');
...@@ -300,7 +309,7 @@ ...@@ -300,7 +309,7 @@
// Chat functions // Chat functions
////////////////////////// //////////////////////////
const submitPrompt = async (userPrompt, _user = null) => { const submitPrompt = async (userPrompt, { _raw = false } = {}) => {
let _responses = []; let _responses = [];
console.log('submitPrompt', $chatId); console.log('submitPrompt', $chatId);
...@@ -344,7 +353,6 @@ ...@@ -344,7 +353,6 @@
parentId: messages.length !== 0 ? messages.at(-1).id : null, parentId: messages.length !== 0 ? messages.at(-1).id : null,
childrenIds: [], childrenIds: [],
role: 'user', role: 'user',
user: _user ?? undefined,
content: userPrompt, content: userPrompt,
files: _files.length > 0 ? _files : undefined, files: _files.length > 0 ? _files : undefined,
timestamp: Math.floor(Date.now() / 1000), // Unix epoch timestamp: Math.floor(Date.now() / 1000), // Unix epoch
...@@ -362,15 +370,13 @@ ...@@ -362,15 +370,13 @@
// Wait until history/message have been updated // Wait until history/message have been updated
await tick(); await tick();
_responses = await sendPrompt(userPrompt, userMessageId, { newChat: true });
// Send prompt
_responses = await sendPrompt(userPrompt, userMessageId);
} }
return _responses; return _responses;
}; };
const sendPrompt = async (prompt, parentId, modelId = null, newChat = true) => { const sendPrompt = async (prompt, parentId, { modelId = null, newChat = false } = {}) => {
let _responses = []; let _responses = [];
// If modelId is provided, use it, else use selected model // If modelId is provided, use it, else use selected model
...@@ -490,7 +496,6 @@ ...@@ -490,7 +496,6 @@
responseMessage.userContext = userContext; responseMessage.userContext = userContext;
const chatEventEmitter = await getChatEventEmitter(model.id, _chatId); const chatEventEmitter = await getChatEventEmitter(model.id, _chatId);
if (webSearchEnabled) { if (webSearchEnabled) {
await getWebSearchResults(model.id, parentId, responseMessageId); await getWebSearchResults(model.id, parentId, responseMessageId);
} }
...@@ -503,8 +508,6 @@ ...@@ -503,8 +508,6 @@
} }
_responses.push(_response); _responses.push(_response);
console.log('chatEventEmitter', chatEventEmitter);
if (chatEventEmitter) clearInterval(chatEventEmitter); if (chatEventEmitter) clearInterval(chatEventEmitter);
} else { } else {
toast.error($i18n.t(`Model {{modelId}} not found`, { modelId })); toast.error($i18n.t(`Model {{modelId}} not found`, { modelId }));
...@@ -513,88 +516,9 @@ ...@@ -513,88 +516,9 @@
); );
await chats.set(await getChatList(localStorage.token)); await chats.set(await getChatList(localStorage.token));
return _responses; return _responses;
}; };
const getWebSearchResults = async (model: string, parentId: string, responseId: string) => {
const responseMessage = history.messages[responseId];
responseMessage.statusHistory = [
{
done: false,
action: 'web_search',
description: $i18n.t('Generating search query')
}
];
messages = messages;
const prompt = history.messages[parentId].content;
let searchQuery = await generateSearchQuery(localStorage.token, model, messages, prompt).catch(
(error) => {
console.log(error);
return prompt;
}
);
if (!searchQuery) {
toast.warning($i18n.t('No search query generated'));
responseMessage.statusHistory.push({
done: true,
error: true,
action: 'web_search',
description: 'No search query generated'
});
messages = messages;
}
responseMessage.statusHistory.push({
done: false,
action: 'web_search',
description: $i18n.t(`Searching "{{searchQuery}}"`, { searchQuery })
});
messages = messages;
const results = await runWebSearch(localStorage.token, searchQuery).catch((error) => {
console.log(error);
toast.error(error);
return null;
});
if (results) {
responseMessage.statusHistory.push({
done: true,
action: 'web_search',
description: $i18n.t('Searched {{count}} sites', { count: results.filenames.length }),
query: searchQuery,
urls: results.filenames
});
if (responseMessage?.files ?? undefined === undefined) {
responseMessage.files = [];
}
responseMessage.files.push({
collection_name: results.collection_name,
name: searchQuery,
type: 'web_search_results',
urls: results.filenames
});
messages = messages;
} else {
responseMessage.statusHistory.push({
done: true,
error: true,
action: 'web_search',
description: 'No search results found'
});
messages = messages;
}
};
const sendPromptOllama = async (model, userPrompt, responseMessageId, _chatId) => { const sendPromptOllama = async (model, userPrompt, responseMessageId, _chatId) => {
let _response = null; let _response = null;
...@@ -610,7 +534,13 @@ ...@@ -610,7 +534,13 @@
$settings.system || (responseMessage?.userContext ?? null) $settings.system || (responseMessage?.userContext ?? null)
? { ? {
role: 'system', role: 'system',
content: `${promptTemplate($settings?.system ?? '', $user.name)}${ content: `${promptTemplate(
$settings?.system ?? '',
$user.name,
$settings?.userLocation
? await getAndUpdateUserLocation(localStorage.token)
: undefined
)}${
responseMessage?.userContext ?? null responseMessage?.userContext ?? null
? `\n\nUser Context:\n${(responseMessage?.userContext ?? []).join('\n')}` ? `\n\nUser Context:\n${(responseMessage?.userContext ?? []).join('\n')}`
: '' : ''
...@@ -676,6 +606,16 @@ ...@@ -676,6 +606,16 @@
array.findIndex((i) => JSON.stringify(i) === JSON.stringify(item)) === index array.findIndex((i) => JSON.stringify(i) === JSON.stringify(item)) === index
); );
eventTarget.dispatchEvent(
new CustomEvent('chat:start', {
detail: {
id: responseMessageId
}
})
);
await tick();
const [res, controller] = await generateChatCompletion(localStorage.token, { const [res, controller] = await generateChatCompletion(localStorage.token, {
model: model.id, model: model.id,
messages: messagesBody, messages: messagesBody,
...@@ -745,6 +685,23 @@ ...@@ -745,6 +685,23 @@
continue; continue;
} else { } else {
responseMessage.content += data.message.content; responseMessage.content += data.message.content;
const sentences = extractSentencesForAudio(responseMessage.content);
sentences.pop();
// dispatch only last sentence and make sure it hasn't been dispatched before
if (
sentences.length > 0 &&
sentences[sentences.length - 1] !== responseMessage.lastSentence
) {
responseMessage.lastSentence = sentences[sentences.length - 1];
eventTarget.dispatchEvent(
new CustomEvent('chat', {
detail: { id: responseMessageId, content: sentences[sentences.length - 1] }
})
);
}
messages = messages; messages = messages;
} }
} else { } else {
...@@ -771,21 +728,13 @@ ...@@ -771,21 +728,13 @@
messages = messages; messages = messages;
if ($settings.notificationEnabled && !document.hasFocus()) { if ($settings.notificationEnabled && !document.hasFocus()) {
const notification = new Notification( const notification = new Notification(`${model.id}`, {
selectedModelfile
? `${
selectedModelfile.title.charAt(0).toUpperCase() +
selectedModelfile.title.slice(1)
}`
: `${model.id}`,
{
body: responseMessage.content, body: responseMessage.content,
icon: selectedModelfile?.imageUrl ?? `${WEBUI_BASE_URL}/static/favicon.png` icon: `${WEBUI_BASE_URL}/static/favicon.png`
} });
);
} }
if ($settings.responseAutoCopy) { if ($settings?.responseAutoCopy ?? false) {
copyToClipboard(responseMessage.content); copyToClipboard(responseMessage.content);
} }
...@@ -847,6 +796,23 @@ ...@@ -847,6 +796,23 @@
stopResponseFlag = false; stopResponseFlag = false;
await tick(); await tick();
let lastSentence = extractSentencesForAudio(responseMessage.content)?.at(-1) ?? '';
if (lastSentence) {
eventTarget.dispatchEvent(
new CustomEvent('chat', {
detail: { id: responseMessageId, content: lastSentence }
})
);
}
eventTarget.dispatchEvent(
new CustomEvent('chat:finish', {
detail: {
id: responseMessageId,
content: responseMessage.content
}
})
);
if (autoScroll) { if (autoScroll) {
scrollToBottom(); scrollToBottom();
} }
...@@ -887,6 +853,15 @@ ...@@ -887,6 +853,15 @@
scrollToBottom(); scrollToBottom();
eventTarget.dispatchEvent(
new CustomEvent('chat:start', {
detail: {
id: responseMessageId
}
})
);
await tick();
try { try {
const [res, controller] = await generateOpenAIChatCompletion( const [res, controller] = await generateOpenAIChatCompletion(
localStorage.token, localStorage.token,
...@@ -903,7 +878,13 @@ ...@@ -903,7 +878,13 @@
$settings.system || (responseMessage?.userContext ?? null) $settings.system || (responseMessage?.userContext ?? null)
? { ? {
role: 'system', role: 'system',
content: `${promptTemplate($settings?.system ?? '', $user.name)}${ content: `${promptTemplate(
$settings?.system ?? '',
$user.name,
$settings?.userLocation
? await getAndUpdateUserLocation(localStorage.token)
: undefined
)}${
responseMessage?.userContext ?? null responseMessage?.userContext ?? null
? `\n\nUser Context:\n${(responseMessage?.userContext ?? []).join('\n')}` ? `\n\nUser Context:\n${(responseMessage?.userContext ?? []).join('\n')}`
: '' : ''
...@@ -1007,6 +988,23 @@ ...@@ -1007,6 +988,23 @@
continue; continue;
} else { } else {
responseMessage.content += value; responseMessage.content += value;
const sentences = extractSentencesForAudio(responseMessage.content);
sentences.pop();
// dispatch only last sentence and make sure it hasn't been dispatched before
if (
sentences.length > 0 &&
sentences[sentences.length - 1] !== responseMessage.lastSentence
) {
responseMessage.lastSentence = sentences[sentences.length - 1];
eventTarget.dispatchEvent(
new CustomEvent('chat', {
detail: { id: responseMessageId, content: sentences[sentences.length - 1] }
})
);
}
messages = messages; messages = messages;
} }
...@@ -1057,6 +1055,24 @@ ...@@ -1057,6 +1055,24 @@
stopResponseFlag = false; stopResponseFlag = false;
await tick(); await tick();
let lastSentence = extractSentencesForAudio(responseMessage.content)?.at(-1) ?? '';
if (lastSentence) {
eventTarget.dispatchEvent(
new CustomEvent('chat', {
detail: { id: responseMessageId, content: lastSentence }
})
);
}
eventTarget.dispatchEvent(
new CustomEvent('chat:finish', {
detail: {
id: responseMessageId,
content: responseMessage.content
}
})
);
if (autoScroll) { if (autoScroll) {
scrollToBottom(); scrollToBottom();
} }
...@@ -1123,9 +1139,12 @@ ...@@ -1123,9 +1139,12 @@
let userPrompt = userMessage.content; let userPrompt = userMessage.content;
if ((userMessage?.models ?? [...selectedModels]).length == 1) { if ((userMessage?.models ?? [...selectedModels]).length == 1) {
await sendPrompt(userPrompt, userMessage.id, undefined, false); // If user message has only one model selected, sendPrompt automatically selects it for regeneration
await sendPrompt(userPrompt, userMessage.id);
} else { } else {
await sendPrompt(userPrompt, userMessage.id, message.model, false); // If there are multiple models selected, use the model of the response message for regeneration
// e.g. many model chat
await sendPrompt(userPrompt, userMessage.id, { modelId: message.model });
} }
} }
}; };
...@@ -1191,6 +1210,84 @@ ...@@ -1191,6 +1210,84 @@
} }
}; };
const getWebSearchResults = async (model: string, parentId: string, responseId: string) => {
const responseMessage = history.messages[responseId];
responseMessage.statusHistory = [
{
done: false,
action: 'web_search',
description: $i18n.t('Generating search query')
}
];
messages = messages;
const prompt = history.messages[parentId].content;
let searchQuery = await generateSearchQuery(localStorage.token, model, messages, prompt).catch(
(error) => {
console.log(error);
return prompt;
}
);
if (!searchQuery) {
toast.warning($i18n.t('No search query generated'));
responseMessage.statusHistory.push({
done: true,
error: true,
action: 'web_search',
description: 'No search query generated'
});
messages = messages;
}
responseMessage.statusHistory.push({
done: false,
action: 'web_search',
description: $i18n.t(`Searching "{{searchQuery}}"`, { searchQuery })
});
messages = messages;
const results = await runWebSearch(localStorage.token, searchQuery).catch((error) => {
console.log(error);
toast.error(error);
return null;
});
if (results) {
responseMessage.statusHistory.push({
done: true,
action: 'web_search',
description: $i18n.t('Searched {{count}} sites', { count: results.filenames.length }),
query: searchQuery,
urls: results.filenames
});
if (responseMessage?.files ?? undefined === undefined) {
responseMessage.files = [];
}
responseMessage.files.push({
collection_name: results.collection_name,
name: searchQuery,
type: 'web_search_results',
urls: results.filenames
});
messages = messages;
} else {
responseMessage.statusHistory.push({
done: true,
error: true,
action: 'web_search',
description: 'No search results found'
});
messages = messages;
}
};
const getTags = async () => { const getTags = async () => {
return await getTagsById(localStorage.token, $chatId).catch(async (error) => { return await getTagsById(localStorage.token, $chatId).catch(async (error) => {
return []; return [];
...@@ -1206,7 +1303,18 @@ ...@@ -1206,7 +1303,18 @@
</title> </title>
</svelte:head> </svelte:head>
<CallOverlay {submitPrompt} bind:files /> <audio id="audioElement" src="" style="display: none;" />
{#if $showCallOverlay}
<CallOverlay
{submitPrompt}
{stopResponse}
bind:files
modelId={selectedModelIds?.at(0) ?? null}
chatId={$chatId}
{eventTarget}
/>
{/if}
{#if !chatIdProp || (loaded && chatIdProp)} {#if !chatIdProp || (loaded && chatIdProp)}
<div <div
......
...@@ -348,7 +348,6 @@ ...@@ -348,7 +348,6 @@
<Models <Models
bind:this={modelsElement} bind:this={modelsElement}
bind:prompt bind:prompt
bind:user
bind:chatInputPlaceholder bind:chatInputPlaceholder
{messages} {messages}
on:select={(e) => { on:select={(e) => {
...@@ -467,7 +466,7 @@ ...@@ -467,7 +466,7 @@
document.getElementById('chat-textarea')?.focus(); document.getElementById('chat-textarea')?.focus();
if ($settings?.speechAutoSend ?? false) { if ($settings?.speechAutoSend ?? false) {
submitPrompt(prompt, user); submitPrompt(prompt);
} }
}} }}
/> />
...@@ -476,7 +475,7 @@ ...@@ -476,7 +475,7 @@
class="w-full flex gap-1.5" class="w-full flex gap-1.5"
on:submit|preventDefault={() => { on:submit|preventDefault={() => {
// check if selectedModels support image input // check if selectedModels support image input
submitPrompt(prompt, user); submitPrompt(prompt);
}} }}
> >
<div <div
...@@ -718,7 +717,7 @@ ...@@ -718,7 +717,7 @@
// Submit the prompt when Enter key is pressed // Submit the prompt when Enter key is pressed
if (prompt !== '' && e.key === 'Enter' && !e.shiftKey) { if (prompt !== '' && e.key === 'Enter' && !e.shiftKey) {
submitPrompt(prompt, user); submitPrompt(prompt);
} }
} }
}} }}
......
...@@ -102,17 +102,19 @@ ...@@ -102,17 +102,19 @@
{#if filteredItems.length > 0 || prompt.split(' ')?.at(0)?.substring(1).startsWith('http')} {#if filteredItems.length > 0 || prompt.split(' ')?.at(0)?.substring(1).startsWith('http')}
<div class="pl-1 pr-12 mb-3 text-left w-full absolute bottom-0 left-0 right-0"> <div class="pl-1 pr-12 mb-3 text-left w-full absolute bottom-0 left-0 right-0">
<div class="flex w-full px-2"> <div class="flex w-full dark:border dark:border-gray-850 rounded-lg">
<div class=" bg-gray-100 dark:bg-gray-700 w-10 rounded-l-xl text-center"> <div class=" bg-gray-50 dark:bg-gray-850 w-10 rounded-l-lg text-center">
<div class=" text-lg font-semibold mt-2">#</div> <div class=" text-lg font-semibold mt-2">#</div>
</div> </div>
<div class="max-h-60 flex flex-col w-full rounded-r-xl bg-white"> <div
<div class="m-1 overflow-y-auto p-1 rounded-r-xl space-y-0.5"> class="max-h-60 flex flex-col w-full rounded-r-xl bg-white dark:bg-gray-900 dark:text-gray-100"
>
<div class="m-1 overflow-y-auto p-1 rounded-r-xl space-y-0.5 scrollbar-hidden">
{#each filteredItems as doc, docIdx} {#each filteredItems as doc, docIdx}
<button <button
class=" px-3 py-1.5 rounded-xl w-full text-left {docIdx === selectedIdx class=" px-3 py-1.5 rounded-xl w-full text-left {docIdx === selectedIdx
? ' bg-gray-100 selected-command-option-button' ? ' bg-gray-50 dark:bg-gray-850 dark:text-gray-100 selected-command-option-button'
: ''}" : ''}"
type="button" type="button"
on:click={() => { on:click={() => {
...@@ -126,17 +128,19 @@ ...@@ -126,17 +128,19 @@
on:focus={() => {}} on:focus={() => {}}
> >
{#if doc.type === 'collection'} {#if doc.type === 'collection'}
<div class=" font-medium text-black line-clamp-1"> <div class=" font-medium text-black dark:text-gray-100 line-clamp-1">
{doc?.title ?? `#${doc.name}`} {doc?.title ?? `#${doc.name}`}
</div> </div>
<div class=" text-xs text-gray-600 line-clamp-1">{$i18n.t('Collection')}</div> <div class=" text-xs text-gray-600 dark:text-gray-100 line-clamp-1">
{$i18n.t('Collection')}
</div>
{:else} {:else}
<div class=" font-medium text-black line-clamp-1"> <div class=" font-medium text-black dark:text-gray-100 line-clamp-1">
#{doc.name} ({doc.filename}) #{doc.name} ({doc.filename})
</div> </div>
<div class=" text-xs text-gray-600 line-clamp-1"> <div class=" text-xs text-gray-600 dark:text-gray-100 line-clamp-1">
{doc.title} {doc.title}
</div> </div>
{/if} {/if}
......
...@@ -134,17 +134,19 @@ ...@@ -134,17 +134,19 @@
{#if prompt.charAt(0) === '@'} {#if prompt.charAt(0) === '@'}
{#if filteredModels.length > 0} {#if filteredModels.length > 0}
<div class="pl-1 pr-12 mb-3 text-left w-full absolute bottom-0 left-0 right-0"> <div class="pl-1 pr-12 mb-3 text-left w-full absolute bottom-0 left-0 right-0">
<div class="flex w-full px-2"> <div class="flex w-full dark:border dark:border-gray-850 rounded-lg">
<div class=" bg-gray-100 dark:bg-gray-700 w-10 rounded-l-xl text-center"> <div class=" bg-gray-50 dark:bg-gray-850 w-10 rounded-l-lg text-center">
<div class=" text-lg font-semibold mt-2">@</div> <div class=" text-lg font-semibold mt-2">@</div>
</div> </div>
<div class="max-h-60 flex flex-col w-full rounded-r-xl bg-white"> <div
<div class="m-1 overflow-y-auto p-1 rounded-r-xl space-y-0.5"> class="max-h-60 flex flex-col w-full rounded-r-lg bg-white dark:bg-gray-900 dark:text-gray-100"
>
<div class="m-1 overflow-y-auto p-1 rounded-r-lg space-y-0.5 scrollbar-hidden">
{#each filteredModels as model, modelIdx} {#each filteredModels as model, modelIdx}
<button <button
class=" px-3 py-1.5 rounded-xl w-full text-left {modelIdx === selectedIdx class=" px-3 py-1.5 rounded-xl w-full text-left {modelIdx === selectedIdx
? ' bg-gray-100 selected-command-option-button' ? ' bg-gray-50 dark:bg-gray-850 selected-command-option-button'
: ''}" : ''}"
type="button" type="button"
on:click={() => { on:click={() => {
...@@ -155,7 +157,7 @@ ...@@ -155,7 +157,7 @@
}} }}
on:focus={() => {}} on:focus={() => {}}
> >
<div class=" font-medium text-black line-clamp-1"> <div class=" font-medium text-black dark:text-gray-100 line-clamp-1">
{model.name} {model.name}
</div> </div>
......
...@@ -89,17 +89,19 @@ ...@@ -89,17 +89,19 @@
{#if filteredPromptCommands.length > 0} {#if filteredPromptCommands.length > 0}
<div class="pl-1 pr-12 mb-3 text-left w-full absolute bottom-0 left-0 right-0"> <div class="pl-1 pr-12 mb-3 text-left w-full absolute bottom-0 left-0 right-0">
<div class="flex w-full px-2"> <div class="flex w-full dark:border dark:border-gray-850 rounded-lg">
<div class=" bg-gray-100 dark:bg-gray-700 w-10 rounded-l-xl text-center"> <div class=" bg-gray-50 dark:bg-gray-850 w-10 rounded-l-lg text-center">
<div class=" text-lg font-semibold mt-2">/</div> <div class=" text-lg font-semibold mt-2">/</div>
</div> </div>
<div class="max-h-60 flex flex-col w-full rounded-r-xl bg-white"> <div
<div class="m-1 overflow-y-auto p-1 rounded-r-xl space-y-0.5"> class="max-h-60 flex flex-col w-full rounded-r-lg bg-white dark:bg-gray-900 dark:text-gray-100"
>
<div class="m-1 overflow-y-auto p-1 rounded-r-lg space-y-0.5 scrollbar-hidden">
{#each filteredPromptCommands as command, commandIdx} {#each filteredPromptCommands as command, commandIdx}
<button <button
class=" px-3 py-1.5 rounded-xl w-full text-left {commandIdx === selectedCommandIdx class=" px-3 py-1.5 rounded-xl w-full text-left {commandIdx === selectedCommandIdx
? ' bg-gray-100 selected-command-option-button' ? ' bg-gray-50 dark:bg-gray-850 selected-command-option-button'
: ''}" : ''}"
type="button" type="button"
on:click={() => { on:click={() => {
...@@ -110,11 +112,11 @@ ...@@ -110,11 +112,11 @@
}} }}
on:focus={() => {}} on:focus={() => {}}
> >
<div class=" font-medium text-black"> <div class=" font-medium text-black dark:text-gray-100">
{command.command} {command.command}
</div> </div>
<div class=" text-xs text-gray-600"> <div class=" text-xs text-gray-600 dark:text-gray-100">
{command.title} {command.title}
</div> </div>
</button> </button>
...@@ -122,7 +124,7 @@ ...@@ -122,7 +124,7 @@
</div> </div>
<div <div
class=" px-2 pb-1 text-xs text-gray-600 bg-white rounded-br-xl flex items-center space-x-1" class=" px-2 pb-1 text-xs text-gray-600 dark:text-gray-100 bg-white dark:bg-gray-900 rounded-br-xl flex items-center space-x-1"
> >
<div> <div>
<svg <svg
......
...@@ -79,7 +79,7 @@ ...@@ -79,7 +79,7 @@
history.currentId = userMessageId; history.currentId = userMessageId;
await tick(); await tick();
await sendPrompt(userPrompt, userMessageId, undefined, false); await sendPrompt(userPrompt, userMessageId);
}; };
const updateChatMessages = async () => { const updateChatMessages = async () => {
...@@ -202,38 +202,51 @@ ...@@ -202,38 +202,51 @@
}, 100); }, 100);
}; };
const messageDeleteHandler = async (messageId) => { const deleteMessageHandler = async (messageId) => {
const messageToDelete = history.messages[messageId]; const messageToDelete = history.messages[messageId];
const messageParentId = messageToDelete.parentId;
const messageChildrenIds = messageToDelete.childrenIds ?? []; const parentMessageId = messageToDelete.parentId;
const hasSibling = messageChildrenIds.some( const childMessageIds = messageToDelete.childrenIds ?? [];
const hasDescendantMessages = childMessageIds.some(
(childId) => history.messages[childId]?.childrenIds?.length > 0 (childId) => history.messages[childId]?.childrenIds?.length > 0
); );
messageChildrenIds.forEach((childId) => {
const child = history.messages[childId]; history.currentId = parentMessageId;
if (child && child.childrenIds) { await tick();
if (child.childrenIds.length === 0 && !hasSibling) {
// if last prompt/response pair // Remove the message itself from the parent message's children array
history.messages[messageParentId].childrenIds = []; history.messages[parentMessageId].childrenIds = history.messages[
history.currentId = messageParentId; parentMessageId
].childrenIds.filter((id) => id !== messageId);
await tick();
childMessageIds.forEach((childId) => {
const childMessage = history.messages[childId];
if (childMessage && childMessage.childrenIds) {
if (childMessage.childrenIds.length === 0 && !hasDescendantMessages) {
// If there are no other responses/prompts
history.messages[parentMessageId].childrenIds = [];
} else { } else {
child.childrenIds.forEach((grandChildId) => { childMessage.childrenIds.forEach((grandChildId) => {
if (history.messages[grandChildId]) { if (history.messages[grandChildId]) {
history.messages[grandChildId].parentId = messageParentId; history.messages[grandChildId].parentId = parentMessageId;
history.messages[messageParentId].childrenIds.push(grandChildId); history.messages[parentMessageId].childrenIds.push(grandChildId);
} }
}); });
} }
} }
// remove response
history.messages[messageParentId].childrenIds = history.messages[ // Remove child message id from the parent message's children array
messageParentId history.messages[parentMessageId].childrenIds = history.messages[
parentMessageId
].childrenIds.filter((id) => id !== childId); ].childrenIds.filter((id) => id !== childId);
}); });
// remove prompt
history.messages[messageParentId].childrenIds = history.messages[ await tick();
messageParentId
].childrenIds.filter((id) => id !== messageId);
await updateChatById(localStorage.token, chatId, { await updateChatById(localStorage.token, chatId, {
messages: messages, messages: messages,
history: history history: history
...@@ -292,7 +305,7 @@ ...@@ -292,7 +305,7 @@
> >
{#if message.role === 'user'} {#if message.role === 'user'}
<UserMessage <UserMessage
on:delete={() => messageDeleteHandler(message.id)} on:delete={() => deleteMessageHandler(message.id)}
{user} {user}
{readOnly} {readOnly}
{message} {message}
...@@ -308,7 +321,7 @@ ...@@ -308,7 +321,7 @@
copyToClipboard={copyToClipboardWithToast} copyToClipboard={copyToClipboardWithToast}
/> />
{:else if $mobile || (history.messages[message.parentId]?.models?.length ?? 1) === 1} {:else if $mobile || (history.messages[message.parentId]?.models?.length ?? 1) === 1}
{#key message.id} {#key message.id && history.currentId}
<ResponseMessage <ResponseMessage
{message} {message}
siblings={history.messages[message.parentId]?.childrenIds ?? []} siblings={history.messages[message.parentId]?.childrenIds ?? []}
......
<script lang="ts"> <script lang="ts">
import { WEBUI_BASE_URL } from '$lib/constants'; import { WEBUI_BASE_URL } from '$lib/constants';
import { marked } from 'marked';
import { config, user, models as _models } from '$lib/stores'; import { config, user, models as _models } from '$lib/stores';
import { onMount, getContext } from 'svelte'; import { onMount, getContext } from 'svelte';
import { blur, fade } from 'svelte/transition'; import { blur, fade } from 'svelte/transition';
import Suggestions from '../MessageInput/Suggestions.svelte'; import Suggestions from '../MessageInput/Suggestions.svelte';
import { sanitizeResponseContent } from '$lib/utils';
const i18n = getContext('i18n'); const i18n = getContext('i18n');
...@@ -65,8 +68,12 @@ ...@@ -65,8 +68,12 @@
<div in:fade={{ duration: 200, delay: 200 }}> <div in:fade={{ duration: 200, delay: 200 }}>
{#if models[selectedModelIdx]?.info?.meta?.description ?? null} {#if models[selectedModelIdx]?.info?.meta?.description ?? null}
<div class="mt-0.5 text-base font-normal text-gray-500 dark:text-gray-400 line-clamp-3"> <div
{models[selectedModelIdx]?.info?.meta?.description} class="mt-0.5 text-base font-normal text-gray-500 dark:text-gray-400 line-clamp-3 markdown"
>
{@html marked.parse(
sanitizeResponseContent(models[selectedModelIdx]?.info?.meta?.description)
)}
</div> </div>
{#if models[selectedModelIdx]?.info?.meta?.user} {#if models[selectedModelIdx]?.info?.meta?.user}
<div class="mt-0.5 text-sm font-normal text-gray-400 dark:text-gray-500"> <div class="mt-0.5 text-sm font-normal text-gray-400 dark:text-gray-500">
......
...@@ -940,9 +940,8 @@ ...@@ -940,9 +940,8 @@
> >
</button> </button>
</Tooltip> </Tooltip>
{/if}
{#if isLastMessage && !readOnly} {#if isLastMessage}
<Tooltip content={$i18n.t('Continue Response')} placement="bottom"> <Tooltip content={$i18n.t('Continue Response')} placement="bottom">
<button <button
type="button" type="button"
...@@ -1004,6 +1003,7 @@ ...@@ -1004,6 +1003,7 @@
</Tooltip> </Tooltip>
{/if} {/if}
{/if} {/if}
{/if}
</div> </div>
{/if} {/if}
......
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