mirror of
https://github.com/voideditor/void
synced 2026-05-24 09:58:23 +00:00
add reasoning slider (still need to hook it up)
This commit is contained in:
parent
3bb9d5b3a5
commit
d2e096957e
5 changed files with 223 additions and 51 deletions
|
|
@ -17,7 +17,7 @@ import { ChatMarkdownRender, ChatMessageLocation } from '../markdown/ChatMarkdow
|
|||
import { URI } from '../../../../../../../base/common/uri.js';
|
||||
import { IDisposable } from '../../../../../../../base/common/lifecycle.js';
|
||||
import { ErrorDisplay } from './ErrorDisplay.js';
|
||||
import { TextAreaFns, VoidInputBox2 } from '../util/inputs.js';
|
||||
import { TextAreaFns, VoidInputBox2, VoidSlider, VoidSwitch } from '../util/inputs.js';
|
||||
import { ModelDropdown, } from '../void-settings-tsx/ModelDropdown.js';
|
||||
import { SidebarThreadSelector } from './SidebarThreadSelector.js';
|
||||
import { useScrollbarStyles } from '../util/useScrollbarStyles.js';
|
||||
|
|
@ -29,7 +29,7 @@ import { WarningBox } from '../void-settings-tsx/WarningBox.js';
|
|||
import { ChatMessage, StagingSelectionItem, ToolMessage } from '../../../chatThreadService.js';
|
||||
import { filenameToVscodeLanguage } from '../../../../common/helpers/detectLanguage.js';
|
||||
import { ToolName } from '../../../toolsService.js';
|
||||
|
||||
import { getModelCapabilities } from '../../../../common/modelCapabilities.js';
|
||||
|
||||
|
||||
|
||||
|
|
@ -146,6 +146,58 @@ export const IconLoading = ({ className = '' }: { className?: string }) => {
|
|||
const getChatBubbleId = (threadId: string, messageIdx: number) => `${threadId}-${messageIdx}`;
|
||||
|
||||
|
||||
|
||||
|
||||
const ReasoningOptionDropdown = () => {
|
||||
const accessor = useAccessor()
|
||||
|
||||
const voidSettingsService = accessor.get('IVoidSettingsService')
|
||||
const voidSettingsState = useSettingsState()
|
||||
|
||||
const modelSelection = voidSettingsState.modelSelectionOfFeature['Ctrl+L']
|
||||
if (!modelSelection) return null
|
||||
|
||||
const { modelName, providerName } = modelSelection
|
||||
const { canToggleReasoning, reasoningBudgetOptions } = getModelCapabilities(providerName, modelName).supportsReasoningOutput || {}
|
||||
|
||||
const defaultEnabledVal = canToggleReasoning ? true : false
|
||||
const isEnabled = voidSettingsState.optionsOfModelSelection[modelSelection.providerName]?.[modelSelection.modelName]?.reasoningEnabled ?? defaultEnabledVal
|
||||
|
||||
let toggleButton: React.ReactNode = null
|
||||
if (canToggleReasoning) {
|
||||
toggleButton = <VoidSwitch
|
||||
size='xs'
|
||||
value={isEnabled}
|
||||
onChange={(newVal) => { voidSettingsService.setOptionsOfModelSelection(modelSelection.providerName, modelSelection.modelName, { reasoningEnabled: newVal }) }}
|
||||
/>
|
||||
}
|
||||
|
||||
let slider: React.ReactNode = null
|
||||
if (isEnabled && reasoningBudgetOptions?.type === 'slider') {
|
||||
const { min, max, default: defaultVal } = reasoningBudgetOptions
|
||||
const value = voidSettingsState.optionsOfModelSelection[modelSelection.providerName]?.[modelSelection.modelName]?.reasoningBudget ?? defaultVal
|
||||
slider = <VoidSlider
|
||||
width={50}
|
||||
size='xs'
|
||||
min={min}
|
||||
max={max}
|
||||
step={(max - min) / 8}
|
||||
value={value}
|
||||
onChange={(newVal) => { voidSettingsService.setOptionsOfModelSelection(modelSelection.providerName, modelSelection.modelName, { reasoningBudget: newVal }) }}
|
||||
label={value + ''}
|
||||
/>
|
||||
}
|
||||
|
||||
return <>
|
||||
{toggleButton}
|
||||
{slider}
|
||||
</>
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
interface VoidChatAreaProps {
|
||||
// Required
|
||||
children: React.ReactNode; // This will be the input component
|
||||
|
|
@ -248,6 +300,8 @@ export const VoidChatArea: React.FC<VoidChatAreaProps> = ({
|
|||
</div>
|
||||
)}
|
||||
|
||||
<ReasoningOptionDropdown />
|
||||
|
||||
{isStreaming ? (
|
||||
<ButtonStop onClick={onAbort} />
|
||||
) : (
|
||||
|
|
|
|||
|
|
@ -214,6 +214,151 @@ export const VoidInputBox = ({ onChangeText, onCreateInstance, inputBoxRef, plac
|
|||
|
||||
|
||||
|
||||
|
||||
export const VoidSlider = ({
|
||||
value,
|
||||
onChange,
|
||||
size = 'md',
|
||||
label,
|
||||
disabled = false,
|
||||
min = 0,
|
||||
max = 7,
|
||||
step = 1,
|
||||
className = '',
|
||||
width = 200,
|
||||
}: {
|
||||
value: number;
|
||||
onChange: (value: number) => void;
|
||||
label?: string;
|
||||
disabled?: boolean;
|
||||
size?: 'xs' | 'sm' | 'sm+' | 'md';
|
||||
min?: number;
|
||||
max?: number;
|
||||
step?: number;
|
||||
className?: string;
|
||||
width?: number;
|
||||
}) => {
|
||||
// Calculate percentage for position
|
||||
const percentage = ((value - min) / (max - min)) * 100;
|
||||
|
||||
// Handle track click
|
||||
const handleTrackClick = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
if (disabled) return;
|
||||
|
||||
const rect = e.currentTarget.getBoundingClientRect();
|
||||
const clickPosition = e.clientX - rect.left;
|
||||
const trackWidth = rect.width;
|
||||
|
||||
// Calculate new value
|
||||
const newPercentage = Math.max(0, Math.min(1, clickPosition / trackWidth));
|
||||
const rawValue = min + newPercentage * (max - min);
|
||||
|
||||
// Special handling to ensure max value is always reachable
|
||||
if (rawValue >= max - step / 2) {
|
||||
onChange(max);
|
||||
return;
|
||||
}
|
||||
|
||||
// Normal step calculation
|
||||
const steppedValue = Math.round((rawValue - min) / step) * step + min;
|
||||
const clampedValue = Math.max(min, Math.min(max, steppedValue));
|
||||
|
||||
onChange(clampedValue);
|
||||
};
|
||||
|
||||
// Helper function to handle thumb dragging that respects steps and max
|
||||
const handleThumbDrag = (moveEvent: MouseEvent, track: Element) => {
|
||||
if (!track) return;
|
||||
|
||||
const rect = (track as HTMLElement).getBoundingClientRect();
|
||||
const movePosition = moveEvent.clientX - rect.left;
|
||||
const trackWidth = rect.width;
|
||||
|
||||
// Calculate new value
|
||||
const newPercentage = Math.max(0, Math.min(1, movePosition / trackWidth));
|
||||
const rawValue = min + newPercentage * (max - min);
|
||||
|
||||
// Special handling to ensure max value is always reachable
|
||||
if (rawValue >= max - step / 2) {
|
||||
onChange(max);
|
||||
return;
|
||||
}
|
||||
|
||||
// Normal step calculation
|
||||
const steppedValue = Math.round((rawValue - min) / step) * step + min;
|
||||
const clampedValue = Math.max(min, Math.min(max, steppedValue));
|
||||
|
||||
onChange(clampedValue);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`inline-flex items-center flex-shrink-0 ${className}`}>
|
||||
<div className={`relative flex-shrink-0 ${disabled ? 'opacity-25' : ''}`} style={{ width }}>
|
||||
{/* Track */}
|
||||
<div
|
||||
className={`relative ${size === 'xs' ? 'h-1' :
|
||||
size === 'sm' ? 'h-1.5' :
|
||||
size === 'sm+' ? 'h-2' : 'h-2.5'
|
||||
} bg-gray-200 dark:bg-gray-700 rounded-full cursor-pointer`}
|
||||
onClick={handleTrackClick}
|
||||
>
|
||||
{/* Filled part of track */}
|
||||
<div
|
||||
className={`absolute left-0 ${size === 'xs' ? 'h-1' :
|
||||
size === 'sm' ? 'h-1.5' :
|
||||
size === 'sm+' ? 'h-2' : 'h-2.5'
|
||||
} bg-gray-900 dark:bg-white rounded-full`}
|
||||
style={{ width: `${percentage}%` }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Thumb */}
|
||||
<div
|
||||
className={`absolute top-1/2 transform -translate-x-1/2 -translate-y-1/2 ${size === 'xs' ? 'h-3 w-3' :
|
||||
size === 'sm' ? 'h-4 w-4' :
|
||||
size === 'sm+' ? 'h-5 w-5' : 'h-6 w-6'
|
||||
} bg-white dark:bg-gray-900 rounded-full shadow-md ${disabled ? 'cursor-not-allowed' : 'cursor-grab active:cursor-grabbing'}`}
|
||||
style={{ left: `${percentage}%` }}
|
||||
onMouseDown={(e) => {
|
||||
if (disabled) return;
|
||||
|
||||
const track = e.currentTarget.previousElementSibling;
|
||||
|
||||
const handleMouseMove = (moveEvent: MouseEvent) => {
|
||||
handleThumbDrag(moveEvent, track as Element);
|
||||
};
|
||||
|
||||
const handleMouseUp = () => {
|
||||
document.removeEventListener('mousemove', handleMouseMove);
|
||||
document.removeEventListener('mouseup', handleMouseUp);
|
||||
document.body.style.cursor = '';
|
||||
document.body.style.userSelect = '';
|
||||
};
|
||||
|
||||
document.body.style.userSelect = 'none';
|
||||
document.body.style.cursor = 'grabbing';
|
||||
document.addEventListener('mousemove', handleMouseMove);
|
||||
document.addEventListener('mouseup', handleMouseUp);
|
||||
|
||||
e.preventDefault();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{label && (
|
||||
<span className={`ml-3 text-gray-900 dark:text-gray-100 ${size === 'xs' ? 'text-xs' : 'text-sm'
|
||||
}`}>
|
||||
{label}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
export const VoidSwitch = ({
|
||||
value,
|
||||
onChange,
|
||||
|
|
|
|||
|
|
@ -21,7 +21,12 @@ type ModelOptions = {
|
|||
supportsReasoningOutput: false | {
|
||||
// you are allowed to not include openSourceThinkTags if it's not open source (no such cases as of writing)
|
||||
// if it's open source, put the think tags here so we parse them out in e.g. ollama
|
||||
readonly openSourceThinkTags?: [string, string]
|
||||
readonly openSourceThinkTags?: [string, string];
|
||||
|
||||
// reasoning options
|
||||
readonly canToggleReasoning?: boolean; // whether or not the user can enable reasoning mode (or if the model only supports reasoning)
|
||||
readonly maxOutputTokens?: number;
|
||||
readonly reasoningBudgetOptions?: { type: 'slider'; min: number; max: number; default: number };
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -153,12 +158,16 @@ const extensiveModelFallback: ProviderSettings['modelOptionsFallback'] = (modelN
|
|||
const anthropicModelOptions = {
|
||||
'claude-3-7-sonnet-20250219': { // https://docs.anthropic.com/en/docs/about-claude/models/all-models#model-comparison-table
|
||||
contextWindow: 200_000,
|
||||
maxOutputTokens: 8_192, // TODO!!! 64_000 for extended thinking, can bump it to 128_000 with output-128k-2025-02-19
|
||||
maxOutputTokens: 8_192,
|
||||
cost: { input: 3.00, cache_read: 0.30, cache_write: 3.75, output: 15.00 },
|
||||
supportsFIM: false,
|
||||
supportsSystemMessage: 'separated',
|
||||
supportsTools: 'anthropic-style',
|
||||
supportsReasoningOutput: {},
|
||||
supportsReasoningOutput: {
|
||||
canToggleReasoning: true,
|
||||
maxOutputTokens: 64_000, // can bump it to 128_000 with beta mode output-128k-2025-02-19
|
||||
reasoningBudgetOptions: { type: 'slider', min: 1024, max: 32_000, default: 1024 }, // they recommend batching if max > 32_000
|
||||
},
|
||||
},
|
||||
'claude-3-5-sonnet-20241022': {
|
||||
contextWindow: 200_000,
|
||||
|
|
@ -520,12 +529,12 @@ const modelSettingsOfProvider: { [providerName in ProviderName]: ProviderSetting
|
|||
|
||||
// ---------------- exports ----------------
|
||||
|
||||
export const getModelCapabilities = (providerName: ProviderName, modelName: string): ModelOptions & { modelName: string; didNotFindModel?: boolean } => {
|
||||
export const getModelCapabilities = (providerName: ProviderName, modelName: string): ModelOptions & { modelName: string; isUnrecognizedModel: boolean } => {
|
||||
const { modelOptions, modelOptionsFallback } = modelSettingsOfProvider[providerName]
|
||||
if (modelName in modelOptions) return { modelName, ...modelOptions[modelName] }
|
||||
if (modelName in modelOptions) return { modelName, ...modelOptions[modelName], isUnrecognizedModel: false }
|
||||
const result = modelOptionsFallback(modelName)
|
||||
if (result) return result
|
||||
return { modelName, ...modelOptionsDefaults, didNotFindModel: true }
|
||||
if (result) return { ...result, isUnrecognizedModel: false }
|
||||
return { modelName, ...modelOptionsDefaults, isUnrecognizedModel: true }
|
||||
}
|
||||
|
||||
// non-model settings
|
||||
|
|
|
|||
|
|
@ -57,8 +57,6 @@ export interface IVoidSettingsService {
|
|||
readonly state: VoidSettingsState; // in order to play nicely with react, you should immutably change state
|
||||
readonly waitForInitState: Promise<void>;
|
||||
|
||||
readAndInitializeState: (providedState?: VoidSettingsState) => Promise<void>;
|
||||
|
||||
onDidChangeState: Event<void>;
|
||||
|
||||
setSettingOfProvider: SetSettingOfProviderFn;
|
||||
|
|
@ -213,48 +211,13 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
|
|||
this.readAndInitializeState()
|
||||
}
|
||||
|
||||
async readAndInitializeState(providedState?: VoidSettingsState) {
|
||||
// If providedState is given, use it instead of reading from storage
|
||||
const readS = providedState || await this._readState();
|
||||
async readAndInitializeState() {
|
||||
const readS = await this._readState();
|
||||
|
||||
// the stored data structure might be outdated, so we need to update it here
|
||||
const newSettingsOfProvider = {
|
||||
// A HACK BECAUSE WE ADDED DEEPSEEK (did not exist before, comes before readS)
|
||||
...{ deepseek: defaultSettingsOfProvider.deepseek },
|
||||
|
||||
// A HACK BECAUSE WE ADDED XAI (did not exist before, comes before readS)
|
||||
...{ xAI: defaultSettingsOfProvider.xAI },
|
||||
|
||||
// A HACK BECAUSE WE ADDED VLLM (did not exist before, comes before readS)
|
||||
...{ vLLM: defaultSettingsOfProvider.vLLM },
|
||||
|
||||
...readS.settingsOfProvider,
|
||||
|
||||
// A HACK BECAUSE WE ADDED NEW GEMINI MODELS (existed before, comes after readS)
|
||||
gemini: {
|
||||
...readS.settingsOfProvider.gemini,
|
||||
models: [
|
||||
...readS.settingsOfProvider.gemini.models,
|
||||
...defaultSettingsOfProvider.gemini.models.filter(m => /* if cant find the model in readS (yes this is O(n^2), very small) */ !readS.settingsOfProvider.gemini.models.find(m2 => m2.modelName === m.modelName))
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
const newModelSelectionOfFeature = {
|
||||
// A HACK BECAUSE WE ADDED FastApply
|
||||
...{ 'Apply': null },
|
||||
...readS.modelSelectionOfFeature,
|
||||
};
|
||||
|
||||
const finalState = {
|
||||
...readS,
|
||||
settingsOfProvider: newSettingsOfProvider,
|
||||
modelSelectionOfFeature: newModelSelectionOfFeature,
|
||||
};
|
||||
|
||||
const finalState = readS
|
||||
this.state = _validatedState(finalState);
|
||||
|
||||
|
||||
this._resolver();
|
||||
this._onDidChangeState.fire();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -462,7 +462,8 @@ export const globalSettingNames = Object.keys(defaultGlobalSettings) as GlobalSe
|
|||
|
||||
|
||||
export type ModelSelectionOptions = {
|
||||
reasoningSelection?: { budget: number }
|
||||
reasoningEnabled?: boolean;
|
||||
reasoningBudget?: number;
|
||||
}
|
||||
|
||||
export type OptionsOfModelSelection = Partial<{ [providerName in ProviderName]: { [modelName: string]: ModelSelectionOptions } }>
|
||||
export type OptionsOfModelSelection = Partial<{ [providerName in ProviderName]: { [modelName: string]: ModelSelectionOptions | undefined } }>
|
||||
|
|
|
|||
Loading…
Reference in a new issue