/** * InsightsView - Dashboard component for displaying and managing project insights * * Two-pane layout: categories on the left, insights for the selected category on the right. */ import "./InsightsView.css"; import { useCallback, useEffect, useMemo, useState } from "react"; import { Sparkles, RefreshCw, X, Plus, AlertCircle, CheckCircle, Lightbulb, Building, Users, LineChart, TrendingUp, ExternalLink, Archive, Clock, } from "lucide-react"; import { useInsights, type InsightSection } from "../hooks/useInsights"; import type { InsightCategory } from "@fusion/core"; import type { ToastType } from "../hooks/useToast"; interface InsightsViewProps { projectId?: string; addToast: (message: string, type?: ToastType) => void; onClose?: () => void; onCreateTask?: (payload: { insightId: string; title: string; description: string }) => Promise; } const CATEGORY_ICONS: Record> = { architecture: Building, quality: CheckCircle, workflow: Clock, performance: TrendingUp, reliability: RefreshCw, security: AlertCircle, ux: Users, testability: Archive, documentation: ExternalLink, dependency: Plus, features: Lightbulb, competitive_analysis: Users, research: LineChart, trends: TrendingUp, other: Sparkles, }; export function InsightsView({ projectId, addToast, onClose, onCreateTask }: InsightsViewProps) { const { sections, loading, error, latestRun, isRunInFlight, runError, refresh, runInsights, dismiss, createTask: createTaskFromInsight, dismissStates, createTaskStates, totalCount, } = useInsights(projectId); const [statusMessage, setStatusMessage] = useState(null); const [statusType, setStatusType] = useState<"success" | "error" | "info">("info"); const populatedSections = useMemo( () => sections.filter((section) => section.items.length > 0), [sections], ); const [selectedCategory, setSelectedCategory] = useState(null); // Keep selection valid as data changes; default to first populated section. useEffect(() => { if (populatedSections.length === 0) { if (selectedCategory !== null) setSelectedCategory(null); return; } const stillExists = selectedCategory && populatedSections.some((s) => s.category === selectedCategory); if (!stillExists) { setSelectedCategory(populatedSections[0].category); } }, [populatedSections, selectedCategory]); const activeSection: InsightSection | undefined = useMemo( () => populatedSections.find((s) => s.category === selectedCategory) ?? populatedSections[0], [populatedSections, selectedCategory], ); useEffect(() => { if (statusMessage) { const timer = setTimeout(() => setStatusMessage(null), 5000); return () => clearTimeout(timer); } }, [statusMessage]); const handleRun = useCallback(async () => { try { setStatusMessage("Generating insights..."); setStatusType("info"); await runInsights(); setStatusMessage("Insight generation started"); setStatusType("success"); addToast("Insight generation started", "success"); } catch (err) { const message = err instanceof Error ? err.message : "Failed to start generation"; setStatusMessage(message); setStatusType("error"); addToast(message, "error"); } }, [runInsights, addToast]); const handleDismiss = useCallback( async (id: string, title: string) => { try { setStatusMessage(`Dismissing "${title}"...`); setStatusType("info"); await dismiss(id); setStatusMessage(`Dismissed "${title}"`); setStatusType("success"); addToast(`Insight dismissed: ${title}`, "success"); } catch (err) { const message = err instanceof Error ? err.message : "Failed to dismiss insight"; setStatusMessage(message); setStatusType("error"); addToast(message, "error"); } }, [dismiss, addToast], ); const handleCreateTask = useCallback( async (id: string, title: string) => { try { setStatusMessage(`Creating task from "${title}"...`); setStatusType("info"); if (!onCreateTask) { throw new Error("Task creation is unavailable in this view"); } const taskData = await createTaskFromInsight(id); if (!taskData) { throw new Error("Failed to prepare task payload from insight"); } await onCreateTask({ insightId: id, title: taskData.title, description: taskData.description, }); setStatusMessage(`Task created from "${title}"`); setStatusType("success"); addToast(`Task created: ${taskData.title}`, "success"); } catch (err) { const message = err instanceof Error ? err.message : "Failed to create task"; setStatusMessage(message); setStatusType("error"); addToast(message, "error"); } }, [createTaskFromInsight, onCreateTask, addToast], ); const renderCategoryItem = (section: InsightSection) => { const IconComponent = CATEGORY_ICONS[section.category] ?? Sparkles; const isActive = activeSection?.category === section.category; return (
  • ); }; const renderActiveInsights = () => { if (!activeSection) return null; const IconComponent = CATEGORY_ICONS[activeSection.category] ?? Sparkles; return (

    {activeSection.label}

    {activeSection.items.length}
      {activeSection.items.map((insight) => { const dismissState = dismissStates.get(insight.id); const createState = createTaskStates.get(insight.id); const isDismissInFlight = dismissState?.running ?? false; const isCreateInFlight = createState?.running ?? false; const isAnyActionInFlight = activeSection.items.some( (item) => dismissStates.get(item.id)?.running || createTaskStates.get(item.id)?.running, ); return (
    • {insight.title}

      {insight.content && (

      {insight.content}

      )}
      {insight.status} {insight.createdAt && ( {new Date(insight.createdAt).toLocaleDateString()} )}
    • ); })}
    ); }; return (

    Insights

    {totalCount} total
    {onClose && ( )}
    {statusMessage && (
    {statusType === "success" && } {statusType === "error" && } {statusType === "info" && } {statusMessage}
    )}
    {runError && (
    {runError}
    )} {latestRun && (
    Latest run: {latestRun.status} {latestRun.status === "completed" && ( <> — {latestRun.insightsCreated} created, {latestRun.insightsUpdated} updated )} {latestRun.status === "failed" && latestRun.error && ( <> — {latestRun.error} )}
    )} {loading ? (

    Loading insights...

    ) : error ? (

    {error}

    ) : totalCount === 0 ? (

    No insights yet

    Generate insights to get AI-powered recommendations for your project.

    ) : (
    {renderActiveInsights()}
    )}
    ); }