Merge branch 'dev' into feat/openai-embeddings-batch
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import mermaid from 'mermaid';
|
||||
|
||||
import { getContext, onMount, tick } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
@@ -26,7 +27,7 @@
|
||||
splitStream
|
||||
} from '$lib/utils';
|
||||
|
||||
import { cancelOllamaRequest, generateChatCompletion } from '$lib/apis/ollama';
|
||||
import { generateChatCompletion } from '$lib/apis/ollama';
|
||||
import {
|
||||
addTagById,
|
||||
createNewChat,
|
||||
@@ -65,7 +66,6 @@
|
||||
let autoScroll = true;
|
||||
let processing = '';
|
||||
let messagesContainerElement: HTMLDivElement;
|
||||
let currentRequestId = null;
|
||||
|
||||
let showModelSelector = true;
|
||||
|
||||
@@ -130,10 +130,6 @@
|
||||
//////////////////////////
|
||||
|
||||
const initNewChat = async () => {
|
||||
if (currentRequestId !== null) {
|
||||
await cancelOllamaRequest(localStorage.token, currentRequestId);
|
||||
currentRequestId = null;
|
||||
}
|
||||
window.history.replaceState(history.state, '', `/`);
|
||||
await chatId.set('');
|
||||
|
||||
@@ -251,6 +247,39 @@
|
||||
}
|
||||
};
|
||||
|
||||
const chatCompletedHandler = async (modelId, messages) => {
|
||||
await mermaid.run({
|
||||
querySelector: '.mermaid'
|
||||
});
|
||||
|
||||
const res = await chatCompleted(localStorage.token, {
|
||||
model: modelId,
|
||||
messages: messages.map((m) => ({
|
||||
id: m.id,
|
||||
role: m.role,
|
||||
content: m.content,
|
||||
timestamp: m.timestamp
|
||||
})),
|
||||
chat_id: $chatId
|
||||
}).catch((error) => {
|
||||
console.error(error);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (res !== null) {
|
||||
// Update chat history with the new messages
|
||||
for (const message of res.messages) {
|
||||
history.messages[message.id] = {
|
||||
...history.messages[message.id],
|
||||
...(history.messages[message.id].content !== message.content
|
||||
? { originalContent: history.messages[message.id].content }
|
||||
: {}),
|
||||
...message
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
//////////////////////////
|
||||
// Ollama functions
|
||||
//////////////////////////
|
||||
@@ -616,39 +645,11 @@
|
||||
|
||||
if (stopResponseFlag) {
|
||||
controller.abort('User: Stop Response');
|
||||
await cancelOllamaRequest(localStorage.token, currentRequestId);
|
||||
} else {
|
||||
const messages = createMessagesList(responseMessageId);
|
||||
const res = await chatCompleted(localStorage.token, {
|
||||
model: model,
|
||||
messages: messages.map((m) => ({
|
||||
id: m.id,
|
||||
role: m.role,
|
||||
content: m.content,
|
||||
timestamp: m.timestamp
|
||||
})),
|
||||
chat_id: $chatId
|
||||
}).catch((error) => {
|
||||
console.error(error);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (res !== null) {
|
||||
// Update chat history with the new messages
|
||||
for (const message of res.messages) {
|
||||
history.messages[message.id] = {
|
||||
...history.messages[message.id],
|
||||
...(history.messages[message.id].content !== message.content
|
||||
? { originalContent: history.messages[message.id].content }
|
||||
: {}),
|
||||
...message
|
||||
};
|
||||
}
|
||||
}
|
||||
await chatCompletedHandler(model, messages);
|
||||
}
|
||||
|
||||
currentRequestId = null;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -669,63 +670,58 @@
|
||||
throw data;
|
||||
}
|
||||
|
||||
if ('id' in data) {
|
||||
console.log(data);
|
||||
currentRequestId = data.id;
|
||||
} else {
|
||||
if (data.done == false) {
|
||||
if (responseMessage.content == '' && data.message.content == '\n') {
|
||||
continue;
|
||||
} else {
|
||||
responseMessage.content += data.message.content;
|
||||
messages = messages;
|
||||
}
|
||||
if (data.done == false) {
|
||||
if (responseMessage.content == '' && data.message.content == '\n') {
|
||||
continue;
|
||||
} else {
|
||||
responseMessage.done = true;
|
||||
|
||||
if (responseMessage.content == '') {
|
||||
responseMessage.error = {
|
||||
code: 400,
|
||||
content: `Oops! No text generated from Ollama, Please try again.`
|
||||
};
|
||||
}
|
||||
|
||||
responseMessage.context = data.context ?? null;
|
||||
responseMessage.info = {
|
||||
total_duration: data.total_duration,
|
||||
load_duration: data.load_duration,
|
||||
sample_count: data.sample_count,
|
||||
sample_duration: data.sample_duration,
|
||||
prompt_eval_count: data.prompt_eval_count,
|
||||
prompt_eval_duration: data.prompt_eval_duration,
|
||||
eval_count: data.eval_count,
|
||||
eval_duration: data.eval_duration
|
||||
};
|
||||
responseMessage.content += data.message.content;
|
||||
messages = messages;
|
||||
}
|
||||
} else {
|
||||
responseMessage.done = true;
|
||||
|
||||
if ($settings.notificationEnabled && !document.hasFocus()) {
|
||||
const notification = new Notification(
|
||||
selectedModelfile
|
||||
? `${
|
||||
selectedModelfile.title.charAt(0).toUpperCase() +
|
||||
selectedModelfile.title.slice(1)
|
||||
}`
|
||||
: `${model}`,
|
||||
{
|
||||
body: responseMessage.content,
|
||||
icon: selectedModelfile?.imageUrl ?? `${WEBUI_BASE_URL}/static/favicon.png`
|
||||
}
|
||||
);
|
||||
}
|
||||
if (responseMessage.content == '') {
|
||||
responseMessage.error = {
|
||||
code: 400,
|
||||
content: `Oops! No text generated from Ollama, Please try again.`
|
||||
};
|
||||
}
|
||||
|
||||
if ($settings.responseAutoCopy) {
|
||||
copyToClipboard(responseMessage.content);
|
||||
}
|
||||
responseMessage.context = data.context ?? null;
|
||||
responseMessage.info = {
|
||||
total_duration: data.total_duration,
|
||||
load_duration: data.load_duration,
|
||||
sample_count: data.sample_count,
|
||||
sample_duration: data.sample_duration,
|
||||
prompt_eval_count: data.prompt_eval_count,
|
||||
prompt_eval_duration: data.prompt_eval_duration,
|
||||
eval_count: data.eval_count,
|
||||
eval_duration: data.eval_duration
|
||||
};
|
||||
messages = messages;
|
||||
|
||||
if ($settings.responseAutoPlayback) {
|
||||
await tick();
|
||||
document.getElementById(`speak-button-${responseMessage.id}`)?.click();
|
||||
}
|
||||
if ($settings.notificationEnabled && !document.hasFocus()) {
|
||||
const notification = new Notification(
|
||||
selectedModelfile
|
||||
? `${
|
||||
selectedModelfile.title.charAt(0).toUpperCase() +
|
||||
selectedModelfile.title.slice(1)
|
||||
}`
|
||||
: `${model}`,
|
||||
{
|
||||
body: responseMessage.content,
|
||||
icon: selectedModelfile?.imageUrl ?? `${WEBUI_BASE_URL}/static/favicon.png`
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if ($settings.responseAutoCopy) {
|
||||
copyToClipboard(responseMessage.content);
|
||||
}
|
||||
|
||||
if ($settings.responseAutoPlayback) {
|
||||
await tick();
|
||||
document.getElementById(`speak-button-${responseMessage.id}`)?.click();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -906,32 +902,7 @@
|
||||
} else {
|
||||
const messages = createMessagesList(responseMessageId);
|
||||
|
||||
const res = await chatCompleted(localStorage.token, {
|
||||
model: model.id,
|
||||
messages: messages.map((m) => ({
|
||||
id: m.id,
|
||||
role: m.role,
|
||||
content: m.content,
|
||||
timestamp: m.timestamp
|
||||
})),
|
||||
chat_id: $chatId
|
||||
}).catch((error) => {
|
||||
console.error(error);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (res !== null) {
|
||||
// Update chat history with the new messages
|
||||
for (const message of res.messages) {
|
||||
history.messages[message.id] = {
|
||||
...history.messages[message.id],
|
||||
...(history.messages[message.id].content !== message.content
|
||||
? { originalContent: history.messages[message.id].content }
|
||||
: {}),
|
||||
...message
|
||||
};
|
||||
}
|
||||
}
|
||||
await chatCompletedHandler(model.id, messages);
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
@@ -810,10 +810,7 @@
|
||||
? $i18n.t('Listening...')
|
||||
: $i18n.t('Send a Message')}
|
||||
bind:value={prompt}
|
||||
on:keypress={(e) => {}}
|
||||
on:keydown={async (e) => {
|
||||
// Check if the device is not a mobile device or if it is a mobile device, check if it is not a touch device
|
||||
// This is to prevent the Enter key from submitting the prompt on mobile devices
|
||||
on:keypress={(e) => {
|
||||
if (
|
||||
!$mobile ||
|
||||
!(
|
||||
@@ -822,22 +819,18 @@
|
||||
navigator.msMaxTouchPoints > 0
|
||||
)
|
||||
) {
|
||||
// Check if Enter is pressed
|
||||
// Check if Shift key is not pressed
|
||||
// Prevent Enter key from creating a new line
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
if (e.key === 'Enter' && !e.shiftKey && prompt !== '') {
|
||||
// Submit the prompt when Enter key is pressed
|
||||
if (prompt !== '' && e.key === 'Enter' && !e.shiftKey) {
|
||||
submitPrompt(prompt, user);
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.key === 'Enter' && e.shiftKey && prompt !== '') {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
}}
|
||||
on:keydown={async (e) => {
|
||||
const isCtrlPressed = e.ctrlKey || e.metaKey; // metaKey is for Cmd key on Mac
|
||||
|
||||
// Check if Ctrl + R is pressed
|
||||
@@ -898,7 +891,9 @@
|
||||
...document.getElementsByClassName('selected-command-option-button')
|
||||
]?.at(-1);
|
||||
|
||||
if (commandOptionButton) {
|
||||
if (e.shiftKey) {
|
||||
prompt = `${prompt}\n`;
|
||||
} else if (commandOptionButton) {
|
||||
commandOptionButton?.click();
|
||||
} else {
|
||||
document.getElementById('send-message-button')?.click();
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
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 { getChatList, updateChatById } from '$lib/apis/chats';
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import tippy from 'tippy.js';
|
||||
import auto_render from 'katex/dist/contrib/auto-render.mjs';
|
||||
import 'katex/dist/katex.min.css';
|
||||
import mermaid from 'mermaid';
|
||||
|
||||
import { fade } from 'svelte/transition';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
@@ -340,9 +341,24 @@
|
||||
generatingImage = false;
|
||||
};
|
||||
|
||||
$: if (!edit) {
|
||||
(async () => {
|
||||
await tick();
|
||||
renderStyling();
|
||||
|
||||
await mermaid.run({
|
||||
querySelector: '.mermaid'
|
||||
});
|
||||
})();
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
await tick();
|
||||
renderStyling();
|
||||
|
||||
await mermaid.run({
|
||||
querySelector: '.mermaid'
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -458,11 +474,15 @@
|
||||
<!-- 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'}
|
||||
<CodeBlock
|
||||
id={`${message.id}-${tokenIdx}`}
|
||||
lang={token?.lang ?? ''}
|
||||
code={revertSanitizedResponseContent(token?.text ?? '')}
|
||||
/>
|
||||
{#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,
|
||||
|
||||
@@ -196,7 +196,7 @@
|
||||
<div class=" mt-2 mb-1 flex justify-end space-x-1.5 text-sm font-medium">
|
||||
<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={() => {
|
||||
cancelEditMessage();
|
||||
}}
|
||||
@@ -206,7 +206,7 @@
|
||||
|
||||
<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={() => {
|
||||
editMessageConfirmHandler();
|
||||
}}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
import Check from '$lib/components/icons/Check.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 { toast } from 'svelte-sonner';
|
||||
@@ -72,10 +72,12 @@
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await pullModel(localStorage.token, sanitizedModelTag, '0').catch((error) => {
|
||||
toast.error(error);
|
||||
return null;
|
||||
});
|
||||
const [res, controller] = await pullModel(localStorage.token, sanitizedModelTag, '0').catch(
|
||||
(error) => {
|
||||
toast.error(error);
|
||||
return null;
|
||||
}
|
||||
);
|
||||
|
||||
if (res) {
|
||||
const reader = res.body
|
||||
@@ -83,6 +85,16 @@
|
||||
.pipeThrough(splitStream('\n'))
|
||||
.getReader();
|
||||
|
||||
MODEL_DOWNLOAD_POOL.set({
|
||||
...$MODEL_DOWNLOAD_POOL,
|
||||
[sanitizedModelTag]: {
|
||||
...$MODEL_DOWNLOAD_POOL[sanitizedModelTag],
|
||||
abortController: controller,
|
||||
reader,
|
||||
done: false
|
||||
}
|
||||
});
|
||||
|
||||
while (true) {
|
||||
try {
|
||||
const { value, done } = await reader.read();
|
||||
@@ -101,19 +113,6 @@
|
||||
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.digest) {
|
||||
let downloadProgress = 0;
|
||||
@@ -181,11 +180,12 @@
|
||||
});
|
||||
|
||||
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) {
|
||||
await reader.cancel();
|
||||
|
||||
await cancelOllamaRequest(localStorage.token, requestId);
|
||||
delete $MODEL_DOWNLOAD_POOL[model];
|
||||
MODEL_DOWNLOAD_POOL.set({
|
||||
...$MODEL_DOWNLOAD_POOL
|
||||
|
||||
@@ -20,6 +20,9 @@
|
||||
tfs_z: '',
|
||||
num_ctx: '',
|
||||
max_tokens: '',
|
||||
use_mmap: null,
|
||||
use_mlock: null,
|
||||
num_thread: null,
|
||||
template: null
|
||||
};
|
||||
|
||||
@@ -559,6 +562,7 @@
|
||||
</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('Max Tokens (num_predict)')}</div>
|
||||
@@ -604,6 +608,93 @@
|
||||
</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('use_mmap (Ollama)')}</div>
|
||||
|
||||
<button
|
||||
class="p-1 px-3 text-xs flex rounded transition"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
params.use_mmap = (params?.use_mmap ?? null) === null ? true : null;
|
||||
}}
|
||||
>
|
||||
{#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 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('use_mlock (Ollama)')}</div>
|
||||
|
||||
<button
|
||||
class="p-1 px-3 text-xs flex rounded transition"
|
||||
type="button"
|
||||
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>
|
||||
{/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>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { user, settings } from '$lib/stores';
|
||||
import { createEventDispatcher, onMount, getContext } from 'svelte';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import Switch from '$lib/components/common/Switch.svelte';
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
@@ -13,6 +14,7 @@
|
||||
|
||||
let OpenAIUrl = '';
|
||||
let OpenAIKey = '';
|
||||
let OpenAISpeaker = '';
|
||||
|
||||
let STTEngines = ['', 'openai'];
|
||||
let STTEngine = '';
|
||||
@@ -20,6 +22,7 @@
|
||||
let conversationMode = false;
|
||||
let speechAutoSend = false;
|
||||
let responseAutoPlayback = false;
|
||||
let nonLocalVoices = false;
|
||||
|
||||
let TTSEngines = ['', 'openai'];
|
||||
let TTSEngine = '';
|
||||
@@ -86,14 +89,14 @@
|
||||
url: OpenAIUrl,
|
||||
key: OpenAIKey,
|
||||
model: model,
|
||||
speaker: speaker
|
||||
speaker: OpenAISpeaker
|
||||
});
|
||||
|
||||
if (res) {
|
||||
OpenAIUrl = res.OPENAI_API_BASE_URL;
|
||||
OpenAIKey = res.OPENAI_API_KEY;
|
||||
model = res.OPENAI_API_MODEL;
|
||||
speaker = res.OPENAI_API_VOICE;
|
||||
OpenAISpeaker = res.OPENAI_API_VOICE;
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -105,6 +108,7 @@
|
||||
|
||||
STTEngine = $settings?.audio?.STTEngine ?? '';
|
||||
TTSEngine = $settings?.audio?.TTSEngine ?? '';
|
||||
nonLocalVoices = $settings.audio?.nonLocalVoices ?? false;
|
||||
speaker = $settings?.audio?.speaker ?? '';
|
||||
model = $settings?.audio?.model ?? '';
|
||||
|
||||
@@ -122,7 +126,10 @@
|
||||
OpenAIUrl = res.OPENAI_API_BASE_URL;
|
||||
OpenAIKey = res.OPENAI_API_KEY;
|
||||
model = res.OPENAI_API_MODEL;
|
||||
speaker = res.OPENAI_API_VOICE;
|
||||
OpenAISpeaker = res.OPENAI_API_VOICE;
|
||||
if (TTSEngine === 'openai') {
|
||||
speaker = OpenAISpeaker;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -138,8 +145,14 @@
|
||||
audio: {
|
||||
STTEngine: STTEngine !== '' ? STTEngine : undefined,
|
||||
TTSEngine: TTSEngine !== '' ? TTSEngine : undefined,
|
||||
speaker: speaker !== '' ? speaker : undefined,
|
||||
model: model !== '' ? model : undefined
|
||||
speaker:
|
||||
(TTSEngine === 'openai' ? OpenAISpeaker : speaker) !== ''
|
||||
? TTSEngine === 'openai'
|
||||
? OpenAISpeaker
|
||||
: speaker
|
||||
: undefined,
|
||||
model: model !== '' ? model : undefined,
|
||||
nonLocalVoices: nonLocalVoices
|
||||
}
|
||||
});
|
||||
dispatch('save');
|
||||
@@ -227,7 +240,7 @@
|
||||
on:change={(e) => {
|
||||
if (e.target.value === 'openai') {
|
||||
getOpenAIVoices();
|
||||
speaker = 'alloy';
|
||||
OpenAISpeaker = 'alloy';
|
||||
model = 'tts-1';
|
||||
} else {
|
||||
getWebAPIVoices();
|
||||
@@ -290,16 +303,27 @@
|
||||
<select
|
||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
bind:value={speaker}
|
||||
placeholder="Select a voice"
|
||||
>
|
||||
<option value="" selected>{$i18n.t('Default')}</option>
|
||||
{#each voices.filter((v) => v.localService === true) as voice}
|
||||
<option value={voice.name} class="bg-gray-100 dark:bg-gray-700">{voice.name}</option
|
||||
<option value="" selected={speaker !== ''}>{$i18n.t('Default')}</option>
|
||||
{#each voices.filter((v) => nonLocalVoices || v.localService === true) as voice}
|
||||
<option
|
||||
value={voice.name}
|
||||
class="bg-gray-100 dark:bg-gray-700"
|
||||
selected={speaker === voice.name}>{voice.name}</option
|
||||
>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center justify-between mb-1">
|
||||
<div class="text-sm">
|
||||
{$i18n.t('Allow non-local voices')}
|
||||
</div>
|
||||
|
||||
<div class="mt-1">
|
||||
<Switch bind:state={nonLocalVoices} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{:else if TTSEngine === 'openai'}
|
||||
<div>
|
||||
@@ -309,7 +333,7 @@
|
||||
<input
|
||||
list="voice-list"
|
||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
bind:value={speaker}
|
||||
bind:value={OpenAISpeaker}
|
||||
placeholder="Select a voice"
|
||||
/>
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { models, user } from '$lib/stores';
|
||||
import { createEventDispatcher, onMount, getContext } from 'svelte';
|
||||
import { createEventDispatcher, onMount, getContext, tick } from 'svelte';
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
import {
|
||||
@@ -74,23 +74,48 @@
|
||||
};
|
||||
|
||||
const updateOpenAIHandler = async () => {
|
||||
// Check if API KEYS length is same than API URLS length
|
||||
if (OPENAI_API_KEYS.length !== OPENAI_API_BASE_URLS.length) {
|
||||
// if there are more keys than urls, remove the extra keys
|
||||
if (OPENAI_API_KEYS.length > OPENAI_API_BASE_URLS.length) {
|
||||
OPENAI_API_KEYS = OPENAI_API_KEYS.slice(0, OPENAI_API_BASE_URLS.length);
|
||||
}
|
||||
|
||||
// if there are more urls than keys, add empty keys
|
||||
if (OPENAI_API_KEYS.length < OPENAI_API_BASE_URLS.length) {
|
||||
const diff = OPENAI_API_BASE_URLS.length - OPENAI_API_KEYS.length;
|
||||
for (let i = 0; i < diff; i++) {
|
||||
OPENAI_API_KEYS.push('');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
OPENAI_API_BASE_URLS = await updateOpenAIUrls(localStorage.token, OPENAI_API_BASE_URLS);
|
||||
OPENAI_API_KEYS = await updateOpenAIKeys(localStorage.token, OPENAI_API_KEYS);
|
||||
|
||||
await models.set(await getModels());
|
||||
};
|
||||
|
||||
const updateOllamaUrlsHandler = async () => {
|
||||
OLLAMA_BASE_URLS = await updateOllamaUrls(localStorage.token, OLLAMA_BASE_URLS);
|
||||
OLLAMA_BASE_URLS = OLLAMA_BASE_URLS.filter((url) => url !== '');
|
||||
console.log(OLLAMA_BASE_URLS);
|
||||
|
||||
const ollamaVersion = await getOllamaVersion(localStorage.token).catch((error) => {
|
||||
toast.error(error);
|
||||
return null;
|
||||
});
|
||||
if (OLLAMA_BASE_URLS.length === 0) {
|
||||
ENABLE_OLLAMA_API = false;
|
||||
await updateOllamaConfig(localStorage.token, ENABLE_OLLAMA_API);
|
||||
|
||||
if (ollamaVersion) {
|
||||
toast.success($i18n.t('Server connection verified'));
|
||||
await models.set(await getModels());
|
||||
toast.info($i18n.t('Ollama API disabled'));
|
||||
} else {
|
||||
OLLAMA_BASE_URLS = await updateOllamaUrls(localStorage.token, OLLAMA_BASE_URLS);
|
||||
|
||||
const ollamaVersion = await getOllamaVersion(localStorage.token).catch((error) => {
|
||||
toast.error(error);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (ollamaVersion) {
|
||||
toast.success($i18n.t('Server connection verified'));
|
||||
await models.set(await getModels());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -286,6 +311,10 @@
|
||||
bind:state={ENABLE_OLLAMA_API}
|
||||
on:change={async () => {
|
||||
updateOllamaConfig(localStorage.token, ENABLE_OLLAMA_API);
|
||||
|
||||
if (OLLAMA_BASE_URLS.length === 0) {
|
||||
OLLAMA_BASE_URLS = [''];
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
getOllamaUrls,
|
||||
getOllamaVersion,
|
||||
pullModel,
|
||||
cancelOllamaRequest,
|
||||
uploadModel
|
||||
uploadModel,
|
||||
getOllamaConfig
|
||||
} from '$lib/apis/ollama';
|
||||
|
||||
import { WEBUI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants';
|
||||
@@ -28,6 +28,8 @@
|
||||
|
||||
// Models
|
||||
|
||||
let ollamaEnabled = null;
|
||||
|
||||
let OLLAMA_URLS = [];
|
||||
let selectedOllamaUrlIdx: string | null = null;
|
||||
|
||||
@@ -67,12 +69,14 @@
|
||||
console.log(model);
|
||||
|
||||
updateModelId = model.id;
|
||||
const res = await pullModel(localStorage.token, model.id, selectedOllamaUrlIdx).catch(
|
||||
(error) => {
|
||||
toast.error(error);
|
||||
return null;
|
||||
}
|
||||
);
|
||||
const [res, controller] = await pullModel(
|
||||
localStorage.token,
|
||||
model.id,
|
||||
selectedOllamaUrlIdx
|
||||
).catch((error) => {
|
||||
toast.error(error);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (res) {
|
||||
const reader = res.body
|
||||
@@ -141,10 +145,12 @@
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await pullModel(localStorage.token, sanitizedModelTag, '0').catch((error) => {
|
||||
toast.error(error);
|
||||
return null;
|
||||
});
|
||||
const [res, controller] = await pullModel(localStorage.token, sanitizedModelTag, '0').catch(
|
||||
(error) => {
|
||||
toast.error(error);
|
||||
return null;
|
||||
}
|
||||
);
|
||||
|
||||
if (res) {
|
||||
const reader = res.body
|
||||
@@ -152,6 +158,16 @@
|
||||
.pipeThrough(splitStream('\n'))
|
||||
.getReader();
|
||||
|
||||
MODEL_DOWNLOAD_POOL.set({
|
||||
...$MODEL_DOWNLOAD_POOL,
|
||||
[sanitizedModelTag]: {
|
||||
...$MODEL_DOWNLOAD_POOL[sanitizedModelTag],
|
||||
abortController: controller,
|
||||
reader,
|
||||
done: false
|
||||
}
|
||||
});
|
||||
|
||||
while (true) {
|
||||
try {
|
||||
const { value, done } = await reader.read();
|
||||
@@ -170,19 +186,6 @@
|
||||
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.digest) {
|
||||
let downloadProgress = 0;
|
||||
@@ -416,11 +419,12 @@
|
||||
};
|
||||
|
||||
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) {
|
||||
await reader.cancel();
|
||||
|
||||
await cancelOllamaRequest(localStorage.token, requestId);
|
||||
delete $MODEL_DOWNLOAD_POOL[model];
|
||||
MODEL_DOWNLOAD_POOL.set({
|
||||
...$MODEL_DOWNLOAD_POOL
|
||||
@@ -431,381 +435,63 @@
|
||||
};
|
||||
|
||||
onMount(async () => {
|
||||
await Promise.all([
|
||||
(async () => {
|
||||
OLLAMA_URLS = await getOllamaUrls(localStorage.token).catch((error) => {
|
||||
toast.error(error);
|
||||
return [];
|
||||
});
|
||||
const ollamaConfig = await getOllamaConfig(localStorage.token);
|
||||
|
||||
if (OLLAMA_URLS.length > 0) {
|
||||
selectedOllamaUrlIdx = 0;
|
||||
}
|
||||
})(),
|
||||
(async () => {
|
||||
ollamaVersion = await getOllamaVersion(localStorage.token).catch((error) => false);
|
||||
})()
|
||||
]);
|
||||
if (ollamaConfig.ENABLE_OLLAMA_API) {
|
||||
ollamaEnabled = true;
|
||||
|
||||
await Promise.all([
|
||||
(async () => {
|
||||
OLLAMA_URLS = await getOllamaUrls(localStorage.token).catch((error) => {
|
||||
toast.error(error);
|
||||
return [];
|
||||
});
|
||||
|
||||
if (OLLAMA_URLS.length > 0) {
|
||||
selectedOllamaUrlIdx = 0;
|
||||
}
|
||||
})(),
|
||||
(async () => {
|
||||
ollamaVersion = await getOllamaVersion(localStorage.token).catch((error) => false);
|
||||
})()
|
||||
]);
|
||||
} else {
|
||||
ollamaEnabled = false;
|
||||
toast.error('Ollama API is disabled');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col h-full justify-between text-sm">
|
||||
<div class=" space-y-3 pr-1.5 overflow-y-scroll h-[24rem]">
|
||||
{#if ollamaVersion !== null}
|
||||
<div class="space-y-2 pr-1.5">
|
||||
<div class="text-sm font-medium">{$i18n.t('Manage Ollama Models')}</div>
|
||||
{#if ollamaEnabled}
|
||||
{#if ollamaVersion !== null}
|
||||
<div class="space-y-2 pr-1.5">
|
||||
<div class="text-sm font-medium">{$i18n.t('Manage Ollama Models')}</div>
|
||||
|
||||
{#if OLLAMA_URLS.length > 0}
|
||||
<div class="flex gap-2">
|
||||
<div class="flex-1 pb-1">
|
||||
<select
|
||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
bind:value={selectedOllamaUrlIdx}
|
||||
placeholder={$i18n.t('Select an Ollama instance')}
|
||||
>
|
||||
{#each OLLAMA_URLS as url, idx}
|
||||
<option value={idx} class="bg-gray-100 dark:bg-gray-700">{url}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="flex w-full justify-end">
|
||||
<Tooltip content="Update All Models" placement="top">
|
||||
<button
|
||||
class="p-2.5 flex gap-2 items-center bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition"
|
||||
on:click={() => {
|
||||
updateModelsHandler();
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
d="M7 1a.75.75 0 0 1 .75.75V6h-1.5V1.75A.75.75 0 0 1 7 1ZM6.25 6v2.94L5.03 7.72a.75.75 0 0 0-1.06 1.06l2.5 2.5a.75.75 0 0 0 1.06 0l2.5-2.5a.75.75 0 1 0-1.06-1.06L7.75 8.94V6H10a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h2.25Z"
|
||||
/>
|
||||
<path
|
||||
d="M4.268 14A2 2 0 0 0 6 15h6a2 2 0 0 0 2-2v-3a2 2 0 0 0-1-1.732V11a3 3 0 0 1-3 3H4.268Z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if updateModelId}
|
||||
Updating "{updateModelId}" {updateProgress ? `(${updateProgress}%)` : ''}
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<div class="space-y-2">
|
||||
<div>
|
||||
<div class=" mb-2 text-sm font-medium">{$i18n.t('Pull a model from Ollama.com')}</div>
|
||||
<div class="flex w-full">
|
||||
<div class="flex-1 mr-2">
|
||||
<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('Enter model tag (e.g. {{modelTag}})', {
|
||||
modelTag: 'mistral:7b'
|
||||
})}
|
||||
bind:value={modelTag}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
class="px-2.5 bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition"
|
||||
on:click={() => {
|
||||
pullModelHandler();
|
||||
}}
|
||||
disabled={modelTransferring}
|
||||
>
|
||||
{#if modelTransferring}
|
||||
<div class="self-center">
|
||||
<svg
|
||||
class=" w-4 h-4"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<style>
|
||||
.spinner_ajPY {
|
||||
transform-origin: center;
|
||||
animation: spinner_AtaB 0.75s infinite linear;
|
||||
}
|
||||
|
||||
@keyframes spinner_AtaB {
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<path
|
||||
d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
|
||||
opacity=".25"
|
||||
/>
|
||||
<path
|
||||
d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
|
||||
class="spinner_ajPY"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
{:else}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
d="M8.75 2.75a.75.75 0 0 0-1.5 0v5.69L5.03 6.22a.75.75 0 0 0-1.06 1.06l3.5 3.5a.75.75 0 0 0 1.06 0l3.5-3.5a.75.75 0 0 0-1.06-1.06L8.75 8.44V2.75Z"
|
||||
/>
|
||||
<path
|
||||
d="M3.5 9.75a.75.75 0 0 0-1.5 0v1.5A2.75 2.75 0 0 0 4.75 14h6.5A2.75 2.75 0 0 0 14 11.25v-1.5a.75.75 0 0 0-1.5 0v1.5c0 .69-.56 1.25-1.25 1.25h-6.5c-.69 0-1.25-.56-1.25-1.25v-1.5Z"
|
||||
/>
|
||||
</svg>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="mt-2 mb-1 text-xs text-gray-400 dark:text-gray-500">
|
||||
{$i18n.t('To access the available model names for downloading,')}
|
||||
<a
|
||||
class=" text-gray-500 dark:text-gray-300 font-medium underline"
|
||||
href="https://ollama.com/library"
|
||||
target="_blank">{$i18n.t('click here.')}</a
|
||||
>
|
||||
</div>
|
||||
|
||||
{#if Object.keys($MODEL_DOWNLOAD_POOL).length > 0}
|
||||
{#each Object.keys($MODEL_DOWNLOAD_POOL) as model}
|
||||
{#if 'pullProgress' in $MODEL_DOWNLOAD_POOL[model]}
|
||||
<div class="flex flex-col">
|
||||
<div class="font-medium mb-1">{model}</div>
|
||||
<div class="">
|
||||
<div class="flex flex-row justify-between space-x-4 pr-2">
|
||||
<div class=" flex-1">
|
||||
<div
|
||||
class="dark:bg-gray-600 bg-gray-500 text-xs font-medium text-gray-100 text-center p-0.5 leading-none rounded-full"
|
||||
style="width: {Math.max(
|
||||
15,
|
||||
$MODEL_DOWNLOAD_POOL[model].pullProgress ?? 0
|
||||
)}%"
|
||||
>
|
||||
{$MODEL_DOWNLOAD_POOL[model].pullProgress ?? 0}%
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Tooltip content={$i18n.t('Cancel')}>
|
||||
<button
|
||||
class="text-gray-800 dark:text-gray-100"
|
||||
on:click={() => {
|
||||
cancelModelPullHandler(model);
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
class="w-4 h-4 text-gray-800 dark:text-white"
|
||||
aria-hidden="true"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M6 18 17.94 6M18 18 6.06 6"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
{#if 'digest' in $MODEL_DOWNLOAD_POOL[model]}
|
||||
<div class="mt-1 text-xs dark:text-gray-500" style="font-size: 0.5rem;">
|
||||
{$MODEL_DOWNLOAD_POOL[model].digest}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class=" mb-2 text-sm font-medium">{$i18n.t('Delete a model')}</div>
|
||||
<div class="flex w-full">
|
||||
<div class="flex-1 mr-2">
|
||||
{#if OLLAMA_URLS.length > 0}
|
||||
<div class="flex gap-2">
|
||||
<div class="flex-1 pb-1">
|
||||
<select
|
||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
bind:value={deleteModelTag}
|
||||
placeholder={$i18n.t('Select a model')}
|
||||
bind:value={selectedOllamaUrlIdx}
|
||||
placeholder={$i18n.t('Select an Ollama instance')}
|
||||
>
|
||||
{#if !deleteModelTag}
|
||||
<option value="" disabled selected>{$i18n.t('Select a model')}</option>
|
||||
{/if}
|
||||
{#each $models.filter((m) => !(m?.preset ?? false) && m.owned_by === 'ollama' && (selectedOllamaUrlIdx === null ? true : (m?.ollama?.urls ?? []).includes(selectedOllamaUrlIdx))) as model}
|
||||
<option value={model.name} class="bg-gray-100 dark:bg-gray-700"
|
||||
>{model.name +
|
||||
' (' +
|
||||
(model.ollama.size / 1024 ** 3).toFixed(1) +
|
||||
' GB)'}</option
|
||||
>
|
||||
{#each OLLAMA_URLS as url, idx}
|
||||
<option value={idx} class="bg-gray-100 dark:bg-gray-700">{url}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
<button
|
||||
class="px-2.5 bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition"
|
||||
on:click={() => {
|
||||
deleteModelHandler();
|
||||
}}
|
||||
>
|
||||
<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="M5 3.25V4H2.75a.75.75 0 0 0 0 1.5h.3l.815 8.15A1.5 1.5 0 0 0 5.357 15h5.285a1.5 1.5 0 0 0 1.493-1.35l.815-8.15h.3a.75.75 0 0 0 0-1.5H11v-.75A2.25 2.25 0 0 0 8.75 1h-1.5A2.25 2.25 0 0 0 5 3.25Zm2.25-.75a.75.75 0 0 0-.75.75V4h3v-.75a.75.75 0 0 0-.75-.75h-1.5ZM6.05 6a.75.75 0 0 1 .787.713l.275 5.5a.75.75 0 0 1-1.498.075l-.275-5.5A.75.75 0 0 1 6.05 6Zm3.9 0a.75.75 0 0 1 .712.787l-.275 5.5a.75.75 0 0 1-1.498-.075l.275-5.5a.75.75 0 0 1 .786-.711Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pt-1">
|
||||
<div class="flex justify-between items-center text-xs">
|
||||
<div class=" text-sm font-medium">{$i18n.t('Experimental')}</div>
|
||||
<button
|
||||
class=" text-xs font-medium text-gray-500"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
showExperimentalOllama = !showExperimentalOllama;
|
||||
}}>{showExperimentalOllama ? $i18n.t('Hide') : $i18n.t('Show')}</button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if showExperimentalOllama}
|
||||
<form
|
||||
on:submit|preventDefault={() => {
|
||||
uploadModelHandler();
|
||||
}}
|
||||
>
|
||||
<div class=" mb-2 flex w-full justify-between">
|
||||
<div class=" text-sm font-medium">{$i18n.t('Upload a GGUF model')}</div>
|
||||
|
||||
<button
|
||||
class="p-1 px-3 text-xs flex rounded transition"
|
||||
on:click={() => {
|
||||
if (modelUploadMode === 'file') {
|
||||
modelUploadMode = 'url';
|
||||
} else {
|
||||
modelUploadMode = 'file';
|
||||
}
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
{#if modelUploadMode === 'file'}
|
||||
<span class="ml-2 self-center">{$i18n.t('File Mode')}</span>
|
||||
{:else}
|
||||
<span class="ml-2 self-center">{$i18n.t('URL Mode')}</span>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex w-full mb-1.5">
|
||||
<div class="flex flex-col w-full">
|
||||
{#if modelUploadMode === 'file'}
|
||||
<div class="flex-1 {modelInputFile && modelInputFile.length > 0 ? 'mr-2' : ''}">
|
||||
<input
|
||||
id="model-upload-input"
|
||||
bind:this={modelUploadInputElement}
|
||||
type="file"
|
||||
bind:files={modelInputFile}
|
||||
on:change={() => {
|
||||
console.log(modelInputFile);
|
||||
}}
|
||||
accept=".gguf,.safetensors"
|
||||
required
|
||||
hidden
|
||||
/>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="w-full rounded-lg text-left py-2 px-4 bg-white dark:text-gray-300 dark:bg-gray-850"
|
||||
on:click={() => {
|
||||
modelUploadInputElement.click();
|
||||
}}
|
||||
>
|
||||
{#if modelInputFile && modelInputFile.length > 0}
|
||||
{modelInputFile[0].name}
|
||||
{:else}
|
||||
{$i18n.t('Click here to select')}
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex-1 {modelFileUrl !== '' ? 'mr-2' : ''}">
|
||||
<input
|
||||
class="w-full rounded-lg text-left py-2 px-4 bg-white dark:text-gray-300 dark:bg-gray-850 outline-none {modelFileUrl !==
|
||||
''
|
||||
? 'mr-2'
|
||||
: ''}"
|
||||
type="url"
|
||||
required
|
||||
bind:value={modelFileUrl}
|
||||
placeholder={$i18n.t('Type Hugging Face Resolve (Download) URL')}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if (modelUploadMode === 'file' && modelInputFile && modelInputFile.length > 0) || (modelUploadMode === 'url' && modelFileUrl !== '')}
|
||||
<button
|
||||
class="px-2.5 bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg disabled:cursor-not-allowed transition"
|
||||
type="submit"
|
||||
disabled={modelTransferring}
|
||||
>
|
||||
{#if modelTransferring}
|
||||
<div class="self-center">
|
||||
<svg
|
||||
class=" w-4 h-4"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<style>
|
||||
.spinner_ajPY {
|
||||
transform-origin: center;
|
||||
animation: spinner_AtaB 0.75s infinite linear;
|
||||
}
|
||||
|
||||
@keyframes spinner_AtaB {
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<path
|
||||
d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
|
||||
opacity=".25"
|
||||
/>
|
||||
<path
|
||||
d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
|
||||
class="spinner_ajPY"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
{:else}
|
||||
<div>
|
||||
<div class="flex w-full justify-end">
|
||||
<Tooltip content="Update All Models" placement="top">
|
||||
<button
|
||||
class="p-2.5 flex gap-2 items-center bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition"
|
||||
on:click={() => {
|
||||
updateModelsHandler();
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
@@ -813,77 +499,416 @@
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
d="M7.25 10.25a.75.75 0 0 0 1.5 0V4.56l2.22 2.22a.75.75 0 1 0 1.06-1.06l-3.5-3.5a.75.75 0 0 0-1.06 0l-3.5 3.5a.75.75 0 0 0 1.06 1.06l2.22-2.22v5.69Z"
|
||||
d="M7 1a.75.75 0 0 1 .75.75V6h-1.5V1.75A.75.75 0 0 1 7 1ZM6.25 6v2.94L5.03 7.72a.75.75 0 0 0-1.06 1.06l2.5 2.5a.75.75 0 0 0 1.06 0l2.5-2.5a.75.75 0 1 0-1.06-1.06L7.75 8.94V6H10a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h2.25Z"
|
||||
/>
|
||||
<path
|
||||
d="M3.5 9.75a.75.75 0 0 0-1.5 0v1.5A2.75 2.75 0 0 0 4.75 14h6.5A2.75 2.75 0 0 0 14 11.25v-1.5a.75.75 0 0 0-1.5 0v1.5c0 .69-.56 1.25-1.25 1.25h-6.5c-.69 0-1.25-.56-1.25-1.25v-1.5Z"
|
||||
d="M4.268 14A2 2 0 0 0 6 15h6a2 2 0 0 0 2-2v-3a2 2 0 0 0-1-1.732V11a3 3 0 0 1-3 3H4.268Z"
|
||||
/>
|
||||
</svg>
|
||||
{/if}
|
||||
</button>
|
||||
{/if}
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if updateModelId}
|
||||
Updating "{updateModelId}" {updateProgress ? `(${updateProgress}%)` : ''}
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<div class="space-y-2">
|
||||
<div>
|
||||
<div class=" mb-2 text-sm font-medium">{$i18n.t('Pull a model from Ollama.com')}</div>
|
||||
<div class="flex w-full">
|
||||
<div class="flex-1 mr-2">
|
||||
<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('Enter model tag (e.g. {{modelTag}})', {
|
||||
modelTag: 'mistral:7b'
|
||||
})}
|
||||
bind:value={modelTag}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
class="px-2.5 bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition"
|
||||
on:click={() => {
|
||||
pullModelHandler();
|
||||
}}
|
||||
disabled={modelTransferring}
|
||||
>
|
||||
{#if modelTransferring}
|
||||
<div class="self-center">
|
||||
<svg
|
||||
class=" w-4 h-4"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<style>
|
||||
.spinner_ajPY {
|
||||
transform-origin: center;
|
||||
animation: spinner_AtaB 0.75s infinite linear;
|
||||
}
|
||||
|
||||
@keyframes spinner_AtaB {
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<path
|
||||
d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
|
||||
opacity=".25"
|
||||
/>
|
||||
<path
|
||||
d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
|
||||
class="spinner_ajPY"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
{:else}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
d="M8.75 2.75a.75.75 0 0 0-1.5 0v5.69L5.03 6.22a.75.75 0 0 0-1.06 1.06l3.5 3.5a.75.75 0 0 0 1.06 0l3.5-3.5a.75.75 0 0 0-1.06-1.06L8.75 8.44V2.75Z"
|
||||
/>
|
||||
<path
|
||||
d="M3.5 9.75a.75.75 0 0 0-1.5 0v1.5A2.75 2.75 0 0 0 4.75 14h6.5A2.75 2.75 0 0 0 14 11.25v-1.5a.75.75 0 0 0-1.5 0v1.5c0 .69-.56 1.25-1.25 1.25h-6.5c-.69 0-1.25-.56-1.25-1.25v-1.5Z"
|
||||
/>
|
||||
</svg>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{#if (modelUploadMode === 'file' && modelInputFile && modelInputFile.length > 0) || (modelUploadMode === 'url' && modelFileUrl !== '')}
|
||||
<div>
|
||||
<div>
|
||||
<div class=" my-2.5 text-sm font-medium">{$i18n.t('Modelfile Content')}</div>
|
||||
<textarea
|
||||
bind:value={modelFileContent}
|
||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-100 dark:text-gray-100 dark:bg-gray-850 outline-none resize-none"
|
||||
rows="6"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<div class=" mt-1 text-xs text-gray-400 dark:text-gray-500">
|
||||
{$i18n.t('To access the GGUF models available for downloading,')}
|
||||
<div class="mt-2 mb-1 text-xs text-gray-400 dark:text-gray-500">
|
||||
{$i18n.t('To access the available model names for downloading,')}
|
||||
<a
|
||||
class=" text-gray-500 dark:text-gray-300 font-medium underline"
|
||||
href="https://huggingface.co/models?search=gguf"
|
||||
href="https://ollama.com/library"
|
||||
target="_blank">{$i18n.t('click here.')}</a
|
||||
>
|
||||
</div>
|
||||
|
||||
{#if uploadMessage}
|
||||
<div class="mt-2">
|
||||
<div class=" mb-2 text-xs">{$i18n.t('Upload Progress')}</div>
|
||||
{#if Object.keys($MODEL_DOWNLOAD_POOL).length > 0}
|
||||
{#each Object.keys($MODEL_DOWNLOAD_POOL) as model}
|
||||
{#if 'pullProgress' in $MODEL_DOWNLOAD_POOL[model]}
|
||||
<div class="flex flex-col">
|
||||
<div class="font-medium mb-1">{model}</div>
|
||||
<div class="">
|
||||
<div class="flex flex-row justify-between space-x-4 pr-2">
|
||||
<div class=" flex-1">
|
||||
<div
|
||||
class="dark:bg-gray-600 bg-gray-500 text-xs font-medium text-gray-100 text-center p-0.5 leading-none rounded-full"
|
||||
style="width: {Math.max(
|
||||
15,
|
||||
$MODEL_DOWNLOAD_POOL[model].pullProgress ?? 0
|
||||
)}%"
|
||||
>
|
||||
{$MODEL_DOWNLOAD_POOL[model].pullProgress ?? 0}%
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="w-full rounded-full dark:bg-gray-800">
|
||||
<div
|
||||
class="dark:bg-gray-600 bg-gray-500 text-xs font-medium text-gray-100 text-center p-0.5 leading-none rounded-full"
|
||||
style="width: 100%"
|
||||
>
|
||||
{uploadMessage}
|
||||
<Tooltip content={$i18n.t('Cancel')}>
|
||||
<button
|
||||
class="text-gray-800 dark:text-gray-100"
|
||||
on:click={() => {
|
||||
cancelModelPullHandler(model);
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
class="w-4 h-4 text-gray-800 dark:text-white"
|
||||
aria-hidden="true"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M6 18 17.94 6M18 18 6.06 6"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
{#if 'digest' in $MODEL_DOWNLOAD_POOL[model]}
|
||||
<div class="mt-1 text-xs dark:text-gray-500" style="font-size: 0.5rem;">
|
||||
{$MODEL_DOWNLOAD_POOL[model].digest}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-1 text-xs dark:text-gray-500" style="font-size: 0.5rem;">
|
||||
{modelFileDigest}
|
||||
</div>
|
||||
</div>
|
||||
{:else if uploadProgress !== null}
|
||||
<div class="mt-2">
|
||||
<div class=" mb-2 text-xs">{$i18n.t('Upload Progress')}</div>
|
||||
|
||||
<div class="w-full rounded-full dark:bg-gray-800">
|
||||
<div
|
||||
class="dark:bg-gray-600 bg-gray-500 text-xs font-medium text-gray-100 text-center p-0.5 leading-none rounded-full"
|
||||
style="width: {Math.max(15, uploadProgress ?? 0)}%"
|
||||
>
|
||||
{uploadProgress ?? 0}%
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-1 text-xs dark:text-gray-500" style="font-size: 0.5rem;">
|
||||
{modelFileDigest}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
{/if}
|
||||
</form>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class=" mb-2 text-sm font-medium">{$i18n.t('Delete a model')}</div>
|
||||
<div class="flex w-full">
|
||||
<div class="flex-1 mr-2">
|
||||
<select
|
||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
bind:value={deleteModelTag}
|
||||
placeholder={$i18n.t('Select a model')}
|
||||
>
|
||||
{#if !deleteModelTag}
|
||||
<option value="" disabled selected>{$i18n.t('Select a model')}</option>
|
||||
{/if}
|
||||
{#each $models.filter((m) => !(m?.preset ?? false) && m.owned_by === 'ollama' && (selectedOllamaUrlIdx === null ? true : (m?.ollama?.urls ?? []).includes(selectedOllamaUrlIdx))) as model}
|
||||
<option value={model.name} class="bg-gray-100 dark:bg-gray-700"
|
||||
>{model.name +
|
||||
' (' +
|
||||
(model.ollama.size / 1024 ** 3).toFixed(1) +
|
||||
' GB)'}</option
|
||||
>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
<button
|
||||
class="px-2.5 bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition"
|
||||
on:click={() => {
|
||||
deleteModelHandler();
|
||||
}}
|
||||
>
|
||||
<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="M5 3.25V4H2.75a.75.75 0 0 0 0 1.5h.3l.815 8.15A1.5 1.5 0 0 0 5.357 15h5.285a1.5 1.5 0 0 0 1.493-1.35l.815-8.15h.3a.75.75 0 0 0 0-1.5H11v-.75A2.25 2.25 0 0 0 8.75 1h-1.5A2.25 2.25 0 0 0 5 3.25Zm2.25-.75a.75.75 0 0 0-.75.75V4h3v-.75a.75.75 0 0 0-.75-.75h-1.5ZM6.05 6a.75.75 0 0 1 .787.713l.275 5.5a.75.75 0 0 1-1.498.075l-.275-5.5A.75.75 0 0 1 6.05 6Zm3.9 0a.75.75 0 0 1 .712.787l-.275 5.5a.75.75 0 0 1-1.498-.075l.275-5.5a.75.75 0 0 1 .786-.711Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pt-1">
|
||||
<div class="flex justify-between items-center text-xs">
|
||||
<div class=" text-sm font-medium">{$i18n.t('Experimental')}</div>
|
||||
<button
|
||||
class=" text-xs font-medium text-gray-500"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
showExperimentalOllama = !showExperimentalOllama;
|
||||
}}>{showExperimentalOllama ? $i18n.t('Hide') : $i18n.t('Show')}</button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if showExperimentalOllama}
|
||||
<form
|
||||
on:submit|preventDefault={() => {
|
||||
uploadModelHandler();
|
||||
}}
|
||||
>
|
||||
<div class=" mb-2 flex w-full justify-between">
|
||||
<div class=" text-sm font-medium">{$i18n.t('Upload a GGUF model')}</div>
|
||||
|
||||
<button
|
||||
class="p-1 px-3 text-xs flex rounded transition"
|
||||
on:click={() => {
|
||||
if (modelUploadMode === 'file') {
|
||||
modelUploadMode = 'url';
|
||||
} else {
|
||||
modelUploadMode = 'file';
|
||||
}
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
{#if modelUploadMode === 'file'}
|
||||
<span class="ml-2 self-center">{$i18n.t('File Mode')}</span>
|
||||
{:else}
|
||||
<span class="ml-2 self-center">{$i18n.t('URL Mode')}</span>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex w-full mb-1.5">
|
||||
<div class="flex flex-col w-full">
|
||||
{#if modelUploadMode === 'file'}
|
||||
<div
|
||||
class="flex-1 {modelInputFile && modelInputFile.length > 0 ? 'mr-2' : ''}"
|
||||
>
|
||||
<input
|
||||
id="model-upload-input"
|
||||
bind:this={modelUploadInputElement}
|
||||
type="file"
|
||||
bind:files={modelInputFile}
|
||||
on:change={() => {
|
||||
console.log(modelInputFile);
|
||||
}}
|
||||
accept=".gguf,.safetensors"
|
||||
required
|
||||
hidden
|
||||
/>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="w-full rounded-lg text-left py-2 px-4 bg-white dark:text-gray-300 dark:bg-gray-850"
|
||||
on:click={() => {
|
||||
modelUploadInputElement.click();
|
||||
}}
|
||||
>
|
||||
{#if modelInputFile && modelInputFile.length > 0}
|
||||
{modelInputFile[0].name}
|
||||
{:else}
|
||||
{$i18n.t('Click here to select')}
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex-1 {modelFileUrl !== '' ? 'mr-2' : ''}">
|
||||
<input
|
||||
class="w-full rounded-lg text-left py-2 px-4 bg-white dark:text-gray-300 dark:bg-gray-850 outline-none {modelFileUrl !==
|
||||
''
|
||||
? 'mr-2'
|
||||
: ''}"
|
||||
type="url"
|
||||
required
|
||||
bind:value={modelFileUrl}
|
||||
placeholder={$i18n.t('Type Hugging Face Resolve (Download) URL')}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if (modelUploadMode === 'file' && modelInputFile && modelInputFile.length > 0) || (modelUploadMode === 'url' && modelFileUrl !== '')}
|
||||
<button
|
||||
class="px-2.5 bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg disabled:cursor-not-allowed transition"
|
||||
type="submit"
|
||||
disabled={modelTransferring}
|
||||
>
|
||||
{#if modelTransferring}
|
||||
<div class="self-center">
|
||||
<svg
|
||||
class=" w-4 h-4"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<style>
|
||||
.spinner_ajPY {
|
||||
transform-origin: center;
|
||||
animation: spinner_AtaB 0.75s infinite linear;
|
||||
}
|
||||
|
||||
@keyframes spinner_AtaB {
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<path
|
||||
d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
|
||||
opacity=".25"
|
||||
/>
|
||||
<path
|
||||
d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
|
||||
class="spinner_ajPY"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
{:else}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
d="M7.25 10.25a.75.75 0 0 0 1.5 0V4.56l2.22 2.22a.75.75 0 1 0 1.06-1.06l-3.5-3.5a.75.75 0 0 0-1.06 0l-3.5 3.5a.75.75 0 0 0 1.06 1.06l2.22-2.22v5.69Z"
|
||||
/>
|
||||
<path
|
||||
d="M3.5 9.75a.75.75 0 0 0-1.5 0v1.5A2.75 2.75 0 0 0 4.75 14h6.5A2.75 2.75 0 0 0 14 11.25v-1.5a.75.75 0 0 0-1.5 0v1.5c0 .69-.56 1.25-1.25 1.25h-6.5c-.69 0-1.25-.56-1.25-1.25v-1.5Z"
|
||||
/>
|
||||
</svg>
|
||||
{/if}
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if (modelUploadMode === 'file' && modelInputFile && modelInputFile.length > 0) || (modelUploadMode === 'url' && modelFileUrl !== '')}
|
||||
<div>
|
||||
<div>
|
||||
<div class=" my-2.5 text-sm font-medium">{$i18n.t('Modelfile Content')}</div>
|
||||
<textarea
|
||||
bind:value={modelFileContent}
|
||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-100 dark:text-gray-100 dark:bg-gray-850 outline-none resize-none"
|
||||
rows="6"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<div class=" mt-1 text-xs text-gray-400 dark:text-gray-500">
|
||||
{$i18n.t('To access the GGUF models available for downloading,')}
|
||||
<a
|
||||
class=" text-gray-500 dark:text-gray-300 font-medium underline"
|
||||
href="https://huggingface.co/models?search=gguf"
|
||||
target="_blank">{$i18n.t('click here.')}</a
|
||||
>
|
||||
</div>
|
||||
|
||||
{#if uploadMessage}
|
||||
<div class="mt-2">
|
||||
<div class=" mb-2 text-xs">{$i18n.t('Upload Progress')}</div>
|
||||
|
||||
<div class="w-full rounded-full dark:bg-gray-800">
|
||||
<div
|
||||
class="dark:bg-gray-600 bg-gray-500 text-xs font-medium text-gray-100 text-center p-0.5 leading-none rounded-full"
|
||||
style="width: 100%"
|
||||
>
|
||||
{uploadMessage}
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-1 text-xs dark:text-gray-500" style="font-size: 0.5rem;">
|
||||
{modelFileDigest}
|
||||
</div>
|
||||
</div>
|
||||
{:else if uploadProgress !== null}
|
||||
<div class="mt-2">
|
||||
<div class=" mb-2 text-xs">{$i18n.t('Upload Progress')}</div>
|
||||
|
||||
<div class="w-full rounded-full dark:bg-gray-800">
|
||||
<div
|
||||
class="dark:bg-gray-600 bg-gray-500 text-xs font-medium text-gray-100 text-center p-0.5 leading-none rounded-full"
|
||||
style="width: {Math.max(15, uploadProgress ?? 0)}%"
|
||||
>
|
||||
{uploadProgress ?? 0}%
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-1 text-xs dark:text-gray-500" style="font-size: 0.5rem;">
|
||||
{modelFileDigest}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</form>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{:else if ollamaVersion === false}
|
||||
<div>Ollama Not Detected</div>
|
||||
{:else if ollamaVersion === false}
|
||||
<div>Ollama Not Detected</div>
|
||||
{:else}
|
||||
<div class="flex h-full justify-center">
|
||||
<div class="my-auto">
|
||||
<Spinner className="size-6" />
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{:else if ollamaEnabled === false}
|
||||
<div>Ollama API is disabled</div>
|
||||
{:else}
|
||||
<div class="flex h-full justify-center">
|
||||
<div class="my-auto">
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
<script lang="ts">
|
||||
export let className = 'size-4';
|
||||
export let strokeWidth = '1.5';
|
||||
</script>
|
||||
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width={strokeWidth}
|
||||
stroke="currentColor"
|
||||
class={className}
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M3 16.5v2.25A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75V16.5M16.5 12 12 16.5m0 0L7.5 12m4.5 4.5V3"
|
||||
/>
|
||||
</svg>
|
||||
@@ -63,6 +63,13 @@
|
||||
// Revoke the URL to release memory
|
||||
window.URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
const downloadJSONExport = async () => {
|
||||
let blob = new Blob([JSON.stringify([chat])], {
|
||||
type: 'application/json'
|
||||
});
|
||||
saveAs(blob, `chat-export-${Date.now()}.json`);
|
||||
};
|
||||
</script>
|
||||
|
||||
<Dropdown
|
||||
@@ -164,6 +171,14 @@
|
||||
transition={flyAndScale}
|
||||
sideOffset={8}
|
||||
>
|
||||
<DropdownMenu.Item
|
||||
class="flex gap-2 items-center px-3 py-2 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
|
||||
on:click={() => {
|
||||
downloadJSONExport();
|
||||
}}
|
||||
>
|
||||
<div class="flex items-center line-clamp-1">{$i18n.t('Export chat (.json)')}</div>
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item
|
||||
class="flex gap-2 items-center px-3 py-2 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
|
||||
on:click={() => {
|
||||
|
||||
@@ -205,6 +205,10 @@
|
||||
await archiveChatById(localStorage.token, id);
|
||||
await chats.set(await getChatList(localStorage.token));
|
||||
};
|
||||
|
||||
const focusEdit = async (node: HTMLInputElement) => {
|
||||
node.focus();
|
||||
};
|
||||
</script>
|
||||
|
||||
<ShareChatModal bind:show={showShareChatModal} chatId={shareChatId} />
|
||||
@@ -489,7 +493,11 @@
|
||||
? 'bg-gray-100 dark:bg-gray-950'
|
||||
: 'group-hover:bg-gray-100 dark:group-hover:bg-gray-950'} whitespace-nowrap text-ellipsis"
|
||||
>
|
||||
<input bind:value={chatTitle} class=" bg-transparent w-full outline-none mr-10" />
|
||||
<input
|
||||
use:focusEdit
|
||||
bind:value={chatTitle}
|
||||
class=" bg-transparent w-full outline-none mr-10"
|
||||
/>
|
||||
</div>
|
||||
{:else}
|
||||
<a
|
||||
@@ -507,6 +515,10 @@
|
||||
showSidebar.set(false);
|
||||
}
|
||||
}}
|
||||
on:dblclick={() => {
|
||||
chatTitle = chat.title;
|
||||
chatTitleEditId = chat.id;
|
||||
}}
|
||||
draggable="false"
|
||||
>
|
||||
<div class=" flex self-center flex-1 w-full">
|
||||
|
||||
@@ -8,7 +8,12 @@
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
import Modal from '$lib/components/common/Modal.svelte';
|
||||
import { archiveChatById, deleteChatById, getArchivedChatList } from '$lib/apis/chats';
|
||||
import {
|
||||
archiveChatById,
|
||||
deleteChatById,
|
||||
getAllArchivedChats,
|
||||
getArchivedChatList
|
||||
} from '$lib/apis/chats';
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
@@ -38,6 +43,7 @@
|
||||
};
|
||||
|
||||
const exportChatsHandler = async () => {
|
||||
const chats = await getAllArchivedChats(localStorage.token);
|
||||
let blob = new Blob([JSON.stringify(chats)], {
|
||||
type: 'application/json'
|
||||
});
|
||||
|
||||
@@ -126,6 +126,13 @@
|
||||
saveAs(blob, `models-export-${Date.now()}.json`);
|
||||
};
|
||||
|
||||
const exportModelHandler = async (model) => {
|
||||
let blob = new Blob([JSON.stringify([model])], {
|
||||
type: 'application/json'
|
||||
});
|
||||
saveAs(blob, `${model.id}-${Date.now()}.json`);
|
||||
};
|
||||
|
||||
const positionChangeHanlder = async () => {
|
||||
// Get the new order of the models
|
||||
const modelIds = Array.from(document.getElementById('model-list').children).map((child) =>
|
||||
@@ -322,6 +329,9 @@
|
||||
cloneHandler={() => {
|
||||
cloneModelHandler(model);
|
||||
}}
|
||||
exportHandler={() => {
|
||||
exportModelHandler(model);
|
||||
}}
|
||||
hideHandler={() => {
|
||||
hideModelHandler(model);
|
||||
}}
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
import Share from '$lib/components/icons/Share.svelte';
|
||||
import ArchiveBox from '$lib/components/icons/ArchiveBox.svelte';
|
||||
import DocumentDuplicate from '$lib/components/icons/DocumentDuplicate.svelte';
|
||||
import ArrowDownTray from '$lib/components/icons/ArrowDownTray.svelte';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
@@ -18,6 +19,8 @@
|
||||
|
||||
export let shareHandler: Function;
|
||||
export let cloneHandler: Function;
|
||||
export let exportHandler: Function;
|
||||
|
||||
export let hideHandler: Function;
|
||||
export let deleteHandler: Function;
|
||||
export let onClose: Function;
|
||||
@@ -66,6 +69,17 @@
|
||||
<div class="flex items-center">{$i18n.t('Clone')}</div>
|
||||
</DropdownMenu.Item>
|
||||
|
||||
<DropdownMenu.Item
|
||||
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
|
||||
on:click={() => {
|
||||
exportHandler();
|
||||
}}
|
||||
>
|
||||
<ArrowDownTray />
|
||||
|
||||
<div class="flex items-center">{$i18n.t('Export')}</div>
|
||||
</DropdownMenu.Item>
|
||||
|
||||
<DropdownMenu.Item
|
||||
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
|
||||
on:click={() => {
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
import { OLLAMA_API_BASE_URL, OPENAI_API_BASE_URL, WEBUI_API_BASE_URL } from '$lib/constants';
|
||||
import { WEBUI_NAME, config, user, models, settings } from '$lib/stores';
|
||||
|
||||
import { cancelOllamaRequest, generateChatCompletion } from '$lib/apis/ollama';
|
||||
import { generateChatCompletion } from '$lib/apis/ollama';
|
||||
import { generateOpenAIChatCompletion } from '$lib/apis/openai';
|
||||
|
||||
import { splitStream } from '$lib/utils';
|
||||
@@ -24,7 +24,6 @@
|
||||
let selectedModelId = '';
|
||||
|
||||
let loading = false;
|
||||
let currentRequestId = null;
|
||||
let stopResponseFlag = false;
|
||||
|
||||
let messagesContainerElement: HTMLDivElement;
|
||||
@@ -46,14 +45,6 @@
|
||||
}
|
||||
};
|
||||
|
||||
// const cancelHandler = async () => {
|
||||
// if (currentRequestId) {
|
||||
// const res = await cancelOllamaRequest(localStorage.token, currentRequestId);
|
||||
// currentRequestId = null;
|
||||
// loading = false;
|
||||
// }
|
||||
// };
|
||||
|
||||
const stopResponse = () => {
|
||||
stopResponseFlag = true;
|
||||
console.log('stopResponse');
|
||||
@@ -171,8 +162,6 @@
|
||||
if (stopResponseFlag) {
|
||||
controller.abort('User: Stop Response');
|
||||
}
|
||||
|
||||
currentRequestId = null;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -229,7 +218,6 @@
|
||||
|
||||
loading = false;
|
||||
stopResponseFlag = false;
|
||||
currentRequestId = null;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user