-
+

llama.cpp

@@ -340,9 +344,13 @@
+ {#if serverWarning()} + + {/if} +
- + File Upload Error @@ -369,7 +377,7 @@ -
+
{#if fileErrorData.generallyUnsupported.length > 0}

Unsupported File Types

@@ -390,8 +398,6 @@ {#if fileErrorData.modalityUnsupported.length > 0}
-

Model Compatibility Issues

-
{#each fileErrorData.modalityUnsupported as file (file.name)}
@@ -407,14 +413,14 @@
{/if} +
-
-

This model supports:

+
+

This model supports:

-

- {fileErrorData.supportedTypes.join(', ')} -

-
+

+ {fileErrorData.supportedTypes.join(', ')} +

@@ -448,6 +454,13 @@ }} /> + + diff --git a/tools/server/webui/src/lib/components/app/dialogs/ChatErrorDialog.svelte b/tools/server/webui/src/lib/components/app/dialogs/ChatErrorDialog.svelte new file mode 100644 index 0000000000..8ecb58905a --- /dev/null +++ b/tools/server/webui/src/lib/components/app/dialogs/ChatErrorDialog.svelte @@ -0,0 +1,60 @@ + + + + + + + {#if isTimeout} + + {:else} + + {/if} + + {title} + + + + {description} + + + +
+

{message}

+
+ + + handleOpenChange(false)}>Close + +
+
diff --git a/tools/server/webui/src/lib/components/app/dialogs/MaximumContextAlertDialog.svelte b/tools/server/webui/src/lib/components/app/dialogs/MaximumContextAlertDialog.svelte deleted file mode 100644 index cea0f631a2..0000000000 --- a/tools/server/webui/src/lib/components/app/dialogs/MaximumContextAlertDialog.svelte +++ /dev/null @@ -1,66 +0,0 @@ - - - !open && clearMaxContextError()} -> - - - - - - Message Too Long - - - - Your message exceeds the model's context window and cannot be processed. - - - - {#if maxContextError()} -
-
-
Token Usage:
- -
-
- Estimated tokens: - - - {maxContextError()?.estimatedTokens.toLocaleString()} - -
- -
- Context window: - - - {maxContextError()?.maxContext.toLocaleString()} - -
-
-
- -
-
Suggestions:
- -
    -
  • Shorten your message
  • - -
  • Remove some file attachments
  • - -
  • Start a new conversation
  • -
-
-
- {/if} - - - clearMaxContextError()}>Got it - -
-
diff --git a/tools/server/webui/src/lib/components/app/index.ts b/tools/server/webui/src/lib/components/app/index.ts index 2f559bd623..a695f99747 100644 --- a/tools/server/webui/src/lib/components/app/index.ts +++ b/tools/server/webui/src/lib/components/app/index.ts @@ -2,12 +2,14 @@ export { default as ChatAttachmentsList } from './chat/ChatAttachments/ChatAttac export { default as ChatAttachmentFilePreview } from './chat/ChatAttachments/ChatAttachmentFilePreview.svelte'; export { default as ChatAttachmentImagePreview } from './chat/ChatAttachments/ChatAttachmentImagePreview.svelte'; export { default as ChatAttachmentPreviewDialog } from './chat/ChatAttachments/ChatAttachmentPreviewDialog.svelte'; +export { default as ChatAttachmentsViewAllDialog } from './chat/ChatAttachments/ChatAttachmentsViewAllDialog.svelte'; export { default as ChatForm } from './chat/ChatForm/ChatForm.svelte'; export { default as ChatFormTextarea } from './chat/ChatForm/ChatFormTextarea.svelte'; export { default as ChatFormActions } from './chat/ChatForm/ChatFormActions.svelte'; export { default as ChatFormActionFileAttachments } from './chat/ChatForm/ChatFormActionFileAttachments.svelte'; export { default as ChatFormActionRecord } from './chat/ChatForm/ChatFormActionRecord.svelte'; +export { default as ChatFormModelSelector } from './chat/ChatForm/ChatFormModelSelector.svelte'; export { default as ChatFormHelperText } from './chat/ChatForm/ChatFormHelperText.svelte'; export { default as ChatFormFileInputInvisible } from './chat/ChatForm/ChatFormFileInputInvisible.svelte'; @@ -19,26 +21,30 @@ export { default as MessageBranchingControls } from './chat/ChatMessages/ChatMes export { default as ChatProcessingInfo } from './chat/ChatProcessingInfo.svelte'; export { default as ChatScreenHeader } from './chat/ChatScreen/ChatScreenHeader.svelte'; +export { default as ChatScreenWarning } from './chat/ChatScreen/ChatScreenWarning.svelte'; export { default as ChatScreen } from './chat/ChatScreen/ChatScreen.svelte'; export { default as ChatSettingsDialog } from './chat/ChatSettings/ChatSettingsDialog.svelte'; export { default as ChatSettingsFooter } from './chat/ChatSettings/ChatSettingsFooter.svelte'; export { default as ChatSettingsFields } from './chat/ChatSettings/ChatSettingsFields.svelte'; +export { default as ImportExportTab } from './chat/ChatSettings/ImportExportTab.svelte'; +export { default as ConversationSelectionDialog } from './chat/ChatSettings/ConversationSelectionDialog.svelte'; +export { default as ParameterSourceIndicator } from './chat/ChatSettings/ParameterSourceIndicator.svelte'; export { default as ChatSidebar } from './chat/ChatSidebar/ChatSidebar.svelte'; export { default as ChatSidebarConversationItem } from './chat/ChatSidebar/ChatSidebarConversationItem.svelte'; export { default as ChatSidebarSearch } from './chat/ChatSidebar/ChatSidebarSearch.svelte'; - +export { default as ChatErrorDialog } from './dialogs/ChatErrorDialog.svelte'; export { default as EmptyFileAlertDialog } from './dialogs/EmptyFileAlertDialog.svelte'; export { default as ConversationTitleUpdateDialog } from './dialogs/ConversationTitleUpdateDialog.svelte'; -export { default as MaximumContextAlertDialog } from './dialogs/MaximumContextAlertDialog.svelte'; - export { default as KeyboardShortcutInfo } from './misc/KeyboardShortcutInfo.svelte'; export { default as MarkdownContent } from './misc/MarkdownContent.svelte'; +export { default as RemoveButton } from './misc/RemoveButton.svelte'; + export { default as ServerStatus } from './server/ServerStatus.svelte'; export { default as ServerErrorSplash } from './server/ServerErrorSplash.svelte'; export { default as ServerLoadingSplash } from './server/ServerLoadingSplash.svelte'; diff --git a/tools/server/webui/src/lib/components/app/misc/ActionButton.svelte b/tools/server/webui/src/lib/components/app/misc/ActionButton.svelte index 2369a31ce1..11c4679a6e 100644 --- a/tools/server/webui/src/lib/components/app/misc/ActionButton.svelte +++ b/tools/server/webui/src/lib/components/app/misc/ActionButton.svelte @@ -34,7 +34,7 @@ {size} {disabled} {onclick} - class="h-6 w-6 p-0 {className}" + class="h-6 w-6 p-0 {className} flex" aria-label={ariaLabel || tooltip} > {@const IconComponent = icon} diff --git a/tools/server/webui/src/lib/components/app/misc/ActionDropdown.svelte b/tools/server/webui/src/lib/components/app/misc/ActionDropdown.svelte index e06bbc5086..da29e2584f 100644 --- a/tools/server/webui/src/lib/components/app/misc/ActionDropdown.svelte +++ b/tools/server/webui/src/lib/components/app/misc/ActionDropdown.svelte @@ -37,6 +37,7 @@ e.stopPropagation()} > {#if triggerTooltip} @@ -53,7 +54,7 @@ {/if} - + {#each actions as action, index (action.label)} {#if action.separator && index > 0} diff --git a/tools/server/webui/src/lib/components/app/misc/CodePreviewDialog.svelte b/tools/server/webui/src/lib/components/app/misc/CodePreviewDialog.svelte new file mode 100644 index 0000000000..702519f9ff --- /dev/null +++ b/tools/server/webui/src/lib/components/app/misc/CodePreviewDialog.svelte @@ -0,0 +1,93 @@ + + + + + + + + + + + + Close preview + + + + + + diff --git a/tools/server/webui/src/lib/components/app/misc/MarkdownContent.svelte b/tools/server/webui/src/lib/components/app/misc/MarkdownContent.svelte index c70583e671..7e83d30f13 100644 --- a/tools/server/webui/src/lib/components/app/misc/MarkdownContent.svelte +++ b/tools/server/webui/src/lib/components/app/misc/MarkdownContent.svelte @@ -8,8 +8,15 @@ import rehypeKatex from 'rehype-katex'; import rehypeStringify from 'rehype-stringify'; import { copyCodeToClipboard } from '$lib/utils/copy'; - import 'highlight.js/styles/github-dark.css'; - import 'katex/dist/katex.min.css'; + import { preprocessLaTeX } from '$lib/utils/latex-protection'; + import { browser } from '$app/environment'; + import '$styles/katex-custom.scss'; + + import githubDarkCss from 'highlight.js/styles/github-dark.css?inline'; + import githubLightCss from 'highlight.js/styles/github.css?inline'; + import { mode } from 'mode-watcher'; + import { remarkLiteralHtml } from '$lib/markdown/literal-html'; + import CodePreviewDialog from './CodePreviewDialog.svelte'; interface Props { content: string; @@ -20,42 +27,88 @@ let containerRef = $state(); let processedHtml = $state(''); + let previewDialogOpen = $state(false); + let previewCode = $state(''); + let previewLanguage = $state('text'); + + function loadHighlightTheme(isDark: boolean) { + if (!browser) return; + + const existingThemes = document.querySelectorAll('style[data-highlight-theme]'); + existingThemes.forEach((style) => style.remove()); + + const style = document.createElement('style'); + style.setAttribute('data-highlight-theme', 'true'); + style.textContent = isDark ? githubDarkCss : githubLightCss; + + document.head.appendChild(style); + } + + $effect(() => { + const currentMode = mode.current; + const isDark = currentMode === 'dark'; + + loadHighlightTheme(isDark); + }); let processor = $derived(() => { return remark() .use(remarkGfm) // GitHub Flavored Markdown .use(remarkMath) // Parse $inline$ and $$block$$ math .use(remarkBreaks) // Convert line breaks to
- .use(remarkRehype) // Convert to rehype (HTML AST) + .use(remarkLiteralHtml) // Treat raw HTML as literal text with preserved indentation + .use(remarkRehype) // Convert Markdown AST to rehype .use(rehypeKatex) // Render math using KaTeX .use(rehypeHighlight) // Add syntax highlighting .use(rehypeStringify); // Convert to HTML string }); function enhanceLinks(html: string): string { + if (!html.includes(' - `; + + `; + + const actions = document.createElement('div'); + actions.className = 'code-block-actions'; + + actions.appendChild(copyButton); + + if (language.toLowerCase() === 'html') { + const previewButton = document.createElement('button'); + previewButton.className = 'preview-code-btn'; + previewButton.setAttribute('data-code-id', codeId); + previewButton.setAttribute('title', 'Preview code'); + previewButton.setAttribute('type', 'button'); + + previewButton.innerHTML = ` + + `; + + actions.appendChild(previewButton); + } header.appendChild(languageLabel); - header.appendChild(copyButton); + header.appendChild(actions); wrapper.appendChild(header); const clonedPre = pre.cloneNode(true) as HTMLElement; @@ -103,12 +174,13 @@ pre.parentNode?.replaceChild(wrapper, pre); } - return tempDiv.innerHTML; + return mutated ? tempDiv.innerHTML : html; } async function processMarkdown(text: string): Promise { try { - const result = await processor().process(text); + let normalized = preprocessLaTeX(text); + const result = await processor().process(normalized); const html = String(result); const enhancedLinks = enhanceLinks(html); @@ -121,49 +193,105 @@ } } - function setupCopyButtons() { + function getCodeInfoFromTarget(target: HTMLElement) { + const wrapper = target.closest('.code-block-wrapper'); + + if (!wrapper) { + console.error('No wrapper found'); + return null; + } + + const codeElement = wrapper.querySelector('code[data-code-id]'); + + if (!codeElement) { + console.error('No code element found in wrapper'); + return null; + } + + const rawCode = codeElement.getAttribute('data-raw-code'); + + if (rawCode === null) { + console.error('No raw code found'); + return null; + } + + const languageLabel = wrapper.querySelector('.code-language'); + const language = languageLabel?.textContent?.trim() || 'text'; + + return { rawCode, language }; + } + + async function handleCopyClick(event: Event) { + event.preventDefault(); + event.stopPropagation(); + + const target = event.currentTarget as HTMLButtonElement | null; + + if (!target) { + return; + } + + const info = getCodeInfoFromTarget(target); + + if (!info) { + return; + } + + try { + await copyCodeToClipboard(info.rawCode); + } catch (error) { + console.error('Failed to copy code:', error); + } + } + + function handlePreviewClick(event: Event) { + event.preventDefault(); + event.stopPropagation(); + + const target = event.currentTarget as HTMLButtonElement | null; + + if (!target) { + return; + } + + const info = getCodeInfoFromTarget(target); + + if (!info) { + return; + } + + previewCode = info.rawCode; + previewLanguage = info.language; + previewDialogOpen = true; + } + + function setupCodeBlockActions() { if (!containerRef) return; - const copyButtons = containerRef.querySelectorAll('.copy-code-btn'); + const wrappers = containerRef.querySelectorAll('.code-block-wrapper'); - for (const button of copyButtons) { - button.addEventListener('click', async (e) => { - e.preventDefault(); - e.stopPropagation(); + for (const wrapper of wrappers) { + const copyButton = wrapper.querySelector('.copy-code-btn'); + const previewButton = wrapper.querySelector('.preview-code-btn'); - const target = e.currentTarget as HTMLButtonElement; - const codeId = target.getAttribute('data-code-id'); + if (copyButton && copyButton.dataset.listenerBound !== 'true') { + copyButton.dataset.listenerBound = 'true'; + copyButton.addEventListener('click', handleCopyClick); + } - if (!codeId) { - console.error('No code ID found on button'); - return; - } + if (previewButton && previewButton.dataset.listenerBound !== 'true') { + previewButton.dataset.listenerBound = 'true'; + previewButton.addEventListener('click', handlePreviewClick); + } + } + } - // Find the code element within the same wrapper - const wrapper = target.closest('.code-block-wrapper'); - if (!wrapper) { - console.error('No wrapper found'); - return; - } + function handlePreviewDialogOpenChange(open: boolean) { + previewDialogOpen = open; - const codeElement = wrapper.querySelector('code[data-code-id]'); - if (!codeElement) { - console.error('No code element found in wrapper'); - return; - } - - const rawCode = codeElement.getAttribute('data-raw-code'); - if (!rawCode) { - console.error('No raw code found'); - return; - } - - try { - await copyCodeToClipboard(rawCode); - } catch (error) { - console.error('Failed to copy code:', error); - } - }); + if (!open) { + previewCode = ''; + previewLanguage = 'text'; } } @@ -184,7 +312,7 @@ $effect(() => { if (containerRef && processedHtml) { - setupCopyButtons(); + setupCodeBlockActions(); } }); @@ -194,9 +322,16 @@ {@html processedHtml}
+ +