@nextclaw/ui 0.6.11 → 0.6.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/dist/assets/ChannelsList-CXLzowHj.js +1 -0
- package/dist/assets/ChatPage-CvtonrzM.js +36 -0
- package/dist/assets/DocBrowser-4NK6-Q_u.js +1 -0
- package/dist/assets/LogoBadge-NI7KQCLa.js +1 -0
- package/dist/assets/{MarketplacePage-BOzko5s9.js → MarketplacePage-n7y-pif2.js} +2 -2
- package/dist/assets/ModelConfig-DztCs0mA.js +1 -0
- package/dist/assets/ProvidersList-hSzfE0pG.js +1 -0
- package/dist/assets/{RuntimeConfig-Dt9pLB9P.js → RuntimeConfig-CKFGVus7.js} +1 -1
- package/dist/assets/SearchConfig-Cxs1744q.js +1 -0
- package/dist/assets/{SecretsConfig-C1PU0Yy8.js → SecretsConfig-C90UckNB.js} +2 -2
- package/dist/assets/SessionsConfig-CRor418P.js +2 -0
- package/dist/assets/{card-C7Gtw2Vs.js → card-BQiPUGaa.js} +1 -1
- package/dist/assets/config-layout-BHnOoweL.js +1 -0
- package/dist/assets/index-BCfS4UY1.css +1 -0
- package/dist/assets/index-CB5eJOGS.js +8 -0
- package/dist/assets/index-CkqvHQAt.js +1 -0
- package/dist/assets/{input-oBvxsnV9.js → input-DmFFMdAk.js} +1 -1
- package/dist/assets/{label-C7F8lMpQ.js → label-BHvlZoIz.js} +1 -1
- package/dist/assets/{page-layout-DO8BlScF.js → page-layout-COPE9JyG.js} +1 -1
- package/dist/assets/popover-gcypYeec.js +1 -0
- package/dist/assets/provider-models-D3B_xWXx.js +1 -0
- package/dist/assets/{session-run-status-Kg0FwAPn.js → session-run-status-BgNvd_-a.js} +1 -1
- package/dist/assets/{switch-C6a5GyZB.js → switch-BampMwqT.js} +1 -1
- package/dist/assets/{tabs-custom-BatFap5k.js → tabs-custom-DSQeYaKd.js} +1 -1
- package/dist/assets/useConfirmDialog-DZUn23Li.js +5 -0
- package/dist/assets/{vendor-TlME1INH.js → vendor-BKtTvQYU.js} +69 -64
- package/dist/index.html +3 -3
- package/package.json +1 -1
- package/src/App.tsx +2 -0
- package/src/api/config.ts +13 -0
- package/src/api/types.ts +61 -0
- package/src/components/chat/ChatPage.tsx +16 -0
- package/src/components/chat/chat-input/ChatInputBottomToolbar.tsx +12 -0
- package/src/components/chat/chat-input/components/ChatInputSlashPanelSection.tsx +3 -3
- package/src/components/chat/chat-input/components/bottom-toolbar/ChatInputThinkingSelector.tsx +74 -0
- package/src/components/chat/chat-input/useChatInputBarController.ts +11 -2
- package/src/components/chat/chat-input.types.ts +8 -0
- package/src/components/chat/chat-page-data.ts +40 -3
- package/src/components/chat/chat-stream/transport.ts +3 -0
- package/src/components/chat/chat-stream/types.ts +3 -1
- package/src/components/chat/managers/chat-input.manager.ts +51 -0
- package/src/components/chat/stores/chat-input.store.ts +5 -1
- package/src/components/common/SearchableModelInput.tsx +22 -5
- package/src/components/config/ModelConfig.tsx +13 -12
- package/src/components/config/ProviderForm.tsx +292 -19
- package/src/components/config/SearchConfig.tsx +297 -0
- package/src/components/layout/Sidebar.tsx +6 -1
- package/src/hooks/useConfig.ts +17 -0
- package/src/lib/i18n.ts +37 -0
- package/src/lib/provider-models.ts +91 -3
- package/dist/assets/ChannelsList-C49JQ-Zt.js +0 -1
- package/dist/assets/ChatPage-DIx05c6s.js +0 -36
- package/dist/assets/DocBrowser-CpOosDEI.js +0 -1
- package/dist/assets/LogoBadge-CL_8ZPXU.js +0 -1
- package/dist/assets/ModelConfig-BZ4ZfaQB.js +0 -1
- package/dist/assets/ProvidersList-fPpJ5gl6.js +0 -1
- package/dist/assets/SessionsConfig-EskBOofQ.js +0 -2
- package/dist/assets/index-Cn6_2To7.js +0 -8
- package/dist/assets/index-nEYGCJTC.css +0 -1
- package/dist/assets/provider-models-y4mUDcGF.js +0 -1
- package/dist/assets/useConfirmDialog-zJzVKMdu.js +0 -5
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useMemo, useState } from 'react';
|
|
1
|
+
import { useEffect, useMemo, useState } from 'react';
|
|
2
2
|
import { Check, ChevronsUpDown } from 'lucide-react';
|
|
3
3
|
import { Input } from '@/components/ui/input';
|
|
4
4
|
import { cn } from '@/lib/utils';
|
|
@@ -8,6 +8,7 @@ type SearchableModelInputProps = {
|
|
|
8
8
|
value: string;
|
|
9
9
|
onChange: (value: string) => void;
|
|
10
10
|
options: string[];
|
|
11
|
+
disabled?: boolean;
|
|
11
12
|
placeholder?: string;
|
|
12
13
|
className?: string;
|
|
13
14
|
inputClassName?: string;
|
|
@@ -33,6 +34,7 @@ export function SearchableModelInput({
|
|
|
33
34
|
value,
|
|
34
35
|
onChange,
|
|
35
36
|
options,
|
|
37
|
+
disabled = false,
|
|
36
38
|
placeholder,
|
|
37
39
|
className,
|
|
38
40
|
inputClassName,
|
|
@@ -43,6 +45,12 @@ export function SearchableModelInput({
|
|
|
43
45
|
}: SearchableModelInputProps) {
|
|
44
46
|
const [open, setOpen] = useState(false);
|
|
45
47
|
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
if (disabled && open) {
|
|
50
|
+
setOpen(false);
|
|
51
|
+
}
|
|
52
|
+
}, [disabled, open]);
|
|
53
|
+
|
|
46
54
|
const normalizedOptions = useMemo(() => normalizeOptions(options), [options]);
|
|
47
55
|
const query = value.trim().toLowerCase();
|
|
48
56
|
|
|
@@ -80,10 +88,15 @@ export function SearchableModelInput({
|
|
|
80
88
|
<Input
|
|
81
89
|
id={id}
|
|
82
90
|
value={value}
|
|
83
|
-
|
|
91
|
+
disabled={disabled}
|
|
92
|
+
onFocus={() => {
|
|
93
|
+
if (!disabled) {
|
|
94
|
+
setOpen(true);
|
|
95
|
+
}
|
|
96
|
+
}}
|
|
84
97
|
onChange={(event) => {
|
|
85
98
|
onChange(event.target.value);
|
|
86
|
-
if (!open) {
|
|
99
|
+
if (!open && !disabled) {
|
|
87
100
|
setOpen(true);
|
|
88
101
|
}
|
|
89
102
|
}}
|
|
@@ -103,13 +116,17 @@ export function SearchableModelInput({
|
|
|
103
116
|
type="button"
|
|
104
117
|
onMouseDown={(event) => event.preventDefault()}
|
|
105
118
|
onClick={() => setOpen((prev) => !prev)}
|
|
106
|
-
|
|
119
|
+
disabled={disabled}
|
|
120
|
+
className={cn(
|
|
121
|
+
'absolute inset-y-0 right-0 inline-flex w-10 items-center justify-center',
|
|
122
|
+
disabled ? 'cursor-not-allowed text-gray-300' : 'text-gray-400 hover:text-gray-600'
|
|
123
|
+
)}
|
|
107
124
|
aria-label="toggle model options"
|
|
108
125
|
>
|
|
109
126
|
<ChevronsUpDown className="h-4 w-4" />
|
|
110
127
|
</button>
|
|
111
128
|
|
|
112
|
-
{open && (
|
|
129
|
+
{open && !disabled && (
|
|
113
130
|
<div className="absolute z-20 mt-1 w-full overflow-hidden rounded-xl border border-gray-200 bg-white shadow-lg">
|
|
114
131
|
<div className="max-h-60 overflow-y-auto py-1">
|
|
115
132
|
{!hasExactMatch && value.trim().length > 0 && (
|
|
@@ -33,33 +33,26 @@ export function ModelConfig() {
|
|
|
33
33
|
const workspaceHint = hintForPath('agents.defaults.workspace', uiHints);
|
|
34
34
|
|
|
35
35
|
const providerCatalog = useMemo(
|
|
36
|
-
() => buildProviderModelCatalog({ meta, config }),
|
|
36
|
+
() => buildProviderModelCatalog({ meta, config, onlyConfigured: true }),
|
|
37
37
|
[config, meta]
|
|
38
38
|
);
|
|
39
39
|
|
|
40
40
|
const providerMap = useMemo(() => new Map(providerCatalog.map((provider) => [provider.name, provider])), [providerCatalog]);
|
|
41
|
-
const selectedProvider = providerMap.get(providerName)
|
|
41
|
+
const selectedProvider = providerMap.get(providerName);
|
|
42
42
|
const selectedProviderName = selectedProvider?.name ?? '';
|
|
43
43
|
const selectedProviderAliases = useMemo(() => selectedProvider?.aliases ?? [], [selectedProvider]);
|
|
44
44
|
const selectedProviderModels = useMemo(() => selectedProvider?.models ?? [], [selectedProvider]);
|
|
45
45
|
|
|
46
|
-
useEffect(() => {
|
|
47
|
-
if (providerName || providerCatalog.length === 0) {
|
|
48
|
-
return;
|
|
49
|
-
}
|
|
50
|
-
setProviderName(providerCatalog[0].name);
|
|
51
|
-
}, [providerName, providerCatalog]);
|
|
52
|
-
|
|
53
46
|
useEffect(() => {
|
|
54
47
|
if (!config?.agents?.defaults) {
|
|
55
48
|
return;
|
|
56
49
|
}
|
|
57
50
|
const currentModel = (config.agents.defaults.model || '').trim();
|
|
58
51
|
const matchedProvider = findProviderByModel(currentModel, providerCatalog);
|
|
59
|
-
const effectiveProvider = matchedProvider ??
|
|
52
|
+
const effectiveProvider = matchedProvider ?? '';
|
|
60
53
|
const aliases = providerMap.get(effectiveProvider)?.aliases ?? [];
|
|
61
54
|
setProviderName(effectiveProvider);
|
|
62
|
-
setModelId(toProviderLocalModel(currentModel, aliases));
|
|
55
|
+
setModelId(effectiveProvider ? toProviderLocalModel(currentModel, aliases) : '');
|
|
63
56
|
setWorkspace(config.agents.defaults.workspace || '');
|
|
64
57
|
}, [config, providerCatalog, providerMap]);
|
|
65
58
|
|
|
@@ -75,11 +68,14 @@ export function ModelConfig() {
|
|
|
75
68
|
}, [selectedProviderModels]);
|
|
76
69
|
|
|
77
70
|
const composedModel = useMemo(() => {
|
|
71
|
+
if (!selectedProvider) {
|
|
72
|
+
return '';
|
|
73
|
+
}
|
|
78
74
|
const normalizedModelId = toProviderLocalModel(modelId, selectedProviderAliases);
|
|
79
75
|
if (!normalizedModelId) {
|
|
80
76
|
return '';
|
|
81
77
|
}
|
|
82
|
-
return composeProviderModel(selectedProvider
|
|
78
|
+
return composeProviderModel(selectedProvider.prefix, normalizedModelId);
|
|
83
79
|
}, [modelId, selectedProvider, selectedProviderAliases]);
|
|
84
80
|
|
|
85
81
|
const modelHelpText = t('modelIdentifierHelp') || modelHint?.help || '';
|
|
@@ -90,6 +86,10 @@ export function ModelConfig() {
|
|
|
90
86
|
};
|
|
91
87
|
|
|
92
88
|
const handleModelChange = (nextModelId: string) => {
|
|
89
|
+
if (!selectedProvider) {
|
|
90
|
+
setModelId('');
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
93
|
setModelId(toProviderLocalModel(nextModelId, selectedProviderAliases));
|
|
94
94
|
};
|
|
95
95
|
|
|
@@ -165,6 +165,7 @@ export function ModelConfig() {
|
|
|
165
165
|
value={modelId}
|
|
166
166
|
onChange={handleModelChange}
|
|
167
167
|
options={modelOptions}
|
|
168
|
+
disabled={!selectedProviderName}
|
|
168
169
|
placeholder={modelHint?.placeholder ?? 'gpt-5.1'}
|
|
169
170
|
className="sm:flex-1"
|
|
170
171
|
inputClassName="h-10 rounded-xl"
|
|
@@ -15,17 +15,19 @@ import { Button } from '@/components/ui/button';
|
|
|
15
15
|
import { Input } from '@/components/ui/input';
|
|
16
16
|
import { Label } from '@/components/ui/label';
|
|
17
17
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
|
18
|
+
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
|
18
19
|
import { MaskedInput } from '@/components/common/MaskedInput';
|
|
19
20
|
import { KeyValueEditor } from '@/components/common/KeyValueEditor';
|
|
20
21
|
import { StatusDot } from '@/components/ui/status-dot';
|
|
21
22
|
import { getLanguage, t } from '@/lib/i18n';
|
|
22
23
|
import { hintForPath } from '@/lib/config-hints';
|
|
23
|
-
import type { ProviderConfigView, ProviderConfigUpdate, ProviderConnectionTestRequest } from '@/api/types';
|
|
24
|
+
import type { ProviderConfigView, ProviderConfigUpdate, ProviderConnectionTestRequest, ThinkingLevel } from '@/api/types';
|
|
24
25
|
import { CircleDotDashed, Plus, X, Trash2, ChevronDown, Settings2 } from 'lucide-react';
|
|
25
26
|
import { toast } from 'sonner';
|
|
26
27
|
import { CONFIG_DETAIL_CARD_CLASS, CONFIG_EMPTY_DETAIL_CARD_CLASS } from './config-layout';
|
|
27
28
|
|
|
28
29
|
type WireApiType = 'auto' | 'chat' | 'responses';
|
|
30
|
+
type ModelThinkingConfig = Record<string, { supported: ThinkingLevel[]; default?: ThinkingLevel | null }>;
|
|
29
31
|
|
|
30
32
|
type ProviderFormProps = {
|
|
31
33
|
providerName?: string;
|
|
@@ -48,8 +50,11 @@ const EMPTY_PROVIDER_CONFIG: ProviderConfigView = {
|
|
|
48
50
|
apiBase: null,
|
|
49
51
|
extraHeaders: null,
|
|
50
52
|
wireApi: null,
|
|
51
|
-
models: []
|
|
53
|
+
models: [],
|
|
54
|
+
modelThinking: {}
|
|
52
55
|
};
|
|
56
|
+
const THINKING_LEVELS: ThinkingLevel[] = ['off', 'minimal', 'low', 'medium', 'high', 'adaptive', 'xhigh'];
|
|
57
|
+
const THINKING_LEVEL_SET = new Set<string>(THINKING_LEVELS);
|
|
53
58
|
|
|
54
59
|
function normalizeHeaders(input: Record<string, string> | null | undefined): Record<string, string> | null {
|
|
55
60
|
if (!input) {
|
|
@@ -160,6 +165,128 @@ function serializeModelsForSave(models: string[], defaultModels: string[]): stri
|
|
|
160
165
|
return models;
|
|
161
166
|
}
|
|
162
167
|
|
|
168
|
+
function parseThinkingLevel(value: unknown): ThinkingLevel | null {
|
|
169
|
+
if (typeof value !== 'string') {
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
const normalized = value.trim().toLowerCase();
|
|
173
|
+
if (!normalized) {
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
return THINKING_LEVEL_SET.has(normalized) ? (normalized as ThinkingLevel) : null;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function normalizeThinkingLevels(values: unknown): ThinkingLevel[] {
|
|
180
|
+
if (!Array.isArray(values)) {
|
|
181
|
+
return [];
|
|
182
|
+
}
|
|
183
|
+
const deduped: ThinkingLevel[] = [];
|
|
184
|
+
for (const value of values) {
|
|
185
|
+
const level = parseThinkingLevel(value);
|
|
186
|
+
if (!level || deduped.includes(level)) {
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
deduped.push(level);
|
|
190
|
+
}
|
|
191
|
+
return deduped;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function normalizeModelThinkingConfig(
|
|
195
|
+
input: ProviderConfigView['modelThinking'],
|
|
196
|
+
aliases: string[]
|
|
197
|
+
): ModelThinkingConfig {
|
|
198
|
+
if (!input) {
|
|
199
|
+
return {};
|
|
200
|
+
}
|
|
201
|
+
const normalized: ModelThinkingConfig = {};
|
|
202
|
+
for (const [rawModel, rawValue] of Object.entries(input)) {
|
|
203
|
+
const model = toProviderLocalModelId(rawModel, aliases);
|
|
204
|
+
if (!model) {
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
const supported = normalizeThinkingLevels(rawValue?.supported);
|
|
208
|
+
if (supported.length === 0) {
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
const defaultLevel = parseThinkingLevel(rawValue?.default);
|
|
212
|
+
normalized[model] =
|
|
213
|
+
defaultLevel && supported.includes(defaultLevel)
|
|
214
|
+
? { supported, default: defaultLevel }
|
|
215
|
+
: { supported };
|
|
216
|
+
}
|
|
217
|
+
return normalized;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function normalizeModelThinkingForModels(modelThinking: ModelThinkingConfig, models: string[]): ModelThinkingConfig {
|
|
221
|
+
const modelSet = new Set(models.map((item) => item.trim()).filter(Boolean));
|
|
222
|
+
const normalized: ModelThinkingConfig = {};
|
|
223
|
+
for (const [model, entry] of Object.entries(modelThinking)) {
|
|
224
|
+
if (!modelSet.has(model)) {
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
const supported = normalizeThinkingLevels(entry.supported);
|
|
228
|
+
if (supported.length === 0) {
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
const defaultLevel = parseThinkingLevel(entry.default);
|
|
232
|
+
normalized[model] =
|
|
233
|
+
defaultLevel && supported.includes(defaultLevel)
|
|
234
|
+
? { supported, default: defaultLevel }
|
|
235
|
+
: { supported };
|
|
236
|
+
}
|
|
237
|
+
return normalized;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function modelThinkingEqual(left: ModelThinkingConfig, right: ModelThinkingConfig): boolean {
|
|
241
|
+
const leftKeys = Object.keys(left).sort();
|
|
242
|
+
const rightKeys = Object.keys(right).sort();
|
|
243
|
+
if (leftKeys.length !== rightKeys.length) {
|
|
244
|
+
return false;
|
|
245
|
+
}
|
|
246
|
+
for (let index = 0; index < leftKeys.length; index += 1) {
|
|
247
|
+
const key = leftKeys[index];
|
|
248
|
+
if (key !== rightKeys[index]) {
|
|
249
|
+
return false;
|
|
250
|
+
}
|
|
251
|
+
const leftEntry = left[key];
|
|
252
|
+
const rightEntry = right[key];
|
|
253
|
+
if (!leftEntry || !rightEntry) {
|
|
254
|
+
return false;
|
|
255
|
+
}
|
|
256
|
+
const leftSupported = [...leftEntry.supported].sort();
|
|
257
|
+
const rightSupported = [...rightEntry.supported].sort();
|
|
258
|
+
if (!modelListsEqual(leftSupported, rightSupported)) {
|
|
259
|
+
return false;
|
|
260
|
+
}
|
|
261
|
+
if ((leftEntry.default ?? null) !== (rightEntry.default ?? null)) {
|
|
262
|
+
return false;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
return true;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function formatThinkingLevelLabel(level: ThinkingLevel): string {
|
|
269
|
+
if (level === 'off') {
|
|
270
|
+
return t('chatThinkingLevelOff');
|
|
271
|
+
}
|
|
272
|
+
if (level === 'minimal') {
|
|
273
|
+
return t('chatThinkingLevelMinimal');
|
|
274
|
+
}
|
|
275
|
+
if (level === 'low') {
|
|
276
|
+
return t('chatThinkingLevelLow');
|
|
277
|
+
}
|
|
278
|
+
if (level === 'medium') {
|
|
279
|
+
return t('chatThinkingLevelMedium');
|
|
280
|
+
}
|
|
281
|
+
if (level === 'high') {
|
|
282
|
+
return t('chatThinkingLevelHigh');
|
|
283
|
+
}
|
|
284
|
+
if (level === 'adaptive') {
|
|
285
|
+
return t('chatThinkingLevelAdaptive');
|
|
286
|
+
}
|
|
287
|
+
return t('chatThinkingLevelXhigh');
|
|
288
|
+
}
|
|
289
|
+
|
|
163
290
|
function resolvePreferredAuthMethodId(params: {
|
|
164
291
|
providerName?: string;
|
|
165
292
|
methods: ProviderAuthMethodOption[];
|
|
@@ -272,6 +399,7 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
|
|
|
272
399
|
const [extraHeaders, setExtraHeaders] = useState<Record<string, string> | null>(null);
|
|
273
400
|
const [wireApi, setWireApi] = useState<WireApiType>('auto');
|
|
274
401
|
const [models, setModels] = useState<string[]>([]);
|
|
402
|
+
const [modelThinking, setModelThinking] = useState<ModelThinkingConfig>({});
|
|
275
403
|
const [modelDraft, setModelDraft] = useState('');
|
|
276
404
|
const [providerDisplayName, setProviderDisplayName] = useState('');
|
|
277
405
|
const [showAdvanced, setShowAdvanced] = useState(false);
|
|
@@ -323,6 +451,14 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
|
|
|
323
451
|
() => resolveEditableModels(defaultModels, currentModels),
|
|
324
452
|
[defaultModels, currentModels]
|
|
325
453
|
);
|
|
454
|
+
const currentModelThinking = useMemo(
|
|
455
|
+
() =>
|
|
456
|
+
normalizeModelThinkingForModels(
|
|
457
|
+
normalizeModelThinkingConfig(resolvedProviderConfig.modelThinking, providerModelAliases),
|
|
458
|
+
currentEditableModels
|
|
459
|
+
),
|
|
460
|
+
[currentEditableModels, providerModelAliases, resolvedProviderConfig.modelThinking]
|
|
461
|
+
);
|
|
326
462
|
const language = getLanguage();
|
|
327
463
|
const apiBaseHelpText =
|
|
328
464
|
providerSpec?.apiBaseHelp?.[language] ||
|
|
@@ -443,6 +579,7 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
|
|
|
443
579
|
setExtraHeaders(null);
|
|
444
580
|
setWireApi('auto');
|
|
445
581
|
setModels([]);
|
|
582
|
+
setModelThinking({});
|
|
446
583
|
setModelDraft('');
|
|
447
584
|
setProviderDisplayName('');
|
|
448
585
|
setAuthSessionId(null);
|
|
@@ -457,6 +594,7 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
|
|
|
457
594
|
setExtraHeaders(resolvedProviderConfig.extraHeaders || null);
|
|
458
595
|
setWireApi(currentWireApi);
|
|
459
596
|
setModels(currentEditableModels);
|
|
597
|
+
setModelThinking(currentModelThinking);
|
|
460
598
|
setModelDraft('');
|
|
461
599
|
setProviderDisplayName(effectiveDisplayName);
|
|
462
600
|
setAuthSessionId(null);
|
|
@@ -469,6 +607,7 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
|
|
|
469
607
|
resolvedProviderConfig.extraHeaders,
|
|
470
608
|
currentWireApi,
|
|
471
609
|
currentEditableModels,
|
|
610
|
+
currentModelThinking,
|
|
472
611
|
effectiveDisplayName,
|
|
473
612
|
preferredAuthMethodId,
|
|
474
613
|
clearAuthPollTimer
|
|
@@ -476,6 +615,10 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
|
|
|
476
615
|
|
|
477
616
|
useEffect(() => () => clearAuthPollTimer(), [clearAuthPollTimer]);
|
|
478
617
|
|
|
618
|
+
useEffect(() => {
|
|
619
|
+
setModelThinking((prev) => normalizeModelThinkingForModels(prev, models));
|
|
620
|
+
}, [models]);
|
|
621
|
+
|
|
479
622
|
const hasChanges = useMemo(() => {
|
|
480
623
|
if (!providerName) {
|
|
481
624
|
return false;
|
|
@@ -485,11 +628,20 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
|
|
|
485
628
|
const headersChanged = !headersEqual(extraHeaders, currentHeaders);
|
|
486
629
|
const wireApiChanged = providerSpec?.supportsWireApi ? wireApi !== currentWireApi : false;
|
|
487
630
|
const modelsChanged = !modelListsEqual(models, currentEditableModels);
|
|
631
|
+
const modelThinkingChanged = !modelThinkingEqual(modelThinking, currentModelThinking);
|
|
488
632
|
const displayNameChanged = isCustomProvider
|
|
489
633
|
? providerDisplayName.trim() !== effectiveDisplayName
|
|
490
634
|
: false;
|
|
491
635
|
|
|
492
|
-
return
|
|
636
|
+
return (
|
|
637
|
+
apiKeyChanged ||
|
|
638
|
+
apiBaseChanged ||
|
|
639
|
+
headersChanged ||
|
|
640
|
+
wireApiChanged ||
|
|
641
|
+
modelsChanged ||
|
|
642
|
+
modelThinkingChanged ||
|
|
643
|
+
displayNameChanged
|
|
644
|
+
);
|
|
493
645
|
}, [
|
|
494
646
|
providerName,
|
|
495
647
|
isCustomProvider,
|
|
@@ -504,7 +656,9 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
|
|
|
504
656
|
wireApi,
|
|
505
657
|
currentWireApi,
|
|
506
658
|
models,
|
|
507
|
-
currentEditableModels
|
|
659
|
+
currentEditableModels,
|
|
660
|
+
modelThinking,
|
|
661
|
+
currentModelThinking
|
|
508
662
|
]);
|
|
509
663
|
|
|
510
664
|
const handleAddModel = () => {
|
|
@@ -520,6 +674,45 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
|
|
|
520
674
|
setModelDraft('');
|
|
521
675
|
};
|
|
522
676
|
|
|
677
|
+
const toggleModelThinkingLevel = (modelName: string, level: ThinkingLevel) => {
|
|
678
|
+
setModelThinking((prev) => {
|
|
679
|
+
const currentEntry = prev[modelName];
|
|
680
|
+
const currentLevels = currentEntry?.supported ?? [];
|
|
681
|
+
const nextLevels = currentLevels.includes(level)
|
|
682
|
+
? currentLevels.filter((item) => item !== level)
|
|
683
|
+
: THINKING_LEVELS.filter((item) => item === level || currentLevels.includes(item));
|
|
684
|
+
if (nextLevels.length === 0) {
|
|
685
|
+
const next = { ...prev };
|
|
686
|
+
delete next[modelName];
|
|
687
|
+
return next;
|
|
688
|
+
}
|
|
689
|
+
const nextDefault =
|
|
690
|
+
currentEntry?.default && nextLevels.includes(currentEntry.default) ? currentEntry.default : undefined;
|
|
691
|
+
return {
|
|
692
|
+
...prev,
|
|
693
|
+
[modelName]: nextDefault ? { supported: nextLevels, default: nextDefault } : { supported: nextLevels }
|
|
694
|
+
};
|
|
695
|
+
});
|
|
696
|
+
};
|
|
697
|
+
|
|
698
|
+
const setModelThinkingDefault = (modelName: string, level: ThinkingLevel | null) => {
|
|
699
|
+
setModelThinking((prev) => {
|
|
700
|
+
const currentEntry = prev[modelName];
|
|
701
|
+
if (!currentEntry || currentEntry.supported.length === 0) {
|
|
702
|
+
return prev;
|
|
703
|
+
}
|
|
704
|
+
if (level && !currentEntry.supported.includes(level)) {
|
|
705
|
+
return prev;
|
|
706
|
+
}
|
|
707
|
+
return {
|
|
708
|
+
...prev,
|
|
709
|
+
[modelName]: level
|
|
710
|
+
? { supported: currentEntry.supported, default: level }
|
|
711
|
+
: { supported: currentEntry.supported }
|
|
712
|
+
};
|
|
713
|
+
});
|
|
714
|
+
};
|
|
715
|
+
|
|
523
716
|
const handleSubmit = (e: React.FormEvent) => {
|
|
524
717
|
e.preventDefault();
|
|
525
718
|
|
|
@@ -555,6 +748,9 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
|
|
|
555
748
|
if (!modelListsEqual(models, currentEditableModels)) {
|
|
556
749
|
payload.models = serializeModelsForSave(models, defaultModels);
|
|
557
750
|
}
|
|
751
|
+
if (!modelThinkingEqual(modelThinking, currentModelThinking)) {
|
|
752
|
+
payload.modelThinking = normalizeModelThinkingForModels(modelThinking, models);
|
|
753
|
+
}
|
|
558
754
|
|
|
559
755
|
updateProvider.mutate({ provider: providerName, data: payload });
|
|
560
756
|
};
|
|
@@ -876,22 +1072,99 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
|
|
|
876
1072
|
</div>
|
|
877
1073
|
) : (
|
|
878
1074
|
<div className="flex flex-wrap gap-2">
|
|
879
|
-
{models.map((modelName) =>
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
<
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
onClick={() => setModels((prev) => prev.filter((name) => name !== modelName))}
|
|
888
|
-
className="inline-flex h-5 w-5 items-center justify-center rounded-full text-gray-400 transition-opacity hover:bg-gray-100 hover:text-gray-600 opacity-100 md:opacity-0 md:group-hover:opacity-100 md:group-focus-within:opacity-100"
|
|
889
|
-
aria-label={t('remove')}
|
|
1075
|
+
{models.map((modelName) => {
|
|
1076
|
+
const thinkingEntry = modelThinking[modelName];
|
|
1077
|
+
const supportedLevels = thinkingEntry?.supported ?? [];
|
|
1078
|
+
const defaultThinkingLevel = thinkingEntry?.default ?? null;
|
|
1079
|
+
return (
|
|
1080
|
+
<div
|
|
1081
|
+
key={modelName}
|
|
1082
|
+
className="group inline-flex max-w-full items-center gap-1 rounded-full border border-gray-200 bg-white px-3 py-1.5"
|
|
890
1083
|
>
|
|
891
|
-
<
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
1084
|
+
<span className="max-w-[140px] truncate text-sm text-gray-800 sm:max-w-[220px]">{modelName}</span>
|
|
1085
|
+
<Popover>
|
|
1086
|
+
<PopoverTrigger asChild>
|
|
1087
|
+
<button
|
|
1088
|
+
type="button"
|
|
1089
|
+
className="inline-flex h-5 w-5 items-center justify-center rounded-full text-gray-400 transition-opacity hover:bg-gray-100 hover:text-gray-600 opacity-100 md:opacity-0 md:group-hover:opacity-100 md:group-focus-within:opacity-100"
|
|
1090
|
+
aria-label={t('providerModelThinkingTitle')}
|
|
1091
|
+
title={t('providerModelThinkingTitle')}
|
|
1092
|
+
>
|
|
1093
|
+
<Settings2 className="h-3 w-3" />
|
|
1094
|
+
</button>
|
|
1095
|
+
</PopoverTrigger>
|
|
1096
|
+
<PopoverContent className="w-80 space-y-3">
|
|
1097
|
+
<div className="space-y-1">
|
|
1098
|
+
<p className="text-xs font-semibold text-gray-800">{t('providerModelThinkingTitle')}</p>
|
|
1099
|
+
<p className="text-xs text-gray-500">{t('providerModelThinkingHint')}</p>
|
|
1100
|
+
</div>
|
|
1101
|
+
<div className="flex flex-wrap gap-1.5">
|
|
1102
|
+
{THINKING_LEVELS.map((level) => {
|
|
1103
|
+
const selected = supportedLevels.includes(level);
|
|
1104
|
+
return (
|
|
1105
|
+
<button
|
|
1106
|
+
key={level}
|
|
1107
|
+
type="button"
|
|
1108
|
+
onClick={() => toggleModelThinkingLevel(modelName, level)}
|
|
1109
|
+
className={`rounded-full border px-2.5 py-1 text-xs font-medium transition-colors ${
|
|
1110
|
+
selected
|
|
1111
|
+
? 'border-primary bg-primary text-white'
|
|
1112
|
+
: 'border-gray-200 bg-white text-gray-600 hover:border-primary/40 hover:text-primary'
|
|
1113
|
+
}`}
|
|
1114
|
+
>
|
|
1115
|
+
{formatThinkingLevelLabel(level)}
|
|
1116
|
+
</button>
|
|
1117
|
+
);
|
|
1118
|
+
})}
|
|
1119
|
+
</div>
|
|
1120
|
+
<div className="space-y-1.5">
|
|
1121
|
+
<Label className="text-xs font-medium text-gray-700">{t('providerModelThinkingDefault')}</Label>
|
|
1122
|
+
<Select
|
|
1123
|
+
value={defaultThinkingLevel ?? '__none__'}
|
|
1124
|
+
onValueChange={(value) =>
|
|
1125
|
+
setModelThinkingDefault(
|
|
1126
|
+
modelName,
|
|
1127
|
+
value === '__none__' ? null : (value as ThinkingLevel)
|
|
1128
|
+
)
|
|
1129
|
+
}
|
|
1130
|
+
disabled={supportedLevels.length === 0}
|
|
1131
|
+
>
|
|
1132
|
+
<SelectTrigger className="h-8 rounded-lg bg-white text-xs">
|
|
1133
|
+
<SelectValue />
|
|
1134
|
+
</SelectTrigger>
|
|
1135
|
+
<SelectContent>
|
|
1136
|
+
<SelectItem value="__none__">{t('providerModelThinkingDefaultNone')}</SelectItem>
|
|
1137
|
+
{supportedLevels.map((level) => (
|
|
1138
|
+
<SelectItem key={level} value={level}>
|
|
1139
|
+
{formatThinkingLevelLabel(level)}
|
|
1140
|
+
</SelectItem>
|
|
1141
|
+
))}
|
|
1142
|
+
</SelectContent>
|
|
1143
|
+
</Select>
|
|
1144
|
+
{supportedLevels.length === 0 ? (
|
|
1145
|
+
<p className="text-xs text-gray-500">{t('providerModelThinkingNoSupported')}</p>
|
|
1146
|
+
) : null}
|
|
1147
|
+
</div>
|
|
1148
|
+
</PopoverContent>
|
|
1149
|
+
</Popover>
|
|
1150
|
+
<button
|
|
1151
|
+
type="button"
|
|
1152
|
+
onClick={() => {
|
|
1153
|
+
setModels((prev) => prev.filter((name) => name !== modelName));
|
|
1154
|
+
setModelThinking((prev) => {
|
|
1155
|
+
const next = { ...prev };
|
|
1156
|
+
delete next[modelName];
|
|
1157
|
+
return next;
|
|
1158
|
+
});
|
|
1159
|
+
}}
|
|
1160
|
+
className="inline-flex h-5 w-5 items-center justify-center rounded-full text-gray-400 transition-opacity hover:bg-gray-100 hover:text-gray-600 opacity-100 md:opacity-0 md:group-hover:opacity-100 md:group-focus-within:opacity-100"
|
|
1161
|
+
aria-label={t('remove')}
|
|
1162
|
+
>
|
|
1163
|
+
<X className="h-3 w-3" />
|
|
1164
|
+
</button>
|
|
1165
|
+
</div>
|
|
1166
|
+
);
|
|
1167
|
+
})}
|
|
895
1168
|
</div>
|
|
896
1169
|
)}
|
|
897
1170
|
</div>
|