refactor: Cleanup

This commit is contained in:
Aleksander Grygier 2026-02-03 14:42:42 +01:00
parent 70efc41eb1
commit 16f333e4ec
12 changed files with 98 additions and 25 deletions

View File

@ -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 GT_REGEX = />/g;
export const FENCE_PATTERN = /^```|\n```/g;

View File

@ -0,0 +1,2 @@
export const GOOGLE_FAVICON_BASE_URL = 'https://www.google.com/s2/favicons';
export const DEFAULT_FAVICON_SIZE = 32;

View File

@ -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;

View File

@ -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';

View File

@ -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';

View File

@ -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'
}

View File

@ -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 {

View File

@ -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 }
);
}

View File

@ -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<T>(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,

View File

@ -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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
return code
.replace(AMPERSAND_REGEX, '&amp;')
.replace(LT_REGEX, '&lt;')
.replace(GT_REGEX, '&gt;');
}
}
@ -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);

View File

@ -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;

View File

@ -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[] = [];