mnm-matin commited on
Commit
004888d
·
verified ·
1 Parent(s): aae6bb6

Compose DeepFashion search demo panels

Browse files
.hyperview/extensions/fashion-search-readout/panel.js CHANGED
@@ -348,7 +348,7 @@ function SearchResultComparison({ item, models, loadingKey, onSelectResult }) {
348
  React.createElement(
349
  "div",
350
  { style: { color: colors.mutedText, fontSize: 10, marginTop: 3 } },
351
- "Green card = exact product. Ranked cards are the proof.",
352
  ),
353
  ),
354
  models.map((model) =>
@@ -475,7 +475,7 @@ function MiniStat({ label, value, accent = false }) {
475
  );
476
  }
477
 
478
- function BusinessRail({ item, metrics }) {
479
  if (!item) return null;
480
  const clipSummary = getSummary(item, "clip");
481
  const candidateSummary = getSummary(item, "candidate");
@@ -484,9 +484,10 @@ function BusinessRail({ item, metrics }) {
484
  "aside",
485
  {
486
  style: {
487
- flex: "0 1 280px",
488
- minWidth: 240,
489
- maxWidth: 300,
 
490
  display: "flex",
491
  flexDirection: "column",
492
  gap: 10,
@@ -605,6 +606,7 @@ export default function FashionSearchComparisonPanel() {
605
  const [loadingKey, setLoadingKey] = React.useState(null);
606
  const [selectedExampleId, setSelectedExampleId] = React.useState(null);
607
  const didApplyDemoLayout = React.useRef(false);
 
608
 
609
  const models = React.useMemo(() => normalizeModels(panelProps.models), [panelProps.models]);
610
  const examples = Array.isArray(panelProps.examples) ? panelProps.examples : [];
@@ -627,6 +629,22 @@ export default function FashionSearchComparisonPanel() {
627
  ];
628
 
629
  React.useEffect(() => {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
630
  if (didApplyDemoLayout.current) return;
631
  didApplyDemoLayout.current = true;
632
 
@@ -640,8 +658,6 @@ export default function FashionSearchComparisonPanel() {
640
  "scatter-euclidean-3d",
641
  "scatter-spherical-3d",
642
  "scatter-default",
643
- "fashion-map-clip",
644
- "fashion-map-hyper3",
645
  ].forEach((panelId) => {
646
  try {
647
  commands.closePanel?.(panelId);
@@ -649,11 +665,12 @@ export default function FashionSearchComparisonPanel() {
649
  // Older HyperView builds may not have every panel id; ignore missing panes.
650
  }
651
  });
652
- commands.focusPanel?.("fashion-text-search-readout");
 
653
  }, 350);
654
 
655
  return () => window.clearTimeout(timer);
656
- }, [commands]);
657
 
658
  const clearSelection = async () => {
659
  if (commands.setLabelFilter) commands.setLabelFilter(null);
@@ -681,6 +698,7 @@ export default function FashionSearchComparisonPanel() {
681
  focus: false,
682
  persist: false,
683
  });
 
684
  } catch (error) {
685
  const message = error instanceof Error ? error.message : String(error);
686
  setPanelError(`Could not select target item: ${message}`);
@@ -708,6 +726,7 @@ export default function FashionSearchComparisonPanel() {
708
  focus: false,
709
  persist: false,
710
  });
 
711
  } catch (error) {
712
  const message = error instanceof Error ? error.message : String(error);
713
  setPanelError(`Could not select ranked result: ${message}`);
@@ -716,130 +735,164 @@ export default function FashionSearchComparisonPanel() {
716
  }
717
  };
718
 
719
- return React.createElement(
720
- Panel,
721
- { className: "h-full" },
722
- React.createElement(PanelToolbar, {
723
- items: [
724
- { id: "dataset", label: "Data", value: "DeepFashion" },
725
- { id: "samples", label: "Items", value: String(samplesState.totalSamples ?? "-") },
726
- { id: "selected", label: "Selected", value: String(selection.selectedIds?.length ?? 0) },
727
- ],
728
- actions: React.createElement(PanelToolbarButton, { onClick: clearSelection }, "Reset"),
729
- }),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
730
  React.createElement(
731
  "div",
732
  {
733
  style: {
734
- height: "100%",
735
- overflow: "auto",
736
- padding: 14,
737
- background: colors.panelBg,
 
 
 
 
738
  },
739
  },
740
  React.createElement(
741
- "div",
742
  {
743
  style: {
744
- width: "100%",
745
- maxWidth: 1130,
746
- margin: "0 auto",
747
  display: "flex",
748
- gap: 12,
749
- alignItems: "flex-start",
750
- justifyContent: "center",
751
- flexWrap: "wrap",
752
  },
753
  },
754
  React.createElement(
755
- "main",
756
- {
757
- style: {
758
- flex: "1 1 680px",
759
- maxWidth: 820,
760
- minWidth: 0,
761
- display: "flex",
762
- flexDirection: "column",
763
- gap: 14,
764
- },
765
- },
766
- React.createElement(
767
- Section,
768
- { title: "Pick A Shopper Query" },
769
- React.createElement(
770
- TextBlock,
771
- null,
772
- "Pick a typed product search and compare where each model ranks the exact item.",
773
- ),
774
- examples.length
775
- ? React.createElement(ExampleChooser, {
776
- examples,
777
- selectedId: selectedExample?.id,
778
- onSelect: setSelectedExampleId,
779
- })
780
- : React.createElement(
781
- "div",
782
- { style: { color: colors.bodyText, fontSize: 12, lineHeight: 1.45 } },
783
- "Demo examples are not configured.",
784
- ),
785
- selectedExample && models.length
786
- ? React.createElement(SearchResultComparison, {
787
- item: selectedExample,
788
- models,
789
- loadingKey,
790
- onSelectResult: selectRankedResult,
791
- })
792
- : null,
793
- ),
794
  React.createElement(
795
- Section,
796
- { title: "Inspect Exact Target" },
797
- React.createElement(
798
- TextBlock,
799
- null,
800
- `Exact-target summary for: ${selectedExample?.targetTitle || "target product"}.`,
801
- ),
802
- selectedExample && models.length
803
- ? React.createElement(ExactTargetActions, {
804
- item: selectedExample,
805
- models,
806
- loadingKey,
807
- onSelectQuery: selectModelQuery,
808
- })
809
- : React.createElement(
810
- "div",
811
- { style: { color: colors.bodyText, fontSize: 12, lineHeight: 1.45 } },
812
- "Demo examples are not configured.",
813
- ),
814
  ),
815
- warnings.length
816
- ? React.createElement(
 
 
 
 
 
817
  "div",
818
- {
819
- style: {
820
- border: `1px solid ${colors.warningBorder}`,
821
- borderRadius: 4,
822
- background: colors.warningBg,
823
- color: colors.warningText,
824
- padding: 8,
825
- fontSize: 11,
826
- lineHeight: 1.35,
827
- },
828
- },
829
- warnings[0],
830
- )
831
  : null,
832
- React.createElement(BenchmarkDetails, { typedMetricRows, imageMetricRows }),
833
- panelError
834
- ? React.createElement(
 
 
 
 
 
 
 
 
 
 
 
 
 
 
835
  "div",
836
- { style: { color: colors.error, fontSize: 11, lineHeight: 1.35 } },
837
- panelError,
838
- )
839
- : null,
840
  ),
841
- React.createElement(BusinessRail, { item: selectedExample, metrics }),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
842
  ),
 
843
  ),
844
  );
 
 
845
  }
 
348
  React.createElement(
349
  "div",
350
  { style: { color: colors.mutedText, fontSize: 10, marginTop: 3 } },
351
+ "Green card = exact product. The context map is secondary.",
352
  ),
353
  ),
354
  models.map((model) =>
 
475
  );
476
  }
477
 
478
+ function BusinessRail({ item, metrics, fill = false }) {
479
  if (!item) return null;
480
  const clipSummary = getSummary(item, "clip");
481
  const candidateSummary = getSummary(item, "candidate");
 
484
  "aside",
485
  {
486
  style: {
487
+ flex: fill ? "none" : "0 1 280px",
488
+ width: fill ? "100%" : undefined,
489
+ minWidth: fill ? 0 : 240,
490
+ maxWidth: fill ? "none" : 300,
491
  display: "flex",
492
  flexDirection: "column",
493
  gap: 10,
 
606
  const [loadingKey, setLoadingKey] = React.useState(null);
607
  const [selectedExampleId, setSelectedExampleId] = React.useState(null);
608
  const didApplyDemoLayout = React.useRef(false);
609
+ const mode = panelProps.mode === "summary" ? "summary" : panelProps.mode === "results" ? "results" : "full";
610
 
611
  const models = React.useMemo(() => normalizeModels(panelProps.models), [panelProps.models]);
612
  const examples = Array.isArray(panelProps.examples) ? panelProps.examples : [];
 
629
  ];
630
 
631
  React.useEffect(() => {
632
+ const handleExampleChange = (event) => {
633
+ if (typeof event.detail === "string") {
634
+ setSelectedExampleId(event.detail);
635
+ }
636
+ };
637
+ window.addEventListener("fashion-search-example-change", handleExampleChange);
638
+ return () => window.removeEventListener("fashion-search-example-change", handleExampleChange);
639
+ }, []);
640
+
641
+ const chooseExample = React.useCallback((exampleId) => {
642
+ setSelectedExampleId(exampleId);
643
+ window.dispatchEvent(new CustomEvent("fashion-search-example-change", { detail: exampleId }));
644
+ }, []);
645
+
646
+ React.useEffect(() => {
647
+ if (mode === "summary") return;
648
  if (didApplyDemoLayout.current) return;
649
  didApplyDemoLayout.current = true;
650
 
 
658
  "scatter-euclidean-3d",
659
  "scatter-spherical-3d",
660
  "scatter-default",
 
 
661
  ].forEach((panelId) => {
662
  try {
663
  commands.closePanel?.(panelId);
 
665
  // Older HyperView builds may not have every panel id; ignore missing panes.
666
  }
667
  });
668
+ commands.focusPanel?.("fashion-buyer-readout");
669
+ commands.focusPanel?.("fashion-ranked-results");
670
  }, 350);
671
 
672
  return () => window.clearTimeout(timer);
673
+ }, [commands, mode]);
674
 
675
  const clearSelection = async () => {
676
  if (commands.setLabelFilter) commands.setLabelFilter(null);
 
698
  focus: false,
699
  persist: false,
700
  });
701
+ commands.focusPanel?.(model.key === "candidate" ? "fashion-map-hyper3" : "fashion-map-clip");
702
  } catch (error) {
703
  const message = error instanceof Error ? error.message : String(error);
704
  setPanelError(`Could not select target item: ${message}`);
 
726
  focus: false,
727
  persist: false,
728
  });
729
+ commands.focusPanel?.(model.key === "candidate" ? "fashion-map-hyper3" : "fashion-map-clip");
730
  } catch (error) {
731
  const message = error instanceof Error ? error.message : String(error);
732
  setPanelError(`Could not select ranked result: ${message}`);
 
735
  }
736
  };
737
 
738
+ const toolbar =
739
+ mode === "summary"
740
+ ? null
741
+ : React.createElement(PanelToolbar, {
742
+ items: [
743
+ { id: "dataset", label: "Data", value: "DeepFashion" },
744
+ { id: "samples", label: "Items", value: String(samplesState.totalSamples ?? "-") },
745
+ { id: "selected", label: "Selected", value: String(selection.selectedIds?.length ?? 0) },
746
+ ],
747
+ actions: React.createElement(PanelToolbarButton, { onClick: clearSelection }, "Reset"),
748
+ });
749
+
750
+ const summaryContent = React.createElement(
751
+ "div",
752
+ {
753
+ style: {
754
+ height: "100%",
755
+ overflow: "auto",
756
+ padding: 12,
757
+ background: colors.panelBg,
758
+ display: "flex",
759
+ flexDirection: "column",
760
+ gap: 12,
761
+ },
762
+ },
763
+ React.createElement(
764
+ Section,
765
+ { title: "Buyer Decision View" },
766
+ React.createElement(
767
+ TextBlock,
768
+ null,
769
+ "Business readout for the selected shopper query. Map tabs are optional neighborhood context.",
770
+ ),
771
+ examples.length
772
+ ? React.createElement(ExampleChooser, {
773
+ examples,
774
+ selectedId: selectedExample?.id,
775
+ onSelect: chooseExample,
776
+ })
777
+ : null,
778
+ ),
779
+ React.createElement(BusinessRail, { item: selectedExample, metrics, fill: true }),
780
+ );
781
+
782
+ const resultsContent = React.createElement(
783
+ "div",
784
+ {
785
+ style: {
786
+ height: "100%",
787
+ overflow: "auto",
788
+ padding: 14,
789
+ background: colors.panelBg,
790
+ },
791
+ },
792
  React.createElement(
793
  "div",
794
  {
795
  style: {
796
+ width: "100%",
797
+ maxWidth: 1130,
798
+ margin: "0 auto",
799
+ display: "flex",
800
+ gap: 12,
801
+ alignItems: "flex-start",
802
+ justifyContent: "center",
803
+ flexWrap: "wrap",
804
  },
805
  },
806
  React.createElement(
807
+ "main",
808
  {
809
  style: {
810
+ flex: "1 1 680px",
811
+ maxWidth: 820,
812
+ minWidth: 0,
813
  display: "flex",
814
+ flexDirection: "column",
815
+ gap: 14,
 
 
816
  },
817
  },
818
  React.createElement(
819
+ Section,
820
+ { title: "Pick A Shopper Query" },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
821
  React.createElement(
822
+ TextBlock,
823
+ null,
824
+ "Pick a typed product search and compare where each model ranks the exact item.",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
825
  ),
826
+ examples.length
827
+ ? React.createElement(ExampleChooser, {
828
+ examples,
829
+ selectedId: selectedExample?.id,
830
+ onSelect: chooseExample,
831
+ })
832
+ : React.createElement(
833
  "div",
834
+ { style: { color: colors.bodyText, fontSize: 12, lineHeight: 1.45 } },
835
+ "Demo examples are not configured.",
836
+ ),
837
+ selectedExample && models.length
838
+ ? React.createElement(SearchResultComparison, {
839
+ item: selectedExample,
840
+ models,
841
+ loadingKey,
842
+ onSelectResult: selectRankedResult,
843
+ })
 
 
 
844
  : null,
845
+ ),
846
+ React.createElement(
847
+ Section,
848
+ { title: "Inspect Exact Target" },
849
+ React.createElement(
850
+ TextBlock,
851
+ null,
852
+ `Exact-target summary for: ${selectedExample?.targetTitle || "target product"}.`,
853
+ ),
854
+ selectedExample && models.length
855
+ ? React.createElement(ExactTargetActions, {
856
+ item: selectedExample,
857
+ models,
858
+ loadingKey,
859
+ onSelectQuery: selectModelQuery,
860
+ })
861
+ : React.createElement(
862
  "div",
863
+ { style: { color: colors.bodyText, fontSize: 12, lineHeight: 1.45 } },
864
+ "Demo examples are not configured.",
865
+ ),
 
866
  ),
867
+ warnings.length
868
+ ? React.createElement(
869
+ "div",
870
+ {
871
+ style: {
872
+ border: `1px solid ${colors.warningBorder}`,
873
+ borderRadius: 4,
874
+ background: colors.warningBg,
875
+ color: colors.warningText,
876
+ padding: 8,
877
+ fontSize: 11,
878
+ lineHeight: 1.35,
879
+ },
880
+ },
881
+ warnings[0],
882
+ )
883
+ : null,
884
+ React.createElement(BenchmarkDetails, { typedMetricRows, imageMetricRows }),
885
+ panelError
886
+ ? React.createElement(
887
+ "div",
888
+ { style: { color: colors.error, fontSize: 11, lineHeight: 1.35 } },
889
+ panelError,
890
+ )
891
+ : null,
892
  ),
893
+ mode === "full" ? React.createElement(BusinessRail, { item: selectedExample, metrics }) : null,
894
  ),
895
  );
896
+
897
+ return React.createElement(Panel, { className: "h-full" }, toolbar, mode === "summary" ? summaryContent : resultsContent);
898
  }
Dockerfile CHANGED
@@ -42,7 +42,7 @@ COPY --chown=user . .
42
 
43
  ENV HYPERVIEW_HOST=0.0.0.0 \
44
  HYPERVIEW_PORT=7860 \
45
- HYPERVIEW_WORKSPACE_ID=fashion-retail-search-demo \
46
  HYPERVIEW_DATASETS_DIR=/home/user/app/demo_data/datasets \
47
  HYPERVIEW_MEDIA_DIR=/home/user/app/demo_data/media \
48
  DEEPFASHION_SAMPLES_PER_CATEGORY=10 \
 
42
 
43
  ENV HYPERVIEW_HOST=0.0.0.0 \
44
  HYPERVIEW_PORT=7860 \
45
+ HYPERVIEW_WORKSPACE_ID=fashion-retail-search-composition \
46
  HYPERVIEW_DATASETS_DIR=/home/user/app/demo_data/datasets \
47
  HYPERVIEW_MEDIA_DIR=/home/user/app/demo_data/media \
48
  DEEPFASHION_SAMPLES_PER_CATEGORY=10 \
demo.py CHANGED
@@ -18,7 +18,7 @@ import hyperview as hv
18
  SPACE_DIR = Path(__file__).resolve().parent
19
  SPACE_HOST = os.environ.get("HYPERVIEW_HOST", "127.0.0.1")
20
  SPACE_PORT = int(os.environ.get("HYPERVIEW_PORT", "6262"))
21
- WORKSPACE_ID = os.environ.get("HYPERVIEW_WORKSPACE_ID", "fashion-retail-search-demo")
22
  DATASET_NAME = os.environ.get("HYPERVIEW_DATASET_NAME", "deepfashion_text_search_clip_hyper3clip")
23
  EXTENSION_DIR = SPACE_DIR / ".hyperview" / "extensions" / "fashion-search-readout"
24
 
@@ -410,74 +410,90 @@ def build_examples(dataset: hv.Dataset) -> list[dict[str, Any]]:
410
 
411
 
412
  def build_demo_view(dataset: hv.Dataset, layouts: dict[str, str]) -> hv.ui.View:
413
- readout_panel = hv.ui.ExtensionPanel(
414
- id="fashion-text-search-readout",
415
- title="Retail Search Results",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
416
  extension="fashion-search-readout",
417
  panel="fashion-comparison",
418
  position="center",
419
  props={
 
 
420
  "dockview": {
421
  "closeSamplesPanel": True,
422
  "hideExplorerPanel": True,
423
- "initialWidth": int(os.environ.get("FASHION_READOUT_WIDTH", "760")),
424
  "minimumWidth": 620,
425
- "maximumWidth": int(os.environ.get("FASHION_READOUT_MAX_WIDTH", "860")),
426
- },
427
- "models": model_panel_props(layouts),
428
- "examples": build_examples(dataset),
429
- "warnings": RUNTIME_WARNINGS,
430
- "metrics": {
431
- "typedQueryCount": 180,
432
- "typedCandidateImages": 1120,
433
- "hit10Hyper3Only": 23,
434
- "hit10ClipOnly": 19,
435
- "strongHyper3Wins": 13,
436
- "strongClipWins": 9,
437
- "imageRetrievalMapHyper3": 0.407,
438
- "imageRetrievalMapClip": 0.240,
439
- "typedHit1Hyper3": 0.244,
440
- "typedHit1Clip": 0.233,
441
- "typedHit10Hyper3": 0.572,
442
- "typedHit10Clip": 0.550,
443
- "typedCategoryP10Hyper3": 0.594,
444
- "typedCategoryP10Clip": 0.561,
445
- "typedMrrHyper3": 0.358,
446
- "typedMrrClip": 0.344,
447
  },
448
  },
449
  )
450
 
451
  clip_spec = MODEL_SPECS[0]
452
  candidate_spec = MODEL_SPECS[1]
453
- map_dockview = {
454
- "initialWidth": int(os.environ.get("FASHION_MAP_CONTEXT_WIDTH", "320")),
455
  "minimumWidth": 260,
456
- "maximumWidth": int(os.environ.get("FASHION_MAP_CONTEXT_MAX_WIDTH", "380")),
457
  }
 
 
 
 
 
 
 
 
 
 
 
 
458
  clip_map = hv.ui.Scatter(
459
  id="fashion-map-clip",
460
- title="Optional Map: CLIP",
461
  layout_key=layouts["clip"],
462
- position="center",
463
- reference_panel_id="fashion-text-search-readout",
464
- direction="right",
465
  geometry=clip_spec["geometry"],
466
  layout_dimension=clip_spec["layout_dimension"],
467
- props={"dockview": map_dockview},
468
  )
469
  candidate_map = hv.ui.Scatter(
470
  id="fashion-map-hyper3",
471
- title="Optional Map: Hyper3",
472
  layout_key=layouts["candidate"],
473
- position="center",
474
  reference_panel_id="fashion-map-clip",
475
  direction="within",
476
  geometry=candidate_spec["geometry"],
477
  layout_dimension=candidate_spec["layout_dimension"],
478
- props={"dockview": map_dockview},
479
  )
480
- return hv.ui.View(readout_panel, clip_map, candidate_map)
481
 
482
 
483
  def launch_demo(dataset: hv.Dataset, layouts: dict[str, str]) -> hv.Session:
@@ -496,7 +512,7 @@ def launch_demo(dataset: hv.Dataset, layouts: dict[str, str]) -> hv.Session:
496
  session.ui.set_active_layout(None, workspace_id=WORKSPACE_ID)
497
  session.ui.set_selection([], workspace_id=WORKSPACE_ID)
498
  print(f"\nHyperView DeepFashion text-search demo is running at {session.url}", flush=True)
499
- print(" Ranked search results are primary; scatter maps are narrow optional context tabs.", flush=True)
500
  return session
501
 
502
 
 
18
  SPACE_DIR = Path(__file__).resolve().parent
19
  SPACE_HOST = os.environ.get("HYPERVIEW_HOST", "127.0.0.1")
20
  SPACE_PORT = int(os.environ.get("HYPERVIEW_PORT", "6262"))
21
+ WORKSPACE_ID = os.environ.get("HYPERVIEW_WORKSPACE_ID", "fashion-retail-search-composition")
22
  DATASET_NAME = os.environ.get("HYPERVIEW_DATASET_NAME", "deepfashion_text_search_clip_hyper3clip")
23
  EXTENSION_DIR = SPACE_DIR / ".hyperview" / "extensions" / "fashion-search-readout"
24
 
 
410
 
411
 
412
  def build_demo_view(dataset: hv.Dataset, layouts: dict[str, str]) -> hv.ui.View:
413
+ shared_props = {
414
+ "models": model_panel_props(layouts),
415
+ "examples": build_examples(dataset),
416
+ "warnings": RUNTIME_WARNINGS,
417
+ "metrics": {
418
+ "typedQueryCount": 180,
419
+ "typedCandidateImages": 1120,
420
+ "hit10Hyper3Only": 23,
421
+ "hit10ClipOnly": 19,
422
+ "strongHyper3Wins": 13,
423
+ "strongClipWins": 9,
424
+ "imageRetrievalMapHyper3": 0.407,
425
+ "imageRetrievalMapClip": 0.240,
426
+ "typedHit1Hyper3": 0.244,
427
+ "typedHit1Clip": 0.233,
428
+ "typedHit10Hyper3": 0.572,
429
+ "typedHit10Clip": 0.550,
430
+ "typedCategoryP10Hyper3": 0.594,
431
+ "typedCategoryP10Clip": 0.561,
432
+ "typedMrrHyper3": 0.358,
433
+ "typedMrrClip": 0.344,
434
+ },
435
+ }
436
+ results_panel = hv.ui.ExtensionPanel(
437
+ id="fashion-ranked-results",
438
+ title="Ranked Search Results",
439
  extension="fashion-search-readout",
440
  panel="fashion-comparison",
441
  position="center",
442
  props={
443
+ **shared_props,
444
+ "mode": "results",
445
  "dockview": {
446
  "closeSamplesPanel": True,
447
  "hideExplorerPanel": True,
448
+ "initialWidth": int(os.environ.get("FASHION_RESULTS_WIDTH", "780")),
449
  "minimumWidth": 620,
450
+ "maximumWidth": int(os.environ.get("FASHION_RESULTS_MAX_WIDTH", "920")),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
451
  },
452
  },
453
  )
454
 
455
  clip_spec = MODEL_SPECS[0]
456
  candidate_spec = MODEL_SPECS[1]
457
+ context_dockview = {
458
+ "initialWidth": int(os.environ.get("FASHION_CONTEXT_WIDTH", "360")),
459
  "minimumWidth": 260,
460
+ "maximumWidth": int(os.environ.get("FASHION_CONTEXT_MAX_WIDTH", "440")),
461
  }
462
+ summary_panel = hv.ui.ExtensionPanel(
463
+ id="fashion-buyer-readout",
464
+ title="Buyer Readout",
465
+ extension="fashion-search-readout",
466
+ panel="fashion-comparison",
467
+ position="right",
468
+ props={
469
+ **shared_props,
470
+ "mode": "summary",
471
+ "dockview": context_dockview,
472
+ },
473
+ )
474
  clip_map = hv.ui.Scatter(
475
  id="fashion-map-clip",
476
+ title="Context Map: CLIP",
477
  layout_key=layouts["clip"],
478
+ position="right",
479
+ reference_panel_id="fashion-buyer-readout",
480
+ direction="within",
481
  geometry=clip_spec["geometry"],
482
  layout_dimension=clip_spec["layout_dimension"],
483
+ props={"dockview": context_dockview},
484
  )
485
  candidate_map = hv.ui.Scatter(
486
  id="fashion-map-hyper3",
487
+ title="Context Map: Hyper3",
488
  layout_key=layouts["candidate"],
489
+ position="right",
490
  reference_panel_id="fashion-map-clip",
491
  direction="within",
492
  geometry=candidate_spec["geometry"],
493
  layout_dimension=candidate_spec["layout_dimension"],
494
+ props={"dockview": context_dockview},
495
  )
496
+ return hv.ui.View(results_panel, summary_panel, clip_map, candidate_map)
497
 
498
 
499
  def launch_demo(dataset: hv.Dataset, layouts: dict[str, str]) -> hv.Session:
 
512
  session.ui.set_active_layout(None, workspace_id=WORKSPACE_ID)
513
  session.ui.set_selection([], workspace_id=WORKSPACE_ID)
514
  print(f"\nHyperView DeepFashion text-search demo is running at {session.url}", flush=True)
515
+ print(" Ranked results are primary; buyer readout and scatter maps are secondary context tabs.", flush=True)
516
  return session
517
 
518