From 4318a5f7e87e551c216f64590ac07b784809a733 Mon Sep 17 00:00:00 2001 From: Asirwad Date: Sat, 1 Nov 2025 16:29:36 +0530 Subject: [PATCH 1/2] feat: add tips carousel to Dashboard for enhanced user guidance - Introduced a TipsCarousel component to display helpful tips on the Dashboard. - Configured five actionable tips to assist users in navigating features like analysis, insights, and system health. - Updated shared components index to include the new TipsCarousel and its type. --- frontend/components/dashboard/Dashboard.tsx | 61 ++++++- frontend/components/shared/TipsCarousel.tsx | 189 ++++++++++++++++++++ frontend/components/shared/index.ts | 2 + 3 files changed, 250 insertions(+), 2 deletions(-) create mode 100644 frontend/components/shared/TipsCarousel.tsx diff --git a/frontend/components/dashboard/Dashboard.tsx b/frontend/components/dashboard/Dashboard.tsx index 640627d..c157e73 100644 --- a/frontend/components/dashboard/Dashboard.tsx +++ b/frontend/components/dashboard/Dashboard.tsx @@ -1,7 +1,7 @@ 'use client'; import React, { useMemo } from 'react'; -import { Card, CardHeader, LoadingSpinner, Badge, Button, AnimatedButton, Breadcrumbs } from '@/components/shared'; +import { Card, CardHeader, LoadingSpinner, Badge, Button, AnimatedButton, Breadcrumbs, TipsCarousel } from '@/components/shared'; import { useInsights } from '@/lib/hooks/useInsights'; import { useHealthStatus as useSystemHealth } from '@/lib/hooks/useSystemHealth'; import { useAnalysisHistory } from '@/lib/hooks/useAnalysis'; @@ -54,6 +54,60 @@ export function Dashboard() { const hasNoData = !insightsLoading && !historyLoading && insights.length === 0 && runs.length === 0; + // Dashboard tips configuration + const dashboardTips = useMemo(() => [ + { + id: 'tip-1', + title: 'Run Analysis to Generate Insights', + description: 'Execute an analysis to correlate metrics with tickets and discover actionable insights. Configure the time window and select metrics to analyze.', + icon: PlayIcon, + action: { + label: 'Run Analysis', + href: '/analysis' + } + }, + { + id: 'tip-2', + title: 'Explore Your Insights', + description: 'View all generated insights with filtering by priority and correlation score. Each insight provides detailed analysis of anomalies and related tickets.', + icon: LightBulbIcon, + action: { + label: 'View Insights', + href: '/insights' + } + }, + { + id: 'tip-3', + title: 'Monitor Analysis History', + description: 'Track all past analysis runs, view their status, and review detailed results including anomalies detected and insights generated.', + icon: ChartBarIcon, + action: { + label: 'View History', + href: '/analysis/history' + } + }, + { + id: 'tip-4', + title: 'Monitor System Health', + description: 'Check system status, configuration, and service health from the Settings page. Monitor system mode, version, and feature flags.', + icon: CheckCircleIcon, + action: { + label: 'View Settings', + href: '/settings' + } + }, + { + id: 'tip-5', + title: 'Use Search and Filters', + description: 'Quickly find insights using the search bar or filter by priority and correlation score. Use the insights page to explore and discover patterns.', + icon: InformationCircleIcon, + action: { + label: 'Explore Insights', + href: '/insights' + } + } + ], []); + return (
{/* AWS-style Page Header with Breadcrumbs */} @@ -77,7 +131,10 @@ export function Dashboard() {
- {/* Info Banner (AWS-style) */} + {/* Dashboard Tips Carousel - Always visible (unless hidden by user) */} + + + {/* Info Banner (AWS-style) - Only shows when no data */} {hasNoData && ( ; + action?: { + label: string; + href: string; + }; +} + +interface TipsCarouselProps { + tips: Tip[]; + storageKey?: string; + className?: string; +} + +export const TipsCarousel: React.FC = ({ + tips, + storageKey = 'dashboard-tips-hidden', + className +}) => { + const [currentIndex, setCurrentIndex] = useState(0); + const [isVisible, setIsVisible] = useState(true); + + // Check localStorage on mount + useEffect(() => { + const isHidden = localStorage.getItem(storageKey) === 'true'; + setIsVisible(!isHidden); + }, [storageKey]); + + const handleHide = () => { + localStorage.setItem(storageKey, 'true'); + setIsVisible(false); + }; + + const handleNext = () => { + setCurrentIndex((prev) => (prev + 1) % tips.length); + }; + + const handlePrevious = () => { + setCurrentIndex((prev) => (prev - 1 + tips.length) % tips.length); + }; + + const handleGoToIndex = (index: number) => { + setCurrentIndex(index); + }; + + if (!isVisible || tips.length === 0) { + return null; + } + + const currentTip = tips[currentIndex]; + const Icon = currentTip.icon || InformationCircleIcon; + + return ( + + {isVisible && ( + +
+ {/* Header with close button */} +
+
+
+ +
+
+

+ Dashboard Tips +

+

+ {currentIndex + 1} of {tips.length} +

+
+
+ +
+ + {/* Tip Content */} + + +

+ {currentTip.title} +

+

+ {currentTip.description} +

+ {currentTip.action && ( +
+ + + +
+ )} +
+
+ + {/* Navigation Controls */} +
+ {/* Stepper Dots */} +
+ {tips.map((_, index) => ( +
+ + {/* Navigation Buttons */} +
+ + +
+
+
+
+ )} +
+ ); +}; + diff --git a/frontend/components/shared/index.ts b/frontend/components/shared/index.ts index c9e962b..af803e9 100644 --- a/frontend/components/shared/index.ts +++ b/frontend/components/shared/index.ts @@ -5,3 +5,5 @@ export { LoadingSpinner, LoadingPage } from './LoadingSpinner'; export { AnimatedButton } from './AnimatedButton'; export { Breadcrumbs } from './Breadcrumbs'; export type { BreadcrumbItem } from './Breadcrumbs'; +export { TipsCarousel } from './TipsCarousel'; +export type { Tip } from './TipsCarousel'; From 8d961e5970b9b064554fcd89094e1d95979f8c89 Mon Sep 17 00:00:00 2001 From: Asirwad Date: Sat, 1 Nov 2025 17:26:40 +0530 Subject: [PATCH 2/2] feat(dashboard): add external control and keyboard navigation to TipsCarousel - Expose show and hide methods on TipsCarousel via ref for external control - Update Dashboard to listen for tipsVisibilityChanged events and control TipsCarousel - Replace help icon button in Header with interactive HelpPopover component - Implement keyboard navigation (arrow keys) for TipsCarousel tips cycling - Enhance TipsCarousel accessibility with focus styles and ARIA labels - Improve localStorage safety by adding try-catch blocks around storage operations - Use useImperativeHandle and forwardRef to allow parent components to control TipsCarousel visibility --- frontend/app/docs/dashboard-tips/page.tsx | 72 +++++++++ frontend/app/docs/index.ts | 1 + frontend/app/docs/layout.tsx | 15 ++ frontend/app/docs/page.tsx | 80 ++++++++++ frontend/components/dashboard/Dashboard.tsx | 26 ++- frontend/components/layout/Header.tsx | 12 +- frontend/components/shared/HelpPopover.tsx | 167 ++++++++++++++++++++ frontend/components/shared/TipsCarousel.tsx | 83 ++++++++-- 8 files changed, 435 insertions(+), 21 deletions(-) create mode 100644 frontend/app/docs/dashboard-tips/page.tsx create mode 100644 frontend/app/docs/index.ts create mode 100644 frontend/app/docs/layout.tsx create mode 100644 frontend/app/docs/page.tsx create mode 100644 frontend/components/shared/HelpPopover.tsx diff --git a/frontend/app/docs/dashboard-tips/page.tsx b/frontend/app/docs/dashboard-tips/page.tsx new file mode 100644 index 0000000..5ce935d --- /dev/null +++ b/frontend/app/docs/dashboard-tips/page.tsx @@ -0,0 +1,72 @@ +'use client'; + +import React from 'react'; +import { Breadcrumbs } from '@/components/shared'; + +export default function DashboardTipsDocs() { + return ( +
+ + +
+

Dashboard Tips

+

+ Learn how to make the most of your ProactivePulse AI dashboard +

+
+ +
+
+

Getting Started with Dashboard Tips

+

+ Dashboard tips provide helpful guidance on using the ProactivePulse AI platform effectively. + These tips appear at the top of your dashboard and can be dismissed or re-enabled at any time. +

+ +

Managing Dashboard Tips

+

+ You can control the visibility of dashboard tips in two ways: +

+ +
    +
  • + Dismiss individual tips by clicking the X icon in the top-right corner of the tip card +
  • +
  • + Toggle all tips on/off using the help icon (❓) in the top navigation bar +
  • +
+ +

Re-enabling Tips

+

+ If you've dismissed the tips and want to see them again: +

+ +
    +
  1. Click the help icon (❓) in the top navigation bar
  2. +
  3. Toggle the "Show Tips" switch to the on position
  4. +
  5. The tips carousel will immediately reappear on your dashboard
  6. +
+ +

Tip Categories

+

+ Dashboard tips cover various aspects of the platform: +

+ +
    +
  • Running analysis to generate insights
  • +
  • Exploring and filtering insights
  • +
  • Monitoring analysis history
  • +
  • Checking system health and configuration
  • +
  • Using search and filter functionality
  • +
+
+
+
+ ); +} \ No newline at end of file diff --git a/frontend/app/docs/index.ts b/frontend/app/docs/index.ts new file mode 100644 index 0000000..d1f210b --- /dev/null +++ b/frontend/app/docs/index.ts @@ -0,0 +1 @@ +// This file is intentionally left empty to ensure proper directory recognition \ No newline at end of file diff --git a/frontend/app/docs/layout.tsx b/frontend/app/docs/layout.tsx new file mode 100644 index 0000000..285df6c --- /dev/null +++ b/frontend/app/docs/layout.tsx @@ -0,0 +1,15 @@ +'use client'; + +import React from 'react'; + +export default function DocsLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( +
+ {children} +
+ ); +} \ No newline at end of file diff --git a/frontend/app/docs/page.tsx b/frontend/app/docs/page.tsx new file mode 100644 index 0000000..6521faf --- /dev/null +++ b/frontend/app/docs/page.tsx @@ -0,0 +1,80 @@ +'use client'; + +import React from 'react'; +import Link from 'next/link'; +import { Breadcrumbs } from '@/components/shared/Breadcrumbs'; +import { + BookOpenIcon, + LightBulbIcon, + Cog6ToothIcon, + ChartBarIcon +} from '@heroicons/react/24/outline'; + +export default function DocsPage() { + const docsSections = [ + { + title: 'Dashboard Tips', + description: 'Learn how to make the most of your dashboard with helpful tips and guidance', + icon: LightBulbIcon, + href: '/docs/dashboard-tips' + }, + { + title: 'Getting Started', + description: 'Quick start guide to setting up and using ProactivePulse AI', + icon: BookOpenIcon, + href: '#' + }, + { + title: 'Configuration', + description: 'Learn how to configure the system for your environment', + icon: Cog6ToothIcon, + href: '#' + }, + { + title: 'Analysis Guide', + description: 'Detailed guide on running and interpreting analysis results', + icon: ChartBarIcon, + href: '#' + } + ]; + + return ( +
+ + +
+

Documentation

+

+ Learn how to use ProactivePulse AI effectively +

+
+ +
+ {docsSections.map((section, index) => { + const Icon = section.icon; + return ( + +
+
+ +
+
+

+ {section.title} +

+

+ {section.description} +

+
+
+ + ); + })} +
+
+ ); +} \ No newline at end of file diff --git a/frontend/components/dashboard/Dashboard.tsx b/frontend/components/dashboard/Dashboard.tsx index c157e73..55effff 100644 --- a/frontend/components/dashboard/Dashboard.tsx +++ b/frontend/components/dashboard/Dashboard.tsx @@ -1,6 +1,6 @@ 'use client'; -import React, { useMemo } from 'react'; +import React, { useMemo, useEffect, useRef } from 'react'; import { Card, CardHeader, LoadingSpinner, Badge, Button, AnimatedButton, Breadcrumbs, TipsCarousel } from '@/components/shared'; import { useInsights } from '@/lib/hooks/useInsights'; import { useHealthStatus as useSystemHealth } from '@/lib/hooks/useSystemHealth'; @@ -23,16 +23,36 @@ import Link from 'next/link'; import { Priority } from '@/lib/types/insight'; import { motion } from 'framer-motion'; import { cn } from '@/lib/utils/helpers'; +import type { TipsCarouselRef } from '@/components/shared/TipsCarousel'; export function Dashboard() { const { insights, total: totalInsights, isLoading: insightsLoading, mutate: refreshInsights } = useInsights({ limit: 10 }); const { health, isHealthy, isLoading: healthLoading } = useSystemHealth(); const { runs, total: totalRuns, isLoading: historyLoading } = useAnalysisHistory(10, 0); + const tipsCarouselRef = useRef(null); const handleRefresh = () => { refreshInsights(); }; + // Listen for tips visibility changes from the HelpPopover + useEffect(() => { + const handleTipsVisibilityChange = (event: CustomEvent) => { + if (tipsCarouselRef.current) { + if (event.detail.show) { + tipsCarouselRef.current.show(); + } else { + tipsCarouselRef.current.hide(); + } + } + }; + + window.addEventListener('tipsVisibilityChanged', handleTipsVisibilityChange as EventListener); + return () => { + window.removeEventListener('tipsVisibilityChanged', handleTipsVisibilityChange as EventListener); + }; + }, []); + // Calculate statistics const stats = useMemo(() => { const criticalInsights = insights.filter(i => i.priority === 'critical').length; @@ -132,7 +152,7 @@ export function Dashboard() { {/* Dashboard Tips Carousel - Always visible (unless hidden by user) */} - + {/* Info Banner (AWS-style) - Only shows when no data */} {hasNoData && ( @@ -624,4 +644,4 @@ function StatsCard({ title, value, subtitle, icon: Icon, iconColor, valueColor,
); -} +} \ No newline at end of file diff --git a/frontend/components/layout/Header.tsx b/frontend/components/layout/Header.tsx index b38fca8..8309a42 100644 --- a/frontend/components/layout/Header.tsx +++ b/frontend/components/layout/Header.tsx @@ -2,12 +2,13 @@ import React from 'react'; import { AnimatedThemeToggler } from '@/components/ui/AnimatedThemeToggler'; +import { HelpPopover } from '@/components/shared/HelpPopover'; 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 { BellIcon, QuestionMarkCircleIcon } from '@heroicons/react/24/outline'; +import { BellIcon } from '@heroicons/react/24/outline'; interface HeaderProps {} @@ -60,13 +61,8 @@ export const Header: React.FC = () => { - {/* Help icon */} - + {/* Help popover */} + {/* Theme toggle */} diff --git a/frontend/components/shared/HelpPopover.tsx b/frontend/components/shared/HelpPopover.tsx new file mode 100644 index 0000000..283a8dd --- /dev/null +++ b/frontend/components/shared/HelpPopover.tsx @@ -0,0 +1,167 @@ +'use client'; + +import React, { useState, useRef, useEffect } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; +import { + QuestionMarkCircleIcon, + EyeIcon, + EyeSlashIcon, + BookOpenIcon +} from '@heroicons/react/24/outline'; +import Link from 'next/link'; +import { clsx } from 'clsx'; +import { twMerge } from 'tailwind-merge'; + +// Utility function to merge Tailwind classes with clsx +function cn(...inputs: any[]) { + return twMerge(clsx(inputs)); +} + +interface HelpPopoverProps { + className?: string; +} + +export const HelpPopover: React.FC = ({ className }) => { + const [isOpen, setIsOpen] = useState(false); + const [showTips, setShowTips] = useState(true); + const popoverRef = useRef(null); + const helpButtonRef = useRef(null); + + // Check localStorage for tips visibility on mount + useEffect(() => { + try { + const isHidden = localStorage.getItem('dashboard-tips-hidden') === 'true'; + setShowTips(!isHidden); + } catch (e) { + // If localStorage is unavailable, default to showing tips + setShowTips(true); + } + }, []); + + // Handle clicks outside the popover to close it + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (popoverRef.current && !popoverRef.current.contains(event.target as Node)) { + setIsOpen(false); + } + }; + + // Handle keyboard events + const handleKeyDown = (event: KeyboardEvent) => { + // Close popover with Escape key + if (event.key === 'Escape' && isOpen) { + setIsOpen(false); + // Focus back to the help button + helpButtonRef.current?.focus(); + } + }; + + if (isOpen) { + document.addEventListener('mousedown', handleClickOutside); + document.addEventListener('keydown', handleKeyDown); + } + + return () => { + document.removeEventListener('mousedown', handleClickOutside); + document.removeEventListener('keydown', handleKeyDown); + }; + }, [isOpen]); + + const toggleTipsVisibility = () => { + const newShowTips = !showTips; + setShowTips(newShowTips); + + try { + if (newShowTips) { + localStorage.removeItem('dashboard-tips-hidden'); + } else { + localStorage.setItem('dashboard-tips-hidden', 'true'); + } + } catch (e) { + // If localStorage is unavailable, continue with in-memory preference + } + + // Dispatch a custom event to notify the dashboard + window.dispatchEvent(new CustomEvent('tipsVisibilityChanged', { + detail: { show: newShowTips } + })); + + // If we're showing tips, close the popover + if (newShowTips) { + setIsOpen(false); + // Focus back to the help button + helpButtonRef.current?.focus(); + } + }; + + return ( +
+ + + + {isOpen && ( + +
+

+ Dashboard Help +

+ +
+
+ + Show Tips + + +
+ +
+ + + Dashboard Tips Docs + +
+
+
+
+ )} +
+
+ ); +}; \ No newline at end of file diff --git a/frontend/components/shared/TipsCarousel.tsx b/frontend/components/shared/TipsCarousel.tsx index 8452c81..3213cc4 100644 --- a/frontend/components/shared/TipsCarousel.tsx +++ b/frontend/components/shared/TipsCarousel.tsx @@ -1,4 +1,6 @@ -import React, { useState, useEffect } from 'react'; +'use client'; + +import React, { useState, useEffect, useImperativeHandle, forwardRef } from 'react'; import Link from 'next/link'; import { motion, AnimatePresence } from 'framer-motion'; import { @@ -9,7 +11,13 @@ import { LightBulbIcon } from '@heroicons/react/24/outline'; import { Button } from './Button'; -import { cn } from '@/lib/utils/helpers'; +import { clsx } from 'clsx'; +import { twMerge } from 'tailwind-merge'; + +// Utility function to merge Tailwind classes with clsx +function cn(...inputs: any[]) { + return twMerge(clsx(inputs)); +} export interface Tip { id: string; @@ -28,25 +36,54 @@ interface TipsCarouselProps { className?: string; } -export const TipsCarousel: React.FC = ({ +export interface TipsCarouselRef { + show: () => void; + hide: () => void; +} + +export const TipsCarousel = forwardRef(({ tips, storageKey = 'dashboard-tips-hidden', className -}) => { +}, ref) => { const [currentIndex, setCurrentIndex] = useState(0); const [isVisible, setIsVisible] = useState(true); // Check localStorage on mount useEffect(() => { - const isHidden = localStorage.getItem(storageKey) === 'true'; - setIsVisible(!isHidden); + try { + const isHidden = localStorage.getItem(storageKey) === 'true'; + setIsVisible(!isHidden); + } catch (e) { + // If localStorage is unavailable (e.g., private mode), default to showing tips + setIsVisible(true); + } }, [storageKey]); const handleHide = () => { - localStorage.setItem(storageKey, 'true'); + try { + localStorage.setItem(storageKey, 'true'); + } catch (e) { + // If localStorage is unavailable, continue with in-memory preference + } setIsVisible(false); }; + const handleShow = () => { + try { + localStorage.removeItem(storageKey); + } catch (e) { + // If localStorage is unavailable, continue with in-memory preference + } + setIsVisible(true); + }; + + // Expose methods for external control + useImperativeHandle(ref, () => ({ + show: handleShow, + hide: handleHide, + })); + const handleNext = () => { setCurrentIndex((prev) => (prev + 1) % tips.length); }; @@ -59,6 +96,29 @@ export const TipsCarousel: React.FC = ({ setCurrentIndex(index); }; + // Handle keyboard navigation + useEffect(() => { + const handleKeyDown = (event: KeyboardEvent) => { + if (!isVisible) return; + + switch (event.key) { + case 'ArrowLeft': + handlePrevious(); + break; + case 'ArrowRight': + handleNext(); + break; + default: + break; + } + }; + + document.addEventListener('keydown', handleKeyDown); + return () => { + document.removeEventListener('keydown', handleKeyDown); + }; + }, [isVisible, tips.length]); + if (!isVisible || tips.length === 0) { return null; } @@ -97,7 +157,7 @@ export const TipsCarousel: React.FC = ({