Commit 4aab4609 authored by Jun Siang Cheah's avatar Jun Siang Cheah
Browse files

Merge remote-tracking branch 'upstream/dev' into feat/oauth

parents 4ff17acc a2ea6b1b
const packages = [ const packages = [
'micropip',
'packaging',
'requests', 'requests',
'beautifulsoup4', 'beautifulsoup4',
'numpy', 'numpy',
...@@ -11,20 +13,64 @@ const packages = [ ...@@ -11,20 +13,64 @@ const packages = [
]; ];
import { loadPyodide } from 'pyodide'; import { loadPyodide } from 'pyodide';
import { writeFile, copyFile, readdir } from 'fs/promises'; import { writeFile, readFile, copyFile, readdir, rmdir } from 'fs/promises';
async function downloadPackages() { async function downloadPackages() {
console.log('Setting up pyodide + micropip'); console.log('Setting up pyodide + micropip');
const pyodide = await loadPyodide({
packageCacheDir: 'static/pyodide' let pyodide;
}); try {
await pyodide.loadPackage('micropip'); pyodide = await loadPyodide({
const micropip = pyodide.pyimport('micropip'); packageCacheDir: 'static/pyodide'
console.log('Downloading Pyodide packages:', packages); });
await micropip.install(packages); } catch (err) {
console.log('Pyodide packages downloaded, freezing into lock file'); console.error('Failed to load Pyodide:', err);
const lockFile = await micropip.freeze(); return;
await writeFile('static/pyodide/pyodide-lock.json', lockFile); }
const packageJson = JSON.parse(await readFile('package.json'));
const pyodideVersion = packageJson.dependencies.pyodide.replace('^', '');
try {
const pyodidePackageJson = JSON.parse(await readFile('static/pyodide/package.json'));
const pyodidePackageVersion = pyodidePackageJson.version.replace('^', '');
if (pyodideVersion !== pyodidePackageVersion) {
console.log('Pyodide version mismatch, removing static/pyodide directory');
await rmdir('static/pyodide', { recursive: true });
}
} catch (e) {
console.log('Pyodide package not found, proceeding with download.');
}
try {
console.log('Loading micropip package');
await pyodide.loadPackage('micropip');
const micropip = pyodide.pyimport('micropip');
console.log('Downloading Pyodide packages:', packages);
try {
for (const pkg of packages) {
console.log(`Installing package: ${pkg}`);
await micropip.install(pkg);
}
} catch (err) {
console.error('Package installation failed:', err);
return;
}
console.log('Pyodide packages downloaded, freezing into lock file');
try {
const lockFile = await micropip.freeze();
await writeFile('static/pyodide/pyodide-lock.json', lockFile);
} catch (err) {
console.error('Failed to write lock file:', err);
}
} catch (err) {
console.error('Failed to load or install micropip:', err);
}
} }
async function copyPyodide() { async function copyPyodide() {
......
...@@ -13,6 +13,12 @@ ...@@ -13,6 +13,12 @@
href="/opensearch.xml" href="/opensearch.xml"
/> />
<script>
function resizeIframe(obj) {
obj.style.height = obj.contentWindow.document.documentElement.scrollHeight + 'px';
}
</script>
<script> <script>
// On page load or when changing themes, best to add inline in `head` to avoid FOUC // On page load or when changing themes, best to add inline in `head` to avoid FOUC
(() => { (() => {
......
...@@ -90,7 +90,8 @@ export const getSessionUser = async (token: string) => { ...@@ -90,7 +90,8 @@ export const getSessionUser = async (token: string) => {
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
Authorization: `Bearer ${token}` Authorization: `Bearer ${token}`
} },
credentials: 'include'
}) })
.then(async (res) => { .then(async (res) => {
if (!res.ok) throw await res.json(); if (!res.ok) throw await res.json();
...@@ -117,6 +118,7 @@ export const userSignIn = async (email: string, password: string) => { ...@@ -117,6 +118,7 @@ export const userSignIn = async (email: string, password: string) => {
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
}, },
credentials: 'include',
body: JSON.stringify({ body: JSON.stringify({
email: email, email: email,
password: password password: password
...@@ -153,6 +155,7 @@ export const userSignUp = async ( ...@@ -153,6 +155,7 @@ export const userSignUp = async (
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
}, },
credentials: 'include',
body: JSON.stringify({ body: JSON.stringify({
name: name, name: name,
email: email, email: email,
......
import { WEBUI_API_BASE_URL } from '$lib/constants';
export const uploadFile = async (token: string, file: File) => {
const data = new FormData();
data.append('file', file);
let error = null;
const res = await fetch(`${WEBUI_API_BASE_URL}/files/`, {
method: 'POST',
headers: {
Accept: 'application/json',
authorization: `Bearer ${token}`
},
body: data
})
.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 getFiles = async (token: string = '') => {
let error = null;
const res = await fetch(`${WEBUI_API_BASE_URL}/files/`, {
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.detail;
console.log(err);
return null;
});
if (error) {
throw error;
}
return res;
};
export const getFileById = async (token: string, id: string) => {
let error = null;
const res = await fetch(`${WEBUI_API_BASE_URL}/files/${id}`, {
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.detail;
console.log(err);
return null;
});
if (error) {
throw error;
}
return res;
};
export const getFileContentById = async (id: string) => {
let error = null;
const res = await fetch(`${WEBUI_API_BASE_URL}/files/${id}/content`, {
method: 'GET',
headers: {
Accept: 'application/json'
},
credentials: 'include'
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return await res.blob();
})
.catch((err) => {
error = err.detail;
console.log(err);
return null;
});
if (error) {
throw error;
}
return res;
};
export const deleteFileById = async (token: string, id: string) => {
let error = null;
const res = await fetch(`${WEBUI_API_BASE_URL}/files/${id}`, {
method: 'DELETE',
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.detail;
console.log(err);
return null;
});
if (error) {
throw error;
}
return res;
};
export const deleteAllFiles = async (token: string) => {
let error = null;
const res = await fetch(`${WEBUI_API_BASE_URL}/files/all`, {
method: 'DELETE',
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.detail;
console.log(err);
return null;
});
if (error) {
throw error;
}
return res;
};
import { WEBUI_API_BASE_URL } from '$lib/constants';
export const createNewFunction = async (token: string, func: object) => {
let error = null;
const res = await fetch(`${WEBUI_API_BASE_URL}/functions/create`, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
authorization: `Bearer ${token}`
},
body: JSON.stringify({
...func
})
})
.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 getFunctions = async (token: string = '') => {
let error = null;
const res = await fetch(`${WEBUI_API_BASE_URL}/functions/`, {
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.detail;
console.log(err);
return null;
});
if (error) {
throw error;
}
return res;
};
export const exportFunctions = async (token: string = '') => {
let error = null;
const res = await fetch(`${WEBUI_API_BASE_URL}/functions/export`, {
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.detail;
console.log(err);
return null;
});
if (error) {
throw error;
}
return res;
};
export const getFunctionById = async (token: string, id: string) => {
let error = null;
const res = await fetch(`${WEBUI_API_BASE_URL}/functions/id/${id}`, {
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.detail;
console.log(err);
return null;
});
if (error) {
throw error;
}
return res;
};
export const updateFunctionById = async (token: string, id: string, func: object) => {
let error = null;
const res = await fetch(`${WEBUI_API_BASE_URL}/functions/id/${id}/update`, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
authorization: `Bearer ${token}`
},
body: JSON.stringify({
...func
})
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.then((json) => {
return json;
})
.catch((err) => {
error = err.detail;
console.log(err);
return null;
});
if (error) {
throw error;
}
return res;
};
export const deleteFunctionById = async (token: string, id: string) => {
let error = null;
const res = await fetch(`${WEBUI_API_BASE_URL}/functions/id/${id}/delete`, {
method: 'DELETE',
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.detail;
console.log(err);
return null;
});
if (error) {
throw error;
}
return res;
};
...@@ -164,6 +164,37 @@ export const updateQuerySettings = async (token: string, settings: QuerySettings ...@@ -164,6 +164,37 @@ export const updateQuerySettings = async (token: string, settings: QuerySettings
return res; return res;
}; };
export const processDocToVectorDB = async (token: string, file_id: string) => {
let error = null;
const res = await fetch(`${RAG_API_BASE_URL}/process/doc`, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
authorization: `Bearer ${token}`
},
body: JSON.stringify({
file_id: file_id
})
})
.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 uploadDocToVectorDB = async (token: string, collection_name: string, file: File) => { export const uploadDocToVectorDB = async (token: string, collection_name: string, file: File) => {
const data = new FormData(); const data = new FormData();
data.append('file', file); data.append('file', file);
......
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;
......
...@@ -153,7 +153,7 @@ ...@@ -153,7 +153,7 @@
type="button" type="button"
on:click={() => { on:click={() => {
tab = ''; tab = '';
}}>Form</button }}>{$i18n.t('Form')}</button
> >
<button <button
...@@ -161,7 +161,7 @@ ...@@ -161,7 +161,7 @@
type="button" type="button"
on:click={() => { on:click={() => {
tab = 'import'; tab = 'import';
}}>CSV Import</button }}>{$i18n.t('CSV Import')}</button
> >
</div> </div>
<div class="px-1"> <div class="px-1">
...@@ -176,9 +176,9 @@ ...@@ -176,9 +176,9 @@
placeholder={$i18n.t('Enter Your Role')} placeholder={$i18n.t('Enter Your Role')}
required required
> >
<option value="pending"> pending </option> <option value="pending"> {$i18n.t('pending')} </option>
<option value="user"> user </option> <option value="user"> {$i18n.t('user')} </option>
<option value="admin"> admin </option> <option value="admin"> {$i18n.t('admin')} </option>
</select> </select>
</div> </div>
</div> </div>
...@@ -262,7 +262,7 @@ ...@@ -262,7 +262,7 @@
class="underline dark:text-gray-200" class="underline dark:text-gray-200"
href="{WEBUI_BASE_URL}/static/user-import.csv" href="{WEBUI_BASE_URL}/static/user-import.csv"
> >
Click here to download user import template file. {$i18n.t('Click here to download user import template file.')}
</a> </a>
</div> </div>
</div> </div>
......
<script lang="ts"> <script lang="ts">
import { getDocs } from '$lib/apis/documents'; import { getDocs } from '$lib/apis/documents';
import { deleteAllFiles, deleteFileById } from '$lib/apis/files';
import { import {
getQuerySettings, getQuerySettings,
scanDocs, scanDocs,
...@@ -217,8 +218,8 @@ ...@@ -217,8 +218,8 @@
<ResetUploadDirConfirmDialog <ResetUploadDirConfirmDialog
bind:show={showResetUploadDirConfirm} bind:show={showResetUploadDirConfirm}
on:confirm={() => { on:confirm={async () => {
const res = resetUploadDir(localStorage.token).catch((error) => { const res = await deleteAllFiles(localStorage.token).catch((error) => {
toast.error(error); toast.error(error);
return null; return null;
}); });
......
...@@ -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">
......
...@@ -31,6 +31,7 @@ ...@@ -31,6 +31,7 @@
convertMessagesToHistory, convertMessagesToHistory,
copyToClipboard, copyToClipboard,
extractSentencesForAudio, extractSentencesForAudio,
getUserPosition,
promptTemplate, promptTemplate,
splitStream splitStream
} from '$lib/utils'; } from '$lib/utils';
...@@ -50,7 +51,7 @@ ...@@ -50,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';
...@@ -272,11 +273,14 @@ ...@@ -272,11 +273,14 @@
id: m.id, id: m.id,
role: m.role, role: m.role,
content: m.content, content: m.content,
info: m.info ? m.info : undefined,
timestamp: m.timestamp timestamp: m.timestamp
})), })),
chat_id: $chatId chat_id: $chatId
}).catch((error) => { }).catch((error) => {
console.error(error); toast.error(error);
messages.at(-1).error = { content: error };
return null; return null;
}); });
...@@ -321,9 +325,16 @@ ...@@ -321,9 +325,16 @@
} else if (messages.length != 0 && messages.at(-1).done != true) { } else if (messages.length != 0 && messages.at(-1).done != true) {
// Response not done // Response not done
console.log('wait'); console.log('wait');
} else if (messages.length != 0 && messages.at(-1).error) {
// Error in response
toast.error(
$i18n.t(
`Oops! There was an error in the previous response. Please try again or contact admin.`
)
);
} else if ( } else if (
files.length > 0 && files.length > 0 &&
files.filter((file) => file.upload_status === false).length > 0 files.filter((file) => file.type !== 'image' && file.status !== 'processed').length > 0
) { ) {
// Upload not done // Upload not done
toast.error( toast.error(
...@@ -533,7 +544,13 @@ ...@@ -533,7 +544,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')}`
: '' : ''
...@@ -578,23 +595,18 @@ ...@@ -578,23 +595,18 @@
} }
}); });
let docs = []; let files = [];
if (model?.info?.meta?.knowledge ?? false) { if (model?.info?.meta?.knowledge ?? false) {
docs = model.info.meta.knowledge; files = model.info.meta.knowledge;
} }
const lastUserMessage = messages.filter((message) => message.role === 'user').at(-1);
docs = [ files = [
...docs, ...files,
...messages ...(lastUserMessage?.files?.filter((item) =>
.filter((message) => message?.files ?? null) ['doc', 'file', 'collection', 'web_search_results'].includes(item.type)
.map((message) => ) ?? [])
message.files.filter((item) =>
['doc', 'collection', 'web_search_results'].includes(item.type)
)
)
.flat(1)
].filter( ].filter(
// Remove duplicates
(item, index, array) => (item, index, array) =>
array.findIndex((i) => JSON.stringify(i) === JSON.stringify(item)) === index array.findIndex((i) => JSON.stringify(i) === JSON.stringify(item)) === index
); );
...@@ -626,8 +638,8 @@ ...@@ -626,8 +638,8 @@
format: $settings.requestFormat ?? undefined, format: $settings.requestFormat ?? undefined,
keep_alive: $settings.keepAlive ?? undefined, keep_alive: $settings.keepAlive ?? undefined,
tool_ids: selectedToolIds.length > 0 ? selectedToolIds : undefined, tool_ids: selectedToolIds.length > 0 ? selectedToolIds : undefined,
docs: docs.length > 0 ? docs : undefined, files: files.length > 0 ? files : undefined,
citations: docs.length > 0, citations: files.length > 0 ? true : undefined,
chat_id: $chatId chat_id: $chatId
}); });
...@@ -823,23 +835,18 @@ ...@@ -823,23 +835,18 @@
let _response = null; let _response = null;
const responseMessage = history.messages[responseMessageId]; const responseMessage = history.messages[responseMessageId];
let docs = []; let files = [];
if (model?.info?.meta?.knowledge ?? false) { if (model?.info?.meta?.knowledge ?? false) {
docs = model.info.meta.knowledge; files = model.info.meta.knowledge;
} }
const lastUserMessage = messages.filter((message) => message.role === 'user').at(-1);
docs = [ files = [
...docs, ...files,
...messages ...(lastUserMessage?.files?.filter((item) =>
.filter((message) => message?.files ?? null) ['doc', 'file', 'collection', 'web_search_results'].includes(item.type)
.map((message) => ) ?? [])
message.files.filter((item) =>
['doc', 'collection', 'web_search_results'].includes(item.type)
)
)
.flat(1)
].filter( ].filter(
// Remove duplicates
(item, index, array) => (item, index, array) =>
array.findIndex((i) => JSON.stringify(i) === JSON.stringify(item)) === index array.findIndex((i) => JSON.stringify(i) === JSON.stringify(item)) === index
); );
...@@ -871,7 +878,13 @@ ...@@ -871,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')}`
: '' : ''
...@@ -923,11 +936,12 @@ ...@@ -923,11 +936,12 @@
frequency_penalty: $settings?.params?.frequency_penalty ?? undefined, frequency_penalty: $settings?.params?.frequency_penalty ?? undefined,
max_tokens: $settings?.params?.max_tokens ?? undefined, max_tokens: $settings?.params?.max_tokens ?? undefined,
tool_ids: selectedToolIds.length > 0 ? selectedToolIds : undefined, tool_ids: selectedToolIds.length > 0 ? selectedToolIds : undefined,
docs: docs.length > 0 ? docs : undefined, files: files.length > 0 ? files : undefined,
citations: docs.length > 0, citations: files.length > 0 ? true : undefined,
chat_id: $chatId chat_id: $chatId
}, },
`${OPENAI_API_BASE_URL}` `${WEBUI_BASE_URL}/api`
); );
// Wait until history/message have been updated // Wait until history/message have been updated
...@@ -1309,6 +1323,19 @@ ...@@ -1309,6 +1323,19 @@
? 'md:max-w-[calc(100%-260px)]' ? 'md:max-w-[calc(100%-260px)]'
: ''} w-full max-w-full flex flex-col" : ''} w-full max-w-full flex flex-col"
> >
{#if $settings?.backgroundImageUrl ?? null}
<div
class="absolute {$showSidebar
? 'md:max-w-[calc(100%-260px)] md:translate-x-[260px]'
: ''} top-0 left-0 w-full h-full bg-cover bg-center bg-no-repeat"
style="background-image: url({$settings.backgroundImageUrl}) "
/>
<div
class="absolute top-0 left-0 w-full h-full bg-gradient-to-t from-white to-white/85 dark:from-gray-900 dark:to-[#171717]/90 z-0"
/>
{/if}
<Navbar <Navbar
{title} {title}
bind:selectedModels bind:selectedModels
...@@ -1320,7 +1347,9 @@ ...@@ -1320,7 +1347,9 @@
{#if $banners.length > 0 && messages.length === 0 && !$chatId && selectedModels.length <= 1} {#if $banners.length > 0 && messages.length === 0 && !$chatId && selectedModels.length <= 1}
<div <div
class="absolute top-[4.25rem] w-full {$showSidebar ? 'md:max-w-[calc(100%-260px)]' : ''}" class="absolute top-[4.25rem] w-full {$showSidebar
? 'md:max-w-[calc(100%-260px)]'
: ''} z-20"
> >
<div class=" flex flex-col gap-1 w-full"> <div class=" flex flex-col gap-1 w-full">
{#each $banners.filter( (b) => (b.dismissible ? !JSON.parse(localStorage.getItem('dismissedBannerIds') ?? '[]').includes(b.id) : true) ) as banner} {#each $banners.filter( (b) => (b.dismissible ? !JSON.parse(localStorage.getItem('dismissedBannerIds') ?? '[]').includes(b.id) : true) ) as banner}
...@@ -1345,9 +1374,9 @@ ...@@ -1345,9 +1374,9 @@
</div> </div>
{/if} {/if}
<div class="flex flex-col flex-auto"> <div class="flex flex-col flex-auto z-10">
<div <div
class=" pb-2.5 flex flex-col justify-between w-full flex-auto overflow-auto h-0 max-w-full" class=" pb-2.5 flex flex-col justify-between w-full flex-auto overflow-auto h-0 max-w-full z-10"
id="messages-container" id="messages-container"
bind:this={messagesContainerElement} bind:this={messagesContainerElement}
on:scroll={(e) => { on:scroll={(e) => {
...@@ -1386,6 +1415,7 @@ ...@@ -1386,6 +1415,7 @@
} }
return a; return a;
}, [])} }, [])}
transparentBackground={$settings?.backgroundImageUrl ?? false}
{selectedModels} {selectedModels}
{messages} {messages}
{submitPrompt} {submitPrompt}
......
...@@ -15,11 +15,19 @@ ...@@ -15,11 +15,19 @@
import { blobToFile, calculateSHA256, findWordIndices } from '$lib/utils'; import { blobToFile, calculateSHA256, findWordIndices } from '$lib/utils';
import { import {
processDocToVectorDB,
uploadDocToVectorDB, uploadDocToVectorDB,
uploadWebToVectorDB, uploadWebToVectorDB,
uploadYoutubeTranscriptionToVectorDB uploadYoutubeTranscriptionToVectorDB
} from '$lib/apis/rag'; } from '$lib/apis/rag';
import { SUPPORTED_FILE_TYPE, SUPPORTED_FILE_EXTENSIONS, WEBUI_BASE_URL } from '$lib/constants';
import { uploadFile } from '$lib/apis/files';
import {
SUPPORTED_FILE_TYPE,
SUPPORTED_FILE_EXTENSIONS,
WEBUI_BASE_URL,
WEBUI_API_BASE_URL
} from '$lib/constants';
import Prompts from './MessageInput/PromptCommands.svelte'; import Prompts from './MessageInput/PromptCommands.svelte';
import Suggestions from './MessageInput/Suggestions.svelte'; import Suggestions from './MessageInput/Suggestions.svelte';
...@@ -35,6 +43,8 @@ ...@@ -35,6 +43,8 @@
const i18n = getContext('i18n'); const i18n = getContext('i18n');
export let transparentBackground = false;
export let submitPrompt: Function; export let submitPrompt: Function;
export let stopResponse: Function; export let stopResponse: Function;
...@@ -84,44 +94,75 @@ ...@@ -84,44 +94,75 @@
element.scrollTop = element.scrollHeight; element.scrollTop = element.scrollHeight;
}; };
const uploadDoc = async (file) => { const uploadFileHandler = async (file) => {
console.log(file); console.log(file);
// Check if the file is an audio file and transcribe/convert it to text file
if (['audio/mpeg', 'audio/wav'].includes(file['type'])) {
const res = await transcribeAudio(localStorage.token, file).catch((error) => {
toast.error(error);
return null;
});
const doc = { if (res) {
type: 'doc', console.log(res);
name: file.name, const blob = new Blob([res.text], { type: 'text/plain' });
collection_name: '', file = blobToFile(blob, `${file.name}.txt`);
upload_status: false, }
error: '' }
};
try {
files = [...files, doc];
if (['audio/mpeg', 'audio/wav'].includes(file['type'])) {
const res = await transcribeAudio(localStorage.token, file).catch((error) => {
toast.error(error);
return null;
});
if (res) { // Upload the file to the server
console.log(res); const uploadedFile = await uploadFile(localStorage.token, file).catch((error) => {
const blob = new Blob([res.text], { type: 'text/plain' }); toast.error(error);
file = blobToFile(blob, `${file.name}.txt`); return null;
} });
if (uploadedFile) {
const fileItem = {
type: 'file',
file: uploadedFile,
id: uploadedFile.id,
url: `${WEBUI_API_BASE_URL}/files/${uploadedFile.id}`,
name: file.name,
collection_name: '',
status: 'uploaded',
error: ''
};
files = [...files, fileItem];
// TODO: Check if tools & functions have files support to skip this step to delegate file processing
// Default Upload to VectorDB
if (
SUPPORTED_FILE_TYPE.includes(file['type']) ||
SUPPORTED_FILE_EXTENSIONS.includes(file.name.split('.').at(-1))
) {
processFileItem(fileItem);
} else {
toast.error(
$i18n.t(`Unknown file type '{{file_type}}'. Proceeding with the file upload anyway.`, {
file_type: file['type']
})
);
processFileItem(fileItem);
} }
}
};
const res = await uploadDocToVectorDB(localStorage.token, '', file); const processFileItem = async (fileItem) => {
try {
const res = await processDocToVectorDB(localStorage.token, fileItem.id);
if (res) { if (res) {
doc.upload_status = true; fileItem.status = 'processed';
doc.collection_name = res.collection_name; fileItem.collection_name = res.collection_name;
files = files; files = files;
} }
} catch (e) { } catch (e) {
// Remove the failed doc from the files array // Remove the failed doc from the files array
files = files.filter((f) => f.name !== file.name); // files = files.filter((f) => f.id !== fileItem.id);
toast.error(e); toast.error(e);
fileItem.status = 'processed';
files = files;
} }
}; };
...@@ -132,7 +173,7 @@ ...@@ -132,7 +173,7 @@
type: 'doc', type: 'doc',
name: url, name: url,
collection_name: '', collection_name: '',
upload_status: false, status: false,
url: url, url: url,
error: '' error: ''
}; };
...@@ -142,7 +183,7 @@ ...@@ -142,7 +183,7 @@
const res = await uploadWebToVectorDB(localStorage.token, '', url); const res = await uploadWebToVectorDB(localStorage.token, '', url);
if (res) { if (res) {
doc.upload_status = true; doc.status = 'processed';
doc.collection_name = res.collection_name; doc.collection_name = res.collection_name;
files = files; files = files;
} }
...@@ -160,7 +201,7 @@ ...@@ -160,7 +201,7 @@
type: 'doc', type: 'doc',
name: url, name: url,
collection_name: '', collection_name: '',
upload_status: false, status: false,
url: url, url: url,
error: '' error: ''
}; };
...@@ -170,7 +211,7 @@ ...@@ -170,7 +211,7 @@
const res = await uploadYoutubeTranscriptionToVectorDB(localStorage.token, url); const res = await uploadYoutubeTranscriptionToVectorDB(localStorage.token, url);
if (res) { if (res) {
doc.upload_status = true; doc.status = 'processed';
doc.collection_name = res.collection_name; doc.collection_name = res.collection_name;
files = files; files = files;
} }
...@@ -228,19 +269,8 @@ ...@@ -228,19 +269,8 @@
]; ];
}; };
reader.readAsDataURL(file); reader.readAsDataURL(file);
} else if (
SUPPORTED_FILE_TYPE.includes(file['type']) ||
SUPPORTED_FILE_EXTENSIONS.includes(file.name.split('.').at(-1))
) {
uploadDoc(file);
} else { } else {
toast.error( uploadFileHandler(file);
$i18n.t(
`Unknown File Type '{{file_type}}', but accepting and treating as plain text`,
{ file_type: file['type'] }
)
);
uploadDoc(file);
} }
}); });
} else { } else {
...@@ -336,9 +366,9 @@ ...@@ -336,9 +366,9 @@
files = [ files = [
...files, ...files,
{ {
type: e?.detail?.type ?? 'doc', type: e?.detail?.type ?? 'file',
...e.detail, ...e.detail,
upload_status: true status: 'processed'
} }
]; ];
}} }}
...@@ -391,7 +421,7 @@ ...@@ -391,7 +421,7 @@
</div> </div>
</div> </div>
<div class="bg-white dark:bg-gray-900"> <div class="{transparentBackground ? 'bg-transparent' : 'bg-white dark:bg-gray-900'} ">
<div class="max-w-6xl px-2.5 md:px-6 mx-auto inset-x-0"> <div class="max-w-6xl px-2.5 md:px-6 mx-auto inset-x-0">
<div class=" pb-2"> <div class=" pb-2">
<input <input
...@@ -407,8 +437,6 @@ ...@@ -407,8 +437,6 @@
if (['image/gif', 'image/webp', 'image/jpeg', 'image/png'].includes(file['type'])) { if (['image/gif', 'image/webp', 'image/jpeg', 'image/png'].includes(file['type'])) {
if (visionCapableModels.length === 0) { if (visionCapableModels.length === 0) {
toast.error($i18n.t('Selected model(s) do not support image inputs')); toast.error($i18n.t('Selected model(s) do not support image inputs'));
inputFiles = null;
filesInputElement.value = '';
return; return;
} }
let reader = new FileReader(); let reader = new FileReader();
...@@ -420,30 +448,17 @@ ...@@ -420,30 +448,17 @@
url: `${event.target.result}` url: `${event.target.result}`
} }
]; ];
inputFiles = null;
filesInputElement.value = '';
}; };
reader.readAsDataURL(file); reader.readAsDataURL(file);
} else if (
SUPPORTED_FILE_TYPE.includes(file['type']) ||
SUPPORTED_FILE_EXTENSIONS.includes(file.name.split('.').at(-1))
) {
uploadDoc(file);
filesInputElement.value = '';
} else { } else {
toast.error( uploadFileHandler(file);
$i18n.t(
`Unknown File Type '{{file_type}}', but accepting and treating as plain text`,
{ file_type: file['type'] }
)
);
uploadDoc(file);
filesInputElement.value = '';
} }
}); });
} else { } else {
toast.error($i18n.t(`File not found.`)); toast.error($i18n.t(`File not found.`));
} }
filesInputElement.value = '';
}} }}
/> />
...@@ -517,12 +532,12 @@ ...@@ -517,12 +532,12 @@
</Tooltip> </Tooltip>
{/if} {/if}
</div> </div>
{:else if file.type === 'doc'} {:else if ['doc', 'file'].includes(file.type)}
<div <div
class="h-16 w-[15rem] flex items-center space-x-3 px-2.5 dark:bg-gray-600 rounded-xl border border-gray-200 dark:border-none" class="h-16 w-[15rem] flex items-center space-x-3 px-2.5 dark:bg-gray-600 rounded-xl border border-gray-200 dark:border-none"
> >
<div class="p-2.5 bg-red-400 text-white rounded-lg"> <div class="p-2.5 bg-red-400 text-white rounded-lg">
{#if file.upload_status} {#if file.status === 'processed'}
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24" viewBox="0 0 24 24"
......
<script lang="ts"> <script lang="ts">
import { config, settings, showCallOverlay } from '$lib/stores'; import { config, models, settings, showCallOverlay } from '$lib/stores';
import { onMount, tick, getContext } from 'svelte'; import { onMount, tick, getContext } from 'svelte';
import { import {
...@@ -28,9 +28,12 @@ ...@@ -28,9 +28,12 @@
export let chatId; export let chatId;
export let modelId; export let modelId;
let model = null;
let loading = false; let loading = false;
let confirmed = false; let confirmed = false;
let interrupted = false; let interrupted = false;
let assistantSpeaking = false;
let emoji = null; let emoji = null;
...@@ -268,6 +271,15 @@ ...@@ -268,6 +271,15 @@
return; return;
} }
if (assistantSpeaking) {
// Mute the audio if the assistant is speaking
analyser.maxDecibels = 0;
analyser.minDecibels = -1;
} else {
analyser.minDecibels = MIN_DECIBELS;
analyser.maxDecibels = -30;
}
analyser.getByteTimeDomainData(timeDomainData); analyser.getByteTimeDomainData(timeDomainData);
analyser.getByteFrequencyData(domainData); analyser.getByteFrequencyData(domainData);
...@@ -379,6 +391,7 @@ ...@@ -379,6 +391,7 @@
}; };
const stopAllAudio = async () => { const stopAllAudio = async () => {
assistantSpeaking = false;
interrupted = true; interrupted = true;
if (chatStreaming) { if (chatStreaming) {
...@@ -485,6 +498,7 @@ ...@@ -485,6 +498,7 @@
} }
} else if (finishedMessages[id] && messages[id] && messages[id].length === 0) { } else if (finishedMessages[id] && messages[id] && messages[id].length === 0) {
// If the message is finished and there are no more messages to process, break the loop // If the message is finished and there are no more messages to process, break the loop
assistantSpeaking = false;
break; break;
} else { } else {
// No messages to process, sleep for a bit // No messages to process, sleep for a bit
...@@ -495,6 +509,8 @@ ...@@ -495,6 +509,8 @@
}; };
onMount(async () => { onMount(async () => {
model = $models.find((m) => m.id === modelId);
startRecording(); startRecording();
const chatStartHandler = async (e) => { const chatStartHandler = async (e) => {
...@@ -511,6 +527,7 @@ ...@@ -511,6 +527,7 @@
} }
audioAbortController = new AbortController(); audioAbortController = new AbortController();
assistantSpeaking = true;
// Start monitoring and playing audio for the message ID // Start monitoring and playing audio for the message ID
monitorAndPlayAudio(id, audioAbortController.signal); monitorAndPlayAudio(id, audioAbortController.signal);
} }
...@@ -545,9 +562,9 @@ ...@@ -545,9 +562,9 @@
const chatFinishHandler = async (e) => { const chatFinishHandler = async (e) => {
const { id, content } = e.detail; const { id, content } = e.detail;
// "content" here is the entire message from the assistant // "content" here is the entire message from the assistant
finishedMessages[id] = true;
chatStreaming = false; chatStreaming = false;
finishedMessages[id] = true;
}; };
eventTarget.addEventListener('chat:start', chatStartHandler); eventTarget.addEventListener('chat:start', chatStartHandler);
...@@ -577,7 +594,15 @@ ...@@ -577,7 +594,15 @@
> >
<div class="max-w-lg w-full h-screen max-h-[100dvh] flex flex-col justify-between p-3 md:p-6"> <div class="max-w-lg w-full h-screen max-h-[100dvh] flex flex-col justify-between p-3 md:p-6">
{#if camera} {#if camera}
<div class="flex justify-center items-center w-full h-20 min-h-20"> <button
type="button"
class="flex justify-center items-center w-full h-20 min-h-20"
on:click={() => {
if (assistantSpeaking) {
stopAllAudio();
}
}}
>
{#if emoji} {#if emoji}
<div <div
class=" transition-all rounded-full" class=" transition-all rounded-full"
...@@ -591,7 +616,7 @@ ...@@ -591,7 +616,7 @@
> >
{emoji} {emoji}
</div> </div>
{:else if loading} {:else if loading || assistantSpeaking}
<svg <svg
class="size-12 text-gray-900 dark:text-gray-400" class="size-12 text-gray-900 dark:text-gray-400"
viewBox="0 0 24 24" viewBox="0 0 24 24"
...@@ -636,76 +661,97 @@ ...@@ -636,76 +661,97 @@
? ' size-16' ? ' size-16'
: rmsLevel * 100 > 1 : rmsLevel * 100 > 1
? 'size-14' ? 'size-14'
: 'size-12'} transition-all bg-black dark:bg-white rounded-full" : 'size-12'} transition-all rounded-full {(model?.info?.meta
?.profile_image_url ?? '/favicon.png') !== '/favicon.png'
? ' bg-cover bg-center bg-no-repeat'
: 'bg-black dark:bg-white'} bg-black dark:bg-white"
style={(model?.info?.meta?.profile_image_url ?? '/favicon.png') !== '/favicon.png'
? `background-image: url('${model?.info?.meta?.profile_image_url}');`
: ''}
/> />
{/if} {/if}
<!-- navbar --> <!-- navbar -->
</div> </button>
{/if} {/if}
<div class="flex justify-center items-center flex-1 h-full w-full max-h-full"> <div class="flex justify-center items-center flex-1 h-full w-full max-h-full">
{#if !camera} {#if !camera}
{#if emoji} <button
<div type="button"
class=" transition-all rounded-full" on:click={() => {
style="font-size:{rmsLevel * 100 > 4 if (assistantSpeaking) {
? '13' stopAllAudio();
: rmsLevel * 100 > 2 }
? '12' }}
: rmsLevel * 100 > 1 >
? '11.5' {#if emoji}
: '11'}rem;width:100%;text-align:center;" <div
> class=" transition-all rounded-full"
{emoji} style="font-size:{rmsLevel * 100 > 4
</div> ? '13'
{:else if loading} : rmsLevel * 100 > 2
<svg ? '12'
class="size-44 text-gray-900 dark:text-gray-400" : rmsLevel * 100 > 1
viewBox="0 0 24 24" ? '11.5'
fill="currentColor" : '11'}rem;width:100%;text-align:center;"
xmlns="http://www.w3.org/2000/svg" >
><style> {emoji}
.spinner_qM83 { </div>
animation: spinner_8HQG 1.05s infinite; {:else if loading || assistantSpeaking}
} <svg
.spinner_oXPr { class="size-44 text-gray-900 dark:text-gray-400"
animation-delay: 0.1s; viewBox="0 0 24 24"
} fill="currentColor"
.spinner_ZTLf { xmlns="http://www.w3.org/2000/svg"
animation-delay: 0.2s; ><style>
} .spinner_qM83 {
@keyframes spinner_8HQG { animation: spinner_8HQG 1.05s infinite;
0%,
57.14% {
animation-timing-function: cubic-bezier(0.33, 0.66, 0.66, 1);
transform: translate(0);
} }
28.57% { .spinner_oXPr {
animation-timing-function: cubic-bezier(0.33, 0, 0.66, 0.33); animation-delay: 0.1s;
transform: translateY(-6px);
} }
100% { .spinner_ZTLf {
transform: translate(0); animation-delay: 0.2s;
} }
} @keyframes spinner_8HQG {
</style><circle class="spinner_qM83" cx="4" cy="12" r="3" /><circle 0%,
class="spinner_qM83 spinner_oXPr" 57.14% {
cx="12" animation-timing-function: cubic-bezier(0.33, 0.66, 0.66, 1);
cy="12" transform: translate(0);
r="3" }
/><circle class="spinner_qM83 spinner_ZTLf" cx="20" cy="12" r="3" /></svg 28.57% {
> animation-timing-function: cubic-bezier(0.33, 0, 0.66, 0.33);
{:else} transform: translateY(-6px);
<div }
class=" {rmsLevel * 100 > 4 100% {
? ' size-52' transform: translate(0);
: rmsLevel * 100 > 2 }
? 'size-48' }
: rmsLevel * 100 > 1 </style><circle class="spinner_qM83" cx="4" cy="12" r="3" /><circle
? 'size-[11.5rem]' class="spinner_qM83 spinner_oXPr"
: 'size-44'} transition-all bg-black dark:bg-white rounded-full" cx="12"
/> cy="12"
{/if} r="3"
/><circle class="spinner_qM83 spinner_ZTLf" cx="20" cy="12" r="3" /></svg
>
{:else}
<div
class=" {rmsLevel * 100 > 4
? ' size-52'
: rmsLevel * 100 > 2
? 'size-48'
: rmsLevel * 100 > 1
? 'size-[11.5rem]'
: 'size-44'} transition-all rounded-full {(model?.info?.meta
?.profile_image_url ?? '/favicon.png') !== '/favicon.png'
? ' bg-cover bg-center bg-no-repeat'
: 'bg-black dark:bg-white'} "
style={(model?.info?.meta?.profile_image_url ?? '/favicon.png') !== '/favicon.png'
? `background-image: url('${model?.info?.meta?.profile_image_url}');`
: ''}
/>
{/if}
</button>
{:else} {:else}
<div <div
class="relative flex video-container w-full max-h-full pt-2 pb-4 md:py-6 px-2 h-full" class="relative flex video-container w-full max-h-full pt-2 pb-4 md:py-6 px-2 h-full"
...@@ -805,10 +851,19 @@ ...@@ -805,10 +851,19 @@
</div> </div>
<div> <div>
<button type="button"> <button
type="button"
on:click={() => {
if (assistantSpeaking) {
stopAllAudio();
}
}}
>
<div class=" line-clamp-1 text-sm font-medium"> <div class=" line-clamp-1 text-sm font-medium">
{#if loading} {#if loading}
{$i18n.t('Thinking...')} {$i18n.t('Thinking...')}
{:else if assistantSpeaking}
{$i18n.t('Tap to interrupt')}
{:else} {:else}
{$i18n.t('Listening...')} {$i18n.t('Listening...')}
{/if} {/if}
......
...@@ -101,20 +101,20 @@ ...@@ -101,20 +101,20 @@
</script> </script>
{#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 z-10">
<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 <div
class="max-h-60 flex flex-col w-full rounded-r-xl bg-white dark:bg-gray-850 dark:text-gray-100" 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"> <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 dark:bg-gray-600 dark:text-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={() => {
......
...@@ -133,18 +133,20 @@ ...@@ -133,18 +133,20 @@
{#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 z-10">
<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 dark:bg-gray-850"> <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 dark:bg-gray-600 selected-command-option-button' ? ' bg-gray-50 dark:bg-gray-850 selected-command-option-button'
: ''}" : ''}"
type="button" type="button"
on:click={() => { on:click={() => {
......
...@@ -88,18 +88,20 @@ ...@@ -88,18 +88,20 @@
</script> </script>
{#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 z-10">
<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 dark:bg-gray-850"> <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 dark:bg-gray-600 selected-command-option-button' ? ' bg-gray-50 dark:bg-gray-850 selected-command-option-button'
: ''}" : ''}"
type="button" type="button"
on:click={() => { on:click={() => {
...@@ -122,7 +124,7 @@ ...@@ -122,7 +124,7 @@
</div> </div>
<div <div
class=" px-2 pb-1 text-xs text-gray-600 dark:text-gray-100 bg-white dark:bg-gray-850 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
......
...@@ -62,7 +62,7 @@ ...@@ -62,7 +62,7 @@
<div class="text-sm text-gray-600 font-normal line-clamp-2">{prompt.title[1]}</div> <div class="text-sm text-gray-600 font-normal line-clamp-2">{prompt.title[1]}</div>
{:else} {:else}
<div <div
class=" self-center text-sm font-medium dark:text-gray-300 dark:group-hover:text-gray-100 transition line-clamp-2" class=" text-sm font-medium dark:text-gray-300 dark:group-hover:text-gray-100 transition line-clamp-2"
> >
{prompt.content} {prompt.content}
</div> </div>
......
...@@ -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 ?? []}
...@@ -372,7 +385,7 @@ ...@@ -372,7 +385,7 @@
{/each} {/each}
{#if bottomPadding} {#if bottomPadding}
<div class=" pb-20" /> <div class=" pb-6" />
{/if} {/if}
{/key} {/key}
</div> </div>
......
...@@ -203,8 +203,18 @@ __builtins__.input = input`); ...@@ -203,8 +203,18 @@ __builtins__.input = input`);
}; };
}; };
let debounceTimeout;
$: if (code) { $: if (code) {
highlightedCode = hljs.highlightAuto(code, hljs.getLanguage(lang)?.aliases).value || code; // Function to perform the code highlighting
const highlightCode = () => {
highlightedCode = hljs.highlightAuto(code, hljs.getLanguage(lang)?.aliases).value || code;
};
// Clear the previous timeout if it exists
clearTimeout(debounceTimeout);
// Set a new timeout to debounce the code highlighting
debounceTimeout = setTimeout(highlightCode, 10);
} }
</script> </script>
......
...@@ -9,6 +9,7 @@ ...@@ -9,6 +9,7 @@
import Suggestions from '../MessageInput/Suggestions.svelte'; import Suggestions from '../MessageInput/Suggestions.svelte';
import { sanitizeResponseContent } from '$lib/utils'; import { sanitizeResponseContent } from '$lib/utils';
import Tooltip from '$lib/components/common/Tooltip.svelte';
const i18n = getContext('i18n'); const i18n = getContext('i18n');
...@@ -41,14 +42,23 @@ ...@@ -41,14 +42,23 @@
selectedModelIdx = modelIdx; selectedModelIdx = modelIdx;
}} }}
> >
<img <Tooltip
crossorigin="anonymous" content={marked.parse(
src={model?.info?.meta?.profile_image_url ?? sanitizeResponseContent(models[selectedModelIdx]?.info?.meta?.description ?? '')
($i18n.language === 'dg-DG' ? `/doge.png` : `${WEBUI_BASE_URL}/static/favicon.png`)} )}
class=" size-[2.7rem] rounded-full border-[1px] border-gray-200 dark:border-none" placement="right"
alt="logo" >
draggable="false" <img
/> crossorigin="anonymous"
src={model?.info?.meta?.profile_image_url ??
($i18n.language === 'dg-DG'
? `/doge.png`
: `${WEBUI_BASE_URL}/static/favicon.png`)}
class=" size-[2.7rem] rounded-full border-[1px] border-gray-200 dark:border-none"
alt="logo"
draggable="false"
/>
</Tooltip>
</button> </button>
{/each} {/each}
</div> </div>
......
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