-
Notifications
You must be signed in to change notification settings - Fork 4.4k
Expand file tree
/
Copy pathModelSelector.tsx
More file actions
210 lines (178 loc) · 6.3 KB
/
Copy pathModelSelector.tsx
File metadata and controls
210 lines (178 loc) · 6.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
"use client";
import React, { useState, useCallback, useMemo, useRef } from "react";
import { Popover, OpenButton } from "@opal/components";
import { getModelIcon } from "@/lib/languageModels";
import {
GLOBAL_DEFAULT_LLM_OPTION,
LLMOption,
ModelOptionProvider,
} from "@/lib/languageModels/options";
import { useLLMProviders } from "@/lib/languageModels/hooks";
import ModelSelectorContent, {
ReasoningManager,
TemperatureManager,
useModelDetailManagers,
} from "@/sections/model-selector/ModelSelectorContent";
export interface ModelSelectorProps {
/** The currently selected model, identified by model_configuration_id. */
value: number | null;
onChange: (option: LLMOption) => void;
providerOptions?: ModelOptionProvider[];
includeHiddenModels?: boolean;
requiresImageInput?: boolean;
/**
* Custom trigger element. When omitted the default OpenButton (icon +
* display name) is rendered.
*/
renderTrigger?: () => React.ReactNode;
/**
* When provided, a temperature slider is shown in the per-model detail
* pane (gated on user.preferences.temperature_override_enabled).
*/
temperatureManager?: TemperatureManager;
/**
* When provided, the reasoning-level slider in the per-model detail pane is
* enabled for models that report supports_reasoning, disabled otherwise.
*/
reasoningManager?: ReasoningManager;
disabled?: boolean;
/**
* When true, a "Global Default Model" entry is prepended to the list.
* Selecting it calls onChange with GLOBAL_DEFAULT_LLM_OPTION
* (modelConfigurationId === null), which callers should treat as "clear."
*/
includeGlobalDefault?: boolean;
/** Which side of the trigger the popover prefers to open on. */
side?: "top" | "bottom" | "left" | "right";
/** Optional controlled open state for the popover. */
open?: boolean;
onOpenChange?: (open: boolean) => void;
}
export default function ModelSelector({
value,
onChange,
providerOptions,
includeHiddenModels = false,
requiresImageInput,
renderTrigger,
temperatureManager,
reasoningManager,
disabled = false,
includeGlobalDefault = false,
side = "top",
open: controlledOpen,
onOpenChange,
}: ModelSelectorProps) {
// Unscoped by default. An agent narrows the model list, but only a chat has
// an agent. The admin and settings pages that embed this picker have none,
// and must not be filtered by whichever agent happens to be active. A chat
// caller passes its own scoped list through providerOptions.
// The list stays defined even before it arrives, so the child never sees
// undefined and never falls through to its own agent-scoped list.
const {
llmProviders: allProviderOptions,
defaultText,
isLoading: allProvidersLoading,
} = useLLMProviders();
const llmProviders = providerOptions ?? allProviderOptions ?? [];
const isLoading = providerOptions === undefined && allProvidersLoading;
const [internalOpen, setInternalOpen] = useState(false);
const isControlled = controlledOpen !== undefined;
const open = isControlled ? controlledOpen : internalOpen;
const handleOpenChange = useCallback(
(nextOpen: boolean) => {
if (!isControlled) {
setInternalOpen(nextOpen);
}
onOpenChange?.(nextOpen);
},
[isControlled, onOpenChange]
);
const scrollContainerRef = useRef<HTMLDivElement>(null);
// Resolve the currently selected option from the ID
const currentOption = useMemo(() => {
if (value == null || !llmProviders) return null;
for (const provider of llmProviders) {
const mc = provider.model_configurations.find((m) => m.id === value);
if (mc) {
return {
provider: provider.provider,
modelName: mc.name,
displayName: mc.effectiveDisplayName,
};
}
}
return null;
}, [value, llmProviders]);
// When no model is explicitly selected, fall back to showing the global default.
const defaultModelOption = useMemo(() => {
if (!defaultText || !llmProviders) return null;
const provider = llmProviders.find(
(p) => p.id === defaultText.provider_id
);
const mc = provider?.model_configurations.find(
(m) => m.name === defaultText.model_name
);
if (!mc || !provider) return null;
return {
provider: provider.provider,
modelName: mc.name,
displayName: mc.custom_display_name ?? mc.display_name ?? mc.name,
};
}, [defaultText, llmProviders]);
const effectiveOption = currentOption ?? defaultModelOption;
const currentDisplayName = effectiveOption?.displayName ?? "Select Model";
const isSelected = useCallback(
(option: LLMOption) => {
if (option === GLOBAL_DEFAULT_LLM_OPTION) return value === null;
return option.modelConfigurationId != null
? option.modelConfigurationId === value
: option.provider === currentOption?.provider &&
option.modelName === currentOption?.modelName;
},
[value, currentOption]
);
const handleSelect = useCallback(
(option: LLMOption) => {
onChange(option);
handleOpenChange(false);
},
[onChange, handleOpenChange]
);
const modelDetail = useModelDetailManagers(
temperatureManager,
reasoningManager
);
const triggerIcon = effectiveOption
? getModelIcon(effectiveOption.provider, effectiveOption.modelName)
: getModelIcon("", "");
return (
<Popover open={open} onOpenChange={handleOpenChange}>
<div data-testid="llm-popover-trigger">
<Popover.Trigger asChild disabled={disabled}>
{renderTrigger ? (
renderTrigger()
) : (
<OpenButton disabled={disabled} icon={triggerIcon}>
{currentDisplayName}
</OpenButton>
)}
</Popover.Trigger>
</div>
<Popover.Content side={side} align="end" width="xl" sticky="partial">
<ModelSelectorContent
currentModelName={currentOption?.modelName}
providerOptions={llmProviders}
isLoading={isLoading}
includeHiddenModels={includeHiddenModels}
requiresImageInput={requiresImageInput}
onSelect={handleSelect}
isSelected={isSelected}
includeGlobalDefault={includeGlobalDefault}
scrollContainerRef={scrollContainerRef}
modelDetail={modelDetail}
/>
</Popover.Content>
</Popover>
);
}