Unverified Commit 75d71305 authored by perf3ct's avatar perf3ct
Browse files

Merge remote-tracking branch 'upstream/main' into feature-external-db-reconnect

parents ad32a2ef 162643a4
...@@ -101,7 +101,7 @@ ...@@ -101,7 +101,7 @@
</script> </script>
{#if filteredItems.length > 0 || prompt.split(' ')?.at(0)?.substring(1).startsWith('http')} {#if filteredItems.length > 0 || prompt.split(' ')?.at(0)?.substring(1).startsWith('http')}
<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>
......
...@@ -4,24 +4,33 @@ ...@@ -4,24 +4,33 @@
import { getContext } from 'svelte'; import { getContext } from 'svelte';
import Dropdown from '$lib/components/common/Dropdown.svelte'; import Dropdown from '$lib/components/common/Dropdown.svelte';
import GarbageBin from '$lib/components/icons/GarbageBin.svelte';
import Pencil from '$lib/components/icons/Pencil.svelte';
import Tooltip from '$lib/components/common/Tooltip.svelte'; import Tooltip from '$lib/components/common/Tooltip.svelte';
import Tags from '$lib/components/chat/Tags.svelte';
import Share from '$lib/components/icons/Share.svelte';
import ArchiveBox from '$lib/components/icons/ArchiveBox.svelte';
import DocumentArrowUpSolid from '$lib/components/icons/DocumentArrowUpSolid.svelte'; import DocumentArrowUpSolid from '$lib/components/icons/DocumentArrowUpSolid.svelte';
import Switch from '$lib/components/common/Switch.svelte'; import Switch from '$lib/components/common/Switch.svelte';
import GlobeAltSolid from '$lib/components/icons/GlobeAltSolid.svelte'; import GlobeAltSolid from '$lib/components/icons/GlobeAltSolid.svelte';
import { config } from '$lib/stores'; import { config } from '$lib/stores';
import WrenchSolid from '$lib/components/icons/WrenchSolid.svelte';
const i18n = getContext('i18n'); const i18n = getContext('i18n');
export let uploadFilesHandler: Function; export let uploadFilesHandler: Function;
export let selectedToolIds: string[] = [];
export let webSearchEnabled: boolean; export let webSearchEnabled: boolean;
export let tools = {};
export let onClose: Function; export let onClose: Function;
$: tools = Object.fromEntries(
Object.keys(tools).map((toolId) => [
toolId,
{
...tools[toolId],
enabled: selectedToolIds.includes(toolId)
}
])
);
let show = false; let show = false;
</script> </script>
...@@ -39,20 +48,48 @@ ...@@ -39,20 +48,48 @@
<div slot="content"> <div slot="content">
<DropdownMenu.Content <DropdownMenu.Content
class="w-full max-w-[190px] rounded-xl px-1 py-1 border-gray-300/30 dark:border-gray-700/50 z-50 bg-white dark:bg-gray-850 dark:text-white shadow" class="w-full max-w-[200px] rounded-xl px-1 py-1 border-gray-300/30 dark:border-gray-700/50 z-50 bg-white dark:bg-gray-850 dark:text-white shadow"
sideOffset={15} sideOffset={15}
alignOffset={-8} alignOffset={-8}
side="top" side="top"
align="start" align="start"
transition={flyAndScale} transition={flyAndScale}
> >
{#if Object.keys(tools).length > 0}
<div class=" max-h-28 overflow-y-auto scrollbar-hidden">
{#each Object.keys(tools) as toolId}
<div
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer rounded-xl"
>
<div class="flex-1 flex items-center gap-2">
<WrenchSolid />
<Tooltip content={tools[toolId]?.description ?? ''} className="flex-1">
<div class=" line-clamp-1">{tools[toolId].name}</div>
</Tooltip>
</div>
<Switch
bind:state={tools[toolId].enabled}
on:change={(e) => {
selectedToolIds = e.detail
? [...selectedToolIds, toolId]
: selectedToolIds.filter((id) => id !== toolId);
}}
/>
</div>
{/each}
</div>
<hr class="border-gray-100 dark:border-gray-800 my-1" />
{/if}
{#if $config?.features?.enable_web_search} {#if $config?.features?.enable_web_search}
<div <div
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer rounded-xl" class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer rounded-xl"
> >
<div class="flex-1 flex items-center gap-2"> <div class="flex-1 flex items-center gap-2">
<GlobeAltSolid /> <GlobeAltSolid />
<div class="flex items-center">{$i18n.t('Web Search')}</div> <div class=" line-clamp-1">{$i18n.t('Web Search')}</div>
</div> </div>
<Switch bind:state={webSearchEnabled} /> <Switch bind:state={webSearchEnabled} />
...@@ -68,7 +105,7 @@ ...@@ -68,7 +105,7 @@
}} }}
> >
<DocumentArrowUpSolid /> <DocumentArrowUpSolid />
<div class="flex items-center">{$i18n.t('Upload Files')}</div> <div class=" line-clamp-1">{$i18n.t('Upload Files')}</div>
</DropdownMenu.Item> </DropdownMenu.Item>
</DropdownMenu.Content> </DropdownMenu.Content>
</div> </div>
......
...@@ -133,7 +133,7 @@ ...@@ -133,7 +133,7 @@
{#if prompt.charAt(0) === '@'} {#if prompt.charAt(0) === '@'}
{#if filteredModels.length > 0} {#if filteredModels.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>
......
...@@ -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>
<script lang="ts"> <script lang="ts">
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import { chats, config, settings, user as _user, mobile } from '$lib/stores'; import { chats, config, settings, user as _user, mobile } from '$lib/stores';
import { tick, getContext } from 'svelte'; import { tick, getContext, onMount } from 'svelte';
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';
import { getChatList, updateChatById } from '$lib/apis/chats'; import { getChatList, updateChatById } from '$lib/apis/chats';
...@@ -80,7 +79,7 @@ ...@@ -80,7 +79,7 @@
history.currentId = userMessageId; history.currentId = userMessageId;
await tick(); await tick();
await sendPrompt(userPrompt, userMessageId); await sendPrompt(userPrompt, userMessageId, undefined, false);
}; };
const updateChatMessages = async () => { const updateChatMessages = async () => {
...@@ -242,7 +241,7 @@ ...@@ -242,7 +241,7 @@
}; };
</script> </script>
<div class="h-full flex mb-16"> <div class="h-full flex">
{#if messages.length == 0} {#if messages.length == 0}
<Placeholder <Placeholder
modelIds={selectedModels} modelIds={selectedModels}
...@@ -285,9 +284,9 @@ ...@@ -285,9 +284,9 @@
<div class="w-full pt-2"> <div class="w-full pt-2">
{#key chatId} {#key chatId}
{#each messages as message, messageIdx} {#each messages as message, messageIdx}
<div class=" w-full {messageIdx === messages.length - 1 ? 'pb-28' : ''}"> <div class=" w-full {messageIdx === messages.length - 1 ? ' pb-12' : ''}">
<div <div
class="flex flex-col justify-between px-5 mb-3 {$settings?.fullScreenMode ?? null class="flex flex-col justify-between px-5 mb-3 {$settings?.widescreenMode ?? null
? 'max-w-full' ? 'max-w-full'
: 'max-w-5xl'} mx-auto rounded-lg group" : 'max-w-5xl'} mx-auto rounded-lg group"
> >
...@@ -340,6 +339,7 @@ ...@@ -340,6 +339,7 @@
<CompareMessages <CompareMessages
bind:history bind:history
{messages} {messages}
{readOnly}
{chatId} {chatId}
parentMessage={history.messages[message.parentId]} parentMessage={history.messages[message.parentId]}
{messageIdx} {messageIdx}
......
...@@ -13,6 +13,8 @@ ...@@ -13,6 +13,8 @@
export let parentMessage; export let parentMessage;
export let readOnly = false;
export let updateChatMessages: Function; export let updateChatMessages: Function;
export let confirmEditResponseMessage: Function; export let confirmEditResponseMessage: Function;
export let rateMessage: Function; export let rateMessage: Function;
...@@ -107,7 +109,7 @@ ...@@ -107,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;
...@@ -134,6 +136,7 @@ ...@@ -134,6 +136,7 @@
{confirmEditResponseMessage} {confirmEditResponseMessage}
showPreviousMessage={() => showPreviousMessage(model)} showPreviousMessage={() => showPreviousMessage(model)}
showNextMessage={() => showNextMessage(model)} showNextMessage={() => showNextMessage(model)}
{readOnly}
{rateMessage} {rateMessage}
{copyToClipboard} {copyToClipboard}
{continueGeneration} {continueGeneration}
......
...@@ -29,7 +29,7 @@ ...@@ -29,7 +29,7 @@
</script> </script>
{#key mounted} {#key mounted}
<div class="m-auto w-full max-w-6xl px-8 lg:px-24 pb-16"> <div class="m-auto w-full max-w-6xl px-8 lg:px-24 pb-10">
<div class="flex justify-start"> <div class="flex justify-start">
<div class="flex -space-x-4 mb-1" in:fade={{ duration: 200 }}> <div class="flex -space-x-4 mb-1" in:fade={{ duration: 200 }}>
{#each models as model, modelIdx} {#each models as model, modelIdx}
...@@ -64,7 +64,7 @@ ...@@ -64,7 +64,7 @@
</div> </div>
<div in:fade={{ duration: 200, delay: 200 }}> <div in:fade={{ duration: 200, delay: 200 }}>
{#if models[selectedModelIdx]?.info} {#if models[selectedModelIdx]?.info?.meta?.description ?? null}
<div class="mt-0.5 text-base font-normal text-gray-500 dark:text-gray-400 line-clamp-3"> <div class="mt-0.5 text-base font-normal text-gray-500 dark:text-gray-400 line-clamp-3">
{models[selectedModelIdx]?.info?.meta?.description} {models[selectedModelIdx]?.info?.meta?.description}
</div> </div>
...@@ -85,7 +85,7 @@ ...@@ -85,7 +85,7 @@
</div> </div>
{/if} {/if}
{:else} {:else}
<div class=" font-medium text-gray-400 dark:text-gray-500"> <div class=" font-medium text-gray-400 dark:text-gray-500 line-clamp-1">
{$i18n.t('How can I help you today?')} {$i18n.t('How can I help you today?')}
</div> </div>
{/if} {/if}
......
...@@ -46,8 +46,8 @@ ...@@ -46,8 +46,8 @@
} }
onMount(() => { onMount(() => {
selectedReason = message.annotation.reason; selectedReason = message?.annotation?.reason ?? '';
comment = message.annotation.comment; comment = message?.annotation?.comment ?? '';
loadReasons(); loadReasons();
}); });
......
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
import tippy from 'tippy.js'; import tippy from 'tippy.js';
import auto_render from 'katex/dist/contrib/auto-render.mjs'; import auto_render from 'katex/dist/contrib/auto-render.mjs';
import 'katex/dist/katex.min.css'; import 'katex/dist/katex.min.css';
import mermaid from 'mermaid';
import { fade } from 'svelte/transition'; import { fade } from 'svelte/transition';
import { createEventDispatcher } from 'svelte'; import { createEventDispatcher } from 'svelte';
...@@ -210,82 +211,98 @@ ...@@ -210,82 +211,98 @@
speaking = null; speaking = null;
speakingIdx = null; speakingIdx = null;
} else { } else {
speaking = true; if ((message?.content ?? '').trim() !== '') {
speaking = true;
if ($settings?.audio?.TTSEngine === 'openai') {
loadingSpeech = true; if ($config.audio.tts.engine === 'openai') {
loadingSpeech = true;
const sentences = extractSentences(message.content).reduce((mergedTexts, currentText) => {
const lastIndex = mergedTexts.length - 1; const sentences = extractSentences(message.content).reduce((mergedTexts, currentText) => {
if (lastIndex >= 0) { const lastIndex = mergedTexts.length - 1;
const previousText = mergedTexts[lastIndex]; if (lastIndex >= 0) {
const wordCount = previousText.split(/\s+/).length; const previousText = mergedTexts[lastIndex];
if (wordCount < 2) { const wordCount = previousText.split(/\s+/).length;
mergedTexts[lastIndex] = previousText + ' ' + currentText; if (wordCount < 2) {
mergedTexts[lastIndex] = previousText + ' ' + currentText;
} else {
mergedTexts.push(currentText);
}
} else { } else {
mergedTexts.push(currentText); mergedTexts.push(currentText);
} }
} else { return mergedTexts;
mergedTexts.push(currentText); }, []);
}
return mergedTexts; console.log(sentences);
}, []);
sentencesAudio = sentences.reduce((a, e, i, arr) => {
console.log(sentences); a[i] = null;
return a;
sentencesAudio = sentences.reduce((a, e, i, arr) => { }, {});
a[i] = null;
return a;
}, {});
let lastPlayedAudioPromise = Promise.resolve(); // Initialize a promise that resolves immediately
for (const [idx, sentence] of sentences.entries()) {
const res = await synthesizeOpenAISpeech(
localStorage.token,
$settings?.audio?.speaker,
sentence,
$settings?.audio?.model
).catch((error) => {
toast.error(error);
speaking = null;
loadingSpeech = false;
return null;
});
if (res) {
const blob = await res.blob();
const blobUrl = URL.createObjectURL(blob);
const audio = new Audio(blobUrl);
sentencesAudio[idx] = audio;
loadingSpeech = false;
lastPlayedAudioPromise = lastPlayedAudioPromise.then(() => playAudio(idx));
}
}
} else {
let voices = [];
const getVoicesLoop = setInterval(async () => {
voices = await speechSynthesis.getVoices();
if (voices.length > 0) {
clearInterval(getVoicesLoop);
const voice = let lastPlayedAudioPromise = Promise.resolve(); // Initialize a promise that resolves immediately
voices?.filter((v) => v.name === $settings?.audio?.speaker)?.at(0) ?? undefined;
const speak = new SpeechSynthesisUtterance(message.content); for (const [idx, sentence] of sentences.entries()) {
const res = await synthesizeOpenAISpeech(
localStorage.token,
$settings?.audio?.tts?.voice ?? $config?.audio?.tts?.voice,
sentence
).catch((error) => {
toast.error(error);
speak.onend = () => {
speaking = null; speaking = null;
if ($settings.conversationMode) { loadingSpeech = false;
document.getElementById('voice-input-button')?.click();
} return null;
}; });
speak.voice = voice;
speechSynthesis.speak(speak); if (res) {
const blob = await res.blob();
const blobUrl = URL.createObjectURL(blob);
const audio = new Audio(blobUrl);
sentencesAudio[idx] = audio;
loadingSpeech = false;
lastPlayedAudioPromise = lastPlayedAudioPromise.then(() => playAudio(idx));
}
} }
}, 100); } else {
let voices = [];
const getVoicesLoop = setInterval(async () => {
voices = await speechSynthesis.getVoices();
if (voices.length > 0) {
clearInterval(getVoicesLoop);
const voice =
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);
console.log(speak);
speak.onend = () => {
speaking = null;
if ($settings.conversationMode) {
document.getElementById('voice-input-button')?.click();
}
};
if (voice) {
speak.voice = voice;
}
speechSynthesis.speak(speak);
}
}, 100);
}
} else {
toast.error('No content to speak');
} }
} }
}; };
...@@ -340,9 +357,24 @@ ...@@ -340,9 +357,24 @@
generatingImage = false; generatingImage = false;
}; };
$: if (!edit) {
(async () => {
await tick();
renderStyling();
await mermaid.run({
querySelector: '.mermaid'
});
})();
}
onMount(async () => { onMount(async () => {
await tick(); await tick();
renderStyling(); renderStyling();
await mermaid.run({
querySelector: '.mermaid'
});
}); });
</script> </script>
...@@ -388,26 +420,29 @@ ...@@ -388,26 +420,29 @@
class="prose chat-{message.role} w-full max-w-full dark:prose-invert prose-headings:my-0 prose-headings:-mb-4 prose-p:m-0 prose-p:-mb-6 prose-pre:my-0 prose-table:my-0 prose-blockquote:my-0 prose-img:my-0 prose-ul:-my-4 prose-ol:-my-4 prose-li:-my-3 prose-ul:-mb-6 prose-ol:-mb-8 prose-ol:p-0 prose-li:-mb-4 whitespace-pre-line" class="prose chat-{message.role} w-full max-w-full dark:prose-invert prose-headings:my-0 prose-headings:-mb-4 prose-p:m-0 prose-p:-mb-6 prose-pre:my-0 prose-table:my-0 prose-blockquote:my-0 prose-img:my-0 prose-ul:-my-4 prose-ol:-my-4 prose-li:-my-3 prose-ul:-mb-6 prose-ol:-mb-8 prose-ol:p-0 prose-li:-mb-4 whitespace-pre-line"
> >
<div> <div>
{#if message?.status} {#if (message?.statusHistory ?? [...(message?.status ? [message?.status] : [])]).length > 0}
{@const status = (
message?.statusHistory ?? [...(message?.status ? [message?.status] : [])]
).at(-1)}
<div class="flex items-center gap-2 pt-1 pb-1"> <div class="flex items-center gap-2 pt-1 pb-1">
{#if message?.status?.done === false} {#if status.done === false}
<div class=""> <div class="">
<Spinner className="size-4" /> <Spinner className="size-4" />
</div> </div>
{/if} {/if}
{#if message?.status?.action === 'web_search' && message?.status?.urls} {#if status?.action === 'web_search' && status?.urls}
<WebSearchResults urls={message?.status?.urls}> <WebSearchResults {status}>
<div class="flex flex-col justify-center -space-y-0.5"> <div class="flex flex-col justify-center -space-y-0.5">
<div class="text-base line-clamp-1 text-wrap"> <div class="text-base line-clamp-1 text-wrap">
{message.status.description} {status?.description}
</div> </div>
</div> </div>
</WebSearchResults> </WebSearchResults>
{:else} {:else}
<div class="flex flex-col justify-center -space-y-0.5"> <div class="flex flex-col justify-center -space-y-0.5">
<div class=" text-gray-500 dark:text-gray-500 text-base line-clamp-1 text-wrap"> <div class=" text-gray-500 dark:text-gray-500 text-base line-clamp-1 text-wrap">
{message.status.description} {status?.description}
</div> </div>
</div> </div>
{/if} {/if}
...@@ -451,7 +486,34 @@ ...@@ -451,7 +486,34 @@
</div> </div>
{:else} {:else}
<div class="w-full"> <div class="w-full">
{#if message?.error === true} {#if message.content === '' && !message.error}
<Skeleton />
{:else if message.content && message.error !== true}
<!-- always show message contents even if there's an error -->
<!-- unless message.error === true which is legacy error handling, where the error message is stored in message.content -->
{#each tokens as token, tokenIdx}
{#if token.type === 'code'}
{#if token.lang === 'mermaid'}
<pre class="mermaid">{revertSanitizedResponseContent(token.text)}</pre>
{:else}
<CodeBlock
id={`${message.id}-${tokenIdx}`}
lang={token?.lang ?? ''}
code={revertSanitizedResponseContent(token?.text ?? '')}
/>
{/if}
{:else}
{@html marked.parse(token.raw, {
...defaults,
gfm: true,
breaks: true,
renderer
})}
{/if}
{/each}
{/if}
{#if message.error}
<div <div
class="flex mt-2 mb-4 space-x-2 border px-4 py-3 border-red-800 bg-red-800/30 font-medium rounded-lg" class="flex mt-2 mb-4 space-x-2 border px-4 py-3 border-red-800 bg-red-800/30 font-medium rounded-lg"
> >
...@@ -471,28 +533,9 @@ ...@@ -471,28 +533,9 @@
</svg> </svg>
<div class=" self-center"> <div class=" self-center">
{message.content} {message?.error?.content ?? message.content}
</div> </div>
</div> </div>
{:else if message.content === ''}
<Skeleton />
{:else}
{#each tokens as token, tokenIdx}
{#if token.type === 'code'}
<CodeBlock
id={`${message.id}-${tokenIdx}`}
lang={token?.lang ?? ''}
code={revertSanitizedResponseContent(token?.text ?? '')}
/>
{:else}
{@html marked.parse(token.raw, {
...defaults,
gfm: true,
breaks: true,
renderer
})}
{/if}
{/each}
{/if} {/if}
{#if message.citations} {#if message.citations}
...@@ -733,7 +776,7 @@ ...@@ -733,7 +776,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'
...@@ -833,8 +876,8 @@ ...@@ -833,8 +876,8 @@
<button <button
class="{isLastMessage class="{isLastMessage
? 'visible' ? 'visible'
: 'invisible group-hover:visible'} p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg {message : 'invisible group-hover:visible'} p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg {(message
?.annotation?.rating === 1 ?.annotation?.rating ?? null) === 1
? 'bg-gray-100 dark:bg-gray-800' ? 'bg-gray-100 dark:bg-gray-800'
: ''} dark:hover:text-white hover:text-black transition" : ''} dark:hover:text-white hover:text-black transition"
on:click={() => { on:click={() => {
...@@ -868,8 +911,8 @@ ...@@ -868,8 +911,8 @@
<button <button
class="{isLastMessage class="{isLastMessage
? 'visible' ? 'visible'
: 'invisible group-hover:visible'} p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg {message : 'invisible group-hover:visible'} p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg {(message
?.annotation?.rating === -1 ?.annotation?.rating ?? null) === -1
? 'bg-gray-100 dark:bg-gray-800' ? 'bg-gray-100 dark:bg-gray-800'
: ''} dark:hover:text-white hover:text-black transition" : ''} dark:hover:text-white hover:text-black transition"
on:click={() => { on:click={() => {
...@@ -939,6 +982,7 @@ ...@@ -939,6 +982,7 @@
? 'visible' ? 'visible'
: '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 regenerate-response-button" : '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 regenerate-response-button"
on:click={() => { on:click={() => {
showRateComment = false;
regenerateResponse(message); regenerateResponse(message);
}} }}
> >
......
<script lang="ts"> <script lang="ts">
import ChevronDown from '$lib/components/icons/ChevronDown.svelte'; import ChevronDown from '$lib/components/icons/ChevronDown.svelte';
import ChevronUp from '$lib/components/icons/ChevronUp.svelte'; import ChevronUp from '$lib/components/icons/ChevronUp.svelte';
import MagnifyingGlass from '$lib/components/icons/MagnifyingGlass.svelte';
import { Collapsible } from 'bits-ui'; import { Collapsible } from 'bits-ui';
import { slide } from 'svelte/transition'; import { slide } from 'svelte/transition';
export let urls = []; export let status = { urls: [], query: '' };
let state = false; let state = false;
</script> </script>
...@@ -27,15 +28,51 @@ ...@@ -27,15 +28,51 @@
class=" text-sm border border-gray-300/30 dark:border-gray-700/50 rounded-xl" class=" text-sm border border-gray-300/30 dark:border-gray-700/50 rounded-xl"
transition={slide} transition={slide}
> >
{#each urls as url, urlIdx} {#if status?.query}
<a
href="https://www.google.com/search?q={status.query}"
target="_blank"
class="flex w-full items-center p-3 px-4 border-b border-gray-300/30 dark:border-gray-700/50 group/item justify-between font-normal text-gray-800 dark:text-gray-300 no-underline"
>
<div class="flex gap-2 items-center">
<MagnifyingGlass />
<div class=" line-clamp-1">
{status.query}
</div>
</div>
<div
class=" ml-1 text-white dark:text-gray-900 group-hover/item:text-gray-600 dark:group-hover/item:text-white transition"
>
<!-- -->
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="size-4"
>
<path
fill-rule="evenodd"
d="M4.22 11.78a.75.75 0 0 1 0-1.06L9.44 5.5H5.75a.75.75 0 0 1 0-1.5h5.5a.75.75 0 0 1 .75.75v5.5a.75.75 0 0 1-1.5 0V6.56l-5.22 5.22a.75.75 0 0 1-1.06 0Z"
clip-rule="evenodd"
/>
</svg>
</div>
</a>
{/if}
{#each status.urls as url, urlIdx}
<a <a
href={url} href={url}
target="_blank" target="_blank"
class="flex w-full items-center p-3 px-4 {urlIdx === urls.length - 1 class="flex w-full items-center p-3 px-4 {urlIdx === status.urls.length - 1
? '' ? ''
: 'border-b border-gray-300/30 dark:border-gray-700/50'} group/item justify-between font-normal text-gray-800 dark:text-gray-300" : 'border-b border-gray-300/30 dark:border-gray-700/50'} group/item justify-between font-normal text-gray-800 dark:text-gray-300"
> >
{url} <div class=" line-clamp-1">
{url}
</div>
<div <div
class=" ml-1 text-white dark:text-gray-900 group-hover/item:text-gray-600 dark:group-hover/item:text-white transition" class=" ml-1 text-white dark:text-gray-900 group-hover/item:text-gray-600 dark:group-hover/item:text-white transition"
......
...@@ -196,7 +196,7 @@ ...@@ -196,7 +196,7 @@
<div class=" mt-2 mb-1 flex justify-end space-x-1.5 text-sm font-medium"> <div class=" mt-2 mb-1 flex justify-end space-x-1.5 text-sm font-medium">
<button <button
id="close-edit-message-button" id="close-edit-message-button"
class="px-4 py-2 bg-white hover:bg-gray-100 text-gray-800 transition rounded-3xl" 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={() => { on:click={() => {
cancelEditMessage(); cancelEditMessage();
}} }}
...@@ -206,7 +206,7 @@ ...@@ -206,7 +206,7 @@
<button <button
id="save-edit-message-button" id="save-edit-message-button"
class=" px-4 py-2 bg-gray-900 hover:bg-gray-850 text-gray-100 transition rounded-3xl" 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={() => { on:click={() => {
editMessageConfirmHandler(); editMessageConfirmHandler();
}} }}
......
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
import Check from '$lib/components/icons/Check.svelte'; import Check from '$lib/components/icons/Check.svelte';
import Search from '$lib/components/icons/Search.svelte'; import Search from '$lib/components/icons/Search.svelte';
import { cancelOllamaRequest, deleteModel, getOllamaVersion, pullModel } from '$lib/apis/ollama'; import { deleteModel, getOllamaVersion, pullModel } from '$lib/apis/ollama';
import { user, MODEL_DOWNLOAD_POOL, models, mobile } from '$lib/stores'; import { user, MODEL_DOWNLOAD_POOL, models, mobile } from '$lib/stores';
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';
...@@ -42,9 +42,16 @@ ...@@ -42,9 +42,16 @@
let searchValue = ''; let searchValue = '';
let ollamaVersion = null; let ollamaVersion = null;
$: filteredItems = searchValue $: filteredItems = items.filter(
? items.filter((item) => item.value.toLowerCase().includes(searchValue.toLowerCase())) (item) =>
: items; (searchValue
? item.value.toLowerCase().includes(searchValue.toLowerCase()) ||
item.label.toLowerCase().includes(searchValue.toLowerCase()) ||
(item.model?.info?.meta?.tags ?? []).some((tag) =>
tag.name.toLowerCase().includes(searchValue.toLowerCase())
)
: true) && !(item.model?.info?.meta?.hidden ?? false)
);
const pullModelHandler = async () => { const pullModelHandler = async () => {
const sanitizedModelTag = searchValue.trim().replace(/^ollama\s+(run|pull)\s+/, ''); const sanitizedModelTag = searchValue.trim().replace(/^ollama\s+(run|pull)\s+/, '');
...@@ -65,10 +72,12 @@ ...@@ -65,10 +72,12 @@
return; return;
} }
const res = await pullModel(localStorage.token, sanitizedModelTag, '0').catch((error) => { const [res, controller] = await pullModel(localStorage.token, sanitizedModelTag, '0').catch(
toast.error(error); (error) => {
return null; toast.error(error);
}); return null;
}
);
if (res) { if (res) {
const reader = res.body const reader = res.body
...@@ -76,6 +85,16 @@ ...@@ -76,6 +85,16 @@
.pipeThrough(splitStream('\n')) .pipeThrough(splitStream('\n'))
.getReader(); .getReader();
MODEL_DOWNLOAD_POOL.set({
...$MODEL_DOWNLOAD_POOL,
[sanitizedModelTag]: {
...$MODEL_DOWNLOAD_POOL[sanitizedModelTag],
abortController: controller,
reader,
done: false
}
});
while (true) { while (true) {
try { try {
const { value, done } = await reader.read(); const { value, done } = await reader.read();
...@@ -94,19 +113,6 @@ ...@@ -94,19 +113,6 @@
throw data.detail; throw data.detail;
} }
if (data.id) {
MODEL_DOWNLOAD_POOL.set({
...$MODEL_DOWNLOAD_POOL,
[sanitizedModelTag]: {
...$MODEL_DOWNLOAD_POOL[sanitizedModelTag],
requestId: data.id,
reader,
done: false
}
});
console.log(data);
}
if (data.status) { if (data.status) {
if (data.digest) { if (data.digest) {
let downloadProgress = 0; let downloadProgress = 0;
...@@ -146,6 +152,7 @@ ...@@ -146,6 +152,7 @@
toast.error(error); toast.error(error);
// opts.callback({ success: false, error, modelName: opts.modelName }); // opts.callback({ success: false, error, modelName: opts.modelName });
break;
} }
} }
...@@ -174,11 +181,12 @@ ...@@ -174,11 +181,12 @@
}); });
const cancelModelPullHandler = async (model: string) => { const cancelModelPullHandler = async (model: string) => {
const { reader, requestId } = $MODEL_DOWNLOAD_POOL[model]; const { reader, abortController } = $MODEL_DOWNLOAD_POOL[model];
if (abortController) {
abortController.abort();
}
if (reader) { if (reader) {
await reader.cancel(); await reader.cancel();
await cancelOllamaRequest(localStorage.token, requestId);
delete $MODEL_DOWNLOAD_POOL[model]; delete $MODEL_DOWNLOAD_POOL[model];
MODEL_DOWNLOAD_POOL.set({ MODEL_DOWNLOAD_POOL.set({
...$MODEL_DOWNLOAD_POOL ...$MODEL_DOWNLOAD_POOL
...@@ -212,7 +220,7 @@ ...@@ -212,7 +220,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}
...@@ -245,87 +253,113 @@ ...@@ -245,87 +253,113 @@
show = false; show = false;
}} }}
> >
<div class="flex items-center gap-2"> <div class="flex flex-col">
<div class="flex items-center"> {#if $mobile && (item?.model?.info?.meta?.tags ?? []).length > 0}
<div class="line-clamp-1"> <div class="flex gap-0.5 self-start h-full mb-0.5 -translate-x-1">
{item.label} {#each item.model?.info?.meta.tags as tag}
</div> <div
{#if item.model.owned_by === 'ollama' && (item.model.ollama?.details?.parameter_size ?? '') !== ''} class=" text-xs font-black px-1 rounded uppercase line-clamp-1 bg-gray-500/20 text-gray-700 dark:text-gray-200"
<div class="flex ml-1 items-center">
<Tooltip
content={`${
item.model.ollama?.details?.quantization_level
? item.model.ollama?.details?.quantization_level + ' '
: ''
}${
item.model.ollama?.size
? `(${(item.model.ollama?.size / 1024 ** 3).toFixed(1)}GB)`
: ''
}`}
className="self-end"
> >
<span class=" text-xs font-medium text-gray-600 dark:text-gray-400" {tag.name}
>{item.model.ollama?.details?.parameter_size ?? ''}</span </div>
{/each}
</div>
{/if}
<div class="flex items-center gap-2">
<div class="flex items-center min-w-fit">
<div class="line-clamp-1">
{item.label}
</div>
{#if item.model.owned_by === 'ollama' && (item.model.ollama?.details?.parameter_size ?? '') !== ''}
<div class="flex ml-1 items-center translate-y-[0.5px]">
<Tooltip
content={`${
item.model.ollama?.details?.quantization_level
? item.model.ollama?.details?.quantization_level + ' '
: ''
}${
item.model.ollama?.size
? `(${(item.model.ollama?.size / 1024 ** 3).toFixed(1)}GB)`
: ''
}`}
className="self-end"
> >
</Tooltip> <span
class=" text-xs font-medium text-gray-600 dark:text-gray-400 line-clamp-1"
>{item.model.ollama?.details?.parameter_size ?? ''}</span
>
</Tooltip>
</div>
{/if}
</div>
{#if !$mobile && (item?.model?.info?.meta?.tags ?? []).length > 0}
<div class="flex gap-0.5 self-center items-center h-full translate-y-[0.5px]">
{#each item.model?.info?.meta.tags as tag}
<div
class=" text-xs font-black px-1 rounded uppercase line-clamp-1 bg-gray-500/20 text-gray-700 dark:text-gray-200"
>
{tag.name}
</div>
{/each}
</div> </div>
{/if} {/if}
</div>
<!-- {JSON.stringify(item.info)} --> <!-- {JSON.stringify(item.info)} -->
{#if item.model.owned_by === 'openai'} {#if item.model.owned_by === 'openai'}
<Tooltip content={`${'External'}`}> <Tooltip content={`${'External'}`}>
<div class=""> <div class="">
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16" viewBox="0 0 16 16"
fill="currentColor" fill="currentColor"
class="size-3" class="size-3"
> >
<path <path
fill-rule="evenodd" fill-rule="evenodd"
d="M8.914 6.025a.75.75 0 0 1 1.06 0 3.5 3.5 0 0 1 0 4.95l-2 2a3.5 3.5 0 0 1-5.396-4.402.75.75 0 0 1 1.251.827 2 2 0 0 0 3.085 2.514l2-2a2 2 0 0 0 0-2.828.75.75 0 0 1 0-1.06Z" d="M8.914 6.025a.75.75 0 0 1 1.06 0 3.5 3.5 0 0 1 0 4.95l-2 2a3.5 3.5 0 0 1-5.396-4.402.75.75 0 0 1 1.251.827 2 2 0 0 0 3.085 2.514l2-2a2 2 0 0 0 0-2.828.75.75 0 0 1 0-1.06Z"
clip-rule="evenodd" clip-rule="evenodd"
/> />
<path <path
fill-rule="evenodd" fill-rule="evenodd"
d="M7.086 9.975a.75.75 0 0 1-1.06 0 3.5 3.5 0 0 1 0-4.95l2-2a3.5 3.5 0 0 1 5.396 4.402.75.75 0 0 1-1.251-.827 2 2 0 0 0-3.085-2.514l-2 2a2 2 0 0 0 0 2.828.75.75 0 0 1 0 1.06Z" d="M7.086 9.975a.75.75 0 0 1-1.06 0 3.5 3.5 0 0 1 0-4.95l2-2a3.5 3.5 0 0 1 5.396 4.402.75.75 0 0 1-1.251-.827 2 2 0 0 0-3.085-2.514l-2 2a2 2 0 0 0 0 2.828.75.75 0 0 1 0 1.06Z"
clip-rule="evenodd" clip-rule="evenodd"
/> />
</svg> </svg>
</div> </div>
</Tooltip> </Tooltip>
{/if} {/if}
{#if item.model?.info?.meta?.description} {#if item.model?.info?.meta?.description}
<Tooltip <Tooltip
content={`${sanitizeResponseContent( content={`${sanitizeResponseContent(
item.model?.info?.meta?.description item.model?.info?.meta?.description
).replaceAll('\n', '<br>')}`} ).replaceAll('\n', '<br>')}`}
> >
<div class=""> <div class="">
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
fill="none" fill="none"
viewBox="0 0 24 24" viewBox="0 0 24 24"
stroke-width="1.5" stroke-width="1.5"
stroke="currentColor" stroke="currentColor"
class="w-4 h-4" class="w-4 h-4"
> >
<path <path
stroke-linecap="round" stroke-linecap="round"
stroke-linejoin="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" 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> </svg>
</div> </div>
</Tooltip> </Tooltip>
{/if} {/if}
</div>
</div> </div>
{#if value === item.value} {#if value === item.value}
<div class="ml-auto"> <div class="ml-auto pl-2">
<Check /> <Check />
</div> </div>
{/if} {/if}
......
...@@ -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>
......
...@@ -5,21 +5,26 @@ ...@@ -5,21 +5,26 @@
const i18n = getContext('i18n'); const i18n = getContext('i18n');
export let admin = false;
export let params = { export let params = {
// Advanced // Advanced
seed: 0, seed: null,
stop: null, stop: null,
temperature: '', temperature: null,
frequency_penalty: '', frequency_penalty: null,
repeat_last_n: '', repeat_last_n: null,
mirostat: '', mirostat: null,
mirostat_eta: '', mirostat_eta: null,
mirostat_tau: '', mirostat_tau: null,
top_k: '', top_k: null,
top_p: '', top_p: null,
tfs_z: '', tfs_z: null,
num_ctx: '', num_ctx: null,
max_tokens: '', max_tokens: null,
use_mmap: null,
use_mlock: null,
num_thread: null,
template: null template: null
}; };
...@@ -109,10 +114,10 @@ ...@@ -109,10 +114,10 @@
class="p-1 px-3 text-xs flex rounded transition" class="p-1 px-3 text-xs flex rounded transition"
type="button" type="button"
on:click={() => { on:click={() => {
params.temperature = (params?.temperature ?? '') === '' ? 0.8 : ''; params.temperature = (params?.temperature ?? null) === null ? 0.8 : null;
}} }}
> >
{#if (params?.temperature ?? '') === ''} {#if (params?.temperature ?? null) === null}
<span class="ml-2 self-center"> {$i18n.t('Default')} </span> <span class="ml-2 self-center"> {$i18n.t('Default')} </span>
{:else} {:else}
<span class="ml-2 self-center"> {$i18n.t('Custom')} </span> <span class="ml-2 self-center"> {$i18n.t('Custom')} </span>
...@@ -120,7 +125,7 @@ ...@@ -120,7 +125,7 @@
</button> </button>
</div> </div>
{#if (params?.temperature ?? '') !== ''} {#if (params?.temperature ?? null) !== null}
<div class="flex mt-0.5 space-x-2"> <div class="flex mt-0.5 space-x-2">
<div class=" flex-1"> <div class=" flex-1">
<input <input
...@@ -140,7 +145,7 @@ ...@@ -140,7 +145,7 @@
class=" bg-transparent text-center w-14" class=" bg-transparent text-center w-14"
min="0" min="0"
max="1" max="1"
step="0.05" step="any"
/> />
</div> </div>
</div> </div>
...@@ -155,10 +160,10 @@ ...@@ -155,10 +160,10 @@
class="p-1 px-3 text-xs flex rounded transition" class="p-1 px-3 text-xs flex rounded transition"
type="button" type="button"
on:click={() => { on:click={() => {
params.mirostat = (params?.mirostat ?? '') === '' ? 0 : ''; params.mirostat = (params?.mirostat ?? null) === null ? 0 : null;
}} }}
> >
{#if (params?.mirostat ?? '') === ''} {#if (params?.mirostat ?? null) === null}
<span class="ml-2 self-center">{$i18n.t('Default')}</span> <span class="ml-2 self-center">{$i18n.t('Default')}</span>
{:else} {:else}
<span class="ml-2 self-center">{$i18n.t('Custom')}</span> <span class="ml-2 self-center">{$i18n.t('Custom')}</span>
...@@ -166,7 +171,7 @@ ...@@ -166,7 +171,7 @@
</button> </button>
</div> </div>
{#if (params?.mirostat ?? '') !== ''} {#if (params?.mirostat ?? null) !== null}
<div class="flex mt-0.5 space-x-2"> <div class="flex mt-0.5 space-x-2">
<div class=" flex-1"> <div class=" flex-1">
<input <input
...@@ -201,10 +206,10 @@ ...@@ -201,10 +206,10 @@
class="p-1 px-3 text-xs flex rounded transition" class="p-1 px-3 text-xs flex rounded transition"
type="button" type="button"
on:click={() => { on:click={() => {
params.mirostat_eta = (params?.mirostat_eta ?? '') === '' ? 0.1 : ''; params.mirostat_eta = (params?.mirostat_eta ?? null) === null ? 0.1 : null;
}} }}
> >
{#if (params?.mirostat_eta ?? '') === ''} {#if (params?.mirostat_eta ?? null) === null}
<span class="ml-2 self-center">{$i18n.t('Default')}</span> <span class="ml-2 self-center">{$i18n.t('Default')}</span>
{:else} {:else}
<span class="ml-2 self-center">{$i18n.t('Custom')}</span> <span class="ml-2 self-center">{$i18n.t('Custom')}</span>
...@@ -212,7 +217,7 @@ ...@@ -212,7 +217,7 @@
</button> </button>
</div> </div>
{#if (params?.mirostat_eta ?? '') !== ''} {#if (params?.mirostat_eta ?? null) !== null}
<div class="flex mt-0.5 space-x-2"> <div class="flex mt-0.5 space-x-2">
<div class=" flex-1"> <div class=" flex-1">
<input <input
...@@ -232,7 +237,7 @@ ...@@ -232,7 +237,7 @@
class=" bg-transparent text-center w-14" class=" bg-transparent text-center w-14"
min="0" min="0"
max="1" max="1"
step="0.05" step="any"
/> />
</div> </div>
</div> </div>
...@@ -247,10 +252,10 @@ ...@@ -247,10 +252,10 @@
class="p-1 px-3 text-xs flex rounded transition" class="p-1 px-3 text-xs flex rounded transition"
type="button" type="button"
on:click={() => { on:click={() => {
params.mirostat_tau = (params?.mirostat_tau ?? '') === '' ? 5.0 : ''; params.mirostat_tau = (params?.mirostat_tau ?? null) === null ? 5.0 : null;
}} }}
> >
{#if (params?.mirostat_tau ?? '') === ''} {#if (params?.mirostat_tau ?? null) === null}
<span class="ml-2 self-center">{$i18n.t('Default')}</span> <span class="ml-2 self-center">{$i18n.t('Default')}</span>
{:else} {:else}
<span class="ml-2 self-center">{$i18n.t('Custom')}</span> <span class="ml-2 self-center">{$i18n.t('Custom')}</span>
...@@ -258,7 +263,7 @@ ...@@ -258,7 +263,7 @@
</button> </button>
</div> </div>
{#if (params?.mirostat_tau ?? '') !== ''} {#if (params?.mirostat_tau ?? null) !== null}
<div class="flex mt-0.5 space-x-2"> <div class="flex mt-0.5 space-x-2">
<div class=" flex-1"> <div class=" flex-1">
<input <input
...@@ -278,7 +283,7 @@ ...@@ -278,7 +283,7 @@
class=" bg-transparent text-center w-14" class=" bg-transparent text-center w-14"
min="0" min="0"
max="10" max="10"
step="0.5" step="any"
/> />
</div> </div>
</div> </div>
...@@ -293,10 +298,10 @@ ...@@ -293,10 +298,10 @@
class="p-1 px-3 text-xs flex rounded transition" class="p-1 px-3 text-xs flex rounded transition"
type="button" type="button"
on:click={() => { on:click={() => {
params.top_k = (params?.top_k ?? '') === '' ? 40 : ''; params.top_k = (params?.top_k ?? null) === null ? 40 : null;
}} }}
> >
{#if (params?.top_k ?? '') === ''} {#if (params?.top_k ?? null) === null}
<span class="ml-2 self-center">{$i18n.t('Default')}</span> <span class="ml-2 self-center">{$i18n.t('Default')}</span>
{:else} {:else}
<span class="ml-2 self-center">{$i18n.t('Custom')}</span> <span class="ml-2 self-center">{$i18n.t('Custom')}</span>
...@@ -304,7 +309,7 @@ ...@@ -304,7 +309,7 @@
</button> </button>
</div> </div>
{#if (params?.top_k ?? '') !== ''} {#if (params?.top_k ?? null) !== null}
<div class="flex mt-0.5 space-x-2"> <div class="flex mt-0.5 space-x-2">
<div class=" flex-1"> <div class=" flex-1">
<input <input
...@@ -324,7 +329,7 @@ ...@@ -324,7 +329,7 @@
class=" bg-transparent text-center w-14" class=" bg-transparent text-center w-14"
min="0" min="0"
max="100" max="100"
step="0.5" step="any"
/> />
</div> </div>
</div> </div>
...@@ -339,10 +344,10 @@ ...@@ -339,10 +344,10 @@
class="p-1 px-3 text-xs flex rounded transition" class="p-1 px-3 text-xs flex rounded transition"
type="button" type="button"
on:click={() => { on:click={() => {
params.top_p = (params?.top_p ?? '') === '' ? 0.9 : ''; params.top_p = (params?.top_p ?? null) === null ? 0.9 : null;
}} }}
> >
{#if (params?.top_p ?? '') === ''} {#if (params?.top_p ?? null) === null}
<span class="ml-2 self-center">{$i18n.t('Default')}</span> <span class="ml-2 self-center">{$i18n.t('Default')}</span>
{:else} {:else}
<span class="ml-2 self-center">{$i18n.t('Custom')}</span> <span class="ml-2 self-center">{$i18n.t('Custom')}</span>
...@@ -350,7 +355,7 @@ ...@@ -350,7 +355,7 @@
</button> </button>
</div> </div>
{#if (params?.top_p ?? '') !== ''} {#if (params?.top_p ?? null) !== null}
<div class="flex mt-0.5 space-x-2"> <div class="flex mt-0.5 space-x-2">
<div class=" flex-1"> <div class=" flex-1">
<input <input
...@@ -370,7 +375,7 @@ ...@@ -370,7 +375,7 @@
class=" bg-transparent text-center w-14" class=" bg-transparent text-center w-14"
min="0" min="0"
max="1" max="1"
step="0.05" step="any"
/> />
</div> </div>
</div> </div>
...@@ -379,16 +384,16 @@ ...@@ -379,16 +384,16 @@
<div class=" py-0.5 w-full justify-between"> <div class=" py-0.5 w-full justify-between">
<div class="flex w-full justify-between"> <div class="flex w-full justify-between">
<div class=" self-center text-xs font-medium">{$i18n.t('Frequencey Penalty')}</div> <div class=" self-center text-xs font-medium">{$i18n.t('Frequency Penalty')}</div>
<button <button
class="p-1 px-3 text-xs flex rounded transition" class="p-1 px-3 text-xs flex rounded transition"
type="button" type="button"
on:click={() => { on:click={() => {
params.frequency_penalty = (params?.frequency_penalty ?? '') === '' ? 1.1 : ''; params.frequency_penalty = (params?.frequency_penalty ?? null) === null ? 1.1 : null;
}} }}
> >
{#if (params?.frequency_penalty ?? '') === ''} {#if (params?.frequency_penalty ?? null) === null}
<span class="ml-2 self-center">{$i18n.t('Default')}</span> <span class="ml-2 self-center">{$i18n.t('Default')}</span>
{:else} {:else}
<span class="ml-2 self-center">{$i18n.t('Custom')}</span> <span class="ml-2 self-center">{$i18n.t('Custom')}</span>
...@@ -396,7 +401,7 @@ ...@@ -396,7 +401,7 @@
</button> </button>
</div> </div>
{#if (params?.frequency_penalty ?? '') !== ''} {#if (params?.frequency_penalty ?? null) !== null}
<div class="flex mt-0.5 space-x-2"> <div class="flex mt-0.5 space-x-2">
<div class=" flex-1"> <div class=" flex-1">
<input <input
...@@ -416,7 +421,7 @@ ...@@ -416,7 +421,7 @@
class=" bg-transparent text-center w-14" class=" bg-transparent text-center w-14"
min="0" min="0"
max="2" max="2"
step="0.05" step="any"
/> />
</div> </div>
</div> </div>
...@@ -431,10 +436,10 @@ ...@@ -431,10 +436,10 @@
class="p-1 px-3 text-xs flex rounded transition" class="p-1 px-3 text-xs flex rounded transition"
type="button" type="button"
on:click={() => { on:click={() => {
params.repeat_last_n = (params?.repeat_last_n ?? '') === '' ? 64 : ''; params.repeat_last_n = (params?.repeat_last_n ?? null) === null ? 64 : null;
}} }}
> >
{#if (params?.repeat_last_n ?? '') === ''} {#if (params?.repeat_last_n ?? null) === null}
<span class="ml-2 self-center">{$i18n.t('Default')}</span> <span class="ml-2 self-center">{$i18n.t('Default')}</span>
{:else} {:else}
<span class="ml-2 self-center">{$i18n.t('Custom')}</span> <span class="ml-2 self-center">{$i18n.t('Custom')}</span>
...@@ -442,7 +447,7 @@ ...@@ -442,7 +447,7 @@
</button> </button>
</div> </div>
{#if (params?.repeat_last_n ?? '') !== ''} {#if (params?.repeat_last_n ?? null) !== null}
<div class="flex mt-0.5 space-x-2"> <div class="flex mt-0.5 space-x-2">
<div class=" flex-1"> <div class=" flex-1">
<input <input
...@@ -477,10 +482,10 @@ ...@@ -477,10 +482,10 @@
class="p-1 px-3 text-xs flex rounded transition" class="p-1 px-3 text-xs flex rounded transition"
type="button" type="button"
on:click={() => { on:click={() => {
params.tfs_z = (params?.tfs_z ?? '') === '' ? 1 : ''; params.tfs_z = (params?.tfs_z ?? null) === null ? 1 : null;
}} }}
> >
{#if (params?.tfs_z ?? '') === ''} {#if (params?.tfs_z ?? null) === null}
<span class="ml-2 self-center">{$i18n.t('Default')}</span> <span class="ml-2 self-center">{$i18n.t('Default')}</span>
{:else} {:else}
<span class="ml-2 self-center">{$i18n.t('Custom')}</span> <span class="ml-2 self-center">{$i18n.t('Custom')}</span>
...@@ -488,7 +493,7 @@ ...@@ -488,7 +493,7 @@
</button> </button>
</div> </div>
{#if (params?.tfs_z ?? '') !== ''} {#if (params?.tfs_z ?? null) !== null}
<div class="flex mt-0.5 space-x-2"> <div class="flex mt-0.5 space-x-2">
<div class=" flex-1"> <div class=" flex-1">
<input <input
...@@ -508,7 +513,7 @@ ...@@ -508,7 +513,7 @@
class=" bg-transparent text-center w-14" class=" bg-transparent text-center w-14"
min="0" min="0"
max="2" max="2"
step="0.05" step="any"
/> />
</div> </div>
</div> </div>
...@@ -523,10 +528,10 @@ ...@@ -523,10 +528,10 @@
class="p-1 px-3 text-xs flex rounded transition" class="p-1 px-3 text-xs flex rounded transition"
type="button" type="button"
on:click={() => { on:click={() => {
params.num_ctx = (params?.num_ctx ?? '') === '' ? 2048 : ''; params.num_ctx = (params?.num_ctx ?? null) === null ? 2048 : null;
}} }}
> >
{#if (params?.num_ctx ?? '') === ''} {#if (params?.num_ctx ?? null) === null}
<span class="ml-2 self-center">{$i18n.t('Default')}</span> <span class="ml-2 self-center">{$i18n.t('Default')}</span>
{:else} {:else}
<span class="ml-2 self-center">{$i18n.t('Custom')}</span> <span class="ml-2 self-center">{$i18n.t('Custom')}</span>
...@@ -534,7 +539,7 @@ ...@@ -534,7 +539,7 @@
</button> </button>
</div> </div>
{#if (params?.num_ctx ?? '') !== ''} {#if (params?.num_ctx ?? null) !== null}
<div class="flex mt-0.5 space-x-2"> <div class="flex mt-0.5 space-x-2">
<div class=" flex-1"> <div class=" flex-1">
<input <input
...@@ -553,12 +558,13 @@ ...@@ -553,12 +558,13 @@
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>
{/if} {/if}
</div> </div>
<div class=" py-0.5 w-full justify-between"> <div class=" py-0.5 w-full justify-between">
<div class="flex w-full justify-between"> <div class="flex w-full justify-between">
<div class=" self-center text-xs font-medium">{$i18n.t('Max Tokens (num_predict)')}</div> <div class=" self-center text-xs font-medium">{$i18n.t('Max Tokens (num_predict)')}</div>
...@@ -567,10 +573,10 @@ ...@@ -567,10 +573,10 @@
class="p-1 px-3 text-xs flex rounded transition" class="p-1 px-3 text-xs flex rounded transition"
type="button" type="button"
on:click={() => { on:click={() => {
params.max_tokens = (params?.max_tokens ?? '') === '' ? 128 : ''; params.max_tokens = (params?.max_tokens ?? null) === null ? 128 : null;
}} }}
> >
{#if (params?.max_tokens ?? '') === ''} {#if (params?.max_tokens ?? null) === null}
<span class="ml-2 self-center">{$i18n.t('Default')}</span> <span class="ml-2 self-center">{$i18n.t('Default')}</span>
{:else} {:else}
<span class="ml-2 self-center">{$i18n.t('Custom')}</span> <span class="ml-2 self-center">{$i18n.t('Custom')}</span>
...@@ -578,7 +584,7 @@ ...@@ -578,7 +584,7 @@
</button> </button>
</div> </div>
{#if (params?.max_tokens ?? '') !== ''} {#if (params?.max_tokens ?? null) !== null}
<div class="flex mt-0.5 space-x-2"> <div class="flex mt-0.5 space-x-2">
<div class=" flex-1"> <div class=" flex-1">
<input <input
...@@ -604,36 +610,125 @@ ...@@ -604,36 +610,125 @@
</div> </div>
{/if} {/if}
</div> </div>
<div class=" py-0.5 w-full justify-between">
<div class="flex w-full justify-between">
<div class=" self-center text-xs font-medium">{$i18n.t('Template')}</div>
<button {#if admin}
class="p-1 px-3 text-xs flex rounded transition" <div class=" py-0.5 w-full justify-between">
type="button" <div class="flex w-full justify-between">
on:click={() => { <div class=" self-center text-xs font-medium">{$i18n.t('use_mmap (Ollama)')}</div>
params.template = (params?.template ?? null) === null ? '' : null;
}} <button
> class="p-1 px-3 text-xs flex rounded transition"
{#if (params?.template ?? null) === null} type="button"
<span class="ml-2 self-center">{$i18n.t('Default')}</span> on:click={() => {
{:else} params.use_mmap = (params?.use_mmap ?? null) === null ? true : null;
<span class="ml-2 self-center">{$i18n.t('Custom')}</span> }}
{/if} >
</button> {#if (params?.use_mmap ?? null) === null}
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
{:else}
<span class="ml-2 self-center">{$i18n.t('On')}</span>
{/if}
</button>
</div>
</div> </div>
{#if (params?.template ?? null) !== null} <div class=" py-0.5 w-full justify-between">
<div class="flex mt-0.5 space-x-2"> <div class="flex w-full justify-between">
<div class=" flex-1"> <div class=" self-center text-xs font-medium">{$i18n.t('use_mlock (Ollama)')}</div>
<textarea
class="px-3 py-1.5 text-sm w-full bg-transparent border dark:border-gray-600 outline-none rounded-lg -mb-1" <button
placeholder="Write your model template content here" class="p-1 px-3 text-xs flex rounded transition"
rows="4" type="button"
bind:value={params.template} on:click={() => {
/> params.use_mlock = (params?.use_mlock ?? null) === null ? true : null;
}}
>
{#if (params?.use_mlock ?? null) === null}
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
{:else}
<span class="ml-2 self-center">{$i18n.t('On')}</span>
{/if}
</button>
</div>
</div>
<div class=" py-0.5 w-full justify-between">
<div class="flex w-full justify-between">
<div class=" self-center text-xs font-medium">{$i18n.t('num_thread (Ollama)')}</div>
<button
class="p-1 px-3 text-xs flex rounded transition"
type="button"
on:click={() => {
params.num_thread = (params?.num_thread ?? null) === null ? 2 : null;
}}
>
{#if (params?.num_thread ?? null) === null}
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
{:else}
<span class="ml-2 self-center">{$i18n.t('Custom')}</span>
{/if}
</button>
</div>
{#if (params?.num_thread ?? null) !== null}
<div class="flex mt-0.5 space-x-2">
<div class=" flex-1">
<input
id="steps-range"
type="range"
min="1"
max="256"
step="1"
bind:value={params.num_thread}
class="w-full h-2 rounded-lg appearance-none cursor-pointer dark:bg-gray-700"
/>
</div>
<div class="">
<input
bind:value={params.num_thread}
type="number"
class=" bg-transparent text-center w-14"
min="1"
max="256"
step="1"
/>
</div>
</div> </div>
{/if}
</div>
<!-- <div class=" py-0.5 w-full justify-between">
<div class="flex w-full justify-between">
<div class=" self-center text-xs font-medium">{$i18n.t('Template')}</div>
<button
class="p-1 px-3 text-xs flex rounded transition"
type="button"
on:click={() => {
params.template = (params?.template ?? null) === null ? '' : null;
}}
>
{#if (params?.template ?? null) === null}
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
{:else}
<span class="ml-2 self-center">{$i18n.t('Custom')}</span>
{/if}
</button>
</div> </div>
{/if}
</div> {#if (params?.template ?? null) !== null}
<div class="flex mt-0.5 space-x-2">
<div class=" flex-1">
<textarea
class="px-3 py-1.5 text-sm w-full bg-transparent border dark:border-gray-600 outline-none rounded-lg -mb-1"
placeholder="Write your model template content here"
rows="4"
bind:value={params.template}
/>
</div>
</div>
{/if}
</div> -->
{/if}
</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';
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
const i18n = getContext('i18n'); const i18n = getContext('i18n');
...@@ -10,24 +10,15 @@ ...@@ -10,24 +10,15 @@
export let saveSettings: Function; export let saveSettings: Function;
// Audio // Audio
let OpenAIUrl = '';
let OpenAIKey = '';
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 TTSEngines = ['', 'openai']; let STTEngine = '';
let TTSEngine = '';
let voices = []; let voices = [];
let speaker = ''; let voice = '';
let models = [];
let model = '';
const getOpenAIVoices = () => { const getOpenAIVoices = () => {
voices = [ voices = [
...@@ -40,10 +31,6 @@ ...@@ -40,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();
...@@ -55,21 +42,6 @@ ...@@ -55,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 });
...@@ -80,66 +52,35 @@ ...@@ -80,66 +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: speaker
});
if (res) {
OpenAIUrl = res.OPENAI_API_BASE_URL;
OpenAIKey = res.OPENAI_API_KEY;
model = res.OPENAI_API_MODEL;
speaker = 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 ?? '';
speaker = $settings?.audio?.speaker ?? ''; nonLocalVoices = $settings.audio?.tts?.nonLocalVoices ?? false;
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;
speaker = res.OPENAI_API_VOICE;
}
}
}); });
</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: speaker !== '' ? speaker : undefined, },
model: model !== '' ? model : undefined tts: {
voice: voice !== '' ? voice : undefined,
nonLocalVoices: $config.audio.tts.engine === '' ? nonLocalVoices : undefined
}
} }
}); });
dispatch('save'); dispatch('save');
...@@ -149,53 +90,25 @@ ...@@ -149,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
...@@ -217,50 +130,6 @@ ...@@ -217,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();
speaker = '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>
...@@ -280,28 +149,39 @@ ...@@ -280,28 +149,39 @@
</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}
placeholder="Select a voice"
> >
<option value="" selected>{$i18n.t('Default')}</option> <option value="" selected={voice !== ''}>{$i18n.t('Default')}</option>
{#each voices.filter((v) => v.localService === true) as voice} {#each voices.filter((v) => nonLocalVoices || v.localService === true) as _voice}
<option value={voice.name} class="bg-gray-100 dark:bg-gray-700">{voice.name}</option <option
value={_voice.name}
class="bg-gray-100 dark:bg-gray-700"
selected={voice === _voice.name}>{_voice.name}</option
> >
{/each} {/each}
</select> </select>
</div> </div>
</div> </div>
<div class="flex items-center justify-between my-1.5">
<div class="text-xs">
{$i18n.t('Allow non-local voices')}
</div>
<div class="mt-1">
<Switch bind:state={nonLocalVoices} />
</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">
...@@ -309,7 +189,7 @@ ...@@ -309,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={speaker} bind:value={voice}
placeholder="Select a voice" placeholder="Select a voice"
/> />
...@@ -321,25 +201,6 @@ ...@@ -321,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}
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
import { getLanguages } from '$lib/i18n'; import { getLanguages } from '$lib/i18n';
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
import { models, settings, theme } from '$lib/stores'; import { models, settings, theme, user } from '$lib/stores';
const i18n = getContext('i18n'); const i18n = getContext('i18n');
...@@ -43,19 +43,19 @@ ...@@ -43,19 +43,19 @@
let params = { let params = {
// Advanced // Advanced
seed: 0, seed: null,
temperature: '', temperature: null,
frequency_penalty: '', frequency_penalty: null,
repeat_last_n: '', repeat_last_n: null,
mirostat: '', mirostat: null,
mirostat_eta: '', mirostat_eta: null,
mirostat_tau: '', mirostat_tau: null,
top_k: '', top_k: null,
top_p: '', top_p: null,
stop: null, stop: null,
tfs_z: '', tfs_z: null,
num_ctx: '', num_ctx: null,
max_tokens: '' max_tokens: null
}; };
const toggleRequestFormat = async () => { const toggleRequestFormat = async () => {
...@@ -79,12 +79,6 @@ ...@@ -79,12 +79,6 @@
requestFormat = $settings.requestFormat ?? ''; requestFormat = $settings.requestFormat ?? '';
keepAlive = $settings.keepAlive ?? null; keepAlive = $settings.keepAlive ?? null;
params.seed = $settings.seed ?? 0;
params.temperature = $settings.temperature ?? '';
params.frequency_penalty = $settings.frequency_penalty ?? '';
params.top_k = $settings.top_k ?? '';
params.top_p = $settings.top_p ?? '';
params.num_ctx = $settings.num_ctx ?? '';
params = { ...params, ...$settings.params }; params = { ...params, ...$settings.params };
params.stop = $settings?.params?.stop ? ($settings?.params?.stop ?? []).join(',') : null; params.stop = $settings?.params?.stop ? ($settings?.params?.stop ?? []).join(',') : null;
}); });
...@@ -146,6 +140,7 @@ ...@@ -146,6 +140,7 @@
<option value="dark">🌑 {$i18n.t('Dark')}</option> <option value="dark">🌑 {$i18n.t('Dark')}</option>
<option value="oled-dark">🌃 {$i18n.t('OLED Dark')}</option> <option value="oled-dark">🌃 {$i18n.t('OLED Dark')}</option>
<option value="light">☀️ {$i18n.t('Light')}</option> <option value="light">☀️ {$i18n.t('Light')}</option>
<option value="her">🌷 Her</option>
<!-- <option value="rose-pine dark">🪻 {$i18n.t('Rosé Pine')}</option> <!-- <option value="rose-pine dark">🪻 {$i18n.t('Rosé Pine')}</option>
<option value="rose-pine-dawn light">🌷 {$i18n.t('Rosé Pine Dawn')}</option> --> <option value="rose-pine-dawn light">🌷 {$i18n.t('Rosé Pine Dawn')}</option> -->
</select> </select>
...@@ -203,7 +198,7 @@ ...@@ -203,7 +198,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>
...@@ -227,8 +222,8 @@ ...@@ -227,8 +222,8 @@
</div> </div>
{#if showAdvanced} {#if showAdvanced}
<AdvancedParams bind:params /> <AdvancedParams admin={$user?.role === 'admin'} 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">
...@@ -300,20 +295,23 @@ ...@@ -300,20 +295,23 @@
saveSettings({ saveSettings({
system: system !== '' ? system : undefined, system: system !== '' ? system : undefined,
params: { params: {
seed: (params.seed !== 0 ? params.seed : undefined) ?? undefined, seed: (params.seed !== null ? params.seed : undefined) ?? undefined,
stop: params.stop ? params.stop.split(',').filter((e) => e) : undefined, stop: params.stop ? params.stop.split(',').filter((e) => e) : undefined,
temperature: params.temperature !== '' ? params.temperature : undefined, temperature: params.temperature !== null ? params.temperature : undefined,
frequency_penalty: frequency_penalty:
params.frequency_penalty !== '' ? params.frequency_penalty : undefined, params.frequency_penalty !== null ? params.frequency_penalty : undefined,
repeat_last_n: params.repeat_last_n !== '' ? params.repeat_last_n : undefined, repeat_last_n: params.repeat_last_n !== null ? params.repeat_last_n : undefined,
mirostat: params.mirostat !== '' ? params.mirostat : undefined, mirostat: params.mirostat !== null ? params.mirostat : undefined,
mirostat_eta: params.mirostat_eta !== '' ? params.mirostat_eta : undefined, mirostat_eta: params.mirostat_eta !== null ? params.mirostat_eta : undefined,
mirostat_tau: params.mirostat_tau !== '' ? params.mirostat_tau : undefined, mirostat_tau: params.mirostat_tau !== null ? params.mirostat_tau : undefined,
top_k: params.top_k !== '' ? params.top_k : undefined, top_k: params.top_k !== null ? params.top_k : undefined,
top_p: params.top_p !== '' ? params.top_p : undefined, top_p: params.top_p !== null ? params.top_p : undefined,
tfs_z: params.tfs_z !== '' ? params.tfs_z : undefined, tfs_z: params.tfs_z !== null ? params.tfs_z : undefined,
num_ctx: params.num_ctx !== '' ? params.num_ctx : undefined, num_ctx: params.num_ctx !== null ? params.num_ctx : undefined,
max_tokens: params.max_tokens !== '' ? params.max_tokens : undefined max_tokens: params.max_tokens !== null ? params.max_tokens : undefined,
use_mmap: params.use_mmap !== null ? params.use_mmap : undefined,
use_mlock: params.use_mlock !== null ? params.use_mlock : undefined,
num_thread: params.num_thread !== null ? params.num_thread : undefined
}, },
keepAlive: keepAlive ? (isNaN(keepAlive) ? keepAlive : parseInt(keepAlive)) : undefined keepAlive: keepAlive ? (isNaN(keepAlive) ? keepAlive : parseInt(keepAlive)) : undefined
}); });
......
...@@ -14,14 +14,11 @@ ...@@ -14,14 +14,11 @@
// Addons // Addons
let titleAutoGenerate = true; let titleAutoGenerate = true;
let responseAutoCopy = false; let responseAutoCopy = false;
let titleAutoGenerateModel = ''; let widescreenMode = false;
let titleAutoGenerateModelExternal = '';
let fullScreenMode = false;
let titleGenerationPrompt = '';
let splitLargeChunks = false; let splitLargeChunks = false;
// Interface // Interface
let promptSuggestions = []; let defaultModelId = '';
let showUsername = false; let showUsername = false;
let chatBubble = true; let chatBubble = true;
let chatDirection: 'LTR' | 'RTL' = 'LTR'; let chatDirection: 'LTR' | 'RTL' = 'LTR';
...@@ -31,9 +28,9 @@ ...@@ -31,9 +28,9 @@
saveSettings({ splitLargeChunks: splitLargeChunks }); saveSettings({ splitLargeChunks: splitLargeChunks });
}; };
const toggleFullScreenMode = async () => { const togglewidescreenMode = async () => {
fullScreenMode = !fullScreenMode; widescreenMode = !widescreenMode;
saveSettings({ fullScreenMode: fullScreenMode }); saveSettings({ widescreenMode: widescreenMode });
}; };
const toggleChatBubble = async () => { const toggleChatBubble = async () => {
...@@ -84,39 +81,21 @@ ...@@ -84,39 +81,21 @@
}; };
const updateInterfaceHandler = async () => { const updateInterfaceHandler = async () => {
if ($user.role === 'admin') {
promptSuggestions = await setDefaultPromptSuggestions(localStorage.token, promptSuggestions);
await config.set(await getBackendConfig());
}
saveSettings({ saveSettings({
title: { models: [defaultModelId]
...$settings.title,
model: titleAutoGenerateModel !== '' ? titleAutoGenerateModel : undefined,
modelExternal:
titleAutoGenerateModelExternal !== '' ? titleAutoGenerateModelExternal : undefined,
prompt: titleGenerationPrompt ? titleGenerationPrompt : undefined
}
}); });
}; };
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;
fullScreenMode = $settings.fullScreenMode ?? false; widescreenMode = $settings.widescreenMode ?? false;
splitLargeChunks = $settings.splitLargeChunks ?? false; splitLargeChunks = $settings.splitLargeChunks ?? false;
chatDirection = $settings.chatDirection ?? 'LTR'; chatDirection = $settings.chatDirection ?? 'LTR';
defaultModelId = ($settings?.models ?? ['']).at(0);
}); });
</script> </script>
...@@ -195,16 +174,16 @@ ...@@ -195,16 +174,16 @@
<div> <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">{$i18n.t('Full Screen Mode')}</div> <div class=" self-center text-xs font-medium">{$i18n.t('Widescreen Mode')}</div>
<button <button
class="p-1 px-3 text-xs flex rounded transition" class="p-1 px-3 text-xs flex rounded transition"
on:click={() => { on:click={() => {
toggleFullScreenMode(); togglewidescreenMode();
}} }}
type="button" type="button"
> >
{#if fullScreenMode === true} {#if widescreenMode === true}
<span class="ml-2 self-center">{$i18n.t('On')}</span> <span class="ml-2 self-center">{$i18n.t('On')}</span>
{:else} {:else}
<span class="ml-2 self-center">{$i18n.t('Off')}</span> <span class="ml-2 self-center">{$i18n.t('Off')}</span>
...@@ -278,161 +257,28 @@ ...@@ -278,161 +257,28 @@
</div> </div>
</div> </div>
<hr class=" dark:border-gray-700" /> <hr class=" dark:border-gray-850" />
<div> <div class=" space-y-1 mb-3">
<div class=" mb-2.5 text-sm font-medium flex"> <div class="mb-2">
<div class=" mr-1">{$i18n.t('Set Task Model')}</div> <div class="flex justify-between items-center text-xs">
<Tooltip <div class=" text-xs font-medium">{$i18n.t('Default Model')}</div>
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>
<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>
</div>
{#if $user.role === 'admin'} <div class="flex-1 mr-2">
<hr class=" dark:border-gray-700" /> <select
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
<div class=" space-y-3 pr-1.5"> bind:value={defaultModelId}
<div class="flex w-full justify-between mb-2"> placeholder="Select a model"
<div class=" self-center text-sm font-semibold"> >
{$i18n.t('Default Prompt Suggestions')} <option value="" disabled selected>{$i18n.t('Select a model')}</option>
</div> {#each $models.filter((model) => model.id) as model}
<option value={model.id} class="bg-gray-100 dark:bg-gray-700">{model.name}</option>
<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} {/each}
</div> </select>
{#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> </div>
{/if} </div>
</div> </div>
<div class="flex justify-end text-sm font-medium"> <div class="flex justify-end text-sm font-medium">
......
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