From 425beb83048d3eed5380e100b404452530474026 Mon Sep 17 00:00:00 2001 From: Asirwad Date: Sat, 1 Nov 2025 16:03:12 +0530 Subject: [PATCH] refactor: enhance analysis and insights pages with improved UI components and functionality - Added Breadcrumbs for better navigation in Analysis, Insights, and History pages. - Implemented auto-redirect to insights upon analysis completion with success notifications. - Enhanced filtering capabilities in Insights with debounced search and priority filters. - Updated Sidebar and Header components for improved layout and user experience. - Introduced new StatsCard component for displaying metrics in a more visually appealing manner. - Refactored various components for better code organization and maintainability. --- frontend/app/analysis/history/page.tsx | 53 +- frontend/app/analysis/page.tsx | 844 ++++++++++---------- frontend/app/insights/[id]/page.tsx | 672 ++++++++-------- frontend/app/insights/page.tsx | 449 +++++++---- frontend/app/settings/page.tsx | 30 +- frontend/components/dashboard/Dashboard.tsx | 721 +++++++++++------ frontend/components/layout/Header.tsx | 86 +- frontend/components/layout/Sidebar.tsx | 175 +++- frontend/components/shared/Breadcrumbs.tsx | 52 ++ frontend/components/shared/index.ts | 12 +- frontend/lib/hooks/useInsights.ts | 19 +- frontend/lib/utils/helpers.ts | 18 + 12 files changed, 1822 insertions(+), 1309 deletions(-) create mode 100644 frontend/components/shared/Breadcrumbs.tsx diff --git a/frontend/app/analysis/history/page.tsx b/frontend/app/analysis/history/page.tsx index b69c2d0..fadf391 100644 --- a/frontend/app/analysis/history/page.tsx +++ b/frontend/app/analysis/history/page.tsx @@ -2,7 +2,7 @@ import React, { useState } from 'react'; import { MainLayout } from '@/components/layout/MainLayout'; -import { Card, CardHeader, Button, LoadingSpinner, Badge } from '@/components/shared'; +import { Card, CardHeader, Button, LoadingSpinner, Badge, Breadcrumbs } from '@/components/shared'; import { useAnalysisHistory } from '@/lib/hooks/useAnalysis'; import { formatDateTime, formatRelativeTime } from '@/lib/utils/formatters'; import { DEFAULT_PAGE_SIZE, PAGE_SIZES } from '@/lib/utils/constants'; @@ -115,29 +115,34 @@ export default function AnalysisHistoryPage() { return (
- {/* Header */} -
-
-

Analysis History

-

- View all past analysis runs -

-
- -
- - + {/* AWS-style Header with Breadcrumbs */} +
+ +
+
+

Analysis History

+

+ View and review all past analysis executions +

+
+
+ + +
diff --git a/frontend/app/analysis/page.tsx b/frontend/app/analysis/page.tsx index b2286ee..39db0bb 100644 --- a/frontend/app/analysis/page.tsx +++ b/frontend/app/analysis/page.tsx @@ -1,32 +1,55 @@ 'use client'; -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; +import { useRouter } from 'next/navigation'; import { MainLayout } from '@/components/layout/MainLayout'; -import { Card, CardHeader, Button, LoadingSpinner, Badge } from '@/components/shared'; +import { Card, CardHeader, Button, LoadingSpinner, Badge, Breadcrumbs } from '@/components/shared'; import { useAnalysisHistory, useAnalysisStatus, triggerAnalysis } from '@/lib/hooks/useAnalysis'; import { useSystemConfig } from '@/lib/hooks/useSystemHealth'; import { METRIC_TYPES, DEFAULT_ANALYSIS_WINDOW, MIN_ANALYSIS_WINDOW, MAX_ANALYSIS_WINDOW } from '@/lib/utils/constants'; import { formatDate, formatRelativeTime } from '@/lib/utils/formatters'; import Link from 'next/link'; +import toast from 'react-hot-toast'; import { PlayIcon, ArrowPathIcon, DocumentTextIcon, ExclamationTriangleIcon, - LightBulbIcon + LightBulbIcon, + CheckCircleIcon, + ClockIcon, + ArrowRightIcon, + InformationCircleIcon, + ChartBarIcon } from '@heroicons/react/24/outline'; +import { motion, AnimatePresence } from 'framer-motion'; export default function AnalysisPage() { + const router = useRouter(); const [windowMinutes, setWindowMinutes] = useState(DEFAULT_ANALYSIS_WINDOW); const [selectedMetrics, setSelectedMetrics] = useState(METRIC_TYPES); const [forceReprocess, setForceReprocess] = useState(false); const [activeRunId, setActiveRunId] = useState(null); const [showResults, setShowResults] = useState(false); + const [isTriggering, setIsTriggering] = useState(false); const { config } = useSystemConfig(); const { runs, isLoading: historyLoading, mutate: refreshHistory } = useAnalysisHistory(3, 0); const { status: activeRun, isLoading: statusLoading } = useAnalysisStatus(activeRunId); - const trigger = triggerAnalysis; + + // Auto-redirect to insights when analysis completes + useEffect(() => { + if (activeRun?.status === 'completed' && activeRun.results?.insights_generated > 0) { + const insightsCount = activeRun.results.insights_generated; + toast.success(`Analysis completed! Generated ${insightsCount} insights.`, { + duration: 5000, + action: { + label: 'View Insights', + onClick: () => router.push('/insights') + } + }); + } + }, [activeRun?.status, activeRun?.results?.insights_generated, router]); const handleMetricToggle = (metric: string) => { setSelectedMetrics(prev => @@ -45,8 +68,14 @@ export default function AnalysisPage() { }; const handleTriggerAnalysis = async () => { + if (selectedMetrics.length === 0) { + toast.error('Please select at least one metric to analyze'); + return; + } + + setIsTriggering(true); try { - const response = await trigger({ + const response = await triggerAnalysis({ window_minutes: windowMinutes, metrics: selectedMetrics.length === METRIC_TYPES.length ? undefined : selectedMetrics, force_reprocess: forceReprocess @@ -54,17 +83,21 @@ export default function AnalysisPage() { if (response.run_id) { setActiveRunId(response.run_id); - setShowResults(false); // Reset results view when starting new analysis + setShowResults(false); + toast.success('Analysis started successfully', { icon: '🚀' }); } - } catch (error) { + } catch (error: any) { console.error('Failed to trigger analysis', error); + toast.error(error?.message || 'Failed to start analysis. Please try again.'); + } finally { + setIsTriggering(false); } }; const getStepProgress = (status: string) => { switch (status) { - case 'started': return 10; - case 'running': return 50; + case 'started': return 25; + case 'running': return 75; case 'completed': return 100; case 'error': return 100; default: return 0; @@ -74,71 +107,101 @@ export default function AnalysisPage() { const getCurrentStep = (status: string) => { switch (status) { case 'started': return 'Loading Data'; - case 'running': return 'Processing'; - case 'completed': return 'Completed'; - case 'error': return 'Error'; - default: return 'Waiting'; + case 'running': return 'Processing Analysis'; + case 'completed': return 'Analysis Complete'; + case 'error': return 'Analysis Failed'; + default: return 'Waiting to Start'; } }; const isAnalysisActive = activeRunId && activeRun && (activeRun.status === 'started' || activeRun.status === 'running'); - // Toggle results view const toggleResultsView = () => { setShowResults(!showResults); }; // Helper functions to extract data from results - const getMetricsProcessed = (results: any) => { - return results?.metrics_processed || 0; - }; - - const getTicketsProcessed = (results: any) => { - return results?.tickets_processed || 0; - }; - - const getAnomaliesDetected = (results: any) => { - return results?.anomalies_detected || 0; - }; - - const getClustersCreated = (results: any) => { - return results?.clusters_created || 0; - }; - - const getInsightsGenerated = (results: any) => { - return results?.insights_generated || 0; - }; + const getMetricsProcessed = (results: any) => results?.metrics_processed || 0; + const getTicketsProcessed = (results: any) => results?.tickets_processed || 0; + const getAnomaliesDetected = (results: any) => results?.anomalies_detected || 0; + const getClustersCreated = (results: any) => results?.clusters_created || 0; + const getInsightsGenerated = (results: any) => results?.insights_generated || 0; return (
-
-

Analysis

-

- Trigger and monitor on-demand analysis runs -

+ {/* AWS-style Header with Breadcrumbs */} +
+ +
+

Run Analysis

+

+ Configure and execute analysis to correlate metrics with tickets and generate actionable insights +

+
-
- {/* Left Column: Trigger Analysis */} -
+ {/* Workflow Steps Indicator */} + +
+
+ {[ + { label: 'Configure', icon: ChartBarIcon, active: !activeRunId }, + { label: 'Execute', icon: PlayIcon, active: isAnalysisActive }, + { label: 'Review', icon: LightBulbIcon, active: activeRun?.status === 'completed' } + ].map((step, index, array) => ( + +
+
+ +
+ + {step.label} + +
+ {index < array.length - 1 && ( +
+ )} + + ))} +
+
+ + +
+ {/* Main Configuration Panel */} +
- {/* Window Minutes Slider */} + {/* Analysis Window */}
- - {windowMinutes} minutes - + + {windowMinutes} {windowMinutes === 1 ? 'minute' : 'minutes'} +
setWindowMinutes(parseInt(e.target.value))} - className="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer dark:bg-slate" + className="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer dark:bg-slate accent-primary" + style={{ + background: `linear-gradient(to right, #0972D3 0%, #0972D3 ${((windowMinutes - MIN_ANALYSIS_WINDOW) / (MAX_ANALYSIS_WINDOW - MIN_ANALYSIS_WINDOW)) * 100}%, #E5E7EB ${((windowMinutes - MIN_ANALYSIS_WINDOW) / (MAX_ANALYSIS_WINDOW - MIN_ANALYSIS_WINDOW)) * 100}%, #E5E7EB 100%)` + }} />
{MIN_ANALYSIS_WINDOW} min - {MAX_ANALYSIS_WINDOW} min + {MAX_ANALYSIS_WINDOW} min (24h)
+

+ Select the time window to analyze. Larger windows provide more context but take longer to process. +

{/* Metrics Selection */}
-
+
@@ -164,462 +233,369 @@ export default function AnalysisPage() { + |
-
+
{METRIC_TYPES.map(metric => ( -
+ {selectedMetrics.length === 0 && ( +

+ + Please select at least one metric to continue +

+ )}
- {/* Force Reprocess */} -
- setForceReprocess(e.target.checked)} - className="h-4 w-4 text-primary rounded border-gray-300 focus:ring-primary" - /> - + {/* Advanced Options */} +
+
+ + + Advanced Options + +
+ +
+
{/* Trigger Button */}
- {/* Last Analysis Result */} - {runs.length > 0 && ( - - - -
-
- Run ID - - {runs[0].run_id.substring(0, 8)}... - -
- -
- Status - - {runs[0].status} - -
- -
- Started - - {formatDate(runs[0].started_at)} - -
- -
-
-

- {getAnomaliesDetected(runs[0].results)} -

-

- Anomalies -

-
-
-

- {getClustersCreated(runs[0].results)} -

-

- Clusters -

-
-
-

- {getInsightsGenerated(runs[0].results)} -

-

- Insights -

-
-
- -
- - - -
-
-
- )} -
- - {/* Right Column: Active Analysis & History */} -
- {/* Active Analysis */} - {(isAnalysisActive || activeRunId) && ( - - - - {statusLoading ? ( -
- -
- ) : activeRun ? ( -
-
- Run ID - - {activeRun.run_id.substring(0, 8)}... - -
- -
- Status - - {activeRun.status} - -
- -
- Started - - {formatDate(activeRun.started_at)} - -
+ {/* Active Analysis Monitor */} + + {(isAnalysisActive || activeRunId) && activeRun && ( + + + - {/* Progress Bar */} -
-
- {getCurrentStep(activeRun.status)} - {getStepProgress(activeRun.status)}% +
+ {/* Progress Bar */} +
+
+ + {getCurrentStep(activeRun.status)} + + + {getStepProgress(activeRun.status)}% + +
+
+ +
-
-
+ + {/* Status Info */} +
+
+

Started

+

+ {formatRelativeTime(activeRun.started_at)} +

+
+
+

Status

+ + {activeRun.status} + +
-
- - {/* Results Summary (if completed) */} - {activeRun.status === 'completed' && activeRun.results && ( -
-
-
-

- {getAnomaliesDetected(activeRun.results)} -

-

- Anomalies -

-
-
-

- {getClustersCreated(activeRun.results)} -

-

- Clusters -

+ + {/* Results Summary (if completed) */} + {activeRun.status === 'completed' && activeRun.results && ( + +
+
+

+ {getInsightsGenerated(activeRun.results)} +

+

+ Insights +

+
+
+

+ {getAnomaliesDetected(activeRun.results)} +

+

+ Anomalies +

+
+
+

+ {getClustersCreated(activeRun.results)} +

+

+ Clusters +

+
+
+

+ {getMetricsProcessed(activeRun.results)} +

+

+ Metrics +

+
-
-

- {getInsightsGenerated(activeRun.results)} -

-

- Insights -

+ +
+ + + +
-
- - {/* View Results Button */} - - - {/* Detailed Results */} - {showResults && ( -
-

Analysis Results

- - {/* Metrics Processed */} -
-

- Data Processed -

-
-
-

Metrics

-

+ + {/* Detailed Results */} + {showResults && ( + +

+
+

Metrics Processed

+

{getMetricsProcessed(activeRun.results)}

-
-

Tickets

-

+

+

Tickets Processed

+

{getTicketsProcessed(activeRun.results)}

-
- - {/* Anomalies Detected */} - {getAnomaliesDetected(activeRun.results) > 0 && ( -
-

- - Anomalies Detected -

-
-

- {getAnomaliesDetected(activeRun.results)} statistical anomalies were detected in the metrics data. -

-
-
- )} - - {/* Clusters Created */} - {getClustersCreated(activeRun.results) > 0 && ( -
-

- Ticket Clusters -

-
-

- {getClustersCreated(activeRun.results)} ticket clusters were created. -

-
-
- )} - - {/* Insights Generated */} - {getInsightsGenerated(activeRun.results) > 0 && ( -
-

- - Insights Generated -

-
-

- {getInsightsGenerated(activeRun.results)} actionable insights were generated. -

-
-
- )} - - {/* Findings */} - {activeRun.results.findings && activeRun.results.findings.length > 0 && ( -
-

- Key Findings -

-
- {activeRun.results.findings.map((insight: any, index: number) => ( -
-
-
- {insight.hypothesis || 'Insight'} -
- - {insight.priority || 'medium'} - -
-

- {insight.explanation?.substring(0, 150)}... -

- {insight.recommended_actions && insight.recommended_actions.length > 0 && ( -
-

- Recommended Actions: -

-
    - {insight.recommended_actions.slice(0, 2).map((action: string, actionIndex: number) => ( -
  • {action}
  • - ))} - {insight.recommended_actions.length > 2 && ( -
  • + {insight.recommended_actions.length - 2} more actions
  • - )} -
-
- )} -
- ))} -
-
- )} + + )} + + )} -
- - - -
-
- )} -
- )} - - {/* Error Message (if error) */} - {activeRun.status === 'error' && activeRun.results?.error && ( -
-

- {activeRun.results.error} -

-
- )} + {/* Error State */} + {activeRun.status === 'error' && activeRun.results?.error && ( +
+

+ {activeRun.results.error} +

+
+ )} +
+ + + )} + +
+ + {/* Right Sidebar */} +
+ {/* Quick Info */} + + +
+
+
+ 1 +
+
+

Configure Analysis

+

+ Select metrics and time window +

+
+
+
+
+ 2 +
+
+

Execute Analysis

+

+ System processes data and detects patterns +

+
+
+
+
+ 3
- ) : ( -
- No active analysis run +
+

Review Insights

+

+ Get actionable insights and recommendations +

- )} - - )} +
+
+ {/* Recent Runs */} - - + runs.length > 0 ? ( + + + + ) : null } /> - {historyLoading ? ( -
+
) : runs.length === 0 ? ( -
-

- No analysis runs yet +

+ +

+ No recent runs

- - -
) : ( -
- {runs.map(run => ( -
-
-
- - {run.run_id.substring(0, 8)} - - - {run.status} - -
-

- {formatRelativeTime(run.started_at)} -

-
- -
-
-

- {getInsightsGenerated(run.results)} insights -

-

- {getAnomaliesDetected(run.results)} anomalies -

-
- - - +
+ {runs.slice(0, 3).map((run) => ( + +
+ + {run.run_id.substring(0, 8)}... + + + {run.status} +
-
+

+ {formatRelativeTime(run.started_at)} +

+ ))}
)} + + {/* Help Card */} + + +

+ Learn more about running analysis and interpreting results. +

+ + + +
); -} \ No newline at end of file +} diff --git a/frontend/app/insights/[id]/page.tsx b/frontend/app/insights/[id]/page.tsx index 654c088..1f96f69 100644 --- a/frontend/app/insights/[id]/page.tsx +++ b/frontend/app/insights/[id]/page.tsx @@ -1,8 +1,8 @@ 'use client'; -import React from 'react'; +import React, { useState } from 'react'; import { MainLayout } from '@/components/layout/MainLayout'; -import { Card, CardHeader, Button, LoadingSpinner, Badge } from '@/components/shared'; +import { Card, CardHeader, Button, LoadingSpinner, Badge, Breadcrumbs } from '@/components/shared'; import { useInsightDetail, useRelatedInsights } from '@/lib/hooks/useInsights'; import { Priority } from '@/lib/types/insight'; import { formatRelativeTime, formatPercentage } from '@/lib/utils/formatters'; @@ -11,7 +11,14 @@ import { ArrowLeftIcon, CheckCircleIcon, XCircleIcon, - ArrowPathIcon + ArrowPathIcon, + ChevronDownIcon, + ChevronUpIcon, + InformationCircleIcon, + EllipsisVerticalIcon, + ExclamationTriangleIcon, + DocumentTextIcon, + LightBulbIcon } from '@heroicons/react/24/outline'; // Props for the page component @@ -25,6 +32,7 @@ export default function InsightDetailPage({ params }: InsightDetailPageProps) { const { id } = params; const { insight, isLoading, isError, mutate } = useInsightDetail(id); const { relatedInsights = [], isLoading: relatedLoading } = useRelatedInsights(id); + const [expandedClusters, setExpandedClusters] = useState>(new Set()); const handleRefresh = () => { mutate(); @@ -40,6 +48,18 @@ export default function InsightDetailPage({ params }: InsightDetailPageProps) { console.log('Dismiss insight', id); }; + const toggleCluster = (clusterId: string) => { + setExpandedClusters(prev => { + const next = new Set(prev); + if (next.has(clusterId)) { + next.delete(clusterId); + } else { + next.add(clusterId); + } + return next; + }); + }; + if (isLoading) { return ( @@ -54,25 +74,22 @@ export default function InsightDetailPage({ params }: InsightDetailPageProps) { return (
- - - Back to Insights - - - -
- -

Insight Not Found

-

- The insight you're looking for doesn't exist or has been removed. -

- - - -
-
+ +
+ +

Insight Not Found

+

+ The insight you're looking for doesn't exist or has been removed. +

+ + + +
); @@ -80,35 +97,17 @@ export default function InsightDetailPage({ params }: InsightDetailPageProps) { return ( -
- {/* Header */} -
- - - Back to Insights - - -
- - - -
-
- - {/* Insight Overview */} - -
+
+ {/* AWS-style Header */} +
+ +
-
+
+

Insight Details

{insight.priority.charAt(0).toUpperCase() + insight.priority.slice(1)} - + {insight.status.toUpperCase()} - - {formatRelativeTime(insight.created_at)} -
- -

- Insight #{insight.id} -

- -

+

{insight.summary}

- -
-
- - - = 0.85 ? '#037F0C' : - insight.correlation_score >= 0.7 ? '#8BC34A' : - insight.correlation_score >= 0.5 ? '#FFB547' : - insight.correlation_score >= 0.3 ? '#FF991F' : '#D13212' - } - strokeWidth="3" - strokeDasharray={`${insight.correlation_score * 100}, 100`} - /> - = 0.85 ? '#037F0C' : - insight.correlation_score >= 0.7 ? '#8BC34A' : - insight.correlation_score >= 0.5 ? '#FFB547' : - insight.correlation_score >= 0.3 ? '#FF991F' : '#D13212' - } fontSize="8" fontWeight="bold"> - {formatPercentage(insight.correlation_score)} - - -
-

- Correlation Score -

+
+ + +
- -
-

Hypothesis

-

- {insight.hypothesis} -

-
- -
-

Narrative

-

- {insight.narrative} -

-
- - {insight.tags && insight.tags.length > 0 && ( -
-

Tags

-
- {insight.tags.map((tag: string, index: number) => ( - - {tag} - - ))} +
+ + {/* Two-Column Layout - AWS Style */} +
+ {/* Left Column - 2 spans */} +
+ {/* Key Metrics Summary Cards */} +
+
+

Correlation

+

+ {formatPercentage(insight.correlation_score)} +

+
+
+

Confidence

+

+ {formatPercentage(insight.confidence)} +

+
+
+

Anomalies

+

+ {insight.anomalies?.length || 0} +

+
+
+

Clusters

+

+ {insight.ticket_clusters?.length || 0} +

- )} - - - {/* Anomaly Details */} - - - -
- - - - - - - - - - - - - {insight.anomalies?.map((anomaly: any) => ( - - - - - - - - - ))} - -
MetricAssetTimestampScoreValueDeviation
{anomaly.metric_name}{anomaly.asset}{formatRelativeTime(anomaly.timestamp)} - = 0.8 ? 'red' : - anomaly.score >= 0.6 ? 'orange' : - anomaly.score >= 0.4 ? 'amber' : 'green' - } - size="sm" - > - {formatPercentage(anomaly.score)} - - - {anomaly.value.toFixed(2)} - - {anomaly.deviation > 0 ? '+' : ''}{(anomaly.deviation * 100).toFixed(1)}% -
-
-
- {/* Ticket Cluster Details */} - - - -
- {insight.ticket_clusters?.map((cluster: any) => ( -
-
-

- Cluster #{cluster.id} -

-
- - {cluster.ticket_count} tickets - - = 0.8 ? 'green' : - cluster.cohesion_score >= 0.6 ? 'amber' : - cluster.cohesion_score >= 0.4 ? 'orange' : 'red' - } - size="sm" - > - Cohesion: {formatPercentage(cluster.cohesion_score)} - -
+ {/* Anomalies Table - Compact AWS Style */} +
+
+
+

+ Contributing Anomalies ({insight.anomalies?.length || 0}) +

+ + Info +
- -

- {cluster.summary} -

- -
-

- "{cluster.centroid}" -

+ +
+ {insight.anomalies && insight.anomalies.length > 0 ? ( +
+ + + + + + + + + + + + {insight.anomalies.slice(0, 5).map((anomaly: any) => ( + + + + + + + + ))} + +
MetricAssetScoreValueDeviation
{anomaly.metric_name}{anomaly.asset} + = 0.8 ? 'red' : + anomaly.score >= 0.6 ? 'orange' : + 'amber' + } + size="sm" + > + {formatPercentage(anomaly.score)} + + {anomaly.value.toFixed(2)} + {anomaly.deviation > 0 ? '+' : ''}{(anomaly.deviation * 100).toFixed(1)}% +
+ {insight.anomalies.length > 5 && ( +
+

+ Showing 5 of {insight.anomalies.length} anomalies +

+
+ )}
- -
- - Show {cluster.tickets.length} tickets - - - - -
- {cluster.tickets.map((ticket: any) => ( -
-
-
- {ticket.title} -
- - {ticket.severity} - -
-

- {ticket.description} -

-
- - {ticket.asset_affected} - - - {formatRelativeTime(ticket.created_at)} + ) : ( +
+ +

No anomalies found

+
+ )} +
+ + {/* Ticket Clusters - Compact */} +
+
+
+

+ Ticket Clusters ({insight.ticket_clusters?.length || 0}) +

+
+ +
+
+ {insight.ticket_clusters && insight.ticket_clusters.length > 0 ? ( + insight.ticket_clusters.slice(0, 3).map((cluster: any) => ( +
+
+
+ + Cluster #{cluster.id} + {cluster.ticket_count} tickets + = 0.6 ? 'green' : 'orange'} + size="sm" + > + {formatPercentage(cluster.cohesion_score)} +
+
- ))} -
-
+

+ {cluster.summary} +

+ {expandedClusters.has(cluster.id) && ( +
+

+ "{cluster.centroid}" +

+
+ {cluster.tickets?.slice(0, 3).map((ticket: any) => ( +
+ • {ticket.title} ({ticket.severity}) +
+ ))} + {cluster.tickets?.length > 3 && ( +

+ + {cluster.tickets.length - 3} more tickets +

+ )} +
+
+ )} +
+ )) + ) : ( +

+ No ticket clusters found +

+ )}
- ))} -
- +
- {/* Recommended Actions */} - {insight.recommended_actions && insight.recommended_actions.length > 0 && ( - - - -
- {insight.recommended_actions.map((action: string, index: number) => ( -
-
- {index + 1} -
-

- {action} + {/* Hypothesis & Narrative - Compact */} +

+

Details

+
+
+

Hypothesis

+

+ {insight.hypothesis} +

+
+
+

Narrative

+

+ {insight.narrative}

-
- ))} +
- - )} +
- {/* Related Insights */} - - - - {relatedLoading ? ( -
- -
- ) : !Array.isArray(relatedInsights) || relatedInsights.length === 0 ? ( -
- No related insights found -
- ) : ( -
- {relatedInsights.map((related) => ( - -
- - {related.priority.charAt(0).toUpperCase() + related.priority.slice(1)} - - - {formatPercentage(related.correlation_score)} - + {/* Right Column - 1 span */} +
+ {/* Recommended Actions - Compact */} + {insight.recommended_actions && insight.recommended_actions.length > 0 && ( +
+
+

+ Recommended Actions ({insight.recommended_actions.length}) +

+ +
+
+ {insight.recommended_actions.map((action: string, index: number) => ( +
+ + {index + 1} + +

{action}

+
+ ))} +
+
+ )} + + {/* Metadata Card */} +
+
+

Metadata

+ +
+
+
+ Insight ID + {id.substring(0, 8)}... +
+
+ Created + {formatRelativeTime(insight.created_at)} +
+
+ Status + + {insight.status} + +
+ {insight.tags && insight.tags.length > 0 && ( +
+

Tags

+
+ {insight.tags.slice(0, 3).map((tag: string, index: number) => ( + {tag} + ))} + {insight.tags.length > 3 && ( + +{insight.tags.length - 3} + )} +
-

- {related.summary} -

- - ))} + )} +
- )} - + + {/* Related Insights - Compact */} + {relatedInsights.length > 0 && ( +
+
+

+ Related Insights ({relatedInsights.length}) +

+ +
+
+ {relatedInsights.slice(0, 3).map((related) => ( + +
+ + {related.priority.charAt(0).toUpperCase() + related.priority.slice(1)} + + + {formatPercentage(related.correlation_score)} + +
+

+ {related.summary} +

+ + ))} + {relatedInsights.length > 3 && ( + + View all related insights + + )} +
+
+ )} +
+
); -} \ No newline at end of file +} diff --git a/frontend/app/insights/page.tsx b/frontend/app/insights/page.tsx index ffaa018..7b73a01 100644 --- a/frontend/app/insights/page.tsx +++ b/frontend/app/insights/page.tsx @@ -1,39 +1,81 @@ 'use client'; -import React, { useState } from 'react'; +import React, { useState, useMemo } from 'react'; import { MainLayout } from '@/components/layout/MainLayout'; -import { Card, CardHeader, Button, LoadingSpinner, Badge } from '@/components/shared'; +import { Card, CardHeader, Button, LoadingSpinner, Badge, Breadcrumbs } from '@/components/shared'; import { useInsights } from '@/lib/hooks/useInsights'; import { Priority } from '@/lib/types/insight'; import { formatRelativeTime, formatPercentage } from '@/lib/utils/formatters'; import { DEFAULT_PAGE_SIZE, PAGE_SIZES } from '@/lib/utils/constants'; import Link from 'next/link'; +import { motion } from 'framer-motion'; import { ArrowPathIcon, FunnelIcon, ArrowsUpDownIcon, ChevronLeftIcon, - ChevronRightIcon + ChevronRightIcon, + LightBulbIcon, + MagnifyingGlassIcon, + PlayIcon, + ArrowRightIcon, + InformationCircleIcon, + XMarkIcon } from '@heroicons/react/24/outline'; +import { useDebounce } from '@/lib/utils/helpers'; export default function InsightsPage() { const [currentPage, setCurrentPage] = useState(0); const [pageSize, setPageSize] = useState(DEFAULT_PAGE_SIZE); + const [searchQuery, setSearchQuery] = useState(''); const [filters, setFilters] = useState({ priority: [] as string[], - status: [] as string[], minScore: 0, }); const [showFilters, setShowFilters] = useState(false); - const { insights, total, isLoading, isError, mutate } = useInsights({ - limit: pageSize, - offset: currentPage * pageSize, + // Debounce search query for better performance + const debouncedSearchQuery = useDebounce(searchQuery, 300); + + // Fetch insights - backend handles minScore, client-side handles priority/status/search + const { insights: allInsights, total: apiTotal, isLoading, isError, mutate } = useInsights({ + limit: 100, // Fetch more for client-side filtering + offset: 0, since: undefined, - minScore: filters.minScore > 0 ? filters.minScore : undefined + minScore: filters.minScore > 0 ? filters.minScore : undefined, }); - const totalPages = Math.ceil(total / pageSize); + // Client-side filtering (priority, status, search) + const { filteredInsights, filteredTotal } = useMemo(() => { + let filtered = [...allInsights]; + + // Apply priority filter (client-side) + if (filters.priority.length > 0) { + filtered = filtered.filter(insight => + filters.priority.includes(insight.priority) + ); + } + + // Apply search query filter + if (debouncedSearchQuery.trim()) { + const query = debouncedSearchQuery.toLowerCase(); + filtered = filtered.filter(insight => + insight.summary.toLowerCase().includes(query) || + insight.id.toLowerCase().includes(query) || + (insight.priority && insight.priority.toLowerCase().includes(query)) + ); + } + + // Apply pagination + const paginated = filtered.slice(currentPage * pageSize, (currentPage + 1) * pageSize); + + return { + filteredInsights: paginated, + filteredTotal: filtered.length, + }; + }, [allInsights, filters.priority, debouncedSearchQuery, currentPage, pageSize]); + + const totalPages = Math.ceil(filteredTotal / pageSize); const handleRefresh = () => { mutate(); @@ -52,25 +94,26 @@ export default function InsightsPage() { ? prev.priority.filter(p => p !== priority) : [...prev.priority, priority] })); + setCurrentPage(0); // Reset to first page when filter changes }; - const toggleStatusFilter = (status: string) => { - setFilters(prev => ({ - ...prev, - status: prev.status.includes(status) - ? prev.status.filter(s => s !== status) - : [...prev.status, status] - })); + + const handleScoreFilterChange = (value: number) => { + setFilters(prev => ({ ...prev, minScore: value })); + setCurrentPage(0); // Reset to first page when filter changes }; const clearFilters = () => { setFilters({ priority: [], - status: [], minScore: 0, }); + setSearchQuery(''); + setCurrentPage(0); }; + const hasActiveFilters = filters.priority.length > 0 || filters.minScore > 0 || searchQuery.trim() !== ''; + if (isError) { return ( @@ -98,56 +141,81 @@ export default function InsightsPage() { ); } + // Define priority values array + const priorityValues = ['critical', 'high', 'medium', 'low']; + return ( -
- {/* Header */} -
-
-

Insights

-

- Browse and analyze all system insights -

-
- -
- - +
+ {/* AWS-style Header with Breadcrumbs */} +
+ +
+
+

Insights

+

+ Discover actionable insights from your metrics and ticket correlations +

+
+ +
+ + + + + +
-
+
{/* Filters Sidebar */} {showFilters && (
- - -
+
+
+

Filters

+ {hasActiveFilters && ( + + )} +
+
{/* Priority Filter */}
-

+

Priority -

+
{priorityValues.map(priority => ( -
- +
)} {/* Main Content */}
- {/* Insights List */} - - + {/* AWS-style Search and Table Header */} +
+
+
+
+ + { + setSearchQuery(e.target.value); + setCurrentPage(0); + }} + className="w-full pl-10 pr-10 py-2 text-body-sm bg-gray-50 dark:bg-slate-dark border border-gray-200 dark:border-slate-light rounded-md focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent text-gray-700 dark:text-gray-300 placeholder-gray-400 dark:placeholder-gray-500" + /> + {searchQuery && ( + + )} +
+
+
+ + {filteredTotal} {filteredTotal === 1 ? 'insight' : 'insights'} + +
+
+ {/* Insights List */} {isLoading ? ( -
+
- ) : insights.length === 0 ? ( -
-

- {filters.priority.length > 0 || filters.status.length > 0 || filters.minScore > 0 - ? 'No insights match your filters. Try adjusting them.' - : 'No insights yet. Run an analysis to get started.'} + ) : filteredInsights.length === 0 ? ( +

+ +

+ {hasActiveFilters + ? 'No insights match your filters' + : 'No insights available'} +

+

+ {hasActiveFilters + ? 'Try adjusting your search or filter criteria to see more results.' + : 'Run an analysis to start generating insights from your metrics and ticket data.'}

-
- - - - {filters.priority.length > 0 || filters.status.length > 0 || filters.minScore > 0 ? ( - - ) : null} + ) : ( + + + + )}
) : (
- {insights.map(insight => ( - ( + -
-
-
- - {insight.priority.charAt(0).toUpperCase() + insight.priority.slice(1)} - - - {formatRelativeTime(insight.timestamp)} - -
-

- {insight.summary} -

-
- - Correlation: - {formatPercentage(insight.correlation_score)} - - - - Confidence: - {formatPercentage(insight.confidence)} + +
+
+
+ + {insight.priority.charAt(0).toUpperCase() + insight.priority.slice(1)} + + + {formatRelativeTime(insight.timestamp)} - +
+

+ {insight.summary} +

+
+
+ Correlation: + + {formatPercentage(insight.correlation_score)} + +
+
+ Confidence: + + {formatPercentage(insight.confidence)} + +
+
+
+
+ + View
- -
- + + ))}
)} - - {/* Pagination */} - {totalPages > 1 && ( -
-
- - Page {currentPage + 1} of {totalPages} - - -
- -
- + {/* Pagination - AWS Style */} + {totalPages > 1 && ( +
+
+ + Page {currentPage + 1} of {totalPages} + + +
- +
+ + + +
-
- )} + )} +
); } - - // Define priority values array - const priorityValues = ['critical', 'high', 'medium', 'low']; diff --git a/frontend/app/settings/page.tsx b/frontend/app/settings/page.tsx index 37e5dcb..149050a 100644 --- a/frontend/app/settings/page.tsx +++ b/frontend/app/settings/page.tsx @@ -2,7 +2,7 @@ import React, { useState } from 'react'; import { MainLayout } from '@/components/layout/MainLayout'; -import { Card, CardHeader, Button, LoadingSpinner, Badge } from '@/components/shared'; +import { Card, CardHeader, Button, LoadingSpinner, Badge, Breadcrumbs } from '@/components/shared'; import { useHealthStatus, useSystemConfig } from '@/lib/hooks/useSystemHealth'; import { SystemMode, HealthStatus } from '@/lib/types/health'; import { formatDate } from '@/lib/utils/formatters'; @@ -28,18 +28,24 @@ export default function SettingsPage() { return (
- {/* Header */} -
-
-

Settings

-

- System configuration and health status -

+ {/* AWS-style Header with Breadcrumbs */} +
+ +
+
+

Settings

+

+ System configuration, health monitoring, and system status +

+
+
-
{/* Tabs */} diff --git a/frontend/components/dashboard/Dashboard.tsx b/frontend/components/dashboard/Dashboard.tsx index c03d008..640627d 100644 --- a/frontend/components/dashboard/Dashboard.tsx +++ b/frontend/components/dashboard/Dashboard.tsx @@ -1,369 +1,570 @@ 'use client'; -import React from 'react'; -import { Card, CardHeader, LoadingSpinner, Badge, Button, AnimatedButton } from '@/components/shared'; +import React, { useMemo } from 'react'; +import { Card, CardHeader, LoadingSpinner, Badge, Button, AnimatedButton, Breadcrumbs } from '@/components/shared'; import { useInsights } from '@/lib/hooks/useInsights'; import { useHealthStatus as useSystemHealth } from '@/lib/hooks/useSystemHealth'; import { useAnalysisHistory } from '@/lib/hooks/useAnalysis'; import { formatRelativeTime, formatPercentage } from '@/lib/utils/formatters'; -import { ArrowPathIcon, PlayIcon, EyeIcon } from '@heroicons/react/24/outline'; +import { + ArrowPathIcon, + PlayIcon, + EyeIcon, + ExclamationTriangleIcon, + LightBulbIcon, + ChartBarIcon, + ClockIcon, + ArrowRightIcon, + CheckCircleIcon, + EllipsisVerticalIcon, + InformationCircleIcon +} from '@heroicons/react/24/outline'; import Link from 'next/link'; import { Priority } from '@/lib/types/insight'; import { motion } from 'framer-motion'; +import { cn } from '@/lib/utils/helpers'; export function Dashboard() { - const { insights, isLoading: insightsLoading, mutate: refreshInsights } = useInsights({ limit: 10 }); + const { insights, total: totalInsights, isLoading: insightsLoading, mutate: refreshInsights } = useInsights({ limit: 10 }); const { health, isHealthy, isLoading: healthLoading } = useSystemHealth(); - const { runs, isLoading: historyLoading } = useAnalysisHistory(5, 0); + const { runs, total: totalRuns, isLoading: historyLoading } = useAnalysisHistory(10, 0); const handleRefresh = () => { refreshInsights(); }; + // Calculate statistics + const stats = useMemo(() => { + const criticalInsights = insights.filter(i => i.priority === 'critical').length; + const highInsights = insights.filter(i => i.priority === 'high').length; + const recentRuns = runs.filter(r => { + const runDate = new Date(r.started_at); + const hoursAgo = (Date.now() - runDate.getTime()) / (1000 * 60 * 60); + return hoursAgo <= 24; + }).length; + const completedRuns = runs.filter(r => r.status === 'completed').length; + + return { + criticalInsights, + highInsights, + recentRuns, + completedRuns, + }; + }, [insights, runs]); + + const hasNoData = !insightsLoading && !historyLoading && insights.length === 0 && runs.length === 0; + return ( - - {/* Page Header */} - -
-

Dashboard

-

- Overview of system status and recent insights -

+
+ {/* AWS-style Page Header with Breadcrumbs */} +
+
+ +
+

Console Home

+

+ Monitor system health, insights, and analysis activity +

+
Refresh - +
- {/* Stats Cards */} - + {/* Info Banner (AWS-style) */} + {hasNoData && ( + +
+ +
+

+ Get Started with ProactivePulse AI +

+

+ Run your first analysis to start generating insights from your metrics and tickets data. +

+ + + +
+
+
+ )} + + {/* Stats Cards - AWS Console Style */} +
0 || stats.highInsights > 0 + ? `${stats.criticalInsights} critical, ${stats.highInsights} high priority` + : 'All insights available'} + icon={LightBulbIcon} + iconColor="text-warning dark:text-warning-dark" loading={insightsLoading} + action={ + insights.length > 0 ? ( + + Go to Insights + + ) : null + } + /> + 0 ? ( + + Go to Analysis History + + ) : null + } /> + Go to Settings + + } /> 0 ? formatRelativeTime(runs[0].started_at) : 'None'} + subtitle={runs.length > 0 ? `Last run: ${runs[0].status}` : 'No runs yet'} + icon={ClockIcon} + iconColor="text-gray-500 dark:text-gray-400" loading={historyLoading} /> - - +
- {/* Main Content Grid */} - - {/* Recent Insights - Takes 2 columns */} - + {/* Main Content - AWS Widget Layout */} +
+ {/* Left Column - 2 spans */} +
+ {/* Recent Insights Widget */} 0 ? `${insights.length} of ${totalInsights} total insights` : 'No insights generated yet'} action={ - - - View All - - + insights.length > 0 ? ( + + + + ) : ( + + + + ) } - className="p-6 pb-4" + className="p-6 pb-4 border-b border-gray-200 dark:border-slate-light" />
{insightsLoading ? ( -
+
) : insights.length === 0 ? ( - -

No insights yet. Run an analysis to get started.

+
+ +

+ No insights available +

+

+ Run an analysis to generate insights from your metrics and tickets. +

- + - +
) : ( - - {insights.map((insight, index) => ( - ( + + - -
-
-
- - {insight.priority.charAt(0).toUpperCase() + insight.priority.slice(1)} - - - {formatRelativeTime(insight.timestamp)} - -
-

- {insight.summary} -

-
- - Correlation: - {formatPercentage(insight.correlation_score)} - +
+
+
+ + {insight.priority.charAt(0).toUpperCase() + insight.priority.slice(1)} + + + {formatRelativeTime(insight.timestamp)} + +
+

+ {insight.summary} +

+
+ + Correlation: + {formatPercentage(insight.correlation_score)} - - Confidence: - {formatPercentage(insight.confidence)} - + + + Confidence: + {formatPercentage(insight.confidence)} -
+
-
- - - ))} - + +
+ + + )) )}
- - {/* System Activity - Takes 1 column */} - - {/* System Health */} - - - - - Details - - - } - /> - {healthLoading ? ( -
- -
- ) : health ? ( -
-
- Status - - {health.status.toUpperCase()} - -
-
- Mode - - {health.mode.toUpperCase()} - -
-
- Version - - {health.version} - -
-
- ) : ( -

- Unable to load system health -

- )} -
-
- - {/* Recent Analysis */} - - - + 0 ? ( - - View All - + - } - /> + ) : null + } + className="p-6 pb-4 border-b border-gray-200 dark:border-slate-light" + /> +
{historyLoading ? ( -
+
) : runs.length === 0 ? ( -

- No analysis runs yet -

+
+ +

+ No analysis runs yet +

+ + + +
) : (
- {runs.map((run, index) => ( - ( +
- - {formatRelativeTime(run.started_at)} - - - {run.status} - - +
+
+ + {run.run_id.substring(0, 8)}... + + + {run.status} + +
+

+ {formatRelativeTime(run.started_at)} +

+
+ + + +
))}
)} - - +
+ +
- {/* Quick Actions */} - - - -
- - - - Run Analysis - + {/* Right Column - 1 span */} +
+ {/* System Health Widget */} + + + + } + /> + {healthLoading ? ( +
+ +
+ ) : health ? ( +
+
+ Overall Status + + {health.status.toUpperCase()} + +
+
+
+

Mode

+

+ {health.mode.toUpperCase()} +

+
+
+

Version

+

+ {health.version} +

+
+
+
+ ) : ( +

+ Unable to load system health +

+ )} +
+ + {/* Quick Actions Widget - AWS Style */} +
+ {/* Header with title and menu */} +
+

Quick Actions

+ +
+ + {/* Actions List */} +
+ +
+
+
+ +
+
+

Run Analysis

+

Execute new analysis run

+
+
+ +
+ + + +
+
+
+ +
+
+

View All Insights

+

Browse all insights

+
+
+ +
+ + + +
+
+
+ +
+
+

Analysis History

+

View past runs

+
+
+ +
+ +
+
+ + {/* Priority Insights Summary */} + {stats.criticalInsights > 0 || stats.highInsights > 0 ? ( + + +
+ {stats.criticalInsights > 0 && ( +
+
+ + Critical +
+ + {stats.criticalInsights} + +
+ )} + {stats.highInsights > 0 && ( +
+
+ + High +
+ + {stats.highInsights} + +
+ )} - - - View All Insights - +
- - - - + ) : null} +
+
+
); } interface StatsCardProps { title: string; value: string; - valueColor?: string; subtitle?: string; + icon: React.ComponentType<{ className?: string }>; + iconColor?: string; + valueColor?: string; loading?: boolean; + action?: React.ReactNode; + infoLink?: string; } -function StatsCard({ title, value, valueColor, subtitle, loading }: StatsCardProps) { +function StatsCard({ title, value, subtitle, icon: Icon, iconColor, valueColor, loading, action, infoLink }: StatsCardProps) { return ( - -
-

{title}

+
+ {/* Header with title and menu */} +
+
+

{title}

+ {infoLink && ( + + Info + + )} +
+ +
+ + {/* Large Value */} +
{loading ? ( -
+
) : ( -

+

{value}

)} - {subtitle && ( -

{subtitle}

+ {subtitle && !loading && ( +

+ {subtitle} +

)}
- + + {/* Action Link */} + {action && ( +
+ {action} +
+ )} +
); -} \ No newline at end of file +} diff --git a/frontend/components/layout/Header.tsx b/frontend/components/layout/Header.tsx index 98db7fd..b38fca8 100644 --- a/frontend/components/layout/Header.tsx +++ b/frontend/components/layout/Header.tsx @@ -6,73 +6,71 @@ import { useHealthStatus as useSystemHealth } from '@/lib/hooks/useSystemHealth' import { Badge } from '../shared'; import { cn } from '@/lib/utils/helpers'; import { motion } from 'framer-motion'; -import { useSidebar } from '.'; // Import the useSidebar hook +import { useSidebar } from '.'; +import { BellIcon, QuestionMarkCircleIcon } from '@heroicons/react/24/outline'; -interface HeaderProps { - // Remove sidebarCollapsed prop since we'll get it from context -} +interface HeaderProps {} export const Header: React.FC = () => { const { health, isHealthy } = useSystemHealth(); - const { collapsed: sidebarCollapsed } = useSidebar(); // Get state from context + const { collapsed: sidebarCollapsed } = useSidebar(); return (
- {/* Breadcrumb / Title */} - -

- ProactivePulse AI -

-
+ {/* Left section - Search bar (AWS-style) */} +
+
+ + + Q + +
+
{/* Right section */} - - {/* System status */} +
+ {/* System status - More compact */} {health && ( - +
{health.mode.toUpperCase()} - - {health.status.toUpperCase()} - - +
)} - {/* Theme toggle */} - + + + + + {/* Help icon */} + + + {/* Theme toggle */} + +
); }; \ No newline at end of file diff --git a/frontend/components/layout/Sidebar.tsx b/frontend/components/layout/Sidebar.tsx index 46a8e4e..3838443 100644 --- a/frontend/components/layout/Sidebar.tsx +++ b/frontend/components/layout/Sidebar.tsx @@ -11,27 +11,49 @@ import { Cog6ToothIcon, ChevronLeftIcon, ChevronRightIcon, + ChartBarIcon, } from '@heroicons/react/24/outline'; import { APP_NAME } from '@/lib/utils/constants'; import { cn } from '@/lib/utils/helpers'; import { motion, AnimatePresence } from 'framer-motion'; -import { useSidebar } from '.'; // Import the useSidebar hook +import { useSidebar } from '.'; interface NavItem { name: string; href: string; icon: React.ComponentType<{ className?: string }>; + badge?: string | number; } -const navigation: NavItem[] = [ - { name: 'Dashboard', href: '/', icon: HomeIcon }, - { name: 'Insights', href: '/insights', icon: LightBulbIcon }, - { name: 'Analysis', href: '/analysis', icon: PlayIcon }, - { name: 'History', href: '/analysis/history', icon: ClockIcon }, - { name: 'Settings', href: '/settings', icon: Cog6ToothIcon }, +interface NavSection { + title: string; + items: NavItem[]; +} + +// Grouped navigation - AWS Console style +const navigation: NavSection[] = [ + { + title: 'Overview', + items: [ + { name: 'Console Home', href: '/', icon: HomeIcon }, + ], + }, + { + title: 'Analysis & Insights', + items: [ + { name: 'Insights', href: '/insights', icon: LightBulbIcon }, + { name: 'Run Analysis', href: '/analysis', icon: PlayIcon }, + { name: 'Analysis History', href: '/analysis/history', icon: ClockIcon }, + ], + }, + { + title: 'Configuration', + items: [ + { name: 'Settings', href: '/settings', icon: Cog6ToothIcon }, + ], + }, ]; -// Remove the props since we'll get the state from context export const Sidebar: React.FC = () => { const pathname = usePathname(); const { collapsed, toggleSidebar } = useSidebar(); @@ -39,53 +61,103 @@ export const Sidebar: React.FC = () => { return ( ); -}; \ No newline at end of file +}; + +// Add Badge component import inline since we're using it +const Badge: React.FC<{ variant?: string; size?: string; className?: string; children: React.ReactNode }> = ({ + variant = 'info', + size = 'sm', + className, + children +}) => { + return ( + + {children} + + ); +}; diff --git a/frontend/components/shared/Breadcrumbs.tsx b/frontend/components/shared/Breadcrumbs.tsx new file mode 100644 index 0000000..b4a63b6 --- /dev/null +++ b/frontend/components/shared/Breadcrumbs.tsx @@ -0,0 +1,52 @@ +import React from 'react'; +import Link from 'next/link'; +import { ChevronRightIcon, HomeIcon } from '@heroicons/react/24/outline'; +import { cn } from '@/lib/utils/helpers'; + +export interface BreadcrumbItem { + label: string; + href?: string; +} + +interface BreadcrumbsProps { + items: BreadcrumbItem[]; + className?: string; +} + +export const Breadcrumbs: React.FC = ({ items, className }) => { + return ( + + ); +}; + diff --git a/frontend/components/shared/index.ts b/frontend/components/shared/index.ts index d2aec8e..c9e962b 100644 --- a/frontend/components/shared/index.ts +++ b/frontend/components/shared/index.ts @@ -1,5 +1,7 @@ -export { Button } from './Button'; -export { Badge } from './Badge'; -export { Card, CardHeader } from './Card'; -export { LoadingSpinner, LoadingPage } from './LoadingSpinner'; -export { AnimatedButton } from './AnimatedButton'; +export { Button } from './Button'; +export { Badge } from './Badge'; +export { Card, CardHeader } from './Card'; +export { LoadingSpinner, LoadingPage } from './LoadingSpinner'; +export { AnimatedButton } from './AnimatedButton'; +export { Breadcrumbs } from './Breadcrumbs'; +export type { BreadcrumbItem } from './Breadcrumbs'; diff --git a/frontend/lib/hooks/useInsights.ts b/frontend/lib/hooks/useInsights.ts index e8d0e7e..636c7da 100644 --- a/frontend/lib/hooks/useInsights.ts +++ b/frontend/lib/hooks/useInsights.ts @@ -66,16 +66,27 @@ const getKey = (pageIndex: number, previousPageData: InsightsListResponse | null return `/insights?limit=${limit}&offset=${offset}`; }; -export function useInsights(options: { limit?: number; since?: string; minScore?: number; offset?: number } = {}) { +export function useInsights(options: { + limit?: number; + since?: string; + minScore?: number; + offset?: number; + priority?: string[]; + status?: string[]; +} = {}) { const { limit = 20, since, minScore, offset = 0 } = options; - // Build query parameters + // Build query parameters - Backend only supports min_score and since + // Priority and status filtering will be done client-side const params = new URLSearchParams(); - params.append('limit', limit.toString()); + params.append('limit', Math.min(limit, 100).toString()); // Backend max is 100 params.append('offset', offset.toString()); if (since) params.append('since', since); - if (minScore !== undefined) params.append('min_score', minScore.toString()); + if (minScore !== undefined && minScore > 0) { + // Convert percentage (0-100) to decimal (0.0-1.0) + params.append('min_score', (minScore / 100).toFixed(2)); + } const query = params.toString() ? `?${params.toString()}` : ''; diff --git a/frontend/lib/utils/helpers.ts b/frontend/lib/utils/helpers.ts index 7b51050..60113ce 100644 --- a/frontend/lib/utils/helpers.ts +++ b/frontend/lib/utils/helpers.ts @@ -1,3 +1,4 @@ +import { useState, useEffect } from 'react'; import { clsx, type ClassValue } from 'clsx'; import { twMerge } from 'tailwind-merge'; @@ -41,4 +42,21 @@ export async function copyToClipboard(text: string) { console.error('Failed to copy text to clipboard', error); return false; } +} + +// Debounce hook for React components +export function useDebounce(value: T, delay: number): T { + const [debouncedValue, setDebouncedValue] = useState(value); + + useEffect(() => { + const handler = setTimeout(() => { + setDebouncedValue(value); + }, delay); + + return () => { + clearTimeout(handler); + }; + }, [value, delay]); + + return debouncedValue; } \ No newline at end of file