From 2a8ab9dacb3264ed63e9738fd2aed41b4bc012c5 Mon Sep 17 00:00:00 2001
From: Chris Estreich
Date: Fri, 16 May 2025 16:20:59 -0700
Subject: [PATCH] Analytics progress (#21)
---
.../dashboard/analytics/ActiveFilter.tsx | 33 +
.../dashboard/analytics/Analytics.tsx | 64 ++
.../dashboard/analytics/Developers.tsx | 112 +++
.../dashboard/analytics/Models.tsx | 103 ++
.../dashboard/analytics/TaskDetails.tsx | 97 ++
.../dashboard/analytics/Tasks.tsx | 298 ++++++
.../dashboard/analytics/ViewModeToggle.tsx | 33 +
.../dashboard/analytics/page.tsx | 18 +-
.../dashboard/analytics/types.ts | 56 ++
.../dashboard/audit-logs/page.tsx | 6 +-
src/components/analytics/AnalyticsPage.tsx | 903 ------------------
.../dashboard/UsageAnalyticsCard.tsx | 23 +-
12 files changed, 819 insertions(+), 927 deletions(-)
create mode 100644 src/app/(authenticated)/dashboard/analytics/ActiveFilter.tsx
create mode 100644 src/app/(authenticated)/dashboard/analytics/Analytics.tsx
create mode 100644 src/app/(authenticated)/dashboard/analytics/Developers.tsx
create mode 100644 src/app/(authenticated)/dashboard/analytics/Models.tsx
create mode 100644 src/app/(authenticated)/dashboard/analytics/TaskDetails.tsx
create mode 100644 src/app/(authenticated)/dashboard/analytics/Tasks.tsx
create mode 100644 src/app/(authenticated)/dashboard/analytics/ViewModeToggle.tsx
create mode 100644 src/app/(authenticated)/dashboard/analytics/types.ts
delete mode 100644 src/components/analytics/AnalyticsPage.tsx
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')}
+
+
+ )}
);
};