Files
fusion/packages/dashboard/app/hooks/useInsights.ts
gsxdsm fc30dc48fd refactor: low-regret cleanup across core, engine, dashboard
- core: extract ai-engine-loader.ts to share @fusion/engine dynamic-import
  boilerplate between ai-summarize and memory-compaction (incl. AgentMessage
  type); collapse getInbox/getOutbox, listInsights/countInsights,
  listRuns/countRuns, and three hasProjectDb* variants behind shared helpers.
- core: drop unused pluginLoaderLog export; tighten two `any` casts
  (db.walCheckpoint row, plugin-loader error.code).
- engine: extract resolveRoleFallback helper from buildSessionSkillContext/Sync;
  remove 22 stale `eslint-disable no-explicit-any` directives across
  project-engine, self-healing, triage, worktree-pool.
- dashboard: apply ESLint autofix (let→const, empty `interface extends`→type).

All three packages: typecheck clean, full test suites pass (14,414 tests),
builds clean. Net lint: -31 warnings. No public behavior changes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 20:48:13 -07:00

324 lines
8.3 KiB
TypeScript

/**
* useInsights hook - Dashboard hook for managing project insights
*
* Provides a unified interface for:
* - Fetching insights grouped by category
* - Triggering manual insight generation runs
* - Dismissing individual insights
* - Converting insights to tasks
*/
import { useState, useCallback, useMemo } from "react";
import type { Insight, InsightCategory, InsightStatus, InsightRun } from "@fusion/core";
import {
fetchInsights,
dismissInsight,
triggerInsightRun,
fetchInsightRuns,
getInsightCreateTaskData,
} from "../api";
// Canonical insight categories (in display order)
export const INSIGHT_CATEGORIES: InsightCategory[] = [
"architecture",
"quality",
"workflow",
"performance",
"reliability",
"security",
"ux",
"testability",
"documentation",
"dependency",
"features",
"competitive_analysis",
"research",
"trends",
"other",
];
// Human-readable labels for categories
export const CATEGORY_LABELS: Record<InsightCategory, string> = {
quality: "Quality",
performance: "Performance",
architecture: "Architecture",
security: "Security",
reliability: "Reliability",
ux: "User Experience",
testability: "Testability",
documentation: "Documentation",
dependency: "Dependencies",
workflow: "Workflow",
other: "Other",
// Extended categories
features: "Features",
competitive_analysis: "Competitive Analysis",
research: "Research",
trends: "Trends",
};
// Status labels
export const STATUS_LABELS: Record<InsightStatus, string> = {
generated: "Generated",
confirmed: "Confirmed",
stale: "Stale",
dismissed: "Dismissed",
};
// Section data structure
export interface InsightSection {
category: InsightCategory;
label: string;
items: Insight[];
isLoading: boolean;
error: string | null;
}
// Action state for tracking in-flight operations
export interface InsightActionState {
running: boolean;
error: string | null;
}
// Main hook result
export interface UseInsightsResult {
// Section data
sections: InsightSection[];
// Loading/error state
loading: boolean;
error: string | null;
// Run state
latestRun: InsightRun | null;
isRunInFlight: boolean;
runError: string | null;
// Actions
refresh: () => Promise<void>;
runInsights: () => Promise<void>;
dismiss: (id: string) => Promise<void>;
createTask: (id: string) => Promise<{ title: string; description: string } | null>;
// Per-insight action states
dismissStates: Map<string, InsightActionState>;
createTaskStates: Map<string, InsightActionState>;
// Dismissed/filtered counts
totalCount: number;
dismissedCount: number;
}
/**
* Hook for managing project insights.
*
* @param projectId - Optional project ID for scoped insights
* @returns Insights data and action handlers
*/
export function useInsights(projectId?: string): UseInsightsResult {
// Section items (keyed by category)
const [sections, setSections] = useState<InsightSection[]>(() =>
INSIGHT_CATEGORIES.map((category) => ({
category,
label: CATEGORY_LABELS[category] ?? category,
items: [],
isLoading: false,
error: null,
})),
);
// Loading state
const [loading, setLoading] = useState(true);
// Top-level error
const [error, setError] = useState<string | null>(null);
// Run state
const [latestRun, setLatestRun] = useState<InsightRun | null>(null);
const [isRunInFlight, setIsRunInFlight] = useState(false);
const [runError, setRunError] = useState<string | null>(null);
// Per-insight action states
const [dismissStates, setDismissStates] = useState<Map<string, InsightActionState>>(new Map());
const [createTaskStates, setCreateTaskStates] = useState<Map<string, InsightActionState>>(new Map());
// Refresh function
const refresh = useCallback(async () => {
setLoading(true);
setError(null);
try {
// Fetch all non-dismissed insights
const response = await fetchInsights(
{ status: undefined, limit: 1000 }, // Fetch all non-dismissed
projectId,
);
// Group by category
const grouped = new Map<InsightCategory, Insight[]>();
// Initialize all categories
for (const category of INSIGHT_CATEGORIES) {
grouped.set(category, []);
}
// Group non-dismissed insights
for (const insight of response.insights) {
if (insight.status !== "dismissed") {
const existing = grouped.get(insight.category) ?? [];
grouped.set(insight.category, [...existing, insight]);
}
}
// Update sections
setSections(
INSIGHT_CATEGORIES.map((category) => ({
category,
label: CATEGORY_LABELS[category] ?? category,
items: grouped.get(category) ?? [],
isLoading: false,
error: null,
})),
);
// Fetch latest run
const runsResponse = await fetchInsightRuns(projectId);
if (runsResponse.runs.length > 0) {
setLatestRun(runsResponse.runs[0]);
}
} catch (err) {
const message = err instanceof Error ? err.message : "Failed to fetch insights";
setError(message);
} finally {
setLoading(false);
}
}, [projectId]);
// Run insights generation
const runInsights = useCallback(async () => {
setIsRunInFlight(true);
setRunError(null);
try {
const run = await triggerInsightRun("manual", undefined, projectId);
setLatestRun(run);
if (run.status === "completed") {
await refresh();
} else if (run.status === "failed" && run.error) {
setRunError(run.error);
}
} catch (err) {
const message = err instanceof Error ? err.message : "Failed to generate insights";
setRunError(message);
throw err;
} finally {
setIsRunInFlight(false);
}
}, [projectId, refresh]);
// Dismiss an insight
const dismiss = useCallback(
async (id: string) => {
setDismissStates((prev) => {
const next = new Map(prev);
next.set(id, { running: true, error: null });
return next;
});
try {
await dismissInsight(id, projectId);
// Update local state
setSections((prev) =>
prev.map((section) => ({
...section,
items: section.items.filter((item) => item.id !== id),
})),
);
setDismissStates((prev) => {
const next = new Map(prev);
next.set(id, { running: false, error: null });
return next;
});
} catch (err) {
const message = err instanceof Error ? err.message : "Failed to dismiss insight";
setDismissStates((prev) => {
const next = new Map(prev);
next.set(id, { running: false, error: message });
return next;
});
throw err;
}
},
[projectId],
);
// Create task from insight
const createTask = useCallback(
async (id: string): Promise<{ title: string; description: string } | null> => {
setCreateTaskStates((prev) => {
const next = new Map(prev);
next.set(id, { running: true, error: null });
return next;
});
try {
const data = await getInsightCreateTaskData(id, projectId);
setCreateTaskStates((prev) => {
const next = new Map(prev);
next.set(id, { running: false, error: null });
return next;
});
return {
title: data.suggestedTitle,
description: data.suggestedDescription,
};
} catch (err) {
const message = err instanceof Error ? err.message : "Failed to create task";
setCreateTaskStates((prev) => {
const next = new Map(prev);
next.set(id, { running: false, error: message });
return next;
});
throw err;
}
},
[projectId],
);
// Computed counts
const totalCount = useMemo(() => {
return sections.reduce((sum, section) => sum + section.items.length, 0);
}, [sections]);
const dismissedCount = useMemo(() => {
// This would need to be tracked separately if needed
return 0;
}, [sections]);
// Initial load - intentionally runs once on mount
useMemo(() => {
void refresh();
}, []);
return {
sections,
loading,
error,
latestRun,
isRunInFlight,
runError,
refresh,
runInsights,
dismiss,
createTask,
dismissStates,
createTaskStates,
totalCount,
dismissedCount,
};
}