Files
AgentCompany/src/renderer/hooks/useUIController.ts
T
ZacharyZhang-NY 1b76a51894 perf(useUIController): hoist page/panel transition objects to module scope (P2-32)
Stable refs across renders eliminate unnecessary motion prop diffing.
2026-04-16 22:56:14 -04:00

207 lines
7.8 KiB
TypeScript

import { useCallback, useDeferredValue, useMemo } from "react";
import { useShallow } from "zustand/shallow";
import type { AgentRecord, GoalRecord, ProjectRecord, RunRecord, SectionId, TaskRecord } from "@shared/types";
import { createTranslator } from "../i18n";
import { useAppStore } from "../state/store";
import { useFocusTrap, useReducedMotion } from "../components/ui";
import {
BookOpen,
Gear,
Tray,
SquaresFour,
UsersThree,
ListChecks,
ChatCircle,
PuzzlePiece,
FileText,
ShieldCheck,
Megaphone,
FolderOpen,
} from "@phosphor-icons/react";
import type { AppLocale } from "@shared/locale";
type NavItemDef = { id: SectionId; labelKey: string; icon: typeof SquaresFour };
export type NavItem = { id: SectionId; label: string; icon: typeof SquaresFour };
export type NavGroup = { items: NavItem[] };
export type PaletteAction = { key: string; section: SectionId; entityId?: string };
const sidebarCoreDefs: NavItemDef[] = [
{ id: "overview", labelKey: "nav.overview", icon: SquaresFour },
{ id: "inbox", labelKey: "nav.inbox", icon: Tray },
{ id: "communication", labelKey: "nav.communication", icon: ChatCircle },
];
const sidebarCollaborateDefs: NavItemDef[] = [
{ id: "tasks", labelKey: "nav.work", icon: ListChecks },
{ id: "approvals", labelKey: "nav.approvals", icon: ShieldCheck },
{ id: "agents", labelKey: "nav.agents", icon: UsersThree },
{ id: "workspaces", labelKey: "nav.workspaces", icon: FolderOpen },
{ id: "social", labelKey: "nav.social", icon: Megaphone },
{ id: "documents", labelKey: "nav.documents", icon: FileText },
];
const paletteOnlyNavDefs: NavItemDef[] = [
{ id: "skills", labelKey: "nav.skills", icon: PuzzlePiece },
{ id: "knowledge", labelKey: "nav.knowledge", icon: BookOpen },
];
const NO_MOTION = { duration: 0 } as const;
const PAGE_TRANSITION = { duration: 0.32, ease: [0.16, 1, 0.3, 1] as const } as const;
const PANEL_TRANSITION = { duration: 0.28, ease: [0.16, 1, 0.3, 1] as const } as const;
export function useUIController() {
const reducedMotion = useReducedMotion();
const pageTransition = reducedMotion ? NO_MOTION : PAGE_TRANSITION;
const panelTransition = reducedMotion ? NO_MOTION : PANEL_TRANSITION;
const locale = useAppStore((s) => s.locale);
const t = useMemo(() => createTranslator(locale), [locale]);
const sidebarGroups: NavGroup[] = useMemo(
() => [
{ items: sidebarCoreDefs.map((item) => ({ id: item.id, label: t(item.labelKey), icon: item.icon })) },
{ items: sidebarCollaborateDefs.map((item) => ({ id: item.id, label: t(item.labelKey), icon: item.icon })) },
],
[t],
);
const sidebarNav: NavItem[] = useMemo(
() => sidebarGroups.flatMap((group) => group.items),
[sidebarGroups],
);
const navItems: NavItem[] = useMemo(
() =>
sidebarNav
.concat(paletteOnlyNavDefs.map((item) => ({ id: item.id, label: t(item.labelKey), icon: item.icon })))
.concat({ id: "settings", label: t("nav.settings"), icon: Gear }),
[sidebarNav, t],
);
const {
section,
consoleOpen,
sidebarCollapsed,
companySwitcherOpen,
confirmDeleteCompanyId,
taskViewMode,
showOnboarding,
initialOnboardingLocked,
commandPaletteOpen,
commandPaletteSearch,
selectedPaletteIndex,
} = useAppStore(
useShallow((s) => ({
section: s.section,
consoleOpen: s.consoleOpen,
sidebarCollapsed: s.sidebarCollapsed,
companySwitcherOpen: s.companySwitcherOpen,
confirmDeleteCompanyId: s.confirmDeleteCompanyId,
taskViewMode: s.taskViewMode,
showOnboarding: s.showOnboarding,
initialOnboardingLocked: s.initialOnboardingLocked,
commandPaletteOpen: s.commandPaletteOpen,
commandPaletteSearch: s.commandPaletteSearch,
selectedPaletteIndex: s.selectedPaletteIndex,
})),
);
const {
setSection,
setConsoleOpen,
setSidebarCollapsed,
setCompanySwitcherOpen,
setConfirmDeleteCompanyId,
setTaskViewMode,
setShowOnboarding,
setInitialOnboardingLocked,
setCommandPaletteOpen,
setCommandPaletteSearch,
setSelectedPaletteIndex,
} = useAppStore.getState();
const handleLocaleChange = useCallback(async (newLocale: AppLocale) => {
useAppStore.getState().setLocale(newLocale);
try { await window.agentCompany.updateSettings({ locale: newLocale }); } catch { /* local state already updated */ }
}, []);
const paletteTrapRef = useFocusTrap(commandPaletteOpen);
const paletteQuery = useDeferredValue(commandPaletteSearch.trim().toLowerCase());
const buildPaletteResults = useCallback((
navItemsList: NavItem[],
companyAgents: AgentRecord[],
tasks: TaskRecord[],
runs: RunRecord[],
companyGoals: GoalRecord[],
companyProjects: ProjectRecord[],
) => {
const filteredNavItems = paletteQuery
? navItemsList.filter((item) => item.label.toLowerCase().includes(paletteQuery) || item.id.toLowerCase().includes(paletteQuery))
: navItemsList;
const matchedAgents = paletteQuery ? companyAgents.filter((a) => a.name.toLowerCase().includes(paletteQuery) || a.role.toLowerCase().includes(paletteQuery)).slice(0, 5) : [];
const matchedTasks = paletteQuery ? tasks.filter((task) => task.title.toLowerCase().includes(paletteQuery)).slice(0, 5) : [];
const matchedRuns = paletteQuery ? runs.filter((run) => {
const matchingTask = tasks.find((task) => task.id === run.taskId);
return [run.id, run.summary ?? "", run.errorMessage ?? "", matchingTask?.title ?? "", run.connectorId]
.some((value) => value.toLowerCase().includes(paletteQuery));
}).slice(0, 5) : [];
const matchedGoals = paletteQuery ? companyGoals.filter((g) => g.title.toLowerCase().includes(paletteQuery)).slice(0, 5) : [];
const matchedProjects = paletteQuery ? companyProjects.filter((p) => p.name.toLowerCase().includes(paletteQuery)).slice(0, 5) : [];
const hasEntityResults = matchedAgents.length > 0 || matchedTasks.length > 0 || matchedRuns.length > 0 || matchedGoals.length > 0 || matchedProjects.length > 0;
return { filteredNavItems, matchedAgents, matchedTasks, matchedRuns, matchedGoals, matchedProjects, hasEntityResults };
}, [paletteQuery]);
const buildPaletteActions = useCallback((
filteredNavItems: NavItem[],
matchedAgents: AgentRecord[],
matchedTasks: TaskRecord[],
matchedRuns: RunRecord[],
matchedGoals: GoalRecord[],
matchedProjects: ProjectRecord[],
): PaletteAction[] => ([
...filteredNavItems.map((item) => ({ key: `section:${item.id}`, section: item.id })),
...matchedAgents.map((agent) => ({ key: `agent:${agent.id}`, section: "agents" as const, entityId: agent.id })),
...matchedTasks.map((task) => ({ key: `task:${task.id}`, section: "tasks" as const, entityId: task.id })),
...matchedRuns.map((run) => ({ key: `run:${run.id}`, section: "runs" as const, entityId: run.id })),
...matchedGoals.map((goal) => ({ key: `goal:${goal.id}`, section: "goals" as const, entityId: goal.id })),
...matchedProjects.map((project) => ({ key: `project:${project.id}`, section: "projects" as const, entityId: project.id })),
]), []);
return {
pageTransition,
panelTransition,
locale,
t,
handleLocaleChange,
sidebarNav,
sidebarGroups,
navItems,
section,
setSection,
consoleOpen,
setConsoleOpen,
sidebarCollapsed,
setSidebarCollapsed,
companySwitcherOpen,
setCompanySwitcherOpen,
confirmDeleteCompanyId,
setConfirmDeleteCompanyId,
taskViewMode,
setTaskViewMode,
showOnboarding,
setShowOnboarding,
initialOnboardingLocked,
setInitialOnboardingLocked,
commandPaletteOpen,
setCommandPaletteOpen,
commandPaletteSearch,
setCommandPaletteSearch,
selectedPaletteIndex,
setSelectedPaletteIndex,
paletteQuery,
paletteTrapRef,
buildPaletteResults,
buildPaletteActions,
};
}