mirror of
https://github.com/open-webui/open-webui.git
synced 2026-07-23 09:10:55 -05:00
refac
This commit is contained in:
@@ -10,6 +10,7 @@
|
||||
import { getFunctions } from '$lib/apis/functions';
|
||||
import { getModelsDefaults } from '$lib/apis/configs';
|
||||
import { getBaseModelTags, getModelTags } from '$lib/apis/models';
|
||||
import { getVoices } from '$lib/apis/audio';
|
||||
|
||||
import AdvancedParams from '$lib/components/chat/Settings/Advanced/AdvancedParams.svelte';
|
||||
import Tags from '$lib/components/common/Tags.svelte';
|
||||
@@ -28,6 +29,7 @@
|
||||
import BuiltinTools from './BuiltinTools.svelte';
|
||||
import PromptSuggestions from './PromptSuggestions.svelte';
|
||||
import TerminalSelector from './TerminalSelector.svelte';
|
||||
import TTSVoiceInput from './TTSVoiceInput.svelte';
|
||||
import AccessControlModal from '../common/AccessControlModal.svelte';
|
||||
import LockClosed from '$lib/components/icons/LockClosed.svelte';
|
||||
|
||||
@@ -108,6 +110,7 @@
|
||||
let terminalId = '';
|
||||
let tts = { voice: '' };
|
||||
export let suggestionTags: { name: string }[] = [];
|
||||
let voices: { id: string; name?: string }[] = [];
|
||||
|
||||
const loadSuggestionTags = async () => {
|
||||
const res: string[] = await (preset ? getModelTags : getBaseModelTags)(
|
||||
@@ -116,6 +119,11 @@
|
||||
suggestionTags = res.map((tag) => ({ name: tag }));
|
||||
};
|
||||
|
||||
const loadVoices = async () => {
|
||||
const res = await getVoices(localStorage.token).catch(() => null);
|
||||
voices = res?.voices ?? [];
|
||||
};
|
||||
|
||||
const submitHandler = async () => {
|
||||
loading = true;
|
||||
|
||||
@@ -267,6 +275,9 @@
|
||||
if (suggestionTags.length === 0) {
|
||||
await loadSuggestionTags();
|
||||
}
|
||||
if (voices.length === 0) {
|
||||
await loadVoices();
|
||||
}
|
||||
|
||||
// Fetch admin-configured default model metadata so the editor
|
||||
// reflects the actual defaults rather than hardcoded values
|
||||
@@ -855,10 +866,9 @@
|
||||
{$i18n.t('TTS Voice')}
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
class="w-full text-sm bg-transparent outline-hidden"
|
||||
type="text"
|
||||
<TTSVoiceInput
|
||||
bind:value={tts.voice}
|
||||
{voices}
|
||||
placeholder={$i18n.t('e.g. alloy, echo, shimmer')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
137
src/lib/components/workspace/Models/TTSVoiceInput.svelte
Normal file
137
src/lib/components/workspace/Models/TTSVoiceInput.svelte
Normal file
@@ -0,0 +1,137 @@
|
||||
<script lang="ts">
|
||||
import { tick } from 'svelte';
|
||||
|
||||
type Voice = {
|
||||
id: string;
|
||||
name?: string;
|
||||
};
|
||||
|
||||
export let value = '';
|
||||
export let voices: Voice[] = [];
|
||||
export let placeholder = '';
|
||||
|
||||
const listboxId = 'tts-voice-options';
|
||||
|
||||
let inputElement: HTMLInputElement | null = null;
|
||||
let popupElement: HTMLDivElement | null = null;
|
||||
let suggestionsOpen = false;
|
||||
|
||||
$: query = value.trim().toLowerCase();
|
||||
$: filteredVoices = (voices ?? [])
|
||||
.filter((voice) => {
|
||||
const id = voice.id.toLowerCase();
|
||||
const name = (voice.name ?? '').toLowerCase();
|
||||
|
||||
return query === '' || id.includes(query) || name.includes(query);
|
||||
})
|
||||
.slice(0, 8);
|
||||
$: if (suggestionsOpen && filteredVoices.length > 0) {
|
||||
tick().then(positionPopup);
|
||||
}
|
||||
|
||||
const portal = (node: HTMLElement) => {
|
||||
document.body.appendChild(node);
|
||||
return {
|
||||
destroy() {
|
||||
node.remove();
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const positionPopup = () => {
|
||||
if (!inputElement || !popupElement) return;
|
||||
|
||||
const rect = inputElement.getBoundingClientRect();
|
||||
const width = Math.min(192, rect.width, window.innerWidth - 16);
|
||||
|
||||
popupElement.style.top = `${rect.bottom + 4}px`;
|
||||
popupElement.style.left = `${Math.max(8, Math.min(rect.left, window.innerWidth - width - 8))}px`;
|
||||
popupElement.style.width = `${width}px`;
|
||||
};
|
||||
|
||||
const selectVoice = (voice: Voice) => {
|
||||
value = voice.id;
|
||||
suggestionsOpen = false;
|
||||
};
|
||||
|
||||
const handlePointerDown = (event: PointerEvent) => {
|
||||
if (!suggestionsOpen) return;
|
||||
|
||||
const target = event.target as Node;
|
||||
if (inputElement?.contains(target) || popupElement?.contains(target)) {
|
||||
return;
|
||||
}
|
||||
|
||||
suggestionsOpen = false;
|
||||
};
|
||||
</script>
|
||||
|
||||
<svelte:window
|
||||
on:pointerdown={handlePointerDown}
|
||||
on:scroll|capture={positionPopup}
|
||||
on:resize={positionPopup}
|
||||
/>
|
||||
|
||||
<input
|
||||
bind:this={inputElement}
|
||||
bind:value
|
||||
class="w-full text-sm bg-transparent outline-hidden"
|
||||
type="text"
|
||||
{placeholder}
|
||||
role="combobox"
|
||||
aria-autocomplete="list"
|
||||
aria-controls={listboxId}
|
||||
aria-expanded={suggestionsOpen && filteredVoices.length > 0}
|
||||
autocomplete="off"
|
||||
on:focus={() => {
|
||||
suggestionsOpen = true;
|
||||
positionPopup();
|
||||
}}
|
||||
on:input={() => {
|
||||
suggestionsOpen = true;
|
||||
positionPopup();
|
||||
}}
|
||||
on:keydown={(event) => {
|
||||
if (event.key === 'Escape') {
|
||||
suggestionsOpen = false;
|
||||
}
|
||||
}}
|
||||
on:blur={() => {
|
||||
setTimeout(() => {
|
||||
if (!popupElement?.contains(document.activeElement)) {
|
||||
suggestionsOpen = false;
|
||||
}
|
||||
}, 0);
|
||||
}}
|
||||
/>
|
||||
|
||||
{#if suggestionsOpen && filteredVoices.length > 0}
|
||||
<div
|
||||
use:portal
|
||||
bind:this={popupElement}
|
||||
id={listboxId}
|
||||
class="fixed max-h-48 overflow-y-auto rounded-lg border border-gray-200 bg-white p-0.5 shadow-lg dark:border-gray-800 dark:bg-gray-850"
|
||||
role="listbox"
|
||||
style="z-index: 9999; top: 0; left: 0;"
|
||||
>
|
||||
{#each filteredVoices as voice (voice.id)}
|
||||
<button
|
||||
type="button"
|
||||
class="flex w-full items-center justify-between gap-3 rounded-md px-2 py-1.5 text-left text-xs text-gray-700 transition-colors hover:bg-gray-50 dark:text-gray-200 dark:hover:bg-gray-800"
|
||||
role="option"
|
||||
aria-selected={value === voice.id}
|
||||
on:mousedown={(event) => {
|
||||
event.preventDefault();
|
||||
}}
|
||||
on:click={() => {
|
||||
selectVoice(voice);
|
||||
}}
|
||||
>
|
||||
<span class="truncate">{voice.id}</span>
|
||||
{#if voice.name && voice.name !== voice.id}
|
||||
<span class="truncate text-gray-500 dark:text-gray-400">{voice.name}</span>
|
||||
{/if}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
Reference in New Issue
Block a user