"...composable_kernel_rocm.git" did not exist on "049cc8afcf880293d4a1bc0ee264f0c681b1b171"
Unverified Commit a58dfccb authored by Timothy Jaeryang Baek's avatar Timothy Jaeryang Baek Committed by GitHub
Browse files

Merge pull request #4273 from open-webui/dev

0.3.11
parents 82079e64 b3529322
/// <reference types="cypress" /> /// <reference types="cypress" />
// eslint-disable-next-line @typescript-eslint/triple-slash-reference
/// <reference path="../support/index.d.ts" />
export const adminUser = { export const adminUser = {
name: 'Admin User', name: 'Admin User',
...@@ -10,6 +12,9 @@ const login = (email: string, password: string) => { ...@@ -10,6 +12,9 @@ const login = (email: string, password: string) => {
return cy.session( return cy.session(
email, email,
() => { () => {
// Make sure to test against us english to have stable tests,
// regardless on local language preferences
localStorage.setItem('locale', 'en-US');
// Visit auth page // Visit auth page
cy.visit('/auth'); cy.visit('/auth');
// Fill out the form // Fill out the form
...@@ -68,6 +73,50 @@ Cypress.Commands.add('register', (name, email, password) => register(name, email ...@@ -68,6 +73,50 @@ Cypress.Commands.add('register', (name, email, password) => register(name, email
Cypress.Commands.add('registerAdmin', () => registerAdmin()); Cypress.Commands.add('registerAdmin', () => registerAdmin());
Cypress.Commands.add('loginAdmin', () => loginAdmin()); Cypress.Commands.add('loginAdmin', () => loginAdmin());
Cypress.Commands.add('uploadTestDocument', (suffix: any) => {
// Login as admin
cy.loginAdmin();
// upload example document
cy.visit('/workspace/documents');
// Create a document
cy.get("button[aria-label='Add Docs']").click();
cy.readFile('cypress/data/example-doc.txt').then((text) => {
// select file
cy.get('#upload-doc-input').selectFile(
{
contents: Cypress.Buffer.from(text + Date.now()),
fileName: `document-test-initial-${suffix}.txt`,
mimeType: 'text/plain',
lastModified: Date.now()
},
{
force: true
}
);
// open tag input
cy.get("button[aria-label='Add Tag']").click();
cy.get("input[placeholder='Add a tag']").type('cypress-test');
cy.get("button[aria-label='Save Tag']").click();
// submit to upload
cy.get("button[type='submit']").click();
// wait for upload to finish
cy.get('button').contains('#cypress-test').should('exist');
cy.get('div').contains(`document-test-initial-${suffix}.txt`).should('exist');
});
});
Cypress.Commands.add('deleteTestDocument', (suffix: any) => {
cy.loginAdmin();
cy.visit('/workspace/documents');
// clean up uploaded documents
cy.get('div')
.contains(`document-test-initial-${suffix}.txt`)
.find("button[aria-label='Delete Doc']")
.click();
});
before(() => { before(() => {
cy.registerAdmin(); cy.registerAdmin();
}); });
...@@ -7,5 +7,7 @@ declare namespace Cypress { ...@@ -7,5 +7,7 @@ declare namespace Cypress {
register(name: string, email: string, password: string): Chainable<Element>; register(name: string, email: string, password: string): Chainable<Element>;
registerAdmin(): Chainable<Element>; registerAdmin(): Chainable<Element>;
loginAdmin(): Chainable<Element>; loginAdmin(): Chainable<Element>;
uploadTestDocument(suffix: any): Chainable<Element>;
deleteTestDocument(suffix: any): Chainable<Element>;
} }
} }
{ {
"name": "open-webui", "name": "open-webui",
"version": "0.3.10", "version": "0.3.11",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "open-webui", "name": "open-webui",
"version": "0.3.10", "version": "0.3.11",
"dependencies": { "dependencies": {
"@codemirror/lang-javascript": "^6.2.2", "@codemirror/lang-javascript": "^6.2.2",
"@codemirror/lang-python": "^6.1.6", "@codemirror/lang-python": "^6.1.6",
...@@ -20,6 +20,7 @@ ...@@ -20,6 +20,7 @@
"dayjs": "^1.11.10", "dayjs": "^1.11.10",
"eventsource-parser": "^1.1.2", "eventsource-parser": "^1.1.2",
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"fuse.js": "^7.0.0",
"highlight.js": "^11.9.0", "highlight.js": "^11.9.0",
"i18next": "^23.10.0", "i18next": "^23.10.0",
"i18next-browser-languagedetector": "^7.2.0", "i18next-browser-languagedetector": "^7.2.0",
...@@ -4820,6 +4821,14 @@ ...@@ -4820,6 +4821,14 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/fuse.js": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-7.0.0.tgz",
"integrity": "sha512-14F4hBIxqKvD4Zz/XjDc3y94mNZN6pRv3U13Udo0lNLCWRBUsrMv2xwcF/y/Z5sV6+FQW+/ow68cHpm4sunt8Q==",
"engines": {
"node": ">=10"
}
},
"node_modules/gc-hook": { "node_modules/gc-hook": {
"version": "0.3.1", "version": "0.3.1",
"resolved": "https://registry.npmjs.org/gc-hook/-/gc-hook-0.3.1.tgz", "resolved": "https://registry.npmjs.org/gc-hook/-/gc-hook-0.3.1.tgz",
......
{ {
"name": "open-webui", "name": "open-webui",
"version": "0.3.10", "version": "0.3.11",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "npm run pyodide:fetch && vite dev --host", "dev": "npm run pyodide:fetch && vite dev --host",
...@@ -60,6 +60,7 @@ ...@@ -60,6 +60,7 @@
"dayjs": "^1.11.10", "dayjs": "^1.11.10",
"eventsource-parser": "^1.1.2", "eventsource-parser": "^1.1.2",
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"fuse.js": "^7.0.0",
"highlight.js": "^11.9.0", "highlight.js": "^11.9.0",
"i18next": "^23.10.0", "i18next": "^23.10.0",
"i18next-browser-languagedetector": "^7.2.0", "i18next-browser-languagedetector": "^7.2.0",
......
...@@ -23,7 +23,7 @@ dependencies = [ ...@@ -23,7 +23,7 @@ dependencies = [
"peewee==3.17.5", "peewee==3.17.5",
"peewee-migrate==1.12.2", "peewee-migrate==1.12.2",
"psycopg2-binary==2.9.9", "psycopg2-binary==2.9.9",
"PyMySQL==1.1.0", "PyMySQL==1.1.1",
"bcrypt==4.1.3", "bcrypt==4.1.3",
"boto3==1.34.110", "boto3==1.34.110",
...@@ -33,7 +33,7 @@ dependencies = [ ...@@ -33,7 +33,7 @@ dependencies = [
"google-generativeai==0.5.4", "google-generativeai==0.5.4",
"langchain==0.2.0", "langchain==0.2.0",
"langchain-community==0.2.0", "langchain-community==0.2.9",
"langchain-chroma==0.1.1", "langchain-chroma==0.1.1",
"fake-useragent==1.5.1", "fake-useragent==1.5.1",
......
...@@ -154,3 +154,7 @@ input[type='number'] { ...@@ -154,3 +154,7 @@ input[type='number'] {
.tippy-box[data-theme~='dark'] { .tippy-box[data-theme~='dark'] {
@apply rounded-lg bg-gray-950 text-xs border border-gray-900 shadow-xl; @apply rounded-lg bg-gray-950 text-xs border border-gray-900 shadow-xl;
} }
.password {
-webkit-text-security: disc;
}
...@@ -131,3 +131,59 @@ export const synthesizeOpenAISpeech = async ( ...@@ -131,3 +131,59 @@ export const synthesizeOpenAISpeech = async (
return res; return res;
}; };
export const getModels = async (token: string = '') => {
let error = null;
const res = await fetch(`${AUDIO_API_BASE_URL}/models`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`
}
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.catch((err) => {
error = err.detail;
console.log(err);
return null;
});
if (error) {
throw error;
}
return res;
};
export const getVoices = async (token: string = '') => {
let error = null;
const res = await fetch(`${AUDIO_API_BASE_URL}/voices`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`
}
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.catch((err) => {
error = err.detail;
console.log(err);
return null;
});
if (error) {
throw error;
}
return res;
};
<script lang="ts"> <script lang="ts">
import { getAudioConfig, updateAudioConfig } from '$lib/apis/audio';
import { user, settings, config } from '$lib/stores';
import { createEventDispatcher, onMount, getContext } from 'svelte';
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';
import Switch from '$lib/components/common/Switch.svelte'; import { createEventDispatcher, onMount, getContext } from 'svelte';
const dispatch = createEventDispatcher();
import { getBackendConfig } from '$lib/apis'; import { getBackendConfig } from '$lib/apis';
import {
getAudioConfig,
updateAudioConfig,
getModels as _getModels,
getVoices as _getVoices
} from '$lib/apis/audio';
import { user, settings, config } from '$lib/stores';
import SensitiveInput from '$lib/components/common/SensitiveInput.svelte'; import SensitiveInput from '$lib/components/common/SensitiveInput.svelte';
const dispatch = createEventDispatcher();
const i18n = getContext('i18n'); const i18n = getContext('i18n');
...@@ -16,6 +22,7 @@ ...@@ -16,6 +22,7 @@
let TTS_OPENAI_API_BASE_URL = ''; let TTS_OPENAI_API_BASE_URL = '';
let TTS_OPENAI_API_KEY = ''; let TTS_OPENAI_API_KEY = '';
let TTS_API_KEY = '';
let TTS_ENGINE = ''; let TTS_ENGINE = '';
let TTS_MODEL = ''; let TTS_MODEL = '';
let TTS_VOICE = ''; let TTS_VOICE = '';
...@@ -29,30 +36,41 @@ ...@@ -29,30 +36,41 @@
let models = []; let models = [];
let nonLocalVoices = false; let nonLocalVoices = false;
const getOpenAIVoices = () => { const getModels = async () => {
voices = [ if (TTS_ENGINE === '') {
{ name: 'alloy' }, models = [];
{ name: 'echo' }, } else {
{ name: 'fable' }, const res = await _getModels(localStorage.token).catch((e) => {
{ name: 'onyx' }, toast.error(e);
{ name: 'nova' }, });
{ name: 'shimmer' }
];
};
const getOpenAIModels = () => { if (res) {
models = [{ name: 'tts-1' }, { name: 'tts-1-hd' }]; console.log(res);
models = res.models;
}
}
}; };
const getWebAPIVoices = () => { const getVoices = async () => {
const getVoicesLoop = setInterval(async () => { if (TTS_ENGINE === '') {
voices = await speechSynthesis.getVoices(); const getVoicesLoop = setInterval(async () => {
voices = await speechSynthesis.getVoices();
// do your loop // do your loop
if (voices.length > 0) { if (voices.length > 0) {
clearInterval(getVoicesLoop); clearInterval(getVoicesLoop);
}
}, 100);
} else {
const res = await _getVoices(localStorage.token).catch((e) => {
toast.error(e);
});
if (res) {
console.log(res);
voices = res.voices;
} }
}, 100); }
}; };
const updateConfigHandler = async () => { const updateConfigHandler = async () => {
...@@ -60,6 +78,7 @@ ...@@ -60,6 +78,7 @@
tts: { tts: {
OPENAI_API_BASE_URL: TTS_OPENAI_API_BASE_URL, OPENAI_API_BASE_URL: TTS_OPENAI_API_BASE_URL,
OPENAI_API_KEY: TTS_OPENAI_API_KEY, OPENAI_API_KEY: TTS_OPENAI_API_KEY,
API_KEY: TTS_API_KEY,
ENGINE: TTS_ENGINE, ENGINE: TTS_ENGINE,
MODEL: TTS_MODEL, MODEL: TTS_MODEL,
VOICE: TTS_VOICE VOICE: TTS_VOICE
...@@ -86,6 +105,7 @@ ...@@ -86,6 +105,7 @@
console.log(res); console.log(res);
TTS_OPENAI_API_BASE_URL = res.tts.OPENAI_API_BASE_URL; TTS_OPENAI_API_BASE_URL = res.tts.OPENAI_API_BASE_URL;
TTS_OPENAI_API_KEY = res.tts.OPENAI_API_KEY; TTS_OPENAI_API_KEY = res.tts.OPENAI_API_KEY;
TTS_API_KEY = res.tts.API_KEY;
TTS_ENGINE = res.tts.ENGINE; TTS_ENGINE = res.tts.ENGINE;
TTS_MODEL = res.tts.MODEL; TTS_MODEL = res.tts.MODEL;
...@@ -98,12 +118,8 @@ ...@@ -98,12 +118,8 @@
STT_MODEL = res.stt.MODEL; STT_MODEL = res.stt.MODEL;
} }
if (TTS_ENGINE === 'openai') { await getVoices();
getOpenAIVoices(); await getModels();
getOpenAIModels();
} else {
getWebAPIVoices();
}
}); });
</script> </script>
...@@ -138,7 +154,7 @@ ...@@ -138,7 +154,7 @@
<div> <div>
<div class="mt-1 flex gap-2 mb-1"> <div class="mt-1 flex gap-2 mb-1">
<input <input
class="flex-1 w-full rounded-l-lg py-2 pl-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none" class="flex-1 w-full rounded-lg py-2 pl-4 text-sm bg-gray-50 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
...@@ -182,19 +198,23 @@ ...@@ -182,19 +198,23 @@
class=" dark:bg-gray-900 w-fit pr-8 rounded px-2 p-1 text-xs bg-transparent outline-none text-right" class=" dark:bg-gray-900 w-fit pr-8 rounded px-2 p-1 text-xs bg-transparent outline-none text-right"
bind:value={TTS_ENGINE} bind:value={TTS_ENGINE}
placeholder="Select a mode" placeholder="Select a mode"
on:change={(e) => { on:change={async (e) => {
await updateConfigHandler();
await getVoices();
await getModels();
if (e.target.value === 'openai') { if (e.target.value === 'openai') {
getOpenAIVoices();
TTS_VOICE = 'alloy'; TTS_VOICE = 'alloy';
TTS_MODEL = 'tts-1'; TTS_MODEL = 'tts-1';
} else { } else {
getWebAPIVoices();
TTS_VOICE = ''; TTS_VOICE = '';
TTS_MODEL = '';
} }
}} }}
> >
<option value="">{$i18n.t('Web API')}</option> <option value="">{$i18n.t('Web API')}</option>
<option value="openai">{$i18n.t('OpenAI')}</option> <option value="openai">{$i18n.t('OpenAI')}</option>
<option value="elevenlabs">{$i18n.t('ElevenLabs')}</option>
</select> </select>
</div> </div>
</div> </div>
...@@ -203,7 +223,7 @@ ...@@ -203,7 +223,7 @@
<div> <div>
<div class="mt-1 flex gap-2 mb-1"> <div class="mt-1 flex gap-2 mb-1">
<input <input
class="flex-1 w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none" class="flex-1 w-full rounded-lg py-2 pl-4 text-sm bg-gray-50 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
...@@ -212,6 +232,17 @@ ...@@ -212,6 +232,17 @@
<SensitiveInput placeholder={$i18n.t('API Key')} bind:value={TTS_OPENAI_API_KEY} /> <SensitiveInput placeholder={$i18n.t('API Key')} bind:value={TTS_OPENAI_API_KEY} />
</div> </div>
</div> </div>
{:else if TTS_ENGINE === 'elevenlabs'}
<div>
<div class="mt-1 flex gap-2 mb-1">
<input
class="flex-1 w-full rounded-lg py-2 pl-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
placeholder={$i18n.t('API Key')}
bind:value={TTS_API_KEY}
required
/>
</div>
</div>
{/if} {/if}
<hr class=" dark:border-gray-850 my-2" /> <hr class=" dark:border-gray-850 my-2" />
...@@ -252,7 +283,48 @@ ...@@ -252,7 +283,48 @@
<datalist id="voice-list"> <datalist id="voice-list">
{#each voices as voice} {#each voices as voice}
<option value={voice.name} /> <option value={voice.id}>{voice.name}</option>
{/each}
</datalist>
</div>
</div>
</div>
<div class="w-full">
<div class=" mb-1.5 text-sm font-medium">{$i18n.t('TTS Model')}</div>
<div class="flex w-full">
<div class="flex-1">
<input
list="tts-model-list"
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
bind:value={TTS_MODEL}
placeholder="Select a model"
/>
<datalist id="tts-model-list">
{#each models as model}
<option value={model.id} />
{/each}
</datalist>
</div>
</div>
</div>
</div>
{:else if TTS_ENGINE === 'elevenlabs'}
<div class=" flex gap-2">
<div class="w-full">
<div class=" mb-1.5 text-sm font-medium">{$i18n.t('TTS Voice')}</div>
<div class="flex w-full">
<div class="flex-1">
<input
list="voice-list"
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
bind:value={TTS_VOICE}
placeholder="Select a voice"
/>
<datalist id="voice-list">
{#each voices as voice}
<option value={voice.id}>{voice.name}</option>
{/each} {/each}
</datalist> </datalist>
</div> </div>
...@@ -263,15 +335,15 @@ ...@@ -263,15 +335,15 @@
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1"> <div class="flex-1">
<input <input
list="model-list" list="tts-model-list"
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none" class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
bind:value={TTS_MODEL} bind:value={TTS_MODEL}
placeholder="Select a model" placeholder="Select a model"
/> />
<datalist id="model-list"> <datalist id="tts-model-list">
{#each models as model} {#each models as model}
<option value={model.name} /> <option value={model.id} />
{/each} {/each}
</datalist> </datalist>
</div> </div>
......
...@@ -269,7 +269,7 @@ ...@@ -269,7 +269,7 @@
saveHandler(); saveHandler();
}} }}
> >
<div class=" space-y-2.5 overflow-y-scroll scrollbar-hidden h-full"> <div class=" space-y-2.5 overflow-y-scroll scrollbar-hidden h-full pr-1.5">
<div class="flex flex-col gap-0.5"> <div class="flex flex-col gap-0.5">
<div class=" mb-0.5 text-sm font-medium">{$i18n.t('General Settings')}</div> <div class=" mb-0.5 text-sm font-medium">{$i18n.t('General Settings')}</div>
...@@ -615,7 +615,7 @@ ...@@ -615,7 +615,7 @@
<div class=" "> <div class=" ">
<div class=" text-sm font-medium">{$i18n.t('Query Params')}</div> <div class=" text-sm font-medium">{$i18n.t('Query Params')}</div>
<div class=" flex"> <div class=" flex gap-1">
<div class=" flex w-full justify-between"> <div class=" flex w-full justify-between">
<div class="self-center text-xs font-medium min-w-fit">{$i18n.t('Top K')}</div> <div class="self-center text-xs font-medium min-w-fit">{$i18n.t('Top K')}</div>
...@@ -632,7 +632,7 @@ ...@@ -632,7 +632,7 @@
</div> </div>
{#if querySettings.hybrid === true} {#if querySettings.hybrid === true}
<div class="flex w-full"> <div class=" flex w-full justify-between">
<div class=" self-center text-xs font-medium min-w-fit"> <div class=" self-center text-xs font-medium min-w-fit">
{$i18n.t('Minimum Score')} {$i18n.t('Minimum Score')}
</div> </div>
......
...@@ -80,6 +80,7 @@ ...@@ -80,6 +80,7 @@
let eventConfirmationMessage = ''; let eventConfirmationMessage = '';
let eventConfirmationInput = false; let eventConfirmationInput = false;
let eventConfirmationInputPlaceholder = ''; let eventConfirmationInputPlaceholder = '';
let eventConfirmationInputValue = '';
let eventCallback = null; let eventCallback = null;
let showModelSelector = true; let showModelSelector = true;
...@@ -108,7 +109,6 @@ ...@@ -108,7 +109,6 @@
}; };
let params = {}; let params = {};
let valves = {};
$: if (history.currentId !== null) { $: if (history.currentId !== null) {
let _messages = []; let _messages = [];
...@@ -182,6 +182,7 @@ ...@@ -182,6 +182,7 @@
eventConfirmationTitle = data.title; eventConfirmationTitle = data.title;
eventConfirmationMessage = data.message; eventConfirmationMessage = data.message;
eventConfirmationInputPlaceholder = data.placeholder; eventConfirmationInputPlaceholder = data.placeholder;
eventConfirmationInputValue = data?.value ?? '';
} else { } else {
console.log('Unknown message type', data); console.log('Unknown message type', data);
} }
...@@ -281,6 +282,10 @@ ...@@ -281,6 +282,10 @@
if ($page.url.searchParams.get('q')) { if ($page.url.searchParams.get('q')) {
prompt = $page.url.searchParams.get('q') ?? ''; prompt = $page.url.searchParams.get('q') ?? '';
selectedToolIds = ($page.url.searchParams.get('tool_ids') ?? '')
.split(',')
.map((id) => id.trim())
.filter((id) => id);
if (prompt) { if (prompt) {
await tick(); await tick();
...@@ -706,6 +711,7 @@ ...@@ -706,6 +711,7 @@
let _response = null; let _response = null;
const responseMessage = history.messages[responseMessageId]; const responseMessage = history.messages[responseMessageId];
const userMessage = history.messages[responseMessage.parentId];
// Wait until history/message have been updated // Wait until history/message have been updated
await tick(); await tick();
...@@ -772,11 +778,12 @@ ...@@ -772,11 +778,12 @@
if (model?.info?.meta?.knowledge ?? false) { if (model?.info?.meta?.knowledge ?? false) {
files.push(...model.info.meta.knowledge); files.push(...model.info.meta.knowledge);
} }
if (responseMessage?.files) { files.push(
files.push( ...(userMessage?.files ?? []).filter((item) =>
...responseMessage?.files.filter((item) => ['web_search_results'].includes(item.type)) ['doc', 'file', 'collection'].includes(item.type)
); ),
} ...(responseMessage?.files ?? []).filter((item) => ['web_search_results'].includes(item.type))
);
eventTarget.dispatchEvent( eventTarget.dispatchEvent(
new CustomEvent('chat:start', { new CustomEvent('chat:start', {
...@@ -808,7 +815,6 @@ ...@@ -808,7 +815,6 @@
keep_alive: $settings.keepAlive ?? undefined, keep_alive: $settings.keepAlive ?? undefined,
tool_ids: selectedToolIds.length > 0 ? selectedToolIds : undefined, tool_ids: selectedToolIds.length > 0 ? selectedToolIds : undefined,
files: files.length > 0 ? files : undefined, files: files.length > 0 ? files : undefined,
...(Object.keys(valves).length ? { valves } : {}),
session_id: $socket?.id, session_id: $socket?.id,
chat_id: $chatId, chat_id: $chatId,
id: responseMessageId id: responseMessageId
...@@ -1006,17 +1012,20 @@ ...@@ -1006,17 +1012,20 @@
const sendPromptOpenAI = async (model, userPrompt, responseMessageId, _chatId) => { const sendPromptOpenAI = async (model, userPrompt, responseMessageId, _chatId) => {
let _response = null; let _response = null;
const responseMessage = history.messages[responseMessageId]; const responseMessage = history.messages[responseMessageId];
const userMessage = history.messages[responseMessage.parentId];
let files = JSON.parse(JSON.stringify(chatFiles)); let files = JSON.parse(JSON.stringify(chatFiles));
if (model?.info?.meta?.knowledge ?? false) { if (model?.info?.meta?.knowledge ?? false) {
files.push(...model.info.meta.knowledge); files.push(...model.info.meta.knowledge);
} }
if (responseMessage?.files) { files.push(
files.push( ...(userMessage?.files ?? []).filter((item) =>
...responseMessage?.files.filter((item) => ['web_search_results'].includes(item.type)) ['doc', 'file', 'collection'].includes(item.type)
); ),
} ...(responseMessage?.files ?? []).filter((item) => ['web_search_results'].includes(item.type))
);
scrollToBottom(); scrollToBottom();
...@@ -1105,7 +1114,6 @@ ...@@ -1105,7 +1114,6 @@
max_tokens: params?.max_tokens ?? $settings?.params?.max_tokens ?? undefined, max_tokens: params?.max_tokens ?? $settings?.params?.max_tokens ?? undefined,
tool_ids: selectedToolIds.length > 0 ? selectedToolIds : undefined, tool_ids: selectedToolIds.length > 0 ? selectedToolIds : undefined,
files: files.length > 0 ? files : undefined, files: files.length > 0 ? files : undefined,
...(Object.keys(valves).length ? { valves } : {}),
session_id: $socket?.id, session_id: $socket?.id,
chat_id: $chatId, chat_id: $chatId,
id: responseMessageId id: responseMessageId
...@@ -1484,6 +1492,7 @@ ...@@ -1484,6 +1492,7 @@
message={eventConfirmationMessage} message={eventConfirmationMessage}
input={eventConfirmationInput} input={eventConfirmationInput}
inputPlaceholder={eventConfirmationInputPlaceholder} inputPlaceholder={eventConfirmationInputPlaceholder}
inputValue={eventConfirmationInputValue}
on:confirm={(e) => { on:confirm={(e) => {
if (e.detail) { if (e.detail) {
eventCallback(e.detail); eventCallback(e.detail);
...@@ -1631,7 +1640,6 @@ ...@@ -1631,7 +1640,6 @@
bind:show={showControls} bind:show={showControls}
bind:chatFiles bind:chatFiles
bind:params bind:params
bind:valves
/> />
</div> </div>
{/if} {/if}
...@@ -9,9 +9,7 @@ ...@@ -9,9 +9,7 @@
export let models = []; export let models = [];
export let chatId = null; export let chatId = null;
export let chatFiles = []; export let chatFiles = [];
export let valves = {};
export let params = {}; export let params = {};
let largeScreen = false; let largeScreen = false;
...@@ -50,7 +48,6 @@ ...@@ -50,7 +48,6 @@
}} }}
{models} {models}
bind:chatFiles bind:chatFiles
bind:valves
bind:params bind:params
/> />
</div> </div>
...@@ -66,7 +63,6 @@ ...@@ -66,7 +63,6 @@
}} }}
{models} {models}
bind:chatFiles bind:chatFiles
bind:valves
bind:params bind:params
/> />
</div> </div>
......
...@@ -5,13 +5,13 @@ ...@@ -5,13 +5,13 @@
import XMark from '$lib/components/icons/XMark.svelte'; import XMark from '$lib/components/icons/XMark.svelte';
import AdvancedParams from '../Settings/Advanced/AdvancedParams.svelte'; import AdvancedParams from '../Settings/Advanced/AdvancedParams.svelte';
import Valves from '$lib/components/common/Valves.svelte'; import Valves from '$lib/components/chat/Controls/Valves.svelte';
import FileItem from '$lib/components/common/FileItem.svelte'; import FileItem from '$lib/components/common/FileItem.svelte';
import Collapsible from '$lib/components/common/Collapsible.svelte';
export let models = []; export let models = [];
export let chatFiles = []; export let chatFiles = [];
export let valves = {};
export let params = {}; export let params = {};
</script> </script>
...@@ -28,18 +28,17 @@ ...@@ -28,18 +28,17 @@
</button> </button>
</div> </div>
<div class=" dark:text-gray-200 text-sm font-primary"> <div class=" dark:text-gray-200 text-sm font-primary py-0.5">
{#if chatFiles.length > 0} {#if chatFiles.length > 0}
<div> <Collapsible title={$i18n.t('Files')} open={true}>
<div class="mb-1.5 font-medium">{$i18n.t('Files')}</div> <div class="flex flex-col gap-1 mt-1.5" slot="content">
<div class="flex flex-col gap-1">
{#each chatFiles as file, fileIdx} {#each chatFiles as file, fileIdx}
<FileItem <FileItem
className="w-full" className="w-full"
url={`${file?.url}`} url={`${file?.url}`}
name={file.name} name={file.name}
type={file.type} type={file.type}
size={file?.size}
dismissible={true} dismissible={true}
on:dismiss={() => { on:dismiss={() => {
// Remove the file from the chatFiles array // Remove the file from the chatFiles array
...@@ -50,44 +49,38 @@ ...@@ -50,44 +49,38 @@
/> />
{/each} {/each}
</div> </div>
</div> </Collapsible>
<hr class="my-2 border-gray-100 dark:border-gray-800" /> <hr class="my-2 border-gray-100 dark:border-gray-800" />
{/if} {/if}
{#if models.length === 1 && models[0]?.pipe?.valves_spec} <Collapsible title={$i18n.t('Valves')}>
<div> <div class="text-sm mt-1.5" slot="content">
<div class=" font-medium">{$i18n.t('Valves')}</div> <Valves />
<div>
<Valves valvesSpec={models[0]?.pipe?.valves_spec} bind:valves />
</div>
</div> </div>
</Collapsible>
<hr class="my-2 border-gray-100 dark:border-gray-800" /> <hr class="my-2 border-gray-100 dark:border-gray-800" />
{/if}
<div>
<div class="mb-1.5 font-medium">{$i18n.t('System Prompt')}</div>
<div> <Collapsible title={$i18n.t('System Prompt')} open={true}>
<div class=" mt-1.5" slot="content">
<textarea <textarea
bind:value={params.system} bind:value={params.system}
class="w-full rounded-lg px-4 py-3 text-sm dark:text-gray-300 dark:bg-gray-850 border border-gray-100 dark:border-gray-800 outline-none resize-none" class="w-full rounded-lg px-3.5 py-2.5 text-sm dark:text-gray-300 dark:bg-gray-850 border border-gray-100 dark:border-gray-800 outline-none resize-none"
rows="3" rows="4"
placeholder={$i18n.t('Enter system prompt')} placeholder={$i18n.t('Enter system prompt')}
/> />
</div> </div>
</div> </Collapsible>
<hr class="my-2 border-gray-100 dark:border-gray-800" /> <hr class="my-2 border-gray-100 dark:border-gray-800" />
<div> <Collapsible title={$i18n.t('Advanced Params')} open={true}>
<div class="mb-1.5 font-medium">{$i18n.t('Advanced Params')}</div> <div class="text-sm mt-1.5" slot="content">
<div>
<div> <AdvancedParams bind:params />
<AdvancedParams bind:params /> </div>
</div> </div>
</div> </Collapsible>
</div> </div>
</div> </div>
...@@ -15,18 +15,14 @@ ...@@ -15,18 +15,14 @@
updateUserValvesById as updateFunctionUserValvesById updateUserValvesById as updateFunctionUserValvesById
} from '$lib/apis/functions'; } from '$lib/apis/functions';
import ManageModal from './Personalization/ManageModal.svelte';
import Tooltip from '$lib/components/common/Tooltip.svelte'; import Tooltip from '$lib/components/common/Tooltip.svelte';
import Spinner from '$lib/components/common/Spinner.svelte'; import Spinner from '$lib/components/common/Spinner.svelte';
import Switch from '$lib/components/common/Switch.svelte';
import Valves from '$lib/components/common/Valves.svelte'; import Valves from '$lib/components/common/Valves.svelte';
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
const i18n = getContext('i18n'); const i18n = getContext('i18n');
export let saveSettings: Function;
let tab = 'tools'; let tab = 'tools';
let selectedId = ''; let selectedId = '';
...@@ -35,6 +31,19 @@ ...@@ -35,6 +31,19 @@
let valvesSpec = null; let valvesSpec = null;
let valves = {}; let valves = {};
let debounceTimer;
const debounceSubmitHandler = async () => {
if (debounceTimer) {
clearTimeout(debounceTimer);
}
// Set a new timer
debounceTimer = setTimeout(() => {
submitHandler();
}, 500); // 0.5 second debounce
};
const getUserValves = async () => { const getUserValves = async () => {
loading = true; loading = true;
if (tab === 'tools') { if (tab === 'tools') {
...@@ -112,53 +121,45 @@ ...@@ -112,53 +121,45 @@
dispatch('save'); dispatch('save');
}} }}
> >
<div class="flex flex-col pr-1.5 overflow-y-scroll max-h-[25rem]"> <div class="flex flex-col">
<div> <div class="space-y-1">
<div class="flex items-center justify-between mb-2"> <div class="flex gap-2">
<Tooltip content=""> <div class="flex-1">
<div class="text-sm font-medium">
{$i18n.t('Manage Valves')}
</div>
</Tooltip>
<div class=" self-end">
<select <select
class=" dark:bg-gray-900 w-fit pr-8 rounded text-xs bg-transparent outline-none text-right" class=" w-full rounded text-xs py-2 px-1 bg-transparent outline-none"
bind:value={tab} bind:value={tab}
placeholder="Select" placeholder="Select"
> >
<option value="tools">{$i18n.t('Tools')}</option> <option value="tools" class="bg-gray-100 dark:bg-gray-800">{$i18n.t('Tools')}</option>
<option value="functions">{$i18n.t('Functions')}</option> <option value="functions" class="bg-gray-100 dark:bg-gray-800"
>{$i18n.t('Functions')}</option
>
</select> </select>
</div> </div>
</div>
</div>
<div class="space-y-1">
<div class="flex gap-2">
<div class="flex-1"> <div class="flex-1">
<select <select
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none" class="w-full rounded py-2 px-1 text-xs bg-transparent outline-none"
bind:value={selectedId} bind:value={selectedId}
on:change={async () => { on:change={async () => {
await tick(); await tick();
}} }}
> >
{#if tab === 'tools'} {#if tab === 'tools'}
<option value="" selected disabled class="bg-gray-100 dark:bg-gray-700" <option value="" selected disabled class="bg-gray-100 dark:bg-gray-800"
>{$i18n.t('Select a tool')}</option >{$i18n.t('Select a tool')}</option
> >
{#each $tools as tool, toolIdx} {#each $tools as tool, toolIdx}
<option value={tool.id} class="bg-gray-100 dark:bg-gray-700">{tool.name}</option> <option value={tool.id} class="bg-gray-100 dark:bg-gray-800">{tool.name}</option>
{/each} {/each}
{:else if tab === 'functions'} {:else if tab === 'functions'}
<option value="" selected disabled class="bg-gray-100 dark:bg-gray-700" <option value="" selected disabled class="bg-gray-100 dark:bg-gray-800"
>{$i18n.t('Select a function')}</option >{$i18n.t('Select a function')}</option
> >
{#each $functions as func, funcIdx} {#each $functions as func, funcIdx}
<option value={func.id} class="bg-gray-100 dark:bg-700">{func.name}</option> <option value={func.id} class="bg-gray-100 dark:bg-gray-800">{func.name}</option>
{/each} {/each}
{/if} {/if}
</select> </select>
...@@ -167,24 +168,21 @@ ...@@ -167,24 +168,21 @@
</div> </div>
{#if selectedId} {#if selectedId}
<hr class="dark:border-gray-800 my-3 w-full" /> <hr class="dark:border-gray-800 my-1 w-full" />
<div> <div class="my-2 text-xs">
{#if !loading} {#if !loading}
<Valves {valvesSpec} bind:valves /> <Valves
{valvesSpec}
bind:valves
on:change={() => {
debounceSubmitHandler();
}}
/>
{:else} {:else}
<Spinner className="size-5" /> <Spinner className="size-5" />
{/if} {/if}
</div> </div>
{/if} {/if}
</div> </div>
<div class="flex justify-end text-sm font-medium">
<button
class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg"
type="submit"
>
{$i18n.t('Save')}
</button>
</div>
</form> </form>
...@@ -98,6 +98,7 @@ ...@@ -98,6 +98,7 @@
const uploadFileHandler = 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 // Check if the file is an audio file and transcribe/convert it to text file
if (['audio/mpeg', 'audio/wav'].includes(file['type'])) { if (['audio/mpeg', 'audio/wav'].includes(file['type'])) {
const res = await transcribeAudio(localStorage.token, file).catch((error) => { const res = await transcribeAudio(localStorage.token, file).catch((error) => {
...@@ -112,40 +113,49 @@ ...@@ -112,40 +113,49 @@
} }
} }
// Upload the file to the server const fileItem = {
const uploadedFile = await uploadFile(localStorage.token, file).catch((error) => { type: 'file',
toast.error(error); file: '',
return null; id: null,
}); url: '',
name: file.name,
if (uploadedFile) { collection_name: '',
const fileItem = { status: '',
type: 'file', size: file.size,
file: uploadedFile, error: ''
id: uploadedFile.id, };
url: `${WEBUI_API_BASE_URL}/files/${uploadedFile.id}`, files = [...files, fileItem];
name: file.name,
collection_name: '', try {
status: 'uploaded', const uploadedFile = await uploadFile(localStorage.token, file);
error: ''
}; if (uploadedFile) {
files = [...files, fileItem]; fileItem.status = 'uploaded';
fileItem.file = uploadedFile;
// TODO: Check if tools & functions have files support to skip this step to delegate file processing fileItem.id = uploadedFile.id;
// Default Upload to VectorDB fileItem.url = `${WEBUI_API_BASE_URL}/files/${uploadedFile.id}`;
if (
SUPPORTED_FILE_TYPE.includes(file['type']) || // TODO: Check if tools & functions have files support to skip this step to delegate file processing
SUPPORTED_FILE_EXTENSIONS.includes(file.name.split('.').at(-1)) // Default Upload to VectorDB
) { if (
processFileItem(fileItem); 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);
}
} else { } else {
toast.error( files = files.filter((item) => item.status !== null);
$i18n.t(`Unknown file type '{{file_type}}'. Proceeding with the file upload anyway.`, {
file_type: file['type']
})
);
processFileItem(fileItem);
} }
} catch (e) {
toast.error(e);
files = files.filter((item) => item.status !== null);
} }
}; };
...@@ -162,7 +172,6 @@ ...@@ -162,7 +172,6 @@
// Remove the failed doc from the files array // Remove the failed doc from the files array
// files = files.filter((f) => f.id !== fileItem.id); // files = files.filter((f) => f.id !== fileItem.id);
toast.error(e); toast.error(e);
fileItem.status = 'processed'; fileItem.status = 'processed';
files = files; files = files;
} }
...@@ -545,6 +554,7 @@ ...@@ -545,6 +554,7 @@
<FileItem <FileItem
name={file.name} name={file.name}
type={file.type} type={file.type}
size={file?.size}
status={file.status} status={file.status}
dismissible={true} dismissible={true}
on:dismiss={() => { on:dismiss={() => {
......
...@@ -175,7 +175,10 @@ ...@@ -175,7 +175,10 @@
delimiters: [ delimiters: [
{ left: '$$', right: '$$', display: false }, { left: '$$', right: '$$', display: false },
{ left: '$ ', right: ' $', display: false }, { left: '$ ', right: ' $', display: false },
{ left: '\\pu{', right: '}', display: false },
{ left: '\\ce{', right: '}', display: false },
{ left: '\\(', right: '\\)', display: false }, { left: '\\(', right: '\\)', display: false },
{ left: '( ', right: ' )', display: false },
{ left: '\\[', right: '\\]', display: false }, { left: '\\[', right: '\\]', display: false },
{ left: '[ ', right: ' ]', display: false } { left: '[ ', right: ' ]', display: false }
], ],
...@@ -218,7 +221,7 @@ ...@@ -218,7 +221,7 @@
if ((message?.content ?? '').trim() !== '') { if ((message?.content ?? '').trim() !== '') {
speaking = true; speaking = true;
if ($config.audio.tts.engine === 'openai') { if ($config.audio.tts.engine !== '') {
loadingSpeech = true; loadingSpeech = true;
const sentences = extractSentences(message.content).reduce((mergedTexts, currentText) => { const sentences = extractSentences(message.content).reduce((mergedTexts, currentText) => {
...@@ -250,7 +253,9 @@ ...@@ -250,7 +253,9 @@
for (const [idx, sentence] of sentences.entries()) { for (const [idx, sentence] of sentences.entries()) {
const res = await synthesizeOpenAISpeech( const res = await synthesizeOpenAISpeech(
localStorage.token, localStorage.token,
$settings?.audio?.tts?.voice ?? $config?.audio?.tts?.voice, $settings?.audio?.tts?.defaultVoice === $config.audio.tts.voice
? $settings?.audio?.tts?.voice ?? $config?.audio?.tts?.voice
: $config?.audio?.tts?.voice,
sentence sentence
).catch((error) => { ).catch((error) => {
toast.error(error); toast.error(error);
...@@ -433,7 +438,7 @@ ...@@ -433,7 +438,7 @@
{@const status = ( {@const status = (
message?.statusHistory ?? [...(message?.status ? [message?.status] : [])] message?.statusHistory ?? [...(message?.status ? [message?.status] : [])]
).at(-1)} ).at(-1)}
<div class="flex items-center gap-2 pt-1 pb-1"> <div class="flex items-center gap-2 pt-0.5 pb-1">
{#if status.done === false} {#if status.done === false}
<div class=""> <div class="">
<Spinner className="size-4" /> <Spinner className="size-4" />
......
...@@ -20,7 +20,10 @@ ...@@ -20,7 +20,10 @@
<ChevronDown strokeWidth="3.5" className="size-3.5 " /> <ChevronDown strokeWidth="3.5" className="size-3.5 " />
{/if} {/if}
</div> </div>
<div class="text-sm border border-gray-300/30 dark:border-gray-700/50 rounded-xl" slot="content"> <div
class="text-sm border border-gray-300/30 dark:border-gray-700/50 rounded-xl mb-1.5"
slot="content"
>
{#if status?.query} {#if status?.query}
<a <a
href="https://www.google.com/search?q={status.query}" href="https://www.google.com/search?q={status.query}"
......
...@@ -100,7 +100,13 @@ ...@@ -100,7 +100,13 @@
{#if file.type === 'image'} {#if file.type === 'image'}
<img src={file.url} alt="input" class=" max-h-96 rounded-lg" draggable="false" /> <img src={file.url} alt="input" class=" max-h-96 rounded-lg" draggable="false" />
{:else} {:else}
<FileItem url={file.url} name={file.name} type={file.type} /> <FileItem
url={file.url}
name={file.name}
type={file.type}
size={file?.size}
colorClassName="bg-white dark:bg-gray-850 "
/>
{/if} {/if}
</div> </div>
{/each} {/each}
......
<script lang="ts"> <script lang="ts">
import { DropdownMenu } from 'bits-ui'; import { DropdownMenu } from 'bits-ui';
import { marked } from 'marked'; import { marked } from 'marked';
import Fuse from 'fuse.js';
import { flyAndScale } from '$lib/utils/transitions'; import { flyAndScale } from '$lib/utils/transitions';
import { createEventDispatcher, onMount, getContext, tick } from 'svelte'; import { createEventDispatcher, onMount, getContext, tick } from 'svelte';
...@@ -33,7 +34,7 @@ ...@@ -33,7 +34,7 @@
[key: string]: any; [key: string]: any;
} = []; } = [];
export let className = 'w-[30rem]'; export let className = 'w-[32rem]';
let show = false; let show = false;
...@@ -43,17 +44,31 @@ ...@@ -43,17 +44,31 @@
let searchValue = ''; let searchValue = '';
let ollamaVersion = null; let ollamaVersion = null;
$: filteredItems = items.filter( let selectedModelIdx = 0;
(item) =>
(searchValue const fuse = new Fuse(
? item.value.toLowerCase().includes(searchValue.toLowerCase()) || items
item.label.toLowerCase().includes(searchValue.toLowerCase()) || .filter((item) => !item.model?.info?.meta?.hidden)
(item.model?.info?.meta?.tags ?? []).some((tag) => .map((item) => {
tag.name.toLowerCase().includes(searchValue.toLowerCase()) const _item = {
) ...item,
: true) && !(item.model?.info?.meta?.hidden ?? false) modelName: item.model?.name,
tags: item.model?.info?.meta?.tags?.map((tag) => tag.name).join(' '),
desc: item.model?.info?.meta?.description
};
return _item;
}),
{
keys: ['value', 'label', 'tags', 'desc', 'modelName']
}
); );
$: filteredItems = searchValue
? fuse.search(searchValue).map((e) => {
return e.item;
})
: items.filter((item) => !item.model?.info?.meta?.hidden);
const pullModelHandler = async () => { const pullModelHandler = async () => {
const sanitizedModelTag = searchValue.trim().replace(/^ollama\s+(run|pull)\s+/, ''); const sanitizedModelTag = searchValue.trim().replace(/^ollama\s+(run|pull)\s+/, '');
...@@ -202,6 +217,7 @@ ...@@ -202,6 +217,7 @@
bind:open={show} bind:open={show}
onOpenChange={async () => { onOpenChange={async () => {
searchValue = ''; searchValue = '';
selectedModelIdx = 0;
window.setTimeout(() => document.getElementById('model-search-input')?.focus(), 0); window.setTimeout(() => document.getElementById('model-search-input')?.focus(), 0);
}} }}
closeFocus={false} closeFocus={false}
...@@ -238,19 +254,41 @@ ...@@ -238,19 +254,41 @@
class="w-full text-sm bg-transparent outline-none" class="w-full text-sm bg-transparent outline-none"
placeholder={searchPlaceholder} placeholder={searchPlaceholder}
autocomplete="off" autocomplete="off"
on:keydown={(e) => {
if (e.code === 'Enter' && filteredItems.length > 0) {
value = filteredItems[selectedModelIdx].value;
show = false;
return; // dont need to scroll on selection
} else if (e.code === 'ArrowDown') {
selectedModelIdx = Math.min(selectedModelIdx + 1, filteredItems.length - 1);
} else if (e.code === 'ArrowUp') {
selectedModelIdx = Math.max(selectedModelIdx - 1, 0);
} else {
// if the user types something, reset to the top selection.
selectedModelIdx = 0;
}
const item = document.querySelector(`[data-arrow-selected="true"]`);
item?.scrollIntoView({ block: 'center', inline: 'nearest', behavior: 'instant' });
}}
/> />
</div> </div>
<hr class="border-gray-100 dark:border-gray-800" /> <hr class="border-gray-100 dark:border-gray-800" />
{/if} {/if}
<div class="px-3 my-2 max-h-64 overflow-y-auto scrollbar-hidden"> <div class="px-3 my-2 max-h-64 overflow-y-auto scrollbar-hidden group">
{#each filteredItems as item} {#each filteredItems as item, index}
<button <button
aria-label="model-item" aria-label="model-item"
class="flex w-full text-left font-medium line-clamp-1 select-none items-center rounded-button py-2 pl-3 pr-1.5 text-sm text-gray-700 dark:text-gray-100 outline-none transition-all duration-75 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg cursor-pointer data-[highlighted]:bg-muted" class="flex w-full text-left font-medium line-clamp-1 select-none items-center rounded-button py-2 pl-3 pr-1.5 text-sm text-gray-700 dark:text-gray-100 outline-none transition-all duration-75 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg cursor-pointer data-[highlighted]:bg-muted {index ===
selectedModelIdx
? 'bg-gray-100 dark:bg-gray-800 group-hover:bg-transparent'
: ''}"
data-arrow-selected={index === selectedModelIdx}
on:click={() => { on:click={() => {
value = item.value; value = item.value;
selectedModelIdx = index;
show = false; show = false;
}} }}
...@@ -270,7 +308,14 @@ ...@@ -270,7 +308,14 @@
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<div class="flex items-center min-w-fit"> <div class="flex items-center min-w-fit">
<div class="line-clamp-1"> <div class="line-clamp-1">
{item.label} <div class="flex items-center min-w-fit">
<img
src={item.model?.info?.meta?.profile_image_url ?? '/static/favicon.png'}
alt="Model"
class="rounded-full size-5 flex items-center mr-2"
/>
{item.label}
</div>
</div> </div>
{#if item.model.owned_by === 'ollama' && (item.model.ollama?.details?.parameter_size ?? '') !== ''} {#if item.model.owned_by === 'ollama' && (item.model.ollama?.details?.parameter_size ?? '') !== ''}
<div class="flex ml-1 items-center translate-y-[0.5px]"> <div class="flex ml-1 items-center translate-y-[0.5px]">
...@@ -295,23 +340,11 @@ ...@@ -295,23 +340,11 @@
{/if} {/if}
</div> </div>
{#if !$mobile && (item?.model?.info?.meta?.tags ?? []).length > 0}
<div class="flex gap-0.5 self-center items-center h-full translate-y-[0.5px]">
{#each item.model?.info?.meta.tags as tag}
<div
class=" text-xs font-bold px-1 rounded uppercase line-clamp-1 bg-gray-500/20 text-gray-700 dark:text-gray-200"
>
{tag.name}
</div>
{/each}
</div>
{/if}
<!-- {JSON.stringify(item.info)} --> <!-- {JSON.stringify(item.info)} -->
{#if item.model.owned_by === 'openai'} {#if item.model.owned_by === 'openai'}
<Tooltip content={`${'External'}`}> <Tooltip content={`${'External'}`}>
<div class=""> <div class="translate-y-[1px]">
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16" viewBox="0 0 16 16"
...@@ -342,7 +375,7 @@ ...@@ -342,7 +375,7 @@
) )
)}`} )}`}
> >
<div class=""> <div class=" translate-y-[1px]">
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
fill="none" fill="none"
...@@ -360,6 +393,20 @@ ...@@ -360,6 +393,20 @@
</div> </div>
</Tooltip> </Tooltip>
{/if} {/if}
{#if !$mobile && (item?.model?.info?.meta?.tags ?? []).length > 0}
<div class="flex gap-0.5 self-center items-center h-full translate-y-[0.5px]">
{#each item.model?.info?.meta.tags as tag}
<Tooltip content={tag.name}>
<div
class=" text-xs font-bold px-1 rounded uppercase line-clamp-1 bg-gray-500/20 text-gray-700 dark:text-gray-200"
>
{tag.name}
</div>
</Tooltip>
{/each}
</div>
{/if}
</div> </div>
</div> </div>
......
...@@ -20,6 +20,7 @@ ...@@ -20,6 +20,7 @@
mirostat_tau: null, mirostat_tau: null,
top_k: null, top_k: null,
top_p: null, top_p: null,
min_p: null,
tfs_z: null, tfs_z: null,
num_ctx: null, num_ctx: null,
num_batch: null, num_batch: null,
...@@ -385,6 +386,52 @@ ...@@ -385,6 +386,52 @@
{/if} {/if}
</div> </div>
<div class=" py-0.5 w-full justify-between">
<div class="flex w-full justify-between">
<div class=" self-center text-xs font-medium">{$i18n.t('Min P')}</div>
<button
class="p-1 px-3 text-xs flex rounded transition flex-shrink-0 outline-none"
type="button"
on:click={() => {
params.min_p = (params?.min_p ?? null) === null ? 0.0 : null;
}}
>
{#if (params?.min_p ?? null) === null}
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
{:else}
<span class="ml-2 self-center">{$i18n.t('Custom')}</span>
{/if}
</button>
</div>
{#if (params?.min_p ?? null) !== null}
<div class="flex mt-0.5 space-x-2">
<div class=" flex-1">
<input
id="steps-range"
type="range"
min="0"
max="1"
step="0.05"
bind:value={params.min_p}
class="w-full h-2 rounded-lg appearance-none cursor-pointer dark:bg-gray-700"
/>
</div>
<div>
<input
bind:value={params.min_p}
type="number"
class=" bg-transparent text-center w-14"
min="0"
max="1"
step="any"
/>
</div>
</div>
{/if}
</div>
<div class=" py-0.5 w-full justify-between"> <div class=" py-0.5 w-full justify-between">
<div class="flex w-full justify-between"> <div class="flex w-full justify-between">
<div class=" self-center text-xs font-medium">{$i18n.t('Frequency Penalty')}</div> <div class=" self-center text-xs font-medium">{$i18n.t('Frequency Penalty')}</div>
......
<script lang="ts"> <script lang="ts">
import { user, settings, config } from '$lib/stores';
import { createEventDispatcher, onMount, getContext } from 'svelte';
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';
import { createEventDispatcher, onMount, getContext } from 'svelte';
import { user, settings, config } from '$lib/stores';
import { getVoices as _getVoices } from '$lib/apis/audio';
import Switch from '$lib/components/common/Switch.svelte'; import Switch from '$lib/components/common/Switch.svelte';
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
...@@ -20,26 +23,26 @@ ...@@ -20,26 +23,26 @@
let voices = []; let voices = [];
let voice = ''; let voice = '';
const getOpenAIVoices = () => { const getVoices = async () => {
voices = [ if ($config.audio.tts.engine === '') {
{ name: 'alloy' }, const getVoicesLoop = setInterval(async () => {
{ name: 'echo' }, voices = await speechSynthesis.getVoices();
{ name: 'fable' },
{ name: 'onyx' },
{ name: 'nova' },
{ name: 'shimmer' }
];
};
const getWebAPIVoices = () => { // do your loop
const getVoicesLoop = setInterval(async () => { if (voices.length > 0) {
voices = await speechSynthesis.getVoices(); clearInterval(getVoicesLoop);
}
}, 100);
} else {
const res = await _getVoices(localStorage.token).catch((e) => {
toast.error(e);
});
// do your loop if (res) {
if (voices.length > 0) { console.log(res);
clearInterval(getVoicesLoop); voices = res.voices;
} }
}, 100); }
}; };
const toggleResponseAutoPlayback = async () => { const toggleResponseAutoPlayback = async () => {
...@@ -58,14 +61,16 @@ ...@@ -58,14 +61,16 @@
responseAutoPlayback = $settings.responseAutoPlayback ?? false; responseAutoPlayback = $settings.responseAutoPlayback ?? false;
STTEngine = $settings?.audio?.stt?.engine ?? ''; STTEngine = $settings?.audio?.stt?.engine ?? '';
voice = $settings?.audio?.tts?.voice ?? $config.audio.tts.voice ?? '';
nonLocalVoices = $settings.audio?.tts?.nonLocalVoices ?? false;
if ($config.audio.tts.engine === 'openai') { if ($settings?.audio?.tts?.defaultVoice === $config.audio.tts.voice) {
getOpenAIVoices(); voice = $settings?.audio?.tts?.voice ?? $config.audio.tts.voice ?? '';
} else { } else {
getWebAPIVoices(); voice = $config.audio.tts.voice ?? '';
} }
nonLocalVoices = $settings.audio?.tts?.nonLocalVoices ?? false;
await getVoices();
}); });
</script> </script>
...@@ -79,6 +84,7 @@ ...@@ -79,6 +84,7 @@
}, },
tts: { tts: {
voice: voice !== '' ? voice : undefined, voice: voice !== '' ? voice : undefined,
defaultVoice: $config?.audio?.tts?.voice ?? '',
nonLocalVoices: $config.audio.tts.engine === '' ? nonLocalVoices : undefined nonLocalVoices: $config.audio.tts.engine === '' ? nonLocalVoices : undefined
} }
} }
...@@ -181,7 +187,7 @@ ...@@ -181,7 +187,7 @@
</div> </div>
</div> </div>
</div> </div>
{:else if $config.audio.tts.engine === 'openai'} {:else if $config.audio.tts.engine !== ''}
<div> <div>
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set Voice')}</div> <div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set Voice')}</div>
<div class="flex w-full"> <div class="flex w-full">
...@@ -195,7 +201,7 @@ ...@@ -195,7 +201,7 @@
<datalist id="voice-list"> <datalist id="voice-list">
{#each voices as voice} {#each voices as voice}
<option value={voice.name} /> <option value={voice.id}>{voice.name}</option>
{/each} {/each}
</datalist> </datalist>
</div> </div>
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment