Inital commit

This commit is contained in:
2026-07-29 01:00:10 -05:00
commit 23e8ea90e4
3301 changed files with 1376308 additions and 0 deletions
+75
View File
@@ -0,0 +1,75 @@
import type { AgenticConfig } from '$lib/types/agentic';
export const ATTACHMENT_SAVED_REGEX = /\[Attachment saved: ([^\]]+)\]/;
// JSON detection: trimmed content opens with an object or array literal.
export const TOOL_RESULT_JSON_OPEN_REGEX = /^[[{]/;
// Markdown structural markers used by `looksLikeMarkdown`. Inline / line-level.
export const MARKDOWN_CODE_FENCE_REGEX = /^(```|~~~)/m;
export const MARKDOWN_ATX_HEADING_REGEX = /^#{1,6}\s+\S/;
export const MARKDOWN_BLOCKQUOTE_REGEX = /^>\s+\S/;
export const MARKDOWN_LIST_BULLET_REGEX = /^\s*[-*+]\s+\S/;
export const MARKDOWN_LIST_NUMBERED_REGEX = /^\s*\d+[.)]\s+\S/;
export const MARKDOWN_LINK_REGEX = /\[[^\]\n]+\]\([^)\s]+\)/;
export const MARKDOWN_BOLD_REGEX = /\*\*[^*\n]+\*\*|__[^_\n]+__/;
export const MARKDOWN_TABLE_SEPARATOR_REGEX = /^\s*\|?[\s:|-]+\|?\s*$/;
// Search-summary wire format used by file-glob and grep tools:
// <matches>
// ---
// Total matches: N
export const SEARCH_SUMMARY_SEPARATOR = '---\n';
export const SEARCH_SUMMARY_TOTAL_REGEX = /Total matches:\s*(\d+)/;
// Separator rendered between stats in the tool-result footer (e.g. between a
// result message and the byte/edit count). Plain ASCII spaces bracket a hyphen
// so the whole " - " sits on one visual line even when the surrounding text
// wraps mid-paragraph.
export const RESULT_STAT_SEPARATOR = ' - ';
export const DEFAULT_AGENTIC_CONFIG: AgenticConfig = {
enabled: true,
maxTurns: 100
} as const;
export const REASONING_TAGS = {
START: '<think>',
END: '</think>'
} as const;
/**
* @deprecated Legacy marker tags - only used for migration of old stored messages.
* New messages use structured fields (reasoningContent, toolCalls, toolCallId).
*/
export const LEGACY_AGENTIC_TAGS = {
TOOL_CALL_START: '<<<AGENTIC_TOOL_CALL_START>>>',
TOOL_CALL_END: '<<<AGENTIC_TOOL_CALL_END>>>',
TOOL_NAME_PREFIX: '<<<TOOL_NAME:',
TOOL_ARGS_START: '<<<TOOL_ARGS_START>>>',
TOOL_ARGS_END: '<<<TOOL_ARGS_END>>>',
TAG_SUFFIX: '>>>'
} as const;
/**
* @deprecated Legacy reasoning tags - only used for migration of old stored messages.
* New messages use the dedicated reasoningContent field.
*/
export const LEGACY_REASONING_TAGS = {
START: '<<<reasoning_content_start>>>',
END: '<<<reasoning_content_end>>>'
} as const;
/**
* @deprecated Legacy regex patterns - only used for migration of old stored messages.
*/
export const LEGACY_AGENTIC_REGEX = {
COMPLETED_TOOL_CALL:
/<<<AGENTIC_TOOL_CALL_START>>>\n<<<TOOL_NAME:(.+?)>>>\n<<<TOOL_ARGS_START>>>([\s\S]*?)<<<TOOL_ARGS_END>>>([\s\S]*?)<<<AGENTIC_TOOL_CALL_END>>>/g,
REASONING_BLOCK: /<<<reasoning_content_start>>>[\s\S]*?<<<reasoning_content_end>>>/g,
REASONING_EXTRACT: /<<<reasoning_content_start>>>([\s\S]*?)<<<reasoning_content_end>>>/,
REASONING_OPEN: /<<<reasoning_content_start>>>[\s\S]*$/,
AGENTIC_TOOL_CALL_BLOCK: /\n*<<<AGENTIC_TOOL_CALL_START>>>[\s\S]*?<<<AGENTIC_TOOL_CALL_END>>>/g,
AGENTIC_TOOL_CALL_OPEN: /\n*<<<AGENTIC_TOOL_CALL_START>>>[\s\S]*$/,
HAS_LEGACY_MARKERS: /<<<(?:AGENTIC_TOOL_CALL_START|reasoning_content_start)>>>/
} as const;
@@ -0,0 +1,35 @@
export const API_MODELS = {
LIST: '/v1/models',
LOAD: '/models/load',
UNLOAD: '/models/unload',
SSE: '/models/sse'
};
// chat completion routes, the control route drives realtime inference (e.g. end reasoning)
export const API_CHAT = {
COMPLETIONS: './v1/chat/completions',
CONTROL: './v1/chat/completions/control'
};
// slot introspection, requires the --slots flag on the server
export const API_SLOTS = {
LIST: './slots'
};
export const API_TOOLS = {
LIST: '/tools',
EXECUTE: '/tools'
};
// resumable stream routes, the conv::model identity travels as the conv_id query param
// because model names can contain slashes that a path segment cannot carry
// resume retry cadence while the owning model is still loading (server answers 503)
export const STREAM_RESUME_RETRY_MS = 2000;
export const API_STREAM = {
BASE: './v1/stream',
LOOKUP: './v1/streams/lookup'
};
/** CORS proxy endpoint path */
export const CORS_PROXY_ENDPOINT = '/cors-proxy';
+1
View File
@@ -0,0 +1 @@
export const APP_NAME = import.meta.env?.VITE_PUBLIC_APP_NAME || 'llama-ui';
@@ -0,0 +1,4 @@
export const ATTACHMENT_LABEL_FILE = 'File';
export const ATTACHMENT_LABEL_PDF_FILE = 'PDF File';
export const ATTACHMENT_LABEL_MCP_PROMPT = 'MCP Prompt';
export const ATTACHMENT_LABEL_MCP_RESOURCE = 'MCP Resource';
@@ -0,0 +1,114 @@
import type { Component } from 'svelte';
import { MessageSquare, Zap, FolderOpen } from '@lucide/svelte';
import { FILE_TYPE_ICONS } from '$lib/constants/icons';
import {
AttachmentAction,
AttachmentItemEnabledWhen,
AttachmentItemVisibleWhen,
AttachmentMenuItemId
} from '$lib/enums';
export interface AttachmentMenuItem {
/** Unique identifier for the item */
id: AttachmentMenuItemId;
/** Display label */
label: string;
/** Lucide icon component */
icon: Component;
/** Extra CSS class applied to the item (e.g. for test selectors) */
class?: string;
/** Whether the item requires a specific modality to be enabled */
enabledWhen?: AttachmentItemEnabledWhen;
/** Tooltip shown when the item is disabled */
disabledTooltip?: string;
/** Callback key on the Props interface to invoke when clicked */
action: AttachmentAction;
/** Whether the item is only shown when a specific capability is present */
visibleWhen?: AttachmentItemVisibleWhen;
/** Whether this item has a tooltip even when enabled (uses dynamic text) */
hasEnabledTooltip?: boolean;
}
/**
* File attachment menu items shown in both the desktop dropdown and mobile sheet.
* The "Tools" submenu is handled separately by each component.
*/
export const ATTACHMENT_FILE_ITEMS: AttachmentMenuItem[] = [
{
id: AttachmentMenuItemId.IMAGES,
label: 'Images',
icon: FILE_TYPE_ICONS.image,
class: 'images-button',
enabledWhen: AttachmentItemEnabledWhen.HAS_VISION_MODALITY,
disabledTooltip: 'Image processing requires a vision model',
action: AttachmentAction.FILE_UPLOAD
},
{
id: AttachmentMenuItemId.AUDIO,
label: 'Audio Files',
icon: FILE_TYPE_ICONS.audio,
class: 'audio-button',
enabledWhen: AttachmentItemEnabledWhen.HAS_AUDIO_MODALITY,
disabledTooltip: 'Audio files processing requires an audio model',
action: AttachmentAction.FILE_UPLOAD
},
{
id: AttachmentMenuItemId.VIDEO,
label: 'Video Files',
icon: FILE_TYPE_ICONS.video,
class: 'video-button',
enabledWhen: AttachmentItemEnabledWhen.HAS_VIDEO_MODALITY,
disabledTooltip: 'Video files processing requires a video model',
action: AttachmentAction.FILE_UPLOAD
},
{
id: AttachmentMenuItemId.TEXT,
label: 'Text Files',
icon: FILE_TYPE_ICONS.text,
enabledWhen: AttachmentItemEnabledWhen.ALWAYS,
action: AttachmentAction.FILE_UPLOAD
},
{
id: AttachmentMenuItemId.PDF,
label: 'PDF Files',
icon: FILE_TYPE_ICONS.pdf,
enabledWhen: AttachmentItemEnabledWhen.ALWAYS,
disabledTooltip: 'PDFs will be converted to text. Image-based PDFs may not work properly.',
hasEnabledTooltip: true,
action: AttachmentAction.FILE_UPLOAD
}
];
export const ATTACHMENT_EXTRA_ITEMS: AttachmentMenuItem[] = [];
export const ATTACHMENT_PROMPT_ITEMS: AttachmentMenuItem[] = [
{
id: AttachmentMenuItemId.SYSTEM_MESSAGE,
label: 'System Message',
icon: MessageSquare,
enabledWhen: AttachmentItemEnabledWhen.ALWAYS,
hasEnabledTooltip: true,
action: AttachmentAction.SYSTEM_PROMPT_CLICK
},
{
id: AttachmentMenuItemId.MCP_PROMPT,
label: 'MCP Prompt',
icon: Zap,
enabledWhen: AttachmentItemEnabledWhen.ALWAYS,
action: AttachmentAction.MCP_PROMPT_CLICK,
visibleWhen: AttachmentItemVisibleWhen.HAS_MCP_PROMPTS_SUPPORT
}
];
export const ATTACHMENT_MCP_ITEMS: AttachmentMenuItem[] = [
{
id: AttachmentMenuItemId.MCP_RESOURCES,
label: 'MCP Resources',
icon: FolderOpen,
enabledWhen: AttachmentItemEnabledWhen.ALWAYS,
action: AttachmentAction.MCP_RESOURCES_CLICK,
visibleWhen: AttachmentItemVisibleWhen.HAS_MCP_RESOURCES_SUPPORT
}
];
export const ATTACHMENT_TOOLTIP_TEXT = 'Add files, prompts, tools or MCP Servers';
+22
View File
@@ -0,0 +1,22 @@
export const AUTO_SCROLL_INTERVAL = 100;
// Conversation landing: the page keeps growing after the first bottom pin
// without DOM mutations (content-visibility size realizations, syntax
// highlight passes), so the pin repeats every frame until the height holds
// for this many consecutive frames, bounded by the time cap below.
export const LANDING_STABLE_FRAMES = 10;
export const LANDING_SETTLE_MAX_MS = 1000;
// Chat main view: tight threshold because scroll-here events come from
// discrete assistant-message appends.
export const AUTO_SCROLL_AT_BOTTOM_THRESHOLD = 10;
// Reasoning block: stickier because reasoning fires many small
// incremental DOM writes that easily drift a few pixels off bottom.
export const REASONING_SCROLL_AT_BOTTOM_THRESHOLD_PX = 64;
// Syntax-highlighted code: stickier than the chat main view because line
// wrap reflows while the highlight.js pass settles can drift a few pixels
// off bottom.
export const SYNTAX_CODE_SCROLL_AT_BOTTOM_THRESHOLD_PX = 32;
// Streaming tool output (e.g. exec_shell_command): shell commands produce
// lots of small line writes and the exit-code line appended at the tail
// past the last user-visible frame is what triggers DOM drift, so use a
// threshold generous enough to capture that tail flush.
export const TOOL_RUNTIME_SCROLL_AT_BOTTOM_THRESHOLD_PX = 64;
@@ -0,0 +1,7 @@
import type { BinaryDetectionOptions } from '$lib/types';
export const DEFAULT_BINARY_DETECTION_OPTIONS: BinaryDetectionOptions = {
prefixLength: 1024 * 10, // Check the first 10KB of the string
suspiciousCharThresholdRatio: 0.15, // Allow up to 15% suspicious chars
maxAbsoluteNullBytes: 2
};
@@ -0,0 +1,59 @@
// Registry of built-in and frontend (browser) tools whose renderer
// shows a recognizable icon and friendly label inline in the chat UI.
//
// To add a new built-in tool, add an entry to BUILTIN_TOOL_UI. To give a
// tool a custom title or body renderer, add a dedicated component under
// ChatMessageToolCall/ and route it in ChatMessageToolCallBlock.svelte
// (see ChatMessageToolCallBlockGetDatetime and
// ChatMessageToolCallBlockSearchResults for prior art).
import type { Component } from 'svelte';
import {
Braces,
Clock,
FilePen,
FilePlus,
FileSearch,
FileText,
SearchCode,
Terminal
} from '@lucide/svelte';
import { BuiltInTool, ToolSource } from '$lib/enums';
export interface BuiltinToolUiEntry {
icon: Component;
label: string;
source: ToolSource.BUILTIN | ToolSource.FRONTEND;
}
export const BUILTIN_TOOL_UI: Readonly<Record<BuiltInTool, BuiltinToolUiEntry>> = {
[BuiltInTool.READ_FILE]: { icon: FileText, label: 'Read file', source: ToolSource.BUILTIN },
[BuiltInTool.EDIT_FILE]: { icon: FilePen, label: 'Edit file', source: ToolSource.BUILTIN },
[BuiltInTool.WRITE_FILE]: { icon: FilePlus, label: 'Write file', source: ToolSource.BUILTIN },
[BuiltInTool.FILE_GLOB_SEARCH]: {
icon: FileSearch,
label: 'Search files',
source: ToolSource.BUILTIN
},
[BuiltInTool.GREP_SEARCH]: {
icon: SearchCode,
label: 'Search in files',
source: ToolSource.BUILTIN
},
[BuiltInTool.GET_DATETIME]: { icon: Clock, label: 'Current time', source: ToolSource.BUILTIN },
[BuiltInTool.EXEC_SHELL_COMMAND]: {
icon: Terminal,
label: 'Run command',
source: ToolSource.BUILTIN
},
[BuiltInTool.RUN_JAVASCRIPT]: {
icon: Braces,
label: 'Run JavaScript',
source: ToolSource.FRONTEND
}
} as const;
export function getBuiltinToolUi(toolName: string | undefined): BuiltinToolUiEntry | null {
if (!toolName) return null;
return (BUILTIN_TOOL_UI as Record<string, BuiltinToolUiEntry>)[toolName] ?? null;
}
+54
View File
@@ -0,0 +1,54 @@
/**
* Cache configuration constants
*/
/**
* Default TTL (Time-To-Live) for cache entries in milliseconds
* @default 5 minutes
*/
export const DEFAULT_CACHE_TTL_MS = 5 * 60 * 1000;
/**
* Default maximum number of entries in a cache
* @default 100
*/
export const DEFAULT_CACHE_MAX_ENTRIES = 100;
/**
* TTL for model props cache in milliseconds
* Props don't change frequently, so we can cache them longer
* @default 10 minutes
*/
export const MODEL_PROPS_CACHE_TTL_MS = 10 * 60 * 1000;
/**
* Maximum number of model props to cache
* @default 50
*/
export const MODEL_PROPS_CACHE_MAX_ENTRIES = 50;
/**
* Maximum number of MCP resources to cache
* @default 50
*/
export const MCP_RESOURCE_CACHE_MAX_ENTRIES = 50;
/**
* TTL for MCP resource cache entries in milliseconds
* @default 5 minutes
*/
export const MCP_RESOURCE_CACHE_TTL_MS = 5 * 60 * 1000;
/**
* Maximum number of inactive conversation states to keep in memory
* States for conversations beyond this limit will be cleaned up
* @default 10
*/
export const MAX_INACTIVE_CONVERSATION_STATES = 10;
/**
* Maximum age (in ms) for inactive conversation states before cleanup
* States older than this will be removed during cleanup
* @default 30 minutes
*/
export const INACTIVE_CONVERSATION_STATE_MAX_AGE_MS = 30 * 60 * 1000;
+6
View File
@@ -0,0 +1,6 @@
export const INITIAL_FILE_SIZE = 0;
export const PROMPT_CONTENT_SEPARATOR = '\n\n';
export const CLIPBOARD_CONTENT_QUOTE_PREFIX = '"';
export const PROMPT_TRIGGER_PREFIX = '/';
export const RESOURCE_TRIGGER_PREFIX = '@';
export const NEW_CHAT_DRAFT_KEY = '__new_chat__';
+6
View File
@@ -0,0 +1,6 @@
export const CLI_FLAGS = {
API_KEY: '--api-key',
MCP_PROXY: '--ui-mcp-proxy',
SLOTS: '--slots',
TOOLS: '--tools'
} as const;
@@ -0,0 +1,8 @@
export const CODE_BLOCK_SCROLL_CONTAINER_CLASS = 'code-block-scroll-container';
export const CODE_BLOCK_WRAPPER_CLASS = 'code-block-wrapper';
export const CODE_BLOCK_HEADER_CLASS = 'code-block-header';
export const CODE_BLOCK_ACTIONS_CLASS = 'code-block-actions';
export const CODE_LANGUAGE_CLASS = 'code-language';
export const COPY_CODE_BTN_CLASS = 'copy-code-btn';
export const PREVIEW_CODE_BTN_CLASS = 'preview-code-btn';
export const RELATIVE_CLASS = 'relative';
+24
View File
@@ -0,0 +1,24 @@
export const NEWLINE = '\n';
export const TAB = '\t';
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;
// Whitespace-only empty lines (between start of string and first non-empty line).
// Used by trimCodePadding to drop leading/trailing phantom blank rows from LLM
// payload wrappers without touching internal blank lines.
export const TRIM_LEADING_PADDING_REGEX = /^(?:[ \t]*\n)+/;
export const TRIM_TRAILING_PADDING_REGEX = /(?:\n[ \t]*)+$/;
// Matches either Unix or Windows path separators so `String.split(REGEX)` can
// recover the trailing file-name segment from either `/foo/bar.txt` or
// `C:\foo\bar.txt`. Used wherever a parameter accepts a user-supplied path.
export const FILE_PATH_SEPARATOR_REGEX = /[\\/]/;
// Matches the `text:` prefix that file-type identifiers use to denote a
// plain-text language (e.g. `text:typescript`). Used by tool-call renderers
// to recover the underlying highlight.js language.
export const TEXT_LANGUAGE_PREFIX_REGEX = /^text:/;
@@ -0,0 +1,8 @@
// Half of the card width, matching the w-64 class on the card.
export const CONTEXT_GAUGE_CARD_HALF_WIDTH_PX = 128;
// Minimum distance kept between the card and the form edges.
export const CONTEXT_GAUGE_EDGE_MARGIN_PX = 8;
// Gap between the top of the dial and the bottom edge of the card.
export const CONTEXT_GAUGE_DIAL_GAP_PX = 8;
// Grace delay before closing, letting the pointer travel from dial to card.
export const CONTEXT_GAUGE_CLOSE_GRACE_MS = 150;
@@ -0,0 +1,3 @@
export const CONTEXT_KEY_MESSAGE_EDIT = 'chat-message-edit';
export const CONTEXT_KEY_CHAT_ACTIONS = 'chat-actions';
export const CONTEXT_KEY_CHAT_SETTINGS_CONFIG = 'chat-settings-config';
@@ -0,0 +1,7 @@
// actions accepted by the realtime inference control endpoint (API_CHAT.CONTROL)
// kept separate from the endpoint paths since these are protocol level verbs
export const CONTROL_ACTION = {
END_REASONING: 'reasoning_end'
} as const;
export type ControlAction = (typeof CONTROL_ACTION)[keyof typeof CONTROL_ACTION];
@@ -0,0 +1,3 @@
// First bytes of every ZIP local file header ("PK"). Import detects an archive
// from these bytes rather than from the filename, which the OS may not preserve.
export const ZIP_MAGIC = [0x50, 0x4b];
+26
View File
@@ -0,0 +1,26 @@
export const BOX_BORDER =
'border border-border/30 focus-within:border-border dark:border-border/20 dark:focus-within:border-border';
export const INPUT_CLASSES = `
bg-muted/60 dark:bg-muted/75
${BOX_BORDER}
shadow-sm
outline-none
text-foreground
`;
export const PANEL_CLASSES = `
bg-background
border border-border/30 dark:border-border/20
shadow-sm backdrop-blur-lg!
rounded-t-lg!
`;
export const CHAT_FORM_POPOVER_MAX_HEIGHT = 'max-h-80';
export const DIALOG_SUBMENU_CONTENT = 'w-60';
/** Default Tailwind size class for inline icon components (lucide, etc.). */
export const ICON_CLASS_DEFAULT = 'h-4 w-4';
/** Icon size + spinning animation; used for live-streaming tool indicators. */
export const ICON_CLASS_SPIN = 'h-4 w-4 animate-spin';
+29
View File
@@ -0,0 +1,29 @@
/**
* Database-related constants (IndexedDB, Dexie).
*
* Centralized to ensure consistency across the app and simplify future
* naming changes.
*/
import { STORAGE_APP_NAME } from './storage';
/** IndexedDB database name */
export const DB_NAME = STORAGE_APP_NAME;
/** IndexedDB store / table names */
export const IDXDB_TABLES = {
conversations: 'conversations',
messages: 'messages'
} as const;
/** IndexedDB store schemas */
export const IDXDB_STORE_SCHEMAS = {
conversations: 'id, lastModified, currNode, name',
messages: 'id, convId, type, role, timestamp, parent, children'
} as const;
/** Combined Dexie stores definition — keys are table names, values are schemas */
export const IDXDB_STORES = {
[IDXDB_TABLES.conversations]: IDXDB_STORE_SCHEMAS.conversations,
[IDXDB_TABLES.messages]: IDXDB_STORE_SCHEMAS.messages
} as const;
@@ -0,0 +1,9 @@
// Shared constants for diagram blocks (mermaid and svg) that toggle between a
// rendered view and a source view. The wrapper carries the active mode, css
// drives the visibility, the click handler only flips the attribute.
export const DIAGRAM_VIEW_MODE_ATTR = 'data-view-mode';
export const DIAGRAM_VIEW_RENDERED = 'rendered';
export const DIAGRAM_VIEW_SOURCE = 'source';
export const DIAGRAM_SOURCE_CLASS = 'diagram-source';
export const TOGGLE_SOURCE_BTN_CLASS = 'toggle-source-btn';
+23
View File
@@ -0,0 +1,23 @@
export const ERROR_MESSAGES = {
NETWORK: {
GENERIC: 'Failed to connect to server',
NXDOMAIN: 'Server not found - check server address',
REFUSED: 'Connection refused - server may be offline',
TIMEOUT: 'Request timed out',
UNREACHABLE: 'Server is not running or unreachable'
},
HTTP: {
GENERIC: 'Request failed',
ACCESS_DENIED: 'Access denied',
INTERNAL_ERROR: 'Server error - check server logs',
NOT_FOUND: 'Not found',
TEMPORARILY_UNAVAILABLE: 'Server temporarily unavailable'
}
};
export const HTTP_CODE_TO_STRING: Record<string, string> = {
401: ERROR_MESSAGES.HTTP.ACCESS_DENIED,
403: ERROR_MESSAGES.HTTP.ACCESS_DENIED,
500: ERROR_MESSAGES.HTTP.INTERNAL_ERROR,
503: ERROR_MESSAGES.HTTP.TEMPORARILY_UNAVAILABLE
};
@@ -0,0 +1,2 @@
export const VIEWPORT_GUTTER = 8;
export const MENU_OFFSET = 6;
+8
View File
@@ -0,0 +1,8 @@
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;
/** Default display value when no performance time is available */
export const DEFAULT_PERFORMANCE_TIME = '0s';
+43
View File
@@ -0,0 +1,43 @@
/**
* Icon mappings for file types and model modalities
* Centralized configuration to ensure consistent icon usage across the app
*/
import {
File as FileIcon,
FileText as FileTextIcon,
Image as ImageIcon,
Eye as VisionIcon,
Mic as AudioIcon,
Video as VideoIcon
} from '@lucide/svelte';
import { FileTypeCategory, ModelModality } from '$lib/enums';
export const FILE_TYPE_ICONS = {
[FileTypeCategory.IMAGE]: ImageIcon,
[FileTypeCategory.AUDIO]: AudioIcon,
[FileTypeCategory.VIDEO]: VideoIcon,
[FileTypeCategory.TEXT]: FileTextIcon,
[FileTypeCategory.PDF]: FileIcon
} as const;
export const DEFAULT_FILE_ICON = FileIcon;
export const MODALITY_ICONS = {
[ModelModality.VISION]: VisionIcon,
[ModelModality.AUDIO]: AudioIcon,
[ModelModality.VIDEO]: VideoIcon
} as const;
export const MODALITY_LABELS = {
[ModelModality.VISION]: 'Vision',
[ModelModality.AUDIO]: 'Audio',
[ModelModality.VIDEO]: 'Video'
} as const;
// Shared SVG icon strings for copy and preview buttons
export const COPY_ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-copy-icon lucide-copy"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/></svg>`;
export const PREVIEW_ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-eye lucide-eye-icon"><path d="M2.062 12.345a1 1 0 0 1 0-.69C3.5 7.73 7.36 5 12 5s8.5 2.73 9.938 6.655a1 1 0 0 1 0 .69C20.5 16.27 16.64 19 12 19s-8.5-2.73-9.938-6.655"/><circle cx="12" cy="12" r="3"/></svg>`;
export const CODE_ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-code lucide-code-icon"><path d="m16 18 6-6-6-6"/><path d="m8 6-6 6 6 6"/></svg>`;
+3
View File
@@ -0,0 +1,3 @@
export const MEGAPIXELS_TO_PIXELS = 1_000_000;
export const HEIC_JPEG_QUALITY = 0.85;
+62
View File
@@ -0,0 +1,62 @@
// Central constants export file
// All constants should be imported from '$lib/constants'
export * from './agentic';
export * from './api-endpoints';
export * from './app';
export * from './attachment-labels';
export * from './database';
export * from './reasoning-effort';
export * from './reasoning-effort-tokens';
export * from './recommended-mcp-servers';
export * from './storage';
export * from './attachment-menu';
export * from './auto-scroll';
export * from './context-gauge-popup';
export * from './conversation-import';
export * from './binary-detection';
export * from './built-in-tools';
export * from './cache';
export * from './chat-form';
export * from './cli-flags';
export * from './code-blocks';
export * from './icons';
export * from './code';
export * from './context-keys';
export * from './control-actions';
export * from './css-classes';
export * from './floating-ui-constraints';
export * from './formatters';
export * from './key-value-pairs';
export * from './icons';
export * from './latex-protection';
export * from './literal-html';
export * from './markdown';
export * from './mermaid-blocks';
export * from './svg-blocks';
export * from './diagram-blocks';
export * from './max-bundle-size';
export * from './mcp';
export * from './mcp-form';
export * from './mcp-resource';
export * from './message-export';
export * from './model-id';
export * from './model-loading';
export * from './sse';
export * from './precision';
export * from './processing-info';
export * from './pwa';
export * from './routes';
export * from './sandbox';
export * from './settings-keys';
export * from './settings-registry';
export * from './stream';
export * from './supported-file-types';
export * from './table-html-restorer';
export * from './title-generation';
export * from './tools';
export * from './tooltip-config';
export * from './ui';
export * from './uri-template';
export * from './url';
export * from './viewport';
+30
View File
@@ -0,0 +1,30 @@
/**
* JPEG and EXIF binary format constants for orientation parsing.
*/
/** Bytes of file prefix to scan, the APP1 EXIF segment sits near the start */
export const EXIF_SCAN_BYTE_LIMIT = 128 * 1024;
/** JPEG start of image marker */
export const JPEG_SOI_MARKER = 0xffd8;
/** APP1 segment marker byte, carries the EXIF payload */
export const APP1_MARKER = 0xe1;
/** Start of scan marker byte, compressed data begins and no EXIF follows */
export const SOS_MARKER = 0xda;
/** "Exif" signature opening the APP1 payload, big endian uint32 */
export const EXIF_SIGNATURE = 0x45786966;
/** TIFF byte order mark for little endian ("II") */
export const TIFF_LITTLE_ENDIAN = 0x4949;
/** TIFF magic number following the byte order mark */
export const TIFF_MAGIC = 42;
/** EXIF tag id holding the orientation value */
export const EXIF_ORIENTATION_TAG = 0x0112;
/** Size in bytes of one IFD directory entry */
export const IFD_ENTRY_SIZE = 12;
@@ -0,0 +1,20 @@
/**
* Key-value pair form constraints and sanitization patterns.
*
* Both regexes target characters dangerous in HTTP-header / env-var contexts:
* \x00 null byte (injection)
* \x0A (\n) LF (HTTP header injection / response splitting)
* \x0D (\r) CR (HTTP header injection / response splitting)
* \x01\x08, \x0B\x0C, \x0E\x1F, \x7F other C0/DEL control chars
*
* KEY_UNSAFE_RE additionally strips TAB (\x09); values keep TAB because it is
* a valid header-value continuation character per RFC 7230.
*/
export const KEY_VALUE_PAIR_KEY_MAX_LENGTH = 256;
export const KEY_VALUE_PAIR_VALUE_MAX_LENGTH = 8192;
// eslint-disable-next-line no-control-regex
export const KEY_VALUE_PAIR_UNSAFE_KEY_RE = /[\x00-\x1F\x7F]/g;
// eslint-disable-next-line no-control-regex
export const KEY_VALUE_PAIR_UNSAFE_VALUE_RE = /[\x00-\x08\x0A-\x0D\x0E-\x1F\x7F]/g;
@@ -0,0 +1,125 @@
/**
* Matches common Markdown code blocks to exclude them from further processing (e.g. LaTeX).
* - Fenced: ```...```
* - Inline: `...` (does NOT support nested backticks or multi-backtick syntax)
*
* Note: This pattern does not handle advanced cases like:
* `` `code with `backticks` `` or \\``...\\``
*/
export const CODE_BLOCK_REGEXP = /(```[\s\S]*?```|`[^`\n]+`)/g;
/**
* Matches LaTeX math delimiters \(...\) and \[...\] only when not preceded by a backslash (i.e., not escaped),
* while also capturing code blocks (```, `...`) so they can be skipped during processing.
*
* Uses negative lookbehind `(?<!\\)` to avoid matching \\( or \\[.
* Using the lookbehind pattern `(?<!\\)` we skip matches
* that are preceded by a backslash, e.g.
* `Definitions\\(also called macros)` (title of chapter 20 in The TeXbook)
* or `\\[4pt]` (LaTeX line-break).
*
* group 1: code-block
* group 2: square-bracket
* group 3: round-bracket
*/
export const LATEX_MATH_AND_CODE_PATTERN =
/(```[\S\s]*?```|`.*?`)|(?<!\\)\\\[([\S\s]*?[^\\])\\]|(?<!\\)\\\((.*?)\\\)/g;
/** Regex to capture the content of a $$...\\\\...$$ block (display-formula with line-break) */
export const LATEX_LINEBREAK_REGEXP = /\$\$([\s\S]*?\\\\[\s\S]*?)\$\$/;
/**
* Matches the unescaped `\[...\]` display-math delimiter and surrounding
* context so callers can insert line-breaks around the placeholder or convert
* to inline when the formula has a non-empty trailing context (e.g. a table
* cell that opens with `\[` and closes with content after `\]`).
*
* group 1: prefix before `\[`
* group 2: formula body
* group 3: trailing context after `\]`
*/
export const LATEX_DISPLAY_BLOCK_REGEXP = /([\S].*?)\\\[([\s\S]*?)\\\](.*)/g;
/**
* Cheap gate for `preprocessLaTeX`. Every transformation it performs is triggered
* by a `$` (inline/display math, currency escaping) or a backslash escape
* (`\(`, `\[`, `\ce{`, `\pu{`). Text containing neither is returned untouched, so
* this lets the caller skip the whole protect/restore pipeline.
*/
export const LATEX_TRIGGER_REGEXP = /[$\\]/;
/** Inline LaTeX math delimiter (the dollar sign). */
export const LATEX_INLINE_DELIMITER = '$';
/** Display LaTeX math delimiter (paired dollar signs). */
export const LATEX_DISPLAY_DELIMITER = '$$';
/** Matches a single non-whitespace character. */
export const LATEX_NON_WHITESPACE_REGEXP = /\S/;
/** Matches a character that may appear adjacent to `$`, indicating a non-TeX
* context such as an identifier (`var$`, `$var`), currency ($5), or code. */
export const LATEX_NEIGHBOR_CHAR_REGEXP = /[A-Za-z0-9_$-]/;
/** Matches a single digit (used to detect currency-like `$5`). */
export const LATEX_DIGIT_REGEXP = /[0-9]/;
/** Matches the leading blockquote prefix (`> ` or `>`) on a markdown line. */
export const LATEX_BLOCKQUOTE_PREFIX_REGEXP = /^(>\s*)/;
/** Matches the placeholder inserted by the protect/restore pipeline for a
* protected LaTeX expression. Group 1 is the index into `latexExpressions`. */
export const LATEX_PLACEHOLDER_REGEXP = /<<LATEX_(\d+)>>/g;
/** Matches the placeholder inserted by the protect/restore pipeline for a
* protected code block. Group 1 is the index into `codeBlocks`. */
export const CODE_BLOCK_PLACEHOLDER_REGEXP = /<<CODE_BLOCK_(\d+)>>/g;
/** Matches a `$` immediately followed by a digit, which is treated as a
* currency amount (e.g. `$5`) and escaped to `\$5` so it isn't parsed as math. */
export const LATEX_CURRENCY_DOLLAR_REGEXP = /\$(?=\d)/g;
/** Captures remaining `$$...$$`, `\[...\]`, `\(...\)` (only unescaped via
* `(?<!\\)`) after the display-block pass has run. Group 1 holds the
* matched formula. */
export const LATEX_PROTECT_REGEXP =
/(\$\$[\s\S]*?\$\$|(?<!\\)\\\[[\s\S]*?\\\]|(?<!\\)\\\(.*?\\\))/g;
/** Matches unescaped inline `\(...\)` (at least one char inside) used to
* convert `\(` → `$` after the protect pass. */
export const LATEX_INLINE_CONVERT_REGEXP = /(?<!\\)\\\((.+?)\\\)/g;
/** Matches unescaped display `\[...\]` used to convert `\[` → `$$`
* after the protect pass. */
export const LATEX_DISPLAY_CONVERT_REGEXP = /(?<!\\)\\\[([\s\S]*?)\\\]/g;
/** `\(` — opens an inline LaTeX math block. */
export const LATEX_INLINE_OPEN = '\\(';
/** `\)` — closes an inline LaTeX math block. */
export const LATEX_INLINE_CLOSE = '\\)';
/** `\[` — opens a display LaTeX math block. */
export const LATEX_DISPLAY_OPEN = '\\[';
/** `\]` — closes a display LaTeX math block. */
export const LATEX_DISPLAY_CLOSE = '\\]';
/** `\` — the LaTeX escape character. */
export const LATEX_BACKSLASH = '\\';
/** `\$` — dollar sign escaped so it isn't parsed as math (used to disambiguate
* currency amounts like `$5`). */
export const LATEX_CURRENCY_ESCAPE = '\\$';
/** `\ce{` — mhchem chemistry command prefix. */
export const LATEX_MHCHEM_CE = '\\ce{';
/** `\pu{` — mhchem physics-unit command prefix. */
export const LATEX_MHCHEM_PU = '\\pu{';
/** map from mchem-regexp to replacement */
export const MHCHEM_PATTERN_MAP: readonly [RegExp, string][] = [
[/(\s)\$\\ce{/g, '$1$\\\\ce{'],
[/(\s)\$\\pu{/g, '$1$\\\\pu{']
] as const;
@@ -0,0 +1,15 @@
export const LINE_BREAK = /\r?\n/;
export const PHRASE_PARENTS = new Set([
'paragraph',
'heading',
'emphasis',
'strong',
'delete',
'link',
'linkReference',
'tableCell'
]);
export const NBSP = '\u00a0';
export const TAB_AS_SPACES = NBSP.repeat(4);
+5
View File
@@ -0,0 +1,5 @@
export const IMAGE_NOT_ERROR_BOUND_SELECTOR = 'img:not([data-error-bound])';
export const DATA_ERROR_BOUND_ATTR = 'errorBound';
export const DATA_ERROR_HANDLED_ATTR = 'errorHandled';
export const BOOL_TRUE_STRING = 'true';
export const BOOL_FALSE_STRING = 'false';
@@ -0,0 +1 @@
export const MAX_BUNDLE_SIZE = 2 * 1024 * 1024;
+2
View File
@@ -0,0 +1,2 @@
export const MCP_SERVER_URL_PLACEHOLDER = 'https://mcp.example.com/sse';
export const MIN_AUTOCOMPLETE_INPUT_LENGTH = 1;
@@ -0,0 +1,55 @@
import { MimeTypeImage } from '$lib/enums';
// File extension patterns for resource type detection
export const IMAGE_FILE_EXTENSION_REGEX = /\.(png|jpg|jpeg|gif|svg|webp)$/i;
export const CODE_FILE_EXTENSION_REGEX =
/\.(js|ts|json|yaml|yml|xml|html|css|py|rs|go|java|cpp|c|h|rb|sh|toml)$/i;
export const TEXT_FILE_EXTENSION_REGEX = /\.(txt|md|log)$/i;
// URI protocol prefix pattern
export const PROTOCOL_PREFIX_REGEX = /^[a-z]+:\/\//;
// File extension regex for display name extraction
export const FILE_EXTENSION_REGEX = /\.[^.]+$/;
// Separator regex for splitting display names (kebab-case/snake_case)
export const DISPLAY_NAME_SEPARATOR_REGEX = /[-_]/;
// Regex for matching base64-encoded data URIs
export const DATA_URI_BASE64_REGEX = /^data:([^;]+);base64,([A-Za-z0-9+/]+=*)$/;
// Prefix for MCP attachment filenames
export const MCP_ATTACHMENT_NAME_PREFIX = 'mcp-attachment';
// Prefix for MCP resource attachment IDs
export const MCP_RESOURCE_ATTACHMENT_ID_PREFIX = 'res';
// Default file extension for unknown image types
export const DEFAULT_IMAGE_EXTENSION = 'img';
// Default filename for resource content downloads
export const DEFAULT_RESOURCE_FILENAME = 'resource.txt';
// Path separator for resource URI parsing
export const PATH_SEPARATOR = '/';
// Separator for joining text content from multiple resource parts
export const RESOURCE_TEXT_CONTENT_SEPARATOR = '\n\n';
// Fallback text for unknown content types
export const RESOURCE_UNKNOWN_TYPE = 'unknown type';
// Label prefix for binary blob content
export const BINARY_CONTENT_LABEL = 'Binary content';
/**
* Mapping from image MIME types to file extensions.
* Used for generating attachment filenames from MIME types.
*/
export const IMAGE_MIME_TO_EXTENSION: Record<string, string> = {
[MimeTypeImage.JPEG]: 'jpg',
[MimeTypeImage.JPG]: 'jpg',
[MimeTypeImage.PNG]: 'png',
[MimeTypeImage.GIF]: 'gif',
[MimeTypeImage.WEBP]: 'webp'
} as const;
+103
View File
@@ -0,0 +1,103 @@
import { Zap, Globe, Radio } from '@lucide/svelte';
import { MCPTransportType } from '$lib/enums';
import type { ClientCapabilities, Implementation } from '$lib/types';
import type { Component } from 'svelte';
import { MimeTypeImage } from '$lib/enums/files.enums';
export const DEFAULT_CLIENT_VERSION = '1.0.0';
export const MCP_CLIENT_NAME = 'llama-ui-mcp';
export const DEFAULT_IMAGE_MIME_TYPE = MimeTypeImage.PNG;
/** MIME types considered safe for rendering MCP server icons */
export const MCP_ALLOWED_ICON_MIME_TYPES = new Set([
MimeTypeImage.PNG,
MimeTypeImage.JPEG,
MimeTypeImage.JPG,
MimeTypeImage.SVG,
MimeTypeImage.WEBP,
MimeTypeImage.ICO,
MimeTypeImage.ICO_MICROSOFT
]);
/**
* MCP specification version this client targets.
* Update when the upstream MCP spec introduces a new stable version:
* https://spec.modelcontextprotocol.io/
*/
export const MCP_PROTOCOL_VERSION = '2025-06-18';
export const DEFAULT_MCP_CONFIG = {
protocolVersion: MCP_PROTOCOL_VERSION,
capabilities: { tools: { listChanged: true } } as ClientCapabilities,
clientInfo: { name: MCP_CLIENT_NAME, version: DEFAULT_CLIENT_VERSION } as Implementation,
requestTimeoutSeconds: 300, // 5 minutes for long-running tools
connectionTimeoutMs: 10_000 // 10 seconds for connection establishment
} as const;
export const MCP_SERVER_ID_PREFIX = 'LlamaUI-MCP-Server';
export const MCP_RECONNECT_INITIAL_DELAY = 1000;
export const MCP_RECONNECT_BACKOFF_MULTIPLIER = 2;
export const MCP_RECONNECT_MAX_DELAY = 30000;
/** Per-attempt timeout for a single reconnection attempt before giving up and backing off. */
export const MCP_RECONNECT_ATTEMPT_TIMEOUT_MS = 15_000;
/** Maximum number of MCP server avatars to display in the chat form */
export const MAX_DISPLAYED_MCP_AVATARS = 4;
/** Expected count when two theme-less icons represent a light/dark pair */
export const EXPECTED_THEMED_ICON_PAIR_COUNT = 2;
/** CORS proxy URL query parameter name */
export const CORS_PROXY_URL_PARAM = 'url';
/** Header prefix for headers that should be forwarded by the CORS proxy */
export const CORS_PROXY_HEADER_PREFIX = 'x-llama-server-proxy-header-';
/** Number of trailing characters to keep visible when partially redacting mcp-session-id */
export const MCP_SESSION_ID_VISIBLE_CHARS = 5;
/** Partial-redaction rules for MCP headers: header name -> visible trailing chars */
export const MCP_PARTIAL_REDACT_HEADERS = new Map<string, number>([
['mcp-session-id', MCP_SESSION_ID_VISIBLE_CHARS]
]);
/** Bearer scheme prefix used for Authorization headers (RFC 6750) */
export const BEARER_PREFIX = 'Bearer ';
/** Canonical casing for the Authorization header (RFC 7235) */
export const AUTHORIZATION_HEADER = 'Authorization';
/** Content-Type HTTP header name */
export const CONTENT_TYPE_HEADER = 'Content-Type';
/** Header names whose values should be redacted in diagnostic logs */
export const REDACTED_HEADERS = new Set([
'authorization',
'api-key',
'cookie',
'mcp-session-id',
'proxy-authorization',
'set-cookie',
'x-auth-token',
'x-api-key'
]);
/** Human-readable labels for MCP transport types */
export const MCP_TRANSPORT_LABELS: Record<MCPTransportType, string> = {
[MCPTransportType.WEBSOCKET]: 'WebSocket',
[MCPTransportType.STREAMABLE_HTTP]: 'HTTP',
[MCPTransportType.SSE]: 'SSE'
};
/** Icon components for MCP transport types */
export const MCP_TRANSPORT_ICONS: Record<MCPTransportType, Component> = {
[MCPTransportType.WEBSOCKET]: Zap,
[MCPTransportType.STREAMABLE_HTTP]: Globe,
[MCPTransportType.SSE]: Radio
};
/** Standard SSE endpoint path indicators */
export const MCP_SSE_ENDPOINT = '/sse';
export const MCP_SSE_ENDPOINT_SLASH = '/sse/';
export const MCP_SSE_ENDPOINT_QUERY = '/sse?';
@@ -0,0 +1,9 @@
export const MERMAID_WRAPPER_CLASS = 'mermaid-block-wrapper';
export const MERMAID_SCROLL_CONTAINER_CLASS = 'mermaid-scroll-container';
export const MERMAID_BLOCK_CLASS = 'mermaid';
export const MERMAID_LANGUAGE = 'mermaid';
export const MERMAID_SYNTAX_ATTR = 'data-mermaid-syntax';
export const MERMAID_ID_ATTR = 'data-mermaid-id';
export const MERMAID_RENDERED_ATTR = 'data-mermaid-rendered';
@@ -0,0 +1,23 @@
// Conversation filename constants
// Length of the trimmed conversation ID in the filename
export const EXPORT_CONV_ID_TRIM_LENGTH = 8;
// Maximum length of the sanitized conversation name snippet
export const EXPORT_CONV_NAME_SUFFIX_MAX_LENGTH = 20;
// Characters to keep in the ISO timestamp. 19 keeps 2026-01-01T00:00:00
export const ISO_TIMESTAMP_SLICE_LENGTH = 19;
// Producer marker carried by the session record of a JSONL export
export const SESSION_HARNESS = 'llama.app';
// Replacements for making the conversation title filename-friendly
export const NON_ALPHANUMERIC_REGEX = /[^a-z0-9]/gi;
export const EXPORT_CONV_NONALNUM_REPLACEMENT = '_';
export const MULTIPLE_UNDERSCORE_REGEX = /_+/g;
// Replacements to the ISO date for use in the export filename
export const ISO_DATE_TIME_SEPARATOR = 'T';
export const ISO_DATE_TIME_SEPARATOR_REPLACEMENT = '_';
export const ISO_TIME_SEPARATOR = ':';
export const ISO_TIME_SEPARATOR_REPLACEMENT = '-';
+46
View File
@@ -0,0 +1,46 @@
/** Sentinel value returned by `indexOf` when a substring is not found. */
export const MODEL_ID_NOT_FOUND = -1;
/** Separates `<org>` from `<model>` in a model ID, e.g. `org/ModelName`. */
export const MODEL_ID_ORG_SEPARATOR = '/';
/** Separates named segments within the model path, e.g. `ModelName-7B-GGUF`. */
export const MODEL_ID_SEGMENT_SEPARATOR = '-';
/** Separates the model path from the quantization tag, e.g. `model:Q4_K_M`. */
export const MODEL_ID_QUANTIZATION_SEPARATOR = ':';
/**
* Matches a quantization/precision segment, e.g. `Q4_K_M`, `IQ4_XS`, `F16`, `BF16`, `MXFP4`.
* Case-insensitive to handle both uppercase and lowercase inputs.
*/
export const MODEL_QUANTIZATION_SEGMENT_RE =
/^(I?Q\d+(_[A-Z0-9]+)*|F\d+|BF\d+|MXFP\d+(_[A-Z0-9]+)*)$/i;
/**
* Matches prefix for custom quantization types, e.g. `UD-Q8_K_XL`.
*/
export const MODEL_CUSTOM_QUANTIZATION_PREFIX_RE = /^UD$/i;
/**
* Matches a parameter-count segment, e.g. `7B`, `1.5b`, `120M`.
* The optional leading `E` covers effective-parameter sizes, e.g. Gemma's
* `E2B`/`E4B` (MatFormer models sized by resident params).
*/
export const MODEL_PARAMS_RE = /^[Ee]?\d+(\.\d+)?[BbMmKkTt]$/;
/**
* Matches an activated-parameter-count segment, e.g. `A10B`, `a2.4b`.
* The leading `A`/`a` distinguishes it from a regular params segment.
*/
export const MODEL_ACTIVATED_PARAMS_RE = /^[Aa]\d+(\.\d+)?[BbMmKkTt]$/;
/**
* Container format segments to exclude from tags (every model uses these).
*/
export const MODEL_IGNORED_SEGMENTS = new Set(['GGUF', 'GGML']);
/**
* Matches a trailing weight file extension, e.g. `model.gguf` -> `model`.
*/
export const MODEL_WEIGHT_EXTENSION_RE = /\.(gguf|ggml)$/i;
@@ -0,0 +1,14 @@
/**
* Labels shown while a model loads, keyed by the stage reported on /models/sse.
*/
export const MODEL_LOAD_STAGE_LABELS: Record<ApiModelLoadStage, string> = {
text_model: 'Loading weights',
spec_model: 'Loading draft',
mmproj_model: 'Loading projector'
};
/**
* Share of the bar reserved for each load phase after text_model.
* text_model fills the rest, so a plain model reaches 100% on its own.
*/
export const MODEL_LOAD_TAIL_SHARE = 0.1;
+2
View File
@@ -0,0 +1,2 @@
export const PRECISION_MULTIPLIER = 1000000;
export const PRECISION_DECIMAL_PLACES = 6;
@@ -0,0 +1,8 @@
export const PROCESSING_INFO_TIMEOUT = 2000;
/**
* Statistics units labels
*/
export const STATS_UNITS = {
TOKENS_PER_SECOND: 't/s'
} as const;
+361
View File
@@ -0,0 +1,361 @@
/**
* Centralized PWA constants to avoid magic strings, regexes, and duplicated
* definitions across the codebase.
*/
import { APP_NAME } from './app';
export const MEDIA_QUERIES = {
PREFERS_DARK: '(prefers-color-scheme: dark)',
PREFERS_LIGHT: '(prefers-color-scheme: light)',
DISPLAY_MODE_STANDALONE: '(display-mode: standalone)'
} as const;
export const THEME_COLORS = {
LIGHT: '#ffffff',
DARK: '#0d0d0d',
ACCENT_BLUE: '#2563eb',
ACCENT_BLUE_HOVER: '#1d4ed8',
BACKGROUND_LIGHT: 'white',
BACKGROUND_DARK: '#111111',
TITLE_UPDATE_ALERT: {
BORDER_LIGHT: 'zinc-200',
BORDER_DARK: 'zinc-700',
BG_LIGHT: 'white',
BG_DARK: 'zinc-800',
TEXT_LIGHT: 'zinc-500',
TEXT_DARK: 'zinc-400'
}
} as const;
export const FAVICON_PATHS = {
ICO_LIGHT: 'favicon.ico',
ICO_DARK: 'favicon-dark.ico',
SVG_LIGHT: 'favicon.svg',
SVG_DARK: 'favicon-dark.svg'
} as const;
// Substituted for `currentColor` in src/lib/assets/logo.svg when generating
// the light/dark static sources consumed by the PWA asset generator.
export const FAVICON_COLORS = {
LIGHT: '#111111',
DARK: '#fafafa'
} as const;
export const FAVICON_SELECTORS = {
ICO_48X48: 'link[rel="icon"][sizes="48x48"]',
SVG_ANY: 'link[rel="icon"][type="image/svg+xml"]'
} as const;
export const APPLE_ASSETS = {
TOUCH_ICON: 'apple-touch-icon-180x180.png'
} as const;
export const PWA_MANIFEST = {
name: APP_NAME,
short_name: APP_NAME,
description: 'Local AI chat interface powered by llama.cpp',
start_url: './',
display: 'standalone' as const,
background_color: THEME_COLORS.BACKGROUND_LIGHT,
theme_color: THEME_COLORS.BACKGROUND_LIGHT,
icons: [
{ src: 'pwa-64x64.png', sizes: '64x64', type: 'image/png' },
{ src: 'pwa-192x192.png', sizes: '192x192', type: 'image/png' },
{ src: 'pwa-512x512.png', sizes: '512x512', type: 'image/png', purpose: 'any' as const },
{
src: 'maskable-icon-512x512.png',
sizes: '512x512',
type: 'image/png',
purpose: 'maskable' as const
}
]
};
export const PWA_ICON_PATHS = {
PWA_64: '/pwa-64x64.png',
PWA_192: '/pwa-192x192.png',
PWA_512: '/pwa-512x512.png',
MASKABLE_512: '/maskable-icon-512x512.png'
} as const;
/** Apple device dimensions (logical points) and DPR, from Apple HIG. */
export const APPLE_DEVICES = {
// iPhones (DPR 3)
'1170x2532': { width: 390, height: 844, dpr: 3 }, // iPhone 13, 15
'1179x2556': { width: 393, height: 852, dpr: 3 }, // iPhone 14, 15 Pro, 16
'1206x2622': { width: 402, height: 874, dpr: 3 }, // iPhone 16 Plus, 16e
'1284x2778': { width: 428, height: 926, dpr: 3 }, // iPhone 15 Plus
'1290x2796': { width: 430, height: 932, dpr: 3 }, // iPhone 15 Pro Max, 16 Pro
'1320x2868': { width: 440, height: 956, dpr: 3 }, // iPhone 16 Pro Max
'750x1334': { width: 375, height: 667, dpr: 2 }, // iPhone 6/7/8, 14
'640x1136': { width: 320, height: 568, dpr: 2 }, // iPhone 6/7/8 Plus
// iPads (DPR 2)
'1668x2388': { width: 834, height: 1194, dpr: 2 }, // iPad Air 11", iPad 11"
'2048x2732': { width: 1024, height: 1366, dpr: 2 }, // iPad Pro 12.9"
'1640x2360': { width: 820, height: 1180, dpr: 2 }, // iPad Air 10.9"
'1032x1376': { width: 1032, height: 1376, dpr: 2 }, // iPad Air 13"
'744x1133': { width: 376, height: 573, dpr: 2 } // iPad mini 8.3"
} as const;
export type AppleDeviceKey = keyof typeof APPLE_DEVICES;
export const PWA_FILE_PATHS = {
MANIFEST: '/manifest.webmanifest',
SERVICE_WORKER: '/sw.js',
VERSION: '/version.json',
WORKBOX: '/workbox-<hash>.js'
} as const;
// Used by the server middleware to skip API key validation.
// Keep in sync with tools/server/server-http.cpp public_endpoints list.
export const PUBLIC_ENDPOINTS = [
'/health',
'/v1/health',
'/models',
'/v1/models',
'/props',
'/metrics',
'/',
'/index.html',
'/favicon.ico',
'/favicon-dark.ico',
'/favicon.svg',
'/favicon-dark.svg',
'/pwa-64x64.png',
'/pwa-192x192.png',
'/pwa-512x512.png',
'/maskable-icon-512x512.png',
'/apple-touch-icon-180x180.png',
'/apple-splash-portrait-640x1136.png',
'/apple-splash-landscape-640x1136.png',
'/apple-splash-portrait-750x1334.png',
'/apple-splash-landscape-750x1334.png',
'/apple-splash-portrait-1170x2532.png',
'/apple-splash-landscape-1170x2532.png',
'/apple-splash-portrait-1179x2556.png',
'/apple-splash-landscape-1179x2556.png',
'/apple-splash-portrait-1206x2622.png',
'/apple-splash-landscape-1206x2622.png',
'/apple-splash-portrait-1284x2778.png',
'/apple-splash-landscape-1284x2778.png',
'/apple-splash-portrait-1290x2796.png',
'/apple-splash-landscape-1290x2796.png',
'/apple-splash-portrait-1320x2868.png',
'/apple-splash-landscape-1320x2868.png',
'/apple-splash-portrait-1488x2266.png',
'/apple-splash-landscape-1488x2266.png',
'/apple-splash-portrait-1640x2360.png',
'/apple-splash-landscape-1640x2360.png',
'/apple-splash-portrait-1668x2388.png',
'/apple-splash-landscape-1668x2388.png',
'/apple-splash-portrait-2048x2732.png',
'/apple-splash-landscape-2048x2732.png',
'/apple-splash-portrait-dark-640x1136.png',
'/apple-splash-landscape-dark-640x1136.png',
'/apple-splash-portrait-dark-750x1334.png',
'/apple-splash-landscape-dark-750x1334.png',
'/apple-splash-portrait-dark-1170x2532.png',
'/apple-splash-landscape-dark-1170x2532.png',
'/apple-splash-portrait-dark-1179x2556.png',
'/apple-splash-landscape-dark-1179x2556.png',
'/apple-splash-portrait-dark-1206x2622.png',
'/apple-splash-landscape-dark-1206x2622.png',
'/apple-splash-portrait-dark-1284x2778.png',
'/apple-splash-landscape-dark-1284x2778.png',
'/apple-splash-portrait-dark-1290x2796.png',
'/apple-splash-landscape-dark-1290x2796.png',
'/apple-splash-portrait-dark-1320x2868.png',
'/apple-splash-landscape-dark-1320x2868.png',
'/apple-splash-portrait-dark-1488x2266.png',
'/apple-splash-landscape-dark-1488x2266.png',
'/apple-splash-portrait-dark-1640x2360.png',
'/apple-splash-landscape-dark-1640x2360.png',
'/apple-splash-portrait-dark-1668x2388.png',
'/apple-splash-landscape-dark-1668x2388.png',
'/apple-splash-portrait-dark-2048x2732.png',
'/apple-splash-landscape-dark-2048x2732.png',
'/manifest.webmanifest',
'/sw.js',
'/version.json',
'/workbox-<hash>.js'
] as const;
export const BUILD_CONFIG = {
OUTPUT_DIR: './dist',
GUIDE_COMMENT: `
<!--
This is a static build of the frontend.
It is automatically generated by the build process.
Do not edit this file directly.
To make changes, refer to the "Web UI" section in the README.
-->
`.trim()
} as const;
export const REGEX_PATTERNS = {
SPLASH_FILE: /^apple-splash-(portrait|landscape)-(dark-)?(\d+)x(\d+)\.png$/,
HEAD_CLOSE: /\t*<\/head>/
} as const;
// Device names used by @vite-pwa/assets-generator for splash screen generation.
// Keep in sync with pwa-assets.config.ts.
export const PWA_GENERATOR_DEVICES = [
'iPhone 13',
'iPhone 13 Pro',
'iPhone 13 Pro Max',
'iPhone 14',
'iPhone 14 Plus',
'iPhone 14 Pro',
'iPhone 14 Pro Max',
'iPhone 15',
'iPhone 15 Plus',
'iPhone 15 Pro',
'iPhone 15 Pro Max',
'iPhone 16',
'iPhone 16 Plus',
'iPhone 16 Pro',
'iPhone 16 Pro Max',
'iPhone 16e',
'iPhone SE 4"',
'iPhone SE 4.7"',
'iPad 11"',
'iPad Air 10.9"',
'iPad Air 11"',
'iPad Air 13"',
'iPad Pro 11"',
'iPad Pro 12.9"',
'iPad mini 8.3"'
] as const;
// PWA assets generator configuration — used by pwa-assets.config.ts
// FAVICON_PADDING: fraction (0..1) of the icon reserved as equal margin on
// each side. Applied to icon PNG/ICO outputs by @vite-pwa/assets-generator and
// post-processed into the static favicon.svg so the in-app logo (which reads
// src/lib/assets/logo.svg directly) is unaffected.
export const PWA_ASSET_GENERATOR = {
LINK_PRESET: '2023',
FAVICON_PADDING: 0.04,
SPLASH_PADDING: 0.75,
FIT_MODE: 'contain',
ADD_MEDIA_SCREEN: true,
BASE_PATH: './',
XHTML: false,
PNG_COMPRESSION_LEVEL: 9,
PNG_QUALITY: 60,
DARK_PREFIX: 'dark-'
} as const;
export const CACHE_SETTINGS = {
IMMUTABLE_MAX_AGE_SECONDS: 31536000,
API_CACHE_MAX_AGE_SECONDS: 60 * 60 * 24,
API_CACHE_MAX_ENTRIES: 50,
MAX_FILE_SIZE_BYTES: 10 * 1024 * 1024
} as const;
export const GLOB_PATTERNS: string[] = [
'**/*.{js,css,html,ico,svg,png,webp,woff,woff2,json,webmanifest}'
];
export const SW_CONFIG = {
CHECK_INTERVAL_MS: 60000,
UPDATE_FETCH_OPTIONS: {
CACHE: 'no-store',
HEADERS: {
CACHE: 'no-store',
CACHE_CONTROL: 'no-cache'
}
}
} as const;
// Runtime caching configuration for Workbox
export const RUNTIME_CACHING = {
HANDLER: 'NetworkFirst',
CACHE_NAME: 'api-cache'
} as const;
// Workbox runtime caching patterns
export const API_CACHING_PATTERNS = {
V1_API: /^\/v1\/.*/,
STATIC_API: /^\/(health|props|models|tools|slots|cors-proxy).*/
} as const;
// SvelteKit PWA plugin options
export const PWA_KIT_OPTIONS = {} as const;
export const APPLE_META_TAGS = {
MOBILE_WEB_APP_CAPABLE: { name: 'apple-mobile-web-app-capable', content: 'yes' },
STATUS_BAR_STYLE: { name: 'apple-mobile-web-app-status-bar-style', content: 'black-translucent' },
MOBILE_WEB_APP_TITLE: { name: 'apple-mobile-web-app-title' }
} as const;
// Splash screen HTML link tag prefix used by generateSplashScreenLinks
export const SPLASH_LINK = {
HTML: '<link rel="apple-touch-startup-image"',
DARK_MEDIA_SUFFIX: ' and (prefers-color-scheme: dark)'
} as const;
// SvelteKit PWA plugin configuration — used by @vite.config.ts
import type { SvelteKitPWAOptions } from '@vite-pwa/sveltekit';
export const SVELTEKIT_PWA_OPTIONS: SvelteKitPWAOptions = {
// Strategy: generateSW - the plugin generates a service worker automatically
// using Workbox. For a custom SW, use 'injectManifest' instead.
// Manifest configuration
manifest: PWA_MANIFEST,
// Workbox configuration for generateSW strategy
workbox: {
// Match all static assets in the build output.
// Uses '**/' because SvelteKit outputs files under _app/immutable/
// subdirectories.
globPatterns: GLOB_PATTERNS,
maximumFileSizeToCacheInBytes: CACHE_SETTINGS.MAX_FILE_SIZE_BYTES,
// Prevent @vite-pwa/sveltekit from auto-adding a NavigationRoute by
// setting navigateFallback to empty string. This keeps the service
// worker from intercepting direct browser navigation to server API
// endpoints (e.g. /slots, /models, /v1/models) which should return
// JSON, not the SPA HTML shell. The server's own static-file fallback
// handles non-API navigation to index.html for the SPA router.
navigateFallback: '',
// Runtime caching for API calls - use NetworkFirst so APIs are always fresh
runtimeCaching: [
{
urlPattern: API_CACHING_PATTERNS.V1_API,
handler: RUNTIME_CACHING.HANDLER,
options: {
cacheName: RUNTIME_CACHING.CACHE_NAME,
expiration: {
maxEntries: CACHE_SETTINGS.API_CACHE_MAX_ENTRIES,
maxAgeSeconds: CACHE_SETTINGS.API_CACHE_MAX_AGE_SECONDS
}
}
},
{
urlPattern: API_CACHING_PATTERNS.STATIC_API,
handler: RUNTIME_CACHING.HANDLER,
options: {
cacheName: RUNTIME_CACHING.CACHE_NAME,
expiration: {
maxEntries: CACHE_SETTINGS.API_CACHE_MAX_ENTRIES,
maxAgeSeconds: CACHE_SETTINGS.API_CACHE_MAX_AGE_SECONDS
}
}
}
]
},
devOptions: {
enabled: true,
suppressWarnings: true
},
// SvelteKit-specific options
kit: {
// Include version file for proper cache invalidation
includeVersionFile: true
}
};
@@ -0,0 +1,12 @@
import { ReasoningEffort } from '$lib/enums';
/**
* Reasoning effort to token budget mapping.
* Maps the ReasoningEffort enum values to concrete token counts for the server.
*/
export const REASONING_EFFORT_TOKENS: Record<string, number> = {
[ReasoningEffort.LOW]: 512,
[ReasoningEffort.MEDIUM]: 2048,
[ReasoningEffort.HIGH]: 8192,
[ReasoningEffort.MAX]: -1 // unlimited
};
@@ -0,0 +1,24 @@
import { ReasoningEffort } from '$lib/enums';
import type { ReasoningEffortLevel } from '$lib/types';
/**
* Reasoning effort UI labels.
* Keys match the ReasoningEffort enum values for type-safe lookups.
*/
export const REASONING_EFFORT_LABELS: Record<string, string> = {
[ReasoningEffort.DEFAULT]: 'Default',
[ReasoningEffort.OFF]: 'Off',
[ReasoningEffort.LOW]: 'Low',
[ReasoningEffort.MEDIUM]: 'Medium',
[ReasoningEffort.HIGH]: 'High',
[ReasoningEffort.MAX]: 'Max'
};
export const REASONING_EFFORT_LEVELS: ReasoningEffortLevel[] = [
{ value: ReasoningEffort.DEFAULT, label: 'Default' },
{ value: ReasoningEffort.OFF, label: 'Off' },
{ value: ReasoningEffort.LOW, label: 'Low' },
{ value: ReasoningEffort.MEDIUM, label: 'Medium' },
{ value: ReasoningEffort.HIGH, label: 'High' },
{ value: ReasoningEffort.MAX, label: 'Max', hasInfo: true }
];
@@ -0,0 +1,38 @@
import type { RecommendedMCPServer } from '$lib/types';
// Suggested MCP servers shown as opt-in cards in the "Add New Server" dialog.
// Rendering these cards never reaches the upstream domain - favicons come
// from local bundles in static/recommended-mcp/ and the URL is only used
// after the user clicks Add.
export const RECOMMENDED_MCP_SERVERS: RecommendedMCPServer[] = [
{
id: 'exa',
name: 'Exa',
description: 'Search the web and fetch full page content as clean markdown.',
url: 'https://mcp.exa.ai/mcp',
iconUrl: '/recommended-mcp/exa.ico'
},
{
id: 'huggingface',
name: 'Hugging Face',
description: 'Search and browse AI models, datasets, spaces, and docs on the Hugging Face Hub.',
url: 'https://huggingface.co/mcp',
iconUrl: '/recommended-mcp/huggingface.ico'
},
{
id: 'github',
name: 'GitHub',
description: 'Search repositories, issues, pull requests and interact with code on GitHub.',
url: 'https://api.githubcopilot.com/mcp',
iconUrlLight: '/recommended-mcp/github-light.png',
iconUrlDark: '/recommended-mcp/github-dark.png',
needsAuthorization: true
},
{
id: 'context7',
name: 'Context7',
description: 'Browse up-to-date documentation and code examples for libraries and frameworks.',
url: 'https://mcp.context7.com/mcp',
iconUrl: '/recommended-mcp/context7.png'
}
];
+28
View File
@@ -0,0 +1,28 @@
export const NEW_CHAT_PARAM = 'new_chat';
/** Settings section slugs — used for routes and navigation. */
export const SETTINGS_SECTION_SLUGS = {
GENERAL: 'general',
DISPLAY: 'display',
SAMPLING: 'sampling',
PENALTIES: 'penalties',
AGENTIC: 'agentic',
DEVELOPER: 'developer',
TOOLS: 'tools',
IMPORT_EXPORT: 'import-export'
} as const;
export const ROUTES = {
/** Root — start of the app. */
START: '#/',
/** New chat — root with new chat query param. */
NEW_CHAT: `?${NEW_CHAT_PARAM}=true#/`,
/** Chat base — for dynamic chat URLs use RouterService. */
CHAT: '#/chat',
/** MCP servers. */
MCP_SERVERS: '#/mcp-servers',
/** Settings base — for dynamic settings URLs use RouterService. */
SETTINGS: '#/settings',
/** Search — mobile-only full-page conversation search. */
SEARCH: '#/search'
} as const;
+59
View File
@@ -0,0 +1,59 @@
import { BuiltInTool, JsonSchemaType, ToolCallType } from '$lib/enums';
import type { OpenAIToolDefinition } from '$lib/types';
export const SANDBOX_TOOL_NAME = BuiltInTool.RUN_JAVASCRIPT;
export const SANDBOX_TIMEOUT_MS_DEFAULT = 10000;
export const SANDBOX_TIMEOUT_MS_MAX = 30000;
export const SANDBOX_OUTPUT_MAX_CHARS = 8192;
export const SANDBOX_EMPTY_OUTPUT = '(no output)';
export const SANDBOX_TRUNCATION_NOTICE = '[output truncated]';
const NERDAMER_DESCRIPTION = `
Symbolic/numeric math via \`nerdamer\`
nerdamer(expr,subs?,opts?)/nerdamer.func(...)→Expression Format via .text(fmt?) (fmt: 'decimals'|'fractions'|'scientific') eval via .evaluate(subs?)
nerdamer(expr,{x:2}) substitutes numeric via opts 'numer' or .evaluate()
simplify/expand/factor(expr) div/gcd/lcm(...) coeffs/partfrac(expr,var)
diff/integrate(expr,var) defint(expr,lo,hi,var?) sum/product(expr,var,lo,hi) limit(expr,var,pt)
solve(expr,var) solveEquations([eq1,eq2],[var1,var2])
polarform/rectform/arg/realpart/imagpart(z)
set/get Var/Constant(name,val?) setFunction(name,[params],body)
IMPORTANT:Identifier 'nerdamer' has already been declared, use it directly`;
/**
* Build the sandbox tool definition. When `includeSymbolicMath` is true,
* the description includes nerdamer API documentation; otherwise it
* describes a plain JavaScript sandbox.
*/
export function buildSandboxToolDefinition(includeSymbolicMath: boolean): OpenAIToolDefinition {
return {
type: ToolCallType.FUNCTION,
function: {
name: SANDBOX_TOOL_NAME,
description: includeSymbolicMath
? `Execute JS in a sandboxed browser worker (no DOM/page access). Top-level await ok; console.log for intermediates; top-level return is captured as result.${NERDAMER_DESCRIPTION}`
: 'Execute JS in a sandboxed browser worker (no DOM/page access). Top-level await ok; console.log for intermediates; top-level return is captured as result.',
parameters: {
type: JsonSchemaType.OBJECT,
properties: {
code: {
type: JsonSchemaType.STRING,
description: 'JavaScript source to execute'
},
timeout_ms: {
type: JsonSchemaType.NUMBER,
description: `Execution timeout in milliseconds, default ${SANDBOX_TIMEOUT_MS_DEFAULT}, max ${SANDBOX_TIMEOUT_MS_MAX}`
}
},
required: ['code']
}
}
};
}
/** @deprecated Use {@link buildSandboxToolDefinition} instead. Kept for backward compatibility. */
export const SANDBOX_TOOL_DEFINITION = buildSandboxToolDefinition(true);
@@ -0,0 +1,74 @@
/**
* Settings key constants for ChatSettings configuration.
*
* These keys correspond to properties in SettingsConfigType and are used
* in settings field configurations to ensure consistency.
*/
export const SETTINGS_KEYS = {
// General
THEME: 'theme',
API_KEY: 'apiKey',
SYSTEM_MESSAGE: 'systemMessage',
PASTE_LONG_TEXT_TO_FILE_LEN: 'pasteLongTextToFileLen',
COPY_TEXT_ATTACHMENTS_AS_PLAIN_TEXT: 'copyTextAttachmentsAsPlainText',
SEND_ON_ENTER: 'sendOnEnter',
ENABLE_CONTINUE_GENERATION: 'enableContinueGeneration',
PDF_AS_IMAGE: 'pdfAsImage',
TITLE_GENERATION_USE_FIRST_LINE: 'titleGenerationUseFirstLine',
TITLE_GENERATION_USE_LLM: 'titleGenerationUseLLM',
TITLE_GENERATION_PROMPT: 'titleGenerationPrompt',
MAX_IMAGE_RESOLUTION: 'maxImageMPixels',
// Display
SHOW_MESSAGE_STATS: 'showMessageStats',
SHOW_AGENTIC_TURN_STATS: 'showAgenticTurnStats',
SHOW_THOUGHT_IN_PROGRESS: 'showThoughtInProgress',
AUTO_MIC_ON_EMPTY: 'autoMicOnEmpty',
RENDER_USER_CONTENT_AS_MARKDOWN: 'renderUserContentAsMarkdown',
DISABLE_AUTO_SCROLL: 'disableAutoScroll',
ALWAYS_SHOW_SIDEBAR_ON_DESKTOP: 'alwaysShowSidebarOnDesktop',
FULL_HEIGHT_CODE_BLOCKS: 'fullHeightCodeBlocks',
SHOW_RAW_MODEL_NAMES: 'showRawModelNames',
SHOW_MODEL_QUANTIZATION: 'showModelQuantization',
SHOW_MODEL_TAGS: 'showModelTags',
SHOW_BUILD_VERSION: 'showBuildVersion',
SHOW_SYSTEM_MESSAGE: 'showSystemMessage',
RENDER_THINKING_AS_MARKDOWN: 'renderThinkingAsMarkdown',
// Sampling
TEMPERATURE: 'temperature',
DYNATEMP_RANGE: 'dynatemp_range',
DYNATEMP_EXPONENT: 'dynatemp_exponent',
TOP_K: 'top_k',
TOP_P: 'top_p',
MIN_P: 'min_p',
XTC_PROBABILITY: 'xtc_probability',
XTC_THRESHOLD: 'xtc_threshold',
TYP_P: 'typ_p',
MAX_TOKENS: 'max_tokens',
SAMPLERS: 'samplers',
BACKEND_SAMPLING: 'backend_sampling',
// Penalties
REPEAT_LAST_N: 'repeat_last_n',
REPEAT_PENALTY: 'repeat_penalty',
PRESENCE_PENALTY: 'presence_penalty',
FREQUENCY_PENALTY: 'frequency_penalty',
DRY_MULTIPLIER: 'dry_multiplier',
DRY_BASE: 'dry_base',
DRY_ALLOWED_LENGTH: 'dry_allowed_length',
DRY_PENALTY_LAST_N: 'dry_penalty_last_n',
// MCP
MCP_SERVERS: 'mcpServers',
MCP_REQUEST_TIMEOUT_SECONDS: 'mcpRequestTimeoutSeconds',
AGENTIC_MAX_TURNS: 'agenticMaxTurns',
ALWAYS_SHOW_TOOL_CALL_CONTENT: 'alwaysShowToolCallContent',
// Performance
PRE_ENCODE_CONVERSATION: 'preEncodeConversation',
// Developer
DISABLE_REASONING_PARSING: 'disableReasoningParsing',
EXCLUDE_REASONING_FROM_CONTEXT: 'excludeReasoningFromContext',
SHOW_RAW_OUTPUT_SWITCH: 'showRawOutputSwitch',
JS_SANDBOX_ENABLED: 'jsSandboxEnabled',
SYMBOLIC_MATH_ENABLED: 'symbolicMathEnabled',
// PY_INTERPRETER_ENABLED: 'pyInterpreterEnabled',
CUSTOM_JSON: 'customJson',
CUSTOM_CSS: 'customCss'
} as const;
@@ -0,0 +1,731 @@
import { ColorMode } from '$lib/enums/ui.enums';
import { SettingsFieldType } from '$lib/enums/settings.enums';
import { SyncableParameterType } from '$lib/enums';
import {
Funnel,
AlertTriangle,
Code,
Monitor,
ListRestart,
Sliders,
PencilRuler,
Database,
Monitor as MonitorIcon,
Sun,
Moon
} from '@lucide/svelte';
import type { Component } from 'svelte';
import type {
SettingsConfigValue,
SyncableParameter,
SettingsEntry,
SettingsSectionTitle,
SettingsSectionEntry,
SettingsSection
} from '$lib/types';
import { CLI_FLAGS, DEFAULT_MCP_CONFIG } from '$lib/constants';
import { SETTINGS_KEYS } from './settings-keys';
import { ROUTES, SETTINGS_SECTION_SLUGS } from './routes';
import { TITLE_GENERATION } from './title-generation';
export const SETTINGS_SECTION_TITLES = {
GENERAL: 'General',
DISPLAY: 'Display',
SAMPLING: 'Sampling',
PENALTIES: 'Penalties',
AGENTIC: 'Agentic',
TOOLS: 'Tools',
IMPORT_EXPORT: 'Import/Export',
DEVELOPER: 'Developer'
} as const;
const STANDALONE_SECTIONS: { title: SettingsSectionTitle; slug: string; icon: Component }[] = [
{ title: SETTINGS_SECTION_TITLES.TOOLS, slug: SETTINGS_SECTION_SLUGS.TOOLS, icon: PencilRuler },
{
title: SETTINGS_SECTION_TITLES.IMPORT_EXPORT,
slug: SETTINGS_SECTION_SLUGS.IMPORT_EXPORT,
icon: Database
}
];
const COLOR_MODE_OPTIONS: Array<{ value: string; label: string; icon: Component }> = [
{ value: ColorMode.SYSTEM, label: 'System', icon: MonitorIcon },
{ value: ColorMode.LIGHT, label: 'Light', icon: Sun },
{ value: ColorMode.DARK, label: 'Dark', icon: Moon }
];
// Shared options for the title-generation radio group. Both paired registry entries
// (USE_FIRST_LINE, USE_LLM) reference this list so labels stay in lockstep.
const TITLE_GENERATION_RADIO_OPTIONS: Array<{
value: string;
label: string;
key: string;
isExperimental?: boolean;
}> = [
{
value: 'firstLine',
label: 'Use first non-empty line for the conversation title',
key: SETTINGS_KEYS.TITLE_GENERATION_USE_FIRST_LINE
},
{
value: 'llm',
label: 'Generate title with LLM',
key: SETTINGS_KEYS.TITLE_GENERATION_USE_LLM,
isExperimental: true
}
];
// Common shape for the conversation title radio entry.
const TITLE_GENERATION_BASE = {
type: SettingsFieldType.RADIO,
section: SETTINGS_SECTION_SLUGS.GENERAL,
radioOptions: TITLE_GENERATION_RADIO_OPTIONS
} as const;
const SETTINGS_REGISTRY: Record<string, SettingsSectionEntry> = {
[SETTINGS_SECTION_SLUGS.GENERAL]: {
title: SETTINGS_SECTION_TITLES.GENERAL,
slug: SETTINGS_SECTION_SLUGS.GENERAL,
icon: Sliders,
settings: [
{
key: SETTINGS_KEYS.THEME,
label: 'Theme',
help: 'Choose the color theme for the interface. You can choose between System (follows your device settings), Light, or Dark.',
defaultValue: ColorMode.SYSTEM,
type: SettingsFieldType.SELECT,
section: SETTINGS_SECTION_SLUGS.GENERAL,
options: COLOR_MODE_OPTIONS
},
{
key: SETTINGS_KEYS.API_KEY,
label: 'API Key',
help: `Set the API Key if you are using <code> ${CLI_FLAGS.API_KEY} </code> option for the server.`,
defaultValue: '',
type: SettingsFieldType.INPUT,
section: SETTINGS_SECTION_SLUGS.GENERAL
},
{
key: SETTINGS_KEYS.SYSTEM_MESSAGE,
label: 'System Message',
help: 'The starting message that defines how model should behave.',
defaultValue: '',
type: SettingsFieldType.TEXTAREA,
section: SETTINGS_SECTION_SLUGS.GENERAL
},
{
key: SETTINGS_KEYS.PASTE_LONG_TEXT_TO_FILE_LEN,
label: 'Paste long text to file length',
help: 'On pasting long text, it will be converted to a file. You can control the file length by setting the value of this parameter. Value 0 means disable.',
defaultValue: 2500,
type: SettingsFieldType.INPUT,
section: SETTINGS_SECTION_SLUGS.GENERAL
},
{
key: SETTINGS_KEYS.SEND_ON_ENTER,
label: 'Send message on Enter',
help: 'Use Enter to send messages and Shift + Enter for new lines. When disabled, use Ctrl/Cmd + Enter.',
defaultValue: true,
type: SettingsFieldType.CHECKBOX,
section: SETTINGS_SECTION_SLUGS.GENERAL
},
{
key: SETTINGS_KEYS.AUTO_MIC_ON_EMPTY,
label: 'Show microphone on empty input',
help: 'Automatically show microphone button instead of send button when textarea is empty for models with audio modality support.',
defaultValue: false,
type: SettingsFieldType.CHECKBOX,
section: SETTINGS_SECTION_SLUGS.GENERAL,
isExperimental: true
},
{
key: SETTINGS_KEYS.ENABLE_CONTINUE_GENERATION,
label: 'Enable "Continue" button',
help: 'Enable "Continue" button for assistant messages, including reasoning models.',
defaultValue: false,
type: SettingsFieldType.CHECKBOX,
section: SETTINGS_SECTION_SLUGS.GENERAL,
isExperimental: true
},
{
...TITLE_GENERATION_BASE,
key: SETTINGS_KEYS.TITLE_GENERATION_USE_FIRST_LINE,
label: 'Conversation title',
help: 'Choose how conversation titles are generated. The first non-empty line uses a fast deterministic rule; the LLM option uses a model-generated title from the first message exchange.',
defaultValue: true
},
{
key: SETTINGS_KEYS.TITLE_GENERATION_PROMPT,
label: 'LLM title generation prompt',
help: 'Optional template for the title generation prompt. Use {{USER}} for the user message and {{ASSISTANT}} for the assistant message.',
defaultValue: TITLE_GENERATION.DEFAULT_PROMPT,
type: SettingsFieldType.TEXTAREA,
section: SETTINGS_SECTION_SLUGS.GENERAL,
dependsOn: SETTINGS_KEYS.TITLE_GENERATION_USE_LLM
},
{
key: SETTINGS_KEYS.COPY_TEXT_ATTACHMENTS_AS_PLAIN_TEXT,
label: 'Copy text attachments as plain text',
help: 'When copying a message with text attachments, combine them into a single plain text string instead of a special format that can be pasted back as attachments.',
defaultValue: false,
type: SettingsFieldType.CHECKBOX,
section: SETTINGS_SECTION_SLUGS.GENERAL
},
{
key: SETTINGS_KEYS.PDF_AS_IMAGE,
label: 'Parse PDF as image',
help: 'Parse PDF as image instead of text. Automatically falls back to text processing for non-vision models.',
defaultValue: false,
type: SettingsFieldType.CHECKBOX,
section: SETTINGS_SECTION_SLUGS.GENERAL
},
{
key: SETTINGS_KEYS.MAX_IMAGE_RESOLUTION,
label: 'Maximum image resolution (megapixels)',
help: 'Images larger than this will be resized before sending to server. Set to 0 to disable.',
defaultValue: 0,
type: SettingsFieldType.INPUT,
section: SETTINGS_SECTION_SLUGS.GENERAL
}
]
},
[SETTINGS_SECTION_SLUGS.DISPLAY]: {
title: SETTINGS_SECTION_TITLES.DISPLAY,
slug: SETTINGS_SECTION_SLUGS.DISPLAY,
icon: Monitor,
settings: [
{
key: SETTINGS_KEYS.SHOW_MESSAGE_STATS,
label: 'Show message generation statistics',
help: 'Display generation statistics (tokens/second, token count, duration) below each assistant message.',
defaultValue: false,
type: SettingsFieldType.CHECKBOX,
section: SETTINGS_SECTION_SLUGS.DISPLAY
},
{
key: SETTINGS_KEYS.SHOW_AGENTIC_TURN_STATS,
label: 'Show statistics for individual agentic turns',
help: 'Display per-turn statistics (tokens, duration) under each turn in agentic responses. Shown only when "Show message generation statistics" is enabled.',
defaultValue: false,
type: SettingsFieldType.CHECKBOX,
section: SETTINGS_SECTION_SLUGS.DISPLAY,
dependsOn: SETTINGS_KEYS.SHOW_MESSAGE_STATS
},
{
key: SETTINGS_KEYS.SHOW_THOUGHT_IN_PROGRESS,
label: 'Show thought in progress',
help: 'Expand thought process by default when generating messages.',
defaultValue: true,
type: SettingsFieldType.CHECKBOX,
section: SETTINGS_SECTION_SLUGS.DISPLAY
},
{
key: SETTINGS_KEYS.ALWAYS_SHOW_TOOL_CALL_CONTENT,
label: 'Always show tool call content',
help: 'Automatically expand tool call details while executing and keep them expanded after completion.',
defaultValue: false,
type: SettingsFieldType.CHECKBOX,
section: SETTINGS_SECTION_SLUGS.DISPLAY
},
{
key: SETTINGS_KEYS.RENDER_USER_CONTENT_AS_MARKDOWN,
label: 'Render user content as Markdown',
help: 'Render user messages using markdown formatting in the chat.',
defaultValue: false,
type: SettingsFieldType.CHECKBOX,
section: SETTINGS_SECTION_SLUGS.DISPLAY
},
{
key: SETTINGS_KEYS.RENDER_THINKING_AS_MARKDOWN,
label: 'Render thinking as Markdown',
help: 'Render the reasoning/thinking block content as formatted Markdown instead of plain text.',
defaultValue: true,
type: SettingsFieldType.CHECKBOX,
section: SETTINGS_SECTION_SLUGS.DISPLAY
},
{
key: SETTINGS_KEYS.FULL_HEIGHT_CODE_BLOCKS,
label: 'Use full height code blocks',
help: 'Always display code blocks at their full natural height, overriding any height limits.',
defaultValue: false,
type: SettingsFieldType.CHECKBOX,
section: SETTINGS_SECTION_SLUGS.DISPLAY
},
{
key: SETTINGS_KEYS.DISABLE_AUTO_SCROLL,
label: 'Disable automatic scroll',
help: 'Disable automatic scrolling while messages stream so you can control the viewport position manually.',
defaultValue: false,
type: SettingsFieldType.CHECKBOX,
section: SETTINGS_SECTION_SLUGS.DISPLAY
},
{
key: SETTINGS_KEYS.ALWAYS_SHOW_SIDEBAR_ON_DESKTOP,
label: 'Always show sidebar on desktop',
help: 'Always keep the sidebar visible on desktop instead of auto-hiding it.',
defaultValue: false,
type: SettingsFieldType.CHECKBOX,
section: SETTINGS_SECTION_SLUGS.DISPLAY
},
{
key: SETTINGS_KEYS.SHOW_RAW_MODEL_NAMES,
label: 'Show raw model names',
help: 'Display full raw model identifiers (e.g. "ggml-org/GLM-4.7-Flash-GGUF:Q8_0") instead of parsed names with badges.',
defaultValue: false,
type: SettingsFieldType.CHECKBOX,
section: SETTINGS_SECTION_SLUGS.DISPLAY
},
{
key: SETTINGS_KEYS.SHOW_MODEL_QUANTIZATION,
label: 'Show model quantization information',
help: 'Display quantization badges (e.g. Q8_0, Q4_K_M) next to model names throughout the interface.',
defaultValue: true,
type: SettingsFieldType.CHECKBOX,
section: SETTINGS_SECTION_SLUGS.DISPLAY
},
{
key: SETTINGS_KEYS.SHOW_MODEL_TAGS,
label: 'Show model tags',
help: 'Display model tags (e.g. "vision", "reasoning") next to model names throughout the interface.',
defaultValue: true,
type: SettingsFieldType.CHECKBOX,
section: SETTINGS_SECTION_SLUGS.DISPLAY
},
{
key: SETTINGS_KEYS.SHOW_BUILD_VERSION,
label: 'Show build version information',
help: 'Display the current build version in the bottom-right corner of the interface.',
defaultValue: false,
type: SettingsFieldType.CHECKBOX,
section: SETTINGS_SECTION_SLUGS.DISPLAY
}
]
},
[SETTINGS_SECTION_SLUGS.SAMPLING]: {
title: SETTINGS_SECTION_TITLES.SAMPLING,
slug: SETTINGS_SECTION_SLUGS.SAMPLING,
icon: Funnel,
settings: [
{
key: SETTINGS_KEYS.TEMPERATURE,
label: 'Temperature',
help: 'Controls the randomness of the generated text by affecting the probability distribution of the output tokens. Higher = more random, lower = more focused.',
defaultValue: undefined,
type: SettingsFieldType.INPUT,
section: SETTINGS_SECTION_SLUGS.SAMPLING,
sync: {
serverKey: SETTINGS_KEYS.TEMPERATURE,
paramType: SyncableParameterType.NUMBER
}
},
{
key: SETTINGS_KEYS.DYNATEMP_RANGE,
label: 'Dynamic temperature range',
help: 'Addon for the temperature sampler. The added value to the range of dynamic temperature, which adjusts probabilities by entropy of tokens.',
defaultValue: undefined,
type: SettingsFieldType.INPUT,
section: SETTINGS_SECTION_SLUGS.SAMPLING,
sync: {
serverKey: SETTINGS_KEYS.DYNATEMP_RANGE,
paramType: SyncableParameterType.NUMBER
}
},
{
key: SETTINGS_KEYS.DYNATEMP_EXPONENT,
label: 'Dynamic temperature exponent',
help: 'Addon for the temperature sampler. Smoothes out the probability redistribution based on the most probable token.',
defaultValue: undefined,
type: SettingsFieldType.INPUT,
section: SETTINGS_SECTION_SLUGS.SAMPLING,
sync: {
serverKey: SETTINGS_KEYS.DYNATEMP_EXPONENT,
paramType: SyncableParameterType.NUMBER
}
},
{
key: SETTINGS_KEYS.TOP_K,
label: 'Top K',
help: 'Keeps only k top tokens.',
defaultValue: undefined,
type: SettingsFieldType.INPUT,
section: SETTINGS_SECTION_SLUGS.SAMPLING,
sync: { serverKey: SETTINGS_KEYS.TOP_K, paramType: SyncableParameterType.NUMBER }
},
{
key: SETTINGS_KEYS.TOP_P,
label: 'Top P',
help: 'Limits tokens to those that together have a cumulative probability of at least p',
defaultValue: undefined,
type: SettingsFieldType.INPUT,
section: SETTINGS_SECTION_SLUGS.SAMPLING,
sync: { serverKey: SETTINGS_KEYS.TOP_P, paramType: SyncableParameterType.NUMBER }
},
{
key: SETTINGS_KEYS.MIN_P,
label: 'Min P',
help: 'Limits tokens based on the minimum probability for a token to be considered, relative to the probability of the most likely token.',
defaultValue: undefined,
type: SettingsFieldType.INPUT,
section: SETTINGS_SECTION_SLUGS.SAMPLING,
sync: { serverKey: SETTINGS_KEYS.MIN_P, paramType: SyncableParameterType.NUMBER }
},
{
key: SETTINGS_KEYS.XTC_PROBABILITY,
label: 'XTC probability',
help: 'XTC sampler cuts out top tokens; this parameter controls the chance of cutting tokens at all. 0 disables XTC.',
defaultValue: undefined,
type: SettingsFieldType.INPUT,
section: SETTINGS_SECTION_SLUGS.SAMPLING,
sync: {
serverKey: SETTINGS_KEYS.XTC_PROBABILITY,
paramType: SyncableParameterType.NUMBER
}
},
{
key: SETTINGS_KEYS.XTC_THRESHOLD,
label: 'XTC threshold',
help: 'XTC sampler cuts out top tokens; this parameter controls the token probability that is required to cut that token.',
defaultValue: undefined,
type: SettingsFieldType.INPUT,
section: SETTINGS_SECTION_SLUGS.SAMPLING,
sync: {
serverKey: SETTINGS_KEYS.XTC_THRESHOLD,
paramType: SyncableParameterType.NUMBER
}
},
{
key: SETTINGS_KEYS.TYP_P,
label: 'Typical P',
help: 'Sorts and limits tokens based on the difference between log-probability and entropy.',
defaultValue: undefined,
type: SettingsFieldType.INPUT,
section: SETTINGS_SECTION_SLUGS.SAMPLING,
sync: { serverKey: SETTINGS_KEYS.TYP_P, paramType: SyncableParameterType.NUMBER }
},
{
key: SETTINGS_KEYS.MAX_TOKENS,
label: 'Max tokens',
help: 'The maximum number of token per output. Use -1 for infinite (no limit).',
defaultValue: undefined,
type: SettingsFieldType.INPUT,
section: SETTINGS_SECTION_SLUGS.SAMPLING,
sync: {
serverKey: SETTINGS_KEYS.MAX_TOKENS,
paramType: SyncableParameterType.NUMBER
}
},
{
key: SETTINGS_KEYS.SAMPLERS,
label: 'Samplers',
help: 'The order at which samplers are applied, in simplified way. Default is "top_k;typ_p;top_p;min_p;temperature": top_k->typ_p->top_p->min_p->temperature',
defaultValue: '',
type: SettingsFieldType.INPUT,
section: SETTINGS_SECTION_SLUGS.SAMPLING,
sync: { serverKey: SETTINGS_KEYS.SAMPLERS, paramType: SyncableParameterType.STRING }
},
{
key: SETTINGS_KEYS.BACKEND_SAMPLING,
label: 'Backend sampling',
help: 'Enable backend-based samplers. When enabled, supported samplers run on the accelerator backend for faster sampling.',
defaultValue: false,
type: SettingsFieldType.CHECKBOX,
section: SETTINGS_SECTION_SLUGS.SAMPLING
}
]
},
[SETTINGS_SECTION_SLUGS.PENALTIES]: {
title: SETTINGS_SECTION_TITLES.PENALTIES,
slug: SETTINGS_SECTION_SLUGS.PENALTIES,
icon: AlertTriangle,
settings: [
{
key: SETTINGS_KEYS.REPEAT_LAST_N,
label: 'Repeat last N',
help: 'Last n tokens to consider for penalizing repetition',
defaultValue: undefined,
type: SettingsFieldType.INPUT,
section: SETTINGS_SECTION_SLUGS.PENALTIES,
sync: {
serverKey: SETTINGS_KEYS.REPEAT_LAST_N,
paramType: SyncableParameterType.NUMBER
}
},
{
key: SETTINGS_KEYS.REPEAT_PENALTY,
label: 'Repeat penalty',
help: 'Controls the repetition of token sequences in the generated text',
defaultValue: undefined,
type: SettingsFieldType.INPUT,
section: SETTINGS_SECTION_SLUGS.PENALTIES,
sync: {
serverKey: SETTINGS_KEYS.REPEAT_PENALTY,
paramType: SyncableParameterType.NUMBER
}
},
{
key: SETTINGS_KEYS.PRESENCE_PENALTY,
label: 'Presence penalty',
help: 'Limits tokens based on whether they appear in the output or not.',
defaultValue: undefined,
type: SettingsFieldType.INPUT,
section: SETTINGS_SECTION_SLUGS.PENALTIES,
sync: {
serverKey: SETTINGS_KEYS.PRESENCE_PENALTY,
paramType: SyncableParameterType.NUMBER
}
},
{
key: SETTINGS_KEYS.FREQUENCY_PENALTY,
label: 'Frequency penalty',
help: 'Limits tokens based on how often they appear in the output.',
defaultValue: undefined,
type: SettingsFieldType.INPUT,
section: SETTINGS_SECTION_SLUGS.PENALTIES,
sync: {
serverKey: SETTINGS_KEYS.FREQUENCY_PENALTY,
paramType: SyncableParameterType.NUMBER
}
},
{
key: SETTINGS_KEYS.DRY_MULTIPLIER,
label: 'DRY multiplier',
help: 'DRY sampling reduces repetition in generated text even across long contexts. This parameter sets the DRY sampling multiplier.',
defaultValue: undefined,
type: SettingsFieldType.INPUT,
section: SETTINGS_SECTION_SLUGS.PENALTIES,
sync: {
serverKey: SETTINGS_KEYS.DRY_MULTIPLIER,
paramType: SyncableParameterType.NUMBER
}
},
{
key: SETTINGS_KEYS.DRY_BASE,
label: 'DRY base',
help: 'DRY sampling reduces repetition in generated text even across long contexts. This parameter sets the DRY sampling base value.',
defaultValue: undefined,
type: SettingsFieldType.INPUT,
section: SETTINGS_SECTION_SLUGS.PENALTIES,
sync: { serverKey: SETTINGS_KEYS.DRY_BASE, paramType: SyncableParameterType.NUMBER }
},
{
key: SETTINGS_KEYS.DRY_ALLOWED_LENGTH,
label: 'DRY allowed length',
help: 'DRY sampling reduces repetition in generated text even across long contexts. This parameter sets the allowed length for DRY sampling.',
defaultValue: undefined,
type: SettingsFieldType.INPUT,
section: SETTINGS_SECTION_SLUGS.PENALTIES,
sync: {
serverKey: SETTINGS_KEYS.DRY_ALLOWED_LENGTH,
paramType: SyncableParameterType.NUMBER
}
},
{
key: SETTINGS_KEYS.DRY_PENALTY_LAST_N,
label: 'DRY penalty last N',
help: 'DRY sampling reduces repetition in generated text even across long contexts. This parameter sets DRY penalty for the last n tokens.',
defaultValue: undefined,
type: SettingsFieldType.INPUT,
section: SETTINGS_SECTION_SLUGS.PENALTIES,
sync: {
serverKey: SETTINGS_KEYS.DRY_PENALTY_LAST_N,
paramType: SyncableParameterType.NUMBER
}
}
]
},
[SETTINGS_SECTION_SLUGS.AGENTIC]: {
title: SETTINGS_SECTION_TITLES.AGENTIC,
slug: SETTINGS_SECTION_SLUGS.AGENTIC,
icon: ListRestart,
settings: [
{
key: SETTINGS_KEYS.AGENTIC_MAX_TURNS,
label: 'Agentic turns',
help: 'Maximum number of tool execution cycles before stopping (prevents infinite loops).',
defaultValue: 10,
type: SettingsFieldType.INPUT,
section: SETTINGS_SECTION_SLUGS.AGENTIC,
isPositiveInteger: true
},
{
key: SETTINGS_KEYS.MCP_REQUEST_TIMEOUT_SECONDS,
label: 'MCP request timeout (seconds)',
help: 'Timeout for individual MCP tool calls.',
defaultValue: DEFAULT_MCP_CONFIG.requestTimeoutSeconds,
type: SettingsFieldType.INPUT,
section: SETTINGS_SECTION_SLUGS.AGENTIC,
isPositiveInteger: true
}
]
},
[SETTINGS_SECTION_SLUGS.DEVELOPER]: {
title: SETTINGS_SECTION_TITLES.DEVELOPER,
slug: SETTINGS_SECTION_SLUGS.DEVELOPER,
icon: Code,
settings: [
{
key: SETTINGS_KEYS.PRE_ENCODE_CONVERSATION,
label: 'Pre-fill KV cache after response',
help: 'After each response, re-submit the conversation to pre-fill the server KV cache. Makes the next turn faster since the prompt is already encoded while you read the response.',
defaultValue: false,
type: SettingsFieldType.CHECKBOX,
section: SETTINGS_SECTION_SLUGS.DEVELOPER
},
{
key: SETTINGS_KEYS.DISABLE_REASONING_PARSING,
label: 'Disable reasoning content parsing',
help: 'Send reasoning_format=none so the server returns thinking tokens inline instead of extracting them into a separate field.',
defaultValue: false,
type: SettingsFieldType.CHECKBOX,
section: SETTINGS_SECTION_SLUGS.DEVELOPER
},
{
key: SETTINGS_KEYS.EXCLUDE_REASONING_FROM_CONTEXT,
label: 'Exclude reasoning from context',
help: 'Strip thinking from previous messages before sending. When off, thinking is sent back via the reasoning_content field so the model sees its own chain-of-thought across turns.',
defaultValue: false,
type: SettingsFieldType.CHECKBOX,
section: SETTINGS_SECTION_SLUGS.DEVELOPER
},
{
key: SETTINGS_KEYS.SHOW_RAW_OUTPUT_SWITCH,
label: 'Enable raw output toggle',
help: 'Show toggle button to display messages as plain text instead of Markdown-formatted content',
defaultValue: false,
type: SettingsFieldType.CHECKBOX,
section: SETTINGS_SECTION_SLUGS.DEVELOPER
},
{
key: SETTINGS_KEYS.JS_SANDBOX_ENABLED,
label: 'JavaScript sandbox tool',
help: 'Expose a run_javascript tool to the model. Code runs in a Web Worker inside a sandboxed iframe with an opaque origin, isolated from the WebUI and its API, with a hard timeout.',
defaultValue: false,
type: SettingsFieldType.CHECKBOX,
section: SETTINGS_SECTION_SLUGS.DEVELOPER
},
{
key: SETTINGS_KEYS.SYMBOLIC_MATH_ENABLED,
label: 'Symbolic math (nerdamer)',
help: 'Pre-load nerdamer in the sandbox for symbolic computation: simplify, diff, integrate, solve, and more. Requires "JavaScript sandbox tool" to be enabled.',
defaultValue: false,
type: SettingsFieldType.CHECKBOX,
section: SETTINGS_SECTION_SLUGS.DEVELOPER,
dependsOn: SETTINGS_KEYS.JS_SANDBOX_ENABLED
},
{
key: SETTINGS_KEYS.CUSTOM_JSON,
label: 'Custom JSON',
help: 'Custom JSON parameters to send to the API. Must be valid JSON format.',
defaultValue: '',
type: SettingsFieldType.TEXTAREA,
section: SETTINGS_SECTION_SLUGS.DEVELOPER
},
{
key: SETTINGS_KEYS.CUSTOM_CSS,
label: 'Custom CSS',
help: 'CSS injected into the page at runtime. Set it here, or ship it server side via the --ui-config customCss field.',
defaultValue: '',
type: SettingsFieldType.TEXTAREA,
section: SETTINGS_SECTION_SLUGS.DEVELOPER
}
]
}
} as const;
const NON_UI_SETTINGS: SettingsEntry[] = [
{
key: SETTINGS_KEYS.SHOW_SYSTEM_MESSAGE,
label: 'Show system message',
help: 'Display the system message at the top of each conversation.',
defaultValue: true,
type: SettingsFieldType.CHECKBOX
},
{
key: SETTINGS_KEYS.MCP_SERVERS,
label: 'MCP servers',
help: 'Configure MCP servers as a JSON list. Use the form in the MCP Client settings section to edit.',
defaultValue: '[]',
type: SettingsFieldType.INPUT
},
{
key: SETTINGS_KEYS.TITLE_GENERATION_USE_LLM,
label: 'Generate title with LLM',
help: 'Counterpart of the conversation title radio; stored and synced without a dedicated UI field.',
defaultValue: false,
type: SettingsFieldType.CHECKBOX
}
// {
// key: SETTINGS_KEYS.PY_INTERPRETER_ENABLED,
// label: 'Python interpreter enabled',
// help: 'Enable Python interpreter using Pyodide. Allows running Python code in markdown code blocks.',
// defaultValue: false,
// type: SettingsFieldType.CHECKBOX,
// isExperimental: true,
//
// }
];
function getAllSettings(): SettingsEntry[] {
const result: SettingsEntry[] = [];
for (const section of Object.values(SETTINGS_REGISTRY)) {
result.push(...section.settings);
}
result.push(...NON_UI_SETTINGS);
return result;
}
/** Flat config object stored in localStorage. */
export const SETTING_CONFIG_DEFAULT: Record<string, SettingsConfigValue> = Object.fromEntries(
getAllSettings().map((s) => [s.key, s.defaultValue])
) as Record<string, SettingsConfigValue>;
/** Help text for every setting (including non-UI). */
export const SETTING_CONFIG_INFO: Record<string, string> = Object.fromEntries(
getAllSettings().map((s) => [s.key, s.help])
) as Record<string, string>;
/** Theme select options. */
export const SETTINGS_COLOR_MODES_CONFIG = COLOR_MODE_OPTIONS;
/** Sidebar sections + field configs (as consumed by UI). */
export const SETTINGS_CHAT_SECTIONS: SettingsSection[] = [
...Object.values(SETTINGS_REGISTRY).map((section) => ({
title: section.title,
slug: section.slug,
icon: section.icon,
fields: section.settings.map((s) => ({
key: s.key,
label: s.label,
type: s.type,
isExperimental: s.isExperimental,
isPositiveInteger: s.isPositiveInteger,
dependsOn: s.dependsOn,
help: s.help,
options: s.options,
radioOptions: s.radioOptions
}))
})),
...STANDALONE_SECTIONS
];
/** INPUT-type settings whose value is a number. */
export const NUMERIC_FIELDS = getAllSettings()
.filter((s) => s.type === SettingsFieldType.INPUT && typeof s.defaultValue !== 'string')
.map((s) => s.key) as readonly string[];
/** Numeric fields clamped to ≥ 1 and rounded. */
export const POSITIVE_INTEGER_FIELDS = getAllSettings()
.filter((s) => s.isPositiveInteger)
.map((s) => s.key) as readonly string[];
/** Derived for the parameter sync service. */
export const SYNCABLE_PARAMETERS: SyncableParameter[] = getAllSettings()
.filter((s) => s.sync !== undefined)
.map((s) => ({
key: s.key,
serverKey: s.sync!.serverKey,
type: s.sync!.paramType,
canSync: true
}));
export const SETTINGS_FALLBACK_EXIT_ROUTE = ROUTES.START;
+16
View File
@@ -0,0 +1,16 @@
/**
* Server-sent events wire format, shared by the chat stream and the
* /models/sse status feed (text/event-stream).
*/
// blank line between two events
export const SSE_RECORD_SEPARATOR = '\n\n';
// line break inside an event
export const SSE_LINE_SEPARATOR = '\n';
// data field prefix, the value follows after an optional space
export const SSE_DATA_PREFIX = 'data:';
// end-of-stream marker on the chat completion stream
export const SSE_DONE_MARKER = '[DONE]';
+53
View File
@@ -0,0 +1,53 @@
/**
* Storage-related constants (localStorage, IndexedDB).
*
* Centralized to ensure consistency across the app and simplify future
* name changes.
*/
/** Name prefix for all localStorage keys */
export const STORAGE_APP_NAME = 'LlamaUi';
/** Deprecated localStorage key prefix (old app name) */
export const STORAGE_APP_NAME_DEPRECATED = 'LlamaCppWebui';
/** @deprecated Deprecated IndexedDB name — will be removed after all users have migrated */
export const DB_APP_NAME_DEPRECATED = 'LlamacppWebui';
export const ALWAYS_ALLOWED_TOOLS_LOCALSTORAGE_KEY = `${STORAGE_APP_NAME}.alwaysAllowedTools`;
export const CONFIG_LOCALSTORAGE_KEY = `${STORAGE_APP_NAME}.config`;
export const DISABLED_TOOLS_LOCALSTORAGE_KEY = `${STORAGE_APP_NAME}.disabledTools`;
/** Disabled tools keyed by stable selection identity, no migration from the name based key */
export const DISABLED_TOOL_KEYS_LOCALSTORAGE_KEY = `${STORAGE_APP_NAME}.disabledToolKeys`;
export const FAVORITE_MODELS_LOCALSTORAGE_KEY = `${STORAGE_APP_NAME}.favoriteModels`;
export const REASONING_EFFORT_DEFAULT_LOCALSTORAGE_KEY = `${STORAGE_APP_NAME}.reasoningEffortDefault`;
export const USER_OVERRIDES_LOCALSTORAGE_KEY = `${STORAGE_APP_NAME}.userOverrides`;
export const DISMISSED_RECOMMENDED_MCP_SERVERS_LOCALSTORAGE_KEY = `${STORAGE_APP_NAME}.dismissedRecommendedMcpServers`;
/** Key prefix for per-conversation resumable stream state, conversationId is appended */
export const STREAM_RESUME_LOCALSTORAGE_KEY_PREFIX = `${STORAGE_APP_NAME}.streamResume.`;
// Deprecated old key names (kept for backward compat while users migrate)
/** @deprecated Use {@link ALWAYS_ALLOWED_TOOLS_LOCALSTORAGE_KEY} instead */
export const DEPRECATED_ALWAYS_ALLOWED_TOOLS_LOCALSTORAGE_KEY = `${STORAGE_APP_NAME_DEPRECATED}.alwaysAllowedTools`;
/** @deprecated Use {@link CONFIG_LOCALSTORAGE_KEY} instead */
export const DEPRECATED_CONFIG_LOCALSTORAGE_KEY = `${STORAGE_APP_NAME_DEPRECATED}.config`;
/** @deprecated Use {@link DISABLED_TOOLS_LOCALSTORAGE_KEY} instead */
export const DEPRECATED_DISABLED_TOOLS_LOCALSTORAGE_KEY = `${STORAGE_APP_NAME_DEPRECATED}.disabledTools`;
/** @deprecated Use {@link FAVORITE_MODELS_LOCALSTORAGE_KEY} instead */
export const DEPRECATED_FAVORITE_MODELS_LOCALSTORAGE_KEY = `${STORAGE_APP_NAME_DEPRECATED}.favoriteModels`;
/** @deprecated Use {@link USER_OVERRIDES_LOCALSTORAGE_KEY} instead */
export const DEPRECATED_USER_OVERRIDES_LOCALSTORAGE_KEY = `${STORAGE_APP_NAME_DEPRECATED}.userOverrides`;
/** Build version stored in localStorage for non-PWA update detection */
export const BUILD_VERSION_LOCALSTORAGE_KEY = `${STORAGE_APP_NAME}.buildVersion`;
/** Maps new keys to their deprecated fallback keys */
export const NEW_TO_DEPRECATED_MAP: Record<string, string> = {
[ALWAYS_ALLOWED_TOOLS_LOCALSTORAGE_KEY]: DEPRECATED_ALWAYS_ALLOWED_TOOLS_LOCALSTORAGE_KEY,
[CONFIG_LOCALSTORAGE_KEY]: DEPRECATED_CONFIG_LOCALSTORAGE_KEY,
[DISABLED_TOOLS_LOCALSTORAGE_KEY]: DEPRECATED_DISABLED_TOOLS_LOCALSTORAGE_KEY,
[FAVORITE_MODELS_LOCALSTORAGE_KEY]: DEPRECATED_FAVORITE_MODELS_LOCALSTORAGE_KEY,
[USER_OVERRIDES_LOCALSTORAGE_KEY]: DEPRECATED_USER_OVERRIDES_LOCALSTORAGE_KEY
};
+3
View File
@@ -0,0 +1,3 @@
// grace window after a visibilitychange before we kick a reader whose socket likely died
// while the tab was hidden. covers brief background pauses without thrashing live streams
export const STREAM_VISIBILITY_KICK_MS = 3000;
@@ -0,0 +1,234 @@
/**
* Comprehensive dictionary of all supported file types in llama-ui
* Organized by category with TypeScript enums for better type safety
*/
import {
FileExtensionAudio,
FileExtensionImage,
FileExtensionPdf,
FileExtensionText,
FileTypeAudio,
FileTypeImage,
FileTypePdf,
FileTypeText,
MimeTypeAudio,
MimeTypeVideo,
MimeTypeImage,
MimeTypeApplication,
MimeTypeText
} from '$lib/enums';
import { FileExtensionVideo, FileTypeVideo } from '$lib/enums/files.enums';
// File type configuration using enums
export const AUDIO_FILE_TYPES = {
[FileTypeAudio.MP3]: {
extensions: [FileExtensionAudio.MP3],
mimeTypes: [MimeTypeAudio.MP3_MPEG, MimeTypeAudio.MP3]
},
[FileTypeAudio.WAV]: {
extensions: [FileExtensionAudio.WAV],
mimeTypes: [MimeTypeAudio.WAV]
}
} as const;
export const VIDEO_FILE_TYPES = {
[FileTypeVideo.MP4]: {
extensions: [FileExtensionVideo.MP4],
mimeTypes: [MimeTypeVideo.MP4]
},
[FileTypeVideo.OGG]: {
extensions: [FileExtensionVideo.OGG],
mimeTypes: [MimeTypeVideo.OGG]
}
} as const;
export const IMAGE_FILE_TYPES = {
[FileTypeImage.JPEG]: {
extensions: [FileExtensionImage.JPG, FileExtensionImage.JPEG],
mimeTypes: [MimeTypeImage.JPEG]
},
[FileTypeImage.PNG]: {
extensions: [FileExtensionImage.PNG],
mimeTypes: [MimeTypeImage.PNG]
},
[FileTypeImage.GIF]: {
extensions: [FileExtensionImage.GIF],
mimeTypes: [MimeTypeImage.GIF]
},
[FileTypeImage.WEBP]: {
extensions: [FileExtensionImage.WEBP],
mimeTypes: [MimeTypeImage.WEBP]
},
[FileTypeImage.SVG]: {
extensions: [FileExtensionImage.SVG],
mimeTypes: [MimeTypeImage.SVG]
},
[FileTypeImage.HEIC]: {
extensions: [FileExtensionImage.HEIC, FileExtensionImage.HEIF],
mimeTypes: [MimeTypeImage.HEIC, MimeTypeImage.HEIF]
}
} as const;
export const PDF_FILE_TYPES = {
[FileTypePdf.PDF]: {
extensions: [FileExtensionPdf.PDF],
mimeTypes: [MimeTypeApplication.PDF]
}
} as const;
export const TEXT_FILE_TYPES = {
[FileTypeText.PLAIN_TEXT]: {
extensions: [FileExtensionText.TXT],
mimeTypes: [MimeTypeText.PLAIN]
},
[FileTypeText.MARKDOWN]: {
extensions: [FileExtensionText.MD],
mimeTypes: [MimeTypeText.MARKDOWN]
},
[FileTypeText.ASCIIDOC]: {
extensions: [FileExtensionText.ADOC],
mimeTypes: [MimeTypeText.ASCIIDOC]
},
[FileTypeText.JAVASCRIPT]: {
extensions: [FileExtensionText.JS],
mimeTypes: [MimeTypeText.JAVASCRIPT, MimeTypeText.JAVASCRIPT_APP]
},
[FileTypeText.TYPESCRIPT]: {
extensions: [FileExtensionText.TS],
mimeTypes: [MimeTypeText.TYPESCRIPT]
},
[FileTypeText.JSX]: {
extensions: [FileExtensionText.JSX],
mimeTypes: [MimeTypeText.JSX]
},
[FileTypeText.TSX]: {
extensions: [FileExtensionText.TSX],
mimeTypes: [MimeTypeText.TSX]
},
[FileTypeText.CSS]: {
extensions: [FileExtensionText.CSS],
mimeTypes: [MimeTypeText.CSS]
},
[FileTypeText.HTML]: {
extensions: [FileExtensionText.HTML, FileExtensionText.HTM],
mimeTypes: [MimeTypeText.HTML]
},
[FileTypeText.JSON]: {
extensions: [FileExtensionText.JSON],
mimeTypes: [MimeTypeText.JSON]
},
[FileTypeText.XML]: {
extensions: [FileExtensionText.XML],
mimeTypes: [MimeTypeText.XML_TEXT, MimeTypeText.XML_APP]
},
[FileTypeText.YAML]: {
extensions: [FileExtensionText.YAML, FileExtensionText.YML],
mimeTypes: [MimeTypeText.YAML_TEXT, MimeTypeText.YAML_APP]
},
[FileTypeText.CSV]: {
extensions: [FileExtensionText.CSV],
mimeTypes: [MimeTypeText.CSV]
},
[FileTypeText.LOG]: {
extensions: [FileExtensionText.LOG],
mimeTypes: [MimeTypeText.PLAIN]
},
[FileTypeText.PYTHON]: {
extensions: [FileExtensionText.PY],
mimeTypes: [MimeTypeText.PYTHON]
},
[FileTypeText.JAVA]: {
extensions: [FileExtensionText.JAVA],
mimeTypes: [MimeTypeText.JAVA]
},
[FileTypeText.CPP]: {
extensions: [
FileExtensionText.CPP,
FileExtensionText.C,
FileExtensionText.H,
FileExtensionText.HPP
],
mimeTypes: [MimeTypeText.CPP_SRC, MimeTypeText.CPP_HDR, MimeTypeText.C_SRC, MimeTypeText.C_HDR]
},
[FileTypeText.PHP]: {
extensions: [FileExtensionText.PHP],
mimeTypes: [MimeTypeText.PHP]
},
[FileTypeText.RUBY]: {
extensions: [FileExtensionText.RB],
mimeTypes: [MimeTypeText.RUBY]
},
[FileTypeText.GO]: {
extensions: [FileExtensionText.GO],
mimeTypes: [MimeTypeText.GO]
},
[FileTypeText.RUST]: {
extensions: [FileExtensionText.RS],
mimeTypes: [MimeTypeText.RUST]
},
[FileTypeText.SHELL]: {
extensions: [FileExtensionText.SH, FileExtensionText.BAT],
mimeTypes: [MimeTypeText.SHELL, MimeTypeText.BAT]
},
[FileTypeText.SQL]: {
extensions: [FileExtensionText.SQL],
mimeTypes: [MimeTypeText.SQL]
},
[FileTypeText.R]: {
extensions: [FileExtensionText.R],
mimeTypes: [MimeTypeText.R]
},
[FileTypeText.SCALA]: {
extensions: [FileExtensionText.SCALA],
mimeTypes: [MimeTypeText.SCALA]
},
[FileTypeText.KOTLIN]: {
extensions: [FileExtensionText.KT],
mimeTypes: [MimeTypeText.KOTLIN]
},
[FileTypeText.SWIFT]: {
extensions: [FileExtensionText.SWIFT],
mimeTypes: [MimeTypeText.SWIFT]
},
[FileTypeText.DART]: {
extensions: [FileExtensionText.DART],
mimeTypes: [MimeTypeText.DART]
},
[FileTypeText.VUE]: {
extensions: [FileExtensionText.VUE],
mimeTypes: [MimeTypeText.VUE]
},
[FileTypeText.SVELTE]: {
extensions: [FileExtensionText.SVELTE],
mimeTypes: [MimeTypeText.SVELTE]
},
[FileTypeText.LATEX]: {
extensions: [FileExtensionText.TEX],
mimeTypes: [MimeTypeText.LATEX, MimeTypeText.TEX, MimeTypeText.TEX_APP]
},
[FileTypeText.BIBTEX]: {
extensions: [FileExtensionText.BIB],
mimeTypes: [MimeTypeText.BIBTEX]
},
[FileTypeText.CUDA]: {
extensions: [FileExtensionText.CU, FileExtensionText.CUH],
mimeTypes: [MimeTypeText.CUDA]
},
[FileTypeText.VULKAN]: {
extensions: [FileExtensionText.COMP],
mimeTypes: [MimeTypeText.PLAIN]
},
[FileTypeText.HASKELL]: {
extensions: [FileExtensionText.HS],
mimeTypes: [MimeTypeText.HASKELL]
},
[FileTypeText.CSHARP]: {
extensions: [FileExtensionText.CS],
mimeTypes: [MimeTypeText.CSHARP]
},
[FileTypeText.PROPERTIES]: {
extensions: [FileExtensionText.PROPERTIES],
mimeTypes: [MimeTypeText.PROPERTIES]
}
} as const;
+49
View File
@@ -0,0 +1,49 @@
export const SVG_WRAPPER_CLASS = 'svg-block-wrapper';
export const SVG_SCROLL_CONTAINER_CLASS = 'svg-scroll-container';
export const SVG_BLOCK_CLASS = 'svg-block';
export const SVG_LANGUAGE = 'svg';
export const XML_LANGUAGE = 'xml';
export const SVG_TAG_PREFIX = '<svg';
export const SVG_SOURCE_ATTR = 'data-svg-source';
export const SVG_ID_ATTR = 'data-svg-id';
export const SVG_RENDERED_ATTR = 'data-svg-rendered';
/**
* Hard size ceiling for a single inline svg block.
* Above this the source is left as raw text instead of being rendered.
*/
export const SVG_MAX_BYTES = 256 * 1024;
/**
* DOMPurify config for untrusted svg coming from model output.
*
* foreignObject and script stay forbidden unconditionally, they are the only
* inline svg vectors that execute arbitrary html or js. Everything else is
* allowed for maximum rendering compatibility: href and xlink:href stay so
* use, image, a and animateMotion work, and DOMPurify still neutralizes
* javascript: and data: uri schemes natively. External resource refs are
* allowed by design on a local first tool, the user browser fetches them.
*
* The sanitized svg is always mounted inside a shadow root (see svg-shadow),
* so an author <style> stays scoped to that root and can not reach the page.
*/
export const SVG_SANITIZE_CONFIG = {
USE_PROFILES: { svg: true, svgFilters: true },
FORBID_TAGS: ['foreignObject', 'script']
};
/**
* Shadow root style for an inline svg block. Mirrors the centered, padded
* sizing the light dom used before the svg moved behind a shadow boundary.
*/
export const SVG_INLINE_SHADOW_STYLE =
':host{display:block;width:100%;text-align:center}svg{display:block;margin:0 auto;width:auto;height:auto;max-width:100%;max-height:70vh;min-height:8rem;padding:3rem 1rem}';
/**
* Shadow root style for the zoom dialog svg. Lets the svg grow past its
* intrinsic size so pan and zoom have room to work.
*/
export const SVG_DIALOG_SHADOW_STYLE =
':host{display:inline-block}svg{min-height:min(50vh,12rem);min-width:min(80vw,20rem);max-width:none;max-height:none;height:auto;width:auto;display:block}';
@@ -0,0 +1,20 @@
/**
* Matches <br>, <br/>, <br /> tags (case-insensitive).
* Used to detect line breaks in table cell text content.
*/
export const BR_PATTERN = /<br\s*\/?\s*>/gi;
/**
* Matches a complete <ul>...</ul> block.
* Captures the inner content (group 1) for further <li> extraction.
* Case-insensitive, allows multiline content.
*/
export const LIST_PATTERN = /^<ul>([\s\S]*)<\/ul>$/i;
/**
* Matches individual <li>...</li> elements within a list.
* Captures the inner content (group 1) of each list item.
* Non-greedy to handle multiple consecutive items.
* Case-insensitive, allows multiline content.
*/
export const LI_PATTERN = /<li>([\s\S]*?)<\/li>/gi;
@@ -0,0 +1,9 @@
/* Title generation constants */
export const TITLE_GENERATION = {
MIN_LENGTH: 3,
FALLBACK: 'New Chat',
DEFAULT_PROMPT:
'Based on the following interaction, generate a short, concise title (maximum 6-8 words) that captures the main topic. Return ONLY the title text, nothing else. Do not use quotes.\n\nUser: {{USER}}\n\nAssistant: {{ASSISTANT}}\n\nTitle:',
PREFIX_PATTERN: /^(Title:|Subject:|Topic:)\s*/i,
QUOTE_PATTERN: /^["]|["]$/g
} as const;
+13
View File
@@ -0,0 +1,13 @@
import { ToolSource } from '$lib/enums/tools.enums';
export const TOOL_GROUP_LABELS = {
[ToolSource.BUILTIN]: 'Built-in',
[ToolSource.CUSTOM]: 'JSON Schema',
[ToolSource.FRONTEND]: 'Browser'
} as const;
export const TOOL_SERVER_LABELS = {
[ToolSource.BUILTIN]: 'Built-in Tools',
[ToolSource.CUSTOM]: 'Custom Tools',
[ToolSource.FRONTEND]: 'Browser Tools'
} as const;
@@ -0,0 +1 @@
export const TOOLTIP_DELAY_DURATION = 500;
+40
View File
@@ -0,0 +1,40 @@
import { Search, Settings, SquarePen } from '@lucide/svelte';
import McpLogo from '$lib/components/app/mcp/McpLogo.svelte';
import type { Component } from 'svelte';
import { ROUTES } from './routes';
export const FORK_TREE_DEPTH_PADDING = 8;
export const SYSTEM_MESSAGE_PLACEHOLDER = 'System message';
export const ICON_STRIP_TRANSITION_DURATION = 150;
export const ICON_STRIP_TRANSITION_DELAY_MULTIPLIER = 50;
/** Max height for tool-result code blocks (json / source / diff / streaming code). */
export const MAX_HEIGHT_CODE_BLOCK = '22rem';
export interface DesktopIconStripItem {
icon: Component;
tooltip: string;
route?: string;
activeRouteId?: string;
activeRoutePrefix?: string;
activeUrlIncludes?: string;
keys?: string[];
}
export const SIDEBAR_ACTIONS_ITEMS: DesktopIconStripItem[] = [
{ icon: SquarePen, tooltip: 'New chat', route: ROUTES.NEW_CHAT, keys: ['shift', 'cmd', 'o'] },
{ icon: Search, tooltip: 'Search', keys: ['cmd', 'k'] },
{
icon: McpLogo,
tooltip: 'MCP Servers',
route: ROUTES.MCP_SERVERS,
activeRouteId: '/mcp-servers'
},
{
icon: Settings,
tooltip: 'Settings',
route: `${ROUTES.SETTINGS}/general`,
activeUrlIncludes: '#/settings'
}
];
@@ -0,0 +1,60 @@
/**
* URI Template constants for RFC 6570 template processing.
*/
/** URI scheme separator */
export const URI_SCHEME_SEPARATOR = '://';
/** Regex to match template expressions like {var}, {+var}, {#var}, {/var} */
export const TEMPLATE_EXPRESSION_REGEX = /\{([+#./;?&]?)([^}]+)\}/g;
/** RFC 6570 URI template operators */
export const URI_TEMPLATE_OPERATORS = {
/** Simple string expansion (default) */
SIMPLE: '',
/** Reserved expansion */
RESERVED: '+',
/** Fragment expansion */
FRAGMENT: '#',
/** Path segment expansion */
PATH_SEGMENT: '/',
/** Label expansion */
LABEL: '.',
/** Path-style parameters */
PATH_PARAM: ';',
/** Form-style query */
FORM_QUERY: '?',
/** Form-style query continuation */
FORM_CONTINUATION: '&'
} as const;
/** URI template separators used in expansion */
export const URI_TEMPLATE_SEPARATORS = {
/** Comma separator for list expansion */
COMMA: ',',
/** Slash separator for path segments */
SLASH: '/',
/** Period separator for label expansion */
PERIOD: '.',
/** Semicolon separator for path parameters */
SEMICOLON: ';',
/** Question mark prefix for query string */
QUERY_PREFIX: '?',
/** Ampersand prefix for query continuation */
QUERY_CONTINUATION: '&'
} as const;
/** Maximum number of leading slashes to strip during URI normalization */
export const MAX_LEADING_SLASHES_TO_STRIP = 3;
/** Regex to strip explode modifier (*) from variable names */
export const VARIABLE_EXPLODE_MODIFIER_REGEX = /[*]$/;
/** Regex to strip prefix modifier (:N) from variable names */
export const VARIABLE_PREFIX_MODIFIER_REGEX = /:[\d]+$/;
/** Regex to strip one or more leading slashes */
export const LEADING_SLASHES_REGEX = /^\/+/;
/** Regex to match base64-encoded image URIs (format: "data:image/[media type];base64,[data]")*/
export const BASE64_IMAGE_URI_REGEX = /^data:(image\/[a-z0-9.\-+]+);base64/;
+189
View File
@@ -0,0 +1,189 @@
const STD = ['com', 'net', 'org', 'gov', 'edu'] as const;
const STD_MIL = [...STD, 'mil'] as const;
const ccTLD_PREFIXES: Record<string, readonly string[]> = {
// --- Standard 5 only ---
ar: STD,
bd: STD,
bg: STD,
cn: STD_MIL,
eg: STD,
gr: STD,
hk: STD,
hr: STD,
lk: STD,
mx: STD_MIL,
my: STD_MIL,
ng: STD,
ph: STD,
pk: STD,
pl: STD,
ro: STD,
ru: STD,
sa: STD,
si: STD,
tr: STD,
tw: STD,
ua: STD,
ve: STD,
au: [...STD_MIL, 'id', 'asn', 'csiro'],
br: [
...STD_MIL,
'art',
'eco',
'eng',
'inf',
'med',
'psi',
'tmp',
'etc',
'adm',
'adv',
'arq',
'bio',
'bmd',
'cim',
'cng',
'cnt',
'coop',
'ecn',
'esp',
'far',
'fm',
'fnd',
'fot',
'fst',
'g12',
'ggf',
'imb',
'ind',
'jor',
'jus',
'leg',
'lel',
'mat',
'mp',
'mus',
'not',
'ntr',
'odo',
'ppg',
'pro',
'psc',
'qsl',
'rec',
'slg',
'srv',
'trd',
'tur',
'tv',
'vet',
'vlog',
'wiki',
'zlg'
],
id: [...STD_MIL, 'co', 'go', 'or', 'web', 'sch'],
in: [...STD_MIL, 'co', 'gen', 'ind', 'firm', 'ernet', 'nic'],
kr: [...STD_MIL, 'co', 'go', 'or', 'ac', 're'],
nz: [
...STD_MIL,
'co',
'gen',
'geek',
'kiwi',
'maori',
'school',
'govt',
'health',
'iwi',
'parliament'
],
sg: [...STD, 'per'],
th: ['co', 'go', 'or', 'in', 'ac', 'mi', 'net'],
ae: ['co', 'net', 'org', 'gov', 'ac', 'sch'],
hu: ['co', 'net', 'org', 'gov', 'edu'],
il: ['co', 'net', 'org', 'gov', 'ac', 'muni'],
jp: ['ac', 'ad', 'co', 'ed', 'go', 'gr', 'lg', 'ne', 'or'],
ke: ['co', 'or', 'ne', 'go', 'ac', 'sc'],
rs: ['co', 'net', 'org', 'gov', 'edu'],
uk: ['co', 'org', 'net', 'ac', 'gov', 'mil', 'nhs', 'police', 'mod', 'ltd', 'plc', 'me', 'sch'],
za: ['co', 'org', 'net', 'web', 'law', 'mil']
};
const WILDCARD_BASES: Record<string, readonly string[]> = {
br: ['nom', 'blog'],
jp: [
'kobe',
'kyoto',
'nagoya',
'osaka',
'sapporo',
'sendai',
'tokyo',
'yokohama',
'aichi',
'akita',
'aomori',
'chiba',
'ehime',
'fukui',
'fukuoka',
'fukushima',
'gifu',
'gunma',
'hiroshima',
'hokkaido',
'hyogo',
'ibaraki',
'ishikawa',
'iwate',
'kagawa',
'kagoshima',
'kanagawa',
'kochi',
'kumamoto',
'mie',
'miyagi',
'miyazaki',
'nagano',
'nara',
'niigata',
'oita',
'okayama',
'okinawa',
'saga',
'saitama',
'shiga',
'shimane',
'shizuoka',
'tochigi',
'tokushima',
'tottori',
'toyama',
'wakayama',
'yamagata',
'yamaguchi',
'yamanashi'
]
};
function buildSuffixSet(suffixes: Record<string, readonly string[]>): Set<string> {
const set = new Set<string>();
for (const [tld, parts] of Object.entries(suffixes)) {
for (const part of parts) {
set.add(`${part}.${tld}`);
}
}
return set;
}
export const TWO_PART_PUBLIC_SUFFIXES = buildSuffixSet(ccTLD_PREFIXES);
export const WILDCARD_PUBLIC_SUFFIXES = buildSuffixSet(WILDCARD_BASES);
// Matches one or more trailing "/" characters at the end of a URL/path.
export const TRAILING_SLASHES_REGEX = /\/+$/;
+1
View File
@@ -0,0 +1 @@
export const DEFAULT_MOBILE_BREAKPOINT = 768;