mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
React Query + Updated @shadcn/ui components (#17)
This commit is contained in:
parent
f45e93b9ec
commit
5fd53e9ee2
31 changed files with 537 additions and 654 deletions
|
|
@ -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
87
pnpm-lock.yaml
generated
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 />
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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 />;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import {
|
|||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@/components/ui/table';
|
||||
} from '@/components/ui';
|
||||
|
||||
interface DataTableProps<TData, TValue> {
|
||||
columns: ColumnDef<TData, TValue>[];
|
||||
27
src/components/layout/ReactQueryProvider.tsx
Normal file
27
src/components/layout/ReactQueryProvider.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
},
|
||||
};
|
||||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
|
|
|
|||
25
src/components/ui/sonner.tsx
Normal file
25
src/components/ui/sonner.tsx
Normal 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 };
|
||||
|
|
@ -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;
|
||||
};
|
||||
|
|
@ -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,
|
||||
};
|
||||
|
|
@ -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>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -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 ({
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue