From 8a88576849db9dc6efd508013a8c80c80eed0b36 Mon Sep 17 00:00:00 2001 From: Aleksander Grygier Date: Thu, 20 Nov 2025 16:34:25 +0100 Subject: [PATCH] refactor: Architecture improvements --- tools/server/webui/src/app.d.ts | 9 ++-- .../ChatAttachmentPreview.svelte | 36 ++++++++------ .../ChatAttachmentsList.svelte | 48 ++++++++++--------- .../ChatAttachmentsViewAll.svelte | 46 +++++++++--------- .../DialogChatAttachmentPreview.svelte | 22 +++++---- .../server/webui/src/lib/enums/attachment.ts | 10 ++++ tools/server/webui/src/lib/enums/model.ts | 12 ++--- tools/server/webui/src/lib/enums/server.ts | 10 ++++ tools/server/webui/src/lib/services/chat.ts | 41 +++++++++++++--- .../webui/src/lib/stores/server.svelte.ts | 5 +- .../server/webui/src/lib/types/database.d.ts | 38 ++++++++------- .../src/lib/utils/convert-files-to-extra.ts | 13 ++--- tools/server/webui/vite.config.ts | 3 +- 13 files changed, 180 insertions(+), 113 deletions(-) create mode 100644 tools/server/webui/src/lib/enums/attachment.ts 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',