2026-02-15 21:17:30 -08:00
|
|
|
'use client';
|
|
|
|
|
|
|
|
|
|
import { useCallback, useMemo } from 'react';
|
|
|
|
|
import { useSearchParams, useRouter } from 'next/navigation';
|
|
|
|
|
|
2026-02-16 10:16:33 -08:00
|
|
|
export type ViewType = 'social' | 'graph' | 'swarm' | 'activity';
|
2026-02-15 21:17:30 -08:00
|
|
|
export type PanelState = 'open' | 'closed';
|
2026-02-16 10:16:33 -08:00
|
|
|
export type DrawerState = 'open' | 'closed';
|
2026-02-15 21:17:30 -08:00
|
|
|
export type GraphTabType = 'flow' | 'overview';
|
|
|
|
|
|
|
|
|
|
export interface UrlState {
|
|
|
|
|
view: ViewType;
|
|
|
|
|
setView: (v: ViewType) => void;
|
|
|
|
|
taskId: string | null;
|
|
|
|
|
setTaskId: (id: string | null) => void;
|
|
|
|
|
swarmId: string | null;
|
|
|
|
|
setSwarmId: (id: string | null) => void;
|
2026-02-16 10:16:33 -08:00
|
|
|
agentId: string | null;
|
|
|
|
|
setAgentId: (id: string | null) => void;
|
2026-02-17 00:10:28 -08:00
|
|
|
epicId: string | null;
|
|
|
|
|
setEpicId: (id: string | null) => void;
|
2026-02-15 21:17:30 -08:00
|
|
|
panel: PanelState;
|
|
|
|
|
togglePanel: () => void;
|
2026-02-16 10:16:33 -08:00
|
|
|
drawer: DrawerState;
|
|
|
|
|
setDrawer: (state: DrawerState) => void;
|
2026-02-15 21:17:30 -08:00
|
|
|
graphTab: GraphTabType;
|
|
|
|
|
setGraphTab: (tab: GraphTabType) => void;
|
|
|
|
|
clearSelection: () => void;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const DEFAULT_VIEW: ViewType = 'social';
|
2026-02-17 00:01:45 -08:00
|
|
|
const DEFAULT_PANEL: PanelState = 'open';
|
2026-02-16 10:16:33 -08:00
|
|
|
const DEFAULT_DRAWER: DrawerState = 'closed';
|
2026-02-15 21:17:30 -08:00
|
|
|
const DEFAULT_GRAPH_TAB: GraphTabType = 'flow';
|
|
|
|
|
|
2026-02-16 10:16:33 -08:00
|
|
|
const VALID_VIEWS: ViewType[] = ['social', 'graph', 'swarm', 'activity'];
|
2026-02-15 21:17:30 -08:00
|
|
|
const VALID_PANELS: PanelState[] = ['open', 'closed'];
|
2026-02-16 10:16:33 -08:00
|
|
|
const VALID_DRAWERS: DrawerState[] = ['open', 'closed'];
|
2026-02-15 21:17:30 -08:00
|
|
|
const VALID_GRAPH_TABS: GraphTabType[] = ['flow', 'overview'];
|
|
|
|
|
|
|
|
|
|
export function parseUrlState(searchParams: URLSearchParams): {
|
|
|
|
|
view: ViewType;
|
|
|
|
|
taskId: string | null;
|
|
|
|
|
swarmId: string | null;
|
2026-02-16 10:16:33 -08:00
|
|
|
agentId: string | null;
|
2026-02-17 00:10:28 -08:00
|
|
|
epicId: string | null;
|
2026-02-15 21:17:30 -08:00
|
|
|
panel: PanelState;
|
2026-02-16 10:16:33 -08:00
|
|
|
drawer: DrawerState;
|
2026-02-15 21:17:30 -08:00
|
|
|
graphTab: GraphTabType;
|
|
|
|
|
} {
|
|
|
|
|
const viewParam = searchParams.get('view');
|
|
|
|
|
const view: ViewType = viewParam && VALID_VIEWS.includes(viewParam as ViewType)
|
|
|
|
|
? (viewParam as ViewType)
|
|
|
|
|
: DEFAULT_VIEW;
|
|
|
|
|
|
|
|
|
|
const taskId = searchParams.get('task');
|
|
|
|
|
const swarmId = searchParams.get('swarm');
|
2026-02-16 10:16:33 -08:00
|
|
|
const agentId = searchParams.get('agent');
|
2026-02-17 00:10:28 -08:00
|
|
|
const epicId = searchParams.get('epic');
|
2026-02-15 21:17:30 -08:00
|
|
|
|
|
|
|
|
const panelParam = searchParams.get('panel');
|
|
|
|
|
const panel: PanelState = panelParam && VALID_PANELS.includes(panelParam as PanelState)
|
|
|
|
|
? (panelParam as PanelState)
|
|
|
|
|
: DEFAULT_PANEL;
|
|
|
|
|
|
2026-02-16 10:16:33 -08:00
|
|
|
const drawerParam = searchParams.get('drawer');
|
|
|
|
|
const drawer: DrawerState = drawerParam && VALID_DRAWERS.includes(drawerParam as DrawerState)
|
|
|
|
|
? (drawerParam as DrawerState)
|
|
|
|
|
: DEFAULT_DRAWER;
|
|
|
|
|
|
2026-02-15 21:17:30 -08:00
|
|
|
const graphTabParam = searchParams.get('graphTab');
|
|
|
|
|
const graphTab: GraphTabType = graphTabParam && VALID_GRAPH_TABS.includes(graphTabParam as GraphTabType)
|
|
|
|
|
? (graphTabParam as GraphTabType)
|
|
|
|
|
: DEFAULT_GRAPH_TAB;
|
|
|
|
|
|
2026-02-17 00:10:28 -08:00
|
|
|
return { view, taskId, swarmId, agentId, epicId, panel, drawer, graphTab };
|
2026-02-15 21:17:30 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function buildUrlParams(
|
|
|
|
|
searchParams: URLSearchParams,
|
|
|
|
|
updates: Record<string, string | null>
|
|
|
|
|
): string {
|
|
|
|
|
const sp = new URLSearchParams(searchParams.toString());
|
|
|
|
|
|
|
|
|
|
for (const [key, value] of Object.entries(updates)) {
|
|
|
|
|
if (value === null || value === undefined || value === '') {
|
|
|
|
|
sp.delete(key);
|
|
|
|
|
} else {
|
|
|
|
|
sp.set(key, value);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const str = sp.toString();
|
|
|
|
|
return str ? `/?${str}` : '/';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function useUrlState(): UrlState {
|
|
|
|
|
const searchParams = useSearchParams();
|
|
|
|
|
const router = useRouter();
|
|
|
|
|
|
|
|
|
|
const state = useMemo(() => parseUrlState(searchParams), [searchParams]);
|
|
|
|
|
|
|
|
|
|
const updateUrl = useCallback((updates: Record<string, string | null>) => {
|
|
|
|
|
const newUrl = buildUrlParams(searchParams, updates);
|
|
|
|
|
router.push(newUrl, { scroll: false });
|
|
|
|
|
}, [searchParams, router]);
|
|
|
|
|
|
|
|
|
|
const setView = useCallback((v: ViewType) => {
|
|
|
|
|
updateUrl({ view: v });
|
|
|
|
|
}, [updateUrl]);
|
|
|
|
|
|
|
|
|
|
const setTaskId = useCallback((id: string | null) => {
|
2026-02-16 00:26:31 -08:00
|
|
|
updateUrl({ task: id, panel: id ? 'open' : null });
|
2026-02-15 21:17:30 -08:00
|
|
|
}, [updateUrl]);
|
|
|
|
|
|
|
|
|
|
const setSwarmId = useCallback((id: string | null) => {
|
2026-02-16 00:26:31 -08:00
|
|
|
updateUrl({ swarm: id, panel: id ? 'open' : null });
|
2026-02-15 21:17:30 -08:00
|
|
|
}, [updateUrl]);
|
|
|
|
|
|
2026-02-16 10:16:33 -08:00
|
|
|
const setAgentId = useCallback((id: string | null) => {
|
|
|
|
|
updateUrl({ agent: id, panel: id ? 'open' : null });
|
|
|
|
|
}, [updateUrl]);
|
|
|
|
|
|
2026-02-17 00:10:28 -08:00
|
|
|
const setEpicId = useCallback((id: string | null) => {
|
|
|
|
|
updateUrl({ epic: id });
|
|
|
|
|
}, [updateUrl]);
|
|
|
|
|
|
2026-02-15 21:17:30 -08:00
|
|
|
const togglePanel = useCallback(() => {
|
|
|
|
|
const newPanel = state.panel === 'open' ? 'closed' : 'open';
|
|
|
|
|
updateUrl({ panel: newPanel });
|
|
|
|
|
}, [state.panel, updateUrl]);
|
|
|
|
|
|
2026-02-16 10:16:33 -08:00
|
|
|
const setDrawer = useCallback((state: DrawerState) => {
|
|
|
|
|
updateUrl({ drawer: state });
|
|
|
|
|
}, [updateUrl]);
|
|
|
|
|
|
2026-02-15 21:17:30 -08:00
|
|
|
const setGraphTab = useCallback((tab: GraphTabType) => {
|
|
|
|
|
updateUrl({ graphTab: tab });
|
|
|
|
|
}, [updateUrl]);
|
|
|
|
|
|
|
|
|
|
const clearSelection = useCallback(() => {
|
2026-02-17 00:10:28 -08:00
|
|
|
updateUrl({ task: null, swarm: null, epic: null, panel: 'closed', drawer: 'closed' });
|
2026-02-15 21:17:30 -08:00
|
|
|
}, [updateUrl]);
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
view: state.view,
|
|
|
|
|
setView,
|
|
|
|
|
taskId: state.taskId,
|
|
|
|
|
setTaskId,
|
|
|
|
|
swarmId: state.swarmId,
|
|
|
|
|
setSwarmId,
|
2026-02-16 10:16:33 -08:00
|
|
|
agentId: state.agentId,
|
|
|
|
|
setAgentId,
|
2026-02-17 00:10:28 -08:00
|
|
|
epicId: state.epicId,
|
|
|
|
|
setEpicId,
|
2026-02-15 21:17:30 -08:00
|
|
|
panel: state.panel,
|
|
|
|
|
togglePanel,
|
2026-02-16 10:16:33 -08:00
|
|
|
drawer: state.drawer,
|
|
|
|
|
setDrawer,
|
2026-02-15 21:17:30 -08:00
|
|
|
graphTab: state.graphTab,
|
|
|
|
|
setGraphTab,
|
|
|
|
|
clearSelection,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default useUrlState;
|