mirror of
https://github.com/open-webui/open-webui.git
synced 2026-10-08 03:08:02 +00:00
refac
This commit is contained in:
parent
1760b073c7
commit
343eb1d659
22 changed files with 572 additions and 308 deletions
|
|
@ -9,7 +9,7 @@
|
|||
import SensitiveInput from '$lib/components/common/SensitiveInput.svelte';
|
||||
import XMark from '$lib/components/icons/XMark.svelte';
|
||||
import AccessControlModal from '$lib/components/workspace/common/AccessControlModal.svelte';
|
||||
import LockClosed from '$lib/components/icons/LockClosed.svelte';
|
||||
import AccessButton from '$lib/components/common/AccessButton.svelte';
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
import ConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
|
||||
import {
|
||||
|
|
@ -775,19 +775,12 @@
|
|||
</button>
|
||||
|
||||
{#if !direct}
|
||||
<button
|
||||
class="bg-gray-50 hover:bg-gray-100 text-black dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-white transition px-2 py-1 object-cover rounded-full flex gap-1 items-center mt-2"
|
||||
type="button"
|
||||
<AccessButton
|
||||
className="mt-2"
|
||||
on:click={() => {
|
||||
showAccessControlModal = true;
|
||||
}}
|
||||
>
|
||||
<LockClosed strokeWidth="2.5" className="size-3.5 shrink-0" />
|
||||
|
||||
<div class="text-xs font-normal shrink-0">
|
||||
{$i18n.t('Access')}
|
||||
</div>
|
||||
</button>
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@
|
|||
import { getToolServerData } from '$lib/apis';
|
||||
import { verifyToolServerConnection, registerOAuthClient } from '$lib/apis/configs';
|
||||
import AccessControlModal from '$lib/components/workspace/common/AccessControlModal.svelte';
|
||||
import LockClosed from '$lib/components/icons/LockClosed.svelte';
|
||||
import AccessButton from '$lib/components/common/AccessButton.svelte';
|
||||
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||
import XMark from '$lib/components/icons/XMark.svelte';
|
||||
import Textarea from './common/Textarea.svelte';
|
||||
|
|
@ -804,19 +804,12 @@
|
|||
</button>
|
||||
|
||||
{#if !direct}
|
||||
<button
|
||||
class="bg-gray-50 hover:bg-gray-100 text-black dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-white transition px-2 py-1 object-cover rounded-full flex gap-1 items-center mt-2"
|
||||
type="button"
|
||||
<AccessButton
|
||||
className="mt-2"
|
||||
on:click={() => {
|
||||
showAccessControlModal = true;
|
||||
}}
|
||||
>
|
||||
<LockClosed strokeWidth="2.5" className="size-3.5 shrink-0" />
|
||||
|
||||
<div class="text-xs font-normal shrink-0">
|
||||
{$i18n.t('Access')}
|
||||
</div>
|
||||
</button>
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -63,6 +63,7 @@
|
|||
|
||||
import { WEBUI_BASE_URL, WEBUI_API_BASE_URL, PASTED_TEXT_CHARACTER_LIMIT } from '$lib/constants';
|
||||
import { initiateOAuthRedirect } from '$lib/apis/configs';
|
||||
import { matchKeybinding, Shortcut } from '$lib/shortcuts';
|
||||
|
||||
import { createNoteHandler } from '../notes/utils';
|
||||
import { getSuggestionRenderer } from '../common/RichTextInput/suggestions';
|
||||
|
|
@ -1009,8 +1010,7 @@
|
|||
shiftKey = true;
|
||||
}
|
||||
|
||||
// Cmd/Ctrl+Shift+L to toggle dictation
|
||||
if (e.key.toLowerCase() === 'l' && (e.metaKey || e.ctrlKey) && e.shiftKey) {
|
||||
if (matchKeybinding(e) === Shortcut.TOGGLE_DICTATION) {
|
||||
e.preventDefault();
|
||||
if (recording) {
|
||||
// Confirm and stop recording
|
||||
|
|
|
|||
|
|
@ -1,47 +1,116 @@
|
|||
<script lang="ts">
|
||||
import { getContext, onMount } from 'svelte';
|
||||
import type { Writable } from 'svelte/store';
|
||||
import { shortcuts } from '$lib/shortcuts';
|
||||
import { updateUserSettings } from '$lib/apis/users';
|
||||
import {
|
||||
DEFAULT_KEYBINDINGS,
|
||||
eventToChord,
|
||||
formatChord,
|
||||
isConfigurableShortcut,
|
||||
keybindings,
|
||||
resetKeybindings,
|
||||
Shortcut,
|
||||
shortcuts
|
||||
} from '$lib/shortcuts';
|
||||
import type { KeybindingsMap, ShortcutDefinition } from '$lib/shortcuts';
|
||||
import { settings } from '$lib/stores';
|
||||
import ShortcutItem from '../ShortcutItem.svelte';
|
||||
|
||||
type ShortcutDefinition = NonNullable<(typeof shortcuts)[keyof typeof shortcuts]>;
|
||||
type CategorizedShortcuts = {
|
||||
[category: string]: ShortcutDefinition[];
|
||||
type ShortcutEntry = {
|
||||
id: Shortcut;
|
||||
shortcut: ShortcutDefinition;
|
||||
};
|
||||
type CategorizedShortcuts = {
|
||||
[category: string]: ShortcutEntry[];
|
||||
};
|
||||
type I18nStore = Writable<{
|
||||
t: (key: string, options?: Record<string, unknown>) => string;
|
||||
}>;
|
||||
|
||||
const i18n: Writable<any> = getContext('i18n');
|
||||
const i18n: I18nStore = getContext('i18n');
|
||||
|
||||
let categorizedShortcuts: CategorizedShortcuts = {};
|
||||
let isMac = false;
|
||||
let recordingShortcut: Shortcut | null = null;
|
||||
|
||||
onMount(() => {
|
||||
isMac = /Mac/i.test(navigator.userAgent);
|
||||
});
|
||||
|
||||
$: {
|
||||
const allShortcuts = Object.values(shortcuts).filter((shortcut) => {
|
||||
const allShortcuts = Object.entries(shortcuts).filter(([, shortcut]) => {
|
||||
if (!shortcut.setting) {
|
||||
return true;
|
||||
}
|
||||
return ($settings as Record<string, any>)[shortcut.setting.id] === shortcut.setting.value;
|
||||
});
|
||||
return ($settings as Record<string, unknown>)[shortcut.setting.id] === shortcut.setting.value;
|
||||
}) as [Shortcut, ShortcutDefinition][];
|
||||
|
||||
categorizedShortcuts = allShortcuts.reduce<CategorizedShortcuts>((acc, shortcut) => {
|
||||
categorizedShortcuts = allShortcuts.reduce<CategorizedShortcuts>((acc, [id, shortcut]) => {
|
||||
const category = shortcut.category;
|
||||
if (!acc[category]) {
|
||||
acc[category] = [];
|
||||
}
|
||||
acc[category].push(shortcut);
|
||||
acc[category].push({ id, shortcut });
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
|
||||
const saveKeybindings = async (next: KeybindingsMap) => {
|
||||
keybindings.set(next);
|
||||
await updateUserSettings(localStorage.token, { keybindings: next });
|
||||
};
|
||||
|
||||
const setBinding = async (id: Shortcut, chord: string) => {
|
||||
if (!isConfigurableShortcut(id)) return;
|
||||
await saveKeybindings({ ...$keybindings, [id]: chord });
|
||||
};
|
||||
|
||||
const resetBindings = async () => {
|
||||
resetKeybindings();
|
||||
await updateUserSettings(localStorage.token, { keybindings: DEFAULT_KEYBINDINGS });
|
||||
};
|
||||
|
||||
const getConflict = (id: Shortcut, chord: string): Shortcut | null => {
|
||||
if (!isConfigurableShortcut(id) || !chord) return null;
|
||||
for (const [otherId, otherChord] of Object.entries($keybindings)) {
|
||||
if (otherId !== id && otherChord === chord) return otherId as Shortcut;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const handleRecordingKeydown = async (event: KeyboardEvent) => {
|
||||
if (!recordingShortcut) return;
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
if (event.key === 'Escape') {
|
||||
recordingShortcut = null;
|
||||
return;
|
||||
}
|
||||
|
||||
const chord = eventToChord(event);
|
||||
if (!chord) return;
|
||||
|
||||
await setBinding(recordingShortcut, chord);
|
||||
recordingShortcut = null;
|
||||
};
|
||||
</script>
|
||||
|
||||
<svelte:window on:keydown={handleRecordingKeydown} />
|
||||
|
||||
<div id="tab-shortcuts" class="flex h-full flex-col text-sm">
|
||||
<h2 class="text-sm font-medium text-gray-900 dark:text-white mb-4">
|
||||
{$i18n.t('Keyboard Shortcuts')}
|
||||
</h2>
|
||||
<div class="mb-4 flex items-center justify-between">
|
||||
<h2 class="text-sm font-medium text-gray-900 dark:text-white">
|
||||
{$i18n.t('Keyboard Shortcuts')}
|
||||
</h2>
|
||||
|
||||
<button
|
||||
class="text-[0.625rem] text-gray-400 transition hover:text-gray-600 dark:hover:text-gray-300"
|
||||
on:click={resetBindings}
|
||||
>
|
||||
{$i18n.t('Reset Defaults')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 min-h-0 overflow-y-auto scrollbar-hover pr-1.5">
|
||||
<div class="flex items-center gap-2 px-1 pb-1">
|
||||
|
|
@ -99,8 +168,55 @@
|
|||
<!-- {$i18n.t('Voice')} -->
|
||||
<!-- {$i18n.t('Toggle Mute')} -->
|
||||
<!-- {$i18n.t('Only active during Voice Mode.')} -->
|
||||
{#each items as shortcut}
|
||||
<ShortcutItem {shortcut} {isMac} />
|
||||
<!-- {$i18n.t('Reset Defaults')} -->
|
||||
<!-- {$i18n.t('Press keys...')} -->
|
||||
<!-- {$i18n.t('Unassigned')} -->
|
||||
<!-- {$i18n.t('Also bound to {{action}}')} -->
|
||||
{#each items as { id, shortcut }}
|
||||
{@const configurable = isConfigurableShortcut(id)}
|
||||
{@const chord = configurable ? $keybindings[id] : ''}
|
||||
{@const conflict = configurable ? getConflict(id, chord) : null}
|
||||
<div class="flex min-h-8 w-full items-center gap-3 px-1 py-1.5">
|
||||
<ShortcutItem {id} {shortcut} {isMac} compact />
|
||||
|
||||
<div class="flex w-[9.5rem] shrink-0 items-center justify-end gap-1">
|
||||
{#if configurable}
|
||||
{#if recordingShortcut === id}
|
||||
<span
|
||||
class="inline-flex min-h-[1.125rem] items-center rounded-full bg-gray-100 px-[0.4375rem] py-0.5 text-[0.625rem] font-medium leading-none text-gray-500 dark:bg-white/6 dark:text-gray-400"
|
||||
>
|
||||
{$i18n.t('Press keys...')}
|
||||
</span>
|
||||
{:else if chord}
|
||||
<button
|
||||
class="inline-flex min-h-[1.125rem] items-center rounded-full bg-gray-100 px-[0.4375rem] py-0.5 text-center text-[0.625rem] font-medium leading-none text-gray-500 dark:bg-white/6 dark:text-gray-400"
|
||||
title={$i18n.t('Click to rebind')}
|
||||
on:click={() => (recordingShortcut = id)}
|
||||
>
|
||||
{formatChord(chord)}
|
||||
</button>
|
||||
{:else}
|
||||
<button
|
||||
class="text-[0.625rem] text-gray-500 transition hover:text-gray-400 dark:text-gray-600"
|
||||
on:click={() => (recordingShortcut = id)}
|
||||
>
|
||||
{$i18n.t('Unassigned')}
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
{#if conflict}
|
||||
<span
|
||||
class="text-[0.5625rem] text-amber-500"
|
||||
title={$i18n.t('Also bound to {{action}}', {
|
||||
action: $i18n.t(shortcuts[conflict]?.name ?? conflict)
|
||||
})}>!</span
|
||||
>
|
||||
{/if}
|
||||
{:else}
|
||||
<ShortcutItem {id} {shortcut} {isMac} keysOnly />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -2,14 +2,21 @@
|
|||
import { getContext, onMount } from 'svelte';
|
||||
import type { Writable } from 'svelte/store';
|
||||
import Tooltip from '../common/Tooltip.svelte';
|
||||
import { shortcuts } from '$lib/shortcuts';
|
||||
import { formatChord, isConfigurableShortcut, keybindings, shortcuts } from '$lib/shortcuts';
|
||||
import type { Shortcut } from '$lib/shortcuts';
|
||||
|
||||
type ShortcutDefinition = NonNullable<(typeof shortcuts)[keyof typeof shortcuts]>;
|
||||
type I18nStore = Writable<{
|
||||
t: (key: string, options?: Record<string, unknown>) => string;
|
||||
}>;
|
||||
|
||||
export let id: Shortcut | undefined = undefined;
|
||||
export let shortcut: ShortcutDefinition;
|
||||
export let isMac: boolean;
|
||||
export let compact = false;
|
||||
export let keysOnly = false;
|
||||
|
||||
const i18n: Writable<any> = getContext('i18n');
|
||||
const i18n: I18nStore = getContext('i18n');
|
||||
let keyboardLayoutMap: Map<string, string> | undefined;
|
||||
|
||||
onMount(async () => {
|
||||
|
|
@ -87,24 +94,49 @@
|
|||
.map(formatKey)
|
||||
.join(isMac ? '' : ' + ');
|
||||
}
|
||||
|
||||
function displayKeys(): string {
|
||||
if (id && isConfigurableShortcut(id)) {
|
||||
return formatChord($keybindings[id]) || $i18n.t('Unassigned');
|
||||
}
|
||||
return formatKeys(shortcut.keys);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex min-h-8 w-full items-center gap-3 px-1 py-1.5">
|
||||
<div class="min-w-0 flex-1 text-[0.71875rem] leading-tight text-gray-700 dark:text-gray-300">
|
||||
{#if shortcut.tooltip}
|
||||
<Tooltip content={$i18n.t(shortcut.tooltip)}>
|
||||
<span class="inline-flex max-w-full items-baseline gap-1">
|
||||
<span class="truncate whitespace-pre-line">{$i18n.t(shortcut.name)}</span>
|
||||
<span class="text-[0.625rem] text-gray-400 dark:text-gray-600">*</span>
|
||||
</span>
|
||||
</Tooltip>
|
||||
{:else}
|
||||
<span class="whitespace-pre-line">{$i18n.t(shortcut.name)}</span>
|
||||
{/if}
|
||||
</div>
|
||||
{#if keysOnly}
|
||||
<span
|
||||
class="inline-flex min-h-[1.125rem] max-w-[9.5rem] shrink-0 items-center justify-center rounded-full bg-gray-100 px-[0.4375rem] py-0.5 text-center text-[0.625rem] font-medium leading-none text-gray-500 dark:bg-white/6 dark:text-gray-400"
|
||||
>
|
||||
{formatKeys(shortcut.keys)}
|
||||
{displayKeys()}
|
||||
</span>
|
||||
</div>
|
||||
{:else}
|
||||
<div
|
||||
class={compact
|
||||
? 'min-w-0 flex-1 text-[0.71875rem] leading-tight text-gray-700 dark:text-gray-300'
|
||||
: 'flex min-h-8 w-full items-center gap-3 px-1 py-1.5'}
|
||||
>
|
||||
<div
|
||||
class={compact
|
||||
? ''
|
||||
: 'min-w-0 flex-1 text-[0.71875rem] leading-tight text-gray-700 dark:text-gray-300'}
|
||||
>
|
||||
{#if shortcut.tooltip}
|
||||
<Tooltip content={$i18n.t(shortcut.tooltip)}>
|
||||
<span class="inline-flex max-w-full items-baseline gap-1">
|
||||
<span class="truncate whitespace-pre-line">{$i18n.t(shortcut.name)}</span>
|
||||
<span class="text-[0.625rem] text-gray-400 dark:text-gray-600">*</span>
|
||||
</span>
|
||||
</Tooltip>
|
||||
{:else}
|
||||
<span class="whitespace-pre-line">{$i18n.t(shortcut.name)}</span>
|
||||
{/if}
|
||||
</div>
|
||||
{#if !compact}
|
||||
<span
|
||||
class="inline-flex min-h-[1.125rem] max-w-[9.5rem] shrink-0 items-center justify-center rounded-full bg-gray-100 px-[0.4375rem] py-0.5 text-center text-[0.625rem] font-medium leading-none text-gray-500 dark:bg-white/6 dark:text-gray-400"
|
||||
>
|
||||
{displayKeys()}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
|
|
|||
22
src/lib/components/common/AccessButton.svelte
Normal file
22
src/lib/components/common/AccessButton.svelte
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
<script lang="ts">
|
||||
import { getContext } from 'svelte';
|
||||
import type { Writable } from 'svelte/store';
|
||||
import type { i18n as i18nType } from 'i18next';
|
||||
|
||||
import LockClosed from '$lib/components/icons/LockClosed.svelte';
|
||||
|
||||
export let className = '';
|
||||
export let disabled = false;
|
||||
|
||||
const i18n = getContext<Writable<i18nType>>('i18n');
|
||||
</script>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="flex shrink-0 items-center gap-1 rounded-lg bg-gray-50 px-2 py-1 text-xs font-normal text-gray-900 transition ring-1 ring-gray-200 hover:bg-gray-100 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-gray-850 dark:text-gray-100 dark:ring-gray-800 dark:hover:bg-gray-800 {className}"
|
||||
{disabled}
|
||||
on:click
|
||||
>
|
||||
<LockClosed strokeWidth="2.5" className="size-3 shrink-0" />
|
||||
<span class="shrink-0">{$i18n.t('Access')}</span>
|
||||
</button>
|
||||
|
|
@ -1,19 +1,17 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { shortcuts } from '$lib/shortcuts';
|
||||
import { settings } from '$lib/stores';
|
||||
import { formatChord, isConfigurableShortcut, keybindings, shortcuts } from '$lib/shortcuts';
|
||||
import type { Shortcut } from '$lib/shortcuts';
|
||||
|
||||
export let name: string;
|
||||
export let className = '';
|
||||
|
||||
let isMac = false;
|
||||
let mounted = false;
|
||||
let keys: string[] = [];
|
||||
let isVisible = true;
|
||||
|
||||
onMount(() => {
|
||||
isMac = /Mac/i.test(navigator.userAgent);
|
||||
keys = shortcuts[name]?.keys ?? [];
|
||||
mounted = true;
|
||||
});
|
||||
|
||||
|
|
@ -26,12 +24,21 @@
|
|||
|
||||
return key;
|
||||
}
|
||||
|
||||
function displayKeys(): string {
|
||||
const id = name as Shortcut;
|
||||
if (isConfigurableShortcut(id)) {
|
||||
return formatChord($keybindings[id]);
|
||||
}
|
||||
const keys = shortcuts[id]?.keys ?? [];
|
||||
return keys.map(formatKey).join(isMac ? '' : '+');
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if mounted && isVisible}
|
||||
<div
|
||||
class="hidden md:flex items-center self-center text-xs text-gray-400 dark:text-gray-600 {className}"
|
||||
>
|
||||
<span>{keys.map(formatKey).join(isMac ? '' : '+')}</span>
|
||||
<span>{displayKeys()}</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -792,7 +792,7 @@
|
|||
|
||||
{#if !$mobile && !$showSidebar}
|
||||
<div
|
||||
class=" w-[42px] py-1 px-1 flex flex-col justify-between text-black dark:text-white hover:bg-gray-50/30 dark:hover:bg-gray-950/30 h-full z-10 transition-all border-e-[0.5px] border-gray-50 dark:border-gray-850/30"
|
||||
class=" w-[42px] shrink-0 py-1 px-1 flex flex-col justify-between text-black dark:text-white hover:bg-gray-50/30 dark:hover:bg-gray-950/30 h-full z-10 transition-all border-e-[0.5px] border-gray-50 dark:border-gray-850/30"
|
||||
id="sidebar"
|
||||
>
|
||||
<button
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@
|
|||
|
||||
import Calendar from '../icons/Calendar.svelte';
|
||||
import Users from '../icons/Users.svelte';
|
||||
import LockClosed from '../icons/LockClosed.svelte';
|
||||
import AccessButton from '../common/AccessButton.svelte';
|
||||
|
||||
import Image from '../common/Image.svelte';
|
||||
import FileItem from '../common/FileItem.svelte';
|
||||
|
|
@ -996,7 +996,7 @@ Provide the enhanced notes in markdown format. Use markdown syntax for headings,
|
|||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="flex items-center gap-0.5 shrink-0 translate-x-1">
|
||||
<div class="flex items-center gap-0.5 shrink-0">
|
||||
{#if note?.write_access}
|
||||
{#if editor}
|
||||
<div>
|
||||
|
|
@ -1107,16 +1107,12 @@ Provide the enhanced notes in markdown format. Use markdown syntax for headings,
|
|||
</NoteMenu>
|
||||
|
||||
{#if note?.write_access}
|
||||
<button
|
||||
class="shrink-0 bg-gray-50 hover:bg-gray-100 text-black dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-white transition px-2.5 py-1 rounded-full flex gap-1.5 items-center text-sm"
|
||||
<AccessButton
|
||||
on:click={() => {
|
||||
showAccessControlModal = true;
|
||||
}}
|
||||
disabled={note?.user_id !== $user?.id && $user?.role !== 'admin'}
|
||||
>
|
||||
<LockClosed strokeWidth="2.5" className="size-3.5" />
|
||||
{$i18n.t('Access')}
|
||||
</button>
|
||||
/>
|
||||
{:else}
|
||||
<div class="shrink-0 text-xs text-gray-500 px-2 py-1">
|
||||
{$i18n.t('Read-Only Access')}
|
||||
|
|
|
|||
|
|
@ -46,6 +46,9 @@
|
|||
};
|
||||
};
|
||||
|
||||
export let showCreateOnMount = false;
|
||||
export let createModalCloseHref = '';
|
||||
|
||||
let loaded = false;
|
||||
let showDeleteConfirm = false;
|
||||
let showCreateModal = false;
|
||||
|
|
@ -162,6 +165,14 @@
|
|||
}
|
||||
};
|
||||
|
||||
const closeCreateModal = async () => {
|
||||
showCreateModal = false;
|
||||
|
||||
if (createModalCloseHref) {
|
||||
await goto(createModalCloseHref);
|
||||
}
|
||||
};
|
||||
|
||||
const exportHandler = async (item: KnowledgeListItem) => {
|
||||
try {
|
||||
const blob = await exportKnowledgeById(localStorage.token, item.id);
|
||||
|
|
@ -185,6 +196,10 @@
|
|||
viewOption = localStorage?.workspaceViewOption || '';
|
||||
sourceOption = localStorage?.workspaceKnowledgeSourceOption || '';
|
||||
loaded = true;
|
||||
|
||||
if (showCreateOnMount) {
|
||||
showCreateModal = true;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
@ -202,11 +217,15 @@
|
|||
}}
|
||||
/>
|
||||
|
||||
<Modal bind:show={showCreateModal} size="sm">
|
||||
<Modal
|
||||
bind:show={showCreateModal}
|
||||
size="full"
|
||||
className="!w-[calc(100vw-2rem)] sm:!w-[calc(100vw-3rem)] lg:!w-[calc(100vw-4rem)] !max-w-[80rem] h-[min(54rem,calc(100dvh-4rem))] max-h-[calc(100dvh-4rem)] flex flex-col bg-white dark:bg-gray-900 rounded-4xl"
|
||||
>
|
||||
<CreateKnowledgeBase
|
||||
modal={true}
|
||||
onBack={() => {
|
||||
showCreateModal = false;
|
||||
closeCreateModal();
|
||||
}}
|
||||
/>
|
||||
</Modal>
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@
|
|||
};
|
||||
</script>
|
||||
|
||||
<div class="w-full max-h-full">
|
||||
<div class="w-full max-h-full {modal ? 'h-full flex flex-col' : ''}">
|
||||
{#if modal}
|
||||
<div class="flex justify-between items-center dark:text-gray-100 px-5 pt-4 pb-2">
|
||||
<h3 class="text-base font-normal">{$i18n.t('Create a knowledge base')}</h3>
|
||||
|
|
@ -92,20 +92,20 @@
|
|||
{/if}
|
||||
|
||||
<form
|
||||
class="flex flex-col {modal ? 'px-5 pb-3' : 'max-w-lg mx-auto mt-10 mb-10'}"
|
||||
class="flex flex-col {modal ? 'px-5 pb-3 flex-1 min-h-0' : 'max-w-lg mx-auto mt-10 mb-10'}"
|
||||
on:submit|preventDefault={() => {
|
||||
submitHandler();
|
||||
}}
|
||||
>
|
||||
<div class=" w-full flex flex-col justify-center">
|
||||
<div class="w-full flex flex-col {modal ? 'flex-1 min-h-0' : 'justify-center'}">
|
||||
{#if !modal}
|
||||
<div class=" text-2xl font-normal mb-2.5">
|
||||
{$i18n.t('Create a knowledge base')}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="w-full flex flex-col gap-2.5">
|
||||
<div class="w-full">
|
||||
<div class="w-full flex flex-col gap-2.5 {modal ? 'flex-1 min-h-0' : ''}">
|
||||
<div class="w-full shrink-0">
|
||||
<div class=" text-sm mb-2">{$i18n.t('What are you working on?')}</div>
|
||||
|
||||
<div class="w-full mt-1">
|
||||
|
|
@ -119,12 +119,14 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class={modal ? 'flex-1 min-h-0 flex flex-col' : ''}>
|
||||
<div class="text-sm mb-2">{$i18n.t('What are you trying to achieve?')}</div>
|
||||
|
||||
<div class=" w-full mt-1">
|
||||
<div class="w-full mt-1 {modal ? 'flex-1 min-h-0 flex' : ''}">
|
||||
<textarea
|
||||
class="w-full resize-none rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
|
||||
class="w-full {modal
|
||||
? 'flex-1 min-h-0'
|
||||
: ''} resize-none rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
|
||||
rows="4"
|
||||
bind:value={description}
|
||||
placeholder={$i18n.t('Describe your knowledge base and objectives')}
|
||||
|
|
@ -135,7 +137,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-2">
|
||||
<div class="mt-2 shrink-0">
|
||||
<AccessControl
|
||||
bind:accessGrants
|
||||
accessRoles={['read', 'write']}
|
||||
|
|
@ -146,7 +148,7 @@
|
|||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end {modal ? 'pt-3 gap-2' : 'mt-2'}">
|
||||
<div class="flex justify-end {modal ? 'pt-3 gap-2 shrink-0' : 'mt-2'}">
|
||||
{#if modal}
|
||||
<button
|
||||
class="px-3 py-1 text-xs text-gray-500 hover:text-gray-700 dark:hover:text-gray-200 transition"
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@
|
|||
import ConfirmDialog from '../../common/ConfirmDialog.svelte';
|
||||
import Drawer from '$lib/components/common/Drawer.svelte';
|
||||
import ChevronLeft from '$lib/components/icons/ChevronLeft.svelte';
|
||||
import LockClosed from '$lib/components/icons/LockClosed.svelte';
|
||||
import AccessButton from '$lib/components/common/AccessButton.svelte';
|
||||
import AccessControlModal from '../common/AccessControlModal.svelte';
|
||||
import Search from '$lib/components/icons/Search.svelte';
|
||||
import FilesOverlay from '$lib/components/chat/MessageInput/FilesOverlay.svelte';
|
||||
|
|
@ -1264,19 +1264,11 @@
|
|||
|
||||
{#if knowledge?.write_access}
|
||||
<div class="self-center shrink-0">
|
||||
<button
|
||||
class="bg-gray-50 hover:bg-gray-100 text-black dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-white transition px-2 py-1 rounded-full flex gap-1 items-center"
|
||||
type="button"
|
||||
<AccessButton
|
||||
on:click={() => {
|
||||
showAccessControlModal = true;
|
||||
}}
|
||||
>
|
||||
<LockClosed strokeWidth="2.5" className="size-3.5" />
|
||||
|
||||
<div class="text-sm font-normal shrink-0">
|
||||
{$i18n.t('Access')}
|
||||
</div>
|
||||
</button>
|
||||
/>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="text-xs shrink-0 text-gray-500">
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@
|
|||
import TerminalSelector from './TerminalSelector.svelte';
|
||||
import TTSVoiceInput from './TTSVoiceInput.svelte';
|
||||
import AccessControlModal from '../common/AccessControlModal.svelte';
|
||||
import LockClosed from '$lib/components/icons/LockClosed.svelte';
|
||||
import AccessButton from '$lib/components/common/AccessButton.svelte';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
|
|
@ -587,19 +587,11 @@
|
|||
required
|
||||
/>
|
||||
|
||||
<button
|
||||
class="flex h-7 shrink-0 items-center gap-1 rounded-lg bg-gray-50 px-2 text-xs text-gray-700 transition hover:bg-gray-100 dark:bg-white/[0.03] dark:text-gray-300 dark:hover:bg-white/[0.06]"
|
||||
type="button"
|
||||
<AccessButton
|
||||
on:click={() => {
|
||||
showAccessControlModal = true;
|
||||
}}
|
||||
>
|
||||
<LockClosed strokeWidth="2.5" className="size-3.5 shrink-0" />
|
||||
|
||||
<div class="font-normal shrink-0">
|
||||
{$i18n.t('Access')}
|
||||
</div>
|
||||
</button>
|
||||
/>
|
||||
</div>
|
||||
|
||||
<input
|
||||
|
|
|
|||
|
|
@ -48,6 +48,9 @@
|
|||
is_production?: boolean;
|
||||
};
|
||||
|
||||
export let showCreateOnMount = false;
|
||||
export let createModalCloseHref = '';
|
||||
|
||||
let shiftKey = false;
|
||||
|
||||
const i18n = getContext<Writable<i18nType>>('i18n');
|
||||
|
|
@ -188,6 +191,15 @@
|
|||
showCreateModal = true;
|
||||
};
|
||||
|
||||
const closeCreateModal = async () => {
|
||||
showCreateModal = false;
|
||||
createPrompt = null;
|
||||
|
||||
if (createModalCloseHref) {
|
||||
await goto(createModalCloseHref);
|
||||
}
|
||||
};
|
||||
|
||||
const createPromptHandler = async (prompt: PromptDraft) => {
|
||||
const res = await createNewPrompt(localStorage.token, prompt).catch((error) => {
|
||||
toast.error(`${error}`);
|
||||
|
|
@ -196,10 +208,9 @@
|
|||
|
||||
if (res) {
|
||||
toast.success($i18n.t('Prompt created successfully'));
|
||||
showCreateModal = false;
|
||||
createPrompt = null;
|
||||
page = 1;
|
||||
await getPromptList();
|
||||
await closeCreateModal();
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -274,6 +285,8 @@
|
|||
const prompt = JSON.parse(sessionStorage.prompt);
|
||||
sessionStorage.removeItem('prompt');
|
||||
openCreateModal(toPromptDraft(prompt));
|
||||
} else if (showCreateOnMount) {
|
||||
openCreateModal();
|
||||
}
|
||||
|
||||
const onKeyDown = (event: KeyboardEvent) => {
|
||||
|
|
@ -329,7 +342,11 @@
|
|||
</div>
|
||||
</DeleteConfirmDialog>
|
||||
|
||||
<Modal bind:show={showCreateModal} size="md">
|
||||
<Modal
|
||||
bind:show={showCreateModal}
|
||||
size="full"
|
||||
className="!w-[calc(100vw-2rem)] sm:!w-[calc(100vw-3rem)] lg:!w-[calc(100vw-4rem)] !max-w-[80rem] h-[min(54rem,calc(100dvh-4rem))] max-h-[calc(100dvh-4rem)] flex flex-col bg-white dark:bg-gray-900 rounded-4xl"
|
||||
>
|
||||
{#key createPrompt}
|
||||
<PromptEditor
|
||||
modal={true}
|
||||
|
|
@ -337,8 +354,7 @@
|
|||
clone={createPrompt !== null}
|
||||
onSubmit={createPromptHandler}
|
||||
onCancel={() => {
|
||||
showCreateModal = false;
|
||||
createPrompt = null;
|
||||
closeCreateModal();
|
||||
}}
|
||||
/>
|
||||
{/key}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
import Textarea from '$lib/components/common/Textarea.svelte';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
import LockClosed from '$lib/components/icons/LockClosed.svelte';
|
||||
import AccessButton from '$lib/components/common/AccessButton.svelte';
|
||||
import Clipboard from '$lib/components/icons/Clipboard.svelte';
|
||||
import Check from '$lib/components/icons/Check.svelte';
|
||||
import AccessControlModal from '../common/AccessControlModal.svelte';
|
||||
|
|
@ -416,13 +416,7 @@
|
|||
{$i18n.t('Edit')}
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="bg-gray-50 hover:bg-gray-100 text-black dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-white transition px-2.5 py-1 rounded-full flex gap-1.5 items-center text-sm border border-gray-100 dark:border-gray-800"
|
||||
on:click={() => (showAccessControlModal = true)}
|
||||
>
|
||||
<LockClosed strokeWidth="2.5" className="size-3.5" />
|
||||
{$i18n.t('Access')}
|
||||
</button>
|
||||
<AccessButton on:click={() => (showAccessControlModal = true)} />
|
||||
{:else}
|
||||
<span class="text-xs text-gray-500 bg-gray-100 dark:bg-gray-800 px-2 py-1 rounded-full"
|
||||
>{$i18n.t('Read Only')}</span
|
||||
|
|
@ -537,7 +531,7 @@
|
|||
</div>
|
||||
{:else}
|
||||
<!-- Create mode: Form -->
|
||||
<div class="w-full max-h-full">
|
||||
<div class="w-full max-h-full {modal ? 'h-full flex flex-col' : ''}">
|
||||
{#if modal}
|
||||
<div class="flex justify-between items-center dark:text-gray-100 px-5 pt-4 pb-2">
|
||||
<h3 class="text-base font-normal">{$i18n.t('Create Prompt')}</h3>
|
||||
|
|
@ -555,10 +549,10 @@
|
|||
{/if}
|
||||
|
||||
<form
|
||||
class="flex flex-col w-full {modal ? 'px-5 pb-3' : 'mb-10'}"
|
||||
class="flex flex-col w-full {modal ? 'px-5 pb-3 flex-1 min-h-0' : 'mb-10'}"
|
||||
on:submit|preventDefault={submitHandler}
|
||||
>
|
||||
<div class="mb-2">
|
||||
<div class="mb-2 shrink-0">
|
||||
<Tooltip
|
||||
content={`${$i18n.t('Only alphanumeric characters and hyphens are allowed')} - ${$i18n.t('Activate this command by typing "/{{COMMAND}}" to chat input.', { COMMAND: command })}`}
|
||||
placement="bottom-start"
|
||||
|
|
@ -572,16 +566,9 @@
|
|||
required
|
||||
/>
|
||||
<div class="self-center shrink-0">
|
||||
<button
|
||||
class="bg-gray-50 hover:bg-gray-100 text-black dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-white transition px-2 py-1 rounded-full flex gap-1 items-center"
|
||||
type="button"
|
||||
on:click={() => (showAccessControlModal = true)}
|
||||
>
|
||||
<LockClosed strokeWidth="2.5" className="size-3.5" />
|
||||
<div class="text-sm font-normal shrink-0">{$i18n.t('Access')}</div>
|
||||
</button>
|
||||
<AccessButton on:click={() => (showAccessControlModal = true)} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-0.5 items-center text-xs text-gray-500">
|
||||
<div>/</div>
|
||||
<input
|
||||
|
|
@ -609,16 +596,25 @@
|
|||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<div class="my-2">
|
||||
<div class={modal ? 'my-2 flex-1 min-h-0 flex flex-col' : 'my-2'}>
|
||||
<div class="text-gray-500 text-xs">{$i18n.t('Prompt Content')}</div>
|
||||
<div class="mt-1">
|
||||
<Textarea
|
||||
className="text-sm w-full bg-transparent outline-hidden overflow-y-hidden resize-none"
|
||||
placeholder={$i18n.t('Write a summary in 50 words that summarizes {{topic}}.')}
|
||||
bind:value={content}
|
||||
rows={6}
|
||||
required
|
||||
/>
|
||||
<div class={modal ? 'mt-1 flex-1 min-h-0 flex flex-col' : 'mt-1'}>
|
||||
{#if modal}
|
||||
<textarea
|
||||
class="text-sm w-full flex-1 min-h-0 bg-transparent outline-hidden resize-none"
|
||||
placeholder={$i18n.t('Write a summary in 50 words that summarizes {{topic}}.')}
|
||||
bind:value={content}
|
||||
required
|
||||
></textarea>
|
||||
{:else}
|
||||
<Textarea
|
||||
className="text-sm w-full bg-transparent outline-hidden overflow-y-hidden resize-none"
|
||||
placeholder={$i18n.t('Write a summary in 50 words that summarizes {{topic}}.')}
|
||||
bind:value={content}
|
||||
rows={6}
|
||||
required
|
||||
/>
|
||||
{/if}
|
||||
<div class="text-xs text-gray-400 dark:text-gray-500">
|
||||
ⓘ {$i18n.t('Use')}
|
||||
<span class="font-normal text-gray-600 dark:text-gray-300"
|
||||
|
|
@ -629,7 +625,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end {modal ? 'pt-3 gap-2' : 'my-4 pb-20'}">
|
||||
<div class="flex justify-end {modal ? 'pt-3 gap-2 shrink-0' : 'my-4 pb-20'}">
|
||||
{#if modal}
|
||||
<button
|
||||
class="px-3 py-1 text-xs text-gray-500 hover:text-gray-700 dark:hover:text-gray-200 transition"
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
import Textarea from '$lib/components/common/Textarea.svelte';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
import LockClosed from '$lib/components/icons/LockClosed.svelte';
|
||||
import AccessButton from '$lib/components/common/AccessButton.svelte';
|
||||
import ChevronLeft from '$lib/components/icons/ChevronLeft.svelte';
|
||||
import AccessControlModal from '../common/AccessControlModal.svelte';
|
||||
import { user } from '$lib/stores';
|
||||
|
|
@ -134,17 +134,7 @@
|
|||
|
||||
<div class="self-center shrink-0">
|
||||
{#if !disabled}
|
||||
<button
|
||||
class="bg-gray-50 hover:bg-gray-100 text-black dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-white transition px-2 py-1 rounded-full flex gap-1 items-center"
|
||||
type="button"
|
||||
on:click={() => (showAccessControlModal = true)}
|
||||
>
|
||||
<LockClosed strokeWidth="2.5" className="size-3.5" />
|
||||
|
||||
<div class="text-sm font-normal shrink-0">
|
||||
{$i18n.t('Access')}
|
||||
</div>
|
||||
</button>
|
||||
<AccessButton on:click={() => (showAccessControlModal = true)} />
|
||||
{:else}
|
||||
<span
|
||||
class="text-xs text-gray-500 bg-gray-100 dark:bg-gray-800 px-2 py-1 rounded-full"
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@
|
|||
import ConfirmDialog from '$lib/components/common/ConfirmDialog.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 AccessButton from '$lib/components/common/AccessButton.svelte';
|
||||
import AccessControlModal from '../common/AccessControlModal.svelte';
|
||||
|
||||
let formElement = null;
|
||||
|
|
@ -252,19 +252,11 @@ class Tools:
|
|||
</div>
|
||||
|
||||
<div class="self-center shrink-0">
|
||||
<button
|
||||
class="bg-gray-50 hover:bg-gray-100 text-black dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-white transition px-2 py-1 rounded-full flex gap-1 items-center"
|
||||
type="button"
|
||||
<AccessButton
|
||||
on:click={() => {
|
||||
showAccessControlModal = true;
|
||||
}}
|
||||
>
|
||||
<LockClosed strokeWidth="2.5" className="size-3.5" />
|
||||
|
||||
<div class="text-sm font-normal shrink-0">
|
||||
{$i18n.t('Access')}
|
||||
</div>
|
||||
</button>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
43
src/lib/shortcuts.test.ts
Normal file
43
src/lib/shortcuts.test.ts
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
import { beforeEach, describe, expect, it } from 'vitest';
|
||||
import {
|
||||
eventToChord,
|
||||
loadKeybindings,
|
||||
matchKeybinding,
|
||||
resetKeybindings,
|
||||
Shortcut
|
||||
} from './shortcuts';
|
||||
|
||||
const key = (
|
||||
key: string,
|
||||
modifiers: Partial<Pick<KeyboardEvent, 'ctrlKey' | 'metaKey' | 'altKey' | 'shiftKey'>> = {}
|
||||
) =>
|
||||
({
|
||||
key,
|
||||
ctrlKey: false,
|
||||
metaKey: false,
|
||||
altKey: false,
|
||||
shiftKey: false,
|
||||
...modifiers
|
||||
}) as KeyboardEvent;
|
||||
|
||||
describe('shortcuts', () => {
|
||||
beforeEach(() => {
|
||||
resetKeybindings();
|
||||
});
|
||||
|
||||
it('normalizes ctrl to Cmd on non-Mac platforms', () => {
|
||||
expect(eventToChord(key('k', { ctrlKey: true }))).toBe('Cmd+K');
|
||||
expect(matchKeybinding(key('k', { ctrlKey: true }))).toBe(Shortcut.SEARCH);
|
||||
});
|
||||
|
||||
it('loads saved bindings, including unassigned shortcuts', () => {
|
||||
loadKeybindings({
|
||||
[Shortcut.SEARCH]: 'Cmd+Shift+P',
|
||||
[Shortcut.NEW_CHAT]: ''
|
||||
});
|
||||
|
||||
expect(matchKeybinding(key('k', { ctrlKey: true }))).toBeNull();
|
||||
expect(matchKeybinding(key('p', { ctrlKey: true, shiftKey: true }))).toBe(Shortcut.SEARCH);
|
||||
expect(matchKeybinding(key('o', { ctrlKey: true, shiftKey: true }))).toBeNull();
|
||||
});
|
||||
});
|
||||
|
|
@ -1,16 +1,21 @@
|
|||
type ShortcutRegistry = {
|
||||
[key in Shortcut]?: {
|
||||
name: string;
|
||||
keys: string[];
|
||||
category: string;
|
||||
tooltip?: string;
|
||||
setting?: {
|
||||
id: string;
|
||||
value: any;
|
||||
};
|
||||
import { get, writable } from 'svelte/store';
|
||||
|
||||
export type ShortcutDefinition = {
|
||||
name: string;
|
||||
keys: string[];
|
||||
category: string;
|
||||
tooltip?: string;
|
||||
configurable?: boolean;
|
||||
setting?: {
|
||||
id: string;
|
||||
value: unknown;
|
||||
};
|
||||
};
|
||||
|
||||
type ShortcutRegistry = {
|
||||
[key in Shortcut]?: ShortcutDefinition;
|
||||
};
|
||||
|
||||
export enum Shortcut {
|
||||
//Chat
|
||||
NEW_CHAT = 'newChat',
|
||||
|
|
@ -46,66 +51,219 @@ export enum Shortcut {
|
|||
TOGGLE_MUTE = 'toggleMute'
|
||||
}
|
||||
|
||||
export const CONFIGURABLE_SHORTCUTS = [
|
||||
Shortcut.NEW_CHAT,
|
||||
Shortcut.NEW_TEMPORARY_CHAT,
|
||||
Shortcut.DELETE_CHAT,
|
||||
Shortcut.OPEN_MODEL_SELECTOR,
|
||||
Shortcut.TOGGLE_DICTATION,
|
||||
Shortcut.SEARCH,
|
||||
Shortcut.OPEN_SETTINGS,
|
||||
Shortcut.SHOW_SHORTCUTS,
|
||||
Shortcut.TOGGLE_SIDEBAR,
|
||||
Shortcut.CLOSE_MODAL,
|
||||
Shortcut.FOCUS_INPUT,
|
||||
Shortcut.GENERATE_MESSAGE_PAIR,
|
||||
Shortcut.REGENERATE_RESPONSE,
|
||||
Shortcut.COPY_LAST_CODE_BLOCK,
|
||||
Shortcut.COPY_LAST_RESPONSE
|
||||
] as const;
|
||||
|
||||
export type ConfigurableShortcut = (typeof CONFIGURABLE_SHORTCUTS)[number];
|
||||
export type KeybindingsMap = Record<ConfigurableShortcut, string>;
|
||||
|
||||
export const DEFAULT_KEYBINDINGS: KeybindingsMap = {
|
||||
[Shortcut.NEW_CHAT]: 'Cmd+Shift+O',
|
||||
[Shortcut.NEW_TEMPORARY_CHAT]: "Cmd+Shift+'",
|
||||
[Shortcut.DELETE_CHAT]: 'Cmd+Shift+Backspace',
|
||||
[Shortcut.OPEN_MODEL_SELECTOR]: 'Cmd+Shift+M',
|
||||
[Shortcut.TOGGLE_DICTATION]: 'Cmd+Shift+L',
|
||||
[Shortcut.SEARCH]: 'Cmd+K',
|
||||
[Shortcut.OPEN_SETTINGS]: 'Cmd+.',
|
||||
[Shortcut.SHOW_SHORTCUTS]: 'Cmd+/',
|
||||
[Shortcut.TOGGLE_SIDEBAR]: 'Cmd+Shift+S',
|
||||
[Shortcut.CLOSE_MODAL]: 'Escape',
|
||||
[Shortcut.FOCUS_INPUT]: 'Shift+Escape',
|
||||
[Shortcut.GENERATE_MESSAGE_PAIR]: 'Cmd+Shift+Enter',
|
||||
[Shortcut.REGENERATE_RESPONSE]: 'Cmd+R',
|
||||
[Shortcut.COPY_LAST_CODE_BLOCK]: 'Cmd+Shift+;',
|
||||
[Shortcut.COPY_LAST_RESPONSE]: 'Cmd+Shift+C'
|
||||
};
|
||||
|
||||
export const keybindings = writable<KeybindingsMap>({ ...DEFAULT_KEYBINDINGS });
|
||||
|
||||
export function isConfigurableShortcut(id: Shortcut): id is ConfigurableShortcut {
|
||||
return (CONFIGURABLE_SHORTCUTS as readonly Shortcut[]).includes(id);
|
||||
}
|
||||
|
||||
export function loadKeybindings(saved: Partial<Record<string, string>> | undefined): void {
|
||||
if (!saved || typeof saved !== 'object') return;
|
||||
|
||||
keybindings.update((current) => {
|
||||
const updated = { ...current };
|
||||
for (const id of CONFIGURABLE_SHORTCUTS) {
|
||||
if (typeof saved[id] === 'string') {
|
||||
updated[id] = saved[id]!;
|
||||
}
|
||||
}
|
||||
return updated;
|
||||
});
|
||||
}
|
||||
|
||||
export function resetKeybindings(): void {
|
||||
keybindings.set({ ...DEFAULT_KEYBINDINGS });
|
||||
}
|
||||
|
||||
const IS_MAC = typeof navigator !== 'undefined' && /Mac|iPhone|iPad/.test(navigator.userAgent);
|
||||
|
||||
export function eventToChord(event: KeyboardEvent): string {
|
||||
const parts: string[] = [];
|
||||
|
||||
if (IS_MAC) {
|
||||
if (event.metaKey) parts.push('Cmd');
|
||||
if (event.ctrlKey) parts.push('Ctrl');
|
||||
} else if (event.ctrlKey) {
|
||||
parts.push('Cmd');
|
||||
}
|
||||
|
||||
if (event.altKey) parts.push('Alt');
|
||||
if (event.shiftKey) parts.push('Shift');
|
||||
|
||||
let key = event.key;
|
||||
if (['Meta', 'Control', 'Alt', 'Shift'].includes(key)) return '';
|
||||
|
||||
if (key === ' ') key = 'Space';
|
||||
if (key.length === 1) key = key.toUpperCase();
|
||||
|
||||
if (key === '"') key = "'";
|
||||
if (key === ':') key = ';';
|
||||
if (key === '?') key = '/';
|
||||
if (key === '{') key = '[';
|
||||
if (key === '}') key = ']';
|
||||
|
||||
parts.push(key);
|
||||
return parts.join('+');
|
||||
}
|
||||
|
||||
function formatChordPart(part: string): string {
|
||||
if (IS_MAC) {
|
||||
switch (part) {
|
||||
case 'Cmd':
|
||||
return '⌘';
|
||||
case 'Ctrl':
|
||||
return '⌃';
|
||||
case 'Alt':
|
||||
return '⌥';
|
||||
case 'Shift':
|
||||
return '⇧';
|
||||
case 'Backspace':
|
||||
return '⌫';
|
||||
case 'Escape':
|
||||
return 'Esc';
|
||||
case 'Enter':
|
||||
return '↩\uFE0E';
|
||||
case 'Tab':
|
||||
return '⇥';
|
||||
}
|
||||
}
|
||||
|
||||
if (part === 'Cmd') return 'Ctrl';
|
||||
if (part === 'Escape') return 'Esc';
|
||||
return part;
|
||||
}
|
||||
|
||||
export function formatChord(chord: string): string {
|
||||
if (!chord) return '';
|
||||
const parts = chord.split('+').map(formatChordPart);
|
||||
return IS_MAC ? parts.join('') : parts.join('+');
|
||||
}
|
||||
|
||||
function buildReverseLookup(bindings: KeybindingsMap): Map<string, ConfigurableShortcut> {
|
||||
const lookup = new Map<string, ConfigurableShortcut>();
|
||||
for (const id of CONFIGURABLE_SHORTCUTS) {
|
||||
const chord = bindings[id];
|
||||
if (chord) lookup.set(chord, id);
|
||||
}
|
||||
return lookup;
|
||||
}
|
||||
|
||||
export function matchKeybinding(event: KeyboardEvent): ConfigurableShortcut | null {
|
||||
const chord = eventToChord(event);
|
||||
if (!chord) return null;
|
||||
return buildReverseLookup(get(keybindings)).get(chord) ?? null;
|
||||
}
|
||||
|
||||
export const shortcuts: ShortcutRegistry = {
|
||||
//Chat
|
||||
[Shortcut.NEW_CHAT]: {
|
||||
name: 'New Chat',
|
||||
keys: ['mod', 'shift', 'O'],
|
||||
category: 'Chat'
|
||||
category: 'Chat',
|
||||
configurable: true
|
||||
},
|
||||
[Shortcut.NEW_TEMPORARY_CHAT]: {
|
||||
name: 'New Temporary Chat',
|
||||
keys: ['mod', 'shift', `'`],
|
||||
category: 'Chat'
|
||||
category: 'Chat',
|
||||
configurable: true
|
||||
},
|
||||
[Shortcut.DELETE_CHAT]: {
|
||||
name: 'Delete Chat',
|
||||
keys: ['mod', 'shift', 'Backspace', 'Delete'],
|
||||
category: 'Chat'
|
||||
keys: ['mod', 'shift', 'Backspace'],
|
||||
category: 'Chat',
|
||||
configurable: true
|
||||
},
|
||||
[Shortcut.OPEN_MODEL_SELECTOR]: {
|
||||
name: 'Open Model Selector',
|
||||
keys: ['mod', 'shift', 'M'],
|
||||
category: 'Chat'
|
||||
category: 'Chat',
|
||||
configurable: true
|
||||
},
|
||||
[Shortcut.TOGGLE_DICTATION]: {
|
||||
name: 'Toggle Dictation',
|
||||
keys: ['mod', 'shift', 'L'],
|
||||
category: 'Chat'
|
||||
category: 'Chat',
|
||||
configurable: true
|
||||
},
|
||||
|
||||
//Global
|
||||
[Shortcut.SEARCH]: {
|
||||
name: 'Search',
|
||||
keys: ['mod', 'K'],
|
||||
category: 'Global'
|
||||
category: 'Global',
|
||||
configurable: true
|
||||
},
|
||||
[Shortcut.OPEN_SETTINGS]: {
|
||||
name: 'Open Settings',
|
||||
keys: ['mod', '.'],
|
||||
category: 'Global'
|
||||
category: 'Global',
|
||||
configurable: true
|
||||
},
|
||||
[Shortcut.SHOW_SHORTCUTS]: {
|
||||
name: 'Show Shortcuts',
|
||||
keys: ['mod', '/'],
|
||||
category: 'Global'
|
||||
category: 'Global',
|
||||
configurable: true
|
||||
},
|
||||
[Shortcut.TOGGLE_SIDEBAR]: {
|
||||
name: 'Toggle Sidebar',
|
||||
keys: ['mod', 'shift', 'S'],
|
||||
category: 'Global'
|
||||
category: 'Global',
|
||||
configurable: true
|
||||
},
|
||||
[Shortcut.CLOSE_MODAL]: {
|
||||
name: 'Close Modal',
|
||||
keys: ['Escape'],
|
||||
category: 'Global'
|
||||
category: 'Global',
|
||||
configurable: true
|
||||
},
|
||||
|
||||
//Input
|
||||
[Shortcut.FOCUS_INPUT]: {
|
||||
name: 'Focus Chat Input',
|
||||
keys: ['shift', 'Escape'],
|
||||
category: 'Input'
|
||||
category: 'Input',
|
||||
configurable: true
|
||||
},
|
||||
[Shortcut.ACCEPT_AUTOCOMPLETE]: {
|
||||
name: 'Accept Autocomplete Generation\nJump to Prompt Variable',
|
||||
|
|
@ -139,12 +297,14 @@ export const shortcuts: ShortcutRegistry = {
|
|||
name: 'Generate Message Pair',
|
||||
keys: ['mod', 'shift', 'Enter'],
|
||||
category: 'Message',
|
||||
configurable: true,
|
||||
tooltip: 'Only active when the chat input is in focus.'
|
||||
},
|
||||
[Shortcut.REGENERATE_RESPONSE]: {
|
||||
name: 'Regenerate Response',
|
||||
keys: ['mod', 'R'],
|
||||
category: 'Message'
|
||||
category: 'Message',
|
||||
configurable: true
|
||||
},
|
||||
[Shortcut.STOP_GENERATING]: {
|
||||
name: 'Stop Generating',
|
||||
|
|
@ -161,12 +321,14 @@ export const shortcuts: ShortcutRegistry = {
|
|||
[Shortcut.COPY_LAST_RESPONSE]: {
|
||||
name: 'Copy Last Response',
|
||||
keys: ['mod', 'shift', 'C'],
|
||||
category: 'Message'
|
||||
category: 'Message',
|
||||
configurable: true
|
||||
},
|
||||
[Shortcut.COPY_LAST_CODE_BLOCK]: {
|
||||
name: 'Copy Last Code Block',
|
||||
keys: ['mod', 'shift', ';'],
|
||||
category: 'Message'
|
||||
category: 'Message',
|
||||
configurable: true
|
||||
},
|
||||
|
||||
//Voice
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@
|
|||
import AccountPending from '$lib/components/layout/Overlay/AccountPending.svelte';
|
||||
import UpdateInfoToast from '$lib/components/layout/UpdateInfoToast.svelte';
|
||||
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||
import { Shortcut, shortcuts } from '$lib/shortcuts';
|
||||
import { loadKeybindings, matchKeybinding, Shortcut } from '$lib/shortcuts';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
|
|
@ -105,6 +105,7 @@
|
|||
if (userSettings?.ui) {
|
||||
settings.set(userSettings.ui);
|
||||
}
|
||||
loadKeybindings(userSettings?.keybindings);
|
||||
|
||||
setTextScale($settings?.textScale ?? 1);
|
||||
|
||||
|
|
@ -245,80 +246,54 @@
|
|||
// Tool servers can be slow or unreachable; they are not needed to initialize chat.
|
||||
setToolServers().catch((e) => console.error('Failed to load tool servers:', e));
|
||||
|
||||
// Helper function to check if the pressed keys match the shortcut definition
|
||||
const isShortcutMatch = (event: KeyboardEvent, shortcut): boolean => {
|
||||
const keys = shortcut?.keys || [];
|
||||
|
||||
const normalized = keys.map((k) => k.toLowerCase());
|
||||
const needCtrl = normalized.includes('ctrl') || normalized.includes('mod');
|
||||
const needShift = normalized.includes('shift');
|
||||
const needAlt = normalized.includes('alt');
|
||||
|
||||
const mainKeys = normalized.filter((k) => !['ctrl', 'shift', 'alt', 'mod'].includes(k));
|
||||
|
||||
// Get the main key pressed
|
||||
const keyPressed = event.key.toLowerCase();
|
||||
|
||||
// Check modifiers
|
||||
if (needShift && !event.shiftKey) return false;
|
||||
|
||||
if (needCtrl && !(event.ctrlKey || event.metaKey)) return false;
|
||||
if (!needCtrl && (event.ctrlKey || event.metaKey)) return false;
|
||||
if (needAlt && !event.altKey) return false;
|
||||
if (!needAlt && event.altKey) return false;
|
||||
|
||||
if (mainKeys.length && !mainKeys.includes(keyPressed)) return false;
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const setupKeyboardShortcuts = () => {
|
||||
document.addEventListener('keydown', async (event) => {
|
||||
if (isShortcutMatch(event, shortcuts[Shortcut.SEARCH])) {
|
||||
const shortcut = matchKeybinding(event);
|
||||
if (shortcut === Shortcut.SEARCH) {
|
||||
console.log('Shortcut triggered: SEARCH');
|
||||
event.preventDefault();
|
||||
showSearch.set(!$showSearch);
|
||||
} else if (isShortcutMatch(event, shortcuts[Shortcut.NEW_CHAT])) {
|
||||
} else if (shortcut === Shortcut.NEW_CHAT) {
|
||||
console.log('Shortcut triggered: NEW_CHAT');
|
||||
event.preventDefault();
|
||||
document.getElementById('sidebar-new-chat-button')?.click();
|
||||
} else if (isShortcutMatch(event, shortcuts[Shortcut.FOCUS_INPUT])) {
|
||||
} else if (shortcut === Shortcut.FOCUS_INPUT) {
|
||||
console.log('Shortcut triggered: FOCUS_INPUT');
|
||||
event.preventDefault();
|
||||
document.getElementById('chat-input')?.focus();
|
||||
} else if (isShortcutMatch(event, shortcuts[Shortcut.COPY_LAST_CODE_BLOCK])) {
|
||||
} else if (shortcut === Shortcut.COPY_LAST_CODE_BLOCK) {
|
||||
console.log('Shortcut triggered: COPY_LAST_CODE_BLOCK');
|
||||
event.preventDefault();
|
||||
[...document.getElementsByClassName('copy-code-button')]?.at(-1)?.click();
|
||||
} else if (isShortcutMatch(event, shortcuts[Shortcut.COPY_LAST_RESPONSE])) {
|
||||
} else if (shortcut === Shortcut.COPY_LAST_RESPONSE) {
|
||||
console.log('Shortcut triggered: COPY_LAST_RESPONSE');
|
||||
event.preventDefault();
|
||||
[...document.getElementsByClassName('copy-response-button')]?.at(-1)?.click();
|
||||
} else if (isShortcutMatch(event, shortcuts[Shortcut.TOGGLE_SIDEBAR])) {
|
||||
} else if (shortcut === Shortcut.TOGGLE_SIDEBAR) {
|
||||
console.log('Shortcut triggered: TOGGLE_SIDEBAR');
|
||||
event.preventDefault();
|
||||
showSidebar.set(!$showSidebar);
|
||||
} else if (isShortcutMatch(event, shortcuts[Shortcut.DELETE_CHAT])) {
|
||||
} else if (shortcut === Shortcut.DELETE_CHAT) {
|
||||
console.log('Shortcut triggered: DELETE_CHAT');
|
||||
event.preventDefault();
|
||||
document.getElementById('delete-chat-button')?.click();
|
||||
} else if (isShortcutMatch(event, shortcuts[Shortcut.OPEN_SETTINGS])) {
|
||||
} else if (shortcut === Shortcut.OPEN_SETTINGS) {
|
||||
console.log('Shortcut triggered: OPEN_SETTINGS');
|
||||
event.preventDefault();
|
||||
showSettings.set(!$showSettings);
|
||||
} else if (isShortcutMatch(event, shortcuts[Shortcut.SHOW_SHORTCUTS])) {
|
||||
} else if (shortcut === Shortcut.SHOW_SHORTCUTS) {
|
||||
console.log('Shortcut triggered: SHOW_SHORTCUTS');
|
||||
event.preventDefault();
|
||||
showSettings.set('shortcuts');
|
||||
} else if (isShortcutMatch(event, shortcuts[Shortcut.CLOSE_MODAL])) {
|
||||
} else if (shortcut === Shortcut.CLOSE_MODAL) {
|
||||
console.log('Shortcut triggered: CLOSE_MODAL');
|
||||
event.preventDefault();
|
||||
showSettings.set(false);
|
||||
} else if (isShortcutMatch(event, shortcuts[Shortcut.OPEN_MODEL_SELECTOR])) {
|
||||
} else if (shortcut === 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])) {
|
||||
} else if (shortcut === Shortcut.NEW_TEMPORARY_CHAT) {
|
||||
console.log('Shortcut triggered: NEW_TEMPORARY_CHAT');
|
||||
event.preventDefault();
|
||||
if ($user?.role !== 'admin' && $user?.permissions?.chat?.temporary_enforced) {
|
||||
|
|
@ -330,12 +305,12 @@
|
|||
setTimeout(() => {
|
||||
document.getElementById('new-chat-button')?.click();
|
||||
}, 0);
|
||||
} else if (isShortcutMatch(event, shortcuts[Shortcut.GENERATE_MESSAGE_PAIR])) {
|
||||
} else if (shortcut === Shortcut.GENERATE_MESSAGE_PAIR) {
|
||||
console.log('Shortcut triggered: GENERATE_MESSAGE_PAIR');
|
||||
event.preventDefault();
|
||||
document.getElementById('generate-message-pair-button')?.click();
|
||||
} else if (
|
||||
isShortcutMatch(event, shortcuts[Shortcut.REGENERATE_RESPONSE]) &&
|
||||
shortcut === Shortcut.REGENERATE_RESPONSE &&
|
||||
document.activeElement?.id === 'chat-input'
|
||||
) {
|
||||
console.log('Shortcut triggered: REGENERATE_RESPONSE');
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import CreateKnowledgeBase from '$lib/components/workspace/Knowledge/CreateKnowledgeBase.svelte';
|
||||
import Knowledge from '$lib/components/workspace/Knowledge.svelte';
|
||||
</script>
|
||||
|
||||
<CreateKnowledgeBase />
|
||||
<Knowledge showCreateOnMount={true} createModalCloseHref="/workspace/knowledge" />
|
||||
|
|
|
|||
|
|
@ -1,79 +1,5 @@
|
|||
<script lang="ts">
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { goto } from '$app/navigation';
|
||||
import { onMount, tick, getContext } from 'svelte';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
import { createNewPrompt } from '$lib/apis/prompts';
|
||||
import PromptEditor from '$lib/components/workspace/Prompts/PromptEditor.svelte';
|
||||
|
||||
let prompt: {
|
||||
name: string;
|
||||
command: string;
|
||||
content: string;
|
||||
tags: string[];
|
||||
access_grants: any[];
|
||||
} | null = null;
|
||||
|
||||
let clone = false;
|
||||
|
||||
const onSubmit = async (_prompt) => {
|
||||
const res = await createNewPrompt(localStorage.token, _prompt).catch((error) => {
|
||||
toast.error(`${error}`);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (res) {
|
||||
toast.success($i18n.t('Prompt created successfully'));
|
||||
await goto('/workspace/prompts');
|
||||
}
|
||||
};
|
||||
|
||||
onMount(async () => {
|
||||
window.addEventListener('message', async (event) => {
|
||||
console.log(event);
|
||||
if (
|
||||
!['https://openwebui.com', 'https://www.openwebui.com', 'http://localhost:9999'].includes(
|
||||
event.origin
|
||||
)
|
||||
)
|
||||
return;
|
||||
const _prompt = JSON.parse(event.data);
|
||||
console.log('Received prompt via window message:', _prompt);
|
||||
|
||||
clone = true;
|
||||
prompt = {
|
||||
name: _prompt.name || _prompt.title || 'Prompt',
|
||||
command: _prompt.command,
|
||||
content: _prompt.content,
|
||||
tags: _prompt.tags || [],
|
||||
access_grants: _prompt.access_grants !== undefined ? _prompt.access_grants : []
|
||||
};
|
||||
});
|
||||
|
||||
if (window.opener ?? false) {
|
||||
window.opener.postMessage('loaded', '*');
|
||||
}
|
||||
|
||||
if (sessionStorage.prompt) {
|
||||
const _prompt = JSON.parse(sessionStorage.prompt);
|
||||
sessionStorage.removeItem('prompt');
|
||||
|
||||
console.log('Received prompt via sessionStorage:', _prompt);
|
||||
|
||||
clone = true;
|
||||
prompt = {
|
||||
name: _prompt.name || _prompt.title || 'Prompt',
|
||||
command: _prompt.command,
|
||||
content: _prompt.content,
|
||||
tags: _prompt.tags || [],
|
||||
access_grants: _prompt.access_grants !== undefined ? _prompt.access_grants : []
|
||||
};
|
||||
}
|
||||
});
|
||||
<script>
|
||||
import Prompts from '$lib/components/workspace/Prompts.svelte';
|
||||
</script>
|
||||
|
||||
{#key prompt}
|
||||
<PromptEditor {prompt} {onSubmit} {clone} />
|
||||
{/key}
|
||||
<Prompts showCreateOnMount={true} createModalCloseHref="/workspace/prompts" />
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue