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({
let pyodide;
try {
pyodide = await loadPyodide({
packageCacheDir: 'static/pyodide' packageCacheDir: 'static/pyodide'
}); });
} catch (err) {
console.error('Failed to load Pyodide:', err);
return;
}
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'); await pyodide.loadPackage('micropip');
const micropip = pyodide.pyimport('micropip'); const micropip = pyodide.pyimport('micropip');
console.log('Downloading Pyodide packages:', packages); console.log('Downloading Pyodide packages:', packages);
await micropip.install(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'); console.log('Pyodide packages downloaded, freezing into lock file');
try {
const lockFile = await micropip.freeze(); const lockFile = await micropip.freeze();
await writeFile('static/pyodide/pyodide-lock.json', lockFile); 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,20 +94,9 @@ ...@@ -84,20 +94,9 @@
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
const doc = {
type: 'doc',
name: file.name,
collection_name: '',
upload_status: false,
error: ''
};
try {
files = [...files, doc];
if (['audio/mpeg', 'audio/wav'].includes(file['type'])) { if (['audio/mpeg', 'audio/wav'].includes(file['type'])) {
const res = await transcribeAudio(localStorage.token, file).catch((error) => { const res = await transcribeAudio(localStorage.token, file).catch((error) => {
toast.error(error); toast.error(error);
...@@ -111,17 +110,59 @@ ...@@ -111,17 +110,59 @@
} }
} }
const res = await uploadDocToVectorDB(localStorage.token, '', file); // Upload the file to the server
const uploadedFile = await uploadFile(localStorage.token, file).catch((error) => {
toast.error(error);
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 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"
......
...@@ -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>
......
This diff is collapsed.
...@@ -203,8 +203,18 @@ __builtins__.input = input`); ...@@ -203,8 +203,18 @@ __builtins__.input = input`);
}; };
}; };
let debounceTimeout;
$: if (code) { $: if (code) {
// Function to perform the code highlighting
const highlightCode = () => {
highlightedCode = hljs.highlightAuto(code, hljs.getLanguage(lang)?.aliases).value || code; 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>
......
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