Unverified Commit 96a004d4 authored by Timothy Jaeryang Baek's avatar Timothy Jaeryang Baek Committed by GitHub
Browse files

Merge pull request #2921 from open-webui/dev

0.3.0
parents a8d80f93 1fa16d73
...@@ -6,6 +6,7 @@ ...@@ -6,6 +6,7 @@
const i18n = getContext('i18n'); const i18n = getContext('i18n');
export let files;
export let prompt = ''; export let prompt = '';
let selectedCommandIdx = 0; let selectedCommandIdx = 0;
let filteredPromptCommands = []; let filteredPromptCommands = [];
...@@ -35,6 +36,32 @@ ...@@ -35,6 +36,32 @@
return '{{CLIPBOARD}}'; return '{{CLIPBOARD}}';
}); });
console.log(clipboardText);
const clipboardItems = await navigator.clipboard.read();
let imageUrl = null;
for (const item of clipboardItems) {
// Check for known image types
for (const type of item.types) {
if (type.startsWith('image/')) {
const blob = await item.getType(type);
imageUrl = URL.createObjectURL(blob);
console.log(`Image URL (${type}): ${imageUrl}`);
}
}
}
if (imageUrl) {
files = [
...files,
{
type: 'image',
url: imageUrl
}
];
}
text = command.content.replaceAll('{{CLIPBOARD}}', clipboardText); text = command.content.replaceAll('{{CLIPBOARD}}', clipboardText);
} }
...@@ -61,7 +88,7 @@ ...@@ -61,7 +88,7 @@
</script> </script>
{#if filteredPromptCommands.length > 0} {#if filteredPromptCommands.length > 0}
<div class="md:px-2 mb-3 text-left w-full absolute bottom-0 left-0 right-0"> <div class="pl-1 pr-12 mb-3 text-left w-full absolute bottom-0 left-0 right-0">
<div class="flex w-full px-2"> <div class="flex w-full px-2">
<div class=" bg-gray-100 dark:bg-gray-700 w-10 rounded-l-xl text-center"> <div class=" bg-gray-100 dark:bg-gray-700 w-10 rounded-l-xl text-center">
<div class=" text-lg font-semibold mt-2">/</div> <div class=" text-lg font-semibold mt-2">/</div>
......
<script lang="ts">
import { toast } from 'svelte-sonner';
import { createEventDispatcher, tick, getContext } from 'svelte';
import { config, settings } from '$lib/stores';
import { blobToFile, calculateSHA256, findWordIndices } from '$lib/utils';
import { transcribeAudio } from '$lib/apis/audio';
const i18n = getContext('i18n');
const dispatch = createEventDispatcher();
export let recording = false;
let loading = false;
let confirmed = false;
let durationSeconds = 0;
let durationCounter = null;
let transcription = '';
const startDurationCounter = () => {
durationCounter = setInterval(() => {
durationSeconds++;
}, 1000);
};
const stopDurationCounter = () => {
clearInterval(durationCounter);
durationSeconds = 0;
};
$: if (recording) {
startRecording();
} else {
stopRecording();
}
const formatSeconds = (seconds) => {
const minutes = Math.floor(seconds / 60);
const remainingSeconds = seconds % 60;
const formattedSeconds = remainingSeconds < 10 ? `0${remainingSeconds}` : remainingSeconds;
return `${minutes}:${formattedSeconds}`;
};
let speechRecognition;
let mediaRecorder;
let audioChunks = [];
const MIN_DECIBELS = -45;
const VISUALIZER_BUFFER_LENGTH = 300;
let visualizerData = Array(VISUALIZER_BUFFER_LENGTH).fill(0);
// Function to calculate the RMS level from time domain data
const calculateRMS = (data: Uint8Array) => {
let sumSquares = 0;
for (let i = 0; i < data.length; i++) {
const normalizedValue = (data[i] - 128) / 128; // Normalize the data
sumSquares += normalizedValue * normalizedValue;
}
return Math.sqrt(sumSquares / data.length);
};
const normalizeRMS = (rms) => {
rms = rms * 10;
const exp = 1.5; // Adjust exponent value; values greater than 1 expand larger numbers more and compress smaller numbers more
const scaledRMS = Math.pow(rms, exp);
// Scale between 0.01 (1%) and 1.0 (100%)
return Math.min(1.0, Math.max(0.01, scaledRMS));
};
const analyseAudio = (stream) => {
const audioContext = new AudioContext();
const audioStreamSource = audioContext.createMediaStreamSource(stream);
const analyser = audioContext.createAnalyser();
analyser.minDecibels = MIN_DECIBELS;
audioStreamSource.connect(analyser);
const bufferLength = analyser.frequencyBinCount;
const domainData = new Uint8Array(bufferLength);
const timeDomainData = new Uint8Array(analyser.fftSize);
let lastSoundTime = Date.now();
const detectSound = () => {
const processFrame = () => {
if (!recording || loading) return;
if (recording && !loading) {
analyser.getByteTimeDomainData(timeDomainData);
analyser.getByteFrequencyData(domainData);
// Calculate RMS level from time domain data
const rmsLevel = calculateRMS(timeDomainData);
// Push the calculated decibel level to visualizerData
visualizerData.push(normalizeRMS(rmsLevel));
// Ensure visualizerData array stays within the buffer length
if (visualizerData.length >= VISUALIZER_BUFFER_LENGTH) {
visualizerData.shift();
}
visualizerData = visualizerData;
// if (domainData.some((value) => value > 0)) {
// lastSoundTime = Date.now();
// }
// if (recording && Date.now() - lastSoundTime > 3000) {
// if ($settings?.speechAutoSend ?? false) {
// confirmRecording();
// }
// }
}
window.requestAnimationFrame(processFrame);
};
window.requestAnimationFrame(processFrame);
};
detectSound();
};
const transcribeHandler = async (audioBlob) => {
// Create a blob from the audio chunks
await tick();
const file = blobToFile(audioBlob, 'recording.wav');
const res = await transcribeAudio(localStorage.token, file).catch((error) => {
toast.error(error);
return null;
});
if (res) {
console.log(res.text);
dispatch('confirm', res.text);
}
};
const saveRecording = (blob) => {
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
document.body.appendChild(a);
a.style = 'display: none';
a.href = url;
a.download = 'recording.wav';
a.click();
window.URL.revokeObjectURL(url);
};
const startRecording = async () => {
startDurationCounter();
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
mediaRecorder = new MediaRecorder(stream);
mediaRecorder.onstart = () => {
console.log('Recording started');
audioChunks = [];
analyseAudio(stream);
};
mediaRecorder.ondataavailable = (event) => audioChunks.push(event.data);
mediaRecorder.onstop = async () => {
console.log('Recording stopped');
if (($settings?.audio?.stt?.engine ?? '') === 'web') {
audioChunks = [];
} else {
if (confirmed) {
const audioBlob = new Blob(audioChunks, { type: 'audio/wav' });
await transcribeHandler(audioBlob);
confirmed = false;
loading = false;
}
audioChunks = [];
recording = false;
}
};
mediaRecorder.start();
if ($config.audio.stt.engine === 'web' || ($settings?.audio?.stt?.engine ?? '') === 'web') {
if ('SpeechRecognition' in window || 'webkitSpeechRecognition' in window) {
// Create a SpeechRecognition object
speechRecognition = new (window.SpeechRecognition || window.webkitSpeechRecognition)();
// Set continuous to true for continuous recognition
speechRecognition.continuous = true;
// Set the timeout for turning off the recognition after inactivity (in milliseconds)
const inactivityTimeout = 2000; // 3 seconds
let timeoutId;
// Start recognition
speechRecognition.start();
// Event triggered when speech is recognized
speechRecognition.onresult = async (event) => {
// Clear the inactivity timeout
clearTimeout(timeoutId);
// Handle recognized speech
console.log(event);
const transcript = event.results[Object.keys(event.results).length - 1][0].transcript;
transcription = `${transcription}${transcript}`;
await tick();
document.getElementById('chat-textarea')?.focus();
// Restart the inactivity timeout
timeoutId = setTimeout(() => {
console.log('Speech recognition turned off due to inactivity.');
speechRecognition.stop();
}, inactivityTimeout);
};
// Event triggered when recognition is ended
speechRecognition.onend = function () {
// Restart recognition after it ends
console.log('recognition ended');
confirmRecording();
dispatch('confirm', transcription);
confirmed = false;
loading = false;
};
// Event triggered when an error occurs
speechRecognition.onerror = function (event) {
console.log(event);
toast.error($i18n.t(`Speech recognition error: {{error}}`, { error: event.error }));
dispatch('cancel');
stopRecording();
};
}
}
};
const stopRecording = async () => {
if (recording && mediaRecorder) {
await mediaRecorder.stop();
}
stopDurationCounter();
audioChunks = [];
};
const confirmRecording = async () => {
loading = true;
confirmed = true;
if (recording && mediaRecorder) {
await mediaRecorder.stop();
}
clearInterval(durationCounter);
};
</script>
<div
class="{loading
? ' bg-gray-100/50 dark:bg-gray-850/50'
: 'bg-indigo-300/10 dark:bg-indigo-500/10 '} rounded-full flex p-2.5"
>
<div class="flex items-center mr-1">
<button
type="button"
class="p-1.5
{loading
? ' bg-gray-200 dark:bg-gray-700/50'
: 'bg-indigo-400/20 text-indigo-600 dark:text-indigo-300 '}
rounded-full"
on:click={async () => {
dispatch('cancel');
stopRecording();
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="3"
stroke="currentColor"
class="size-4"
>
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12" />
</svg>
</button>
</div>
<div
class="flex flex-1 self-center items-center justify-between ml-2 mx-1 overflow-hidden h-6"
dir="rtl"
>
<div class="flex-1 flex items-center gap-0.5 h-6">
{#each visualizerData.slice().reverse() as rms}
<div
class="w-[2px]
{loading
? ' bg-gray-500 dark:bg-gray-400 '
: 'bg-indigo-500 dark:bg-indigo-400 '}
inline-block h-full"
style="height: {Math.min(100, Math.max(14, rms * 100))}%;"
/>
{/each}
</div>
</div>
<div class=" mx-1.5 pr-1 flex justify-center items-center">
<div
class="text-sm
{loading ? ' text-gray-500 dark:text-gray-400 ' : ' text-indigo-400 '}
font-medium flex-1 mx-auto text-center"
>
{formatSeconds(durationSeconds)}
</div>
</div>
<div class="flex items-center mr-1">
{#if loading}
<div class=" text-gray-500 rounded-full cursor-not-allowed">
<svg
width="24"
height="24"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
fill="currentColor"
><style>
.spinner_OSmW {
transform-origin: center;
animation: spinner_T6mA 0.75s step-end infinite;
}
@keyframes spinner_T6mA {
8.3% {
transform: rotate(30deg);
}
16.6% {
transform: rotate(60deg);
}
25% {
transform: rotate(90deg);
}
33.3% {
transform: rotate(120deg);
}
41.6% {
transform: rotate(150deg);
}
50% {
transform: rotate(180deg);
}
58.3% {
transform: rotate(210deg);
}
66.6% {
transform: rotate(240deg);
}
75% {
transform: rotate(270deg);
}
83.3% {
transform: rotate(300deg);
}
91.6% {
transform: rotate(330deg);
}
100% {
transform: rotate(360deg);
}
}
</style><g class="spinner_OSmW"
><rect x="11" y="1" width="2" height="5" opacity=".14" /><rect
x="11"
y="1"
width="2"
height="5"
transform="rotate(30 12 12)"
opacity=".29"
/><rect
x="11"
y="1"
width="2"
height="5"
transform="rotate(60 12 12)"
opacity=".43"
/><rect
x="11"
y="1"
width="2"
height="5"
transform="rotate(90 12 12)"
opacity=".57"
/><rect
x="11"
y="1"
width="2"
height="5"
transform="rotate(120 12 12)"
opacity=".71"
/><rect
x="11"
y="1"
width="2"
height="5"
transform="rotate(150 12 12)"
opacity=".86"
/><rect x="11" y="1" width="2" height="5" transform="rotate(180 12 12)" /></g
></svg
>
</div>
{:else}
<button
type="button"
class="p-1.5 bg-indigo-500 text-white dark:bg-indigo-500 dark:text-blue-950 rounded-full"
on:click={async () => {
await confirmRecording();
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="2.5"
stroke="currentColor"
class="size-4"
>
<path stroke-linecap="round" stroke-linejoin="round" d="m4.5 12.75 6 6 9-13.5" />
</svg>
</button>
{/if}
</div>
</div>
<style>
.visualizer {
display: flex;
height: 100%;
}
.visualizer-bar {
width: 2px;
background-color: #4a5aba; /* or whatever color you need */
}
</style>
...@@ -109,7 +109,7 @@ ...@@ -109,7 +109,7 @@
class=" snap-center min-w-80 w-full max-w-full m-1 border {history.messages[ class=" snap-center min-w-80 w-full max-w-full m-1 border {history.messages[
currentMessageId currentMessageId
].model === model ].model === model
? 'border-gray-100 dark:border-gray-700 border-[1.5px]' ? 'border-gray-100 dark:border-gray-850 border-[1.5px]'
: 'border-gray-50 dark:border-gray-850 '} transition p-5 rounded-3xl" : 'border-gray-50 dark:border-gray-850 '} transition p-5 rounded-3xl"
on:click={() => { on:click={() => {
currentMessageId = groupedMessages[model].messages[groupedMessagesIdx[model]].id; currentMessageId = groupedMessages[model].messages[groupedMessagesIdx[model]].id;
......
...@@ -213,7 +213,7 @@ ...@@ -213,7 +213,7 @@
} else { } else {
speaking = true; speaking = true;
if ($settings?.audio?.TTSEngine === 'openai') { if ($config.audio.tts.engine === 'openai') {
loadingSpeech = true; loadingSpeech = true;
const sentences = extractSentences(message.content).reduce((mergedTexts, currentText) => { const sentences = extractSentences(message.content).reduce((mergedTexts, currentText) => {
...@@ -244,9 +244,8 @@ ...@@ -244,9 +244,8 @@
for (const [idx, sentence] of sentences.entries()) { for (const [idx, sentence] of sentences.entries()) {
const res = await synthesizeOpenAISpeech( const res = await synthesizeOpenAISpeech(
localStorage.token, localStorage.token,
$settings?.audio?.speaker, $settings?.audio?.tts?.voice ?? $config?.audio?.tts?.voice,
sentence, sentence
$settings?.audio?.model
).catch((error) => { ).catch((error) => {
toast.error(error); toast.error(error);
...@@ -273,17 +272,29 @@ ...@@ -273,17 +272,29 @@
clearInterval(getVoicesLoop); clearInterval(getVoicesLoop);
const voice = const voice =
voices?.filter((v) => v.name === $settings?.audio?.speaker)?.at(0) ?? undefined; voices
?.filter(
(v) => v.voiceURI === ($settings?.audio?.tts?.voice ?? $config?.audio?.tts?.voice)
)
?.at(0) ?? undefined;
console.log(voice);
const speak = new SpeechSynthesisUtterance(message.content); const speak = new SpeechSynthesisUtterance(message.content);
console.log(speak);
speak.onend = () => { speak.onend = () => {
speaking = null; speaking = null;
if ($settings.conversationMode) { if ($settings.conversationMode) {
document.getElementById('voice-input-button')?.click(); document.getElementById('voice-input-button')?.click();
} }
}; };
speak.voice = voice;
if (voice) {
speak.voice = voice;
}
speechSynthesis.speak(speak); speechSynthesis.speak(speak);
} }
}, 100); }, 100);
...@@ -757,7 +768,7 @@ ...@@ -757,7 +768,7 @@
</Tooltip> </Tooltip>
{#if $config?.features.enable_image_generation && !readOnly} {#if $config?.features.enable_image_generation && !readOnly}
<Tooltip content="Generate Image" placement="bottom"> <Tooltip content={$i18n.t('Generate Image')} placement="bottom">
<button <button
class="{isLastMessage class="{isLastMessage
? 'visible' ? 'visible'
......
...@@ -219,7 +219,7 @@ ...@@ -219,7 +219,7 @@
<DropdownMenu.Content <DropdownMenu.Content
class=" z-40 {$mobile class=" z-40 {$mobile
? `w-full` ? `w-full`
: `${className}`} max-w-[calc(100vw-1rem)] justify-start rounded-xl bg-white dark:bg-gray-850 dark:text-white shadow-lg border border-gray-300/30 dark:border-gray-700/50 outline-none " : `${className}`} max-w-[calc(100vw-1rem)] justify-start rounded-xl bg-white dark:bg-gray-850 dark:text-white shadow-lg border border-gray-300/30 dark:border-gray-850/50 outline-none "
transition={flyAndScale} transition={flyAndScale}
side={$mobile ? 'bottom' : 'bottom-start'} side={$mobile ? 'bottom' : 'bottom-start'}
sideOffset={4} sideOffset={4}
...@@ -265,7 +265,7 @@ ...@@ -265,7 +265,7 @@
</div> </div>
{/if} {/if}
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<div class="flex items-center"> <div class="flex items-center min-w-fit">
<div class="line-clamp-1"> <div class="line-clamp-1">
{item.label} {item.label}
</div> </div>
......
...@@ -92,7 +92,7 @@ ...@@ -92,7 +92,7 @@
</div> </div>
{#if ollamaVersion} {#if ollamaVersion}
<hr class=" dark:border-gray-700" /> <hr class=" dark:border-gray-850" />
<div> <div>
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Ollama Version')}</div> <div class=" mb-2.5 text-sm font-medium">{$i18n.t('Ollama Version')}</div>
...@@ -104,7 +104,7 @@ ...@@ -104,7 +104,7 @@
</div> </div>
{/if} {/if}
<hr class=" dark:border-gray-700" /> <hr class=" dark:border-gray-850" />
<div class="flex space-x-1"> <div class="flex space-x-1">
<a href="https://discord.gg/5rJgQTnV4s" target="_blank"> <a href="https://discord.gg/5rJgQTnV4s" target="_blank">
......
...@@ -234,7 +234,7 @@ ...@@ -234,7 +234,7 @@
<UpdatePassword /> <UpdatePassword />
</div> </div>
<hr class=" dark:border-gray-700 my-4" /> <hr class=" dark:border-gray-850 my-4" />
<div class="flex justify-between items-center text-sm"> <div class="flex justify-between items-center text-sm">
<div class=" font-medium">{$i18n.t('API keys')}</div> <div class=" font-medium">{$i18n.t('API keys')}</div>
......
...@@ -556,7 +556,7 @@ ...@@ -556,7 +556,7 @@
type="number" type="number"
class=" bg-transparent text-center w-14" class=" bg-transparent text-center w-14"
min="-1" min="-1"
step="10" step="1"
/> />
</div> </div>
</div> </div>
......
<script lang="ts"> <script lang="ts">
import { getAudioConfig, updateAudioConfig } from '$lib/apis/audio'; import { user, settings, config } from '$lib/stores';
import { user, settings } from '$lib/stores';
import { createEventDispatcher, onMount, getContext } from 'svelte'; import { createEventDispatcher, onMount, getContext } from 'svelte';
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';
import Switch from '$lib/components/common/Switch.svelte'; import Switch from '$lib/components/common/Switch.svelte';
...@@ -11,26 +10,15 @@ ...@@ -11,26 +10,15 @@
export let saveSettings: Function; export let saveSettings: Function;
// Audio // Audio
let OpenAIUrl = '';
let OpenAIKey = '';
let OpenAISpeaker = '';
let STTEngines = ['', 'openai'];
let STTEngine = '';
let conversationMode = false; let conversationMode = false;
let speechAutoSend = false; let speechAutoSend = false;
let responseAutoPlayback = false; let responseAutoPlayback = false;
let nonLocalVoices = false; let nonLocalVoices = false;
let TTSEngines = ['', 'openai']; let STTEngine = '';
let TTSEngine = '';
let voices = []; let voices = [];
let speaker = ''; let voice = '';
let models = [];
let model = '';
const getOpenAIVoices = () => { const getOpenAIVoices = () => {
voices = [ voices = [
...@@ -43,10 +31,6 @@ ...@@ -43,10 +31,6 @@
]; ];
}; };
const getOpenAIVoicesModel = () => {
models = [{ name: 'tts-1' }, { name: 'tts-1-hd' }];
};
const getWebAPIVoices = () => { const getWebAPIVoices = () => {
const getVoicesLoop = setInterval(async () => { const getVoicesLoop = setInterval(async () => {
voices = await speechSynthesis.getVoices(); voices = await speechSynthesis.getVoices();
...@@ -58,21 +42,6 @@ ...@@ -58,21 +42,6 @@
}, 100); }, 100);
}; };
const toggleConversationMode = async () => {
conversationMode = !conversationMode;
if (conversationMode) {
responseAutoPlayback = true;
speechAutoSend = true;
}
saveSettings({
conversationMode: conversationMode,
responseAutoPlayback: responseAutoPlayback,
speechAutoSend: speechAutoSend
});
};
const toggleResponseAutoPlayback = async () => { const toggleResponseAutoPlayback = async () => {
responseAutoPlayback = !responseAutoPlayback; responseAutoPlayback = !responseAutoPlayback;
saveSettings({ responseAutoPlayback: responseAutoPlayback }); saveSettings({ responseAutoPlayback: responseAutoPlayback });
...@@ -83,76 +52,35 @@ ...@@ -83,76 +52,35 @@
saveSettings({ speechAutoSend: speechAutoSend }); saveSettings({ speechAutoSend: speechAutoSend });
}; };
const updateConfigHandler = async () => {
if (TTSEngine === 'openai') {
const res = await updateAudioConfig(localStorage.token, {
url: OpenAIUrl,
key: OpenAIKey,
model: model,
speaker: OpenAISpeaker
});
if (res) {
OpenAIUrl = res.OPENAI_API_BASE_URL;
OpenAIKey = res.OPENAI_API_KEY;
model = res.OPENAI_API_MODEL;
OpenAISpeaker = res.OPENAI_API_VOICE;
}
}
};
onMount(async () => { onMount(async () => {
conversationMode = $settings.conversationMode ?? false; conversationMode = $settings.conversationMode ?? false;
speechAutoSend = $settings.speechAutoSend ?? false; speechAutoSend = $settings.speechAutoSend ?? false;
responseAutoPlayback = $settings.responseAutoPlayback ?? false; responseAutoPlayback = $settings.responseAutoPlayback ?? false;
STTEngine = $settings?.audio?.STTEngine ?? ''; STTEngine = $settings?.audio?.stt?.engine ?? '';
TTSEngine = $settings?.audio?.TTSEngine ?? ''; voice = $settings?.audio?.tts?.voice ?? $config.audio.tts.voice ?? '';
nonLocalVoices = $settings.audio?.nonLocalVoices ?? false; nonLocalVoices = $settings.audio?.tts?.nonLocalVoices ?? false;
speaker = $settings?.audio?.speaker ?? '';
model = $settings?.audio?.model ?? '';
if (TTSEngine === 'openai') { if ($config.audio.tts.engine === 'openai') {
getOpenAIVoices(); getOpenAIVoices();
getOpenAIVoicesModel();
} else { } else {
getWebAPIVoices(); getWebAPIVoices();
} }
if ($user.role === 'admin') {
const res = await getAudioConfig(localStorage.token);
if (res) {
OpenAIUrl = res.OPENAI_API_BASE_URL;
OpenAIKey = res.OPENAI_API_KEY;
model = res.OPENAI_API_MODEL;
OpenAISpeaker = res.OPENAI_API_VOICE;
if (TTSEngine === 'openai') {
speaker = OpenAISpeaker;
}
}
}
}); });
</script> </script>
<form <form
class="flex flex-col h-full justify-between space-y-3 text-sm" class="flex flex-col h-full justify-between space-y-3 text-sm"
on:submit|preventDefault={async () => { on:submit|preventDefault={async () => {
if ($user.role === 'admin') {
await updateConfigHandler();
}
saveSettings({ saveSettings({
audio: { audio: {
STTEngine: STTEngine !== '' ? STTEngine : undefined, stt: {
TTSEngine: TTSEngine !== '' ? TTSEngine : undefined, engine: STTEngine !== '' ? STTEngine : undefined
speaker: },
(TTSEngine === 'openai' ? OpenAISpeaker : speaker) !== '' tts: {
? TTSEngine === 'openai' voice: voice !== '' ? voice : undefined,
? OpenAISpeaker nonLocalVoices: $config.audio.tts.engine === '' ? nonLocalVoices : undefined
: speaker }
: undefined,
model: model !== '' ? model : undefined,
nonLocalVoices: nonLocalVoices
} }
}); });
dispatch('save'); dispatch('save');
...@@ -162,53 +90,25 @@ ...@@ -162,53 +90,25 @@
<div> <div>
<div class=" mb-1 text-sm font-medium">{$i18n.t('STT Settings')}</div> <div class=" mb-1 text-sm font-medium">{$i18n.t('STT Settings')}</div>
<div class=" py-0.5 flex w-full justify-between"> {#if $config.audio.stt.engine !== 'web'}
<div class=" self-center text-xs font-medium">{$i18n.t('Speech-to-Text Engine')}</div> <div class=" py-0.5 flex w-full justify-between">
<div class="flex items-center relative"> <div class=" self-center text-xs font-medium">{$i18n.t('Speech-to-Text Engine')}</div>
<select <div class="flex items-center relative">
class="dark:bg-gray-900 w-fit pr-8 rounded px-2 p-1 text-xs bg-transparent outline-none text-right" <select
bind:value={STTEngine} class="dark:bg-gray-900 w-fit pr-8 rounded px-2 p-1 text-xs bg-transparent outline-none text-right"
placeholder="Select a mode" bind:value={STTEngine}
on:change={(e) => { placeholder="Select an engine"
if (e.target.value !== '') { >
navigator.mediaDevices.getUserMedia({ audio: true }).catch(function (err) { <option value="">{$i18n.t('Default')}</option>
toast.error( <option value="web">{$i18n.t('Web API')}</option>
$i18n.t(`Permission denied when accessing microphone: {{error}}`, { </select>
error: err </div>
})
);
STTEngine = '';
});
}
}}
>
<option value="">{$i18n.t('Default (Web API)')}</option>
<option value="whisper-local">{$i18n.t('Whisper (Local)')}</option>
</select>
</div> </div>
</div> {/if}
<div class=" py-0.5 flex w-full justify-between">
<div class=" self-center text-xs font-medium">{$i18n.t('Conversation Mode')}</div>
<button
class="p-1 px-3 text-xs flex rounded transition"
on:click={() => {
toggleConversationMode();
}}
type="button"
>
{#if conversationMode === true}
<span class="ml-2 self-center">{$i18n.t('On')}</span>
{:else}
<span class="ml-2 self-center">{$i18n.t('Off')}</span>
{/if}
</button>
</div>
<div class=" py-0.5 flex w-full justify-between"> <div class=" py-0.5 flex w-full justify-between">
<div class=" self-center text-xs font-medium"> <div class=" self-center text-xs font-medium">
{$i18n.t('Auto-send input after 3 sec.')} {$i18n.t('Instant Auto-Send After Voice Transcription')}
</div> </div>
<button <button
...@@ -230,50 +130,6 @@ ...@@ -230,50 +130,6 @@
<div> <div>
<div class=" mb-1 text-sm font-medium">{$i18n.t('TTS Settings')}</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 a mode"
on:change={(e) => {
if (e.target.value === 'openai') {
getOpenAIVoices();
OpenAISpeaker = 'alloy';
model = 'tts-1';
} else {
getWebAPIVoices();
speaker = '';
}
}}
>
<option value="">{$i18n.t('Default (Web API)')}</option>
<option value="openai">{$i18n.t('Open AI')}</option>
</select>
</div>
</div>
{#if $user.role === 'admin'}
{#if TTSEngine === 'openai'}
<div class="mt-1 flex gap-2 mb-1">
<input
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
placeholder={$i18n.t('API Base URL')}
bind:value={OpenAIUrl}
required
/>
<input
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
placeholder={$i18n.t('API Key')}
bind:value={OpenAIKey}
required
/>
</div>
{/if}
{/if}
<div class=" py-0.5 flex w-full justify-between"> <div class=" py-0.5 flex w-full justify-between">
<div class=" self-center text-xs font-medium">{$i18n.t('Auto-playback response')}</div> <div class=" self-center text-xs font-medium">{$i18n.t('Auto-playback response')}</div>
...@@ -293,23 +149,23 @@ ...@@ -293,23 +149,23 @@
</div> </div>
</div> </div>
<hr class=" dark:border-gray-700" /> <hr class=" dark:border-gray-850" />
{#if TTSEngine === ''} {#if $config.audio.tts.engine === ''}
<div> <div>
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set Voice')}</div> <div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set Voice')}</div>
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1"> <div class="flex-1">
<select <select
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none" class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
bind:value={speaker} bind:value={voice}
> >
<option value="" selected={speaker !== ''}>{$i18n.t('Default')}</option> <option value="" selected={voice !== ''}>{$i18n.t('Default')}</option>
{#each voices.filter((v) => nonLocalVoices || v.localService === true) as voice} {#each voices.filter((v) => nonLocalVoices || v.localService === true) as _voice}
<option <option
value={voice.name} value={_voice.name}
class="bg-gray-100 dark:bg-gray-700" class="bg-gray-100 dark:bg-gray-700"
selected={speaker === voice.name}>{voice.name}</option selected={voice === _voice.name}>{_voice.name}</option
> >
{/each} {/each}
</select> </select>
...@@ -325,7 +181,7 @@ ...@@ -325,7 +181,7 @@
</div> </div>
</div> </div>
</div> </div>
{:else if TTSEngine === 'openai'} {:else if $config.audio.tts.engine === 'openai'}
<div> <div>
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set Voice')}</div> <div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set Voice')}</div>
<div class="flex w-full"> <div class="flex w-full">
...@@ -333,7 +189,7 @@ ...@@ -333,7 +189,7 @@
<input <input
list="voice-list" list="voice-list"
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none" class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
bind:value={OpenAISpeaker} bind:value={voice}
placeholder="Select a voice" placeholder="Select a voice"
/> />
...@@ -345,25 +201,6 @@ ...@@ -345,25 +201,6 @@
</div> </div>
</div> </div>
</div> </div>
<div>
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set Model')}</div>
<div class="flex w-full">
<div class="flex-1">
<input
list="model-list"
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
bind:value={model}
placeholder="Select a model"
/>
<datalist id="model-list">
{#each models as model}
<option value={model.name} />
{/each}
</datalist>
</div>
</div>
</div>
{/if} {/if}
</div> </div>
......
...@@ -161,7 +161,7 @@ ...@@ -161,7 +161,7 @@
</div> </div>
</div> </div>
<hr class=" dark:border-gray-700" /> <hr class=" dark:border-gray-850" />
<div class="flex flex-col"> <div class="flex flex-col">
<input <input
...@@ -218,7 +218,7 @@ ...@@ -218,7 +218,7 @@
</button> </button>
</div> </div>
<hr class=" dark:border-gray-700" /> <hr class=" dark:border-gray-850" />
<div class="flex flex-col"> <div class="flex flex-col">
{#if showArchiveConfirm} {#if showArchiveConfirm}
......
...@@ -203,7 +203,7 @@ ...@@ -203,7 +203,7 @@
</div> </div>
</div> </div>
<hr class=" dark:border-gray-700 my-3" /> <hr class=" dark:border-gray-850 my-3" />
<div> <div>
<div class=" my-2.5 text-sm font-medium">{$i18n.t('System Prompt')}</div> <div class=" my-2.5 text-sm font-medium">{$i18n.t('System Prompt')}</div>
...@@ -228,7 +228,7 @@ ...@@ -228,7 +228,7 @@
{#if showAdvanced} {#if showAdvanced}
<AdvancedParams bind:params /> <AdvancedParams bind:params />
<hr class=" dark:border-gray-700" /> <hr class=" dark:border-gray-850" />
<div class=" py-1 w-full justify-between"> <div class=" py-1 w-full justify-between">
<div class="flex w-full justify-between"> <div class="flex w-full justify-between">
......
...@@ -14,15 +14,11 @@ ...@@ -14,15 +14,11 @@
// Addons // Addons
let titleAutoGenerate = true; let titleAutoGenerate = true;
let responseAutoCopy = false; let responseAutoCopy = false;
let titleAutoGenerateModel = '';
let titleAutoGenerateModelExternal = '';
let widescreenMode = false; let widescreenMode = false;
let titleGenerationPrompt = '';
let splitLargeChunks = false; let splitLargeChunks = false;
// Interface // Interface
let defaultModelId = ''; let defaultModelId = '';
let promptSuggestions = [];
let showUsername = false; let showUsername = false;
let chatBubble = true; let chatBubble = true;
let chatDirection: 'LTR' | 'RTL' = 'LTR'; let chatDirection: 'LTR' | 'RTL' = 'LTR';
...@@ -85,34 +81,13 @@ ...@@ -85,34 +81,13 @@
}; };
const updateInterfaceHandler = async () => { const updateInterfaceHandler = async () => {
if ($user.role === 'admin') {
promptSuggestions = await setDefaultPromptSuggestions(localStorage.token, promptSuggestions);
await config.set(await getBackendConfig());
}
saveSettings({ saveSettings({
title: {
...$settings.title,
model: titleAutoGenerateModel !== '' ? titleAutoGenerateModel : undefined,
modelExternal:
titleAutoGenerateModelExternal !== '' ? titleAutoGenerateModelExternal : undefined,
prompt: titleGenerationPrompt ? titleGenerationPrompt : undefined
},
models: [defaultModelId] models: [defaultModelId]
}); });
}; };
onMount(async () => { onMount(async () => {
if ($user.role === 'admin') {
promptSuggestions = $config?.default_prompt_suggestions;
}
titleAutoGenerate = $settings?.title?.auto ?? true; titleAutoGenerate = $settings?.title?.auto ?? true;
titleAutoGenerateModel = $settings?.title?.model ?? '';
titleAutoGenerateModelExternal = $settings?.title?.modelExternal ?? '';
titleGenerationPrompt =
$settings?.title?.prompt ??
`Create a concise, 3-5 word phrase as a header for the following query, strictly adhering to the 3-5 word limit and avoiding the use of the word 'title': {{prompt}}`;
responseAutoCopy = $settings.responseAutoCopy ?? false; responseAutoCopy = $settings.responseAutoCopy ?? false;
showUsername = $settings.showUsername ?? false; showUsername = $settings.showUsername ?? false;
chatBubble = $settings.chatBubble ?? true; chatBubble = $settings.chatBubble ?? true;
...@@ -304,162 +279,6 @@ ...@@ -304,162 +279,6 @@
</select> </select>
</div> </div>
</div> </div>
<hr class=" dark:border-gray-850" />
<div>
<div class=" mb-2.5 text-sm font-medium flex">
<div class=" mr-1">{$i18n.t('Set Task Model')}</div>
<Tooltip
content={$i18n.t(
'A task model is used when performing tasks such as generating titles for chats and web search queries'
)}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-5 h-5"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="m11.25 11.25.041-.02a.75.75 0 0 1 1.063.852l-.708 2.836a.75.75 0 0 0 1.063.853l.041-.021M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9-3.75h.008v.008H12V8.25Z"
/>
</svg>
</Tooltip>
</div>
<div class="flex w-full gap-2 pr-2">
<div class="flex-1">
<div class=" text-xs mb-1">Local Models</div>
<select
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
bind:value={titleAutoGenerateModel}
placeholder={$i18n.t('Select a model')}
>
<option value="" selected>{$i18n.t('Current Model')}</option>
{#each $models.filter((m) => m.owned_by === 'ollama') as model}
<option value={model.id} class="bg-gray-100 dark:bg-gray-700">
{model.name}
</option>
{/each}
</select>
</div>
<div class="flex-1">
<div class=" text-xs mb-1">External Models</div>
<select
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
bind:value={titleAutoGenerateModelExternal}
placeholder={$i18n.t('Select a model')}
>
<option value="" selected>{$i18n.t('Current Model')}</option>
{#each $models as model}
<option value={model.id} class="bg-gray-100 dark:bg-gray-700">
{model.name}
</option>
{/each}
</select>
</div>
</div>
<div class="mt-3 mr-2">
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Title Generation Prompt')}</div>
<textarea
bind:value={titleGenerationPrompt}
class="w-full rounded-lg p-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none resize-none"
rows="3"
/>
</div>
</div>
{#if $user.role === 'admin'}
<hr class=" dark:border-gray-700" />
<div class=" space-y-3 pr-1.5">
<div class="flex w-full justify-between mb-2">
<div class=" self-center text-sm font-semibold">
{$i18n.t('Default Prompt Suggestions')}
</div>
<button
class="p-1 px-3 text-xs flex rounded transition"
type="button"
on:click={() => {
if (promptSuggestions.length === 0 || promptSuggestions.at(-1).content !== '') {
promptSuggestions = [...promptSuggestions, { content: '', title: ['', ''] }];
}
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-4 h-4"
>
<path
d="M10.75 4.75a.75.75 0 00-1.5 0v4.5h-4.5a.75.75 0 000 1.5h4.5v4.5a.75.75 0 001.5 0v-4.5h4.5a.75.75 0 000-1.5h-4.5v-4.5z"
/>
</svg>
</button>
</div>
<div class="flex flex-col space-y-1">
{#each promptSuggestions as prompt, promptIdx}
<div class=" flex border dark:border-gray-600 rounded-lg">
<div class="flex flex-col flex-1">
<div class="flex border-b dark:border-gray-600 w-full">
<input
class="px-3 py-1.5 text-xs w-full bg-transparent outline-none border-r dark:border-gray-600"
placeholder={$i18n.t('Title (e.g. Tell me a fun fact)')}
bind:value={prompt.title[0]}
/>
<input
class="px-3 py-1.5 text-xs w-full bg-transparent outline-none border-r dark:border-gray-600"
placeholder={$i18n.t('Subtitle (e.g. about the Roman Empire)')}
bind:value={prompt.title[1]}
/>
</div>
<input
class="px-3 py-1.5 text-xs w-full bg-transparent outline-none border-r dark:border-gray-600"
placeholder={$i18n.t('Prompt (e.g. Tell me a fun fact about the Roman Empire)')}
bind:value={prompt.content}
/>
</div>
<button
class="px-2"
type="button"
on:click={() => {
promptSuggestions.splice(promptIdx, 1);
promptSuggestions = promptSuggestions;
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-4 h-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"
/>
</svg>
</button>
</div>
{/each}
</div>
{#if promptSuggestions.length > 0}
<div class="text-xs text-left w-full mt-2">
{$i18n.t('Adjusting these settings will apply changes universally to all users.')}
</div>
{/if}
</div>
{/if}
</div> </div>
<div class="flex justify-end text-sm font-medium"> <div class="flex justify-end text-sm font-medium">
......
...@@ -541,7 +541,7 @@ ...@@ -541,7 +541,7 @@
]); ]);
} else { } else {
ollamaEnabled = false; ollamaEnabled = false;
toast.error('Ollama API is disabled'); toast.error($i18n.t('Ollama API is disabled'));
} }
}); });
</script> </script>
...@@ -1063,7 +1063,7 @@ ...@@ -1063,7 +1063,7 @@
</div> </div>
{/if} {/if}
{:else if ollamaEnabled === false} {:else if ollamaEnabled === false}
<div>Ollama API is disabled</div> <div>{$i18n.t('Ollama API is disabled')}</div>
{:else} {:else}
<div class="flex h-full justify-center"> <div class="flex h-full justify-center">
<div class="my-auto"> <div class="my-auto">
......
...@@ -35,7 +35,9 @@ ...@@ -35,7 +35,9 @@
<div> <div>
<div class="flex items-center justify-between mb-1"> <div class="flex items-center justify-between mb-1">
<Tooltip <Tooltip
content="This is an experimental feature, it may not function as expected and is subject to change at any time." content={$i18n.t(
'This is an experimental feature, it may not function as expected and is subject to change at any time.'
)}
> >
<div class="text-sm font-medium"> <div class="text-sm font-medium">
{$i18n.t('Memory')} {$i18n.t('Memory')}
...@@ -57,8 +59,9 @@ ...@@ -57,8 +59,9 @@
<div class="text-xs text-gray-600 dark:text-gray-400"> <div class="text-xs text-gray-600 dark:text-gray-400">
<div> <div>
You can personalize your interactions with LLMs by adding memories through the 'Manage' {$i18n.t(
button below, making them more helpful and tailored to you. "You can personalize your interactions with LLMs by adding memories through the 'Manage' button below, making them more helpful and tailored to you."
)}
</div> </div>
<!-- <div class="mt-3"> <!-- <div class="mt-3">
...@@ -79,7 +82,7 @@ ...@@ -79,7 +82,7 @@
showManageModal = true; showManageModal = true;
}} }}
> >
Manage {$i18n.t('Manage')}
</button> </button>
</div> </div>
</div> </div>
......
...@@ -75,7 +75,7 @@ ...@@ -75,7 +75,7 @@
/> />
<div class="text-xs text-gray-500"> <div class="text-xs text-gray-500">
ⓘ Refer to yourself as "User" (e.g., "User is learning Spanish") {$i18n.t('Refer to yourself as "User" (e.g., "User is learning Spanish")')}
</div> </div>
</div> </div>
......
...@@ -136,7 +136,7 @@ ...@@ -136,7 +136,7 @@
class=" px-3.5 py-1.5 font-medium hover:bg-black/5 dark:hover:bg-white/5 outline outline-1 outline-gray-300 dark:outline-gray-800 rounded-3xl" class=" px-3.5 py-1.5 font-medium hover:bg-black/5 dark:hover:bg-white/5 outline outline-1 outline-gray-300 dark:outline-gray-800 rounded-3xl"
on:click={() => { on:click={() => {
showAddMemoryModal = true; showAddMemoryModal = true;
}}>Add memory</button }}>{$i18n.t('Add Memory')}</button
> >
<button <button
class=" px-3.5 py-1.5 font-medium text-red-500 hover:bg-black/5 dark:hover:bg-white/5 outline outline-1 outline-red-300 dark:outline-red-800 rounded-3xl" class=" px-3.5 py-1.5 font-medium text-red-500 hover:bg-black/5 dark:hover:bg-white/5 outline outline-1 outline-red-300 dark:outline-red-800 rounded-3xl"
...@@ -150,7 +150,7 @@ ...@@ -150,7 +150,7 @@
toast.success('Memory cleared successfully'); toast.success('Memory cleared successfully');
memories = []; memories = [];
} }
}}>Clear memory</button }}>{$i18n.t('Clear memory')}</button
> >
</div> </div>
</div> </div>
......
...@@ -8,16 +8,14 @@ ...@@ -8,16 +8,14 @@
import Modal from '../common/Modal.svelte'; import Modal from '../common/Modal.svelte';
import Account from './Settings/Account.svelte'; import Account from './Settings/Account.svelte';
import About from './Settings/About.svelte'; import About from './Settings/About.svelte';
import Models from './Settings/Models.svelte';
import General from './Settings/General.svelte'; import General from './Settings/General.svelte';
import Interface from './Settings/Interface.svelte'; import Interface from './Settings/Interface.svelte';
import Audio from './Settings/Audio.svelte'; import Audio from './Settings/Audio.svelte';
import Chats from './Settings/Chats.svelte'; import Chats from './Settings/Chats.svelte';
import Connections from './Settings/Connections.svelte';
import Images from './Settings/Images.svelte';
import User from '../icons/User.svelte'; import User from '../icons/User.svelte';
import Personalization from './Settings/Personalization.svelte'; import Personalization from './Settings/Personalization.svelte';
import { updateUserSettings } from '$lib/apis/users'; import { updateUserSettings } from '$lib/apis/users';
import { goto } from '$app/navigation';
const i18n = getContext('i18n'); const i18n = getContext('i18n');
...@@ -90,55 +88,32 @@ ...@@ -90,55 +88,32 @@
<div class=" self-center">{$i18n.t('General')}</div> <div class=" self-center">{$i18n.t('General')}</div>
</button> </button>
{#if $user?.role === 'admin'} {#if $user.role === 'admin'}
<button
class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
'connections'
? 'bg-gray-200 dark:bg-gray-700'
: ' hover:bg-gray-300 dark:hover:bg-gray-800'}"
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>
<button <button
class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab === class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
'models' 'admin'
? 'bg-gray-200 dark:bg-gray-700' ? 'bg-gray-200 dark:bg-gray-700'
: ' hover:bg-gray-300 dark:hover:bg-gray-800'}" : ' hover:bg-gray-300 dark:hover:bg-gray-800'}"
on:click={() => { on:click={async () => {
selectedTab = 'models'; await goto('/admin/settings');
show = false;
}} }}
> >
<div class=" self-center mr-2"> <div class=" self-center mr-2">
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20" viewBox="0 0 24 24"
fill="currentColor" fill="currentColor"
class="w-4 h-4" class="size-4"
> >
<path <path
fill-rule="evenodd" fill-rule="evenodd"
d="M10 1c3.866 0 7 1.79 7 4s-3.134 4-7 4-7-1.79-7-4 3.134-4 7-4zm5.694 8.13c.464-.264.91-.583 1.306-.952V10c0 2.21-3.134 4-7 4s-7-1.79-7-4V8.178c.396.37.842.688 1.306.953C5.838 10.006 7.854 10.5 10 10.5s4.162-.494 5.694-1.37zM3 13.179V15c0 2.21 3.134 4 7 4s7-1.79 7-4v-1.822c-.396.37-.842.688-1.306.953-1.532.875-3.548 1.369-5.694 1.369s-4.162-.494-5.694-1.37A7.009 7.009 0 013 13.179z" d="M4.5 3.75a3 3 0 0 0-3 3v10.5a3 3 0 0 0 3 3h15a3 3 0 0 0 3-3V6.75a3 3 0 0 0-3-3h-15Zm4.125 3a2.25 2.25 0 1 0 0 4.5 2.25 2.25 0 0 0 0-4.5Zm-3.873 8.703a4.126 4.126 0 0 1 7.746 0 .75.75 0 0 1-.351.92 7.47 7.47 0 0 1-3.522.877 7.47 7.47 0 0 1-3.522-.877.75.75 0 0 1-.351-.92ZM15 8.25a.75.75 0 0 0 0 1.5h3.75a.75.75 0 0 0 0-1.5H15ZM14.25 12a.75.75 0 0 1 .75-.75h3.75a.75.75 0 0 1 0 1.5H15a.75.75 0 0 1-.75-.75Zm.75 2.25a.75.75 0 0 0 0 1.5h3.75a.75.75 0 0 0 0-1.5H15Z"
clip-rule="evenodd" clip-rule="evenodd"
/> />
</svg> </svg>
</div> </div>
<div class=" self-center">{$i18n.t('Models')}</div> <div class=" self-center">{$i18n.t('Admin Settings')}</div>
</button> </button>
{/if} {/if}
...@@ -210,34 +185,6 @@ ...@@ -210,34 +185,6 @@
<div class=" self-center">{$i18n.t('Audio')}</div> <div class=" self-center">{$i18n.t('Audio')}</div>
</button> </button>
{#if $user.role === 'admin'}
<button
class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
'images'
? 'bg-gray-200 dark:bg-gray-700'
: ' hover:bg-gray-300 dark:hover:bg-gray-800'}"
on:click={() => {
selectedTab = 'images';
}}
>
<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
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-2V4Zm10.5 5.707a.5.5 0 0 0-.146-.353l-1-1a.5.5 0 0 0-.708 0L9.354 9.646a.5.5 0 0 1-.708 0L6.354 7.354a.5.5 0 0 0-.708 0l-2 2a.5.5 0 0 0-.146.353V12a.5.5 0 0 0 .5.5h8a.5.5 0 0 0 .5-.5V9.707ZM12 5a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z"
clip-rule="evenodd"
/>
</svg>
</div>
<div class=" self-center">{$i18n.t('Images')}</div>
</button>
{/if}
<button <button
class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab === class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
'chats' 'chats'
...@@ -325,15 +272,6 @@ ...@@ -325,15 +272,6 @@
toast.success($i18n.t('Settings saved successfully!')); toast.success($i18n.t('Settings saved successfully!'));
}} }}
/> />
{:else if selectedTab === 'models'}
<Models {getModels} />
{:else if selectedTab === 'connections'}
<Connections
{getModels}
on:save={() => {
toast.success($i18n.t('Settings saved successfully!'));
}}
/>
{:else if selectedTab === 'interface'} {:else if selectedTab === 'interface'}
<Interface <Interface
{saveSettings} {saveSettings}
...@@ -355,13 +293,6 @@ ...@@ -355,13 +293,6 @@
toast.success($i18n.t('Settings saved successfully!')); toast.success($i18n.t('Settings saved successfully!'));
}} }}
/> />
{:else if selectedTab === 'images'}
<Images
{saveSettings}
on:save={() => {
toast.success($i18n.t('Settings saved successfully!'));
}}
/>
{:else if selectedTab === 'chats'} {:else if selectedTab === 'chats'}
<Chats {saveSettings} /> <Chats {saveSettings} />
{:else if selectedTab === 'account'} {:else if selectedTab === 'account'}
......
...@@ -48,7 +48,7 @@ ...@@ -48,7 +48,7 @@
<ChevronDown className="absolute end-2 top-1/2 -translate-y-[45%] size-3.5" strokeWidth="2.5" /> <ChevronDown className="absolute end-2 top-1/2 -translate-y-[45%] size-3.5" strokeWidth="2.5" />
</Select.Trigger> </Select.Trigger>
<Select.Content <Select.Content
class="w-full rounded-lg bg-white dark:bg-gray-900 dark:text-white shadow-lg border border-gray-300/30 dark:border-gray-700/50 outline-none" class="w-full rounded-lg bg-white dark:bg-gray-900 dark:text-white shadow-lg border border-gray-300/30 dark:border-gray-850/50 outline-none"
transition={flyAndScale} transition={flyAndScale}
sideOffset={4} sideOffset={4}
> >
......
...@@ -95,7 +95,7 @@ ...@@ -95,7 +95,7 @@
)} )}
</div> </div>
<hr class=" dark:border-gray-700 my-3" /> <hr class=" dark:border-gray-850 my-3" />
{/if} {/if}
<div> <div>
......
...@@ -68,10 +68,10 @@ ...@@ -68,10 +68,10 @@
<select <select
class="dark:bg-gray-900 w-fit pr-8 rounded px-2 p-1 text-xs bg-transparent outline-none text-right" class="dark:bg-gray-900 w-fit pr-8 rounded px-2 p-1 text-xs bg-transparent outline-none text-right"
bind:value={webConfig.search.engine} bind:value={webConfig.search.engine}
placeholder="Select a engine" placeholder={$i18n.t('Select a engine')}
required required
> >
<option disabled selected value="">Select a engine</option> <option disabled selected value="">{$i18n.t('Select a engine')}</option>
{#each webSearchEngines as engine} {#each webSearchEngines as engine}
<option value={engine}>{engine}</option> <option value={engine}>{engine}</option>
{/each} {/each}
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment