This commit is contained in:
Timothy Jaeryang Baek
2026-02-25 16:56:32 -06:00
parent 8b2160f2f7
commit 3271a5277c
10 changed files with 165 additions and 142 deletions
-20
View File
@@ -33,7 +33,6 @@
currentChatPage,
temporaryChatEnabled,
mobile,
showOverview,
chatTitle,
showArtifacts,
artifactContents,
@@ -704,31 +703,13 @@
}
}
// Persist controls panel state for desktop only
if (!$mobile) {
localStorage.setItem('showControls', JSON.stringify(value));
}
if (!value) {
showCallOverlay.set(false);
showOverview.set(false);
showArtifacts.set(false);
showEmbeds.set(false);
}
});
// Restore controls panel state on desktop
if (!$mobile) {
try {
const savedShowControls = localStorage.getItem('showControls');
if (savedShowControls !== null && JSON.parse(savedShowControls)) {
showControls.set(true);
}
} catch (e) {
// ignore
}
}
selectedFolderSubscribe = selectedFolder.subscribe(async (folder) => {
if (
folder?.data?.model_ids &&
@@ -1110,7 +1091,6 @@
await showControls.set(false);
}
await showCallOverlay.set(false);
await showOverview.set(false);
await showArtifacts.set(false);
if ($page.url.pathname.includes('/c/')) {
+80 -39
View File
@@ -1,5 +1,5 @@
<script context="module">
let savedTab = 'controls';
let savedTab: 'controls' | 'files' | 'overview' = 'controls';
</script>
<script lang="ts">
@@ -13,7 +13,6 @@
mobile,
showControls,
showCallOverlay,
showOverview,
showArtifacts,
showEmbeds,
settings
@@ -28,6 +27,7 @@
import Artifacts from './Artifacts.svelte';
import Embeds from './ChatControls/Embeds.svelte';
import FileNav from './FileNav.svelte';
import Overview from './Overview.svelte';
const i18n = getContext('i18n');
@@ -52,11 +52,14 @@
let largeScreen = false;
let dragged = false;
let minSize = 0;
let paneReady = false;
// Tab state for Controls+Files panel
let activeTab: 'controls' | 'files' = savedTab as 'controls' | 'files';
let activeTab: 'controls' | 'files' | 'overview' = savedTab;
$: savedTab = activeTab;
$: hasTerminal = !!($settings?.terminalServers ?? []).find((s) => s.enabled)?.url;
$: hasMessages = history?.messages && Object.keys(history.messages).length > 0;
$: if (!hasMessages && activeTab === 'overview') activeTab = 'controls';
// Attach a terminal file to the chat input
const handleTerminalAttach = async (blob: Blob, name: string, contentType: string) => {
@@ -166,9 +169,20 @@
document.addEventListener('mousedown', onMouseDown);
document.addEventListener('mouseup', onMouseUp);
// Delay enabling onCollapse so the Pane's initial collapsed state
// (defaultSize=0) doesn't reset showControls
setTimeout(() => {
paneReady = true;
// If controls were persisted as open, expand the pane now that it's ready
if ($showControls && pane) {
openPane();
}
}, 0);
});
onDestroy(() => {
paneReady = false;
if (!largeScreen) {
showControls.set(false);
}
@@ -178,8 +192,9 @@
});
const closeHandler = () => {
showControls.set(false);
showOverview.set(false);
if (!largeScreen) {
showControls.set(false);
}
showArtifacts.set(false);
showEmbeds.set(false);
if ($showCallOverlay) showCallOverlay.set(false);
@@ -188,7 +203,7 @@
$: if (!chatId) closeHandler();
// Helper: is a "special" full-screen panel active?
$: specialPanel = $showCallOverlay || $showOverview || $showArtifacts || $showEmbeds;
$: specialPanel = $showCallOverlay || $showArtifacts || $showEmbeds;
</script>
{#if !largeScreen}
@@ -198,7 +213,7 @@
onClose={() => showControls.set(false)}
className="min-h-[100dvh] !bg-white dark:!bg-gray-850"
>
<div class="h-full flex flex-col">
<div class="h-[100dvh] flex flex-col">
{#if $showCallOverlay}
<div
class="h-full max-h-[100dvh] bg-white text-gray-700 dark:bg-black dark:text-gray-300 flex justify-center"
@@ -217,17 +232,6 @@
<Embeds />
{:else if $showArtifacts}
<Artifacts {history} />
{:else if $showOverview}
{#await import('./Overview.svelte') then { default: Overview }}
<Overview
{history}
onNodeClick={(e) => {
const node = e.node;
showMessage(node.data.message, true);
}}
onClose={() => showControls.set(false)}
/>
{/await}
{:else}
<!-- Controls + Files tabs -->
<div class="flex flex-col h-full min-h-0">
@@ -252,6 +256,16 @@
{$i18n.t('Files')}
</button>
{/if}
{#if hasMessages}
<button
class="px-2.5 py-1 text-sm rounded-lg transition {activeTab === 'overview'
? 'bg-gray-100 dark:bg-gray-800 font-medium text-gray-900 dark:text-white'
: 'text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300'}"
on:click={() => (activeTab = 'overview')}
>
{$i18n.t('Overview')}
</button>
{/if}
</div>
<button
class="p-1 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800 transition text-gray-500 dark:text-gray-400"
@@ -272,9 +286,22 @@
</div>
<div
class="flex-1 overflow-y-auto min-h-0 {activeTab === 'controls' ? 'px-3 pt-1' : ''}"
class="flex-1 min-h-0 {activeTab === 'overview'
? 'h-full'
: activeTab === 'controls'
? 'overflow-y-auto px-3 pt-1'
: 'overflow-y-auto'}"
>
{#if activeTab === 'files' && hasTerminal}
{#if activeTab === 'overview'}
<Overview
{history}
onNodeClick={(e) => {
const node = e.node;
showMessage(node.data.message, true);
}}
onClose={() => showControls.set(false)}
/>
{:else if activeTab === 'files' && hasTerminal}
<FileNav onAttach={handleTerminalAttach} />
{:else}
<Controls embed={true} {models} bind:chatFiles bind:params />
@@ -311,7 +338,9 @@
}
}
}}
onCollapse={() => showControls.set(false)}
onCollapse={() => {
if (paneReady) showControls.set(false);
}}
collapsible={true}
class="z-10 bg-white dark:bg-gray-850"
>
@@ -339,22 +368,6 @@
<Embeds overlay={dragged} />
{:else if $showArtifacts}
<Artifacts {history} overlay={dragged} />
{:else if $showOverview}
{#await import('./Overview.svelte') then { default: Overview }}
<Overview
{history}
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);
}}
onClose={() => showControls.set(false)}
/>
{/await}
{:else}
<!-- Controls + Files tabs -->
<div class="flex flex-col h-full min-h-0">
@@ -379,6 +392,16 @@
{$i18n.t('Files')}
</button>
{/if}
{#if hasMessages}
<button
class="px-2.5 py-1 text-sm rounded-lg transition {activeTab === 'overview'
? 'bg-gray-100 dark:bg-gray-800 font-medium text-gray-900 dark:text-white'
: 'text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300'}"
on:click={() => (activeTab = 'overview')}
>
{$i18n.t('Overview')}
</button>
{/if}
</div>
<button
class="p-1 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800 transition text-gray-500 dark:text-gray-400"
@@ -399,9 +422,27 @@
</div>
<div
class="flex-1 overflow-y-auto min-h-0 {activeTab === 'controls' ? 'px-3 pt-1' : ''}"
class="flex-1 min-h-0 {activeTab === 'overview'
? 'h-full'
: activeTab === 'controls'
? 'overflow-y-auto px-3 pt-1'
: 'overflow-y-auto'}"
>
{#if activeTab === 'files' && hasTerminal}
{#if activeTab === 'overview'}
<Overview
{history}
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);
}}
onClose={() => showControls.set(false)}
/>
{:else if activeTab === 'files' && hasTerminal}
<FileNav onAttach={handleTerminalAttach} />
{:else}
<Controls embed={true} {models} bind:chatFiles bind:params />
@@ -15,7 +15,19 @@
export let params = {};
export let embed = false;
let showValves = false;
// Persist collapsible section open/close state
const getOpen = (key: string, fallback = true): boolean => {
const v = localStorage.getItem(`chatControls.${key}`);
return v !== null ? v === 'true' : fallback;
};
const setOpen = (key: string) => (open: boolean) => {
localStorage.setItem(`chatControls.${key}`, String(open));
};
let showFiles = getOpen('files');
let showValves = getOpen('valves', false);
let showSystemPrompt = getOpen('systemPrompt');
let showAdvancedParams = getOpen('advancedParams');
</script>
<div class=" dark:text-white">
@@ -37,7 +49,12 @@
{#if $user?.role === 'admin' || ($user?.permissions.chat?.controls ?? true)}
<div class=" dark:text-gray-200 text-sm py-0.5 px-0.5">
{#if chatFiles.length > 0}
<Collapsible title={$i18n.t('Files')} open={true} buttonClassName="w-full">
<Collapsible
title={$i18n.t('Files')}
bind:open={showFiles}
onChange={setOpen('files')}
buttonClassName="w-full"
>
<div class="flex flex-col gap-1 mt-1.5" slot="content">
{#each chatFiles as file, fileIdx}
<FileItem
@@ -68,7 +85,12 @@
{/if}
{#if $user?.role === 'admin' || ($user?.permissions.chat?.valves ?? true)}
<Collapsible bind:open={showValves} title={$i18n.t('Valves')} buttonClassName="w-full">
<Collapsible
bind:open={showValves}
onChange={setOpen('valves')}
title={$i18n.t('Valves')}
buttonClassName="w-full"
>
<div class="text-sm" slot="content">
<Valves show={showValves} />
</div>
@@ -78,7 +100,12 @@
{/if}
{#if $user?.role === 'admin' || ($user?.permissions.chat?.system_prompt ?? true)}
<Collapsible title={$i18n.t('System Prompt')} open={true} buttonClassName="w-full">
<Collapsible
title={$i18n.t('System Prompt')}
bind:open={showSystemPrompt}
onChange={setOpen('systemPrompt')}
buttonClassName="w-full"
>
<div class="" slot="content">
<textarea
bind:value={params.system}
@@ -95,7 +122,12 @@
{/if}
{#if $user?.role === 'admin' || ($user?.permissions.chat?.params ?? true)}
<Collapsible title={$i18n.t('Advanced Params')} open={true} buttonClassName="w-full">
<Collapsible
title={$i18n.t('Advanced Params')}
bind:open={showAdvancedParams}
onChange={setOpen('advancedParams')}
buttonClassName="w-full"
>
<div class="text-sm mt-1.5" slot="content">
<div>
<AdvancedParams admin={$user?.role === 'admin'} custom={true} bind:params />
+23 -24
View File
@@ -253,7 +253,6 @@
onDestroy(() => {
if (fileImageUrl) URL.revokeObjectURL(fileImageUrl);
});
</script>
@@ -524,30 +523,30 @@
align="end"
transition={flyAndScale}
>
{#if entry.type !== "directory"}
<DropdownMenu.Item
type="button"
class="select-none flex rounded-xl py-1.5 px-3 w-full hover:bg-gray-50 dark:hover:bg-gray-800 transition items-center gap-2 text-sm"
on:click={(e) => {
e.stopPropagation();
downloadFile(`${currentPath}${entry.name}`);
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="size-4"
{#if entry.type !== 'directory'}
<DropdownMenu.Item
type="button"
class="select-none flex rounded-xl py-1.5 px-3 w-full hover:bg-gray-50 dark:hover:bg-gray-800 transition items-center gap-2 text-sm"
on:click={(e) => {
e.stopPropagation();
downloadFile(`${currentPath}${entry.name}`);
}}
>
<path
d="M10.75 2.75a.75.75 0 0 0-1.5 0v8.614L6.295 8.235a.75.75 0 1 0-1.09 1.03l4.25 4.5a.75.75 0 0 0 1.09 0l4.25-4.5a.75.75 0 0 0-1.09-1.03l-2.955 3.129V2.75Z"
/>
<path
d="M3.5 12.75a.75.75 0 0 0-1.5 0v2.5A2.75 2.75 0 0 0 4.75 18h10.5A2.75 2.75 0 0 0 18 15.25v-2.5a.75.75 0 0 0-1.5 0v2.5c0 .69-.56 1.25-1.25 1.25H4.75c-.69 0-1.25-.56-1.25-1.25v-2.5Z"
/>
</svg>
<div class="flex items-center">{$i18n.t('Download')}</div>
</DropdownMenu.Item>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="size-4"
>
<path
d="M10.75 2.75a.75.75 0 0 0-1.5 0v8.614L6.295 8.235a.75.75 0 1 0-1.09 1.03l4.25 4.5a.75.75 0 0 0 1.09 0l4.25-4.5a.75.75 0 0 0-1.09-1.03l-2.955 3.129V2.75Z"
/>
<path
d="M3.5 12.75a.75.75 0 0 0-1.5 0v2.5A2.75 2.75 0 0 0 4.75 18h10.5A2.75 2.75 0 0 0 18 15.25v-2.5a.75.75 0 0 0-1.5 0v2.5c0 .69-.56 1.25-1.25 1.25H4.75c-.69 0-1.25-.56-1.25-1.25v-2.5Z"
/>
</svg>
<div class="flex items-center">{$i18n.t('Download')}</div>
</DropdownMenu.Item>
{/if}
<DropdownMenu.Item
@@ -10,8 +10,7 @@
settings,
showArtifacts,
showControls,
showEmbeds,
showOverview
showEmbeds
} from '$lib/stores';
import FloatingButtons from '../ContentRenderer/FloatingButtons.svelte';
import { createMessagesList } from '$lib/utils';
@@ -195,7 +194,6 @@
await artifactCode.set(value);
await showControls.set(true);
await showArtifacts.set(true);
await showOverview.set(false);
await showEmbeds.set(false);
}}
/>
+1 -1
View File
@@ -14,7 +14,7 @@
</script>
<div
class="px-4 py-3 shadow-md rounded-xl dark:bg-black bg-white border dark:border-gray-900 w-60 h-20 group"
class="px-4 py-3 shadow-md rounded-xl dark:bg-black bg-white border border-gray-100 dark:border-gray-900 w-60 h-20 group"
>
<Tooltip
content={data?.message?.error ? data.message.error.content : data.message.content}
+1 -26
View File
@@ -8,7 +8,7 @@
import { onMount, tick } from 'svelte';
import { writable } from 'svelte/store';
import { models, showOverview, theme, user } from '$lib/stores';
import { models, theme, user } from '$lib/stores';
import '@xyflow/svelte/dist/style.css';
@@ -172,31 +172,6 @@
</script>
<div class="w-full h-full relative">
<div class=" absolute z-50 w-full flex justify-between dark:text-gray-100 px-4 py-3">
<div class="flex items-center gap-2.5">
<button
class="self-center p-0.5"
aria-label={$i18n.t('Back')}
on:click={() => {
showOverview.set(false);
}}
>
<ArrowLeft className="size-3.5" />
</button>
<div class=" text-lg font-medium self-center font-primary">{$i18n.t('Chat Overview')}</div>
</div>
<button
class="self-center p-0.5"
aria-label={$i18n.t('Close overview')}
on:click={() => {
onClose();
showOverview.set(false);
}}
>
<XMark className="size-3.5" />
</button>
</div>
{#if $nodes.length > 0}
<Flow
{nodes}
+10 -2
View File
@@ -11,6 +11,14 @@
class={className}
>
<path d="M18 6H20M22 6H20M20 6V4M20 6V8" stroke-linecap="round" stroke-linejoin="round" />
<path d="M21.4 20H2.6C2.26863 20 2 19.7314 2 19.4V11H21.4C21.7314 11 22 11.2686 22 11.6V19.4C22 19.7314 21.7314 20 21.4 20Z" stroke-linecap="round" stroke-linejoin="round" />
<path d="M2 11V4.6C2 4.26863 2.26863 4 2.6 4H8.77805C8.92127 4 9.05977 4.05124 9.16852 4.14445L12.3315 6.85555C12.4402 6.94876 12.5787 7 12.722 7H14" stroke-linecap="round" stroke-linejoin="round" />
<path
d="M21.4 20H2.6C2.26863 20 2 19.7314 2 19.4V11H21.4C21.7314 11 22 11.2686 22 11.6V19.4C22 19.7314 21.7314 20 21.4 20Z"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M2 11V4.6C2 4.26863 2.26863 4 2.6 4H8.77805C8.92127 4 9.05977 4.05124 9.16852 4.14445L12.3315 6.85555C12.4402 6.94876 12.5787 7 12.722 7H14"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
+1 -21
View File
@@ -10,7 +10,6 @@
import { copyToClipboard, createMessagesList } from '$lib/utils';
import {
showOverview,
showControls,
showArtifacts,
mobile,
@@ -27,7 +26,6 @@
import Dropdown from '$lib/components/common/Dropdown.svelte';
import Tags from '$lib/components/chat/Tags.svelte';
import Map from '$lib/components/icons/Map.svelte';
import Clipboard from '$lib/components/icons/Clipboard.svelte';
import AdjustmentsHorizontal from '$lib/components/icons/AdjustmentsHorizontal.svelte';
import Cube from '$lib/components/icons/Cube.svelte';
@@ -320,7 +318,6 @@
id="chat-controls-button"
on:click={async () => {
await showControls.set(true);
await showOverview.set(false);
await showArtifacts.set(false);
await showEmbeds.set(false);
}}
@@ -329,31 +326,14 @@
<div class="flex items-center">{$i18n.t('Controls')}</div>
</DropdownMenu.Item>
{/if}
<DropdownMenu.Item
draggable="false"
class="flex gap-2 items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl select-none w-full"
id="chat-overview-button"
on:click={async () => {
await showControls.set(true);
await showOverview.set(true);
await showArtifacts.set(false);
await showEmbeds.set(false);
}}
>
<Map className=" size-4" strokeWidth="1.5" />
<div class="flex items-center">{$i18n.t('Overview')}</div>
</DropdownMenu.Item>
{#if ($artifactContents ?? []).length > 0}
<DropdownMenu.Item
draggable="false"
class="flex gap-2 items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl select-none w-full"
id="chat-overview-button"
id="chat-artifacts-button"
on:click={async () => {
await showControls.set(true);
await showArtifacts.set(true);
await showOverview.set(false);
await showEmbeds.set(false);
}}
>
+11 -1
View File
@@ -34,7 +34,9 @@
toolServers,
terminalServers,
showSearch,
showSidebar
showSidebar,
showControls,
mobile
} from '$lib/stores';
import Sidebar from '$lib/components/layout/Sidebar.svelte';
@@ -322,6 +324,14 @@
checkForVersionUpdates();
}
}
// Persist showControls via chatControlsSize (0 = closed, >0 = open at that size)
await showControls.set(!$mobile ? parseInt(localStorage.chatControlsSize || '0') > 0 : false);
showControls.subscribe((value) => {
if (!value) {
localStorage.chatControlsSize = '0';
}
});
await tick();
loaded = true;