Inital commit
This commit is contained in:
@@ -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';
|
||||
@@ -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';
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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__';
|
||||
@@ -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';
|
||||
@@ -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];
|
||||
@@ -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';
|
||||
@@ -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';
|
||||
@@ -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;
|
||||
@@ -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';
|
||||
@@ -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>`;
|
||||
@@ -0,0 +1,3 @@
|
||||
export const MEGAPIXELS_TO_PIXELS = 1_000_000;
|
||||
|
||||
export const HEIC_JPEG_QUALITY = 0.85;
|
||||
@@ -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';
|
||||
@@ -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 look‑behind 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);
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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 = '-';
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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'
|
||||
}
|
||||
];
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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]';
|
||||
@@ -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
|
||||
};
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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/;
|
||||
@@ -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 = /\/+$/;
|
||||
@@ -0,0 +1 @@
|
||||
export const DEFAULT_MOBILE_BREAKPOINT = 768;
|
||||
Reference in New Issue
Block a user