mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
I mean, it's at a good place rn...
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
export * from "./use-tasks";
|
||||
export * from "./use-agents";
|
||||
export * from "./use-channels";
|
||||
export * from "./use-notifications";
|
||||
// Re-export dashboard hooks excluding duplicates from use-agents
|
||||
export {
|
||||
dashboardKeys,
|
||||
useCeoOverview,
|
||||
useMetrics,
|
||||
useVelocityMetrics,
|
||||
useBlockerMetrics,
|
||||
useKanbanDev,
|
||||
useKanbanQa,
|
||||
useKanbanPm,
|
||||
useCeoTeamDetails,
|
||||
useCeoBlockerDetails,
|
||||
useCeoVelocity,
|
||||
useRecentActivity,
|
||||
useAgentStatus as useDashboardAgentStatus, // Renamed to avoid conflict
|
||||
useKanbanDocumenter,
|
||||
useKanbanCellPm,
|
||||
useKanbanBoard,
|
||||
useKanbanStats,
|
||||
useAuditorDashboard,
|
||||
useAuditorFlags,
|
||||
useAuditorReports,
|
||||
useCreateAuditorFlag,
|
||||
useResolveAuditorFlag,
|
||||
useCreateAuditorReport,
|
||||
useSendAuditorReport,
|
||||
} from "./use-dashboard";
|
||||
export * from "./use-websocket";
|
||||
export * from "./use-journals";
|
||||
export * from "./use-projects";
|
||||
export * from "./use-work-sessions";
|
||||
@@ -0,0 +1,154 @@
|
||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { orchestratorApi, type SpawnAgentRequest } from "@/lib/api/orchestrator";
|
||||
import { agentsApi } from "@/lib/api/agents";
|
||||
import type { Agent, AgentRole, Team, AgentState } from "@/types";
|
||||
|
||||
export type { AgentDefinition } from "@/lib/api/agents";
|
||||
|
||||
// Static agent roster for RoboCo (18 agents)
|
||||
const AGENT_ROSTER: Agent[] = [
|
||||
// Board / Management
|
||||
{ id: "1", agent_id: "main-pm", name: "Main PM", role: "main_pm" as AgentRole, team: null, cell: null, status: "idle" as AgentState },
|
||||
{ id: "2", agent_id: "product-owner", name: "Product Owner", role: "product_owner" as AgentRole, team: "board" as Team, cell: null, status: "idle" as AgentState },
|
||||
{ id: "3", agent_id: "head-marketing", name: "Head of Marketing", role: "head_marketing" as AgentRole, team: "board" as Team, cell: null, status: "idle" as AgentState },
|
||||
{ id: "4", agent_id: "auditor", name: "Auditor", role: "auditor" as AgentRole, team: null, cell: null, status: "idle" as AgentState },
|
||||
// Backend Cell
|
||||
{ id: "5", agent_id: "be-dev-1", name: "Backend Dev 1", role: "developer" as AgentRole, team: "backend" as Team, cell: "backend", status: "idle" as AgentState },
|
||||
{ id: "6", agent_id: "be-dev-2", name: "Backend Dev 2", role: "developer" as AgentRole, team: "backend" as Team, cell: "backend", status: "idle" as AgentState },
|
||||
{ id: "7", agent_id: "be-qa", name: "Backend QA", role: "qa" as AgentRole, team: "backend" as Team, cell: "backend", status: "idle" as AgentState },
|
||||
{ id: "8", agent_id: "be-pm", name: "Backend PM", role: "cell_pm" as AgentRole, team: "backend" as Team, cell: "backend", status: "idle" as AgentState },
|
||||
{ id: "9", agent_id: "be-doc", name: "Backend Documenter", role: "documenter" as AgentRole, team: "backend" as Team, cell: "backend", status: "idle" as AgentState },
|
||||
// Frontend Cell
|
||||
{ id: "10", agent_id: "fe-dev-1", name: "Frontend Dev 1", role: "developer" as AgentRole, team: "frontend" as Team, cell: "frontend", status: "idle" as AgentState },
|
||||
{ id: "11", agent_id: "fe-dev-2", name: "Frontend Dev 2", role: "developer" as AgentRole, team: "frontend" as Team, cell: "frontend", status: "idle" as AgentState },
|
||||
{ id: "12", agent_id: "fe-qa", name: "Frontend QA", role: "qa" as AgentRole, team: "frontend" as Team, cell: "frontend", status: "idle" as AgentState },
|
||||
{ id: "13", agent_id: "fe-pm", name: "Frontend PM", role: "cell_pm" as AgentRole, team: "frontend" as Team, cell: "frontend", status: "idle" as AgentState },
|
||||
{ id: "14", agent_id: "fe-doc", name: "Frontend Documenter", role: "documenter" as AgentRole, team: "frontend" as Team, cell: "frontend", status: "idle" as AgentState },
|
||||
// UX/UI Cell
|
||||
{ id: "15", agent_id: "ux-dev-1", name: "UX/UI Dev 1", role: "developer" as AgentRole, team: "ux_ui" as Team, cell: "ux_ui", status: "idle" as AgentState },
|
||||
{ id: "16", agent_id: "ux-dev-2", name: "UX/UI Dev 2", role: "developer" as AgentRole, team: "ux_ui" as Team, cell: "ux_ui", status: "idle" as AgentState },
|
||||
{ id: "16", agent_id: "ux-qa", name: "UX/UI QA", role: "qa" as AgentRole, team: "ux_ui" as Team, cell: "ux_ui", status: "idle" as AgentState },
|
||||
{ id: "17", agent_id: "ux-pm", name: "UX/UI PM", role: "cell_pm" as AgentRole, team: "ux_ui" as Team, cell: "ux_ui", status: "idle" as AgentState },
|
||||
{ id: "18", agent_id: "ux-doc", name: "UX/UI Documenter", role: "documenter" as AgentRole, team: "ux_ui" as Team, cell: "ux_ui", status: "idle" as AgentState },
|
||||
];
|
||||
|
||||
// Query keys
|
||||
export const agentKeys = {
|
||||
all: ["agents"] as const,
|
||||
definitions: () => [...agentKeys.all, "definitions"] as const,
|
||||
orchestrator: () => [...agentKeys.all, "orchestrator"] as const,
|
||||
status: () => [...agentKeys.orchestrator(), "status"] as const,
|
||||
waiting: () => [...agentKeys.orchestrator(), "waiting"] as const,
|
||||
agent: (id: string) => [...agentKeys.orchestrator(), "agent", id] as const,
|
||||
};
|
||||
|
||||
// Fetch agent definitions from API
|
||||
export function useAgentDefinitions() {
|
||||
return useQuery({
|
||||
queryKey: agentKeys.definitions(),
|
||||
queryFn: agentsApi.getAll,
|
||||
staleTime: 5 * 60 * 1000, // 5 min - agents don't change often
|
||||
});
|
||||
}
|
||||
|
||||
// Hooks
|
||||
|
||||
// Returns the static agent roster (optionally enriched with live status)
|
||||
export function useAgents() {
|
||||
const { data: orchestratorStatus } = useOrchestratorStatus();
|
||||
|
||||
return useQuery({
|
||||
queryKey: [...agentKeys.all, "roster"],
|
||||
queryFn: async (): Promise<Agent[]> => {
|
||||
// Build a map of agent statuses from the agents array
|
||||
const statusMap = new Map<string, string>();
|
||||
if (orchestratorStatus?.agents) {
|
||||
for (const agentStatus of orchestratorStatus.agents) {
|
||||
statusMap.set(agentStatus.agent_id, agentStatus.state);
|
||||
}
|
||||
}
|
||||
|
||||
// Enrich static roster with live status from orchestrator
|
||||
return AGENT_ROSTER.map((agent) => {
|
||||
const liveState = statusMap.get(agent.agent_id);
|
||||
return {
|
||||
...agent,
|
||||
status: (liveState as AgentState) ?? agent.status,
|
||||
};
|
||||
});
|
||||
},
|
||||
staleTime: Infinity, // Static data, only updates when orchestrator updates
|
||||
enabled: true,
|
||||
});
|
||||
}
|
||||
|
||||
export function useOrchestratorStatus() {
|
||||
return useQuery({
|
||||
queryKey: agentKeys.status(),
|
||||
queryFn: () => orchestratorApi.getStatus(),
|
||||
refetchInterval: 10000, // Refetch every 10 seconds
|
||||
});
|
||||
}
|
||||
|
||||
export function useAgentStatus(agentId: string) {
|
||||
return useQuery({
|
||||
queryKey: agentKeys.agent(agentId),
|
||||
queryFn: () => orchestratorApi.getAgentStatus(agentId),
|
||||
enabled: !!agentId,
|
||||
refetchInterval: 5000, // Refetch every 5 seconds
|
||||
});
|
||||
}
|
||||
|
||||
export function useWaitingAgents() {
|
||||
return useQuery({
|
||||
queryKey: agentKeys.waiting(),
|
||||
queryFn: () => orchestratorApi.getWaitingAgents(),
|
||||
refetchInterval: 10000,
|
||||
});
|
||||
}
|
||||
|
||||
export function useSpawnAgent() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ({ agentId, request }: { agentId: string; request?: SpawnAgentRequest }) =>
|
||||
orchestratorApi.spawn(agentId, request),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: agentKeys.orchestrator() });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useStopAgent() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ({ agentId, graceful = true }: { agentId: string; graceful?: boolean }) =>
|
||||
orchestratorApi.stop(agentId, graceful),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: agentKeys.orchestrator() });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useResolveWait() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ({ agentId, resolution }: { agentId: string; resolution: string }) =>
|
||||
orchestratorApi.resolveWait(agentId, resolution),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: agentKeys.orchestrator() });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Fetch a single agent definition by ID or slug
|
||||
export function useAgentDefinition(agentId: string) {
|
||||
return useQuery({
|
||||
queryKey: [...agentKeys.definitions(), agentId],
|
||||
queryFn: () => agentsApi.getOne(agentId),
|
||||
enabled: !!agentId,
|
||||
staleTime: 5 * 60 * 1000, // 5 min - agent definitions don't change often
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
"use client";
|
||||
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { channelsApi, type ChannelFilters } from "@/lib/api/channels";
|
||||
import { sessionsApi } from "@/lib/api/sessions";
|
||||
import { messagesApi } from "@/lib/api/messages";
|
||||
import { tasksApi } from "@/lib/api/tasks";
|
||||
|
||||
export const channelKeys = {
|
||||
all: ["channels"] as const,
|
||||
list: (filters?: ChannelFilters) => [...channelKeys.all, "list", filters] as const,
|
||||
detail: (id: string) => [...channelKeys.all, "detail", id] as const,
|
||||
groups: (channelId: string) => [...channelKeys.all, "groups", channelId] as const,
|
||||
};
|
||||
|
||||
export const sessionKeys = {
|
||||
all: ["sessions"] as const,
|
||||
list: (groupId: string) => [...sessionKeys.all, "list", groupId] as const,
|
||||
detail: (id: string) => [...sessionKeys.all, "detail", id] as const,
|
||||
};
|
||||
|
||||
export const messageKeys = {
|
||||
all: ["messages"] as const,
|
||||
list: (sessionId: string) => [...messageKeys.all, "list", sessionId] as const,
|
||||
};
|
||||
|
||||
// Fetch channel list once - manual refresh available
|
||||
export function useChannels(filters?: ChannelFilters) {
|
||||
return useQuery({
|
||||
queryKey: channelKeys.list(filters),
|
||||
queryFn: () => channelsApi.list(filters),
|
||||
staleTime: Infinity,
|
||||
});
|
||||
}
|
||||
|
||||
export function useChannel(channelId: string | null) {
|
||||
return useQuery({
|
||||
queryKey: channelKeys.detail(channelId || ""),
|
||||
queryFn: () => channelsApi.get(channelId!),
|
||||
enabled: !!channelId,
|
||||
staleTime: Infinity,
|
||||
});
|
||||
}
|
||||
|
||||
// Fetch groups for a channel
|
||||
export function useChannelGroups(channelId: string | null) {
|
||||
return useQuery({
|
||||
queryKey: channelKeys.groups(channelId || ""),
|
||||
queryFn: () => channelsApi.getGroups(channelId!),
|
||||
enabled: !!channelId,
|
||||
staleTime: Infinity,
|
||||
});
|
||||
}
|
||||
|
||||
// Fetch sessions for a group
|
||||
export function useGroupSessions(groupId: string | null) {
|
||||
return useQuery({
|
||||
queryKey: sessionKeys.list(groupId || ""),
|
||||
queryFn: () => sessionsApi.listByGroup(groupId!),
|
||||
enabled: !!groupId,
|
||||
staleTime: Infinity,
|
||||
});
|
||||
}
|
||||
|
||||
// Fetch a single session by ID (with task links and task titles)
|
||||
export function useSession(sessionId: string | null) {
|
||||
return useQuery({
|
||||
queryKey: sessionKeys.detail(sessionId || ""),
|
||||
queryFn: async () => {
|
||||
const session = await sessionsApi.get(sessionId!);
|
||||
// Fetch task links separately since the endpoint doesn't include them
|
||||
try {
|
||||
const taskLinks = await sessionsApi.getTasksForSession(sessionId!);
|
||||
|
||||
// Fetch task details to get titles
|
||||
const taskLinksWithTitles = await Promise.all(
|
||||
taskLinks.map(async (link) => {
|
||||
try {
|
||||
const task = await tasksApi.get(link.task_id);
|
||||
return {
|
||||
task_id: link.task_id,
|
||||
task_title: task.title,
|
||||
is_primary: link.is_primary,
|
||||
relationship_type: link.relationship_type,
|
||||
};
|
||||
} catch {
|
||||
return {
|
||||
task_id: link.task_id,
|
||||
task_title: null,
|
||||
is_primary: link.is_primary,
|
||||
relationship_type: link.relationship_type,
|
||||
};
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
return {
|
||||
...session,
|
||||
task_links: taskLinksWithTitles,
|
||||
};
|
||||
} catch {
|
||||
// If fetching task links fails, return session without them
|
||||
return session;
|
||||
}
|
||||
},
|
||||
enabled: !!sessionId,
|
||||
staleTime: 1000 * 60 * 5, // 5 minutes
|
||||
});
|
||||
}
|
||||
|
||||
// Fetch messages for a session - WebSocket handles new messages
|
||||
export function useSessionMessages(sessionId: string | null) {
|
||||
return useQuery({
|
||||
queryKey: messageKeys.list(sessionId || ""),
|
||||
queryFn: () => messagesApi.listBySession(sessionId!),
|
||||
enabled: !!sessionId,
|
||||
staleTime: Infinity,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,312 @@
|
||||
"use client";
|
||||
|
||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
dashboardApi,
|
||||
MetricsSummary,
|
||||
CreateFlagRequest,
|
||||
CreateReportRequest,
|
||||
} from "@/lib/api/dashboard";
|
||||
import type {
|
||||
Team,
|
||||
AuditorDashboard,
|
||||
AuditorFlag,
|
||||
AuditorReport,
|
||||
FlagSeverity,
|
||||
} from "@/types";
|
||||
|
||||
export const dashboardKeys = {
|
||||
all: ["dashboard"] as const,
|
||||
ceoOverview: () => [...dashboardKeys.all, "ceo-overview"] as const,
|
||||
ceoTeams: () => [...dashboardKeys.all, "ceo-teams"] as const,
|
||||
ceoBlockers: () => [...dashboardKeys.all, "ceo-blockers"] as const,
|
||||
ceoVelocity: (days: number) =>
|
||||
[...dashboardKeys.all, "ceo-velocity", days] as const,
|
||||
metrics: () => [...dashboardKeys.all, "metrics"] as const,
|
||||
velocity: () => [...dashboardKeys.all, "velocity"] as const,
|
||||
blockers: () => [...dashboardKeys.all, "blockers"] as const,
|
||||
activity: (hours: number) =>
|
||||
[...dashboardKeys.all, "activity", hours] as const,
|
||||
agentStatus: () => [...dashboardKeys.all, "agent-status"] as const,
|
||||
// Kanban keys
|
||||
kanbanDev: (team: Team) =>
|
||||
[...dashboardKeys.all, "kanban", "dev", team] as const,
|
||||
kanbanQa: (team: Team) =>
|
||||
[...dashboardKeys.all, "kanban", "qa", team] as const,
|
||||
kanbanDocumenter: (team: Team) =>
|
||||
[...dashboardKeys.all, "kanban", "documenter", team] as const,
|
||||
kanbanCellPm: (team: Team) =>
|
||||
[...dashboardKeys.all, "kanban", "cell-pm", team] as const,
|
||||
kanbanPm: () => [...dashboardKeys.all, "kanban", "pm"] as const,
|
||||
kanbanBoard: () => [...dashboardKeys.all, "kanban", "board"] as const,
|
||||
kanbanStats: (team?: Team) =>
|
||||
[...dashboardKeys.all, "kanban", "stats", team] as const,
|
||||
// Auditor keys
|
||||
auditor: () => [...dashboardKeys.all, "auditor"] as const,
|
||||
auditorFlags: (params?: { severity?: FlagSeverity; resolved?: boolean }) =>
|
||||
[...dashboardKeys.all, "auditor", "flags", params] as const,
|
||||
auditorReports: (params?: { report_type?: string; limit?: number }) =>
|
||||
[...dashboardKeys.all, "auditor", "reports", params] as const,
|
||||
};
|
||||
|
||||
export function useCeoOverview() {
|
||||
return useQuery({
|
||||
queryKey: dashboardKeys.ceoOverview(),
|
||||
queryFn: () => dashboardApi.getCeoOverview(),
|
||||
refetchInterval: 60000, // Refetch every minute
|
||||
});
|
||||
}
|
||||
|
||||
export function useMetrics() {
|
||||
return useQuery({
|
||||
queryKey: dashboardKeys.metrics(),
|
||||
queryFn: async (): Promise<MetricsSummary> => {
|
||||
// Fetch all metrics in parallel
|
||||
const [velocity, blockers, communication] = await Promise.all([
|
||||
dashboardApi.getVelocityMetrics(),
|
||||
dashboardApi.getBlockerMetrics(),
|
||||
dashboardApi.getCommunicationMetrics(),
|
||||
]);
|
||||
return {
|
||||
velocity,
|
||||
blockers,
|
||||
communication,
|
||||
agents: { total_agents: 0, running: 0, idle: 0, waiting: 0, errors: 0 },
|
||||
};
|
||||
},
|
||||
refetchInterval: 60000,
|
||||
});
|
||||
}
|
||||
|
||||
export function useVelocityMetrics() {
|
||||
return useQuery({
|
||||
queryKey: dashboardKeys.velocity(),
|
||||
queryFn: () => dashboardApi.getVelocityMetrics(),
|
||||
refetchInterval: 60000,
|
||||
});
|
||||
}
|
||||
|
||||
export function useBlockerMetrics() {
|
||||
return useQuery({
|
||||
queryKey: dashboardKeys.blockers(),
|
||||
queryFn: () => dashboardApi.getBlockerMetrics(),
|
||||
refetchInterval: 60000,
|
||||
});
|
||||
}
|
||||
|
||||
export function useKanbanDev(team: Team) {
|
||||
return useQuery({
|
||||
queryKey: dashboardKeys.kanbanDev(team),
|
||||
queryFn: () => dashboardApi.getKanbanDev(team),
|
||||
refetchInterval: 30000,
|
||||
});
|
||||
}
|
||||
|
||||
export function useKanbanQa(team: Team) {
|
||||
return useQuery({
|
||||
queryKey: dashboardKeys.kanbanQa(team),
|
||||
queryFn: () => dashboardApi.getKanbanQa(team),
|
||||
refetchInterval: 30000,
|
||||
});
|
||||
}
|
||||
|
||||
export function useKanbanPm() {
|
||||
return useQuery({
|
||||
queryKey: dashboardKeys.kanbanPm(),
|
||||
queryFn: () => dashboardApi.getKanbanPm(),
|
||||
refetchInterval: 30000,
|
||||
});
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// ADDITIONAL CEO HOOKS
|
||||
// =============================================================================
|
||||
|
||||
export function useCeoTeamDetails() {
|
||||
return useQuery({
|
||||
queryKey: dashboardKeys.ceoTeams(),
|
||||
queryFn: () => dashboardApi.getCeoTeamDetails(),
|
||||
refetchInterval: 60000,
|
||||
});
|
||||
}
|
||||
|
||||
export function useCeoBlockerDetails() {
|
||||
return useQuery({
|
||||
queryKey: dashboardKeys.ceoBlockers(),
|
||||
queryFn: () => dashboardApi.getCeoBlockerDetails(),
|
||||
refetchInterval: 60000,
|
||||
});
|
||||
}
|
||||
|
||||
export function useCeoVelocity(days: number = 7) {
|
||||
return useQuery({
|
||||
queryKey: dashboardKeys.ceoVelocity(days),
|
||||
queryFn: () => dashboardApi.getCeoVelocity(days),
|
||||
refetchInterval: 60000,
|
||||
});
|
||||
}
|
||||
|
||||
export function useRecentActivity(hours: number = 24) {
|
||||
return useQuery({
|
||||
queryKey: dashboardKeys.activity(hours),
|
||||
queryFn: () => dashboardApi.getRecentActivity(hours),
|
||||
refetchInterval: 30000,
|
||||
});
|
||||
}
|
||||
|
||||
export function useAgentStatus() {
|
||||
return useQuery({
|
||||
queryKey: dashboardKeys.agentStatus(),
|
||||
queryFn: () => dashboardApi.getAgentStatus(),
|
||||
refetchInterval: 10000,
|
||||
});
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// ADDITIONAL KANBAN HOOKS
|
||||
// =============================================================================
|
||||
|
||||
export function useKanbanDocumenter(team: Team) {
|
||||
return useQuery({
|
||||
queryKey: dashboardKeys.kanbanDocumenter(team),
|
||||
queryFn: () => dashboardApi.getKanbanDocumenter(team),
|
||||
refetchInterval: 30000,
|
||||
});
|
||||
}
|
||||
|
||||
export function useKanbanCellPm(team: Team) {
|
||||
return useQuery({
|
||||
queryKey: dashboardKeys.kanbanCellPm(team),
|
||||
queryFn: () => dashboardApi.getKanbanCellPm(team),
|
||||
refetchInterval: 30000,
|
||||
});
|
||||
}
|
||||
|
||||
export function useKanbanBoard() {
|
||||
return useQuery({
|
||||
queryKey: dashboardKeys.kanbanBoard(),
|
||||
queryFn: () => dashboardApi.getKanbanBoard(),
|
||||
refetchInterval: 30000,
|
||||
});
|
||||
}
|
||||
|
||||
export function useKanbanStats(team?: Team) {
|
||||
return useQuery({
|
||||
queryKey: dashboardKeys.kanbanStats(team),
|
||||
queryFn: () => dashboardApi.getKanbanStats(team),
|
||||
refetchInterval: 60000,
|
||||
});
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// AUDITOR DASHBOARD HOOKS
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Get the complete auditor dashboard
|
||||
*/
|
||||
export function useAuditorDashboard() {
|
||||
return useQuery<AuditorDashboard>({
|
||||
queryKey: dashboardKeys.auditor(),
|
||||
queryFn: () => dashboardApi.getAuditorDashboard(),
|
||||
refetchInterval: 30000,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get auditor flags with optional filters
|
||||
*/
|
||||
export function useAuditorFlags(params?: {
|
||||
severity?: FlagSeverity;
|
||||
resolved?: boolean;
|
||||
}) {
|
||||
return useQuery<AuditorFlag[]>({
|
||||
queryKey: dashboardKeys.auditorFlags(params),
|
||||
queryFn: () => dashboardApi.getAuditorFlags(params),
|
||||
refetchInterval: 30000,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get auditor reports
|
||||
*/
|
||||
export function useAuditorReports(params?: {
|
||||
report_type?: string;
|
||||
limit?: number;
|
||||
}) {
|
||||
return useQuery<AuditorReport[]>({
|
||||
queryKey: dashboardKeys.auditorReports(params),
|
||||
queryFn: () => dashboardApi.getAuditorReports(params),
|
||||
refetchInterval: 60000,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new auditor flag
|
||||
*/
|
||||
export function useCreateAuditorFlag() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (data: CreateFlagRequest) => dashboardApi.createAuditorFlag(data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: dashboardKeys.auditor() });
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: dashboardKeys.auditorFlags(),
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve an auditor flag
|
||||
*/
|
||||
export function useResolveAuditorFlag() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ({ flagId, notes }: { flagId: string; notes?: string }) =>
|
||||
dashboardApi.resolveAuditorFlag(flagId, notes),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: dashboardKeys.auditor() });
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: dashboardKeys.auditorFlags(),
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new auditor report
|
||||
*/
|
||||
export function useCreateAuditorReport() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (data: CreateReportRequest) =>
|
||||
dashboardApi.createAuditorReport(data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: dashboardKeys.auditor() });
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: dashboardKeys.auditorReports(),
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Send an auditor report to CEO
|
||||
*/
|
||||
export function useSendAuditorReport() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (reportId: string) => dashboardApi.sendAuditorReport(reportId),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: dashboardKeys.auditor() });
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: dashboardKeys.auditorReports(),
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
/**
|
||||
* Git React Query Hooks
|
||||
*
|
||||
* React Query hooks for Git operations.
|
||||
*/
|
||||
|
||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { gitApi } from "@/lib/api/git";
|
||||
import type {
|
||||
GitStatusResponse,
|
||||
GitLogResponse,
|
||||
GitBranchListResponse,
|
||||
GitDiffResponse,
|
||||
GitCommitRequest,
|
||||
GitCommitResponse,
|
||||
GitPushRequest,
|
||||
GitPushResponse,
|
||||
GitCreateBranchRequest,
|
||||
GitCreateBranchResponse,
|
||||
GitCheckoutRequest,
|
||||
GitCheckoutResponse,
|
||||
GitCreatePRRequest,
|
||||
GitCreatePRResponse,
|
||||
GitMergePRRequest,
|
||||
GitMergePRResponse,
|
||||
} from "@/types/git";
|
||||
|
||||
// =============================================================================
|
||||
// Query Keys
|
||||
// =============================================================================
|
||||
|
||||
export const gitKeys = {
|
||||
all: ["git"] as const,
|
||||
status: (projectSlug: string) => [...gitKeys.all, "status", projectSlug] as const,
|
||||
log: (projectSlug: string, limit?: number, branch?: string) =>
|
||||
[...gitKeys.all, "log", projectSlug, { limit, branch }] as const,
|
||||
branches: (projectSlug: string, includeRemote?: boolean) =>
|
||||
[...gitKeys.all, "branches", projectSlug, { includeRemote }] as const,
|
||||
diff: (projectSlug: string, staged?: boolean, filePath?: string) =>
|
||||
[...gitKeys.all, "diff", projectSlug, { staged, filePath }] as const,
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
// Query Hooks
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Get git status for a project
|
||||
*/
|
||||
export function useGitStatus(projectSlug: string, taskId?: string, enabled: boolean = true) {
|
||||
return useQuery<GitStatusResponse>({
|
||||
queryKey: gitKeys.status(projectSlug),
|
||||
queryFn: () => gitApi.getStatus(projectSlug, taskId),
|
||||
enabled: enabled && !!projectSlug,
|
||||
staleTime: 10000, // 10 seconds - status changes frequently
|
||||
refetchInterval: 30000, // Auto-refresh every 30s
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get git log for a project
|
||||
*/
|
||||
export function useGitLog(
|
||||
projectSlug: string,
|
||||
limit: number = 10,
|
||||
branch?: string,
|
||||
enabled: boolean = true
|
||||
) {
|
||||
return useQuery<GitLogResponse>({
|
||||
queryKey: gitKeys.log(projectSlug, limit, branch),
|
||||
queryFn: () => gitApi.getLog(projectSlug, limit, branch),
|
||||
enabled: enabled && !!projectSlug,
|
||||
staleTime: 30000, // 30 seconds
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get git branches for a project
|
||||
*/
|
||||
export function useGitBranches(
|
||||
projectSlug: string,
|
||||
includeRemote: boolean = false,
|
||||
enabled: boolean = true
|
||||
) {
|
||||
return useQuery<GitBranchListResponse>({
|
||||
queryKey: gitKeys.branches(projectSlug, includeRemote),
|
||||
queryFn: () => gitApi.getBranches(projectSlug, includeRemote),
|
||||
enabled: enabled && !!projectSlug,
|
||||
staleTime: 60000, // 1 minute
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get git diff for a project
|
||||
*/
|
||||
export function useGitDiff(
|
||||
projectSlug: string,
|
||||
staged: boolean = false,
|
||||
filePath?: string,
|
||||
enabled: boolean = true
|
||||
) {
|
||||
return useQuery<GitDiffResponse>({
|
||||
queryKey: gitKeys.diff(projectSlug, staged, filePath),
|
||||
queryFn: () => gitApi.getDiff(projectSlug, staged, filePath),
|
||||
enabled: enabled && !!projectSlug,
|
||||
staleTime: 10000, // 10 seconds - diff can change frequently
|
||||
});
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Mutation Hooks
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Create a git commit
|
||||
*/
|
||||
export function useGitCommit() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<GitCommitResponse, Error, GitCommitRequest>({
|
||||
mutationFn: (request) => gitApi.commit(request),
|
||||
onSuccess: (_, variables) => {
|
||||
// Invalidate status and log after commit
|
||||
queryClient.invalidateQueries({ queryKey: gitKeys.status(variables.project_slug) });
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [...gitKeys.all, "log", variables.project_slug],
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [...gitKeys.all, "diff", variables.project_slug],
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Push commits to remote
|
||||
*/
|
||||
export function useGitPush() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<GitPushResponse, Error, GitPushRequest>({
|
||||
mutationFn: (request) => gitApi.push(request),
|
||||
onSuccess: (_, variables) => {
|
||||
// Invalidate status after push
|
||||
queryClient.invalidateQueries({ queryKey: gitKeys.status(variables.project_slug) });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a task branch
|
||||
*/
|
||||
export function useCreateBranch() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<GitCreateBranchResponse, Error, GitCreateBranchRequest>({
|
||||
mutationFn: (request) => gitApi.createBranch(request),
|
||||
onSuccess: (_, variables) => {
|
||||
// Invalidate branches after creating a new one
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [...gitKeys.all, "branches", variables.project_slug],
|
||||
});
|
||||
queryClient.invalidateQueries({ queryKey: gitKeys.status(variables.project_slug) });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Checkout a branch
|
||||
*/
|
||||
export function useCheckout() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<GitCheckoutResponse, Error, GitCheckoutRequest>({
|
||||
mutationFn: (request) => gitApi.checkout(request),
|
||||
onSuccess: (_, variables) => {
|
||||
// Invalidate everything for this project after checkout
|
||||
queryClient.invalidateQueries({ queryKey: gitKeys.status(variables.project_slug) });
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [...gitKeys.all, "log", variables.project_slug],
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [...gitKeys.all, "diff", variables.project_slug],
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [...gitKeys.all, "branches", variables.project_slug],
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a pull request
|
||||
*/
|
||||
export function useCreatePR() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<GitCreatePRResponse, Error, GitCreatePRRequest>({
|
||||
mutationFn: (request) => gitApi.createPR(request),
|
||||
onSuccess: (_, variables) => {
|
||||
// Invalidate status after PR creation
|
||||
queryClient.invalidateQueries({ queryKey: gitKeys.status(variables.project_slug) });
|
||||
// Also invalidate tasks since PR creation updates task state
|
||||
queryClient.invalidateQueries({ queryKey: ["tasks"] });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge a pull request
|
||||
*/
|
||||
export function useMergePR() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<GitMergePRResponse, Error, GitMergePRRequest>({
|
||||
mutationFn: (request) => gitApi.mergePR(request),
|
||||
onSuccess: (_, variables) => {
|
||||
// Invalidate branches after merge
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [...gitKeys.all, "branches", variables.project_slug],
|
||||
});
|
||||
queryClient.invalidateQueries({ queryKey: gitKeys.status(variables.project_slug) });
|
||||
// Also invalidate tasks since merge updates task state
|
||||
queryClient.invalidateQueries({ queryKey: ["tasks"] });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Bundled Hook for Git Operations
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Bundled hook for all git write operations
|
||||
*/
|
||||
export function useGitOperations() {
|
||||
const commit = useGitCommit();
|
||||
const push = useGitPush();
|
||||
const createBranch = useCreateBranch();
|
||||
const checkout = useCheckout();
|
||||
const createPR = useCreatePR();
|
||||
const mergePR = useMergePR();
|
||||
|
||||
return {
|
||||
commit,
|
||||
push,
|
||||
createBranch,
|
||||
checkout,
|
||||
createPR,
|
||||
mergePR,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,273 @@
|
||||
/**
|
||||
* Journals Hooks
|
||||
*
|
||||
* React Query hooks for journal operations.
|
||||
*/
|
||||
|
||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { journalsApi } from "@/lib/api/journals";
|
||||
import type {
|
||||
Journal,
|
||||
JournalEntry,
|
||||
JournalEntryCreate,
|
||||
JournalEntryType,
|
||||
JournalStats,
|
||||
GrowthMetrics,
|
||||
} from "@/types";
|
||||
|
||||
// =============================================================================
|
||||
// QUERY KEYS
|
||||
// =============================================================================
|
||||
|
||||
export const journalKeys = {
|
||||
all: ["journals"] as const,
|
||||
myJournal: () => [...journalKeys.all, "my"] as const,
|
||||
journal: (agentId: string) => [...journalKeys.all, agentId] as const,
|
||||
entries: () => [...journalKeys.all, "entries"] as const,
|
||||
myEntries: (params?: { entry_type?: JournalEntryType; task_id?: string }) =>
|
||||
[...journalKeys.entries(), "my", params] as const,
|
||||
entry: (entryId: string) => [...journalKeys.entries(), entryId] as const,
|
||||
stats: () => [...journalKeys.all, "stats"] as const,
|
||||
myStats: () => [...journalKeys.stats(), "my"] as const,
|
||||
growth: () => [...journalKeys.all, "growth"] as const,
|
||||
myGrowth: () => [...journalKeys.growth(), "my"] as const,
|
||||
search: (query: string) => [...journalKeys.all, "search", query] as const,
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
// JOURNAL QUERIES
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Get the current agent's journal
|
||||
*/
|
||||
export function useMyJournal() {
|
||||
return useQuery<Journal>({
|
||||
queryKey: journalKeys.myJournal(),
|
||||
queryFn: journalsApi.getMyJournal,
|
||||
staleTime: 1000 * 60 * 5, // 5 minutes
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a journal by agent ID or slug (e.g., "be-dev-1")
|
||||
*/
|
||||
export function useJournalByAgent(agentIdOrSlug: string, enabled = true) {
|
||||
return useQuery<Journal>({
|
||||
queryKey: journalKeys.journal(agentIdOrSlug),
|
||||
queryFn: () => journalsApi.getJournalByAgent(agentIdOrSlug),
|
||||
enabled: enabled && !!agentIdOrSlug,
|
||||
staleTime: 1000 * 60 * 5, // 5 minutes
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* List entries for a specific agent by ID or slug
|
||||
*/
|
||||
export function useAgentJournalEntries(
|
||||
agentIdOrSlug: string,
|
||||
params?: {
|
||||
entry_type?: JournalEntryType;
|
||||
task_id?: string;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
}
|
||||
) {
|
||||
return useQuery<JournalEntry[]>({
|
||||
queryKey: [...journalKeys.entries(), "agent", agentIdOrSlug, params],
|
||||
queryFn: () => journalsApi.listAgentEntries(agentIdOrSlug, params),
|
||||
enabled: !!agentIdOrSlug,
|
||||
staleTime: 1000 * 60 * 2, // 2 minutes
|
||||
});
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// ENTRY QUERIES
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* List current agent's journal entries
|
||||
*/
|
||||
export function useMyJournalEntries(params?: {
|
||||
entry_type?: JournalEntryType;
|
||||
task_id?: string;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
}) {
|
||||
return useQuery<JournalEntry[]>({
|
||||
queryKey: journalKeys.myEntries(params),
|
||||
queryFn: () => journalsApi.listMyEntries(params),
|
||||
staleTime: 1000 * 60 * 2, // 2 minutes
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific journal entry
|
||||
*/
|
||||
export function useJournalEntry(entryId: string, enabled = true) {
|
||||
return useQuery<JournalEntry>({
|
||||
queryKey: journalKeys.entry(entryId),
|
||||
queryFn: () => journalsApi.getEntry(entryId),
|
||||
enabled,
|
||||
staleTime: 1000 * 60 * 5, // 5 minutes
|
||||
});
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// ANALYTICS QUERIES
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Get journal statistics
|
||||
*/
|
||||
export function useMyJournalStats() {
|
||||
return useQuery<JournalStats>({
|
||||
queryKey: journalKeys.myStats(),
|
||||
queryFn: journalsApi.getMyStats,
|
||||
staleTime: 1000 * 60 * 5, // 5 minutes
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get growth metrics
|
||||
*/
|
||||
export function useMyGrowthMetrics() {
|
||||
return useQuery<GrowthMetrics>({
|
||||
queryKey: journalKeys.myGrowth(),
|
||||
queryFn: journalsApi.getMyGrowthMetrics,
|
||||
staleTime: 1000 * 60 * 5, // 5 minutes
|
||||
});
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// MUTATIONS
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Create a new journal entry
|
||||
*/
|
||||
export function useCreateJournalEntry() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (data: JournalEntryCreate) => journalsApi.createEntry(data),
|
||||
onSuccess: () => {
|
||||
// Invalidate entries and stats
|
||||
queryClient.invalidateQueries({ queryKey: journalKeys.entries() });
|
||||
queryClient.invalidateQueries({ queryKey: journalKeys.stats() });
|
||||
queryClient.invalidateQueries({ queryKey: journalKeys.myJournal() });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a journal entry
|
||||
*/
|
||||
export function useDeleteJournalEntry() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (entryId: string) => journalsApi.deleteEntry(entryId),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: journalKeys.entries() });
|
||||
queryClient.invalidateQueries({ queryKey: journalKeys.stats() });
|
||||
queryClient.invalidateQueries({ queryKey: journalKeys.myJournal() });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a task reflection
|
||||
*/
|
||||
export function useAddTaskReflection() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (data: Parameters<typeof journalsApi.addTaskReflection>[0]) =>
|
||||
journalsApi.addTaskReflection(data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: journalKeys.entries() });
|
||||
queryClient.invalidateQueries({ queryKey: journalKeys.stats() });
|
||||
queryClient.invalidateQueries({ queryKey: journalKeys.growth() });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a decision log
|
||||
*/
|
||||
export function useAddDecisionLog() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (data: Parameters<typeof journalsApi.addDecisionLog>[0]) =>
|
||||
journalsApi.addDecisionLog(data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: journalKeys.entries() });
|
||||
queryClient.invalidateQueries({ queryKey: journalKeys.stats() });
|
||||
queryClient.invalidateQueries({ queryKey: journalKeys.growth() });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a learning entry
|
||||
*/
|
||||
export function useAddLearning() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (data: Parameters<typeof journalsApi.addLearning>[0]) =>
|
||||
journalsApi.addLearning(data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: journalKeys.entries() });
|
||||
queryClient.invalidateQueries({ queryKey: journalKeys.stats() });
|
||||
queryClient.invalidateQueries({ queryKey: journalKeys.growth() });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a struggle entry
|
||||
*/
|
||||
export function useAddStruggle() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (data: Parameters<typeof journalsApi.addStruggle>[0]) =>
|
||||
journalsApi.addStruggle(data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: journalKeys.entries() });
|
||||
queryClient.invalidateQueries({ queryKey: journalKeys.stats() });
|
||||
queryClient.invalidateQueries({ queryKey: journalKeys.growth() });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a general note
|
||||
*/
|
||||
export function useAddNote() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (data: Parameters<typeof journalsApi.addNote>[0]) =>
|
||||
journalsApi.addNote(data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: journalKeys.entries() });
|
||||
queryClient.invalidateQueries({ queryKey: journalKeys.stats() });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Search journal entries
|
||||
*/
|
||||
export function useSearchJournalEntries(query: string, topK = 10) {
|
||||
return useQuery<JournalEntry[]>({
|
||||
queryKey: journalKeys.search(query),
|
||||
queryFn: () => journalsApi.searchEntries(query, topK),
|
||||
enabled: query.length > 2, // Only search if query is meaningful
|
||||
staleTime: 1000 * 60 * 2, // 2 minutes
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,377 @@
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* Knowledge Base Hooks
|
||||
*
|
||||
* React Query hooks for KB search and RAG operations.
|
||||
*/
|
||||
|
||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { knowledgeBaseApi } from "@/lib/api/knowledge-base";
|
||||
import type {
|
||||
KBSearchRequest,
|
||||
KBSearchResponse,
|
||||
RAGQueryRequest,
|
||||
RAGQueryResponse,
|
||||
KBStats,
|
||||
KBIndexType,
|
||||
KBIndexStats,
|
||||
RAGHealthResponse,
|
||||
MentorAskRequest,
|
||||
MentorAskResponse,
|
||||
ErrorSearchRequest,
|
||||
ErrorSearchResponse,
|
||||
ErrorRecordRequest,
|
||||
ErrorRecordResponse,
|
||||
DecisionCheckRequest,
|
||||
DecisionCheckResponse,
|
||||
DecisionRecordRequest,
|
||||
DecisionRecordResponse,
|
||||
StandardsGetRequest,
|
||||
StandardsGetResponse,
|
||||
ValidateActionRequest,
|
||||
ValidateActionResponse,
|
||||
CodeReviewRequest,
|
||||
CodeReviewResponse,
|
||||
LearningRecordRequest,
|
||||
LearningRecordResponse,
|
||||
LearningSearchRequest,
|
||||
ProactiveContextResponse,
|
||||
TokenEstimateRequest,
|
||||
TokenEstimateResponse,
|
||||
RefreshIndexRequest,
|
||||
RefreshIndexResponse,
|
||||
ClearIndexResponse,
|
||||
ReindexResponse,
|
||||
ReindexRequest,
|
||||
IndexStalenessResponse,
|
||||
} from "@/types";
|
||||
|
||||
// =============================================================================
|
||||
// QUERY KEYS
|
||||
// =============================================================================
|
||||
|
||||
export const kbKeys = {
|
||||
all: ["knowledge-base"] as const,
|
||||
stats: () => [...kbKeys.all, "stats"] as const,
|
||||
indexStats: (indexType: KBIndexType) => [...kbKeys.all, "stats", indexType] as const,
|
||||
health: () => [...kbKeys.all, "health"] as const,
|
||||
staleness: () => [...kbKeys.all, "staleness"] as const,
|
||||
search: (query: string, filters?: string) => [...kbKeys.all, "search", query, filters] as const,
|
||||
documents: (indexType: KBIndexType, params?: string) => [...kbKeys.all, "documents", indexType, params] as const,
|
||||
learnings: (query: string, filters?: string) => [...kbKeys.all, "learnings", query, filters] as const,
|
||||
proactiveContext: (taskId: string) => [...kbKeys.all, "proactive-context", taskId] as const,
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
// STATS QUERIES
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Get KB index statistics
|
||||
*/
|
||||
export function useKBStats() {
|
||||
return useQuery<KBStats>({
|
||||
queryKey: kbKeys.stats(),
|
||||
queryFn: () => knowledgeBaseApi.getStats(),
|
||||
staleTime: 1000 * 60 * 5, // 5 minutes
|
||||
retry: 1,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get stats for a specific index type
|
||||
*/
|
||||
export function useKBIndexStats(indexType: KBIndexType) {
|
||||
return useQuery<KBIndexStats>({
|
||||
queryKey: kbKeys.indexStats(indexType),
|
||||
queryFn: () => knowledgeBaseApi.getIndexStats(indexType),
|
||||
staleTime: 1000 * 60 * 5, // 5 minutes
|
||||
});
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// SEARCH QUERIES
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Semantic search (enabled only when query has content)
|
||||
*/
|
||||
export function useKBSearch(params: KBSearchRequest, enabled = true) {
|
||||
return useQuery<KBSearchResponse>({
|
||||
queryKey: kbKeys.search(params.query, JSON.stringify(params.index_types)),
|
||||
queryFn: () => knowledgeBaseApi.search(params),
|
||||
enabled: enabled && params.query.length >= 3,
|
||||
staleTime: 1000 * 60 * 2, // 2 minutes
|
||||
});
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// BROWSE QUERIES
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* List documents in a specific index
|
||||
*/
|
||||
export function useKBDocuments(
|
||||
indexType: KBIndexType,
|
||||
params?: { limit?: number; offset?: number },
|
||||
enabled = true
|
||||
) {
|
||||
return useQuery({
|
||||
queryKey: kbKeys.documents(indexType, JSON.stringify(params)),
|
||||
queryFn: () => knowledgeBaseApi.listDocuments(indexType, params),
|
||||
enabled,
|
||||
staleTime: 1000 * 60 * 5, // 5 minutes
|
||||
});
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// MUTATIONS
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* RAG Query - mutation because it triggers AI generation
|
||||
*/
|
||||
export function useRAGQuery() {
|
||||
return useMutation<RAGQueryResponse, Error, RAGQueryRequest>({
|
||||
mutationFn: (params) => knowledgeBaseApi.ragQuery(params),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get context without generating answer
|
||||
*/
|
||||
export function useRAGContext() {
|
||||
return useMutation({
|
||||
mutationFn: (params: RAGQueryRequest) => knowledgeBaseApi.getContext(params),
|
||||
});
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// HEALTH QUERY
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Get RAG system health status
|
||||
*/
|
||||
export function useRAGHealth() {
|
||||
return useQuery<RAGHealthResponse>({
|
||||
queryKey: kbKeys.health(),
|
||||
queryFn: () => knowledgeBaseApi.getHealth(),
|
||||
staleTime: 1000 * 60, // 1 minute
|
||||
refetchInterval: 1000 * 60 * 5, // Check every 5 minutes
|
||||
});
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// INDEX MANAGEMENT MUTATIONS
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Delete/clear an index
|
||||
*/
|
||||
export function useDeleteIndex() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<ClearIndexResponse, Error, KBIndexType>({
|
||||
mutationFn: (indexType) => knowledgeBaseApi.deleteIndex(indexType),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: kbKeys.stats() });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh an index
|
||||
*/
|
||||
export function useRefreshIndex() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<RefreshIndexResponse, Error, RefreshIndexRequest>({
|
||||
mutationFn: (request) => knowledgeBaseApi.refreshIndex(request),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: kbKeys.stats() });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reindex all with detailed reporting
|
||||
*
|
||||
* Returns detailed IndexingReport for both code and documentation,
|
||||
* including success/failure counts and failed file paths.
|
||||
*/
|
||||
export function useReindexAll() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<ReindexResponse, Error, ReindexRequest | undefined>({
|
||||
mutationFn: (request) => knowledgeBaseApi.reindexAll(request),
|
||||
onSuccess: () => {
|
||||
// Invalidate all relevant queries after reindexing
|
||||
queryClient.invalidateQueries({ queryKey: kbKeys.stats() });
|
||||
queryClient.invalidateQueries({ queryKey: kbKeys.staleness() });
|
||||
queryClient.invalidateQueries({ queryKey: kbKeys.health() });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if indexes are stale (source files modified after indexing)
|
||||
*
|
||||
* Use this to show a "Reindex recommended" warning in the UI.
|
||||
*/
|
||||
export function useIndexStaleness() {
|
||||
return useQuery<IndexStalenessResponse>({
|
||||
queryKey: kbKeys.staleness(),
|
||||
queryFn: () => knowledgeBaseApi.checkStaleness(),
|
||||
staleTime: 5 * 60 * 1000, // 5 minutes
|
||||
refetchInterval: 10 * 60 * 1000, // Check every 10 minutes
|
||||
});
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// MENTOR MUTATIONS
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Ask the mentor for help
|
||||
*/
|
||||
export function useAskMentor() {
|
||||
return useMutation<MentorAskResponse, Error, MentorAskRequest>({
|
||||
mutationFn: (request) => knowledgeBaseApi.askMentor(request),
|
||||
});
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// ERROR MUTATIONS
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Search for known error solutions
|
||||
*/
|
||||
export function useSearchErrors() {
|
||||
return useMutation<ErrorSearchResponse, Error, ErrorSearchRequest>({
|
||||
mutationFn: (request) => knowledgeBaseApi.searchErrors(request),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Record an error solution
|
||||
*/
|
||||
export function useRecordError() {
|
||||
return useMutation<ErrorRecordResponse, Error, ErrorRecordRequest>({
|
||||
mutationFn: (request) => knowledgeBaseApi.recordError(request),
|
||||
});
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// DECISION MUTATIONS
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Check if a similar decision was made before
|
||||
*/
|
||||
export function useCheckDecision() {
|
||||
return useMutation<DecisionCheckResponse, Error, DecisionCheckRequest>({
|
||||
mutationFn: (request) => knowledgeBaseApi.checkDecision(request),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Record a decision for future reference
|
||||
*/
|
||||
export function useRecordDecision() {
|
||||
return useMutation<DecisionRecordResponse, Error, DecisionRecordRequest>({
|
||||
mutationFn: (request) => knowledgeBaseApi.recordDecision(request),
|
||||
});
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// STANDARDS MUTATIONS
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Get standards for a domain
|
||||
*/
|
||||
export function useGetStandards() {
|
||||
return useMutation<StandardsGetResponse, Error, StandardsGetRequest>({
|
||||
mutationFn: (request) => knowledgeBaseApi.getStandards(request),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate an action against standards
|
||||
*/
|
||||
export function useValidateAction() {
|
||||
return useMutation<ValidateActionResponse, Error, ValidateActionRequest>({
|
||||
mutationFn: (request) => knowledgeBaseApi.validateAction(request),
|
||||
});
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// CODE REVIEW MUTATIONS
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Review code against standards
|
||||
*/
|
||||
export function useReviewCode() {
|
||||
return useMutation<CodeReviewResponse, Error, CodeReviewRequest>({
|
||||
mutationFn: (request) => knowledgeBaseApi.reviewCode(request),
|
||||
});
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// LEARNING HOOKS
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Record a learning
|
||||
*/
|
||||
export function useRecordLearning() {
|
||||
return useMutation<LearningRecordResponse, Error, LearningRecordRequest>({
|
||||
mutationFn: (request) => knowledgeBaseApi.recordLearning(request),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Search learnings (as query for persistent results)
|
||||
*/
|
||||
export function useSearchLearnings(request: LearningSearchRequest, enabled = true) {
|
||||
return useQuery<KBSearchResponse>({
|
||||
queryKey: kbKeys.learnings(request.query, JSON.stringify({ category: request.category, team: request.team })),
|
||||
queryFn: () => knowledgeBaseApi.searchLearnings(request),
|
||||
enabled: enabled && request.query.length >= 3,
|
||||
staleTime: 1000 * 60 * 2, // 2 minutes
|
||||
});
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// PROACTIVE CONTEXT HOOKS
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Get proactive context for a task
|
||||
*/
|
||||
export function useProactiveContext(taskId: string, enabled = true) {
|
||||
return useQuery<ProactiveContextResponse>({
|
||||
queryKey: kbKeys.proactiveContext(taskId),
|
||||
queryFn: () => knowledgeBaseApi.getProactiveContext({ task_id: taskId }),
|
||||
enabled: enabled && !!taskId,
|
||||
staleTime: 1000 * 60 * 5, // 5 minutes
|
||||
});
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// TOKEN ESTIMATION
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Estimate token count
|
||||
*/
|
||||
export function useEstimateTokens() {
|
||||
return useMutation<TokenEstimateResponse, Error, TokenEstimateRequest>({
|
||||
mutationFn: (request) => knowledgeBaseApi.estimateTokens(request),
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
"use client";
|
||||
|
||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { notificationsApi, type NotificationFilters } from "@/lib/api/notifications";
|
||||
|
||||
export const notificationKeys = {
|
||||
all: ["notifications"] as const,
|
||||
list: (filters?: NotificationFilters) => [...notificationKeys.all, "list", filters] as const,
|
||||
detail: (id: string) => [...notificationKeys.all, "detail", id] as const,
|
||||
};
|
||||
|
||||
export function useNotifications(filters?: NotificationFilters) {
|
||||
return useQuery({
|
||||
queryKey: notificationKeys.list(filters),
|
||||
queryFn: () => notificationsApi.list(filters),
|
||||
refetchInterval: 30000, // Refetch every 30 seconds
|
||||
});
|
||||
}
|
||||
|
||||
export function useNotification(notificationId: string | null) {
|
||||
return useQuery({
|
||||
queryKey: notificationKeys.detail(notificationId || ""),
|
||||
queryFn: () => notificationsApi.get(notificationId!),
|
||||
enabled: !!notificationId,
|
||||
});
|
||||
}
|
||||
|
||||
export function useMarkNotificationRead() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (notificationId: string) => notificationsApi.markRead(notificationId),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: notificationKeys.all });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useAcknowledgeNotification() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (notificationId: string) => notificationsApi.acknowledge(notificationId),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: notificationKeys.all });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useMarkAllNotificationsRead() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: () => notificationsApi.markAllRead(),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: notificationKeys.all });
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { projectsApi, type ProjectFilters } from "@/lib/api/projects";
|
||||
import type { ProjectCreate, ProjectUpdate } from "@/types";
|
||||
|
||||
// Query keys
|
||||
export const projectKeys = {
|
||||
all: ["projects"] as const,
|
||||
lists: () => [...projectKeys.all, "list"] as const,
|
||||
list: (filters?: ProjectFilters) => [...projectKeys.lists(), filters] as const,
|
||||
details: () => [...projectKeys.all, "detail"] as const,
|
||||
detail: (id: string) => [...projectKeys.details(), id] as const,
|
||||
};
|
||||
|
||||
// Hooks
|
||||
export function useProjects(filters?: ProjectFilters) {
|
||||
return useQuery({
|
||||
queryKey: projectKeys.list(filters),
|
||||
queryFn: () => projectsApi.list(filters),
|
||||
staleTime: 60000, // 1 minute
|
||||
});
|
||||
}
|
||||
|
||||
export function useProject(projectId: string) {
|
||||
return useQuery({
|
||||
queryKey: projectKeys.detail(projectId),
|
||||
queryFn: () => projectsApi.get(projectId),
|
||||
enabled: !!projectId,
|
||||
});
|
||||
}
|
||||
|
||||
export function useCreateProject() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (project: ProjectCreate) => projectsApi.create(project),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: projectKeys.lists() });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useUpdateProject() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ({ projectId, updates }: { projectId: string; updates: ProjectUpdate }) =>
|
||||
projectsApi.update(projectId, updates),
|
||||
onSuccess: (project) => {
|
||||
queryClient.invalidateQueries({ queryKey: projectKeys.lists() });
|
||||
queryClient.setQueryData(projectKeys.detail(project.id), project);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useSetWorkspace() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ({ projectId, localPath }: { projectId: string; localPath: string }) =>
|
||||
projectsApi.setWorkspace(projectId, localPath),
|
||||
onSuccess: (project) => {
|
||||
queryClient.invalidateQueries({ queryKey: projectKeys.lists() });
|
||||
queryClient.setQueryData(projectKeys.detail(project.id), project);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useDeactivateProject() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (projectId: string) => projectsApi.deactivate(projectId),
|
||||
onSuccess: (project) => {
|
||||
queryClient.invalidateQueries({ queryKey: projectKeys.lists() });
|
||||
queryClient.setQueryData(projectKeys.detail(project.id), project);
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
/**
|
||||
* Scroll Restoration Hook
|
||||
*
|
||||
* Saves and restores scroll position when navigating between pages.
|
||||
*/
|
||||
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef } from "react";
|
||||
import { usePathname, useSearchParams } from "next/navigation";
|
||||
import { useUIStore } from "@/lib/stores/ui-store";
|
||||
|
||||
export function useScrollRestoration(scrollContainerRef?: React.RefObject<HTMLElement>) {
|
||||
const pathname = usePathname();
|
||||
const searchParams = useSearchParams();
|
||||
const { setScrollPosition, getScrollPosition } = useUIStore();
|
||||
|
||||
// Create a unique key for current route including search params
|
||||
const routeKey = `${pathname}?${searchParams.toString()}`;
|
||||
const hasRestored = useRef(false);
|
||||
|
||||
// Save scroll position on scroll
|
||||
useEffect(() => {
|
||||
const container = scrollContainerRef?.current ?? window;
|
||||
const isWindow = container === window;
|
||||
|
||||
const handleScroll = () => {
|
||||
const position = isWindow
|
||||
? { x: window.scrollX, y: window.scrollY }
|
||||
: { x: (container as HTMLElement).scrollLeft, y: (container as HTMLElement).scrollTop };
|
||||
|
||||
setScrollPosition(routeKey, position);
|
||||
};
|
||||
|
||||
// Debounce scroll handler
|
||||
let timeout: NodeJS.Timeout;
|
||||
const debouncedScroll = () => {
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(handleScroll, 100);
|
||||
};
|
||||
|
||||
container.addEventListener("scroll", debouncedScroll, { passive: true });
|
||||
|
||||
return () => {
|
||||
clearTimeout(timeout);
|
||||
container.removeEventListener("scroll", debouncedScroll);
|
||||
};
|
||||
}, [routeKey, scrollContainerRef, setScrollPosition]);
|
||||
|
||||
// Restore scroll position on mount
|
||||
useEffect(() => {
|
||||
if (hasRestored.current) return;
|
||||
|
||||
const savedPosition = getScrollPosition(routeKey);
|
||||
if (savedPosition) {
|
||||
const container = scrollContainerRef?.current ?? window;
|
||||
const isWindow = container === window;
|
||||
|
||||
// Delay restoration to ensure content is rendered
|
||||
requestAnimationFrame(() => {
|
||||
if (isWindow) {
|
||||
window.scrollTo(savedPosition.x, savedPosition.y);
|
||||
} else {
|
||||
(container as HTMLElement).scrollLeft = savedPosition.x;
|
||||
(container as HTMLElement).scrollTop = savedPosition.y;
|
||||
}
|
||||
hasRestored.current = true;
|
||||
});
|
||||
}
|
||||
}, [routeKey, scrollContainerRef, getScrollPosition]);
|
||||
|
||||
// Reset restoration flag when route changes
|
||||
useEffect(() => {
|
||||
hasRestored.current = false;
|
||||
}, [routeKey]);
|
||||
}
|
||||
@@ -0,0 +1,305 @@
|
||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { tasksApi, type TaskFilters } from "@/lib/api/tasks";
|
||||
import type {
|
||||
Task,
|
||||
TaskCreate,
|
||||
ProgressRequest,
|
||||
CheckpointRequest,
|
||||
CommitRequest,
|
||||
SoftBlockRequest,
|
||||
EscalateRequest,
|
||||
} from "@/types";
|
||||
|
||||
// Type for task updates - allows any Task field to be updated
|
||||
export type TaskUpdate = Partial<Task>;
|
||||
|
||||
// Query keys
|
||||
export const taskKeys = {
|
||||
all: ["tasks"] as const,
|
||||
lists: () => [...taskKeys.all, "list"] as const,
|
||||
list: (filters?: TaskFilters) => [...taskKeys.lists(), filters] as const,
|
||||
details: () => [...taskKeys.all, "detail"] as const,
|
||||
detail: (id: string) => [...taskKeys.details(), id] as const,
|
||||
subtasks: (parentId: string) => [...taskKeys.all, "subtasks", parentId] as const,
|
||||
stats: () => [...taskKeys.all, "stats"] as const,
|
||||
statsByTeam: () => [...taskKeys.all, "stats-by-team"] as const,
|
||||
};
|
||||
|
||||
// Hooks
|
||||
export function useTasks(filters?: TaskFilters) {
|
||||
return useQuery({
|
||||
queryKey: taskKeys.list(filters),
|
||||
queryFn: () => tasksApi.list(filters),
|
||||
staleTime: 30000, // 30 seconds
|
||||
});
|
||||
}
|
||||
|
||||
export function useTask(taskId: string) {
|
||||
return useQuery({
|
||||
queryKey: taskKeys.detail(taskId),
|
||||
queryFn: () => tasksApi.get(taskId),
|
||||
enabled: !!taskId,
|
||||
});
|
||||
}
|
||||
|
||||
export function useSubtasks(parentTaskId: string) {
|
||||
const { data: allTasks = [] } = useTasks();
|
||||
|
||||
return useQuery({
|
||||
queryKey: taskKeys.subtasks(parentTaskId),
|
||||
queryFn: async (): Promise<Task[]> => {
|
||||
// Filter tasks where parent_task_id matches
|
||||
return allTasks.filter((task) => task.parent_task_id === parentTaskId);
|
||||
},
|
||||
enabled: !!parentTaskId && allTasks.length > 0,
|
||||
});
|
||||
}
|
||||
|
||||
export function useTaskStats() {
|
||||
return useQuery({
|
||||
queryKey: taskKeys.stats(),
|
||||
queryFn: () => tasksApi.getStats(),
|
||||
staleTime: 60000, // 1 minute
|
||||
});
|
||||
}
|
||||
|
||||
export function useTaskStatsByTeam() {
|
||||
return useQuery({
|
||||
queryKey: taskKeys.statsByTeam(),
|
||||
queryFn: () => tasksApi.getStatsByTeam(),
|
||||
staleTime: 60000, // 1 minute
|
||||
});
|
||||
}
|
||||
|
||||
export function useCreateTask() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (task: TaskCreate) => tasksApi.create(task),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: taskKeys.lists() });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useUpdateTask() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ({ taskId, updates }: { taskId: string; updates: TaskUpdate }) =>
|
||||
tasksApi.update(taskId, updates),
|
||||
onSuccess: (task) => {
|
||||
queryClient.invalidateQueries({ queryKey: taskKeys.lists() });
|
||||
queryClient.setQueryData(taskKeys.detail(task.id), task);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useDeleteTask() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (taskId: string) => tasksApi.delete(taskId),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: taskKeys.lists() });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Lifecycle action hooks
|
||||
export function useTaskLifecycle() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const invalidateTask = (task: Task) => {
|
||||
queryClient.invalidateQueries({ queryKey: taskKeys.lists() });
|
||||
queryClient.setQueryData(taskKeys.detail(task.id), task);
|
||||
};
|
||||
|
||||
const claim = useMutation({
|
||||
mutationFn: (taskId: string) => tasksApi.claim(taskId),
|
||||
onSuccess: invalidateTask,
|
||||
});
|
||||
|
||||
const start = useMutation({
|
||||
mutationFn: (taskId: string) => tasksApi.start(taskId),
|
||||
onSuccess: invalidateTask,
|
||||
});
|
||||
|
||||
const block = useMutation({
|
||||
mutationFn: ({ taskId, blockerId }: { taskId: string; blockerId?: string }) =>
|
||||
tasksApi.block(taskId, blockerId),
|
||||
onSuccess: invalidateTask,
|
||||
});
|
||||
|
||||
const unblock = useMutation({
|
||||
mutationFn: (taskId: string) => tasksApi.unblock(taskId),
|
||||
onSuccess: invalidateTask,
|
||||
});
|
||||
|
||||
const pause = useMutation({
|
||||
mutationFn: (taskId: string) => tasksApi.pause(taskId),
|
||||
onSuccess: invalidateTask,
|
||||
});
|
||||
|
||||
const resume = useMutation({
|
||||
mutationFn: (taskId: string) => tasksApi.resume(taskId),
|
||||
onSuccess: invalidateTask,
|
||||
});
|
||||
|
||||
const verify = useMutation({
|
||||
mutationFn: (taskId: string) => tasksApi.verify(taskId),
|
||||
onSuccess: invalidateTask,
|
||||
});
|
||||
|
||||
const submitQa = useMutation({
|
||||
mutationFn: ({ taskId, devNotes }: { taskId: string; devNotes?: string }) =>
|
||||
tasksApi.submitQa(taskId, devNotes),
|
||||
onSuccess: invalidateTask,
|
||||
});
|
||||
|
||||
const passQa = useMutation({
|
||||
mutationFn: ({ taskId, qaNotes }: { taskId: string; qaNotes?: string }) =>
|
||||
tasksApi.passQa(taskId, qaNotes),
|
||||
onSuccess: invalidateTask,
|
||||
});
|
||||
|
||||
const failQa = useMutation({
|
||||
mutationFn: ({ taskId, qaNotes }: { taskId: string; qaNotes?: string }) =>
|
||||
tasksApi.failQa(taskId, qaNotes),
|
||||
onSuccess: invalidateTask,
|
||||
});
|
||||
|
||||
const complete = useMutation({
|
||||
mutationFn: (taskId: string) => tasksApi.complete(taskId),
|
||||
onSuccess: invalidateTask,
|
||||
});
|
||||
|
||||
const cancel = useMutation({
|
||||
mutationFn: (taskId: string) => tasksApi.cancel(taskId),
|
||||
onSuccess: invalidateTask,
|
||||
});
|
||||
|
||||
const reopen = useMutation({
|
||||
mutationFn: (taskId: string) => tasksApi.reopen(taskId),
|
||||
onSuccess: invalidateTask,
|
||||
});
|
||||
|
||||
const activate = useMutation({
|
||||
mutationFn: (taskId: string) => tasksApi.activate(taskId),
|
||||
onSuccess: invalidateTask,
|
||||
});
|
||||
|
||||
const docsComplete = useMutation({
|
||||
mutationFn: (taskId: string) => tasksApi.docsComplete(taskId),
|
||||
onSuccess: invalidateTask,
|
||||
});
|
||||
|
||||
const submitPmReview = useMutation({
|
||||
mutationFn: (taskId: string) => tasksApi.submitPmReview(taskId),
|
||||
onSuccess: invalidateTask,
|
||||
});
|
||||
|
||||
// Progress tracking
|
||||
const addProgress = useMutation({
|
||||
mutationFn: ({ taskId, request }: { taskId: string; request: ProgressRequest }) =>
|
||||
tasksApi.addProgress(taskId, request),
|
||||
onSuccess: invalidateTask,
|
||||
});
|
||||
|
||||
const addCheckpoint = useMutation({
|
||||
mutationFn: ({ taskId, request }: { taskId: string; request: CheckpointRequest }) =>
|
||||
tasksApi.addCheckpoint(taskId, request),
|
||||
onSuccess: invalidateTask,
|
||||
});
|
||||
|
||||
const addCommit = useMutation({
|
||||
mutationFn: ({ taskId, request }: { taskId: string; request: CommitRequest }) =>
|
||||
tasksApi.addCommit(taskId, request),
|
||||
onSuccess: invalidateTask,
|
||||
});
|
||||
|
||||
// Soft block and escalation
|
||||
const softBlock = useMutation({
|
||||
mutationFn: ({ taskId, request }: { taskId: string; request: SoftBlockRequest }) =>
|
||||
tasksApi.softBlock(taskId, request),
|
||||
onSuccess: invalidateTask,
|
||||
});
|
||||
|
||||
const escalate = useMutation({
|
||||
mutationFn: ({ taskId, request }: { taskId: string; request: EscalateRequest }) =>
|
||||
tasksApi.escalate(taskId, request),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: taskKeys.lists() });
|
||||
},
|
||||
});
|
||||
|
||||
// CEO Approval workflow
|
||||
const ceoApprove = useMutation({
|
||||
mutationFn: ({ taskId, notes }: { taskId: string; notes?: string }) =>
|
||||
tasksApi.ceoApprove(taskId, notes),
|
||||
onSuccess: invalidateTask,
|
||||
});
|
||||
|
||||
const ceoReject = useMutation({
|
||||
mutationFn: ({ taskId, notes }: { taskId: string; notes: string }) =>
|
||||
tasksApi.ceoReject(taskId, notes),
|
||||
onSuccess: invalidateTask,
|
||||
});
|
||||
|
||||
const escalateToCeo = useMutation({
|
||||
mutationFn: ({ taskId, reason }: { taskId: string; reason: string }) =>
|
||||
tasksApi.escalateToCeo(taskId, reason),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: taskKeys.lists() });
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
// Lifecycle
|
||||
claim,
|
||||
start,
|
||||
block,
|
||||
unblock,
|
||||
pause,
|
||||
resume,
|
||||
verify,
|
||||
submitQa,
|
||||
passQa,
|
||||
failQa,
|
||||
complete,
|
||||
cancel,
|
||||
reopen,
|
||||
activate,
|
||||
docsComplete,
|
||||
submitPmReview,
|
||||
// Progress tracking
|
||||
addProgress,
|
||||
addCheckpoint,
|
||||
addCommit,
|
||||
// Soft block and escalation
|
||||
softBlock,
|
||||
escalate,
|
||||
// CEO Approval
|
||||
ceoApprove,
|
||||
ceoReject,
|
||||
escalateToCeo,
|
||||
};
|
||||
}
|
||||
|
||||
// Query hook for tasks awaiting CEO approval
|
||||
export function useTasksAwaitingCeoApproval() {
|
||||
return useQuery({
|
||||
queryKey: [...taskKeys.all, "awaiting-ceo"] as const,
|
||||
queryFn: () => tasksApi.getAwaitingCeoApproval(),
|
||||
staleTime: 30000,
|
||||
});
|
||||
}
|
||||
|
||||
// Query hook for tasks awaiting PM review
|
||||
export function useTasksAwaitingPmReview() {
|
||||
return useQuery({
|
||||
queryKey: [...taskKeys.all, "awaiting-pm-review"] as const,
|
||||
queryFn: () => tasksApi.getAwaitingPmReview(),
|
||||
staleTime: 30000,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,234 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef, useState, useCallback } from "react";
|
||||
import {
|
||||
WebSocketConnection,
|
||||
getWebSocketUrl
|
||||
} from "@/lib/websocket/connection";
|
||||
import { CEO_AGENT_ID, STREAM_MAX_MESSAGES } from "@/lib/constants";
|
||||
|
||||
// Re-export ConnectionState type
|
||||
export type { ConnectionState } from "@/lib/websocket/connection";
|
||||
import type { ConnectionState } from "@/lib/websocket/connection";
|
||||
|
||||
// =============================================================================
|
||||
// Types
|
||||
// =============================================================================
|
||||
|
||||
export interface AgentStreamMessage {
|
||||
type: "connected" | "agent.stream";
|
||||
agent_id: string;
|
||||
chunk?: string;
|
||||
watcher_count?: number;
|
||||
timestamp?: string;
|
||||
}
|
||||
|
||||
export interface ChannelMessage {
|
||||
type: "connected" | "message.new" | "session.closed";
|
||||
channel_id?: string;
|
||||
message_id?: string;
|
||||
agent_id?: string;
|
||||
content?: string;
|
||||
message_type?: string;
|
||||
subscriber_count?: number;
|
||||
timestamp?: string;
|
||||
}
|
||||
|
||||
export interface NotificationMessage {
|
||||
type: "connected" | "notification";
|
||||
agent_id?: string;
|
||||
notification_id?: string;
|
||||
notification_type?: string;
|
||||
subject?: string;
|
||||
priority?: string;
|
||||
timestamp?: string;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Generic WebSocket Hook
|
||||
// =============================================================================
|
||||
|
||||
export function useWebSocket<T>(
|
||||
endpoint: string,
|
||||
queryParams?: Record<string, string>,
|
||||
enabled: boolean = true
|
||||
) {
|
||||
const [state, setState] = useState<ConnectionState>("disconnected");
|
||||
const [lastMessage, setLastMessage] = useState<T | null>(null);
|
||||
const [messages, setMessages] = useState<T[]>([]);
|
||||
const connectionRef = useRef<WebSocketConnection | null>(null);
|
||||
|
||||
// Memoize queryParams string to prevent unnecessary reconnects
|
||||
const queryString = queryParams ? new URLSearchParams(queryParams).toString() : "";
|
||||
|
||||
useEffect(() => {
|
||||
// Don't connect if disabled or no endpoint
|
||||
if (!enabled || !endpoint) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Build URL
|
||||
const baseUrl = getWebSocketUrl();
|
||||
const url = baseUrl + endpoint + (queryString ? "?" + queryString : "");
|
||||
|
||||
// Create connection
|
||||
const connection = new WebSocketConnection({
|
||||
url,
|
||||
onMessage: (data) => {
|
||||
const message = data as T;
|
||||
setLastMessage(message);
|
||||
setMessages((prev) => [...prev.slice(-(STREAM_MAX_MESSAGES - 1)), message]);
|
||||
},
|
||||
onStateChange: setState,
|
||||
});
|
||||
|
||||
connectionRef.current = connection;
|
||||
connection.connect();
|
||||
|
||||
// Cleanup on unmount or when dependencies change
|
||||
return () => {
|
||||
connection.disconnect();
|
||||
connectionRef.current = null;
|
||||
};
|
||||
}, [enabled, endpoint, queryString]); // Stable dependencies
|
||||
|
||||
const disconnect = useCallback(() => {
|
||||
connectionRef.current?.disconnect();
|
||||
connectionRef.current = null;
|
||||
setState("disconnected");
|
||||
}, []);
|
||||
|
||||
const clearMessages = useCallback(() => {
|
||||
setMessages([]);
|
||||
setLastMessage(null);
|
||||
}, []);
|
||||
|
||||
return {
|
||||
state,
|
||||
lastMessage,
|
||||
messages,
|
||||
disconnect,
|
||||
clearMessages,
|
||||
isConnected: state === "connected",
|
||||
isConnecting: state === "connecting" || state === "reconnecting",
|
||||
};
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Specialized Hooks
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Subscribe to an agent's output stream
|
||||
*/
|
||||
export function useAgentStream(agentId: string | null) {
|
||||
const { state, lastMessage, messages, clearMessages, isConnected, isConnecting } =
|
||||
useWebSocket<AgentStreamMessage>(
|
||||
agentId ? "/agents/" + agentId : "",
|
||||
{ viewer_id: CEO_AGENT_ID },
|
||||
!!agentId
|
||||
);
|
||||
|
||||
// Extract stream chunks
|
||||
const streamChunks = messages
|
||||
.filter((m) => m.type === "agent.stream" && m.chunk)
|
||||
.map((m) => m.chunk as string);
|
||||
|
||||
// Combine chunks into full output
|
||||
const streamOutput = streamChunks.join("");
|
||||
|
||||
return {
|
||||
state,
|
||||
lastMessage,
|
||||
messages,
|
||||
streamChunks,
|
||||
streamOutput,
|
||||
clearMessages,
|
||||
isConnected,
|
||||
isConnecting,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to a channel's message stream
|
||||
*/
|
||||
export function useChannelStream(channelId: string | null) {
|
||||
const { state, lastMessage, messages, clearMessages, isConnected, isConnecting } =
|
||||
useWebSocket<ChannelMessage>(
|
||||
channelId ? "/channels/" + channelId : "",
|
||||
{ agent_id: CEO_AGENT_ID },
|
||||
!!channelId
|
||||
);
|
||||
|
||||
// Filter to only actual messages
|
||||
const channelMessages = messages.filter((m) => m.type === "message.new");
|
||||
|
||||
return {
|
||||
state,
|
||||
lastMessage,
|
||||
channelMessages,
|
||||
allMessages: messages,
|
||||
clearMessages,
|
||||
isConnected,
|
||||
isConnecting,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to notifications for the CEO
|
||||
*/
|
||||
export function useNotificationStream() {
|
||||
const { state, lastMessage, messages, clearMessages, isConnected, isConnecting } =
|
||||
useWebSocket<NotificationMessage>(
|
||||
"/notifications/" + CEO_AGENT_ID,
|
||||
undefined,
|
||||
true
|
||||
);
|
||||
|
||||
// Filter to only notification events
|
||||
const notifications = messages.filter((m) => m.type === "notification");
|
||||
|
||||
return {
|
||||
state,
|
||||
lastMessage,
|
||||
notifications,
|
||||
allMessages: messages,
|
||||
clearMessages,
|
||||
isConnected,
|
||||
isConnecting,
|
||||
};
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Connection Status Hook (for UI indicators)
|
||||
// =============================================================================
|
||||
|
||||
export function useConnectionStatus() {
|
||||
const [connections, setConnections] = useState<Record<string, ConnectionState>>({});
|
||||
|
||||
const updateConnection = useCallback((id: string, state: ConnectionState) => {
|
||||
setConnections((prev) => ({ ...prev, [id]: state }));
|
||||
}, []);
|
||||
|
||||
const removeConnection = useCallback((id: string) => {
|
||||
setConnections((prev) => {
|
||||
const next = { ...prev };
|
||||
delete next[id];
|
||||
return next;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const hasActiveConnections = Object.values(connections).some(
|
||||
(s) => s === "connected" || s === "connecting" || s === "reconnecting"
|
||||
);
|
||||
|
||||
const allConnected = Object.values(connections).every((s) => s === "connected");
|
||||
|
||||
return {
|
||||
connections,
|
||||
updateConnection,
|
||||
removeConnection,
|
||||
hasActiveConnections,
|
||||
allConnected,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { workSessionsApi, type WorkSessionFilters } from "@/lib/api/work-sessions";
|
||||
import type { WorkSession, WorkSessionCreate } from "@/types";
|
||||
|
||||
// Query keys
|
||||
export const workSessionKeys = {
|
||||
all: ["work-sessions"] as const,
|
||||
lists: () => [...workSessionKeys.all, "list"] as const,
|
||||
list: (filters?: WorkSessionFilters) => [...workSessionKeys.lists(), filters] as const,
|
||||
details: () => [...workSessionKeys.all, "detail"] as const,
|
||||
detail: (id: string) => [...workSessionKeys.details(), id] as const,
|
||||
forTask: (taskId: string) => [...workSessionKeys.all, "task", taskId] as const,
|
||||
};
|
||||
|
||||
// Hooks
|
||||
export function useWorkSessions(filters?: WorkSessionFilters) {
|
||||
return useQuery({
|
||||
queryKey: workSessionKeys.list(filters),
|
||||
queryFn: () => workSessionsApi.list(filters),
|
||||
staleTime: 30000, // 30 seconds
|
||||
});
|
||||
}
|
||||
|
||||
export function useWorkSession(sessionId: string) {
|
||||
return useQuery({
|
||||
queryKey: workSessionKeys.detail(sessionId),
|
||||
queryFn: () => workSessionsApi.get(sessionId),
|
||||
enabled: !!sessionId,
|
||||
});
|
||||
}
|
||||
|
||||
export function useWorkSessionForTask(taskId: string) {
|
||||
return useQuery({
|
||||
queryKey: workSessionKeys.forTask(taskId),
|
||||
queryFn: () => workSessionsApi.getForTask(taskId),
|
||||
enabled: !!taskId,
|
||||
staleTime: 10000, // 10 seconds
|
||||
});
|
||||
}
|
||||
|
||||
export function useCreateWorkSession() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (session: WorkSessionCreate) => workSessionsApi.create(session),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: workSessionKeys.lists() });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useWorkSessionActions() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const invalidateSession = (session: WorkSession) => {
|
||||
queryClient.invalidateQueries({ queryKey: workSessionKeys.lists() });
|
||||
queryClient.setQueryData(workSessionKeys.detail(session.id), session);
|
||||
if (session.task_id) {
|
||||
queryClient.invalidateQueries({ queryKey: workSessionKeys.forTask(session.task_id) });
|
||||
}
|
||||
};
|
||||
|
||||
const addCommit = useMutation({
|
||||
mutationFn: ({ sessionId, commitSha }: { sessionId: string; commitSha: string }) =>
|
||||
workSessionsApi.addCommit(sessionId, commitSha),
|
||||
onSuccess: invalidateSession,
|
||||
});
|
||||
|
||||
const addFiles = useMutation({
|
||||
mutationFn: ({ sessionId, filePaths }: { sessionId: string; filePaths: string[] }) =>
|
||||
workSessionsApi.addFiles(sessionId, filePaths),
|
||||
onSuccess: invalidateSession,
|
||||
});
|
||||
|
||||
const createPR = useMutation({
|
||||
mutationFn: ({
|
||||
sessionId,
|
||||
prNumber,
|
||||
prUrl,
|
||||
}: {
|
||||
sessionId: string;
|
||||
prNumber: number;
|
||||
prUrl: string;
|
||||
}) => workSessionsApi.createPR(sessionId, prNumber, prUrl),
|
||||
onSuccess: invalidateSession,
|
||||
});
|
||||
|
||||
const updatePRStatus = useMutation({
|
||||
mutationFn: ({ sessionId, prStatus }: { sessionId: string; prStatus: string }) =>
|
||||
workSessionsApi.updatePRStatus(sessionId, prStatus),
|
||||
onSuccess: invalidateSession,
|
||||
});
|
||||
|
||||
const mergePR = useMutation({
|
||||
mutationFn: ({ sessionId, mergedBy }: { sessionId: string; mergedBy: string }) =>
|
||||
workSessionsApi.mergePR(sessionId, mergedBy),
|
||||
onSuccess: invalidateSession,
|
||||
});
|
||||
|
||||
const complete = useMutation({
|
||||
mutationFn: (sessionId: string) => workSessionsApi.complete(sessionId),
|
||||
onSuccess: invalidateSession,
|
||||
});
|
||||
|
||||
const abandon = useMutation({
|
||||
mutationFn: ({ sessionId, reason }: { sessionId: string; reason?: string }) =>
|
||||
workSessionsApi.abandon(sessionId, reason),
|
||||
onSuccess: invalidateSession,
|
||||
});
|
||||
|
||||
return {
|
||||
addCommit,
|
||||
addFiles,
|
||||
createPR,
|
||||
updatePRStatus,
|
||||
mergePR,
|
||||
complete,
|
||||
abandon,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user