diff --git a/tools/server/webui/package-lock.json b/tools/server/webui/package-lock.json
index 4af5e86ab9..9c1c2499cf 100644
--- a/tools/server/webui/package-lock.json
+++ b/tools/server/webui/package-lock.json
@@ -64,7 +64,7 @@
"svelte": "^5.0.0",
"svelte-check": "^4.0.0",
"tailwind-merge": "^3.3.1",
- "tailwind-variants": "^1.0.0",
+ "tailwind-variants": "^3.2.2",
"tailwindcss": "^4.0.0",
"tw-animate-css": "^1.3.5",
"typescript": "^5.0.0",
@@ -8324,31 +8324,23 @@
}
},
"node_modules/tailwind-variants": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/tailwind-variants/-/tailwind-variants-1.0.0.tgz",
- "integrity": "sha512-2WSbv4ulEEyuBKomOunut65D8UZwxrHoRfYnxGcQNnHqlSCp2+B7Yz2W+yrNDrxRodOXtGD/1oCcKGNBnUqMqA==",
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/tailwind-variants/-/tailwind-variants-3.2.2.tgz",
+ "integrity": "sha512-Mi4kHeMTLvKlM98XPnK+7HoBPmf4gygdFmqQPaDivc3DpYS6aIY6KiG/PgThrGvii5YZJqRsPz0aPyhoFzmZgg==",
"dev": true,
"license": "MIT",
- "dependencies": {
- "tailwind-merge": "3.0.2"
- },
"engines": {
"node": ">=16.x",
"pnpm": ">=7.x"
},
"peerDependencies": {
+ "tailwind-merge": ">=3.0.0",
"tailwindcss": "*"
- }
- },
- "node_modules/tailwind-variants/node_modules/tailwind-merge": {
- "version": "3.0.2",
- "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.0.2.tgz",
- "integrity": "sha512-l7z+OYZ7mu3DTqrL88RiKrKIqO3NcpEO8V/Od04bNpvk0kiIFndGEoqfuzvj4yuhRkHKjRkII2z+KS2HfPcSxw==",
- "dev": true,
- "license": "MIT",
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/dcastil"
+ },
+ "peerDependenciesMeta": {
+ "tailwind-merge": {
+ "optional": true
+ }
}
},
"node_modules/tailwindcss": {
diff --git a/tools/server/webui/package.json b/tools/server/webui/package.json
index 8b88f691a4..987a7239ed 100644
--- a/tools/server/webui/package.json
+++ b/tools/server/webui/package.json
@@ -66,7 +66,7 @@
"svelte": "^5.0.0",
"svelte-check": "^4.0.0",
"tailwind-merge": "^3.3.1",
- "tailwind-variants": "^1.0.0",
+ "tailwind-variants": "^3.2.2",
"tailwindcss": "^4.0.0",
"tw-animate-css": "^1.3.5",
"typescript": "^5.0.0",
diff --git a/tools/server/webui/src/lib/components/app/chat/ChatAttachments/ChatAttachmentPreview.svelte b/tools/server/webui/src/lib/components/app/chat/ChatAttachments/ChatAttachmentPreview.svelte
index fa01075496..ea8b330d68 100644
--- a/tools/server/webui/src/lib/components/app/chat/ChatAttachments/ChatAttachmentPreview.svelte
+++ b/tools/server/webui/src/lib/components/app/chat/ChatAttachments/ChatAttachmentPreview.svelte
@@ -1,9 +1,13 @@
+
+
+
{@html highlightedHtml}
+
+
+
diff --git a/tools/server/webui/src/lib/components/ui/alert/alert-description.svelte b/tools/server/webui/src/lib/components/ui/alert/alert-description.svelte
new file mode 100644
index 0000000000..440d0069d3
--- /dev/null
+++ b/tools/server/webui/src/lib/components/ui/alert/alert-description.svelte
@@ -0,0 +1,23 @@
+
+
+
+ {@render children?.()}
+
diff --git a/tools/server/webui/src/lib/components/ui/alert/alert-title.svelte b/tools/server/webui/src/lib/components/ui/alert/alert-title.svelte
new file mode 100644
index 0000000000..0721aebf12
--- /dev/null
+++ b/tools/server/webui/src/lib/components/ui/alert/alert-title.svelte
@@ -0,0 +1,20 @@
+
+
+
+ {@render children?.()}
+
diff --git a/tools/server/webui/src/lib/components/ui/alert/alert.svelte b/tools/server/webui/src/lib/components/ui/alert/alert.svelte
new file mode 100644
index 0000000000..7d79e4bc0e
--- /dev/null
+++ b/tools/server/webui/src/lib/components/ui/alert/alert.svelte
@@ -0,0 +1,44 @@
+
+
+
+
+
+ {@render children?.()}
+
diff --git a/tools/server/webui/src/lib/components/ui/alert/index.ts b/tools/server/webui/src/lib/components/ui/alert/index.ts
new file mode 100644
index 0000000000..5e0f854da6
--- /dev/null
+++ b/tools/server/webui/src/lib/components/ui/alert/index.ts
@@ -0,0 +1,14 @@
+import Root from './alert.svelte';
+import Description from './alert-description.svelte';
+import Title from './alert-title.svelte';
+export { alertVariants, type AlertVariant } from './alert.svelte';
+
+export {
+ Root,
+ Description,
+ Title,
+ //
+ Root as Alert,
+ Description as AlertDescription,
+ Title as AlertTitle
+};
diff --git a/tools/server/webui/src/lib/utils/attachment-display.ts b/tools/server/webui/src/lib/utils/attachment-display.ts
index 1fc166a881..15a8a13c0b 100644
--- a/tools/server/webui/src/lib/utils/attachment-display.ts
+++ b/tools/server/webui/src/lib/utils/attachment-display.ts
@@ -2,13 +2,26 @@ import { FileTypeCategory } from '$lib/enums';
import type { ChatAttachmentDisplayItem } from '$lib/types/chat';
import type { DatabaseMessageExtra } from '$lib/types/database';
import { isImageFile } from '$lib/utils/attachment-type';
-import { getFileTypeCategory } from '$lib/utils/file-type';
+import { getFileTypeCategory, getFileTypeCategoryByExtension } from '$lib/utils/file-type';
export interface AttachmentDisplayItemsOptions {
uploadedFiles?: ChatUploadedFile[];
attachments?: DatabaseMessageExtra[];
}
+/**
+ * Gets the file type category from an uploaded file, checking both MIME type and extension
+ */
+function getUploadedFileCategory(file: ChatUploadedFile): FileTypeCategory | null {
+ const categoryByMime = getFileTypeCategory(file.type);
+
+ if (categoryByMime) {
+ return categoryByMime;
+ }
+
+ return getFileTypeCategoryByExtension(file.name);
+}
+
/**
* Creates a unified list of display items from uploaded files and stored attachments.
* Items are returned in reverse order (newest first).
@@ -26,7 +39,7 @@ export function getAttachmentDisplayItems(
name: file.name,
size: file.size,
preview: file.preview,
- isImage: getFileTypeCategory(file.type) === FileTypeCategory.IMAGE,
+ isImage: getUploadedFileCategory(file) === FileTypeCategory.IMAGE,
uploadedFile: file,
textContent: file.textContent
});
diff --git a/tools/server/webui/src/lib/utils/attachment-type.ts b/tools/server/webui/src/lib/utils/attachment-type.ts
index 05a6e24fc9..b8ff33595d 100644
--- a/tools/server/webui/src/lib/utils/attachment-type.ts
+++ b/tools/server/webui/src/lib/utils/attachment-type.ts
@@ -1,8 +1,24 @@
import { AttachmentType, FileTypeCategory } from '$lib/enums';
-import { getFileTypeCategory } from '$lib/utils/file-type';
-import { getFileTypeLabel } from '$lib/utils/file-preview';
+import { getFileTypeCategory, getFileTypeCategoryByExtension } from '$lib/utils/file-type';
import type { DatabaseMessageExtra } from '$lib/types/database';
+/**
+ * Gets the file type category from an uploaded file, checking both MIME type and extension
+ * @param uploadedFile - The uploaded file to check
+ * @returns The file type category or null if not recognized
+ */
+function getUploadedFileCategory(uploadedFile: ChatUploadedFile): FileTypeCategory | null {
+ // First try MIME type
+ const categoryByMime = getFileTypeCategory(uploadedFile.type);
+
+ if (categoryByMime) {
+ return categoryByMime;
+ }
+
+ // Fallback to extension (browsers don't always provide correct MIME types)
+ return getFileTypeCategoryByExtension(uploadedFile.name);
+}
+
/**
* Determines if an attachment or uploaded file is a text file
* @param uploadedFile - Optional uploaded file
@@ -14,7 +30,7 @@ export function isTextFile(
uploadedFile?: ChatUploadedFile
): boolean {
if (uploadedFile) {
- return getFileTypeCategory(uploadedFile.type) === FileTypeCategory.TEXT;
+ return getUploadedFileCategory(uploadedFile) === FileTypeCategory.TEXT;
}
if (attachment) {
@@ -37,7 +53,7 @@ export function isImageFile(
uploadedFile?: ChatUploadedFile
): boolean {
if (uploadedFile) {
- return getFileTypeCategory(uploadedFile.type) === FileTypeCategory.IMAGE;
+ return getUploadedFileCategory(uploadedFile) === FileTypeCategory.IMAGE;
}
if (attachment) {
@@ -58,7 +74,7 @@ export function isPdfFile(
uploadedFile?: ChatUploadedFile
): boolean {
if (uploadedFile) {
- return uploadedFile.type === 'application/pdf';
+ return getUploadedFileCategory(uploadedFile) === FileTypeCategory.PDF;
}
if (attachment) {
@@ -79,7 +95,7 @@ export function isAudioFile(
uploadedFile?: ChatUploadedFile
): boolean {
if (uploadedFile) {
- return getFileTypeCategory(uploadedFile.type) === FileTypeCategory.AUDIO;
+ return getUploadedFileCategory(uploadedFile) === FileTypeCategory.AUDIO;
}
if (attachment) {
@@ -88,38 +104,3 @@ export function isAudioFile(
return false;
}
-
-/**
- * Gets a human-readable type label for display
- * @param uploadedFile - Optional uploaded file
- * @param attachment - Optional database attachment
- * @returns A formatted type label string
- */
-export function getAttachmentTypeLabel(
- attachment?: DatabaseMessageExtra,
- uploadedFile?: ChatUploadedFile
-): string {
- if (uploadedFile) {
- // For uploaded files, use the file type label utility
- return getFileTypeLabel(uploadedFile.type);
- }
-
- if (attachment) {
- // For attachments, convert enum to readable format
- switch (attachment.type) {
- case AttachmentType.IMAGE:
- return 'image';
- case AttachmentType.AUDIO:
- return 'audio';
- case AttachmentType.PDF:
- return 'pdf';
- case AttachmentType.TEXT:
- case AttachmentType.LEGACY_CONTEXT:
- return 'text';
- default:
- return 'unknown';
- }
- }
-
- return 'unknown';
-}
diff --git a/tools/server/webui/src/lib/utils/file-type.ts b/tools/server/webui/src/lib/utils/file-type.ts
index db38645c6e..f096b463d4 100644
--- a/tools/server/webui/src/lib/utils/file-type.ts
+++ b/tools/server/webui/src/lib/utils/file-type.ts
@@ -4,42 +4,151 @@ import {
PDF_FILE_TYPES,
TEXT_FILE_TYPES
} from '$lib/constants/supported-file-types';
-import { FileTypeCategory } from '$lib/enums';
+import {
+ FileExtensionAudio,
+ FileExtensionImage,
+ FileExtensionPdf,
+ FileExtensionText,
+ FileTypeCategory,
+ MimeTypeApplication,
+ MimeTypeAudio,
+ MimeTypeImage,
+ MimeTypeText
+} from '$lib/enums';
export function getFileTypeCategory(mimeType: string): FileTypeCategory | null {
- if (
- Object.values(IMAGE_FILE_TYPES).some((type) =>
- (type.mimeTypes as readonly string[]).includes(mimeType)
- )
- ) {
- return FileTypeCategory.IMAGE;
- }
+ switch (mimeType) {
+ // Images
+ case MimeTypeImage.JPEG:
+ case MimeTypeImage.PNG:
+ case MimeTypeImage.GIF:
+ case MimeTypeImage.WEBP:
+ case MimeTypeImage.SVG:
+ return FileTypeCategory.IMAGE;
- if (
- Object.values(AUDIO_FILE_TYPES).some((type) =>
- (type.mimeTypes as readonly string[]).includes(mimeType)
- )
- ) {
- return FileTypeCategory.AUDIO;
- }
+ // Audio
+ case MimeTypeAudio.MP3_MPEG:
+ case MimeTypeAudio.MP3:
+ case MimeTypeAudio.MP4:
+ case MimeTypeAudio.WAV:
+ case MimeTypeAudio.WEBM:
+ case MimeTypeAudio.WEBM_OPUS:
+ return FileTypeCategory.AUDIO;
- if (
- Object.values(PDF_FILE_TYPES).some((type) =>
- (type.mimeTypes as readonly string[]).includes(mimeType)
- )
- ) {
- return FileTypeCategory.PDF;
- }
+ // PDF
+ case MimeTypeApplication.PDF:
+ return FileTypeCategory.PDF;
- if (
- Object.values(TEXT_FILE_TYPES).some((type) =>
- (type.mimeTypes as readonly string[]).includes(mimeType)
- )
- ) {
- return FileTypeCategory.TEXT;
- }
+ // Text
+ case MimeTypeText.PLAIN:
+ case MimeTypeText.MARKDOWN:
+ case MimeTypeText.ASCIIDOC:
+ case MimeTypeText.JAVASCRIPT:
+ case MimeTypeText.JAVASCRIPT_APP:
+ case MimeTypeText.TYPESCRIPT:
+ case MimeTypeText.JSX:
+ case MimeTypeText.TSX:
+ case MimeTypeText.CSS:
+ case MimeTypeText.HTML:
+ case MimeTypeText.JSON:
+ case MimeTypeText.XML_TEXT:
+ case MimeTypeText.XML_APP:
+ case MimeTypeText.YAML_TEXT:
+ case MimeTypeText.YAML_APP:
+ case MimeTypeText.CSV:
+ case MimeTypeText.PYTHON:
+ case MimeTypeText.JAVA:
+ case MimeTypeText.CPP_SRC:
+ case MimeTypeText.C_SRC:
+ case MimeTypeText.C_HDR:
+ case MimeTypeText.PHP:
+ case MimeTypeText.RUBY:
+ case MimeTypeText.GO:
+ case MimeTypeText.RUST:
+ case MimeTypeText.SHELL:
+ case MimeTypeText.BAT:
+ case MimeTypeText.SQL:
+ case MimeTypeText.R:
+ case MimeTypeText.SCALA:
+ case MimeTypeText.KOTLIN:
+ case MimeTypeText.SWIFT:
+ case MimeTypeText.DART:
+ case MimeTypeText.VUE:
+ case MimeTypeText.SVELTE:
+ case MimeTypeText.LATEX:
+ case MimeTypeText.BIBTEX:
+ return FileTypeCategory.TEXT;
- return null;
+ default:
+ return null;
+ }
+}
+
+export function getFileTypeCategoryByExtension(filename: string): FileTypeCategory | null {
+ const extension = filename.toLowerCase().substring(filename.lastIndexOf('.'));
+
+ switch (extension) {
+ // Images
+ case FileExtensionImage.JPG:
+ case FileExtensionImage.JPEG:
+ case FileExtensionImage.PNG:
+ case FileExtensionImage.GIF:
+ case FileExtensionImage.WEBP:
+ case FileExtensionImage.SVG:
+ return FileTypeCategory.IMAGE;
+
+ // Audio
+ case FileExtensionAudio.MP3:
+ case FileExtensionAudio.WAV:
+ return FileTypeCategory.AUDIO;
+
+ // PDF
+ case FileExtensionPdf.PDF:
+ return FileTypeCategory.PDF;
+
+ // Text
+ case FileExtensionText.TXT:
+ case FileExtensionText.MD:
+ case FileExtensionText.ADOC:
+ case FileExtensionText.JS:
+ case FileExtensionText.TS:
+ case FileExtensionText.JSX:
+ case FileExtensionText.TSX:
+ case FileExtensionText.CSS:
+ case FileExtensionText.HTML:
+ case FileExtensionText.HTM:
+ case FileExtensionText.JSON:
+ case FileExtensionText.XML:
+ case FileExtensionText.YAML:
+ case FileExtensionText.YML:
+ case FileExtensionText.CSV:
+ case FileExtensionText.LOG:
+ case FileExtensionText.PY:
+ case FileExtensionText.JAVA:
+ case FileExtensionText.CPP:
+ case FileExtensionText.C:
+ case FileExtensionText.H:
+ case FileExtensionText.PHP:
+ case FileExtensionText.RB:
+ case FileExtensionText.GO:
+ case FileExtensionText.RS:
+ case FileExtensionText.SH:
+ case FileExtensionText.BAT:
+ case FileExtensionText.SQL:
+ case FileExtensionText.R:
+ case FileExtensionText.SCALA:
+ case FileExtensionText.KT:
+ case FileExtensionText.SWIFT:
+ case FileExtensionText.DART:
+ case FileExtensionText.VUE:
+ case FileExtensionText.SVELTE:
+ case FileExtensionText.TEX:
+ case FileExtensionText.BIB:
+ return FileTypeCategory.TEXT;
+
+ default:
+ return null;
+ }
}
export function getFileTypeByExtension(filename: string): string | null {
diff --git a/tools/server/webui/src/lib/utils/process-uploaded-files.ts b/tools/server/webui/src/lib/utils/process-uploaded-files.ts
index 372b433502..43383b467a 100644
--- a/tools/server/webui/src/lib/utils/process-uploaded-files.ts
+++ b/tools/server/webui/src/lib/utils/process-uploaded-files.ts
@@ -6,6 +6,7 @@ import { getFileTypeCategory } from '$lib/utils/file-type';
import { modelsStore } from '$lib/stores/models.svelte';
import { settingsStore } from '$lib/stores/settings.svelte';
import { toast } from 'svelte-sonner';
+import { convertPDFToText } from '$lib/utils/pdf-processing';
/**
* Read a file as a data URL (base64 encoded)
@@ -95,8 +96,14 @@ export async function processFilesToChatUploaded(
results.push(base);
}
} else if (getFileTypeCategory(file.type) === FileTypeCategory.PDF) {
- // PDFs handled later when building extras; keep metadata only
- results.push(base);
+ // Extract text content from PDF for preview
+ try {
+ const textContent = await convertPDFToText(file);
+ results.push({ ...base, textContent });
+ } catch (err) {
+ console.warn('Failed to extract text from PDF, adding without content:', err);
+ results.push(base);
+ }
// Show suggestion toast if vision model is available but PDF as image is disabled
const hasVisionSupport = activeModelId
diff --git a/tools/server/webui/src/lib/utils/syntax-highlight-language.ts b/tools/server/webui/src/lib/utils/syntax-highlight-language.ts
new file mode 100644
index 0000000000..538429182e
--- /dev/null
+++ b/tools/server/webui/src/lib/utils/syntax-highlight-language.ts
@@ -0,0 +1,145 @@
+/**
+ * Maps file extensions to highlight.js language identifiers
+ */
+export function getLanguageFromFilename(filename: string): string {
+ const extension = filename.toLowerCase().substring(filename.lastIndexOf('.'));
+
+ switch (extension) {
+ // JavaScript / TypeScript
+ case '.js':
+ case '.mjs':
+ case '.cjs':
+ return 'javascript';
+ case '.ts':
+ case '.mts':
+ case '.cts':
+ return 'typescript';
+ case '.jsx':
+ return 'javascript';
+ case '.tsx':
+ return 'typescript';
+
+ // Web
+ case '.html':
+ case '.htm':
+ return 'html';
+ case '.css':
+ return 'css';
+ case '.scss':
+ return 'scss';
+ case '.less':
+ return 'less';
+ case '.vue':
+ return 'html';
+ case '.svelte':
+ return 'html';
+
+ // Data formats
+ case '.json':
+ return 'json';
+ case '.xml':
+ return 'xml';
+ case '.yaml':
+ case '.yml':
+ return 'yaml';
+ case '.toml':
+ return 'ini';
+ case '.csv':
+ return 'plaintext';
+
+ // Programming languages
+ case '.py':
+ return 'python';
+ case '.java':
+ return 'java';
+ case '.kt':
+ case '.kts':
+ return 'kotlin';
+ case '.scala':
+ return 'scala';
+ case '.cpp':
+ case '.cc':
+ case '.cxx':
+ case '.c++':
+ return 'cpp';
+ case '.c':
+ return 'c';
+ case '.h':
+ case '.hpp':
+ return 'cpp';
+ case '.cs':
+ return 'csharp';
+ case '.go':
+ return 'go';
+ case '.rs':
+ return 'rust';
+ case '.rb':
+ return 'ruby';
+ case '.php':
+ return 'php';
+ case '.swift':
+ return 'swift';
+ case '.dart':
+ return 'dart';
+ case '.r':
+ return 'r';
+ case '.lua':
+ return 'lua';
+ case '.pl':
+ case '.pm':
+ return 'perl';
+
+ // Shell
+ case '.sh':
+ case '.bash':
+ case '.zsh':
+ return 'bash';
+ case '.bat':
+ case '.cmd':
+ return 'dos';
+ case '.ps1':
+ return 'powershell';
+
+ // Database
+ case '.sql':
+ return 'sql';
+
+ // Markup / Documentation
+ case '.md':
+ case '.markdown':
+ return 'markdown';
+ case '.tex':
+ case '.latex':
+ return 'latex';
+ case '.adoc':
+ case '.asciidoc':
+ return 'asciidoc';
+
+ // Config
+ case '.ini':
+ case '.cfg':
+ case '.conf':
+ return 'ini';
+ case '.dockerfile':
+ return 'dockerfile';
+ case '.nginx':
+ return 'nginx';
+
+ // Other
+ case '.graphql':
+ case '.gql':
+ return 'graphql';
+ case '.proto':
+ return 'protobuf';
+ case '.diff':
+ case '.patch':
+ return 'diff';
+ case '.log':
+ return 'plaintext';
+ case '.txt':
+ return 'plaintext';
+
+ default:
+ return 'plaintext';
+ }
+}