@tangle-network/agent-app 0.60.153 → 0.60.154

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.
@@ -91,6 +91,32 @@ export declare function OptionPill({ label, value, choices, onSelect, bandRef, c
91
91
  }) => ReactNode;
92
92
  };
93
93
  }): JSX.Element;
94
+ export interface GenerationSetting {
95
+ key: string;
96
+ label: string;
97
+ summary: string;
98
+ value: ModelOptionValue | undefined;
99
+ choices: readonly OptionChoice[];
100
+ onSelect: (value: ModelOptionValue) => void;
101
+ icon?: LucideIcon;
102
+ /** A binary setting acts immediately instead of opening a two-row menu. */
103
+ onToggle?: () => void;
104
+ custom?: {
105
+ label: string;
106
+ render: (args: {
107
+ close: () => void;
108
+ }) => ReactNode;
109
+ };
110
+ }
111
+ /** The model's current defaults and optional parameters in one compact menu.
112
+ * The value list opens inside the panel, so no nested portal can be clipped or
113
+ * mistaken for an outside click. A long duration or voice list scrolls within
114
+ * 16rem while the rest of the settings stay in place. */
115
+ export declare function GenerationSettings({ settings, bandRef, resetKey, }: {
116
+ settings: readonly GenerationSetting[];
117
+ bandRef: RefObject<HTMLDivElement | null>;
118
+ resetKey: string;
119
+ }): JSX.Element;
94
120
  /**
95
121
  * The custom-size form the Size menu swaps to.
96
122
  *
@@ -127,7 +127,7 @@ import {
127
127
  Monitor,
128
128
  Ratio,
129
129
  Scaling,
130
- SlidersHorizontal,
130
+ SlidersHorizontal as SlidersHorizontal2,
131
131
  Sparkles,
132
132
  TriangleAlert,
133
133
  Video
@@ -140,7 +140,7 @@ import {
140
140
  useId,
141
141
  useState as useState2
142
142
  } from "react";
143
- import { ImagePlus, Volume2, VolumeX, X } from "lucide-react";
143
+ import { ImagePlus, SlidersHorizontal, Volume2, VolumeX, X } from "lucide-react";
144
144
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
145
145
  var PILL = "inline-flex h-7 flex-none items-center gap-1.5 whitespace-nowrap rounded-full border border-border bg-card px-2.5 text-[14px] font-medium text-foreground transition hover:bg-accent";
146
146
  var PILL_LABEL = "[text-box:trim-both_cap_alphabetic]";
@@ -403,6 +403,166 @@ function OptionPill({
403
403
  )
404
404
  ] });
405
405
  }
406
+ function GenerationSettings({
407
+ settings,
408
+ bandRef,
409
+ resetKey
410
+ }) {
411
+ const [open, setOpen] = useState2(false);
412
+ const [more, setMore] = useState2(false);
413
+ const [active, setActive] = useState2(null);
414
+ const [customOpen, setCustomOpen] = useState2(false);
415
+ const [focusAfterSelect, setFocusAfterSelect] = useState2(null);
416
+ const { containerRef, triggerRef, panelRef, triggerProps } = usePopover(open, setOpen);
417
+ const panelId = useId();
418
+ useCloseWhenScrolledOut(open, setOpen, triggerRef, bandRef);
419
+ useEffect2(() => {
420
+ setOpen(false);
421
+ setMore(false);
422
+ setActive(null);
423
+ setCustomOpen(false);
424
+ setFocusAfterSelect(null);
425
+ }, [resetKey]);
426
+ useEffect2(() => {
427
+ if (open) panelRef.current?.querySelector("[data-setting-key]")?.focus();
428
+ }, [open, panelRef]);
429
+ useEffect2(() => {
430
+ if (!focusAfterSelect) return;
431
+ const rows = panelRef.current?.querySelectorAll("[data-setting-key]");
432
+ Array.from(rows ?? []).find((row) => row.dataset.settingKey === focusAfterSelect)?.focus();
433
+ setFocusAfterSelect(null);
434
+ }, [focusAfterSelect, panelRef]);
435
+ const primary = settings.slice(0, 3);
436
+ const visible = more ? settings : primary;
437
+ const summary = primary.map((setting) => setting.summary).join(" \xB7 ");
438
+ const fullSummary = settings.map((setting) => `${setting.label}: ${setting.summary}`).join(", ");
439
+ return /* @__PURE__ */ jsxs("div", { ref: containerRef, className: "relative inline-flex flex-none", children: [
440
+ /* @__PURE__ */ jsxs(
441
+ "button",
442
+ {
443
+ type: "button",
444
+ ...triggerProps,
445
+ "aria-haspopup": "dialog",
446
+ "aria-controls": open ? panelId : void 0,
447
+ "aria-label": `Generation settings: ${fullSummary}`,
448
+ title: "Generation settings",
449
+ onClick: () => setOpen(!open),
450
+ className: PILL,
451
+ children: [
452
+ /* @__PURE__ */ jsx(SlidersHorizontal, { "aria-hidden": true, className: "h-3.5 w-3.5 shrink-0 text-muted-foreground", strokeWidth: 1.5 }),
453
+ /* @__PURE__ */ jsx("span", { className: PILL_LABEL, children: "Settings" }),
454
+ /* @__PURE__ */ jsx("span", { "aria-hidden": true, className: "hidden max-w-[150px] truncate text-muted-foreground @[640px]/studio-composer:inline", children: summary }),
455
+ /* @__PURE__ */ jsx(ChevronDown, { "aria-hidden": true, className: "h-3 w-3 shrink-0 text-muted-foreground" })
456
+ ]
457
+ }
458
+ ),
459
+ /* @__PURE__ */ jsxs(
460
+ PopoverSurface,
461
+ {
462
+ open,
463
+ id: panelId,
464
+ role: "dialog",
465
+ "aria-label": "Generation settings",
466
+ triggerRef,
467
+ panelRef,
468
+ contentKey: `${active}:${more}:${customOpen}`,
469
+ className: `${MENU_PANEL} w-[min(320px,calc(100vw-24px))] gap-0.5`,
470
+ children: [
471
+ /* @__PURE__ */ jsx("div", { className: MENU_HEADER, children: "Generation settings" }),
472
+ visible.map((setting) => {
473
+ const Icon = setting.icon;
474
+ const expanded = active === setting.key;
475
+ return /* @__PURE__ */ jsx("div", { children: setting.onToggle ? /* @__PURE__ */ jsxs(
476
+ "button",
477
+ {
478
+ type: "button",
479
+ role: "switch",
480
+ "data-setting-key": setting.key,
481
+ "aria-checked": setting.value === true,
482
+ "aria-label": setting.label,
483
+ onClick: setting.onToggle,
484
+ className: menuRowClass(false),
485
+ children: [
486
+ Icon && /* @__PURE__ */ jsx(Icon, { "aria-hidden": true, className: "h-3.5 w-3.5 shrink-0", strokeWidth: 1.5 }),
487
+ /* @__PURE__ */ jsx("span", { children: setting.label }),
488
+ /* @__PURE__ */ jsx("span", { className: "ml-auto text-muted-foreground", children: setting.summary })
489
+ ]
490
+ }
491
+ ) : /* @__PURE__ */ jsxs(Fragment, { children: [
492
+ /* @__PURE__ */ jsxs(
493
+ "button",
494
+ {
495
+ type: "button",
496
+ "data-setting-key": setting.key,
497
+ "aria-expanded": expanded,
498
+ "aria-label": `${setting.label}: ${setting.summary}`,
499
+ onClick: () => {
500
+ setActive(expanded ? null : setting.key);
501
+ setCustomOpen(false);
502
+ },
503
+ className: menuRowClass(expanded),
504
+ children: [
505
+ Icon && /* @__PURE__ */ jsx(Icon, { "aria-hidden": true, className: "h-3.5 w-3.5 shrink-0", strokeWidth: 1.5 }),
506
+ /* @__PURE__ */ jsx("span", { children: setting.label }),
507
+ /* @__PURE__ */ jsx("span", { className: "ml-auto truncate text-muted-foreground", children: setting.summary }),
508
+ /* @__PURE__ */ jsx(ChevronDown, { "aria-hidden": true, className: "h-3 w-3 shrink-0 text-muted-foreground" })
509
+ ]
510
+ }
511
+ ),
512
+ expanded && /* @__PURE__ */ jsx("div", { role: "group", "aria-label": `${setting.label} choices`, className: "ml-5 max-h-64 overflow-y-auto overscroll-contain border-l border-border pl-1", children: customOpen && setting.custom ? setting.custom.render({ close: () => {
513
+ setActive(null);
514
+ setCustomOpen(false);
515
+ setFocusAfterSelect(setting.key);
516
+ } }) : /* @__PURE__ */ jsxs(Fragment, { children: [
517
+ setting.choices.map((choice, index) => /* @__PURE__ */ jsxs(
518
+ "label",
519
+ {
520
+ className: `${menuRowClass(choice.value === setting.value)} relative focus-within:outline focus-within:outline-2 focus-within:outline-ring`,
521
+ children: [
522
+ /* @__PURE__ */ jsx(
523
+ "input",
524
+ {
525
+ type: "radio",
526
+ name: `${panelId}-${setting.key}`,
527
+ value: String(choice.value),
528
+ checked: choice.value === setting.value,
529
+ onChange: () => {
530
+ setting.onSelect(choice.value);
531
+ setActive(null);
532
+ setFocusAfterSelect(setting.key);
533
+ },
534
+ className: "absolute inset-0 h-full w-full cursor-pointer opacity-0"
535
+ }
536
+ ),
537
+ /* @__PURE__ */ jsx("span", { className: "truncate", children: choice.label }),
538
+ choice.value === setting.value && /* @__PURE__ */ jsx(CheckGlyph, { "aria-hidden": true, className: "ml-auto h-3.5 w-3.5 shrink-0 text-primary" })
539
+ ]
540
+ },
541
+ `${String(choice.value)}-${index}`
542
+ )),
543
+ setting.custom && /* @__PURE__ */ jsx("button", { type: "button", onClick: () => setCustomOpen(true), className: `${menuRowClass(false)} text-muted-foreground`, children: setting.custom.label })
544
+ ] }) })
545
+ ] }) }, setting.key);
546
+ }),
547
+ settings.length > 3 && /* @__PURE__ */ jsx(
548
+ "button",
549
+ {
550
+ type: "button",
551
+ "aria-expanded": more,
552
+ onClick: () => {
553
+ setMore(!more);
554
+ setActive(null);
555
+ setCustomOpen(false);
556
+ },
557
+ className: `${menuRowClass(false)} text-primary`,
558
+ children: more ? "Less" : `More settings (${settings.length - 3})`
559
+ }
560
+ )
561
+ ]
562
+ }
563
+ )
564
+ ] });
565
+ }
406
566
  function CustomSizeForm({
407
567
  initial,
408
568
  onApply,
@@ -681,7 +841,7 @@ var SEGMENTS = [
681
841
  { type: "video", label: "Video", icon: Video },
682
842
  { type: "speech", label: "Audio", icon: AudioLines }
683
843
  ];
684
- var PILL_ORDER = {
844
+ var PARAM_ORDER = {
685
845
  image: ["size", "quality", "n"],
686
846
  video: ["duration", "resolution", "aspect_ratio", "mode"],
687
847
  speech: ["voice", "speed"]
@@ -704,7 +864,7 @@ var PARAM_ICONS = {
704
864
  duration: Clock,
705
865
  resolution: Monitor,
706
866
  aspect_ratio: Ratio,
707
- mode: SlidersHorizontal,
867
+ mode: SlidersHorizontal2,
708
868
  voice: Mic,
709
869
  speed: Gauge
710
870
  };
@@ -743,7 +903,7 @@ function pickerModel(model, type) {
743
903
  }
744
904
  function visibleParams(type, options) {
745
905
  if (!options) return [];
746
- return PILL_ORDER[type].filter((param) => {
906
+ return PARAM_ORDER[type].filter((param) => {
747
907
  const meta = options[param];
748
908
  return Boolean(meta) && meta?.supported !== false;
749
909
  });
@@ -1019,6 +1179,50 @@ function StudioComposer({
1019
1179
  const modelDisplayName = laneDown ? "Unavailable" : modelOption?.name || modelId || "Select a model";
1020
1180
  const modelUnavailable = laneDown || modelOption?.status === "unavailable";
1021
1181
  const canReloadModels = Boolean(workspaceId) && !loadingModels && Boolean(catalogError || catalogReady && curatedModels.length === 0);
1182
+ const settingParams = type === "video" ? ["aspect_ratio", "duration", ...params.filter((param) => param !== "aspect_ratio" && param !== "duration")].filter((param) => params.includes(param)) : params;
1183
+ const settings = settingParams.map((param) => {
1184
+ const value = values[param] ?? options?.[param]?.default;
1185
+ const choices = choicesForSetting(param, options?.[param], value);
1186
+ const label = PARAM_LABELS[param] ?? param;
1187
+ return {
1188
+ key: param,
1189
+ label,
1190
+ summary: choices.find((choice) => choice.value === value)?.label ?? "\u2014",
1191
+ value,
1192
+ choices,
1193
+ onSelect: (next) => setOption(param, next),
1194
+ icon: PARAM_ICONS[param] ?? SlidersHorizontal2,
1195
+ ...param === "size" && supportsCustomImageSize(modelId) ? {
1196
+ custom: {
1197
+ label: "Custom\u2026",
1198
+ render: ({ close }) => /* @__PURE__ */ jsx2(
1199
+ CustomSizeForm,
1200
+ {
1201
+ initial: asText(value),
1202
+ onApply: (size) => {
1203
+ setOption("size", size);
1204
+ close();
1205
+ },
1206
+ onCancel: close
1207
+ }
1208
+ )
1209
+ }
1210
+ } : {}
1211
+ };
1212
+ });
1213
+ if (audioSupported) {
1214
+ const audioOn = (values.audio ?? audioMeta?.default) === true;
1215
+ settings.splice(Math.min(2, settings.length), 0, {
1216
+ key: "audio",
1217
+ label: "Audio",
1218
+ summary: audioOn ? "Audio on" : "Audio off",
1219
+ value: audioOn,
1220
+ choices: [],
1221
+ onSelect: (next) => setOption("audio", next),
1222
+ onToggle: () => setOption("audio", !audioOn),
1223
+ icon: AudioLines
1224
+ });
1225
+ }
1022
1226
  function reloadModels() {
1023
1227
  if (!workspaceId || catalogLoading || catalogPendingRef.current) return;
1024
1228
  catalogPendingRef.current = true;
@@ -1073,38 +1277,7 @@ function StudioComposer({
1073
1277
  ] })
1074
1278
  }
1075
1279
  ) }),
1076
- params.map((param) => /* @__PURE__ */ jsx2("div", { className: "studio-pill-in flex-none", children: /* @__PURE__ */ jsx2(
1077
- OptionPill,
1078
- {
1079
- label: PARAM_LABELS[param] ?? param,
1080
- icon: PARAM_ICONS[param] ?? SlidersHorizontal,
1081
- value: values[param],
1082
- choices: choicesForPill(param, options?.[param], values[param]),
1083
- onSelect: (value) => setOption(param, value),
1084
- bandRef,
1085
- custom: param === "size" && supportsCustomImageSize(modelId) ? {
1086
- label: "Custom\u2026",
1087
- render: ({ close }) => /* @__PURE__ */ jsx2(
1088
- CustomSizeForm,
1089
- {
1090
- initial: asText(values.size),
1091
- onApply: (size) => {
1092
- setOption("size", size);
1093
- close();
1094
- },
1095
- onCancel: close
1096
- }
1097
- )
1098
- } : void 0
1099
- }
1100
- ) }, `${type}-${param}`)),
1101
- audioSupported && /* @__PURE__ */ jsx2("div", { className: "studio-pill-in flex-none", children: /* @__PURE__ */ jsx2(
1102
- AudioTogglePill,
1103
- {
1104
- on: values.audio === true,
1105
- onToggle: (on) => setOption("audio", on)
1106
- }
1107
- ) }, `${type}-audio`),
1280
+ settings.length > 0 && /* @__PURE__ */ jsx2("div", { className: "studio-pill-in flex-none", children: /* @__PURE__ */ jsx2(GenerationSettings, { settings, bandRef, resetKey: `${type}:${modelId}` }) }),
1108
1281
  referenceSupported && /* @__PURE__ */ jsx2("div", { className: "studio-pill-in flex-none", children: /* @__PURE__ */ jsx2(
1109
1282
  ReferencePill,
1110
1283
  {
@@ -1140,7 +1313,7 @@ function asText(value) {
1140
1313
  function asWireScalar(value) {
1141
1314
  return typeof value === "string" || typeof value === "number" ? value : void 0;
1142
1315
  }
1143
- function choicesForPill(param, meta, value) {
1316
+ function choicesForSetting(param, meta, value) {
1144
1317
  if (!meta) return [];
1145
1318
  const choices = choicesFor(param, meta).map((choice) => ({
1146
1319
  value: choice,
@@ -3334,6 +3507,7 @@ export {
3334
3507
  ComposerBand,
3335
3508
  CustomSizeForm,
3336
3509
  GenerationNoticeChip,
3510
+ GenerationSettings,
3337
3511
  MediaTile,
3338
3512
  MediaTypeSegments,
3339
3513
  MediaViewerModal,