"vscode:/vscode.git/clone" did not exist on "7fec9ed62cd23852f06866efe30fd6e3c34fa8dd"
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 @@ ...@@ -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>
......
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';
import Switch from '$lib/components/common/Switch.svelte'; import Switch from '$lib/components/common/Switch.svelte';
import { getBackendConfig } from '$lib/apis'; import { getBackendConfig } from '$lib/apis';
import SensitiveInput from '$lib/components/common/SensitiveInput.svelte';
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
const i18n = getContext('i18n'); const i18n = getContext('i18n');
...@@ -72,7 +73,7 @@ ...@@ -72,7 +73,7 @@
}); });
if (res) { if (res) {
toast.success('Audio settings updated successfully'); toast.success($i18n.t('Audio settings updated successfully'));
config.set(await getBackendConfig()); config.set(await getBackendConfig());
} }
...@@ -137,18 +138,13 @@ ...@@ -137,18 +138,13 @@
<div> <div>
<div class="mt-1 flex gap-2 mb-1"> <div class="mt-1 flex gap-2 mb-1">
<input <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')} placeholder={$i18n.t('API Base URL')}
bind:value={STT_OPENAI_API_BASE_URL} bind:value={STT_OPENAI_API_BASE_URL}
required required
/> />
<input <SensitiveInput placeholder={$i18n.t('API Key')} bind:value={STT_OPENAI_API_KEY} />
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
/>
</div> </div>
</div> </div>
...@@ -198,7 +194,7 @@ ...@@ -198,7 +194,7 @@
}} }}
> >
<option value="">{$i18n.t('Web API')}</option> <option value="">{$i18n.t('Web API')}</option>
<option value="openai">{$i18n.t('Open AI')}</option> <option value="openai">{$i18n.t('OpenAI')}</option>
</select> </select>
</div> </div>
</div> </div>
...@@ -207,18 +203,13 @@ ...@@ -207,18 +203,13 @@
<div> <div>
<div class="mt-1 flex gap-2 mb-1"> <div class="mt-1 flex gap-2 mb-1">
<input <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')} placeholder={$i18n.t('API Base URL')}
bind:value={TTS_OPENAI_API_BASE_URL} bind:value={TTS_OPENAI_API_BASE_URL}
required required
/> />
<input <SensitiveInput placeholder={$i18n.t('API Key')} bind:value={TTS_OPENAI_API_KEY} />
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
/>
</div> </div>
</div> </div>
{/if} {/if}
......
<script lang="ts"> <script lang="ts">
import { models, user } from '$lib/stores'; import { models, user } from '$lib/stores';
import { createEventDispatcher, onMount, getContext, tick } from 'svelte'; import { createEventDispatcher, onMount, getContext, tick } from 'svelte';
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
import { import {
...@@ -24,6 +25,7 @@ ...@@ -24,6 +25,7 @@
import Spinner from '$lib/components/common/Spinner.svelte'; import Spinner from '$lib/components/common/Spinner.svelte';
import Tooltip from '$lib/components/common/Tooltip.svelte'; import Tooltip from '$lib/components/common/Tooltip.svelte';
import { getModels as _getModels } from '$lib/apis'; import { getModels as _getModels } from '$lib/apis';
import SensitiveInput from '$lib/components/common/SensitiveInput.svelte';
const i18n = getContext('i18n'); const i18n = getContext('i18n');
...@@ -228,14 +230,10 @@ ...@@ -228,14 +230,10 @@
{/if} {/if}
</div> </div>
<div class="flex-1"> <SensitiveInput
<input placeholder={$i18n.t('API Key')}
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none" bind:value={OPENAI_API_KEYS[idx]}
placeholder={$i18n.t('API Key')} />
bind:value={OPENAI_API_KEYS[idx]}
autocomplete="off"
/>
</div>
<div class="self-center flex items-center"> <div class="self-center flex items-center">
{#if idx === 0} {#if idx === 0}
<button <button
......
...@@ -126,7 +126,9 @@ ...@@ -126,7 +126,9 @@
/> />
</svg> </svg>
</div> </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> </button>
</div> </div>
</div> </div>
...@@ -137,7 +139,7 @@ ...@@ -137,7 +139,7 @@
class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg" class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg"
type="submit" type="submit"
> >
Save {$i18n.t('Save')}
</button> </button>
</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,
...@@ -19,6 +20,7 @@ ...@@ -19,6 +20,7 @@
import { documents, models } from '$lib/stores'; import { documents, models } from '$lib/stores';
import { onMount, getContext } from 'svelte'; import { onMount, getContext } from 'svelte';
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';
import SensitiveInput from '$lib/components/common/SensitiveInput.svelte';
const i18n = getContext('i18n'); const i18n = getContext('i18n');
...@@ -217,8 +219,8 @@ ...@@ -217,8 +219,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;
}); });
...@@ -279,24 +281,28 @@ ...@@ -279,24 +281,28 @@
viewBox="0 0 24 24" viewBox="0 0 24 24"
fill="currentColor" fill="currentColor"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
><style> >
<style>
.spinner_ajPY { .spinner_ajPY {
transform-origin: center; transform-origin: center;
animation: spinner_AtaB 0.75s infinite linear; animation: spinner_AtaB 0.75s infinite linear;
} }
@keyframes spinner_AtaB { @keyframes spinner_AtaB {
100% { 100% {
transform: rotate(360deg); 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" 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" 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" 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" class="spinner_ajPY"
/></svg />
> </svg>
</div> </div>
{/if} {/if}
</button> </button>
...@@ -329,18 +335,13 @@ ...@@ -329,18 +335,13 @@
{#if embeddingEngine === 'openai'} {#if embeddingEngine === 'openai'}
<div class="my-0.5 flex gap-2"> <div class="my-0.5 flex gap-2">
<input <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')} placeholder={$i18n.t('API Base URL')}
bind:value={OpenAIUrl} bind:value={OpenAIUrl}
required required
/> />
<input <SensitiveInput placeholder={$i18n.t('API Key')} bind:value={OpenAIKey} />
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
/>
</div> </div>
<div class="flex mt-0.5 space-x-2"> <div class="flex mt-0.5 space-x-2">
<div class=" self-center text-xs font-medium">{$i18n.t('Embedding Batch Size')}</div> <div class=" self-center text-xs font-medium">{$i18n.t('Embedding Batch Size')}</div>
...@@ -438,24 +439,28 @@ ...@@ -438,24 +439,28 @@
viewBox="0 0 24 24" viewBox="0 0 24 24"
fill="currentColor" fill="currentColor"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
><style> >
<style>
.spinner_ajPY { .spinner_ajPY {
transform-origin: center; transform-origin: center;
animation: spinner_AtaB 0.75s infinite linear; animation: spinner_AtaB 0.75s infinite linear;
} }
@keyframes spinner_AtaB { @keyframes spinner_AtaB {
100% { 100% {
transform: rotate(360deg); 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" 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" 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" 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" class="spinner_ajPY"
/></svg />
> </svg>
</div> </div>
{:else} {:else}
<svg <svg
...@@ -511,24 +516,28 @@ ...@@ -511,24 +516,28 @@
viewBox="0 0 24 24" viewBox="0 0 24 24"
fill="currentColor" fill="currentColor"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
><style> >
<style>
.spinner_ajPY { .spinner_ajPY {
transform-origin: center; transform-origin: center;
animation: spinner_AtaB 0.75s infinite linear; animation: spinner_AtaB 0.75s infinite linear;
} }
@keyframes spinner_AtaB { @keyframes spinner_AtaB {
100% { 100% {
transform: rotate(360deg); 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" 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" 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" 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" class="spinner_ajPY"
/></svg />
> </svg>
</div> </div>
{:else} {:else}
<svg <svg
......
...@@ -19,6 +19,7 @@ ...@@ -19,6 +19,7 @@
updateOpenAIConfig updateOpenAIConfig
} from '$lib/apis/images'; } from '$lib/apis/images';
import { getBackendConfig } from '$lib/apis'; import { getBackendConfig } from '$lib/apis';
import SensitiveInput from '$lib/components/common/SensitiveInput.svelte';
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
const i18n = getContext('i18n'); const i18n = getContext('i18n');
...@@ -29,6 +30,7 @@ ...@@ -29,6 +30,7 @@
let enableImageGeneration = false; let enableImageGeneration = false;
let AUTOMATIC1111_BASE_URL = ''; let AUTOMATIC1111_BASE_URL = '';
let AUTOMATIC1111_API_AUTH = '';
let COMFYUI_BASE_URL = ''; let COMFYUI_BASE_URL = '';
let OPENAI_API_BASE_URL = ''; let OPENAI_API_BASE_URL = '';
...@@ -74,7 +76,8 @@ ...@@ -74,7 +76,8 @@
} }
} else { } else {
const res = await updateImageGenerationEngineUrls(localStorage.token, { 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) => { }).catch((error) => {
toast.error(error); toast.error(error);
return null; return null;
...@@ -82,6 +85,7 @@ ...@@ -82,6 +85,7 @@
if (res) { if (res) {
AUTOMATIC1111_BASE_URL = res.AUTOMATIC1111_BASE_URL; AUTOMATIC1111_BASE_URL = res.AUTOMATIC1111_BASE_URL;
AUTOMATIC1111_API_AUTH = res.AUTOMATIC1111_API_AUTH;
await getModels(); await getModels();
...@@ -89,7 +93,9 @@ ...@@ -89,7 +93,9 @@
toast.success($i18n.t('Server connection verified')); toast.success($i18n.t('Server connection verified'));
} }
} else { } else {
({ AUTOMATIC1111_BASE_URL } = await getImageGenerationEngineUrls(localStorage.token)); ({ AUTOMATIC1111_BASE_URL, AUTOMATIC1111_API_AUTH } = await getImageGenerationEngineUrls(
localStorage.token
));
} }
} }
}; };
...@@ -128,6 +134,7 @@ ...@@ -128,6 +134,7 @@
const URLS = await getImageGenerationEngineUrls(localStorage.token); const URLS = await getImageGenerationEngineUrls(localStorage.token);
AUTOMATIC1111_BASE_URL = URLS.AUTOMATIC1111_BASE_URL; AUTOMATIC1111_BASE_URL = URLS.AUTOMATIC1111_BASE_URL;
AUTOMATIC1111_API_AUTH = URLS.AUTOMATIC1111_API_AUTH;
COMFYUI_BASE_URL = URLS.COMFYUI_BASE_URL; COMFYUI_BASE_URL = URLS.COMFYUI_BASE_URL;
const config = await getOpenAIConfig(localStorage.token); const config = await getOpenAIConfig(localStorage.token);
...@@ -270,6 +277,23 @@ ...@@ -270,6 +277,23 @@
{$i18n.t('(e.g. `sh webui.sh --api`)')} {$i18n.t('(e.g. `sh webui.sh --api`)')}
</a> </a>
</div> </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'} {:else if imageGenerationEngine === 'comfyui'}
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('ComfyUI Base URL')}</div> <div class=" mb-2.5 text-sm font-medium">{$i18n.t('ComfyUI Base URL')}</div>
<div class="flex w-full"> <div class="flex w-full">
...@@ -307,18 +331,13 @@ ...@@ -307,18 +331,13 @@
<div class="flex gap-2 mb-1"> <div class="flex gap-2 mb-1">
<input <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')} placeholder={$i18n.t('API Base URL')}
bind:value={OPENAI_API_BASE_URL} bind:value={OPENAI_API_BASE_URL}
required required
/> />
<input <SensitiveInput placeholder={$i18n.t('API Key')} bind:value={OPENAI_API_KEY} />
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
/>
</div> </div>
</div> </div>
{/if} {/if}
......
...@@ -60,13 +60,13 @@ ...@@ -60,13 +60,13 @@
}); });
if (res) { if (res) {
toast.success('Valves updated successfully'); toast.success($i18n.t('Valves updated successfully'));
setPipelines(); setPipelines();
models.set(await getModels(localStorage.token)); models.set(await getModels(localStorage.token));
saveHandler(); saveHandler();
} }
} else { } else {
toast.error('No valves to update'); toast.error($i18n.t('No valves to update'));
} }
}; };
...@@ -122,7 +122,7 @@ ...@@ -122,7 +122,7 @@
}); });
if (res) { if (res) {
toast.success('Pipeline downloaded successfully'); toast.success($i18n.t('Pipeline downloaded successfully'));
setPipelines(); setPipelines();
models.set(await getModels(localStorage.token)); models.set(await getModels(localStorage.token));
} }
...@@ -147,12 +147,12 @@ ...@@ -147,12 +147,12 @@
); );
if (res) { if (res) {
toast.success('Pipeline downloaded successfully'); toast.success($i18n.t('Pipeline downloaded successfully'));
setPipelines(); setPipelines();
models.set(await getModels(localStorage.token)); models.set(await getModels(localStorage.token));
} }
} else { } else {
toast.error('No file selected'); toast.error($i18n.t('No file selected'));
} }
pipelineFiles = null; pipelineFiles = null;
...@@ -176,7 +176,7 @@ ...@@ -176,7 +176,7 @@
}); });
if (res) { if (res) {
toast.success('Pipeline deleted successfully'); toast.success($i18n.t('Pipeline deleted successfully'));
setPipelines(); setPipelines();
models.set(await getModels(localStorage.token)); models.set(await getModels(localStorage.token));
} }
...@@ -509,7 +509,7 @@ ...@@ -509,7 +509,7 @@
</div> </div>
{/if} {/if}
{:else} {:else}
<div>Pipelines Not Detected</div> <div>{$i18n.t('Pipelines Not Detected')}</div>
{/if} {/if}
{:else} {:else}
<div class="flex justify-center h-full"> <div class="flex justify-center h-full">
...@@ -525,7 +525,7 @@ ...@@ -525,7 +525,7 @@
class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg" class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg"
type="submit" type="submit"
> >
Save {$i18n.t('Save')}
</button> </button>
</div> </div>
</form> </form>
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
import { documents, models } from '$lib/stores'; import { documents, models } from '$lib/stores';
import { onMount, getContext } from 'svelte'; import { onMount, getContext } from 'svelte';
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';
import SensitiveInput from '$lib/components/common/SensitiveInput.svelte';
const i18n = getContext('i18n'); const i18n = getContext('i18n');
...@@ -19,7 +20,8 @@ ...@@ -19,7 +20,8 @@
'serper', 'serper',
'serply', 'serply',
'duckduckgo', 'duckduckgo',
'tavily' 'tavily',
'jina'
]; ];
let youtubeLanguage = 'en'; let youtubeLanguage = 'en';
...@@ -114,17 +116,10 @@ ...@@ -114,17 +116,10 @@
{$i18n.t('Google PSE API Key')} {$i18n.t('Google PSE API Key')}
</div> </div>
<div class="flex w-full"> <SensitiveInput
<div class="flex-1"> placeholder={$i18n.t('Enter Google PSE API Key')}
<input bind:value={webConfig.search.google_pse_api_key}
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>
</div> </div>
<div class="mt-1.5"> <div class="mt-1.5">
<div class=" self-center text-xs font-medium mb-1"> <div class=" self-center text-xs font-medium mb-1">
...@@ -149,17 +144,10 @@ ...@@ -149,17 +144,10 @@
{$i18n.t('Brave Search API Key')} {$i18n.t('Brave Search API Key')}
</div> </div>
<div class="flex w-full"> <SensitiveInput
<div class="flex-1"> placeholder={$i18n.t('Enter Brave Search API Key')}
<input bind:value={webConfig.search.brave_search_api_key}
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>
</div> </div>
{:else if webConfig.search.engine === 'serpstack'} {:else if webConfig.search.engine === 'serpstack'}
<div> <div>
...@@ -167,17 +155,10 @@ ...@@ -167,17 +155,10 @@
{$i18n.t('Serpstack API Key')} {$i18n.t('Serpstack API Key')}
</div> </div>
<div class="flex w-full"> <SensitiveInput
<div class="flex-1"> placeholder={$i18n.t('Enter Serpstack API Key')}
<input bind:value={webConfig.search.serpstack_api_key}
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>
</div> </div>
{:else if webConfig.search.engine === 'serper'} {:else if webConfig.search.engine === 'serper'}
<div> <div>
...@@ -185,17 +166,10 @@ ...@@ -185,17 +166,10 @@
{$i18n.t('Serper API Key')} {$i18n.t('Serper API Key')}
</div> </div>
<div class="flex w-full"> <SensitiveInput
<div class="flex-1"> placeholder={$i18n.t('Enter Serper API Key')}
<input bind:value={webConfig.search.serper_api_key}
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>
</div> </div>
{:else if webConfig.search.engine === 'serply'} {:else if webConfig.search.engine === 'serply'}
<div> <div>
...@@ -203,17 +177,10 @@ ...@@ -203,17 +177,10 @@
{$i18n.t('Serply API Key')} {$i18n.t('Serply API Key')}
</div> </div>
<div class="flex w-full"> <SensitiveInput
<div class="flex-1"> placeholder={$i18n.t('Enter Serply API Key')}
<input bind:value={webConfig.search.serply_api_key}
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>
</div> </div>
{:else if webConfig.search.engine === 'tavily'} {:else if webConfig.search.engine === 'tavily'}
<div> <div>
...@@ -221,17 +188,10 @@ ...@@ -221,17 +188,10 @@
{$i18n.t('Tavily API Key')} {$i18n.t('Tavily API Key')}
</div> </div>
<div class="flex w-full"> <SensitiveInput
<div class="flex-1"> placeholder={$i18n.t('Enter Tavily API Key')}
<input bind:value={webConfig.search.tavily_api_key}
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none" />
type="text"
placeholder={$i18n.t('Enter Tavily API Key')}
bind:value={webConfig.search.tavily_api_key}
autocomplete="off"
/>
</div>
</div>
</div> </div>
{/if} {/if}
</div> </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 @@ ...@@ -127,6 +127,42 @@
} }
onMount(async () => { 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) { if (!$chatId) {
chatId.subscribe(async (value) => { chatId.subscribe(async (value) => {
if (!value) { if (!value) {
...@@ -138,6 +174,10 @@ ...@@ -138,6 +174,10 @@
await goto('/'); await goto('/');
} }
} }
return () => {
window.removeEventListener('message', onMessageHandler);
};
}); });
////////////////////////// //////////////////////////
...@@ -273,11 +313,14 @@ ...@@ -273,11 +313,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;
}); });
...@@ -322,9 +365,16 @@ ...@@ -322,9 +365,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(
...@@ -479,14 +529,13 @@ ...@@ -479,14 +529,13 @@
}); });
if (res) { if (res) {
if (res.documents[0].length > 0) { if (res.documents[0].length > 0) {
userContext = res.documents.reduce((acc, doc, index) => { userContext = res.documents[0].reduce((acc, doc, index) => {
const createdAtTimestamp = res.metadatas[index][0].created_at; const createdAtTimestamp = res.metadatas[0][index].created_at;
const createdAtDate = new Date(createdAtTimestamp * 1000) const createdAtDate = new Date(createdAtTimestamp * 1000)
.toISOString() .toISOString()
.split('T')[0]; .split('T')[0];
acc.push(`${index + 1}. [${createdAtDate}]. ${doc[0]}`); return `${acc}${index + 1}. [${createdAtDate}]. ${doc}\n`;
return acc; }, '');
}, []);
} }
console.log(userContext); console.log(userContext);
...@@ -542,7 +591,7 @@ ...@@ -542,7 +591,7 @@
: undefined : undefined
)}${ )}${
responseMessage?.userContext ?? null responseMessage?.userContext ?? null
? `\n\nUser Context:\n${(responseMessage?.userContext ?? []).join('\n')}` ? `\n\nUser Context:\n${responseMessage?.userContext ?? ''}`
: '' : ''
}` }`
} }
...@@ -585,23 +634,22 @@ ...@@ -585,23 +634,22 @@
} }
}); });
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 = [
...docs, files = [
...messages ...files,
.filter((message) => message?.files ?? null) ...(lastUserMessage?.files?.filter((item) =>
.map((message) => ['doc', 'file', 'collection', 'web_search_results'].includes(item.type)
message.files.filter((item) => ) ?? []),
['doc', 'collection', 'web_search_results'].includes(item.type) ...(responseMessage?.files?.filter((item) =>
) ['doc', 'file', '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
); );
...@@ -633,8 +681,8 @@ ...@@ -633,8 +681,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
}); });
...@@ -830,23 +878,21 @@ ...@@ -830,23 +878,21 @@
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) => ...(responseMessage?.files?.filter((item) =>
['doc', 'collection', 'web_search_results'].includes(item.type) ['doc', 'file', '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
); );
...@@ -886,7 +932,7 @@ ...@@ -886,7 +932,7 @@
: undefined : undefined
)}${ )}${
responseMessage?.userContext ?? null responseMessage?.userContext ?? null
? `\n\nUser Context:\n${(responseMessage?.userContext ?? []).join('\n')}` ? `\n\nUser Context:\n${responseMessage?.userContext ?? ''}`
: '' : ''
}` }`
} }
...@@ -936,11 +982,12 @@ ...@@ -936,11 +982,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
...@@ -1212,6 +1259,7 @@ ...@@ -1212,6 +1259,7 @@
const getWebSearchResults = async (model: string, parentId: string, responseId: string) => { const getWebSearchResults = async (model: string, parentId: string, responseId: string) => {
const responseMessage = history.messages[responseId]; const responseMessage = history.messages[responseId];
const userMessage = history.messages[parentId];
responseMessage.statusHistory = [ responseMessage.statusHistory = [
{ {
...@@ -1222,7 +1270,7 @@ ...@@ -1222,7 +1270,7 @@
]; ];
messages = messages; messages = messages;
const prompt = history.messages[parentId].content; const prompt = userMessage.content;
let searchQuery = await generateSearchQuery(localStorage.token, model, messages, prompt).catch( let searchQuery = await generateSearchQuery(localStorage.token, model, messages, prompt).catch(
(error) => { (error) => {
console.log(error); console.log(error);
...@@ -1322,6 +1370,19 @@ ...@@ -1322,6 +1370,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
...@@ -1333,7 +1394,9 @@ ...@@ -1333,7 +1394,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}
...@@ -1358,9 +1421,9 @@ ...@@ -1358,9 +1421,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) => {
...@@ -1399,6 +1462,7 @@ ...@@ -1399,6 +1462,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 {
...@@ -291,9 +321,11 @@ ...@@ -291,9 +321,11 @@
<div class="flex flex-col max-w-6xl px-2.5 md:px-6 w-full"> <div class="flex flex-col max-w-6xl px-2.5 md:px-6 w-full">
<div class="relative"> <div class="relative">
{#if autoScroll === false && messages.length > 0} {#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 <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={() => { on:click={() => {
autoScroll = true; autoScroll = true;
scrollToBottom(); scrollToBottom();
...@@ -336,9 +368,9 @@ ...@@ -336,9 +368,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 +423,7 @@ ...@@ -391,7 +423,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 +439,6 @@ ...@@ -407,8 +439,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 +450,17 @@ ...@@ -420,30 +450,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 +534,12 @@ ...@@ -517,12 +534,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,6 +28,8 @@ ...@@ -28,6 +28,8 @@
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;
...@@ -269,7 +271,7 @@ ...@@ -269,7 +271,7 @@
return; return;
} }
if (assistantSpeaking) { if (assistantSpeaking && !($settings?.voiceInterruption ?? false)) {
// Mute the audio if the assistant is speaking // Mute the audio if the assistant is speaking
analyser.maxDecibels = 0; analyser.maxDecibels = 0;
analyser.minDecibels = -1; analyser.minDecibels = -1;
...@@ -507,6 +509,8 @@ ...@@ -507,6 +509,8 @@
}; };
onMount(async () => { onMount(async () => {
model = $models.find((m) => m.id === modelId);
startRecording(); startRecording();
const chatStartHandler = async (e) => { const chatStartHandler = async (e) => {
...@@ -657,7 +661,13 @@ ...@@ -657,7 +661,13 @@
? ' 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 -->
...@@ -732,7 +742,13 @@ ...@@ -732,7 +742,13 @@
? 'size-48' ? 'size-48'
: rmsLevel * 100 > 1 : rmsLevel * 100 > 1
? 'size-[11.5rem]' ? '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} {/if}
</button> </button>
......
...@@ -43,11 +43,11 @@ ...@@ -43,11 +43,11 @@
]; ];
$: filteredCollections = collections $: 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)); .sort((a, b) => a.name.localeCompare(b.name));
$: filteredDocs = $documents $: 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)); .sort((a, b) => a.title.localeCompare(b.title));
$: filteredItems = [...filteredCollections, ...filteredDocs]; $: filteredItems = [...filteredCollections, ...filteredDocs];
...@@ -58,6 +58,15 @@ ...@@ -58,6 +58,15 @@
console.log(filteredCollections); 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 = () => { export const selectUp = () => {
selectedIdx = Math.max(0, selectedIdx - 1); selectedIdx = Math.max(0, selectedIdx - 1);
}; };
...@@ -101,7 +110,7 @@ ...@@ -101,7 +110,7 @@
</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 dark:border dark:border-gray-850 rounded-lg"> <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=" 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>
......
...@@ -21,7 +21,9 @@ ...@@ -21,7 +21,9 @@
let filteredModels = []; let filteredModels = [];
$: filteredModels = $models $: 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)); .sort((a, b) => a.name.localeCompare(b.name));
$: if (prompt) { $: if (prompt) {
...@@ -133,7 +135,7 @@ ...@@ -133,7 +135,7 @@
{#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 dark:border dark:border-gray-850 rounded-lg"> <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=" 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>
......
...@@ -12,7 +12,7 @@ ...@@ -12,7 +12,7 @@
let filteredPromptCommands = []; let filteredPromptCommands = [];
$: filteredPromptCommands = $prompts $: filteredPromptCommands = $prompts
.filter((p) => p.command.includes(prompt)) .filter((p) => p.command.toLowerCase().includes(prompt.toLowerCase()))
.sort((a, b) => a.title.localeCompare(b.title)); .sort((a, b) => a.title.localeCompare(b.title));
$: if (prompt) { $: if (prompt) {
...@@ -88,7 +88,7 @@ ...@@ -88,7 +88,7 @@
</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 dark:border dark:border-gray-850 rounded-lg"> <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=" 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>
......
...@@ -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>
......
...@@ -385,7 +385,7 @@ ...@@ -385,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');
...@@ -32,7 +33,7 @@ ...@@ -32,7 +33,7 @@
</script> </script>
{#key mounted} {#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 justify-start">
<div class="flex -space-x-4 mb-1" in:fade={{ duration: 200 }}> <div class="flex -space-x-4 mb-1" in:fade={{ duration: 200 }}>
{#each models as model, modelIdx} {#each models as model, modelIdx}
...@@ -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>
......
...@@ -2,10 +2,12 @@ ...@@ -2,10 +2,12 @@
import { settings } from '$lib/stores'; import { settings } from '$lib/stores';
import { WEBUI_BASE_URL } from '$lib/constants'; import { WEBUI_BASE_URL } from '$lib/constants';
export let className = 'size-8';
export let src = '/user.png'; export let src = '/user.png';
</script> </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 <img
crossorigin="anonymous" crossorigin="anonymous"
src={src.startsWith(WEBUI_BASE_URL) || src={src.startsWith(WEBUI_BASE_URL) ||
...@@ -14,7 +16,7 @@ ...@@ -14,7 +16,7 @@
src.startsWith('/') src.startsWith('/')
? src ? src
: `/user.png`} : `/user.png`}
class=" w-8 object-cover rounded-full" class=" {className} object-cover rounded-full -translate-y-[1px]"
alt="profile" alt="profile"
draggable="false" 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