summaryrefslogtreecommitdiff
path: root/llama.cpp/tools/server/webui/src/lib/components/app/dialogs
diff options
context:
space:
mode:
authorMitja Felicijan <mitja.felicijan@gmail.com>2026-02-12 20:57:17 +0100
committerMitja Felicijan <mitja.felicijan@gmail.com>2026-02-12 20:57:17 +0100
commitb333b06772c89d96aacb5490d6a219fba7c09cc6 (patch)
tree211df60083a5946baa2ed61d33d8121b7e251b06 /llama.cpp/tools/server/webui/src/lib/components/app/dialogs
downloadllmnpc-b333b06772c89d96aacb5490d6a219fba7c09cc6.tar.gz
Engage!
Diffstat (limited to 'llama.cpp/tools/server/webui/src/lib/components/app/dialogs')
-rw-r--r--llama.cpp/tools/server/webui/src/lib/components/app/dialogs/DialogChatAttachmentPreview.svelte67
-rw-r--r--llama.cpp/tools/server/webui/src/lib/components/app/dialogs/DialogChatAttachmentsViewAll.svelte54
-rw-r--r--llama.cpp/tools/server/webui/src/lib/components/app/dialogs/DialogChatError.svelte70
-rw-r--r--llama.cpp/tools/server/webui/src/lib/components/app/dialogs/DialogChatSettings.svelte37
-rw-r--r--llama.cpp/tools/server/webui/src/lib/components/app/dialogs/DialogConfirmation.svelte72
-rw-r--r--llama.cpp/tools/server/webui/src/lib/components/app/dialogs/DialogConversationSelection.svelte68
-rw-r--r--llama.cpp/tools/server/webui/src/lib/components/app/dialogs/DialogConversationTitleUpdate.svelte46
-rw-r--r--llama.cpp/tools/server/webui/src/lib/components/app/dialogs/DialogEmptyFileAlert.svelte61
-rw-r--r--llama.cpp/tools/server/webui/src/lib/components/app/dialogs/DialogModelInformation.svelte211
-rw-r--r--llama.cpp/tools/server/webui/src/lib/components/app/dialogs/DialogModelNotAvailable.svelte76
10 files changed, 762 insertions, 0 deletions
diff --git a/llama.cpp/tools/server/webui/src/lib/components/app/dialogs/DialogChatAttachmentPreview.svelte b/llama.cpp/tools/server/webui/src/lib/components/app/dialogs/DialogChatAttachmentPreview.svelte
new file mode 100644
index 0000000..012ba00
--- /dev/null
+++ b/llama.cpp/tools/server/webui/src/lib/components/app/dialogs/DialogChatAttachmentPreview.svelte
@@ -0,0 +1,67 @@
+<script lang="ts">
+ import * as Dialog from '$lib/components/ui/dialog';
+ import { ChatAttachmentPreview } from '$lib/components/app';
+ import { formatFileSize } from '$lib/utils';
+
+ interface Props {
+ open: boolean;
+ onOpenChange?: (open: boolean) => void;
+ // Either an uploaded file or a stored attachment
+ uploadedFile?: ChatUploadedFile;
+ attachment?: DatabaseMessageExtra;
+ // For uploaded files
+ preview?: string;
+ name?: string;
+ size?: number;
+ textContent?: string;
+ // For vision modality check
+ activeModelId?: string;
+ }
+
+ let {
+ open = $bindable(),
+ onOpenChange,
+ uploadedFile,
+ attachment,
+ preview,
+ name,
+ size,
+ textContent,
+ activeModelId
+ }: Props = $props();
+
+ let chatAttachmentPreviewRef: ChatAttachmentPreview | undefined = $state();
+
+ let displayName = $derived(uploadedFile?.name || attachment?.name || name || 'Unknown File');
+
+ let displaySize = $derived(uploadedFile?.size || size);
+
+ $effect(() => {
+ if (open && chatAttachmentPreviewRef) {
+ chatAttachmentPreviewRef.reset();
+ }
+ });
+</script>
+
+<Dialog.Root bind:open {onOpenChange}>
+ <Dialog.Content class="grid max-h-[90vh] max-w-5xl overflow-hidden sm:w-auto sm:max-w-6xl">
+ <Dialog.Header>
+ <Dialog.Title class="pr-8">{displayName}</Dialog.Title>
+ <Dialog.Description>
+ {#if displaySize}
+ {formatFileSize(displaySize)}
+ {/if}
+ </Dialog.Description>
+ </Dialog.Header>
+
+ <ChatAttachmentPreview
+ bind:this={chatAttachmentPreviewRef}
+ {uploadedFile}
+ {attachment}
+ {preview}
+ name={displayName}
+ {textContent}
+ {activeModelId}
+ />
+ </Dialog.Content>
+</Dialog.Root>
diff --git a/llama.cpp/tools/server/webui/src/lib/components/app/dialogs/DialogChatAttachmentsViewAll.svelte b/llama.cpp/tools/server/webui/src/lib/components/app/dialogs/DialogChatAttachmentsViewAll.svelte
new file mode 100644
index 0000000..33ab0fe
--- /dev/null
+++ b/llama.cpp/tools/server/webui/src/lib/components/app/dialogs/DialogChatAttachmentsViewAll.svelte
@@ -0,0 +1,54 @@
+<script lang="ts">
+ import * as Dialog from '$lib/components/ui/dialog';
+ import { ChatAttachmentsViewAll } from '$lib/components/app';
+
+ interface Props {
+ open?: boolean;
+ uploadedFiles?: ChatUploadedFile[];
+ attachments?: DatabaseMessageExtra[];
+ readonly?: boolean;
+ onFileRemove?: (fileId: string) => void;
+ imageHeight?: string;
+ imageWidth?: string;
+ imageClass?: string;
+ activeModelId?: string;
+ }
+
+ let {
+ open = $bindable(false),
+ uploadedFiles = [],
+ attachments = [],
+ readonly = false,
+ onFileRemove,
+ imageHeight = 'h-24',
+ imageWidth = 'w-auto',
+ imageClass = '',
+ activeModelId
+ }: Props = $props();
+
+ let totalCount = $derived(uploadedFiles.length + attachments.length);
+</script>
+
+<Dialog.Root bind:open>
+ <Dialog.Portal>
+ <Dialog.Overlay />
+
+ <Dialog.Content class="flex !max-h-[90vh] !max-w-6xl flex-col">
+ <Dialog.Header>
+ <Dialog.Title>All Attachments ({totalCount})</Dialog.Title>
+ <Dialog.Description>View and manage all attached files</Dialog.Description>
+ </Dialog.Header>
+
+ <ChatAttachmentsViewAll
+ {uploadedFiles}
+ {attachments}
+ {readonly}
+ {onFileRemove}
+ {imageHeight}
+ {imageWidth}
+ {imageClass}
+ {activeModelId}
+ />
+ </Dialog.Content>
+ </Dialog.Portal>
+</Dialog.Root>
diff --git a/llama.cpp/tools/server/webui/src/lib/components/app/dialogs/DialogChatError.svelte b/llama.cpp/tools/server/webui/src/lib/components/app/dialogs/DialogChatError.svelte
new file mode 100644
index 0000000..b4340e8
--- /dev/null
+++ b/llama.cpp/tools/server/webui/src/lib/components/app/dialogs/DialogChatError.svelte
@@ -0,0 +1,70 @@
+<script lang="ts">
+ import * as AlertDialog from '$lib/components/ui/alert-dialog';
+ import { AlertTriangle, TimerOff } from '@lucide/svelte';
+
+ interface Props {
+ open: boolean;
+ type: 'timeout' | 'server';
+ message: string;
+ contextInfo?: { n_prompt_tokens: number; n_ctx: number };
+ onOpenChange?: (open: boolean) => void;
+ }
+
+ let { open = $bindable(), type, message, contextInfo, onOpenChange }: Props = $props();
+
+ const isTimeout = $derived(type === 'timeout');
+ const title = $derived(isTimeout ? 'TCP Timeout' : 'Server Error');
+ const description = $derived(
+ isTimeout
+ ? 'The request did not receive a response from the server before timing out.'
+ : 'The server responded with an error message. Review the details below.'
+ );
+ const iconClass = $derived(isTimeout ? 'text-destructive' : 'text-amber-500');
+ const badgeClass = $derived(
+ isTimeout
+ ? 'border-destructive/40 bg-destructive/10 text-destructive'
+ : 'border-amber-500/40 bg-amber-500/10 text-amber-600 dark:text-amber-400'
+ );
+
+ function handleOpenChange(newOpen: boolean) {
+ open = newOpen;
+ onOpenChange?.(newOpen);
+ }
+</script>
+
+<AlertDialog.Root {open} onOpenChange={handleOpenChange}>
+ <AlertDialog.Content>
+ <AlertDialog.Header>
+ <AlertDialog.Title class="flex items-center gap-2">
+ {#if isTimeout}
+ <TimerOff class={`h-5 w-5 ${iconClass}`} />
+ {:else}
+ <AlertTriangle class={`h-5 w-5 ${iconClass}`} />
+ {/if}
+
+ {title}
+ </AlertDialog.Title>
+
+ <AlertDialog.Description>
+ {description}
+ </AlertDialog.Description>
+ </AlertDialog.Header>
+
+ <div class={`rounded-lg border px-4 py-3 text-sm ${badgeClass}`}>
+ <p class="font-medium">{message}</p>
+ {#if contextInfo}
+ <div class="mt-2 space-y-1 text-xs opacity-80">
+ <p>
+ <span class="font-medium">Prompt tokens:</span>
+ {contextInfo.n_prompt_tokens.toLocaleString()}
+ </p>
+ <p><span class="font-medium">Context size:</span> {contextInfo.n_ctx.toLocaleString()}</p>
+ </div>
+ {/if}
+ </div>
+
+ <AlertDialog.Footer>
+ <AlertDialog.Action onclick={() => handleOpenChange(false)}>Close</AlertDialog.Action>
+ </AlertDialog.Footer>
+ </AlertDialog.Content>
+</AlertDialog.Root>
diff --git a/llama.cpp/tools/server/webui/src/lib/components/app/dialogs/DialogChatSettings.svelte b/llama.cpp/tools/server/webui/src/lib/components/app/dialogs/DialogChatSettings.svelte
new file mode 100644
index 0000000..e9aaa10
--- /dev/null
+++ b/llama.cpp/tools/server/webui/src/lib/components/app/dialogs/DialogChatSettings.svelte
@@ -0,0 +1,37 @@
+<script lang="ts">
+ import * as Dialog from '$lib/components/ui/dialog';
+ import { ChatSettings } from '$lib/components/app';
+
+ interface Props {
+ onOpenChange?: (open: boolean) => void;
+ open?: boolean;
+ }
+
+ let { onOpenChange, open = false }: Props = $props();
+
+ let chatSettingsRef: ChatSettings | undefined = $state();
+
+ function handleClose() {
+ onOpenChange?.(false);
+ }
+
+ function handleSave() {
+ onOpenChange?.(false);
+ }
+
+ $effect(() => {
+ if (open && chatSettingsRef) {
+ chatSettingsRef.reset();
+ }
+ });
+</script>
+
+<Dialog.Root {open} onOpenChange={handleClose}>
+ <Dialog.Content
+ class="z-999999 flex h-[100dvh] max-h-[100dvh] min-h-[100dvh] flex-col gap-0 rounded-none p-0
+ md:h-[64vh] md:max-h-[64vh] md:min-h-0 md:rounded-lg"
+ style="max-width: 48rem;"
+ >
+ <ChatSettings bind:this={chatSettingsRef} onSave={handleSave} />
+ </Dialog.Content>
+</Dialog.Root>
diff --git a/llama.cpp/tools/server/webui/src/lib/components/app/dialogs/DialogConfirmation.svelte b/llama.cpp/tools/server/webui/src/lib/components/app/dialogs/DialogConfirmation.svelte
new file mode 100644
index 0000000..b5175a9
--- /dev/null
+++ b/llama.cpp/tools/server/webui/src/lib/components/app/dialogs/DialogConfirmation.svelte
@@ -0,0 +1,72 @@
+<script lang="ts">
+ import * as AlertDialog from '$lib/components/ui/alert-dialog';
+ import type { Component } from 'svelte';
+
+ interface Props {
+ open: boolean;
+ title: string;
+ description: string;
+ confirmText?: string;
+ cancelText?: string;
+ variant?: 'default' | 'destructive';
+ icon?: Component;
+ onConfirm: () => void;
+ onCancel: () => void;
+ onKeydown?: (event: KeyboardEvent) => void;
+ }
+
+ let {
+ open = $bindable(),
+ title,
+ description,
+ confirmText = 'Confirm',
+ cancelText = 'Cancel',
+ variant = 'default',
+ icon,
+ onConfirm,
+ onCancel,
+ onKeydown
+ }: Props = $props();
+
+ function handleKeydown(event: KeyboardEvent) {
+ if (event.key === 'Enter') {
+ event.preventDefault();
+ onConfirm();
+ }
+ onKeydown?.(event);
+ }
+
+ function handleOpenChange(newOpen: boolean) {
+ if (!newOpen) {
+ onCancel();
+ }
+ }
+</script>
+
+<AlertDialog.Root {open} onOpenChange={handleOpenChange}>
+ <AlertDialog.Content onkeydown={handleKeydown}>
+ <AlertDialog.Header>
+ <AlertDialog.Title class="flex items-center gap-2">
+ {#if icon}
+ {@const IconComponent = icon}
+ <IconComponent class="h-5 w-5 {variant === 'destructive' ? 'text-destructive' : ''}" />
+ {/if}
+ {title}
+ </AlertDialog.Title>
+
+ <AlertDialog.Description>
+ {description}
+ </AlertDialog.Description>
+ </AlertDialog.Header>
+
+ <AlertDialog.Footer>
+ <AlertDialog.Cancel onclick={onCancel}>{cancelText}</AlertDialog.Cancel>
+ <AlertDialog.Action
+ onclick={onConfirm}
+ class={variant === 'destructive' ? 'bg-destructive text-white hover:bg-destructive/80' : ''}
+ >
+ {confirmText}
+ </AlertDialog.Action>
+ </AlertDialog.Footer>
+ </AlertDialog.Content>
+</AlertDialog.Root>
diff --git a/llama.cpp/tools/server/webui/src/lib/components/app/dialogs/DialogConversationSelection.svelte b/llama.cpp/tools/server/webui/src/lib/components/app/dialogs/DialogConversationSelection.svelte
new file mode 100644
index 0000000..1f8ea64
--- /dev/null
+++ b/llama.cpp/tools/server/webui/src/lib/components/app/dialogs/DialogConversationSelection.svelte
@@ -0,0 +1,68 @@
+<script lang="ts">
+ import * as Dialog from '$lib/components/ui/dialog';
+ import { ConversationSelection } from '$lib/components/app';
+
+ interface Props {
+ conversations: DatabaseConversation[];
+ messageCountMap?: Map<string, number>;
+ mode: 'export' | 'import';
+ onCancel: () => void;
+ onConfirm: (selectedConversations: DatabaseConversation[]) => void;
+ open?: boolean;
+ }
+
+ let {
+ conversations,
+ messageCountMap = new Map(),
+ mode,
+ onCancel,
+ onConfirm,
+ open = $bindable(false)
+ }: Props = $props();
+
+ let conversationSelectionRef: ConversationSelection | undefined = $state();
+
+ let previousOpen = $state(false);
+
+ $effect(() => {
+ if (open && !previousOpen && conversationSelectionRef) {
+ conversationSelectionRef.reset();
+ } else if (!open && previousOpen) {
+ onCancel();
+ }
+
+ previousOpen = open;
+ });
+</script>
+
+<Dialog.Root bind:open>
+ <Dialog.Portal>
+ <Dialog.Overlay class="z-[1000000]" />
+
+ <Dialog.Content class="z-[1000001] max-w-2xl">
+ <Dialog.Header>
+ <Dialog.Title>
+ Select Conversations to {mode === 'export' ? 'Export' : 'Import'}
+ </Dialog.Title>
+ <Dialog.Description>
+ {#if mode === 'export'}
+ Choose which conversations you want to export. Selected conversations will be downloaded
+ as a JSON file.
+ {:else}
+ Choose which conversations you want to import. Selected conversations will be merged
+ with your existing conversations.
+ {/if}
+ </Dialog.Description>
+ </Dialog.Header>
+
+ <ConversationSelection
+ bind:this={conversationSelectionRef}
+ {conversations}
+ {messageCountMap}
+ {mode}
+ {onCancel}
+ {onConfirm}
+ />
+ </Dialog.Content>
+ </Dialog.Portal>
+</Dialog.Root>
diff --git a/llama.cpp/tools/server/webui/src/lib/components/app/dialogs/DialogConversationTitleUpdate.svelte b/llama.cpp/tools/server/webui/src/lib/components/app/dialogs/DialogConversationTitleUpdate.svelte
new file mode 100644
index 0000000..4a9ecce
--- /dev/null
+++ b/llama.cpp/tools/server/webui/src/lib/components/app/dialogs/DialogConversationTitleUpdate.svelte
@@ -0,0 +1,46 @@
+<script lang="ts">
+ import * as AlertDialog from '$lib/components/ui/alert-dialog';
+ import { Button } from '$lib/components/ui/button';
+
+ interface Props {
+ open: boolean;
+ currentTitle: string;
+ newTitle: string;
+ onConfirm: () => void;
+ onCancel: () => void;
+ }
+
+ let { open = $bindable(), currentTitle, newTitle, onConfirm, onCancel }: Props = $props();
+</script>
+
+<AlertDialog.Root bind:open>
+ <AlertDialog.Content>
+ <AlertDialog.Header>
+ <AlertDialog.Title>Update Conversation Title?</AlertDialog.Title>
+
+ <AlertDialog.Description>
+ Do you want to update the conversation title to match the first message content?
+ </AlertDialog.Description>
+ </AlertDialog.Header>
+
+ <div class="space-y-4 pt-2 pb-6">
+ <div class="space-y-2">
+ <p class="text-sm font-medium text-muted-foreground">Current title:</p>
+
+ <p class="rounded-md bg-muted/50 p-3 text-sm font-medium">{currentTitle}</p>
+ </div>
+
+ <div class="space-y-2">
+ <p class="text-sm font-medium text-muted-foreground">New title would be:</p>
+
+ <p class="rounded-md bg-muted/50 p-3 text-sm font-medium">{newTitle}</p>
+ </div>
+ </div>
+
+ <AlertDialog.Footer>
+ <Button variant="outline" onclick={onCancel}>Keep Current Title</Button>
+
+ <Button onclick={onConfirm}>Update Title</Button>
+ </AlertDialog.Footer>
+ </AlertDialog.Content>
+</AlertDialog.Root>
diff --git a/llama.cpp/tools/server/webui/src/lib/components/app/dialogs/DialogEmptyFileAlert.svelte b/llama.cpp/tools/server/webui/src/lib/components/app/dialogs/DialogEmptyFileAlert.svelte
new file mode 100644
index 0000000..f875b0a
--- /dev/null
+++ b/llama.cpp/tools/server/webui/src/lib/components/app/dialogs/DialogEmptyFileAlert.svelte
@@ -0,0 +1,61 @@
+<script lang="ts">
+ import * as AlertDialog from '$lib/components/ui/alert-dialog';
+ import { FileX } from '@lucide/svelte';
+
+ interface Props {
+ open: boolean;
+ emptyFiles: string[];
+ onOpenChange?: (open: boolean) => void;
+ }
+
+ let { open = $bindable(), emptyFiles, onOpenChange }: Props = $props();
+
+ function handleOpenChange(newOpen: boolean) {
+ open = newOpen;
+ onOpenChange?.(newOpen);
+ }
+</script>
+
+<AlertDialog.Root {open} onOpenChange={handleOpenChange}>
+ <AlertDialog.Content>
+ <AlertDialog.Header>
+ <AlertDialog.Title class="flex items-center gap-2">
+ <FileX class="h-5 w-5 text-destructive" />
+
+ Empty Files Detected
+ </AlertDialog.Title>
+
+ <AlertDialog.Description>
+ The following files are empty and have been removed from your attachments:
+ </AlertDialog.Description>
+ </AlertDialog.Header>
+
+ <div class="space-y-3 text-sm">
+ <div class="rounded-lg bg-muted p-3">
+ <div class="mb-2 font-medium">Empty Files:</div>
+
+ <ul class="list-inside list-disc space-y-1 text-muted-foreground">
+ {#each emptyFiles as fileName (fileName)}
+ <li class="font-mono text-sm">{fileName}</li>
+ {/each}
+ </ul>
+ </div>
+
+ <div>
+ <div class="mb-2 font-medium">What happened:</div>
+
+ <ul class="list-inside list-disc space-y-1 text-muted-foreground">
+ <li>Empty files cannot be processed or sent to the AI model</li>
+
+ <li>These files have been automatically removed from your attachments</li>
+
+ <li>You can try uploading files with content instead</li>
+ </ul>
+ </div>
+ </div>
+
+ <AlertDialog.Footer>
+ <AlertDialog.Action onclick={() => handleOpenChange(false)}>Got it</AlertDialog.Action>
+ </AlertDialog.Footer>
+ </AlertDialog.Content>
+</AlertDialog.Root>
diff --git a/llama.cpp/tools/server/webui/src/lib/components/app/dialogs/DialogModelInformation.svelte b/llama.cpp/tools/server/webui/src/lib/components/app/dialogs/DialogModelInformation.svelte
new file mode 100644
index 0000000..dfea47c
--- /dev/null
+++ b/llama.cpp/tools/server/webui/src/lib/components/app/dialogs/DialogModelInformation.svelte
@@ -0,0 +1,211 @@
+<script lang="ts">
+ import * as Dialog from '$lib/components/ui/dialog';
+ import * as Table from '$lib/components/ui/table';
+ import { BadgeModality, CopyToClipboardIcon } from '$lib/components/app';
+ import { serverStore } from '$lib/stores/server.svelte';
+ import { modelsStore, modelOptions, modelsLoading } from '$lib/stores/models.svelte';
+ import { formatFileSize, formatParameters, formatNumber } from '$lib/utils';
+
+ interface Props {
+ open?: boolean;
+ onOpenChange?: (open: boolean) => void;
+ }
+
+ let { open = $bindable(), onOpenChange }: Props = $props();
+
+ let serverProps = $derived(serverStore.props);
+ let modelName = $derived(modelsStore.singleModelName);
+ let models = $derived(modelOptions());
+ let isLoadingModels = $derived(modelsLoading());
+
+ // Get the first model for single-model mode display
+ let firstModel = $derived(models[0] ?? null);
+
+ // Get modalities from modelStore using the model ID from the first model
+ let modalities = $derived.by(() => {
+ if (!firstModel?.id) return [];
+ return modelsStore.getModelModalitiesArray(firstModel.id);
+ });
+
+ // Ensure models are fetched when dialog opens
+ $effect(() => {
+ if (open && models.length === 0) {
+ modelsStore.fetch();
+ }
+ });
+</script>
+
+<Dialog.Root bind:open {onOpenChange}>
+ <Dialog.Content class="@container z-9999 !max-w-[60rem] max-w-full">
+ <style>
+ @container (max-width: 56rem) {
+ .resizable-text-container {
+ max-width: calc(100vw - var(--threshold));
+ }
+ }
+ </style>
+
+ <Dialog.Header>
+ <Dialog.Title>Model Information</Dialog.Title>
+ <Dialog.Description>Current model details and capabilities</Dialog.Description>
+ </Dialog.Header>
+
+ <div class="space-y-6 py-4">
+ {#if isLoadingModels}
+ <div class="flex items-center justify-center py-8">
+ <div class="text-sm text-muted-foreground">Loading model information...</div>
+ </div>
+ {:else if firstModel}
+ {@const modelMeta = firstModel.meta}
+
+ {#if serverProps}
+ <Table.Root>
+ <Table.Header>
+ <Table.Row>
+ <Table.Head class="w-[10rem]">Model</Table.Head>
+
+ <Table.Head>
+ <div class="inline-flex items-center gap-2">
+ <span
+ class="resizable-text-container min-w-0 flex-1 truncate"
+ style:--threshold="12rem"
+ >
+ {modelName}
+ </span>
+
+ <CopyToClipboardIcon
+ text={modelName || ''}
+ canCopy={!!modelName}
+ ariaLabel="Copy model name to clipboard"
+ />
+ </div>
+ </Table.Head>
+ </Table.Row>
+ </Table.Header>
+ <Table.Body>
+ <!-- Model Path -->
+ <Table.Row>
+ <Table.Cell class="h-10 align-middle font-medium">File Path</Table.Cell>
+
+ <Table.Cell
+ class="inline-flex h-10 items-center gap-2 align-middle font-mono text-xs"
+ >
+ <span
+ class="resizable-text-container min-w-0 flex-1 truncate"
+ style:--threshold="14rem"
+ >
+ {serverProps.model_path}
+ </span>
+
+ <CopyToClipboardIcon
+ text={serverProps.model_path}
+ ariaLabel="Copy model path to clipboard"
+ />
+ </Table.Cell>
+ </Table.Row>
+
+ <!-- Context Size -->
+ <Table.Row>
+ <Table.Cell class="h-10 align-middle font-medium">Context Size</Table.Cell>
+ <Table.Cell
+ >{formatNumber(serverProps.default_generation_settings.n_ctx)} tokens</Table.Cell
+ >
+ </Table.Row>
+
+ <!-- Training Context -->
+ {#if modelMeta?.n_ctx_train}
+ <Table.Row>
+ <Table.Cell class="h-10 align-middle font-medium">Training Context</Table.Cell>
+ <Table.Cell>{formatNumber(modelMeta.n_ctx_train)} tokens</Table.Cell>
+ </Table.Row>
+ {/if}
+
+ <!-- Model Size -->
+ {#if modelMeta?.size}
+ <Table.Row>
+ <Table.Cell class="h-10 align-middle font-medium">Model Size</Table.Cell>
+ <Table.Cell>{formatFileSize(modelMeta.size)}</Table.Cell>
+ </Table.Row>
+ {/if}
+
+ <!-- Parameters -->
+ {#if modelMeta?.n_params}
+ <Table.Row>
+ <Table.Cell class="h-10 align-middle font-medium">Parameters</Table.Cell>
+ <Table.Cell>{formatParameters(modelMeta.n_params)}</Table.Cell>
+ </Table.Row>
+ {/if}
+
+ <!-- Embedding Size -->
+ {#if modelMeta?.n_embd}
+ <Table.Row>
+ <Table.Cell class="align-middle font-medium">Embedding Size</Table.Cell>
+ <Table.Cell>{formatNumber(modelMeta.n_embd)}</Table.Cell>
+ </Table.Row>
+ {/if}
+
+ <!-- Vocabulary Size -->
+ {#if modelMeta?.n_vocab}
+ <Table.Row>
+ <Table.Cell class="align-middle font-medium">Vocabulary Size</Table.Cell>
+ <Table.Cell>{formatNumber(modelMeta.n_vocab)} tokens</Table.Cell>
+ </Table.Row>
+ {/if}
+
+ <!-- Vocabulary Type -->
+ {#if modelMeta?.vocab_type}
+ <Table.Row>
+ <Table.Cell class="align-middle font-medium">Vocabulary Type</Table.Cell>
+ <Table.Cell class="align-middle capitalize">{modelMeta.vocab_type}</Table.Cell>
+ </Table.Row>
+ {/if}
+
+ <!-- Total Slots -->
+ <Table.Row>
+ <Table.Cell class="align-middle font-medium">Parallel Slots</Table.Cell>
+ <Table.Cell>{serverProps.total_slots}</Table.Cell>
+ </Table.Row>
+
+ <!-- Modalities -->
+ {#if modalities.length > 0}
+ <Table.Row>
+ <Table.Cell class="align-middle font-medium">Modalities</Table.Cell>
+ <Table.Cell>
+ <div class="flex flex-wrap gap-1">
+ <BadgeModality {modalities} />
+ </div>
+ </Table.Cell>
+ </Table.Row>
+ {/if}
+
+ <!-- Build Info -->
+ <Table.Row>
+ <Table.Cell class="align-middle font-medium">Build Info</Table.Cell>
+ <Table.Cell class="align-middle font-mono text-xs"
+ >{serverProps.build_info}</Table.Cell
+ >
+ </Table.Row>
+
+ <!-- Chat Template -->
+ {#if serverProps.chat_template}
+ <Table.Row>
+ <Table.Cell class="align-middle font-medium">Chat Template</Table.Cell>
+ <Table.Cell class="py-10">
+ <div class="max-h-120 overflow-y-auto rounded-md bg-muted p-4">
+ <pre
+ class="font-mono text-xs whitespace-pre-wrap">{serverProps.chat_template}</pre>
+ </div>
+ </Table.Cell>
+ </Table.Row>
+ {/if}
+ </Table.Body>
+ </Table.Root>
+ {/if}
+ {:else if !isLoadingModels}
+ <div class="flex items-center justify-center py-8">
+ <div class="text-sm text-muted-foreground">No model information available</div>
+ </div>
+ {/if}
+ </div>
+ </Dialog.Content>
+</Dialog.Root>
diff --git a/llama.cpp/tools/server/webui/src/lib/components/app/dialogs/DialogModelNotAvailable.svelte b/llama.cpp/tools/server/webui/src/lib/components/app/dialogs/DialogModelNotAvailable.svelte
new file mode 100644
index 0000000..a6c2029
--- /dev/null
+++ b/llama.cpp/tools/server/webui/src/lib/components/app/dialogs/DialogModelNotAvailable.svelte
@@ -0,0 +1,76 @@
+<script lang="ts">
+ import * as AlertDialog from '$lib/components/ui/alert-dialog';
+ import { AlertTriangle, ArrowRight } from '@lucide/svelte';
+ import { goto } from '$app/navigation';
+ import { page } from '$app/state';
+
+ interface Props {
+ open: boolean;
+ modelName: string;
+ availableModels?: string[];
+ onOpenChange?: (open: boolean) => void;
+ }
+
+ let { open = $bindable(), modelName, availableModels = [], onOpenChange }: Props = $props();
+
+ function handleOpenChange(newOpen: boolean) {
+ open = newOpen;
+ onOpenChange?.(newOpen);
+ }
+
+ function handleSelectModel(model: string) {
+ // Build URL with selected model, preserving other params
+ const url = new URL(page.url);
+ url.searchParams.set('model', model);
+
+ handleOpenChange(false);
+ goto(url.toString());
+ }
+</script>
+
+<AlertDialog.Root {open} onOpenChange={handleOpenChange}>
+ <AlertDialog.Content class="max-w-lg">
+ <AlertDialog.Header>
+ <AlertDialog.Title class="flex items-center gap-2">
+ <AlertTriangle class="h-5 w-5 text-amber-500" />
+ Model Not Available
+ </AlertDialog.Title>
+
+ <AlertDialog.Description>
+ The requested model could not be found. Select an available model to continue.
+ </AlertDialog.Description>
+ </AlertDialog.Header>
+
+ <div class="space-y-3">
+ <div class="rounded-lg border border-amber-500/40 bg-amber-500/10 px-4 py-3 text-sm">
+ <p class="font-medium text-amber-600 dark:text-amber-400">
+ Requested: <code class="rounded bg-amber-500/20 px-1.5 py-0.5">{modelName}</code>
+ </p>
+ </div>
+
+ {#if availableModels.length > 0}
+ <div class="text-sm">
+ <p class="mb-2 font-medium text-muted-foreground">Select an available model:</p>
+ <div class="max-h-48 space-y-1 overflow-y-auto rounded-md border p-1">
+ {#each availableModels as model (model)}
+ <button
+ type="button"
+ class="group flex w-full items-center justify-between gap-2 rounded-sm px-3 py-2 text-left text-sm transition-colors hover:bg-accent hover:text-accent-foreground"
+ onclick={() => handleSelectModel(model)}
+ >
+ <span class="min-w-0 truncate font-mono text-xs">{model}</span>
+ <ArrowRight
+ class="h-4 w-4 shrink-0 text-muted-foreground opacity-0 transition-opacity group-hover:opacity-100"
+ />
+ </button>
+ {/each}
+ </div>
+ </div>
+ {/if}
+ </div>
+
+ <AlertDialog.Footer>
+ <AlertDialog.Action onclick={() => handleOpenChange(false)}>Cancel</AlertDialog.Action>
+ </AlertDialog.Footer>
+ </AlertDialog.Content>
+</AlertDialog.Root>