refac
This commit is contained in:
1 parent
9e2536aa57
commit
588f129695
17 files changed
+54
-33
No files matched your search
@@ -580,7 +580,7 @@ async def update_note_by_id(
|
|||||||
note.is_pinned = note.id in pinned_note_ids
|
note.is_pinned = note.id in pinned_note_ids
|
||||||
|
|
||||||
await sio.emit(
|
await sio.emit(
|
||||||
'note-events',
|
'events:note',
|
||||||
note.model_dump(),
|
note.model_dump(),
|
||||||
to=f'note:{note.id}',
|
to=f'note:{note.id}',
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ async def _has_write_access_to_note(note, user_id: str) -> bool:
|
|||||||
|
|
||||||
|
|
||||||
async def _emit_note_updated(request: Request, user: dict, note) -> None:
|
async def _emit_note_updated(request: Request, user: dict, note) -> None:
|
||||||
await sio.emit('note-events', note.model_dump(), to=f'note:{note.id}')
|
await sio.emit('events:note', note.model_dump(), to=f'note:{note.id}')
|
||||||
await publish_event(
|
await publish_event(
|
||||||
request,
|
request,
|
||||||
EVENTS.NOTE_UPDATED,
|
EVENTS.NOTE_UPDATED,
|
||||||
|
|||||||
@@ -2523,10 +2523,16 @@ async def process_chat_payload(request, form_data, user, metadata, model):
|
|||||||
)
|
)
|
||||||
available_skills = []
|
available_skills = []
|
||||||
view_skill_ids = []
|
view_skill_ids = []
|
||||||
|
chat = None
|
||||||
|
if metadata.get('chat_id') and not metadata['chat_id'].startswith(('local:', 'channel:')):
|
||||||
|
chat = await Chats.get_chat_by_id(metadata['chat_id'])
|
||||||
use_builtin_tools = (
|
use_builtin_tools = (
|
||||||
bool(metadata.get('session_id'))
|
(chat and (chat.meta or {}).get('internal') is True and (chat.meta or {}).get('type') == 'note')
|
||||||
and metadata.get('params', {}).get('function_calling') != 'legacy'
|
or (
|
||||||
and (model.get('info', {}).get('meta', {}).get('capabilities') or {}).get('builtin_tools', True)
|
bool(metadata.get('session_id'))
|
||||||
|
and metadata.get('params', {}).get('function_calling') != 'legacy'
|
||||||
|
and (model.get('info', {}).get('meta', {}).get('capabilities') or {}).get('builtin_tools', True)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
if skill_ids:
|
if skill_ids:
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ from open_webui.env import (
|
|||||||
REDIS_KEY_PREFIX,
|
REDIS_KEY_PREFIX,
|
||||||
)
|
)
|
||||||
from open_webui.models.access_grants import AccessGrants
|
from open_webui.models.access_grants import AccessGrants
|
||||||
|
from open_webui.models.chats import Chats
|
||||||
from open_webui.models.config import Config
|
from open_webui.models.config import Config
|
||||||
from open_webui.models.groups import Groups
|
from open_webui.models.groups import Groups
|
||||||
from open_webui.models.tools import Tools
|
from open_webui.models.tools import Tools
|
||||||
@@ -606,7 +607,8 @@ async def get_builtin_tools(
|
|||||||
):
|
):
|
||||||
builtin_functions.extend([search_web, fetch_url])
|
builtin_functions.extend([search_web, fetch_url])
|
||||||
|
|
||||||
# Add image generation/edit tools if builtin category enabled AND enabled globally AND model has image_generation capability
|
# Add image generation/edit tools if builtin category enabled,
|
||||||
|
# globally enabled, and allowed by model capability.
|
||||||
if (
|
if (
|
||||||
is_builtin_tool_enabled('image_generation')
|
is_builtin_tool_enabled('image_generation')
|
||||||
and config.get('image_generation.enable')
|
and config.get('image_generation.enable')
|
||||||
@@ -624,7 +626,8 @@ async def get_builtin_tools(
|
|||||||
):
|
):
|
||||||
builtin_functions.append(edit_image)
|
builtin_functions.append(edit_image)
|
||||||
|
|
||||||
# Add code interpreter tool if builtin category enabled AND enabled globally AND model has code_interpreter capability
|
# Add code interpreter tool if builtin category enabled,
|
||||||
|
# globally enabled, and allowed by model capability.
|
||||||
if (
|
if (
|
||||||
is_builtin_tool_enabled('code_interpreter')
|
is_builtin_tool_enabled('code_interpreter')
|
||||||
and config.get('code_interpreter.enable')
|
and config.get('code_interpreter.enable')
|
||||||
@@ -634,8 +637,17 @@ async def get_builtin_tools(
|
|||||||
):
|
):
|
||||||
builtin_functions.append(execute_code)
|
builtin_functions.append(execute_code)
|
||||||
|
|
||||||
|
metadata = extra_params.get('__metadata__') or {}
|
||||||
|
chat_id = metadata.get('chat_id') or ''
|
||||||
|
chat = None
|
||||||
|
if chat_id and not chat_id.startswith(('local:', 'channel:')):
|
||||||
|
chat = await Chats.get_chat_by_id(chat_id)
|
||||||
|
|
||||||
# Notes tools - search, view, create, and update user's notes
|
# Notes tools - search, view, create, and update user's notes
|
||||||
if is_builtin_tool_enabled('notes') and config.get('notes.enable') and await has_user_permission('notes'):
|
if (
|
||||||
|
(chat and (chat.meta or {}).get('internal') is True and (chat.meta or {}).get('type') == 'note')
|
||||||
|
or (is_builtin_tool_enabled('notes') and config.get('notes.enable') and await has_user_permission('notes'))
|
||||||
|
):
|
||||||
builtin_functions.extend([search_notes, view_note, write_note, replace_note_content, replace_note_text])
|
builtin_functions.extend([search_notes, view_note, write_note, replace_note_content, replace_note_text])
|
||||||
|
|
||||||
# Channels tools - search channels and messages
|
# Channels tools - search channels and messages
|
||||||
|
|||||||
+1
-1
@@ -122,7 +122,7 @@ textarea::-webkit-scrollbar-corner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.markdown-prose {
|
.markdown-prose {
|
||||||
@apply prose prose-sm dark:prose-invert max-w-none break-words font-normal leading-relaxed prose-p:mt-0 prose-p:mb-2 prose-p:font-normal prose-p:leading-relaxed prose-headings:mt-2 prose-headings:mb-1 prose-headings:font-normal prose-headings:leading-snug prose-h1:text-xl prose-h2:text-lg prose-h3:text-base prose-strong:font-medium prose-code:before:content-none prose-code:after:content-none prose-ul:my-2 prose-ol:my-2 prose-li:my-0.5 prose-li:font-normal prose-pre:my-3 prose-table:my-0 prose-blockquote:my-3 prose-blockquote:font-normal prose-hr:my-4 prose-hr:border-gray-50 prose-hr:dark:border-gray-850/30 prose-img:my-2 [&>:first-child]:mt-0 [&>:last-child]:mb-0 [&_p:first-child]:mt-0 [&_h1:first-child]:mt-0 [&_h2:first-child]:mt-0 [&_h3:first-child]:mt-0 [&_h4:first-child]:mt-0 [&_h5:first-child]:mt-0 [&_h6:first-child]:mt-0;
|
@apply prose prose-sm dark:prose-invert max-w-none break-words !text-[0.9375rem] font-normal leading-relaxed prose-p:mt-0 prose-p:mb-2 prose-p:font-normal prose-p:leading-relaxed prose-headings:mt-2 prose-headings:mb-1 prose-headings:font-normal prose-headings:leading-snug prose-h1:text-xl prose-h2:text-lg prose-h3:text-base prose-strong:font-medium prose-code:before:content-none prose-code:after:content-none prose-ul:my-2 prose-ol:my-2 prose-li:my-0.5 prose-li:font-normal prose-pre:my-3 prose-table:my-0 prose-blockquote:my-3 prose-blockquote:font-normal prose-hr:my-4 prose-hr:border-gray-50 prose-hr:dark:border-gray-850/30 prose-img:my-2 [&>:first-child]:mt-0 [&>:last-child]:mb-0 [&_p:first-child]:mt-0 [&_h1:first-child]:mt-0 [&_h2:first-child]:mt-0 [&_h3:first-child]:mt-0 [&_h4:first-child]:mt-0 [&_h5:first-child]:mt-0 [&_h6:first-child]:mt-0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-prose-sm {
|
.markdown-prose-sm {
|
||||||
|
|||||||
@@ -311,7 +311,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{#if extracted.plainContent}
|
{#if extracted.plainContent}
|
||||||
<div class="whitespace-pre-wrap text-sm">{extracted.plainContent}</div>
|
<div class="whitespace-pre-wrap text-[0.9375rem]">{extracted.plainContent}</div>
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -112,7 +112,7 @@
|
|||||||
<div {id} class="w-full">
|
<div {id} class="w-full">
|
||||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||||
<button
|
<button
|
||||||
class="w-fit py-1 text-left text-sm text-gray-500 hover:text-gray-700 dark:hover:text-gray-300 transition cursor-pointer"
|
class="w-fit py-1 text-left text-[0.9375rem] text-gray-500 hover:text-gray-700 dark:hover:text-gray-300 transition cursor-pointer"
|
||||||
aria-label={$i18n.t('Toggle details')}
|
aria-label={$i18n.t('Toggle details')}
|
||||||
aria-expanded={open}
|
aria-expanded={open}
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
<div class=" self-center text-sm font-normal line-clamp-1 flex gap-1 items-center">
|
<div class=" self-center text-[0.9375rem] font-normal line-clamp-1 flex gap-1 items-center">
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
@@ -270,7 +270,7 @@
|
|||||||
{#if di.type === 'message'}
|
{#if di.type === 'message'}
|
||||||
<textarea
|
<textarea
|
||||||
use:fitContent
|
use:fitContent
|
||||||
class="w-full bg-transparent outline-hidden resize-none overflow-hidden text-sm p-1.5 rounded-lg"
|
class="w-full bg-transparent outline-hidden resize-none overflow-hidden text-[0.9375rem] p-1.5 rounded-lg"
|
||||||
value={getMessageText(di.item)}
|
value={getMessageText(di.item)}
|
||||||
on:input={(e) => {
|
on:input={(e) => {
|
||||||
updateMessageText(di.indices[0], e.target.value);
|
updateMessageText(di.indices[0], e.target.value);
|
||||||
@@ -282,7 +282,7 @@
|
|||||||
{:else if di.type === 'reasoning'}
|
{:else if di.type === 'reasoning'}
|
||||||
<textarea
|
<textarea
|
||||||
use:fitContent
|
use:fitContent
|
||||||
class="w-full bg-transparent outline-hidden resize-none overflow-hidden text-sm text-gray-500 dark:text-gray-400 p-1.5 rounded-lg"
|
class="w-full bg-transparent outline-hidden resize-none overflow-hidden text-[0.9375rem] text-gray-500 dark:text-gray-400 p-1.5 rounded-lg"
|
||||||
value={getReasoningText(di.item)}
|
value={getReasoningText(di.item)}
|
||||||
on:input={(e) => {
|
on:input={(e) => {
|
||||||
updateReasoningText(di.indices[0], e.target.value);
|
updateReasoningText(di.indices[0], e.target.value);
|
||||||
@@ -292,7 +292,7 @@
|
|||||||
rows="1"
|
rows="1"
|
||||||
/>
|
/>
|
||||||
{:else if di.type === 'function_call'}
|
{:else if di.type === 'function_call'}
|
||||||
<div class="text-sm p-1.5 text-gray-500 dark:text-gray-400">
|
<div class="text-[0.9375rem] p-1.5 text-gray-500 dark:text-gray-400">
|
||||||
{#if di.item.arguments}
|
{#if di.item.arguments}
|
||||||
<pre
|
<pre
|
||||||
class="text-xs font-mono whitespace-pre-wrap overflow-x-auto pb-0.5">{formatArgs(
|
class="text-xs font-mono whitespace-pre-wrap overflow-x-auto pb-0.5">{formatArgs(
|
||||||
@@ -309,7 +309,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{:else if di.type === 'code_interpreter'}
|
{:else if di.type === 'code_interpreter'}
|
||||||
<div class="text-sm p-1.5 text-gray-500 dark:text-gray-400">
|
<div class="text-[0.9375rem] p-1.5 text-gray-500 dark:text-gray-400">
|
||||||
{#if di.item.code}
|
{#if di.item.code}
|
||||||
<pre class="text-xs font-mono whitespace-pre overflow-x-auto">{di.item.code}</pre>
|
<pre class="text-xs font-mono whitespace-pre overflow-x-auto">{di.item.code}</pre>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -322,7 +322,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{:else if di.type === 'openai_tool'}
|
{:else if di.type === 'openai_tool'}
|
||||||
<div class="text-sm p-1.5 text-gray-500 dark:text-gray-400">
|
<div class="text-[0.9375rem] p-1.5 text-gray-500 dark:text-gray-400">
|
||||||
{#if di.item.action?.queries || di.item.queries}
|
{#if di.item.action?.queries || di.item.queries}
|
||||||
<span class="text-xs"
|
<span class="text-xs"
|
||||||
>{(di.item.action?.queries ?? di.item.queries ?? []).join(', ')}</span
|
>{(di.item.action?.queries ?? di.item.queries ?? []).join(', ')}</span
|
||||||
|
|||||||
@@ -736,7 +736,7 @@
|
|||||||
<textarea
|
<textarea
|
||||||
id="message-edit-{message.id}"
|
id="message-edit-{message.id}"
|
||||||
bind:this={editTextAreaElement}
|
bind:this={editTextAreaElement}
|
||||||
class=" bg-transparent outline-hidden w-full resize-none"
|
class=" bg-transparent outline-hidden w-full resize-none text-[0.9375rem]"
|
||||||
bind:value={editedContent}
|
bind:value={editedContent}
|
||||||
on:input={(e) => {
|
on:input={(e) => {
|
||||||
const messagesContainer = document.getElementById('messages-container');
|
const messagesContainer = document.getElementById('messages-container');
|
||||||
|
|||||||
@@ -29,7 +29,7 @@
|
|||||||
|
|
||||||
{#if history && history.length > 0}
|
{#if history && history.length > 0}
|
||||||
{#if status?.hidden !== true}
|
{#if status?.hidden !== true}
|
||||||
<div class="text-sm flex flex-col w-full">
|
<div class="text-[0.9375rem] flex flex-col w-full">
|
||||||
<button
|
<button
|
||||||
class="w-full"
|
class="w-full"
|
||||||
aria-label={$i18n.t('Toggle status history')}
|
aria-label={$i18n.t('Toggle status history')}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
<div
|
<div
|
||||||
class="{(done || status?.done) === false
|
class="{(done || status?.done) === false
|
||||||
? 'shimmer'
|
? 'shimmer'
|
||||||
: ''} text-base line-clamp-1 text-wrap"
|
: ''} text-[0.9375rem] line-clamp-1 text-wrap"
|
||||||
>
|
>
|
||||||
<!-- $i18n.t("Generating search query") -->
|
<!-- $i18n.t("Generating search query") -->
|
||||||
<!-- $i18n.t("No search query generated") -->
|
<!-- $i18n.t("No search query generated") -->
|
||||||
@@ -41,7 +41,7 @@
|
|||||||
<div
|
<div
|
||||||
class="{(done || status?.done) === false
|
class="{(done || status?.done) === false
|
||||||
? 'shimmer'
|
? 'shimmer'
|
||||||
: ''} text-gray-500 dark:text-gray-500 text-base line-clamp-1 text-wrap"
|
: ''} text-gray-500 dark:text-gray-500 text-[0.9375rem] line-clamp-1 text-wrap"
|
||||||
>
|
>
|
||||||
{$i18n.t(`Searching Knowledge for "{{searchQuery}}"`, {
|
{$i18n.t(`Searching Knowledge for "{{searchQuery}}"`, {
|
||||||
searchQuery: status.query
|
searchQuery: status.query
|
||||||
@@ -53,7 +53,7 @@
|
|||||||
<div
|
<div
|
||||||
class="{(done || status?.done) === false
|
class="{(done || status?.done) === false
|
||||||
? 'shimmer'
|
? 'shimmer'
|
||||||
: ''} text-gray-500 dark:text-gray-500 text-base line-clamp-1 text-wrap"
|
: ''} text-gray-500 dark:text-gray-500 text-[0.9375rem] line-clamp-1 text-wrap"
|
||||||
>
|
>
|
||||||
{$i18n.t(`Searching`)}
|
{$i18n.t(`Searching`)}
|
||||||
</div>
|
</div>
|
||||||
@@ -79,7 +79,7 @@
|
|||||||
<div
|
<div
|
||||||
class="{(done || status?.done) === false
|
class="{(done || status?.done) === false
|
||||||
? 'shimmer'
|
? 'shimmer'
|
||||||
: ''} text-gray-500 dark:text-gray-500 text-base line-clamp-1 text-wrap"
|
: ''} text-gray-500 dark:text-gray-500 text-[0.9375rem] line-clamp-1 text-wrap"
|
||||||
>
|
>
|
||||||
{$i18n.t(`Querying`)}
|
{$i18n.t(`Querying`)}
|
||||||
</div>
|
</div>
|
||||||
@@ -105,7 +105,7 @@
|
|||||||
<div
|
<div
|
||||||
class="{(done || status?.done) === false
|
class="{(done || status?.done) === false
|
||||||
? 'shimmer'
|
? 'shimmer'
|
||||||
: ''} text-gray-500 dark:text-gray-500 text-base line-clamp-1 text-wrap"
|
: ''} text-gray-500 dark:text-gray-500 text-[0.9375rem] line-clamp-1 text-wrap"
|
||||||
>
|
>
|
||||||
{#if status.count === 0}
|
{#if status.count === 0}
|
||||||
{$i18n.t('No sources found')}
|
{$i18n.t('No sources found')}
|
||||||
@@ -127,7 +127,7 @@
|
|||||||
<div
|
<div
|
||||||
class="{(done || status?.done) === false
|
class="{(done || status?.done) === false
|
||||||
? 'shimmer'
|
? 'shimmer'
|
||||||
: ''} text-gray-500 dark:text-gray-500 text-base line-clamp-1 text-wrap"
|
: ''} text-gray-500 dark:text-gray-500 text-[0.9375rem] line-clamp-1 text-wrap"
|
||||||
>
|
>
|
||||||
<!-- $i18n.t(`Searching "{{searchQuery}}"`) -->
|
<!-- $i18n.t(`Searching "{{searchQuery}}"`) -->
|
||||||
{#if status?.description?.includes('{{searchQuery}}')}
|
{#if status?.description?.includes('{{searchQuery}}')}
|
||||||
|
|||||||
@@ -57,7 +57,7 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="whitespace-pre-wrap text-sm">{displayItem.text}</div>
|
<div class="whitespace-pre-wrap text-[0.9375rem]">{displayItem.text}</div>
|
||||||
{/if}
|
{/if}
|
||||||
{:else if displayItem.type === 'detail_group'}
|
{:else if displayItem.type === 'detail_group'}
|
||||||
<ConsecutiveDetailsGroup
|
<ConsecutiveDetailsGroup
|
||||||
|
|||||||
@@ -159,7 +159,7 @@
|
|||||||
<Name>
|
<Name>
|
||||||
{#if message.user}
|
{#if message.user}
|
||||||
{$i18n.t('You')}
|
{$i18n.t('You')}
|
||||||
<span class=" text-gray-500 text-sm font-normal">{message?.user ?? ''}</span>
|
<span class=" text-gray-500 text-[0.9375rem] font-normal">{message?.user ?? ''}</span>
|
||||||
{:else if $settings.showUsername || $_user?.name !== user?.name}
|
{:else if $settings.showUsername || $_user?.name !== user?.name}
|
||||||
{user?.name ?? $i18n.t('You')}
|
{user?.name ?? $i18n.t('You')}
|
||||||
{:else}
|
{:else}
|
||||||
@@ -271,7 +271,7 @@
|
|||||||
<textarea
|
<textarea
|
||||||
id="message-edit-{message.id}"
|
id="message-edit-{message.id}"
|
||||||
bind:this={messageEditTextAreaElement}
|
bind:this={messageEditTextAreaElement}
|
||||||
class=" bg-transparent outline-hidden w-full resize-none text-sm"
|
class=" bg-transparent outline-hidden w-full resize-none text-[0.9375rem]"
|
||||||
bind:value={editedContent}
|
bind:value={editedContent}
|
||||||
on:input={(e) => {
|
on:input={(e) => {
|
||||||
const messagesContainer = document.getElementById('messages-container');
|
const messagesContainer = document.getElementById('messages-container');
|
||||||
@@ -358,7 +358,10 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="whitespace-pre-wrap text-sm" dir={$settings?.chatDirection ?? 'auto'}>
|
<div
|
||||||
|
class="whitespace-pre-wrap text-[0.9375rem]"
|
||||||
|
dir={$settings?.chatDirection ?? 'auto'}
|
||||||
|
>
|
||||||
{message.content}
|
{message.content}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -40,7 +40,7 @@
|
|||||||
|
|
||||||
export let className = '';
|
export let className = '';
|
||||||
export let buttonClassName =
|
export let buttonClassName =
|
||||||
'w-fit py-1 text-sm text-gray-500 hover:text-gray-700 dark:hover:text-gray-300 transition';
|
'w-fit py-1 text-[0.9375rem] text-gray-500 hover:text-gray-700 dark:hover:text-gray-300 transition';
|
||||||
|
|
||||||
export let id = '';
|
export let id = '';
|
||||||
export let title = null;
|
export let title = null;
|
||||||
|
|||||||
@@ -38,7 +38,7 @@
|
|||||||
|
|
||||||
$: if (!open) expandedResult = false;
|
$: if (!open) expandedResult = false;
|
||||||
export let buttonClassName =
|
export let buttonClassName =
|
||||||
'w-fit py-1 text-sm text-gray-500 hover:text-gray-700 dark:hover:text-gray-300 transition';
|
'w-fit py-1 text-[0.9375rem] text-gray-500 hover:text-gray-700 dark:hover:text-gray-300 transition';
|
||||||
|
|
||||||
const componentId = id || uuidv4();
|
const componentId = id || uuidv4();
|
||||||
|
|
||||||
|
|||||||
@@ -193,7 +193,7 @@
|
|||||||
token: localStorage.token
|
token: localStorage.token
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
$socket?.on('note-events', noteEventHandler);
|
$socket?.on('events:note', noteEventHandler);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
goto('/');
|
goto('/');
|
||||||
@@ -829,7 +829,7 @@ ${content}
|
|||||||
|
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
console.log('destroy');
|
console.log('destroy');
|
||||||
$socket?.off('note-events', noteEventHandler);
|
$socket?.off('events:note', noteEventHandler);
|
||||||
|
|
||||||
const dropzoneElement = document.getElementById('note-editor');
|
const dropzoneElement = document.getElementById('note-editor');
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user