From 16f333e4ec7ae628a7e3067a51b38e8a3386662d Mon Sep 17 00:00:00 2001 From: Aleksander Grygier Date: Tue, 3 Feb 2026 14:42:42 +0100 Subject: [PATCH] refactor: Cleanup --- tools/server/webui/src/lib/constants/code.ts | 7 +++++ .../server/webui/src/lib/constants/favicon.ts | 2 ++ .../webui/src/lib/constants/formatters.ts | 5 ++++ tools/server/webui/src/lib/constants/mcp.ts | 2 ++ tools/server/webui/src/lib/enums/index.ts | 9 +++++- tools/server/webui/src/lib/enums/mcp.ts | 17 +++++++++++ .../webui/src/lib/services/mcp.service.ts | 28 +++++++++++++------ .../server/webui/src/lib/stores/mcp.svelte.ts | 4 +-- tools/server/webui/src/lib/utils/api-fetch.ts | 4 ++- tools/server/webui/src/lib/utils/code.ts | 22 +++++++++++---- tools/server/webui/src/lib/utils/favicon.ts | 3 +- .../server/webui/src/lib/utils/formatters.ts | 20 +++++++++---- 12 files changed, 98 insertions(+), 25 deletions(-) create mode 100644 tools/server/webui/src/lib/constants/code.ts create mode 100644 tools/server/webui/src/lib/constants/favicon.ts create mode 100644 tools/server/webui/src/lib/constants/formatters.ts diff --git a/tools/server/webui/src/lib/constants/code.ts b/tools/server/webui/src/lib/constants/code.ts new file mode 100644 index 0000000000..12bcd0db77 --- /dev/null +++ b/tools/server/webui/src/lib/constants/code.ts @@ -0,0 +1,7 @@ +export const NEWLINE = '\n'; +export const DEFAULT_LANGUAGE = 'text'; +export const LANG_PATTERN = /^(\w*)\n?/; +export const AMPERSAND_REGEX = /&/g; +export const LT_REGEX = //g; +export const FENCE_PATTERN = /^```|\n```/g; diff --git a/tools/server/webui/src/lib/constants/favicon.ts b/tools/server/webui/src/lib/constants/favicon.ts new file mode 100644 index 0000000000..f0281f935c --- /dev/null +++ b/tools/server/webui/src/lib/constants/favicon.ts @@ -0,0 +1,2 @@ +export const GOOGLE_FAVICON_BASE_URL = 'https://www.google.com/s2/favicons'; +export const DEFAULT_FAVICON_SIZE = 32; diff --git a/tools/server/webui/src/lib/constants/formatters.ts b/tools/server/webui/src/lib/constants/formatters.ts new file mode 100644 index 0000000000..3ef6ae0038 --- /dev/null +++ b/tools/server/webui/src/lib/constants/formatters.ts @@ -0,0 +1,5 @@ +export const MS_PER_SECOND = 1000; +export const SECONDS_PER_MINUTE = 60; +export const SECONDS_PER_HOUR = 3600; +export const SHORT_DURATION_THRESHOLD = 1; +export const MEDIUM_DURATION_THRESHOLD = 10; diff --git a/tools/server/webui/src/lib/constants/mcp.ts b/tools/server/webui/src/lib/constants/mcp.ts index bc07ecad4a..824990c9e7 100644 --- a/tools/server/webui/src/lib/constants/mcp.ts +++ b/tools/server/webui/src/lib/constants/mcp.ts @@ -9,3 +9,5 @@ export const DEFAULT_MCP_CONFIG = { } as const; export const MCP_SERVER_ID_PREFIX = 'LlamaCpp-WebUI-MCP-Server-'; +export const DEFAULT_CLIENT_VERSION = '1.0.0'; +export const DEFAULT_IMAGE_MIME_TYPE = 'image/png'; diff --git a/tools/server/webui/src/lib/enums/index.ts b/tools/server/webui/src/lib/enums/index.ts index c35abdc27a..0e8504cc1d 100644 --- a/tools/server/webui/src/lib/enums/index.ts +++ b/tools/server/webui/src/lib/enums/index.ts @@ -28,7 +28,14 @@ export { SpecialFileType } from './files'; -export { MCPConnectionPhase, MCPLogLevel, MCPTransportType, HealthCheckStatus } from './mcp'; +export { + MCPConnectionPhase, + MCPLogLevel, + MCPTransportType, + HealthCheckStatus, + MCPContentType, + MCPRefType +} from './mcp'; export { ModelModality } from './model'; diff --git a/tools/server/webui/src/lib/enums/mcp.ts b/tools/server/webui/src/lib/enums/mcp.ts index 5fe15e582c..3b05a073b4 100644 --- a/tools/server/webui/src/lib/enums/mcp.ts +++ b/tools/server/webui/src/lib/enums/mcp.ts @@ -40,3 +40,20 @@ export enum HealthCheckStatus { SUCCESS = 'success', ERROR = 'error' } + +/** + * Content types for MCP tool results + */ +export enum MCPContentType { + TEXT = 'text', + IMAGE = 'image', + RESOURCE = 'resource' +} + +/** + * Reference types for MCP completions + */ +export enum MCPRefType { + PROMPT = 'ref/prompt', + RESOURCE = 'ref/resource' +} diff --git a/tools/server/webui/src/lib/services/mcp.service.ts b/tools/server/webui/src/lib/services/mcp.service.ts index e5c8737609..6a1ac97111 100644 --- a/tools/server/webui/src/lib/services/mcp.service.ts +++ b/tools/server/webui/src/lib/services/mcp.service.ts @@ -39,8 +39,18 @@ import type { MCPResourceContent, MCPReadResourceResult } from '$lib/types'; -import { MCPConnectionPhase, MCPLogLevel, MCPTransportType } from '$lib/enums'; -import { DEFAULT_MCP_CONFIG } from '$lib/constants/mcp'; +import { + MCPConnectionPhase, + MCPLogLevel, + MCPTransportType, + MCPContentType, + MCPRefType +} from '$lib/enums'; +import { + DEFAULT_MCP_CONFIG, + DEFAULT_CLIENT_VERSION, + DEFAULT_IMAGE_MIME_TYPE +} from '$lib/constants/mcp'; import { throwIfAborted, isAbortError } from '$lib/utils'; import { buildProxiedUrl } from '$lib/utils/cors-proxy'; @@ -102,7 +112,7 @@ export class MCPService { requestInit.credentials = config.credentials; } - if (config.transport === 'websocket') { + if (config.transport === MCPTransportType.WEBSOCKET) { if (useProxy) { throw new Error( 'WebSocket transport is not supported when using CORS proxy. Use HTTP transport instead.' @@ -215,7 +225,7 @@ export class MCPService { const client = new Client( { name: effectiveClientInfo.name, - version: effectiveClientInfo.version ?? '1.0.0' + version: effectiveClientInfo.version ?? DEFAULT_CLIENT_VERSION }, { capabilities: effectiveCapabilities, @@ -406,15 +416,15 @@ export class MCPService { } private static formatSingleContent(content: ToolResultContentItem): string { - if (content.type === 'text' && content.text) { + if (content.type === MCPContentType.TEXT && content.text) { return content.text; } - if (content.type === 'image' && content.data) { - return `data:${content.mimeType ?? 'image/png'};base64,${content.data}`; + if (content.type === MCPContentType.IMAGE && content.data) { + return `data:${content.mimeType ?? DEFAULT_IMAGE_MIME_TYPE};base64,${content.data}`; } - if (content.type === 'resource' && content.resource) { + if (content.type === MCPContentType.RESOURCE && content.resource) { const resource = content.resource; if (resource.text) return resource.text; @@ -449,7 +459,7 @@ export class MCPService { */ static async complete( connection: MCPConnection, - ref: { type: 'ref/prompt'; name: string } | { type: 'ref/resource'; uri: string }, + ref: { type: MCPRefType.PROMPT; name: string } | { type: MCPRefType.RESOURCE; uri: string }, argument: { name: string; value: string } ): Promise<{ values: string[]; total?: number; hasMore?: boolean } | null> { try { diff --git a/tools/server/webui/src/lib/stores/mcp.svelte.ts b/tools/server/webui/src/lib/stores/mcp.svelte.ts index 19e2edd8e9..c57c9c449a 100644 --- a/tools/server/webui/src/lib/stores/mcp.svelte.ts +++ b/tools/server/webui/src/lib/stores/mcp.svelte.ts @@ -24,7 +24,7 @@ import { MCPService } from '$lib/services/mcp.service'; import { config, settingsStore } from '$lib/stores/settings.svelte'; import { mcpResourceStore } from '$lib/stores/mcp-resources.svelte'; import { parseMcpServerSettings, detectMcpTransportFromUrl } from '$lib/utils'; -import { MCPConnectionPhase, MCPLogLevel, HealthCheckStatus } from '$lib/enums'; +import { MCPConnectionPhase, MCPLogLevel, HealthCheckStatus, MCPRefType } from '$lib/enums'; import { DEFAULT_MCP_CONFIG, MCP_SERVER_ID_PREFIX } from '$lib/constants/mcp'; import type { MCPToolCall, @@ -732,7 +732,7 @@ class MCPStore { if (!connection.serverCapabilities?.completions) return null; return MCPService.complete( connection, - { type: 'ref/prompt', name: promptName }, + { type: MCPRefType.PROMPT, name: promptName }, { name: argumentName, value: argumentValue } ); } diff --git a/tools/server/webui/src/lib/utils/api-fetch.ts b/tools/server/webui/src/lib/utils/api-fetch.ts index 3f3e6ee58d..7d12a34276 100644 --- a/tools/server/webui/src/lib/utils/api-fetch.ts +++ b/tools/server/webui/src/lib/utils/api-fetch.ts @@ -1,5 +1,6 @@ import { base } from '$app/paths'; import { getJsonHeaders, getAuthHeaders } from './api-headers'; +import { UrlPrefix } from '$lib/enums'; /** * API Fetch Utilities @@ -48,7 +49,8 @@ export async function apiFetch(path: string, options: ApiFetchOptions = {}): const baseHeaders = authOnly ? getAuthHeaders() : getJsonHeaders(); const headers = { ...baseHeaders, ...customHeaders }; - const url = path.startsWith('http') ? path : `${base}${path}`; + const url = + path.startsWith(UrlPrefix.HTTP) || path.startsWith(UrlPrefix.HTTPS) ? path : `${base}${path}`; const response = await fetch(url, { ...fetchOptions, diff --git a/tools/server/webui/src/lib/utils/code.ts b/tools/server/webui/src/lib/utils/code.ts index c205ee7fbc..67efc6b27e 100644 --- a/tools/server/webui/src/lib/utils/code.ts +++ b/tools/server/webui/src/lib/utils/code.ts @@ -1,4 +1,13 @@ import hljs from 'highlight.js'; +import { + NEWLINE, + DEFAULT_LANGUAGE, + LANG_PATTERN, + AMPERSAND_REGEX, + LT_REGEX, + GT_REGEX, + FENCE_PATTERN +} from '$lib/constants/code'; export interface IncompleteCodeBlock { language: string; @@ -26,7 +35,10 @@ export function highlightCode(code: string, language: string): string { } } catch { // Fallback to escaped plain text - return code.replace(/&/g, '&').replace(//g, '>'); + return code + .replace(AMPERSAND_REGEX, '&') + .replace(LT_REGEX, '<') + .replace(GT_REGEX, '>'); } } @@ -39,13 +51,13 @@ export function highlightCode(code: string, language: string): string { export function detectIncompleteCodeBlock(markdown: string): IncompleteCodeBlock | null { // Count all code fences in the markdown // A code block is incomplete if there's an odd number of ``` fences - const fencePattern = /^```|\n```/g; + const fencePattern = new RegExp(FENCE_PATTERN.source, FENCE_PATTERN.flags); const fences: number[] = []; let fenceMatch; while ((fenceMatch = fencePattern.exec(markdown)) !== null) { // Store the position after the ``` - const pos = fenceMatch[0].startsWith('\n') ? fenceMatch.index + 1 : fenceMatch.index; + const pos = fenceMatch[0].startsWith(NEWLINE) ? fenceMatch.index + 1 : fenceMatch.index; fences.push(pos); } @@ -60,8 +72,8 @@ export function detectIncompleteCodeBlock(markdown: string): IncompleteCodeBlock const afterOpening = markdown.slice(openingIndex + 3); // Extract language and code content - const langMatch = afterOpening.match(/^(\w*)\n?/); - const language = langMatch?.[1] || 'text'; + const langMatch = afterOpening.match(LANG_PATTERN); + const language = langMatch?.[1] || DEFAULT_LANGUAGE; const codeStartIndex = openingIndex + 3 + (langMatch?.[0]?.length ?? 0); const code = markdown.slice(codeStartIndex); diff --git a/tools/server/webui/src/lib/utils/favicon.ts b/tools/server/webui/src/lib/utils/favicon.ts index c7801de198..3b4cd48b4d 100644 --- a/tools/server/webui/src/lib/utils/favicon.ts +++ b/tools/server/webui/src/lib/utils/favicon.ts @@ -3,6 +3,7 @@ */ import { getProxiedUrlString } from './cors-proxy'; +import { GOOGLE_FAVICON_BASE_URL, DEFAULT_FAVICON_SIZE } from '$lib/constants/favicon'; /** * Gets a favicon URL for a given URL using Google's favicon service. @@ -17,7 +18,7 @@ export function getFaviconUrl(urlString: string): string | null { const hostnameParts = url.hostname.split('.'); const rootDomain = hostnameParts.length >= 2 ? hostnameParts.slice(-2).join('.') : url.hostname; - const googleFaviconUrl = `https://www.google.com/s2/favicons?domain=${rootDomain}&sz=32`; + const googleFaviconUrl = `${GOOGLE_FAVICON_BASE_URL}?domain=${rootDomain}&sz=${DEFAULT_FAVICON_SIZE}`; return getProxiedUrlString(googleFaviconUrl); } catch { return null; diff --git a/tools/server/webui/src/lib/utils/formatters.ts b/tools/server/webui/src/lib/utils/formatters.ts index 4e4a7c1aae..37a8a3358c 100644 --- a/tools/server/webui/src/lib/utils/formatters.ts +++ b/tools/server/webui/src/lib/utils/formatters.ts @@ -1,3 +1,11 @@ +import { + MS_PER_SECOND, + SECONDS_PER_MINUTE, + SECONDS_PER_HOUR, + SHORT_DURATION_THRESHOLD, + MEDIUM_DURATION_THRESHOLD +} from '$lib/constants/formatters'; + /** * Formats file size in bytes to human readable format * Supports Bytes, KB, MB, and GB @@ -93,19 +101,19 @@ export function formatTime(date: Date): string { export function formatPerformanceTime(ms: number): string { if (ms < 0) return '0s'; - const totalSeconds = ms / 1000; + const totalSeconds = ms / MS_PER_SECOND; - if (totalSeconds < 1) { + if (totalSeconds < SHORT_DURATION_THRESHOLD) { return `${totalSeconds.toFixed(1)}s`; } - if (totalSeconds < 10) { + if (totalSeconds < MEDIUM_DURATION_THRESHOLD) { return `${totalSeconds.toFixed(1)}s`; } - const hours = Math.floor(totalSeconds / 3600); - const minutes = Math.floor((totalSeconds % 3600) / 60); - const seconds = Math.floor(totalSeconds % 60); + const hours = Math.floor(totalSeconds / SECONDS_PER_HOUR); + const minutes = Math.floor((totalSeconds % SECONDS_PER_HOUR) / SECONDS_PER_MINUTE); + const seconds = Math.floor(totalSeconds % SECONDS_PER_MINUTE); const parts: string[] = [];