refac
This commit is contained in:
@@ -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">
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user