refactor: Server store
This commit is contained in:
parent
456828b365
commit
d6ee3d133a
|
|
@ -16,7 +16,7 @@
|
||||||
supportsVision,
|
supportsVision,
|
||||||
fetchModelProps,
|
fetchModelProps,
|
||||||
getModelProps
|
getModelProps
|
||||||
} from '$lib/stores/props.svelte';
|
} from '$lib/stores/server.svelte';
|
||||||
import { getConversationModel } from '$lib/stores/chat.svelte';
|
import { getConversationModel } from '$lib/stores/chat.svelte';
|
||||||
import { activeMessages } from '$lib/stores/conversations.svelte';
|
import { activeMessages } from '$lib/stores/conversations.svelte';
|
||||||
import {
|
import {
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@
|
||||||
isRouterMode,
|
isRouterMode,
|
||||||
fetchModelProps,
|
fetchModelProps,
|
||||||
getModelProps
|
getModelProps
|
||||||
} from '$lib/stores/props.svelte';
|
} from '$lib/stores/server.svelte';
|
||||||
import { config } from '$lib/stores/settings.svelte';
|
import { config } from '$lib/stores/settings.svelte';
|
||||||
import { modelOptions, selectedModelId, selectModelByName } from '$lib/stores/models.svelte';
|
import { modelOptions, selectedModelId, selectModelByName } from '$lib/stores/models.svelte';
|
||||||
import { getConversationModel } from '$lib/stores/chat.svelte';
|
import { getConversationModel } from '$lib/stores/chat.svelte';
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@
|
||||||
import { INPUT_CLASSES } from '$lib/constants/input-classes';
|
import { INPUT_CLASSES } from '$lib/constants/input-classes';
|
||||||
import Label from '$lib/components/ui/label/label.svelte';
|
import Label from '$lib/components/ui/label/label.svelte';
|
||||||
import { config } from '$lib/stores/settings.svelte';
|
import { config } from '$lib/stores/settings.svelte';
|
||||||
import { isRouterMode } from '$lib/stores/props.svelte';
|
import { isRouterMode } from '$lib/stores/server.svelte';
|
||||||
import { selectModel } from '$lib/stores/models.svelte';
|
import { selectModel } from '$lib/stores/models.svelte';
|
||||||
import { copyToClipboard } from '$lib/utils/copy';
|
import { copyToClipboard } from '$lib/utils/copy';
|
||||||
import type { ApiChatCompletionToolCall } from '$lib/types/api';
|
import type { ApiChatCompletionToolCall } from '$lib/types/api';
|
||||||
|
|
|
||||||
|
|
@ -32,13 +32,13 @@
|
||||||
import {
|
import {
|
||||||
supportsVision,
|
supportsVision,
|
||||||
supportsAudio,
|
supportsAudio,
|
||||||
propsLoading,
|
serverLoading,
|
||||||
propsError,
|
serverError,
|
||||||
propsStore,
|
serverStore,
|
||||||
isRouterMode,
|
isRouterMode,
|
||||||
fetchModelProps,
|
fetchModelProps,
|
||||||
getModelProps
|
getModelProps
|
||||||
} from '$lib/stores/props.svelte';
|
} from '$lib/stores/server.svelte';
|
||||||
import { modelOptions, selectedModelId } from '$lib/stores/models.svelte';
|
import { modelOptions, selectedModelId } from '$lib/stores/models.svelte';
|
||||||
import { getConversationModel } from '$lib/stores/chat.svelte';
|
import { getConversationModel } from '$lib/stores/chat.svelte';
|
||||||
import { parseFilesToMessageExtras } from '$lib/utils/convert-files-to-extra';
|
import { parseFilesToMessageExtras } from '$lib/utils/convert-files-to-extra';
|
||||||
|
|
@ -87,8 +87,8 @@
|
||||||
);
|
);
|
||||||
|
|
||||||
let activeErrorDialog = $derived(errorDialog());
|
let activeErrorDialog = $derived(errorDialog());
|
||||||
let isServerLoading = $derived(propsLoading());
|
let isServerLoading = $derived(serverLoading());
|
||||||
let hasPropsError = $derived(!!propsError());
|
let hasPropsError = $derived(!!serverError());
|
||||||
|
|
||||||
let isCurrentConversationLoading = $derived(isLoading());
|
let isCurrentConversationLoading = $derived(isLoading());
|
||||||
|
|
||||||
|
|
@ -407,10 +407,10 @@
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<AlertTriangle class="h-4 w-4 text-destructive" />
|
<AlertTriangle class="h-4 w-4 text-destructive" />
|
||||||
<span class="text-sm font-medium text-destructive">Server unavailable</span>
|
<span class="text-sm font-medium text-destructive">Server unavailable</span>
|
||||||
<span class="text-sm text-muted-foreground">— {propsError()}</span>
|
<span class="text-sm text-muted-foreground">— {serverError()}</span>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onclick={() => propsStore.fetch()}
|
onclick={() => serverStore.fetch()}
|
||||||
disabled={isServerLoading}
|
disabled={isServerLoading}
|
||||||
class="flex items-center gap-1.5 rounded-lg bg-destructive/20 px-3 py-1.5 text-xs font-medium text-destructive hover:bg-destructive/30 disabled:opacity-50"
|
class="flex items-center gap-1.5 rounded-lg bg-destructive/20 px-3 py-1.5 text-xs font-medium text-destructive hover:bg-destructive/30 disabled:opacity-50"
|
||||||
>
|
>
|
||||||
|
|
@ -454,7 +454,7 @@
|
||||||
<h1 class="mb-4 text-3xl font-semibold tracking-tight">llama.cpp</h1>
|
<h1 class="mb-4 text-3xl font-semibold tracking-tight">llama.cpp</h1>
|
||||||
|
|
||||||
<p class="text-lg text-muted-foreground">
|
<p class="text-lg text-muted-foreground">
|
||||||
{propsStore.serverProps?.modalities?.audio
|
{serverStore.props?.modalities?.audio
|
||||||
? 'Record audio, type a message '
|
? 'Record audio, type a message '
|
||||||
: 'Type a message'} or upload files to get started
|
: 'Type a message'} or upload files to get started
|
||||||
</p>
|
</p>
|
||||||
|
|
@ -467,10 +467,10 @@
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<AlertTriangle class="h-4 w-4 text-destructive" />
|
<AlertTriangle class="h-4 w-4 text-destructive" />
|
||||||
<span class="text-sm font-medium text-destructive">Server unavailable</span>
|
<span class="text-sm font-medium text-destructive">Server unavailable</span>
|
||||||
<span class="text-sm text-muted-foreground">— {propsError()}</span>
|
<span class="text-sm text-muted-foreground">— {serverError()}</span>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onclick={() => propsStore.fetch()}
|
onclick={() => serverStore.fetch()}
|
||||||
disabled={isServerLoading}
|
disabled={isServerLoading}
|
||||||
class="flex items-center gap-1.5 rounded-lg bg-destructive/20 px-3 py-1.5 text-xs font-medium text-destructive hover:bg-destructive/30 disabled:opacity-50"
|
class="flex items-center gap-1.5 rounded-lg bg-destructive/20 px-3 py-1.5 text-xs font-medium text-destructive hover:bg-destructive/30 disabled:opacity-50"
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
import * as Select from '$lib/components/ui/select';
|
import * as Select from '$lib/components/ui/select';
|
||||||
import { Textarea } from '$lib/components/ui/textarea';
|
import { Textarea } from '$lib/components/ui/textarea';
|
||||||
import { SETTING_CONFIG_DEFAULT, SETTING_CONFIG_INFO } from '$lib/constants/settings-config';
|
import { SETTING_CONFIG_DEFAULT, SETTING_CONFIG_INFO } from '$lib/constants/settings-config';
|
||||||
import { supportsVision } from '$lib/stores/props.svelte';
|
import { supportsVision } from '$lib/stores/server.svelte';
|
||||||
import { getParameterInfo, resetParameterToServerDefault } from '$lib/stores/settings.svelte';
|
import { getParameterInfo, resetParameterToServerDefault } from '$lib/stores/settings.svelte';
|
||||||
import { ParameterSyncService } from '$lib/services/parameter-sync';
|
import { ParameterSyncService } from '$lib/services/parameter-sync';
|
||||||
import { ChatSettingsParameterSourceIndicator } from '$lib/components/app';
|
import { ChatSettingsParameterSourceIndicator } from '$lib/components/app';
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
import * as Dialog from '$lib/components/ui/dialog';
|
import * as Dialog from '$lib/components/ui/dialog';
|
||||||
import * as Table from '$lib/components/ui/table';
|
import * as Table from '$lib/components/ui/table';
|
||||||
import { BadgeModality, CopyToClipboardIcon } from '$lib/components/app';
|
import { BadgeModality, CopyToClipboardIcon } from '$lib/components/app';
|
||||||
import { propsStore } from '$lib/stores/props.svelte';
|
import { serverStore } from '$lib/stores/server.svelte';
|
||||||
import { ChatService } from '$lib/services/chat';
|
import { ChatService } from '$lib/services/chat';
|
||||||
import type { ApiModelListResponse } from '$lib/types/api';
|
import type { ApiModelListResponse } from '$lib/types/api';
|
||||||
import { formatFileSize, formatParameters, formatNumber } from '$lib/utils/formatters';
|
import { formatFileSize, formatParameters, formatNumber } from '$lib/utils/formatters';
|
||||||
|
|
@ -14,8 +14,8 @@
|
||||||
|
|
||||||
let { open = $bindable(), onOpenChange }: Props = $props();
|
let { open = $bindable(), onOpenChange }: Props = $props();
|
||||||
|
|
||||||
let serverProps = $derived(propsStore.serverProps);
|
let serverProps = $derived(serverStore.props);
|
||||||
let modalities = $derived(propsStore.supportedModalities);
|
let modalities = $derived(serverStore.supportedModalities);
|
||||||
|
|
||||||
let modelsData = $state<ApiModelListResponse | null>(null);
|
let modelsData = $state<ApiModelListResponse | null>(null);
|
||||||
let isLoadingModels = $state(false);
|
let isLoadingModels = $state(false);
|
||||||
|
|
@ -77,12 +77,12 @@
|
||||||
class="resizable-text-container min-w-0 flex-1 truncate"
|
class="resizable-text-container min-w-0 flex-1 truncate"
|
||||||
style:--threshold="12rem"
|
style:--threshold="12rem"
|
||||||
>
|
>
|
||||||
{propsStore.modelName}
|
{serverStore.modelName}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<CopyToClipboardIcon
|
<CopyToClipboardIcon
|
||||||
text={propsStore.modelName || ''}
|
text={serverStore.modelName || ''}
|
||||||
canCopy={!!propsStore.modelName}
|
canCopy={!!serverStore.modelName}
|
||||||
ariaLabel="Copy model name to clipboard"
|
ariaLabel="Copy model name to clipboard"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Package } from '@lucide/svelte';
|
import { Package } from '@lucide/svelte';
|
||||||
import { BadgeInfo, CopyToClipboardIcon } from '$lib/components/app';
|
import { BadgeInfo, CopyToClipboardIcon } from '$lib/components/app';
|
||||||
import { propsStore } from '$lib/stores/props.svelte';
|
import { serverStore } from '$lib/stores/server.svelte';
|
||||||
import * as Tooltip from '$lib/components/ui/tooltip';
|
import * as Tooltip from '$lib/components/ui/tooltip';
|
||||||
import { TOOLTIP_DELAY_DURATION } from '$lib/constants/tooltip-config';
|
import { TOOLTIP_DELAY_DURATION } from '$lib/constants/tooltip-config';
|
||||||
|
|
||||||
|
|
@ -21,8 +21,8 @@
|
||||||
showTooltip = false
|
showTooltip = false
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
let model = $derived(modelProp || propsStore.modelName);
|
let model = $derived(modelProp || serverStore.modelName);
|
||||||
let isModelMode = $derived(propsStore.isModelMode);
|
let isModelMode = $derived(serverStore.isModelMode);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#snippet badgeContent()}
|
{#snippet badgeContent()}
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@
|
||||||
loadModel
|
loadModel
|
||||||
} from '$lib/stores/models.svelte';
|
} from '$lib/stores/models.svelte';
|
||||||
import { ServerModelStatus } from '$lib/enums';
|
import { ServerModelStatus } from '$lib/enums';
|
||||||
import { isRouterMode, propsStore } from '$lib/stores/props.svelte';
|
import { isRouterMode, serverStore } from '$lib/stores/server.svelte';
|
||||||
import { DialogModelInformation } from '$lib/components/app';
|
import { DialogModelInformation } from '$lib/components/app';
|
||||||
import type { ModelOption } from '$lib/types/models';
|
import type { ModelOption } from '$lib/types/models';
|
||||||
|
|
||||||
|
|
@ -43,7 +43,7 @@
|
||||||
let updating = $derived(modelsUpdating());
|
let updating = $derived(modelsUpdating());
|
||||||
let activeId = $derived(selectedModelId());
|
let activeId = $derived(selectedModelId());
|
||||||
let isRouter = $derived(isRouterMode());
|
let isRouter = $derived(isRouterMode());
|
||||||
let serverModel = $derived(propsStore.modelName);
|
let serverModel = $derived(serverStore.modelName);
|
||||||
|
|
||||||
// Reactive router models state - needed for proper reactivity of status checks
|
// Reactive router models state - needed for proper reactivity of status checks
|
||||||
let currentRouterModels = $derived(routerModels());
|
let currentRouterModels = $derived(routerModels());
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
import { Button } from '$lib/components/ui/button';
|
import { Button } from '$lib/components/ui/button';
|
||||||
import { Input } from '$lib/components/ui/input';
|
import { Input } from '$lib/components/ui/input';
|
||||||
import Label from '$lib/components/ui/label/label.svelte';
|
import Label from '$lib/components/ui/label/label.svelte';
|
||||||
import { propsStore, propsLoading } from '$lib/stores/props.svelte';
|
import { serverStore, serverLoading } from '$lib/stores/server.svelte';
|
||||||
import { config, updateConfig } from '$lib/stores/settings.svelte';
|
import { config, updateConfig } from '$lib/stores/settings.svelte';
|
||||||
import { fade, fly, scale } from 'svelte/transition';
|
import { fade, fly, scale } from 'svelte/transition';
|
||||||
|
|
||||||
|
|
@ -24,7 +24,7 @@
|
||||||
showTroubleshooting = false
|
showTroubleshooting = false
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
let isServerLoading = $derived(propsLoading());
|
let isServerLoading = $derived(serverLoading());
|
||||||
let isAccessDeniedError = $derived(
|
let isAccessDeniedError = $derived(
|
||||||
error.toLowerCase().includes('access denied') ||
|
error.toLowerCase().includes('access denied') ||
|
||||||
error.toLowerCase().includes('invalid api key') ||
|
error.toLowerCase().includes('invalid api key') ||
|
||||||
|
|
@ -42,7 +42,7 @@
|
||||||
if (onRetry) {
|
if (onRetry) {
|
||||||
onRetry();
|
onRetry();
|
||||||
} else {
|
} else {
|
||||||
propsStore.fetch();
|
serverStore.fetch();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
import { AlertTriangle, Server } from '@lucide/svelte';
|
import { AlertTriangle, Server } from '@lucide/svelte';
|
||||||
import { Badge } from '$lib/components/ui/badge';
|
import { Badge } from '$lib/components/ui/badge';
|
||||||
import { Button } from '$lib/components/ui/button';
|
import { Button } from '$lib/components/ui/button';
|
||||||
import { serverProps, propsLoading, propsError, modelName } from '$lib/stores/props.svelte';
|
import { serverProps, serverLoading, serverError, modelName } from '$lib/stores/server.svelte';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
class?: string;
|
class?: string;
|
||||||
|
|
@ -11,8 +11,8 @@
|
||||||
|
|
||||||
let { class: className = '', showActions = false }: Props = $props();
|
let { class: className = '', showActions = false }: Props = $props();
|
||||||
|
|
||||||
let error = $derived(propsError());
|
let error = $derived(serverError());
|
||||||
let loading = $derived(propsLoading());
|
let loading = $derived(serverLoading());
|
||||||
let model = $derived(modelName());
|
let model = $derived(modelName());
|
||||||
let serverData = $derived(serverProps());
|
let serverData = $derived(serverProps());
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { config } from '$lib/stores/settings.svelte';
|
import { config } from '$lib/stores/settings.svelte';
|
||||||
import { getJsonHeaders } from '$lib/utils/api-headers';
|
import { getJsonHeaders } from '$lib/utils/api-headers';
|
||||||
import { selectedModelName } from '$lib/stores/models.svelte';
|
import { selectedModelName } from '$lib/stores/models.svelte';
|
||||||
import { isRouterMode, propsStore } from '$lib/stores/props.svelte';
|
import { isRouterMode, serverStore } from '$lib/stores/server.svelte';
|
||||||
import type {
|
import type {
|
||||||
ApiChatCompletionRequest,
|
ApiChatCompletionRequest,
|
||||||
ApiChatCompletionResponse,
|
ApiChatCompletionResponse,
|
||||||
|
|
@ -792,7 +792,7 @@ export class ChatService {
|
||||||
* Handles various response formats including streaming chunks and final responses.
|
* Handles various response formats including streaming chunks and final responses.
|
||||||
*
|
*
|
||||||
* WORKAROUND: In single model mode, llama-server returns a default/incorrect model name
|
* WORKAROUND: In single model mode, llama-server returns a default/incorrect model name
|
||||||
* in the response. We override it with the actual model name from propsStore.
|
* in the response. We override it with the actual model name from serverStore.
|
||||||
*
|
*
|
||||||
* @param data - Raw response data from the Chat Completions API
|
* @param data - Raw response data from the Chat Completions API
|
||||||
* @returns Model name string if found, undefined otherwise
|
* @returns Model name string if found, undefined otherwise
|
||||||
|
|
@ -803,7 +803,7 @@ export class ChatService {
|
||||||
// because llama-server returns `gpt-3.5-turbo` value in the `model` field
|
// because llama-server returns `gpt-3.5-turbo` value in the `model` field
|
||||||
const isRouter = isRouterMode();
|
const isRouter = isRouterMode();
|
||||||
if (!isRouter) {
|
if (!isRouter) {
|
||||||
const propsModelName = propsStore.modelName;
|
const propsModelName = serverStore.modelName;
|
||||||
if (propsModelName) {
|
if (propsModelName) {
|
||||||
return propsModelName;
|
return propsModelName;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import { DatabaseService } from '$lib/services/database';
|
||||||
import { chatService } from '$lib/services';
|
import { chatService } from '$lib/services';
|
||||||
import { conversationsStore } from '$lib/stores/conversations.svelte';
|
import { conversationsStore } from '$lib/stores/conversations.svelte';
|
||||||
import { config } from '$lib/stores/settings.svelte';
|
import { config } from '$lib/stores/settings.svelte';
|
||||||
import { contextSize } from '$lib/stores/props.svelte';
|
import { contextSize } from '$lib/stores/server.svelte';
|
||||||
import { normalizeModelName } from '$lib/utils/model-names';
|
import { normalizeModelName } from '$lib/utils/model-names';
|
||||||
import { filterByLeafNodeId, findDescendantMessages, findLeafNode } from '$lib/utils/branching';
|
import { filterByLeafNodeId, findDescendantMessages, findLeafNode } from '$lib/utils/branching';
|
||||||
import { SvelteMap, SvelteSet } from 'svelte/reactivity';
|
import { SvelteMap, SvelteSet } from 'svelte/reactivity';
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { SvelteSet } from 'svelte/reactivity';
|
import { SvelteSet } from 'svelte/reactivity';
|
||||||
import { ModelsService } from '$lib/services/models';
|
import { ModelsService } from '$lib/services/models';
|
||||||
import { ServerModelStatus } from '$lib/enums';
|
import { ServerModelStatus } from '$lib/enums';
|
||||||
import { propsStore } from '$lib/stores/props.svelte';
|
import { serverStore } from '$lib/stores/server.svelte';
|
||||||
import type { ModelOption, ModelModalities } from '$lib/types/models';
|
import type { ModelOption, ModelModalities } from '$lib/types/models';
|
||||||
import type { ApiModelDataEntry } from '$lib/types/api';
|
import type { ApiModelDataEntry } from '$lib/types/api';
|
||||||
|
|
||||||
|
|
@ -18,7 +18,7 @@ import type { ApiModelDataEntry } from '$lib/types/api';
|
||||||
* **Architecture & Relationships:**
|
* **Architecture & Relationships:**
|
||||||
* - **ModelsService**: Stateless service for API communication
|
* - **ModelsService**: Stateless service for API communication
|
||||||
* - **ModelsStore** (this class): Reactive store for model state
|
* - **ModelsStore** (this class): Reactive store for model state
|
||||||
* - **PropsStore**: Provides server mode detection
|
* - **ServerStore**: Provides server mode detection
|
||||||
* - **ConversationsStore**: Tracks which conversations use which models
|
* - **ConversationsStore**: Tracks which conversations use which models
|
||||||
*
|
*
|
||||||
* **Key Features:**
|
* **Key Features:**
|
||||||
|
|
@ -32,121 +32,62 @@ class ModelsStore {
|
||||||
// State
|
// State
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
private _models = $state<ModelOption[]>([]);
|
models = $state<ModelOption[]>([]);
|
||||||
private _routerModels = $state<ApiModelDataEntry[]>([]);
|
routerModels = $state<ApiModelDataEntry[]>([]);
|
||||||
private _loading = $state(false);
|
loading = $state(false);
|
||||||
private _updating = $state(false);
|
updating = $state(false);
|
||||||
private _error = $state<string | null>(null);
|
error = $state<string | null>(null);
|
||||||
private _selectedModelId = $state<string | null>(null);
|
selectedModelId = $state<string | null>(null);
|
||||||
private _selectedModelName = $state<string | null>(null);
|
selectedModelName = $state<string | null>(null);
|
||||||
|
|
||||||
/** Maps modelId -> Set of conversationIds that use this model */
|
private modelUsage = $state<Map<string, SvelteSet<string>>>(new Map());
|
||||||
private _modelUsage = $state<Map<string, SvelteSet<string>>>(new Map());
|
private modelLoadingStates = $state<Map<string, boolean>>(new Map());
|
||||||
|
|
||||||
/** Maps modelId -> loading state for load/unload operations */
|
|
||||||
private _modelLoadingStates = $state<Map<string, boolean>>(new Map());
|
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
// Getters - Basic
|
// Computed Getters
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
get models(): ModelOption[] {
|
|
||||||
return this._models;
|
|
||||||
}
|
|
||||||
|
|
||||||
get routerModels(): ApiModelDataEntry[] {
|
|
||||||
return this._routerModels;
|
|
||||||
}
|
|
||||||
|
|
||||||
get loading(): boolean {
|
|
||||||
return this._loading;
|
|
||||||
}
|
|
||||||
|
|
||||||
get updating(): boolean {
|
|
||||||
return this._updating;
|
|
||||||
}
|
|
||||||
|
|
||||||
get error(): string | null {
|
|
||||||
return this._error;
|
|
||||||
}
|
|
||||||
|
|
||||||
get selectedModelId(): string | null {
|
|
||||||
return this._selectedModelId;
|
|
||||||
}
|
|
||||||
|
|
||||||
get selectedModelName(): string | null {
|
|
||||||
return this._selectedModelName;
|
|
||||||
}
|
|
||||||
|
|
||||||
get selectedModel(): ModelOption | null {
|
get selectedModel(): ModelOption | null {
|
||||||
if (!this._selectedModelId) {
|
if (!this.selectedModelId) return null;
|
||||||
return null;
|
return this.models.find((model) => model.id === this.selectedModelId) ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return this._models.find((model) => model.id === this._selectedModelId) ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
|
||||||
// Getters - Loaded Models (ROUTER mode)
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get list of currently loaded model IDs
|
|
||||||
*/
|
|
||||||
get loadedModelIds(): string[] {
|
get loadedModelIds(): string[] {
|
||||||
return this._routerModels
|
return this.routerModels
|
||||||
.filter((m) => m.status.value === ServerModelStatus.LOADED)
|
.filter((m) => m.status.value === ServerModelStatus.LOADED)
|
||||||
.map((m) => m.name);
|
.map((m) => m.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get list of models currently being loaded/unloaded
|
|
||||||
*/
|
|
||||||
get loadingModelIds(): string[] {
|
get loadingModelIds(): string[] {
|
||||||
return Array.from(this._modelLoadingStates.entries())
|
return Array.from(this.modelLoadingStates.entries())
|
||||||
.filter(([, loading]) => loading)
|
.filter(([, loading]) => loading)
|
||||||
.map(([id]) => id);
|
.map(([id]) => id);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
* Check if a specific model is loaded
|
// Methods - Model Status
|
||||||
*/
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
isModelLoaded(modelId: string): boolean {
|
isModelLoaded(modelId: string): boolean {
|
||||||
const model = this._routerModels.find((m) => m.name === modelId);
|
const model = this.routerModels.find((m) => m.name === modelId);
|
||||||
return model?.status.value === ServerModelStatus.LOADED || false;
|
return model?.status.value === ServerModelStatus.LOADED || false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if a specific model is currently loading/unloading
|
|
||||||
*/
|
|
||||||
isModelOperationInProgress(modelId: string): boolean {
|
isModelOperationInProgress(modelId: string): boolean {
|
||||||
return this._modelLoadingStates.get(modelId) ?? false;
|
return this.modelLoadingStates.get(modelId) ?? false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the status of a specific model
|
|
||||||
*/
|
|
||||||
getModelStatus(modelId: string): ServerModelStatus | null {
|
getModelStatus(modelId: string): ServerModelStatus | null {
|
||||||
const model = this._routerModels.find((m) => m.name === modelId);
|
const model = this.routerModels.find((m) => m.name === modelId);
|
||||||
return model?.status.value ?? null;
|
return model?.status.value ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
|
||||||
// Getters - Model Usage
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get set of conversation IDs using a specific model
|
|
||||||
*/
|
|
||||||
getModelUsage(modelId: string): SvelteSet<string> {
|
getModelUsage(modelId: string): SvelteSet<string> {
|
||||||
return this._modelUsage.get(modelId) ?? new SvelteSet<string>();
|
return this.modelUsage.get(modelId) ?? new SvelteSet<string>();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if a model is used by any conversation
|
|
||||||
*/
|
|
||||||
isModelInUse(modelId: string): boolean {
|
isModelInUse(modelId: string): boolean {
|
||||||
const usage = this._modelUsage.get(modelId);
|
const usage = this.modelUsage.get(modelId);
|
||||||
return usage !== undefined && usage.size > 0;
|
return usage !== undefined && usage.size > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -158,11 +99,11 @@ class ModelsStore {
|
||||||
* Fetch list of models from server
|
* Fetch list of models from server
|
||||||
*/
|
*/
|
||||||
async fetch(force = false): Promise<void> {
|
async fetch(force = false): Promise<void> {
|
||||||
if (this._loading) return;
|
if (this.loading) return;
|
||||||
if (this._models.length > 0 && !force) return;
|
if (this.models.length > 0 && !force) return;
|
||||||
|
|
||||||
this._loading = true;
|
this.loading = true;
|
||||||
this._error = null;
|
this.error = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await ModelsService.list();
|
const response = await ModelsService.list();
|
||||||
|
|
@ -185,18 +126,13 @@ class ModelsStore {
|
||||||
} satisfies ModelOption;
|
} satisfies ModelOption;
|
||||||
});
|
});
|
||||||
|
|
||||||
this._models = models;
|
this.models = models;
|
||||||
|
|
||||||
// Don't auto-select any model - selection should come from:
|
|
||||||
// 1. User explicitly selecting a model in the UI
|
|
||||||
// 2. Conversation model (synced via ChatFormActions effect)
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this._models = [];
|
this.models = [];
|
||||||
this._error = error instanceof Error ? error.message : 'Failed to load models';
|
this.error = error instanceof Error ? error.message : 'Failed to load models';
|
||||||
|
|
||||||
throw error;
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
this._loading = false;
|
this.loading = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -207,13 +143,11 @@ class ModelsStore {
|
||||||
async fetchRouterModels(): Promise<void> {
|
async fetchRouterModels(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const response = await ModelsService.listRouter();
|
const response = await ModelsService.listRouter();
|
||||||
this._routerModels = response.data;
|
this.routerModels = response.data;
|
||||||
|
|
||||||
// Fetch modalities for loaded models
|
|
||||||
await this.fetchModalitiesForLoadedModels();
|
await this.fetchModalitiesForLoadedModels();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Failed to fetch router models:', error);
|
console.warn('Failed to fetch router models:', error);
|
||||||
this._routerModels = [];
|
this.routerModels = [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -226,13 +160,13 @@ class ModelsStore {
|
||||||
if (loadedModelIds.length === 0) return;
|
if (loadedModelIds.length === 0) return;
|
||||||
|
|
||||||
// Fetch props for each loaded model in parallel
|
// Fetch props for each loaded model in parallel
|
||||||
const propsPromises = loadedModelIds.map((modelId) => propsStore.fetchModelProps(modelId));
|
const propsPromises = loadedModelIds.map((modelId) => serverStore.fetchModelProps(modelId));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const results = await Promise.all(propsPromises);
|
const results = await Promise.all(propsPromises);
|
||||||
|
|
||||||
// Update models with modalities
|
// Update models with modalities
|
||||||
this._models = this._models.map((model) => {
|
this.models = this.models.map((model) => {
|
||||||
const modelIndex = loadedModelIds.indexOf(model.model);
|
const modelIndex = loadedModelIds.indexOf(model.model);
|
||||||
if (modelIndex === -1) return model;
|
if (modelIndex === -1) return model;
|
||||||
|
|
||||||
|
|
@ -257,7 +191,7 @@ class ModelsStore {
|
||||||
*/
|
*/
|
||||||
async updateModelModalities(modelId: string): Promise<void> {
|
async updateModelModalities(modelId: string): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const props = await propsStore.fetchModelProps(modelId);
|
const props = await serverStore.fetchModelProps(modelId);
|
||||||
if (!props?.modalities) return;
|
if (!props?.modalities) return;
|
||||||
|
|
||||||
const modalities: ModelModalities = {
|
const modalities: ModelModalities = {
|
||||||
|
|
@ -265,7 +199,7 @@ class ModelsStore {
|
||||||
audio: props.modalities.audio ?? false
|
audio: props.modalities.audio ?? false
|
||||||
};
|
};
|
||||||
|
|
||||||
this._models = this._models.map((model) =>
|
this.models = this.models.map((model) =>
|
||||||
model.model === modelId ? { ...model, modalities } : model
|
model.model === modelId ? { ...model, modalities } : model
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -281,27 +215,20 @@ class ModelsStore {
|
||||||
* Select a model for new conversations
|
* Select a model for new conversations
|
||||||
*/
|
*/
|
||||||
async select(modelId: string): Promise<void> {
|
async select(modelId: string): Promise<void> {
|
||||||
if (!modelId || this._updating) {
|
if (!modelId || this.updating) return;
|
||||||
return;
|
if (this.selectedModelId === modelId) return;
|
||||||
}
|
|
||||||
|
|
||||||
if (this._selectedModelId === modelId) {
|
const option = this.models.find((model) => model.id === modelId);
|
||||||
return;
|
if (!option) throw new Error('Selected model is not available');
|
||||||
}
|
|
||||||
|
|
||||||
const option = this._models.find((model) => model.id === modelId);
|
this.updating = true;
|
||||||
if (!option) {
|
this.error = null;
|
||||||
throw new Error('Selected model is not available');
|
|
||||||
}
|
|
||||||
|
|
||||||
this._updating = true;
|
|
||||||
this._error = null;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this._selectedModelId = option.id;
|
this.selectedModelId = option.id;
|
||||||
this._selectedModelName = option.model;
|
this.selectedModelName = option.model;
|
||||||
} finally {
|
} finally {
|
||||||
this._updating = false;
|
this.updating = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -310,47 +237,28 @@ class ModelsStore {
|
||||||
* @param modelName - Model name to select (e.g., "unsloth/gemma-3-12b-it-GGUF:latest")
|
* @param modelName - Model name to select (e.g., "unsloth/gemma-3-12b-it-GGUF:latest")
|
||||||
*/
|
*/
|
||||||
selectModelByName(modelName: string): void {
|
selectModelByName(modelName: string): void {
|
||||||
const option = this._models.find((model) => model.model === modelName);
|
const option = this.models.find((model) => model.model === modelName);
|
||||||
if (option) {
|
if (option) {
|
||||||
this._selectedModelId = option.id;
|
this.selectedModelId = option.id;
|
||||||
this._selectedModelName = option.model;
|
this.selectedModelName = option.model;
|
||||||
// Don't persist - this is just for syncing with conversation
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Clear the current model selection
|
|
||||||
*/
|
|
||||||
clearSelection(): void {
|
clearSelection(): void {
|
||||||
this._selectedModelId = null;
|
this.selectedModelId = null;
|
||||||
this._selectedModelName = null;
|
this.selectedModelName = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Find a model by its model name
|
|
||||||
* @param modelName - Model name to search for (e.g., "unsloth/gemma-3-12b-it-GGUF:latest")
|
|
||||||
* @returns ModelOption if found, null otherwise
|
|
||||||
*/
|
|
||||||
findModelByName(modelName: string): ModelOption | null {
|
findModelByName(modelName: string): ModelOption | null {
|
||||||
return this._models.find((model) => model.model === modelName) ?? null;
|
return this.models.find((model) => model.model === modelName) ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Find a model by its display ID
|
|
||||||
* @param modelId - Model ID to search for
|
|
||||||
* @returns ModelOption if found, null otherwise
|
|
||||||
*/
|
|
||||||
findModelById(modelId: string): ModelOption | null {
|
findModelById(modelId: string): ModelOption | null {
|
||||||
return this._models.find((model) => model.id === modelId) ?? null;
|
return this.models.find((model) => model.id === modelId) ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if a model exists by name
|
|
||||||
* @param modelName - Model name to check
|
|
||||||
* @returns true if model exists
|
|
||||||
*/
|
|
||||||
hasModel(modelName: string): boolean {
|
hasModel(modelName: string): boolean {
|
||||||
return this._models.some((model) => model.model === modelName);
|
return this.models.some((model) => model.model === modelName);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
@ -414,12 +322,10 @@ class ModelsStore {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this._modelLoadingStates.get(modelId)) {
|
if (this.modelLoadingStates.get(modelId)) return;
|
||||||
return; // Already loading
|
|
||||||
}
|
|
||||||
|
|
||||||
this._modelLoadingStates.set(modelId, true);
|
this.modelLoadingStates.set(modelId, true);
|
||||||
this._error = null;
|
this.error = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await ModelsService.load(modelId);
|
await ModelsService.load(modelId);
|
||||||
|
|
@ -427,13 +333,12 @@ class ModelsStore {
|
||||||
// Poll until model is loaded
|
// Poll until model is loaded
|
||||||
await this.pollForModelStatus(modelId, ServerModelStatus.LOADED);
|
await this.pollForModelStatus(modelId, ServerModelStatus.LOADED);
|
||||||
|
|
||||||
// Update modalities for this specific model
|
|
||||||
await this.updateModelModalities(modelId);
|
await this.updateModelModalities(modelId);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this._error = error instanceof Error ? error.message : 'Failed to load model';
|
this.error = error instanceof Error ? error.message : 'Failed to load model';
|
||||||
throw error;
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
this._modelLoadingStates.set(modelId, false);
|
this.modelLoadingStates.set(modelId, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -446,23 +351,20 @@ class ModelsStore {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this._modelLoadingStates.get(modelId)) {
|
if (this.modelLoadingStates.get(modelId)) return;
|
||||||
return; // Already unloading
|
|
||||||
}
|
|
||||||
|
|
||||||
this._modelLoadingStates.set(modelId, true);
|
this.modelLoadingStates.set(modelId, true);
|
||||||
this._error = null;
|
this.error = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await ModelsService.unload(modelId);
|
await ModelsService.unload(modelId);
|
||||||
|
|
||||||
// Poll until model is unloaded
|
|
||||||
await this.pollForModelStatus(modelId, ServerModelStatus.UNLOADED);
|
await this.pollForModelStatus(modelId, ServerModelStatus.UNLOADED);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this._error = error instanceof Error ? error.message : 'Failed to unload model';
|
this.error = error instanceof Error ? error.message : 'Failed to unload model';
|
||||||
throw error;
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
this._modelLoadingStates.set(modelId, false);
|
this.modelLoadingStates.set(modelId, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -486,9 +388,9 @@ class ModelsStore {
|
||||||
* Register that a conversation is using a model
|
* Register that a conversation is using a model
|
||||||
*/
|
*/
|
||||||
registerModelUsage(modelId: string, conversationId: string): void {
|
registerModelUsage(modelId: string, conversationId: string): void {
|
||||||
const usage = this._modelUsage.get(modelId) ?? new SvelteSet<string>();
|
const usage = this.modelUsage.get(modelId) ?? new SvelteSet<string>();
|
||||||
usage.add(conversationId);
|
usage.add(conversationId);
|
||||||
this._modelUsage.set(modelId, usage);
|
this.modelUsage.set(modelId, usage);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -502,14 +404,11 @@ class ModelsStore {
|
||||||
conversationId: string,
|
conversationId: string,
|
||||||
autoUnload = true
|
autoUnload = true
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const usage = this._modelUsage.get(modelId);
|
const usage = this.modelUsage.get(modelId);
|
||||||
if (usage) {
|
if (usage) {
|
||||||
usage.delete(conversationId);
|
usage.delete(conversationId);
|
||||||
|
|
||||||
if (usage.size === 0) {
|
if (usage.size === 0) {
|
||||||
this._modelUsage.delete(modelId);
|
this.modelUsage.delete(modelId);
|
||||||
|
|
||||||
// Auto-unload if model is not used by any conversation
|
|
||||||
if (autoUnload && this.isModelLoaded(modelId)) {
|
if (autoUnload && this.isModelLoaded(modelId)) {
|
||||||
await this.unloadModel(modelId);
|
await this.unloadModel(modelId);
|
||||||
}
|
}
|
||||||
|
|
@ -521,10 +420,8 @@ class ModelsStore {
|
||||||
* Clear all usage for a conversation (when conversation is deleted)
|
* Clear all usage for a conversation (when conversation is deleted)
|
||||||
*/
|
*/
|
||||||
async clearConversationUsage(conversationId: string): Promise<void> {
|
async clearConversationUsage(conversationId: string): Promise<void> {
|
||||||
for (const [modelId, usage] of this._modelUsage.entries()) {
|
for (const [modelId, usage] of this.modelUsage.entries()) {
|
||||||
if (usage.has(conversationId)) {
|
if (usage.has(conversationId)) await this.unregisterModelUsage(modelId, conversationId);
|
||||||
await this.unregisterModelUsage(modelId, conversationId);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -544,15 +441,15 @@ class ModelsStore {
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
clear(): void {
|
clear(): void {
|
||||||
this._models = [];
|
this.models = [];
|
||||||
this._routerModels = [];
|
this.routerModels = [];
|
||||||
this._loading = false;
|
this.loading = false;
|
||||||
this._updating = false;
|
this.updating = false;
|
||||||
this._error = null;
|
this.error = null;
|
||||||
this._selectedModelId = null;
|
this.selectedModelId = null;
|
||||||
this._selectedModelName = null;
|
this.selectedModelName = null;
|
||||||
this._modelUsage.clear();
|
this.modelUsage.clear();
|
||||||
this._modelLoadingStates.clear();
|
this.modelLoadingStates.clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,143 +2,93 @@ import { PropsService } from '$lib/services/props';
|
||||||
import { ServerRole, ModelModality } from '$lib/enums';
|
import { ServerRole, ModelModality } from '$lib/enums';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* PropsStore - Server properties management and mode detection
|
* ServerStore - Server state, capabilities, and mode detection
|
||||||
*
|
*
|
||||||
* This store manages the server properties fetched from the `/props` endpoint.
|
* This store manages the server connection state and properties fetched from `/props`.
|
||||||
* It provides reactive state for server configuration, capabilities, and mode detection.
|
* It provides reactive state for server configuration, capabilities, and role detection.
|
||||||
*
|
*
|
||||||
* **Architecture & Relationships:**
|
* **Architecture & Relationships:**
|
||||||
* - **PropsService**: Stateless service for fetching `/props` data
|
* - **PropsService**: Stateless service for fetching `/props` data
|
||||||
* - **PropsStore** (this class): Reactive store for server properties
|
* - **ServerStore** (this class): Reactive store for server state
|
||||||
* - **ModelsStore**: Uses server mode for model management strategy
|
* - **ModelsStore**: Uses server role for model management strategy
|
||||||
*
|
*
|
||||||
* **Key Features:**
|
* **Key Features:**
|
||||||
* - **Server Properties**: Model info, context size, build information
|
* - **Server State**: Connection status, loading, error handling
|
||||||
* - **Mode Detection**: MODEL (single model) vs ROUTER (multi-model)
|
* - **Role Detection**: MODEL (single model) vs ROUTER (multi-model)
|
||||||
* - **Capability Detection**: Vision and audio modality support
|
* - **Capability Detection**: Vision and audio modality support
|
||||||
* - **Error Handling**: Clear error states when server unavailable
|
* - **Props Cache**: Per-model props caching for ROUTER mode
|
||||||
*/
|
*/
|
||||||
class PropsStore {
|
class ServerStore {
|
||||||
private _serverProps = $state<ApiLlamaCppServerProps | null>(null);
|
props = $state<ApiLlamaCppServerProps | null>(null);
|
||||||
private _loading = $state(false);
|
loading = $state(false);
|
||||||
private _error = $state<string | null>(null);
|
error = $state<string | null>(null);
|
||||||
private _serverRole = $state<ServerRole | null>(null);
|
role = $state<ServerRole | null>(null);
|
||||||
private fetchPromise: Promise<void> | null = null;
|
private fetchPromise: Promise<void> | null = null;
|
||||||
|
|
||||||
// Model-specific props cache (ROUTER mode)
|
// Model-specific props cache (ROUTER mode)
|
||||||
private _modelPropsCache = $state<Map<string, ApiLlamaCppServerProps>>(new Map());
|
private modelPropsCache = $state<Map<string, ApiLlamaCppServerProps>>(new Map());
|
||||||
private _modelPropsFetching = $state<Set<string>>(new Set());
|
private modelPropsFetching = $state<Set<string>>(new Set());
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
// Getters - Server Properties
|
// Computed Getters
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
get serverProps(): ApiLlamaCppServerProps | null {
|
|
||||||
return this._serverProps;
|
|
||||||
}
|
|
||||||
|
|
||||||
get loading(): boolean {
|
|
||||||
return this._loading;
|
|
||||||
}
|
|
||||||
|
|
||||||
get error(): string | null {
|
|
||||||
return this._error;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get model name from server props.
|
* Get model name from server props.
|
||||||
* In MODEL mode: extracts from model_path or model_alias
|
* In MODEL mode: extracts from model_path or model_alias
|
||||||
* In ROUTER mode: returns null (model is per-conversation)
|
* In ROUTER mode: returns null (model is per-conversation)
|
||||||
*/
|
*/
|
||||||
get modelName(): string | null {
|
get modelName(): string | null {
|
||||||
if (this._serverRole === ServerRole.ROUTER) {
|
if (this.role === ServerRole.ROUTER) return null;
|
||||||
return null;
|
if (this.props?.model_alias) return this.props.model_alias;
|
||||||
}
|
if (!this.props?.model_path) return null;
|
||||||
|
return this.props.model_path.split(/(\\|\/)/).pop() || null;
|
||||||
if (this._serverProps?.model_alias) {
|
|
||||||
return this._serverProps.model_alias;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this._serverProps?.model_path) return null;
|
|
||||||
return this._serverProps.model_path.split(/(\\|\/)/).pop() || null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get supportedModalities(): ModelModality[] {
|
get supportedModalities(): ModelModality[] {
|
||||||
const modalities: ModelModality[] = [];
|
const modalities: ModelModality[] = [];
|
||||||
if (this._serverProps?.modalities?.audio) {
|
if (this.props?.modalities?.audio) modalities.push(ModelModality.AUDIO);
|
||||||
modalities.push(ModelModality.AUDIO);
|
if (this.props?.modalities?.vision) modalities.push(ModelModality.VISION);
|
||||||
}
|
|
||||||
if (this._serverProps?.modalities?.vision) {
|
|
||||||
modalities.push(ModelModality.VISION);
|
|
||||||
}
|
|
||||||
return modalities;
|
return modalities;
|
||||||
}
|
}
|
||||||
|
|
||||||
get supportsVision(): boolean {
|
get supportsVision(): boolean {
|
||||||
return this._serverProps?.modalities?.vision ?? false;
|
return this.props?.modalities?.vision ?? false;
|
||||||
}
|
}
|
||||||
|
|
||||||
get supportsAudio(): boolean {
|
get supportsAudio(): boolean {
|
||||||
return this._serverProps?.modalities?.audio ?? false;
|
return this.props?.modalities?.audio ?? false;
|
||||||
}
|
}
|
||||||
|
|
||||||
get defaultParams(): ApiLlamaCppServerProps['default_generation_settings']['params'] | null {
|
get defaultParams(): ApiLlamaCppServerProps['default_generation_settings']['params'] | null {
|
||||||
return this._serverProps?.default_generation_settings?.params || null;
|
return this.props?.default_generation_settings?.params || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get context size (n_ctx) from server props
|
|
||||||
*/
|
|
||||||
get contextSize(): number | null {
|
get contextSize(): number | null {
|
||||||
return this._serverProps?.default_generation_settings?.n_ctx ?? null;
|
return this.props?.default_generation_settings?.n_ctx ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if slots endpoint is available (set by --slots flag on server)
|
|
||||||
*/
|
|
||||||
get slotsEndpointAvailable(): boolean {
|
get slotsEndpointAvailable(): boolean {
|
||||||
return this._serverProps?.endpoint_slots ?? false;
|
return this.props?.endpoint_slots ?? false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
|
||||||
// Getters - Server Mode
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get current server mode
|
|
||||||
*/
|
|
||||||
get serverRole(): ServerRole | null {
|
|
||||||
return this._serverRole;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Detect if server is running in router mode (multi-model management)
|
|
||||||
*/
|
|
||||||
get isRouterMode(): boolean {
|
get isRouterMode(): boolean {
|
||||||
return this._serverRole === ServerRole.ROUTER;
|
return this.role === ServerRole.ROUTER;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Detect if server is running in model mode (single model loaded)
|
|
||||||
*/
|
|
||||||
get isModelMode(): boolean {
|
get isModelMode(): boolean {
|
||||||
return this._serverRole === ServerRole.MODEL;
|
return this.role === ServerRole.MODEL;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
// Server Mode Detection
|
// Server Role Detection
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
private detectServerRole(props: ApiLlamaCppServerProps): void {
|
private detectRole(props: ApiLlamaCppServerProps): void {
|
||||||
console.log('Server props role:', props?.role);
|
const newRole = props?.role === ServerRole.ROUTER ? ServerRole.ROUTER : ServerRole.MODEL;
|
||||||
const newMode =
|
if (this.role !== newRole) {
|
||||||
// todo - `role` attribute should always be available on the `/props` endpoint
|
this.role = newRole;
|
||||||
props?.role === ServerRole.ROUTER ? ServerRole.ROUTER : ServerRole.MODEL;
|
console.info(`Server running in ${newRole === ServerRole.ROUTER ? 'ROUTER' : 'MODEL'} mode`);
|
||||||
|
|
||||||
// Only log when mode changes
|
|
||||||
if (this._serverRole !== newMode) {
|
|
||||||
this._serverRole = newMode;
|
|
||||||
console.info(`Server running in ${newMode === ServerRole.ROUTER ? 'ROUTER' : 'MODEL'} mode`);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -146,18 +96,13 @@ class PropsStore {
|
||||||
// Fetch Server Properties
|
// Fetch Server Properties
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetches server properties from the server
|
|
||||||
*/
|
|
||||||
async fetch(): Promise<void> {
|
async fetch(): Promise<void> {
|
||||||
if (this.fetchPromise) {
|
if (this.fetchPromise) return this.fetchPromise;
|
||||||
return this.fetchPromise;
|
|
||||||
}
|
|
||||||
|
|
||||||
this._loading = true;
|
this.loading = true;
|
||||||
this._error = null;
|
this.error = null;
|
||||||
|
|
||||||
const previousBuildInfo = this._serverProps?.build_info;
|
const previousBuildInfo = this.props?.build_info;
|
||||||
|
|
||||||
const fetchPromise = (async () => {
|
const fetchPromise = (async () => {
|
||||||
try {
|
try {
|
||||||
|
|
@ -165,18 +110,18 @@ class PropsStore {
|
||||||
|
|
||||||
// Clear model-specific props cache if server was restarted
|
// Clear model-specific props cache if server was restarted
|
||||||
if (previousBuildInfo && previousBuildInfo !== props.build_info) {
|
if (previousBuildInfo && previousBuildInfo !== props.build_info) {
|
||||||
this._modelPropsCache.clear();
|
this.modelPropsCache.clear();
|
||||||
console.info('Cleared model props cache due to server restart');
|
console.info('Cleared model props cache due to server restart');
|
||||||
}
|
}
|
||||||
|
|
||||||
this._serverProps = props;
|
this.props = props;
|
||||||
this._error = null;
|
this.error = null;
|
||||||
this.detectServerRole(props);
|
this.detectRole(props);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this._error = this.getErrorMessage(error);
|
this.error = this.getErrorMessage(error);
|
||||||
console.error('Error fetching server properties:', error);
|
console.error('Error fetching server properties:', error);
|
||||||
} finally {
|
} finally {
|
||||||
this._loading = false;
|
this.loading = false;
|
||||||
this.fetchPromise = null;
|
this.fetchPromise = null;
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
@ -189,45 +134,31 @@ class PropsStore {
|
||||||
// Fetch Model-Specific Properties (ROUTER mode)
|
// Fetch Model-Specific Properties (ROUTER mode)
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/**
|
|
||||||
* Get cached props for a specific model
|
|
||||||
*/
|
|
||||||
getModelProps(modelId: string): ApiLlamaCppServerProps | null {
|
getModelProps(modelId: string): ApiLlamaCppServerProps | null {
|
||||||
return this._modelPropsCache.get(modelId) ?? null;
|
return this.modelPropsCache.get(modelId) ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if model props are being fetched
|
|
||||||
*/
|
|
||||||
isModelPropsFetching(modelId: string): boolean {
|
isModelPropsFetching(modelId: string): boolean {
|
||||||
return this._modelPropsFetching.has(modelId);
|
return this.modelPropsFetching.has(modelId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetches properties for a specific model (ROUTER mode)
|
|
||||||
* Results are cached for subsequent calls
|
|
||||||
*/
|
|
||||||
async fetchModelProps(modelId: string): Promise<ApiLlamaCppServerProps | null> {
|
async fetchModelProps(modelId: string): Promise<ApiLlamaCppServerProps | null> {
|
||||||
// Return cached if available
|
const cached = this.modelPropsCache.get(modelId);
|
||||||
const cached = this._modelPropsCache.get(modelId);
|
|
||||||
if (cached) return cached;
|
if (cached) return cached;
|
||||||
|
|
||||||
// Don't fetch if already fetching
|
if (this.modelPropsFetching.has(modelId)) return null;
|
||||||
if (this._modelPropsFetching.has(modelId)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
this._modelPropsFetching.add(modelId);
|
this.modelPropsFetching.add(modelId);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const props = await PropsService.fetchForModel(modelId);
|
const props = await PropsService.fetchForModel(modelId);
|
||||||
this._modelPropsCache.set(modelId, props);
|
this.modelPropsCache.set(modelId, props);
|
||||||
return props;
|
return props;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn(`Failed to fetch props for model ${modelId}:`, error);
|
console.warn(`Failed to fetch props for model ${modelId}:`, error);
|
||||||
return null;
|
return null;
|
||||||
} finally {
|
} finally {
|
||||||
this._modelPropsFetching.delete(modelId);
|
this.modelPropsFetching.delete(modelId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -265,42 +196,37 @@ class PropsStore {
|
||||||
// Clear State
|
// Clear State
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/**
|
|
||||||
* Clears all server state
|
|
||||||
*/
|
|
||||||
clear(): void {
|
clear(): void {
|
||||||
this._serverProps = null;
|
this.props = null;
|
||||||
this._error = null;
|
this.error = null;
|
||||||
this._loading = false;
|
this.loading = false;
|
||||||
this._serverRole = null;
|
this.role = null;
|
||||||
this.fetchPromise = null;
|
this.fetchPromise = null;
|
||||||
this._modelPropsCache.clear();
|
this.modelPropsCache.clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const propsStore = new PropsStore();
|
export const serverStore = new ServerStore();
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
// Reactive Getters (for use in components)
|
// Reactive Getters (for use in components)
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
export const serverProps = () => propsStore.serverProps;
|
export const serverProps = () => serverStore.props;
|
||||||
export const propsLoading = () => propsStore.loading;
|
export const serverLoading = () => serverStore.loading;
|
||||||
export const propsError = () => propsStore.error;
|
export const serverError = () => serverStore.error;
|
||||||
export const modelName = () => propsStore.modelName;
|
export const serverRole = () => serverStore.role;
|
||||||
export const supportedModalities = () => propsStore.supportedModalities;
|
export const modelName = () => serverStore.modelName;
|
||||||
export const supportsVision = () => propsStore.supportsVision;
|
export const supportedModalities = () => serverStore.supportedModalities;
|
||||||
export const supportsAudio = () => propsStore.supportsAudio;
|
export const supportsVision = () => serverStore.supportsVision;
|
||||||
export const slotsEndpointAvailable = () => propsStore.slotsEndpointAvailable;
|
export const supportsAudio = () => serverStore.supportsAudio;
|
||||||
export const defaultParams = () => propsStore.defaultParams;
|
export const slotsEndpointAvailable = () => serverStore.slotsEndpointAvailable;
|
||||||
export const contextSize = () => propsStore.contextSize;
|
export const defaultParams = () => serverStore.defaultParams;
|
||||||
|
export const contextSize = () => serverStore.contextSize;
|
||||||
// Server mode exports
|
export const isRouterMode = () => serverStore.isRouterMode;
|
||||||
export const serverRole = () => propsStore.serverRole;
|
export const isModelMode = () => serverStore.isModelMode;
|
||||||
export const isRouterMode = () => propsStore.isRouterMode;
|
|
||||||
export const isModelMode = () => propsStore.isModelMode;
|
|
||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
export const fetchProps = propsStore.fetch.bind(propsStore);
|
export const fetchServerProps = serverStore.fetch.bind(serverStore);
|
||||||
export const fetchModelProps = propsStore.fetchModelProps.bind(propsStore);
|
export const fetchModelProps = serverStore.fetchModelProps.bind(serverStore);
|
||||||
export const getModelProps = propsStore.getModelProps.bind(propsStore);
|
export const getModelProps = serverStore.getModelProps.bind(serverStore);
|
||||||
|
|
@ -35,7 +35,7 @@ import { browser } from '$app/environment';
|
||||||
import { SETTING_CONFIG_DEFAULT } from '$lib/constants/settings-config';
|
import { SETTING_CONFIG_DEFAULT } from '$lib/constants/settings-config';
|
||||||
import { normalizeFloatingPoint } from '$lib/utils/precision';
|
import { normalizeFloatingPoint } from '$lib/utils/precision';
|
||||||
import { ParameterSyncService } from '$lib/services/parameter-sync';
|
import { ParameterSyncService } from '$lib/services/parameter-sync';
|
||||||
import { propsStore } from '$lib/stores/props.svelte';
|
import { serverStore } from '$lib/stores/server.svelte';
|
||||||
import { setConfigValue, getConfigValue, configToParameterRecord } from '$lib/utils/config-helpers';
|
import { setConfigValue, getConfigValue, configToParameterRecord } from '$lib/utils/config-helpers';
|
||||||
import {
|
import {
|
||||||
CONFIG_LOCALSTORAGE_KEY,
|
CONFIG_LOCALSTORAGE_KEY,
|
||||||
|
|
@ -53,7 +53,7 @@ class SettingsStore {
|
||||||
* Centralizes the pattern of getting and extracting server defaults
|
* Centralizes the pattern of getting and extracting server defaults
|
||||||
*/
|
*/
|
||||||
private getServerDefaults(): Record<string, string | number | boolean> {
|
private getServerDefaults(): Record<string, string | number | boolean> {
|
||||||
const serverParams = propsStore.defaultParams;
|
const serverParams = serverStore.defaultParams;
|
||||||
return serverParams ? ParameterSyncService.extractServerDefaults(serverParams) : {};
|
return serverParams ? ParameterSyncService.extractServerDefaults(serverParams) : {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -259,7 +259,7 @@ class SettingsStore {
|
||||||
* This sets up the default values from /props endpoint
|
* This sets up the default values from /props endpoint
|
||||||
*/
|
*/
|
||||||
syncWithServerDefaults(): void {
|
syncWithServerDefaults(): void {
|
||||||
const serverParams = propsStore.defaultParams;
|
const serverParams = serverStore.defaultParams;
|
||||||
if (!serverParams) {
|
if (!serverParams) {
|
||||||
console.warn('No server parameters available for initialization');
|
console.warn('No server parameters available for initialization');
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import { isSvgMimeType, svgBase64UrlToPngDataURL } from './svg-to-png';
|
||||||
import { isWebpMimeType, webpBase64UrlToPngDataURL } from './webp-to-png';
|
import { isWebpMimeType, webpBase64UrlToPngDataURL } from './webp-to-png';
|
||||||
import { FileTypeCategory, AttachmentType } from '$lib/enums';
|
import { FileTypeCategory, AttachmentType } from '$lib/enums';
|
||||||
import { config, settingsStore } from '$lib/stores/settings.svelte';
|
import { config, settingsStore } from '$lib/stores/settings.svelte';
|
||||||
import { supportsVision } from '$lib/stores/props.svelte';
|
import { supportsVision } from '$lib/stores/server.svelte';
|
||||||
import { getFileTypeCategory } from '$lib/utils/file-type';
|
import { getFileTypeCategory } from '$lib/utils/file-type';
|
||||||
import { readFileAsText, isLikelyTextFile } from './text-files';
|
import { readFileAsText, isLikelyTextFile } from './text-files';
|
||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import { isTextFileByName } from './text-files';
|
||||||
import { isWebpMimeType, webpBase64UrlToPngDataURL } from './webp-to-png';
|
import { isWebpMimeType, webpBase64UrlToPngDataURL } from './webp-to-png';
|
||||||
import { FileTypeCategory } from '$lib/enums';
|
import { FileTypeCategory } from '$lib/enums';
|
||||||
import { getFileTypeCategory } from '$lib/utils/file-type';
|
import { getFileTypeCategory } from '$lib/utils/file-type';
|
||||||
import { supportsVision } from '$lib/stores/props.svelte';
|
import { supportsVision } from '$lib/stores/server.svelte';
|
||||||
import { settingsStore } from '$lib/stores/settings.svelte';
|
import { settingsStore } from '$lib/stores/settings.svelte';
|
||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@
|
||||||
setTitleUpdateConfirmationCallback
|
setTitleUpdateConfirmationCallback
|
||||||
} from '$lib/stores/conversations.svelte';
|
} from '$lib/stores/conversations.svelte';
|
||||||
import * as Sidebar from '$lib/components/ui/sidebar/index.js';
|
import * as Sidebar from '$lib/components/ui/sidebar/index.js';
|
||||||
import { isRouterMode, propsStore } from '$lib/stores/props.svelte';
|
import { isRouterMode, serverStore } from '$lib/stores/server.svelte';
|
||||||
import { config, settingsStore } from '$lib/stores/settings.svelte';
|
import { config, settingsStore } from '$lib/stores/settings.svelte';
|
||||||
import { ModeWatcher } from 'mode-watcher';
|
import { ModeWatcher } from 'mode-watcher';
|
||||||
import { Toaster } from 'svelte-sonner';
|
import { Toaster } from 'svelte-sonner';
|
||||||
|
|
@ -95,16 +95,16 @@
|
||||||
// Initialize server properties on app load (run once)
|
// Initialize server properties on app load (run once)
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
// Only fetch if we don't already have props
|
// Only fetch if we don't already have props
|
||||||
if (!propsStore.serverProps) {
|
if (!serverStore.props) {
|
||||||
untrack(() => {
|
untrack(() => {
|
||||||
propsStore.fetch();
|
serverStore.fetch();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Sync settings when server props are loaded
|
// Sync settings when server props are loaded
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
const serverProps = propsStore.serverProps;
|
const serverProps = serverStore.props;
|
||||||
|
|
||||||
if (serverProps?.default_generation_settings?.params) {
|
if (serverProps?.default_generation_settings?.params) {
|
||||||
settingsStore.syncWithServerDefaults();
|
settingsStore.syncWithServerDefaults();
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
import { propsStore } from '$lib/stores/props.svelte';
|
import { serverStore } from '$lib/stores/server.svelte';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mock server properties for Storybook testing
|
* Mock server properties for Storybook testing
|
||||||
* This utility allows setting mock server configurations without polluting production code
|
* This utility allows setting mock server configurations without polluting production code
|
||||||
*/
|
*/
|
||||||
export function mockServerProps(props: Partial<ApiLlamaCppServerProps>): void {
|
export function mockServerProps(props: Partial<ApiLlamaCppServerProps>): void {
|
||||||
// Directly set the private _serverProps for testing purposes
|
// Directly set the props for testing purposes
|
||||||
(propsStore as unknown as { _serverProps: ApiLlamaCppServerProps })._serverProps = {
|
(serverStore as unknown as { props: ApiLlamaCppServerProps }).props = {
|
||||||
model_path: props.model_path || 'test-model',
|
model_path: props.model_path || 'test-model',
|
||||||
modalities: {
|
modalities: {
|
||||||
vision: props.modalities?.vision ?? false,
|
vision: props.modalities?.vision ?? false,
|
||||||
|
|
@ -17,18 +17,18 @@ export function mockServerProps(props: Partial<ApiLlamaCppServerProps>): void {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reset props store to clean state for testing
|
* Reset server store to clean state for testing
|
||||||
*/
|
*/
|
||||||
export function resetPropsStore(): void {
|
export function resetServerStore(): void {
|
||||||
(propsStore as unknown as { _serverProps: ApiLlamaCppServerProps })._serverProps = {
|
(serverStore as unknown as { props: ApiLlamaCppServerProps }).props = {
|
||||||
model_path: '',
|
model_path: '',
|
||||||
modalities: {
|
modalities: {
|
||||||
vision: false,
|
vision: false,
|
||||||
audio: false
|
audio: false
|
||||||
}
|
}
|
||||||
} as ApiLlamaCppServerProps;
|
} as ApiLlamaCppServerProps;
|
||||||
(propsStore as unknown as { _error: string })._error = '';
|
(serverStore as unknown as { error: string }).error = '';
|
||||||
(propsStore as unknown as { _loading: boolean })._loading = false;
|
(serverStore as unknown as { loading: boolean }).loading = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue