The mobile "+" sheet was missing the reasoning effort section present in the desktop dropdown, so thinking could not be toggled on touch. Extract the shared derivation and selection logic into useReasoningMenu and consume it from both the desktop submenu and the mobile sheet, keeping a single source of truth and preserving each surface idiom.
97 lines
3.3 KiB
TypeScript
97 lines
3.3 KiB
TypeScript
import { ReasoningEffort } from '$lib/enums';
|
|
import { REASONING_EFFORT_LEVELS } from '$lib/constants/reasoning-effort';
|
|
import { REASONING_EFFORT_TOKENS } from '$lib/constants/reasoning-effort-tokens';
|
|
import type { ReasoningEffortLevel } from '$lib/types';
|
|
import type { DatabaseMessage } from '$lib/types/database';
|
|
import {
|
|
modelsStore,
|
|
checkModelSupportsThinking,
|
|
supportsThinking,
|
|
propsCacheVersion,
|
|
loadedModelIds
|
|
} from '$lib/stores/models.svelte';
|
|
import { chatStore } from '$lib/stores/chat.svelte';
|
|
import { conversationsStore, activeMessages } from '$lib/stores/conversations.svelte';
|
|
import { isRouterMode } from '$lib/stores/server.svelte';
|
|
|
|
export interface UseReasoningMenuReturn {
|
|
readonly modelSupportsThinking: boolean;
|
|
readonly thinkingEnabled: boolean;
|
|
readonly currentEffort: ReasoningEffort;
|
|
readonly levels: ReasoningEffortLevel[];
|
|
isSelected(level: ReasoningEffortLevel): boolean;
|
|
tokenLabel(level: ReasoningEffortLevel): string | null;
|
|
select(level: ReasoningEffortLevel): void;
|
|
}
|
|
|
|
/**
|
|
* Shared reactive state and helpers for the reasoning effort menu.
|
|
*
|
|
* Used by both the desktop dropdown (`ChatFormActionAddReasoningSubmenu`)
|
|
* and the mobile sheet (`ChatFormActionAddSheet`) to avoid duplicating the
|
|
* thinking-support derivation and the effort selection logic.
|
|
*/
|
|
export function useReasoningMenu(): UseReasoningMenuReturn {
|
|
const conversationModel = $derived(
|
|
chatStore.getConversationModel(activeMessages() as DatabaseMessage[])
|
|
);
|
|
|
|
// a router chat can carry reasoning from an earlier turn before the props
|
|
// cache is primed, so a model that already produced thinking still qualifies
|
|
const modelSupportsThinkingFromMessages = $derived.by(() => {
|
|
const modelId = isRouterMode() ? modelsStore.selectedModelName || conversationModel : null;
|
|
if (!modelId) return false;
|
|
|
|
return conversationsStore.activeMessages.some(
|
|
(m) => m.role === 'assistant' && m.model === modelId && !!m.reasoningContent
|
|
);
|
|
});
|
|
|
|
const modelSupportsThinking = $derived.by(() => {
|
|
loadedModelIds();
|
|
propsCacheVersion();
|
|
|
|
if (isRouterMode()) {
|
|
const modelId = modelsStore.selectedModelName || conversationModel;
|
|
return checkModelSupportsThinking(modelId ?? '') || modelSupportsThinkingFromMessages;
|
|
}
|
|
|
|
return supportsThinking() || modelSupportsThinkingFromMessages;
|
|
});
|
|
|
|
const thinkingEnabled = $derived(conversationsStore.getThinkingEnabled());
|
|
const currentEffort = $derived(conversationsStore.getReasoningEffort());
|
|
|
|
return {
|
|
get modelSupportsThinking() {
|
|
return modelSupportsThinking;
|
|
},
|
|
get thinkingEnabled() {
|
|
return thinkingEnabled;
|
|
},
|
|
get currentEffort() {
|
|
return currentEffort;
|
|
},
|
|
get levels() {
|
|
return REASONING_EFFORT_LEVELS;
|
|
},
|
|
isSelected(level: ReasoningEffortLevel): boolean {
|
|
if (level.isOff) return !thinkingEnabled;
|
|
return thinkingEnabled && currentEffort === level.value;
|
|
},
|
|
tokenLabel(level: ReasoningEffortLevel): string | null {
|
|
if (level.isOff) return null;
|
|
const tokens = REASONING_EFFORT_TOKENS[level.value];
|
|
return tokens === -1 ? 'Unlimited' : `Max ${tokens.toLocaleString()} tokens`;
|
|
},
|
|
select(level: ReasoningEffortLevel): void {
|
|
if (level.isOff) {
|
|
conversationsStore.setThinkingEnabled(false);
|
|
return;
|
|
}
|
|
conversationsStore.setThinkingEnabled(true);
|
|
conversationsStore.setReasoningEffort(level.value as ReasoningEffort);
|
|
}
|
|
};
|
|
}
|