React Query + Updated @shadcn/ui components (#17)

This commit is contained in:
Chris Estreich 2025-05-16 09:54:07 -07:00 committed by GitHub
parent f45e93b9ec
commit 5fd53e9ee2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
31 changed files with 537 additions and 654 deletions

View file

@ -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": {

87
pnpm-lock.yaml generated
View file

@ -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

View file

@ -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<typeof usageSchema>;
export const getUsage = async (orgId?: string | null): Promise<Usage> => {
/**
* getUsage
*/
type UsageRecord = Partial<Record<TelemetryEventName, Usage>>;
export const getUsage = async ({
orgId,
timePeriod,
}: {
orgId?: string | null;
timePeriod: TimePeriod;
}): Promise<UsageRecord> => {
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,
);
};

View file

@ -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 (
<>
<TitleBar title={t('title_bar')} />
<div>Org: {orgId}</div>
<div>User: {userId}</div>
<pre>Usage: {JSON.stringify(usage, null, 2)}</pre>
<UsageAnalyticsCard />
<AuditLogCard />
</>

View file

@ -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<TimePeriod>('7');
const [selectedLog, setSelectedLog] = useState<AuditLog | null>(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 = () => {
)}
</div>
</div>
<Drawer
isOpen={isDrawerOpen}
onClose={handleCloseDrawer}
title="Activity Details"
>
{selectedLog && <AuditLogDetails log={selectedLog} />}
<Drawer open={!!selectedLog} onClose={handleCloseDrawer}>
<DrawerContent>
<DrawerHeader>
<DrawerTitle>Activity Details</DrawerTitle>
</DrawerHeader>
{selectedLog && <AuditLogDetails log={selectedLog} />}
</DrawerContent>
</Drawer>
</>
);

View file

@ -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 <Dashboard usage={usage} />;
return <Dashboard />;
}

View file

@ -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);

View file

@ -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<Metadata> {
const locale = await getLocale();
@ -54,13 +52,10 @@ export default async function RootLayout({
<NextIntlClientProvider>
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
<ClerkProvider localization={getClerkLocale(locale)}>
<ToastProvider>
<ToastContextProvider>
{children}
<Toaster />
<ToastViewport />
</ToastContextProvider>
</ToastProvider>
<ReactQueryProvider>
{children}
<Toaster />
</ReactQueryProvider>
</ClerkProvider>
</ThemeProvider>
</NextIntlClientProvider>

View file

@ -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<string, ConversationMessage[]> = {
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 = ({
<h3 className="text-lg font-semibold">{t('summary_title')}</h3>
</div>
{/* Time period toggle */}
<div className="mb-4 flex space-x-2">
<Button
variant={timePeriod === '7' ? 'default' : 'outline'}
@ -399,7 +392,6 @@ const SummaryMetrics = ({
</Button>
</div>
{/* Metrics grid */}
<div className="grid grid-cols-2 gap-4 md:grid-cols-5">
{/* Active Developers */}
<div className="rounded-lg bg-background p-3">
@ -411,7 +403,6 @@ const SummaryMetrics = ({
</div>
</div>
{/* Tasks Started */}
<div className="rounded-lg bg-background p-3">
<div className="text-xs text-muted-foreground">
{t('tasks_started')}
@ -419,7 +410,6 @@ const SummaryMetrics = ({
<div className="mt-1 text-2xl font-semibold">{data.tasksStarted}</div>
</div>
{/* Tasks Completed */}
<div className="rounded-lg bg-background p-3">
<div className="text-xs text-muted-foreground">
{t('tasks_completed')}
@ -429,7 +419,6 @@ const SummaryMetrics = ({
</div>
</div>
{/* Tokens Consumed */}
<div className="rounded-lg bg-background p-3">
<div className="text-xs text-muted-foreground">
{t('tokens_consumed')}
@ -439,7 +428,6 @@ const SummaryMetrics = ({
</div>
</div>
{/* LLM Model Costs */}
<div className="rounded-lg bg-background p-3">
<div className="text-xs text-muted-foreground">{t('llm_costs')}</div>
<div className="mt-1 text-2xl font-semibold">
@ -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<TimePeriod>('7');
const [viewMode, setViewMode] = useState<ViewMode>('tasks');
@ -626,10 +610,7 @@ export const AnalyticsPage = () => {
const [selectedTask, setSelectedTask] = useState<Task | null>(null);
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
// Generate summary data based on time period
const summaryData = useMemo<SummaryData>(() => {
// 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<Developer>[] = [
{
@ -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 <DataTable columns={taskColumns} data={filteredTasks} />;
};
// Render the appropriate table based on view mode
const renderTable = () => {
switch (viewMode) {
case 'developers':
@ -905,20 +870,16 @@ export const AnalyticsPage = () => {
return (
<div>
{/* Summary metrics */}
<SummaryMetrics
data={summaryData}
timePeriod={timePeriod}
setTimePeriod={setTimePeriod}
/>
{/* View mode toggle */}
<ViewModeToggle viewMode={viewMode} setViewMode={setViewMode} />
{/* Active filter */}
{filter && <ActiveFilter filter={filter} onClear={clearFilter} />}
{/* Data table */}
<div className="rounded-md border bg-card p-5">
<div className="mb-4">
<h3 className="text-lg font-semibold">
@ -932,7 +893,6 @@ export const AnalyticsPage = () => {
{renderTable()}
</div>
{/* Task details drawer */}
<TaskDetailsDrawer
task={selectedTask}
isOpen={isDrawerOpen}

View file

@ -3,7 +3,12 @@
import Link from 'next/link';
import React, { useState } from 'react';
import { Drawer } from '@/components/ui/drawer';
import {
Drawer,
DrawerContent,
DrawerHeader,
DrawerTitle,
} from '@/components/ui/drawer';
import { AuditLogDetails } from './AuditLogDetails';
import { AuditLogEntry } from './AuditLogEntry';
@ -11,21 +16,11 @@ import type { AuditLog } from './mockAuditLogs';
import { mockAuditLogs } from './mockAuditLogs';
export function AuditLogCard() {
// Always show the most recent 5 entries
const [selectedLog, setSelectedLog] = useState<AuditLog | null>(null);
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
// Get the 5 most recent logs
const logs = mockAuditLogs.slice(0, 5);
const [selectedLog, setSelectedLog] = useState<AuditLog | null>(null);
const handleLogClick = (log: AuditLog) => {
setSelectedLog(log);
setIsDrawerOpen(true);
};
const handleCloseDrawer = () => {
setIsDrawerOpen(false);
};
const handleLogClick = (log: AuditLog) => setSelectedLog(log);
const handleCloseDrawer = () => setSelectedLog(null);
return (
<div className="mb-6 w-2/3 rounded-md bg-card p-5">
@ -59,13 +54,13 @@ export function AuditLogCard() {
</Link>
</div>
{/* Drawer for log details */}
<Drawer
isOpen={isDrawerOpen}
onClose={handleCloseDrawer}
title="Activity Details"
>
{selectedLog && <AuditLogDetails log={selectedLog} />}
<Drawer open={!!selectedLog} onClose={handleCloseDrawer}>
<DrawerContent>
<DrawerHeader>
<DrawerTitle>Activity Details</DrawerTitle>
</DrawerHeader>
{selectedLog && <AuditLogDetails log={selectedLog} />}
</DrawerContent>
</Drawer>
</div>
);

View file

@ -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 = () => {
<div className="space-y-8">
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
{/* Checkpoints Section */}
<div className="space-y-4 rounded-lg bg-muted/50 p-4">
<div className="flex items-center gap-2">
<svg
@ -157,7 +151,6 @@ const DefaultParametersPage = () => {
</div>
</div>
{/* Context Settings Section */}
<div className="space-y-4 rounded-lg bg-muted/50 p-4">
<div className="flex items-center gap-2">
<svg

View file

@ -3,11 +3,8 @@
import { useTranslations } from 'next-intl';
import { useState } from 'react';
import { Badge } from '@/components/ui/badge';
import { Checkbox } from '@/components/ui/checkbox';
import { Label } from '@/components/ui/label';
import { Badge, Checkbox, Label } from '@/components/ui';
// Mock data for providers and models
const initialProviders = [
{
id: 'openai',
@ -53,18 +50,15 @@ const initialProviders = [
const ProviderWhitelistPage = () => {
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;

View file

@ -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<TimePeriod>('7');
const { orgId } = useAuth();
const [timePeriod, setTimePeriod] = useState<TimePeriod>(7);
const analyticsData = useMemo<AnalyticsData>(() => {
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 (
<div className="mb-6 rounded-md bg-card p-5">
@ -57,36 +29,27 @@ export const UsageAnalyticsCard = () => {
{t('analytics_description')}
</p>
</div>
<div className="mb-4 flex space-x-2">
<Button
variant={timePeriod === '7' ? 'default' : 'outline'}
size="sm"
onClick={() => setTimePeriod('7')}
>
{t('analytics_period_7_days')}
</Button>
<Button
variant={timePeriod === '30' ? 'default' : 'outline'}
size="sm"
onClick={() => setTimePeriod('30')}
>
{t('analytics_period_30_days')}
</Button>
<Button
variant={timePeriod === '90' ? 'default' : 'outline'}
size="sm"
onClick={() => setTimePeriod('90')}
>
{t('analytics_period_90_days')}
</Button>
{timePeriods.map((period) => (
<Button
key={period}
variant={period === timePeriod ? 'default' : 'outline'}
size="sm"
onClick={() => setTimePeriod(period)}
>
{t(`analytics_period_${period}_days`)}
</Button>
))}
</div>
<div className="grid grid-cols-2 gap-4 md:grid-cols-5">
<div className="rounded-lg bg-background p-3">
<div className="text-xs text-muted-foreground">
{t('analytics_active_developers')}
</div>
<div className="mt-1 text-2xl font-semibold">
{analyticsData.activeDevelopers}
{usage.data?.[TelemetryEventName.LLM_COMPLETION]?.userCount ?? '-'}
</div>
</div>
<div className="rounded-lg bg-background p-3">
@ -94,7 +57,7 @@ export const UsageAnalyticsCard = () => {
{t('analytics_tasks_started')}
</div>
<div className="mt-1 text-2xl font-semibold">
{analyticsData.tasksStarted}
{usage.data?.[TelemetryEventName.TASK_CREATED]?.eventCount ?? '-'}
</div>
</div>
<div className="rounded-lg bg-background p-3">
@ -102,7 +65,7 @@ export const UsageAnalyticsCard = () => {
{t('analytics_tasks_completed')}
</div>
<div className="mt-1 text-2xl font-semibold">
{analyticsData.tasksCompleted}
{usage.data?.[TelemetryEventName.TASK_COMPLETED]?.eventCount ?? '-'}
</div>
</div>
<div className="rounded-lg bg-background p-3">
@ -110,7 +73,8 @@ export const UsageAnalyticsCard = () => {
{t('analytics_tokens_consumed')}
</div>
<div className="mt-1 text-2xl font-semibold">
{analyticsData.tokensConsumed}
{usage.data?.[TelemetryEventName.LLM_COMPLETION]?.inputTokens ??
'-'}
</div>
</div>
<div className="rounded-lg bg-background p-3">
@ -118,7 +82,9 @@ export const UsageAnalyticsCard = () => {
{t('analytics_llm_costs')}
</div>
<div className="mt-1 text-2xl font-semibold">
${analyticsData.costs.toFixed(2)}
{usage.data?.[TelemetryEventName.LLM_COMPLETION]?.cost?.toFixed(
2,
) ?? '-'}
</div>
</div>
</div>

View file

@ -14,7 +14,7 @@ import {
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table';
} from '@/components/ui';
interface DataTableProps<TData, TValue> {
columns: ColumnDef<TData, TValue>[];

View file

@ -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 (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);
};

View file

@ -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';

View file

@ -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: {

View file

@ -7,7 +7,7 @@ function Card({ className, ...props }: React.ComponentProps<'div'>) {
<div
data-slot="card"
className={cn(
'bg-card text-card-foreground flex flex-col gap-6 rounded-sm border py-6 shadow-sm',
'bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm',
className,
)}
{...props}

View file

@ -1,30 +1,32 @@
'use client';
import * as CheckboxPrimitive from '@radix-ui/react-checkbox';
import { Check } from 'lucide-react';
import * as React from 'react';
import * as CheckboxPrimitive from '@radix-ui/react-checkbox';
import { CheckIcon } from 'lucide-react';
import { cn } from '@/lib/utils';
const Checkbox = React.forwardRef<
React.ElementRef<typeof CheckboxPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
>(({ className, ...props }, ref) => (
<CheckboxPrimitive.Root
ref={ref}
className={cn(
'peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground',
className,
)}
{...props}
>
<CheckboxPrimitive.Indicator
className={cn('flex items-center justify-center text-current')}
function Checkbox({
className,
...props
}: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
return (
<CheckboxPrimitive.Root
data-slot="checkbox"
className={cn(
'peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50',
className,
)}
{...props}
>
<Check className="size-3.5" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
));
Checkbox.displayName = CheckboxPrimitive.Root.displayName;
<CheckboxPrimitive.Indicator
data-slot="checkbox-indicator"
className="flex items-center justify-center text-current transition-none"
>
<CheckIcon className="size-3.5" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
);
}
export { Checkbox };

View file

@ -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<typeof drawerVariants> & {
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<typeof DrawerPrimitive.Root>) {
return <DrawerPrimitive.Root data-slot="drawer" {...props} />;
}
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [isOpen, onClose]);
function DrawerTrigger({
...props
}: React.ComponentProps<typeof DrawerPrimitive.Trigger>) {
return <DrawerPrimitive.Trigger data-slot="drawer-trigger" {...props} />;
}
// Handle focus trap
const drawerRef = React.useRef<HTMLDivElement>(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<typeof DrawerPrimitive.Portal>) {
return <DrawerPrimitive.Portal data-slot="drawer-portal" {...props} />;
}
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<typeof DrawerPrimitive.Close>) {
return <DrawerPrimitive.Close data-slot="drawer-close" {...props} />;
}
function DrawerOverlay({
className,
...props
}: React.ComponentProps<typeof DrawerPrimitive.Overlay>) {
return (
<div className="fixed inset-0 z-50 overflow-hidden">
{/* Overlay */}
<div
className={cn(
'fixed inset-0 bg-black/50 transition-opacity',
overlayClassName,
)}
onClick={onClose}
aria-hidden="true"
/>
<DrawerPrimitive.Overlay
data-slot="drawer-overlay"
className={cn(
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50',
className,
)}
{...props}
/>
);
}
{/* Drawer */}
<div
ref={drawerRef}
function DrawerContent({
className,
children,
...props
}: React.ComponentProps<typeof DrawerPrimitive.Content>) {
return (
<DrawerPortal data-slot="drawer-portal">
<DrawerOverlay />
<DrawerPrimitive.Content
data-slot="drawer-content"
className={cn(
drawerVariants({ side, size }),
isOpen ? openClass : closedClass,
'group/drawer-content bg-background fixed z-50 flex h-auto flex-col',
'data-[vaul-drawer-direction=top]:inset-x-0 data-[vaul-drawer-direction=top]:top-0 data-[vaul-drawer-direction=top]:mb-24 data-[vaul-drawer-direction=top]:max-h-[80vh] data-[vaul-drawer-direction=top]:rounded-b-lg data-[vaul-drawer-direction=top]:border-b',
'data-[vaul-drawer-direction=bottom]:inset-x-0 data-[vaul-drawer-direction=bottom]:bottom-0 data-[vaul-drawer-direction=bottom]:mt-24 data-[vaul-drawer-direction=bottom]:max-h-[80vh] data-[vaul-drawer-direction=bottom]:rounded-t-lg data-[vaul-drawer-direction=bottom]:border-t',
'data-[vaul-drawer-direction=right]:inset-y-0 data-[vaul-drawer-direction=right]:right-0 data-[vaul-drawer-direction=right]:w-3/4 data-[vaul-drawer-direction=right]:border-l data-[vaul-drawer-direction=right]:sm:max-w-sm',
'data-[vaul-drawer-direction=left]:inset-y-0 data-[vaul-drawer-direction=left]:left-0 data-[vaul-drawer-direction=left]:w-3/4 data-[vaul-drawer-direction=left]:border-r data-[vaul-drawer-direction=left]:sm:max-w-sm',
className,
)}
{...props}
>
{/* Header */}
<div className="flex items-center justify-between border-b p-4">
<h2 className="text-lg font-semibold">{title}</h2>
<Button
variant="ghost"
size="sm"
onClick={onClose}
className="size-8 p-0"
aria-label="Close"
>
<X className="size-4" />
</Button>
</div>
{/* Content */}
<div className="overflow-auto p-4">{children}</div>
</div>
</div>
<div className="bg-muted mx-auto mt-4 hidden h-2 w-[100px] shrink-0 rounded-full group-data-[vaul-drawer-direction=bottom]/drawer-content:block" />
{children}
</DrawerPrimitive.Content>
</DrawerPortal>
);
}
function DrawerHeader({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot="drawer-header"
className={cn('flex flex-col gap-1.5 p-4', className)}
{...props}
/>
);
}
function DrawerFooter({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot="drawer-footer"
className={cn('mt-auto flex flex-col gap-2 p-4', className)}
{...props}
/>
);
}
function DrawerTitle({
className,
...props
}: React.ComponentProps<typeof DrawerPrimitive.Title>) {
return (
<DrawerPrimitive.Title
data-slot="drawer-title"
className={cn('text-foreground font-semibold', className)}
{...props}
/>
);
}
function DrawerDescription({
className,
...props
}: React.ComponentProps<typeof DrawerPrimitive.Description>) {
return (
<DrawerPrimitive.Description
data-slot="drawer-description"
className={cn('text-muted-foreground text-sm', className)}
{...props}
/>
);
}
export {
Drawer,
DrawerPortal,
DrawerOverlay,
DrawerTrigger,
DrawerClose,
DrawerContent,
DrawerHeader,
DrawerFooter,
DrawerTitle,
DrawerDescription,
};

View file

@ -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',
},
};

View file

@ -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({
<DropdownMenuPrimitive.CheckboxItem
data-slot="dropdown-menu-checkbox-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
'cursor-pointer',
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
checked={checked}
@ -130,8 +128,7 @@ function DropdownMenuRadioItem({
<DropdownMenuPrimitive.RadioItem
data-slot="dropdown-menu-radio-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
'cursor-pointer',
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
@ -214,8 +211,7 @@ function DropdownMenuSubTrigger({
data-slot="dropdown-menu-sub-trigger"
data-inset={inset}
className={cn(
'focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8',
'cursor-pointer',
'focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8',
className,
)}
{...props}

View file

@ -14,7 +14,7 @@ import {
} from 'react-hook-form';
import { cn } from '@/lib/utils';
import { Label } from '@/components/ui/label';
import { Label } from '@/components/ui';
const Form = FormProvider;

View file

@ -1,4 +1,13 @@
export * from './badge';
export * from './button';
export * from './card';
export * from './checkbox';
export * from './drawer';
export * from './dropdown-menu';
export * from './form';
export * from './input';
export * from './label';
export * from './separator';
export * from './slider';
export * from './sonner';
export * from './table';

View file

@ -1,28 +1,63 @@
'use client';
import * as SliderPrimitive from '@radix-ui/react-slider';
import * as React from 'react';
import * as SliderPrimitive from '@radix-ui/react-slider';
import { cn } from '@/lib/utils';
const Slider = React.forwardRef<
React.ElementRef<typeof SliderPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root>
>(({ className, ...props }, ref) => (
<SliderPrimitive.Root
ref={ref}
className={cn(
'relative flex w-full touch-none select-none items-center',
className,
)}
{...props}
>
<SliderPrimitive.Track className="relative h-2 w-full grow overflow-hidden rounded-full bg-secondary">
<SliderPrimitive.Range className="absolute h-full bg-primary" />
</SliderPrimitive.Track>
<SliderPrimitive.Thumb className="block size-5 rounded-full border-2 border-primary bg-background ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50" />
</SliderPrimitive.Root>
));
Slider.displayName = SliderPrimitive.Root.displayName;
function Slider({
className,
defaultValue,
value,
min = 0,
max = 100,
...props
}: React.ComponentProps<typeof SliderPrimitive.Root>) {
const _values = React.useMemo(
() =>
Array.isArray(value)
? value
: Array.isArray(defaultValue)
? defaultValue
: [min, max],
[value, defaultValue, min, max],
);
return (
<SliderPrimitive.Root
data-slot="slider"
defaultValue={defaultValue}
value={value}
min={min}
max={max}
className={cn(
'relative flex w-full touch-none items-center select-none data-[disabled]:opacity-50 data-[orientation=vertical]:h-full data-[orientation=vertical]:min-h-44 data-[orientation=vertical]:w-auto data-[orientation=vertical]:flex-col',
className,
)}
{...props}
>
<SliderPrimitive.Track
data-slot="slider-track"
className={cn(
'bg-muted relative grow overflow-hidden rounded-full data-[orientation=horizontal]:h-1.5 data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-1.5',
)}
>
<SliderPrimitive.Range
data-slot="slider-range"
className={cn(
'bg-primary absolute data-[orientation=horizontal]:h-full data-[orientation=vertical]:w-full',
)}
/>
</SliderPrimitive.Track>
{Array.from({ length: _values.length }, (_, index) => (
<SliderPrimitive.Thumb
data-slot="slider-thumb"
key={index}
className="border-primary bg-background ring-ring/50 block size-4 shrink-0 rounded-full border shadow-sm transition-[color,box-shadow] hover:ring-4 focus-visible:ring-4 focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50"
/>
))}
</SliderPrimitive.Root>
);
}
export { Slider };

View file

@ -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 (
<Sonner
theme={theme as ToasterProps['theme']}
className="toaster group"
style={
{
'--normal-bg': 'var(--popover)',
'--normal-text': 'var(--popover-foreground)',
'--normal-border': 'var(--border)',
} as React.CSSProperties
}
{...props}
/>
);
};
export { Toaster };

View file

@ -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<ToastType, 'id'>) => void;
removeToast: (id: string) => void;
}>({
toasts: [],
addToast: () => {},
removeToast: () => {},
});
export function ToastProvider({ children }: { children: React.ReactNode }) {
const [toasts, setToasts] = React.useState<ToastType[]>([]);
const addToast = React.useCallback(
(props: Omit<ToastType, 'id'>) => {
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 (
<ToastContext.Provider value={contextValue}>
{children}
</ToastContext.Provider>
);
}
export const useToast = () => {
const context = React.useContext(ToastContext);
if (context === undefined) {
throw new Error('useToast must be used within a ToastProvider');
}
return context;
};

View file

@ -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<typeof ToastPrimitives.Viewport>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Viewport
ref={ref}
className={cn(
'fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]',
className,
)}
{...props}
/>
));
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<typeof ToastPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
VariantProps<typeof toastVariants>
>(({ className, variant, ...props }, ref) => {
return (
<ToastPrimitives.Root
ref={ref}
className={cn(toastVariants({ variant }), className)}
{...props}
/>
);
});
Toast.displayName = ToastPrimitives.Root.displayName;
const ToastAction = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Action>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Action
ref={ref}
className={cn(
'inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium ring-offset-background transition-colors hover:bg-secondary focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive',
className,
)}
{...props}
/>
));
ToastAction.displayName = ToastPrimitives.Action.displayName;
const ToastClose = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Close>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Close
ref={ref}
className={cn(
'absolute right-2 top-2 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600',
className,
)}
toast-close=""
{...props}
>
<Cross2Icon className="size-4" />
</ToastPrimitives.Close>
));
ToastClose.displayName = ToastPrimitives.Close.displayName;
const ToastTitle = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Title>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Title
ref={ref}
className={cn('text-sm font-semibold', className)}
{...props}
/>
));
ToastTitle.displayName = ToastPrimitives.Title.displayName;
const ToastDescription = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Description>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Description
ref={ref}
className={cn('text-sm opacity-90', className)}
{...props}
/>
));
ToastDescription.displayName = ToastPrimitives.Description.displayName;
type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>;
type ToastActionElement = React.ReactElement<typeof ToastAction>;
export {
Toast,
ToastAction,
type ToastActionElement,
ToastClose,
ToastDescription,
type ToastProps,
ToastProvider,
ToastTitle,
ToastViewport,
};

View file

@ -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 }) => (
<Toast key={id} {...props}>
<div className="grid gap-1">
{title && <ToastTitle>{title}</ToastTitle>}
{description && <ToastDescription>{description}</ToastDescription>}
</div>
{action}
<ToastClose />
</Toast>
))}
</>
);
}

View file

@ -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 ({

View file

@ -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<typeof cloudEventSchema>;
export type TelemetryEvent = z.infer<typeof telemetryEventSchema>;
/**
* TimePeriod
*/
export const timePeriods = [7, 30, 90] as const;
export type TimePeriod = (typeof timePeriods)[number];