mirror of
https://github.com/open-webui/open-webui.git
synced 2026-10-09 03:18:18 +00:00
Merge branch 'open-webui:dev' into dev
This commit is contained in:
commit
fad7514e42
13 changed files with 143 additions and 131 deletions
|
|
@ -3462,6 +3462,12 @@ IMAGE_GENERATION_MODEL = PersistentConfig(
|
|||
os.getenv("IMAGE_GENERATION_MODEL", ""),
|
||||
)
|
||||
|
||||
# Regex pattern for models that support IMAGE_SIZE = "auto".
|
||||
IMAGE_AUTO_SIZE_MODELS_REGEX_PATTERN = os.getenv("IMAGE_AUTO_SIZE_MODELS_REGEX_PATTERN", "^gpt-image")
|
||||
|
||||
# Regex pattern for models that return URLs instead of base64 data.
|
||||
IMAGE_URL_RESPONSE_MODELS_REGEX_PATTERN = os.getenv("IMAGE_URL_RESPONSE_MODELS_REGEX_PATTERN", "^gpt-image")
|
||||
|
||||
IMAGE_SIZE = PersistentConfig(
|
||||
"IMAGE_SIZE", "image_generation.size", os.getenv("IMAGE_SIZE", "512x512")
|
||||
)
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import requests
|
|||
from fastapi import APIRouter, Depends, HTTPException, Request, UploadFile
|
||||
from fastapi.responses import FileResponse
|
||||
|
||||
from open_webui.config import CACHE_DIR
|
||||
from open_webui.config import CACHE_DIR, IMAGE_AUTO_SIZE_MODELS_REGEX_PATTERN, IMAGE_URL_RESPONSE_MODELS_REGEX_PATTERN
|
||||
from open_webui.constants import ERROR_MESSAGES
|
||||
from open_webui.retrieval.web.utils import validate_url
|
||||
from open_webui.env import ENABLE_FORWARD_USER_INFO_HEADERS
|
||||
|
|
@ -201,12 +201,12 @@ async def update_config(
|
|||
set_image_model(request, form_data.IMAGE_GENERATION_MODEL)
|
||||
if (
|
||||
form_data.IMAGE_SIZE == "auto"
|
||||
and not form_data.IMAGE_GENERATION_MODEL.startswith("gpt-image")
|
||||
and not re.match(IMAGE_AUTO_SIZE_MODELS_REGEX_PATTERN, form_data.IMAGE_GENERATION_MODEL)
|
||||
):
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=ERROR_MESSAGES.INCORRECT_FORMAT(
|
||||
" (auto is only allowed with gpt-image models)."
|
||||
f" (auto is only allowed with models matching {IMAGE_AUTO_SIZE_MODELS_REGEX_PATTERN})."
|
||||
),
|
||||
)
|
||||
|
||||
|
|
@ -610,9 +610,7 @@ async def image_generations(
|
|||
),
|
||||
**(
|
||||
{}
|
||||
if request.app.state.config.IMAGE_GENERATION_MODEL.startswith(
|
||||
"gpt-image"
|
||||
)
|
||||
if re.match(IMAGE_URL_RESPONSE_MODELS_REGEX_PATTERN, request.app.state.config.IMAGE_GENERATION_MODEL)
|
||||
else {"response_format": "b64_json"}
|
||||
),
|
||||
**(
|
||||
|
|
@ -949,7 +947,7 @@ async def image_edits(
|
|||
**({"size": size} if size else {}),
|
||||
**(
|
||||
{}
|
||||
if request.app.state.config.IMAGE_EDIT_MODEL.startswith("gpt-image")
|
||||
if re.match(IMAGE_URL_RESPONSE_MODELS_REGEX_PATTERN, request.app.state.config.IMAGE_EDIT_MODEL)
|
||||
else {"response_format": "b64_json"}
|
||||
),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ from opentelemetry.instrumentation.sqlalchemy import SQLAlchemyInstrumentor
|
|||
from opentelemetry.instrumentation.aiohttp_client import AioHttpClientInstrumentor
|
||||
from opentelemetry.trace import Span, StatusCode
|
||||
from redis import Redis
|
||||
from redis.cluster import RedisCluster
|
||||
from requests import PreparedRequest, Response
|
||||
from sqlalchemy import Engine
|
||||
from fastapi import status
|
||||
|
|
@ -59,16 +60,28 @@ def response_hook(span: Span, request: PreparedRequest, response: Response):
|
|||
span.set_status(StatusCode.ERROR if response.status_code >= 400 else StatusCode.OK)
|
||||
|
||||
|
||||
def redis_request_hook(span: Span, instance: Redis, args, kwargs):
|
||||
def redis_request_hook(span: Span, instance: Union[Redis|RedisCluster], args, kwargs):
|
||||
"""
|
||||
Redis Request Hook
|
||||
"""
|
||||
|
||||
# In cluster mode, the instance can be of two types:
|
||||
# - redis.asyncio.cluster.RedisCluster
|
||||
# - redis.cluster.RedisCluster
|
||||
# Instead of checking the type, we check if the instance has a nodes_manager attribute.
|
||||
try:
|
||||
connection_kwargs: dict = instance.connection_pool.connection_kwargs
|
||||
host = connection_kwargs.get("host")
|
||||
port = connection_kwargs.get("port")
|
||||
db = connection_kwargs.get("db")
|
||||
db = ""
|
||||
if hasattr(instance, 'nodes_manager'):
|
||||
default_node = instance.nodes_manager.default_node
|
||||
if not default_node:
|
||||
return
|
||||
host = default_node.host
|
||||
port = default_node.port
|
||||
else:
|
||||
connection_kwargs: dict = instance.connection_pool.connection_kwargs
|
||||
host = connection_kwargs.get("host")
|
||||
port = connection_kwargs.get("port")
|
||||
db = connection_kwargs.get("db")
|
||||
span.set_attributes(
|
||||
{
|
||||
SpanAttributes.DB_INSTANCE: f"{host}/{db}",
|
||||
|
|
|
|||
|
|
@ -258,7 +258,7 @@
|
|||
class="flex cursor-pointer items-center justify-between hover:bg-gray-50 dark:hover:bg-gray-850 w-full px-3 py-2 rounded-xl transition"
|
||||
>
|
||||
<div class="self-center">
|
||||
<div class="text-sm font-medium">{$i18n.t('Leave a review for {{modelName}}', { modelName: message.model })}</div>
|
||||
<div class="text-sm font-medium">{$i18n.t('Leave a public review for {{modelName}}', { modelName: message.model })}</div>
|
||||
<div class="text-xs text-gray-500">{$i18n.t('Help the community discover great models')}</div>
|
||||
</div>
|
||||
<ChevronRight className="size-4" />
|
||||
|
|
|
|||
|
|
@ -96,6 +96,8 @@
|
|||
</DropdownMenu.Item>
|
||||
|
||||
{#if $config?.features.enable_community_sharing}
|
||||
<hr class="border-gray-50 dark:border-gray-850/30 my-1" />
|
||||
|
||||
<DropdownMenu.Item
|
||||
type="button"
|
||||
class="flex rounded-xl py-1.5 px-3 w-full hover:bg-gray-50 dark:hover:bg-gray-800 transition items-center gap-2"
|
||||
|
|
|
|||
|
|
@ -73,6 +73,7 @@
|
|||
<!-- {$i18n.t('New Chat')} -->
|
||||
<!-- {$i18n.t('New Temporary Chat')} -->
|
||||
<!-- {$i18n.t('Delete Chat')} -->
|
||||
<!-- {$i18n.t('Open Model Selector')} -->
|
||||
<!-- {$i18n.t('Search')} -->
|
||||
<!-- {$i18n.t('Open Settings')} -->
|
||||
<!-- {$i18n.t('Show Shortcuts')} -->
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
<script lang="ts">
|
||||
import TagInput from './Tags/TagInput.svelte';
|
||||
import TagList from './Tags/TagList.svelte';
|
||||
import { getContext, createEventDispatcher } from 'svelte';
|
||||
const dispatch = createEventDispatcher();
|
||||
|
|
@ -8,9 +7,19 @@
|
|||
|
||||
export let tags = [];
|
||||
export let suggestionTags = [];
|
||||
|
||||
let inputValue = '';
|
||||
|
||||
const addTag = () => {
|
||||
const value = inputValue.trim();
|
||||
if (value !== '') {
|
||||
dispatch('add', value);
|
||||
inputValue = '';
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<ul class="flex flex-row flex-wrap gap-[0.3rem] line-clamp-1">
|
||||
<div class="flex flex-wrap items-center gap-1 w-full">
|
||||
<TagList
|
||||
{tags}
|
||||
on:delete={(e) => {
|
||||
|
|
@ -18,11 +27,17 @@
|
|||
}}
|
||||
/>
|
||||
|
||||
<TagInput
|
||||
label={tags.length == 0 ? $i18n.t('Add Tags') : ''}
|
||||
{suggestionTags}
|
||||
on:add={(e) => {
|
||||
dispatch('add', e.detail);
|
||||
<input
|
||||
bind:value={inputValue}
|
||||
class="flex-1 min-w-24 {tags.length > 0
|
||||
? 'px-0.5'
|
||||
: ''} text-xs bg-transparent outline-hidden placeholder:text-gray-400 dark:placeholder:text-gray-500"
|
||||
placeholder={$i18n.t('Add a tag...')}
|
||||
on:keydown={(event) => {
|
||||
if (event.key === 'Enter' || event.key === ' ') {
|
||||
event.preventDefault();
|
||||
addTag();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</ul>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,95 +1,72 @@
|
|||
<script lang="ts">
|
||||
import { createEventDispatcher, getContext } from 'svelte';
|
||||
import { tags } from '$lib/stores';
|
||||
import { toast } from 'svelte-sonner';
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
export let label = '';
|
||||
export let suggestionTags = [];
|
||||
|
||||
let showTagInput = false;
|
||||
let tagName = '';
|
||||
let showInput = false;
|
||||
let inputElement: HTMLInputElement;
|
||||
|
||||
const addTagHandler = async () => {
|
||||
tagName = tagName.trim();
|
||||
if (tagName !== '') {
|
||||
dispatch('add', tagName);
|
||||
tagName = '';
|
||||
showTagInput = false;
|
||||
} else {
|
||||
toast.error($i18n.t(`Invalid Tag`));
|
||||
}
|
||||
};
|
||||
|
||||
const openInput = () => {
|
||||
showInput = true;
|
||||
setTimeout(() => inputElement?.focus(), 0);
|
||||
};
|
||||
|
||||
const closeInput = () => {
|
||||
if (tagName.trim() === '') {
|
||||
showInput = false;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="px-0.5 flex {showTagInput ? 'flex-row-reverse' : ''}">
|
||||
{#if showTagInput}
|
||||
<div class="flex items-center">
|
||||
<input
|
||||
bind:value={tagName}
|
||||
class=" px-2 cursor-pointer self-center text-xs h-fit bg-transparent outline-hidden line-clamp-1 w-[6.5rem]"
|
||||
placeholder={$i18n.t('Add a tag')}
|
||||
aria-label={$i18n.t('Add a tag')}
|
||||
list="tagOptions"
|
||||
on:keydown={(event) => {
|
||||
if (event.key === 'Enter') {
|
||||
addTagHandler();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{#if suggestionTags.length > 0}
|
||||
<datalist id="tagOptions">
|
||||
{#each suggestionTags as tag}
|
||||
<option value={tag.name} />
|
||||
{/each}
|
||||
</datalist>
|
||||
{/if}
|
||||
|
||||
<button type="button" aria-label={$i18n.t('Save Tag')} on:click={addTagHandler}>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
stroke-width="2"
|
||||
aria-hidden="true"
|
||||
class="w-3 h-3"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M12.416 3.376a.75.75 0 0 1 .208 1.04l-5 7.5a.75.75 0 0 1-1.154.114l-3-3a.75.75 0 0 1 1.06-1.06l2.353 2.353 4.493-6.74a.75.75 0 0 1 1.04-.207Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if showInput}
|
||||
<div class="flex items-center gap-2 px-3 py-1.5 rounded-lg bg-gray-200/80 dark:bg-gray-700">
|
||||
<span class="text-gray-500 dark:text-blue-400">+</span>
|
||||
<input
|
||||
bind:this={inputElement}
|
||||
bind:value={tagName}
|
||||
class="w-20 text-sm bg-transparent outline-hidden text-gray-700 dark:text-blue-400 placeholder:text-gray-400 dark:placeholder:text-blue-400/50"
|
||||
placeholder={$i18n.t('Add tag')}
|
||||
aria-label={$i18n.t('Add a tag')}
|
||||
list="tagOptions"
|
||||
on:keydown={(event) => {
|
||||
if (event.key === 'Enter' || event.key === ' ') {
|
||||
event.preventDefault();
|
||||
addTagHandler();
|
||||
} else if (event.key === 'Escape') {
|
||||
tagName = '';
|
||||
showInput = false;
|
||||
}
|
||||
}}
|
||||
on:blur={closeInput}
|
||||
/>
|
||||
</div>
|
||||
{:else}
|
||||
<button
|
||||
class=" cursor-pointer self-center p-0.5 flex h-fit items-center rounded-full transition border dark:border-gray-600 border-dashed"
|
||||
type="button"
|
||||
aria-label={$i18n.t('Add Tag')}
|
||||
on:click={() => {
|
||||
showTagInput = !showTagInput;
|
||||
}}
|
||||
class="flex items-center gap-2 px-3 py-1.5 rounded-lg bg-gray-200/80 dark:bg-gray-700 text-gray-500 dark:text-blue-400 text-sm font-medium hover:bg-gray-300 dark:hover:bg-gray-600 transition-colors"
|
||||
on:click={openInput}
|
||||
>
|
||||
<div class=" m-auto self-center">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
aria-hidden="true"
|
||||
fill="currentColor"
|
||||
class="size-2.5 {showTagInput ? 'rotate-45' : ''} transition-all transform"
|
||||
>
|
||||
<path
|
||||
d="M8.75 3.75a.75.75 0 0 0-1.5 0v3.5h-3.5a.75.75 0 0 0 0 1.5h3.5v3.5a.75.75 0 0 0 1.5 0v-3.5h3.5a.75.75 0 0 0 0-1.5h-3.5v-3.5Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<span>+</span>
|
||||
<span>{$i18n.t('Add Tag')}</span>
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
{#if label && !showTagInput}
|
||||
<span class="text-xs pl-2 self-center">{label}</span>
|
||||
{/if}
|
||||
</div>
|
||||
{#if suggestionTags.length > 0}
|
||||
<datalist id="tagOptions">
|
||||
{#each suggestionTags as tag}
|
||||
<option value={tag.name} />
|
||||
{/each}
|
||||
</datalist>
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@
|
|||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
import Tooltip from '../Tooltip.svelte';
|
||||
import XMark from '$lib/components/icons/XMark.svelte';
|
||||
|
||||
export let tag;
|
||||
|
|
@ -11,23 +10,14 @@
|
|||
</script>
|
||||
|
||||
{#if tag}
|
||||
<Tooltip content={tag.name}>
|
||||
<button
|
||||
aria-label={$i18n.t('Remove this tag from list')}
|
||||
class="relative group/tags px-1.5 py-[0.5px] gap-0.5 flex justify-between h-fit max-h-fit w-fit items-center rounded-lg bg-gray-500/20 text-gray-700 dark:text-gray-200 transition cursor-pointer"
|
||||
on:click={() => {
|
||||
onDelete();
|
||||
}}
|
||||
>
|
||||
<div class=" text-[0.7rem] font-medium self-center line-clamp-1 w-fit">
|
||||
{tag.name}
|
||||
</div>
|
||||
|
||||
<div class="hidden group-hover/tags:block transition">
|
||||
<div class="rounded-full pl-[1px] backdrop-blur-sm h-full flex self-center cursor-pointer">
|
||||
<XMark className="size-3" strokeWidth="2.5" />
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</Tooltip>
|
||||
<button
|
||||
type="button"
|
||||
class="flex items-center gap-1 px-1.5 py-[1px] rounded-full bg-gray-100/50 dark:bg-gray-800/50 border border-gray-100 dark:border-gray-800 text-gray-600 dark:text-gray-300 text-xs font-medium hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
|
||||
on:click={() => {
|
||||
onDelete();
|
||||
}}
|
||||
>
|
||||
<span class="line-clamp-1">{tag.name}</span>
|
||||
<XMark className="size-3" strokeWidth="2.5" />
|
||||
</button>
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -724,19 +724,19 @@
|
|||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="my-2">
|
||||
<div class="my-4">
|
||||
<Knowledge bind:selectedItems={knowledge} />
|
||||
</div>
|
||||
|
||||
<div class="my-2">
|
||||
<div class="my-4">
|
||||
<ToolsSelector bind:selectedToolIds={toolIds} tools={$tools ?? []} />
|
||||
</div>
|
||||
|
||||
{#if ($functions ?? []).filter((func) => func.type === 'filter').length > 0 || ($functions ?? []).filter((func) => func.type === 'action').length > 0}
|
||||
<hr class=" border-gray-100/30 dark:border-gray-850/30 my-2" />
|
||||
<hr class=" border-gray-100/30 dark:border-gray-850/30 my-4" />
|
||||
|
||||
{#if ($functions ?? []).filter((func) => func.type === 'filter').length > 0}
|
||||
<div class="my-2">
|
||||
<div class="my-4">
|
||||
<FiltersSelector
|
||||
bind:selectedFilterIds={filterIds}
|
||||
filters={($functions ?? []).filter((func) => func.type === 'filter')}
|
||||
|
|
@ -751,7 +751,7 @@
|
|||
)}
|
||||
|
||||
{#if toggleableFilters.length > 0}
|
||||
<div class="my-2">
|
||||
<div class="my-4">
|
||||
<DefaultFiltersSelector
|
||||
bind:selectedFilterIds={defaultFilterIds}
|
||||
filters={toggleableFilters}
|
||||
|
|
@ -761,7 +761,7 @@
|
|||
{/if}
|
||||
|
||||
{#if ($functions ?? []).filter((func) => func.type === 'action').length > 0}
|
||||
<div class="my-2">
|
||||
<div class="my-4">
|
||||
<ActionsSelector
|
||||
bind:selectedActionIds={actionIds}
|
||||
actions={($functions ?? []).filter((func) => func.type === 'action')}
|
||||
|
|
@ -770,9 +770,9 @@
|
|||
{/if}
|
||||
{/if}
|
||||
|
||||
<hr class=" border-gray-100/30 dark:border-gray-850/30 my-2" />
|
||||
<hr class=" border-gray-100/30 dark:border-gray-850/30 my-4" />
|
||||
|
||||
<div class="my-2">
|
||||
<div class="my-4">
|
||||
<Capabilities bind:capabilities />
|
||||
</div>
|
||||
|
||||
|
|
@ -785,19 +785,19 @@
|
|||
.map(([key, value]) => key)}
|
||||
|
||||
{#if availableFeatures.length > 0}
|
||||
<div class="my-2">
|
||||
<div class="my-4">
|
||||
<DefaultFeatures {availableFeatures} bind:featureIds={defaultFeatureIds} />
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
{#if capabilities.builtin_tools}
|
||||
<div class="my-2">
|
||||
<div class="my-4">
|
||||
<BuiltinTools bind:builtinTools />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="my-2">
|
||||
<div class="my-4">
|
||||
<div class="flex w-full justify-between mb-1">
|
||||
<div class="self-center text-xs font-medium text-gray-500">
|
||||
{$i18n.t('TTS Voice')}
|
||||
|
|
@ -811,7 +811,7 @@
|
|||
/>
|
||||
</div>
|
||||
|
||||
<hr class=" border-gray-100/30 dark:border-gray-850/30 my-2" />
|
||||
<hr class=" border-gray-100/30 dark:border-gray-850/30 my-4" />
|
||||
|
||||
<div class="my-2 flex justify-end">
|
||||
<button
|
||||
|
|
|
|||
|
|
@ -418,7 +418,7 @@
|
|||
<div class="mt-1.5">
|
||||
<Tooltip content={$i18n.t('Click to copy ID')}>
|
||||
<button
|
||||
class="text-xs text-gray-500 font-mono bg-gray-50 dark:bg-gray-850 px-2 py-1 rounded-lg cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-800 transition"
|
||||
class="text-xs text-gray-500 font-mono px-2 py-1 rounded-lg cursor-pointer hover:underline transition"
|
||||
on:click={() => {
|
||||
copyToClipboard(prompt.id);
|
||||
toast.success($i18n.t('ID copied to clipboard'));
|
||||
|
|
@ -465,7 +465,7 @@
|
|||
</div>
|
||||
{#if selectedHistoryEntry}
|
||||
<span
|
||||
class="text-xs text-gray-500 font-mono bg-gray-100 dark:bg-gray-800 px-1.5 py-0.5 rounded"
|
||||
class="text-xs text-gray-500 font-mono bg-gray-100 dark:bg-gray-800 px-1.5 rounded"
|
||||
>
|
||||
{selectedHistoryEntry.id.slice(0, 7)}
|
||||
</span>
|
||||
|
|
@ -509,9 +509,9 @@
|
|||
</div>
|
||||
<!-- Scrollable content -->
|
||||
<div
|
||||
class="bg-gray-50 dark:bg-gray-900 rounded-xl px-4 py-3 border border-gray-100 dark:border-gray-800 h-full overflow-y-auto"
|
||||
class="bg-gray-50 dark:bg-gray-900 rounded-xl px-4 py-3 border border-gray-100/50 dark:border-gray-850/50 h-full overflow-y-auto"
|
||||
>
|
||||
<pre class="text-sm whitespace-pre-wrap font-mono pr-8">{selectedHistoryEntry?.snapshot
|
||||
<pre class="text-xs whitespace-pre-wrap font-mono pr-8">{selectedHistoryEntry?.snapshot
|
||||
?.content || content}</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -623,10 +623,10 @@
|
|||
<div class="flex">
|
||||
<!-- Content -->
|
||||
<button
|
||||
class="flex-1 text-left px-3.5 py-2 mb-1 rounded-xl transition group
|
||||
class="flex-1 text-left px-3.5 py-2 mb-1 rounded-2xl transition group
|
||||
{selectedHistoryEntry?.id === entry.id
|
||||
? 'bg-gray-50 dark:bg-gray-850'
|
||||
: 'hover:bg-gray-50 dark:hover:bg-gray-850'}"
|
||||
? 'bg-gray-100/50 dark:bg-gray-850/50'
|
||||
: 'hover:bg-gray-100/50 dark:hover:bg-gray-850/50'}"
|
||||
on:click={() => (selectedHistoryEntry = entry)}
|
||||
>
|
||||
<!-- Commit Message -->
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ export enum Shortcut {
|
|||
NEW_CHAT = 'newChat',
|
||||
NEW_TEMPORARY_CHAT = 'newTemporaryChat',
|
||||
DELETE_CHAT = 'deleteChat',
|
||||
OPEN_MODEL_SELECTOR = 'openModelSelector',
|
||||
|
||||
//Global
|
||||
SEARCH = 'search',
|
||||
|
|
@ -58,6 +59,11 @@ export const shortcuts: ShortcutRegistry = {
|
|||
keys: ['mod', 'shift', 'Backspace', 'Delete'],
|
||||
category: 'Chat'
|
||||
},
|
||||
[Shortcut.OPEN_MODEL_SELECTOR]: {
|
||||
name: 'Open Model Selector',
|
||||
keys: ['mod', 'shift', 'M'],
|
||||
category: 'Chat'
|
||||
},
|
||||
|
||||
//Global
|
||||
[Shortcut.SEARCH]: {
|
||||
|
|
|
|||
|
|
@ -230,6 +230,10 @@
|
|||
event.preventDefault();
|
||||
showSettings.set(false);
|
||||
showShortcuts.set(false);
|
||||
} else if (isShortcutMatch(event, shortcuts[Shortcut.OPEN_MODEL_SELECTOR])) {
|
||||
console.log('Shortcut triggered: OPEN_MODEL_SELECTOR');
|
||||
event.preventDefault();
|
||||
document.getElementById('model-selector-0-button')?.click();
|
||||
} else if (isShortcutMatch(event, shortcuts[Shortcut.NEW_TEMPORARY_CHAT])) {
|
||||
console.log('Shortcut triggered: NEW_TEMPORARY_CHAT');
|
||||
event.preventDefault();
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue