Skip to content

Commit 7b42c13

Browse files
committed
feat: update dockview layout version and improve panel management
- Updated LAYOUT_STORAGE_KEY to version v6 for dockview layout persistence. - Refactored panel title management for the Curation panel to use a constant. - Enhanced panel creation logic to ensure proper positioning and active state management. - Added utility functions to check for meaningful curation filters and to enforce panel titles. - Improved layout restoration logic to handle cases with missing center panels. feat: enhance ExplorerPanel with tag filtering capabilities - Introduced normalization for tag tokens to ensure consistent filtering. - Added functionality to check for meaningful curation filters in the query. - Updated tag filtering logic to manage active tags more effectively. refactor: clean up TimelinePanel and VideoGrid components - Removed unnecessary progress bar from TimelinePanel. - Refactored VideoGrid to include tag filtering and improved tag chip interaction. - Enhanced visual styles for tag chips and video elements. feat: implement Cosmos Curate API integration - Added functions to fetch Cosmos Curate options and submit jobs. - Introduced types for Cosmos Curate requests and responses to improve type safety. style: update icon colors for better visibility - Changed CheckIcon color to blue-400 for improved contrast.
1 parent b49b09c commit 7b42c13

9 files changed

Lines changed: 892 additions & 170 deletions

File tree

frontend/src/components/CurationPanel.tsx

Lines changed: 610 additions & 36 deletions
Large diffs are not rendered by default.

frontend/src/components/DockviewWorkspace.tsx

Lines changed: 64 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ import { CurationPanel } from "./CurationPanel";
3333
import { HyperViewLogo } from "./icons";
3434
import { PanelTitle } from "./PanelTitle";
3535

36-
const LAYOUT_STORAGE_KEY = "hyperview:dockview-layout:v4";
36+
const LAYOUT_STORAGE_KEY = "hyperview:dockview-layout:v6";
3737

3838
// Panel IDs
3939
const PANEL = {
@@ -48,6 +48,8 @@ const PANEL = {
4848
BOTTOM_PLACEHOLDER: "bottom-placeholder",
4949
} as const;
5050

51+
const CURATION_PANEL_TITLE = "Cosmos Curate";
52+
5153
const CENTER_PANEL_IDS = [
5254
PANEL.GRID,
5355
PANEL.VIDEO,
@@ -60,7 +62,7 @@ const CENTER_PANEL_IDS = [
6062
export const CENTER_PANEL_DEFS = [
6163
{ id: PANEL.GRID, label: "Clip Previews", icon: Grid3X3 },
6264
{ id: PANEL.VIDEO, label: "Video", icon: Film },
63-
{ id: PANEL.CURATION, label: "Cosmos Curate", icon: SlidersHorizontal },
65+
{ id: PANEL.CURATION, label: CURATION_PANEL_TITLE, icon: SlidersHorizontal },
6466
{ id: PANEL.SCATTER_EUCLIDEAN, label: "Euclidean", icon: Circle },
6567
{ id: PANEL.SCATTER_POINCARE, label: "Hyperbolic", icon: Disc },
6668
] as const;
@@ -108,6 +110,10 @@ function getCenterAnchorPanel(api: DockviewApi) {
108110
return fallback ?? api.activePanel;
109111
}
110112

113+
function hasCenterPanels(api: DockviewApi): boolean {
114+
return CENTER_PANEL_IDS.some((id) => !!api.getPanel(id));
115+
}
116+
111117
function getZonePosition(zone: "left" | "right" | "bottom") {
112118
return { direction: zone === "bottom" ? "below" : zone };
113119
}
@@ -195,7 +201,7 @@ export function useDockviewApi() {
195201
api.addPanel({
196202
id: PANEL.CURATION,
197203
component: "curation",
198-
title: "Cosmos Curate",
204+
title: CURATION_PANEL_TITLE,
199205
tabComponent: "curationTab",
200206
renderer: "always",
201207
...baseOptions,
@@ -543,6 +549,13 @@ function applyZonePolicies(api: DockviewApi) {
543549
}
544550
}
545551

552+
function enforcePanelTitles(api: DockviewApi) {
553+
const curationPanel = api.getPanel(PANEL.CURATION);
554+
if (curationPanel && curationPanel.api.title !== CURATION_PANEL_TITLE) {
555+
curationPanel.api.setTitle(CURATION_PANEL_TITLE);
556+
}
557+
}
558+
546559
// -----------------------------------------------------------------------------
547560
// Workspace Component - the actual dockview renderer
548561
// -----------------------------------------------------------------------------
@@ -559,43 +572,48 @@ export function DockviewWorkspace() {
559572
const fallbackLayout = !euclideanLayout && !poincareLayout ? layouts[0] : null;
560573
const hasLayouts = layouts.length > 0;
561574

562-
// Create the grid panel first (center zone)
563-
const gridPanel =
564-
api.getPanel(PANEL.GRID) ??
565-
api.addPanel({
566-
id: PANEL.GRID,
567-
component: "grid",
568-
title: "Clip Previews",
569-
tabComponent: "samplesTab",
570-
renderer: "always",
571-
});
572-
573-
api.getPanel(PANEL.VIDEO) ??
575+
// Middle center group: Video + Cosmos Curate (tabs)
576+
const middlePanel =
577+
api.getPanel(PANEL.VIDEO) ??
574578
api.addPanel({
575579
id: PANEL.VIDEO,
576580
component: "video",
577581
title: "Video",
578582
tabComponent: "videoTab",
579-
position: {
580-
referencePanel: gridPanel.id,
581-
direction: "within",
582-
},
583583
renderer: "always",
584584
});
585585

586586
api.getPanel(PANEL.CURATION) ??
587587
api.addPanel({
588588
id: PANEL.CURATION,
589589
component: "curation",
590-
title: "Cosmos Curate",
590+
title: CURATION_PANEL_TITLE,
591591
tabComponent: "curationTab",
592592
position: {
593-
referencePanel: gridPanel.id,
593+
referencePanel: middlePanel.id,
594594
direction: "within",
595595
},
596596
renderer: "always",
597597
});
598598

599+
// Keep Video as the initially selected tab in the middle group.
600+
middlePanel.api.setActive();
601+
602+
// Left center group: Clip previews
603+
const gridPanel =
604+
api.getPanel(PANEL.GRID) ??
605+
api.addPanel({
606+
id: PANEL.GRID,
607+
component: "grid",
608+
title: "Clip Previews",
609+
tabComponent: "samplesTab",
610+
position: {
611+
referencePanel: middlePanel.id,
612+
direction: "left",
613+
},
614+
renderer: "always",
615+
});
616+
599617
let scatterPanel: typeof gridPanel | null = null;
600618

601619
if (hasLayouts && euclideanLayout) {
@@ -611,38 +629,13 @@ export function DockviewWorkspace() {
611629
geometry: "euclidean" as Geometry,
612630
},
613631
position: {
614-
referencePanel: gridPanel.id,
632+
referencePanel: middlePanel.id,
615633
direction: "right",
616634
},
617635
renderer: "always",
618636
});
619637
}
620638

621-
if (hasLayouts && poincareLayout) {
622-
const position = scatterPanel
623-
? { referencePanel: scatterPanel.id, direction: "within" as const }
624-
: { referencePanel: gridPanel.id, direction: "right" as const };
625-
626-
const poincarePanel =
627-
api.getPanel(PANEL.SCATTER_POINCARE) ??
628-
api.addPanel({
629-
id: PANEL.SCATTER_POINCARE,
630-
component: "scatter",
631-
title: "Hyperbolic",
632-
tabComponent: "hyperbolicTab",
633-
params: {
634-
layoutKey: poincareLayout.layout_key,
635-
geometry: "poincare" as Geometry,
636-
},
637-
position,
638-
renderer: "always",
639-
});
640-
641-
if (!scatterPanel) {
642-
scatterPanel = poincarePanel;
643-
}
644-
}
645-
646639
if (!hasLayouts) {
647640
const euclideanPanel =
648641
api.getPanel(PANEL.SCATTER_EUCLIDEAN) ??
@@ -655,28 +648,12 @@ export function DockviewWorkspace() {
655648
geometry: "euclidean" as Geometry,
656649
},
657650
position: {
658-
referencePanel: gridPanel.id,
651+
referencePanel: middlePanel.id,
659652
direction: "right",
660653
},
661654
renderer: "always",
662655
});
663656

664-
api.getPanel(PANEL.SCATTER_POINCARE) ??
665-
api.addPanel({
666-
id: PANEL.SCATTER_POINCARE,
667-
component: "scatter",
668-
title: "Hyperbolic",
669-
tabComponent: "hyperbolicTab",
670-
params: {
671-
geometry: "poincare" as Geometry,
672-
},
673-
position: {
674-
referencePanel: euclideanPanel.id,
675-
direction: "within" as const,
676-
},
677-
renderer: "always",
678-
});
679-
680657
scatterPanel = euclideanPanel;
681658
}
682659

@@ -690,7 +667,7 @@ export function DockviewWorkspace() {
690667
layoutKey: fallbackLayout.layout_key,
691668
},
692669
position: {
693-
referencePanel: gridPanel.id,
670+
referencePanel: middlePanel.id,
694671
direction: "right",
695672
},
696673
renderer: "always",
@@ -753,13 +730,22 @@ export function DockviewWorkspace() {
753730
buildDefaultLayout(event.api);
754731
}
755732

733+
// Keep key tab titles stable even with older persisted layouts.
734+
enforcePanelTitles(event.api);
735+
756736
// Re-apply side-zone policies after restore (header hidden, no-drop targets, etc)
757737
applyZonePolicies(event.api);
758738

759739
// Sync store state with restored layout
760740
setLeftPanelOpen(!!event.api.getPanel(PANEL.EXPLORER));
761741
setRightPanelOpen(!!event.api.getPanel(PANEL.RIGHT_PLACEHOLDER));
762742
setBottomPanelOpen(!!event.api.getPanel(PANEL.BOTTOM_PLACEHOLDER));
743+
744+
// Recover from stale/corrupt saved layouts that have no center content.
745+
if (!hasCenterPanels(event.api)) {
746+
localStorage.removeItem(LAYOUT_STORAGE_KEY);
747+
buildDefaultLayout(event.api);
748+
}
763749
return;
764750
} catch (err) {
765751
console.warn("Failed to restore dock layout, resetting.", err);
@@ -797,10 +783,22 @@ export function DockviewWorkspace() {
797783
if (e.id === PANEL.EXPLORER) setLeftPanelOpen(false);
798784
if (e.id === PANEL.RIGHT_PLACEHOLDER) setRightPanelOpen(false);
799785
if (e.id === PANEL.BOTTOM_PLACEHOLDER) setBottomPanelOpen(false);
786+
787+
// Keep the workspace usable when the last center panel gets closed.
788+
if (!hasCenterPanels(api)) {
789+
localStorage.removeItem(LAYOUT_STORAGE_KEY);
790+
buildDefaultLayout(api);
791+
}
800792
});
801793

802794
return () => disposable.dispose();
803-
}, [ctx.api, setLeftPanelOpen, setRightPanelOpen, setBottomPanelOpen]);
795+
}, [
796+
buildDefaultLayout,
797+
ctx.api,
798+
setLeftPanelOpen,
799+
setRightPanelOpen,
800+
setBottomPanelOpen,
801+
]);
804802

805803
// When a real panel is dropped into a placeholder group, close the placeholder
806804
useEffect(() => {

frontend/src/components/ExplorerPanel.tsx

Lines changed: 39 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,21 @@ interface ExplorerPanelProps {
1414
className?: string;
1515
}
1616

17+
function normalizeTagToken(tag: string): string {
18+
return tag.trim().toLowerCase().replace(/\s+/g, " ");
19+
}
20+
21+
function hasMeaningfulCurationFilters(query: Record<string, unknown>): boolean {
22+
return Object.entries(query).some(([key, value]) => {
23+
if (key === "offset" || key === "limit" || key === "include_thumbnails") {
24+
return false;
25+
}
26+
if (Array.isArray(value)) return value.length > 0;
27+
if (typeof value === "string") return value.trim().length > 0;
28+
return value !== undefined && value !== null;
29+
});
30+
}
31+
1732
export function ExplorerPanel({ className }: ExplorerPanelProps) {
1833
const {
1934
datasetInfo,
@@ -46,7 +61,10 @@ export function ExplorerPanel({ className }: ExplorerPanelProps) {
4661
const hasCounts = labelCounts.size > 0;
4762

4863
const activeLabel = labelFilter ? normalizeLabel(labelFilter) : null;
49-
const activeTagFilter = curationQuery?.include_tags_any?.[0] ?? null;
64+
const activeTagFilters = React.useMemo(() => {
65+
const tags = curationQuery?.include_tags_any ?? [];
66+
return new Set(tags.map(normalizeTagToken).filter((tag) => tag.length > 0));
67+
}, [curationQuery]);
5068

5169
const tagEntries = React.useMemo(() => {
5270
const query = labelSearch.trim().toLowerCase();
@@ -57,33 +75,33 @@ export function ExplorerPanel({ className }: ExplorerPanelProps) {
5775

5876
const toggleTagFilter = React.useCallback(
5977
(tag: string) => {
78+
const normalizedTag = normalizeTagToken(tag);
79+
if (!normalizedTag) return;
80+
6081
const currentQuery = curationQuery ?? {};
82+
const includeAny = (currentQuery.include_tags_any ?? [])
83+
.map(normalizeTagToken)
84+
.filter((value, index, array) => value.length > 0 && array.indexOf(value) === index);
85+
const nextTags = new Set(includeAny);
6186

62-
if (activeTagFilter === tag) {
63-
const nextQuery = {
64-
...currentQuery,
65-
include_tags_any: undefined,
66-
offset: 0,
67-
};
68-
setCurationFilters({ includeTagsAny: "" });
69-
70-
const hasAnyFilter = Object.values(nextQuery).some((value) => {
71-
if (Array.isArray(value)) return value.length > 0;
72-
return value !== undefined && value !== null && value !== "";
73-
});
74-
setCurationQuery(hasAnyFilter ? nextQuery : null);
75-
return;
87+
if (nextTags.has(normalizedTag)) {
88+
nextTags.delete(normalizedTag);
89+
} else {
90+
nextTags.add(normalizedTag);
7691
}
7792

93+
const includeTagsAny = Array.from(nextTags);
94+
setCurationFilters({ includeTagsAny: includeTagsAny.join(", ") });
95+
7896
const nextQuery = {
7997
...currentQuery,
80-
include_tags_any: [tag],
98+
include_tags_any: includeTagsAny.length > 0 ? includeTagsAny : undefined,
8199
offset: 0,
82100
};
83-
setCurationFilters({ includeTagsAny: tag });
84-
setCurationQuery(nextQuery);
101+
102+
setCurationQuery(hasMeaningfulCurationFilters(nextQuery) ? nextQuery : null);
85103
},
86-
[activeTagFilter, curationQuery, setCurationFilters, setCurationQuery]
104+
[curationQuery, setCurationFilters, setCurationQuery]
87105
);
88106

89107
React.useEffect(() => {
@@ -96,6 +114,7 @@ export function ExplorerPanel({ className }: ExplorerPanelProps) {
96114
})
97115
.catch(() => {
98116
if (cancelled) return;
117+
setSampleTagCounts({});
99118
});
100119

101120
return () => {
@@ -206,7 +225,7 @@ export function ExplorerPanel({ className }: ExplorerPanelProps) {
206225
) : (
207226
<div className="space-y-px">
208227
{tagEntries.map(([tag, count]) => {
209-
const isActive = activeTagFilter === tag;
228+
const isActive = activeTagFilters.has(normalizeTagToken(tag));
210229
return (
211230
<button
212231
key={tag}

frontend/src/components/TimelinePanel.tsx

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -196,13 +196,6 @@ export function TimelinePanel() {
196196
className="h-5 w-full cursor-pointer accent-primary disabled:cursor-not-allowed disabled:opacity-50"
197197
aria-label="Timeline seek"
198198
/>
199-
200-
<div className="h-1.5 w-full rounded-full bg-muted overflow-hidden">
201-
<div
202-
className="h-full bg-primary/70 transition-[width] duration-150 ease-out"
203-
style={{ width: `${progressPercent}%` }}
204-
/>
205-
</div>
206199
</div>
207200
</div>
208201
</Panel>

0 commit comments

Comments
 (0)