Merge branch 'open-webui:dev' into dev

This commit is contained in:
Andrew Baek
2025-09-26 01:25:50 +09:00
committed by GitHub
58 changed files with 1988 additions and 566 deletions
+123 -27
View File
@@ -13,7 +13,7 @@
import Switch from '$lib/components/common/Switch.svelte';
import Tags from './common/Tags.svelte';
import { getToolServerData } from '$lib/apis';
import { verifyToolServerConnection } from '$lib/apis/configs';
import { verifyToolServerConnection, registerOAuthClient } from '$lib/apis/configs';
import AccessControl from './workspace/common/AccessControl.svelte';
import Spinner from '$lib/components/common/Spinner.svelte';
import XMark from '$lib/components/icons/XMark.svelte';
@@ -41,10 +41,47 @@
let name = '';
let description = '';
let enable = true;
let oauthClientInfo = null;
let enable = true;
let loading = false;
const registerOAuthClientHandler = async () => {
if (url === '') {
toast.error($i18n.t('Please enter a valid URL'));
return;
}
if (id === '') {
toast.error($i18n.t('Please enter a valid ID'));
return;
}
const res = await registerOAuthClient(
localStorage.token,
{
url: url,
client_id: id
},
'mcp'
).catch((err) => {
toast.error($i18n.t('Registration failed'));
return null;
});
if (res) {
toast.warning(
$i18n.t(
'Please save the connection to persist the OAuth client information and do not change the ID'
)
);
toast.success($i18n.t('Registration successful'));
console.debug('Registration successful', res);
oauthClientInfo = res?.oauth_client_info ?? null;
}
};
const verifyHandler = async () => {
if (url === '') {
toast.error($i18n.t('Please enter a valid URL'));
@@ -106,6 +143,12 @@
return;
}
if (type === 'mcp' && auth_type === 'oauth_2.1' && !oauthClientInfo) {
toast.error($i18n.t('Please register the OAuth client'));
loading = false;
return;
}
const connection = {
url,
path,
@@ -119,7 +162,8 @@
info: {
id: id,
name: name,
description: description
description: description,
...(oauthClientInfo ? { oauth_client_info: oauthClientInfo } : {})
}
};
@@ -139,6 +183,7 @@
id = '';
name = '';
description = '';
oauthClientInfo = null;
enable = true;
accessControl = null;
@@ -156,6 +201,7 @@
id = connection.info?.id ?? '';
name = connection.info?.name ?? '';
description = connection.info?.description ?? '';
oauthClientInfo = connection.info?.oauth_client_info ?? null;
enable = connection.config?.enable ?? true;
accessControl = connection.config?.access_control ?? null;
@@ -227,25 +273,6 @@
</div>
{/if}
{#if type === 'mcp'}
<div
class=" bg-yellow-500/20 text-yellow-700 dark:text-yellow-200 rounded-2xl text-xs px-4 py-3 mb-2"
>
<span class="font-medium">
{$i18n.t('Warning')}:
</span>
{$i18n.t(
'MCP support is experimental and its specification changes often, which can lead to incompatibilities. OpenAPI specification support is directly maintained by the Open WebUI team, making it the more reliable option for compatibility.'
)}
<a
class="font-medium underline"
href="https://docs.openwebui.com/features/mcp"
target="_blank">{$i18n.t('Read more →')}</a
>
</div>
{/if}
<div class="flex gap-2">
<div class="flex flex-col w-full">
<div class="flex justify-between mb-0.5">
@@ -333,11 +360,52 @@
<div class="flex gap-2 mt-2">
<div class="flex flex-col w-full">
<label
for="select-bearer-or-session"
class={`text-xs ${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : 'text-gray-500'}`}
>{$i18n.t('Auth')}</label
>
<div class="flex justify-between items-center">
<div class="flex gap-2 items-center">
<div
for="select-bearer-or-session"
class={`text-xs ${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : 'text-gray-500'}`}
>
{$i18n.t('Auth')}
</div>
</div>
{#if auth_type === 'oauth_2.1'}
<div class="flex items-center gap-2">
<div class="flex flex-col justify-end items-center shrink-0">
<Tooltip
content={oauthClientInfo
? $i18n.t('Register Again')
: $i18n.t('Register Client')}
>
<button
class=" text-xs underline dark:text-gray-500 dark:hover:text-gray-200 text-gray-700 hover:text-gray-900 transition"
type="button"
on:click={() => {
registerOAuthClientHandler();
}}
>
{$i18n.t('Register Client')}
</button>
</Tooltip>
</div>
{#if !oauthClientInfo}
<div
class="text-xs font-medium px-1.5 rounded-md bg-yellow-500/20 text-yellow-700 dark:text-yellow-200"
>
{$i18n.t('Not Registered')}
</div>
{:else}
<div
class="text-xs font-medium px-1.5 rounded-md bg-green-500/20 text-green-700 dark:text-green-200"
>
{$i18n.t('Registered')}
</div>
{/if}
</div>
{/if}
</div>
<div class="flex gap-2">
<div class="flex-shrink-0 self-start">
@@ -353,6 +421,9 @@
{#if !direct}
<option value="system_oauth">{$i18n.t('OAuth')}</option>
{#if type === 'mcp'}
<option value="oauth_2.1">{$i18n.t('OAuth 2.1')}</option>
{/if}
{/if}
</select>
</div>
@@ -382,6 +453,12 @@
>
{$i18n.t('Forwards system user OAuth access token to authenticate')}
</div>
{:else if auth_type === 'oauth_2.1'}
<div
class={`flex items-center text-xs self-center translate-y-[1px] ${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : 'text-gray-500'}`}
>
{$i18n.t('Uses ​OAuth 2.1 Dynamic Client Registration')}
</div>
{/if}
</div>
</div>
@@ -470,6 +547,25 @@
{/if}
</div>
{#if type === 'mcp'}
<div
class=" bg-yellow-500/20 text-yellow-700 dark:text-yellow-200 rounded-2xl text-xs px-4 py-3 mb-2"
>
<span class="font-medium">
{$i18n.t('Warning')}:
</span>
{$i18n.t(
'MCP support is experimental and its specification changes often, which can lead to incompatibilities. OpenAPI specification support is directly maintained by the Open WebUI team, making it the more reliable option for compatibility.'
)}
<a
class="font-medium underline"
href="https://docs.openwebui.com/features/mcp"
target="_blank">{$i18n.t('Read more →')}</a
>
</div>
{/if}
<div class="flex justify-end pt-3 text-sm font-medium gap-1.5">
{#if edit}
<button
@@ -202,7 +202,7 @@
{:else}
<table class="w-full text-sm text-left text-gray-500 dark:text-gray-400 table-auto max-w-full">
<thead class="text-xs text-gray-800 uppercase bg-transparent dark:text-gray-200">
<tr class=" border-b-2 border-gray-100 dark:border-gray-800">
<tr class=" border-b-[1.5px] border-gray-50 dark:border-gray-850">
<th
scope="col"
class="px-2.5 py-2 cursor-pointer select-none w-3"
@@ -389,7 +389,7 @@
: ''}"
>
<thead class="text-xs text-gray-800 uppercase bg-transparent dark:text-gray-200">
<tr class=" border-b-2 border-gray-100 dark:border-gray-800">
<tr class=" border-b-[1.5px] border-gray-50 dark:border-gray-850">
<th
scope="col"
class="px-2.5 py-2 cursor-pointer select-none w-3"
@@ -4,6 +4,7 @@
const i18n = getContext('i18n');
import CodeEditor from '$lib/components/common/CodeEditor.svelte';
import ConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
import Badge from '$lib/components/common/Badge.svelte';
import Tooltip from '$lib/components/common/Tooltip.svelte';
@@ -366,22 +367,20 @@ class Pipe:
</div>
<div class="mb-2 flex-1 overflow-auto h-0 rounded-lg">
{#await import('$lib/components/common/CodeEditor.svelte') then { default: CodeEditor }}
<CodeEditor
bind:this={codeEditor}
value={content}
lang="python"
{boilerplate}
onChange={(e) => {
_content = e;
}}
onSave={async () => {
if (formElement) {
formElement.requestSubmit();
}
}}
/>
{/await}
<CodeEditor
bind:this={codeEditor}
value={content}
lang="python"
{boilerplate}
onChange={(e) => {
_content = e;
}}
onSave={async () => {
if (formElement) {
formElement.requestSubmit();
}
}}
/>
</div>
<div class="pb-3 flex justify-between">
@@ -13,6 +13,7 @@
export let saveHandler: Function;
let webSearchEngines = [
'ollama_cloud',
'searxng',
'yacy',
'google_pse',
@@ -130,7 +131,24 @@
</div>
{#if webConfig.WEB_SEARCH_ENGINE !== ''}
{#if webConfig.WEB_SEARCH_ENGINE === 'searxng'}
{#if webConfig.WEB_SEARCH_ENGINE === 'ollama_cloud'}
<div class="mb-2.5 flex w-full flex-col">
<div>
<div class=" self-center text-xs font-medium mb-1">
{$i18n.t('Ollama Cloud API Key')}
</div>
<div class="flex w-full">
<div class="flex-1">
<SensitiveInput
placeholder={$i18n.t('Enter Ollama Cloud API Key')}
bind:value={webConfig.OLLAMA_CLOUD_WEB_SEARCH_API_KEY}
/>
</div>
</div>
</div>
</div>
{:else if webConfig.WEB_SEARCH_ENGINE === 'searxng'}
<div class="mb-2.5 flex w-full flex-col">
<div>
<div class=" self-center text-xs font-medium mb-1">
@@ -14,6 +14,7 @@
import Drawer from '../common/Drawer.svelte';
import EllipsisVertical from '../icons/EllipsisVertical.svelte';
import Thread from './Thread.svelte';
import i18n from '$lib/i18n';
export let id = '';
@@ -252,6 +253,10 @@
{typingUsers}
userSuggestions={true}
channelSuggestions={true}
disabled={!channel?.write_access}
placeholder={!channel?.write_access
? $i18n.t('You do not have permission to send messages in this channel.')
: $i18n.t('Type here...')}
{onChange}
onSubmit={submitHandler}
{scrollToBottom}
@@ -38,7 +38,7 @@
import MentionList from './MessageInput/MentionList.svelte';
import Skeleton from '../chat/Messages/Skeleton.svelte';
export let placeholder = $i18n.t('Send a Message');
export let placeholder = $i18n.t('Type here...');
export let id = null;
export let chatInputElement;
@@ -53,6 +53,7 @@
export let scrollEnd = true;
export let scrollToBottom: Function = () => {};
export let disabled = false;
export let acceptFiles = true;
export let showFormattingToolbar = true;
@@ -731,7 +732,9 @@
</div>
</div>
<div class="">
<div
class="{disabled ? 'opacity-50 pointer-events-none cursor-not-allowed' : ''} relative z-20"
>
{#if recording}
<VoiceRecording
bind:recording
@@ -837,6 +840,8 @@
bind:this={chatInputElement}
json={true}
messageInput={true}
editable={!disabled}
{placeholder}
richText={$settings?.richTextInput ?? true}
showFormattingToolbar={$settings?.showFormattingToolbar ?? false}
shiftEnter={!($settings?.ctrlEnterToSend ?? false) &&
@@ -94,6 +94,7 @@
<Message
{message}
{thread}
disabled={!channel?.write_access}
showUserProfile={messageIdx === 0 ||
messageList.at(messageIdx - 1)?.user_id !== message.user_id ||
messageList.at(messageIdx - 1)?.meta?.model_id !== message?.meta?.model_id}
@@ -40,6 +40,7 @@
export let message;
export let showUserProfile = true;
export let thread = false;
export let disabled = false;
export let onDelete: Function = () => {};
export let onEdit: Function = () => {};
@@ -68,7 +69,7 @@
? 'pt-1.5 pb-0.5'
: ''} w-full max-w-full mx-auto group hover:bg-gray-300/5 dark:hover:bg-gray-700/5 transition relative"
>
{#if !edit}
{#if !edit && !disabled}
<div
class=" absolute {showButtons ? '' : 'invisible group-hover:visible'} right-1 -top-2 z-10"
>
+31 -20
View File
@@ -10,6 +10,7 @@
import Messages from './Messages.svelte';
import { onDestroy, onMount, tick, getContext } from 'svelte';
import { toast } from 'svelte-sonner';
import Spinner from '../common/Spinner.svelte';
const i18n = getContext('i18n');
@@ -175,32 +176,42 @@
</div>
<div class=" max-h-full w-full overflow-y-auto" bind:this={messagesContainerElement}>
<Messages
id={threadId}
{channel}
{messages}
{top}
thread={true}
onLoad={async () => {
const newMessages = await getChannelThreadMessages(
localStorage.token,
channel.id,
threadId,
messages.length
);
{#if messages !== null}
<Messages
id={threadId}
{channel}
{messages}
{top}
thread={true}
onLoad={async () => {
const newMessages = await getChannelThreadMessages(
localStorage.token,
channel.id,
threadId,
messages.length
);
messages = [...messages, ...newMessages];
messages = [...messages, ...newMessages];
if (newMessages.length < 50) {
top = true;
return;
}
}}
/>
if (newMessages.length < 50) {
top = true;
return;
}
}}
/>
{:else}
<div class="w-full flex justify-center pt-5 pb-10">
<Spinner />
</div>
{/if}
<div class=" pb-[1rem] px-2.5 w-full">
<MessageInput
id={threadId}
disabled={!channel?.write_access}
placeholder={!channel?.write_access
? $i18n.t('You do not have permission to send messages in this thread.')
: $i18n.t('Reply to thread...')}
typingUsersClassName="from-gray-50 dark:from-gray-850"
{typingUsers}
userSuggestions={true}
@@ -1415,6 +1415,9 @@
console.error('OneDrive Error:', error);
}
}}
onUpload={async (e) => {
dispatch('upload', e);
}}
onClose={async () => {
await tick();
@@ -0,0 +1,85 @@
<script lang="ts">
import { toast } from 'svelte-sonner';
import { getContext } from 'svelte';
const i18n = getContext('i18n');
import { settings } from '$lib/stores';
import Modal from '$lib/components/common/Modal.svelte';
import XMark from '$lib/components/icons/XMark.svelte';
import { isValidHttpUrl, isYoutubeUrl } from '$lib/utils';
export let show = false;
export let onSubmit: (e) => void;
let url = '';
const submitHandler = () => {
if (isValidHttpUrl(url)) {
onSubmit({
type: isYoutubeUrl(url) ? 'youtube' : 'web',
data: url
});
show = false;
url = '';
} else {
toast.error($i18n.t('Please enter a valid URL.'));
}
};
</script>
<Modal bind:show size="sm">
<div class="flex flex-col h-full">
<div class="flex justify-between items-center dark:text-gray-100 px-5 pt-4 pb-1.5">
<h1 class="text-lg font-medium self-center font-primary">
{$i18n.t('Attach Webpage')}
</h1>
<button
class="self-center"
aria-label={$i18n.t('Close modal')}
on:click={() => {
show = false;
}}
>
<XMark className="size-5" />
</button>
</div>
<div class="px-5 pb-4">
<form
on:submit={(e) => {
e.preventDefault();
submitHandler();
}}
>
<div class="flex justify-between mb-0.5">
<label
for="webpage-url"
class={`text-xs ${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : 'text-gray-500'}`}
>{$i18n.t('Webpage URL')}</label
>
</div>
<input
id="webpage-url"
class={`w-full flex-1 text-sm bg-transparent ${($settings?.highContrastMode ?? false) ? 'placeholder:text-gray-700 dark:placeholder:text-gray-100' : 'outline-hidden placeholder:text-gray-300 dark:placeholder:text-gray-700'}`}
type="text"
bind:value={url}
placeholder={'https://example.com'}
autocomplete="off"
required
/>
<div class="flex justify-end gap-2 pt-3 bg-gray-50 dark:bg-gray-900/50">
<button
class="px-3.5 py-1.5 text-sm font-medium bg-black hover:bg-gray-800 text-white dark:bg-white dark:text-black dark:hover:bg-gray-200 transition rounded-full"
type="submit"
>
{$i18n.t('Add')}
</button>
</div>
</form>
</div>
</div>
</Modal>
@@ -7,7 +7,7 @@
dayjs.extend(relativeTime);
import { tick, getContext, onMount, onDestroy } from 'svelte';
import { removeLastWordFromString, isValidHttpUrl } from '$lib/utils';
import { removeLastWordFromString, isValidHttpUrl, isYoutubeUrl } from '$lib/utils';
import Tooltip from '$lib/components/common/Tooltip.svelte';
import DocumentPage from '$lib/components/icons/DocumentPage.svelte';
import Database from '$lib/components/icons/Database.svelte';
@@ -36,7 +36,7 @@
: items),
...(query.startsWith('http')
? query.startsWith('https://www.youtube.com') || query.startsWith('https://youtu.be')
? isYoutubeUrl(query)
? [{ type: 'youtube', name: query, description: query }]
: [
{
@@ -228,7 +228,7 @@
{/if}
{/each}
{#if query.startsWith('https://www.youtube.com') || query.startsWith('https://youtu.be')}
{#if isYoutubeUrl(query)}
<button
class="px-2 py-1 rounded-xl w-full text-left bg-gray-50 dark:bg-gray-800 dark:text-gray-100 selected-command-option-button"
type="button"
@@ -24,6 +24,8 @@
import Chats from './InputMenu/Chats.svelte';
import Notes from './InputMenu/Notes.svelte';
import Knowledge from './InputMenu/Knowledge.svelte';
import AttachWebpageModal from './AttachWebpageModal.svelte';
import GlobeAlt from '$lib/components/icons/GlobeAlt.svelte';
const i18n = getContext('i18n');
@@ -39,11 +41,14 @@
export let uploadGoogleDriveHandler: Function;
export let uploadOneDriveHandler: Function;
export let onUpload: Function;
export let onClose: Function;
let show = false;
let tab = '';
let showAttachWebpageModal = false;
let fileUploadEnabled = true;
$: fileUploadEnabled =
fileUploadCapableModels.length === selectedModels.length &&
@@ -78,6 +83,13 @@
};
</script>
<AttachWebpageModal
bind:show={showAttachWebpageModal}
onSubmit={(e) => {
onUpload(e);
}}
/>
<!-- Hidden file input used to open the camera on mobile -->
<input
id="camera-input"
@@ -166,6 +178,16 @@
</DropdownMenu.Item>
</Tooltip>
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl"
on:click={() => {
showAttachWebpageModal = true;
}}
>
<GlobeAlt />
<div class="line-clamp-1">{$i18n.t('Attach Webpage')}</div>
</DropdownMenu.Item>
{#if $config?.features?.enable_notes ?? false}
<Tooltip
content={fileUploadCapableModels.length !== selectedModels.length
@@ -31,7 +31,7 @@
const getItemsPage = async () => {
itemsLoading = true;
let res = await getChatList(localStorage.token, page).catch(() => {
let res = await getChatList(localStorage.token, page, true).catch(() => {
return [];
});
@@ -19,6 +19,9 @@
import Terminal from '$lib/components/icons/Terminal.svelte';
import ChevronRight from '$lib/components/icons/ChevronRight.svelte';
import ChevronLeft from '$lib/components/icons/ChevronLeft.svelte';
import ValvesModal from '$lib/components/workspace/common/ValvesModal.svelte';
import { getOAuthClientAuthorizationUrl } from '$lib/apis/configs';
import { partition } from 'd3-hierarchy';
const i18n = getContext('i18n');
@@ -43,6 +46,11 @@
let show = false;
let tab = '';
let showValvesModal = false;
let selectedValvesType = 'tool';
let selectedValvesItemId = null;
let tools = null;
$: if (show) {
@@ -64,7 +72,8 @@
a[tool.id] = {
name: tool.name,
description: tool.meta.description,
enabled: selectedToolIds.includes(tool.id)
enabled: selectedToolIds.includes(tool.id),
...tool
};
return a;
}, {});
@@ -87,6 +96,16 @@
};
</script>
<ValvesModal
bind:show={showValvesModal}
userValves={true}
type={selectedValvesType}
id={selectedValvesItemId ?? null}
on:save={async () => {
await tick();
}}
/>
<Dropdown
bind:show
on:change={(e) => {
@@ -304,11 +323,25 @@
{#each Object.keys(tools) as toolId}
<button
class="flex w-full justify-between gap-2 items-center px-3 py-1.5 text-sm cursor-pointer rounded-xl hover:bg-gray-50 dark:hover:bg-gray-800/50"
on:click={() => {
tools[toolId].enabled = !tools[toolId].enabled;
class="relative flex w-full justify-between gap-2 items-center px-3 py-1.5 text-sm cursor-pointer rounded-xl hover:bg-gray-50 dark:hover:bg-gray-800/50"
on:click={(e) => {
if (!(tools[toolId]?.authenticated ?? true)) {
e.preventDefault();
let parts = toolId.split(':');
let serverId = parts?.at(-1) ?? toolId;
const authUrl = getOAuthClientAuthorizationUrl(serverId, 'mcp');
window.open(authUrl, '_blank', 'noopener');
} else {
tools[toolId].enabled = !tools[toolId].enabled;
}
}}
>
{#if !(tools[toolId]?.authenticated ?? true)}
<!-- make it slighly darker and not clickable -->
<div class="absolute inset-0 opacity-50 rounded-xl cursor-not-allowed z-10" />
{/if}
<div class="flex-1 truncate">
<div class="flex flex-1 gap-2 items-center">
<Tooltip content={tools[toolId]?.name ?? ''} placement="top">
@@ -322,6 +355,44 @@
</div>
</div>
{#if tools[toolId]?.has_user_valves}
<div class=" shrink-0">
<Tooltip content={$i18n.t('Valves')}>
<button
class="self-center w-fit text-sm text-gray-600 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 transition rounded-full"
type="button"
on:click={(e) => {
e.stopPropagation();
e.preventDefault();
selectedValvesType = 'tool';
selectedValvesItemId = toolId;
showValvesModal = true;
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="size-4"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.325.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 0 1 1.37.49l1.296 2.247a1.125 1.125 0 0 1-.26 1.431l-1.003.827c-.293.241-.438.613-.43.992a7.723 7.723 0 0 1 0 .255c-.008.378.137.75.43.991l1.004.827c.424.35.534.955.26 1.43l-1.298 2.247a1.125 1.125 0 0 1-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.47 6.47 0 0 1-.22.128c-.331.183-.581.495-.644.869l-.213 1.281c-.09.543-.56.94-1.11.94h-2.594c-.55 0-1.019-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 0 1-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 0 1-1.369-.49l-1.297-2.247a1.125 1.125 0 0 1 .26-1.431l1.004-.827c.292-.24.437-.613.43-.991a6.932 6.932 0 0 1 0-.255c.007-.38-.138-.751-.43-.992l-1.004-.827a1.125 1.125 0 0 1-.26-1.43l1.297-2.247a1.125 1.125 0 0 1 1.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.086.22-.128.332-.183.582-.495.644-.869l.214-1.28Z"
/>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z"
/>
</svg>
</button>
</Tooltip>
</div>
{/if}
<div class=" shrink-0">
<Switch
state={tools[toolId].enabled}
@@ -1,16 +1,18 @@
<script lang="ts">
import hljs from 'highlight.js';
import { toast } from 'svelte-sonner';
import { getContext, onMount, tick, onDestroy } from 'svelte';
import { config } from '$lib/stores';
import PyodideWorker from '$lib/workers/pyodide.worker?worker';
import { executeCode } from '$lib/apis/utils';
import { copyToClipboard, renderMermaidDiagram } from '$lib/utils';
import 'highlight.js/styles/github-dark.min.css';
import PyodideWorker from '$lib/workers/pyodide.worker?worker';
import CodeEditor from '$lib/components/common/CodeEditor.svelte';
import SvgPanZoom from '$lib/components/common/SVGPanZoom.svelte';
import { config } from '$lib/stores';
import { executeCode } from '$lib/apis/utils';
import { toast } from 'svelte-sonner';
import ChevronUp from '$lib/components/icons/ChevronUp.svelte';
import ChevronUpDown from '$lib/components/icons/ChevronUpDown.svelte';
import CommandLine from '$lib/components/icons/CommandLine.svelte';
@@ -480,19 +482,17 @@
{#if !collapsed}
{#if edit}
{#await import('$lib/components/common/CodeEditor.svelte') then { default: CodeEditor }}
<CodeEditor
value={code}
{id}
{lang}
onSave={() => {
saveCode();
}}
onChange={(value) => {
_code = value;
}}
/>
{/await}
<CodeEditor
value={code}
{id}
{lang}
onSave={() => {
saveCode();
}}
onChange={(value) => {
_code = value;
}}
/>
{:else}
<pre
class=" hljs p-4 px-5 overflow-x-auto"
+15 -10
View File
@@ -1,5 +1,5 @@
<script lang="ts">
import { onDestroy, onMount } from 'svelte';
import { onDestroy, onMount, getContext } from 'svelte';
import panzoom, { type PanZoom } from 'panzoom';
import fileSaver from 'file-saver';
@@ -11,6 +11,8 @@
export let src = '';
export let alt = '';
const i18n = getContext('i18n');
let mounted = false;
let previewElement = null;
@@ -100,9 +102,10 @@
const mimeType = blob.type || 'image/png';
// create file name based on the MIME type, alt should be a valid file name with extension
const fileName = alt
? `${alt.replaceAll('.', '')}.${mimeType.split('/')[1]}`
: 'download.png';
const fileName = `${$i18n
.t('Generated Image')
.toLowerCase()
.replace(/ /g, '_')}.${mimeType.split('/')[1]}`;
// Use FileSaver to save the blob
saveAs(blob, fileName);
@@ -119,9 +122,10 @@
const blobWithType = new Blob([blob], { type: mimeType });
// create file name based on the MIME type, alt should be a valid file name with extension
const fileName = alt
? `${alt.replaceAll('.', '')}.${mimeType.split('/')[1]}`
: 'download.png';
const fileName = `${$i18n
.t('Generated Image')
.toLowerCase()
.replace(/ /g, '_')}.${mimeType.split('/')[1]}`;
// Use FileSaver to save the blob
saveAs(blobWithType, fileName);
@@ -146,9 +150,10 @@
const blobWithType = new Blob([blob], { type: mimeType });
// create file name based on the MIME type, alt should be a valid file name with extension
const fileName = alt
? `${alt.replaceAll('.', '')}.${mimeType.split('/')[1]}`
: 'download.png';
const fileName = `${$i18n
.t('Generated Image')
.toLowerCase()
.replace(/ /g, '_')}.${mimeType.split('/')[1]}`;
// Use FileSaver to save the blob
saveAs(blobWithType, fileName);
+25 -10
View File
@@ -149,10 +149,15 @@
export let onChange = (e) => {};
// create a lowlight instance with all languages loaded
const lowlight = createLowlight(hljs.listLanguages().reduce((obj, lang) => {
obj[lang] = () => hljs.getLanguage(lang);
return obj;
}, {} as Record<string, any>));
const lowlight = createLowlight(
hljs.listLanguages().reduce(
(obj, lang) => {
obj[lang] = () => hljs.getLanguage(lang);
return obj;
},
{} as Record<string, any>
)
);
export let editor: Editor | null = null;
@@ -163,7 +168,7 @@
export let documentId = '';
export let className = 'input-prose';
export let placeholder = 'Type here...';
export let placeholder = $i18n.t('Type here...');
let _placeholder = placeholder;
$: if (placeholder !== _placeholder) {
@@ -501,9 +506,14 @@
export const focus = () => {
if (editor) {
editor.view.focus();
// Scroll to the current selection
editor.view.dispatch(editor.view.state.tr.scrollIntoView());
try {
editor.view?.focus();
// Scroll to the current selection
editor.view?.dispatch(editor.view.state.tr.scrollIntoView());
} catch (e) {
// sometimes focusing throws an error, ignore
console.warn('Error focusing editor', e);
}
}
};
@@ -679,7 +689,7 @@
link: link
}),
...(dragHandle ? [ListItemDragHandle] : []),
Placeholder.configure({ placeholder: () => _placeholder }),
Placeholder.configure({ placeholder: () => _placeholder, showOnlyWhenEditable: false }),
SelectionDecoration,
...(richText
@@ -1113,4 +1123,9 @@
</div>
{/if}
<div bind:this={element} class="relative w-full min-w-full h-full min-h-fit {className}" />
<div
bind:this={element}
class="relative w-full min-w-full h-full min-h-fit {className} {!editable
? 'cursor-not-allowed'
: ''}"
/>
@@ -1,4 +1,7 @@
<script lang="ts">
import DOMPurify from 'dompurify';
import { marked } from 'marked';
import { getAdminDetails } from '$lib/apis/auths';
import { onMount, tick, getContext } from 'svelte';
import { config } from '$lib/stores';
@@ -38,7 +41,11 @@
style="white-space: pre-wrap;"
>
{#if ($config?.ui?.pending_user_overlay_content ?? '').trim() !== ''}
{$config.ui.pending_user_overlay_content}
{@html marked.parse(
DOMPurify.sanitize(
($config?.ui?.pending_user_overlay_content ?? '').replace(/\n/g, '<br>')
)
)}
{:else}
{$i18n.t('Your account status is currently pending activation.')}{'\n'}{$i18n.t(
'To access the WebUI, please reach out to the administrator. Admins can manage user statuses from the Admin Panel.'
+82 -12
View File
@@ -12,14 +12,28 @@
import calendar from 'dayjs/plugin/calendar';
import Loader from '../common/Loader.svelte';
import { createMessagesList } from '$lib/utils';
import { user } from '$lib/stores';
import { config, user } from '$lib/stores';
import Messages from '../chat/Messages.svelte';
import { goto } from '$app/navigation';
import PencilSquare from '../icons/PencilSquare.svelte';
import Note from '../icons/Note.svelte';
dayjs.extend(calendar);
export let show = false;
export let onClose = () => {};
let actions = [
{
label: 'Start a new conversation',
onClick: async () => {
await goto(`/${query ? `?q=${query}` : ''}`);
show = false;
onClose();
},
icon: PencilSquare
}
];
let query = '';
let page = 1;
@@ -55,7 +69,13 @@
return;
}
const chatId = chatList[selectedIdx].id;
const selectedChatIdx = selectedIdx - actions.length;
if (selectedChatIdx < 0) {
selectedChat = null;
return;
}
const chatId = chatList[selectedChatIdx].id;
const chat = await getChatById(localStorage.token, chatId).catch(async (error) => {
return null;
@@ -162,13 +182,13 @@
if (e.code === 'Escape') {
show = false;
onClose();
} else if (e.code === 'Enter' && (chatList ?? []).length > 0) {
} else if (e.code === 'Enter') {
const item = document.querySelector(`[data-arrow-selected="true"]`);
if (item) {
item?.click();
show = false;
}
show = false;
return;
} else if (e.code === 'ArrowDown') {
const searchInput = document.getElementById('search-input');
@@ -182,7 +202,7 @@
}
}
selectedIdx = Math.min(selectedIdx + 1, (chatList ?? []).length - 1);
selectedIdx = Math.min(selectedIdx + 1, (chatList ?? []).length - 1 + actions.length);
} else if (e.code === 'ArrowUp') {
if (selectedIdx === 0) {
const searchInput = document.getElementById('search-input');
@@ -205,6 +225,24 @@
};
onMount(() => {
actions = [
...actions,
...(($config?.features?.enable_notes ?? false) &&
($user?.role === 'admin' || ($user?.permissions?.features?.notes ?? true))
? [
{
label: 'Create a new note',
onClick: async () => {
await goto(`/notes${query ? `?content=${query}` : ''}`);
show = false;
onClose();
},
icon: Note
}
]
: [])
];
document.addEventListener('keydown', onKeyDown);
});
@@ -240,7 +278,7 @@
show = false;
return;
} else if (e.code === 'ArrowDown') {
selectedIdx = Math.min(selectedIdx + 1, (chatList ?? []).length - 1);
selectedIdx = Math.min(selectedIdx + 1, (chatList ?? []).length - 1 + actions.length);
} else if (e.code === 'ArrowUp') {
selectedIdx = Math.max(selectedIdx - 1, 0);
} else {
@@ -257,11 +295,43 @@
<div class="flex px-4 pb-1">
<div
class="flex flex-col overflow-y-auto h-96 md:h-[40rem] max-h-full scrollbar-hidden w-full flex-1"
class="flex flex-col overflow-y-auto h-96 md:h-[40rem] max-h-full scrollbar-hidden w-full flex-1 pr-2"
>
<div class="w-full text-xs text-gray-500 dark:text-gray-500 font-medium pb-2 px-2">
{$i18n.t('Actions')}
</div>
{#each actions as action, idx (action.label)}
<button
class=" w-full flex items-center rounded-xl text-sm py-2 px-3 hover:bg-gray-50 dark:hover:bg-gray-850 {selectedIdx ===
idx
? 'bg-gray-50 dark:bg-gray-850'
: ''}"
data-arrow-selected={selectedIdx === idx ? 'true' : undefined}
dragabble="false"
on:mouseenter={() => {
selectedIdx = idx;
}}
on:click={async () => {
await action.onClick();
}}
>
<div class="pr-2">
<svelte:component this={action.icon} />
</div>
<div class=" flex-1 text-left">
<div class="text-ellipsis line-clamp-1 w-full">
{$i18n.t(action.label)}
</div>
</div>
</button>
{/each}
{#if chatList}
<hr class="border-gray-50 dark:border-gray-850 my-3" />
{#if chatList.length === 0}
<div class="text-xs text-gray-500 dark:text-gray-400 text-center px-5">
<div class="text-xs text-gray-500 dark:text-gray-400 text-center px-5 py-4">
{$i18n.t('No results found')}
</div>
{/if}
@@ -296,15 +366,15 @@
{/if}
<a
class=" w-full flex justify-between items-center rounded-lg text-sm py-2 px-3 hover:bg-gray-50 dark:hover:bg-gray-850 {selectedIdx ===
idx
class=" w-full flex justify-between items-center rounded-xl text-sm py-2 px-3 hover:bg-gray-50 dark:hover:bg-gray-850 {selectedIdx ===
idx + actions.length
? 'bg-gray-50 dark:bg-gray-850'
: ''}"
href="/c/{chat.id}"
draggable="false"
data-arrow-selected={selectedIdx === idx ? 'true' : undefined}
data-arrow-selected={selectedIdx === idx + actions.length ? 'true' : undefined}
on:mouseenter={() => {
selectedIdx = idx;
selectedIdx = idx + actions.length;
}}
on:click={async () => {
await goto(`/c/${chat.id}`);
@@ -126,7 +126,7 @@
<div class="my-2 -mx-2">
<div class="px-4 py-3 bg-gray-50 dark:bg-gray-950 rounded-3xl">
<AccessControl bind:accessControl />
<AccessControl bind:accessControl accessRoles={['read', 'write']} />
</div>
</div>
@@ -339,10 +339,6 @@
}, 500);
};
$: if (open) {
isExpandedUpdateDebounceHandler();
}
const renameHandler = async () => {
console.log('Edit');
await tick();
@@ -471,6 +467,7 @@
on:click={(e) => {
e.stopPropagation();
open = !open;
isExpandedUpdateDebounceHandler();
}}
>
{#if folders[folderId]?.meta?.icon}
+2 -1
View File
@@ -872,7 +872,8 @@ Provide the enhanced notes in markdown format. Use markdown syntax for headings,
}
if (!selectedModelId) {
selectedModelId = $models.at(0)?.id || '';
selectedModelId =
$models.filter((model) => !(model?.info?.meta?.hidden ?? false)).at(0)?.id || '';
}
const dropzoneElement = document.getElementById('note-editor');
@@ -423,7 +423,7 @@ Based on the user's instruction, update and enhance the existing notes or select
class=" bg-transparent rounded-lg py-1 px-2 -mx-0.5 text-sm outline-hidden w-full text-right pr-5"
bind:value={selectedModelId}
>
{#each $models as model}
{#each $models.filter((model) => !(model?.info?.meta?.hidden ?? false)) as model}
<option value={model.id} class="bg-gray-50 dark:bg-gray-700"
>{model.name}</option
>
+14 -4
View File
@@ -1,4 +1,6 @@
<script lang="ts">
import { marked } from 'marked';
import { toast } from 'svelte-sonner';
import fileSaver from 'file-saver';
import Fuse from 'fuse.js';
@@ -26,6 +28,7 @@
// Assuming $i18n.languages is an array of language codes
$: loadLocale($i18n.languages);
import { page } from '$app/stores';
import { goto } from '$app/navigation';
import { onMount, getContext, onDestroy } from 'svelte';
import { WEBUI_NAME, config, prompts as _prompts, user } from '$lib/stores';
@@ -42,7 +45,6 @@
import Tooltip from '../common/Tooltip.svelte';
import NoteMenu from './Notes/NoteMenu.svelte';
import FilesOverlay from '../chat/MessageInput/FilesOverlay.svelte';
import { marked } from 'marked';
import XMark from '../icons/XMark.svelte';
const i18n = getContext('i18n');
@@ -97,7 +99,7 @@
});
};
const createNoteHandler = async () => {
const createNoteHandler = async (content?: string) => {
// $i18n.t('New Note'),
const res = await createNewNote(localStorage.token, {
// YYYY-MM-DD
@@ -105,8 +107,8 @@
data: {
content: {
json: null,
html: '',
md: ''
html: content ?? '',
md: content ?? ''
}
},
meta: null,
@@ -301,6 +303,14 @@
});
onMount(async () => {
if ($page.url.searchParams.get('content')) {
const content = $page.url.searchParams.get('content') ?? '';
if (content) {
createNoteHandler(content);
return;
}
}
await init();
loaded = true;
@@ -4,13 +4,14 @@
const i18n = getContext('i18n');
import { goto } from '$app/navigation';
import { user } from '$lib/stores';
import CodeEditor from '$lib/components/common/CodeEditor.svelte';
import ConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
import Badge from '$lib/components/common/Badge.svelte';
import ChevronLeft from '$lib/components/icons/ChevronLeft.svelte';
import Tooltip from '$lib/components/common/Tooltip.svelte';
import LockClosed from '$lib/components/icons/LockClosed.svelte';
import AccessControlModal from '../common/AccessControlModal.svelte';
import { user } from '$lib/stores';
let formElement = null;
let loading = false;
@@ -285,22 +286,20 @@ class Tools:
</div>
<div class="mb-2 flex-1 overflow-auto h-0 rounded-lg">
{#await import('$lib/components/common/CodeEditor.svelte') then { default: CodeEditor }}
<CodeEditor
bind:this={codeEditor}
value={content}
lang="python"
{boilerplate}
onChange={(e) => {
_content = e;
}}
onSave={async () => {
if (formElement) {
formElement.requestSubmit();
}
}}
/>
{/await}
<CodeEditor
bind:this={codeEditor}
value={content}
lang="python"
{boilerplate}
onChange={(e) => {
_content = e;
}}
onSave={async () => {
if (formElement) {
formElement.requestSubmit();
}
}}
/>
</div>
<div class="pb-3 flex justify-between">
@@ -11,6 +11,20 @@
updateFunctionValvesById
} from '$lib/apis/functions';
import { getToolValvesById, getToolValvesSpecById, updateToolValvesById } from '$lib/apis/tools';
import {
getUserValvesSpecById as getToolUserValvesSpecById,
getUserValvesById as getToolUserValvesById,
updateUserValvesById as updateToolUserValvesById,
getTools
} from '$lib/apis/tools';
import {
getUserValvesSpecById as getFunctionUserValvesSpecById,
getUserValvesById as getFunctionUserValvesById,
updateUserValvesById as updateFunctionUserValvesById,
getFunctions
} from '$lib/apis/functions';
import Spinner from '../../common/Spinner.svelte';
import Switch from '$lib/components/common/Switch.svelte';
import Valves from '$lib/components/common/Valves.svelte';
@@ -23,6 +37,7 @@
export let type = 'tool';
export let id = null;
export let userValves = false;
let saving = false;
let loading = false;
@@ -43,14 +58,28 @@
let res = null;
if (type === 'tool') {
res = await updateToolValvesById(localStorage.token, id, valves).catch((error) => {
toast.error(`${error}`);
});
} else if (type === 'function') {
res = await updateFunctionValvesById(localStorage.token, id, valves).catch((error) => {
toast.error(`${error}`);
});
if (userValves) {
if (type === 'tool') {
res = await updateToolUserValvesById(localStorage.token, id, valves).catch((error) => {
toast.error(`${error}`);
});
} else if (type === 'function') {
res = await updateFunctionUserValvesById(localStorage.token, id, valves).catch(
(error) => {
toast.error(`${error}`);
}
);
}
} else {
if (type === 'tool') {
res = await updateToolValvesById(localStorage.token, id, valves).catch((error) => {
toast.error(`${error}`);
});
} else if (type === 'function') {
res = await updateFunctionValvesById(localStorage.token, id, valves).catch((error) => {
toast.error(`${error}`);
});
}
}
if (res) {
@@ -67,28 +96,43 @@
valves = {};
valvesSpec = null;
if (type === 'tool') {
valves = await getToolValvesById(localStorage.token, id);
valvesSpec = await getToolValvesSpecById(localStorage.token, id);
} else if (type === 'function') {
valves = await getFunctionValvesById(localStorage.token, id);
valvesSpec = await getFunctionValvesSpecById(localStorage.token, id);
}
if (!valves) {
valves = {};
}
if (valvesSpec) {
// Convert array to string
for (const property in valvesSpec.properties) {
if (valvesSpec.properties[property]?.type === 'array') {
valves[property] = (valves[property] ?? []).join(',');
try {
if (userValves) {
if (type === 'tool') {
valves = await getToolUserValvesById(localStorage.token, id);
valvesSpec = await getToolUserValvesSpecById(localStorage.token, id);
} else if (type === 'function') {
valves = await getFunctionUserValvesById(localStorage.token, id);
valvesSpec = await getFunctionUserValvesSpecById(localStorage.token, id);
}
} else {
if (type === 'tool') {
valves = await getToolValvesById(localStorage.token, id);
valvesSpec = await getToolValvesSpecById(localStorage.token, id);
} else if (type === 'function') {
valves = await getFunctionValvesById(localStorage.token, id);
valvesSpec = await getFunctionValvesSpecById(localStorage.token, id);
}
}
}
loading = false;
if (!valves) {
valves = {};
}
if (valvesSpec) {
// Convert array to string
for (const property in valvesSpec.properties) {
if (valvesSpec.properties[property]?.type === 'array') {
valves[property] = (valves[property] ?? []).join(',');
}
}
}
loading = false;
} catch (e) {
toast.error(`Error fetching valves`);
show = false;
}
};
$: if (show) {