diff --git a/src/app/(authenticated)/dashboard/analytics/ActiveFilter.tsx b/src/app/(authenticated)/dashboard/analytics/ActiveFilter.tsx new file mode 100644 index 0000000000..6cdcb374ca --- /dev/null +++ b/src/app/(authenticated)/dashboard/analytics/ActiveFilter.tsx @@ -0,0 +1,33 @@ +import { X } from 'lucide-react'; + +import { Button } from '@/components/ui'; + +import type { Filter } from './types'; + +export const ActiveFilter = ({ + filter, + onClear, +}: { + filter: Filter; + onClear: () => void; +}) => { + if (!filter) { + return null; + } + + return ( +
+ + Filtered by {filter.type}: {filter.name} + + +
+ ); +}; diff --git a/src/app/(authenticated)/dashboard/analytics/Analytics.tsx b/src/app/(authenticated)/dashboard/analytics/Analytics.tsx new file mode 100644 index 0000000000..23078e364d --- /dev/null +++ b/src/app/(authenticated)/dashboard/analytics/Analytics.tsx @@ -0,0 +1,64 @@ +'use client'; + +import React, { useState } from 'react'; + +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui'; +import { UsageAnalyticsCard } from '@/components/dashboard'; + +import type { Developer, Filter, Model, Task, ViewMode } from './types'; +import { ViewModeToggle } from './ViewModeToggle'; +import { ActiveFilter } from './ActiveFilter'; +import { Developers } from './Developers'; +import { Models } from './Models'; +import { Tasks } from './Tasks'; +import { TaskDetails } from './TaskDetails'; + +export const Analytics = () => { + const [viewMode, setViewMode] = useState('tasks'); + const [filter, setFilter] = useState(null); + const [selectedTask, setSelectedTask] = useState(null); + + const onDeveloperClick = (developer: Developer) => { + setFilter({ type: 'developer', id: developer.id, name: developer.name }); + setViewMode('tasks'); + }; + + const onModelClick = (model: Model) => { + setFilter({ type: 'model', id: model.id, name: model.name }); + setViewMode('tasks'); + }; + + return ( +
+ + + {filter && ( + setFilter(null)} /> + )} + + + + {viewMode === 'developers' + ? 'Developers' + : viewMode === 'models' + ? 'Models' + : 'Tasks'} + + + + {viewMode === 'tasks' && ( + setSelectedTask(task)} + /> + )} + {viewMode === 'developers' && ( + + )} + {viewMode === 'models' && } + + + setSelectedTask(null)} /> +
+ ); +}; diff --git a/src/app/(authenticated)/dashboard/analytics/Developers.tsx b/src/app/(authenticated)/dashboard/analytics/Developers.tsx new file mode 100644 index 0000000000..9418357cca --- /dev/null +++ b/src/app/(authenticated)/dashboard/analytics/Developers.tsx @@ -0,0 +1,112 @@ +import type { ColumnDef } from '@tanstack/react-table'; + +import { DataTable } from '@/components/data-table'; + +import type { Developer } from './types'; + +const mockDevelopers: Developer[] = [ + { + id: 'dev1', + name: 'John Doe', + email: 'john@example.com', + tasksStarted: 42, + tasksCompleted: 38, + tokensConsumed: 1200000, + cost: 24.5, + }, + { + id: 'dev2', + name: 'Jane Smith', + email: 'jane@example.com', + tasksStarted: 35, + tasksCompleted: 32, + tokensConsumed: 980000, + cost: 19.6, + }, + { + id: 'dev3', + name: 'Bob Johnson', + email: 'bob@example.com', + tasksStarted: 28, + tasksCompleted: 25, + tokensConsumed: 750000, + cost: 15.0, + }, + { + id: 'dev4', + name: 'Alice Williams', + email: 'alice@example.com', + tasksStarted: 31, + tasksCompleted: 29, + tokensConsumed: 820000, + cost: 16.4, + }, + { + id: 'dev5', + name: 'Charlie Brown', + email: 'charlie@example.com', + tasksStarted: 22, + tasksCompleted: 19, + tokensConsumed: 650000, + cost: 13.0, + }, +]; + +export const Developers = ({ + onDeveloperClick, +}: { + onDeveloperClick: (developer: Developer) => void; +}) => { + const developerColumns: ColumnDef[] = [ + { + accessorKey: 'name', + header: 'Developer', + cell: ({ row }) => { + const developer = row.original; + + return ( + + ); + }, + }, + { + accessorKey: 'email', + header: 'Email', + }, + { + accessorKey: 'tasksStarted', + header: 'Tasks Started', + }, + { + accessorKey: 'tasksCompleted', + header: 'Tasks Completed', + }, + { + accessorKey: 'tokensConsumed', + header: 'Tokens', + cell: ({ row }) => { + const tokens = row.getValue('tokensConsumed') as number; + return tokens >= 1000000 + ? `${(tokens / 1000000).toFixed(1)}M` + : tokens >= 1000 + ? `${(tokens / 1000).toFixed(1)}K` + : tokens; + }, + }, + { + accessorKey: 'cost', + header: 'Cost (USD)', + cell: ({ row }) => { + const cost = row.getValue('cost') as number; + return `$${cost.toFixed(2)}`; + }, + }, + ]; + + return ; +}; diff --git a/src/app/(authenticated)/dashboard/analytics/Models.tsx b/src/app/(authenticated)/dashboard/analytics/Models.tsx new file mode 100644 index 0000000000..0126ac2234 --- /dev/null +++ b/src/app/(authenticated)/dashboard/analytics/Models.tsx @@ -0,0 +1,103 @@ +import type { ColumnDef } from '@tanstack/react-table'; + +import { DataTable } from '@/components/data-table'; + +import type { Model } from './types'; + +const mockModels: Model[] = [ + { + id: 'model1', + name: 'GPT-4', + provider: 'OpenAI', + tasks: 45, + tokensConsumed: 1500000, + cost: 30.0, + }, + { + id: 'model2', + name: 'Claude 3 Opus', + provider: 'Anthropic', + tasks: 35, + tokensConsumed: 1000000, + cost: 20.0, + }, + { + id: 'model3', + name: 'Mistral Large', + provider: 'Mistral AI', + tasks: 25, + tokensConsumed: 430000, + cost: 9.1, + }, + { + id: 'model4', + name: 'GPT-3.5 Turbo', + provider: 'OpenAI', + tasks: 38, + tokensConsumed: 850000, + cost: 8.5, + }, + { + id: 'model5', + name: 'Claude 3 Sonnet', + provider: 'Anthropic', + tasks: 30, + tokensConsumed: 720000, + cost: 14.4, + }, +]; + +export const Models = ({ + onModelClick, +}: { + onModelClick: (model: Model) => void; +}) => { + const modelColumns: ColumnDef[] = [ + { + accessorKey: 'name', + header: 'Model', + cell: ({ row }) => { + const model = row.original; + + return ( + + ); + }, + }, + { + accessorKey: 'provider', + header: 'Provider', + }, + { + accessorKey: 'tasks', + header: 'Tasks', + }, + { + accessorKey: 'tokensConsumed', + header: 'Tokens', + cell: ({ row }) => { + const tokens = row.getValue('tokensConsumed') as number; + return tokens >= 1000000 + ? `${(tokens / 1000000).toFixed(1)}M` + : tokens >= 1000 + ? `${(tokens / 1000).toFixed(1)}K` + : tokens; + }, + }, + { + accessorKey: 'cost', + header: 'Cost (USD)', + cell: ({ row }) => { + const cost = row.getValue('cost') as number; + return `$${cost.toFixed(2)}`; + }, + }, + ]; + + return ; +}; diff --git a/src/app/(authenticated)/dashboard/analytics/TaskDetails.tsx b/src/app/(authenticated)/dashboard/analytics/TaskDetails.tsx new file mode 100644 index 0000000000..436d59b50e --- /dev/null +++ b/src/app/(authenticated)/dashboard/analytics/TaskDetails.tsx @@ -0,0 +1,97 @@ +import { + Drawer, + DrawerContent, + DrawerHeader, + DrawerTitle, +} from '@/components/ui'; + +import type { Task } from './types'; + +export const TaskDetails = ({ + task, + onClose, +}: { + task: Task | null; + onClose: () => void; +}) => + task ? ( + + +
+ + Task Details + +
+
+
+ Task ID: + {task.id} +
+
+ Date: + + {task.date.toLocaleString()} + +
+
+ + Developer: + + {task.developerName} +
+
+ Model: + {task.modelName} +
+
+ Tokens: + + {task.tokensConsumed.toLocaleString()} + +
+
+ Cost: + ${task.cost.toFixed(2)} +
+
+ Status: + + {task.status.charAt(0).toUpperCase() + task.status.slice(1)} + +
+
+

Conversation

+
+ {task.conversation.map((message) => ( +
+
+ {message.role.charAt(0).toUpperCase() + + message.role.slice(1)}{' '} + •{message.timestamp.toLocaleTimeString()} +
+
{message.content}
+
+ ))} +
+
+
+
+
+ ) : null; diff --git a/src/app/(authenticated)/dashboard/analytics/Tasks.tsx b/src/app/(authenticated)/dashboard/analytics/Tasks.tsx new file mode 100644 index 0000000000..205773e94a --- /dev/null +++ b/src/app/(authenticated)/dashboard/analytics/Tasks.tsx @@ -0,0 +1,298 @@ +import { useMemo } from 'react'; +import type { ColumnDef } from '@tanstack/react-table'; + +import { DataTable } from '@/components/data-table'; + +import type { Filter, Task, ConversationMessage } from './types'; + +const generateMockConversation = (taskId: string): ConversationMessage[] => { + const baseConversations: Record = { + task1: [ + { + id: 'msg1', + role: 'user', + content: + 'Create a React component that displays a list of items with pagination.', + timestamp: new Date('2025-05-01T10:00:00'), + }, + { + id: 'msg2', + role: 'assistant', + content: + "I'll create a React component for displaying a paginated list. Here's how we can implement it...", + timestamp: new Date('2025-05-01T10:00:30'), + }, + { + id: 'msg3', + role: 'user', + content: 'Can you add sorting functionality as well?', + timestamp: new Date('2025-05-01T10:02:00'), + }, + { + id: 'msg4', + role: 'assistant', + content: + "Certainly! I'll add sorting functionality to the component. Here's the updated implementation...", + timestamp: new Date('2025-05-01T10:02:30'), + }, + ], + task2: [ + { + id: 'msg1', + role: 'user', + content: + 'Write a function to calculate the Fibonacci sequence recursively.', + timestamp: new Date('2025-05-02T11:30:00'), + }, + { + id: 'msg2', + role: 'assistant', + content: + "Here's a recursive function to calculate the Fibonacci sequence in JavaScript...", + timestamp: new Date('2025-05-02T11:30:30'), + }, + { + id: 'msg3', + role: 'user', + content: 'Can you optimize it to avoid redundant calculations?', + timestamp: new Date('2025-05-02T11:32:00'), + }, + { + id: 'msg4', + role: 'assistant', + content: + "Yes, we can optimize it using memoization to avoid redundant calculations. Here's the optimized version...", + timestamp: new Date('2025-05-02T11:32:30'), + }, + ], + }; + + if (baseConversations[taskId]) { + return baseConversations[taskId]; + } + + return [ + { + id: `${taskId}-msg1`, + role: 'user', + content: 'I need help with a coding task.', + timestamp: new Date(), + }, + { + id: `${taskId}-msg2`, + role: 'assistant', + content: + "I'd be happy to help! What kind of coding task are you working on?", + timestamp: new Date(Date.now() + 30000), + }, + { + id: `${taskId}-msg3`, + role: 'user', + content: "I'm trying to implement a feature in my application.", + timestamp: new Date(Date.now() + 60000), + }, + { + id: `${taskId}-msg4`, + role: 'assistant', + content: + 'I can help with that. Let me provide some guidance on implementing that feature...', + timestamp: new Date(Date.now() + 90000), + }, + ]; +}; + +const mockTasks: Task[] = [ + { + id: 'task1', + date: new Date('2025-05-01T10:00:00'), + developerId: 'dev1', + developerName: 'John Doe', + modelId: 'model1', + modelName: 'GPT-4', + tokensConsumed: 15000, + cost: 0.3, + status: 'completed', + conversation: generateMockConversation('task1'), + }, + { + id: 'task2', + date: new Date('2025-05-02T11:30:00'), + developerId: 'dev2', + developerName: 'Jane Smith', + modelId: 'model2', + modelName: 'Claude 3 Opus', + tokensConsumed: 12000, + cost: 0.24, + status: 'completed', + conversation: generateMockConversation('task2'), + }, + { + id: 'task3', + date: new Date('2025-05-03T14:15:00'), + developerId: 'dev3', + developerName: 'Bob Johnson', + modelId: 'model3', + modelName: 'Mistral Large', + tokensConsumed: 8000, + cost: 0.16, + status: 'completed', + conversation: generateMockConversation('task3'), + }, + { + id: 'task4', + date: new Date('2025-05-04T09:45:00'), + developerId: 'dev1', + developerName: 'John Doe', + modelId: 'model2', + modelName: 'Claude 3 Opus', + tokensConsumed: 10000, + cost: 0.2, + status: 'completed', + conversation: generateMockConversation('task4'), + }, + { + id: 'task5', + date: new Date('2025-05-05T16:30:00'), + developerId: 'dev2', + developerName: 'Jane Smith', + modelId: 'model1', + modelName: 'GPT-4', + tokensConsumed: 18000, + cost: 0.36, + status: 'started', + conversation: generateMockConversation('task5'), + }, + { + id: 'task6', + date: new Date('2025-05-06T13:20:00'), + developerId: 'dev4', + developerName: 'Alice Williams', + modelId: 'model4', + modelName: 'GPT-3.5 Turbo', + tokensConsumed: 7500, + cost: 0.08, + status: 'completed', + conversation: generateMockConversation('task6'), + }, + { + id: 'task7', + date: new Date('2025-05-07T10:10:00'), + developerId: 'dev5', + developerName: 'Charlie Brown', + modelId: 'model5', + modelName: 'Claude 3 Sonnet', + tokensConsumed: 9200, + cost: 0.18, + status: 'failed', + conversation: generateMockConversation('task7'), + }, + { + id: 'task8', + date: new Date('2025-05-07T15:45:00'), + developerId: 'dev3', + developerName: 'Bob Johnson', + modelId: 'model1', + modelName: 'GPT-4', + tokensConsumed: 14000, + cost: 0.28, + status: 'completed', + conversation: generateMockConversation('task8'), + }, +]; + +export const Tasks = ({ + filter, + onTaskClick, +}: { + filter: Filter; + onTaskClick: (task: Task) => void; +}) => { + const filteredTasks = useMemo(() => { + if (!filter) { + return mockTasks; + } + + return mockTasks.filter((task) => + filter.type === 'developer' + ? task.developerId === filter.id + : task.modelId === filter.id, + ); + }, [filter]); + + const taskColumns: ColumnDef[] = [ + { + accessorKey: 'id', + header: 'Task ID', + cell: ({ row }) => { + const task = row.original; + + return ( + + ); + }, + }, + { + accessorKey: 'date', + header: 'Date', + cell: ({ row }) => { + const date = row.getValue('date') as Date; + return date.toLocaleString(); + }, + }, + { + accessorKey: 'developerName', + header: 'Developer', + }, + { + accessorKey: 'modelName', + header: 'Model', + }, + { + accessorKey: 'tokensConsumed', + header: 'Tokens', + cell: ({ row }) => { + const tokens = row.getValue('tokensConsumed') as number; + return tokens >= 1000000 + ? `${(tokens / 1000000).toFixed(1)}M` + : tokens >= 1000 + ? `${(tokens / 1000).toFixed(1)}K` + : tokens; + }, + }, + { + accessorKey: 'cost', + header: 'Cost (USD)', + cell: ({ row }) => { + const cost = row.getValue('cost') as number; + return `$${cost.toFixed(2)}`; + }, + }, + { + accessorKey: 'status', + header: 'Status', + cell: ({ row }) => { + const status = row.getValue('status') as string; + return ( + + {status.charAt(0).toUpperCase() + status.slice(1)} + + ); + }, + }, + ]; + + return ; +}; diff --git a/src/app/(authenticated)/dashboard/analytics/ViewModeToggle.tsx b/src/app/(authenticated)/dashboard/analytics/ViewModeToggle.tsx new file mode 100644 index 0000000000..a8592b017d --- /dev/null +++ b/src/app/(authenticated)/dashboard/analytics/ViewModeToggle.tsx @@ -0,0 +1,33 @@ +import { useTranslations } from 'next-intl'; + +import { Button } from '@/components/ui'; + +import { type ViewMode, viewModes } from './types'; + +export const ViewModeToggle = ({ + viewMode, + setViewMode, +}: { + viewMode: ViewMode; + setViewMode: (mode: ViewMode) => void; +}) => { + const t = useTranslations('Analytics'); + + return ( +
+

{t('view_mode_title')}

+
+ {viewModes.map((mode) => ( + + ))} +
+
+ ); +}; diff --git a/src/app/(authenticated)/dashboard/analytics/page.tsx b/src/app/(authenticated)/dashboard/analytics/page.tsx index 530e36abc5..8ff84c2035 100644 --- a/src/app/(authenticated)/dashboard/analytics/page.tsx +++ b/src/app/(authenticated)/dashboard/analytics/page.tsx @@ -1,12 +1,12 @@ -'use client'; - -import { useTranslations } from 'next-intl'; +import { getLocale, getTranslations } from 'next-intl/server'; import { TitleBar } from '@/components/dashboard/TitleBar'; -import { AnalyticsPage } from '@/components/analytics/AnalyticsPage'; -const AnalyticsPageContainer = () => { - const t = useTranslations('Analytics'); +import { Analytics } from './Analytics'; + +export default async function Page() { + const locale = await getLocale(); + const t = await getTranslations({ locale, namespace: 'Analytics' }); return ( <> @@ -14,9 +14,7 @@ const AnalyticsPageContainer = () => { title={t('title_bar')} description={t('title_bar_description')} /> - + ); -}; - -export default AnalyticsPageContainer; +} diff --git a/src/app/(authenticated)/dashboard/analytics/types.ts b/src/app/(authenticated)/dashboard/analytics/types.ts new file mode 100644 index 0000000000..2ac2e7f2db --- /dev/null +++ b/src/app/(authenticated)/dashboard/analytics/types.ts @@ -0,0 +1,56 @@ +export const viewModes = ['developers', 'models', 'tasks'] as const; + +export type ViewMode = (typeof viewModes)[number]; + +export type Developer = { + id: string; + name: string; + email: string; + tasksStarted: number; + tasksCompleted: number; + tokensConsumed: number; + cost: number; +}; + +export type Model = { + id: string; + name: string; + provider: string; + tasks: number; + tokensConsumed: number; + cost: number; +}; + +export type ConversationMessage = { + id: string; + role: 'user' | 'assistant' | 'system'; + content: string; + timestamp: Date; +}; + +export type Task = { + id: string; + date: Date; + developerId: string; + developerName: string; + modelId: string; + modelName: string; + tokensConsumed: number; + cost: number; + status: 'completed' | 'started' | 'failed'; + conversation: ConversationMessage[]; +}; + +export type SummaryData = { + activeDevelopers: number; + tasksStarted: number; + tasksCompleted: number; + tokensConsumed: string; + costs: number; +}; + +export type Filter = { + type: 'developer' | 'model'; + id: string; + name: string; +} | null; diff --git a/src/app/(authenticated)/dashboard/audit-logs/page.tsx b/src/app/(authenticated)/dashboard/audit-logs/page.tsx index 8599f0e07b..ad7b6b71a7 100644 --- a/src/app/(authenticated)/dashboard/audit-logs/page.tsx +++ b/src/app/(authenticated)/dashboard/audit-logs/page.tsx @@ -19,7 +19,7 @@ import { getAuditLogs } from '@/actions/auditLogs'; type TimePeriod = '7' | '30' | '90'; -const AuditLogsPage = () => { +export default function Page() { const [timePeriod, setTimePeriod] = useState('7'); const [selectedLog, setSelectedLog] = useState(null); const { organization } = useOrganization(); @@ -100,6 +100,4 @@ const AuditLogsPage = () => { ); -}; - -export default AuditLogsPage; +} diff --git a/src/components/analytics/AnalyticsPage.tsx b/src/components/analytics/AnalyticsPage.tsx deleted file mode 100644 index a6bef8c96d..0000000000 --- a/src/components/analytics/AnalyticsPage.tsx +++ /dev/null @@ -1,903 +0,0 @@ -'use client'; - -import type { ColumnDef } from '@tanstack/react-table'; -import { X } from 'lucide-react'; -import { useTranslations } from 'next-intl'; -import React, { useMemo, useState } from 'react'; - -import { Button } from '@/components/ui'; -import { DataTable } from '@/components/data-table'; - -type TimePeriod = '7' | '30' | '90'; - -type ViewMode = 'developers' | 'models' | 'tasks'; - -type Developer = { - id: string; - name: string; - email: string; - tasksStarted: number; - tasksCompleted: number; - tokensConsumed: number; - cost: number; -}; - -type Model = { - id: string; - name: string; - provider: string; - tasks: number; - tokensConsumed: number; - cost: number; -}; - -type ConversationMessage = { - id: string; - role: 'user' | 'assistant' | 'system'; - content: string; - timestamp: Date; -}; - -type Task = { - id: string; - date: Date; - developerId: string; - developerName: string; - modelId: string; - modelName: string; - tokensConsumed: number; - cost: number; - status: 'completed' | 'started' | 'failed'; - conversation: ConversationMessage[]; -}; - -type SummaryData = { - activeDevelopers: number; - tasksStarted: number; - tasksCompleted: number; - tokensConsumed: string; - costs: number; -}; - -type Filter = { - type: 'developer' | 'model'; - id: string; - name: string; -} | null; - -const generateMockConversation = (taskId: string): ConversationMessage[] => { - const baseConversations: Record = { - task1: [ - { - id: 'msg1', - role: 'user', - content: - 'Create a React component that displays a list of items with pagination.', - timestamp: new Date('2025-05-01T10:00:00'), - }, - { - id: 'msg2', - role: 'assistant', - content: - "I'll create a React component for displaying a paginated list. Here's how we can implement it...", - timestamp: new Date('2025-05-01T10:00:30'), - }, - { - id: 'msg3', - role: 'user', - content: 'Can you add sorting functionality as well?', - timestamp: new Date('2025-05-01T10:02:00'), - }, - { - id: 'msg4', - role: 'assistant', - content: - "Certainly! I'll add sorting functionality to the component. Here's the updated implementation...", - timestamp: new Date('2025-05-01T10:02:30'), - }, - ], - task2: [ - { - id: 'msg1', - role: 'user', - content: - 'Write a function to calculate the Fibonacci sequence recursively.', - timestamp: new Date('2025-05-02T11:30:00'), - }, - { - id: 'msg2', - role: 'assistant', - content: - "Here's a recursive function to calculate the Fibonacci sequence in JavaScript...", - timestamp: new Date('2025-05-02T11:30:30'), - }, - { - id: 'msg3', - role: 'user', - content: 'Can you optimize it to avoid redundant calculations?', - timestamp: new Date('2025-05-02T11:32:00'), - }, - { - id: 'msg4', - role: 'assistant', - content: - "Yes, we can optimize it using memoization to avoid redundant calculations. Here's the optimized version...", - timestamp: new Date('2025-05-02T11:32:30'), - }, - ], - }; - - if (baseConversations[taskId]) { - return baseConversations[taskId]; - } - - return [ - { - id: `${taskId}-msg1`, - role: 'user', - content: 'I need help with a coding task.', - timestamp: new Date(), - }, - { - id: `${taskId}-msg2`, - role: 'assistant', - content: - "I'd be happy to help! What kind of coding task are you working on?", - timestamp: new Date(Date.now() + 30000), - }, - { - id: `${taskId}-msg3`, - role: 'user', - content: "I'm trying to implement a feature in my application.", - timestamp: new Date(Date.now() + 60000), - }, - { - id: `${taskId}-msg4`, - role: 'assistant', - content: - 'I can help with that. Let me provide some guidance on implementing that feature...', - timestamp: new Date(Date.now() + 90000), - }, - ]; -}; - -const mockDevelopers: Developer[] = [ - { - id: 'dev1', - name: 'John Doe', - email: 'john@example.com', - tasksStarted: 42, - tasksCompleted: 38, - tokensConsumed: 1200000, - cost: 24.5, - }, - { - id: 'dev2', - name: 'Jane Smith', - email: 'jane@example.com', - tasksStarted: 35, - tasksCompleted: 32, - tokensConsumed: 980000, - cost: 19.6, - }, - { - id: 'dev3', - name: 'Bob Johnson', - email: 'bob@example.com', - tasksStarted: 28, - tasksCompleted: 25, - tokensConsumed: 750000, - cost: 15.0, - }, - { - id: 'dev4', - name: 'Alice Williams', - email: 'alice@example.com', - tasksStarted: 31, - tasksCompleted: 29, - tokensConsumed: 820000, - cost: 16.4, - }, - { - id: 'dev5', - name: 'Charlie Brown', - email: 'charlie@example.com', - tasksStarted: 22, - tasksCompleted: 19, - tokensConsumed: 650000, - cost: 13.0, - }, -]; - -const mockModels: Model[] = [ - { - id: 'model1', - name: 'GPT-4', - provider: 'OpenAI', - tasks: 45, - tokensConsumed: 1500000, - cost: 30.0, - }, - { - id: 'model2', - name: 'Claude 3 Opus', - provider: 'Anthropic', - tasks: 35, - tokensConsumed: 1000000, - cost: 20.0, - }, - { - id: 'model3', - name: 'Mistral Large', - provider: 'Mistral AI', - tasks: 25, - tokensConsumed: 430000, - cost: 9.1, - }, - { - id: 'model4', - name: 'GPT-3.5 Turbo', - provider: 'OpenAI', - tasks: 38, - tokensConsumed: 850000, - cost: 8.5, - }, - { - id: 'model5', - name: 'Claude 3 Sonnet', - provider: 'Anthropic', - tasks: 30, - tokensConsumed: 720000, - cost: 14.4, - }, -]; - -const mockTasks: Task[] = [ - { - id: 'task1', - date: new Date('2025-05-01T10:00:00'), - developerId: 'dev1', - developerName: 'John Doe', - modelId: 'model1', - modelName: 'GPT-4', - tokensConsumed: 15000, - cost: 0.3, - status: 'completed', - conversation: generateMockConversation('task1'), - }, - { - id: 'task2', - date: new Date('2025-05-02T11:30:00'), - developerId: 'dev2', - developerName: 'Jane Smith', - modelId: 'model2', - modelName: 'Claude 3 Opus', - tokensConsumed: 12000, - cost: 0.24, - status: 'completed', - conversation: generateMockConversation('task2'), - }, - { - id: 'task3', - date: new Date('2025-05-03T14:15:00'), - developerId: 'dev3', - developerName: 'Bob Johnson', - modelId: 'model3', - modelName: 'Mistral Large', - tokensConsumed: 8000, - cost: 0.16, - status: 'completed', - conversation: generateMockConversation('task3'), - }, - { - id: 'task4', - date: new Date('2025-05-04T09:45:00'), - developerId: 'dev1', - developerName: 'John Doe', - modelId: 'model2', - modelName: 'Claude 3 Opus', - tokensConsumed: 10000, - cost: 0.2, - status: 'completed', - conversation: generateMockConversation('task4'), - }, - { - id: 'task5', - date: new Date('2025-05-05T16:30:00'), - developerId: 'dev2', - developerName: 'Jane Smith', - modelId: 'model1', - modelName: 'GPT-4', - tokensConsumed: 18000, - cost: 0.36, - status: 'started', - conversation: generateMockConversation('task5'), - }, - { - id: 'task6', - date: new Date('2025-05-06T13:20:00'), - developerId: 'dev4', - developerName: 'Alice Williams', - modelId: 'model4', - modelName: 'GPT-3.5 Turbo', - tokensConsumed: 7500, - cost: 0.08, - status: 'completed', - conversation: generateMockConversation('task6'), - }, - { - id: 'task7', - date: new Date('2025-05-07T10:10:00'), - developerId: 'dev5', - developerName: 'Charlie Brown', - modelId: 'model5', - modelName: 'Claude 3 Sonnet', - tokensConsumed: 9200, - cost: 0.18, - status: 'failed', - conversation: generateMockConversation('task7'), - }, - { - id: 'task8', - date: new Date('2025-05-07T15:45:00'), - developerId: 'dev3', - developerName: 'Bob Johnson', - modelId: 'model1', - modelName: 'GPT-4', - tokensConsumed: 14000, - cost: 0.28, - status: 'completed', - conversation: generateMockConversation('task8'), - }, -]; - -const SummaryMetrics = ({ - data, - timePeriod, - setTimePeriod, -}: { - data: SummaryData; - timePeriod: TimePeriod; - setTimePeriod: (period: TimePeriod) => void; -}) => { - const t = useTranslations('Analytics'); - - return ( -
-
-

{t('summary_title')}

-
- -
- - - -
- -
- {/* Active Developers */} -
-
- {t('active_developers')} -
-
- {data.activeDevelopers} -
-
- -
-
- {t('tasks_started')} -
-
{data.tasksStarted}
-
- -
-
- {t('tasks_completed')} -
-
- {data.tasksCompleted} -
-
- -
-
- {t('tokens_consumed')} -
-
- {data.tokensConsumed} -
-
- -
-
{t('llm_costs')}
-
- ${data.costs.toFixed(2)} -
-
-
-
- ); -}; - -const ViewModeToggle = ({ - viewMode, - setViewMode, -}: { - viewMode: ViewMode; - setViewMode: (mode: ViewMode) => void; -}) => { - const t = useTranslations('Analytics'); - - return ( -
-

{t('view_mode_title')}

-
- - - -
-
- ); -}; - -const ActiveFilter = ({ - filter, - onClear, -}: { - filter: Filter; - onClear: () => void; -}) => { - if (!filter) { - return null; - } - - return ( -
- - Filtered by {filter.type}: {filter.name} - - -
- ); -}; - -const TaskDetailsDrawer = ({ - task, - isOpen, - onClose, -}: { - task: Task | null; - isOpen: boolean; - onClose: () => void; -}) => { - if (!isOpen || !task) { - return null; - } - - return ( -
-
-
-

Task Details

- -
- -
-
- Task ID: - {task.id} -
-
- Date: - {task.date.toLocaleString()} -
-
- Developer: - {task.developerName} -
-
- Model: - {task.modelName} -
-
- Tokens: - - {task.tokensConsumed.toLocaleString()} - -
-
- Cost: - ${task.cost.toFixed(2)} -
-
- Status: - - {task.status.charAt(0).toUpperCase() + task.status.slice(1)} - -
-
- -

Conversation

-
- {task.conversation.map((message) => ( -
-
- {message.role.charAt(0).toUpperCase() + message.role.slice(1)} • - {message.timestamp.toLocaleTimeString()} -
-
{message.content}
-
- ))} -
-
-
- ); -}; - -export const AnalyticsPage = () => { - const [timePeriod, setTimePeriod] = useState('7'); - const [viewMode, setViewMode] = useState('tasks'); - const [filter, setFilter] = useState(null); - const [selectedTask, setSelectedTask] = useState(null); - const [isDrawerOpen, setIsDrawerOpen] = useState(false); - - const summaryData = useMemo(() => { - switch (timePeriod) { - case '7': - return { - activeDevelopers: 8, - tasksStarted: 42, - tasksCompleted: 38, - tokensConsumed: '1.2M', - costs: 24.5, - }; - case '30': - return { - activeDevelopers: 12, - tasksStarted: 187, - tasksCompleted: 165, - tokensConsumed: '5.8M', - costs: 112.75, - }; - case '90': - return { - activeDevelopers: 15, - tasksStarted: 563, - tasksCompleted: 498, - tokensConsumed: '18.3M', - costs: 347.2, - }; - } - }, [timePeriod]); - - const handleDeveloperClick = (developer: Developer) => { - setFilter({ type: 'developer', id: developer.id, name: developer.name }); - setViewMode('tasks'); - }; - - const handleModelClick = (model: Model) => { - setFilter({ type: 'model', id: model.id, name: model.name }); - setViewMode('tasks'); - }; - - const handleTaskClick = (task: Task) => { - setSelectedTask(task); - setIsDrawerOpen(true); - }; - - const clearFilter = () => { - setFilter(null); - }; - - const closeDrawer = () => { - setIsDrawerOpen(false); - setSelectedTask(null); - }; - - const DevelopersTable = () => { - const developerColumns: ColumnDef[] = [ - { - accessorKey: 'name', - header: 'Developer', - cell: ({ row }) => { - const developer = row.original; - return ( - - ); - }, - }, - { - accessorKey: 'email', - header: 'Email', - }, - { - accessorKey: 'tasksStarted', - header: 'Tasks Started', - }, - { - accessorKey: 'tasksCompleted', - header: 'Tasks Completed', - }, - { - accessorKey: 'tokensConsumed', - header: 'Tokens', - cell: ({ row }) => { - const tokens = row.getValue('tokensConsumed') as number; - return tokens >= 1000000 - ? `${(tokens / 1000000).toFixed(1)}M` - : tokens >= 1000 - ? `${(tokens / 1000).toFixed(1)}K` - : tokens; - }, - }, - { - accessorKey: 'cost', - header: 'Cost (USD)', - cell: ({ row }) => { - const cost = row.getValue('cost') as number; - return `$${cost.toFixed(2)}`; - }, - }, - ]; - - return ; - }; - - const ModelsTable = () => { - const modelColumns: ColumnDef[] = [ - { - accessorKey: 'name', - header: 'Model', - cell: ({ row }) => { - const model = row.original; - return ( - - ); - }, - }, - { - accessorKey: 'provider', - header: 'Provider', - }, - { - accessorKey: 'tasks', - header: 'Tasks', - }, - { - accessorKey: 'tokensConsumed', - header: 'Tokens', - cell: ({ row }) => { - const tokens = row.getValue('tokensConsumed') as number; - return tokens >= 1000000 - ? `${(tokens / 1000000).toFixed(1)}M` - : tokens >= 1000 - ? `${(tokens / 1000).toFixed(1)}K` - : tokens; - }, - }, - { - accessorKey: 'cost', - header: 'Cost (USD)', - cell: ({ row }) => { - const cost = row.getValue('cost') as number; - return `$${cost.toFixed(2)}`; - }, - }, - ]; - - return ; - }; - - const TasksTable = () => { - const filteredTasks = useMemo(() => { - if (!filter) { - return mockTasks; - } - - return mockTasks.filter((task) => - filter.type === 'developer' - ? task.developerId === filter.id - : task.modelId === filter.id, - ); - }, []); - - const taskColumns: ColumnDef[] = [ - { - accessorKey: 'id', - header: 'Task ID', - cell: ({ row }) => { - const task = row.original; - return ( - - ); - }, - }, - { - accessorKey: 'date', - header: 'Date', - cell: ({ row }) => { - const date = row.getValue('date') as Date; - return date.toLocaleString(); - }, - }, - { - accessorKey: 'developerName', - header: 'Developer', - }, - { - accessorKey: 'modelName', - header: 'Model', - }, - { - accessorKey: 'tokensConsumed', - header: 'Tokens', - cell: ({ row }) => { - const tokens = row.getValue('tokensConsumed') as number; - return tokens >= 1000000 - ? `${(tokens / 1000000).toFixed(1)}M` - : tokens >= 1000 - ? `${(tokens / 1000).toFixed(1)}K` - : tokens; - }, - }, - { - accessorKey: 'cost', - header: 'Cost (USD)', - cell: ({ row }) => { - const cost = row.getValue('cost') as number; - return `$${cost.toFixed(2)}`; - }, - }, - { - accessorKey: 'status', - header: 'Status', - cell: ({ row }) => { - const status = row.getValue('status') as string; - return ( - - {status.charAt(0).toUpperCase() + status.slice(1)} - - ); - }, - }, - ]; - - return ; - }; - - const renderTable = () => { - switch (viewMode) { - case 'developers': - return ; - case 'models': - return ; - case 'tasks': - return ; - } - }; - - return ( -
- - - - - {filter && } - -
-
-

- {viewMode === 'developers' - ? 'Developers' - : viewMode === 'models' - ? 'Models' - : 'Tasks'} -

-
- {renderTable()} -
- - -
- ); -}; diff --git a/src/components/dashboard/UsageAnalyticsCard.tsx b/src/components/dashboard/UsageAnalyticsCard.tsx index cf683269dc..a8d32b807e 100644 --- a/src/components/dashboard/UsageAnalyticsCard.tsx +++ b/src/components/dashboard/UsageAnalyticsCard.tsx @@ -1,6 +1,7 @@ 'use client'; import React, { useState } from 'react'; +import { usePathname } from 'next/navigation'; import Link from 'next/link'; import { useTranslations } from 'next-intl'; import { useAuth } from '@clerk/nextjs'; @@ -17,6 +18,8 @@ export const UsageAnalyticsCard = () => { const { orgId } = useAuth(); const [timePeriod, setTimePeriod] = useState(7); + const path = usePathname(); + const usage = useQuery({ queryKey: ['usage', orgId, timePeriod], queryFn: () => getUsage({ orgId, timePeriod }), @@ -30,7 +33,6 @@ export const UsageAnalyticsCard = () => { {t('analytics_description')}

-
{timePeriods.map((period) => ( ))}
-
@@ -89,14 +90,16 @@ export const UsageAnalyticsCard = () => {
-
- - {t('analytics_view_details')} - -
+ {path !== '/dashboard/analytics' && ( +
+ + {t('analytics_view_details')} + +
+ )} ); };