-
-
Save shricodev/934c3841101c073b50a5dad18746d78d to your computer and use it in GitHub Desktop.
Test 2: Tool Usage Analytics + Insights Dashboard - claude-opus-4.5
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| diff --git a/.claude/settings.local.json b/.claude/settings.local.json | |
| new file mode 100644 | |
| index 0000000..227cd31 | |
| --- /dev/null | |
| +++ b/.claude/settings.local.json | |
| @@ -0,0 +1,7 @@ | |
| +{ | |
| + "permissions": { | |
| + "allow": ["Bash(cat:*)", "Bash(tree:*)"], | |
| + "deny": [], | |
| + "ask": [] | |
| + } | |
| +} | |
| diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json | |
| index 98b0d5a..90b15d6 100644 | |
| --- a/public/locales/en/translation.json | |
| +++ b/public/locales/en/translation.json | |
| @@ -135,6 +135,7 @@ | |
| "buyMeACoffee": "Buy me a coffee", | |
| "hireMe": "Hire me", | |
| "home": "Home", | |
| + "insights": "Insights", | |
| "tools": "Tools" | |
| }, | |
| "number": { | |
| diff --git a/src/@types/analytics.ts b/src/@types/analytics.ts | |
| new file mode 100644 | |
| index 0000000..d7ae4f2 | |
| --- /dev/null | |
| +++ b/src/@types/analytics.ts | |
| @@ -0,0 +1,41 @@ | |
| +import { ToolCategory, UserType } from '@tools/defineTool'; | |
| + | |
| +export type ToolEventType = 'open' | 'execute'; | |
| + | |
| +export interface ToolUsageEvent { | |
| + toolPath: string; | |
| + toolName: string; | |
| + category: ToolCategory; | |
| + eventType: ToolEventType; | |
| + timestamp: number; | |
| + userType?: UserType; | |
| +} | |
| + | |
| +export interface ToolUsageStats { | |
| + toolPath: string; | |
| + toolName: string; | |
| + category: ToolCategory; | |
| + totalCount: number; | |
| + openCount: number; | |
| + executeCount: number; | |
| + lastUsed: number; | |
| + userTypes: UserType[]; | |
| +} | |
| + | |
| +export interface AnalyticsData { | |
| + events: ToolUsageEvent[]; | |
| + version: number; | |
| +} | |
| + | |
| +export type TimeRangeFilter = 'last7days' | 'allTime'; | |
| + | |
| +export interface AnalyticsFilters { | |
| + timeRange: TimeRangeFilter; | |
| + userTypes: UserType[]; | |
| +} | |
| + | |
| +export interface CategoryUsageStats { | |
| + category: ToolCategory; | |
| + totalCount: number; | |
| + toolCount: number; | |
| +} | |
| diff --git a/src/components/ActionPalette/ActionPalette.tsx b/src/components/ActionPalette/ActionPalette.tsx | |
| index eacdc72..07f3850 100644 | |
| --- a/src/components/ActionPalette/ActionPalette.tsx | |
| +++ b/src/components/ActionPalette/ActionPalette.tsx | |
| @@ -226,6 +226,20 @@ const ActionPalette: React.FC<ActionPaletteProps> = ({ | |
| } | |
| }); | |
| + actions.push({ | |
| + id: 'go-insights', | |
| + type: 'action', | |
| + label: 'View Insights', | |
| + description: 'See your tool usage analytics', | |
| + icon: 'mdi:chart-box', | |
| + category: 'Navigation', | |
| + keywords: ['insights', 'analytics', 'stats', 'usage', 'dashboard'], | |
| + action: () => { | |
| + navigate('/insights'); | |
| + onClose(); | |
| + } | |
| + }); | |
| + | |
| // Bookmarks action | |
| if (bookmarkedPaths.length > 0) { | |
| actions.push({ | |
| @@ -354,7 +368,11 @@ const ActionPalette: React.FC<ActionPaletteProps> = ({ | |
| } else if (action.type === 'action' && action.action) { | |
| action.action(); | |
| // Only close for navigation actions | |
| - if (action.id.startsWith('go-') || action.id === 'view-bookmarks') { | |
| + if ( | |
| + action.id.startsWith('go-') || | |
| + action.id === 'view-bookmarks' || | |
| + action.id === 'go-insights' | |
| + ) { | |
| onClose(); | |
| } | |
| } | |
| diff --git a/src/components/App.tsx b/src/components/App.tsx | |
| index 889442d..326d1d5 100644 | |
| --- a/src/components/App.tsx | |
| +++ b/src/components/App.tsx | |
| @@ -13,6 +13,7 @@ import ScrollToTopButton from './ScrollToTopButton'; | |
| import { I18nextProvider } from 'react-i18next'; | |
| import i18n from '../i18n'; | |
| import { UserTypeFilterProvider } from 'providers/UserTypeFilterProvider'; | |
| +import { AnalyticsProvider } from 'providers/AnalyticsProvider'; | |
| import ActionPalette from './ActionPalette'; | |
| export type Mode = 'dark' | 'light' | 'system'; | |
| @@ -80,18 +81,20 @@ function App() { | |
| > | |
| <CustomSnackBarProvider> | |
| <UserTypeFilterProvider> | |
| - <BrowserRouter> | |
| - <Navbar mode={mode} onChangeMode={handleChangeMode} /> | |
| - <Suspense fallback={<Loading />}> | |
| - <AppRoutes /> | |
| - </Suspense> | |
| - <ActionPalette | |
| - open={actionPaletteOpen} | |
| - onClose={() => setActionPaletteOpen(false)} | |
| - mode={mode} | |
| - onChangeMode={handleChangeMode} | |
| - /> | |
| - </BrowserRouter> | |
| + <AnalyticsProvider> | |
| + <BrowserRouter> | |
| + <Navbar mode={mode} onChangeMode={handleChangeMode} /> | |
| + <Suspense fallback={<Loading />}> | |
| + <AppRoutes /> | |
| + </Suspense> | |
| + <ActionPalette | |
| + open={actionPaletteOpen} | |
| + onClose={() => setActionPaletteOpen(false)} | |
| + mode={mode} | |
| + onChangeMode={handleChangeMode} | |
| + /> | |
| + </BrowserRouter> | |
| + </AnalyticsProvider> | |
| </UserTypeFilterProvider> | |
| </CustomSnackBarProvider> | |
| </SnackbarProvider> | |
| diff --git a/src/components/Navbar/index.tsx b/src/components/Navbar/index.tsx | |
| index dbacb3a..46636dd 100644 | |
| --- a/src/components/Navbar/index.tsx | |
| +++ b/src/components/Navbar/index.tsx | |
| @@ -61,8 +61,7 @@ const Navbar: React.FC<NavbarProps> = ({ | |
| }; | |
| const navItems: { label: string; path: string }[] = [ | |
| - // { label: 'Features', path: '/features' } | |
| - // { label: 'About Us', path: '/about-us' } | |
| + { label: t('navbar.insights'), path: '/insights' } | |
| ]; | |
| const languageSelector = ( | |
| diff --git a/src/components/ToolLayout.tsx b/src/components/ToolLayout.tsx | |
| index 40ad761..d6779d7 100644 | |
| --- a/src/components/ToolLayout.tsx | |
| +++ b/src/components/ToolLayout.tsx | |
| @@ -1,5 +1,5 @@ | |
| import { Box } from '@mui/material'; | |
| -import React, { ReactNode } from 'react'; | |
| +import React, { ReactNode, useEffect, useRef } from 'react'; | |
| import { Helmet } from 'react-helmet'; | |
| import ToolHeader from './ToolHeader'; | |
| import Separator from './Separator'; | |
| @@ -11,8 +11,9 @@ import { | |
| } from '../utils/string'; | |
| import { IconifyIcon } from '@iconify/react'; | |
| import { useTranslation } from 'react-i18next'; | |
| -import { ToolCategory } from '@tools/defineTool'; | |
| +import { ToolCategory, UserType } from '@tools/defineTool'; | |
| import { FullI18nKey } from '../i18n'; | |
| +import { useAnalytics } from '../providers/AnalyticsProvider'; | |
| export default function ToolLayout({ | |
| children, | |
| @@ -29,12 +30,15 @@ export default function ToolLayout({ | |
| name: FullI18nKey; | |
| description: FullI18nKey; | |
| shortDescription: FullI18nKey; | |
| + userTypes?: UserType[]; | |
| }; | |
| }) { | |
| const { t } = useTranslation([ | |
| 'translation', | |
| getI18nNamespaceFromToolCategory(type) | |
| ]); | |
| + const { trackToolOpen } = useAnalytics(); | |
| + const hasTrackedOpen = useRef(false); | |
| // Use i18n keys if available, otherwise fall back to provided strings | |
| //@ts-ignore | |
| @@ -42,6 +46,15 @@ export default function ToolLayout({ | |
| //@ts-ignore | |
| const toolDescription: string = t(i18n.description); | |
| + // Track tool open on mount | |
| + useEffect(() => { | |
| + if (!hasTrackedOpen.current && toolTitle) { | |
| + const userType = i18n?.userTypes?.[0]; | |
| + trackToolOpen(fullPath, toolTitle, type, userType); | |
| + hasTrackedOpen.current = true; | |
| + } | |
| + }, [fullPath, toolTitle, type, i18n?.userTypes, trackToolOpen]); | |
| + | |
| const otherCategoryTools = | |
| getToolsByCategory([], t) | |
| .find((category) => category.type === type) | |
| diff --git a/src/config/routesConfig.tsx b/src/config/routesConfig.tsx | |
| index 78f47e6..e9cdb7e 100644 | |
| --- a/src/config/routesConfig.tsx | |
| +++ b/src/config/routesConfig.tsx | |
| @@ -3,6 +3,7 @@ import { lazy } from 'react'; | |
| const Home = lazy(() => import('../pages/home')); | |
| const ToolsByCategory = lazy(() => import('../pages/tools-by-category')); | |
| +const Insights = lazy(() => import('../pages/insights')); | |
| const routes: RouteObject[] = [ | |
| { | |
| @@ -13,6 +14,10 @@ const routes: RouteObject[] = [ | |
| path: '/categories/:categoryName', | |
| element: <ToolsByCategory /> | |
| }, | |
| + { | |
| + path: '/insights', | |
| + element: <Insights /> | |
| + }, | |
| { | |
| path: '*', | |
| element: <Navigate to="404" /> | |
| diff --git a/src/hooks/index.ts b/src/hooks/index.ts | |
| index 1929925..ac7e1ee 100644 | |
| --- a/src/hooks/index.ts | |
| +++ b/src/hooks/index.ts | |
| @@ -2,3 +2,4 @@ export { default as useDebounce } from './useDebounce'; | |
| export { default as useTimeout } from './useTimeout'; | |
| export { default as usePrevious } from './usePrevious'; | |
| export { default as useUpdateEffect } from './useUpdateEffect'; | |
| +export { useToolExecution } from './useToolExecution'; | |
| diff --git a/src/hooks/useToolExecution.ts b/src/hooks/useToolExecution.ts | |
| new file mode 100644 | |
| index 0000000..2b321f2 | |
| --- /dev/null | |
| +++ b/src/hooks/useToolExecution.ts | |
| @@ -0,0 +1,47 @@ | |
| +import { useCallback, useRef } from 'react'; | |
| +import { useAnalytics } from '../providers/AnalyticsProvider'; | |
| +import { ToolCategory, UserType } from '@tools/defineTool'; | |
| + | |
| +interface UseToolExecutionProps { | |
| + toolPath: string; | |
| + toolName: string; | |
| + category: ToolCategory; | |
| + userType?: UserType; | |
| +} | |
| + | |
| +/** | |
| + * Hook for tracking tool execution events. | |
| + * Use this in individual tool components to track when the tool is executed. | |
| + * | |
| + * @example | |
| + * const { trackExecution } = useToolExecution({ | |
| + * toolPath: 'string/reverse', | |
| + * toolName: 'Reverse String', | |
| + * category: 'string' | |
| + * }); | |
| + * | |
| + * const handleCompute = () => { | |
| + * // ... your compute logic | |
| + * trackExecution(); | |
| + * }; | |
| + */ | |
| +export function useToolExecution({ | |
| + toolPath, | |
| + toolName, | |
| + category, | |
| + userType | |
| +}: UseToolExecutionProps) { | |
| + const { trackToolExecute } = useAnalytics(); | |
| + const lastExecutionTime = useRef<number>(0); | |
| + | |
| + // Debounce execution tracking to prevent spam (min 1 second between tracks) | |
| + const trackExecution = useCallback(() => { | |
| + const now = Date.now(); | |
| + if (now - lastExecutionTime.current > 1000) { | |
| + trackToolExecute(toolPath, toolName, category, userType); | |
| + lastExecutionTime.current = now; | |
| + } | |
| + }, [toolPath, toolName, category, userType, trackToolExecute]); | |
| + | |
| + return { trackExecution }; | |
| +} | |
| diff --git a/src/pages/insights/components/CategoryUsage.tsx b/src/pages/insights/components/CategoryUsage.tsx | |
| new file mode 100644 | |
| index 0000000..4510be6 | |
| --- /dev/null | |
| +++ b/src/pages/insights/components/CategoryUsage.tsx | |
| @@ -0,0 +1,114 @@ | |
| +import React from 'react'; | |
| +import { Box, Typography, Grid, Paper, LinearProgress } from '@mui/material'; | |
| +import { Icon } from '@iconify/react'; | |
| +import { useNavigate } from 'react-router-dom'; | |
| +import { CategoryUsageStats } from '../../../@types/analytics'; | |
| +import { ToolCategory } from '@tools/defineTool'; | |
| +import InsightCard from './InsightCard'; | |
| + | |
| +interface CategoryUsageProps { | |
| + categories: CategoryUsageStats[]; | |
| +} | |
| + | |
| +const categoryIcons: Record<ToolCategory, string> = { | |
| + string: 'solar:text-bold-duotone', | |
| + 'image-generic': 'material-symbols-light:image-outline-rounded', | |
| + png: 'ph:file-png-thin', | |
| + number: 'lsicon:number-filled', | |
| + gif: 'material-symbols-light:gif-rounded', | |
| + list: 'solar:list-bold-duotone', | |
| + json: 'lets-icons:json-light', | |
| + time: 'mdi:clock-time-five', | |
| + csv: 'material-symbols-light:csv-outline', | |
| + video: 'lets-icons:video-light', | |
| + pdf: 'tabler:pdf', | |
| + audio: 'ic:twotone-audiotrack', | |
| + xml: 'mdi-light:xml', | |
| + converters: 'streamline-plump:convert-pdf-1' | |
| +}; | |
| + | |
| +export default function CategoryUsage({ categories }: CategoryUsageProps) { | |
| + const navigate = useNavigate(); | |
| + const maxCount = categories.length > 0 ? categories[0].totalCount : 0; | |
| + | |
| + return ( | |
| + <InsightCard | |
| + title="Usage by Category" | |
| + icon="mdi:folder-multiple" | |
| + isEmpty={categories.length === 0} | |
| + emptyMessage="No category usage data" | |
| + > | |
| + <Grid container spacing={2}> | |
| + {categories.map((category) => { | |
| + const percentage = | |
| + maxCount > 0 ? (category.totalCount / maxCount) * 100 : 0; | |
| + const icon = categoryIcons[category.category] || 'mdi:folder'; | |
| + | |
| + return ( | |
| + <Grid item xs={12} sm={6} key={category.category}> | |
| + <Paper | |
| + elevation={0} | |
| + sx={{ | |
| + p: 2, | |
| + borderRadius: 1, | |
| + border: 1, | |
| + borderColor: 'divider', | |
| + cursor: 'pointer', | |
| + transition: 'all 0.2s', | |
| + '&:hover': { | |
| + borderColor: 'primary.main', | |
| + backgroundColor: 'action.hover' | |
| + } | |
| + }} | |
| + onClick={() => navigate(`/categories/${category.category}`)} | |
| + > | |
| + <Box | |
| + sx={{ | |
| + display: 'flex', | |
| + alignItems: 'center', | |
| + gap: 1, | |
| + mb: 1 | |
| + }} | |
| + > | |
| + <Icon icon={icon} fontSize={20} /> | |
| + <Typography | |
| + variant="body2" | |
| + fontWeight={600} | |
| + sx={{ textTransform: 'capitalize' }} | |
| + > | |
| + {category.category.replace('-', ' ')} | |
| + </Typography> | |
| + </Box> | |
| + <Box | |
| + sx={{ | |
| + display: 'flex', | |
| + justifyContent: 'space-between', | |
| + alignItems: 'center', | |
| + mb: 1 | |
| + }} | |
| + > | |
| + <Typography variant="caption" color="text.secondary"> | |
| + {category.toolCount} tool | |
| + {category.toolCount !== 1 ? 's' : ''} used | |
| + </Typography> | |
| + <Typography variant="body2" fontWeight={500} color="primary"> | |
| + {category.totalCount} | |
| + </Typography> | |
| + </Box> | |
| + <LinearProgress | |
| + variant="determinate" | |
| + value={percentage} | |
| + sx={{ | |
| + height: 4, | |
| + borderRadius: 1, | |
| + backgroundColor: 'action.hover' | |
| + }} | |
| + /> | |
| + </Paper> | |
| + </Grid> | |
| + ); | |
| + })} | |
| + </Grid> | |
| + </InsightCard> | |
| + ); | |
| +} | |
| diff --git a/src/pages/insights/components/InsightCard.tsx b/src/pages/insights/components/InsightCard.tsx | |
| new file mode 100644 | |
| index 0000000..25195a5 | |
| --- /dev/null | |
| +++ b/src/pages/insights/components/InsightCard.tsx | |
| @@ -0,0 +1,64 @@ | |
| +import React, { ReactNode } from 'react'; | |
| +import { Box, Typography, Paper } from '@mui/material'; | |
| +import { Icon } from '@iconify/react'; | |
| + | |
| +interface InsightCardProps { | |
| + title: string; | |
| + icon: string; | |
| + children: ReactNode; | |
| + emptyMessage?: string; | |
| + isEmpty?: boolean; | |
| +} | |
| + | |
| +export default function InsightCard({ | |
| + title, | |
| + icon, | |
| + children, | |
| + emptyMessage = 'No data available', | |
| + isEmpty = false | |
| +}: InsightCardProps) { | |
| + return ( | |
| + <Paper | |
| + elevation={0} | |
| + sx={{ | |
| + p: 3, | |
| + borderRadius: 2, | |
| + border: 1, | |
| + borderColor: 'divider', | |
| + height: '100%', | |
| + display: 'flex', | |
| + flexDirection: 'column' | |
| + }} | |
| + > | |
| + <Box | |
| + sx={{ | |
| + display: 'flex', | |
| + alignItems: 'center', | |
| + gap: 1, | |
| + mb: 2 | |
| + }} | |
| + > | |
| + <Icon icon={icon} fontSize={24} /> | |
| + <Typography variant="h6" fontWeight={600}> | |
| + {title} | |
| + </Typography> | |
| + </Box> | |
| + {isEmpty ? ( | |
| + <Box | |
| + sx={{ | |
| + flex: 1, | |
| + display: 'flex', | |
| + alignItems: 'center', | |
| + justifyContent: 'center', | |
| + color: 'text.secondary', | |
| + minHeight: 120 | |
| + }} | |
| + > | |
| + <Typography variant="body2">{emptyMessage}</Typography> | |
| + </Box> | |
| + ) : ( | |
| + <Box sx={{ flex: 1 }}>{children}</Box> | |
| + )} | |
| + </Paper> | |
| + ); | |
| +} | |
| diff --git a/src/pages/insights/components/InsightsFilters.tsx b/src/pages/insights/components/InsightsFilters.tsx | |
| new file mode 100644 | |
| index 0000000..8315678 | |
| --- /dev/null | |
| +++ b/src/pages/insights/components/InsightsFilters.tsx | |
| @@ -0,0 +1,109 @@ | |
| +import React from 'react'; | |
| +import { | |
| + Box, | |
| + ToggleButton, | |
| + ToggleButtonGroup, | |
| + Typography, | |
| + Stack, | |
| + Paper | |
| +} from '@mui/material'; | |
| +import { Icon } from '@iconify/react'; | |
| +import { AnalyticsFilters, TimeRangeFilter } from '../../../@types/analytics'; | |
| +import { UserType } from '@tools/defineTool'; | |
| + | |
| +interface InsightsFiltersProps { | |
| + filters: AnalyticsFilters; | |
| + onFiltersChange: (filters: AnalyticsFilters) => void; | |
| +} | |
| + | |
| +export default function InsightsFilters({ | |
| + filters, | |
| + onFiltersChange | |
| +}: InsightsFiltersProps) { | |
| + const handleTimeRangeChange = ( | |
| + _: React.MouseEvent<HTMLElement>, | |
| + newValue: TimeRangeFilter | null | |
| + ) => { | |
| + if (newValue) { | |
| + onFiltersChange({ ...filters, timeRange: newValue }); | |
| + } | |
| + }; | |
| + | |
| + const handleUserTypesChange = ( | |
| + _: React.MouseEvent<HTMLElement>, | |
| + newValue: UserType[] | |
| + ) => { | |
| + onFiltersChange({ ...filters, userTypes: newValue }); | |
| + }; | |
| + | |
| + return ( | |
| + <Paper | |
| + elevation={0} | |
| + sx={{ | |
| + p: 2, | |
| + borderRadius: 2, | |
| + border: 1, | |
| + borderColor: 'divider', | |
| + mb: 3 | |
| + }} | |
| + > | |
| + <Stack | |
| + direction={{ xs: 'column', sm: 'row' }} | |
| + spacing={3} | |
| + alignItems={{ xs: 'stretch', sm: 'center' }} | |
| + justifyContent="space-between" | |
| + > | |
| + <Stack direction="row" spacing={3} alignItems="center" flexWrap="wrap"> | |
| + <Box> | |
| + <Typography | |
| + variant="caption" | |
| + color="text.secondary" | |
| + sx={{ mb: 0.5, display: 'block' }} | |
| + > | |
| + Time Range | |
| + </Typography> | |
| + <ToggleButtonGroup | |
| + value={filters.timeRange} | |
| + exclusive | |
| + onChange={handleTimeRangeChange} | |
| + size="small" | |
| + > | |
| + <ToggleButton value="last7days"> | |
| + <Icon icon="mdi:calendar-week" style={{ marginRight: 4 }} /> | |
| + Last 7 Days | |
| + </ToggleButton> | |
| + <ToggleButton value="allTime"> | |
| + <Icon icon="mdi:calendar-all" style={{ marginRight: 4 }} /> | |
| + All Time | |
| + </ToggleButton> | |
| + </ToggleButtonGroup> | |
| + </Box> | |
| + | |
| + <Box> | |
| + <Typography | |
| + variant="caption" | |
| + color="text.secondary" | |
| + sx={{ mb: 0.5, display: 'block' }} | |
| + > | |
| + User Type | |
| + </Typography> | |
| + <ToggleButtonGroup | |
| + value={filters.userTypes} | |
| + onChange={handleUserTypesChange} | |
| + size="small" | |
| + > | |
| + <ToggleButton value="generalUsers"> | |
| + <Icon icon="mdi:account" style={{ marginRight: 4 }} /> | |
| + General | |
| + </ToggleButton> | |
| + <ToggleButton value="developers"> | |
| + <Icon icon="mdi:code-braces" style={{ marginRight: 4 }} /> | |
| + Developer | |
| + </ToggleButton> | |
| + </ToggleButtonGroup> | |
| + </Box> | |
| + </Stack> | |
| + </Stack> | |
| + </Paper> | |
| + ); | |
| +} | |
| diff --git a/src/pages/insights/components/MostUsedTools.tsx b/src/pages/insights/components/MostUsedTools.tsx | |
| new file mode 100644 | |
| index 0000000..cedd992 | |
| --- /dev/null | |
| +++ b/src/pages/insights/components/MostUsedTools.tsx | |
| @@ -0,0 +1,107 @@ | |
| +import React from 'react'; | |
| +import { | |
| + Box, | |
| + Typography, | |
| + List, | |
| + ListItem, | |
| + ListItemIcon, | |
| + ListItemText, | |
| + LinearProgress, | |
| + Chip, | |
| + Stack | |
| +} from '@mui/material'; | |
| +import { Icon } from '@iconify/react'; | |
| +import { useNavigate } from 'react-router-dom'; | |
| +import { ToolUsageStats } from '../../../@types/analytics'; | |
| +import InsightCard from './InsightCard'; | |
| + | |
| +interface MostUsedToolsProps { | |
| + tools: ToolUsageStats[]; | |
| +} | |
| + | |
| +export default function MostUsedTools({ tools }: MostUsedToolsProps) { | |
| + const navigate = useNavigate(); | |
| + const maxCount = tools.length > 0 ? tools[0].totalCount : 0; | |
| + | |
| + return ( | |
| + <InsightCard | |
| + title="Most Used Tools" | |
| + icon="mdi:chart-bar" | |
| + isEmpty={tools.length === 0} | |
| + emptyMessage="No tool usage recorded yet" | |
| + > | |
| + <List sx={{ py: 0 }}> | |
| + {tools.map((tool, index) => { | |
| + const percentage = | |
| + maxCount > 0 ? (tool.totalCount / maxCount) * 100 : 0; | |
| + return ( | |
| + <ListItem | |
| + key={tool.toolPath} | |
| + sx={{ | |
| + px: 0, | |
| + cursor: 'pointer', | |
| + borderRadius: 1, | |
| + '&:hover': { | |
| + backgroundColor: 'action.hover' | |
| + } | |
| + }} | |
| + onClick={() => navigate('/' + tool.toolPath)} | |
| + > | |
| + <ListItemIcon sx={{ minWidth: 32 }}> | |
| + <Typography | |
| + variant="body2" | |
| + color="text.secondary" | |
| + fontWeight={500} | |
| + > | |
| + {index + 1} | |
| + </Typography> | |
| + </ListItemIcon> | |
| + <ListItemText | |
| + primary={ | |
| + <Stack spacing={1}> | |
| + <Box | |
| + sx={{ | |
| + display: 'flex', | |
| + justifyContent: 'space-between', | |
| + alignItems: 'center' | |
| + }} | |
| + > | |
| + <Typography variant="body2" fontWeight={500}> | |
| + {tool.toolName} | |
| + </Typography> | |
| + <Chip | |
| + label={tool.totalCount} | |
| + size="small" | |
| + color="primary" | |
| + variant="outlined" | |
| + sx={{ height: 20, fontSize: '0.7rem' }} | |
| + /> | |
| + </Box> | |
| + <LinearProgress | |
| + variant="determinate" | |
| + value={percentage} | |
| + sx={{ | |
| + height: 6, | |
| + borderRadius: 1, | |
| + backgroundColor: 'action.hover' | |
| + }} | |
| + /> | |
| + </Stack> | |
| + } | |
| + secondary={ | |
| + <Typography | |
| + variant="caption" | |
| + color="text.secondary" | |
| + sx={{ mt: 0.5, display: 'block' }} | |
| + > | |
| + {tool.category} | |
| + </Typography> | |
| + } | |
| + /> | |
| + </ListItem> | |
| + ); | |
| + })} | |
| + </List> | |
| + </InsightCard> | |
| + ); | |
| +} | |
| diff --git a/src/pages/insights/components/RecentlyUsedTools.tsx b/src/pages/insights/components/RecentlyUsedTools.tsx | |
| new file mode 100644 | |
| index 0000000..71d2332 | |
| --- /dev/null | |
| +++ b/src/pages/insights/components/RecentlyUsedTools.tsx | |
| @@ -0,0 +1,97 @@ | |
| +import React from 'react'; | |
| +import { | |
| + Box, | |
| + Typography, | |
| + List, | |
| + ListItem, | |
| + ListItemText, | |
| + Chip | |
| +} from '@mui/material'; | |
| +import { Icon } from '@iconify/react'; | |
| +import { useNavigate } from 'react-router-dom'; | |
| +import { ToolUsageStats } from '../../../@types/analytics'; | |
| +import InsightCard from './InsightCard'; | |
| + | |
| +interface RecentlyUsedToolsProps { | |
| + tools: ToolUsageStats[]; | |
| +} | |
| + | |
| +function formatTimeAgo(timestamp: number): string { | |
| + const seconds = Math.floor((Date.now() - timestamp) / 1000); | |
| + | |
| + if (seconds < 60) return 'Just now'; | |
| + if (seconds < 3600) return `${Math.floor(seconds / 60)}m ago`; | |
| + if (seconds < 86400) return `${Math.floor(seconds / 3600)}h ago`; | |
| + if (seconds < 604800) return `${Math.floor(seconds / 86400)}d ago`; | |
| + return new Date(timestamp).toLocaleDateString(); | |
| +} | |
| + | |
| +export default function RecentlyUsedTools({ tools }: RecentlyUsedToolsProps) { | |
| + const navigate = useNavigate(); | |
| + | |
| + return ( | |
| + <InsightCard | |
| + title="Recently Used" | |
| + icon="mdi:history" | |
| + isEmpty={tools.length === 0} | |
| + emptyMessage="No recent tool usage" | |
| + > | |
| + <List sx={{ py: 0 }}> | |
| + {tools.map((tool) => ( | |
| + <ListItem | |
| + key={tool.toolPath} | |
| + sx={{ | |
| + px: 1, | |
| + py: 1, | |
| + cursor: 'pointer', | |
| + borderRadius: 1, | |
| + '&:hover': { | |
| + backgroundColor: 'action.hover' | |
| + } | |
| + }} | |
| + onClick={() => navigate('/' + tool.toolPath)} | |
| + > | |
| + <ListItemText | |
| + primary={ | |
| + <Box | |
| + sx={{ | |
| + display: 'flex', | |
| + justifyContent: 'space-between', | |
| + alignItems: 'center' | |
| + }} | |
| + > | |
| + <Typography variant="body2" fontWeight={500}> | |
| + {tool.toolName} | |
| + </Typography> | |
| + <Typography variant="caption" color="text.secondary"> | |
| + {formatTimeAgo(tool.lastUsed)} | |
| + </Typography> | |
| + </Box> | |
| + } | |
| + secondary={ | |
| + <Box | |
| + sx={{ | |
| + display: 'flex', | |
| + justifyContent: 'space-between', | |
| + alignItems: 'center', | |
| + mt: 0.5 | |
| + }} | |
| + > | |
| + <Typography variant="caption" color="text.secondary"> | |
| + {tool.category} | |
| + </Typography> | |
| + <Chip | |
| + label={`${tool.totalCount} uses`} | |
| + size="small" | |
| + variant="outlined" | |
| + sx={{ height: 18, fontSize: '0.65rem' }} | |
| + /> | |
| + </Box> | |
| + } | |
| + /> | |
| + </ListItem> | |
| + ))} | |
| + </List> | |
| + </InsightCard> | |
| + ); | |
| +} | |
| diff --git a/src/pages/insights/components/StatsOverview.tsx b/src/pages/insights/components/StatsOverview.tsx | |
| new file mode 100644 | |
| index 0000000..b2c8590 | |
| --- /dev/null | |
| +++ b/src/pages/insights/components/StatsOverview.tsx | |
| @@ -0,0 +1,94 @@ | |
| +import React from 'react'; | |
| +import { Box, Typography, Paper, Grid, Stack } from '@mui/material'; | |
| +import { Icon } from '@iconify/react'; | |
| + | |
| +interface StatCardProps { | |
| + label: string; | |
| + value: number | string; | |
| + icon: string; | |
| + color?: string; | |
| +} | |
| + | |
| +function StatCard({ | |
| + label, | |
| + value, | |
| + icon, | |
| + color = 'primary.main' | |
| +}: StatCardProps) { | |
| + return ( | |
| + <Paper | |
| + elevation={0} | |
| + sx={{ | |
| + p: 2.5, | |
| + borderRadius: 2, | |
| + border: 1, | |
| + borderColor: 'divider', | |
| + height: '100%' | |
| + }} | |
| + > | |
| + <Stack direction="row" spacing={2} alignItems="center"> | |
| + <Box | |
| + sx={{ | |
| + p: 1.5, | |
| + borderRadius: 2, | |
| + backgroundColor: `${color}15`, | |
| + display: 'flex', | |
| + alignItems: 'center', | |
| + justifyContent: 'center' | |
| + }} | |
| + > | |
| + <Icon icon={icon} fontSize={28} color={color} /> | |
| + </Box> | |
| + <Box> | |
| + <Typography variant="h4" fontWeight={700}> | |
| + {value} | |
| + </Typography> | |
| + <Typography variant="body2" color="text.secondary"> | |
| + {label} | |
| + </Typography> | |
| + </Box> | |
| + </Stack> | |
| + </Paper> | |
| + ); | |
| +} | |
| + | |
| +interface StatsOverviewProps { | |
| + totalEvents: number; | |
| + uniqueTools: number; | |
| + categoryCount: number; | |
| +} | |
| + | |
| +export default function StatsOverview({ | |
| + totalEvents, | |
| + uniqueTools, | |
| + categoryCount | |
| +}: StatsOverviewProps) { | |
| + return ( | |
| + <Grid container spacing={2} sx={{ mb: 3 }}> | |
| + <Grid item xs={12} sm={4}> | |
| + <StatCard | |
| + label="Total Usage Events" | |
| + value={totalEvents} | |
| + icon="mdi:chart-line" | |
| + color="#1976d2" | |
| + /> | |
| + </Grid> | |
| + <Grid item xs={12} sm={4}> | |
| + <StatCard | |
| + label="Tools Used" | |
| + value={uniqueTools} | |
| + icon="mdi:tools" | |
| + color="#2e7d32" | |
| + /> | |
| + </Grid> | |
| + <Grid item xs={12} sm={4}> | |
| + <StatCard | |
| + label="Categories" | |
| + value={categoryCount} | |
| + icon="mdi:folder-multiple" | |
| + color="#ed6c02" | |
| + /> | |
| + </Grid> | |
| + </Grid> | |
| + ); | |
| +} | |
| diff --git a/src/pages/insights/index.tsx b/src/pages/insights/index.tsx | |
| new file mode 100644 | |
| index 0000000..7f8da15 | |
| --- /dev/null | |
| +++ b/src/pages/insights/index.tsx | |
| @@ -0,0 +1,123 @@ | |
| +import React from 'react'; | |
| +import { Box, Typography, Grid, Button, Stack } from '@mui/material'; | |
| +import { Helmet } from 'react-helmet'; | |
| +import { Icon } from '@iconify/react'; | |
| +import { useAnalytics } from '../../providers/AnalyticsProvider'; | |
| +import InsightsFilters from './components/InsightsFilters'; | |
| +import StatsOverview from './components/StatsOverview'; | |
| +import MostUsedTools from './components/MostUsedTools'; | |
| +import RecentlyUsedTools from './components/RecentlyUsedTools'; | |
| +import CategoryUsage from './components/CategoryUsage'; | |
| + | |
| +export default function Insights() { | |
| + const { | |
| + filters, | |
| + setFilters, | |
| + mostUsedTools, | |
| + recentlyUsedTools, | |
| + usageByCategory, | |
| + totalEventCount, | |
| + uniqueToolCount, | |
| + clearAnalytics | |
| + } = useAnalytics(); | |
| + | |
| + const hasData = totalEventCount > 0; | |
| + | |
| + return ( | |
| + <Box | |
| + sx={{ | |
| + width: '100%', | |
| + minHeight: '100vh', | |
| + backgroundColor: 'background.default', | |
| + py: 4 | |
| + }} | |
| + > | |
| + <Helmet> | |
| + <title>Insights - OmniTools</title> | |
| + </Helmet> | |
| + <Box | |
| + sx={{ | |
| + maxWidth: 1200, | |
| + mx: 'auto', | |
| + px: { xs: 2, md: 4 } | |
| + }} | |
| + > | |
| + <Stack | |
| + direction={{ xs: 'column', sm: 'row' }} | |
| + justifyContent="space-between" | |
| + alignItems={{ xs: 'flex-start', sm: 'center' }} | |
| + sx={{ mb: 3 }} | |
| + > | |
| + <Box> | |
| + <Typography variant="h4" fontWeight={700} gutterBottom> | |
| + Insights | |
| + </Typography> | |
| + <Typography variant="body1" color="text.secondary"> | |
| + Track and analyze your tool usage patterns | |
| + </Typography> | |
| + </Box> | |
| + {hasData && ( | |
| + <Button | |
| + variant="outlined" | |
| + color="error" | |
| + size="small" | |
| + startIcon={<Icon icon="mdi:delete-outline" />} | |
| + onClick={clearAnalytics} | |
| + sx={{ mt: { xs: 2, sm: 0 } }} | |
| + > | |
| + Clear Data | |
| + </Button> | |
| + )} | |
| + </Stack> | |
| + | |
| + <InsightsFilters filters={filters} onFiltersChange={setFilters} /> | |
| + | |
| + <StatsOverview | |
| + totalEvents={totalEventCount} | |
| + uniqueTools={uniqueToolCount} | |
| + categoryCount={usageByCategory.length} | |
| + /> | |
| + | |
| + {!hasData ? ( | |
| + <Box | |
| + sx={{ | |
| + textAlign: 'center', | |
| + py: 8, | |
| + px: 2, | |
| + border: 1, | |
| + borderColor: 'divider', | |
| + borderRadius: 2, | |
| + backgroundColor: 'background.paper' | |
| + }} | |
| + > | |
| + <Icon | |
| + icon="mdi:chart-box-outline" | |
| + fontSize={64} | |
| + style={{ color: '#9e9e9e', marginBottom: 16 }} | |
| + /> | |
| + <Typography variant="h6" gutterBottom> | |
| + No Usage Data Yet | |
| + </Typography> | |
| + <Typography variant="body2" color="text.secondary"> | |
| + Start using tools to see your analytics here. | |
| + <br /> | |
| + Usage data is tracked when you open and execute tools. | |
| + </Typography> | |
| + </Box> | |
| + ) : ( | |
| + <Grid container spacing={3}> | |
| + <Grid item xs={12} md={6}> | |
| + <MostUsedTools tools={mostUsedTools} /> | |
| + </Grid> | |
| + <Grid item xs={12} md={6}> | |
| + <RecentlyUsedTools tools={recentlyUsedTools} /> | |
| + </Grid> | |
| + <Grid item xs={12}> | |
| + <CategoryUsage categories={usageByCategory} /> | |
| + </Grid> | |
| + </Grid> | |
| + )} | |
| + </Box> | |
| + </Box> | |
| + ); | |
| +} | |
| diff --git a/src/providers/AnalyticsProvider.tsx b/src/providers/AnalyticsProvider.tsx | |
| new file mode 100644 | |
| index 0000000..8b376eb | |
| --- /dev/null | |
| +++ b/src/providers/AnalyticsProvider.tsx | |
| @@ -0,0 +1,183 @@ | |
| +import React, { | |
| + createContext, | |
| + useContext, | |
| + useState, | |
| + useCallback, | |
| + useMemo, | |
| + ReactNode | |
| +} from 'react'; | |
| +import { ToolCategory, UserType } from '@tools/defineTool'; | |
| +import { | |
| + ToolUsageEvent, | |
| + ToolUsageStats, | |
| + ToolEventType, | |
| + AnalyticsFilters, | |
| + CategoryUsageStats | |
| +} from '../@types/analytics'; | |
| +import { | |
| + getAnalyticsData, | |
| + recordToolEvent, | |
| + filterEvents, | |
| + getMostUsedTools, | |
| + getRecentlyUsedTools, | |
| + getUsageByCategory, | |
| + getTotalEventCount, | |
| + getUniqueToolCount, | |
| + clearAnalyticsData | |
| +} from '../utils/analytics'; | |
| + | |
| +interface AnalyticsContextType { | |
| + // Actions | |
| + trackToolOpen: ( | |
| + toolPath: string, | |
| + toolName: string, | |
| + category: ToolCategory, | |
| + userType?: UserType | |
| + ) => void; | |
| + trackToolExecute: ( | |
| + toolPath: string, | |
| + toolName: string, | |
| + category: ToolCategory, | |
| + userType?: UserType | |
| + ) => void; | |
| + clearAnalytics: () => void; | |
| + refreshAnalytics: () => void; | |
| + | |
| + // Filters | |
| + filters: AnalyticsFilters; | |
| + setFilters: (filters: AnalyticsFilters) => void; | |
| + | |
| + // Computed data | |
| + filteredEvents: ToolUsageEvent[]; | |
| + mostUsedTools: ToolUsageStats[]; | |
| + recentlyUsedTools: ToolUsageStats[]; | |
| + usageByCategory: CategoryUsageStats[]; | |
| + totalEventCount: number; | |
| + uniqueToolCount: number; | |
| +} | |
| + | |
| +const AnalyticsContext = createContext<AnalyticsContextType | null>(null); | |
| + | |
| +interface AnalyticsProviderProps { | |
| + children: ReactNode; | |
| +} | |
| + | |
| +export function AnalyticsProvider({ children }: AnalyticsProviderProps) { | |
| + const [events, setEvents] = useState<ToolUsageEvent[]>(() => { | |
| + return getAnalyticsData().events; | |
| + }); | |
| + | |
| + const [filters, setFilters] = useState<AnalyticsFilters>({ | |
| + timeRange: 'allTime', | |
| + userTypes: [] | |
| + }); | |
| + | |
| + const refreshAnalytics = useCallback(() => { | |
| + setEvents(getAnalyticsData().events); | |
| + }, []); | |
| + | |
| + const trackToolOpen = useCallback( | |
| + ( | |
| + toolPath: string, | |
| + toolName: string, | |
| + category: ToolCategory, | |
| + userType?: UserType | |
| + ) => { | |
| + recordToolEvent(toolPath, toolName, category, 'open', userType); | |
| + refreshAnalytics(); | |
| + }, | |
| + [refreshAnalytics] | |
| + ); | |
| + | |
| + const trackToolExecute = useCallback( | |
| + ( | |
| + toolPath: string, | |
| + toolName: string, | |
| + category: ToolCategory, | |
| + userType?: UserType | |
| + ) => { | |
| + recordToolEvent(toolPath, toolName, category, 'execute', userType); | |
| + refreshAnalytics(); | |
| + }, | |
| + [refreshAnalytics] | |
| + ); | |
| + | |
| + const clearAnalytics = useCallback(() => { | |
| + clearAnalyticsData(); | |
| + setEvents([]); | |
| + }, []); | |
| + | |
| + // Memoized computed data | |
| + const filteredEvents = useMemo(() => { | |
| + return filterEvents(events, filters); | |
| + }, [events, filters]); | |
| + | |
| + const mostUsedTools = useMemo(() => { | |
| + return getMostUsedTools(filteredEvents, 10); | |
| + }, [filteredEvents]); | |
| + | |
| + const recentlyUsedTools = useMemo(() => { | |
| + return getRecentlyUsedTools(filteredEvents, 10); | |
| + }, [filteredEvents]); | |
| + | |
| + const usageByCategory = useMemo(() => { | |
| + return getUsageByCategory(filteredEvents); | |
| + }, [filteredEvents]); | |
| + | |
| + const totalEventCount = useMemo(() => { | |
| + return getTotalEventCount(filteredEvents); | |
| + }, [filteredEvents]); | |
| + | |
| + const uniqueToolCount = useMemo(() => { | |
| + return getUniqueToolCount(filteredEvents); | |
| + }, [filteredEvents]); | |
| + | |
| + const contextValue = useMemo( | |
| + () => ({ | |
| + trackToolOpen, | |
| + trackToolExecute, | |
| + clearAnalytics, | |
| + refreshAnalytics, | |
| + filters, | |
| + setFilters, | |
| + filteredEvents, | |
| + mostUsedTools, | |
| + recentlyUsedTools, | |
| + usageByCategory, | |
| + totalEventCount, | |
| + uniqueToolCount | |
| + }), | |
| + [ | |
| + trackToolOpen, | |
| + trackToolExecute, | |
| + clearAnalytics, | |
| + refreshAnalytics, | |
| + filters, | |
| + filteredEvents, | |
| + mostUsedTools, | |
| + recentlyUsedTools, | |
| + usageByCategory, | |
| + totalEventCount, | |
| + uniqueToolCount | |
| + ] | |
| + ); | |
| + | |
| + return ( | |
| + <AnalyticsContext.Provider value={contextValue}> | |
| + {children} | |
| + </AnalyticsContext.Provider> | |
| + ); | |
| +} | |
| + | |
| +export function useAnalytics(): AnalyticsContextType { | |
| + const context = useContext(AnalyticsContext); | |
| + | |
| + if (!context) { | |
| + throw new Error( | |
| + 'useAnalytics must be used within an AnalyticsProvider. ' + | |
| + 'Make sure your component is wrapped with <AnalyticsProvider>.' | |
| + ); | |
| + } | |
| + | |
| + return context; | |
| +} | |
| diff --git a/src/utils/analytics.ts b/src/utils/analytics.ts | |
| new file mode 100644 | |
| index 0000000..d4de54c | |
| --- /dev/null | |
| +++ b/src/utils/analytics.ts | |
| @@ -0,0 +1,234 @@ | |
| +import { | |
| + AnalyticsData, | |
| + ToolUsageEvent, | |
| + ToolUsageStats, | |
| + ToolEventType, | |
| + TimeRangeFilter, | |
| + CategoryUsageStats, | |
| + AnalyticsFilters | |
| +} from '../@types/analytics'; | |
| +import { ToolCategory, UserType } from '@tools/defineTool'; | |
| + | |
| +const ANALYTICS_STORAGE_KEY = 'toolAnalytics'; | |
| +const ANALYTICS_VERSION = 1; | |
| + | |
| +// 7 days in milliseconds | |
| +const SEVEN_DAYS_MS = 7 * 24 * 60 * 60 * 1000; | |
| + | |
| +/** | |
| + * Get analytics data from localStorage | |
| + */ | |
| +export function getAnalyticsData(): AnalyticsData { | |
| + try { | |
| + const stored = localStorage.getItem(ANALYTICS_STORAGE_KEY); | |
| + if (stored) { | |
| + const data = JSON.parse(stored) as AnalyticsData; | |
| + // Handle version migrations if needed | |
| + if (data.version === ANALYTICS_VERSION) { | |
| + return data; | |
| + } | |
| + } | |
| + } catch (error) { | |
| + console.error('Error loading analytics data:', error); | |
| + } | |
| + return { events: [], version: ANALYTICS_VERSION }; | |
| +} | |
| + | |
| +/** | |
| + * Save analytics data to localStorage | |
| + */ | |
| +export function saveAnalyticsData(data: AnalyticsData): void { | |
| + try { | |
| + localStorage.setItem(ANALYTICS_STORAGE_KEY, JSON.stringify(data)); | |
| + } catch (error) { | |
| + console.error('Error saving analytics data:', error); | |
| + } | |
| +} | |
| + | |
| +/** | |
| + * Record a tool usage event | |
| + */ | |
| +export function recordToolEvent( | |
| + toolPath: string, | |
| + toolName: string, | |
| + category: ToolCategory, | |
| + eventType: ToolEventType, | |
| + userType?: UserType | |
| +): ToolUsageEvent { | |
| + const event: ToolUsageEvent = { | |
| + toolPath, | |
| + toolName, | |
| + category, | |
| + eventType, | |
| + timestamp: Date.now(), | |
| + userType | |
| + }; | |
| + | |
| + const data = getAnalyticsData(); | |
| + data.events.push(event); | |
| + | |
| + // Keep only last 1000 events to prevent localStorage from getting too large | |
| + if (data.events.length > 1000) { | |
| + data.events = data.events.slice(-1000); | |
| + } | |
| + | |
| + saveAnalyticsData(data); | |
| + return event; | |
| +} | |
| + | |
| +/** | |
| + * Filter events by time range | |
| + */ | |
| +export function filterEventsByTimeRange( | |
| + events: ToolUsageEvent[], | |
| + timeRange: TimeRangeFilter | |
| +): ToolUsageEvent[] { | |
| + if (timeRange === 'allTime') { | |
| + return events; | |
| + } | |
| + | |
| + const cutoffTime = Date.now() - SEVEN_DAYS_MS; | |
| + return events.filter((event) => event.timestamp >= cutoffTime); | |
| +} | |
| + | |
| +/** | |
| + * Filter events by user types | |
| + */ | |
| +export function filterEventsByUserTypes( | |
| + events: ToolUsageEvent[], | |
| + userTypes: UserType[] | |
| +): ToolUsageEvent[] { | |
| + if (userTypes.length === 0) { | |
| + return events; | |
| + } | |
| + return events.filter( | |
| + (event) => event.userType && userTypes.includes(event.userType) | |
| + ); | |
| +} | |
| + | |
| +/** | |
| + * Apply all filters to events | |
| + */ | |
| +export function filterEvents( | |
| + events: ToolUsageEvent[], | |
| + filters: AnalyticsFilters | |
| +): ToolUsageEvent[] { | |
| + let filtered = filterEventsByTimeRange(events, filters.timeRange); | |
| + filtered = filterEventsByUserTypes(filtered, filters.userTypes); | |
| + return filtered; | |
| +} | |
| + | |
| +/** | |
| + * Calculate usage statistics for each tool | |
| + */ | |
| +export function calculateToolStats(events: ToolUsageEvent[]): ToolUsageStats[] { | |
| + const statsMap = new Map<string, ToolUsageStats>(); | |
| + | |
| + events.forEach((event) => { | |
| + const existing = statsMap.get(event.toolPath); | |
| + if (existing) { | |
| + existing.totalCount++; | |
| + if (event.eventType === 'open') { | |
| + existing.openCount++; | |
| + } else { | |
| + existing.executeCount++; | |
| + } | |
| + if (event.timestamp > existing.lastUsed) { | |
| + existing.lastUsed = event.timestamp; | |
| + } | |
| + if (event.userType && !existing.userTypes.includes(event.userType)) { | |
| + existing.userTypes.push(event.userType); | |
| + } | |
| + } else { | |
| + statsMap.set(event.toolPath, { | |
| + toolPath: event.toolPath, | |
| + toolName: event.toolName, | |
| + category: event.category, | |
| + totalCount: 1, | |
| + openCount: event.eventType === 'open' ? 1 : 0, | |
| + executeCount: event.eventType === 'execute' ? 1 : 0, | |
| + lastUsed: event.timestamp, | |
| + userTypes: event.userType ? [event.userType] : [] | |
| + }); | |
| + } | |
| + }); | |
| + | |
| + return Array.from(statsMap.values()); | |
| +} | |
| + | |
| +/** | |
| + * Get most used tools sorted by total count | |
| + */ | |
| +export function getMostUsedTools( | |
| + events: ToolUsageEvent[], | |
| + limit: number = 10 | |
| +): ToolUsageStats[] { | |
| + const stats = calculateToolStats(events); | |
| + return stats.sort((a, b) => b.totalCount - a.totalCount).slice(0, limit); | |
| +} | |
| + | |
| +/** | |
| + * Get recently used tools sorted by last used timestamp | |
| + */ | |
| +export function getRecentlyUsedTools( | |
| + events: ToolUsageEvent[], | |
| + limit: number = 10 | |
| +): ToolUsageStats[] { | |
| + const stats = calculateToolStats(events); | |
| + return stats.sort((a, b) => b.lastUsed - a.lastUsed).slice(0, limit); | |
| +} | |
| + | |
| +/** | |
| + * Get usage statistics grouped by category | |
| + */ | |
| +export function getUsageByCategory( | |
| + events: ToolUsageEvent[] | |
| +): CategoryUsageStats[] { | |
| + const categoryMap = new Map< | |
| + ToolCategory, | |
| + { count: number; tools: Set<string> } | |
| + >(); | |
| + | |
| + events.forEach((event) => { | |
| + const existing = categoryMap.get(event.category); | |
| + if (existing) { | |
| + existing.count++; | |
| + existing.tools.add(event.toolPath); | |
| + } else { | |
| + categoryMap.set(event.category, { | |
| + count: 1, | |
| + tools: new Set([event.toolPath]) | |
| + }); | |
| + } | |
| + }); | |
| + | |
| + return Array.from(categoryMap.entries()) | |
| + .map(([category, data]) => ({ | |
| + category, | |
| + totalCount: data.count, | |
| + toolCount: data.tools.size | |
| + })) | |
| + .sort((a, b) => b.totalCount - a.totalCount); | |
| +} | |
| + | |
| +/** | |
| + * Get total event count | |
| + */ | |
| +export function getTotalEventCount(events: ToolUsageEvent[]): number { | |
| + return events.length; | |
| +} | |
| + | |
| +/** | |
| + * Get unique tool count | |
| + */ | |
| +export function getUniqueToolCount(events: ToolUsageEvent[]): number { | |
| + const uniqueTools = new Set(events.map((e) => e.toolPath)); | |
| + return uniqueTools.size; | |
| +} | |
| + | |
| +/** | |
| + * Clear all analytics data | |
| + */ | |
| +export function clearAnalyticsData(): void { | |
| + localStorage.removeItem(ANALYTICS_STORAGE_KEY); | |
| +} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment