This commit is contained in:
Timothy Jaeryang Baek
2026-08-19 16:35:49 -07:00
parent dd7158b45f
commit 33dff414e8
9 changed files with 301 additions and 414 deletions
-31
View File
@@ -77,7 +77,6 @@
"mammoth": "^1.11.0",
"marked": "^9.1.0",
"mermaid": "^11.10.1",
"paneforge": "^0.0.6",
"panzoom": "^9.4.3",
"pdfjs-dist": "^5.4.149",
"prosemirror-collab": "^1.3.1",
@@ -11188,24 +11187,6 @@
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
"node_modules/nanoid": {
"version": "5.1.7",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.7.tgz",
"integrity": "sha512-ua3NDgISf6jdwezAheMOk4mbE1LXjm1DfMUDMuJf4AqxLFK3ccGpgWizwa5YV7Yz9EpXwEaWoRXSb/BnV0t5dQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"bin": {
"nanoid": "bin/nanoid.js"
},
"engines": {
"node": "^18 || >=20"
}
},
"node_modules/natural-compare": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
@@ -11502,18 +11483,6 @@
"integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==",
"license": "(MIT AND Zlib)"
},
"node_modules/paneforge": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/paneforge/-/paneforge-0.0.6.tgz",
"integrity": "sha512-jYeN/wdREihja5c6nK3S5jritDQ+EbCqC5NrDo97qCZzZ9GkmEcN5C0ZCjF4nmhBwkDKr6tLIgz4QUKWxLXjAw==",
"license": "MIT",
"dependencies": {
"nanoid": "^5.0.4"
},
"peerDependencies": {
"svelte": "^4.0.0 || ^5.0.0-next.1"
}
},
"node_modules/panzoom": {
"version": "9.4.3",
"resolved": "https://registry.npmjs.org/panzoom/-/panzoom-9.4.3.tgz",
-1
View File
@@ -121,7 +121,6 @@
"mammoth": "^1.11.0",
"marked": "^9.1.0",
"mermaid": "^11.10.1",
"paneforge": "^0.0.6",
"panzoom": "^9.4.3",
"pdfjs-dist": "^5.4.149",
"prosemirror-collab": "^1.3.1",
+20 -25
View File
@@ -1,6 +1,5 @@
<script lang="ts">
import { toast } from 'svelte-sonner';
import { Pane, PaneGroup, PaneResizer } from 'paneforge';
import { onDestroy, onMount, tick } from 'svelte';
import { goto } from '$app/navigation';
@@ -20,6 +19,7 @@
import MessageInput from './MessageInput.svelte';
import Navbar from './Navbar.svelte';
import Drawer from '../common/Drawer.svelte';
import ResizableSidePanel from '../common/ResizableSidePanel.svelte';
import EllipsisVertical from '../icons/EllipsisVertical.svelte';
import Thread from './Thread.svelte';
import i18n from '$lib/i18n';
@@ -35,11 +35,11 @@
let top = false;
let channel = null;
let channel: any = null;
let messages = null;
let replyToMessage = null;
let threadId = null;
let threadId: any = null;
let typingUsers = [];
let typingUsersTimeout = {};
@@ -265,6 +265,11 @@
let mediaQuery;
let largeScreen = false;
let threadPanelWidth = 420;
const handleMediaQuery = (e) => {
largeScreen = e.matches;
};
onMount(() => {
if ($chatId) {
@@ -275,14 +280,6 @@
mediaQuery = window.matchMedia('(min-width: 1024px)');
const handleMediaQuery = async (e) => {
if (e.matches) {
largeScreen = true;
} else {
largeScreen = false;
}
};
mediaQuery.addEventListener('change', handleMediaQuery);
handleMediaQuery(mediaQuery);
});
@@ -292,6 +289,7 @@
updateLastReadAt(id);
_channelId.set(null);
$socket?.off('events:channel', channelEventHandler);
mediaQuery?.removeEventListener('change', handleMediaQuery);
});
</script>
@@ -325,8 +323,8 @@
: ''} w-full max-w-full flex flex-col"
id="channel-container"
>
<PaneGroup direction="horizontal" class="w-full h-full">
<Pane defaultSize={50} minSize={50} class="h-full flex flex-col w-full relative">
<div class="w-full h-full flex">
<div class="h-full flex flex-col min-w-0 flex-1 relative">
<Navbar
{channel}
onPin={pinHandler}
@@ -407,7 +405,7 @@
</div>
</div>
{/if}
</Pane>
</div>
{#if !largeScreen}
{#if threadId !== null}
@@ -430,16 +428,13 @@
</Drawer>
{/if}
{:else if threadId !== null}
<PaneResizer
class="relative flex items-center justify-center group border-l border-gray-50 dark:border-gray-850/30 hover:border-gray-200 dark:hover:border-gray-800 transition z-20"
id="controls-resizer"
<ResizableSidePanel
open={true}
bind:width={threadPanelWidth}
minWidth={320}
maxWidth={640}
className="h-full"
>
<div
class=" absolute -left-1.5 -right-1.5 -top-0 -bottom-0 z-20 cursor-col-resize bg-transparent"
/>
</PaneResizer>
<Pane defaultSize={50} minSize={30} class="h-full w-full">
<div class="h-full w-full shadow-xl">
<Thread
{threadId}
@@ -450,7 +445,7 @@
}}
/>
</div>
</Pane>
</ResizableSidePanel>
{/if}
</PaneGroup>
</div>
</div>
+5 -24
View File
@@ -1,7 +1,6 @@
<script lang="ts">
import { v4 as uuidv4 } from 'uuid';
import { toast } from 'svelte-sonner';
import { PaneGroup, Pane, PaneResizer } from 'paneforge';
import { getContext, onDestroy, onMount, tick } from 'svelte';
import { fade } from 'svelte/transition';
@@ -147,8 +146,6 @@
$: messageInputDropzoneId = embedded ? 'note-chat-input-dropzone' : 'chat-pane';
const eventTarget = new EventTarget();
let controlPane: Pane | undefined;
let controlPaneComponent: ChatControls | undefined;
let messageInput: MessageInput | undefined;
let messagesRef: Messages | undefined;
@@ -1556,20 +1553,7 @@
stopAudio();
});
const showControlsSubscribe = showControls.subscribe(async (value) => {
await tick();
if (controlPane && !$mobile) {
try {
if (value) {
controlPaneComponent?.openPane();
} else {
controlPane.collapse();
}
} catch (e) {
// ignore
}
}
const showControlsSubscribe = showControls.subscribe((value) => {
if (!value) {
showCallOverlay.set(false);
showArtifacts.set(false);
@@ -4249,8 +4233,8 @@
/>
{/if}
<PaneGroup direction="horizontal" class="w-full h-full">
<Pane defaultSize={50} minSize={30} class="h-full flex relative max-w-full flex-col">
<div class="w-full h-full flex">
<div class="h-full flex relative max-w-full min-w-0 flex-1 flex-col">
<FilesOverlay show={dragged} />
{#if embedded}
<div
@@ -4595,16 +4579,14 @@
</div>
{/if}
</div>
</Pane>
</div>
{#if !embedded}
<ChatControls
bind:this={controlPaneComponent}
bind:history
bind:chatFiles
bind:params
bind:files
bind:pane={controlPane}
chatId={$chatId}
chatUser={chatOwner}
modelId={selectedModelIds?.at(0) ?? null}
@@ -4620,10 +4602,9 @@
{showMessage}
{eventTarget}
{codeInterpreterEnabled}
containerId={chatContainerId}
/>
{/if}
</PaneGroup>
</div>
</div>
{:else if loading}
<div class=" flex items-center justify-center h-full w-full">
+130 -202
View File
@@ -5,10 +5,9 @@
<script lang="ts">
import { SvelteFlowProvider } from '@xyflow/svelte';
import { slide } from 'svelte/transition';
import { Pane, PaneResizer } from 'paneforge';
import { v4 as uuidv4 } from 'uuid';
import { onDestroy, onMount, tick, getContext } from 'svelte';
import { onMount, tick, getContext } from 'svelte';
import {
config,
terminalServers,
@@ -29,6 +28,7 @@
import Controls from './Controls/Controls.svelte';
import CallOverlay from './MessageInput/CallOverlay.svelte';
import Drawer from '../common/Drawer.svelte';
import ResizableSidePanel from '../common/ResizableSidePanel.svelte';
import Artifacts from './Artifacts.svelte';
import Embeds from './ChatControls/Embeds.svelte';
import FileNav from './FileNav.svelte';
@@ -56,13 +56,10 @@
export let codeInterpreterEnabled = false;
export let pane: Pane | null = null;
export let containerId = 'chat-container';
let largeScreen = false;
let dragged = false;
let minSize = 0;
let paneReady = false;
let mounted = false;
let controlsWidth = 350;
// Tab state for Controls+Files panel
let activeTab = savedTab;
@@ -92,11 +89,10 @@
chatContextAvailable(selectedSystemTerminal) &&
!(chatContextNeedsSavedChat(selectedSystemTerminal) && !isSavedChatId(chatId));
$: terminalFilesAvailable = !!(
($selectedTerminalId &&
(selectedSystemTerminalAvailable ||
(!selectedSystemTerminal &&
($user?.role === 'admin' ||
($user?.permissions?.features?.direct_tool_servers ?? true)))))
$selectedTerminalId &&
(selectedSystemTerminalAvailable ||
(!selectedSystemTerminal &&
($user?.role === 'admin' || ($user?.permissions?.features?.direct_tool_servers ?? true))))
);
$: showFilesTab =
terminalFilesAvailable ||
@@ -179,18 +175,6 @@
}
};
export const openPane = () => {
if (parseInt(localStorage?.chatControlsSize)) {
const container = document.getElementById(containerId);
let size = Math.floor(
(parseInt(localStorage?.chatControlsSize) / container.clientWidth) * 100
);
pane.resize(size);
} else {
pane.resize(minSize);
}
};
const handleMediaQuery = async (e) => {
if (e.matches) {
largeScreen = true;
@@ -206,7 +190,6 @@
await tick();
showCallOverlay.set(true);
}
pane = null;
}
};
@@ -222,45 +205,16 @@
mediaQuery.addEventListener('change', handleMediaQuery);
handleMediaQuery(mediaQuery);
let resizeObserver: ResizeObserver | null = null;
let isDestroyed = false;
// Wait for Svelte to render the Pane after largeScreen changed
const init = async () => {
await tick();
if (isDestroyed) return;
// If controls were persisted as open, set the pane to the saved size
if ($showControls && pane) {
openPane();
}
setTimeout(() => {
paneReady = true;
mounted = true;
}, 0);
const container = document.getElementById(containerId) as HTMLElement;
if (!container) return;
minSize = Math.floor((350 / container.clientWidth) * 100);
resizeObserver = new ResizeObserver((entries) => {
for (let entry of entries) {
const width = entry.contentRect.width;
minSize = Math.floor((350 / width) * 100);
if ($showControls) {
if (pane && pane.isExpanded() && pane.getSize() < minSize) {
pane.resize(minSize);
} else {
let size = Math.floor(
(parseInt(localStorage?.chatControlsSize) / container.clientWidth) * 100
);
if (size < minSize && pane) pane.resize(minSize);
}
}
}
});
resizeObserver.observe(container);
};
init();
@@ -269,8 +223,7 @@
return () => {
isDestroyed = true;
paneReady = false;
resizeObserver?.disconnect();
mounted = false;
if (!largeScreen) {
showControls.set(false);
}
@@ -289,7 +242,7 @@
if ($showCallOverlay) showCallOverlay.set(false);
};
$: if (paneReady && !chatId) closeHandler();
$: if (mounted && !chatId) closeHandler();
// Helper: is a "special" full-screen panel active?
$: specialPanel = $showCallOverlay || $showArtifacts || $showEmbeds;
@@ -409,154 +362,129 @@
</Drawer>
{/if}
{:else}
{#if $showControls}
<PaneResizer
class="relative flex items-center justify-center group border-l border-gray-50 dark:border-gray-850/30 hover:border-gray-200 dark:hover:border-gray-800 transition z-20"
id="controls-resizer"
>
<div
class="absolute -left-1.5 -right-1.5 -top-0 -bottom-0 z-20 cursor-col-resize bg-transparent"
/>
</PaneResizer>
{/if}
<Pane
bind:pane
defaultSize={0}
onResize={(size) => {
if ($showControls && pane.isExpanded()) {
if (size < minSize) pane.resize(minSize);
if (size < minSize) {
localStorage.chatControlsSize = 0;
} else {
const container = document.getElementById(containerId);
localStorage.chatControlsSize = Math.floor((size / 100) * container.clientWidth);
}
}
}}
onCollapse={() => {
if (paneReady) showControls.set(false);
}}
collapsible={true}
class="z-10 bg-white dark:bg-gray-900"
<ResizableSidePanel
open={$showControls}
bind:width={controlsWidth}
minWidth={350}
maxWidth={640}
storageKey="chatControlsSize"
className="h-full z-10 bg-white dark:bg-gray-900"
>
{#if $showControls}
<div class="flex max-h-full min-h-full">
<div
class="w-full {specialPanel && !$showCallOverlay
? ' '
: 'bg-white dark:bg-gray-900'} z-40 pointer-events-auto {activeTab === 'files'
? ''
: 'overflow-y-auto'} scrollbar-hidden"
id="controls-container"
>
{#if $showCallOverlay}
<div class="w-full h-full flex justify-center">
<CallOverlay
bind:files
{submitPrompt}
{stopResponse}
{modelId}
{chatId}
{eventTarget}
on:close={() => showControls.set(false)}
/>
</div>
{:else if $showEmbeds}
<Embeds overlay={dragged} />
{:else if $showArtifacts}
<Artifacts {history} overlay={dragged} />
{:else}
<!-- Controls + Files tabs -->
<div class="flex flex-col h-full min-h-0">
<!-- Tab bar -->
<div class="flex items-center justify-between px-2 pt-2 pb-2 shrink-0">
<div class="flex gap-1 min-w-0 overflow-x-auto scrollbar-hidden">
{#if showControlsTab}
<button
class="px-2.5 py-1 text-sm rounded-lg transition whitespace-nowrap {activeTab ===
'controls'
? 'bg-gray-100/40 dark:bg-gray-800/25 font-normal text-gray-700 dark:text-gray-200'
: 'text-gray-500 dark:text-gray-400 hover:bg-gray-100/30 dark:hover:bg-gray-800/20 hover:text-gray-600 dark:hover:text-gray-300'}"
on:click={() => (activeTab = 'controls')}
>
{$i18n.t('Controls')}
</button>
{/if}
{#if showFilesTab}
<button
class="px-2.5 py-1 text-sm rounded-lg transition whitespace-nowrap {activeTab ===
'files'
? 'bg-gray-100/40 dark:bg-gray-800/25 font-normal text-gray-700 dark:text-gray-200'
: 'text-gray-500 dark:text-gray-400 hover:bg-gray-100/30 dark:hover:bg-gray-800/20 hover:text-gray-600 dark:hover:text-gray-300'}"
on:click={() => (activeTab = 'files')}
>
{$i18n.t('Files')}
</button>
{/if}
{#if showOverviewTab}
<button
class="px-2.5 py-1 text-sm rounded-lg transition whitespace-nowrap {activeTab ===
'overview'
? 'bg-gray-100/40 dark:bg-gray-800/25 font-normal text-gray-700 dark:text-gray-200'
: 'text-gray-500 dark:text-gray-400 hover:bg-gray-100/30 dark:hover:bg-gray-800/20 hover:text-gray-600 dark:hover:text-gray-300'}"
on:click={() => (activeTab = 'overview')}
>
{$i18n.t('Overview')}
</button>
{/if}
</div>
<button
class="p-1 rounded-lg text-gray-500 dark:text-gray-400"
on:click={() => showControls.set(false)}
aria-label={$i18n.t('Close')}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="1.5"
class="size-4"
<div class="flex h-full max-h-full min-h-full">
<div
class="w-full {specialPanel && !$showCallOverlay
? ' '
: 'bg-white dark:bg-gray-900'} z-40 pointer-events-auto {activeTab === 'files'
? ''
: 'overflow-y-auto'} scrollbar-hidden"
id="controls-container"
>
{#if $showCallOverlay}
<div class="w-full h-full flex justify-center">
<CallOverlay
bind:files
{submitPrompt}
{stopResponse}
{modelId}
{chatId}
{eventTarget}
on:close={() => showControls.set(false)}
/>
</div>
{:else if $showEmbeds}
<Embeds overlay={dragged} />
{:else if $showArtifacts}
<Artifacts {history} overlay={dragged} />
{:else}
<!-- Controls + Files tabs -->
<div class="flex flex-col h-full min-h-0">
<!-- Tab bar -->
<div class="flex items-center justify-between px-2 pt-2 pb-2 shrink-0">
<div class="flex gap-1 min-w-0 overflow-x-auto scrollbar-hidden">
{#if showControlsTab}
<button
class="px-2.5 py-1 text-sm rounded-lg transition whitespace-nowrap {activeTab ===
'controls'
? 'bg-gray-100/40 dark:bg-gray-800/25 font-normal text-gray-700 dark:text-gray-200'
: 'text-gray-500 dark:text-gray-400 hover:bg-gray-100/30 dark:hover:bg-gray-800/20 hover:text-gray-600 dark:hover:text-gray-300'}"
on:click={() => (activeTab = 'controls')}
>
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12" />
</svg>
</button>
</div>
<div
class="flex-1 min-h-0 {activeTab === 'overview'
? 'h-full'
: activeTab === 'controls'
? 'overflow-y-auto px-3 pt-1'
: ''}"
>
{#if activeTab === 'overview'}
<Overview
{history}
{chatUser}
onNodeClick={(e) => {
const node = e.node;
if (node?.data?.message?.favorite) {
history.messages[node.data.message.id].favorite = true;
} else {
history.messages[node.data.message.id].favorite = null;
}
showMessage(node.data.message, true);
}}
/>
{:else if activeTab === 'files' && terminalFilesAvailable && $selectedTerminalId}
<FileNav onAttach={handleTerminalAttach} overlay={dragged} {chatId} />
{:else if activeTab === 'files' && codeInterpreterEnabled}
<PyodideFileNav overlay={dragged} />
{:else}
<Controls embed={true} {models} bind:chatFiles bind:params />
{$i18n.t('Controls')}
</button>
{/if}
{#if showFilesTab}
<button
class="px-2.5 py-1 text-sm rounded-lg transition whitespace-nowrap {activeTab ===
'files'
? 'bg-gray-100/40 dark:bg-gray-800/25 font-normal text-gray-700 dark:text-gray-200'
: 'text-gray-500 dark:text-gray-400 hover:bg-gray-100/30 dark:hover:bg-gray-800/20 hover:text-gray-600 dark:hover:text-gray-300'}"
on:click={() => (activeTab = 'files')}
>
{$i18n.t('Files')}
</button>
{/if}
{#if showOverviewTab}
<button
class="px-2.5 py-1 text-sm rounded-lg transition whitespace-nowrap {activeTab ===
'overview'
? 'bg-gray-100/40 dark:bg-gray-800/25 font-normal text-gray-700 dark:text-gray-200'
: 'text-gray-500 dark:text-gray-400 hover:bg-gray-100/30 dark:hover:bg-gray-800/20 hover:text-gray-600 dark:hover:text-gray-300'}"
on:click={() => (activeTab = 'overview')}
>
{$i18n.t('Overview')}
</button>
{/if}
</div>
<button
class="p-1 rounded-lg text-gray-500 dark:text-gray-400"
on:click={() => showControls.set(false)}
aria-label={$i18n.t('Close')}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="1.5"
class="size-4"
>
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12" />
</svg>
</button>
</div>
{/if}
</div>
<div
class="flex-1 min-h-0 {activeTab === 'overview'
? 'h-full'
: activeTab === 'controls'
? 'overflow-y-auto px-3 pt-1'
: ''}"
>
{#if activeTab === 'overview'}
<Overview
{history}
{chatUser}
onNodeClick={(e) => {
const node = e.node;
if (node?.data?.message?.favorite) {
history.messages[node.data.message.id].favorite = true;
} else {
history.messages[node.data.message.id].favorite = null;
}
showMessage(node.data.message, true);
}}
/>
{:else if activeTab === 'files' && terminalFilesAvailable && $selectedTerminalId}
<FileNav onAttach={handleTerminalAttach} overlay={dragged} {chatId} />
{:else if activeTab === 'files' && codeInterpreterEnabled}
<PyodideFileNav overlay={dragged} />
{:else}
<Controls embed={true} {models} bind:chatFiles bind:params />
{/if}
</div>
</div>
{/if}
</div>
{/if}
</Pane>
</div>
</ResizableSidePanel>
{/if}
@@ -0,0 +1,120 @@
<script lang="ts">
import { onDestroy, onMount } from 'svelte';
export let open = false;
export let side: 'left' | 'right' = 'right';
export let width = 350;
export let minWidth = 300;
export let maxWidth = 640;
export let storageKey = '';
export let className = '';
export let resizerId = 'controls-resizer';
let isResizing = false;
let startClientX = 0;
let startWidth = 0;
const clamp = (value: number) => Math.min(maxWidth, Math.max(minWidth, value));
const persistWidth = () => {
if (storageKey) {
localStorage.setItem(storageKey, String(width));
}
};
const resizeStartHandler = (e: MouseEvent) => {
if (!open) return;
isResizing = true;
startClientX = e.clientX;
startWidth = width;
document.body.style.userSelect = 'none';
};
const resizeEndHandler = () => {
if (!isResizing) return;
isResizing = false;
document.body.style.userSelect = '';
persistWidth();
};
const resizeHandler = (endClientX: number) => {
const dx = endClientX - startClientX;
width = clamp(side === 'right' ? startWidth - dx : startWidth + dx);
};
const resizeKeyHandler = (e: KeyboardEvent) => {
if (e.key !== 'ArrowLeft' && e.key !== 'ArrowRight') return;
e.preventDefault();
const delta = e.key === 'ArrowLeft' ? -10 : 10;
width = clamp(width + (side === 'right' ? -delta : delta));
persistWidth();
};
onMount(() => {
if (!storageKey) return;
const storedWidth = Number(localStorage.getItem(storageKey));
if (!Number.isNaN(storedWidth)) {
width = clamp(storedWidth);
}
});
onDestroy(() => {
if (isResizing) {
document.body.style.userSelect = '';
}
});
</script>
<svelte:window
on:mousemove={(e) => {
if (!isResizing) return;
resizeHandler(e.clientX);
}}
on:mouseup={resizeEndHandler}
/>
{#if open}
{#if side === 'right'}
<!-- svelte-ignore a11y_no_noninteractive_element_interactions, a11y_no_noninteractive_tabindex -->
<div
class="relative flex items-center justify-center group border-l border-gray-50 dark:border-gray-850/30 hover:border-gray-200 dark:hover:border-gray-800 transition z-20 bg-transparent p-0 appearance-none"
id={resizerId}
on:mousedown={resizeStartHandler}
on:keydown={resizeKeyHandler}
role="separator"
tabindex="0"
aria-label="Resize panel"
aria-orientation="vertical"
>
<span
class="absolute -left-1.5 -right-1.5 -top-0 -bottom-0 z-20 cursor-col-resize bg-transparent"
></span>
</div>
{/if}
<div class={className} style="width: {width}px; flex: 0 0 {width}px;">
<slot />
</div>
{#if side === 'left'}
<!-- svelte-ignore a11y_no_noninteractive_element_interactions, a11y_no_noninteractive_tabindex -->
<div
class="relative flex items-center justify-center group border-r border-gray-50 dark:border-gray-850/30 hover:border-gray-200 dark:hover:border-gray-800 transition z-20 bg-transparent p-0 appearance-none"
id={resizerId}
on:mousedown={resizeStartHandler}
on:keydown={resizeKeyHandler}
role="separator"
tabindex="0"
aria-label="Resize panel"
aria-orientation="vertical"
>
<span
class="absolute -left-1.5 -right-1.5 -top-0 -bottom-0 z-20 cursor-col-resize bg-transparent"
></span>
</div>
{/if}
{/if}
+4 -6
View File
@@ -21,8 +21,6 @@
dayjs.extend(duration);
dayjs.extend(relativeTime);
import { PaneGroup, Pane, PaneResizer } from 'paneforge';
import { compressImage, copyToClipboard, convertHeicToJpeg } from '$lib/utils';
import { WEBUI_BASE_URL } from '$lib/constants';
import { getFileById, uploadFile } from '$lib/apis/files';
@@ -1006,8 +1004,8 @@ ${content}
</div>
</DeleteConfirmDialog>
<PaneGroup direction="horizontal" class="w-full h-full">
<Pane defaultSize={70} minSize={30} class="h-full flex flex-col w-full relative">
<div class="w-full h-full flex">
<div class="h-full flex flex-col min-w-0 flex-1 relative">
<div class="relative flex-1 w-full h-full flex justify-center pt-2" id="note-editor">
{#if loading}
<div class=" absolute top-0 bottom-0 left-0 right-0 flex">
@@ -1468,7 +1466,7 @@ ${content}
</div>
</div>
{/if}
</Pane>
</div>
<NotePanel bind:show={showNoteChat}>
{#if noteChatLoading}
<div class="flex h-full items-center justify-center">
@@ -1511,4 +1509,4 @@ ${content}
/>
{/if}
</NotePanel>
</PaneGroup>
</div>
+22 -72
View File
@@ -1,63 +1,27 @@
<script lang="ts">
import { onDestroy, onMount, tick } from 'svelte';
import { Pane, PaneResizer } from 'paneforge';
import { onDestroy, onMount } from 'svelte';
import Drawer from '../common/Drawer.svelte';
import ResizableSidePanel from '../common/ResizableSidePanel.svelte';
export let show = false;
export let pane = null;
export let containerId = 'note-container';
let mediaQuery;
let mediaQuery: MediaQueryList;
let largeScreen = false;
let panelWidth = 350;
let minSize = 0;
const handleMediaQuery = async (e) => {
if (e.matches) {
largeScreen = true;
} else {
largeScreen = false;
pane = null;
}
const handleMediaQuery = (e: MediaQueryListEvent | MediaQueryList) => {
largeScreen = e.matches;
};
onMount(() => {
// listen to resize 1000px
mediaQuery = window.matchMedia('(min-width: 1000px)');
mediaQuery.addEventListener('change', handleMediaQuery);
handleMediaQuery(mediaQuery);
// Select the container element you want to observe
const container = document.getElementById(containerId);
// initialize the minSize based on the container width
minSize = Math.floor((350 / container.clientWidth) * 100);
// Create a new ResizeObserver instance
const resizeObserver = new ResizeObserver((entries) => {
for (let entry of entries) {
const width = entry.contentRect.width;
const percentage = (350 / width) * 100;
// set the minSize to the percentage, must be an integer
minSize = Math.floor(percentage);
if (show) {
if (pane && pane.isExpanded() && pane.getSize() < minSize) {
pane.resize(minSize);
}
}
}
});
// Start observing the container's size changes
resizeObserver.observe(container);
});
onDestroy(() => {
mediaQuery.removeEventListener('change', handleMediaQuery);
mediaQuery?.removeEventListener('change', handleMediaQuery);
});
</script>
@@ -69,39 +33,25 @@
show = false;
}}
>
<div class=" h-screen max-h-dvh flex flex-col">
<div class="h-screen max-h-dvh flex flex-col">
<slot />
</div>
</Drawer>
{/if}
{:else if show}
<PaneResizer
class="relative flex items-center justify-center group border-l border-gray-50 dark:border-gray-850/30 hover:border-gray-200 dark:hover:border-gray-800 transition z-20"
id="controls-resizer"
{:else}
<ResizableSidePanel
bind:open={show}
bind:width={panelWidth}
minWidth={350}
maxWidth={640}
className="h-full z-10"
>
<div
class=" absolute -left-1.5 -right-1.5 -top-0 -bottom-0 z-20 cursor-col-resize bg-transparent"
/>
</PaneResizer>
<Pane
bind:pane
defaultSize={Math.max(20, minSize)}
{minSize}
onCollapse={() => {
show = false;
}}
collapsible={true}
class=" z-10 "
>
{#if show}
<div class="flex max-h-full min-h-full">
<div
class="w-full bg-white dark:bg-gray-900 z-40 pointer-events-auto overflow-hidden scrollbar-hidden flex flex-col"
>
<slot />
</div>
<div class="flex h-full max-h-full min-h-full">
<div
class="w-full bg-white dark:bg-gray-900 z-40 pointer-events-auto overflow-hidden scrollbar-hidden flex flex-col"
>
<slot />
</div>
{/if}
</Pane>
</div>
</ResizableSidePanel>
{/if}
@@ -2,7 +2,6 @@
import Fuse from 'fuse.js';
import { toast } from 'svelte-sonner';
import { v4 as uuidv4 } from 'uuid';
import { PaneGroup, Pane, PaneResizer } from 'paneforge';
import { onMount, getContext, onDestroy, tick } from 'svelte';
import type { Writable } from 'svelte/store';
@@ -77,11 +76,6 @@
import Pagination from '$lib/components/common/Pagination.svelte';
import AttachWebpageModal from '$lib/components/chat/MessageInput/AttachWebpageModal.svelte';
let largeScreen = true;
let pane;
let showSidepanel = true;
let showAddWebpageModal = false;
let showAddTextContentModal = false;
let showNewDirectoryModal = false;
@@ -92,7 +86,6 @@
let showAccessControlModal = false;
let showResetConfirm = false;
let minSize = 0;
type DirectoryFileEntry = { path: string; filename: string; file: File };
type DirectoryManifestEntry = DirectoryFileEntry & { checksum: string; size: number };
@@ -917,7 +910,6 @@
};
let debounceTimeout = null;
let mediaQuery;
let dragged = false;
let isSaving = false;
@@ -980,14 +972,6 @@
}, 1000);
};
const handleMediaQuery = async (e) => {
if (e.matches) {
largeScreen = true;
} else {
largeScreen = false;
}
};
const readDirectoryEntries = async (reader: any) => {
const entries: any[] = [];
@@ -1096,42 +1080,6 @@
};
onMount(async () => {
// listen to resize 1024px
mediaQuery = window.matchMedia('(min-width: 1024px)');
mediaQuery.addEventListener('change', handleMediaQuery);
handleMediaQuery(mediaQuery);
// Select the container element you want to observe
const container = document.getElementById('collection-container');
// initialize the minSize based on the container width
minSize = !largeScreen ? 100 : Math.floor((300 / container.clientWidth) * 100);
// Create a new ResizeObserver instance
const resizeObserver = new ResizeObserver((entries) => {
for (let entry of entries) {
const width = entry.contentRect.width;
// calculate the percentage of 300
const percentage = (300 / width) * 100;
// set the minSize to the percentage, must be an integer
minSize = !largeScreen ? 100 : Math.floor(percentage);
if (showSidepanel) {
if (pane && pane.isExpanded() && pane.getSize() < minSize) {
pane.resize(minSize);
}
}
}
});
// Start observing the container's size changes
resizeObserver.observe(container);
if (pane) {
pane.expand();
}
id = $page.params.id;
const res = await getKnowledgeById(localStorage.token, id).catch((e) => {
toast.error(`${e}`);
@@ -1160,7 +1108,6 @@
clearInterval(pendingPollTimer);
pendingPollTimer = null;
}
mediaQuery?.removeEventListener('change', handleMediaQuery);
const dropZone = document.querySelector('body');
dropZone?.removeEventListener('dragover', onDragOver);
dropZone?.removeEventListener('drop', onDrop);