refactor: Cleanup

This commit is contained in:
Aleksander Grygier 2026-02-03 15:15:57 +01:00
parent 3120a9fc94
commit 72ef132465
22 changed files with 133 additions and 74 deletions

View File

@ -10,7 +10,8 @@
} from '$lib/components/app';
import { INPUT_CLASSES } from '$lib/constants/css-classes';
import { SETTING_CONFIG_DEFAULT } from '$lib/constants/settings-config';
import { MimeTypeText, SpecialFileType } from '$lib/enums';
import { INITIAL_FILE_SIZE, PROMPT_CONTENT_SEPARATOR } from '$lib/constants/chat-form';
import { ContentPartType, KeyboardKey, MimeTypeText, SpecialFileType } from '$lib/enums';
import { config } from '$lib/stores/settings.svelte';
import { modelOptions, selectedModelId } from '$lib/stores/models.svelte';
import { isRouterMode } from '$lib/stores/server.svelte';
@ -232,13 +233,13 @@
return;
}
if (event.key === 'Escape' && isPromptPickerOpen) {
if (event.key === KeyboardKey.ESCAPE && isPromptPickerOpen) {
isPromptPickerOpen = false;
promptSearchQuery = '';
return;
}
if (event.key === 'Enter' && !event.shiftKey && !isIMEComposing(event)) {
if (event.key === KeyboardKey.ENTER && !event.shiftKey && !isIMEComposing(event)) {
event.preventDefault();
if (!canSubmit || disabled || isLoading || hasLoadingAttachments) return;
@ -289,7 +290,7 @@
name: att.name,
size: att.content.length,
type: SpecialFileType.MCP_PROMPT,
file: new File([att.content], `${att.name}.txt`, { type: 'text/plain' }),
file: new File([att.content], `${att.name}.txt`, { type: MimeTypeText.PLAIN }),
isLoading: false,
textContent: att.content,
mcpPrompt: {
@ -348,7 +349,7 @@
const placeholder: ChatUploadedFile = {
id: placeholderId,
name: promptName,
size: 0,
size: INITIAL_FILE_SIZE,
type: SpecialFileType.MCP_PROMPT,
file: new File([], 'loading'),
isLoading: true,
@ -370,13 +371,13 @@
if (typeof msg.content === 'string') {
return msg.content;
}
if (msg.content.type === 'text') {
if (msg.content.type === ContentPartType.TEXT) {
return msg.content.text;
}
return '';
})
.filter(Boolean)
.join('\n\n');
.join(PROMPT_CONTENT_SEPARATOR);
uploadedFiles = uploadedFiles.map((f) =>
f.id === placeholderId
@ -385,7 +386,7 @@
isLoading: false,
textContent: promptText,
size: promptText.length,
file: new File([promptText], `${f.name}.txt`, { type: 'text/plain' })
file: new File([promptText], `${f.name}.txt`, { type: MimeTypeText.PLAIN })
}
: f
);

View File

@ -2,6 +2,7 @@
import { conversationsStore } from '$lib/stores/conversations.svelte';
import { mcpStore } from '$lib/stores/mcp.svelte';
import { debounce } from '$lib/utils';
import { KeyboardKey } from '$lib/enums';
import type { MCPPromptInfo, GetPromptResult, MCPServerSettingsEntry } from '$lib/types';
import { SvelteMap } from 'svelte/reactivity';
import ChatFormPromptPickerList from './ChatFormPromptPickerList.svelte';
@ -213,17 +214,17 @@
if (argSuggestions.length === 0 || activeAutocomplete !== argName) return;
if (event.key === 'ArrowDown') {
if (event.key === KeyboardKey.ARROW_DOWN) {
event.preventDefault();
autocompleteIndex = Math.min(autocompleteIndex + 1, argSuggestions.length - 1);
} else if (event.key === 'ArrowUp') {
} else if (event.key === KeyboardKey.ARROW_UP) {
event.preventDefault();
autocompleteIndex = Math.max(autocompleteIndex - 1, 0);
} else if (event.key === 'Enter' && argSuggestions[autocompleteIndex]) {
} else if (event.key === KeyboardKey.ENTER && argSuggestions[autocompleteIndex]) {
event.preventDefault();
event.stopPropagation();
selectSuggestion(argName, argSuggestions[autocompleteIndex]);
} else if (event.key === 'Escape') {
} else if (event.key === KeyboardKey.ESCAPE) {
event.preventDefault();
suggestions[argName] = [];
activeAutocomplete = null;
@ -255,7 +256,7 @@
export function handleKeydown(event: KeyboardEvent): boolean {
if (!isOpen) return false;
if (event.key === 'Escape') {
if (event.key === KeyboardKey.ESCAPE) {
event.preventDefault();
if (selectedPrompt) {
selectedPrompt = null;
@ -267,7 +268,7 @@
return true;
}
if (event.key === 'ArrowDown') {
if (event.key === KeyboardKey.ARROW_DOWN) {
event.preventDefault();
if (selectedIndex < filteredPrompts.length - 1) {
selectedIndex++;
@ -276,7 +277,7 @@
return true;
}
if (event.key === 'ArrowUp') {
if (event.key === KeyboardKey.ARROW_UP) {
event.preventDefault();
if (selectedIndex > 0) {
selectedIndex--;
@ -285,7 +286,7 @@
return true;
}
if (event.key === 'Enter' && !selectedPrompt) {
if (event.key === KeyboardKey.ENTER && !selectedPrompt) {
event.preventDefault();
if (filteredPrompts[selectedIndex]) {
handlePromptClick(filteredPrompts[selectedIndex]);

View File

@ -6,8 +6,9 @@
} from '$lib/components/app';
import { config } from '$lib/stores/settings.svelte';
import { Wrench, Loader2, AlertTriangle, Brain } from '@lucide/svelte';
import { AgenticSectionType, AttachmentType } from '$lib/enums';
import { AgenticSectionType, AttachmentType, FileTypeText } from '$lib/enums';
import { formatJsonPretty } from '$lib/utils';
import { ATTACHMENT_SAVED_REGEX } from '$lib/constants/agentic-ui';
import { parseAgenticContent, type AgenticSection } from '$lib/utils/agentic';
import type { DatabaseMessage, DatabaseMessageExtraImageFile } from '$lib/types/database';
@ -78,7 +79,7 @@
): ToolResultLine[] {
const lines = toolResult.split('\n');
return lines.map((line) => {
const match = line.match(/\[Attachment saved: ([^\]]+)\]/);
const match = line.match(ATTACHMENT_SAVED_REGEX);
if (!match || !extras) return { text: line };
const attachmentName = match[1];
@ -124,7 +125,7 @@
{#if section.toolArgs}
<SyntaxHighlightedCode
code={formatJsonPretty(section.toolArgs)}
language="json"
language={FileTypeText.JSON}
maxHeight="20rem"
class="text-xs"
/>
@ -162,7 +163,7 @@
<SyntaxHighlightedCode
code={formatJsonPretty(section.toolArgs)}
language="json"
language={FileTypeText.JSON}
maxHeight="20rem"
class="text-xs"
/>

View File

@ -17,7 +17,7 @@
import { Button } from '$lib/components/ui/button';
import { Checkbox } from '$lib/components/ui/checkbox';
import { INPUT_CLASSES } from '$lib/constants/css-classes';
import { MessageRole } from '$lib/enums';
import { MessageRole, KeyboardKey } from '$lib/enums';
import Label from '$lib/components/ui/label/label.svelte';
import { config } from '$lib/stores/settings.svelte';
import { isRouterMode } from '$lib/stores/server.svelte';
@ -75,10 +75,10 @@
let shouldBranchAfterEdit = $state(false);
function handleEditKeydown(event: KeyboardEvent) {
if (event.key === 'Enter' && !event.shiftKey && !isIMEComposing(event)) {
if (event.key === KeyboardKey.ENTER && !event.shiftKey && !isIMEComposing(event)) {
event.preventDefault();
editCtx.save();
} else if (event.key === 'Escape') {
} else if (event.key === KeyboardKey.ESCAPE) {
event.preventDefault();
editCtx.cancel();
}

View File

@ -4,6 +4,7 @@
import { Switch } from '$lib/components/ui/switch';
import { ChatForm, DialogConfirmation } from '$lib/components/app';
import { getMessageEditContext } from '$lib/contexts';
import { KeyboardKey } from '$lib/enums';
import { chatStore } from '$lib/stores/chat.svelte';
import { processFilesToChatUploaded } from '$lib/utils/browser-only';
@ -34,7 +35,7 @@
let canSubmit = $derived(editCtx.editedContent.trim().length > 0 || hasAttachments);
function handleGlobalKeydown(event: KeyboardEvent) {
if (event.key === 'Escape') {
if (event.key === KeyboardKey.ESCAPE) {
event.preventDefault();
attemptCancel();
}

View File

@ -8,7 +8,7 @@
import { config } from '$lib/stores/settings.svelte';
import { isIMEComposing } from '$lib/utils';
import ChatMessageActions from './ChatMessageActions.svelte';
import { MessageRole } from '$lib/enums';
import { KeyboardKey, MessageRole } from '$lib/enums';
interface Props {
class?: string;
@ -48,11 +48,11 @@
const editCtx = getMessageEditContext();
function handleEditKeydown(event: KeyboardEvent) {
if (event.key === 'Enter' && !event.shiftKey && !isIMEComposing(event)) {
if (event.key === KeyboardKey.ENTER && !event.shiftKey && !isIMEComposing(event)) {
event.preventDefault();
editCtx.save();
} else if (event.key === 'Escape') {
} else if (event.key === KeyboardKey.ESCAPE) {
event.preventDefault();
editCtx.cancel();

View File

@ -13,6 +13,7 @@
import * as Alert from '$lib/components/ui/alert';
import * as AlertDialog from '$lib/components/ui/alert-dialog';
import { INITIAL_SCROLL_DELAY } from '$lib/constants/auto-scroll';
import { KeyboardKey } from '$lib/enums';
import { createAutoScrollController } from '$lib/hooks/use-auto-scroll.svelte';
import {
chatStore,
@ -211,7 +212,11 @@
function handleKeydown(event: KeyboardEvent) {
const isCtrlOrCmd = event.ctrlKey || event.metaKey;
if (isCtrlOrCmd && event.shiftKey && (event.key === 'd' || event.key === 'D')) {
if (
isCtrlOrCmd &&
event.shiftKey &&
(event.key === KeyboardKey.D_LOWER || event.key === KeyboardKey.D_UPPER)
) {
event.preventDefault();
if (activeConversation()) {
showDeleteDialog = true;

View File

@ -18,6 +18,13 @@
import { rehypeResolveAttachmentImages } from '$lib/markdown/resolve-attachment-images';
import { remarkLiteralHtml } from '$lib/markdown/literal-html';
import { copyCodeToClipboard, preprocessLaTeX, getImageErrorFallbackHtml } from '$lib/utils';
import {
IMAGE_NOT_ERROR_BOUND_SELECTOR,
DATA_ERROR_BOUND_ATTR,
DATA_ERROR_HANDLED_ATTR,
BOOL_TRUE_STRING
} from '$lib/constants/markdown';
import { UrlPrefix } from '$lib/enums';
import {
highlightCode,
detectIncompleteCodeBlock,
@ -470,10 +477,10 @@
function setupImageErrorHandlers() {
if (!containerRef) return;
const images = containerRef.querySelectorAll<HTMLImageElement>('img:not([data-error-bound])');
const images = containerRef.querySelectorAll<HTMLImageElement>(IMAGE_NOT_ERROR_BOUND_SELECTOR);
for (const img of images) {
img.dataset.errorBound = 'true';
img.dataset[DATA_ERROR_BOUND_ATTR] = BOOL_TRUE_STRING;
img.addEventListener('error', handleImageError);
}
}
@ -487,8 +494,12 @@
if (!img || !img.src) return;
// Don't handle data URLs or already-handled images
if (img.src.startsWith('data:') || img.dataset.errorHandled === 'true') return;
img.dataset.errorHandled = 'true';
if (
img.src.startsWith(UrlPrefix.DATA) ||
img.dataset[DATA_ERROR_HANDLED_ATTR] === BOOL_TRUE_STRING
)
return;
img.dataset[DATA_ERROR_HANDLED_ATTR] = BOOL_TRUE_STRING;
const src = img.src;
// Create fallback element

View File

@ -1,6 +1,7 @@
<script lang="ts">
import * as AlertDialog from '$lib/components/ui/alert-dialog';
import type { Component } from 'svelte';
import { KeyboardKey } from '$lib/enums';
interface Props {
open: boolean;
@ -29,7 +30,7 @@
}: Props = $props();
function handleKeydown(event: KeyboardEvent) {
if (event.key === 'Enter') {
if (event.key === KeyboardKey.ENTER) {
event.preventDefault();
onConfirm();
}

View File

@ -0,0 +1,34 @@
<script lang="ts">
import * as Card from '$lib/components/ui/card';
import { Skeleton } from '$lib/components/ui/skeleton';
</script>
<Card.Root class="grid gap-3 p-4">
<div class="flex items-center justify-between gap-4">
<div class="flex items-center gap-2">
<Skeleton class="h-5 w-5 rounded" />
<Skeleton class="h-5 w-28" />
<Skeleton class="h-5 w-12 rounded-full" />
</div>
<Skeleton class="h-6 w-11 rounded-full" />
</div>
<div class="flex flex-wrap gap-1.5">
<Skeleton class="h-5 w-14 rounded-full" />
<Skeleton class="h-5 w-12 rounded-full" />
<Skeleton class="h-5 w-16 rounded-full" />
</div>
<div class="space-y-1.5">
<Skeleton class="h-4 w-40" />
<Skeleton class="h-4 w-52" />
</div>
<Skeleton class="h-3.5 w-36" />
<div class="flex justify-end gap-2">
<Skeleton class="h-8 w-8 rounded" />
<Skeleton class="h-8 w-8 rounded" />
<Skeleton class="h-8 w-8 rounded" />
</div>
</Card.Root>

View File

@ -4,6 +4,8 @@
import { KeyValuePairs } from '$lib/components/app';
import type { KeyValuePair } from '$lib/types';
import { parseHeadersToArray, serializeHeaders } from '$lib/utils';
import { UrlPrefix } from '$lib/enums';
import { MCP_SERVER_URL_PLACEHOLDER } from '$lib/constants/mcp-form';
interface Props {
url: string;
@ -28,7 +30,8 @@
}: Props = $props();
let isWebSocket = $derived(
url.toLowerCase().startsWith('ws://') || url.toLowerCase().startsWith('wss://')
url.toLowerCase().startsWith(UrlPrefix.WEBSOCKET) ||
url.toLowerCase().startsWith(UrlPrefix.WEBSOCKET_SECURE)
);
let headerPairs = $derived<KeyValuePair[]>(parseHeadersToArray(headers));
@ -48,7 +51,7 @@
<Input
id="server-url-{id}"
type="url"
placeholder="https://mcp.example.com/sse"
placeholder={MCP_SERVER_URL_PLACEHOLDER}
value={url}
oninput={(e) => onUrlChange(e.currentTarget.value)}
class={urlError ? 'border-destructive' : ''}

View File

@ -5,8 +5,7 @@
import { getFaviconUrl } from '$lib/utils';
import { mcpStore } from '$lib/stores/mcp.svelte';
import { conversationsStore } from '$lib/stores/conversations.svelte';
import { McpServerCard, McpServerForm } from '$lib/components/app/mcp';
import { Skeleton } from '$lib/components/ui/skeleton';
import { McpServerCard, McpServerCardSkeleton, McpServerForm } from '$lib/components/app/mcp';
let servers = $derived(mcpStore.getServersSorted());
@ -135,35 +134,7 @@
<div class="space-y-3">
{#each servers as server (server.id)}
{#if !initialLoadComplete}
<Card.Root class="grid gap-3 p-4">
<div class="flex items-center justify-between gap-4">
<div class="flex items-center gap-2">
<Skeleton class="h-5 w-5 rounded" />
<Skeleton class="h-5 w-28" />
<Skeleton class="h-5 w-12 rounded-full" />
</div>
<Skeleton class="h-6 w-11 rounded-full" />
</div>
<div class="flex flex-wrap gap-1.5">
<Skeleton class="h-5 w-14 rounded-full" />
<Skeleton class="h-5 w-12 rounded-full" />
<Skeleton class="h-5 w-16 rounded-full" />
</div>
<div class="space-y-1.5">
<Skeleton class="h-4 w-40" />
<Skeleton class="h-4 w-52" />
</div>
<Skeleton class="h-3.5 w-36" />
<div class="flex justify-end gap-2">
<Skeleton class="h-8 w-8 rounded" />
<Skeleton class="h-8 w-8 rounded" />
<Skeleton class="h-8 w-8 rounded" />
</div>
</Card.Root>
<McpServerCardSkeleton />
{:else}
<McpServerCard
{server}

View File

@ -204,6 +204,9 @@ export { default as McpServerCardEditForm } from './McpServerCard/McpServerCardE
/** Delete confirmation dialog with server name display. */
export { default as McpServerCardDeleteDialog } from './McpServerCard/McpServerCardDeleteDialog.svelte';
/** Skeleton loading state for server card during health checks. */
export { default as McpServerCardSkeleton } from './McpServerCardSkeleton.svelte';
/**
* **McpServerInfo** - Server instructions display
*

View File

@ -12,7 +12,7 @@
routerModels,
singleModelName
} from '$lib/stores/models.svelte';
import { ServerModelStatus } from '$lib/enums';
import { KeyboardKey, ServerModelStatus } from '$lib/enums';
import { isRouterMode } from '$lib/stores/server.svelte';
import {
DialogModelInformation,
@ -133,7 +133,7 @@
function handleSearchKeyDown(event: KeyboardEvent) {
if (event.isComposing) return;
if (event.key === 'ArrowDown') {
if (event.key === KeyboardKey.ARROW_DOWN) {
event.preventDefault();
if (filteredOptions.length === 0) return;
@ -142,7 +142,7 @@
} else {
highlightedIndex += 1;
}
} else if (event.key === 'ArrowUp') {
} else if (event.key === KeyboardKey.ARROW_UP) {
event.preventDefault();
if (filteredOptions.length === 0) return;
@ -151,7 +151,7 @@
} else {
highlightedIndex -= 1;
}
} else if (event.key === 'Enter') {
} else if (event.key === KeyboardKey.ENTER) {
event.preventDefault();
if (highlightedIndex >= 0 && highlightedIndex < filteredOptions.length) {
const option = filteredOptions[highlightedIndex];

View File

@ -8,6 +8,7 @@
import { serverStore, serverLoading } from '$lib/stores/server.svelte';
import { config, settingsStore } from '$lib/stores/settings.svelte';
import { fade, fly, scale } from 'svelte/transition';
import { KeyboardKey } from '$lib/enums';
interface Props {
class?: string;
@ -117,7 +118,7 @@
}
function handleApiKeyKeydown(event: KeyboardEvent) {
if (event.key === 'Enter') {
if (event.key === KeyboardKey.ENTER) {
handleSaveApiKey();
}
}

View File

@ -0,0 +1 @@
export const ATTACHMENT_SAVED_REGEX = /\[Attachment saved: ([^\]]+)\]/;

View File

@ -0,0 +1,2 @@
export const INITIAL_FILE_SIZE = 0;
export const PROMPT_CONTENT_SEPARATOR = '\n\n';

View File

@ -0,0 +1,4 @@
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';

View File

@ -0,0 +1 @@
export const MCP_SERVER_URL_PLACEHOLDER = 'https://mcp.example.com/sse';

View File

@ -44,3 +44,5 @@ export { ServerRole, ServerModelStatus } from './server';
export { ParameterSource, SyncableParameterType } from './settings';
export { ColorMode, McpPromptVariant, UrlPrefix } from './ui';
export { KeyboardKey } from './keyboard';

View File

@ -0,0 +1,15 @@
/**
* Keyboard key names for event handling
*/
export enum KeyboardKey {
ENTER = 'Enter',
ESCAPE = 'Escape',
ARROW_UP = 'ArrowUp',
ARROW_DOWN = 'ArrowDown',
TAB = 'Tab',
D_LOWER = 'd',
D_UPPER = 'D',
E_UPPER = 'E',
K_LOWER = 'k',
O_UPPER = 'O'
}

View File

@ -15,6 +15,7 @@
import { goto } from '$app/navigation';
import { modelsStore } from '$lib/stores/models.svelte';
import { TOOLTIP_DELAY_DURATION } from '$lib/constants/tooltip-config';
import { KeyboardKey } from '$lib/enums';
import { IsMobile } from '$lib/hooks/is-mobile.svelte';
let { children } = $props();
@ -43,7 +44,7 @@
function handleKeydown(event: KeyboardEvent) {
const isCtrlOrCmd = event.ctrlKey || event.metaKey;
if (isCtrlOrCmd && event.key === 'k') {
if (isCtrlOrCmd && event.key === KeyboardKey.K_LOWER) {
event.preventDefault();
if (chatSidebar?.activateSearchMode) {
chatSidebar.activateSearchMode();
@ -51,12 +52,12 @@
}
}
if (isCtrlOrCmd && event.shiftKey && event.key === 'O') {
if (isCtrlOrCmd && event.shiftKey && event.key === KeyboardKey.O_UPPER) {
event.preventDefault();
goto('?new_chat=true#/');
}
if (event.shiftKey && isCtrlOrCmd && event.key === 'E') {
if (event.shiftKey && isCtrlOrCmd && event.key === KeyboardKey.E_UPPER) {
event.preventDefault();
if (chatSidebar?.editActiveConversation) {