feat: Separate dialogs for MCP Servers Settings and Import/Export

This commit is contained in:
Aleksander Grygier 2026-04-01 00:21:36 +02:00
parent 9c922bae32
commit 8c55e86cba
20 changed files with 345 additions and 181 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -18,7 +18,7 @@
<div style="display: contents">
<script>
{
__sveltekit_pcz11y = {
__sveltekit_uoz4e7 = {
base: new URL('.', location).pathname.slice(0, -1)
};

View File

@ -29,7 +29,7 @@
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--sidebar: oklch(0.987 0 0);
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0);
--sidebar-primary-foreground: oklch(0.985 0 0);
@ -77,7 +77,7 @@
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.19 0 0);
--sidebar: oklch(0.2 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);

View File

@ -10,8 +10,7 @@
ModelsSelector,
ModelsSelectorSheet
} from '$lib/components/app';
import { SETTINGS_SECTION_TITLES } from '$lib/constants';
import { getChatSettingsDialogContext } from '$lib/contexts';
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';
@ -54,7 +53,7 @@
onMcpResourcesClick
}: Props = $props();
const chatSettingsDialog = getChatSettingsDialogContext();
const mcpServersDialog = getMcpServersDialogContext();
let currentConfig = $derived(config());
let isRouter = $derived(isRouterMode());
@ -214,7 +213,7 @@
</div>
<div class="ml-auto flex items-center gap-2">
<McpActiveServersAvatars onClick={() => chatSettingsDialog.open(SETTINGS_SECTION_TITLES.MCP)} />
<McpActiveServersAvatars onClick={() => mcpServersDialog.open()} />
{#if isMobile.current}
<ModelsSelectorSheet

View File

@ -2,7 +2,6 @@
import { afterNavigate } from '$app/navigation';
import {
ChatScreenForm,
ChatScreenHeader,
ChatMessages,
ChatScreenProcessingInfo,
DialogEmptyFileAlert,
@ -342,8 +341,6 @@
<svelte:window onkeydown={handleKeydown} />
<ChatScreenHeader />
{#if !isEmpty}
<div
bind:this={chatScrollContainer}
@ -427,7 +424,7 @@
>
<div class="w-full max-w-[48rem] px-4">
<div class="mb-10 text-center" in:fade={{ duration: 300 }}>
<h1 class="mb-2 text-2xl font-semibold tracking-tight md:text-3xl">llama.cpp</h1>
<h1 class="mb-2 text-2xl font-semibold tracking-tight md:text-3xl">Hello there</h1>
<p class="text-muted-foreground md:text-lg">
{serverStore.props?.modalities?.audio

View File

@ -7,16 +7,9 @@
Monitor,
ChevronLeft,
ChevronRight,
Database,
ListRestart
} from '@lucide/svelte';
import {
ChatSettingsFooter,
ChatSettingsImportExportTab,
ChatSettingsFields,
McpLogo,
McpServersSettings
} from '$lib/components/app';
import { ChatSettingsFooter, ChatSettingsFields } from '$lib/components/app';
import { ScrollArea } from '$lib/components/ui/scroll-area';
import { config, settingsStore } from '$lib/stores/settings.svelte';
import {
@ -283,16 +276,6 @@
}
]
},
{
title: SETTINGS_SECTION_TITLES.IMPORT_EXPORT,
icon: Database,
fields: []
},
{
title: SETTINGS_SECTION_TITLES.MCP,
icon: McpLogo,
fields: []
},
{
title: SETTINGS_SECTION_TITLES.DEVELOPER,
icon: Code,
@ -529,12 +512,6 @@
<h3 class="text-lg font-semibold">{currentSection.title}</h3>
</div>
{#if currentSection.title === SETTINGS_SECTION_TITLES.IMPORT_EXPORT}
<ChatSettingsImportExportTab />
{:else if currentSection.title === SETTINGS_SECTION_TITLES.MCP}
<McpServersSettings />
{/if}
{#if currentSection.fields}
<div class="space-y-6">
<ChatSettingsFields

View File

@ -138,8 +138,8 @@
}
</script>
<ScrollArea class="h-[100vh]">
<Sidebar.Header class=" top-0 z-10 gap-4 bg-sidebar/50 p-4 pb-2 backdrop-blur-lg md:sticky">
<ScrollArea>
<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>
</a>
@ -147,7 +147,7 @@
<ChatSidebarActions {handleMobileSidebarItemClick} bind:isSearchModeActive bind:searchQuery />
</Sidebar.Header>
<Sidebar.Group class="mt-2 space-y-2 p-0 px-4">
<Sidebar.Group class="mt-2 space-y-2 p-0 px-3">
{#if (filteredConversations.length > 0 && isSearchModeActive) || !isSearchModeActive}
<Sidebar.GroupLabel>
{isSearchModeActive ? 'Search results' : 'Conversations'}

View File

@ -1,11 +1,14 @@
<script lang="ts">
import { Search, SquarePen, X } from '@lucide/svelte';
import { Database, Search, Settings, 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 { SETTINGS_SECTION_TITLES } from '$lib/constants';
import { getChatSettingsDialogContext } from '$lib/contexts';
import {
getChatSettingsDialogContext,
getMcpServersDialogContext,
getImportExportDialogContext
} from '$lib/contexts';
interface Props {
handleMobileSidebarItemClick: () => void;
@ -22,6 +25,8 @@
let searchInput: HTMLInputElement | null = $state(null);
const chatSettingsDialog = getChatSettingsDialogContext();
const mcpServersDialog = getMcpServersDialogContext();
const importExportDialog = getImportExportDialogContext();
function handleSearchModeDeactivate() {
isSearchModeActive = false;
@ -55,7 +60,7 @@
</div>
{:else}
<Button
class="w-full justify-between backdrop-blur-none! hover:[&>kbd]:opacity-100"
class="w-full justify-between px-2 backdrop-blur-none! hover:[&>kbd]:opacity-100"
href="?new_chat=true#/"
onclick={handleMobileSidebarItemClick}
variant="ghost"
@ -70,7 +75,7 @@
</Button>
<Button
class="w-full justify-between backdrop-blur-none! hover:[&>kbd]:opacity-100"
class="w-full justify-between px-2 backdrop-blur-none! hover:[&>kbd]:opacity-100"
onclick={() => {
isSearchModeActive = true;
}}
@ -86,9 +91,9 @@
</Button>
<Button
class="w-full justify-between backdrop-blur-none! hover:[&>kbd]:opacity-100"
class="w-full justify-between px-2 backdrop-blur-none! hover:[&>kbd]:opacity-100"
onclick={() => {
chatSettingsDialog.open(SETTINGS_SECTION_TITLES.MCP);
mcpServersDialog.open();
}}
variant="ghost"
>
@ -98,5 +103,33 @@
MCP Servers
</div>
</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>
<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,33 @@
<script lang="ts">
import * as Dialog from '$lib/components/ui/dialog';
import { ChatSettingsImportExportTab } from '$lib/components/app/chat';
interface Props {
onOpenChange?: (open: boolean) => void;
open?: boolean;
}
let { onOpenChange, open = false }: Props = $props();
function handleClose() {
onOpenChange?.(false);
}
</script>
<Dialog.Root {open} onOpenChange={handleClose}>
<Dialog.Content
class="z-999999 flex h-[100dvh] max-h-[100dvh] min-h-[100dvh] max-w-4xl! flex-col gap-0 rounded-none
p-0 md:h-[80vh] md:max-h-[80vh] md:min-h-0 md:rounded-lg"
>
<div class="grid gap-2 border-b border-border/30 p-4 md:p-6">
<Dialog.Title class="text-lg font-semibold">Import/Export Conversations</Dialog.Title>
<Dialog.Description class="text-sm text-muted-foreground">
Export your conversations to a JSON file or import previously exported conversations.
</Dialog.Description>
</div>
<div class="flex-1 overflow-y-auto px-4 py-6">
<ChatSettingsImportExportTab />
</div>
</Dialog.Content>
</Dialog.Root>

View File

@ -7,7 +7,7 @@
open?: boolean;
}
let { onOpenChange, open = $bindable(false) }: Props = $props();
let { onOpenChange, open = false }: Props = $props();
function handleClose() {
onOpenChange?.(false);

View File

@ -36,6 +36,50 @@
*/
export { default as DialogChatSettings } from './DialogChatSettings.svelte';
/**
* **DialogMcpServersSettings** - MCP servers management dialog
*
* Modal dialog for managing MCP servers with dedicated context.
*
* **Architecture:**
* - Uses context-based state management
* - Provides `open()` method via context
* - Contains McpServersSettings component
*
* @example
* ```svelte
* <!-- In parent component -->
* <DialogMcpServersSettings />
*
* <!-- Trigger via context -->
* {#const mcpDialog = getMcpServersDialogContext()}
* <Button onclick={() => mcpDialog.open()}>Manage MCP Servers</Button>
* ```
*/
export { default as DialogMcpServersSettings } from './DialogMcpServersSettings.svelte';
/**
* **DialogChatSettingsImportExport** - Import/Export conversations dialog
*
* Modal dialog for importing and exporting conversations with dedicated context.
*
* **Architecture:**
* - Uses context-based state management
* - Provides `open()` method via context
* - Contains ChatSettingsImportExportTab component
*
* @example
* ```svelte
* <!-- In parent component -->
* <DialogChatSettingsImportExport />
*
* <!-- Trigger via context -->
* {#const importExportDialog = getImportExportDialogContext()}
* <Button onclick={() => importExportDialog.open()}>Import/Export</Button>
* ```
*/
export { default as DialogChatSettingsImportExport } from './DialogChatSettingsImportExport.svelte';
/**
*
* CONFIRMATION DIALOGS

View File

@ -2,7 +2,7 @@
import { tv, type VariantProps } from 'tailwind-variants';
export const sidebarMenuButtonVariants = tv({
base: 'peer/menu-button outline-hidden ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground group-has-data-[sidebar=menu-action]/menu-item:pr-8 data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm transition-[width,height,padding] focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:font-medium [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0',
base: 'peer/menu-button outline-hidden ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground group-has-data-[sidebar=menu-action]/menu-item:pr-8 data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! flex w-full items-center gap-2 overflow-hidden rounded-md py-2 px-1 text-left text-sm transition-[width,height,padding] focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:font-medium [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0',
variants: {
variant: {
default: 'hover:bg-sidebar-accent hover:text-sidebar-accent-foreground',

View File

@ -21,9 +21,7 @@
data-slot="sidebar-trigger"
variant="ghost"
size="icon-lg"
class="rounded-full backdrop-blur-lg {className} md:left-{sidebar.open
? 'unset'
: '2'} -top-2 -left-2 md:top-0"
class="rounded-full backdrop-blur-lg {className} top-1.5 md:left-[14.5rem]"
type="button"
onclick={(e) => {
onclick?.(e);

View File

@ -74,19 +74,23 @@
: 'group-data-[collapsible=icon]:w-(--sidebar-width-icon)'
)}
></div>
<div
data-slot="sidebar-container"
class={cn(
'fixed inset-y-0 z-999 hidden h-svh 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] duration-200 ease-linear md:z-0 md:flex',
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)]',
? 'left-3 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]'
: 'right-1 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]',
// 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)]'
: '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',
className
)}
style="height: calc(100dvh - 1.5rem);"
{...restProps}
>
<div

View File

@ -1,3 +1,5 @@
export const CONTEXT_KEY_MESSAGE_EDIT = 'chat-message-edit';
export const CONTEXT_KEY_CHAT_ACTIONS = 'chat-actions';
export const CONTEXT_KEY_CHAT_SETTINGS_DIALOG = 'chat-settings-dialog';
export const CONTEXT_KEY_MCP_SERVERS_DIALOG = 'mcp-servers-dialog';
export const CONTEXT_KEY_IMPORT_EXPORT_DIALOG = 'import-export-dialog';

View File

@ -0,0 +1,18 @@
import { getContext, setContext } from 'svelte';
import { CONTEXT_KEY_IMPORT_EXPORT_DIALOG } from '$lib/constants';
export interface ImportExportDialogContext {
open: () => void;
}
const IMPORT_EXPORT_DIALOG_KEY = Symbol.for(CONTEXT_KEY_IMPORT_EXPORT_DIALOG);
export function setImportExportDialogContext(
ctx: ImportExportDialogContext
): ImportExportDialogContext {
return setContext(IMPORT_EXPORT_DIALOG_KEY, ctx);
}
export function getImportExportDialogContext(): ImportExportDialogContext {
return getContext(IMPORT_EXPORT_DIALOG_KEY);
}

View File

@ -17,3 +17,15 @@ export {
setChatSettingsDialogContext,
type ChatSettingsDialogContext
} from './chat-settings-dialog.context';
export {
getMcpServersDialogContext,
setMcpServersDialogContext,
type McpServersDialogContext
} from './mcp-servers-dialog.context';
export {
getImportExportDialogContext,
setImportExportDialogContext,
type ImportExportDialogContext
} from './import-export-dialog.context';

View File

@ -0,0 +1,16 @@
import { getContext, setContext } from 'svelte';
import { CONTEXT_KEY_MCP_SERVERS_DIALOG } from '$lib/constants';
export interface McpServersDialogContext {
open: () => void;
}
const MCP_SERVERS_DIALOG_KEY = Symbol.for(CONTEXT_KEY_MCP_SERVERS_DIALOG);
export function setMcpServersDialogContext(ctx: McpServersDialogContext): McpServersDialogContext {
return setContext(MCP_SERVERS_DIALOG_KEY, ctx);
}
export function getMcpServersDialogContext(): McpServersDialogContext {
return getContext(MCP_SERVERS_DIALOG_KEY);
}

View File

@ -7,7 +7,9 @@
import {
ChatSidebar,
DialogConversationTitleUpdate,
DialogChatSettings
DialogChatSettings,
DialogMcpServersSettings,
DialogChatSettingsImportExport
} from '$lib/components/app';
import { isLoading } from '$lib/stores/chat.svelte';
import { conversationsStore, activeMessages } from '$lib/stores/conversations.svelte';
@ -24,7 +26,11 @@
import type { SettingsSectionTitle } from '$lib/constants';
import { KeyboardKey } from '$lib/enums';
import { IsMobile } from '$lib/hooks/is-mobile.svelte';
import { setChatSettingsDialogContext } from '$lib/contexts';
import {
setChatSettingsDialogContext,
setMcpServersDialogContext,
setImportExportDialogContext
} from '$lib/contexts';
let { children } = $props();
@ -50,6 +56,8 @@
let chatSettingsDialogOpen = $state(false);
let chatSettingsDialogInitialSection = $state<SettingsSectionTitle | undefined>(undefined);
let mcpServersDialogOpen = $state(false);
let importExportDialogOpen = $state(false);
setChatSettingsDialogContext({
open: (initialSection?: SettingsSectionTitle) => {
@ -58,6 +66,18 @@
}
});
setMcpServersDialogContext({
open: () => {
mcpServersDialogOpen = true;
}
});
setImportExportDialogContext({
open: () => {
importExportDialogOpen = true;
}
});
// Global keyboard shortcuts
function handleKeydown(event: KeyboardEvent) {
const isCtrlOrCmd = event.ctrlKey || event.metaKey;
@ -235,6 +255,15 @@
initialSection={chatSettingsDialogInitialSection}
/>
<DialogMcpServersSettings
open={mcpServersDialogOpen}
onOpenChange={(open) => (mcpServersDialogOpen = open)}
/>
<DialogChatSettingsImportExport
open={importExportDialogOpen}
onOpenChange={(open) => (importExportDialogOpen = open)}
/>
<DialogConversationTitleUpdate
bind:open={titleUpdateDialogOpen}
currentTitle={titleUpdateCurrentTitle}