diff --git a/tools/server/webui/src/app.d.ts b/tools/server/webui/src/app.d.ts
index 41d3457e2c..a362c24b0f 100644
--- a/tools/server/webui/src/app.d.ts
+++ b/tools/server/webui/src/app.d.ts
@@ -23,8 +23,8 @@ import type {
ApiRouterModelsUnloadResponse
} from '$lib/types/api';
-import { ServerMode } from '$lib/enums/server';
-import { ServerModelStatus } from '$lib/enums/model';
+import { ServerMode, ServerModelStatus } from '$lib/enums/server';
+import { ModelModality } from '$lib/enums/model';
import type {
ChatMessageType,
@@ -79,8 +79,6 @@ declare global {
ApiRouterModelsListResponse,
ApiRouterModelsUnloadRequest,
ApiRouterModelsUnloadResponse,
- ServerMode,
- ServerModelStatus,
ChatMessageData,
ChatMessagePromptProgress,
ChatMessageSiblingInfo,
@@ -96,6 +94,9 @@ declare global {
DatabaseMessageExtraTextFile,
DatabaseMessageExtraPdfFile,
DatabaseMessageExtraLegacyContext,
+ ModelModality,
+ ServerMode,
+ ServerModelStatus,
SettingsConfigValue,
SettingsFieldConfig,
SettingsConfigType,
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 212b1fe890..015f6fc3d7 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,8 +1,11 @@
-
+
{displayName}
diff --git a/tools/server/webui/src/lib/enums/attachment.ts b/tools/server/webui/src/lib/enums/attachment.ts
new file mode 100644
index 0000000000..7c7d0da994
--- /dev/null
+++ b/tools/server/webui/src/lib/enums/attachment.ts
@@ -0,0 +1,10 @@
+/**
+ * Attachment type enum for database message extras
+ */
+export enum AttachmentType {
+ AUDIO = 'AUDIO',
+ IMAGE = 'IMAGE',
+ PDF = 'PDF',
+ TEXT = 'TEXT',
+ LEGACY_CONTEXT = 'context' // Legacy attachment type for backward compatibility
+}
diff --git a/tools/server/webui/src/lib/enums/model.ts b/tools/server/webui/src/lib/enums/model.ts
index dfdf85e263..7729ecfeab 100644
--- a/tools/server/webui/src/lib/enums/model.ts
+++ b/tools/server/webui/src/lib/enums/model.ts
@@ -1,9 +1,5 @@
-/**
- * Model status enum - matches tools/server/server-models.h from C++ server
- */
-export enum ServerModelStatus {
- UNLOADED = 'UNLOADED',
- LOADING = 'LOADING',
- LOADED = 'LOADED',
- FAILED = 'FAILED'
+export enum ModelModality {
+ TEXT = 'TEXT',
+ AUDIO = 'AUDIO',
+ VISION = 'VISION'
}
diff --git a/tools/server/webui/src/lib/enums/server.ts b/tools/server/webui/src/lib/enums/server.ts
index 105a400d27..f2d893537d 100644
--- a/tools/server/webui/src/lib/enums/server.ts
+++ b/tools/server/webui/src/lib/enums/server.ts
@@ -7,3 +7,13 @@ export enum ServerMode {
/** Router mode - server managing multiple model instances */
ROUTER = 'ROUTER'
}
+
+/**
+ * Model status enum - matches tools/server/server-models.h from C++ server
+ */
+export enum ServerModelStatus {
+ UNLOADED = 'UNLOADED',
+ LOADING = 'LOADING',
+ LOADED = 'LOADED',
+ FAILED = 'FAILED'
+}
diff --git a/tools/server/webui/src/lib/services/chat.ts b/tools/server/webui/src/lib/services/chat.ts
index aa83910b27..bc5cf680ae 100644
--- a/tools/server/webui/src/lib/services/chat.ts
+++ b/tools/server/webui/src/lib/services/chat.ts
@@ -7,8 +7,10 @@ import type {
ApiChatCompletionStreamChunk,
ApiChatCompletionToolCall,
ApiChatCompletionToolCallDelta,
- ApiChatMessageData
+ ApiChatMessageData,
+ ApiModelListResponse
} from '$lib/types/api';
+import { AttachmentType } from '$lib/enums/attachment';
import type {
DatabaseMessage,
DatabaseMessageExtra,
@@ -618,7 +620,7 @@ export class ChatService {
const imageFiles = message.extra.filter(
(extra: DatabaseMessageExtra): extra is DatabaseMessageExtraImageFile =>
- extra.type === 'imageFile'
+ extra.type === AttachmentType.IMAGE
);
for (const image of imageFiles) {
@@ -630,7 +632,7 @@ export class ChatService {
const textFiles = message.extra.filter(
(extra: DatabaseMessageExtra): extra is DatabaseMessageExtraTextFile =>
- extra.type === 'textFile'
+ extra.type === AttachmentType.TEXT
);
for (const textFile of textFiles) {
@@ -643,7 +645,7 @@ export class ChatService {
// Handle legacy 'context' type from old webui (pasted content)
const legacyContextFiles = message.extra.filter(
(extra: DatabaseMessageExtra): extra is DatabaseMessageExtraLegacyContext =>
- extra.type === 'context'
+ extra.type === AttachmentType.LEGACY_CONTEXT
);
for (const legacyContextFile of legacyContextFiles) {
@@ -655,7 +657,7 @@ export class ChatService {
const audioFiles = message.extra.filter(
(extra: DatabaseMessageExtra): extra is DatabaseMessageExtraAudioFile =>
- extra.type === 'audioFile'
+ extra.type === AttachmentType.AUDIO
);
for (const audio of audioFiles) {
@@ -670,7 +672,7 @@ export class ChatService {
const pdfFiles = message.extra.filter(
(extra: DatabaseMessageExtra): extra is DatabaseMessageExtraPdfFile =>
- extra.type === 'pdfFile'
+ extra.type === AttachmentType.PDF
);
for (const pdfFile of pdfFiles) {
@@ -722,6 +724,33 @@ export class ChatService {
}
}
+ /**
+ * Get model information from /models endpoint
+ */
+ static async getModels(): Promise {
+ try {
+ const currentConfig = config();
+ const apiKey = currentConfig.apiKey?.toString().trim();
+
+ const response = await fetch(`./models`, {
+ headers: {
+ 'Content-Type': 'application/json',
+ ...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {})
+ }
+ });
+
+ if (!response.ok) {
+ throw new Error(`Failed to fetch models: ${response.status} ${response.statusText}`);
+ }
+
+ const data = await response.json();
+ return data;
+ } catch (error) {
+ console.error('Error fetching models:', error);
+ throw error;
+ }
+ }
+
/**
* Aborts any ongoing chat completion request.
* Cancels the current request and cleans up the abort controller.
diff --git a/tools/server/webui/src/lib/stores/server.svelte.ts b/tools/server/webui/src/lib/stores/server.svelte.ts
index cbe6fcb609..0620ac3b04 100644
--- a/tools/server/webui/src/lib/stores/server.svelte.ts
+++ b/tools/server/webui/src/lib/stores/server.svelte.ts
@@ -3,6 +3,7 @@ import { SERVER_PROPS_LOCALSTORAGE_KEY } from '$lib/constants/localstorage-keys'
import { ChatService } from '$lib/services/chat';
import { config } from '$lib/stores/settings.svelte';
import { ServerMode } from '$lib/enums/server';
+import { ModelModality } from '$lib/enums/model';
import { updateConfig } from '$lib/stores/settings.svelte';
/**
@@ -115,10 +116,10 @@ class ServerStore {
get supportedModalities(): string[] {
const modalities: string[] = [];
if (this._serverProps?.modalities?.audio) {
- modalities.push('audio');
+ modalities.push(ModelModality.AUDIO);
}
if (this._serverProps?.modalities?.vision) {
- modalities.push('vision');
+ modalities.push(ModelModality.VISION);
}
return modalities;
}
diff --git a/tools/server/webui/src/lib/types/database.d.ts b/tools/server/webui/src/lib/types/database.d.ts
index 16debc6d67..9a5d9204de 100644
--- a/tools/server/webui/src/lib/types/database.d.ts
+++ b/tools/server/webui/src/lib/types/database.d.ts
@@ -1,4 +1,5 @@
-import type { ChatMessageTimings } from './chat';
+import type { ChatMessageTimings, ChatRole, ChatMessageType } from '$lib/types/chat';
+import { AttachmentType } from '$lib/enums/attachment';
export interface DatabaseConversation {
currNode: string | null;
@@ -8,38 +9,39 @@ export interface DatabaseConversation {
}
export interface DatabaseMessageExtraAudioFile {
- type: 'audioFile';
+ type: AttachmentType.AUDIO;
name: string;
base64Data: string;
mimeType: string;
}
export interface DatabaseMessageExtraImageFile {
- type: 'imageFile';
+ type: AttachmentType.IMAGE;
name: string;
base64Url: string;
}
-export interface DatabaseMessageExtraTextFile {
- type: 'textFile';
- name: string;
- content: string;
-}
-
-export interface DatabaseMessageExtraPdfFile {
- type: 'pdfFile';
- name: string;
- content: string; // Text content extracted from PDF
- images?: string[]; // Optional: PDF pages as base64 images
- processedAsImages: boolean; // Whether PDF was processed as images
-}
-
/**
* Legacy format from old webui - pasted content was stored as "context" type
* @deprecated Use DatabaseMessageExtraTextFile instead
*/
export interface DatabaseMessageExtraLegacyContext {
- type: 'context';
+ type: AttachmentType.LEGACY_CONTEXT;
+ name: string;
+ content: string;
+}
+
+export interface DatabaseMessageExtraPdfFile {
+ type: AttachmentType.PDF;
+ base64Data: string;
+ name: string;
+ content: string; // Text content extracted from PDF
+ images?: string[]; // Optional: PDF pages as base64 images
+ processedAsImages: boolean; // Whether PDF was processed as images
+}
+
+export interface DatabaseMessageExtraTextFile {
+ type: AttachmentType.TEXT;
name: string;
content: string;
}
diff --git a/tools/server/webui/src/lib/utils/convert-files-to-extra.ts b/tools/server/webui/src/lib/utils/convert-files-to-extra.ts
index 70c6f772d9..8229d2b0bd 100644
--- a/tools/server/webui/src/lib/utils/convert-files-to-extra.ts
+++ b/tools/server/webui/src/lib/utils/convert-files-to-extra.ts
@@ -2,6 +2,7 @@ import { convertPDFToImage, convertPDFToText } from './pdf-processing';
import { isSvgMimeType, svgBase64UrlToPngDataURL } from './svg-to-png';
import { isWebpMimeType, webpBase64UrlToPngDataURL } from './webp-to-png';
import { FileTypeCategory } from '$lib/enums/files';
+import { AttachmentType } from '$lib/enums/attachment';
import { config, settingsStore } from '$lib/stores/settings.svelte';
import { supportsVision } from '$lib/stores/server.svelte';
import { getFileTypeCategory } from '$lib/utils/file-type';
@@ -56,7 +57,7 @@ export async function parseFilesToMessageExtras(
}
extras.push({
- type: 'imageFile',
+ type: AttachmentType.IMAGE,
name: file.name,
base64Url
});
@@ -67,7 +68,7 @@ export async function parseFilesToMessageExtras(
const base64Data = await readFileAsBase64(file.file);
extras.push({
- type: 'audioFile',
+ type: AttachmentType.AUDIO,
name: file.name,
base64Data: base64Data,
mimeType: file.type
@@ -117,7 +118,7 @@ export async function parseFilesToMessageExtras(
);
extras.push({
- type: 'pdfFile',
+ type: AttachmentType.PDF,
name: file.name,
content: `PDF file with ${images.length} pages`,
images: images,
@@ -134,7 +135,7 @@ export async function parseFilesToMessageExtras(
const content = await convertPDFToText(file.file);
extras.push({
- type: 'pdfFile',
+ type: AttachmentType.PDF,
name: file.name,
content: content,
processedAsImages: false,
@@ -151,7 +152,7 @@ export async function parseFilesToMessageExtras(
});
extras.push({
- type: 'pdfFile',
+ type: AttachmentType.PDF,
name: file.name,
content: content,
processedAsImages: false,
@@ -171,7 +172,7 @@ export async function parseFilesToMessageExtras(
emptyFiles.push(file.name);
} else if (isLikelyTextFile(content)) {
extras.push({
- type: 'textFile',
+ type: AttachmentType.TEXT,
name: file.name,
content: content
});
diff --git a/tools/server/webui/vite.config.ts b/tools/server/webui/vite.config.ts
index 11ff665d8b..f2df5dc287 100644
--- a/tools/server/webui/vite.config.ts
+++ b/tools/server/webui/vite.config.ts
@@ -158,7 +158,8 @@ export default defineConfig({
proxy: {
'/v1': 'http://localhost:8080',
'/props': 'http://localhost:8080',
- '/slots': 'http://localhost:8080'
+ '/slots': 'http://localhost:8080',
+ '/models': 'http://localhost:8080'
},
headers: {
'Cross-Origin-Embedder-Policy': 'require-corp',