feat: WIP

This commit is contained in:
Aleksander Grygier 2026-04-01 10:36:44 +02:00
parent 5acfc403bd
commit cfd5a6b1ce
10 changed files with 165 additions and 165 deletions

View File

@ -432,104 +432,113 @@
});
</script>
<div class="flex h-full flex-col overflow-hidden md:flex-row {className}">
<!-- Desktop Sidebar -->
<div class="hidden w-64 border-r border-border/30 p-6 md:block">
<nav class="space-y-1 py-2">
{#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 ===
section.title
? 'bg-accent text-accent-foreground'
: 'text-muted-foreground'}"
onclick={() => (activeSection = section.title)}
>
<section.icon class="h-4 w-4" />
<span class="ml-2">{section.title}</span>
</button>
{/each}
</nav>
<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>
<!-- 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">
<!-- Horizontal Scrollable Category Menu with Navigation -->
<div class="relative flex items-center" style="scroll-padding: 1rem;">
<button
class="absolute left-2 z-10 flex h-6 w-6 items-center justify-center rounded-full bg-muted shadow-md backdrop-blur-sm transition-opacity hover:bg-accent {canScrollLeft
? 'opacity-100'
: 'pointer-events-none opacity-0'}"
onclick={scrollLeft}
aria-label="Scroll left"
>
<ChevronLeft class="h-4 w-4" />
</button>
<div class="flex flex-col overflow-hidden 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">
{#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 ===
section.title
? 'bg-accent text-accent-foreground'
: 'text-muted-foreground'}"
onclick={() => (activeSection = section.title)}
>
<section.icon class="h-4 w-4" />
<div
class="scrollbar-hide overflow-x-auto py-2"
bind:this={scrollContainer}
onscroll={updateScrollButtons}
>
<div class="flex min-w-max gap-2">
{#each settingSections as section (section.title)}
<button
class="flex cursor-pointer items-center gap-2 rounded-lg px-3 py-2 text-sm whitespace-nowrap transition-colors first:ml-4 last:mr-4 hover:bg-accent {activeSection ===
section.title
? 'bg-accent text-accent-foreground'
: 'text-muted-foreground'}"
onclick={(e: MouseEvent) => {
activeSection = section.title;
scrollToCenter(e.currentTarget as HTMLElement);
}}
>
<section.icon class="h-4 w-4 flex-shrink-0" />
<span>{section.title}</span>
</button>
{/each}
<span class="ml-2">{section.title}</span>
</button>
{/each}
</nav>
</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">
<!-- Horizontal Scrollable Category Menu with Navigation -->
<div class="relative flex items-center" style="scroll-padding: 1rem;">
<button
class="absolute left-2 z-10 flex h-6 w-6 items-center justify-center rounded-full bg-muted shadow-md backdrop-blur-sm transition-opacity hover:bg-accent {canScrollLeft
? 'opacity-100'
: 'pointer-events-none opacity-0'}"
onclick={scrollLeft}
aria-label="Scroll left"
>
<ChevronLeft class="h-4 w-4" />
</button>
<div
class="scrollbar-hide overflow-x-auto py-2"
bind:this={scrollContainer}
onscroll={updateScrollButtons}
>
<div class="flex min-w-max gap-2">
{#each settingSections as section (section.title)}
<button
class="flex cursor-pointer items-center gap-2 rounded-lg px-3 py-2 text-sm whitespace-nowrap transition-colors first:ml-4 last:mr-4 hover:bg-accent {activeSection ===
section.title
? 'bg-accent text-accent-foreground'
: 'text-muted-foreground'}"
onclick={(e: MouseEvent) => {
activeSection = section.title;
scrollToCenter(e.currentTarget as HTMLElement);
}}
>
<section.icon class="h-4 w-4 flex-shrink-0" />
<span>{section.title}</span>
</button>
{/each}
</div>
</div>
</div>
<button
class="absolute right-2 z-10 flex h-6 w-6 items-center justify-center rounded-full bg-muted shadow-md backdrop-blur-sm transition-opacity hover:bg-accent {canScrollRight
? 'opacity-100'
: 'pointer-events-none opacity-0'}"
onclick={scrollRight}
aria-label="Scroll right"
>
<ChevronRight class="h-4 w-4" />
</button>
<button
class="absolute right-2 z-10 flex h-6 w-6 items-center justify-center rounded-full bg-muted shadow-md backdrop-blur-sm transition-opacity hover:bg-accent {canScrollRight
? 'opacity-100'
: 'pointer-events-none opacity-0'}"
onclick={scrollRight}
aria-label="Scroll right"
>
<ChevronRight class="h-4 w-4" />
</button>
</div>
</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="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" />
<h3 class="text-lg font-semibold">{currentSection.title}</h3>
</div>
{#if currentSection.fields}
<div class="space-y-6">
<ChatSettingsFields
fields={currentSection.fields}
{localConfig}
onConfigChange={handleConfigChange}
onThemeChange={handleThemeChange}
/>
</div>
{/if}
</div>
<div class="mt-8 border-t border-border/30 pt-6">
<p class="text-xs text-muted-foreground">Settings are saved in browser's localStorage</p>
</div>
</div>
</ScrollArea>
</div>
<ScrollArea class="max-h-[calc(100dvh-13.5rem)] flex-1 md:max-h-[calc(100vh-13.5rem)]">
<div class="space-y-6 p-4 md:p-6">
<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" />
<h3 class="text-lg font-semibold">{currentSection.title}</h3>
</div>
{#if currentSection.fields}
<div class="space-y-6">
<ChatSettingsFields
fields={currentSection.fields}
{localConfig}
onConfigChange={handleConfigChange}
onThemeChange={handleThemeChange}
/>
</div>
{/if}
</div>
<div class="mt-8 border-t border-border/30 pt-6">
<p class="text-xs text-muted-foreground">Settings are saved in browser's localStorage</p>
</div>
</div>
</ScrollArea>
</div>
<ChatSettingsFooter onReset={handleReset} onSave={handleSave} />

View File

@ -29,7 +29,7 @@
}
</script>
<div class="flex justify-between border-t border-border/30 p-6">
<div class="flex justify-between p-6">
<div class="flex gap-2">
<Button variant="outline" onclick={handleResetClick}>
<RotateCcw class="h-3 w-3" />

View File

@ -1,56 +1,41 @@
<script lang="ts">
import { Database, Search, SquarePen, X } from '@lucide/svelte';
import { Database, Search, SquarePen } from '@lucide/svelte';
import { KeyboardShortcutInfo } from '$lib/components/app';
import { Button } from '$lib/components/ui/button';
import { Input } from '$lib/components/ui/input';
import { SearchInput } from '$lib/components/app';
import { getImportExportDialogContext } from '$lib/contexts';
interface Props {
handleMobileSidebarItemClick: () => void;
isSearchModeActive: boolean;
searchQuery: string;
isCancelAlwaysVisible?: boolean;
}
let {
handleMobileSidebarItemClick,
isSearchModeActive = $bindable(),
searchQuery = $bindable()
searchQuery = $bindable(),
isCancelAlwaysVisible = false
}: Props = $props();
let searchInput: HTMLInputElement | null = $state(null);
const importExportDialog = getImportExportDialogContext();
function handleSearchModeDeactivate() {
isSearchModeActive = false;
searchQuery = '';
}
$effect(() => {
if (isSearchModeActive) {
searchInput?.focus();
}
});
</script>
<div class="my-1 space-y-1">
{#if isSearchModeActive}
<div class="relative">
<Search class="absolute top-2.5 left-2 h-4 w-4 text-muted-foreground" />
<Input
bind:ref={searchInput}
bind:value={searchQuery}
onkeydown={(e) => e.key === 'Escape' && handleSearchModeDeactivate()}
placeholder="Search conversations..."
class="pl-8"
/>
<X
class="cursor-pointertext-muted-foreground absolute top-2.5 right-2 h-4 w-4"
onclick={handleSearchModeDeactivate}
/>
</div>
<SearchInput
bind:value={searchQuery}
onClose={handleSearchModeDeactivate}
onKeyDown={(e) => e.key === 'Escape' && handleSearchModeDeactivate()}
placeholder="Search conversations..."
isCancelAlwaysVisible={isCancelAlwaysVisible}
/>
{:else}
<Button
class="w-full justify-between px-2 backdrop-blur-none! hover:[&>kbd]:opacity-100"

View File

@ -2,13 +2,14 @@
import { Settings } from '@lucide/svelte';
import { Button } from '$lib/components/ui/button';
import { McpLogo } from '$lib/components/app';
import { getChatSettingsDialogContext, getMcpServersDialogContext } from '$lib/contexts';
// import { getChatSettingsDialogContext, getMcpServersDialogContext } from '$lib/contexts';
import { page } from '$app/state';
const chatSettingsDialog = getChatSettingsDialogContext();
const mcpServersDialog = getMcpServersDialogContext();
// const chatSettingsDialog = getChatSettingsDialogContext();
// const mcpServersDialog = getMcpServersDialogContext();
let isMcpActive = $derived(mcpServersDialog.isActive());
let isSettingsActive = $derived(chatSettingsDialog.isActive());
let isMcpActive = $derived(page.route.id === '/settings/mcp');
let isSettingsActive = $derived(page.route.id === '/settings/chat');
</script>
<div class="space-y-1 pt-0">
@ -16,9 +17,7 @@
class="w-full justify-between px-2 backdrop-blur-none! hover:[&>kbd]:opacity-100 {isMcpActive
? 'bg-accent text-accent-foreground'
: ''}"
onclick={() => {
mcpServersDialog.open();
}}
href="#/settings/mcp"
variant="ghost"
>
<div class="flex items-center gap-2">
@ -32,9 +31,7 @@
class="w-full justify-between px-2 backdrop-blur-none! hover:[&>kbd]:opacity-100 {isSettingsActive
? 'bg-accent text-accent-foreground'
: ''}"
onclick={() => {
chatSettingsDialog.open();
}}
href="#/settings/chat"
variant="ghost"
>
<div class="flex items-center gap-2">

View File

@ -11,6 +11,7 @@
class?: string;
id?: string;
ref?: HTMLInputElement | null;
isCancelAlwaysVisible?: boolean;
}
let {
@ -21,10 +22,11 @@
onKeyDown,
class: className,
id,
ref = $bindable(null)
ref = $bindable(null),
isCancelAlwaysVisible = false
}: Props = $props();
let showClearButton = $derived(!!value || !!onClose);
let showClearButton = $derived(isCancelAlwaysVisible || !!value || !!onClose);
function handleInput(event: Event) {
const target = event.target as HTMLInputElement;
@ -63,7 +65,7 @@
{#if showClearButton}
<button
type="button"
class="absolute top-1/2 right-3 -translate-y-1/2 transform text-muted-foreground transition-colors hover:text-foreground"
class="cursor-pointer absolute top-1/2 right-3 -translate-y-1/2 transform text-muted-foreground transition-colors hover:text-foreground"
onclick={handleClear}
aria-label={value ? 'Clear search' : 'Close'}
>

View File

@ -89,7 +89,7 @@
<div class="flex items-center gap-2">
<McpLogo class="h-6 w-6" />
<h2 class="text-2xl font-semibold">MCP Servers</h2>
<h1 class="text-2xl font-semibold">MCP Servers</h1>
</div>
{#if !isAddingServer}

View File

@ -66,7 +66,7 @@
<div
data-slot="sidebar-gap"
class={cn(
'relative w-(--sidebar-width) bg-transparent transition-[width] duration-200 ease-linear',
'relative w-[calc(var(--sidebar-width)+0.75rem)] bg-transparent transition-[width] duration-200 ease-linear',
'group-data-[collapsible=offcanvas]:w-0',
'group-data-[side=right]:rotate-180',
variant === 'floating' || variant === 'inset'

View File

@ -7,7 +7,6 @@
import {
ChatSidebar,
ChatSettings,
McpServersSettings,
McpLogo,
DialogConversationTitleUpdate,
DialogChatSettingsImportExport
@ -26,12 +25,12 @@
import { modelsStore } from '$lib/stores/models.svelte';
import { mcpStore } from '$lib/stores/mcp.svelte';
import { TOOLTIP_DELAY_DURATION } from '$lib/constants';
import type { SettingsSectionTitle } from '$lib/constants';
// import type { SettingsSectionTitle } from '$lib/constants';
import { KeyboardKey } from '$lib/enums';
import { IsMobile } from '$lib/hooks/is-mobile.svelte';
import {
setChatSettingsDialogContext,
setMcpServersDialogContext,
// setChatSettingsDialogContext,
// setMcpServersDialogContext,
setImportExportDialogContext
} from '$lib/contexts';
@ -58,24 +57,24 @@
let titleUpdateResolve: ((value: boolean) => void) | null = null;
let activePanel = $state<'chat' | 'settings' | 'mcp'>('chat');
let chatSettingsInitialSection = $state<SettingsSectionTitle | undefined>(undefined);
// let chatSettingsInitialSection = $state<SettingsSectionTitle | undefined>(undefined);
let chatSettingsRef: ChatSettings | undefined = $state();
let importExportDialogOpen = $state(false);
setChatSettingsDialogContext({
open: (initialSection?: SettingsSectionTitle) => {
chatSettingsInitialSection = initialSection;
activePanel = 'settings';
},
isActive: () => activePanel === 'settings'
});
// setChatSettingsDialogContext({
// open: (initialSection?: SettingsSectionTitle) => {
// chatSettingsInitialSection = initialSection;
// activePanel = 'settings';
// },
// isActive: () => activePanel === 'settings'
// });
setMcpServersDialogContext({
open: () => {
activePanel = 'mcp';
},
isActive: () => activePanel === 'mcp'
});
// setMcpServersDialogContext({
// open: () => {
// activePanel = 'mcp';
// },
// isActive: () => activePanel === 'mcp'
// });
setImportExportDialogContext({
open: () => {
@ -288,14 +287,14 @@
{#if !(alwaysShowSidebarOnDesktop && isDesktop)}
<Sidebar.Trigger
class="transition-left absolute left-0 z-[900] duration-200 ease-linear {sidebarOpen
? 'md:left-[var(--sidebar-width)]'
? 'md:left-[calc(var(--sidebar-width)+0.75rem)]'
: 'md:left-0!'}"
style="translate: 1rem 1rem;"
/>
{/if}
{#if !sidebarOpen}
<div class="absolute bottom-0 left-0 z-[900] flex flex-col gap-1 p-2">
<div class="absolute bottom-3 left-3 z-[900] flex flex-col gap-1 p-2">
<Button
variant="ghost"
size="icon"
@ -316,18 +315,7 @@
{/if}
<Sidebar.Inset class="flex flex-1 flex-col overflow-hidden">
{#if activePanel === 'settings'}
<ChatSettings
bind:this={chatSettingsRef}
onSave={() => (activePanel = 'chat')}
initialSection={chatSettingsInitialSection}
class="mx-auto h-full p-8!"
/>
{:else if activePanel === 'mcp'}
<McpServersSettings class="mx-auto w-full p-8! md:translate-x-1.5" />
{:else}
{@render children?.()}
{/if}
{@render children?.()}
</Sidebar.Inset>
</div>
</Sidebar.Provider>

View File

@ -0,0 +1,14 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { page } from '$app/state';
import { ChatSettings } from '$lib/components/app';
import type { SettingsSectionTitle } from '$lib/constants';
let sectionParam = $derived(page.url.searchParams.get('section') as SettingsSectionTitle | null);
</script>
<ChatSettings
onSave={() => goto('#/')}
initialSection={sectionParam ?? undefined}
class="mx-auto h-full md:pl-8"
/>

View File

@ -0,0 +1,5 @@
<script lang="ts">
import { McpServersSettings } from '$lib/components/app';
</script>
<McpServersSettings class="mx-auto w-full p-4 md:p-8 md:pt-0!" />