Merge branch 'dev' into fix-9864

This commit is contained in:
Feynman Liang
2025-02-12 13:47:54 -08:00
committed by GitHub
133 changed files with 4335 additions and 1702 deletions

View File

@@ -20,7 +20,9 @@
export let show = false;
export let edit = false;
export let ollama = false;
export let direct = false;
export let connection = null;
@@ -46,9 +48,11 @@
};
const verifyOpenAIHandler = async () => {
const res = await verifyOpenAIConnection(localStorage.token, url, key).catch((error) => {
toast.error(`${error}`);
});
const res = await verifyOpenAIConnection(localStorage.token, url, key, direct).catch(
(error) => {
toast.error(`${error}`);
}
);
if (res) {
toast.success($i18n.t('Server connection verified'));

View File

@@ -65,7 +65,7 @@
};
const shareHandler = async () => {
toast.success($i18n.t('Redirecting you to OpenWebUI Community'));
toast.success($i18n.t('Redirecting you to Open WebUI Community'));
// remove snapshot from feedbacks
const feedbacksToShare = feedbacks.map((f) => {
@@ -266,7 +266,7 @@
}}
>
<div class=" self-center mr-2 font-medium line-clamp-1">
{$i18n.t('Share to OpenWebUI Community')}
{$i18n.t('Share to Open WebUI Community')}
</div>
<div class=" self-center">

View File

@@ -1,6 +1,8 @@
<script lang="ts">
import * as ort from 'onnxruntime-web';
import { AutoModel, AutoTokenizer } from '@huggingface/transformers';
import { env, AutoModel, AutoTokenizer } from '@huggingface/transformers';
env.backends.onnx.wasm.wasmPaths = '/wasm/';
import { onMount, getContext } from 'svelte';
import { models } from '$lib/stores';

View File

@@ -3,7 +3,7 @@
import fileSaver from 'file-saver';
const { saveAs } = fileSaver;
import { WEBUI_NAME, config, functions, models } from '$lib/stores';
import { WEBUI_NAME, config, functions, models, settings } from '$lib/stores';
import { onMount, getContext, tick } from 'svelte';
import { goto } from '$app/navigation';
@@ -65,7 +65,7 @@
return null;
});
toast.success($i18n.t('Redirecting you to OpenWebUI Community'));
toast.success($i18n.t('Redirecting you to Open WebUI Community'));
const url = 'https://openwebui.com';
@@ -126,7 +126,12 @@
toast.success($i18n.t('Function deleted successfully'));
functions.set(await getFunctions(localStorage.token));
models.set(await getModels(localStorage.token));
models.set(
await getModels(
localStorage.token,
$config?.features?.enable_direct_connections && ($settings?.directConnections ?? null)
)
);
}
};
@@ -147,7 +152,12 @@
}
functions.set(await getFunctions(localStorage.token));
models.set(await getModels(localStorage.token));
models.set(
await getModels(
localStorage.token,
$config?.features?.enable_direct_connections && ($settings?.directConnections ?? null)
)
);
}
};
@@ -359,7 +369,13 @@
bind:state={func.is_active}
on:change={async (e) => {
toggleFunctionById(localStorage.token, func.id);
models.set(await getModels(localStorage.token));
models.set(
await getModels(
localStorage.token,
$config?.features?.enable_direct_connections &&
($settings?.directConnections ?? null)
)
);
}}
/>
</Tooltip>
@@ -453,7 +469,7 @@
{#if $config?.features.enable_community_sharing}
<div class=" my-16">
<div class=" text-xl font-medium mb-1 line-clamp-1">
{$i18n.t('Made by OpenWebUI Community')}
{$i18n.t('Made by Open WebUI Community')}
</div>
<a
@@ -496,7 +512,12 @@
id={selectedFunction?.id ?? null}
on:save={async () => {
await tick();
models.set(await getModels(localStorage.token));
models.set(
await getModels(
localStorage.token,
$config?.features?.enable_direct_connections && ($settings?.directConnections ?? null)
)
);
}}
/>
@@ -517,7 +538,12 @@
toast.success($i18n.t('Functions imported successfully'));
functions.set(await getFunctions(localStorage.token));
models.set(await getModels(localStorage.token));
models.set(
await getModels(
localStorage.token,
$config?.features?.enable_direct_connections && ($settings?.directConnections ?? null)
)
);
};
reader.readAsText(importFiles[0]);

View File

@@ -19,6 +19,7 @@
import ChartBar from '../icons/ChartBar.svelte';
import DocumentChartBar from '../icons/DocumentChartBar.svelte';
import Evaluations from './Settings/Evaluations.svelte';
import CodeInterpreter from './Settings/CodeInterpreter.svelte';
const i18n = getContext('i18n');
@@ -188,6 +189,32 @@
<div class=" self-center">{$i18n.t('Web Search')}</div>
</button>
<button
class="px-0.5 py-1 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
'code-interpreter'
? ''
: ' text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'}"
on:click={() => {
selectedTab = 'code-interpreter';
}}
>
<div class=" self-center mr-2">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="size-4"
>
<path
fill-rule="evenodd"
d="M2 4a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V4Zm2.22 1.97a.75.75 0 0 0 0 1.06l.97.97-.97.97a.75.75 0 1 0 1.06 1.06l1.5-1.5a.75.75 0 0 0 0-1.06l-1.5-1.5a.75.75 0 0 0-1.06 0ZM8.75 8.5a.75.75 0 0 0 0 1.5h2.5a.75.75 0 0 0 0-1.5h-2.5Z"
clip-rule="evenodd"
/>
</svg>
</div>
<div class=" self-center">{$i18n.t('Code Interpreter')}</div>
</button>
<button
class="px-0.5 py-1 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
'interface'
@@ -364,6 +391,15 @@
await config.set(await getBackendConfig());
}}
/>
{:else if selectedTab === 'code-interpreter'}
<CodeInterpreter
saveHandler={async () => {
toast.success($i18n.t('Settings saved successfully!'));
await tick();
await config.set(await getBackendConfig());
}}
/>
{:else if selectedTab === 'interface'}
<Interface
on:save={() => {

View File

@@ -10,7 +10,7 @@
getModels as _getModels,
getVoices as _getVoices
} from '$lib/apis/audio';
import { config } from '$lib/stores';
import { config, settings } from '$lib/stores';
import SensitiveInput from '$lib/components/common/SensitiveInput.svelte';
@@ -39,6 +39,7 @@
let STT_ENGINE = '';
let STT_MODEL = '';
let STT_WHISPER_MODEL = '';
let STT_DEEPGRAM_API_KEY = '';
let STT_WHISPER_MODEL_LOADING = false;
@@ -50,7 +51,10 @@
if (TTS_ENGINE === '') {
models = [];
} else {
const res = await _getModels(localStorage.token).catch((e) => {
const res = await _getModels(
localStorage.token,
$config?.features?.enable_direct_connections && ($settings?.directConnections ?? null)
).catch((e) => {
toast.error(`${e}`);
});
@@ -103,7 +107,8 @@
OPENAI_API_KEY: STT_OPENAI_API_KEY,
ENGINE: STT_ENGINE,
MODEL: STT_MODEL,
WHISPER_MODEL: STT_WHISPER_MODEL
WHISPER_MODEL: STT_WHISPER_MODEL,
DEEPGRAM_API_KEY: STT_DEEPGRAM_API_KEY
}
});
@@ -143,6 +148,7 @@
STT_ENGINE = res.stt.ENGINE;
STT_MODEL = res.stt.MODEL;
STT_WHISPER_MODEL = res.stt.WHISPER_MODEL;
STT_DEEPGRAM_API_KEY = res.stt.DEEPGRAM_API_KEY;
}
await getVoices();
@@ -173,6 +179,7 @@
<option value="">{$i18n.t('Whisper (Local)')}</option>
<option value="openai">OpenAI</option>
<option value="web">{$i18n.t('Web API')}</option>
<option value="deepgram">Deepgram</option>
</select>
</div>
</div>
@@ -210,6 +217,37 @@
</div>
</div>
</div>
{:else if STT_ENGINE === 'deepgram'}
<div>
<div class="mt-1 flex gap-2 mb-1">
<SensitiveInput placeholder={$i18n.t('API Key')} bind:value={STT_DEEPGRAM_API_KEY} />
</div>
</div>
<hr class=" dark:border-gray-850 my-2" />
<div>
<div class=" mb-1.5 text-sm font-medium">{$i18n.t('STT Model')}</div>
<div class="flex w-full">
<div class="flex-1">
<input
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={STT_MODEL}
placeholder="Select a model (optional)"
/>
</div>
</div>
<div class="mt-2 mb-1 text-xs text-gray-400 dark:text-gray-500">
{$i18n.t('Leave model field empty to use the default model.')}
<a
class=" hover:underline dark:text-gray-200 text-gray-800"
href="https://developers.deepgram.com/docs/models"
target="_blank"
>
{$i18n.t('Click here to see available models.')}
</a>
</div>
</div>
{:else if STT_ENGINE === ''}
<div>
<div class=" mb-1.5 text-sm font-medium">{$i18n.t('STT Model')}</div>

View File

@@ -0,0 +1,166 @@
<script lang="ts">
import { toast } from 'svelte-sonner';
import { onMount, getContext } from 'svelte';
import { getCodeInterpreterConfig, setCodeInterpreterConfig } from '$lib/apis/configs';
import SensitiveInput from '$lib/components/common/SensitiveInput.svelte';
import Tooltip from '$lib/components/common/Tooltip.svelte';
import Textarea from '$lib/components/common/Textarea.svelte';
import Switch from '$lib/components/common/Switch.svelte';
const i18n = getContext('i18n');
export let saveHandler: Function;
let config = null;
let engines = ['pyodide', 'jupyter'];
const submitHandler = async () => {
const res = await setCodeInterpreterConfig(localStorage.token, config);
};
onMount(async () => {
const res = await getCodeInterpreterConfig(localStorage.token);
if (res) {
config = res;
}
});
</script>
<form
class="flex flex-col h-full justify-between space-y-3 text-sm"
on:submit|preventDefault={async () => {
await submitHandler();
saveHandler();
}}
>
<div class=" space-y-3 overflow-y-scroll scrollbar-hidden h-full">
{#if config}
<div>
<div class=" mb-1 text-sm font-medium">
{$i18n.t('Code Interpreter')}
</div>
<div>
<div class=" py-0.5 flex w-full justify-between">
<div class=" self-center text-xs font-medium">
{$i18n.t('Enable Code Interpreter')}
</div>
<Switch bind:state={config.ENABLE_CODE_INTERPRETER} />
</div>
</div>
<div class=" py-0.5 flex w-full justify-between">
<div class=" self-center text-xs font-medium">{$i18n.t('Code Interpreter Engine')}</div>
<div class="flex items-center relative">
<select
class="dark:bg-gray-900 w-fit pr-8 rounded px-2 p-1 text-xs bg-transparent outline-none text-right"
bind:value={config.CODE_INTERPRETER_ENGINE}
placeholder={$i18n.t('Select a engine')}
required
>
<option disabled selected value="">{$i18n.t('Select a engine')}</option>
{#each engines as engine}
<option value={engine}>{engine}</option>
{/each}
</select>
</div>
</div>
{#if config.CODE_INTERPRETER_ENGINE === 'jupyter'}
<div class="mt-1 flex flex-col gap-1.5 mb-1 w-full">
<div class="text-xs font-medium">
{$i18n.t('Jupyter URL')}
</div>
<div class="flex w-full">
<div class="flex-1">
<input
class="w-full text-sm py-0.5 placeholder:text-gray-300 dark:placeholder:text-gray-700 bg-transparent outline-none"
type="text"
placeholder={$i18n.t('Enter Jupyter URL')}
bind:value={config.CODE_INTERPRETER_JUPYTER_URL}
autocomplete="off"
/>
</div>
</div>
</div>
<div class="mt-1 flex gap-2 mb-1 w-full items-center justify-between">
<div class="text-xs font-medium">
{$i18n.t('Jupyter Auth')}
</div>
<div>
<select
class="dark:bg-gray-900 w-fit pr-8 rounded px-2 p-1 text-xs bg-transparent outline-none text-left"
bind:value={config.CODE_INTERPRETER_JUPYTER_AUTH}
placeholder={$i18n.t('Select an auth method')}
>
<option selected value="">{$i18n.t('None')}</option>
<option value="token">{$i18n.t('Token')}</option>
<option value="password">{$i18n.t('Password')}</option>
</select>
</div>
</div>
{#if config.CODE_INTERPRETER_JUPYTER_AUTH}
<div class="flex w-full gap-2">
<div class="flex-1">
{#if config.CODE_INTERPRETER_JUPYTER_AUTH === 'password'}
<SensitiveInput
type="text"
placeholder={$i18n.t('Enter Jupyter Password')}
bind:value={config.CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD}
autocomplete="off"
/>
{:else}
<SensitiveInput
type="text"
placeholder={$i18n.t('Enter Jupyter Token')}
bind:value={config.CODE_INTERPRETER_JUPYTER_AUTH_TOKEN}
autocomplete="off"
/>
{/if}
</div>
</div>
{/if}
{/if}
</div>
<hr class=" dark:border-gray-850 my-2" />
<div>
<div class="py-0.5 w-full">
<div class=" mb-2.5 text-xs font-medium">
{$i18n.t('Code Interpreter Prompt Template')}
</div>
<Tooltip
content={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')}
placement="top-start"
>
<Textarea
bind:value={config.CODE_INTERPRETER_PROMPT_TEMPLATE}
placeholder={$i18n.t(
'Leave empty to use the default prompt, or enter a custom prompt'
)}
/>
</Tooltip>
</div>
</div>
{/if}
</div>
<div class="flex justify-end pt-3 text-sm font-medium">
<button
class="px-3.5 py-1.5 text-sm font-medium bg-black hover:bg-gray-900 text-white dark:bg-white dark:text-black dark:hover:bg-gray-100 transition rounded-full"
type="submit"
>
{$i18n.t('Save')}
</button>
</div>
</form>

View File

@@ -7,8 +7,9 @@
import { getOllamaConfig, updateOllamaConfig } from '$lib/apis/ollama';
import { getOpenAIConfig, updateOpenAIConfig, getOpenAIModels } from '$lib/apis/openai';
import { getModels as _getModels } from '$lib/apis';
import { getDirectConnectionsConfig, setDirectConnectionsConfig } from '$lib/apis/configs';
import { models, user } from '$lib/stores';
import { config, models, settings, user } from '$lib/stores';
import Switch from '$lib/components/common/Switch.svelte';
import Spinner from '$lib/components/common/Spinner.svelte';
@@ -16,13 +17,16 @@
import Plus from '$lib/components/icons/Plus.svelte';
import OpenAIConnection from './Connections/OpenAIConnection.svelte';
import AddConnectionModal from './Connections/AddConnectionModal.svelte';
import AddConnectionModal from '$lib/components/AddConnectionModal.svelte';
import OllamaConnection from './Connections/OllamaConnection.svelte';
const i18n = getContext('i18n');
const getModels = async () => {
const models = await _getModels(localStorage.token);
const models = await _getModels(
localStorage.token,
$config?.features?.enable_direct_connections && ($settings?.directConnections ?? null)
);
return models;
};
@@ -37,6 +41,8 @@
let ENABLE_OPENAI_API: null | boolean = null;
let ENABLE_OLLAMA_API: null | boolean = null;
let directConnectionsConfig = null;
let pipelineUrls = {};
let showAddOpenAIConnectionModal = false;
let showAddOllamaConnectionModal = false;
@@ -98,17 +104,33 @@
}
};
const updateDirectConnectionsHandler = async () => {
const res = await setDirectConnectionsConfig(localStorage.token, directConnectionsConfig).catch(
(error) => {
toast.error(`${error}`);
}
);
if (res) {
toast.success($i18n.t('Direct Connections settings updated'));
await models.set(await getModels());
}
};
const addOpenAIConnectionHandler = async (connection) => {
OPENAI_API_BASE_URLS = [...OPENAI_API_BASE_URLS, connection.url];
OPENAI_API_KEYS = [...OPENAI_API_KEYS, connection.key];
OPENAI_API_CONFIGS[OPENAI_API_BASE_URLS.length] = connection.config;
OPENAI_API_CONFIGS[OPENAI_API_BASE_URLS.length - 1] = connection.config;
await updateOpenAIHandler();
};
const addOllamaConnectionHandler = async (connection) => {
OLLAMA_BASE_URLS = [...OLLAMA_BASE_URLS, connection.url];
OLLAMA_API_CONFIGS[OLLAMA_BASE_URLS.length] = connection.config;
OLLAMA_API_CONFIGS[OLLAMA_BASE_URLS.length - 1] = {
...connection.config,
key: connection.key
};
await updateOllamaHandler();
};
@@ -124,6 +146,9 @@
})(),
(async () => {
openaiConfig = await getOpenAIConfig(localStorage.token);
})(),
(async () => {
directConnectionsConfig = await getDirectConnectionsConfig(localStorage.token);
})()
]);
@@ -167,6 +192,14 @@
}
}
});
const submitHandler = async () => {
updateOpenAIHandler();
updateOllamaHandler();
updateDirectConnectionsHandler();
dispatch('save');
};
</script>
<AddConnectionModal
@@ -180,17 +213,9 @@
onSubmit={addOllamaConnectionHandler}
/>
<form
class="flex flex-col h-full justify-between text-sm"
on:submit|preventDefault={() => {
updateOpenAIHandler();
updateOllamaHandler();
dispatch('save');
}}
>
<form class="flex flex-col h-full justify-between text-sm" on:submit|preventDefault={submitHandler}>
<div class=" overflow-y-scroll scrollbar-hidden h-full">
{#if ENABLE_OPENAI_API !== null && ENABLE_OLLAMA_API !== null}
{#if ENABLE_OPENAI_API !== null && ENABLE_OLLAMA_API !== null && directConnectionsConfig !== null}
<div class="my-2">
<div class="mt-2 space-y-2 pr-1.5">
<div class="flex justify-between items-center text-sm">
@@ -244,7 +269,11 @@
);
OPENAI_API_KEYS = OPENAI_API_KEYS.filter((key, keyIdx) => idx !== keyIdx);
delete OPENAI_API_CONFIGS[idx];
let newConfig = {};
OPENAI_API_BASE_URLS.forEach((url, newIdx) => {
newConfig[newIdx] = OPENAI_API_CONFIGS[newIdx < idx ? newIdx : newIdx + 1];
});
OPENAI_API_CONFIGS = newConfig;
}}
/>
{/each}
@@ -302,7 +331,12 @@
}}
onDelete={() => {
OLLAMA_BASE_URLS = OLLAMA_BASE_URLS.filter((url, urlIdx) => idx !== urlIdx);
delete OLLAMA_API_CONFIGS[idx];
let newConfig = {};
OLLAMA_BASE_URLS.forEach((url, newIdx) => {
newConfig[newIdx] = OLLAMA_API_CONFIGS[newIdx < idx ? newIdx : newIdx + 1];
});
OLLAMA_API_CONFIGS = newConfig;
}}
/>
{/each}
@@ -322,6 +356,33 @@
</div>
{/if}
</div>
<hr class=" border-gray-50 dark:border-gray-850" />
<div class="pr-1.5 my-2">
<div class="flex justify-between items-center text-sm">
<div class=" font-medium">{$i18n.t('Direct Connections')}</div>
<div class="flex items-center">
<div class="">
<Switch
bind:state={directConnectionsConfig.ENABLE_DIRECT_CONNECTIONS}
on:change={async () => {
updateDirectConnectionsHandler();
}}
/>
</div>
</div>
</div>
<div class="mt-1.5">
<div class="text-xs text-gray-500">
{$i18n.t(
'Direct Connections allow users to connect to their own OpenAI compatible API endpoints.'
)}
</div>
</div>
</div>
{:else}
<div class="flex h-full justify-center">
<div class="my-auto">

View File

@@ -4,7 +4,7 @@
import Tooltip from '$lib/components/common/Tooltip.svelte';
import SensitiveInput from '$lib/components/common/SensitiveInput.svelte';
import AddConnectionModal from './AddConnectionModal.svelte';
import AddConnectionModal from '$lib/components/AddConnectionModal.svelte';
import Cog6 from '$lib/components/icons/Cog6.svelte';
import Wrench from '$lib/components/icons/Wrench.svelte';

View File

@@ -5,7 +5,8 @@
import Tooltip from '$lib/components/common/Tooltip.svelte';
import SensitiveInput from '$lib/components/common/SensitiveInput.svelte';
import Cog6 from '$lib/components/icons/Cog6.svelte';
import AddConnectionModal from './AddConnectionModal.svelte';
import AddConnectionModal from '$lib/components/AddConnectionModal.svelte';
import { connect } from 'socket.io-client';
export let onDelete = () => {};

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import { toast } from 'svelte-sonner';
import { models, user } from '$lib/stores';
import { models, settings, user, config } from '$lib/stores';
import { createEventDispatcher, onMount, getContext, tick } from 'svelte';
const dispatch = createEventDispatcher();
@@ -16,49 +16,69 @@
const i18n = getContext('i18n');
let config = null;
let evaluationConfig = null;
let showAddModel = false;
const submitHandler = async () => {
config = await updateConfig(localStorage.token, config).catch((err) => {
evaluationConfig = await updateConfig(localStorage.token, evaluationConfig).catch((err) => {
toast.error(err);
return null;
});
if (config) {
if (evaluationConfig) {
toast.success('Settings saved successfully');
models.set(await getModels(localStorage.token));
models.set(
await getModels(
localStorage.token,
$config?.features?.enable_direct_connections && ($settings?.directConnections ?? null)
)
);
}
};
const addModelHandler = async (model) => {
config.EVALUATION_ARENA_MODELS.push(model);
config.EVALUATION_ARENA_MODELS = [...config.EVALUATION_ARENA_MODELS];
evaluationConfig.EVALUATION_ARENA_MODELS.push(model);
evaluationConfig.EVALUATION_ARENA_MODELS = [...evaluationConfig.EVALUATION_ARENA_MODELS];
await submitHandler();
models.set(await getModels(localStorage.token));
models.set(
await getModels(
localStorage.token,
$config?.features?.enable_direct_connections && ($settings?.directConnections ?? null)
)
);
};
const editModelHandler = async (model, modelIdx) => {
config.EVALUATION_ARENA_MODELS[modelIdx] = model;
config.EVALUATION_ARENA_MODELS = [...config.EVALUATION_ARENA_MODELS];
evaluationConfig.EVALUATION_ARENA_MODELS[modelIdx] = model;
evaluationConfig.EVALUATION_ARENA_MODELS = [...evaluationConfig.EVALUATION_ARENA_MODELS];
await submitHandler();
models.set(await getModels(localStorage.token));
models.set(
await getModels(
localStorage.token,
$config?.features?.enable_direct_connections && ($settings?.directConnections ?? null)
)
);
};
const deleteModelHandler = async (modelIdx) => {
config.EVALUATION_ARENA_MODELS = config.EVALUATION_ARENA_MODELS.filter(
evaluationConfig.EVALUATION_ARENA_MODELS = evaluationConfig.EVALUATION_ARENA_MODELS.filter(
(m, mIdx) => mIdx !== modelIdx
);
await submitHandler();
models.set(await getModels(localStorage.token));
models.set(
await getModels(
localStorage.token,
$config?.features?.enable_direct_connections && ($settings?.directConnections ?? null)
)
);
};
onMount(async () => {
if ($user.role === 'admin') {
config = await getConfig(localStorage.token).catch((err) => {
evaluationConfig = await getConfig(localStorage.token).catch((err) => {
toast.error(err);
return null;
});
@@ -81,7 +101,7 @@
}}
>
<div class="overflow-y-scroll scrollbar-hidden h-full">
{#if config !== null}
{#if evaluationConfig !== null}
<div class="">
<div class="text-sm font-medium mb-2">{$i18n.t('General Settings')}</div>
@@ -90,12 +110,12 @@
<div class=" text-xs font-medium">{$i18n.t('Arena Models')}</div>
<Tooltip content={$i18n.t(`Message rating should be enabled to use this feature`)}>
<Switch bind:state={config.ENABLE_EVALUATION_ARENA_MODELS} />
<Switch bind:state={evaluationConfig.ENABLE_EVALUATION_ARENA_MODELS} />
</Tooltip>
</div>
</div>
{#if config.ENABLE_EVALUATION_ARENA_MODELS}
{#if evaluationConfig.ENABLE_EVALUATION_ARENA_MODELS}
<hr class=" border-gray-50 dark:border-gray-700/10 my-2" />
<div class="flex justify-between items-center mb-2">
@@ -117,8 +137,8 @@
</div>
<div class="flex flex-col gap-2">
{#if (config?.EVALUATION_ARENA_MODELS ?? []).length > 0}
{#each config.EVALUATION_ARENA_MODELS as model, index}
{#if (evaluationConfig?.EVALUATION_ARENA_MODELS ?? []).length > 0}
{#each evaluationConfig.EVALUATION_ARENA_MODELS as model, index}
<Model
{model}
on:edit={(e) => {

View File

@@ -68,7 +68,7 @@
const init = async () => {
workspaceModels = await getBaseModels(localStorage.token);
baseModels = await getModels(localStorage.token, true);
baseModels = await getModels(localStorage.token, null, true);
models = baseModels.map((m) => {
const workspaceModel = workspaceModels.find((wm) => wm.id === m.id);
@@ -111,7 +111,12 @@
}
}
_models.set(await getModels(localStorage.token));
_models.set(
await getModels(
localStorage.token,
$config?.features?.enable_direct_connections && ($settings?.directConnections ?? null)
)
);
await init();
};
@@ -133,7 +138,12 @@
}
// await init();
_models.set(await getModels(localStorage.token));
_models.set(
await getModels(
localStorage.token,
$config?.features?.enable_direct_connections && ($settings?.directConnections ?? null)
)
);
};
onMount(async () => {
@@ -330,7 +340,13 @@
}
}
await _models.set(await getModels(localStorage.token));
await _models.set(
await getModels(
localStorage.token,
$config?.features?.enable_direct_connections &&
($settings?.directConnections ?? null)
)
);
init();
};

View File

@@ -3,7 +3,7 @@
import { getContext, onMount } from 'svelte';
const i18n = getContext('i18n');
import { WEBUI_NAME, models, MODEL_DOWNLOAD_POOL, user, config } from '$lib/stores';
import { WEBUI_NAME, models, MODEL_DOWNLOAD_POOL, user, config, settings } from '$lib/stores';
import { splitStream } from '$lib/utils';
import {
@@ -235,7 +235,12 @@
})
);
models.set(await getModels(localStorage.token));
models.set(
await getModels(
localStorage.token,
$config?.features?.enable_direct_connections && ($settings?.directConnections ?? null)
)
);
} else {
toast.error($i18n.t('Download canceled'));
}
@@ -394,7 +399,12 @@
modelTransferring = false;
uploadProgress = null;
models.set(await getModels(localStorage.token));
models.set(
await getModels(
localStorage.token,
$config?.features?.enable_direct_connections && ($settings?.directConnections ?? null)
)
);
};
const deleteModelHandler = async () => {
@@ -407,7 +417,12 @@
}
deleteModelTag = '';
models.set(await getModels(localStorage.token));
models.set(
await getModels(
localStorage.token,
$config?.features?.enable_direct_connections && ($settings?.directConnections ?? null)
)
);
};
const cancelModelPullHandler = async (model: string) => {
@@ -506,7 +521,12 @@
}
}
models.set(await getModels(localStorage.token));
models.set(
await getModels(
localStorage.token,
$config?.features?.enable_direct_connections && ($settings?.directConnections ?? null)
)
);
createModelLoading = false;

View File

@@ -2,7 +2,7 @@
import { v4 as uuidv4 } from 'uuid';
import { toast } from 'svelte-sonner';
import { models } from '$lib/stores';
import { config, models, settings } from '$lib/stores';
import { getContext, onMount, tick } from 'svelte';
import type { Writable } from 'svelte/store';
import type { i18n as i18nType } from 'i18next';
@@ -63,7 +63,12 @@
if (res) {
toast.success($i18n.t('Valves updated successfully'));
setPipelines();
models.set(await getModels(localStorage.token));
models.set(
await getModels(
localStorage.token,
$config?.features?.enable_direct_connections && ($settings?.directConnections ?? null)
)
);
saveHandler();
}
} else {
@@ -125,7 +130,12 @@
if (res) {
toast.success($i18n.t('Pipeline downloaded successfully'));
setPipelines();
models.set(await getModels(localStorage.token));
models.set(
await getModels(
localStorage.token,
$config?.features?.enable_direct_connections && ($settings?.directConnections ?? null)
)
);
}
downloading = false;
@@ -150,7 +160,12 @@
if (res) {
toast.success($i18n.t('Pipeline downloaded successfully'));
setPipelines();
models.set(await getModels(localStorage.token));
models.set(
await getModels(
localStorage.token,
$config?.features?.enable_direct_connections && ($settings?.directConnections ?? null)
)
);
}
} else {
toast.error($i18n.t('No file selected'));
@@ -179,7 +194,12 @@
if (res) {
toast.success($i18n.t('Pipeline deleted successfully'));
setPipelines();
models.set(await getModels(localStorage.token));
models.set(
await getModels(
localStorage.token,
$config?.features?.enable_direct_connections && ($settings?.directConnections ?? null)
)
);
}
};

View File

@@ -18,6 +18,7 @@
'brave',
'kagi',
'mojeek',
'bocha',
'serpstack',
'serper',
'serply',
@@ -34,6 +35,16 @@
let youtubeProxyUrl = '';
const submitHandler = async () => {
// Convert domain filter string to array before sending
if (webConfig.search.domain_filter_list) {
webConfig.search.domain_filter_list = webConfig.search.domain_filter_list
.split(',')
.map((domain) => domain.trim())
.filter((domain) => domain.length > 0);
} else {
webConfig.search.domain_filter_list = [];
}
const res = await updateRAGConfig(localStorage.token, {
web: webConfig,
youtube: {
@@ -42,6 +53,8 @@
proxy_url: youtubeProxyUrl
}
});
webConfig.search.domain_filter_list = webConfig.search.domain_filter_list.join(', ');
};
onMount(async () => {
@@ -49,6 +62,10 @@
if (res) {
webConfig = res.web;
// Convert array back to comma-separated string for display
if (webConfig?.search?.domain_filter_list) {
webConfig.search.domain_filter_list = webConfig.search.domain_filter_list.join(', ');
}
youtubeLanguage = res.youtube.language.join(',');
youtubeTranslation = res.youtube.translation;
@@ -179,6 +196,17 @@
bind:value={webConfig.search.mojeek_search_api_key}
/>
</div>
{:else if webConfig.search.engine === 'bocha'}
<div>
<div class=" self-center text-xs font-medium mb-1">
{$i18n.t('Bocha Search API Key')}
</div>
<SensitiveInput
placeholder={$i18n.t('Enter Bocha Search API Key')}
bind:value={webConfig.search.bocha_search_api_key}
/>
</div>
{:else if webConfig.search.engine === 'serpstack'}
<div>
<div class=" self-center text-xs font-medium mb-1">
@@ -334,6 +362,20 @@
/>
</div>
</div>
<div class="mt-2">
<div class=" self-center text-xs font-medium mb-1">
{$i18n.t('Domain Filter List')}
</div>
<input
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
placeholder={$i18n.t(
'Enter domains separated by commas (e.g., example.com,site.org)'
)}
bind:value={webConfig.search.domain_filter_list}
/>
</div>
{/if}
</div>

View File

@@ -1226,7 +1226,7 @@
selectedModels = _selectedModels;
}
if (userPrompt === '') {
if (userPrompt === '' && files.length === 0) {
toast.error($i18n.t('Please enter a prompt'));
return;
}
@@ -1478,7 +1478,7 @@
params?.stream_response ??
true;
const messages = [
let messages = [
params?.system || $settings.system || (responseMessage?.userContext ?? null)
? {
role: 'system',
@@ -1499,8 +1499,9 @@
...message,
content: removeDetails(message.content, ['reasoning', 'code_interpreter'])
}))
]
.filter((message) => message?.content?.trim())
].filter((message) => message);
messages = messages
.map((message, idx, arr) => ({
role: message.role,
...((message.files?.filter((file) => file.type === 'image').length > 0 ?? false) &&
@@ -1524,7 +1525,8 @@
: {
content: message?.merged?.content ?? message.content
})
}));
}))
.filter((message) => message?.role === 'user' || message?.content?.trim());
const res = await generateOpenAIChatCompletion(
localStorage.token,
@@ -1556,7 +1558,8 @@
? imageGenerationEnabled
: false,
code_interpreter:
$user.role === 'admin' || $user?.permissions?.features?.code_interpreter
$config?.features?.enable_code_interpreter &&
($user.role === 'admin' || $user?.permissions?.features?.code_interpreter)
? codeInterpreterEnabled
: false,
web_search:
@@ -1581,7 +1584,7 @@
(messages.length == 2 &&
messages.at(0)?.role === 'system' &&
messages.at(1)?.role === 'user')) &&
selectedModels[0] === model.id
(selectedModels[0] === model.id || atSelectedModel !== undefined)
? {
background_tasks: {
title_generation: $settings?.title?.auto ?? true,
@@ -2006,7 +2009,7 @@
}
}}
on:submit={async (e) => {
if (e.detail) {
if (e.detail || files.length > 0) {
await tick();
submitPrompt(
($settings?.richTextInput ?? true)
@@ -2049,7 +2052,7 @@
}
}}
on:submit={async (e) => {
if (e.detail) {
if (e.detail || files.length > 0) {
await tick();
submitPrompt(
($settings?.richTextInput ?? true)

View File

@@ -16,7 +16,8 @@
showCallOverlay,
tools,
user as _user,
showControls
showControls,
TTSWorker
} from '$lib/stores';
import { blobToFile, compressImage, createMessagesList, findWordIndices } from '$lib/utils';
@@ -43,6 +44,7 @@
import PhotoSolid from '../icons/PhotoSolid.svelte';
import Photo from '../icons/Photo.svelte';
import CommandLine from '../icons/CommandLine.svelte';
import { KokoroWorker } from '$lib/workers/KokoroWorker';
const i18n = getContext('i18n');
@@ -638,7 +640,7 @@
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-4 h-4"
class="size-4"
>
<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"
@@ -695,7 +697,7 @@
)}
placeholder={placeholder ? placeholder : $i18n.t('Send a Message')}
largeTextAsFile={$settings?.largeTextAsFile ?? false}
autocomplete={true}
autocomplete={$config?.features.enable_autocomplete_generation}
generateAutoCompletion={async (text) => {
if (selectedModelIds.length === 0 || !selectedModelIds.at(0)) {
toast.error($i18n.t('Please select a model first.'));
@@ -1169,7 +1171,7 @@
</Tooltip>
{/if}
{#if $_user.role === 'admin' || $_user?.permissions?.features?.code_interpreter}
{#if $config?.features?.enable_code_interpreter && ($_user.role === 'admin' || $_user?.permissions?.features?.code_interpreter)}
<Tooltip content={$i18n.t('Execute code for analysis')} placement="top">
<button
on:click|preventDefault={() =>
@@ -1242,7 +1244,7 @@
{/if}
{#if !history.currentId || history.messages[history.currentId]?.done == true}
{#if prompt === ''}
{#if prompt === '' && files.length === 0}
<div class=" flex items-center">
<Tooltip content={$i18n.t('Call')}>
<button
@@ -1281,6 +1283,16 @@
stream = null;
if (!$TTSWorker) {
await TTSWorker.set(
new KokoroWorker({
dtype: $settings.audio?.tts?.engineConfig?.dtype ?? 'fp32'
})
);
await $TTSWorker.init();
}
showCallOverlay.set(true);
showControls.set(true);
} catch (err) {
@@ -1301,13 +1313,13 @@
<Tooltip content={$i18n.t('Send message')}>
<button
id="send-message-button"
class="{prompt !== ''
class="{!(prompt === '' && files.length === 0)
? webSearchEnabled || ($settings?.webSearch ?? false) === 'always'
? 'bg-blue-500 text-white hover:bg-blue-400 '
: 'bg-black text-white hover:bg-gray-900 dark:bg-white dark:text-black dark:hover:bg-gray-100 '
: 'text-white bg-gray-200 dark:text-gray-900 dark:bg-gray-700 disabled'} transition rounded-full p-1.5 self-center"
type="submit"
disabled={prompt === ''}
disabled={prompt === '' && files.length === 0}
>
<svg
xmlns="http://www.w3.org/2000/svg"

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import { config, models, settings, showCallOverlay } from '$lib/stores';
import { config, models, settings, showCallOverlay, TTSWorker } from '$lib/stores';
import { onMount, tick, getContext, onDestroy, createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
@@ -12,6 +12,7 @@
import Tooltip from '$lib/components/common/Tooltip.svelte';
import VideoInputMenu from './CallOverlay/VideoInputMenu.svelte';
import { KokoroWorker } from '$lib/workers/KokoroWorker';
const i18n = getContext('i18n');
@@ -459,7 +460,21 @@
}
}
if ($config.audio.tts.engine !== '') {
if ($settings.audio?.tts?.engine === 'browser-kokoro') {
const blob = await $TTSWorker
.generate({
text: content,
voice: $settings?.audio?.tts?.voice ?? $config?.audio?.tts?.voice
})
.catch((error) => {
console.error(error);
toast.error(`${error}`);
});
if (blob) {
audioCache.set(content, new Audio(blob));
}
} else if ($config.audio.tts.engine !== '') {
const res = await synthesizeOpenAISpeech(
localStorage.token,
$settings?.audio?.tts?.defaultVoice === $config.audio.tts.voice

View File

@@ -97,7 +97,7 @@
{#if citations.length > 0}
<div class=" py-0.5 -mx-0.5 w-full flex gap-1 items-center flex-wrap">
{#if citations.length <= 3}
<div class="flex text-xs font-medium">
<div class="flex text-xs font-medium flex-wrap">
{#each citations as citation, idx}
<button
id={`source-${citation.source.name}`}
@@ -113,7 +113,7 @@
</div>
{/if}
<div
class="flex-1 mx-1 line-clamp-1 text-black/60 hover:text-black dark:text-white/60 dark:hover:text-white transition"
class="flex-1 mx-1 truncate text-black/60 hover:text-black dark:text-white/60 dark:hover:text-white transition"
>
{citation.source.name}
</div>
@@ -121,13 +121,22 @@
{/each}
</div>
{:else}
<Collapsible id="collapsible-sources" bind:open={isCollapsibleOpen} className="w-full">
<Collapsible
id="collapsible-sources"
bind:open={isCollapsibleOpen}
className="w-full max-w-full "
buttonClassName="w-fit max-w-full"
>
<div
class="flex items-center gap-2 text-gray-500 hover:text-gray-600 dark:hover:text-gray-400 transition cursor-pointer"
class="flex w-full overflow-auto items-center gap-2 text-gray-500 hover:text-gray-600 dark:hover:text-gray-400 transition cursor-pointer"
>
<div class="flex-grow flex items-center gap-1 overflow-hidden">
<span class="whitespace-nowrap hidden sm:inline">{$i18n.t('References from')}</span>
<div class="flex items-center">
<div
class="flex-1 flex items-center gap-1 overflow-auto scrollbar-none w-full max-w-full"
>
<span class="whitespace-nowrap hidden sm:inline flex-shrink-0"
>{$i18n.t('References from')}</span
>
<div class="flex items-center overflow-auto scrollbar-none w-full max-w-full flex-1">
<div class="flex text-xs font-medium items-center">
{#each citations.slice(0, 2) as citation, idx}
<button
@@ -145,14 +154,14 @@
{idx + 1}
</div>
{/if}
<div class="flex-1 mx-1 line-clamp-1 truncate">
<div class="flex-1 mx-1 truncate">
{citation.source.name}
</div>
</button>
{/each}
</div>
</div>
<div class="flex items-center gap-1 whitespace-nowrap">
<div class="flex items-center gap-1 whitespace-nowrap flex-shrink-0">
<span class="hidden sm:inline">{$i18n.t('and')}</span>
{citations.length - 2}
<span>{$i18n.t('more')}</span>
@@ -167,7 +176,7 @@
</div>
</div>
<div slot="content">
<div class="flex text-xs font-medium">
<div class="flex text-xs font-medium flex-wrap">
{#each citations as citation, idx}
<button
id={`source-${citation.source.name}`}
@@ -182,7 +191,7 @@
{idx + 1}
</div>
{/if}
<div class="flex-1 mx-1 line-clamp-1 truncate">
<div class="flex-1 mx-1 truncate">
{citation.source.name}
</div>
</button>

View File

@@ -5,7 +5,14 @@
import { v4 as uuidv4 } from 'uuid';
import { getContext, getAllContexts, onMount, tick, createEventDispatcher } from 'svelte';
import {
getContext,
getAllContexts,
onMount,
tick,
createEventDispatcher,
onDestroy
} from 'svelte';
import { copyToClipboard } from '$lib/utils';
import 'highlight.js/styles/github-dark.min.css';
@@ -31,6 +38,8 @@
export let editorClassName = '';
export let stickyButtonsClassName = 'top-8';
let pyodideWorker = null;
let _code = '';
$: if (code) {
updateCode();
@@ -138,7 +147,7 @@
console.log(packages);
const pyodideWorker = new PyodideWorker();
pyodideWorker = new PyodideWorker();
pyodideWorker.postMessage({
id: id,
@@ -280,6 +289,12 @@
});
}
});
onDestroy(() => {
if (pyodideWorker) {
pyodideWorker.terminate();
}
});
</script>
<div>

View File

@@ -4,12 +4,18 @@
import { createEventDispatcher } from 'svelte';
import { onMount, tick, getContext } from 'svelte';
import type { Writable } from 'svelte/store';
import type { i18n as i18nType } from 'i18next';
const i18n = getContext<Writable<i18nType>>('i18n');
const dispatch = createEventDispatcher();
import { config, models, settings, user } from '$lib/stores';
import { createNewFeedback, getFeedbackById, updateFeedbackById } from '$lib/apis/evaluations';
import { getChatById } from '$lib/apis/chats';
import { generateTags } from '$lib/apis';
import { config, models, settings, TTSWorker, user } from '$lib/stores';
import { synthesizeOpenAISpeech } from '$lib/apis/audio';
import { imageGenerations } from '$lib/apis/images';
import {
@@ -34,13 +40,8 @@
import Error from './Error.svelte';
import Citations from './Citations.svelte';
import CodeExecutions from './CodeExecutions.svelte';
import type { Writable } from 'svelte/store';
import type { i18n as i18nType } from 'i18next';
import ContentRenderer from './ContentRenderer.svelte';
import { createNewFeedback, getFeedbackById, updateFeedbackById } from '$lib/apis/evaluations';
import { getChatById } from '$lib/apis/chats';
import { generateTags } from '$lib/apis';
import { KokoroWorker } from '$lib/workers/KokoroWorker';
interface MessageType {
id: string;
@@ -193,62 +194,7 @@
speaking = true;
if ($config.audio.tts.engine !== '') {
loadingSpeech = true;
const messageContentParts: string[] = getMessageContentParts(
message.content,
$config?.audio?.tts?.split_on ?? 'punctuation'
);
if (!messageContentParts.length) {
console.log('No content to speak');
toast.info($i18n.t('No content to speak'));
speaking = false;
loadingSpeech = false;
return;
}
console.debug('Prepared message content for TTS', messageContentParts);
audioParts = messageContentParts.reduce(
(acc, _sentence, idx) => {
acc[idx] = null;
return acc;
},
{} as typeof audioParts
);
let lastPlayedAudioPromise = Promise.resolve(); // Initialize a promise that resolves immediately
for (const [idx, sentence] of messageContentParts.entries()) {
const res = await synthesizeOpenAISpeech(
localStorage.token,
$settings?.audio?.tts?.defaultVoice === $config.audio.tts.voice
? ($settings?.audio?.tts?.voice ?? $config?.audio?.tts?.voice)
: $config?.audio?.tts?.voice,
sentence
).catch((error) => {
console.error(error);
toast.error(`${error}`);
speaking = false;
loadingSpeech = false;
});
if (res) {
const blob = await res.blob();
const blobUrl = URL.createObjectURL(blob);
const audio = new Audio(blobUrl);
audio.playbackRate = $settings.audio?.tts?.playbackRate ?? 1;
audioParts[idx] = audio;
loadingSpeech = false;
lastPlayedAudioPromise = lastPlayedAudioPromise.then(() => playAudio(idx));
}
}
} else {
if ($config.audio.tts.engine === '') {
let voices = [];
const getVoicesLoop = setInterval(() => {
voices = speechSynthesis.getVoices();
@@ -283,6 +229,97 @@
speechSynthesis.speak(speak);
}
}, 100);
} else {
loadingSpeech = true;
const messageContentParts: string[] = getMessageContentParts(
message.content,
$config?.audio?.tts?.split_on ?? 'punctuation'
);
if (!messageContentParts.length) {
console.log('No content to speak');
toast.info($i18n.t('No content to speak'));
speaking = false;
loadingSpeech = false;
return;
}
console.debug('Prepared message content for TTS', messageContentParts);
audioParts = messageContentParts.reduce(
(acc, _sentence, idx) => {
acc[idx] = null;
return acc;
},
{} as typeof audioParts
);
let lastPlayedAudioPromise = Promise.resolve(); // Initialize a promise that resolves immediately
if ($settings.audio?.tts?.engine === 'browser-kokoro') {
if (!$TTSWorker) {
await TTSWorker.set(
new KokoroWorker({
dtype: $settings.audio?.tts?.engineConfig?.dtype ?? 'fp32'
})
);
await $TTSWorker.init();
}
for (const [idx, sentence] of messageContentParts.entries()) {
const blob = await $TTSWorker
.generate({
text: sentence,
voice: $settings?.audio?.tts?.voice ?? $config?.audio?.tts?.voice
})
.catch((error) => {
console.error(error);
toast.error(`${error}`);
speaking = false;
loadingSpeech = false;
});
if (blob) {
const audio = new Audio(blob);
audio.playbackRate = $settings.audio?.tts?.playbackRate ?? 1;
audioParts[idx] = audio;
loadingSpeech = false;
lastPlayedAudioPromise = lastPlayedAudioPromise.then(() => playAudio(idx));
}
}
} else {
for (const [idx, sentence] of messageContentParts.entries()) {
const res = await synthesizeOpenAISpeech(
localStorage.token,
$settings?.audio?.tts?.defaultVoice === $config.audio.tts.voice
? ($settings?.audio?.tts?.voice ?? $config?.audio?.tts?.voice)
: $config?.audio?.tts?.voice,
sentence
).catch((error) => {
console.error(error);
toast.error(`${error}`);
speaking = false;
loadingSpeech = false;
});
if (res) {
const blob = await res.blob();
const blobUrl = URL.createObjectURL(blob);
const audio = new Audio(blobUrl);
audio.playbackRate = $settings.audio?.tts?.playbackRate ?? 1;
audioParts[idx] = audio;
loadingSpeech = false;
lastPlayedAudioPromise = lastPlayedAudioPromise.then(() => playAudio(idx));
}
}
}
}
};

View File

@@ -145,149 +145,177 @@
</div>
{/if}
{#if edit === true}
<div class=" w-full bg-gray-50 dark:bg-gray-800 rounded-3xl px-5 py-3 mb-2">
<div class="max-h-96 overflow-auto">
<textarea
id="message-edit-{message.id}"
bind:this={messageEditTextAreaElement}
class=" bg-transparent outline-none w-full resize-none"
bind:value={editedContent}
on:input={(e) => {
e.target.style.height = '';
e.target.style.height = `${e.target.scrollHeight}px`;
}}
on:keydown={(e) => {
if (e.key === 'Escape') {
document.getElementById('close-edit-message-button')?.click();
}
const isCmdOrCtrlPressed = e.metaKey || e.ctrlKey;
const isEnterPressed = e.key === 'Enter';
if (isCmdOrCtrlPressed && isEnterPressed) {
document.getElementById('confirm-edit-message-button')?.click();
}
}}
/>
</div>
<div class=" mt-2 mb-1 flex justify-between text-sm font-medium">
<div>
<button
id="save-edit-message-button"
class=" px-4 py-2 bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 border dark:border-gray-700 text-gray-700 dark:text-gray-200 transition rounded-3xl"
on:click={() => {
editMessageConfirmHandler(false);
{#if message.content !== ''}
{#if edit === true}
<div class=" w-full bg-gray-50 dark:bg-gray-800 rounded-3xl px-5 py-3 mb-2">
<div class="max-h-96 overflow-auto">
<textarea
id="message-edit-{message.id}"
bind:this={messageEditTextAreaElement}
class=" bg-transparent outline-none w-full resize-none"
bind:value={editedContent}
on:input={(e) => {
e.target.style.height = '';
e.target.style.height = `${e.target.scrollHeight}px`;
}}
>
{$i18n.t('Save')}
</button>
on:keydown={(e) => {
if (e.key === 'Escape') {
document.getElementById('close-edit-message-button')?.click();
}
const isCmdOrCtrlPressed = e.metaKey || e.ctrlKey;
const isEnterPressed = e.key === 'Enter';
if (isCmdOrCtrlPressed && isEnterPressed) {
document.getElementById('confirm-edit-message-button')?.click();
}
}}
/>
</div>
<div class="flex space-x-1.5">
<button
id="close-edit-message-button"
class="px-4 py-2 bg-white dark:bg-gray-900 hover:bg-gray-100 text-gray-800 dark:text-gray-100 transition rounded-3xl"
on:click={() => {
cancelEditMessage();
}}
>
{$i18n.t('Cancel')}
</button>
<button
id="confirm-edit-message-button"
class=" px-4 py-2 bg-gray-900 dark:bg-white hover:bg-gray-850 text-gray-100 dark:text-gray-800 transition rounded-3xl"
on:click={() => {
editMessageConfirmHandler();
}}
>
{$i18n.t('Send')}
</button>
</div>
</div>
</div>
{:else}
<div class="w-full">
<div class="flex {($settings?.chatBubble ?? true) ? 'justify-end pb-1' : 'w-full'}">
<div
class="rounded-3xl {($settings?.chatBubble ?? true)
? `max-w-[90%] px-5 py-2 bg-gray-50 dark:bg-gray-850 ${
message.files ? 'rounded-tr-lg' : ''
}`
: ' w-full'}"
>
{#if message.content}
<Markdown id={message.id} content={message.content} />
{/if}
</div>
</div>
<div
class=" flex {($settings?.chatBubble ?? true)
? 'justify-end'
: ''} text-gray-600 dark:text-gray-500"
>
{#if !($settings?.chatBubble ?? true)}
{#if siblings.length > 1}
<div class="flex self-center" dir="ltr">
<button
class="self-center p-1 hover:bg-black/5 dark:hover:bg-white/5 dark:hover:text-white hover:text-black rounded-md transition"
on:click={() => {
showPreviousMessage(message);
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2.5"
class="size-3.5"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M15.75 19.5 8.25 12l7.5-7.5"
/>
</svg>
</button>
<div class="text-sm tracking-widest font-semibold self-center dark:text-gray-100">
{siblings.indexOf(message.id) + 1}/{siblings.length}
</div>
<button
class="self-center p-1 hover:bg-black/5 dark:hover:bg-white/5 dark:hover:text-white hover:text-black rounded-md transition"
on:click={() => {
showNextMessage(message);
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2.5"
class="size-3.5"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="m8.25 4.5 7.5 7.5-7.5 7.5"
/>
</svg>
</button>
</div>
{/if}
{/if}
{#if !readOnly}
<Tooltip content={$i18n.t('Edit')} placement="bottom">
<div class=" mt-2 mb-1 flex justify-between text-sm font-medium">
<div>
<button
class="invisible group-hover:visible p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg dark:hover:text-white hover:text-black transition edit-user-message-button"
id="save-edit-message-button"
class=" px-4 py-2 bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 border dark:border-gray-700 text-gray-700 dark:text-gray-200 transition rounded-3xl"
on:click={() => {
editMessageHandler();
editMessageConfirmHandler(false);
}}
>
{$i18n.t('Save')}
</button>
</div>
<div class="flex space-x-1.5">
<button
id="close-edit-message-button"
class="px-4 py-2 bg-white dark:bg-gray-900 hover:bg-gray-100 text-gray-800 dark:text-gray-100 transition rounded-3xl"
on:click={() => {
cancelEditMessage();
}}
>
{$i18n.t('Cancel')}
</button>
<button
id="confirm-edit-message-button"
class=" px-4 py-2 bg-gray-900 dark:bg-white hover:bg-gray-850 text-gray-100 dark:text-gray-800 transition rounded-3xl"
on:click={() => {
editMessageConfirmHandler();
}}
>
{$i18n.t('Send')}
</button>
</div>
</div>
</div>
{:else}
<div class="w-full">
<div class="flex {($settings?.chatBubble ?? true) ? 'justify-end pb-1' : 'w-full'}">
<div
class="rounded-3xl {($settings?.chatBubble ?? true)
? `max-w-[90%] px-5 py-2 bg-gray-50 dark:bg-gray-850 ${
message.files ? 'rounded-tr-lg' : ''
}`
: ' w-full'}"
>
{#if message.content}
<Markdown id={message.id} content={message.content} />
{/if}
</div>
</div>
<div
class=" flex {($settings?.chatBubble ?? true)
? 'justify-end'
: ''} text-gray-600 dark:text-gray-500"
>
{#if !($settings?.chatBubble ?? true)}
{#if siblings.length > 1}
<div class="flex self-center" dir="ltr">
<button
class="self-center p-1 hover:bg-black/5 dark:hover:bg-white/5 dark:hover:text-white hover:text-black rounded-md transition"
on:click={() => {
showPreviousMessage(message);
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2.5"
class="size-3.5"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M15.75 19.5 8.25 12l7.5-7.5"
/>
</svg>
</button>
<div
class="text-sm tracking-widest font-semibold self-center dark:text-gray-100"
>
{siblings.indexOf(message.id) + 1}/{siblings.length}
</div>
<button
class="self-center p-1 hover:bg-black/5 dark:hover:bg-white/5 dark:hover:text-white hover:text-black rounded-md transition"
on:click={() => {
showNextMessage(message);
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2.5"
class="size-3.5"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="m8.25 4.5 7.5 7.5-7.5 7.5"
/>
</svg>
</button>
</div>
{/if}
{/if}
{#if !readOnly}
<Tooltip content={$i18n.t('Edit')} placement="bottom">
<button
class="invisible group-hover:visible p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg dark:hover:text-white hover:text-black transition edit-user-message-button"
on:click={() => {
editMessageHandler();
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="2.3"
stroke="currentColor"
class="w-4 h-4"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L6.832 19.82a4.5 4.5 0 01-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 011.13-1.897L16.863 4.487zm0 0L19.5 7.125"
/>
</svg>
</button>
</Tooltip>
{/if}
<Tooltip content={$i18n.t('Copy')} placement="bottom">
<button
class="invisible group-hover:visible p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg dark:hover:text-white hover:text-black transition"
on:click={() => {
copyToClipboard(message.content);
}}
>
<svg
@@ -301,118 +329,96 @@
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L6.832 19.82a4.5 4.5 0 01-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 011.13-1.897L16.863 4.487zm0 0L19.5 7.125"
d="M15.666 3.888A2.25 2.25 0 0013.5 2.25h-3c-1.03 0-1.9.693-2.166 1.638m7.332 0c.055.194.084.4.084.612v0a.75.75 0 01-.75.75H9a.75.75 0 01-.75-.75v0c0-.212.03-.418.084-.612m7.332 0c.646.049 1.288.11 1.927.184 1.1.128 1.907 1.077 1.907 2.185V19.5a2.25 2.25 0 01-2.25 2.25H6.75A2.25 2.25 0 014.5 19.5V6.257c0-1.108.806-2.057 1.907-2.185a48.208 48.208 0 011.927-.184"
/>
</svg>
</button>
</Tooltip>
{/if}
<Tooltip content={$i18n.t('Copy')} placement="bottom">
<button
class="invisible group-hover:visible p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg dark:hover:text-white hover:text-black transition"
on:click={() => {
copyToClipboard(message.content);
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="2.3"
stroke="currentColor"
class="w-4 h-4"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M15.666 3.888A2.25 2.25 0 0013.5 2.25h-3c-1.03 0-1.9.693-2.166 1.638m7.332 0c.055.194.084.4.084.612v0a.75.75 0 01-.75.75H9a.75.75 0 01-.75-.75v0c0-.212.03-.418.084-.612m7.332 0c.646.049 1.288.11 1.927.184 1.1.128 1.907 1.077 1.907 2.185V19.5a2.25 2.25 0 01-2.25 2.25H6.75A2.25 2.25 0 014.5 19.5V6.257c0-1.108.806-2.057 1.907-2.185a48.208 48.208 0 011.927-.184"
/>
</svg>
</button>
</Tooltip>
{#if !isFirstMessage && !readOnly}
<Tooltip content={$i18n.t('Delete')} placement="bottom">
<button
class="invisible group-hover:visible p-1 rounded dark:hover:text-white hover:text-black transition"
on:click={() => {
deleteMessageHandler();
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
class="w-4 h-4"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0"
/>
</svg>
</button>
</Tooltip>
{/if}
{#if $settings?.chatBubble ?? true}
{#if siblings.length > 1}
<div class="flex self-center" dir="ltr">
{#if !isFirstMessage && !readOnly}
<Tooltip content={$i18n.t('Delete')} placement="bottom">
<button
class="self-center p-1 hover:bg-black/5 dark:hover:bg-white/5 dark:hover:text-white hover:text-black rounded-md transition"
class="invisible group-hover:visible p-1 rounded dark:hover:text-white hover:text-black transition"
on:click={() => {
showPreviousMessage(message);
deleteMessageHandler();
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
stroke-width="2.5"
class="size-3.5"
class="w-4 h-4"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M15.75 19.5 8.25 12l7.5-7.5"
d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0"
/>
</svg>
</button>
<div class="text-sm tracking-widest font-semibold self-center dark:text-gray-100">
{siblings.indexOf(message.id) + 1}/{siblings.length}
</div>
<button
class="self-center p-1 hover:bg-black/5 dark:hover:bg-white/5 dark:hover:text-white hover:text-black rounded-md transition"
on:click={() => {
showNextMessage(message);
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2.5"
class="size-3.5"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="m8.25 4.5 7.5 7.5-7.5 7.5"
/>
</svg>
</button>
</div>
</Tooltip>
{/if}
{/if}
{#if $settings?.chatBubble ?? true}
{#if siblings.length > 1}
<div class="flex self-center" dir="ltr">
<button
class="self-center p-1 hover:bg-black/5 dark:hover:bg-white/5 dark:hover:text-white hover:text-black rounded-md transition"
on:click={() => {
showPreviousMessage(message);
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2.5"
class="size-3.5"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M15.75 19.5 8.25 12l7.5-7.5"
/>
</svg>
</button>
<div
class="text-sm tracking-widest font-semibold self-center dark:text-gray-100"
>
{siblings.indexOf(message.id) + 1}/{siblings.length}
</div>
<button
class="self-center p-1 hover:bg-black/5 dark:hover:bg-white/5 dark:hover:text-white hover:text-black rounded-md transition"
on:click={() => {
showNextMessage(message);
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2.5"
class="size-3.5"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="m8.25 4.5 7.5 7.5-7.5 7.5"
/>
</svg>
</button>
</div>
{/if}
{/if}
</div>
</div>
</div>
{/if}
{/if}
</div>
</div>

View File

@@ -12,7 +12,15 @@
import { deleteModel, getOllamaVersion, pullModel } from '$lib/apis/ollama';
import { user, MODEL_DOWNLOAD_POOL, models, mobile, temporaryChatEnabled } from '$lib/stores';
import {
user,
MODEL_DOWNLOAD_POOL,
models,
mobile,
temporaryChatEnabled,
settings,
config
} from '$lib/stores';
import { toast } from 'svelte-sonner';
import { capitalizeFirstLetter, sanitizeResponseContent, splitStream } from '$lib/utils';
import { getModels } from '$lib/apis';
@@ -186,7 +194,12 @@
})
);
models.set(await getModels(localStorage.token));
models.set(
await getModels(
localStorage.token,
$config?.features?.enable_direct_connections && ($settings?.directConnections ?? null)
)
);
} else {
toast.error($i18n.t('Download canceled'));
}
@@ -358,7 +371,24 @@
<!-- {JSON.stringify(item.info)} -->
{#if item.model.owned_by === 'openai'}
{#if item.model?.direct}
<Tooltip content={`${'Direct'}`}>
<div class="translate-y-[1px]">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="size-3"
>
<path
fill-rule="evenodd"
d="M2 2.75A.75.75 0 0 1 2.75 2C8.963 2 14 7.037 14 13.25a.75.75 0 0 1-1.5 0c0-5.385-4.365-9.75-9.75-9.75A.75.75 0 0 1 2 2.75Zm0 4.5a.75.75 0 0 1 .75-.75 6.75 6.75 0 0 1 6.75 6.75.75.75 0 0 1-1.5 0C8 10.35 5.65 8 2.75 8A.75.75 0 0 1 2 7.25ZM3.5 11a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3Z"
clip-rule="evenodd"
/>
</svg>
</div>
</Tooltip>
{:else if item.model.owned_by === 'openai'}
<Tooltip content={`${'External'}`}>
<div class="translate-y-[1px]">
<svg

View File

@@ -114,7 +114,7 @@
</div>
</button>
</Menu>
{:else if $mobile && ($user.role === 'admin' || $user?.permissions.chat?.controls)}
{:else if $mobile && ($user.role === 'admin' || $user?.permissions?.chat?.controls)}
<Tooltip content={$i18n.t('Controls')}>
<button
class=" flex cursor-pointer px-2 py-2 rounded-xl hover:bg-gray-50 dark:hover:bg-gray-850 transition"
@@ -130,7 +130,7 @@
</Tooltip>
{/if}
{#if !$mobile && ($user.role === 'admin' || $user?.permissions.chat?.controls)}
{#if !$mobile && ($user.role === 'admin' || $user?.permissions?.chat?.controls)}
<Tooltip content={$i18n.t('Controls')}>
<button
class=" flex cursor-pointer px-2 py-2 rounded-xl hover:bg-gray-50 dark:hover:bg-gray-850 transition"

View File

@@ -3,7 +3,7 @@
import { onMount, getContext } from 'svelte';
import { user, config, settings } from '$lib/stores';
import { updateUserProfile, createAPIKey, getAPIKey } from '$lib/apis/auths';
import { updateUserProfile, createAPIKey, getAPIKey, getSessionUser } from '$lib/apis/auths';
import UpdatePassword from './Account/UpdatePassword.svelte';
import { getGravatarUrl } from '$lib/apis/utils';
@@ -53,7 +53,13 @@
);
if (updatedUser) {
await user.set(updatedUser);
// Get Session User Info
const sessionUser = await getSessionUser(localStorage.token).catch((error) => {
toast.error(`${error}`);
return null;
});
await user.set(sessionUser);
return true;
}
return false;

View File

@@ -1,11 +1,14 @@
<script lang="ts">
import { toast } from 'svelte-sonner';
import { createEventDispatcher, onMount, getContext } from 'svelte';
import { KokoroTTS } from 'kokoro-js';
import { user, settings, config } from '$lib/stores';
import { getVoices as _getVoices } from '$lib/apis/audio';
import Switch from '$lib/components/common/Switch.svelte';
import { round } from '@huggingface/transformers';
import Spinner from '$lib/components/common/Spinner.svelte';
const dispatch = createEventDispatcher();
const i18n = getContext('i18n');
@@ -20,6 +23,13 @@
let STTEngine = '';
let TTSEngine = '';
let TTSEngineConfig = {};
let TTSModel = null;
let TTSModelProgress = null;
let TTSModelLoading = false;
let voices = [];
let voice = '';
@@ -28,23 +38,37 @@
const speedOptions = [2, 1.75, 1.5, 1.25, 1, 0.75, 0.5];
const getVoices = async () => {
if ($config.audio.tts.engine === '') {
const getVoicesLoop = setInterval(async () => {
voices = await speechSynthesis.getVoices();
if (TTSEngine === 'browser-kokoro') {
if (!TTSModel) {
await loadKokoro();
}
// do your loop
if (voices.length > 0) {
clearInterval(getVoicesLoop);
}
}, 100);
} else {
const res = await _getVoices(localStorage.token).catch((e) => {
toast.error(`${e}`);
voices = Object.entries(TTSModel.voices).map(([key, value]) => {
return {
id: key,
name: value.name,
localService: false
};
});
} else {
if ($config.audio.tts.engine === '') {
const getVoicesLoop = setInterval(async () => {
voices = await speechSynthesis.getVoices();
if (res) {
console.log(res);
voices = res.voices;
// do your loop
if (voices.length > 0) {
clearInterval(getVoicesLoop);
}
}, 100);
} else {
const res = await _getVoices(localStorage.token).catch((e) => {
toast.error(`${e}`);
});
if (res) {
console.log(res);
voices = res.voices;
}
}
}
};
@@ -67,6 +91,9 @@
STTEngine = $settings?.audio?.stt?.engine ?? '';
TTSEngine = $settings?.audio?.tts?.engine ?? '';
TTSEngineConfig = $settings?.audio?.tts?.engineConfig ?? {};
if ($settings?.audio?.tts?.defaultVoice === $config.audio.tts.voice) {
voice = $settings?.audio?.tts?.voice ?? $config.audio.tts.voice ?? '';
} else {
@@ -77,6 +104,51 @@
await getVoices();
});
$: if (TTSEngine && TTSEngineConfig) {
onTTSEngineChange();
}
const onTTSEngineChange = async () => {
if (TTSEngine === 'browser-kokoro') {
await loadKokoro();
}
};
const loadKokoro = async () => {
if (TTSEngine === 'browser-kokoro') {
voices = [];
if (TTSEngineConfig?.dtype) {
TTSModel = null;
TTSModelProgress = null;
TTSModelLoading = true;
const model_id = 'onnx-community/Kokoro-82M-v1.0-ONNX';
TTSModel = await KokoroTTS.from_pretrained(model_id, {
dtype: TTSEngineConfig.dtype, // Options: "fp32", "fp16", "q8", "q4", "q4f16"
device: !!navigator?.gpu ? 'webgpu' : 'wasm', // Detect WebGPU
progress_callback: (e) => {
TTSModelProgress = e;
console.log(e);
}
});
await getVoices();
// const rawAudio = await tts.generate(inputText, {
// // Use `tts.list_voices()` to list all available voices
// voice: voice
// });
// const blobUrl = URL.createObjectURL(await rawAudio.toBlob());
// const audio = new Audio(blobUrl);
// audio.play();
}
}
};
</script>
<form
@@ -88,6 +160,8 @@
engine: STTEngine !== '' ? STTEngine : undefined
},
tts: {
engine: TTSEngine !== '' ? TTSEngine : undefined,
engineConfig: TTSEngineConfig,
playbackRate: playbackRate,
voice: voice !== '' ? voice : undefined,
defaultVoice: $config?.audio?.tts?.voice ?? '',
@@ -142,6 +216,39 @@
<div>
<div class=" mb-1 text-sm font-medium">{$i18n.t('TTS Settings')}</div>
<div class=" py-0.5 flex w-full justify-between">
<div class=" self-center text-xs font-medium">{$i18n.t('Text-to-Speech Engine')}</div>
<div class="flex items-center relative">
<select
class="dark:bg-gray-900 w-fit pr-8 rounded px-2 p-1 text-xs bg-transparent outline-none text-right"
bind:value={TTSEngine}
placeholder="Select an engine"
>
<option value="">{$i18n.t('Default')}</option>
<option value="browser-kokoro">{$i18n.t('Kokoro.js (Browser)')}</option>
</select>
</div>
</div>
{#if TTSEngine === 'browser-kokoro'}
<div class=" py-0.5 flex w-full justify-between">
<div class=" self-center text-xs font-medium">{$i18n.t('Kokoro.js Dtype')}</div>
<div class="flex items-center relative">
<select
class="dark:bg-gray-900 w-fit pr-8 rounded px-2 p-1 text-xs bg-transparent outline-none text-right"
bind:value={TTSEngineConfig.dtype}
placeholder="Select dtype"
>
<option value="" disabled selected>Select dtype</option>
<option value="fp32">fp32</option>
<option value="fp16">fp16</option>
<option value="q8">q8</option>
<option value="q4">q4</option>
</select>
</div>
</div>
{/if}
<div class=" py-0.5 flex w-full justify-between">
<div class=" self-center text-xs font-medium">{$i18n.t('Auto-playback response')}</div>
@@ -178,7 +285,46 @@
<hr class=" dark:border-gray-850" />
{#if $config.audio.tts.engine === ''}
{#if TTSEngine === 'browser-kokoro'}
{#if TTSModel}
<div>
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set 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 dark:text-gray-300 dark:bg-gray-850 outline-none"
bind:value={voice}
placeholder="Select a voice"
/>
<datalist id="voice-list">
{#each voices as voice}
<option value={voice.id}>{voice.name}</option>
{/each}
</datalist>
</div>
</div>
</div>
{:else}
<div>
<div class=" mb-2.5 text-sm font-medium flex gap-2 items-center">
<Spinner className="size-4" />
<div class=" text-sm font-medium shimmer">
{$i18n.t('Loading Kokoro.js...')}
{TTSModelProgress && TTSModelProgress.status === 'progress'
? `(${Math.round(TTSModelProgress.progress * 10) / 10}%)`
: ''}
</div>
</div>
<div class="text-xs text-gray-500">
{$i18n.t('Please do not close the settings page while loading the model.')}
</div>
</div>
{/if}
{:else if $config.audio.tts.engine === ''}
<div>
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set Voice')}</div>
<div class="flex w-full">

View File

@@ -0,0 +1,152 @@
<script lang="ts">
import { toast } from 'svelte-sonner';
import { createEventDispatcher, onMount, getContext, tick } from 'svelte';
import { getModels as _getModels } from '$lib/apis';
const dispatch = createEventDispatcher();
const i18n = getContext('i18n');
import { models, settings, user } from '$lib/stores';
import Switch from '$lib/components/common/Switch.svelte';
import Spinner from '$lib/components/common/Spinner.svelte';
import Tooltip from '$lib/components/common/Tooltip.svelte';
import Plus from '$lib/components/icons/Plus.svelte';
import Connection from './Connections/Connection.svelte';
import AddConnectionModal from '$lib/components/AddConnectionModal.svelte';
export let saveSettings: Function;
let config = null;
let showConnectionModal = false;
const addConnectionHandler = async (connection) => {
config.OPENAI_API_BASE_URLS.push(connection.url);
config.OPENAI_API_KEYS.push(connection.key);
config.OPENAI_API_CONFIGS[config.OPENAI_API_BASE_URLS.length - 1] = connection.config;
await updateHandler();
};
const updateHandler = async () => {
// Remove trailing slashes
config.OPENAI_API_BASE_URLS = config.OPENAI_API_BASE_URLS.map((url) => url.replace(/\/$/, ''));
// Check if API KEYS length is same than API URLS length
if (config.OPENAI_API_KEYS.length !== config.OPENAI_API_BASE_URLS.length) {
// if there are more keys than urls, remove the extra keys
if (config.OPENAI_API_KEYS.length > config.OPENAI_API_BASE_URLS.length) {
config.OPENAI_API_KEYS = config.OPENAI_API_KEYS.slice(
0,
config.OPENAI_API_BASE_URLS.length
);
}
// if there are more urls than keys, add empty keys
if (config.OPENAI_API_KEYS.length < config.OPENAI_API_BASE_URLS.length) {
const diff = config.OPENAI_API_BASE_URLS.length - config.OPENAI_API_KEYS.length;
for (let i = 0; i < diff; i++) {
config.OPENAI_API_KEYS.push('');
}
}
}
await saveSettings({
directConnections: config
});
};
onMount(async () => {
config = $settings?.directConnections ?? {
OPENAI_API_BASE_URLS: [],
OPENAI_API_KEYS: [],
OPENAI_API_CONFIGS: {}
};
});
</script>
<AddConnectionModal direct bind:show={showConnectionModal} onSubmit={addConnectionHandler} />
<form
class="flex flex-col h-full justify-between text-sm"
on:submit|preventDefault={() => {
updateHandler();
}}
>
<div class=" overflow-y-scroll scrollbar-hidden h-full">
{#if config !== null}
<div class="">
<div class="pr-1.5">
<div class="">
<div class="flex justify-between items-center mb-0.5">
<div class="font-medium">{$i18n.t('Manage Direct Connections')}</div>
<Tooltip content={$i18n.t(`Add Connection`)}>
<button
class="px-1"
on:click={() => {
showConnectionModal = true;
}}
type="button"
>
<Plus />
</button>
</Tooltip>
</div>
<div class="flex flex-col gap-1.5">
{#each config?.OPENAI_API_BASE_URLS ?? [] as url, idx}
<Connection
bind:url
bind:key={config.OPENAI_API_KEYS[idx]}
bind:config={config.OPENAI_API_CONFIGS[idx]}
onSubmit={() => {
updateHandler();
}}
onDelete={() => {
config.OPENAI_API_BASE_URLS = config.OPENAI_API_BASE_URLS.filter(
(url, urlIdx) => idx !== urlIdx
);
config.OPENAI_API_KEYS = config.OPENAI_API_KEYS.filter(
(key, keyIdx) => idx !== keyIdx
);
let newConfig = {};
config.OPENAI_API_BASE_URLS.forEach((url, newIdx) => {
newConfig[newIdx] =
config.OPENAI_API_CONFIGS[newIdx < idx ? newIdx : newIdx + 1];
});
config.OPENAI_API_CONFIGS = newConfig;
}}
/>
{/each}
</div>
</div>
<div class="my-1.5">
<div class="text-xs text-gray-500">
{$i18n.t('Connect to your own OpenAI compatible API endpoints.')}
</div>
</div>
</div>
</div>
{:else}
<div class="flex h-full justify-center">
<div class="my-auto">
<Spinner className="size-6" />
</div>
</div>
{/if}
</div>
<div class="flex justify-end pt-3 text-sm font-medium">
<button
class="px-3.5 py-1.5 text-sm font-medium bg-black hover:bg-gray-900 text-white dark:bg-white dark:text-black dark:hover:bg-gray-100 transition rounded-full"
type="submit"
>
{$i18n.t('Save')}
</button>
</div>
</form>

View File

@@ -0,0 +1,83 @@
<script lang="ts">
import { getContext, tick } from 'svelte';
const i18n = getContext('i18n');
import Tooltip from '$lib/components/common/Tooltip.svelte';
import SensitiveInput from '$lib/components/common/SensitiveInput.svelte';
import Cog6 from '$lib/components/icons/Cog6.svelte';
import AddConnectionModal from '$lib/components/AddConnectionModal.svelte';
export let onDelete = () => {};
export let onSubmit = () => {};
export let pipeline = false;
export let url = '';
export let key = '';
export let config = {};
let showConfigModal = false;
</script>
<AddConnectionModal
edit
bind:show={showConfigModal}
connection={{
url,
key,
config
}}
{onDelete}
onSubmit={(connection) => {
url = connection.url;
key = connection.key;
config = connection.config;
onSubmit(connection);
}}
/>
<div class="flex w-full gap-2 items-center">
<Tooltip
className="w-full relative"
content={$i18n.t(`WebUI will make requests to "{{url}}/chat/completions"`, {
url
})}
placement="top-start"
>
{#if !(config?.enable ?? true)}
<div
class="absolute top-0 bottom-0 left-0 right-0 opacity-60 bg-white dark:bg-gray-900 z-10"
></div>
{/if}
<div class="flex w-full">
<div class="flex-1 relative">
<input
class=" outline-none w-full bg-transparent {pipeline ? 'pr-8' : ''}"
placeholder={$i18n.t('API Base URL')}
bind:value={url}
autocomplete="off"
/>
</div>
<SensitiveInput
inputClassName=" outline-none bg-transparent w-full"
placeholder={$i18n.t('API Key')}
bind:value={key}
/>
</div>
</Tooltip>
<div class="flex gap-1">
<Tooltip content={$i18n.t('Configure')} className="self-start">
<button
class="self-center p-1 bg-transparent hover:bg-gray-100 dark:bg-gray-900 dark:hover:bg-gray-850 rounded-lg transition"
on:click={() => {
showConfigModal = true;
}}
type="button"
>
<Cog6 />
</button>
</Tooltip>
</div>
</div>

View File

@@ -49,6 +49,7 @@
function_calling: null,
seed: null,
temperature: null,
reasoning_effort: null,
frequency_penalty: null,
repeat_last_n: null,
mirostat: null,
@@ -333,9 +334,13 @@
system: system !== '' ? system : undefined,
params: {
stream_response: params.stream_response !== null ? params.stream_response : undefined,
function_calling:
params.function_calling !== null ? params.function_calling : undefined,
seed: (params.seed !== null ? params.seed : undefined) ?? undefined,
stop: params.stop ? params.stop.split(',').filter((e) => e) : undefined,
temperature: params.temperature !== null ? params.temperature : undefined,
reasoning_effort:
params.reasoning_effort !== null ? params.reasoning_effort : undefined,
frequency_penalty:
params.frequency_penalty !== null ? params.frequency_penalty : undefined,
repeat_last_n: params.repeat_last_n !== null ? params.repeat_last_n : undefined,

View File

@@ -1,7 +1,7 @@
<script lang="ts">
import { getContext, tick } from 'svelte';
import { toast } from 'svelte-sonner';
import { models, settings, user } from '$lib/stores';
import { config, models, settings, user } from '$lib/stores';
import { updateUserSettings } from '$lib/apis/users';
import { getModels as _getModels } from '$lib/apis';
import { goto } from '$app/navigation';
@@ -17,6 +17,7 @@
import Personalization from './Settings/Personalization.svelte';
import SearchInput from '../layout/Sidebar/SearchInput.svelte';
import Search from '../icons/Search.svelte';
import Connections from './Settings/Connections.svelte';
const i18n = getContext('i18n');
@@ -122,6 +123,11 @@
'alwaysonwebsearch'
]
},
{
id: 'connections',
title: 'Connections',
keywords: []
},
{
id: 'personalization',
title: 'Personalization',
@@ -316,7 +322,10 @@
};
const getModels = async () => {
return await _getModels(localStorage.token);
return await _getModels(
localStorage.token,
$config?.features?.enable_direct_connections && ($settings?.directConnections ?? null)
);
};
let selectedTab = 'general';
@@ -447,6 +456,32 @@
</div>
<div class=" self-center">{$i18n.t('Interface')}</div>
</button>
{:else if tabId === 'connections'}
{#if $user.role === 'admin' || ($user.role === 'user' && $config?.features?.enable_direct_connections)}
<button
class="px-0.5 py-1 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
'connections'
? ''
: ' text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'}"
on:click={() => {
selectedTab = 'connections';
}}
>
<div class=" self-center mr-2">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
>
<path
d="M1 9.5A3.5 3.5 0 0 0 4.5 13H12a3 3 0 0 0 .917-5.857 2.503 2.503 0 0 0-3.198-3.019 3.5 3.5 0 0 0-6.628 2.171A3.5 3.5 0 0 0 1 9.5Z"
/>
</svg>
</div>
<div class=" self-center">{$i18n.t('Connections')}</div>
</button>
{/if}
{:else if tabId === 'personalization'}
<button
class="px-0.5 py-1 min-w-fit rounded-lg flex-1 md:flex-none flex text-left transition {selectedTab ===
@@ -620,6 +655,13 @@
toast.success($i18n.t('Settings saved successfully!'));
}}
/>
{:else if selectedTab === 'connections'}
<Connections
saveSettings={async (updated) => {
await saveSettings(updated);
toast.success($i18n.t('Settings saved successfully!'));
}}
/>
{:else if selectedTab === 'personalization'}
<Personalization
{saveSettings}

View File

@@ -30,7 +30,7 @@
const _chat = chat.chat;
console.log('share', _chat);
toast.success($i18n.t('Redirecting you to OpenWebUI Community'));
toast.success($i18n.t('Redirecting you to Open WebUI Community'));
const url = 'https://openwebui.com';
// const url = 'http://localhost:5173';
@@ -143,7 +143,7 @@
show = false;
}}
>
{$i18n.t('Share to OpenWebUI Community')}
{$i18n.t('Share to Open WebUI Community')}
</button>
{/if}

View File

@@ -75,9 +75,15 @@
<div class="">
{#if attributes?.type === 'reasoning'}
{#if attributes?.done === 'true' && attributes?.duration}
{$i18n.t('Thought for {{DURATION}}', {
DURATION: dayjs.duration(attributes.duration, 'seconds').humanize()
})}
{#if attributes.duration < 60}
{$i18n.t('Thought for {{DURATION}} seconds', {
DURATION: attributes.duration
})}
{:else}
{$i18n.t('Thought for {{DURATION}}', {
DURATION: dayjs.duration(attributes.duration, 'seconds').humanize()
})}
{/if}
{:else}
{$i18n.t('Thinking...')}
{/if}

View File

@@ -23,6 +23,7 @@
/>
<button
class={showButtonClassName}
type="button"
on:click={(e) => {
e.preventDefault();
show = !show;

View File

@@ -68,7 +68,12 @@
toast.success($i18n.t(`Deleted {{name}}`, { name: model.id }));
}
await _models.set(await getModels(localStorage.token));
await _models.set(
await getModels(
localStorage.token,
$config?.features?.enable_direct_connections && ($settings?.directConnections ?? null)
)
);
models = await getWorkspaceModels(localStorage.token);
};
@@ -82,7 +87,7 @@
};
const shareModelHandler = async (model) => {
toast.success($i18n.t('Redirecting you to OpenWebUI Community'));
toast.success($i18n.t('Redirecting you to Open WebUI Community'));
const url = 'https://openwebui.com';
@@ -134,7 +139,12 @@
);
}
await _models.set(await getModels(localStorage.token));
await _models.set(
await getModels(
localStorage.token,
$config?.features?.enable_direct_connections && ($settings?.directConnections ?? null)
)
);
models = await getWorkspaceModels(localStorage.token);
};
@@ -371,7 +381,13 @@
bind:state={model.is_active}
on:change={async (e) => {
toggleModelById(localStorage.token, model.id);
_models.set(await getModels(localStorage.token));
_models.set(
await getModels(
localStorage.token,
$config?.features?.enable_direct_connections &&
($settings?.directConnections ?? null)
)
);
}}
/>
</Tooltip>
@@ -417,7 +433,13 @@
}
}
await _models.set(await getModels(localStorage.token));
await _models.set(
await getModels(
localStorage.token,
$config?.features?.enable_direct_connections &&
($settings?.directConnections ?? null)
)
);
models = await getWorkspaceModels(localStorage.token);
};
@@ -479,7 +501,7 @@
{#if $config?.features.enable_community_sharing}
<div class=" my-16">
<div class=" text-xl font-medium mb-1 line-clamp-1">
{$i18n.t('Made by OpenWebUI Community')}
{$i18n.t('Made by Open WebUI Community')}
</div>
<a

View File

@@ -40,7 +40,7 @@
$: filteredItems = prompts.filter((p) => query === '' || p.command.includes(query));
const shareHandler = async (prompt) => {
toast.success($i18n.t('Redirecting you to OpenWebUI Community'));
toast.success($i18n.t('Redirecting you to Open WebUI Community'));
const url = 'https://openwebui.com';
@@ -319,7 +319,7 @@
{#if $config?.features.enable_community_sharing}
<div class=" my-16">
<div class=" text-xl font-medium mb-1 line-clamp-1">
{$i18n.t('Made by OpenWebUI Community')}
{$i18n.t('Made by Open WebUI Community')}
</div>
<a

View File

@@ -65,7 +65,7 @@
return null;
});
toast.success($i18n.t('Redirecting you to OpenWebUI Community'));
toast.success($i18n.t('Redirecting you to Open WebUI Community'));
const url = 'https://openwebui.com';
@@ -438,7 +438,7 @@
{#if $config?.features.enable_community_sharing}
<div class=" my-16">
<div class=" text-xl font-medium mb-1 line-clamp-1">
{$i18n.t('Made by OpenWebUI Community')}
{$i18n.t('Made by Open WebUI Community')}
</div>
<a