From 5fd53e9ee239768fca551edfdca5bc7cb5a16bd5 Mon Sep 17 00:00:00 2001 From: Chris Estreich Date: Fri, 16 May 2025 09:54:07 -0700 Subject: [PATCH] React Query + Updated @shadcn/ui components (#17) --- package.json | 4 + pnpm-lock.yaml | 87 ++++++++ src/actions/analytics.ts | 65 ++++-- .../(authenticated)/dashboard/Dashboard.tsx | 9 +- .../dashboard/audit-logs/page.tsx | 33 ++- src/app/(authenticated)/dashboard/page.tsx | 8 +- src/app/api/events/route.ts | 4 +- src/app/layout.tsx | 17 +- src/components/analytics/AnalyticsPage.tsx | 50 +---- src/components/dashboard/AuditLogCard.tsx | 37 ++-- .../dashboard/DefaultParametersPage.tsx | 21 +- .../dashboard/ProviderWhitelistPage.tsx | 11 +- .../dashboard/UsageAnalyticsCard.tsx | 98 +++------ .../data-table.tsx => data-table/index.tsx} | 2 +- src/components/layout/ReactQueryProvider.tsx | 27 +++ src/components/layout/index.ts | 1 + src/components/ui/button.tsx | 2 +- src/components/ui/card.tsx | 2 +- src/components/ui/checkbox.tsx | 44 ++-- src/components/ui/drawer.tsx | 201 ++++++++++-------- src/components/ui/drawerVariants.ts | 46 ---- src/components/ui/dropdown-menu.tsx | 12 +- src/components/ui/form.tsx | 2 +- src/components/ui/index.ts | 9 + src/components/ui/slider.tsx | 75 +++++-- src/components/ui/sonner.tsx | 25 +++ src/components/ui/toast-context.tsx | 65 ------ src/components/ui/toast.tsx | 129 ----------- src/components/ui/toaster.tsx | 28 --- src/lib/server/analytics.ts | 4 +- src/schemas/index.ts | 73 ++++--- 31 files changed, 537 insertions(+), 654 deletions(-) rename src/components/{ui/data-table.tsx => data-table/index.tsx} (98%) create mode 100644 src/components/layout/ReactQueryProvider.tsx delete mode 100644 src/components/ui/drawerVariants.ts create mode 100644 src/components/ui/sonner.tsx delete mode 100644 src/components/ui/toast-context.tsx delete mode 100644 src/components/ui/toast.tsx delete mode 100644 src/components/ui/toaster.tsx diff --git a/package.json b/package.json index e5642b0ed3..6456f6a3f7 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "@logtail/pino": "^0.5.2", "@radix-ui/react-accordion": "^1.2.1", "@radix-ui/react-checkbox": "^1.3.1", + "@radix-ui/react-dialog": "^1.1.13", "@radix-ui/react-dropdown-menu": "^2.1.14", "@radix-ui/react-icons": "^1.3.2", "@radix-ui/react-label": "^2.1.0", @@ -44,6 +45,7 @@ "@spotlightjs/spotlight": "^2.5.0", "@t3-oss/env-nextjs": "^0.13.4", "@tailwindcss/postcss": "^4.1.6", + "@tanstack/react-query": "^5.76.1", "@tanstack/react-table": "^8.20.5", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -59,9 +61,11 @@ "react": "^19.1.0", "react-dom": "^19.1.0", "react-hook-form": "^7.53.0", + "sonner": "^2.0.3", "stripe": "^18.1.0", "tailwind-merge": "^3.3.0", "uuid": "^11.1.0", + "vaul": "^1.1.2", "zod": "^3.23.8" }, "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8b7848b89b..21a20ff2df 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -32,6 +32,9 @@ importers: '@radix-ui/react-checkbox': specifier: ^1.3.1 version: 1.3.1(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-dialog': + specifier: ^1.1.13 + version: 1.1.13(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@radix-ui/react-dropdown-menu': specifier: ^2.1.14 version: 2.1.14(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -68,6 +71,9 @@ importers: '@tailwindcss/postcss': specifier: ^4.1.6 version: 4.1.6 + '@tanstack/react-query': + specifier: ^5.76.1 + version: 5.76.1(react@19.1.0) '@tanstack/react-table': specifier: ^8.20.5 version: 8.21.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -113,6 +119,9 @@ importers: react-hook-form: specifier: ^7.53.0 version: 7.56.3(react@19.1.0) + sonner: + specifier: ^2.0.3 + version: 2.0.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0) stripe: specifier: ^18.1.0 version: 18.1.0(@types/node@22.15.17) @@ -122,6 +131,9 @@ importers: uuid: specifier: ^11.1.0 version: 11.1.0 + vaul: + specifier: ^1.1.2 + version: 1.1.2(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) zod: specifier: ^3.23.8 version: 3.24.4 @@ -2499,6 +2511,19 @@ packages: '@types/react': optional: true + '@radix-ui/react-dialog@1.1.13': + resolution: {integrity: sha512-ARFmqUyhIVS3+riWzwGTe7JLjqwqgnODBUZdqpWar/z1WFs9z76fuOs/2BOWCR+YboRn4/WN9aoaGVwqNRr8VA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-direction@1.1.1': resolution: {integrity: sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==} peerDependencies: @@ -3609,6 +3634,14 @@ packages: '@tailwindcss/postcss@4.1.6': resolution: {integrity: sha512-ELq+gDMBuRXPJlpE3PEen+1MhnHAQQrh2zF0dI1NXOlEWfr2qWf2CQdr5jl9yANv8RErQaQ2l6nIFO9OSCVq/g==} + '@tanstack/query-core@5.76.0': + resolution: {integrity: sha512-FN375hb8ctzfNAlex5gHI6+WDXTNpe0nbxp/d2YJtnP+IBM6OUm7zcaoCW6T63BawGOYZBbKC0iPvr41TteNVg==} + + '@tanstack/react-query@5.76.1': + resolution: {integrity: sha512-YxdLZVGN4QkT5YT1HKZQWiIlcgauIXEIsMOTSjvyD5wLYK8YVvKZUPAysMqossFJJfDpJW3pFn7WNZuPOqq+fw==} + peerDependencies: + react: ^18 || ^19 + '@tanstack/react-table@8.21.3': resolution: {integrity: sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww==} engines: {node: '>=12'} @@ -7773,6 +7806,12 @@ packages: sonic-boom@4.2.0: resolution: {integrity: sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==} + sonner@2.0.3: + resolution: {integrity: sha512-njQ4Hht92m0sMqqHVDL32V2Oun9W1+PHO9NDv9FHfJjT3JT22IG4Jpo3FPQy+mouRKCXFWO+r67v6MrHX2zeIA==} + peerDependencies: + react: ^18.0.0 || ^19.0.0 || ^19.0.0-rc + react-dom: ^18.0.0 || ^19.0.0 || ^19.0.0-rc + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} @@ -8443,6 +8482,12 @@ packages: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} + vaul@1.1.2: + resolution: {integrity: sha512-ZFkClGpWyI2WUQjdLJ/BaGuV6AVQiJ3uELGk3OYtP+B6yCO7Cmn9vPFXVJkRaGkOJu3m8bQMgtyzNHixULceQA==} + peerDependencies: + react: ^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc + vite-node@3.1.3: resolution: {integrity: sha512-uHV4plJ2IxCl4u1up1FQRrqclylKAogbtBfOTwcuJ28xFi+89PZ57BRh+naIRvH70HPwxy5QHYzg1OrEaC7AbA==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} @@ -11297,6 +11342,27 @@ snapshots: optionalDependencies: '@types/react': 19.1.3 + '@radix-ui/react-dialog@1.1.13(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/primitive': 1.1.2 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.3)(react@19.1.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.1.3)(react@19.1.0) + '@radix-ui/react-dismissable-layer': 1.1.9(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-focus-guards': 1.1.2(@types/react@19.1.3)(react@19.1.0) + '@radix-ui/react-focus-scope': 1.1.6(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-id': 1.1.1(@types/react@19.1.3)(react@19.1.0) + '@radix-ui/react-portal': 1.1.8(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-presence': 1.1.4(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-primitive': 2.1.2(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-slot': 1.2.2(@types/react@19.1.3)(react@19.1.0) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.3)(react@19.1.0) + aria-hidden: 1.2.4 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + react-remove-scroll: 2.6.3(@types/react@19.1.3)(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.3 + '@radix-ui/react-direction@1.1.1(@types/react@19.1.3)(react@19.1.0)': dependencies: react: 19.1.0 @@ -12523,6 +12589,13 @@ snapshots: postcss: 8.5.3 tailwindcss: 4.1.6 + '@tanstack/query-core@5.76.0': {} + + '@tanstack/react-query@5.76.1(react@19.1.0)': + dependencies: + '@tanstack/query-core': 5.76.0 + react: 19.1.0 + '@tanstack/react-table@8.21.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: '@tanstack/table-core': 8.21.3 @@ -17433,6 +17506,11 @@ snapshots: dependencies: atomic-sleep: 1.0.0 + sonner@2.0.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0): + dependencies: + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + source-map-js@1.2.1: {} source-map-support@0.5.13: @@ -18096,6 +18174,15 @@ snapshots: vary@1.1.2: {} + vaul@1.1.2(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0): + dependencies: + '@radix-ui/react-dialog': 1.1.13(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + transitivePeerDependencies: + - '@types/react' + - '@types/react-dom' + vite-node@3.1.3(@types/node@22.15.17)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.39.0)(tsx@4.19.4)(yaml@2.7.1): dependencies: cac: 6.7.14 diff --git a/src/actions/analytics.ts b/src/actions/analytics.ts index b776372ab6..a72fed41ac 100644 --- a/src/actions/analytics.ts +++ b/src/actions/analytics.ts @@ -1,31 +1,68 @@ +'use server'; + import { z } from 'zod'; +import { TelemetryEventName, type TimePeriod } from '@/schemas'; import { client } from '@/lib/server/analytics'; -const usageSchema = z.array( - z.object({ - userId: z.string(), - count: z.coerce.number(), - }), -); +/** + * Usage + */ + +const usageSchema = z.object({ + type: z.nativeEnum(TelemetryEventName), + userCount: z.coerce.number(), + eventCount: z.coerce.number(), + inputTokens: z.coerce.number(), + outputTokens: z.coerce.number(), + cost: z.number(), +}); export type Usage = z.infer; -export const getUsage = async (orgId?: string | null): Promise => { +/** + * getUsage + */ + +type UsageRecord = Partial>; + +export const getUsage = async ({ + orgId, + timePeriod, +}: { + orgId?: string | null; + timePeriod: TimePeriod; +}): Promise => { if (!orgId) { - return []; + return {}; } const resultSet = await client.query({ query: ` - SELECT userId, COUNT(1) as count - FROM "events" - WHERE orgId = {orgId: String} - GROUP BY 1; + SELECT + type, + COUNT(distinct userId) as userCount, + COUNT(1) as eventCount, + SUM(COALESCE(inputTokens, 0)) AS inputTokens, + SUM(COALESCE(outputTokens, 0)) AS outputTokens, + SUM(COALESCE(cost, 0)) AS cost + FROM + events + WHERE + orgId = {orgId: String} + AND timestamp >= toUnixTimestamp(now() - INTERVAL {timePeriod: Int32} DAY) + GROUP BY + type `, format: 'JSONEachRow', - query_params: { orgId }, + query_params: { orgId, timePeriod }, }); - return usageSchema.parse(await resultSet.json()); + const data = await resultSet.json(); + const usages = z.array(usageSchema).parse(data); + + return usages.reduce( + (collect, usage) => ({ ...collect, [usage.type]: usage }), + {} as UsageRecord, + ); }; diff --git a/src/app/(authenticated)/dashboard/Dashboard.tsx b/src/app/(authenticated)/dashboard/Dashboard.tsx index 50ad26ae83..542e5496cc 100644 --- a/src/app/(authenticated)/dashboard/Dashboard.tsx +++ b/src/app/(authenticated)/dashboard/Dashboard.tsx @@ -1,26 +1,19 @@ 'use client'; -import { useAuth } from '@clerk/nextjs'; import { useTranslations } from 'next-intl'; -import type { Usage } from '@/actions/analytics'; - import { TitleBar, UsageAnalyticsCard, AuditLogCard, } from '@/components/dashboard'; -export const Dashboard = ({ usage }: { usage: Usage }) => { - const { userId, orgId } = useAuth(); +export const Dashboard = () => { const t = useTranslations('DashboardIndex'); return ( <> -
Org: {orgId}
-
User: {userId}
-
Usage: {JSON.stringify(usage, null, 2)}
diff --git a/src/app/(authenticated)/dashboard/audit-logs/page.tsx b/src/app/(authenticated)/dashboard/audit-logs/page.tsx index fa710ee14d..77e5383995 100644 --- a/src/app/(authenticated)/dashboard/audit-logs/page.tsx +++ b/src/app/(authenticated)/dashboard/audit-logs/page.tsx @@ -2,8 +2,13 @@ import { useState } from 'react'; -import { Button } from '@/components/ui/button'; -import { Drawer } from '@/components/ui/drawer'; +import { + Button, + Drawer, + DrawerContent, + DrawerHeader, + DrawerTitle, +} from '@/components/ui'; import { TitleBar } from '@/components/dashboard/TitleBar'; import { AuditLogDetails } from '@/components/dashboard/AuditLogDetails'; import { AuditLogEntry } from '@/components/dashboard/AuditLogEntry'; @@ -15,18 +20,11 @@ type TimePeriod = '7' | '30' | '90'; const AuditLogsPage = () => { const [timePeriod, setTimePeriod] = useState('7'); const [selectedLog, setSelectedLog] = useState(null); - const [isDrawerOpen, setIsDrawerOpen] = useState(false); const logs = getFilteredLogs(Number(timePeriod)); - const handleLogClick = (log: AuditLog) => { - setSelectedLog(log); - setIsDrawerOpen(true); - }; - - const handleCloseDrawer = () => { - setIsDrawerOpen(false); - }; + const handleLogClick = (log: AuditLog) => setSelectedLog(log); + const handleCloseDrawer = () => setSelectedLog(null); return ( <> @@ -76,12 +74,13 @@ const AuditLogsPage = () => { )} - - {selectedLog && } + + + + Activity Details + + {selectedLog && } + ); diff --git a/src/app/(authenticated)/dashboard/page.tsx b/src/app/(authenticated)/dashboard/page.tsx index 65865d5b5f..02eb3c4208 100644 --- a/src/app/(authenticated)/dashboard/page.tsx +++ b/src/app/(authenticated)/dashboard/page.tsx @@ -1,11 +1,5 @@ -import { auth } from '@clerk/nextjs/server'; - -import { getUsage } from '@/actions/analytics'; - import { Dashboard } from './Dashboard'; export default async function Page() { - const { orgId } = await auth(); - const usage = await getUsage(orgId); - return ; + return ; } diff --git a/src/app/api/events/route.ts b/src/app/api/events/route.ts index 5115154005..aaea0ff084 100644 --- a/src/app/api/events/route.ts +++ b/src/app/api/events/route.ts @@ -2,7 +2,7 @@ import { auth } from '@clerk/nextjs/server'; import { NextRequest, NextResponse } from 'next/server'; import { v4 as uuidv4 } from 'uuid'; -import { cloudEventSchema } from '@/schemas'; +import { telemetryEventSchema } from '@/schemas'; import { captureEvent } from '@/lib/server/analytics'; export async function POST(request: NextRequest) { @@ -24,7 +24,7 @@ export async function POST(request: NextRequest) { const id = uuidv4(); const timestamp = Math.round(Date.now() / 1000); - const result = cloudEventSchema.safeParse(await request.json()); + const result = telemetryEventSchema.safeParse(await request.json()); if (!result.success) { console.error(result.error); diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 98126527b3..91bb4ba9a6 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -6,10 +6,8 @@ import { getLocale, getTranslations, setRequestLocale } from 'next-intl/server'; import { ClerkProvider } from '@clerk/nextjs'; import { getClerkLocale } from '@/i18n/locale'; -import { ThemeProvider } from '@/components/layout'; -import { ToastProvider, ToastViewport } from '@/components/ui/toast'; -import { ToastProvider as ToastContextProvider } from '@/components/ui/toast-context'; -import { Toaster } from '@/components/ui/toaster'; +import { ThemeProvider, ReactQueryProvider } from '@/components/layout'; +import { Toaster } from '@/components/ui'; export async function generateMetadata(): Promise { const locale = await getLocale(); @@ -54,13 +52,10 @@ export default async function RootLayout({ - - - {children} - - - - + + {children} + + diff --git a/src/components/analytics/AnalyticsPage.tsx b/src/components/analytics/AnalyticsPage.tsx index 49784f28b0..a6bef8c96d 100644 --- a/src/components/analytics/AnalyticsPage.tsx +++ b/src/components/analytics/AnalyticsPage.tsx @@ -5,14 +5,13 @@ import { X } from 'lucide-react'; import { useTranslations } from 'next-intl'; import React, { useMemo, useState } from 'react'; -import { Button } from '@/components/ui/button'; -import { DataTable } from '@/components/ui/data-table'; +import { Button } from '@/components/ui'; +import { DataTable } from '@/components/data-table'; -// Types type TimePeriod = '7' | '30' | '90'; + type ViewMode = 'developers' | 'models' | 'tasks'; -// Mock data types type Developer = { id: string; name: string; @@ -66,7 +65,6 @@ type Filter = { name: string; } | null; -// Generate mock conversation data const generateMockConversation = (taskId: string): ConversationMessage[] => { const baseConversations: Record = { task1: [ @@ -129,12 +127,10 @@ const generateMockConversation = (taskId: string): ConversationMessage[] => { ], }; - // Return specific conversation if available, otherwise generate a generic one if (baseConversations[taskId]) { return baseConversations[taskId]; } - // Generic conversation return [ { id: `${taskId}-msg1`, @@ -165,7 +161,6 @@ const generateMockConversation = (taskId: string): ConversationMessage[] => { ]; }; -// Mock data const mockDevelopers: Developer[] = [ { id: 'dev1', @@ -356,7 +351,6 @@ const mockTasks: Task[] = [ }, ]; -// Component for summary metrics const SummaryMetrics = ({ data, timePeriod, @@ -374,7 +368,6 @@ const SummaryMetrics = ({

{t('summary_title')}

- {/* Time period toggle */}
- {/* Metrics grid */}
{/* Active Developers */}
@@ -411,7 +403,6 @@ const SummaryMetrics = ({
- {/* Tasks Started */}
{t('tasks_started')} @@ -419,7 +410,6 @@ const SummaryMetrics = ({
{data.tasksStarted}
- {/* Tasks Completed */}
{t('tasks_completed')} @@ -429,7 +419,6 @@ const SummaryMetrics = ({
- {/* Tokens Consumed */}
{t('tokens_consumed')} @@ -439,7 +428,6 @@ const SummaryMetrics = ({
- {/* LLM Model Costs */}
{t('llm_costs')}
@@ -451,7 +439,6 @@ const SummaryMetrics = ({ ); }; -// Component for view mode toggle const ViewModeToggle = ({ viewMode, setViewMode, @@ -491,7 +478,6 @@ const ViewModeToggle = ({ ); }; -// Component for active filter display const ActiveFilter = ({ filter, onClear, @@ -520,7 +506,6 @@ const ActiveFilter = ({ ); }; -// Component for task details drawer const TaskDetailsDrawer = ({ task, isOpen, @@ -618,7 +603,6 @@ const TaskDetailsDrawer = ({ ); }; -// Main component export const AnalyticsPage = () => { const [timePeriod, setTimePeriod] = useState('7'); const [viewMode, setViewMode] = useState('tasks'); @@ -626,10 +610,7 @@ export const AnalyticsPage = () => { const [selectedTask, setSelectedTask] = useState(null); const [isDrawerOpen, setIsDrawerOpen] = useState(false); - // Generate summary data based on time period const summaryData = useMemo(() => { - // In a real implementation, this would fetch data from an API - // For now, we'll use mock data with different values for each time period switch (timePeriod) { case '7': return { @@ -658,44 +639,30 @@ export const AnalyticsPage = () => { } }, [timePeriod]); - // Handle developer click const handleDeveloperClick = (developer: Developer) => { - setFilter({ - type: 'developer', - id: developer.id, - name: developer.name, - }); + setFilter({ type: 'developer', id: developer.id, name: developer.name }); setViewMode('tasks'); }; - // Handle model click const handleModelClick = (model: Model) => { - setFilter({ - type: 'model', - id: model.id, - name: model.name, - }); + setFilter({ type: 'model', id: model.id, name: model.name }); setViewMode('tasks'); }; - // Handle task click const handleTaskClick = (task: Task) => { setSelectedTask(task); setIsDrawerOpen(true); }; - // Clear filter const clearFilter = () => { setFilter(null); }; - // Close drawer const closeDrawer = () => { setIsDrawerOpen(false); setSelectedTask(null); }; - // Data tables for each view mode const DevelopersTable = () => { const developerColumns: ColumnDef[] = [ { @@ -801,7 +768,6 @@ export const AnalyticsPage = () => { }; const TasksTable = () => { - // Filter tasks based on the active filter const filteredTasks = useMemo(() => { if (!filter) { return mockTasks; @@ -891,7 +857,6 @@ export const AnalyticsPage = () => { return ; }; - // Render the appropriate table based on view mode const renderTable = () => { switch (viewMode) { case 'developers': @@ -905,20 +870,16 @@ export const AnalyticsPage = () => { return (
- {/* Summary metrics */} - {/* View mode toggle */} - {/* Active filter */} {filter && } - {/* Data table */}

@@ -932,7 +893,6 @@ export const AnalyticsPage = () => { {renderTable()}

- {/* Task details drawer */} (null); - const [isDrawerOpen, setIsDrawerOpen] = useState(false); - - // Get the 5 most recent logs const logs = mockAuditLogs.slice(0, 5); + const [selectedLog, setSelectedLog] = useState(null); - const handleLogClick = (log: AuditLog) => { - setSelectedLog(log); - setIsDrawerOpen(true); - }; - - const handleCloseDrawer = () => { - setIsDrawerOpen(false); - }; + const handleLogClick = (log: AuditLog) => setSelectedLog(log); + const handleCloseDrawer = () => setSelectedLog(null); return (
@@ -59,13 +54,13 @@ export function AuditLogCard() {
- {/* Drawer for log details */} - - {selectedLog && } + + + + Activity Details + + {selectedLog && } +
); diff --git a/src/components/dashboard/DefaultParametersPage.tsx b/src/components/dashboard/DefaultParametersPage.tsx index 1bc3bb4917..0c7729d1a8 100644 --- a/src/components/dashboard/DefaultParametersPage.tsx +++ b/src/components/dashboard/DefaultParametersPage.tsx @@ -5,20 +5,20 @@ import { useTranslations } from 'next-intl'; import { useState } from 'react'; import { useForm } from 'react-hook-form'; +import { toast } from 'sonner'; -import { Button } from '@/components/ui/button'; -import { Checkbox } from '@/components/ui/checkbox'; import { + Button, + Checkbox, Form, FormControl, FormDescription, FormField, FormItem, FormLabel, -} from '@/components/ui/form'; -import { Input } from '@/components/ui/input'; -import { Slider } from '@/components/ui/slider'; -import { useToast } from '@/components/ui/toast-context'; + Input, + Slider, +} from '@/components/ui'; type DefaultParamsFormValues = { experimentalPowerSteering: boolean; @@ -47,7 +47,6 @@ type DefaultParamsFormValues = { const DefaultParametersPage = () => { const t = useTranslations('ProviderWhitelist'); - const { addToast } = useToast(); const [isSaving, setIsSaving] = useState(false); @@ -82,13 +81,9 @@ const DefaultParametersPage = () => { const onSubmit = (_data: DefaultParamsFormValues) => { setIsSaving(true); - // Simulate API call setTimeout(() => { - // Show success toast - addToast({ - title: 'Settings saved', + toast('Settings saved', { description: 'Default parameters have been updated successfully.', - variant: 'default', }); setIsSaving(false); @@ -112,7 +107,6 @@ const DefaultParametersPage = () => {
- {/* Checkpoints Section */}
{
- {/* Context Settings Section */}
{ const t = useTranslations('ProviderWhitelist'); - // State for providers and models const [providers, setProviders] = useState(initialProviders); const [policyVersion] = useState(1); const [allowAllProviders, setAllowAllProviders] = useState(true); - // Toggle allow all providers const toggleAllowAllProviders = () => { const newAllowAllProviders = !allowAllProviders; setAllowAllProviders(newAllowAllProviders); if (newAllowAllProviders) { - // Enable all providers and their models setProviders( providers.map((provider) => ({ ...provider, @@ -78,7 +72,6 @@ const ProviderWhitelistPage = () => { } }; - // Toggle provider enabled state const toggleProvider = (providerId: string) => { if (allowAllProviders) { return; @@ -91,7 +84,6 @@ const ProviderWhitelistPage = () => { return { ...provider, enabled: newEnabled, - // If provider is disabled, disable all its models models: provider.models.map((model) => ({ ...model, enabled: newEnabled ? model.enabled : false, @@ -103,7 +95,6 @@ const ProviderWhitelistPage = () => { ); }; - // Toggle model enabled state const toggleModel = (providerId: string, modelId: string) => { if (allowAllProviders) { return; diff --git a/src/components/dashboard/UsageAnalyticsCard.tsx b/src/components/dashboard/UsageAnalyticsCard.tsx index 433b0e8600..66ad564486 100644 --- a/src/components/dashboard/UsageAnalyticsCard.tsx +++ b/src/components/dashboard/UsageAnalyticsCard.tsx @@ -1,53 +1,25 @@ 'use client'; -import React, { useMemo, useState } from 'react'; +import React, { useState } from 'react'; import Link from 'next/link'; import { useTranslations } from 'next-intl'; +import { useAuth } from '@clerk/nextjs'; +import { useQuery } from '@tanstack/react-query'; + +import { TelemetryEventName, type TimePeriod, timePeriods } from '@/schemas'; +import { getUsage } from '@/actions/analytics'; import { Button } from '@/components/ui'; -type TimePeriod = '7' | '30' | '90'; - -type AnalyticsData = { - tasksStarted: number; - tasksCompleted: number; - tokensConsumed: string; - costs: number; - activeDevelopers: number; -}; - export const UsageAnalyticsCard = () => { const t = useTranslations('DashboardIndex'); - const [timePeriod, setTimePeriod] = useState('7'); + const { orgId } = useAuth(); + const [timePeriod, setTimePeriod] = useState(7); - const analyticsData = useMemo(() => { - switch (timePeriod) { - case '7': - return { - tasksStarted: 42, - tasksCompleted: 38, - tokensConsumed: '1.2M', - costs: 24.5, - activeDevelopers: 8, - }; - case '30': - return { - tasksStarted: 187, - tasksCompleted: 165, - tokensConsumed: '5.8M', - costs: 112.75, - activeDevelopers: 12, - }; - case '90': - return { - tasksStarted: 563, - tasksCompleted: 498, - tokensConsumed: '18.3M', - costs: 347.2, - activeDevelopers: 15, - }; - } - }, [timePeriod]); + const usage = useQuery({ + queryKey: ['usage', orgId, timePeriod], + queryFn: () => getUsage({ orgId, timePeriod }), + }); return (
@@ -57,36 +29,27 @@ export const UsageAnalyticsCard = () => { {t('analytics_description')}

+
- - - + {timePeriods.map((period) => ( + + ))}
+
{t('analytics_active_developers')}
- {analyticsData.activeDevelopers} + {usage.data?.[TelemetryEventName.LLM_COMPLETION]?.userCount ?? '-'}
@@ -94,7 +57,7 @@ export const UsageAnalyticsCard = () => { {t('analytics_tasks_started')}
- {analyticsData.tasksStarted} + {usage.data?.[TelemetryEventName.TASK_CREATED]?.eventCount ?? '-'}
@@ -102,7 +65,7 @@ export const UsageAnalyticsCard = () => { {t('analytics_tasks_completed')}
- {analyticsData.tasksCompleted} + {usage.data?.[TelemetryEventName.TASK_COMPLETED]?.eventCount ?? '-'}
@@ -110,7 +73,8 @@ export const UsageAnalyticsCard = () => { {t('analytics_tokens_consumed')}
- {analyticsData.tokensConsumed} + {usage.data?.[TelemetryEventName.LLM_COMPLETION]?.inputTokens ?? + '-'}
@@ -118,7 +82,9 @@ export const UsageAnalyticsCard = () => { {t('analytics_llm_costs')}
- ${analyticsData.costs.toFixed(2)} + {usage.data?.[TelemetryEventName.LLM_COMPLETION]?.cost?.toFixed( + 2, + ) ?? '-'}
diff --git a/src/components/ui/data-table.tsx b/src/components/data-table/index.tsx similarity index 98% rename from src/components/ui/data-table.tsx rename to src/components/data-table/index.tsx index 2620dbaa02..b59e38ecd9 100644 --- a/src/components/ui/data-table.tsx +++ b/src/components/data-table/index.tsx @@ -14,7 +14,7 @@ import { TableHead, TableHeader, TableRow, -} from '@/components/ui/table'; +} from '@/components/ui'; interface DataTableProps { columns: ColumnDef[]; diff --git a/src/components/layout/ReactQueryProvider.tsx b/src/components/layout/ReactQueryProvider.tsx new file mode 100644 index 0000000000..8f71cc2133 --- /dev/null +++ b/src/components/layout/ReactQueryProvider.tsx @@ -0,0 +1,27 @@ +'use client'; + +import { useState } from 'react'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; + +export const ReactQueryProvider = ({ + children, +}: { + children: React.ReactNode; +}) => { + const [queryClient] = useState( + () => + new QueryClient({ + defaultOptions: { + queries: { + // With SSR, we usually want to set some default staleTime + // above 0 to avoid refetching immediately on the client. + staleTime: 60 * 1000, + }, + }, + }), + ); + + return ( + {children} + ); +}; diff --git a/src/components/layout/index.ts b/src/components/layout/index.ts index 26164551e4..1003f4754a 100644 --- a/src/components/layout/index.ts +++ b/src/components/layout/index.ts @@ -3,4 +3,5 @@ export { Section } from './Section'; export { LocaleSwitcher } from './LocaleSwitcher'; export { ThemeProvider } from './ThemeProvider'; export { ThemeSwitcher } from './ThemeSwitcher'; +export { ReactQueryProvider } from './ReactQueryProvider'; export { Logo } from './Logo'; diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx index 903736b26f..28691b2c76 100644 --- a/src/components/ui/button.tsx +++ b/src/components/ui/button.tsx @@ -5,7 +5,7 @@ import { cva, type VariantProps } from 'class-variance-authority'; import { cn } from '@/lib/utils'; const buttonVariants = cva( - "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-sm text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive cursor-pointer", + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", { variants: { variant: { diff --git a/src/components/ui/card.tsx b/src/components/ui/card.tsx index 7cda3847f4..4898dfa6de 100644 --- a/src/components/ui/card.tsx +++ b/src/components/ui/card.tsx @@ -7,7 +7,7 @@ function Card({ className, ...props }: React.ComponentProps<'div'>) {
, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - - ) { + return ( + - - - -)); -Checkbox.displayName = CheckboxPrimitive.Root.displayName; + + + + + ); +} export { Checkbox }; diff --git a/src/components/ui/drawer.tsx b/src/components/ui/drawer.tsx index bad4c216ee..d09477ef50 100644 --- a/src/components/ui/drawer.tsx +++ b/src/components/ui/drawer.tsx @@ -1,109 +1,132 @@ 'use client'; -import type { VariantProps } from 'class-variance-authority'; -import { X } from 'lucide-react'; import * as React from 'react'; +import { Drawer as DrawerPrimitive } from 'vaul'; import { cn } from '@/lib/utils'; -import { Button } from './button'; -import { drawerTransformVariants, drawerVariants } from './drawerVariants'; - -export type DrawerProps = VariantProps & { - isOpen: boolean; - onClose: () => void; - title?: React.ReactNode; - children: React.ReactNode; - className?: string; - overlayClassName?: string; -}; - -export function Drawer({ - isOpen, - onClose, - title, - children, - side = 'right', - size = 'md', - className, - overlayClassName, +function Drawer({ ...props -}: DrawerProps) { - // Handle ESC key press to close the drawer - React.useEffect(() => { - const handleKeyDown = (e: KeyboardEvent) => { - if (e.key === 'Escape' && isOpen) { - onClose(); - } - }; +}: React.ComponentProps) { + return ; +} - window.addEventListener('keydown', handleKeyDown); - return () => window.removeEventListener('keydown', handleKeyDown); - }, [isOpen, onClose]); +function DrawerTrigger({ + ...props +}: React.ComponentProps) { + return ; +} - // Handle focus trap - const drawerRef = React.useRef(null); - React.useEffect(() => { - if (isOpen && drawerRef.current) { - const focusableElements = drawerRef.current.querySelectorAll( - 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])', - ); +function DrawerPortal({ + ...props +}: React.ComponentProps) { + return ; +} - if (focusableElements.length > 0) { - (focusableElements[0] as HTMLElement).focus(); - } - } - }, [isOpen]); - - if (!isOpen) { - return null; - } - - // Get the transform class based on the side - const transformClass = - drawerTransformVariants[side as keyof typeof drawerTransformVariants]; - const openClass = transformClass ? transformClass.open : ''; - const closedClass = transformClass ? transformClass.closed : ''; +function DrawerClose({ + ...props +}: React.ComponentProps) { + return ; +} +function DrawerOverlay({ + className, + ...props +}: React.ComponentProps) { return ( -
- {/* Overlay */} - +
+ {children} + + ); } + +function DrawerHeader({ className, ...props }: React.ComponentProps<'div'>) { + return ( +
+ ); +} + +function DrawerFooter({ className, ...props }: React.ComponentProps<'div'>) { + return ( +
+ ); +} + +function DrawerTitle({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function DrawerDescription({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +export { + Drawer, + DrawerPortal, + DrawerOverlay, + DrawerTrigger, + DrawerClose, + DrawerContent, + DrawerHeader, + DrawerFooter, + DrawerTitle, + DrawerDescription, +}; diff --git a/src/components/ui/drawerVariants.ts b/src/components/ui/drawerVariants.ts deleted file mode 100644 index 2dd581dfdb..0000000000 --- a/src/components/ui/drawerVariants.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { cva } from 'class-variance-authority'; - -export const drawerVariants = cva( - 'fixed z-50 bg-background shadow-lg transition-transform duration-300 ease-in-out', - { - variants: { - side: { - top: 'inset-x-0 top-0 border-b', - right: 'inset-y-0 right-0 border-l', - bottom: 'inset-x-0 bottom-0 border-t', - left: 'inset-y-0 left-0 border-r', - }, - size: { - sm: 'sm:max-w-sm', - md: 'sm:max-w-md', - lg: 'sm:max-w-lg', - xl: 'sm:max-w-xl', - '2xl': 'sm:max-w-2xl', - full: 'sm:max-w-full', - }, - }, - defaultVariants: { - side: 'right', - size: 'md', - }, - }, -); - -export const drawerTransformVariants = { - top: { - open: 'translate-y-0', - closed: '-translate-y-full', - }, - right: { - open: 'translate-x-0', - closed: 'translate-x-full', - }, - bottom: { - open: 'translate-y-0', - closed: 'translate-y-full', - }, - left: { - open: 'translate-x-0', - closed: '-translate-x-full', - }, -}; diff --git a/src/components/ui/dropdown-menu.tsx b/src/components/ui/dropdown-menu.tsx index 700a399a4c..26027549db 100644 --- a/src/components/ui/dropdown-menu.tsx +++ b/src/components/ui/dropdown-menu.tsx @@ -74,8 +74,7 @@ function DropdownMenuItem({ data-inset={inset} data-variant={variant} className={cn( - "focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", - 'cursor-pointer', + "focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", className, )} {...props} @@ -93,8 +92,7 @@ function DropdownMenuCheckboxItem({ , - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - - - - - - -)); -Slider.displayName = SliderPrimitive.Root.displayName; +function Slider({ + className, + defaultValue, + value, + min = 0, + max = 100, + ...props +}: React.ComponentProps) { + const _values = React.useMemo( + () => + Array.isArray(value) + ? value + : Array.isArray(defaultValue) + ? defaultValue + : [min, max], + [value, defaultValue, min, max], + ); + + return ( + + + + + {Array.from({ length: _values.length }, (_, index) => ( + + ))} + + ); +} export { Slider }; diff --git a/src/components/ui/sonner.tsx b/src/components/ui/sonner.tsx new file mode 100644 index 0000000000..564a6547de --- /dev/null +++ b/src/components/ui/sonner.tsx @@ -0,0 +1,25 @@ +'use client'; + +import { useTheme } from 'next-themes'; +import { Toaster as Sonner, ToasterProps } from 'sonner'; + +const Toaster = ({ ...props }: ToasterProps) => { + const { theme = 'system' } = useTheme(); + + return ( + + ); +}; + +export { Toaster }; diff --git a/src/components/ui/toast-context.tsx b/src/components/ui/toast-context.tsx deleted file mode 100644 index 265fce8668..0000000000 --- a/src/components/ui/toast-context.tsx +++ /dev/null @@ -1,65 +0,0 @@ -'use client'; - -import * as React from 'react'; - -import type { ToastActionElement, ToastProps } from '@/components/ui/toast'; - -type ToastType = ToastProps & { - id: string; - title?: React.ReactNode; - description?: React.ReactNode; - action?: ToastActionElement; -}; - -const ToastContext = React.createContext<{ - toasts: ToastType[]; - addToast: (props: Omit) => void; - removeToast: (id: string) => void; -}>({ - toasts: [], - addToast: () => {}, - removeToast: () => {}, -}); - -export function ToastProvider({ children }: { children: React.ReactNode }) { - const [toasts, setToasts] = React.useState([]); - - const addToast = React.useCallback( - (props: Omit) => { - const id = Math.random().toString(36).substring(2, 9); - setToasts((prev) => [...prev, { id, ...props }]); - - // Auto-dismiss after 5 seconds - setTimeout(() => { - setToasts((prev) => prev.filter((toast) => toast.id !== id)); - }, 5000); - }, - [setToasts], - ); - - const removeToast = React.useCallback( - (id: string) => { - setToasts((prev) => prev.filter((toast) => toast.id !== id)); - }, - [setToasts], - ); - - const contextValue = React.useMemo( - () => ({ toasts, addToast, removeToast }), - [toasts, addToast, removeToast], - ); - - return ( - - {children} - - ); -} - -export const useToast = () => { - const context = React.useContext(ToastContext); - if (context === undefined) { - throw new Error('useToast must be used within a ToastProvider'); - } - return context; -}; diff --git a/src/components/ui/toast.tsx b/src/components/ui/toast.tsx deleted file mode 100644 index 86110ed833..0000000000 --- a/src/components/ui/toast.tsx +++ /dev/null @@ -1,129 +0,0 @@ -'use client'; - -import { Cross2Icon } from '@radix-ui/react-icons'; -import * as ToastPrimitives from '@radix-ui/react-toast'; -import { cva, type VariantProps } from 'class-variance-authority'; -import * as React from 'react'; - -import { cn } from '@/lib/utils'; - -const ToastProvider = ToastPrimitives.Provider; - -const ToastViewport = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)); -ToastViewport.displayName = ToastPrimitives.Viewport.displayName; - -const toastVariants = cva( - 'group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full', - { - variants: { - variant: { - default: 'border bg-background text-foreground', - destructive: - 'destructive group border-destructive bg-destructive text-destructive-foreground', - }, - }, - defaultVariants: { - variant: 'default', - }, - }, -); - -const Toast = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef & - VariantProps ->(({ className, variant, ...props }, ref) => { - return ( - - ); -}); -Toast.displayName = ToastPrimitives.Root.displayName; - -const ToastAction = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)); -ToastAction.displayName = ToastPrimitives.Action.displayName; - -const ToastClose = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - - - -)); -ToastClose.displayName = ToastPrimitives.Close.displayName; - -const ToastTitle = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)); -ToastTitle.displayName = ToastPrimitives.Title.displayName; - -const ToastDescription = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)); -ToastDescription.displayName = ToastPrimitives.Description.displayName; - -type ToastProps = React.ComponentPropsWithoutRef; - -type ToastActionElement = React.ReactElement; - -export { - Toast, - ToastAction, - type ToastActionElement, - ToastClose, - ToastDescription, - type ToastProps, - ToastProvider, - ToastTitle, - ToastViewport, -}; diff --git a/src/components/ui/toaster.tsx b/src/components/ui/toaster.tsx deleted file mode 100644 index 0f6483f7dc..0000000000 --- a/src/components/ui/toaster.tsx +++ /dev/null @@ -1,28 +0,0 @@ -'use client'; - -import { - Toast, - ToastClose, - ToastDescription, - ToastTitle, -} from '@/components/ui/toast'; -import { useToast } from '@/components/ui/toast-context'; - -export function Toaster() { - const { toasts } = useToast(); - - return ( - <> - {toasts.map(({ id, title, description, action, ...props }) => ( - -
- {title && {title}} - {description && {description}} -
- {action} - -
- ))} - - ); -} diff --git a/src/lib/server/analytics.ts b/src/lib/server/analytics.ts index ace477b45c..57d5e88a50 100644 --- a/src/lib/server/analytics.ts +++ b/src/lib/server/analytics.ts @@ -1,6 +1,6 @@ import { createClient } from '@clickhouse/client'; -import { CloudEvent } from '@/schemas'; +import type { TelemetryEvent } from '@/schemas'; import { Env } from './env'; export const client = createClient({ @@ -14,7 +14,7 @@ type AnalyticsEvent = { orgId: string; userId: string; timestamp: number; - event: CloudEvent; + event: TelemetryEvent; }; export const captureEvent = async ({ diff --git a/src/schemas/index.ts b/src/schemas/index.ts index f70c39584b..f23a930665 100644 --- a/src/schemas/index.ts +++ b/src/schemas/index.ts @@ -1,7 +1,8 @@ import { z } from 'zod'; -// TODO: I'll add these types to our @roo-code/types NPM package so we -// don't need to manually copy them. +/** + * ProviderName + */ export const providerNames = [ 'anthropic', @@ -27,30 +28,11 @@ export const providerNames = [ 'litellm', ] as const; -export const appPropertiesSchema = z.object({ - appVersion: z.string(), - vscodeVersion: z.string(), - platform: z.string(), - editorName: z.string(), - language: z.string(), - mode: z.string(), -}); +export type ProviderName = (typeof providerNames)[number]; -export const taskPropertiesSchema = z.object({ - taskId: z.string(), - apiProvider: z.enum(providerNames).optional(), - modelId: z.string().optional(), - diffStrategy: z.string().optional(), - isSubtask: z.boolean().optional(), -}); - -export const completionPropertiesSchema = z.object({ - inputTokens: z.number(), - outputTokens: z.number(), - cacheReadTokens: z.number().optional(), - cacheWriteTokens: z.number().optional(), - cost: z.number().optional(), -}); +/** + * TelemetryEventName + */ export enum TelemetryEventName { TASK_CREATED = 'Task Created', @@ -78,7 +60,36 @@ export enum TelemetryEventName { CONSECUTIVE_MISTAKE_ERROR = 'Consecutive Mistake Error', } -export const cloudEventSchema = z.discriminatedUnion('type', [ +/** + * TelemetryEvent + */ + +export const appPropertiesSchema = z.object({ + appVersion: z.string(), + vscodeVersion: z.string(), + platform: z.string(), + editorName: z.string(), + language: z.string(), + mode: z.string(), +}); + +export const taskPropertiesSchema = z.object({ + taskId: z.string(), + apiProvider: z.enum(providerNames).optional(), + modelId: z.string().optional(), + diffStrategy: z.string().optional(), + isSubtask: z.boolean().optional(), +}); + +export const completionPropertiesSchema = z.object({ + inputTokens: z.number(), + outputTokens: z.number(), + cacheReadTokens: z.number().optional(), + cacheWriteTokens: z.number().optional(), + cost: z.number().optional(), +}); + +export const telemetryEventSchema = z.discriminatedUnion('type', [ z.object({ type: z.literal(TelemetryEventName.TASK_CREATED), properties: z.object({ @@ -96,4 +107,12 @@ export const cloudEventSchema = z.discriminatedUnion('type', [ }), ]); -export type CloudEvent = z.infer; +export type TelemetryEvent = z.infer; + +/** + * TimePeriod + */ + +export const timePeriods = [7, 30, 90] as const; + +export type TimePeriod = (typeof timePeriods)[number];