@tangle-network/agent-app 0.46.7 → 0.46.9

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.
@@ -0,0 +1,12 @@
1
+ import type { ModelOptionValue } from '../studio';
2
+ import type { ComposerType } from './studio-composer';
3
+ export interface PersistedComposerSelections {
4
+ v: 1;
5
+ type: ComposerType;
6
+ selectedModels: Partial<Record<ComposerType, string>>;
7
+ /** Option values are model-specific: two models in one lane may publish
8
+ * different enums for the same parameter. */
9
+ optionsByModel: Record<string, Record<string, ModelOptionValue>>;
10
+ }
11
+ export declare function loadComposerSelections(workspaceId: string): PersistedComposerSelections | null;
12
+ export declare function saveComposerSelections(workspaceId: string, snapshot: PersistedComposerSelections): void;
@@ -625,7 +625,7 @@ function ModelPill({
625
625
  children: [
626
626
  unavailable && /* @__PURE__ */ jsx(TriangleAlert, { "aria-hidden": true, className: "h-3.5 w-3.5 shrink-0 text-warning", strokeWidth: 2 }),
627
627
  provider && /* @__PURE__ */ jsx(ProviderLogo, { provider, size: 14 }),
628
- /* @__PURE__ */ jsx("span", { className: `max-w-[168px] truncate ${PILL_LABEL}`, children: displayName }),
628
+ /* @__PURE__ */ jsx("span", { className: "max-w-[168px] truncate leading-normal", children: displayName }),
629
629
  /* @__PURE__ */ jsx(ChevronDown, { className: "h-3 w-3 shrink-0 text-muted-foreground" })
630
630
  ]
631
631
  }
@@ -685,6 +685,58 @@ function optionValueLabel(param, value) {
685
685
  return text.charAt(0).toUpperCase() + text.slice(1);
686
686
  }
687
687
 
688
+ // src/studio-react/composer-persistence.ts
689
+ var COMPOSER_TYPES = ["image", "video", "speech"];
690
+ function storageKey(workspaceId) {
691
+ return `studio-composer:${workspaceId}`;
692
+ }
693
+ function isPlainObject(value) {
694
+ if (value === null || typeof value !== "object") return false;
695
+ const prototype = Object.getPrototypeOf(value);
696
+ return prototype === Object.prototype || prototype === null;
697
+ }
698
+ function isComposerType(value) {
699
+ return COMPOSER_TYPES.some((type) => type === value);
700
+ }
701
+ function isOptionValue(value) {
702
+ return typeof value === "string" || typeof value === "boolean" || typeof value === "number" && Number.isFinite(value);
703
+ }
704
+ function parseSelections(value) {
705
+ if (!isPlainObject(value) || value.v !== 1 || !isComposerType(value.type)) return null;
706
+ if (!isPlainObject(value.selectedModels) || !isPlainObject(value.optionsByModel)) return null;
707
+ const selectedModels = {};
708
+ for (const type of COMPOSER_TYPES) {
709
+ const modelId = value.selectedModels[type];
710
+ if (typeof modelId === "string") selectedModels[type] = modelId;
711
+ }
712
+ const optionsByModel = {};
713
+ for (const [modelId, rawOptions] of Object.entries(value.optionsByModel)) {
714
+ if (!isPlainObject(rawOptions)) continue;
715
+ const options = {};
716
+ for (const [param, optionValue] of Object.entries(rawOptions)) {
717
+ if (isOptionValue(optionValue)) options[param] = optionValue;
718
+ }
719
+ optionsByModel[modelId] = options;
720
+ }
721
+ return { v: 1, type: value.type, selectedModels, optionsByModel };
722
+ }
723
+ function loadComposerSelections(workspaceId) {
724
+ if (typeof window === "undefined") return null;
725
+ try {
726
+ const raw = window.localStorage.getItem(storageKey(workspaceId));
727
+ return raw === null ? null : parseSelections(JSON.parse(raw));
728
+ } catch {
729
+ return null;
730
+ }
731
+ }
732
+ function saveComposerSelections(workspaceId, snapshot) {
733
+ if (typeof window === "undefined") return;
734
+ try {
735
+ window.localStorage.setItem(storageKey(workspaceId), JSON.stringify(snapshot));
736
+ } catch {
737
+ }
738
+ }
739
+
688
740
  // src/studio-react/studio-composer.tsx
689
741
  import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
690
742
  var SEGMENTS = [
@@ -760,8 +812,26 @@ function StudioComposer({
760
812
  const [referenceImageUrl, setReferenceImageUrl] = useState3(null);
761
813
  const [isSubmitting, setIsSubmitting] = useState3(false);
762
814
  const [error, setError] = useState3(null);
815
+ const [hydratedWorkspaceId, setHydratedWorkspaceId] = useState3(null);
763
816
  const submitLockRef = useRef2(false);
764
817
  const bandRef = useRef2(null);
818
+ const hydratedRef = useRef2(false);
819
+ const persistedOptionsRef = useRef2({});
820
+ useEffect3(() => {
821
+ hydratedRef.current = false;
822
+ setHydratedWorkspaceId(null);
823
+ if (!workspaceId) return;
824
+ const persisted = loadComposerSelections(workspaceId);
825
+ if (persisted) {
826
+ setType(persisted.type);
827
+ setSelectedModels(persisted.selectedModels);
828
+ persistedOptionsRef.current = persisted.optionsByModel;
829
+ } else {
830
+ persistedOptionsRef.current = {};
831
+ }
832
+ hydratedRef.current = true;
833
+ setHydratedWorkspaceId(workspaceId);
834
+ }, [workspaceId]);
765
835
  useEffect3(() => {
766
836
  if (!workspaceId) return;
767
837
  let cancelled = false;
@@ -814,13 +884,35 @@ function StudioComposer({
814
884
  );
815
885
  useEffect3(() => {
816
886
  setOptionValues((current) => {
817
- const next = reconcileOptionValues(options, current[type], {
887
+ const seed = { ...persistedOptionsRef.current[modelId], ...current[type] };
888
+ const next = reconcileOptionValues(options, seed, {
818
889
  allowCustomSize: supportsCustomImageSize(modelId)
819
890
  });
820
891
  const unchanged = Object.keys(next).length === Object.keys(current[type]).length && Object.entries(next).every(([key, value]) => current[type][key] === value);
821
892
  return unchanged ? current : { ...current, [type]: next };
822
893
  });
823
894
  }, [modelId, options, type]);
895
+ useEffect3(() => {
896
+ if (!workspaceId || !hydratedRef.current || hydratedWorkspaceId !== workspaceId) return;
897
+ if (!catalog && modelId && persistedOptionsRef.current[modelId] !== void 0 && !options) return;
898
+ const seed = { ...persistedOptionsRef.current[modelId], ...optionValues[type] };
899
+ const reconciled = reconcileOptionValues(options, seed, {
900
+ allowCustomSize: supportsCustomImageSize(modelId)
901
+ });
902
+ const reconciledReady = Object.keys(reconciled).length === Object.keys(optionValues[type]).length && Object.entries(reconciled).every(([key, value]) => optionValues[type][key] === value);
903
+ if (!reconciledReady) return;
904
+ const optionsByModel = {
905
+ ...persistedOptionsRef.current,
906
+ ...modelId ? { [modelId]: optionValues[type] } : {}
907
+ };
908
+ saveComposerSelections(workspaceId, {
909
+ v: 1,
910
+ type,
911
+ selectedModels,
912
+ optionsByModel
913
+ });
914
+ persistedOptionsRef.current = optionsByModel;
915
+ }, [catalog, hydratedWorkspaceId, modelId, optionValues, options, selectedModels, type, workspaceId]);
824
916
  const values = optionValues[type];
825
917
  const params = visibleParams(type, options);
826
918
  const audioMeta = type === "video" ? options?.audio : void 0;
@@ -1023,7 +1115,7 @@ function StudioComposer({
1023
1115
  title: "Generate",
1024
1116
  disabled: !canSubmit,
1025
1117
  onClick: () => void generate(),
1026
- className: "studio-send ml-auto inline-flex h-8 w-8 flex-none items-center justify-center rounded-full text-white transition disabled:opacity-40",
1118
+ className: "ml-auto inline-flex h-8 w-8 flex-none items-center justify-center rounded-full bg-foreground text-background transition hover:opacity-90 disabled:cursor-not-allowed disabled:opacity-40 focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-card",
1027
1119
  children: /* @__PURE__ */ jsx2(ArrowUp, { className: "h-4 w-4", strokeWidth: 2 })
1028
1120
  }
1029
1121
  )
@@ -2490,7 +2582,7 @@ function StudioHomeScreen({
2490
2582
  }
2491
2583
 
2492
2584
  // src/studio-react/studio-generation-screen.tsx
2493
- import { ArrowLeft } from "lucide-react";
2585
+ import { ArrowLeft, CircleAlert } from "lucide-react";
2494
2586
  import {
2495
2587
  useCallback as useCallback9,
2496
2588
  useEffect as useEffect12,
@@ -2618,12 +2710,17 @@ function StudioGenerationScreen({
2618
2710
  return /* @__PURE__ */ jsx11("div", { className: "studio-skeleton", style: { "--r": ratio } }, row.id);
2619
2711
  }
2620
2712
  if (generationStatus(row) === "failed") {
2621
- return /* @__PURE__ */ jsx11(
2713
+ const reason = generationError(row);
2714
+ return /* @__PURE__ */ jsxs10(
2622
2715
  "div",
2623
2716
  {
2624
- className: "grid place-items-center bg-accent p-3 text-center text-[12px] text-destructive",
2717
+ className: "flex flex-col items-center justify-center gap-1.5 bg-accent p-3 text-center",
2625
2718
  style: { aspectRatio: ratio },
2626
- children: generationError(row) ?? "Generation failed"
2719
+ children: [
2720
+ /* @__PURE__ */ jsx11(CircleAlert, { "aria-hidden": true, className: "h-4 w-4 flex-none text-destructive", strokeWidth: 2 }),
2721
+ /* @__PURE__ */ jsx11("p", { className: "text-[13px] font-medium text-foreground", children: "Generation failed" }),
2722
+ reason && reason !== "Generation failed" && /* @__PURE__ */ jsx11("p", { className: "line-clamp-3 text-[12px] text-muted-foreground", children: reason })
2723
+ ]
2627
2724
  },
2628
2725
  row.id
2629
2726
  );