diff options
| author | Mitja Felicijan <mitja.felicijan@gmail.com> | 2026-02-12 20:57:17 +0100 |
|---|---|---|
| committer | Mitja Felicijan <mitja.felicijan@gmail.com> | 2026-02-12 20:57:17 +0100 |
| commit | b333b06772c89d96aacb5490d6a219fba7c09cc6 (patch) | |
| tree | 211df60083a5946baa2ed61d33d8121b7e251b06 /llama.cpp/tools/server/webui/src/lib/components/app/dialogs | |
| download | llmnpc-b333b06772c89d96aacb5490d6a219fba7c09cc6.tar.gz | |
Engage!
Diffstat (limited to 'llama.cpp/tools/server/webui/src/lib/components/app/dialogs')
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> |
