Unverified Commit 1eebb85f authored by Timothy Jaeryang Baek's avatar Timothy Jaeryang Baek Committed by GitHub
Browse files

Merge pull request #3323 from open-webui/dev

0.3.6
parents 9e4dd4b8 b224ba00
......@@ -153,7 +153,7 @@
type="button"
on:click={() => {
tab = '';
}}>Form</button
}}>{$i18n.t('Form')}</button
>
<button
......@@ -161,7 +161,7 @@
type="button"
on:click={() => {
tab = 'import';
}}>CSV Import</button
}}>{$i18n.t('CSV Import')}</button
>
</div>
<div class="px-1">
......@@ -176,9 +176,9 @@
placeholder={$i18n.t('Enter Your Role')}
required
>
<option value="pending"> pending </option>
<option value="user"> user </option>
<option value="admin"> admin </option>
<option value="pending"> {$i18n.t('pending')} </option>
<option value="user"> {$i18n.t('user')} </option>
<option value="admin"> {$i18n.t('admin')} </option>
</select>
</div>
</div>
......@@ -262,7 +262,7 @@
class="underline dark:text-gray-200"
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>
</div>
</div>
......
......@@ -5,6 +5,7 @@
import { toast } from 'svelte-sonner';
import Switch from '$lib/components/common/Switch.svelte';
import { getBackendConfig } from '$lib/apis';
import SensitiveInput from '$lib/components/common/SensitiveInput.svelte';
const dispatch = createEventDispatcher();
const i18n = getContext('i18n');
......@@ -72,7 +73,7 @@
});
if (res) {
toast.success('Audio settings updated successfully');
toast.success($i18n.t('Audio settings updated successfully'));
config.set(await getBackendConfig());
}
......@@ -137,18 +138,13 @@
<div>
<div class="mt-1 flex gap-2 mb-1">
<input
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
class="flex-1 w-full rounded-l-lg py-2 pl-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
placeholder={$i18n.t('API Base URL')}
bind:value={STT_OPENAI_API_BASE_URL}
required
/>
<input
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
placeholder={$i18n.t('API Key')}
bind:value={STT_OPENAI_API_KEY}
required
/>
<SensitiveInput placeholder={$i18n.t('API Key')} bind:value={STT_OPENAI_API_KEY} />
</div>
</div>
......@@ -198,7 +194,7 @@
}}
>
<option value="">{$i18n.t('Web API')}</option>
<option value="openai">{$i18n.t('Open AI')}</option>
<option value="openai">{$i18n.t('OpenAI')}</option>
</select>
</div>
</div>
......@@ -207,18 +203,13 @@
<div>
<div class="mt-1 flex gap-2 mb-1">
<input
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
class="flex-1 w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
placeholder={$i18n.t('API Base URL')}
bind:value={TTS_OPENAI_API_BASE_URL}
required
/>
<input
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
placeholder={$i18n.t('API Key')}
bind:value={TTS_OPENAI_API_KEY}
required
/>
<SensitiveInput placeholder={$i18n.t('API Key')} bind:value={TTS_OPENAI_API_KEY} />
</div>
</div>
{/if}
......
<script lang="ts">
import { models, user } from '$lib/stores';
import { createEventDispatcher, onMount, getContext, tick } from 'svelte';
const dispatch = createEventDispatcher();
import {
......@@ -24,6 +25,7 @@
import Spinner from '$lib/components/common/Spinner.svelte';
import Tooltip from '$lib/components/common/Tooltip.svelte';
import { getModels as _getModels } from '$lib/apis';
import SensitiveInput from '$lib/components/common/SensitiveInput.svelte';
const i18n = getContext('i18n');
......@@ -228,14 +230,10 @@
{/if}
</div>
<div class="flex-1">
<input
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
placeholder={$i18n.t('API Key')}
bind:value={OPENAI_API_KEYS[idx]}
autocomplete="off"
/>
</div>
<SensitiveInput
placeholder={$i18n.t('API Key')}
bind:value={OPENAI_API_KEYS[idx]}
/>
<div class="self-center flex items-center">
{#if idx === 0}
<button
......
......@@ -126,7 +126,9 @@
/>
</svg>
</div>
<div class=" self-center text-sm font-medium">Export LiteLLM config.yaml</div>
<div class=" self-center text-sm font-medium">
{$i18n.t('Export LiteLLM config.yaml')}
</div>
</button>
</div>
</div>
......@@ -137,7 +139,7 @@
class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg"
type="submit"
>
Save
{$i18n.t('Save')}
</button>
</div> -->
......
<script lang="ts">
import { getDocs } from '$lib/apis/documents';
import { deleteAllFiles, deleteFileById } from '$lib/apis/files';
import {
getQuerySettings,
scanDocs,
......@@ -19,6 +20,7 @@
import { documents, models } from '$lib/stores';
import { onMount, getContext } from 'svelte';
import { toast } from 'svelte-sonner';
import SensitiveInput from '$lib/components/common/SensitiveInput.svelte';
const i18n = getContext('i18n');
......@@ -217,8 +219,8 @@
<ResetUploadDirConfirmDialog
bind:show={showResetUploadDirConfirm}
on:confirm={() => {
const res = resetUploadDir(localStorage.token).catch((error) => {
on:confirm={async () => {
const res = await deleteAllFiles(localStorage.token).catch((error) => {
toast.error(error);
return null;
});
......@@ -279,24 +281,28 @@
viewBox="0 0 24 24"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
><style>
>
<style>
.spinner_ajPY {
transform-origin: center;
animation: spinner_AtaB 0.75s infinite linear;
}
@keyframes spinner_AtaB {
100% {
transform: rotate(360deg);
}
}
</style><path
</style>
<path
d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
opacity=".25"
/><path
/>
<path
d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
class="spinner_ajPY"
/></svg
>
/>
</svg>
</div>
{/if}
</button>
......@@ -329,18 +335,13 @@
{#if embeddingEngine === 'openai'}
<div class="my-0.5 flex gap-2">
<input
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
class="flex-1 w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
placeholder={$i18n.t('API Base URL')}
bind:value={OpenAIUrl}
required
/>
<input
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
placeholder={$i18n.t('API Key')}
bind:value={OpenAIKey}
required
/>
<SensitiveInput placeholder={$i18n.t('API Key')} bind:value={OpenAIKey} />
</div>
<div class="flex mt-0.5 space-x-2">
<div class=" self-center text-xs font-medium">{$i18n.t('Embedding Batch Size')}</div>
......@@ -438,24 +439,28 @@
viewBox="0 0 24 24"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
><style>
>
<style>
.spinner_ajPY {
transform-origin: center;
animation: spinner_AtaB 0.75s infinite linear;
}
@keyframes spinner_AtaB {
100% {
transform: rotate(360deg);
}
}
</style><path
</style>
<path
d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
opacity=".25"
/><path
/>
<path
d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
class="spinner_ajPY"
/></svg
>
/>
</svg>
</div>
{:else}
<svg
......@@ -511,24 +516,28 @@
viewBox="0 0 24 24"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
><style>
>
<style>
.spinner_ajPY {
transform-origin: center;
animation: spinner_AtaB 0.75s infinite linear;
}
@keyframes spinner_AtaB {
100% {
transform: rotate(360deg);
}
}
</style><path
</style>
<path
d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
opacity=".25"
/><path
/>
<path
d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
class="spinner_ajPY"
/></svg
>
/>
</svg>
</div>
{:else}
<svg
......
......@@ -19,6 +19,7 @@
updateOpenAIConfig
} from '$lib/apis/images';
import { getBackendConfig } from '$lib/apis';
import SensitiveInput from '$lib/components/common/SensitiveInput.svelte';
const dispatch = createEventDispatcher();
const i18n = getContext('i18n');
......@@ -29,6 +30,7 @@
let enableImageGeneration = false;
let AUTOMATIC1111_BASE_URL = '';
let AUTOMATIC1111_API_AUTH = '';
let COMFYUI_BASE_URL = '';
let OPENAI_API_BASE_URL = '';
......@@ -74,7 +76,8 @@
}
} else {
const res = await updateImageGenerationEngineUrls(localStorage.token, {
AUTOMATIC1111_BASE_URL: AUTOMATIC1111_BASE_URL
AUTOMATIC1111_BASE_URL: AUTOMATIC1111_BASE_URL,
AUTOMATIC1111_API_AUTH: AUTOMATIC1111_API_AUTH
}).catch((error) => {
toast.error(error);
return null;
......@@ -82,6 +85,7 @@
if (res) {
AUTOMATIC1111_BASE_URL = res.AUTOMATIC1111_BASE_URL;
AUTOMATIC1111_API_AUTH = res.AUTOMATIC1111_API_AUTH;
await getModels();
......@@ -89,7 +93,9 @@
toast.success($i18n.t('Server connection verified'));
}
} else {
({ AUTOMATIC1111_BASE_URL } = await getImageGenerationEngineUrls(localStorage.token));
({ AUTOMATIC1111_BASE_URL, AUTOMATIC1111_API_AUTH } = await getImageGenerationEngineUrls(
localStorage.token
));
}
}
};
......@@ -128,6 +134,7 @@
const URLS = await getImageGenerationEngineUrls(localStorage.token);
AUTOMATIC1111_BASE_URL = URLS.AUTOMATIC1111_BASE_URL;
AUTOMATIC1111_API_AUTH = URLS.AUTOMATIC1111_API_AUTH;
COMFYUI_BASE_URL = URLS.COMFYUI_BASE_URL;
const config = await getOpenAIConfig(localStorage.token);
......@@ -270,6 +277,23 @@
{$i18n.t('(e.g. `sh webui.sh --api`)')}
</a>
</div>
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('AUTOMATIC1111 Api Auth String')}</div>
<SensitiveInput
placeholder={$i18n.t('Enter api auth string (e.g. username:password)')}
bind:value={AUTOMATIC1111_API_AUTH}
/>
<div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
{$i18n.t('Include `--api-auth` flag when running stable-diffusion-webui')}
<a
class=" text-gray-300 font-medium"
href="https://github.com/AUTOMATIC1111/stable-diffusion-webui/discussions/13993"
target="_blank"
>
{$i18n.t('(e.g. `sh webui.sh --api --api-auth username_password`)').replace('_', ':')}
</a>
</div>
{:else if imageGenerationEngine === 'comfyui'}
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('ComfyUI Base URL')}</div>
<div class="flex w-full">
......@@ -307,18 +331,13 @@
<div class="flex gap-2 mb-1">
<input
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
class="flex-1 w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
placeholder={$i18n.t('API Base URL')}
bind:value={OPENAI_API_BASE_URL}
required
/>
<input
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
placeholder={$i18n.t('API Key')}
bind:value={OPENAI_API_KEY}
required
/>
<SensitiveInput placeholder={$i18n.t('API Key')} bind:value={OPENAI_API_KEY} />
</div>
</div>
{/if}
......
......@@ -60,13 +60,13 @@
});
if (res) {
toast.success('Valves updated successfully');
toast.success($i18n.t('Valves updated successfully'));
setPipelines();
models.set(await getModels(localStorage.token));
saveHandler();
}
} else {
toast.error('No valves to update');
toast.error($i18n.t('No valves to update'));
}
};
......@@ -122,7 +122,7 @@
});
if (res) {
toast.success('Pipeline downloaded successfully');
toast.success($i18n.t('Pipeline downloaded successfully'));
setPipelines();
models.set(await getModels(localStorage.token));
}
......@@ -147,12 +147,12 @@
);
if (res) {
toast.success('Pipeline downloaded successfully');
toast.success($i18n.t('Pipeline downloaded successfully'));
setPipelines();
models.set(await getModels(localStorage.token));
}
} else {
toast.error('No file selected');
toast.error($i18n.t('No file selected'));
}
pipelineFiles = null;
......@@ -176,7 +176,7 @@
});
if (res) {
toast.success('Pipeline deleted successfully');
toast.success($i18n.t('Pipeline deleted successfully'));
setPipelines();
models.set(await getModels(localStorage.token));
}
......@@ -509,7 +509,7 @@
</div>
{/if}
{:else}
<div>Pipelines Not Detected</div>
<div>{$i18n.t('Pipelines Not Detected')}</div>
{/if}
{:else}
<div class="flex justify-center h-full">
......@@ -525,7 +525,7 @@
class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg"
type="submit"
>
Save
{$i18n.t('Save')}
</button>
</div>
</form>
......@@ -5,6 +5,7 @@
import { documents, models } from '$lib/stores';
import { onMount, getContext } from 'svelte';
import { toast } from 'svelte-sonner';
import SensitiveInput from '$lib/components/common/SensitiveInput.svelte';
const i18n = getContext('i18n');
......@@ -19,7 +20,8 @@
'serper',
'serply',
'duckduckgo',
'tavily'
'tavily',
'jina'
];
let youtubeLanguage = 'en';
......@@ -114,17 +116,10 @@
{$i18n.t('Google PSE API Key')}
</div>
<div class="flex w-full">
<div class="flex-1">
<input
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
type="text"
placeholder={$i18n.t('Enter Google PSE API Key')}
bind:value={webConfig.search.google_pse_api_key}
autocomplete="off"
/>
</div>
</div>
<SensitiveInput
placeholder={$i18n.t('Enter Google PSE API Key')}
bind:value={webConfig.search.google_pse_api_key}
/>
</div>
<div class="mt-1.5">
<div class=" self-center text-xs font-medium mb-1">
......@@ -149,17 +144,10 @@
{$i18n.t('Brave Search API Key')}
</div>
<div class="flex w-full">
<div class="flex-1">
<input
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
type="text"
placeholder={$i18n.t('Enter Brave Search API Key')}
bind:value={webConfig.search.brave_search_api_key}
autocomplete="off"
/>
</div>
</div>
<SensitiveInput
placeholder={$i18n.t('Enter Brave Search API Key')}
bind:value={webConfig.search.brave_search_api_key}
/>
</div>
{:else if webConfig.search.engine === 'serpstack'}
<div>
......@@ -167,17 +155,10 @@
{$i18n.t('Serpstack API Key')}
</div>
<div class="flex w-full">
<div class="flex-1">
<input
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
type="text"
placeholder={$i18n.t('Enter Serpstack API Key')}
bind:value={webConfig.search.serpstack_api_key}
autocomplete="off"
/>
</div>
</div>
<SensitiveInput
placeholder={$i18n.t('Enter Serpstack API Key')}
bind:value={webConfig.search.serpstack_api_key}
/>
</div>
{:else if webConfig.search.engine === 'serper'}
<div>
......@@ -185,17 +166,10 @@
{$i18n.t('Serper API Key')}
</div>
<div class="flex w-full">
<div class="flex-1">
<input
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
type="text"
placeholder={$i18n.t('Enter Serper API Key')}
bind:value={webConfig.search.serper_api_key}
autocomplete="off"
/>
</div>
</div>
<SensitiveInput
placeholder={$i18n.t('Enter Serper API Key')}
bind:value={webConfig.search.serper_api_key}
/>
</div>
{:else if webConfig.search.engine === 'serply'}
<div>
......@@ -203,17 +177,10 @@
{$i18n.t('Serply API Key')}
</div>
<div class="flex w-full">
<div class="flex-1">
<input
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
type="text"
placeholder={$i18n.t('Enter Serply API Key')}
bind:value={webConfig.search.serply_api_key}
autocomplete="off"
/>
</div>
</div>
<SensitiveInput
placeholder={$i18n.t('Enter Serply API Key')}
bind:value={webConfig.search.serply_api_key}
/>
</div>
{:else if webConfig.search.engine === 'tavily'}
<div>
......@@ -221,17 +188,10 @@
{$i18n.t('Tavily API Key')}
</div>
<div class="flex w-full">
<div class="flex-1">
<input
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
type="text"
placeholder={$i18n.t('Enter Tavily API Key')}
bind:value={webConfig.search.tavily_api_key}
autocomplete="off"
/>
</div>
</div>
<SensitiveInput
placeholder={$i18n.t('Enter Tavily API Key')}
bind:value={webConfig.search.tavily_api_key}
/>
</div>
{/if}
</div>
......
<script>
import { getContext } from 'svelte';
import Modal from '../common/Modal.svelte';
import Database from './Settings/Database.svelte';
import General from './Settings/General.svelte';
import Users from './Settings/Users.svelte';
import Banners from '$lib/components/admin/Settings/Banners.svelte';
import { toast } from 'svelte-sonner';
import Pipelines from './Settings/Pipelines.svelte';
const i18n = getContext('i18n');
export let show = false;
let selectedTab = 'general';
</script>
<Modal bind:show>
<div>
<div class=" flex justify-between dark:text-gray-300 px-5 pt-4 pb-2">
<div class=" text-lg font-medium self-center">{$i18n.t('Admin Settings')}</div>
<button
class="self-center"
on:click={() => {
show = false;
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-5 h-5"
>
<path
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
/>
</svg>
</button>
</div>
</div>
</Modal>
......@@ -127,6 +127,42 @@
}
onMount(async () => {
const onMessageHandler = async (event) => {
if (event.origin === window.origin) {
// Replace with your iframe's origin
console.log('Message received from iframe:', event.data);
if (event.data.type === 'input:prompt') {
console.log(event.data.text);
const inputElement = document.getElementById('chat-textarea');
if (inputElement) {
prompt = event.data.text;
inputElement.focus();
}
}
if (event.data.type === 'action:submit') {
console.log(event.data.text);
if (prompt !== '') {
await tick();
submitPrompt(prompt);
}
}
if (event.data.type === 'input:prompt:submit') {
console.log(event.data.text);
if (prompt !== '') {
await tick();
submitPrompt(event.data.text);
}
}
}
};
window.addEventListener('message', onMessageHandler);
if (!$chatId) {
chatId.subscribe(async (value) => {
if (!value) {
......@@ -138,6 +174,10 @@
await goto('/');
}
}
return () => {
window.removeEventListener('message', onMessageHandler);
};
});
//////////////////////////
......@@ -273,11 +313,14 @@
id: m.id,
role: m.role,
content: m.content,
info: m.info ? m.info : undefined,
timestamp: m.timestamp
})),
chat_id: $chatId
}).catch((error) => {
console.error(error);
toast.error(error);
messages.at(-1).error = { content: error };
return null;
});
......@@ -322,9 +365,16 @@
} else if (messages.length != 0 && messages.at(-1).done != true) {
// Response not done
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 (
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
toast.error(
......@@ -479,14 +529,13 @@
});
if (res) {
if (res.documents[0].length > 0) {
userContext = res.documents.reduce((acc, doc, index) => {
const createdAtTimestamp = res.metadatas[index][0].created_at;
userContext = res.documents[0].reduce((acc, doc, index) => {
const createdAtTimestamp = res.metadatas[0][index].created_at;
const createdAtDate = new Date(createdAtTimestamp * 1000)
.toISOString()
.split('T')[0];
acc.push(`${index + 1}. [${createdAtDate}]. ${doc[0]}`);
return acc;
}, []);
return `${acc}${index + 1}. [${createdAtDate}]. ${doc}\n`;
}, '');
}
console.log(userContext);
......@@ -542,7 +591,7 @@
: undefined
)}${
responseMessage?.userContext ?? null
? `\n\nUser Context:\n${(responseMessage?.userContext ?? []).join('\n')}`
? `\n\nUser Context:\n${responseMessage?.userContext ?? ''}`
: ''
}`
}
......@@ -585,23 +634,22 @@
}
});
let docs = [];
let files = [];
if (model?.info?.meta?.knowledge ?? false) {
docs = model.info.meta.knowledge;
files = model.info.meta.knowledge;
}
docs = [
...docs,
...messages
.filter((message) => message?.files ?? null)
.map((message) =>
message.files.filter((item) =>
['doc', 'collection', 'web_search_results'].includes(item.type)
)
)
.flat(1)
const lastUserMessage = messages.filter((message) => message.role === 'user').at(-1);
files = [
...files,
...(lastUserMessage?.files?.filter((item) =>
['doc', 'file', 'collection', 'web_search_results'].includes(item.type)
) ?? []),
...(responseMessage?.files?.filter((item) =>
['doc', 'file', 'collection', 'web_search_results'].includes(item.type)
) ?? [])
].filter(
// Remove duplicates
(item, index, array) =>
array.findIndex((i) => JSON.stringify(i) === JSON.stringify(item)) === index
);
......@@ -633,8 +681,8 @@
format: $settings.requestFormat ?? undefined,
keep_alive: $settings.keepAlive ?? undefined,
tool_ids: selectedToolIds.length > 0 ? selectedToolIds : undefined,
docs: docs.length > 0 ? docs : undefined,
citations: docs.length > 0,
files: files.length > 0 ? files : undefined,
citations: files.length > 0 ? true : undefined,
chat_id: $chatId
});
......@@ -830,23 +878,21 @@
let _response = null;
const responseMessage = history.messages[responseMessageId];
let docs = [];
let files = [];
if (model?.info?.meta?.knowledge ?? false) {
docs = model.info.meta.knowledge;
files = model.info.meta.knowledge;
}
docs = [
...docs,
...messages
.filter((message) => message?.files ?? null)
.map((message) =>
message.files.filter((item) =>
['doc', 'collection', 'web_search_results'].includes(item.type)
)
)
.flat(1)
const lastUserMessage = messages.filter((message) => message.role === 'user').at(-1);
files = [
...files,
...(lastUserMessage?.files?.filter((item) =>
['doc', 'file', 'collection', 'web_search_results'].includes(item.type)
) ?? []),
...(responseMessage?.files?.filter((item) =>
['doc', 'file', 'collection', 'web_search_results'].includes(item.type)
) ?? [])
].filter(
// Remove duplicates
(item, index, array) =>
array.findIndex((i) => JSON.stringify(i) === JSON.stringify(item)) === index
);
......@@ -886,7 +932,7 @@
: undefined
)}${
responseMessage?.userContext ?? null
? `\n\nUser Context:\n${(responseMessage?.userContext ?? []).join('\n')}`
? `\n\nUser Context:\n${responseMessage?.userContext ?? ''}`
: ''
}`
}
......@@ -936,11 +982,12 @@
frequency_penalty: $settings?.params?.frequency_penalty ?? undefined,
max_tokens: $settings?.params?.max_tokens ?? undefined,
tool_ids: selectedToolIds.length > 0 ? selectedToolIds : undefined,
docs: docs.length > 0 ? docs : undefined,
citations: docs.length > 0,
files: files.length > 0 ? files : undefined,
citations: files.length > 0 ? true : undefined,
chat_id: $chatId
},
`${OPENAI_API_BASE_URL}`
`${WEBUI_BASE_URL}/api`
);
// Wait until history/message have been updated
......@@ -1212,6 +1259,7 @@
const getWebSearchResults = async (model: string, parentId: string, responseId: string) => {
const responseMessage = history.messages[responseId];
const userMessage = history.messages[parentId];
responseMessage.statusHistory = [
{
......@@ -1222,7 +1270,7 @@
];
messages = messages;
const prompt = history.messages[parentId].content;
const prompt = userMessage.content;
let searchQuery = await generateSearchQuery(localStorage.token, model, messages, prompt).catch(
(error) => {
console.log(error);
......@@ -1322,6 +1370,19 @@
? 'md:max-w-[calc(100%-260px)]'
: ''} 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
{title}
bind:selectedModels
......@@ -1333,7 +1394,9 @@
{#if $banners.length > 0 && messages.length === 0 && !$chatId && selectedModels.length <= 1}
<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">
{#each $banners.filter( (b) => (b.dismissible ? !JSON.parse(localStorage.getItem('dismissedBannerIds') ?? '[]').includes(b.id) : true) ) as banner}
......@@ -1358,9 +1421,9 @@
</div>
{/if}
<div class="flex flex-col flex-auto">
<div class="flex flex-col flex-auto z-10">
<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"
bind:this={messagesContainerElement}
on:scroll={(e) => {
......@@ -1399,6 +1462,7 @@
}
return a;
}, [])}
transparentBackground={$settings?.backgroundImageUrl ?? false}
{selectedModels}
{messages}
{submitPrompt}
......
......@@ -15,11 +15,19 @@
import { blobToFile, calculateSHA256, findWordIndices } from '$lib/utils';
import {
processDocToVectorDB,
uploadDocToVectorDB,
uploadWebToVectorDB,
uploadYoutubeTranscriptionToVectorDB
} 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 Suggestions from './MessageInput/Suggestions.svelte';
......@@ -35,6 +43,8 @@
const i18n = getContext('i18n');
export let transparentBackground = false;
export let submitPrompt: Function;
export let stopResponse: Function;
......@@ -84,44 +94,75 @@
element.scrollTop = element.scrollHeight;
};
const uploadDoc = async (file) => {
const uploadFileHandler = async (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 = {
type: 'doc',
name: file.name,
collection_name: '',
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) {
console.log(res);
const blob = new Blob([res.text], { type: 'text/plain' });
file = blobToFile(blob, `${file.name}.txt`);
}
}
if (res) {
console.log(res);
const blob = new Blob([res.text], { type: 'text/plain' });
file = blobToFile(blob, `${file.name}.txt`);
}
// 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 res = await uploadDocToVectorDB(localStorage.token, '', file);
const processFileItem = async (fileItem) => {
try {
const res = await processDocToVectorDB(localStorage.token, fileItem.id);
if (res) {
doc.upload_status = true;
doc.collection_name = res.collection_name;
fileItem.status = 'processed';
fileItem.collection_name = res.collection_name;
files = files;
}
} catch (e) {
// 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);
fileItem.status = 'processed';
files = files;
}
};
......@@ -132,7 +173,7 @@
type: 'doc',
name: url,
collection_name: '',
upload_status: false,
status: false,
url: url,
error: ''
};
......@@ -142,7 +183,7 @@
const res = await uploadWebToVectorDB(localStorage.token, '', url);
if (res) {
doc.upload_status = true;
doc.status = 'processed';
doc.collection_name = res.collection_name;
files = files;
}
......@@ -160,7 +201,7 @@
type: 'doc',
name: url,
collection_name: '',
upload_status: false,
status: false,
url: url,
error: ''
};
......@@ -170,7 +211,7 @@
const res = await uploadYoutubeTranscriptionToVectorDB(localStorage.token, url);
if (res) {
doc.upload_status = true;
doc.status = 'processed';
doc.collection_name = res.collection_name;
files = files;
}
......@@ -228,19 +269,8 @@
];
};
reader.readAsDataURL(file);
} else if (
SUPPORTED_FILE_TYPE.includes(file['type']) ||
SUPPORTED_FILE_EXTENSIONS.includes(file.name.split('.').at(-1))
) {
uploadDoc(file);
} else {
toast.error(
$i18n.t(
`Unknown File Type '{{file_type}}', but accepting and treating as plain text`,
{ file_type: file['type'] }
)
);
uploadDoc(file);
uploadFileHandler(file);
}
});
} else {
......@@ -291,9 +321,11 @@
<div class="flex flex-col max-w-6xl px-2.5 md:px-6 w-full">
<div class="relative">
{#if autoScroll === false && messages.length > 0}
<div class=" absolute -top-12 left-0 right-0 flex justify-center z-30">
<div
class=" absolute -top-12 left-0 right-0 flex justify-center z-30 pointer-events-none"
>
<button
class=" bg-white border border-gray-100 dark:border-none dark:bg-white/20 p-1.5 rounded-full"
class=" bg-white border border-gray-100 dark:border-none dark:bg-white/20 p-1.5 rounded-full pointer-events-auto"
on:click={() => {
autoScroll = true;
scrollToBottom();
......@@ -336,9 +368,9 @@
files = [
...files,
{
type: e?.detail?.type ?? 'doc',
type: e?.detail?.type ?? 'file',
...e.detail,
upload_status: true
status: 'processed'
}
];
}}
......@@ -391,7 +423,7 @@
</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=" pb-2">
<input
......@@ -407,8 +439,6 @@
if (['image/gif', 'image/webp', 'image/jpeg', 'image/png'].includes(file['type'])) {
if (visionCapableModels.length === 0) {
toast.error($i18n.t('Selected model(s) do not support image inputs'));
inputFiles = null;
filesInputElement.value = '';
return;
}
let reader = new FileReader();
......@@ -420,30 +450,17 @@
url: `${event.target.result}`
}
];
inputFiles = null;
filesInputElement.value = '';
};
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 {
toast.error(
$i18n.t(
`Unknown File Type '{{file_type}}', but accepting and treating as plain text`,
{ file_type: file['type'] }
)
);
uploadDoc(file);
filesInputElement.value = '';
uploadFileHandler(file);
}
});
} else {
toast.error($i18n.t(`File not found.`));
}
filesInputElement.value = '';
}}
/>
......@@ -517,12 +534,12 @@
</Tooltip>
{/if}
</div>
{:else if file.type === 'doc'}
{:else if ['doc', 'file'].includes(file.type)}
<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"
>
<div class="p-2.5 bg-red-400 text-white rounded-lg">
{#if file.upload_status}
{#if file.status === 'processed'}
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
......
<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 {
......@@ -28,6 +28,8 @@
export let chatId;
export let modelId;
let model = null;
let loading = false;
let confirmed = false;
let interrupted = false;
......@@ -269,7 +271,7 @@
return;
}
if (assistantSpeaking) {
if (assistantSpeaking && !($settings?.voiceInterruption ?? false)) {
// Mute the audio if the assistant is speaking
analyser.maxDecibels = 0;
analyser.minDecibels = -1;
......@@ -507,6 +509,8 @@
};
onMount(async () => {
model = $models.find((m) => m.id === modelId);
startRecording();
const chatStartHandler = async (e) => {
......@@ -657,7 +661,13 @@
? ' size-16'
: rmsLevel * 100 > 1
? '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}
<!-- navbar -->
......@@ -732,7 +742,13 @@
? 'size-48'
: rmsLevel * 100 > 1
? 'size-[11.5rem]'
: 'size-44'} transition-all bg-black dark:bg-white rounded-full"
: '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>
......
......@@ -43,11 +43,11 @@
];
$: filteredCollections = collections
.filter((collection) => collection.name.includes(prompt.split(' ')?.at(0)?.substring(1) ?? ''))
.filter((collection) => findByName(collection, prompt))
.sort((a, b) => a.name.localeCompare(b.name));
$: filteredDocs = $documents
.filter((doc) => doc.name.includes(prompt.split(' ')?.at(0)?.substring(1) ?? ''))
.filter((doc) => findByName(doc, prompt))
.sort((a, b) => a.title.localeCompare(b.title));
$: filteredItems = [...filteredCollections, ...filteredDocs];
......@@ -58,6 +58,15 @@
console.log(filteredCollections);
}
type ObjectWithName = {
name: string;
};
const findByName = (obj: ObjectWithName, prompt: string) => {
const name = obj.name.toLowerCase();
return name.includes(prompt.toLowerCase().split(' ')?.at(0)?.substring(1) ?? '');
};
export const selectUp = () => {
selectedIdx = Math.max(0, selectedIdx - 1);
};
......@@ -101,7 +110,7 @@
</script>
{#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 dark:border dark:border-gray-850 rounded-lg">
<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>
......
......@@ -21,7 +21,9 @@
let filteredModels = [];
$: filteredModels = $models
.filter((p) => p.name.includes(prompt.split(' ')?.at(0)?.substring(1) ?? ''))
.filter((p) =>
p.name.toLowerCase().includes(prompt.toLowerCase().split(' ')?.at(0)?.substring(1) ?? '')
)
.sort((a, b) => a.name.localeCompare(b.name));
$: if (prompt) {
......@@ -133,7 +135,7 @@
{#if prompt.charAt(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 dark:border dark:border-gray-850 rounded-lg">
<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>
......
......@@ -12,7 +12,7 @@
let filteredPromptCommands = [];
$: filteredPromptCommands = $prompts
.filter((p) => p.command.includes(prompt))
.filter((p) => p.command.toLowerCase().includes(prompt.toLowerCase()))
.sort((a, b) => a.title.localeCompare(b.title));
$: if (prompt) {
......@@ -88,7 +88,7 @@
</script>
{#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 dark:border dark:border-gray-850 rounded-lg">
<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>
......
......@@ -62,7 +62,7 @@
<div class="text-sm text-gray-600 font-normal line-clamp-2">{prompt.title[1]}</div>
{:else}
<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}
</div>
......
......@@ -385,7 +385,7 @@
{/each}
{#if bottomPadding}
<div class=" pb-20" />
<div class=" pb-6" />
{/if}
{/key}
</div>
......
......@@ -203,8 +203,18 @@ __builtins__.input = input`);
};
};
let debounceTimeout;
$: 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>
......
......@@ -9,6 +9,7 @@
import Suggestions from '../MessageInput/Suggestions.svelte';
import { sanitizeResponseContent } from '$lib/utils';
import Tooltip from '$lib/components/common/Tooltip.svelte';
const i18n = getContext('i18n');
......@@ -32,7 +33,7 @@
</script>
{#key mounted}
<div class="m-auto w-full max-w-6xl px-8 lg:px-24 pb-10">
<div class="m-auto w-full max-w-6xl px-8 lg:px-20 pb-10">
<div class="flex justify-start">
<div class="flex -space-x-4 mb-1" in:fade={{ duration: 200 }}>
{#each models as model, modelIdx}
......@@ -41,14 +42,23 @@
selectedModelIdx = modelIdx;
}}
>
<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
content={marked.parse(
sanitizeResponseContent(models[selectedModelIdx]?.info?.meta?.description ?? '')
)}
placement="right"
>
<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>
{/each}
</div>
......
......@@ -2,10 +2,12 @@
import { settings } from '$lib/stores';
import { WEBUI_BASE_URL } from '$lib/constants';
export let className = 'size-8';
export let src = '/user.png';
</script>
<div class={($settings?.chatDirection ?? 'LTR') === 'LTR' ? 'mr-3' : 'ml-3'}>
<div class={`flex-shrink-0 ${($settings?.chatDirection ?? 'LTR') === 'LTR' ? 'mr-3' : 'ml-3'}`}>
<img
crossorigin="anonymous"
src={src.startsWith(WEBUI_BASE_URL) ||
......@@ -14,7 +16,7 @@
src.startsWith('/')
? src
: `/user.png`}
class=" w-8 object-cover rounded-full"
class=" {className} object-cover rounded-full -translate-y-[1px]"
alt="profile"
draggable="false"
/>
......
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