feat: WIP

This commit is contained in:
Aleksander Grygier 2026-04-01 18:25:12 +02:00
parent ec6302960e
commit 2d2ef88aaf
16 changed files with 166 additions and 162 deletions

View File

@ -10,7 +10,6 @@
ModelsSelector,
ModelsSelectorSheet
} from '$lib/components/app';
import { getMcpServersDialogContext } from '$lib/contexts';
import { FileTypeCategory } from '$lib/enums';
import { IsMobile } from '$lib/hooks/is-mobile.svelte';
import { chatStore } from '$lib/stores/chat.svelte';
@ -20,6 +19,7 @@
import { config } from '$lib/stores/settings.svelte';
import { activeMessages, conversationsStore } from '$lib/stores/conversations.svelte';
import { getFileTypeCategory } from '$lib/utils';
import { goto } from '$app/navigation';
interface Props {
canSend?: boolean;
@ -53,8 +53,6 @@
onMcpResourcesClick
}: Props = $props();
const mcpServersDialog = getMcpServersDialogContext();
let currentConfig = $derived(config());
let isRouter = $derived(isRouterMode());
let isOffline = $derived(!!serverError());
@ -213,7 +211,7 @@
</div>
<div class="ml-auto flex items-center gap-2">
<McpActiveServersAvatars onClick={() => mcpServersDialog.open()} />
<McpActiveServersAvatars onClick={() => goto('/#/settings/mcp')} />
{#if isMobile.current}
<ModelsSelectorSheet

View File

@ -572,21 +572,3 @@
open={Boolean(activeErrorDialog)}
type={activeErrorDialog?.type ?? ErrorDialogType.SERVER}
/>
<style>
.conversation-chat-form {
position: relative;
&::after {
content: '';
position: absolute;
bottom: 0;
z-index: -1;
left: 0;
right: 0;
width: 100%;
height: 2.375rem;
background-color: var(--background);
}
}
</style>

View File

@ -8,9 +8,7 @@
ChevronLeft,
ChevronRight,
ListRestart,
Sliders
} from '@lucide/svelte';
import { ChatSettingsFooter, ChatSettingsFields } from '$lib/components/app';
import { config, settingsStore } from '$lib/stores/settings.svelte';
@ -429,12 +427,11 @@
});
</script>
<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">
<div class="flex flex-1 flex-col gap-4 md:flex-row">
<!-- Desktop Sidebar -->
<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">
<div class="sticky top-0 hidden w-64 flex-col self-start bg-background pt-8 pb-4 md:flex">
<div class="flex items-center gap-2 pb-12">
<Settings class="h-6 w-6" />
<h1 class="text-2xl font-semibold">Settings</h1>
</div>
@ -456,11 +453,13 @@
</div>
<!-- Mobile Header with Horizontal Scrollable Menu -->
<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 class="sticky top-0 z-10 flex flex-col bg-background md:hidden">
<div class="flex items-center gap-2 px-4 pt-4 md:pt-6 pb-2">
<Settings class="h-5 w-5 md:h-6 md:w-6" />
<h1 class="text-xl md: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;">
@ -511,8 +510,8 @@
</div>
</div>
<div class="flex-1 max-w-5xl mx-auto">
<div class="space-y-6 p-4 md:p-6 md:pt-22">
<div class="mx-auto max-w-3xl flex-1">
<div class="space-y-6 p-4 md:p-6 md:pt-28">
<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" />
@ -536,8 +535,8 @@
<p class="text-xs text-muted-foreground">Settings are saved in browser's localStorage</p>
</div>
</div>
<ChatSettingsFooter onReset={handleReset} onSave={handleSave} />
</div>
</div>
</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="sticky bottom-0 mx-auto mt-4 flex w-full max-w-4xl justify-between p-6">
<div class="flex gap-2">
<Button variant="outline" onclick={handleResetClick}>
<RotateCcw class="h-3 w-3" />

View File

@ -1,5 +1,5 @@
<script lang="ts">
import { Download, Upload, Trash2 } from '@lucide/svelte';
import { Download, Upload, Trash2, Database } from '@lucide/svelte';
import { Button } from '$lib/components/ui/button';
import { DialogConversationSelection, DialogConfirmation } from '$lib/components/app';
import { createMessageCountMap } from '$lib/utils';
@ -175,9 +175,15 @@
</script>
<div class="space-y-6">
<div class="flex items-center gap-2 pb-4">
<Database class="h-5 w-5 md:h-6 md:w-6" />
<h1 class="text-xl md:text-2xl font-semibold">Import / Export</h1>
</div>
<div class="space-y-4">
<div class="grid">
<h4 class="mb-2 text-sm font-medium">Export Conversations</h4>
<h4 class="mt-0 mb-2 text-sm font-medium">Export Conversations</h4>
<p class="mb-4 text-sm text-muted-foreground">
Download all your conversations as a JSON file. This includes all messages, attachments, and
@ -218,7 +224,7 @@
</div>
<div class="grid border-t border-border/30 pt-4">
<h4 class="mb-2 text-sm font-medium">Import Conversations</h4>
<h4 class="mt-0 mb-2 text-sm font-medium">Import Conversations</h4>
<p class="mb-4 text-sm text-muted-foreground">
Import one or more conversations from a previously exported JSON file. This will merge with
@ -258,7 +264,7 @@
</div>
<div class="grid border-t border-border/30 pt-4">
<h4 class="mb-2 text-sm font-medium text-destructive">Delete All Conversations</h4>
<h4 class="mt-0 mb-2 text-sm font-medium text-destructive">Delete All Conversations</h4>
<p class="mb-4 text-sm text-muted-foreground">
Permanently delete all conversations and their messages. This action cannot be undone.

View File

@ -157,7 +157,12 @@
<h1 class="inline-flex items-center gap-1 px-2 text-xl font-semibold">llama.cpp</h1>
</a>
<ChatSidebarActions bind:this={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

@ -1,9 +1,8 @@
<script lang="ts">
import { Database, Search, SquarePen } from '@lucide/svelte';
import { Search, SquarePen } from '@lucide/svelte';
import { KeyboardShortcutInfo } from '$lib/components/app';
import { Button } from '$lib/components/ui/button';
import { SearchInput } from '$lib/components/app';
import { getImportExportDialogContext } from '$lib/contexts';
interface Props {
handleMobileSidebarItemClick: () => void;
@ -19,7 +18,6 @@
isCancelAlwaysVisible = false
}: Props = $props();
const importExportDialog = getImportExportDialogContext();
let searchInputRef = $state<HTMLInputElement | null>(null);
function handleSearchModeDeactivate() {
@ -42,7 +40,7 @@
onClose={handleSearchModeDeactivate}
onKeyDown={(e) => e.key === 'Escape' && handleSearchModeDeactivate()}
placeholder="Search conversations..."
isCancelAlwaysVisible={isCancelAlwaysVisible}
{isCancelAlwaysVisible}
/>
{:else}
<Button
@ -73,19 +71,5 @@
<KeyboardShortcutInfo keys={['cmd', 'k']} />
</Button>
<Button
class="w-full justify-between px-2 backdrop-blur-none! hover:[&>kbd]:opacity-100"
onclick={() => {
importExportDialog.open();
}}
variant="ghost"
>
<div class="flex items-center gap-2">
<Database class="h-4 w-4" />
Import / Export
</div>
</Button>
{/if}
</div>

View File

@ -1,15 +1,25 @@
<script lang="ts">
import { Settings } from '@lucide/svelte';
import { Database, Settings } from '@lucide/svelte';
import { Button } from '$lib/components/ui/button';
import { McpLogo } from '$lib/components/app';
// import { getChatSettingsDialogContext, getMcpServersDialogContext } from '$lib/contexts';
import { page } from '$app/state';
import * as Sidebar from '$lib/components/ui/sidebar';
// const chatSettingsDialog = getChatSettingsDialogContext();
// const mcpServersDialog = getMcpServersDialogContext();
const sidebar = Sidebar.useSidebar();
let isMcpActive = $derived(page.route.id === '/settings/mcp');
let isSettingsActive = $derived(page.route.id === '/settings/chat');
let isImportExportActive = $derived(page.route.id === '/settings/import-export');
function handleMobileSidebarItemClick() {
if (sidebar.isMobile) {
sidebar.toggle();
}
}
</script>
<div class="space-y-1 pt-0">
@ -18,6 +28,7 @@
? 'bg-accent text-accent-foreground'
: ''}"
href="#/settings/mcp"
onclick={handleMobileSidebarItemClick}
variant="ghost"
>
<div class="flex items-center gap-2">
@ -27,11 +38,27 @@
</div>
</Button>
<Button
class="w-full justify-between px-2 backdrop-blur-none! hover:[&>kbd]:opacity-100 {isImportExportActive
? 'bg-accent text-accent-foreground'
: ''}"
href="#/settings/import-export"
onclick={handleMobileSidebarItemClick}
variant="ghost"
>
<div class="flex items-center gap-2">
<Database class="h-4 w-4" />
Import / Export
</div>
</Button>
<Button
class="w-full justify-between px-2 backdrop-blur-none! hover:[&>kbd]:opacity-100 {isSettingsActive
? 'bg-accent text-accent-foreground'
: ''}"
href="#/settings/chat"
onclick={handleMobileSidebarItemClick}
variant="ghost"
>
<div class="flex items-center gap-2">

View File

@ -65,7 +65,7 @@
{#if showClearButton}
<button
type="button"
class="cursor-pointer absolute top-1/2 right-3 -translate-y-1/2 transform text-muted-foreground transition-colors hover:text-foreground"
class="absolute top-1/2 right-3 -translate-y-1/2 transform cursor-pointer text-muted-foreground transition-colors hover:text-foreground"
onclick={handleClear}
aria-label={value ? 'Clear search' : 'Close'}
>

View File

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

View File

@ -1,8 +1,7 @@
<script lang="ts">
import { Search, X } from '@lucide/svelte';
import { Button } from '$lib/components/ui/button';
import { Input } from '$lib/components/ui/input';
import { Checkbox } from '$lib/components/ui/checkbox';
import SearchInput from '$lib/components/app/forms/SearchInput.svelte';
import { ScrollArea } from '$lib/components/ui/scroll-area';
import { SvelteSet } from 'svelte/reactivity';
@ -111,21 +110,7 @@
</script>
<div class="space-y-4">
<div class="relative">
<Search class="absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<Input bind:value={searchQuery} placeholder="Search conversations..." class="pr-9 pl-9" />
{#if searchQuery}
<button
class="absolute top-1/2 right-3 -translate-y-1/2 text-muted-foreground hover:text-foreground"
onclick={() => (searchQuery = '')}
type="button"
>
<X class="h-4 w-4" />
</button>
{/if}
</div>
<SearchInput bind:value={searchQuery} placeholder="Search conversations..." />
<div class="flex items-center justify-between text-sm text-muted-foreground">
<span>

View File

@ -9,7 +9,7 @@
variant: {
default: 'bg-primary text-primary-foreground shadow-xs hover:bg-primary/90',
destructive:
'bg-destructive shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60 text-white',
'bg-destructive shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60 text-white!',
outline:
'bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50 border',
secondary:

View File

@ -86,14 +86,14 @@
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',
]
'my-3 overflow-hidden rounded-2xl border border-sidebar-border shadow-md'
]
: [
'transition-[left,right,width] h-svh',
'h-svh transition-[left,right,width]',
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)]',
],
: '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)]'

View File

@ -11,7 +11,7 @@
DialogConversationTitleUpdate,
DialogChatSettingsImportExport
} from '$lib/components/app';
import { Settings, Search, SquarePen } from '@lucide/svelte';
import { Database, 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';
@ -36,10 +36,6 @@
let { children } = $props();
let isChatRoute = $derived(page.route.id === '/chat/[id]');
let isHomeRoute = $derived(page.route.id === '/');
let isNewChatMode = $derived(page.url.searchParams.get('new_chat') === 'true');
let showSidebarByDefault = $derived(activeMessages().length > 0 || isLoading());
let alwaysShowSidebarOnDesktop = $derived(config().alwaysShowSidebarOnDesktop);
let isMobile = new IsMobile();
let isDesktop = $derived(!isMobile.current);
@ -57,7 +53,9 @@
let activePanel = $state<'chat' | 'settings' | 'mcp'>('chat');
let isMcpActive = $derived(page.route.id === '/settings/mcp');
let isImportExportActive = $derived(page.route.id === '/settings/import-export');
let isSettingsActive = $derived(page.route.id === '/settings/chat');
let isSettingsRoute = $derived(!!page.route.id?.startsWith('/settings'));
// let chatSettingsInitialSection = $state<SettingsSectionTitle | undefined>(undefined);
let chatSettingsRef: ChatSettings | undefined = $state();
let importExportDialogOpen = $state(false);
@ -269,7 +267,7 @@
<ChatSidebar bind:this={chatSidebar} />
</Sidebar.Root>
{#if !(alwaysShowSidebarOnDesktop && isDesktop)}
{#if !(alwaysShowSidebarOnDesktop && isDesktop) && !(isSettingsRoute && !isDesktop)}
<Sidebar.Trigger
class="transition-left absolute left-0 z-[900] duration-200 ease-linear {sidebarOpen
? 'md:left-[calc(var(--sidebar-width)+0.75rem)]'
@ -280,76 +278,64 @@
{#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'}">
<aside
class="hidden shrink-0 flex-col items-center justify-between overflow-hidden py-3 transition-[width,opacity] duration-200 ease-linear md:flex {sidebarOpen
? 'pointer-events-none w-0 opacity-0'
: '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>
<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/import-export"
class="rounded-full {isImportExportActive ? 'bg-accent text-accent-foreground' : ''}"
>
<Database class="h-4 w-4" />
<span class="sr-only">Import / Export</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-lg"
href="#/settings/mcp"
class={isMcpActive ? 'bg-accent text-accent-foreground' : ''}
>
<McpLogo class="h-4 w-4" />
</Button>
<Button
variant="ghost"
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-auto">
{@render children?.()}

View File

@ -0,0 +1,26 @@
<script lang="ts">
import { X } from '@lucide/svelte';
import { goto } from '$app/navigation';
import { browser } from '$app/environment';
import { ActionIcon } from '$lib/components/app';
let { children } = $props();
function handleClose() {
if (browser && window.history.length > 1) {
history.back();
} else {
goto('#/');
}
}
</script>
<div class="relative h-full">
<div class="fixed top-4.5 right-4 z-50 md:hidden">
<ActionIcon icon={X} tooltip="Close" onclick={handleClose} />
</div>
<div class="min-h-full">
{@render children?.()}
</div>
</div>

View File

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