* ui: add Default reasoning option that defers to the server The webui always injected enable_thinking, overriding the chat template default and the --reasoning flag, breaking models that reason unconditionally (e.g. Gemma 4 E4B) on a fresh client. Default sends nothing so the server decides, Off and effort levels force the value as before. All choices are remembered. Also remove the boolean thinking API from the conversations store and drop ChatFormReasoningEffortSubmenu.svelte (dead code). * ui: close the whole menu tree on reasoning level selection The reasoning levels were raw buttons inside the SubContent, so selecting one only closed the submenu via manual state while the root dropdown stayed open. DropdownMenu.Item closes the full tree on select like the sibling entries and brings native keyboard navigation. * ui: prevent the add menu tooltip from flashing when the dropdown closes
98 lines
3.3 KiB
TypeScript
98 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 isOff: 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 currentEffort = $derived(conversationsStore.getReasoningEffort());
|
|
const thinkingEnabled = $derived(
|
|
currentEffort !== ReasoningEffort.OFF && currentEffort !== ReasoningEffort.DEFAULT
|
|
);
|
|
|
|
return {
|
|
get modelSupportsThinking() {
|
|
return modelSupportsThinking;
|
|
},
|
|
get thinkingEnabled() {
|
|
return thinkingEnabled;
|
|
},
|
|
get isOff() {
|
|
return currentEffort === ReasoningEffort.OFF;
|
|
},
|
|
get currentEffort() {
|
|
return currentEffort;
|
|
},
|
|
get levels() {
|
|
return REASONING_EFFORT_LEVELS;
|
|
},
|
|
isSelected(level: ReasoningEffortLevel): boolean {
|
|
return currentEffort === level.value;
|
|
},
|
|
tokenLabel(level: ReasoningEffortLevel): string | null {
|
|
if (level.value === ReasoningEffort.DEFAULT) return 'Model default';
|
|
const tokens = REASONING_EFFORT_TOKENS[level.value];
|
|
if (tokens === undefined) return null;
|
|
return tokens === -1 ? 'Unlimited' : `Max ${tokens.toLocaleString()} tokens`;
|
|
},
|
|
select(level: ReasoningEffortLevel): void {
|
|
conversationsStore.setReasoningEffort(level.value as ReasoningEffort);
|
|
}
|
|
};
|
|
}
|