feat: WIP

This commit is contained in:
Aleksander Grygier 2026-04-01 14:14:13 +02:00
parent 7a13b4191a
commit ec6302960e
11 changed files with 160 additions and 104 deletions

View File

@ -35,7 +35,7 @@
{size}
{disabled}
{onclick}
class="h-6 w-6 p-0 {className} flex"
class="h-6 w-6 p-0 {className} flex hover:bg-transparent! data-[state=open]:bg-transparent!"
aria-label={ariaLabel || tooltip}
>
{@const IconComponent = icon}

View File

@ -7,10 +7,12 @@
Monitor,
ChevronLeft,
ChevronRight,
ListRestart
ListRestart,
Sliders
} from '@lucide/svelte';
import { ChatSettingsFooter, ChatSettingsFields } from '$lib/components/app';
import { ScrollArea } from '$lib/components/ui/scroll-area';
import { config, settingsStore } from '$lib/stores/settings.svelte';
import {
SETTINGS_SECTION_TITLES,
@ -40,7 +42,7 @@
}> = [
{
title: SETTINGS_SECTION_TITLES.GENERAL,
icon: Settings,
icon: Sliders,
fields: [
{
key: SETTINGS_KEYS.THEME,
@ -128,13 +130,18 @@
type: SettingsFieldType.CHECKBOX
},
{
key: SETTINGS_KEYS.AUTO_SHOW_SIDEBAR_ON_NEW_CHAT,
label: 'Auto-show sidebar on new chat',
key: SETTINGS_KEYS.SHOW_RAW_MODEL_NAMES,
label: 'Show raw model names',
type: SettingsFieldType.CHECKBOX
},
{
key: SETTINGS_KEYS.SHOW_RAW_MODEL_NAMES,
label: 'Show raw model names',
key: SETTINGS_KEYS.ALWAYS_SHOW_AGENTIC_TURNS,
label: 'Always show agentic turns in conversation',
type: SettingsFieldType.CHECKBOX
},
{
key: SETTINGS_KEYS.SHOW_TOOL_CALL_IN_PROGRESS,
label: 'Show tool call in progress',
type: SettingsFieldType.CHECKBOX
}
]
@ -260,20 +267,10 @@
label: 'Agentic turns',
type: SettingsFieldType.INPUT
},
{
key: SETTINGS_KEYS.ALWAYS_SHOW_AGENTIC_TURNS,
label: 'Always show agentic turns in conversation',
type: SettingsFieldType.CHECKBOX
},
{
key: SETTINGS_KEYS.AGENTIC_MAX_TOOL_PREVIEW_LINES,
label: 'Max lines per tool preview',
type: SettingsFieldType.INPUT
},
{
key: SETTINGS_KEYS.SHOW_TOOL_CALL_IN_PROGRESS,
label: 'Show tool call in progress',
type: SettingsFieldType.CHECKBOX
}
]
},
@ -433,17 +430,15 @@
</script>
<div class="flex h-full flex-col {className} w-full">
<div class="flex items-center gap-2 w-full md:absolute md:top-8">
<Settings class="h-6 w-6" />
<h1 class="text-2xl font-semibold">Settings</h1>
</div>
<div class="flex flex-col overflow-hidden md:flex-row gap-4">
<div class="flex h-full flex-col overflow-y-auto {className} w-full">
<div class="flex flex-1 flex-col md:flex-row gap-4">
<!-- Desktop Sidebar -->
<div class="hidden w-64 pt-8 mt-16 md:block">
<nav class="space-y-1 py-2">
<div class="hidden w-64 md:flex flex-col sticky top-0 self-start bg-background pt-8 pb-4">
<div class="flex items-center gap-2 pb-6">
<Settings class="h-6 w-6" />
<h1 class="text-2xl font-semibold">Settings</h1>
</div>
<nav class="space-y-1">
{#each settingSections as section (section.title)}
<button
class="flex w-full cursor-pointer items-center gap-3 rounded-lg px-3 py-2 text-left text-sm transition-colors hover:bg-accent {activeSection ===
@ -461,8 +456,12 @@
</div>
<!-- Mobile Header with Horizontal Scrollable Menu -->
<div class="flex flex-col pt-6 md:hidden">
<div class="border-b border-border/30 pt-4 md:py-4">
<div class="flex flex-col md:hidden sticky top-0 z-10 bg-background">
<div class="flex items-center gap-2 px-4 pt-6 pb-2">
<Settings class="h-6 w-6" />
<h1 class="text-2xl font-semibold">Settings</h1>
</div>
<div class="border-b border-border/30 py-2">
<!-- Horizontal Scrollable Category Menu with Navigation -->
<div class="relative flex items-center" style="scroll-padding: 1rem;">
<button
@ -512,8 +511,8 @@
</div>
</div>
<ScrollArea class="flex-1 max-w-5xl mx-auto">
<div class="space-y-6 p-4 md:p-6 md:mt-20">
<div class="flex-1 max-w-5xl mx-auto">
<div class="space-y-6 p-4 md:p-6 md:pt-22">
<div class="grid">
<div class="mb-6 flex hidden items-center gap-2 border-b border-border/30 pb-6 md:flex">
<currentSection.icon class="h-5 w-5" />
@ -537,7 +536,7 @@
<p class="text-xs text-muted-foreground">Settings are saved in browser's localStorage</p>
</div>
</div>
</ScrollArea>
</div>
</div>
</div>

View File

@ -29,7 +29,7 @@
}
</script>
<div class="flex justify-between p-6">
<div class="flex justify-between border-t border-border/30 p-6">
<div class="flex gap-2">
<Button variant="outline" onclick={handleResetClick}>
<RotateCcw class="h-3 w-3" />
@ -38,7 +38,7 @@
</Button>
</div>
<Button onclick={handleSave}>Save settings</Button>
<Button class="sticky bottom-6 z-10" onclick={handleSave}>Save settings</Button>
</div>
<AlertDialog.Root bind:open={showResetDialog}>

View File

@ -108,10 +108,19 @@
}
}
let chatSidebarActions: { activateSearch?: () => void } | undefined = $state();
export function activateSearchMode() {
isSearchModeActive = true;
chatSidebarActions?.activateSearch?.();
}
$effect(() => {
if (!sidebar.open) {
isSearchModeActive = false;
searchQuery = '';
}
});
export function editActiveConversation() {
if (currentChatId) {
const activeConversation = filteredConversations.find((conv) => conv.id === currentChatId);
@ -148,7 +157,7 @@
<h1 class="inline-flex items-center gap-1 px-2 text-xl font-semibold">llama.cpp</h1>
</a>
<ChatSidebarActions {handleMobileSidebarItemClick} bind:isSearchModeActive bind:searchQuery />
<ChatSidebarActions bind:this={chatSidebarActions} {handleMobileSidebarItemClick} bind:isSearchModeActive bind:searchQuery />
</Sidebar.Header>
<Sidebar.Group class="mt-2 space-y-2 p-0 px-3">

View File

@ -20,17 +20,25 @@
}: Props = $props();
const importExportDialog = getImportExportDialogContext();
let searchInputRef = $state<HTMLInputElement | null>(null);
function handleSearchModeDeactivate() {
isSearchModeActive = false;
searchQuery = '';
}
export function activateSearch() {
isSearchModeActive = true;
// Focus after Svelte renders the input
queueMicrotask(() => searchInputRef?.focus());
}
</script>
<div class="my-1 space-y-1">
{#if isSearchModeActive}
<SearchInput
bind:value={searchQuery}
bind:ref={searchInputRef}
onClose={handleSearchModeDeactivate}
onKeyDown={(e) => e.key === 'Escape' && handleSearchModeDeactivate()}
placeholder="Search conversations..."
@ -54,9 +62,7 @@
<Button
class="w-full justify-between px-2 backdrop-blur-none! hover:[&>kbd]:opacity-100"
onclick={() => {
isSearchModeActive = true;
}}
onclick={activateSearch}
variant="ghost"
>
<div class="flex items-center gap-2">

View File

@ -84,22 +84,23 @@
}
</script>
<div class="flex items-center gap-2 absolute left-8 top-8">
<McpLogo class="h-6 w-6" />
<h1 class="text-2xl font-semibold">MCP Servers</h1>
</div>
<div class="flex items-start justify-end gap-4 py-4 sticky top-0 z-10 px-8 mt-4">
{#if !isAddingServer}
<Button variant="outline" size="sm" class="shrink-0" onclick={showAddServerForm}>
<Plus class="h-4 w-4" />
Add New Server
</Button>
{/if}
</div>
<div class="grid gap-5 md:space-y-4 {className}">
<div class="flex items-start justify-between gap-4">
<div class="flex items-center gap-2">
<McpLogo class="h-6 w-6" />
<h1 class="text-2xl font-semibold">MCP Servers</h1>
</div>
{#if !isAddingServer}
<Button variant="outline" size="sm" class="shrink-0" onclick={showAddServerForm}>
<Plus class="h-4 w-4" />
Add New Server
</Button>
{/if}
</div>
{#if isAddingServer}
<Card.Root class="bg-muted/30 p-4">

View File

@ -66,7 +66,8 @@
<div
data-slot="sidebar-gap"
class={cn(
'relative w-[calc(var(--sidebar-width)+0.75rem)] bg-transparent transition-[width] duration-200 ease-linear',
'relative bg-transparent transition-[width] duration-200 ease-linear',
variant === 'floating' ? 'w-[calc(var(--sidebar-width)+0.75rem)]' : 'w-(--sidebar-width)',
'group-data-[collapsible=offcanvas]:w-0',
'group-data-[side=right]:rotate-180',
variant === 'floating' || variant === 'inset'
@ -78,25 +79,36 @@
<div
data-slot="sidebar-container"
class={cn(
'fixed inset-y-0 z-999 hidden w-(--sidebar-width) transition-[left,right,width,opacity] duration-200 ease-linear md:z-0 md:flex',
side === 'left'
? 'left-3 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-0.775)] group-data-[collapsible=offcanvas]:opacity-0'
: 'right-1 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-0.775)] group-data-[collapsible=offcanvas]:opacity-0',
// Adjust the padding for floating and inset variants.
variant === 'floating' || variant === 'inset'
'fixed inset-y-0 z-999 hidden w-(--sidebar-width) duration-200 ease-linear md:z-0 md:flex',
variant === 'floating'
? [
'transition-[left,right,width,opacity]',
side === 'left'
? 'left-3 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-0.775)] group-data-[collapsible=offcanvas]:opacity-0'
: 'right-3 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-0.775)] group-data-[collapsible=offcanvas]:opacity-0',
'my-3 overflow-hidden rounded-2xl border border-sidebar-border shadow-md',
]
: [
'transition-[left,right,width] h-svh',
side === 'left'
? 'left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]'
: 'right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]',
],
// Adjust the padding for inset variant.
variant === 'inset'
? 'p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4))+2px)]'
: 'group-data-[collapsible=icon]:w-(--sidebar-width-icon)',
// Add margin and rounded corners
'my-3 overflow-hidden rounded-2xl border border-sidebar-border shadow-md',
: variant === 'floating'
? 'group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4))+2px)]'
: 'group-data-[collapsible=icon]:w-(--sidebar-width-icon)',
className
)}
style="height: calc(100dvh - 1.5rem);"
style={variant === 'floating' ? 'height: calc(100dvh - 1.5rem);' : undefined}
{...restProps}
>
<div
data-sidebar="sidebar"
data-slot="sidebar-inner"
class="flex h-full w-full flex-col bg-sidebar group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:border-sidebar-border group-data-[variant=floating]:shadow-sm"
class="flex h-full w-full flex-col bg-sidebar"
>
{@render children?.()}
</div>

View File

@ -21,7 +21,6 @@ export const SETTING_CONFIG_DEFAULT: Record<string, string | number | boolean |
disableAutoScroll: false,
renderUserContentAsMarkdown: false,
alwaysShowSidebarOnDesktop: false,
autoShowSidebarOnNewChat: true,
autoMicOnEmpty: false,
fullHeightCodeBlocks: false,
showRawModelNames: false,

View File

@ -22,7 +22,6 @@ export const SETTINGS_KEYS = {
RENDER_USER_CONTENT_AS_MARKDOWN: 'renderUserContentAsMarkdown',
DISABLE_AUTO_SCROLL: 'disableAutoScroll',
ALWAYS_SHOW_SIDEBAR_ON_DESKTOP: 'alwaysShowSidebarOnDesktop',
AUTO_SHOW_SIDEBAR_ON_NEW_CHAT: 'autoShowSidebarOnNewChat',
FULL_HEIGHT_CODE_BLOCKS: 'fullHeightCodeBlocks',
SHOW_RAW_MODEL_NAMES: 'showRawModelNames',
// Sampling

View File

@ -191,12 +191,6 @@ export const SYNCABLE_PARAMETERS: SyncableParameter[] = [
type: SyncableParameterType.BOOLEAN,
canSync: true
},
{
key: 'autoShowSidebarOnNewChat',
serverKey: 'autoShowSidebarOnNewChat',
type: SyncableParameterType.BOOLEAN,
canSync: true
},
{
key: 'showRawModelNames',
serverKey: 'showRawModelNames',

View File

@ -11,7 +11,7 @@
DialogConversationTitleUpdate,
DialogChatSettingsImportExport
} from '$lib/components/app';
import { Settings } from '@lucide/svelte';
import { Settings, Search, SquarePen } from '@lucide/svelte';
import { Button } from '$lib/components/ui/button';
import { isLoading } from '$lib/stores/chat.svelte';
import { conversationsStore, activeMessages } from '$lib/stores/conversations.svelte';
@ -41,7 +41,6 @@
let isNewChatMode = $derived(page.url.searchParams.get('new_chat') === 'true');
let showSidebarByDefault = $derived(activeMessages().length > 0 || isLoading());
let alwaysShowSidebarOnDesktop = $derived(config().alwaysShowSidebarOnDesktop);
let autoShowSidebarOnNewChat = $derived(config().autoShowSidebarOnNewChat);
let isMobile = new IsMobile();
let isDesktop = $derived(!isMobile.current);
let sidebarOpen = $state(false);
@ -57,6 +56,8 @@
let titleUpdateResolve: ((value: boolean) => void) | null = null;
let activePanel = $state<'chat' | 'settings' | 'mcp'>('chat');
let isMcpActive = $derived(page.route.id === '/settings/mcp');
let isSettingsActive = $derived(page.route.id === '/settings/chat');
// let chatSettingsInitialSection = $state<SettingsSectionTitle | undefined>(undefined);
let chatSettingsRef: ChatSettings | undefined = $state();
let importExportDialogOpen = $state(false);
@ -141,23 +142,7 @@
sidebarOpen = true;
return;
}
if (isHomeRoute && !isNewChatMode) {
// Auto-collapse sidebar when navigating to home route (but not in new chat mode)
sidebarOpen = false;
} else if (isHomeRoute && isNewChatMode) {
// Keep sidebar open in new chat mode
sidebarOpen = true;
} else if (isChatRoute) {
// On chat routes, only auto-show sidebar if setting is enabled
if (autoShowSidebarOnNewChat) {
sidebarOpen = true;
}
// If setting is disabled, don't change sidebar state - let user control it manually
} else {
// Other routes follow default behavior
sidebarOpen = showSidebarByDefault;
}
// Don't auto-open or auto-close sidebar during navigation - user controls it manually
});
// Initialize server properties on app load (run once)
@ -280,7 +265,7 @@
<Sidebar.Provider bind:open={sidebarOpen}>
<div class="flex h-screen w-full" style:height="{innerHeight}px">
<Sidebar.Root class="h-full">
<Sidebar.Root variant="floating" class="h-full">
<ChatSidebar bind:this={chatSidebar} />
</Sidebar.Root>
@ -293,28 +278,80 @@
/>
{/if}
{#if !sidebarOpen}
<div class="absolute bottom-3 left-3 z-[900] flex flex-col gap-1 p-2">
{#if isDesktop && !alwaysShowSidebarOnDesktop}
<!-- Desktop: icon strip, always rendered, transitions width/opacity -->
<aside class="hidden md:flex shrink-0 flex-col items-center justify-between overflow-hidden py-3 transition-[width,opacity] duration-200 ease-linear {sidebarOpen ? 'w-0 opacity-0 pointer-events-none' : 'w-[calc(var(--sidebar-width-icon)+1.5rem)] opacity-100'}">
<div class="mt-12 flex flex-col items-center gap-1">
<Button
variant="ghost"
size="icon-lg"
class="rounded-full"
href="?new_chat=true#/"
>
<SquarePen class="h-4 w-4" />
<span class="sr-only">New Chat</span>
</Button>
<Button
variant="ghost"
size="icon-lg"
class="rounded-full"
onclick={() => {
if (chatSidebar?.activateSearchMode) {
chatSidebar.activateSearchMode();
}
sidebarOpen = true;
}}
>
<Search class="h-4 w-4" />
<span class="sr-only">Search</span>
</Button>
</div>
<div class="flex flex-col items-center gap-1">
<Button
variant="ghost"
size="icon-lg"
href="#/settings/mcp"
class="rounded-full {isMcpActive ? 'bg-accent text-accent-foreground' : ''}"
>
<McpLogo class="h-4 w-4" />
<span class="sr-only">MCP Servers</span>
</Button>
<Button
variant="ghost"
size="icon-lg"
href="#/settings/chat"
class="rounded-full {isSettingsActive ? 'bg-accent text-accent-foreground' : ''}"
>
<Settings class="h-4 w-4" />
<span class="sr-only">Settings</span>
</Button>
</div>
</aside>
{/if}
{#if !sidebarOpen && !isDesktop}
<!-- Mobile quick-access buttons -->
<div class="absolute bottom-3 left-3 z-[900] flex flex-col gap-1 p-2 md:hidden">
<Button
variant="ghost"
size="icon"
class="h-8 w-8 {activePanel === 'mcp' ? 'bg-accent text-accent-foreground' : ''}"
onclick={() => (activePanel = activePanel === 'mcp' ? 'chat' : 'mcp')}
size="icon-lg"
href="#/settings/mcp"
class={isMcpActive ? 'bg-accent text-accent-foreground' : ''}
>
<McpLogo class="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="icon"
class="h-8 w-8 {activePanel === 'settings' ? 'bg-accent text-accent-foreground' : ''}"
onclick={() => (activePanel = activePanel === 'settings' ? 'chat' : 'settings')}
size="icon-lg"
href="#/settings/chat"
class={isSettingsActive ? 'bg-accent text-accent-foreground' : ''}
>
<Settings class="h-4 w-4" />
</Button>
</div>
{/if}
<Sidebar.Inset class="flex flex-1 flex-col overflow-hidden">
<Sidebar.Inset class="flex flex-1 flex-col overflow-auto">
{@render children?.()}
</Sidebar.Inset>
</div>