refactor: Enums

This commit is contained in:
Aleksander Grygier 2026-01-24 18:37:43 +01:00
parent 85b8da45f9
commit 13f756421c
16 changed files with 84 additions and 65 deletions

View File

@ -48,7 +48,7 @@ import type {
DatabaseMessageExtraImageFile,
McpServerOverride
} from '$lib/types/database';
import { AttachmentType } from '$lib/enums';
import { AttachmentType, MessageRole } from '$lib/enums';
export interface AgenticFlowCallbacks {
onChunk?: (chunk: string) => void;
@ -190,7 +190,7 @@ export class AgenticClient {
return msg as ApiChatMessageData;
})
.filter((msg) => {
if (msg.role === 'system') {
if (msg.role === MessageRole.SYSTEM) {
const content = typeof msg.content === 'string' ? msg.content : '';
return content.trim().length > 0;
}
@ -467,7 +467,7 @@ export class AgenticClient {
onToolCallChunk?.(JSON.stringify(allToolCalls));
sessionMessages.push({
role: 'assistant',
role: MessageRole.ASSISTANT,
content: turnContent || undefined,
tool_calls: normalizedCalls
});
@ -562,7 +562,7 @@ export class AgenticClient {
}
sessionMessages.push({
role: 'tool',
role: MessageRole.TOOL,
tool_call_id: toolCall.id,
content: contentParts.length === 1 ? cleanedResult : contentParts
});

View File

@ -20,6 +20,7 @@ import { SYSTEM_MESSAGE_PLACEHOLDER } from '$lib/constants/ui';
import { REASONING_TAGS } from '$lib/constants/agentic';
import type { ChatMessageTimings, ChatMessagePromptProgress } from '$lib/types/chat';
import type { DatabaseMessage, DatabaseMessageExtra } from '$lib/types/database';
import { MessageRole, MessageType } from '$lib/enums';
export interface ApiProcessingState {
status: 'idle' | 'preparing' | 'generating';
@ -68,9 +69,6 @@ export interface ChatStreamCallbacks {
onError?: (error: Error) => void;
}
type ChatRole = 'user' | 'assistant' | 'system' | 'tool';
type ChatMessageType = 'text' | 'root';
interface ChatStoreStateCallbacks {
setChatLoading: (convId: string, loading: boolean) => void;
setChatStreaming: (convId: string, response: string, messageId: string) => void;
@ -157,7 +155,7 @@ export class ChatClient {
private getMessageByIdWithRole(
messageId: string,
expectedRole?: ChatRole
expectedRole?: MessageRole
): { message: DatabaseMessage; index: number } | null {
const index = conversationsStore.findMessageIndex(messageId);
if (index === -1) return null;
@ -178,9 +176,9 @@ export class ChatClient {
* @returns The created message or null if failed
*/
async addMessage(
role: ChatRole,
role: MessageRole,
content: string,
type: ChatMessageType = 'text',
type: MessageType = MessageType.TEXT,
parent: string = '-1',
extras?: DatabaseMessageExtra[]
): Promise<DatabaseMessage | null> {
@ -261,7 +259,7 @@ export class ChatClient {
}
const existingSystemMessage = allMessages.find(
(m) => m.role === 'system' && m.parent === rootId
(m) => m.role === MessageRole.SYSTEM && m.parent === rootId
);
if (existingSystemMessage) {
@ -326,7 +324,7 @@ export class ChatClient {
try {
const allMessages = await conversationsStore.getConversationMessages(activeConv.id);
const systemMessage = allMessages.find((m) => m.id === messageId);
if (!systemMessage || systemMessage.role !== 'system') return false;
if (!systemMessage || systemMessage.role !== MessageRole.SYSTEM) return false;
const rootMessage = allMessages.find((m) => m.type === 'root' && m.parent === null);
if (!rootMessage) return false;
@ -385,7 +383,7 @@ export class ChatClient {
{
convId: activeConv.id,
type: 'text',
role: 'assistant',
role: MessageRole.ASSISTANT,
content: '',
timestamp: Date.now(),
toolCalls: '',
@ -443,9 +441,9 @@ export class ChatClient {
}
const userMessage = await this.addMessage(
'user',
MessageRole.USER,
content,
'text',
MessageType.TEXT,
parentIdForUserMessage ?? '-1',
extras
);
@ -768,7 +766,7 @@ export class ChatClient {
const lastMessage = messages[messages.length - 1];
if (lastMessage?.role === 'assistant') {
if (lastMessage?.role === MessageRole.ASSISTANT) {
try {
const updateData: { content: string; timings?: ChatMessageTimings } = {
content: streamingState.response
@ -818,7 +816,7 @@ export class ChatClient {
if (!activeConv) return;
if (this.isChatLoading(activeConv.id)) await this.stopGeneration();
const result = this.getMessageByIdWithRole(messageId, 'user');
const result = this.getMessageByIdWithRole(messageId, MessageRole.USER);
if (!result) return;
const { message: messageToUpdate, index: messageIndex } = result;
const originalContent = messageToUpdate.content;
@ -886,7 +884,7 @@ export class ChatClient {
const activeConv = conversationsStore.activeConversation;
if (!activeConv || this.isChatLoading(activeConv.id)) return;
const result = this.getMessageByIdWithRole(messageId, 'assistant');
const result = this.getMessageByIdWithRole(messageId, MessageRole.ASSISTANT);
if (!result) return;
const { index: messageIndex } = result;
@ -929,7 +927,7 @@ export class ChatClient {
const idx = conversationsStore.findMessageIndex(messageId);
if (idx === -1) return;
const msg = conversationsStore.activeMessages[idx];
if (msg.role !== 'assistant') return;
if (msg.role !== MessageRole.ASSISTANT) return;
const allMessages = await conversationsStore.getConversationMessages(activeConv.id);
const parentMessage = allMessages.find((m) => m.id === msg.parent);
@ -1006,10 +1004,10 @@ export class ChatClient {
assistantMessages = 0;
const messageTypes: string[] = [];
for (const msg of messagesToDelete) {
if (msg.role === 'user') {
if (msg.role === MessageRole.USER) {
userMessages++;
if (!messageTypes.includes('user message')) messageTypes.push('user message');
} else if (msg.role === 'assistant') {
} else if (msg.role === MessageRole.ASSISTANT) {
assistantMessages++;
if (!messageTypes.includes('assistant response')) messageTypes.push('assistant response');
}
@ -1075,7 +1073,7 @@ export class ChatClient {
const activeConv = conversationsStore.activeConversation;
if (!activeConv || this.isChatLoading(activeConv.id)) return;
const result = this.getMessageByIdWithRole(messageId, 'assistant');
const result = this.getMessageByIdWithRole(messageId, MessageRole.ASSISTANT);
if (!result) return;
const { message: msg, index: idx } = result;
@ -1097,7 +1095,7 @@ export class ChatClient {
const conversationContext = conversationsStore.activeMessages.slice(0, idx);
const contextWithContinue = [
...conversationContext,
{ role: 'assistant' as const, content: originalContent }
{ role: MessageRole.ASSISTANT as const, content: originalContent }
];
let appendedContent = '';
@ -1252,7 +1250,7 @@ export class ChatClient {
const activeConv = conversationsStore.activeConversation;
if (!activeConv || this.isChatLoading(activeConv.id)) return;
const result = this.getMessageByIdWithRole(messageId, 'assistant');
const result = this.getMessageByIdWithRole(messageId, MessageRole.ASSISTANT);
if (!result) return;
const { message: msg, index: idx } = result;
@ -1301,7 +1299,7 @@ export class ChatClient {
const activeConv = conversationsStore.activeConversation;
if (!activeConv) return;
const result = this.getMessageByIdWithRole(messageId, 'user');
const result = this.getMessageByIdWithRole(messageId, MessageRole.USER);
if (!result) return;
const { message: msg, index: idx } = result;
@ -1347,10 +1345,10 @@ export class ChatClient {
const activeConv = conversationsStore.activeConversation;
if (!activeConv || this.isChatLoading(activeConv.id)) return;
let result = this.getMessageByIdWithRole(messageId, 'user');
let result = this.getMessageByIdWithRole(messageId, MessageRole.USER);
if (!result) {
result = this.getMessageByIdWithRole(messageId, 'system');
result = this.getMessageByIdWithRole(messageId, MessageRole.SYSTEM);
}
if (!result) return;
@ -1360,7 +1358,7 @@ export class ChatClient {
const allMessages = await conversationsStore.getConversationMessages(activeConv.id);
const rootMessage = allMessages.find((m) => m.type === 'root' && m.parent === null);
const isFirstUserMessage =
msg.role === 'user' && rootMessage && msg.parent === rootMessage.id;
msg.role === MessageRole.USER && rootMessage && msg.parent === rootMessage.id;
const parentId = msg.parent || rootMessage?.id;
if (!parentId) return;
@ -1397,7 +1395,7 @@ export class ChatClient {
}
await conversationsStore.refreshActiveMessages();
if (msg.role === 'user') {
if (msg.role === MessageRole.USER) {
await this.generateResponseForMessage(newMessage.id);
}
} catch (error) {
@ -1426,7 +1424,7 @@ export class ChatClient {
convId: activeConv.id,
type: 'text',
timestamp: Date.now(),
role: 'assistant',
role: MessageRole.ASSISTANT,
content: '',
toolCalls: '',
children: [],
@ -1566,7 +1564,7 @@ export class ChatClient {
restoreProcessingStateFromMessages(messages: DatabaseMessage[], conversationId: string): void {
for (let i = messages.length - 1; i >= 0; i--) {
const message = messages[i];
if (message.role === 'assistant' && message.timings) {
if (message.role === MessageRole.ASSISTANT && message.timings) {
const restoredState = this.parseTimingData({
prompt_n: message.timings.prompt_n || 0,
prompt_ms: message.timings.prompt_ms,
@ -1592,7 +1590,7 @@ export class ChatClient {
getConversationModel(messages: DatabaseMessage[]): string | null {
for (let i = messages.length - 1; i >= 0; i--) {
const message = messages[i];
if (message.role === 'assistant' && message.model) {
if (message.role === MessageRole.ASSISTANT && message.model) {
return message.model;
}
}

View File

@ -28,6 +28,7 @@ import { filterByLeafNodeId, findLeafNode } from '$lib/utils';
import { getEnabledServersForConversation } from '$lib/utils/mcp';
import { mcpClient } from '$lib/clients/mcp.client';
import type { McpServerOverride } from '$lib/types/database';
import { MessageRole } from '$lib/enums';
interface ConversationsStoreStateCallbacks {
getConversations: () => DatabaseConversation[];
@ -410,7 +411,7 @@ export class ConversationsClient {
const rootMessage = allMessages.find((m) => m.type === 'root' && m.parent === null);
const activeMessages = this.store.getActiveMessages();
const currentFirstUserMessage = activeMessages.find(
(m) => m.role === 'user' && m.parent === rootMessage?.id
(m) => m.role === MessageRole.USER && m.parent === rootMessage?.id
);
const currentLeafNodeId = findLeafNode(allMessages, siblingId);
@ -422,7 +423,7 @@ export class ConversationsClient {
const updatedActiveMessages = this.store.getActiveMessages();
if (rootMessage && updatedActiveMessages.length > 0) {
const newFirstUserMessage = updatedActiveMessages.find(
(m) => m.role === 'user' && m.parent === rootMessage.id
(m) => m.role === MessageRole.USER && m.parent === rootMessage.id
);
if (

View File

@ -121,7 +121,7 @@
role="group"
aria-label="Assistant message with actions"
>
{#if message?.role === 'assistant' && isLoading() && !message?.content?.trim()}
{#if message?.role === MessageRole.ASSISTANT && isLoading() && !message?.content?.trim()}
<div class="mt-6 w-full max-w-[48rem]" in:fade>
<div class="processing-container">
<span class="processing-text">
@ -169,7 +169,7 @@
</div>
</div>
</div>
{:else if message.role === 'assistant'}
{:else if message.role === MessageRole.ASSISTANT}
{#if showRawOutput}
<pre class="raw-output">{messageContent || ''}</pre>
{:else if isStructuredContent}

View File

@ -1,5 +1,6 @@
<script lang="ts">
import { ChatMessage } from '$lib/components/app';
import { MessageRole } from '$lib/enums';
import { chatStore } from '$lib/stores/chat.svelte';
import { conversationsStore, activeConversation } from '$lib/stores/conversations.svelte';
import { config } from '$lib/stores/settings.svelte';
@ -45,7 +46,7 @@
// Filter out system messages if showSystemMessage is false
const filteredMessages = currentConfig.showSystemMessage
? messages
: messages.filter((msg) => msg.type !== 'system');
: messages.filter((msg) => msg.type !== MessageRole.SYSTEM);
return filteredMessages.map((message) => {
const siblingInfo = getMessageSiblings(allConversationMessages, message.id);

View File

@ -20,6 +20,7 @@
import { ScrollArea } from '$lib/components/ui/scroll-area';
import { config, settingsStore } from '$lib/stores/settings.svelte';
import { setMode } from 'mode-watcher';
import { ColorMode } from '$lib/enums/ui';
import type { Component } from 'svelte';
interface Props {
@ -42,9 +43,9 @@
label: 'Theme',
type: 'select',
options: [
{ value: 'system', label: 'System', icon: Monitor },
{ value: 'light', label: 'Light', icon: Sun },
{ value: 'dark', label: 'Dark', icon: Moon }
{ value: ColorMode.SYSTEM, label: 'System', icon: Monitor },
{ value: ColorMode.LIGHT, label: 'Light', icon: Sun },
{ value: ColorMode.DARK, label: 'Dark', icon: Moon }
]
},
{ key: 'apiKey', label: 'API Key', type: 'input' },
@ -315,7 +316,7 @@
function handleThemeChange(newTheme: string) {
localConfig.theme = newTheme;
setMode(newTheme as 'light' | 'dark' | 'system');
setMode(newTheme as ColorMode);
}
function handleConfigChange(key: string, value: string | boolean) {
@ -325,7 +326,7 @@
function handleReset() {
localConfig = { ...config() };
setMode(localConfig.theme as 'light' | 'dark' | 'system');
setMode(localConfig.theme as ColorMode);
}
function handleSave() {

View File

@ -1,10 +1,12 @@
import { ColorMode } from '$lib/enums/ui';
export const SETTING_CONFIG_DEFAULT: Record<string, string | number | boolean> = {
// Note: in order not to introduce breaking changes, please keep the same data type (number, string, etc) if you want to change the default value. Do not use null or undefined for default value.
// Do not use nested objects, keep it single level. Prefix the key if you need to group them.
apiKey: '',
systemMessage: '',
showSystemMessage: true,
theme: 'system',
theme: ColorMode.SYSTEM,
showThoughtInProgress: false,
disableReasoningParsing: false,
showRawOutputSwitch: false,

View File

@ -11,7 +11,8 @@ export enum ChatMessageStatsView {
export enum MessageRole {
USER = 'user',
ASSISTANT = 'assistant',
SYSTEM = 'system'
SYSTEM = 'system',
TOOL = 'tool'
}
/**

View File

@ -0,0 +1,5 @@
export enum ColorMode {
LIGHT = 'light',
DARK = 'dark',
SYSTEM = 'system'
}

View File

@ -1,6 +1,6 @@
import { getJsonHeaders } from '$lib/utils';
import { AGENTIC_REGEX } from '$lib/constants/agentic';
import { AttachmentType } from '$lib/enums';
import { AttachmentType, MessageRole } from '$lib/enums';
import type { ApiChatMessageContentPart } from '$lib/types/api';
import type { DatabaseMessageExtraMcpPrompt } from '$lib/types';
@ -141,7 +141,7 @@ export class ChatService {
})
.filter((msg) => {
// Filter out empty system messages
if (msg.role === 'system') {
if (msg.role === MessageRole.SYSTEM) {
const content = typeof msg.content === 'string' ? msg.content : '';
return content.trim().length > 0;
@ -634,7 +634,7 @@ export class ChatService {
): ApiChatMessageData {
if (!message.extra || message.extra.length === 0) {
return {
role: message.role as 'user' | 'assistant' | 'system',
role: message.role as MessageRole,
content: message.content
};
}
@ -735,7 +735,7 @@ export class ChatService {
}
return {
role: message.role as 'user' | 'assistant' | 'system',
role: message.role as MessageRole,
content: contentParts
};
}

View File

@ -17,6 +17,7 @@ class LlamacppDatabase extends Dexie {
const db = new LlamacppDatabase();
import { v4 as uuid } from 'uuid';
import { MessageRole } from '$lib/enums';
/**
* DatabaseService - Stateless IndexedDB communication layer
@ -162,7 +163,7 @@ export class DatabaseService {
convId,
type: 'root',
timestamp: Date.now(),
role: 'system',
role: MessageRole.SYSTEM,
content: '',
parent: null,
toolCalls: '',
@ -195,9 +196,9 @@ export class DatabaseService {
const systemMessage: DatabaseMessage = {
id: uuid(),
convId,
type: 'system',
type: MessageRole.SYSTEM,
timestamp: Date.now(),
role: 'system',
role: MessageRole.SYSTEM,
content: trimmedPrompt,
parent: parentId,
children: []

View File

@ -23,6 +23,7 @@ import { SvelteMap } from 'svelte/reactivity';
import { browser } from '$app/environment';
import { chatClient, type ApiProcessingState, type ErrorDialogState } from '$lib/clients';
import type { DatabaseMessage, DatabaseMessageExtra } from '$lib/types/database';
import { MessageRole, MessageType } from '$lib/enums';
export type { ApiProcessingState, ErrorDialogState };
@ -265,9 +266,9 @@ class ChatStore {
}
async addMessage(
role: 'user' | 'assistant' | 'system' | 'tool',
role: MessageRole,
content: string,
type: 'text' | 'root' = 'text',
type: MessageType = MessageType.TEXT,
parent: string = '-1',
extras?: DatabaseMessageExtra[]
): Promise<DatabaseMessage | null> {

View File

@ -1,3 +1,4 @@
import type { MessageRole } from '$lib/enums';
import type { ApiChatCompletionRequest, ApiChatMessageContentPart } from './api';
/**
@ -26,21 +27,21 @@ export type AgenticToolCallPayload = {
*/
export type AgenticMessage =
| {
role: 'system' | 'user';
role: MessageRole.SYSTEM | MessageRole.USER;
content: string | ApiChatMessageContentPart[];
}
| {
role: 'assistant';
role: MessageRole.ASSISTANT;
content?: string | ApiChatMessageContentPart[];
tool_calls?: AgenticToolCallPayload[];
}
| {
role: 'tool';
role: MessageRole.TOOL;
tool_call_id: string;
content: string | ApiChatMessageContentPart[];
};
export type AgenticAssistantMessage = Extract<AgenticMessage, { role: 'assistant' }>;
export type AgenticAssistantMessage = Extract<AgenticMessage, { role: MessageRole.ASSISTANT }>;
export type AgenticToolCallList = NonNullable<AgenticAssistantMessage['tool_calls']>;
export type AgenticChatCompletionRequest = Omit<ApiChatCompletionRequest, 'messages'> & {

View File

@ -1,5 +1,5 @@
import type { ServerModelStatus, ServerRole } from '$lib/enums';
import type { ChatMessagePromptProgress } from './chat';
import type { ChatMessagePromptProgress, ChatRole } from './chat';
export interface ApiChatCompletionToolFunction {
name: string;

View File

@ -5,6 +5,7 @@ import type { McpServerOverride } from '$lib/types/database';
import { DEFAULT_AGENTIC_CONFIG } from '$lib/constants/agentic';
import { normalizePositiveNumber } from '$lib/utils/number';
import { mcpStore } from '$lib/stores/mcp.svelte';
import { MessageRole } from '$lib/enums';
/**
* Gets the current agentic configuration.
@ -37,9 +38,13 @@ export function getAgenticConfig(
*/
export function toAgenticMessages(messages: ApiChatMessageData[]): AgenticMessage[] {
return messages.map((message) => {
if (message.role === 'assistant' && message.tool_calls && message.tool_calls.length > 0) {
if (
message.role === MessageRole.ASSISTANT &&
message.tool_calls &&
message.tool_calls.length > 0
) {
return {
role: 'assistant',
role: MessageRole.ASSISTANT,
content: message.content,
tool_calls: message.tool_calls.map((call, index) => ({
id: call.id ?? `call_${index}`,
@ -52,16 +57,16 @@ export function toAgenticMessages(messages: ApiChatMessageData[]): AgenticMessag
} satisfies AgenticMessage;
}
if (message.role === 'tool' && message.tool_call_id) {
if (message.role === MessageRole.TOOL && message.tool_call_id) {
return {
role: 'tool',
role: MessageRole.TOOL,
tool_call_id: message.tool_call_id,
content: typeof message.content === 'string' ? message.content : ''
} satisfies AgenticMessage;
}
return {
role: message.role,
role: message.role as MessageRole.SYSTEM | MessageRole.USER,
content: message.content
} satisfies AgenticMessage;
});

View File

@ -15,6 +15,8 @@
* message 5 (assistant)
*/
import { MessageRole } from '$lib/enums';
/**
* Filters messages to get the conversation path from root to a specific leaf node.
* If the leafNodeId doesn't exist, returns the path with the latest timestamp.
@ -67,8 +69,8 @@ export function filterByLeafNodeId(
// Sort: system messages first, then by timestamp
result.sort((a, b) => {
if (a.role === 'system' && b.role !== 'system') return -1;
if (a.role !== 'system' && b.role === 'system') return 1;
if (a.role === MessageRole.SYSTEM && b.role !== MessageRole.SYSTEM) return -1;
if (a.role !== MessageRole.SYSTEM && b.role === MessageRole.SYSTEM) return 1;
return a.timestamp - b.timestamp;
});