feat: WIP
This commit is contained in:
parent
5acfc403bd
commit
cfd5a6b1ce
|
|
@ -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} />
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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'}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
/>
|
||||
|
|
@ -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!" />
|
||||
Loading…
Reference in New Issue