@nucleux/model-selection 0.0.0-stage → 0.7.0
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/LICENSE +21 -0
- package/README.md +44 -2
- package/dist/index.cjs +257 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +63 -0
- package/dist/index.d.ts +63 -0
- package/dist/index.js +235 -0
- package/dist/index.js.map +1 -0
- package/package.json +58 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Nucleux
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,45 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @nucleux/model-selection
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
ModelSelection — choose which model handles the generation — Nucleux agentic UI for React.
|
|
4
|
+
|
|
5
|
+
Part of **[Nucleux](https://github.com/intelligaia-open-labs/nucleux)** — agentic UI components for React. Install a single component, or the full kit via [`@nucleux/react`](https://www.npmjs.com/package/@nucleux/react).
|
|
6
|
+
|
|
7
|
+
## Exports
|
|
8
|
+
|
|
9
|
+
- **`ModelSelection`** — lets the user pick the model for the task. Props: `models`, `value` / `defaultValue` / `onValueChange`, `label`, and
|
|
10
|
+
- `variant`: `"dropdown"` (default — a composer-anchored listbox: arrow keys, Home/End, Escape returns focus to the trigger), `"tradeoff"` (radio cards with speed, quality and cost in human terms), `"auto-route"` (on by default — "Aria picks the model per task", shows `routedModel`; switching it off reveals the dropdown)
|
|
11
|
+
- `autoRoute` / `defaultAutoRoute` (true) / `onAutoRouteChange`, `routedModel`, `agentName`
|
|
12
|
+
- **`ModelOption`**: `value`, `label`, `description`, `icon`, plus `recommended` (a "Recommended" badge), `speed`, `quality`, `cost` (human terms, never parameter names), `disabled` with `unavailableReason` (the row stays and says what answers instead).
|
|
13
|
+
- Types: `ModelSelectionProps`, `ModelSelectionVariant`, `ModelOption`.
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pnpm add @nucleux/model-selection @nucleux/tokens
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
`react` / `react-dom` are peer dependencies.
|
|
22
|
+
|
|
23
|
+
## Usage
|
|
24
|
+
|
|
25
|
+
```tsx
|
|
26
|
+
import { ModelSelection } from "@nucleux/model-selection";
|
|
27
|
+
|
|
28
|
+
<ModelSelection
|
|
29
|
+
variant="tradeoff"
|
|
30
|
+
defaultValue="sonnet"
|
|
31
|
+
models={[
|
|
32
|
+
{ value: "opus", label: "Opus", quality: "Best quality", speed: "Slower", cost: "$$$" },
|
|
33
|
+
{ value: "sonnet", label: "Sonnet", quality: "Balanced", speed: "Medium", cost: "$$", recommended: true },
|
|
34
|
+
{ value: "haiku", label: "Haiku", quality: "Good enough", speed: "Fastest", cost: "$" },
|
|
35
|
+
]}
|
|
36
|
+
onValueChange={setModel}
|
|
37
|
+
/>
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Requires the theme once in your app: `import "@nucleux/tokens/styles.css";`
|
|
41
|
+
and the Tailwind preset from `@nucleux/tokens/preset`.
|
|
42
|
+
|
|
43
|
+
## License
|
|
44
|
+
|
|
45
|
+
MIT © Nucleux
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
var React = require('react');
|
|
5
|
+
var utils = require('@nucleux/utils');
|
|
6
|
+
var badge = require('@nucleux/badge');
|
|
7
|
+
var radioGroup = require('@nucleux/radio-group');
|
|
8
|
+
var _switch = require('@nucleux/switch');
|
|
9
|
+
var lucideReact = require('lucide-react');
|
|
10
|
+
var jsxRuntime = require('react/jsx-runtime');
|
|
11
|
+
|
|
12
|
+
function _interopNamespace(e) {
|
|
13
|
+
if (e && e.__esModule) return e;
|
|
14
|
+
var n = Object.create(null);
|
|
15
|
+
if (e) {
|
|
16
|
+
Object.keys(e).forEach(function (k) {
|
|
17
|
+
if (k !== 'default') {
|
|
18
|
+
var d = Object.getOwnPropertyDescriptor(e, k);
|
|
19
|
+
Object.defineProperty(n, k, d.get ? d : {
|
|
20
|
+
enumerable: true,
|
|
21
|
+
get: function () { return e[k]; }
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
n.default = e;
|
|
27
|
+
return Object.freeze(n);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
var React__namespace = /*#__PURE__*/_interopNamespace(React);
|
|
31
|
+
|
|
32
|
+
// src/index.tsx
|
|
33
|
+
function tradeoffLine(m) {
|
|
34
|
+
const parts = [m.speed, m.quality].filter((p) => p != null && p !== "");
|
|
35
|
+
if (!parts.length) return null;
|
|
36
|
+
return parts.map((p, i) => /* @__PURE__ */ jsxRuntime.jsxs(React__namespace.Fragment, { children: [
|
|
37
|
+
i > 0 && " \xB7 ",
|
|
38
|
+
p
|
|
39
|
+
] }, i));
|
|
40
|
+
}
|
|
41
|
+
var RecommendedBadge = () => /* @__PURE__ */ jsxRuntime.jsx(badge.Badge, { variant: "outline", className: "border-brand/40 px-1.5 py-0 text-[10px] uppercase tracking-wide text-brand", children: "Recommended" });
|
|
42
|
+
function ModelDropdown({ models, current, select, label, className }) {
|
|
43
|
+
const [open, setOpen] = React__namespace.useState(false);
|
|
44
|
+
const rootRef = React__namespace.useRef(null);
|
|
45
|
+
const triggerRef = React__namespace.useRef(null);
|
|
46
|
+
const listRef = React__namespace.useRef(null);
|
|
47
|
+
const listId = React__namespace.useId();
|
|
48
|
+
const close = React__namespace.useCallback((refocus) => {
|
|
49
|
+
setOpen(false);
|
|
50
|
+
if (refocus) triggerRef.current?.focus();
|
|
51
|
+
}, []);
|
|
52
|
+
React__namespace.useEffect(() => {
|
|
53
|
+
if (!open) return;
|
|
54
|
+
const onDown = (e) => {
|
|
55
|
+
if (rootRef.current && !rootRef.current.contains(e.target)) setOpen(false);
|
|
56
|
+
};
|
|
57
|
+
const onKey = (e) => e.key === "Escape" && close(true);
|
|
58
|
+
document.addEventListener("mousedown", onDown);
|
|
59
|
+
document.addEventListener("keydown", onKey);
|
|
60
|
+
return () => {
|
|
61
|
+
document.removeEventListener("mousedown", onDown);
|
|
62
|
+
document.removeEventListener("keydown", onKey);
|
|
63
|
+
};
|
|
64
|
+
}, [open, close]);
|
|
65
|
+
const options = () => Array.from(listRef.current?.querySelectorAll('[role="option"]:not([aria-disabled="true"])') ?? []);
|
|
66
|
+
React__namespace.useEffect(() => {
|
|
67
|
+
if (!open) return;
|
|
68
|
+
const all = options();
|
|
69
|
+
(all.find((o) => o.getAttribute("aria-selected") === "true") ?? all[0])?.focus();
|
|
70
|
+
}, [open]);
|
|
71
|
+
const onListKeyDown = (e) => {
|
|
72
|
+
const all = options();
|
|
73
|
+
const i = all.indexOf(document.activeElement);
|
|
74
|
+
const go = (n) => all[(n + all.length) % all.length]?.focus();
|
|
75
|
+
if (e.key === "ArrowDown") e.preventDefault(), go(i + 1);
|
|
76
|
+
else if (e.key === "ArrowUp") e.preventDefault(), go(i - 1);
|
|
77
|
+
else if (e.key === "Home") e.preventDefault(), go(0);
|
|
78
|
+
else if (e.key === "End") e.preventDefault(), go(all.length - 1);
|
|
79
|
+
else if (e.key === "Tab") close(false);
|
|
80
|
+
};
|
|
81
|
+
const selected = models.find((m) => m.value === current);
|
|
82
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { ref: rootRef, className: utils.cn("relative inline-block", className), children: [
|
|
83
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
84
|
+
"button",
|
|
85
|
+
{
|
|
86
|
+
ref: triggerRef,
|
|
87
|
+
type: "button",
|
|
88
|
+
"aria-label": label,
|
|
89
|
+
"aria-haspopup": "listbox",
|
|
90
|
+
"aria-expanded": open,
|
|
91
|
+
"aria-controls": open ? listId : void 0,
|
|
92
|
+
onClick: () => setOpen((o) => !o),
|
|
93
|
+
onKeyDown: (e) => {
|
|
94
|
+
if ((e.key === "ArrowDown" || e.key === "ArrowUp") && !open) {
|
|
95
|
+
e.preventDefault();
|
|
96
|
+
setOpen(true);
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
className: "inline-flex items-center gap-2 rounded-lg border border-border bg-background px-3 py-2 text-sm font-medium text-foreground transition-colors hover:bg-accent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring [&>svg]:size-4",
|
|
100
|
+
children: [
|
|
101
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Sparkles, { className: "text-brand", "aria-hidden": "true" }),
|
|
102
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { children: selected?.label ?? "Select model" }),
|
|
103
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.ChevronDown, { className: utils.cn("text-muted-foreground transition-transform", open && "rotate-180"), "aria-hidden": "true" })
|
|
104
|
+
]
|
|
105
|
+
}
|
|
106
|
+
),
|
|
107
|
+
open && /* @__PURE__ */ jsxRuntime.jsx(
|
|
108
|
+
"div",
|
|
109
|
+
{
|
|
110
|
+
ref: listRef,
|
|
111
|
+
id: listId,
|
|
112
|
+
role: "listbox",
|
|
113
|
+
"aria-label": label,
|
|
114
|
+
onKeyDown: onListKeyDown,
|
|
115
|
+
className: "absolute z-50 mt-1 flex min-w-64 flex-col gap-0.5 rounded-xl border border-border bg-background p-1.5 shadow-md animate-nx-fade-in motion-reduce:animate-none",
|
|
116
|
+
children: models.map((model) => {
|
|
117
|
+
const active = model.value === current;
|
|
118
|
+
const line = tradeoffLine(model);
|
|
119
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
120
|
+
"button",
|
|
121
|
+
{
|
|
122
|
+
type: "button",
|
|
123
|
+
role: "option",
|
|
124
|
+
"aria-selected": active,
|
|
125
|
+
"aria-disabled": model.disabled || void 0,
|
|
126
|
+
tabIndex: -1,
|
|
127
|
+
onClick: () => {
|
|
128
|
+
if (model.disabled) return;
|
|
129
|
+
select(model.value);
|
|
130
|
+
close(true);
|
|
131
|
+
},
|
|
132
|
+
className: utils.cn(
|
|
133
|
+
"flex items-start gap-2.5 rounded-lg px-2 py-2 text-left transition-colors focus-visible:outline-none",
|
|
134
|
+
model.disabled ? "cursor-not-allowed opacity-60" : "hover:bg-accent focus-visible:bg-accent",
|
|
135
|
+
active && "bg-brand-muted/40"
|
|
136
|
+
),
|
|
137
|
+
children: [
|
|
138
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "mt-0.5 inline-flex size-4 shrink-0 items-center justify-center text-brand [&_svg]:size-4", children: active ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Check, { "aria-hidden": "true" }) : model.icon }),
|
|
139
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex min-w-0 flex-1 flex-col", children: [
|
|
140
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex items-center gap-2 text-sm font-medium text-foreground", children: [
|
|
141
|
+
model.label,
|
|
142
|
+
model.recommended && /* @__PURE__ */ jsxRuntime.jsx(RecommendedBadge, {})
|
|
143
|
+
] }),
|
|
144
|
+
model.description && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-muted-foreground", children: model.description }),
|
|
145
|
+
line && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-muted-foreground", children: line }),
|
|
146
|
+
model.disabled && model.unavailableReason && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-foreground", children: model.unavailableReason })
|
|
147
|
+
] }),
|
|
148
|
+
model.cost && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "shrink-0 text-xs font-semibold text-muted-foreground", children: model.cost })
|
|
149
|
+
]
|
|
150
|
+
},
|
|
151
|
+
model.value
|
|
152
|
+
);
|
|
153
|
+
})
|
|
154
|
+
}
|
|
155
|
+
)
|
|
156
|
+
] });
|
|
157
|
+
}
|
|
158
|
+
var ModelSelection = React__namespace.forwardRef(
|
|
159
|
+
({
|
|
160
|
+
models,
|
|
161
|
+
value,
|
|
162
|
+
defaultValue,
|
|
163
|
+
onValueChange,
|
|
164
|
+
label = "Model",
|
|
165
|
+
variant = "dropdown",
|
|
166
|
+
autoRoute,
|
|
167
|
+
defaultAutoRoute = true,
|
|
168
|
+
onAutoRouteChange,
|
|
169
|
+
routedModel,
|
|
170
|
+
agentName = "Aria",
|
|
171
|
+
className,
|
|
172
|
+
...props
|
|
173
|
+
}, ref) => {
|
|
174
|
+
const [internal, setInternal] = React__namespace.useState(defaultValue ?? models[0]?.value);
|
|
175
|
+
const current = value !== void 0 ? value : internal;
|
|
176
|
+
const select = (next) => {
|
|
177
|
+
if (value === void 0) setInternal(next);
|
|
178
|
+
onValueChange?.(next);
|
|
179
|
+
};
|
|
180
|
+
const [autoInternal, setAutoInternal] = React__namespace.useState(defaultAutoRoute);
|
|
181
|
+
const auto = autoRoute !== void 0 ? autoRoute : autoInternal;
|
|
182
|
+
const setAuto = (on) => {
|
|
183
|
+
if (autoRoute === void 0) setAutoInternal(on);
|
|
184
|
+
onAutoRouteChange?.(on);
|
|
185
|
+
};
|
|
186
|
+
const autoLabelId = React__namespace.useId();
|
|
187
|
+
const headingId = React__namespace.useId();
|
|
188
|
+
if (variant === "tradeoff") {
|
|
189
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { ref, className: utils.cn("flex flex-col gap-2", className), ...props, children: [
|
|
190
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { id: headingId, className: "sr-only", children: label }),
|
|
191
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
192
|
+
radioGroup.RadioGroup,
|
|
193
|
+
{
|
|
194
|
+
"aria-labelledby": headingId,
|
|
195
|
+
value: current,
|
|
196
|
+
onValueChange: select,
|
|
197
|
+
className: "gap-1.5",
|
|
198
|
+
children: models.map((model) => {
|
|
199
|
+
const id = `${headingId}-${model.value}`;
|
|
200
|
+
const line = tradeoffLine(model);
|
|
201
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
202
|
+
"label",
|
|
203
|
+
{
|
|
204
|
+
className: utils.cn(
|
|
205
|
+
"flex cursor-pointer items-center gap-3 rounded-lg border border-border bg-background px-3 py-2 transition-colors hover:bg-accent",
|
|
206
|
+
"has-[[aria-checked=true]]:border-brand has-[[aria-checked=true]]:bg-brand-muted/40",
|
|
207
|
+
model.disabled && "cursor-not-allowed opacity-60 hover:bg-background"
|
|
208
|
+
),
|
|
209
|
+
children: [
|
|
210
|
+
/* @__PURE__ */ jsxRuntime.jsx(radioGroup.Radio, { value: model.value, disabled: model.disabled, "aria-labelledby": id }),
|
|
211
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { id, className: "flex min-w-0 flex-1 flex-col", children: [
|
|
212
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex items-center gap-2 text-sm font-semibold text-foreground", children: [
|
|
213
|
+
model.label,
|
|
214
|
+
model.recommended && /* @__PURE__ */ jsxRuntime.jsx(RecommendedBadge, {})
|
|
215
|
+
] }),
|
|
216
|
+
(line ?? model.description) && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-muted-foreground", children: line ?? model.description }),
|
|
217
|
+
model.disabled && model.unavailableReason && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-foreground", children: model.unavailableReason })
|
|
218
|
+
] }),
|
|
219
|
+
model.cost && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "shrink-0 text-xs font-semibold text-muted-foreground", children: model.cost })
|
|
220
|
+
]
|
|
221
|
+
},
|
|
222
|
+
model.value
|
|
223
|
+
);
|
|
224
|
+
})
|
|
225
|
+
}
|
|
226
|
+
)
|
|
227
|
+
] });
|
|
228
|
+
}
|
|
229
|
+
if (variant === "auto-route") {
|
|
230
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { ref, className: utils.cn("flex flex-col gap-2", className), ...props, children: [
|
|
231
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between gap-3 rounded-lg border border-border bg-background px-3.5 py-2.5", children: [
|
|
232
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex min-w-0 flex-col", children: [
|
|
233
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { id: autoLabelId, className: "text-sm font-semibold text-foreground", children: "Auto-route" }),
|
|
234
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-xs text-muted-foreground", children: [
|
|
235
|
+
agentName,
|
|
236
|
+
" picks the model per task"
|
|
237
|
+
] })
|
|
238
|
+
] }),
|
|
239
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex shrink-0 items-center gap-2", children: [
|
|
240
|
+
auto && routedModel && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-xs text-muted-foreground", "aria-live": "polite", children: [
|
|
241
|
+
"currently: ",
|
|
242
|
+
routedModel
|
|
243
|
+
] }),
|
|
244
|
+
/* @__PURE__ */ jsxRuntime.jsx(_switch.Switch, { checked: auto, onCheckedChange: setAuto, "aria-labelledby": autoLabelId })
|
|
245
|
+
] })
|
|
246
|
+
] }),
|
|
247
|
+
!auto && /* @__PURE__ */ jsxRuntime.jsx(ModelDropdown, { models, current, select, label })
|
|
248
|
+
] });
|
|
249
|
+
}
|
|
250
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { ref, className: utils.cn("relative inline-block", className), ...props, children: /* @__PURE__ */ jsxRuntime.jsx(ModelDropdown, { models, current, select, label }) });
|
|
251
|
+
}
|
|
252
|
+
);
|
|
253
|
+
ModelSelection.displayName = "ModelSelection";
|
|
254
|
+
|
|
255
|
+
exports.ModelSelection = ModelSelection;
|
|
256
|
+
//# sourceMappingURL=index.cjs.map
|
|
257
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.tsx"],"names":["jsxs","React","jsx","Badge","cn","Sparkles","ChevronDown","Check","RadioGroup","Radio","Switch"],"mappings":"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","file":"index.cjs","sourcesContent":["import * as React from \"react\";\nimport { cn } from \"@nucleux/utils\";\nimport { Badge } from \"@nucleux/badge\";\nimport { Radio, RadioGroup } from \"@nucleux/radio-group\";\nimport { Switch } from \"@nucleux/switch\";\nimport { Check, ChevronDown, Sparkles } from \"lucide-react\";\n\n/** Layout from the spec: a compact dropdown, tradeoff cards, or an auto-route toggle that reveals the picker. */\nexport type ModelSelectionVariant = \"dropdown\" | \"tradeoff\" | \"auto-route\";\n\nexport interface ModelOption {\n /** Stable value passed to `onValueChange`. */\n value: string;\n /** Display label, e.g. `\"Nebula 5.5 Instant\"`. */\n label: React.ReactNode;\n /** Supporting line under the label — what the model is *for*, e.g. `\"Everyday tasks\"`. */\n description?: React.ReactNode;\n /** Optional leading icon. */\n icon?: React.ReactNode;\n /** Marks the suggested default with a \"Recommended\" badge. */\n recommended?: boolean;\n /** Speed in human terms, e.g. `\"Fastest\"`. */\n speed?: React.ReactNode;\n /** Quality in human terms, e.g. `\"Best quality\"`. */\n quality?: React.ReactNode;\n /** Cost in human terms, e.g. `\"$$\"` or `\"Uses more of your allowance\"`. */\n cost?: React.ReactNode;\n /** The option can't be chosen right now (allowance used up, not in your plan). The row stays. */\n disabled?: boolean;\n /** Why it is disabled and what answers instead, e.g. `\"Limit reached until 3 pm — Sonnet answers instead\"`. */\n unavailableReason?: React.ReactNode;\n}\n\nexport interface ModelSelectionProps\n extends Omit<React.HTMLAttributes<HTMLDivElement>, \"onChange\"> {\n /** The available models. */\n models: ModelOption[];\n /** Selected value (controlled). */\n value?: string;\n /** Initial value (uncontrolled). */\n defaultValue?: string;\n /** Called when a model is chosen. */\n onValueChange?: (value: string) => void;\n /** Accessible label for the trigger. Defaults to `\"Model\"`. */\n label?: string;\n /** Layout. Defaults to `\"dropdown\"`. */\n variant?: ModelSelectionVariant;\n /** Auto-route on (controlled), for `variant=\"auto-route\"`. */\n autoRoute?: boolean;\n /** Initial auto-route state (uncontrolled). Defaults to `true`: auto-route is the default. */\n defaultAutoRoute?: boolean;\n /** Called when auto-route is switched. */\n onAutoRouteChange?: (on: boolean) => void;\n /** The model auto-route is using now, shown as \"currently: …\". */\n routedModel?: React.ReactNode;\n /** Who routes, for the description. Defaults to `\"Aria\"`. */\n agentName?: React.ReactNode;\n}\n\n/** \"Fastest · Best quality · $$\" from whichever tradeoff facts are given. */\nfunction tradeoffLine(m: ModelOption): React.ReactNode {\n const parts = [m.speed, m.quality].filter((p) => p != null && p !== \"\");\n if (!parts.length) return null;\n return parts.map((p, i) => (\n <React.Fragment key={i}>\n {i > 0 && \" · \"}\n {p}\n </React.Fragment>\n ));\n}\n\nconst RecommendedBadge = () => (\n <Badge variant=\"outline\" className=\"border-brand/40 px-1.5 py-0 text-[10px] uppercase tracking-wide text-brand\">\n Recommended\n </Badge>\n);\n\ninterface PickerProps {\n models: ModelOption[];\n current: string | undefined;\n select: (value: string) => void;\n label: string;\n className?: string;\n}\n\n/** The composer-anchored dropdown (listbox) used by the \"dropdown\" and \"auto-route\" variants. */\nfunction ModelDropdown({ models, current, select, label, className }: PickerProps) {\n const [open, setOpen] = React.useState(false);\n const rootRef = React.useRef<HTMLDivElement>(null);\n const triggerRef = React.useRef<HTMLButtonElement>(null);\n const listRef = React.useRef<HTMLDivElement>(null);\n const listId = React.useId();\n\n const close = React.useCallback((refocus: boolean) => {\n setOpen(false);\n if (refocus) triggerRef.current?.focus();\n }, []);\n\n React.useEffect(() => {\n if (!open) return;\n const onDown = (e: MouseEvent) => {\n if (rootRef.current && !rootRef.current.contains(e.target as Node)) setOpen(false);\n };\n const onKey = (e: KeyboardEvent) => e.key === \"Escape\" && close(true);\n document.addEventListener(\"mousedown\", onDown);\n document.addEventListener(\"keydown\", onKey);\n return () => {\n document.removeEventListener(\"mousedown\", onDown);\n document.removeEventListener(\"keydown\", onKey);\n };\n }, [open, close]);\n\n const options = () =>\n Array.from(listRef.current?.querySelectorAll<HTMLElement>('[role=\"option\"]:not([aria-disabled=\"true\"])') ?? []);\n\n // Focus the selected option (or the first available) when the list opens.\n React.useEffect(() => {\n if (!open) return;\n const all = options();\n (all.find((o) => o.getAttribute(\"aria-selected\") === \"true\") ?? all[0])?.focus();\n }, [open]);\n\n const onListKeyDown = (e: React.KeyboardEvent) => {\n const all = options();\n const i = all.indexOf(document.activeElement as HTMLElement);\n const go = (n: number) => all[(n + all.length) % all.length]?.focus();\n if (e.key === \"ArrowDown\") (e.preventDefault(), go(i + 1));\n else if (e.key === \"ArrowUp\") (e.preventDefault(), go(i - 1));\n else if (e.key === \"Home\") (e.preventDefault(), go(0));\n else if (e.key === \"End\") (e.preventDefault(), go(all.length - 1));\n else if (e.key === \"Tab\") close(false);\n };\n\n const selected = models.find((m) => m.value === current);\n\n return (\n <div ref={rootRef} className={cn(\"relative inline-block\", className)}>\n <button\n ref={triggerRef}\n type=\"button\"\n aria-label={label}\n aria-haspopup=\"listbox\"\n aria-expanded={open}\n aria-controls={open ? listId : undefined}\n onClick={() => setOpen((o) => !o)}\n onKeyDown={(e) => {\n if ((e.key === \"ArrowDown\" || e.key === \"ArrowUp\") && !open) {\n e.preventDefault();\n setOpen(true);\n }\n }}\n className=\"inline-flex items-center gap-2 rounded-lg border border-border bg-background px-3 py-2 text-sm font-medium text-foreground transition-colors hover:bg-accent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring [&>svg]:size-4\"\n >\n <Sparkles className=\"text-brand\" aria-hidden=\"true\" />\n <span>{selected?.label ?? \"Select model\"}</span>\n <ChevronDown className={cn(\"text-muted-foreground transition-transform\", open && \"rotate-180\")} aria-hidden=\"true\" />\n </button>\n {open && (\n <div\n ref={listRef}\n id={listId}\n role=\"listbox\"\n aria-label={label}\n onKeyDown={onListKeyDown}\n className=\"absolute z-50 mt-1 flex min-w-64 flex-col gap-0.5 rounded-xl border border-border bg-background p-1.5 shadow-md animate-nx-fade-in motion-reduce:animate-none\"\n >\n {models.map((model) => {\n const active = model.value === current;\n const line = tradeoffLine(model);\n return (\n <button\n key={model.value}\n type=\"button\"\n role=\"option\"\n aria-selected={active}\n aria-disabled={model.disabled || undefined}\n tabIndex={-1}\n onClick={() => {\n if (model.disabled) return;\n select(model.value);\n close(true);\n }}\n className={cn(\n \"flex items-start gap-2.5 rounded-lg px-2 py-2 text-left transition-colors focus-visible:outline-none\",\n model.disabled ? \"cursor-not-allowed opacity-60\" : \"hover:bg-accent focus-visible:bg-accent\",\n active && \"bg-brand-muted/40\",\n )}\n >\n <span className=\"mt-0.5 inline-flex size-4 shrink-0 items-center justify-center text-brand [&_svg]:size-4\">\n {active ? <Check aria-hidden=\"true\" /> : model.icon}\n </span>\n <span className=\"flex min-w-0 flex-1 flex-col\">\n <span className=\"flex items-center gap-2 text-sm font-medium text-foreground\">\n {model.label}\n {model.recommended && <RecommendedBadge />}\n </span>\n {model.description && <span className=\"text-xs text-muted-foreground\">{model.description}</span>}\n {line && <span className=\"text-xs text-muted-foreground\">{line}</span>}\n {model.disabled && model.unavailableReason && (\n <span className=\"text-xs text-foreground\">{model.unavailableReason}</span>\n )}\n </span>\n {model.cost && <span className=\"shrink-0 text-xs font-semibold text-muted-foreground\">{model.cost}</span>}\n </button>\n );\n })}\n </div>\n )}\n </div>\n );\n}\n\n/**\n * Lets the user pick the model for the task. `variant=\"dropdown\"` (default) is\n * a composer-anchored listbox (arrow keys, Home/End, Escape returns focus);\n * `\"tradeoff\"` lays models out as radio cards with speed, quality and cost in\n * human terms; `\"auto-route\"` defaults to letting the agent route per task,\n * shows which model it is using, and reveals the picker when switched off.\n * Recommended and unavailable models are marked in words; an unavailable model\n * keeps its row and says what answers instead.\n * Source: Figma \"Context Expansion / Model Selection\".\n */\nexport const ModelSelection = React.forwardRef<HTMLDivElement, ModelSelectionProps>(\n (\n {\n models,\n value,\n defaultValue,\n onValueChange,\n label = \"Model\",\n variant = \"dropdown\",\n autoRoute,\n defaultAutoRoute = true,\n onAutoRouteChange,\n routedModel,\n agentName = \"Aria\",\n className,\n ...props\n },\n ref,\n ) => {\n const [internal, setInternal] = React.useState<string | undefined>(defaultValue ?? models[0]?.value);\n const current = value !== undefined ? value : internal;\n const select = (next: string) => {\n if (value === undefined) setInternal(next);\n onValueChange?.(next);\n };\n\n const [autoInternal, setAutoInternal] = React.useState(defaultAutoRoute);\n const auto = autoRoute !== undefined ? autoRoute : autoInternal;\n const setAuto = (on: boolean) => {\n if (autoRoute === undefined) setAutoInternal(on);\n onAutoRouteChange?.(on);\n };\n const autoLabelId = React.useId();\n const headingId = React.useId();\n\n if (variant === \"tradeoff\") {\n return (\n <div ref={ref} className={cn(\"flex flex-col gap-2\", className)} {...props}>\n <p id={headingId} className=\"sr-only\">\n {label}\n </p>\n <RadioGroup\n aria-labelledby={headingId}\n value={current}\n onValueChange={select}\n className=\"gap-1.5\"\n >\n {models.map((model) => {\n const id = `${headingId}-${model.value}`;\n const line = tradeoffLine(model);\n return (\n <label\n key={model.value}\n className={cn(\n \"flex cursor-pointer items-center gap-3 rounded-lg border border-border bg-background px-3 py-2 transition-colors hover:bg-accent\",\n \"has-[[aria-checked=true]]:border-brand has-[[aria-checked=true]]:bg-brand-muted/40\",\n model.disabled && \"cursor-not-allowed opacity-60 hover:bg-background\",\n )}\n >\n <Radio value={model.value} disabled={model.disabled} aria-labelledby={id} />\n <span id={id} className=\"flex min-w-0 flex-1 flex-col\">\n <span className=\"flex items-center gap-2 text-sm font-semibold text-foreground\">\n {model.label}\n {model.recommended && <RecommendedBadge />}\n </span>\n {(line ?? model.description) && (\n <span className=\"text-xs text-muted-foreground\">{line ?? model.description}</span>\n )}\n {model.disabled && model.unavailableReason && (\n <span className=\"text-xs text-foreground\">{model.unavailableReason}</span>\n )}\n </span>\n {model.cost && (\n <span className=\"shrink-0 text-xs font-semibold text-muted-foreground\">{model.cost}</span>\n )}\n </label>\n );\n })}\n </RadioGroup>\n </div>\n );\n }\n\n if (variant === \"auto-route\") {\n return (\n <div ref={ref} className={cn(\"flex flex-col gap-2\", className)} {...props}>\n <div className=\"flex items-center justify-between gap-3 rounded-lg border border-border bg-background px-3.5 py-2.5\">\n <div className=\"flex min-w-0 flex-col\">\n <span id={autoLabelId} className=\"text-sm font-semibold text-foreground\">\n Auto-route\n </span>\n <span className=\"text-xs text-muted-foreground\">{agentName} picks the model per task</span>\n </div>\n <div className=\"flex shrink-0 items-center gap-2\">\n {auto && routedModel && (\n <span className=\"text-xs text-muted-foreground\" aria-live=\"polite\">\n currently: {routedModel}\n </span>\n )}\n <Switch checked={auto} onCheckedChange={setAuto} aria-labelledby={autoLabelId} />\n </div>\n </div>\n {!auto && <ModelDropdown models={models} current={current} select={select} label={label} />}\n </div>\n );\n }\n\n return (\n <div ref={ref} className={cn(\"relative inline-block\", className)} {...props}>\n <ModelDropdown models={models} current={current} select={select} label={label} />\n </div>\n );\n },\n);\nModelSelection.displayName = \"ModelSelection\";\n"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
/** Layout from the spec: a compact dropdown, tradeoff cards, or an auto-route toggle that reveals the picker. */
|
|
4
|
+
type ModelSelectionVariant = "dropdown" | "tradeoff" | "auto-route";
|
|
5
|
+
interface ModelOption {
|
|
6
|
+
/** Stable value passed to `onValueChange`. */
|
|
7
|
+
value: string;
|
|
8
|
+
/** Display label, e.g. `"Nebula 5.5 Instant"`. */
|
|
9
|
+
label: React.ReactNode;
|
|
10
|
+
/** Supporting line under the label — what the model is *for*, e.g. `"Everyday tasks"`. */
|
|
11
|
+
description?: React.ReactNode;
|
|
12
|
+
/** Optional leading icon. */
|
|
13
|
+
icon?: React.ReactNode;
|
|
14
|
+
/** Marks the suggested default with a "Recommended" badge. */
|
|
15
|
+
recommended?: boolean;
|
|
16
|
+
/** Speed in human terms, e.g. `"Fastest"`. */
|
|
17
|
+
speed?: React.ReactNode;
|
|
18
|
+
/** Quality in human terms, e.g. `"Best quality"`. */
|
|
19
|
+
quality?: React.ReactNode;
|
|
20
|
+
/** Cost in human terms, e.g. `"$$"` or `"Uses more of your allowance"`. */
|
|
21
|
+
cost?: React.ReactNode;
|
|
22
|
+
/** The option can't be chosen right now (allowance used up, not in your plan). The row stays. */
|
|
23
|
+
disabled?: boolean;
|
|
24
|
+
/** Why it is disabled and what answers instead, e.g. `"Limit reached until 3 pm — Sonnet answers instead"`. */
|
|
25
|
+
unavailableReason?: React.ReactNode;
|
|
26
|
+
}
|
|
27
|
+
interface ModelSelectionProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "onChange"> {
|
|
28
|
+
/** The available models. */
|
|
29
|
+
models: ModelOption[];
|
|
30
|
+
/** Selected value (controlled). */
|
|
31
|
+
value?: string;
|
|
32
|
+
/** Initial value (uncontrolled). */
|
|
33
|
+
defaultValue?: string;
|
|
34
|
+
/** Called when a model is chosen. */
|
|
35
|
+
onValueChange?: (value: string) => void;
|
|
36
|
+
/** Accessible label for the trigger. Defaults to `"Model"`. */
|
|
37
|
+
label?: string;
|
|
38
|
+
/** Layout. Defaults to `"dropdown"`. */
|
|
39
|
+
variant?: ModelSelectionVariant;
|
|
40
|
+
/** Auto-route on (controlled), for `variant="auto-route"`. */
|
|
41
|
+
autoRoute?: boolean;
|
|
42
|
+
/** Initial auto-route state (uncontrolled). Defaults to `true`: auto-route is the default. */
|
|
43
|
+
defaultAutoRoute?: boolean;
|
|
44
|
+
/** Called when auto-route is switched. */
|
|
45
|
+
onAutoRouteChange?: (on: boolean) => void;
|
|
46
|
+
/** The model auto-route is using now, shown as "currently: …". */
|
|
47
|
+
routedModel?: React.ReactNode;
|
|
48
|
+
/** Who routes, for the description. Defaults to `"Aria"`. */
|
|
49
|
+
agentName?: React.ReactNode;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Lets the user pick the model for the task. `variant="dropdown"` (default) is
|
|
53
|
+
* a composer-anchored listbox (arrow keys, Home/End, Escape returns focus);
|
|
54
|
+
* `"tradeoff"` lays models out as radio cards with speed, quality and cost in
|
|
55
|
+
* human terms; `"auto-route"` defaults to letting the agent route per task,
|
|
56
|
+
* shows which model it is using, and reveals the picker when switched off.
|
|
57
|
+
* Recommended and unavailable models are marked in words; an unavailable model
|
|
58
|
+
* keeps its row and says what answers instead.
|
|
59
|
+
* Source: Figma "Context Expansion / Model Selection".
|
|
60
|
+
*/
|
|
61
|
+
declare const ModelSelection: React.ForwardRefExoticComponent<ModelSelectionProps & React.RefAttributes<HTMLDivElement>>;
|
|
62
|
+
|
|
63
|
+
export { type ModelOption, ModelSelection, type ModelSelectionProps, type ModelSelectionVariant };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
/** Layout from the spec: a compact dropdown, tradeoff cards, or an auto-route toggle that reveals the picker. */
|
|
4
|
+
type ModelSelectionVariant = "dropdown" | "tradeoff" | "auto-route";
|
|
5
|
+
interface ModelOption {
|
|
6
|
+
/** Stable value passed to `onValueChange`. */
|
|
7
|
+
value: string;
|
|
8
|
+
/** Display label, e.g. `"Nebula 5.5 Instant"`. */
|
|
9
|
+
label: React.ReactNode;
|
|
10
|
+
/** Supporting line under the label — what the model is *for*, e.g. `"Everyday tasks"`. */
|
|
11
|
+
description?: React.ReactNode;
|
|
12
|
+
/** Optional leading icon. */
|
|
13
|
+
icon?: React.ReactNode;
|
|
14
|
+
/** Marks the suggested default with a "Recommended" badge. */
|
|
15
|
+
recommended?: boolean;
|
|
16
|
+
/** Speed in human terms, e.g. `"Fastest"`. */
|
|
17
|
+
speed?: React.ReactNode;
|
|
18
|
+
/** Quality in human terms, e.g. `"Best quality"`. */
|
|
19
|
+
quality?: React.ReactNode;
|
|
20
|
+
/** Cost in human terms, e.g. `"$$"` or `"Uses more of your allowance"`. */
|
|
21
|
+
cost?: React.ReactNode;
|
|
22
|
+
/** The option can't be chosen right now (allowance used up, not in your plan). The row stays. */
|
|
23
|
+
disabled?: boolean;
|
|
24
|
+
/** Why it is disabled and what answers instead, e.g. `"Limit reached until 3 pm — Sonnet answers instead"`. */
|
|
25
|
+
unavailableReason?: React.ReactNode;
|
|
26
|
+
}
|
|
27
|
+
interface ModelSelectionProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "onChange"> {
|
|
28
|
+
/** The available models. */
|
|
29
|
+
models: ModelOption[];
|
|
30
|
+
/** Selected value (controlled). */
|
|
31
|
+
value?: string;
|
|
32
|
+
/** Initial value (uncontrolled). */
|
|
33
|
+
defaultValue?: string;
|
|
34
|
+
/** Called when a model is chosen. */
|
|
35
|
+
onValueChange?: (value: string) => void;
|
|
36
|
+
/** Accessible label for the trigger. Defaults to `"Model"`. */
|
|
37
|
+
label?: string;
|
|
38
|
+
/** Layout. Defaults to `"dropdown"`. */
|
|
39
|
+
variant?: ModelSelectionVariant;
|
|
40
|
+
/** Auto-route on (controlled), for `variant="auto-route"`. */
|
|
41
|
+
autoRoute?: boolean;
|
|
42
|
+
/** Initial auto-route state (uncontrolled). Defaults to `true`: auto-route is the default. */
|
|
43
|
+
defaultAutoRoute?: boolean;
|
|
44
|
+
/** Called when auto-route is switched. */
|
|
45
|
+
onAutoRouteChange?: (on: boolean) => void;
|
|
46
|
+
/** The model auto-route is using now, shown as "currently: …". */
|
|
47
|
+
routedModel?: React.ReactNode;
|
|
48
|
+
/** Who routes, for the description. Defaults to `"Aria"`. */
|
|
49
|
+
agentName?: React.ReactNode;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Lets the user pick the model for the task. `variant="dropdown"` (default) is
|
|
53
|
+
* a composer-anchored listbox (arrow keys, Home/End, Escape returns focus);
|
|
54
|
+
* `"tradeoff"` lays models out as radio cards with speed, quality and cost in
|
|
55
|
+
* human terms; `"auto-route"` defaults to letting the agent route per task,
|
|
56
|
+
* shows which model it is using, and reveals the picker when switched off.
|
|
57
|
+
* Recommended and unavailable models are marked in words; an unavailable model
|
|
58
|
+
* keeps its row and says what answers instead.
|
|
59
|
+
* Source: Figma "Context Expansion / Model Selection".
|
|
60
|
+
*/
|
|
61
|
+
declare const ModelSelection: React.ForwardRefExoticComponent<ModelSelectionProps & React.RefAttributes<HTMLDivElement>>;
|
|
62
|
+
|
|
63
|
+
export { type ModelOption, ModelSelection, type ModelSelectionProps, type ModelSelectionVariant };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
import { cn } from '@nucleux/utils';
|
|
4
|
+
import { Badge } from '@nucleux/badge';
|
|
5
|
+
import { RadioGroup, Radio } from '@nucleux/radio-group';
|
|
6
|
+
import { Switch } from '@nucleux/switch';
|
|
7
|
+
import { Sparkles, ChevronDown, Check } from 'lucide-react';
|
|
8
|
+
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
9
|
+
|
|
10
|
+
// src/index.tsx
|
|
11
|
+
function tradeoffLine(m) {
|
|
12
|
+
const parts = [m.speed, m.quality].filter((p) => p != null && p !== "");
|
|
13
|
+
if (!parts.length) return null;
|
|
14
|
+
return parts.map((p, i) => /* @__PURE__ */ jsxs(React.Fragment, { children: [
|
|
15
|
+
i > 0 && " \xB7 ",
|
|
16
|
+
p
|
|
17
|
+
] }, i));
|
|
18
|
+
}
|
|
19
|
+
var RecommendedBadge = () => /* @__PURE__ */ jsx(Badge, { variant: "outline", className: "border-brand/40 px-1.5 py-0 text-[10px] uppercase tracking-wide text-brand", children: "Recommended" });
|
|
20
|
+
function ModelDropdown({ models, current, select, label, className }) {
|
|
21
|
+
const [open, setOpen] = React.useState(false);
|
|
22
|
+
const rootRef = React.useRef(null);
|
|
23
|
+
const triggerRef = React.useRef(null);
|
|
24
|
+
const listRef = React.useRef(null);
|
|
25
|
+
const listId = React.useId();
|
|
26
|
+
const close = React.useCallback((refocus) => {
|
|
27
|
+
setOpen(false);
|
|
28
|
+
if (refocus) triggerRef.current?.focus();
|
|
29
|
+
}, []);
|
|
30
|
+
React.useEffect(() => {
|
|
31
|
+
if (!open) return;
|
|
32
|
+
const onDown = (e) => {
|
|
33
|
+
if (rootRef.current && !rootRef.current.contains(e.target)) setOpen(false);
|
|
34
|
+
};
|
|
35
|
+
const onKey = (e) => e.key === "Escape" && close(true);
|
|
36
|
+
document.addEventListener("mousedown", onDown);
|
|
37
|
+
document.addEventListener("keydown", onKey);
|
|
38
|
+
return () => {
|
|
39
|
+
document.removeEventListener("mousedown", onDown);
|
|
40
|
+
document.removeEventListener("keydown", onKey);
|
|
41
|
+
};
|
|
42
|
+
}, [open, close]);
|
|
43
|
+
const options = () => Array.from(listRef.current?.querySelectorAll('[role="option"]:not([aria-disabled="true"])') ?? []);
|
|
44
|
+
React.useEffect(() => {
|
|
45
|
+
if (!open) return;
|
|
46
|
+
const all = options();
|
|
47
|
+
(all.find((o) => o.getAttribute("aria-selected") === "true") ?? all[0])?.focus();
|
|
48
|
+
}, [open]);
|
|
49
|
+
const onListKeyDown = (e) => {
|
|
50
|
+
const all = options();
|
|
51
|
+
const i = all.indexOf(document.activeElement);
|
|
52
|
+
const go = (n) => all[(n + all.length) % all.length]?.focus();
|
|
53
|
+
if (e.key === "ArrowDown") e.preventDefault(), go(i + 1);
|
|
54
|
+
else if (e.key === "ArrowUp") e.preventDefault(), go(i - 1);
|
|
55
|
+
else if (e.key === "Home") e.preventDefault(), go(0);
|
|
56
|
+
else if (e.key === "End") e.preventDefault(), go(all.length - 1);
|
|
57
|
+
else if (e.key === "Tab") close(false);
|
|
58
|
+
};
|
|
59
|
+
const selected = models.find((m) => m.value === current);
|
|
60
|
+
return /* @__PURE__ */ jsxs("div", { ref: rootRef, className: cn("relative inline-block", className), children: [
|
|
61
|
+
/* @__PURE__ */ jsxs(
|
|
62
|
+
"button",
|
|
63
|
+
{
|
|
64
|
+
ref: triggerRef,
|
|
65
|
+
type: "button",
|
|
66
|
+
"aria-label": label,
|
|
67
|
+
"aria-haspopup": "listbox",
|
|
68
|
+
"aria-expanded": open,
|
|
69
|
+
"aria-controls": open ? listId : void 0,
|
|
70
|
+
onClick: () => setOpen((o) => !o),
|
|
71
|
+
onKeyDown: (e) => {
|
|
72
|
+
if ((e.key === "ArrowDown" || e.key === "ArrowUp") && !open) {
|
|
73
|
+
e.preventDefault();
|
|
74
|
+
setOpen(true);
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
className: "inline-flex items-center gap-2 rounded-lg border border-border bg-background px-3 py-2 text-sm font-medium text-foreground transition-colors hover:bg-accent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring [&>svg]:size-4",
|
|
78
|
+
children: [
|
|
79
|
+
/* @__PURE__ */ jsx(Sparkles, { className: "text-brand", "aria-hidden": "true" }),
|
|
80
|
+
/* @__PURE__ */ jsx("span", { children: selected?.label ?? "Select model" }),
|
|
81
|
+
/* @__PURE__ */ jsx(ChevronDown, { className: cn("text-muted-foreground transition-transform", open && "rotate-180"), "aria-hidden": "true" })
|
|
82
|
+
]
|
|
83
|
+
}
|
|
84
|
+
),
|
|
85
|
+
open && /* @__PURE__ */ jsx(
|
|
86
|
+
"div",
|
|
87
|
+
{
|
|
88
|
+
ref: listRef,
|
|
89
|
+
id: listId,
|
|
90
|
+
role: "listbox",
|
|
91
|
+
"aria-label": label,
|
|
92
|
+
onKeyDown: onListKeyDown,
|
|
93
|
+
className: "absolute z-50 mt-1 flex min-w-64 flex-col gap-0.5 rounded-xl border border-border bg-background p-1.5 shadow-md animate-nx-fade-in motion-reduce:animate-none",
|
|
94
|
+
children: models.map((model) => {
|
|
95
|
+
const active = model.value === current;
|
|
96
|
+
const line = tradeoffLine(model);
|
|
97
|
+
return /* @__PURE__ */ jsxs(
|
|
98
|
+
"button",
|
|
99
|
+
{
|
|
100
|
+
type: "button",
|
|
101
|
+
role: "option",
|
|
102
|
+
"aria-selected": active,
|
|
103
|
+
"aria-disabled": model.disabled || void 0,
|
|
104
|
+
tabIndex: -1,
|
|
105
|
+
onClick: () => {
|
|
106
|
+
if (model.disabled) return;
|
|
107
|
+
select(model.value);
|
|
108
|
+
close(true);
|
|
109
|
+
},
|
|
110
|
+
className: cn(
|
|
111
|
+
"flex items-start gap-2.5 rounded-lg px-2 py-2 text-left transition-colors focus-visible:outline-none",
|
|
112
|
+
model.disabled ? "cursor-not-allowed opacity-60" : "hover:bg-accent focus-visible:bg-accent",
|
|
113
|
+
active && "bg-brand-muted/40"
|
|
114
|
+
),
|
|
115
|
+
children: [
|
|
116
|
+
/* @__PURE__ */ jsx("span", { className: "mt-0.5 inline-flex size-4 shrink-0 items-center justify-center text-brand [&_svg]:size-4", children: active ? /* @__PURE__ */ jsx(Check, { "aria-hidden": "true" }) : model.icon }),
|
|
117
|
+
/* @__PURE__ */ jsxs("span", { className: "flex min-w-0 flex-1 flex-col", children: [
|
|
118
|
+
/* @__PURE__ */ jsxs("span", { className: "flex items-center gap-2 text-sm font-medium text-foreground", children: [
|
|
119
|
+
model.label,
|
|
120
|
+
model.recommended && /* @__PURE__ */ jsx(RecommendedBadge, {})
|
|
121
|
+
] }),
|
|
122
|
+
model.description && /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: model.description }),
|
|
123
|
+
line && /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: line }),
|
|
124
|
+
model.disabled && model.unavailableReason && /* @__PURE__ */ jsx("span", { className: "text-xs text-foreground", children: model.unavailableReason })
|
|
125
|
+
] }),
|
|
126
|
+
model.cost && /* @__PURE__ */ jsx("span", { className: "shrink-0 text-xs font-semibold text-muted-foreground", children: model.cost })
|
|
127
|
+
]
|
|
128
|
+
},
|
|
129
|
+
model.value
|
|
130
|
+
);
|
|
131
|
+
})
|
|
132
|
+
}
|
|
133
|
+
)
|
|
134
|
+
] });
|
|
135
|
+
}
|
|
136
|
+
var ModelSelection = React.forwardRef(
|
|
137
|
+
({
|
|
138
|
+
models,
|
|
139
|
+
value,
|
|
140
|
+
defaultValue,
|
|
141
|
+
onValueChange,
|
|
142
|
+
label = "Model",
|
|
143
|
+
variant = "dropdown",
|
|
144
|
+
autoRoute,
|
|
145
|
+
defaultAutoRoute = true,
|
|
146
|
+
onAutoRouteChange,
|
|
147
|
+
routedModel,
|
|
148
|
+
agentName = "Aria",
|
|
149
|
+
className,
|
|
150
|
+
...props
|
|
151
|
+
}, ref) => {
|
|
152
|
+
const [internal, setInternal] = React.useState(defaultValue ?? models[0]?.value);
|
|
153
|
+
const current = value !== void 0 ? value : internal;
|
|
154
|
+
const select = (next) => {
|
|
155
|
+
if (value === void 0) setInternal(next);
|
|
156
|
+
onValueChange?.(next);
|
|
157
|
+
};
|
|
158
|
+
const [autoInternal, setAutoInternal] = React.useState(defaultAutoRoute);
|
|
159
|
+
const auto = autoRoute !== void 0 ? autoRoute : autoInternal;
|
|
160
|
+
const setAuto = (on) => {
|
|
161
|
+
if (autoRoute === void 0) setAutoInternal(on);
|
|
162
|
+
onAutoRouteChange?.(on);
|
|
163
|
+
};
|
|
164
|
+
const autoLabelId = React.useId();
|
|
165
|
+
const headingId = React.useId();
|
|
166
|
+
if (variant === "tradeoff") {
|
|
167
|
+
return /* @__PURE__ */ jsxs("div", { ref, className: cn("flex flex-col gap-2", className), ...props, children: [
|
|
168
|
+
/* @__PURE__ */ jsx("p", { id: headingId, className: "sr-only", children: label }),
|
|
169
|
+
/* @__PURE__ */ jsx(
|
|
170
|
+
RadioGroup,
|
|
171
|
+
{
|
|
172
|
+
"aria-labelledby": headingId,
|
|
173
|
+
value: current,
|
|
174
|
+
onValueChange: select,
|
|
175
|
+
className: "gap-1.5",
|
|
176
|
+
children: models.map((model) => {
|
|
177
|
+
const id = `${headingId}-${model.value}`;
|
|
178
|
+
const line = tradeoffLine(model);
|
|
179
|
+
return /* @__PURE__ */ jsxs(
|
|
180
|
+
"label",
|
|
181
|
+
{
|
|
182
|
+
className: cn(
|
|
183
|
+
"flex cursor-pointer items-center gap-3 rounded-lg border border-border bg-background px-3 py-2 transition-colors hover:bg-accent",
|
|
184
|
+
"has-[[aria-checked=true]]:border-brand has-[[aria-checked=true]]:bg-brand-muted/40",
|
|
185
|
+
model.disabled && "cursor-not-allowed opacity-60 hover:bg-background"
|
|
186
|
+
),
|
|
187
|
+
children: [
|
|
188
|
+
/* @__PURE__ */ jsx(Radio, { value: model.value, disabled: model.disabled, "aria-labelledby": id }),
|
|
189
|
+
/* @__PURE__ */ jsxs("span", { id, className: "flex min-w-0 flex-1 flex-col", children: [
|
|
190
|
+
/* @__PURE__ */ jsxs("span", { className: "flex items-center gap-2 text-sm font-semibold text-foreground", children: [
|
|
191
|
+
model.label,
|
|
192
|
+
model.recommended && /* @__PURE__ */ jsx(RecommendedBadge, {})
|
|
193
|
+
] }),
|
|
194
|
+
(line ?? model.description) && /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: line ?? model.description }),
|
|
195
|
+
model.disabled && model.unavailableReason && /* @__PURE__ */ jsx("span", { className: "text-xs text-foreground", children: model.unavailableReason })
|
|
196
|
+
] }),
|
|
197
|
+
model.cost && /* @__PURE__ */ jsx("span", { className: "shrink-0 text-xs font-semibold text-muted-foreground", children: model.cost })
|
|
198
|
+
]
|
|
199
|
+
},
|
|
200
|
+
model.value
|
|
201
|
+
);
|
|
202
|
+
})
|
|
203
|
+
}
|
|
204
|
+
)
|
|
205
|
+
] });
|
|
206
|
+
}
|
|
207
|
+
if (variant === "auto-route") {
|
|
208
|
+
return /* @__PURE__ */ jsxs("div", { ref, className: cn("flex flex-col gap-2", className), ...props, children: [
|
|
209
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-3 rounded-lg border border-border bg-background px-3.5 py-2.5", children: [
|
|
210
|
+
/* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-col", children: [
|
|
211
|
+
/* @__PURE__ */ jsx("span", { id: autoLabelId, className: "text-sm font-semibold text-foreground", children: "Auto-route" }),
|
|
212
|
+
/* @__PURE__ */ jsxs("span", { className: "text-xs text-muted-foreground", children: [
|
|
213
|
+
agentName,
|
|
214
|
+
" picks the model per task"
|
|
215
|
+
] })
|
|
216
|
+
] }),
|
|
217
|
+
/* @__PURE__ */ jsxs("div", { className: "flex shrink-0 items-center gap-2", children: [
|
|
218
|
+
auto && routedModel && /* @__PURE__ */ jsxs("span", { className: "text-xs text-muted-foreground", "aria-live": "polite", children: [
|
|
219
|
+
"currently: ",
|
|
220
|
+
routedModel
|
|
221
|
+
] }),
|
|
222
|
+
/* @__PURE__ */ jsx(Switch, { checked: auto, onCheckedChange: setAuto, "aria-labelledby": autoLabelId })
|
|
223
|
+
] })
|
|
224
|
+
] }),
|
|
225
|
+
!auto && /* @__PURE__ */ jsx(ModelDropdown, { models, current, select, label })
|
|
226
|
+
] });
|
|
227
|
+
}
|
|
228
|
+
return /* @__PURE__ */ jsx("div", { ref, className: cn("relative inline-block", className), ...props, children: /* @__PURE__ */ jsx(ModelDropdown, { models, current, select, label }) });
|
|
229
|
+
}
|
|
230
|
+
);
|
|
231
|
+
ModelSelection.displayName = "ModelSelection";
|
|
232
|
+
|
|
233
|
+
export { ModelSelection };
|
|
234
|
+
//# sourceMappingURL=index.js.map
|
|
235
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.tsx"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import * as React from \"react\";\nimport { cn } from \"@nucleux/utils\";\nimport { Badge } from \"@nucleux/badge\";\nimport { Radio, RadioGroup } from \"@nucleux/radio-group\";\nimport { Switch } from \"@nucleux/switch\";\nimport { Check, ChevronDown, Sparkles } from \"lucide-react\";\n\n/** Layout from the spec: a compact dropdown, tradeoff cards, or an auto-route toggle that reveals the picker. */\nexport type ModelSelectionVariant = \"dropdown\" | \"tradeoff\" | \"auto-route\";\n\nexport interface ModelOption {\n /** Stable value passed to `onValueChange`. */\n value: string;\n /** Display label, e.g. `\"Nebula 5.5 Instant\"`. */\n label: React.ReactNode;\n /** Supporting line under the label — what the model is *for*, e.g. `\"Everyday tasks\"`. */\n description?: React.ReactNode;\n /** Optional leading icon. */\n icon?: React.ReactNode;\n /** Marks the suggested default with a \"Recommended\" badge. */\n recommended?: boolean;\n /** Speed in human terms, e.g. `\"Fastest\"`. */\n speed?: React.ReactNode;\n /** Quality in human terms, e.g. `\"Best quality\"`. */\n quality?: React.ReactNode;\n /** Cost in human terms, e.g. `\"$$\"` or `\"Uses more of your allowance\"`. */\n cost?: React.ReactNode;\n /** The option can't be chosen right now (allowance used up, not in your plan). The row stays. */\n disabled?: boolean;\n /** Why it is disabled and what answers instead, e.g. `\"Limit reached until 3 pm — Sonnet answers instead\"`. */\n unavailableReason?: React.ReactNode;\n}\n\nexport interface ModelSelectionProps\n extends Omit<React.HTMLAttributes<HTMLDivElement>, \"onChange\"> {\n /** The available models. */\n models: ModelOption[];\n /** Selected value (controlled). */\n value?: string;\n /** Initial value (uncontrolled). */\n defaultValue?: string;\n /** Called when a model is chosen. */\n onValueChange?: (value: string) => void;\n /** Accessible label for the trigger. Defaults to `\"Model\"`. */\n label?: string;\n /** Layout. Defaults to `\"dropdown\"`. */\n variant?: ModelSelectionVariant;\n /** Auto-route on (controlled), for `variant=\"auto-route\"`. */\n autoRoute?: boolean;\n /** Initial auto-route state (uncontrolled). Defaults to `true`: auto-route is the default. */\n defaultAutoRoute?: boolean;\n /** Called when auto-route is switched. */\n onAutoRouteChange?: (on: boolean) => void;\n /** The model auto-route is using now, shown as \"currently: …\". */\n routedModel?: React.ReactNode;\n /** Who routes, for the description. Defaults to `\"Aria\"`. */\n agentName?: React.ReactNode;\n}\n\n/** \"Fastest · Best quality · $$\" from whichever tradeoff facts are given. */\nfunction tradeoffLine(m: ModelOption): React.ReactNode {\n const parts = [m.speed, m.quality].filter((p) => p != null && p !== \"\");\n if (!parts.length) return null;\n return parts.map((p, i) => (\n <React.Fragment key={i}>\n {i > 0 && \" · \"}\n {p}\n </React.Fragment>\n ));\n}\n\nconst RecommendedBadge = () => (\n <Badge variant=\"outline\" className=\"border-brand/40 px-1.5 py-0 text-[10px] uppercase tracking-wide text-brand\">\n Recommended\n </Badge>\n);\n\ninterface PickerProps {\n models: ModelOption[];\n current: string | undefined;\n select: (value: string) => void;\n label: string;\n className?: string;\n}\n\n/** The composer-anchored dropdown (listbox) used by the \"dropdown\" and \"auto-route\" variants. */\nfunction ModelDropdown({ models, current, select, label, className }: PickerProps) {\n const [open, setOpen] = React.useState(false);\n const rootRef = React.useRef<HTMLDivElement>(null);\n const triggerRef = React.useRef<HTMLButtonElement>(null);\n const listRef = React.useRef<HTMLDivElement>(null);\n const listId = React.useId();\n\n const close = React.useCallback((refocus: boolean) => {\n setOpen(false);\n if (refocus) triggerRef.current?.focus();\n }, []);\n\n React.useEffect(() => {\n if (!open) return;\n const onDown = (e: MouseEvent) => {\n if (rootRef.current && !rootRef.current.contains(e.target as Node)) setOpen(false);\n };\n const onKey = (e: KeyboardEvent) => e.key === \"Escape\" && close(true);\n document.addEventListener(\"mousedown\", onDown);\n document.addEventListener(\"keydown\", onKey);\n return () => {\n document.removeEventListener(\"mousedown\", onDown);\n document.removeEventListener(\"keydown\", onKey);\n };\n }, [open, close]);\n\n const options = () =>\n Array.from(listRef.current?.querySelectorAll<HTMLElement>('[role=\"option\"]:not([aria-disabled=\"true\"])') ?? []);\n\n // Focus the selected option (or the first available) when the list opens.\n React.useEffect(() => {\n if (!open) return;\n const all = options();\n (all.find((o) => o.getAttribute(\"aria-selected\") === \"true\") ?? all[0])?.focus();\n }, [open]);\n\n const onListKeyDown = (e: React.KeyboardEvent) => {\n const all = options();\n const i = all.indexOf(document.activeElement as HTMLElement);\n const go = (n: number) => all[(n + all.length) % all.length]?.focus();\n if (e.key === \"ArrowDown\") (e.preventDefault(), go(i + 1));\n else if (e.key === \"ArrowUp\") (e.preventDefault(), go(i - 1));\n else if (e.key === \"Home\") (e.preventDefault(), go(0));\n else if (e.key === \"End\") (e.preventDefault(), go(all.length - 1));\n else if (e.key === \"Tab\") close(false);\n };\n\n const selected = models.find((m) => m.value === current);\n\n return (\n <div ref={rootRef} className={cn(\"relative inline-block\", className)}>\n <button\n ref={triggerRef}\n type=\"button\"\n aria-label={label}\n aria-haspopup=\"listbox\"\n aria-expanded={open}\n aria-controls={open ? listId : undefined}\n onClick={() => setOpen((o) => !o)}\n onKeyDown={(e) => {\n if ((e.key === \"ArrowDown\" || e.key === \"ArrowUp\") && !open) {\n e.preventDefault();\n setOpen(true);\n }\n }}\n className=\"inline-flex items-center gap-2 rounded-lg border border-border bg-background px-3 py-2 text-sm font-medium text-foreground transition-colors hover:bg-accent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring [&>svg]:size-4\"\n >\n <Sparkles className=\"text-brand\" aria-hidden=\"true\" />\n <span>{selected?.label ?? \"Select model\"}</span>\n <ChevronDown className={cn(\"text-muted-foreground transition-transform\", open && \"rotate-180\")} aria-hidden=\"true\" />\n </button>\n {open && (\n <div\n ref={listRef}\n id={listId}\n role=\"listbox\"\n aria-label={label}\n onKeyDown={onListKeyDown}\n className=\"absolute z-50 mt-1 flex min-w-64 flex-col gap-0.5 rounded-xl border border-border bg-background p-1.5 shadow-md animate-nx-fade-in motion-reduce:animate-none\"\n >\n {models.map((model) => {\n const active = model.value === current;\n const line = tradeoffLine(model);\n return (\n <button\n key={model.value}\n type=\"button\"\n role=\"option\"\n aria-selected={active}\n aria-disabled={model.disabled || undefined}\n tabIndex={-1}\n onClick={() => {\n if (model.disabled) return;\n select(model.value);\n close(true);\n }}\n className={cn(\n \"flex items-start gap-2.5 rounded-lg px-2 py-2 text-left transition-colors focus-visible:outline-none\",\n model.disabled ? \"cursor-not-allowed opacity-60\" : \"hover:bg-accent focus-visible:bg-accent\",\n active && \"bg-brand-muted/40\",\n )}\n >\n <span className=\"mt-0.5 inline-flex size-4 shrink-0 items-center justify-center text-brand [&_svg]:size-4\">\n {active ? <Check aria-hidden=\"true\" /> : model.icon}\n </span>\n <span className=\"flex min-w-0 flex-1 flex-col\">\n <span className=\"flex items-center gap-2 text-sm font-medium text-foreground\">\n {model.label}\n {model.recommended && <RecommendedBadge />}\n </span>\n {model.description && <span className=\"text-xs text-muted-foreground\">{model.description}</span>}\n {line && <span className=\"text-xs text-muted-foreground\">{line}</span>}\n {model.disabled && model.unavailableReason && (\n <span className=\"text-xs text-foreground\">{model.unavailableReason}</span>\n )}\n </span>\n {model.cost && <span className=\"shrink-0 text-xs font-semibold text-muted-foreground\">{model.cost}</span>}\n </button>\n );\n })}\n </div>\n )}\n </div>\n );\n}\n\n/**\n * Lets the user pick the model for the task. `variant=\"dropdown\"` (default) is\n * a composer-anchored listbox (arrow keys, Home/End, Escape returns focus);\n * `\"tradeoff\"` lays models out as radio cards with speed, quality and cost in\n * human terms; `\"auto-route\"` defaults to letting the agent route per task,\n * shows which model it is using, and reveals the picker when switched off.\n * Recommended and unavailable models are marked in words; an unavailable model\n * keeps its row and says what answers instead.\n * Source: Figma \"Context Expansion / Model Selection\".\n */\nexport const ModelSelection = React.forwardRef<HTMLDivElement, ModelSelectionProps>(\n (\n {\n models,\n value,\n defaultValue,\n onValueChange,\n label = \"Model\",\n variant = \"dropdown\",\n autoRoute,\n defaultAutoRoute = true,\n onAutoRouteChange,\n routedModel,\n agentName = \"Aria\",\n className,\n ...props\n },\n ref,\n ) => {\n const [internal, setInternal] = React.useState<string | undefined>(defaultValue ?? models[0]?.value);\n const current = value !== undefined ? value : internal;\n const select = (next: string) => {\n if (value === undefined) setInternal(next);\n onValueChange?.(next);\n };\n\n const [autoInternal, setAutoInternal] = React.useState(defaultAutoRoute);\n const auto = autoRoute !== undefined ? autoRoute : autoInternal;\n const setAuto = (on: boolean) => {\n if (autoRoute === undefined) setAutoInternal(on);\n onAutoRouteChange?.(on);\n };\n const autoLabelId = React.useId();\n const headingId = React.useId();\n\n if (variant === \"tradeoff\") {\n return (\n <div ref={ref} className={cn(\"flex flex-col gap-2\", className)} {...props}>\n <p id={headingId} className=\"sr-only\">\n {label}\n </p>\n <RadioGroup\n aria-labelledby={headingId}\n value={current}\n onValueChange={select}\n className=\"gap-1.5\"\n >\n {models.map((model) => {\n const id = `${headingId}-${model.value}`;\n const line = tradeoffLine(model);\n return (\n <label\n key={model.value}\n className={cn(\n \"flex cursor-pointer items-center gap-3 rounded-lg border border-border bg-background px-3 py-2 transition-colors hover:bg-accent\",\n \"has-[[aria-checked=true]]:border-brand has-[[aria-checked=true]]:bg-brand-muted/40\",\n model.disabled && \"cursor-not-allowed opacity-60 hover:bg-background\",\n )}\n >\n <Radio value={model.value} disabled={model.disabled} aria-labelledby={id} />\n <span id={id} className=\"flex min-w-0 flex-1 flex-col\">\n <span className=\"flex items-center gap-2 text-sm font-semibold text-foreground\">\n {model.label}\n {model.recommended && <RecommendedBadge />}\n </span>\n {(line ?? model.description) && (\n <span className=\"text-xs text-muted-foreground\">{line ?? model.description}</span>\n )}\n {model.disabled && model.unavailableReason && (\n <span className=\"text-xs text-foreground\">{model.unavailableReason}</span>\n )}\n </span>\n {model.cost && (\n <span className=\"shrink-0 text-xs font-semibold text-muted-foreground\">{model.cost}</span>\n )}\n </label>\n );\n })}\n </RadioGroup>\n </div>\n );\n }\n\n if (variant === \"auto-route\") {\n return (\n <div ref={ref} className={cn(\"flex flex-col gap-2\", className)} {...props}>\n <div className=\"flex items-center justify-between gap-3 rounded-lg border border-border bg-background px-3.5 py-2.5\">\n <div className=\"flex min-w-0 flex-col\">\n <span id={autoLabelId} className=\"text-sm font-semibold text-foreground\">\n Auto-route\n </span>\n <span className=\"text-xs text-muted-foreground\">{agentName} picks the model per task</span>\n </div>\n <div className=\"flex shrink-0 items-center gap-2\">\n {auto && routedModel && (\n <span className=\"text-xs text-muted-foreground\" aria-live=\"polite\">\n currently: {routedModel}\n </span>\n )}\n <Switch checked={auto} onCheckedChange={setAuto} aria-labelledby={autoLabelId} />\n </div>\n </div>\n {!auto && <ModelDropdown models={models} current={current} select={select} label={label} />}\n </div>\n );\n }\n\n return (\n <div ref={ref} className={cn(\"relative inline-block\", className)} {...props}>\n <ModelDropdown models={models} current={current} select={select} label={label} />\n </div>\n );\n },\n);\nModelSelection.displayName = \"ModelSelection\";\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,61 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nucleux/model-selection",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.7.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"sideEffects": false,
|
|
6
|
+
"description": "Model Selection — choose which model handles the request — Nucleux agentic UI for React.",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"nucleux",
|
|
9
|
+
"react",
|
|
10
|
+
"ui",
|
|
11
|
+
"components",
|
|
12
|
+
"agentic",
|
|
13
|
+
"ai",
|
|
14
|
+
"tailwind",
|
|
15
|
+
"model-selection"
|
|
16
|
+
],
|
|
17
|
+
"main": "./dist/index.cjs",
|
|
18
|
+
"module": "./dist/index.js",
|
|
19
|
+
"types": "./dist/index.d.ts",
|
|
20
|
+
"exports": {
|
|
21
|
+
".": {
|
|
22
|
+
"types": "./dist/index.d.ts",
|
|
23
|
+
"import": "./dist/index.js",
|
|
24
|
+
"require": "./dist/index.cjs"
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
"files": [
|
|
28
|
+
"dist",
|
|
29
|
+
"README.md"
|
|
30
|
+
],
|
|
31
|
+
"peerDependencies": {
|
|
32
|
+
"react": ">=18",
|
|
33
|
+
"react-dom": ">=18"
|
|
34
|
+
},
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"@nucleux/utils": "0.7.0",
|
|
37
|
+
"@nucleux/badge": "0.7.0",
|
|
38
|
+
"@nucleux/radio-group": "0.7.0",
|
|
39
|
+
"@nucleux/switch": "0.7.0",
|
|
40
|
+
"lucide-react": "^0.469.0"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"tsup": "^8.3.5",
|
|
44
|
+
"typescript": "^5.7.2",
|
|
45
|
+
"@types/react": "^18.3.18",
|
|
46
|
+
"react": "^18.3.1"
|
|
47
|
+
},
|
|
48
|
+
"publishConfig": {
|
|
49
|
+
"access": "public"
|
|
50
|
+
},
|
|
51
|
+
"license": "MIT",
|
|
52
|
+
"repository": {
|
|
53
|
+
"type": "git",
|
|
54
|
+
"url": "git+https://github.com/intelligaia-open-labs/nucleux.git",
|
|
55
|
+
"directory": "packages/model-selection"
|
|
56
|
+
},
|
|
57
|
+
"homepage": "https://github.com/intelligaia-open-labs/nucleux#readme",
|
|
58
|
+
"scripts": {
|
|
59
|
+
"build": "tsup"
|
|
60
|
+
}
|
|
6
61
|
}
|