@tangle-network/sandbox-ui 0.97.0 → 0.99.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.
@@ -1,529 +0,0 @@
1
- import {
2
- BrandLogo,
3
- ModelBrandStack,
4
- brandInfo,
5
- canonicalModelId,
6
- normalizeBrandKey,
7
- resolveModelBrandIdentity
8
- } from "./chunk-5XRAKVIV.js";
9
- import {
10
- cn
11
- } from "./chunk-EI44GEQ5.js";
12
-
13
- // src/dashboard/model-picker.tsx
14
- import * as React2 from "react";
15
- import { ChevronDown, Sparkles } from "lucide-react";
16
- import * as Popover from "@radix-ui/react-dropdown-menu";
17
-
18
- // src/lib/picker-menu.tsx
19
- import * as React from "react";
20
- import { Loader2, Search } from "lucide-react";
21
- import { jsx, jsxs } from "react/jsx-runtime";
22
- var pickerMenuContentClass = cn(
23
- "z-50 w-[var(--radix-dropdown-menu-trigger-width)] min-w-[320px] max-w-[460px]",
24
- "max-h-[min(440px,var(--radix-dropdown-menu-content-available-height))] overflow-hidden flex flex-col",
25
- "rounded-[var(--radius-md)] border border-[var(--md3-outline-variant)] bg-surface-container-highest shadow-[0_8px_30px_rgba(0,0,0,0.45)] ring-1 ring-[#ffffff14]",
26
- "data-[state=open]:animate-in data-[state=closed]:animate-out",
27
- "data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
28
- "data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95"
29
- );
30
- var pickerMenuBodyClass = "flex-1 overflow-y-auto";
31
- function pickerMenuItemClass({
32
- active,
33
- featured
34
- } = {}) {
35
- return cn(
36
- "flex cursor-pointer items-start gap-2 rounded-md px-3 py-2 outline-none",
37
- "transition-colors duration-[var(--transition-fast)]",
38
- "hover:bg-accent/50 focus:bg-accent/50",
39
- featured && "mx-1 border border-primary/10 bg-primary/[0.04] px-2.5",
40
- active && "bg-primary/10 font-medium text-foreground ring-1 ring-inset ring-primary/25 hover:bg-primary/15 focus:bg-primary/15"
41
- );
42
- }
43
- function PickerMenuSearch({
44
- value,
45
- onChange,
46
- placeholder,
47
- loading,
48
- inputRef
49
- }) {
50
- return /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 border-b border-[var(--md3-outline-variant)] bg-surface-container-high px-3 py-2", children: [
51
- /* @__PURE__ */ jsx(Search, { className: "h-3.5 w-3.5 shrink-0 text-muted-foreground" }),
52
- /* @__PURE__ */ jsx(
53
- "input",
54
- {
55
- ref: inputRef,
56
- type: "text",
57
- value,
58
- onChange: (e) => onChange(e.target.value),
59
- onKeyDown: (e) => {
60
- if (e.key.length === 1) e.stopPropagation();
61
- },
62
- placeholder,
63
- autoFocus: true,
64
- className: "flex-1 bg-transparent text-sm font-medium outline-none placeholder:font-normal placeholder:text-muted-foreground"
65
- }
66
- ),
67
- loading && /* @__PURE__ */ jsx(Loader2, { className: "h-3.5 w-3.5 animate-spin text-muted-foreground" })
68
- ] });
69
- }
70
- function PickerMenuSection({
71
- label,
72
- glyph,
73
- tone,
74
- children
75
- }) {
76
- return /* @__PURE__ */ jsxs("div", { className: "py-1", children: [
77
- /* @__PURE__ */ jsxs(
78
- "div",
79
- {
80
- className: cn(
81
- "flex items-center gap-1.5 px-3 pt-1.5 pb-0.5 text-[10px] font-mono uppercase tracking-widest",
82
- tone === "featured" ? "text-primary" : "text-muted-foreground"
83
- ),
84
- children: [
85
- glyph,
86
- /* @__PURE__ */ jsx("span", { children: label })
87
- ]
88
- }
89
- ),
90
- /* @__PURE__ */ jsx("div", { children })
91
- ] });
92
- }
93
- function PickerMenuFooter({ children }) {
94
- return /* @__PURE__ */ jsx("div", { className: "border-t border-[var(--md3-outline-variant)] px-3 py-1.5 text-[10px] text-muted-foreground", children });
95
- }
96
- function PickerMenuMeta({ parts }) {
97
- const shown = parts.filter(Boolean);
98
- if (shown.length === 0) return null;
99
- return /* @__PURE__ */ jsx("div", { className: "mt-0.5 flex items-center gap-1.5 text-[11px] text-muted-foreground", children: shown.map((part, i) => /* @__PURE__ */ jsxs(React.Fragment, { children: [
100
- i > 0 && /* @__PURE__ */ jsx("span", { className: "shrink-0 opacity-40", children: "\xB7" }),
101
- /* @__PURE__ */ jsx("span", { className: i === 0 ? "min-w-0 truncate" : "shrink-0", children: part })
102
- ] }, i)) });
103
- }
104
- function PickerMenuTag({ children }) {
105
- return /* @__PURE__ */ jsx("span", { className: "shrink-0 rounded-full border border-[var(--md3-outline-variant)] bg-surface-container px-1.5 py-px text-[9px] font-medium text-muted-foreground", children });
106
- }
107
-
108
- // src/dashboard/model-picker.tsx
109
- import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
110
- function modelDedupKey(model) {
111
- const canonical = canonicalModelId(model).toLowerCase();
112
- const segments = canonical.split("/").filter(Boolean);
113
- const modelId = segments[segments.length - 1] ?? canonical;
114
- const lab = resolveModelBrandIdentity(model).lab.key;
115
- return lab === "unknown" ? canonical : `${lab}/${modelId}`;
116
- }
117
- function dedupeModels(models) {
118
- const byKey = /* @__PURE__ */ new Map();
119
- const result = [];
120
- for (const model of models) {
121
- const key = modelDedupKey(model);
122
- const existingIndex = byKey.get(key);
123
- if (existingIndex === void 0) {
124
- byKey.set(key, result.length);
125
- result.push(model);
126
- continue;
127
- }
128
- const existing = result[existingIndex];
129
- if (!existing.featured && model.featured) {
130
- result[existingIndex] = model;
131
- }
132
- }
133
- return result;
134
- }
135
- var DEFAULT_FEATURED_MODEL_IDS = [
136
- "anthropic/claude-fable-5",
137
- "anthropic/claude-opus-5",
138
- "openai/gpt-5.6-sol",
139
- "google/gemini-3.1-pro-preview",
140
- "deepseek/deepseek-v4-flash"
141
- ];
142
- function isTextChatModel(model) {
143
- const arch = model.architecture;
144
- if (!arch) return true;
145
- const inputs = arch.input_modalities;
146
- const outputs = arch.output_modalities;
147
- if (inputs?.length || outputs?.length) {
148
- const inOk = !inputs?.length || inputs.includes("text");
149
- const outOk = !outputs?.length || outputs.includes("text");
150
- return inOk && outOk;
151
- }
152
- const modality = arch.modality;
153
- if (!modality) return true;
154
- const arrow = modality.split("->");
155
- if (arrow.length === 2) {
156
- return arrow[0].split("+").includes("text") && arrow[1].split("+").includes("text");
157
- }
158
- return modality === "text" || modality === "multimodal";
159
- }
160
- function formatPricing(pricing) {
161
- const prompt = Number(pricing?.prompt ?? 0);
162
- const completion = Number(pricing?.completion ?? 0);
163
- if (!prompt && !completion) return null;
164
- const fmt = (n) => {
165
- const perM = n * 1e6;
166
- if (perM === 0) return "0";
167
- if (perM >= 1) return `$${perM.toFixed(2)}`;
168
- return `$${perM.toFixed(2)}`;
169
- };
170
- return `${fmt(prompt)} / ${fmt(completion)} per 1M`;
171
- }
172
- function formatContext(ctx) {
173
- if (!ctx) return null;
174
- if (ctx >= 1e6) return `${(ctx / 1e6).toFixed(1)}M ctx`;
175
- if (ctx >= 1e3) return `${Math.round(ctx / 1e3)}k ctx`;
176
- return `${ctx} ctx`;
177
- }
178
- var PRIORITY_MODEL_GROUPS = ["anthropic", "openai", "google", "deepseek", "zai", "moonshot"];
179
- function modelGroupKey(model) {
180
- const identity = resolveModelBrandIdentity(model);
181
- if (identity.lab.key !== "unknown") return identity.lab.key;
182
- if (identity.host.key !== "unknown") return identity.host.key;
183
- return (model._provider ?? model.provider ?? "other").toLowerCase();
184
- }
185
- function modelGroupLabel(key) {
186
- if (key === "moonshot") return "Kimi";
187
- const brand = brandInfo(normalizeBrandKey(key));
188
- return brand.key === "unknown" ? key : brand.label;
189
- }
190
- function modelGroupRank(key) {
191
- const priorityIndex = PRIORITY_MODEL_GROUPS.indexOf(normalizeBrandKey(key));
192
- return priorityIndex === -1 ? Number.POSITIVE_INFINITY : priorityIndex;
193
- }
194
- function compareModelGroups(a, b) {
195
- const aRank = modelGroupRank(a);
196
- const bRank = modelGroupRank(b);
197
- if (aRank !== bRank) return aRank - bRank;
198
- return modelGroupLabel(a).localeCompare(modelGroupLabel(b));
199
- }
200
- function compareModelsByDisplayName(a, b) {
201
- return (a.name ?? a.id).localeCompare(b.name ?? b.id);
202
- }
203
- function stripBrandPrefix(name, identity) {
204
- if (!identity) return name;
205
- for (const label of [identity.lab.label, identity.host.label]) {
206
- if (!label) continue;
207
- if (name.toLowerCase().startsWith(label.toLowerCase())) {
208
- const after = name.slice(label.length);
209
- if (/^[\s:]/.test(after)) {
210
- const rest = after.replace(/^[\s:]+/, "").trim();
211
- if (rest) return rest;
212
- }
213
- }
214
- }
215
- return name;
216
- }
217
- function ModelPicker({
218
- value,
219
- onChange,
220
- models,
221
- loading = false,
222
- recents,
223
- popular,
224
- excludeProviders,
225
- modalities,
226
- variant = "pill",
227
- label = "Model",
228
- placeholder = "Choose a model",
229
- className,
230
- triggerClassName,
231
- disabled,
232
- side,
233
- avoidCollisions
234
- }) {
235
- const [query, setQuery] = React2.useState("");
236
- const [open, setOpen] = React2.useState(false);
237
- const searchInputRef = React2.useRef(null);
238
- React2.useLayoutEffect(() => {
239
- if (!open) return;
240
- const frame = requestAnimationFrame(() => {
241
- searchInputRef.current?.focus();
242
- searchInputRef.current?.select();
243
- });
244
- return () => cancelAnimationFrame(frame);
245
- }, [open]);
246
- const deduped = React2.useMemo(() => dedupeModels(models), [models]);
247
- const passesCatalogFilters = React2.useMemo(() => {
248
- const excluded = new Set((excludeProviders ?? []).map((p) => p.toLowerCase()));
249
- const allowedModalities = modalities ? new Set(modalities) : null;
250
- return (m) => {
251
- const provider = (m._provider ?? m.provider ?? "").toLowerCase();
252
- if (excluded.has(provider)) return false;
253
- if (allowedModalities && m.architecture?.modality && !allowedModalities.has(m.architecture.modality)) return false;
254
- return true;
255
- };
256
- }, [modalities, excludeProviders]);
257
- const eligible = React2.useMemo(
258
- () => deduped.filter(passesCatalogFilters),
259
- [deduped, passesCatalogFilters]
260
- );
261
- const filtered = React2.useMemo(() => {
262
- const q = query.trim().toLowerCase();
263
- if (!q) return eligible;
264
- return eligible.filter((m) => {
265
- const provider = (m._provider ?? m.provider ?? "").toLowerCase();
266
- const id = canonicalModelId(m).toLowerCase();
267
- const name = (m.name ?? "").toLowerCase();
268
- const identity = resolveModelBrandIdentity(m);
269
- return id.includes(q) || name.includes(q) || provider.includes(q) || identity.host.label.toLowerCase().includes(q) || identity.lab.label.toLowerCase().includes(q);
270
- });
271
- }, [eligible, query]);
272
- const current = React2.useMemo(
273
- () => models.find((m) => canonicalModelId(m) === value),
274
- [models, value]
275
- );
276
- const currentIdentity = current ? resolveModelBrandIdentity(current) : null;
277
- const currentLabel = stripBrandPrefix(
278
- current?.name ?? current?.id ?? value,
279
- currentIdentity
280
- );
281
- const recentIds = React2.useMemo(() => {
282
- if (!recents?.length) return [];
283
- const lookup = new Map(models.map((m) => [canonicalModelId(m), m]));
284
- return recents.map((id) => lookup.get(id)).filter((m) => Boolean(m)).filter(passesCatalogFilters).slice(0, 4);
285
- }, [recents, models, passesCatalogFilters]);
286
- const popularModels = React2.useMemo(() => {
287
- if (!popular?.length) return [];
288
- const lookup = new Map(models.map((m) => [canonicalModelId(m), m]));
289
- return popular.map((id) => lookup.get(id)).filter((m) => Boolean(m)).filter(passesCatalogFilters);
290
- }, [popular, models, passesCatalogFilters]);
291
- const featuredModels = React2.useMemo(() => {
292
- const flagged = eligible.filter((m) => m.featured);
293
- if (flagged.length > 0) return flagged;
294
- const seedKeys = new Set(
295
- DEFAULT_FEATURED_MODEL_IDS.map((id) => modelDedupKey({ id }))
296
- );
297
- const seeded = eligible.filter((m) => seedKeys.has(modelDedupKey(m)));
298
- const order = DEFAULT_FEATURED_MODEL_IDS.map((id) => modelDedupKey({ id }));
299
- return seeded.sort(
300
- (a, b) => order.indexOf(modelDedupKey(a)) - order.indexOf(modelDedupKey(b))
301
- );
302
- }, [eligible]);
303
- const grouped = React2.useMemo(() => {
304
- const curatedKeys = new Set(
305
- [...featuredModels, ...popularModels, ...recentIds].map(
306
- (m) => modelDedupKey(m)
307
- )
308
- );
309
- const groups = /* @__PURE__ */ new Map();
310
- for (const m of filtered) {
311
- if (!query.trim() && curatedKeys.has(modelDedupKey(m))) continue;
312
- const key = modelGroupKey(m);
313
- const list = groups.get(key);
314
- if (list) list.push(m);
315
- else groups.set(key, [m]);
316
- }
317
- return Array.from(groups.entries()).map(([key, list]) => [key, [...list].sort(compareModelsByDisplayName)]).sort(([a], [b]) => compareModelGroups(a, b));
318
- }, [filtered, featuredModels, popularModels, recentIds, query]);
319
- const handleSelect = (id) => {
320
- onChange(id);
321
- setOpen(false);
322
- setQuery("");
323
- };
324
- const triggerLabel = label ? `${label}: ${currentLabel || placeholder}` : currentLabel || placeholder;
325
- const trigger = variant === "pill" ? /* @__PURE__ */ jsxs2(
326
- "button",
327
- {
328
- type: "button",
329
- "aria-label": triggerLabel,
330
- disabled,
331
- className: cn(
332
- "inline-flex h-9 items-center gap-1.5 rounded-full border border-[var(--md3-outline-variant)] bg-surface-container",
333
- "px-3 text-sm font-medium text-foreground shadow-sm",
334
- "transition-colors duration-[var(--transition-fast)]",
335
- "hover:border-[var(--md3-outline-variant)] hover:bg-surface-container-high",
336
- "focus:outline-none focus-visible:ring-1 focus-visible:ring-ring",
337
- "data-[state=open]:border-[var(--md3-outline)] data-[state=open]:bg-surface-container-high",
338
- "disabled:opacity-50 disabled:cursor-not-allowed",
339
- className,
340
- triggerClassName
341
- ),
342
- children: [
343
- currentIdentity ? /* @__PURE__ */ jsx2(ModelBrandStack, { identity: currentIdentity, size: "sm" }) : /* @__PURE__ */ jsx2(Sparkles, { className: "h-3.5 w-3.5 text-muted-foreground" }),
344
- /* @__PURE__ */ jsx2("span", { className: "truncate max-w-[180px]", children: currentLabel || placeholder }),
345
- /* @__PURE__ */ jsx2(ChevronDown, { className: "ml-auto h-3.5 w-3.5 shrink-0 text-muted-foreground transition-transform data-[state=open]:rotate-180" })
346
- ]
347
- }
348
- ) : /* @__PURE__ */ jsxs2(
349
- "button",
350
- {
351
- type: "button",
352
- "aria-label": triggerLabel,
353
- disabled,
354
- className: cn(
355
- "flex w-full items-center justify-between gap-2 rounded-[var(--radius-md)]",
356
- "border border-[var(--md3-outline-variant)] bg-surface-container px-3 py-2.5 text-sm text-left",
357
- "transition-colors duration-[var(--transition-fast)]",
358
- "hover:border-[var(--md3-outline-variant)] hover:bg-surface-container-high",
359
- "focus:outline-none focus-visible:ring-1 focus-visible:ring-ring",
360
- "data-[state=open]:border-[var(--md3-outline)] data-[state=open]:bg-surface-container-high",
361
- "disabled:opacity-50 disabled:cursor-not-allowed",
362
- className,
363
- triggerClassName
364
- ),
365
- children: [
366
- /* @__PURE__ */ jsxs2("span", { className: "flex min-w-0 items-center gap-2", children: [
367
- currentIdentity && /* @__PURE__ */ jsx2(ModelBrandStack, { identity: currentIdentity, size: "sm" }),
368
- /* @__PURE__ */ jsx2("span", { className: cn("truncate", current ? "text-foreground font-medium" : "text-muted-foreground"), children: currentLabel || placeholder })
369
- ] }),
370
- /* @__PURE__ */ jsx2(ChevronDown, { className: "h-4 w-4 shrink-0 text-muted-foreground transition-transform data-[state=open]:rotate-180" })
371
- ]
372
- }
373
- );
374
- return /* @__PURE__ */ jsxs2("div", { className: cn(variant === "field" ? "space-y-1.5" : "inline-flex", variant === "field" ? className : void 0), children: [
375
- variant === "field" && label && /* @__PURE__ */ jsx2("label", { className: "block text-xs font-medium text-muted-foreground uppercase tracking-[0.06em]", children: label }),
376
- /* @__PURE__ */ jsxs2(Popover.Root, { open, onOpenChange: setOpen, modal: false, children: [
377
- /* @__PURE__ */ jsx2(Popover.Trigger, { asChild: true, children: trigger }),
378
- /* @__PURE__ */ jsx2(Popover.Portal, { children: /* @__PURE__ */ jsxs2(
379
- Popover.Content,
380
- {
381
- side,
382
- sideOffset: 4,
383
- align: variant === "pill" ? "start" : "start",
384
- avoidCollisions,
385
- collisionPadding: 24,
386
- className: pickerMenuContentClass,
387
- children: [
388
- /* @__PURE__ */ jsx2(
389
- PickerMenuSearch,
390
- {
391
- value: query,
392
- onChange: setQuery,
393
- placeholder: "Search models...",
394
- loading,
395
- inputRef: searchInputRef
396
- }
397
- ),
398
- /* @__PURE__ */ jsxs2("div", { className: pickerMenuBodyClass, children: [
399
- !query && featuredModels.length > 0 && /* @__PURE__ */ jsx2(Section, { label: "Recommended", tone: "featured", children: featuredModels.map((m) => /* @__PURE__ */ jsx2(
400
- ModelRow,
401
- {
402
- model: m,
403
- active: canonicalModelId(m) === value,
404
- onSelect: handleSelect,
405
- featured: true
406
- },
407
- `featured-${canonicalModelId(m)}`
408
- )) }),
409
- !query && popularModels.length > 0 && /* @__PURE__ */ jsx2(Section, { label: "Top models", tone: "featured", children: popularModels.map((m) => /* @__PURE__ */ jsx2(
410
- ModelRow,
411
- {
412
- model: m,
413
- active: canonicalModelId(m) === value,
414
- onSelect: handleSelect,
415
- featured: true
416
- },
417
- `popular-${canonicalModelId(m)}`
418
- )) }),
419
- !query && recentIds.length > 0 && /* @__PURE__ */ jsx2(Section, { label: "Recent", children: recentIds.map((m) => /* @__PURE__ */ jsx2(
420
- ModelRow,
421
- {
422
- model: m,
423
- active: canonicalModelId(m) === value,
424
- onSelect: handleSelect
425
- },
426
- `recent-${canonicalModelId(m)}`
427
- )) }),
428
- grouped.length === 0 ? /* @__PURE__ */ jsx2("div", { className: "px-3 py-8 text-center text-xs text-muted-foreground", children: loading ? "Loading models..." : query ? "No models match." : "No models available." }) : grouped.map(([groupKey, list]) => /* @__PURE__ */ jsx2(Section, { label: modelGroupLabel(groupKey), children: list.map((m) => /* @__PURE__ */ jsx2(
429
- ModelRow,
430
- {
431
- model: m,
432
- active: canonicalModelId(m) === value,
433
- onSelect: handleSelect
434
- },
435
- canonicalModelId(m)
436
- )) }, groupKey))
437
- ] }),
438
- /* @__PURE__ */ jsxs2(PickerMenuFooter, { children: [
439
- filtered.length,
440
- " of ",
441
- eligible.length,
442
- " model",
443
- eligible.length === 1 ? "" : "s"
444
- ] })
445
- ]
446
- }
447
- ) })
448
- ] })
449
- ] });
450
- }
451
- function Section({ label, children, tone }) {
452
- const identity = brandInfo(normalizeBrandKey(label));
453
- return /* @__PURE__ */ jsx2(
454
- PickerMenuSection,
455
- {
456
- label,
457
- tone,
458
- glyph: identity.key !== "unknown" ? /* @__PURE__ */ jsx2(BrandLogo, { brand: identity, size: "xs" }) : void 0,
459
- children
460
- }
461
- );
462
- }
463
- function PickerItem({
464
- onSelect,
465
- active,
466
- featured,
467
- children
468
- }) {
469
- return /* @__PURE__ */ jsx2(
470
- Popover.Item,
471
- {
472
- onSelect: (e) => {
473
- e.preventDefault();
474
- onSelect();
475
- },
476
- className: pickerMenuItemClass({ active, featured }),
477
- children
478
- }
479
- );
480
- }
481
- function ModelRow({
482
- model,
483
- active,
484
- onSelect,
485
- featured
486
- }) {
487
- const id = canonicalModelId(model);
488
- const pricing = formatPricing(model.pricing);
489
- const ctx = formatContext(model.context_length);
490
- const identity = resolveModelBrandIdentity(model);
491
- const via = !identity.combined ? identity.host.label : null;
492
- return /* @__PURE__ */ jsxs2(PickerItem, { onSelect: () => onSelect(id), active, featured, children: [
493
- /* @__PURE__ */ jsx2(ModelBrandStack, { identity, size: "md" }),
494
- /* @__PURE__ */ jsxs2("div", { className: "min-w-0 flex-1", children: [
495
- /* @__PURE__ */ jsxs2("div", { className: "flex items-center gap-1.5", children: [
496
- /* @__PURE__ */ jsx2("span", { className: "min-w-0 truncate text-sm font-medium", children: model.name ?? model.id }),
497
- via && /* @__PURE__ */ jsxs2(PickerMenuTag, { children: [
498
- "via ",
499
- via
500
- ] })
501
- ] }),
502
- /* @__PURE__ */ jsx2(
503
- PickerMenuMeta,
504
- {
505
- parts: [ctx, pricing ? /* @__PURE__ */ jsx2("span", { className: "font-mono", children: pricing }) : null]
506
- }
507
- )
508
- ] })
509
- ] });
510
- }
511
-
512
- export {
513
- pickerMenuContentClass,
514
- pickerMenuBodyClass,
515
- pickerMenuItemClass,
516
- PickerMenuSearch,
517
- PickerMenuSection,
518
- PickerMenuFooter,
519
- PickerMenuMeta,
520
- PickerMenuTag,
521
- modelDedupKey,
522
- dedupeModels,
523
- DEFAULT_FEATURED_MODEL_IDS,
524
- isTextChatModel,
525
- formatPricing,
526
- formatContext,
527
- stripBrandPrefix,
528
- ModelPicker
529
- };