This commit is contained in:
Timothy Jaeryang Baek
2026-07-15 16:21:47 -04:00
parent 946487a3ba
commit 86f55d04ec
7 changed files with 138 additions and 101 deletions
+2 -1
View File
@@ -12,9 +12,10 @@
stroke="currentColor"
class={className}
>
<rect x="9" y="2" width="6" height="12" rx="3" />
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M12 18.75a6 6 0 0 0 6-6v-1.5m-6 7.5a6 6 0 0 1-6-6v-1.5m6 7.5v3.75m-3.75 0h7.5M12 15.75a3 3 0 0 1-3-3V4.5a3 3 0 1 1 6 0v8.25a3 3 0 0 1-3 3Z"
d="M5 10v1a7 7 0 0 0 14 0v-1M12 18v4m0 0H9m3 0h3"
/>
</svg>
+58 -26
View File
@@ -4,6 +4,7 @@
import Sortable from 'sortablejs';
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import {
user,
chats,
@@ -161,6 +162,26 @@
return items[id];
};
const menuItemPathPrefixes = {
notes: '/notes',
workspace: '/workspace',
calendar: '/calendar',
automations: '/automations',
playground: '/playground'
};
const getActiveMenuItemId = (pathname) => {
for (const [id, pathPrefix] of Object.entries(menuItemPathPrefixes)) {
if (pathname === pathPrefix || pathname.startsWith(`${pathPrefix}/`)) {
return id;
}
}
return null;
};
$: activeMenuItemId = getActiveMenuItemId($page.url.pathname);
const initPinnedMenuSortable = () => {
const el = document.getElementById('pinned-menu-items-list');
if (el && !$mobile) {
@@ -795,7 +816,7 @@
{#if !$mobile && !$showSidebar}
<div
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"
class=" w-[42px] shrink-0 py-1 px-1 flex flex-col justify-between text-gray-700 dark:text-gray-300 hover:bg-gray-50/30 dark:hover:bg-gray-800/30 h-full z-10 transition-all border-e-[0.5px] border-gray-50 dark:border-gray-850/30"
id="sidebar"
>
<button
@@ -816,7 +837,7 @@
aria-label={$showSidebar ? $i18n.t('Close Sidebar') : $i18n.t('Open Sidebar')}
>
<div
class=" self-center flex size-[30px] items-center justify-center rounded-lg transition group-hover:bg-gray-100/50 dark:group-hover:bg-gray-850/50"
class=" self-center flex size-[30px] items-center justify-center rounded-lg transition group-hover:bg-gray-50/40 dark:group-hover:bg-gray-800/40"
>
<img
src="{WEBUI_BASE_URL}/static/favicon.png"
@@ -847,9 +868,9 @@
aria-label={$i18n.t('New Chat')}
>
<div
class=" self-center flex size-[30px] items-center justify-center rounded-lg transition group-hover:bg-gray-100/50 dark:group-hover:bg-gray-850/50"
class=" self-center flex size-[30px] items-center justify-center rounded-lg transition group-hover:bg-gray-50/40 dark:group-hover:bg-gray-800/40"
>
<EditPencilIcon className="size-4" />
<EditPencilIcon className="size-4" strokeWidth="1.5" />
</div>
</a>
</Tooltip>
@@ -869,9 +890,9 @@
aria-label={$i18n.t('Search')}
>
<div
class=" self-center flex size-[30px] items-center justify-center rounded-lg transition group-hover:bg-gray-100/50 dark:group-hover:bg-gray-850/50"
class=" self-center flex size-[30px] items-center justify-center rounded-lg transition group-hover:bg-gray-50/40 dark:group-hover:bg-gray-800/40"
>
<SearchIcon className="size-4" />
<SearchIcon className="size-4" strokeWidth="1.5" />
</div>
</button>
</Tooltip>
@@ -883,7 +904,10 @@
<div class="">
<Tooltip content={$i18n.t(meta.label)} placement="right">
<a
class=" cursor-pointer flex size-8 items-center justify-center transition group"
class=" cursor-pointer flex size-8 items-center justify-center transition group {itemId ===
activeMenuItemId
? 'text-gray-900 dark:text-gray-100'
: ''}"
href={meta.href}
on:click={async (e) => {
e.stopImmediatePropagation();
@@ -895,18 +919,21 @@
aria-label={$i18n.t(meta.label)}
>
<div
class=" self-center flex size-[30px] items-center justify-center rounded-lg transition group-hover:bg-gray-100/50 dark:group-hover:bg-gray-850/50"
class=" self-center flex size-[30px] items-center justify-center rounded-lg transition {itemId ===
activeMenuItemId
? 'bg-gray-100/80 dark:bg-gray-850/50'
: 'group-hover:bg-gray-50/40 dark:group-hover:bg-gray-800/40'}"
>
{#if itemId === 'notes'}
<NotesIcon className="size-4" />
<NotesIcon className="size-4" strokeWidth="1.5" />
{:else if itemId === 'workspace'}
<WorkspaceIcon className="size-4" />
<WorkspaceIcon className="size-4" strokeWidth="1.5" />
{:else if itemId === 'automations'}
<ClockIcon className="size-4" />
<ClockIcon className="size-4" strokeWidth="1.5" />
{:else if itemId === 'calendar'}
<CalendarIcon className="size-4" />
<CalendarIcon className="size-4" strokeWidth="1.5" />
{:else if itemId === 'playground'}
<CodeIcon className="size-4" />
<CodeIcon className="size-4" strokeWidth="1.5" />
{/if}
</div>
</a>
@@ -928,7 +955,7 @@
aria-label={$i18n.t('User menu')}
>
<div
class="self-center relative flex size-[30px] items-center justify-center rounded-lg transition group-hover:bg-gray-100/50 dark:group-hover:bg-gray-850/50"
class="self-center relative flex size-[30px] items-center justify-center rounded-lg transition group-hover:bg-gray-50/40 dark:group-hover:bg-gray-800/40"
>
<img
src={`${WEBUI_API_BASE_URL}/users/${$user?.id}/profile/image`}
@@ -969,7 +996,7 @@
? `${$mobile ? 'bg-gray-50 dark:bg-gray-950' : 'bg-gray-50/70 dark:bg-gray-950/70'} z-50`
: ' bg-transparent z-0 '} {$isApp
? `ml-[4.5rem] md:ml-0 `
: ' transition-all duration-300 '} shrink-0 text-gray-900 dark:text-gray-200 text-sm fixed top-0 left-0 overflow-x-hidden
: ' transition-all duration-300 '} shrink-0 text-gray-700 dark:text-gray-300 text-sm fixed top-0 left-0 overflow-x-hidden
"
transition:slide={{ duration: 250, axis: 'x' }}
data-state={$showSidebar}
@@ -983,7 +1010,7 @@
class="sidebar px-1 pt-1.5 pb-1 flex justify-between space-x-1 text-gray-600 dark:text-gray-400 sticky top-0 z-10 -mb-2"
>
<a
class="flex items-center rounded-xl size-8.5 h-full justify-center hover:bg-gray-100/50 dark:hover:bg-gray-850/50 transition no-drag-region"
class="flex items-center rounded-xl size-8.5 h-full justify-center hover:bg-gray-50/60 dark:hover:bg-gray-800/40 transition no-drag-region"
href="/"
draggable="false"
on:click={newChatHandler}
@@ -999,7 +1026,7 @@
<a href="/" class="flex flex-1 px-0.5" on:click={newChatHandler}>
<div
id="sidebar-webui-name"
class=" self-center font-normal text-gray-850 dark:text-white"
class=" self-center font-normal text-gray-700 dark:text-gray-200"
>
{$WEBUI_NAME}
</div>
@@ -1009,7 +1036,7 @@
placement="bottom"
>
<button
class="flex size-[30px] justify-center items-center rounded-lg hover:bg-gray-100/50 dark:hover:bg-gray-850/50 transition {isWindows
class="flex size-[30px] justify-center items-center rounded-lg hover:bg-gray-50/60 dark:hover:bg-gray-800/40 transition {isWindows
? 'cursor-pointer'
: 'cursor-[w-resize]'}"
on:click={() => {
@@ -1041,10 +1068,10 @@
}}
>
<div class="pb-1">
<div class="px-1 flex justify-center text-gray-800 dark:text-gray-200">
<div class="px-1 flex justify-center text-gray-700 dark:text-gray-300">
<a
id="sidebar-new-chat-button"
class="group grow flex items-center space-x-2 rounded-xl px-2 py-1.5 hover:bg-gray-100 dark:hover:bg-gray-900 transition outline-none"
class="group grow flex items-center space-x-2 rounded-xl px-2 py-1.5 hover:bg-gray-50/40 dark:hover:bg-gray-800/40 transition outline-none"
href="/"
draggable="false"
on:click={newChatHandler}
@@ -1062,10 +1089,10 @@
</a>
</div>
<div class="px-1 flex justify-center text-gray-800 dark:text-gray-200">
<div class="px-1 flex justify-center text-gray-700 dark:text-gray-300">
<button
id="sidebar-search-button"
class="group grow flex items-center space-x-2 rounded-xl px-2 py-1.5 hover:bg-gray-100 dark:hover:bg-gray-900 transition outline-none"
class="group grow flex items-center space-x-2 rounded-xl px-2 py-1.5 hover:bg-gray-50/40 dark:hover:bg-gray-800/40 transition outline-none"
on:click={() => {
showSearch.set(true);
}}
@@ -1088,12 +1115,17 @@
{@const meta = getMenuItemMeta(itemId)}
{#if meta && isMenuItemVisible(itemId)}
<div
class="px-1 flex justify-center text-gray-800 dark:text-gray-200"
class="px-1 flex justify-center {itemId === activeMenuItemId
? 'text-gray-900 dark:text-gray-100'
: 'text-gray-700 dark:text-gray-300'}"
data-id={itemId}
>
<a
id="sidebar-{itemId}-button"
class="grow flex items-center space-x-2 rounded-xl px-2 py-1.5 hover:bg-gray-100 dark:hover:bg-gray-900 transition"
class="grow flex items-center space-x-2 rounded-xl px-2 py-1.5 transition {itemId ===
activeMenuItemId
? 'bg-gray-100/80 dark:bg-gray-850/50'
: 'hover:bg-gray-50/40 dark:hover:bg-gray-800/40'}"
href={meta.href}
on:click={itemClickHandler}
draggable="false"
@@ -1371,7 +1403,7 @@
name={$i18n.t('Pinned')}
>
<div
class="ml-3 pl-1 mt-[1px] flex flex-col overflow-y-auto scrollbar-hidden border-s border-gray-100 dark:border-gray-900 text-gray-900 dark:text-gray-200"
class="ml-3 pl-1 mt-[1px] flex flex-col overflow-y-auto scrollbar-hidden border-s border-gray-100 dark:border-gray-900 text-gray-700 dark:text-gray-300"
>
{#each $pinnedChats as chat, idx (`pinned-chat-${chat?.id ?? idx}`)}
<ChatItem
@@ -1504,7 +1536,7 @@
>
<button
type="button"
class=" flex items-center rounded-xl py-1.5 px-1.5 w-full hover:bg-gray-100/50 dark:hover:bg-gray-900/50 transition"
class=" flex items-center rounded-xl py-1.5 px-1.5 w-full hover:bg-gray-50/40 dark:hover:bg-gray-800/40 transition"
aria-label={$i18n.t('User menu')}
>
<div class=" self-center mr-3 relative flex-shrink-0">
@@ -459,8 +459,8 @@
: selected
? ($settings?.highContrastMode ?? false)
? 'bg-gray-100 dark:bg-gray-900 selected'
: 'bg-gray-100 dark:bg-gray-950 selected'
: 'group-hover:bg-gray-100 dark:group-hover:bg-gray-950'} whitespace-nowrap text-ellipsis relative {generating
: 'bg-gray-100 dark:bg-gray-900 selected'
: 'hover:bg-gray-50 dark:hover:bg-gray-900 group-hover:bg-gray-50 dark:group-hover:bg-gray-900'} whitespace-nowrap text-ellipsis relative transition {generating
? 'cursor-not-allowed'
: ''}"
>
@@ -498,8 +498,8 @@
: selected
? ($settings?.highContrastMode ?? false)
? 'bg-gray-100 dark:bg-gray-900 selected'
: 'bg-gray-100 dark:bg-gray-950 selected'
: ' group-hover:bg-gray-100 dark:group-hover:bg-gray-950'} whitespace-nowrap text-ellipsis"
: 'bg-gray-100 dark:bg-gray-900 selected'
: ' hover:bg-gray-50 dark:hover:bg-gray-900 group-hover:bg-gray-50 dark:group-hover:bg-gray-900'} whitespace-nowrap text-ellipsis transition"
href="/c/{id}"
on:click={() => {
dispatch('select');
@@ -559,7 +559,7 @@
<div
dir="auto"
class="text-left self-center overflow-hidden w-full h-[20px] truncate {unread
? 'font-normal text-gray-900 dark:text-gray-100'
? 'font-normal text-gray-800 dark:text-gray-200'
: ''}"
>
{title}
@@ -587,8 +587,8 @@
: selected
? ($settings?.highContrastMode ?? false)
? 'from-gray-100 dark:from-gray-900 selected'
: 'from-gray-100 dark:from-gray-950 selected'
: 'invisible group-hover:visible from-gray-100 dark:from-gray-950'}
: 'from-gray-100 dark:from-gray-900 selected'
: 'invisible group-hover:visible from-gray-50 dark:from-gray-900'}
absolute {className === 'pr-2'
? 'right-[8px]'
: 'right-1'} top-[4px] py-1 pr-0.5 mr-1.5 pl-5 bg-linear-to-l from-80%
@@ -629,7 +629,7 @@
}}
type="button"
>
<ArchiveIcon className="size-4 translate-y-[0.5px]" strokeWidth="1.5" />
<ArchiveIcon className="size-4 translate-y-[0.5px]" strokeWidth="1.5" />
</button>
</Tooltip>
@@ -535,9 +535,9 @@
<div class="w-full group">
<div
id="folder-{folderId}-button"
class="relative w-full py-1 px-1.5 rounded-xl flex items-center gap-1.5 hover:bg-gray-100 dark:hover:bg-gray-900 transition {$selectedFolder?.id ===
class="relative w-full py-1 px-1.5 rounded-xl flex items-center gap-1.5 hover:bg-gray-50/40 dark:hover:bg-gray-800/40 transition {$selectedFolder?.id ===
folderId
? 'bg-gray-100 dark:bg-gray-900 selected'
? 'bg-gray-100/80 dark:bg-gray-850/50 selected'
: ''}"
on:dblclick={(e) => {
if (folders[folderId]?.shared && folders[folderId]?.permission !== 'write') return;
@@ -577,7 +577,7 @@
}}
>
<button
class="text-gray-500 dark:text-gray-500 transition-all p-1 hover:bg-gray-200 dark:hover:bg-gray-850 rounded-lg"
class="text-gray-500 dark:text-gray-500 transition-all p-1 hover:bg-gray-50/40 dark:hover:bg-gray-800/40 rounded-lg"
on:click={(e) => {
e.stopPropagation();
e.stopImmediatePropagation();
@@ -58,7 +58,7 @@
<button
class="w-full py-1 flex items-center gap-1 text-xs text-left font-normal
text-gray-600 dark:text-gray-400
hover:text-gray-900 dark:hover:text-gray-200
hover:text-gray-800 dark:hover:text-gray-300
transition-colors duration-100 cursor-pointer"
on:click={toggleExpand}
>
+65 -62
View File
@@ -73,7 +73,7 @@
import RichTextInput from '../common/RichTextInput.svelte';
import Spinner from '../common/Spinner.svelte';
import MicSolid from '../icons/MicSolid.svelte';
import Mic from '../icons/Mic.svelte';
import VoiceRecording from '../chat/MessageInput/VoiceRecording.svelte';
import DeleteConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
import ChatBubbleOval from '../icons/ChatBubbleOval.svelte';
@@ -1063,6 +1063,62 @@ Provide the enhanced notes in markdown format. Use markdown syntax for headings,
</Tooltip>
{/if}
{#if note?.write_access}
<RecordMenu
onRecord={async () => {
displayMediaRecord = false;
try {
let stream = await navigator.mediaDevices
.getUserMedia({ audio: true })
.catch(function (err) {
toast.error(
$i18n.t(`Permission denied when accessing microphone: {{error}}`, {
error: err
})
);
return null;
});
if (stream) {
recording = true;
const tracks = stream.getTracks();
tracks.forEach((track) => track.stop());
}
stream = null;
} catch {
toast.error($i18n.t('Permission denied when accessing microphone'));
}
}}
onCaptureAudio={async () => {
displayMediaRecord = true;
recording = true;
}}
onUpload={async () => {
const input = document.createElement('input');
input.type = 'file';
input.accept = 'audio/*';
input.multiple = false;
input.click();
input.onchange = async (e) => {
const files = e.target.files;
if (files && files.length > 0) {
await uploadFileHandler(files[0]);
}
};
}}
>
<Tooltip content={$i18n.t('Record')} placement="top">
<div class="p-1 bg-transparent hover:bg-white/5 transition rounded-lg">
<Mic className="size-4" />
</div>
</Tooltip>
</RecordMenu>
{/if}
<NoteMenu
onDownload={(type) => {
downloadHandler(type);
@@ -1107,12 +1163,14 @@ Provide the enhanced notes in markdown format. Use markdown syntax for headings,
</NoteMenu>
{#if note?.write_access}
<AccessButton
on:click={() => {
showAccessControlModal = true;
}}
disabled={note?.user_id !== $user?.id && $user?.role !== 'admin'}
/>
<div class="ml-1.5">
<AccessButton
on:click={() => {
showAccessControlModal = true;
}}
disabled={note?.user_id !== $user?.id && $user?.role !== 'admin'}
/>
</div>
{:else}
<div class="shrink-0 text-xs text-gray-500 px-2 py-1">
{$i18n.t('Read-Only Access')}
@@ -1350,61 +1408,6 @@ Provide the enhanced notes in markdown format. Use markdown syntax for headings,
{/if}
</Tooltip>
</div>
<RecordMenu
onRecord={async () => {
displayMediaRecord = false;
try {
let stream = await navigator.mediaDevices
.getUserMedia({ audio: true })
.catch(function (err) {
toast.error(
$i18n.t(`Permission denied when accessing microphone: {{error}}`, {
error: err
})
);
return null;
});
if (stream) {
recording = true;
const tracks = stream.getTracks();
tracks.forEach((track) => track.stop());
}
stream = null;
} catch {
toast.error($i18n.t('Permission denied when accessing microphone'));
}
}}
onCaptureAudio={async () => {
displayMediaRecord = true;
recording = true;
}}
onUpload={async () => {
const input = document.createElement('input');
input.type = 'file';
input.accept = 'audio/*';
input.multiple = false;
input.click();
input.onchange = async (e) => {
const files = e.target.files;
if (files && files.length > 0) {
await uploadFileHandler(files[0]);
}
};
}}
>
<Tooltip content={$i18n.t('Record')} placement="top">
<div
class="cursor-pointer p-2.5 flex rounded-full border border-gray-50 bg-white dark:border-none dark:bg-gray-850 hover:bg-gray-50 dark:hover:bg-gray-800 transition shadow-xl"
>
<MicSolid className="size-4.5" />
</div>
</Tooltip>
</RecordMenu>
{/if}
</div>
</div>
@@ -21,6 +21,7 @@
<Dropdown
bind:show
align="end"
sideOffset={8}
onOpenChange={(state) => {
dispatch('change', state);