207 lines
7.8 KiB
TypeScript
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,
|
|
};
|
|
}
|