mirror of
https://github.com/open-webui/open-webui
synced 2025-06-26 18:26:48 +00:00
Merge branch 'dev' into fix-9864
This commit is contained in:
@@ -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'));
|
||||
@@ -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">
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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={() => {
|
||||
|
||||
@@ -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>
|
||||
|
||||
166
src/lib/components/admin/Settings/CodeInterpreter.svelte
Normal file
166
src/lib/components/admin/Settings/CodeInterpreter.svelte
Normal 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>
|
||||
@@ -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">
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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 = () => {};
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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">
|
||||
|
||||
152
src/lib/components/chat/Settings/Connections.svelte
Normal file
152
src/lib/components/chat/Settings/Connections.svelte
Normal 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>
|
||||
@@ -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>
|
||||
@@ -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,
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
/>
|
||||
<button
|
||||
class={showButtonClassName}
|
||||
type="button"
|
||||
on:click={(e) => {
|
||||
e.preventDefault();
|
||||
show = !show;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user