diff options
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 @@ | |||
| 1 | <script lang="ts"> | ||
| 2 | import * as Dialog from '$lib/components/ui/dialog'; | ||
| 3 | import { ChatAttachmentPreview } from '$lib/components/app'; | ||
| 4 | import { formatFileSize } from '$lib/utils'; | ||
| 5 | |||
| 6 | interface Props { | ||
| 7 | open: boolean; | ||
| 8 | onOpenChange?: (open: boolean) => void; | ||
| 9 | // Either an uploaded file or a stored attachment | ||
| 10 | uploadedFile?: ChatUploadedFile; | ||
| 11 | attachment?: DatabaseMessageExtra; | ||
| 12 | // For uploaded files | ||
| 13 | preview?: string; | ||
| 14 | name?: string; | ||
| 15 | size?: number; | ||
| 16 | textContent?: string; | ||
| 17 | // For vision modality check | ||
| 18 | activeModelId?: string; | ||
| 19 | } | ||
| 20 | |||
| 21 | let { | ||
| 22 | open = $bindable(), | ||
| 23 | onOpenChange, | ||
| 24 | uploadedFile, | ||
| 25 | attachment, | ||
| 26 | preview, | ||
| 27 | name, | ||
| 28 | size, | ||
| 29 | textContent, | ||
| 30 | activeModelId | ||
| 31 | }: Props = $props(); | ||
| 32 | |||
| 33 | let chatAttachmentPreviewRef: ChatAttachmentPreview | undefined = $state(); | ||
| 34 | |||
| 35 | let displayName = $derived(uploadedFile?.name || attachment?.name || name || 'Unknown File'); | ||
| 36 | |||
| 37 | let displaySize = $derived(uploadedFile?.size || size); | ||
| 38 | |||
| 39 | $effect(() => { | ||
| 40 | if (open && chatAttachmentPreviewRef) { | ||
| 41 | chatAttachmentPreviewRef.reset(); | ||
| 42 | } | ||
| 43 | }); | ||
| 44 | </script> | ||
| 45 | |||
| 46 | <Dialog.Root bind:open {onOpenChange}> | ||
| 47 | <Dialog.Content class="grid max-h-[90vh] max-w-5xl overflow-hidden sm:w-auto sm:max-w-6xl"> | ||
| 48 | <Dialog.Header> | ||
| 49 | <Dialog.Title class="pr-8">{displayName}</Dialog.Title> | ||
| 50 | <Dialog.Description> | ||
| 51 | {#if displaySize} | ||
| 52 | {formatFileSize(displaySize)} | ||
| 53 | {/if} | ||
| 54 | </Dialog.Description> | ||
| 55 | </Dialog.Header> | ||
| 56 | |||
| 57 | <ChatAttachmentPreview | ||
| 58 | bind:this={chatAttachmentPreviewRef} | ||
| 59 | {uploadedFile} | ||
| 60 | {attachment} | ||
| 61 | {preview} | ||
| 62 | name={displayName} | ||
| 63 | {textContent} | ||
| 64 | {activeModelId} | ||
| 65 | /> | ||
| 66 | </Dialog.Content> | ||
| 67 | </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 @@ | |||
| 1 | <script lang="ts"> | ||
| 2 | import * as Dialog from '$lib/components/ui/dialog'; | ||
| 3 | import { ChatAttachmentsViewAll } from '$lib/components/app'; | ||
| 4 | |||
| 5 | interface Props { | ||
| 6 | open?: boolean; | ||
| 7 | uploadedFiles?: ChatUploadedFile[]; | ||
| 8 | attachments?: DatabaseMessageExtra[]; | ||
| 9 | readonly?: boolean; | ||
| 10 | onFileRemove?: (fileId: string) => void; | ||
| 11 | imageHeight?: string; | ||
| 12 | imageWidth?: string; | ||
| 13 | imageClass?: string; | ||
| 14 | activeModelId?: string; | ||
| 15 | } | ||
| 16 | |||
| 17 | let { | ||
| 18 | open = $bindable(false), | ||
| 19 | uploadedFiles = [], | ||
| 20 | attachments = [], | ||
| 21 | readonly = false, | ||
| 22 | onFileRemove, | ||
| 23 | imageHeight = 'h-24', | ||
| 24 | imageWidth = 'w-auto', | ||
| 25 | imageClass = '', | ||
| 26 | activeModelId | ||
| 27 | }: Props = $props(); | ||
| 28 | |||
| 29 | let totalCount = $derived(uploadedFiles.length + attachments.length); | ||
| 30 | </script> | ||
| 31 | |||
| 32 | <Dialog.Root bind:open> | ||
| 33 | <Dialog.Portal> | ||
| 34 | <Dialog.Overlay /> | ||
| 35 | |||
| 36 | <Dialog.Content class="flex !max-h-[90vh] !max-w-6xl flex-col"> | ||
| 37 | <Dialog.Header> | ||
| 38 | <Dialog.Title>All Attachments ({totalCount})</Dialog.Title> | ||
| 39 | <Dialog.Description>View and manage all attached files</Dialog.Description> | ||
| 40 | </Dialog.Header> | ||
| 41 | |||
| 42 | <ChatAttachmentsViewAll | ||
| 43 | {uploadedFiles} | ||
| 44 | {attachments} | ||
| 45 | {readonly} | ||
| 46 | {onFileRemove} | ||
| 47 | {imageHeight} | ||
| 48 | {imageWidth} | ||
| 49 | {imageClass} | ||
| 50 | {activeModelId} | ||
| 51 | /> | ||
| 52 | </Dialog.Content> | ||
| 53 | </Dialog.Portal> | ||
| 54 | </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 @@ | |||
| 1 | <script lang="ts"> | ||
| 2 | import * as AlertDialog from '$lib/components/ui/alert-dialog'; | ||
| 3 | import { AlertTriangle, TimerOff } from '@lucide/svelte'; | ||
| 4 | |||
| 5 | interface Props { | ||
| 6 | open: boolean; | ||
| 7 | type: 'timeout' | 'server'; | ||
| 8 | message: string; | ||
| 9 | contextInfo?: { n_prompt_tokens: number; n_ctx: number }; | ||
| 10 | onOpenChange?: (open: boolean) => void; | ||
| 11 | } | ||
| 12 | |||
| 13 | let { open = $bindable(), type, message, contextInfo, onOpenChange }: Props = $props(); | ||
| 14 | |||
| 15 | const isTimeout = $derived(type === 'timeout'); | ||
| 16 | const title = $derived(isTimeout ? 'TCP Timeout' : 'Server Error'); | ||
| 17 | const description = $derived( | ||
| 18 | isTimeout | ||
| 19 | ? 'The request did not receive a response from the server before timing out.' | ||
| 20 | : 'The server responded with an error message. Review the details below.' | ||
| 21 | ); | ||
| 22 | const iconClass = $derived(isTimeout ? 'text-destructive' : 'text-amber-500'); | ||
| 23 | const badgeClass = $derived( | ||
| 24 | isTimeout | ||
| 25 | ? 'border-destructive/40 bg-destructive/10 text-destructive' | ||
| 26 | : 'border-amber-500/40 bg-amber-500/10 text-amber-600 dark:text-amber-400' | ||
| 27 | ); | ||
| 28 | |||
| 29 | function handleOpenChange(newOpen: boolean) { | ||
| 30 | open = newOpen; | ||
| 31 | onOpenChange?.(newOpen); | ||
| 32 | } | ||
| 33 | </script> | ||
| 34 | |||
| 35 | <AlertDialog.Root {open} onOpenChange={handleOpenChange}> | ||
| 36 | <AlertDialog.Content> | ||
| 37 | <AlertDialog.Header> | ||
| 38 | <AlertDialog.Title class="flex items-center gap-2"> | ||
| 39 | {#if isTimeout} | ||
| 40 | <TimerOff class={`h-5 w-5 ${iconClass}`} /> | ||
| 41 | {:else} | ||
| 42 | <AlertTriangle class={`h-5 w-5 ${iconClass}`} /> | ||
| 43 | {/if} | ||
| 44 | |||
| 45 | {title} | ||
| 46 | </AlertDialog.Title> | ||
| 47 | |||
| 48 | <AlertDialog.Description> | ||
| 49 | {description} | ||
| 50 | </AlertDialog.Description> | ||
| 51 | </AlertDialog.Header> | ||
| 52 | |||
| 53 | <div class={`rounded-lg border px-4 py-3 text-sm ${badgeClass}`}> | ||
| 54 | <p class="font-medium">{message}</p> | ||
| 55 | {#if contextInfo} | ||
| 56 | <div class="mt-2 space-y-1 text-xs opacity-80"> | ||
| 57 | <p> | ||
| 58 | <span class="font-medium">Prompt tokens:</span> | ||
| 59 | {contextInfo.n_prompt_tokens.toLocaleString()} | ||
| 60 | </p> | ||
| 61 | <p><span class="font-medium">Context size:</span> {contextInfo.n_ctx.toLocaleString()}</p> | ||
| 62 | </div> | ||
| 63 | {/if} | ||
| 64 | </div> | ||
| 65 | |||
| 66 | <AlertDialog.Footer> | ||
| 67 | <AlertDialog.Action onclick={() => handleOpenChange(false)}>Close</AlertDialog.Action> | ||
| 68 | </AlertDialog.Footer> | ||
| 69 | </AlertDialog.Content> | ||
| 70 | </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 @@ | |||
| 1 | <script lang="ts"> | ||
| 2 | import * as Dialog from '$lib/components/ui/dialog'; | ||
| 3 | import { ChatSettings } from '$lib/components/app'; | ||
| 4 | |||
| 5 | interface Props { | ||
| 6 | onOpenChange?: (open: boolean) => void; | ||
| 7 | open?: boolean; | ||
| 8 | } | ||
| 9 | |||
| 10 | let { onOpenChange, open = false }: Props = $props(); | ||
| 11 | |||
| 12 | let chatSettingsRef: ChatSettings | undefined = $state(); | ||
| 13 | |||
| 14 | function handleClose() { | ||
| 15 | onOpenChange?.(false); | ||
| 16 | } | ||
| 17 | |||
| 18 | function handleSave() { | ||
| 19 | onOpenChange?.(false); | ||
| 20 | } | ||
| 21 | |||
| 22 | $effect(() => { | ||
| 23 | if (open && chatSettingsRef) { | ||
| 24 | chatSettingsRef.reset(); | ||
| 25 | } | ||
| 26 | }); | ||
| 27 | </script> | ||
| 28 | |||
| 29 | <Dialog.Root {open} onOpenChange={handleClose}> | ||
| 30 | <Dialog.Content | ||
| 31 | class="z-999999 flex h-[100dvh] max-h-[100dvh] min-h-[100dvh] flex-col gap-0 rounded-none p-0 | ||
| 32 | md:h-[64vh] md:max-h-[64vh] md:min-h-0 md:rounded-lg" | ||
| 33 | style="max-width: 48rem;" | ||
| 34 | > | ||
| 35 | <ChatSettings bind:this={chatSettingsRef} onSave={handleSave} /> | ||
| 36 | </Dialog.Content> | ||
| 37 | </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 @@ | |||
| 1 | <script lang="ts"> | ||
| 2 | import * as AlertDialog from '$lib/components/ui/alert-dialog'; | ||
| 3 | import type { Component } from 'svelte'; | ||
| 4 | |||
| 5 | interface Props { | ||
| 6 | open: boolean; | ||
| 7 | title: string; | ||
| 8 | description: string; | ||
| 9 | confirmText?: string; | ||
| 10 | cancelText?: string; | ||
| 11 | variant?: 'default' | 'destructive'; | ||
| 12 | icon?: Component; | ||
| 13 | onConfirm: () => void; | ||
| 14 | onCancel: () => void; | ||
| 15 | onKeydown?: (event: KeyboardEvent) => void; | ||
| 16 | } | ||
| 17 | |||
| 18 | let { | ||
| 19 | open = $bindable(), | ||
| 20 | title, | ||
| 21 | description, | ||
| 22 | confirmText = 'Confirm', | ||
| 23 | cancelText = 'Cancel', | ||
| 24 | variant = 'default', | ||
| 25 | icon, | ||
| 26 | onConfirm, | ||
| 27 | onCancel, | ||
| 28 | onKeydown | ||
| 29 | }: Props = $props(); | ||
| 30 | |||
| 31 | function handleKeydown(event: KeyboardEvent) { | ||
| 32 | if (event.key === 'Enter') { | ||
| 33 | event.preventDefault(); | ||
| 34 | onConfirm(); | ||
| 35 | } | ||
| 36 | onKeydown?.(event); | ||
| 37 | } | ||
| 38 | |||
| 39 | function handleOpenChange(newOpen: boolean) { | ||
| 40 | if (!newOpen) { | ||
| 41 | onCancel(); | ||
| 42 | } | ||
| 43 | } | ||
| 44 | </script> | ||
| 45 | |||
| 46 | <AlertDialog.Root {open} onOpenChange={handleOpenChange}> | ||
| 47 | <AlertDialog.Content onkeydown={handleKeydown}> | ||
| 48 | <AlertDialog.Header> | ||
| 49 | <AlertDialog.Title class="flex items-center gap-2"> | ||
| 50 | {#if icon} | ||
| 51 | {@const IconComponent = icon} | ||
| 52 | <IconComponent class="h-5 w-5 {variant === 'destructive' ? 'text-destructive' : ''}" /> | ||
| 53 | {/if} | ||
| 54 | {title} | ||
| 55 | </AlertDialog.Title> | ||
| 56 | |||
| 57 | <AlertDialog.Description> | ||
| 58 | {description} | ||
| 59 | </AlertDialog.Description> | ||
| 60 | </AlertDialog.Header> | ||
| 61 | |||
| 62 | <AlertDialog.Footer> | ||
| 63 | <AlertDialog.Cancel onclick={onCancel}>{cancelText}</AlertDialog.Cancel> | ||
| 64 | <AlertDialog.Action | ||
| 65 | onclick={onConfirm} | ||
| 66 | class={variant === 'destructive' ? 'bg-destructive text-white hover:bg-destructive/80' : ''} | ||
| 67 | > | ||
| 68 | {confirmText} | ||
| 69 | </AlertDialog.Action> | ||
| 70 | </AlertDialog.Footer> | ||
| 71 | </AlertDialog.Content> | ||
| 72 | </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 @@ | |||
| 1 | <script lang="ts"> | ||
| 2 | import * as Dialog from '$lib/components/ui/dialog'; | ||
| 3 | import { ConversationSelection } from '$lib/components/app'; | ||
| 4 | |||
| 5 | interface Props { | ||
| 6 | conversations: DatabaseConversation[]; | ||
| 7 | messageCountMap?: Map<string, number>; | ||
| 8 | mode: 'export' | 'import'; | ||
| 9 | onCancel: () => void; | ||
| 10 | onConfirm: (selectedConversations: DatabaseConversation[]) => void; | ||
| 11 | open?: boolean; | ||
| 12 | } | ||
| 13 | |||
| 14 | let { | ||
| 15 | conversations, | ||
| 16 | messageCountMap = new Map(), | ||
| 17 | mode, | ||
| 18 | onCancel, | ||
| 19 | onConfirm, | ||
| 20 | open = $bindable(false) | ||
| 21 | }: Props = $props(); | ||
| 22 | |||
| 23 | let conversationSelectionRef: ConversationSelection | undefined = $state(); | ||
| 24 | |||
| 25 | let previousOpen = $state(false); | ||
| 26 | |||
| 27 | $effect(() => { | ||
| 28 | if (open && !previousOpen && conversationSelectionRef) { | ||
| 29 | conversationSelectionRef.reset(); | ||
| 30 | } else if (!open && previousOpen) { | ||
| 31 | onCancel(); | ||
| 32 | } | ||
| 33 | |||
| 34 | previousOpen = open; | ||
| 35 | }); | ||
| 36 | </script> | ||
| 37 | |||
| 38 | <Dialog.Root bind:open> | ||
| 39 | <Dialog.Portal> | ||
| 40 | <Dialog.Overlay class="z-[1000000]" /> | ||
| 41 | |||
| 42 | <Dialog.Content class="z-[1000001] max-w-2xl"> | ||
| 43 | <Dialog.Header> | ||
| 44 | <Dialog.Title> | ||
| 45 | Select Conversations to {mode === 'export' ? 'Export' : 'Import'} | ||
| 46 | </Dialog.Title> | ||
| 47 | <Dialog.Description> | ||
| 48 | {#if mode === 'export'} | ||
| 49 | Choose which conversations you want to export. Selected conversations will be downloaded | ||
| 50 | as a JSON file. | ||
| 51 | {:else} | ||
| 52 | Choose which conversations you want to import. Selected conversations will be merged | ||
| 53 | with your existing conversations. | ||
| 54 | {/if} | ||
| 55 | </Dialog.Description> | ||
| 56 | </Dialog.Header> | ||
| 57 | |||
| 58 | <ConversationSelection | ||
| 59 | bind:this={conversationSelectionRef} | ||
| 60 | {conversations} | ||
| 61 | {messageCountMap} | ||
| 62 | {mode} | ||
| 63 | {onCancel} | ||
| 64 | {onConfirm} | ||
| 65 | /> | ||
| 66 | </Dialog.Content> | ||
| 67 | </Dialog.Portal> | ||
| 68 | </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 @@ | |||
| 1 | <script lang="ts"> | ||
| 2 | import * as AlertDialog from '$lib/components/ui/alert-dialog'; | ||
| 3 | import { Button } from '$lib/components/ui/button'; | ||
| 4 | |||
| 5 | interface Props { | ||
| 6 | open: boolean; | ||
| 7 | currentTitle: string; | ||
| 8 | newTitle: string; | ||
| 9 | onConfirm: () => void; | ||
| 10 | onCancel: () => void; | ||
| 11 | } | ||
| 12 | |||
| 13 | let { open = $bindable(), currentTitle, newTitle, onConfirm, onCancel }: Props = $props(); | ||
| 14 | </script> | ||
| 15 | |||
| 16 | <AlertDialog.Root bind:open> | ||
| 17 | <AlertDialog.Content> | ||
| 18 | <AlertDialog.Header> | ||
| 19 | <AlertDialog.Title>Update Conversation Title?</AlertDialog.Title> | ||
| 20 | |||
| 21 | <AlertDialog.Description> | ||
| 22 | Do you want to update the conversation title to match the first message content? | ||
| 23 | </AlertDialog.Description> | ||
| 24 | </AlertDialog.Header> | ||
| 25 | |||
| 26 | <div class="space-y-4 pt-2 pb-6"> | ||
| 27 | <div class="space-y-2"> | ||
| 28 | <p class="text-sm font-medium text-muted-foreground">Current title:</p> | ||
| 29 | |||
| 30 | <p class="rounded-md bg-muted/50 p-3 text-sm font-medium">{currentTitle}</p> | ||
| 31 | </div> | ||
| 32 | |||
| 33 | <div class="space-y-2"> | ||
| 34 | <p class="text-sm font-medium text-muted-foreground">New title would be:</p> | ||
| 35 | |||
| 36 | <p class="rounded-md bg-muted/50 p-3 text-sm font-medium">{newTitle}</p> | ||
| 37 | </div> | ||
| 38 | </div> | ||
| 39 | |||
| 40 | <AlertDialog.Footer> | ||
| 41 | <Button variant="outline" onclick={onCancel}>Keep Current Title</Button> | ||
| 42 | |||
| 43 | <Button onclick={onConfirm}>Update Title</Button> | ||
| 44 | </AlertDialog.Footer> | ||
| 45 | </AlertDialog.Content> | ||
| 46 | </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 @@ | |||
| 1 | <script lang="ts"> | ||
| 2 | import * as AlertDialog from '$lib/components/ui/alert-dialog'; | ||
| 3 | import { FileX } from '@lucide/svelte'; | ||
| 4 | |||
| 5 | interface Props { | ||
| 6 | open: boolean; | ||
| 7 | emptyFiles: string[]; | ||
| 8 | onOpenChange?: (open: boolean) => void; | ||
| 9 | } | ||
| 10 | |||
| 11 | let { open = $bindable(), emptyFiles, onOpenChange }: Props = $props(); | ||
| 12 | |||
| 13 | function handleOpenChange(newOpen: boolean) { | ||
| 14 | open = newOpen; | ||
| 15 | onOpenChange?.(newOpen); | ||
| 16 | } | ||
| 17 | </script> | ||
| 18 | |||
| 19 | <AlertDialog.Root {open} onOpenChange={handleOpenChange}> | ||
| 20 | <AlertDialog.Content> | ||
| 21 | <AlertDialog.Header> | ||
| 22 | <AlertDialog.Title class="flex items-center gap-2"> | ||
| 23 | <FileX class="h-5 w-5 text-destructive" /> | ||
| 24 | |||
| 25 | Empty Files Detected | ||
| 26 | </AlertDialog.Title> | ||
| 27 | |||
| 28 | <AlertDialog.Description> | ||
| 29 | The following files are empty and have been removed from your attachments: | ||
| 30 | </AlertDialog.Description> | ||
| 31 | </AlertDialog.Header> | ||
| 32 | |||
| 33 | <div class="space-y-3 text-sm"> | ||
| 34 | <div class="rounded-lg bg-muted p-3"> | ||
| 35 | <div class="mb-2 font-medium">Empty Files:</div> | ||
| 36 | |||
| 37 | <ul class="list-inside list-disc space-y-1 text-muted-foreground"> | ||
| 38 | {#each emptyFiles as fileName (fileName)} | ||
| 39 | <li class="font-mono text-sm">{fileName}</li> | ||
| 40 | {/each} | ||
| 41 | </ul> | ||
| 42 | </div> | ||
| 43 | |||
| 44 | <div> | ||
| 45 | <div class="mb-2 font-medium">What happened:</div> | ||
| 46 | |||
| 47 | <ul class="list-inside list-disc space-y-1 text-muted-foreground"> | ||
| 48 | <li>Empty files cannot be processed or sent to the AI model</li> | ||
| 49 | |||
| 50 | <li>These files have been automatically removed from your attachments</li> | ||
| 51 | |||
| 52 | <li>You can try uploading files with content instead</li> | ||
| 53 | </ul> | ||
| 54 | </div> | ||
| 55 | </div> | ||
| 56 | |||
| 57 | <AlertDialog.Footer> | ||
| 58 | <AlertDialog.Action onclick={() => handleOpenChange(false)}>Got it</AlertDialog.Action> | ||
| 59 | </AlertDialog.Footer> | ||
| 60 | </AlertDialog.Content> | ||
| 61 | </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 @@ | |||
| 1 | <script lang="ts"> | ||
| 2 | import * as Dialog from '$lib/components/ui/dialog'; | ||
| 3 | import * as Table from '$lib/components/ui/table'; | ||
| 4 | import { BadgeModality, CopyToClipboardIcon } from '$lib/components/app'; | ||
| 5 | import { serverStore } from '$lib/stores/server.svelte'; | ||
| 6 | import { modelsStore, modelOptions, modelsLoading } from '$lib/stores/models.svelte'; | ||
| 7 | import { formatFileSize, formatParameters, formatNumber } from '$lib/utils'; | ||
| 8 | |||
| 9 | interface Props { | ||
| 10 | open?: boolean; | ||
| 11 | onOpenChange?: (open: boolean) => void; | ||
| 12 | } | ||
| 13 | |||
| 14 | let { open = $bindable(), onOpenChange }: Props = $props(); | ||
| 15 | |||
| 16 | let serverProps = $derived(serverStore.props); | ||
| 17 | let modelName = $derived(modelsStore.singleModelName); | ||
| 18 | let models = $derived(modelOptions()); | ||
| 19 | let isLoadingModels = $derived(modelsLoading()); | ||
| 20 | |||
| 21 | // Get the first model for single-model mode display | ||
| 22 | let firstModel = $derived(models[0] ?? null); | ||
| 23 | |||
| 24 | // Get modalities from modelStore using the model ID from the first model | ||
| 25 | let modalities = $derived.by(() => { | ||
| 26 | if (!firstModel?.id) return []; | ||
| 27 | return modelsStore.getModelModalitiesArray(firstModel.id); | ||
| 28 | }); | ||
| 29 | |||
| 30 | // Ensure models are fetched when dialog opens | ||
| 31 | $effect(() => { | ||
| 32 | if (open && models.length === 0) { | ||
| 33 | modelsStore.fetch(); | ||
| 34 | } | ||
| 35 | }); | ||
| 36 | </script> | ||
| 37 | |||
| 38 | <Dialog.Root bind:open {onOpenChange}> | ||
| 39 | <Dialog.Content class="@container z-9999 !max-w-[60rem] max-w-full"> | ||
| 40 | <style> | ||
| 41 | @container (max-width: 56rem) { | ||
| 42 | .resizable-text-container { | ||
| 43 | max-width: calc(100vw - var(--threshold)); | ||
| 44 | } | ||
| 45 | } | ||
| 46 | </style> | ||
| 47 | |||
| 48 | <Dialog.Header> | ||
| 49 | <Dialog.Title>Model Information</Dialog.Title> | ||
| 50 | <Dialog.Description>Current model details and capabilities</Dialog.Description> | ||
| 51 | </Dialog.Header> | ||
| 52 | |||
| 53 | <div class="space-y-6 py-4"> | ||
| 54 | {#if isLoadingModels} | ||
| 55 | <div class="flex items-center justify-center py-8"> | ||
| 56 | <div class="text-sm text-muted-foreground">Loading model information...</div> | ||
| 57 | </div> | ||
| 58 | {:else if firstModel} | ||
| 59 | {@const modelMeta = firstModel.meta} | ||
| 60 | |||
| 61 | {#if serverProps} | ||
| 62 | <Table.Root> | ||
| 63 | <Table.Header> | ||
| 64 | <Table.Row> | ||
| 65 | <Table.Head class="w-[10rem]">Model</Table.Head> | ||
| 66 | |||
| 67 | <Table.Head> | ||
| 68 | <div class="inline-flex items-center gap-2"> | ||
| 69 | <span | ||
| 70 | class="resizable-text-container min-w-0 flex-1 truncate" | ||
| 71 | style:--threshold="12rem" | ||
| 72 | > | ||
| 73 | {modelName} | ||
| 74 | </span> | ||
| 75 | |||
| 76 | <CopyToClipboardIcon | ||
| 77 | text={modelName || ''} | ||
| 78 | canCopy={!!modelName} | ||
| 79 | ariaLabel="Copy model name to clipboard" | ||
| 80 | /> | ||
| 81 | </div> | ||
| 82 | </Table.Head> | ||
| 83 | </Table.Row> | ||
| 84 | </Table.Header> | ||
| 85 | <Table.Body> | ||
| 86 | <!-- Model Path --> | ||
| 87 | <Table.Row> | ||
| 88 | <Table.Cell class="h-10 align-middle font-medium">File Path</Table.Cell> | ||
| 89 | |||
| 90 | <Table.Cell | ||
| 91 | class="inline-flex h-10 items-center gap-2 align-middle font-mono text-xs" | ||
| 92 | > | ||
| 93 | <span | ||
| 94 | class="resizable-text-container min-w-0 flex-1 truncate" | ||
| 95 | style:--threshold="14rem" | ||
| 96 | > | ||
| 97 | {serverProps.model_path} | ||
| 98 | </span> | ||
| 99 | |||
| 100 | <CopyToClipboardIcon | ||
| 101 | text={serverProps.model_path} | ||
| 102 | ariaLabel="Copy model path to clipboard" | ||
| 103 | /> | ||
| 104 | </Table.Cell> | ||
| 105 | </Table.Row> | ||
| 106 | |||
| 107 | <!-- Context Size --> | ||
| 108 | <Table.Row> | ||
| 109 | <Table.Cell class="h-10 align-middle font-medium">Context Size</Table.Cell> | ||
| 110 | <Table.Cell | ||
| 111 | >{formatNumber(serverProps.default_generation_settings.n_ctx)} tokens</Table.Cell | ||
| 112 | > | ||
| 113 | </Table.Row> | ||
| 114 | |||
| 115 | <!-- Training Context --> | ||
| 116 | {#if modelMeta?.n_ctx_train} | ||
| 117 | <Table.Row> | ||
| 118 | <Table.Cell class="h-10 align-middle font-medium">Training Context</Table.Cell> | ||
| 119 | <Table.Cell>{formatNumber(modelMeta.n_ctx_train)} tokens</Table.Cell> | ||
| 120 | </Table.Row> | ||
| 121 | {/if} | ||
| 122 | |||
| 123 | <!-- Model Size --> | ||
| 124 | {#if modelMeta?.size} | ||
| 125 | <Table.Row> | ||
| 126 | <Table.Cell class="h-10 align-middle font-medium">Model Size</Table.Cell> | ||
| 127 | <Table.Cell>{formatFileSize(modelMeta.size)}</Table.Cell> | ||
| 128 | </Table.Row> | ||
| 129 | {/if} | ||
| 130 | |||
| 131 | <!-- Parameters --> | ||
| 132 | {#if modelMeta?.n_params} | ||
| 133 | <Table.Row> | ||
| 134 | <Table.Cell class="h-10 align-middle font-medium">Parameters</Table.Cell> | ||
| 135 | <Table.Cell>{formatParameters(modelMeta.n_params)}</Table.Cell> | ||
| 136 | </Table.Row> | ||
| 137 | {/if} | ||
| 138 | |||
| 139 | <!-- Embedding Size --> | ||
| 140 | {#if modelMeta?.n_embd} | ||
| 141 | <Table.Row> | ||
| 142 | <Table.Cell class="align-middle font-medium">Embedding Size</Table.Cell> | ||
| 143 | <Table.Cell>{formatNumber(modelMeta.n_embd)}</Table.Cell> | ||
| 144 | </Table.Row> | ||
| 145 | {/if} | ||
| 146 | |||
| 147 | <!-- Vocabulary Size --> | ||
| 148 | {#if modelMeta?.n_vocab} | ||
| 149 | <Table.Row> | ||
| 150 | <Table.Cell class="align-middle font-medium">Vocabulary Size</Table.Cell> | ||
| 151 | <Table.Cell>{formatNumber(modelMeta.n_vocab)} tokens</Table.Cell> | ||
| 152 | </Table.Row> | ||
| 153 | {/if} | ||
| 154 | |||
| 155 | <!-- Vocabulary Type --> | ||
| 156 | {#if modelMeta?.vocab_type} | ||
| 157 | <Table.Row> | ||
| 158 | <Table.Cell class="align-middle font-medium">Vocabulary Type</Table.Cell> | ||
| 159 | <Table.Cell class="align-middle capitalize">{modelMeta.vocab_type}</Table.Cell> | ||
| 160 | </Table.Row> | ||
| 161 | {/if} | ||
| 162 | |||
| 163 | <!-- Total Slots --> | ||
| 164 | <Table.Row> | ||
| 165 | <Table.Cell class="align-middle font-medium">Parallel Slots</Table.Cell> | ||
| 166 | <Table.Cell>{serverProps.total_slots}</Table.Cell> | ||
| 167 | </Table.Row> | ||
| 168 | |||
| 169 | <!-- Modalities --> | ||
| 170 | {#if modalities.length > 0} | ||
| 171 | <Table.Row> | ||
| 172 | <Table.Cell class="align-middle font-medium">Modalities</Table.Cell> | ||
| 173 | <Table.Cell> | ||
| 174 | <div class="flex flex-wrap gap-1"> | ||
| 175 | <BadgeModality {modalities} /> | ||
| 176 | </div> | ||
| 177 | </Table.Cell> | ||
| 178 | </Table.Row> | ||
| 179 | {/if} | ||
| 180 | |||
| 181 | <!-- Build Info --> | ||
| 182 | <Table.Row> | ||
| 183 | <Table.Cell class="align-middle font-medium">Build Info</Table.Cell> | ||
| 184 | <Table.Cell class="align-middle font-mono text-xs" | ||
| 185 | >{serverProps.build_info}</Table.Cell | ||
| 186 | > | ||
| 187 | </Table.Row> | ||
| 188 | |||
| 189 | <!-- Chat Template --> | ||
| 190 | {#if serverProps.chat_template} | ||
| 191 | <Table.Row> | ||
| 192 | <Table.Cell class="align-middle font-medium">Chat Template</Table.Cell> | ||
| 193 | <Table.Cell class="py-10"> | ||
| 194 | <div class="max-h-120 overflow-y-auto rounded-md bg-muted p-4"> | ||
| 195 | <pre | ||
| 196 | class="font-mono text-xs whitespace-pre-wrap">{serverProps.chat_template}</pre> | ||
| 197 | </div> | ||
| 198 | </Table.Cell> | ||
| 199 | </Table.Row> | ||
| 200 | {/if} | ||
| 201 | </Table.Body> | ||
| 202 | </Table.Root> | ||
| 203 | {/if} | ||
| 204 | {:else if !isLoadingModels} | ||
| 205 | <div class="flex items-center justify-center py-8"> | ||
| 206 | <div class="text-sm text-muted-foreground">No model information available</div> | ||
| 207 | </div> | ||
| 208 | {/if} | ||
| 209 | </div> | ||
| 210 | </Dialog.Content> | ||
| 211 | </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 @@ | |||
| 1 | <script lang="ts"> | ||
| 2 | import * as AlertDialog from '$lib/components/ui/alert-dialog'; | ||
| 3 | import { AlertTriangle, ArrowRight } from '@lucide/svelte'; | ||
| 4 | import { goto } from '$app/navigation'; | ||
| 5 | import { page } from '$app/state'; | ||
| 6 | |||
| 7 | interface Props { | ||
| 8 | open: boolean; | ||
| 9 | modelName: string; | ||
| 10 | availableModels?: string[]; | ||
| 11 | onOpenChange?: (open: boolean) => void; | ||
| 12 | } | ||
| 13 | |||
| 14 | let { open = $bindable(), modelName, availableModels = [], onOpenChange }: Props = $props(); | ||
| 15 | |||
| 16 | function handleOpenChange(newOpen: boolean) { | ||
| 17 | open = newOpen; | ||
| 18 | onOpenChange?.(newOpen); | ||
| 19 | } | ||
| 20 | |||
| 21 | function handleSelectModel(model: string) { | ||
| 22 | // Build URL with selected model, preserving other params | ||
| 23 | const url = new URL(page.url); | ||
| 24 | url.searchParams.set('model', model); | ||
| 25 | |||
| 26 | handleOpenChange(false); | ||
| 27 | goto(url.toString()); | ||
| 28 | } | ||
| 29 | </script> | ||
| 30 | |||
| 31 | <AlertDialog.Root {open} onOpenChange={handleOpenChange}> | ||
| 32 | <AlertDialog.Content class="max-w-lg"> | ||
| 33 | <AlertDialog.Header> | ||
| 34 | <AlertDialog.Title class="flex items-center gap-2"> | ||
| 35 | <AlertTriangle class="h-5 w-5 text-amber-500" /> | ||
| 36 | Model Not Available | ||
| 37 | </AlertDialog.Title> | ||
| 38 | |||
| 39 | <AlertDialog.Description> | ||
| 40 | The requested model could not be found. Select an available model to continue. | ||
| 41 | </AlertDialog.Description> | ||
| 42 | </AlertDialog.Header> | ||
| 43 | |||
| 44 | <div class="space-y-3"> | ||
| 45 | <div class="rounded-lg border border-amber-500/40 bg-amber-500/10 px-4 py-3 text-sm"> | ||
| 46 | <p class="font-medium text-amber-600 dark:text-amber-400"> | ||
| 47 | Requested: <code class="rounded bg-amber-500/20 px-1.5 py-0.5">{modelName}</code> | ||
| 48 | </p> | ||
| 49 | </div> | ||
| 50 | |||
| 51 | {#if availableModels.length > 0} | ||
| 52 | <div class="text-sm"> | ||
| 53 | <p class="mb-2 font-medium text-muted-foreground">Select an available model:</p> | ||
| 54 | <div class="max-h-48 space-y-1 overflow-y-auto rounded-md border p-1"> | ||
| 55 | {#each availableModels as model (model)} | ||
| 56 | <button | ||
| 57 | type="button" | ||
| 58 | 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" | ||
| 59 | onclick={() => handleSelectModel(model)} | ||
| 60 | > | ||
| 61 | <span class="min-w-0 truncate font-mono text-xs">{model}</span> | ||
| 62 | <ArrowRight | ||
| 63 | class="h-4 w-4 shrink-0 text-muted-foreground opacity-0 transition-opacity group-hover:opacity-100" | ||
| 64 | /> | ||
| 65 | </button> | ||
| 66 | {/each} | ||
| 67 | </div> | ||
| 68 | </div> | ||
| 69 | {/if} | ||
| 70 | </div> | ||
| 71 | |||
| 72 | <AlertDialog.Footer> | ||
| 73 | <AlertDialog.Action onclick={() => handleOpenChange(false)}>Cancel</AlertDialog.Action> | ||
| 74 | </AlertDialog.Footer> | ||
| 75 | </AlertDialog.Content> | ||
| 76 | </AlertDialog.Root> | ||
