feat: WIP

This commit is contained in:
Aleksander Grygier 2026-04-01 02:02:52 +02:00
parent 8c55e86cba
commit c3520f1e2c
12 changed files with 113 additions and 69 deletions

View File

@ -26,11 +26,12 @@
import type { Component } from 'svelte';
interface Props {
class: string;
onSave?: () => void;
initialSection?: SettingsSectionTitle;
}
let { onSave, initialSection }: Props = $props();
let { class: className, onSave, initialSection }: Props = $props();
const settingSections: Array<{
fields: SettingsFieldConfig[];
@ -431,7 +432,7 @@
});
</script>
<div class="flex h-full flex-col overflow-hidden md:flex-row">
<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">

View File

@ -16,6 +16,7 @@
import { chatStore } from '$lib/stores/chat.svelte';
import { getPreviewText } from '$lib/utils';
import ChatSidebarActions from './ChatSidebarActions.svelte';
import ChatSidebarFooter from './ChatSidebarFooter.svelte';
const sidebar = Sidebar.useSidebar();
@ -138,7 +139,8 @@
}
</script>
<ScrollArea>
<div class="flex h-full flex-col">
<ScrollArea class="flex-1">
<Sidebar.Header class=" top-0 z-10 gap-4 bg-sidebar/50 p-3 pt-4 pb-2 backdrop-blur-lg md:sticky">
<a href="#/" onclick={handleMobileSidebarItemClick}>
<h1 class="inline-flex items-center gap-1 px-2 text-xl font-semibold">llama.cpp</h1>
@ -193,6 +195,11 @@
</Sidebar.Group>
</ScrollArea>
<Sidebar.Footer>
<ChatSidebarFooter />
</Sidebar.Footer>
</div>
<DialogConfirmation
bind:open={showDeleteDialog}
title="Delete Conversation"

View File

@ -1,14 +1,9 @@
<script lang="ts">
import { Database, Search, Settings, SquarePen, X } from '@lucide/svelte';
import { Database, Search, SquarePen, X } from '@lucide/svelte';
import { KeyboardShortcutInfo } from '$lib/components/app';
import { Button } from '$lib/components/ui/button';
import { Input } from '$lib/components/ui/input';
import { McpLogo } from '$lib/components/app';
import {
getChatSettingsDialogContext,
getMcpServersDialogContext,
getImportExportDialogContext
} from '$lib/contexts';
import { getImportExportDialogContext } from '$lib/contexts';
interface Props {
handleMobileSidebarItemClick: () => void;
@ -24,8 +19,6 @@
let searchInput: HTMLInputElement | null = $state(null);
const chatSettingsDialog = getChatSettingsDialogContext();
const mcpServersDialog = getMcpServersDialogContext();
const importExportDialog = getImportExportDialogContext();
function handleSearchModeDeactivate() {
@ -90,20 +83,6 @@
<KeyboardShortcutInfo keys={['cmd', 'k']} />
</Button>
<Button
class="w-full justify-between px-2 backdrop-blur-none! hover:[&>kbd]:opacity-100"
onclick={() => {
mcpServersDialog.open();
}}
variant="ghost"
>
<div class="flex items-center gap-2">
<McpLogo class="h-4 w-4" />
MCP Servers
</div>
</Button>
<Button
class="w-full justify-between px-2 backdrop-blur-none! hover:[&>kbd]:opacity-100"
onclick={() => {
@ -118,18 +97,5 @@
</div>
</Button>
<Button
class="w-full justify-between px-2 backdrop-blur-none! hover:[&>kbd]:opacity-100"
onclick={() => {
chatSettingsDialog.open();
}}
variant="ghost"
>
<div class="flex items-center gap-2">
<Settings class="h-4 w-4" />
Settings
</div>
</Button>
{/if}
</div>

View File

@ -0,0 +1,42 @@
<script lang="ts">
import { Settings } from '@lucide/svelte';
import { Button } from '$lib/components/ui/button';
import { McpLogo } from '$lib/components/app';
import { getChatSettingsDialogContext, getMcpServersDialogContext } from '$lib/contexts';
const chatSettingsDialog = getChatSettingsDialogContext();
const mcpServersDialog = getMcpServersDialogContext();
let isMcpActive = $derived(mcpServersDialog.isActive());
let isSettingsActive = $derived(chatSettingsDialog.isActive());
</script>
<div class="space-y-1 pt-0">
<Button
class="w-full justify-between px-2 backdrop-blur-none! hover:[&>kbd]:opacity-100 {isMcpActive ? 'bg-accent text-accent-foreground' : ''}"
onclick={() => {
mcpServersDialog.open();
}}
variant="ghost"
>
<div class="flex items-center gap-2">
<McpLogo class="h-4 w-4" />
MCP Servers
</div>
</Button>
<Button
class="w-full justify-between px-2 backdrop-blur-none! hover:[&>kbd]:opacity-100 {isSettingsActive ? 'bg-accent text-accent-foreground' : ''}"
onclick={() => {
chatSettingsDialog.open();
}}
variant="ghost"
>
<div class="flex items-center gap-2">
<Settings class="h-4 w-4" />
Settings
</div>
</Button>
</div>

View File

@ -9,6 +9,12 @@
import { MCP_SERVER_ID_PREFIX } from '$lib/constants';
import { HealthCheckStatus } from '$lib/enums';
interface Props {
class?: string;
}
let { class: className }: Props = $props();
let servers = $derived(mcpStore.getServersSorted());
let initialLoadComplete = $state(false);
@ -77,7 +83,7 @@
}
</script>
<div class="space-y-5 md:space-y-4">
<div class="gap-5 grid md:space-y-4 {className}">
<div class="flex items-start justify-between gap-4">
<div>
<h4 class="text-base font-semibold">Manage Servers</h4>
@ -130,7 +136,7 @@
{/if}
{#if servers.length > 0}
<div class="space-y-3">
<div class="grid gap-3" style="grid-template-columns: repeat(auto-fill, minmax(28rem, 1fr));">
{#each servers as server (server.id)}
{#if !initialLoadComplete}
<McpServerCardSkeleton />

View File

@ -14,7 +14,7 @@
bind:this={ref}
data-slot="sidebar-footer"
data-sidebar="footer"
class={cn('flex flex-col gap-2 p-2', className)}
class={cn('flex flex-col gap-2 p-3', className)}
{...restProps}
>
{@render children?.()}

View File

@ -3,6 +3,7 @@
import PanelLeftIcon from '@lucide/svelte/icons/panel-left';
import type { ComponentProps } from 'svelte';
import { useSidebar } from './context.svelte.js';
import { PanelLeftClose } from '@lucide/svelte';
let {
ref = $bindable(null),
@ -21,7 +22,7 @@
data-slot="sidebar-trigger"
variant="ghost"
size="icon-lg"
class="rounded-full backdrop-blur-lg {className} top-1.5 md:left-[14.5rem]"
class="rounded-full backdrop-blur-lg {className} {sidebar.open ? 'top-1.5' : 'top-0'} md:left-[14.5rem]"
type="button"
onclick={(e) => {
onclick?.(e);
@ -29,6 +30,10 @@
}}
{...restProps}
>
<PanelLeftIcon />
{#if sidebar.open}
<PanelLeftClose />
{:else}
<PanelLeftIcon />
{/if}
<span class="sr-only">Toggle Sidebar</span>
</Button>

View File

@ -78,10 +78,10 @@
<div
data-slot="sidebar-container"
class={cn(
'fixed inset-y-0 z-999 hidden w-(--sidebar-width) transition-[left,right,width] duration-200 ease-linear md:z-0 md:flex',
'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)*-1)]'
: 'right-1 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]',
? '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'
? 'p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4))+2px)]'

View File

@ -4,6 +4,7 @@ import { CONTEXT_KEY_CHAT_SETTINGS_DIALOG } from '$lib/constants';
export interface ChatSettingsDialogContext {
open: (initialSection?: SettingsSectionTitle) => void;
isActive: () => boolean;
}
const CHAT_SETTINGS_DIALOG_KEY = Symbol.for(CONTEXT_KEY_CHAT_SETTINGS_DIALOG);

View File

@ -3,6 +3,7 @@ import { CONTEXT_KEY_MCP_SERVERS_DIALOG } from '$lib/constants';
export interface McpServersDialogContext {
open: () => void;
isActive: () => boolean;
}
const MCP_SERVERS_DIALOG_KEY = Symbol.for(CONTEXT_KEY_MCP_SERVERS_DIALOG);

View File

@ -6,9 +6,9 @@
import { untrack } from 'svelte';
import {
ChatSidebar,
ChatSettings,
McpServersSettings,
DialogConversationTitleUpdate,
DialogChatSettings,
DialogMcpServersSettings,
DialogChatSettingsImportExport
} from '$lib/components/app';
import { isLoading } from '$lib/stores/chat.svelte';
@ -54,22 +54,24 @@
let titleUpdateNewTitle = $state('');
let titleUpdateResolve: ((value: boolean) => void) | null = null;
let chatSettingsDialogOpen = $state(false);
let chatSettingsDialogInitialSection = $state<SettingsSectionTitle | undefined>(undefined);
let mcpServersDialogOpen = $state(false);
let activePanel = $state<'chat' | 'settings' | 'mcp'>('chat');
let chatSettingsInitialSection = $state<SettingsSectionTitle | undefined>(undefined);
let chatSettingsRef: ChatSettings | undefined = $state();
let importExportDialogOpen = $state(false);
setChatSettingsDialogContext({
open: (initialSection?: SettingsSectionTitle) => {
chatSettingsDialogInitialSection = initialSection;
chatSettingsDialogOpen = true;
}
chatSettingsInitialSection = initialSection;
activePanel = 'settings';
},
isActive: () => activePanel === 'settings'
});
setMcpServersDialogContext({
open: () => {
mcpServersDialogOpen = true;
}
activePanel = 'mcp';
},
isActive: () => activePanel === 'mcp'
});
setImportExportDialogContext({
@ -78,6 +80,18 @@
}
});
$effect(() => {
if (activePanel === 'settings' && chatSettingsRef) {
chatSettingsRef.reset();
}
});
// Return to chat when navigating to a new route
$effect(() => {
void page.url;
activePanel = 'chat';
});
// Global keyboard shortcuts
function handleKeydown(event: KeyboardEvent) {
const isCtrlOrCmd = event.ctrlKey || event.metaKey;
@ -249,16 +263,6 @@
<Toaster richColors />
<DialogChatSettings
open={chatSettingsDialogOpen}
onOpenChange={(open) => (chatSettingsDialogOpen = open)}
initialSection={chatSettingsDialogInitialSection}
/>
<DialogMcpServersSettings
open={mcpServersDialogOpen}
onOpenChange={(open) => (mcpServersDialogOpen = open)}
/>
<DialogChatSettingsImportExport
open={importExportDialogOpen}
onOpenChange={(open) => (importExportDialogOpen = open)}
@ -288,7 +292,18 @@
{/if}
<Sidebar.Inset class="flex flex-1 flex-col overflow-hidden">
{@render children?.()}
{#if activePanel === 'settings'}
<ChatSettings
bind:this={chatSettingsRef}
onSave={() => (activePanel = 'chat')}
initialSection={chatSettingsInitialSection}
class="p-8! mx-auto h-full"
/>
{:else if activePanel === 'mcp'}
<McpServersSettings class="w-full mx-auto p-8! md:translate-x-1.5" />
{:else}
{@render children?.()}
{/if}
</Sidebar.Inset>
</div>
</Sidebar.Provider>

View File

@ -87,7 +87,7 @@
</script>
<svelte:head>
<title>llama.cpp - AI Chat Interface</title>
<title>llama.cpp</title>
</svelte:head>
<ChatScreen showCenteredEmpty />