diff --git a/@types/nextjs-routes.d.ts b/@types/nextjs-routes.d.ts index 0c8dd88..08e1564 100644 --- a/@types/nextjs-routes.d.ts +++ b/@types/nextjs-routes.d.ts @@ -14,6 +14,7 @@ declare module "nextjs-routes" { | DynamicRoute<"/api/auth/[...nextauth]", { "nextauth": string[] }> | DynamicRoute<"/api/trpc/[trpc]", { "trpc": string }> | DynamicRoute<"/experiments/[id]", { "id": string }> + | StaticRoute<"/experiments"> | StaticRoute<"/">; interface StaticRoute { diff --git a/public/favicon.ico b/public/favicon.ico index b48618a..9069131 100644 Binary files a/public/favicon.ico and b/public/favicon.ico differ diff --git a/public/favicons/android-chrome-192x192.png b/public/favicons/android-chrome-192x192.png index 67a4a5f..c754a4d 100644 Binary files a/public/favicons/android-chrome-192x192.png and b/public/favicons/android-chrome-192x192.png differ diff --git a/public/favicons/android-chrome-512x512.png b/public/favicons/android-chrome-512x512.png index 005d003..7bb045c 100644 Binary files a/public/favicons/android-chrome-512x512.png and b/public/favicons/android-chrome-512x512.png differ diff --git a/public/favicons/apple-touch-icon.png b/public/favicons/apple-touch-icon.png index 441f6d6..008f87d 100644 Binary files a/public/favicons/apple-touch-icon.png and b/public/favicons/apple-touch-icon.png differ diff --git a/public/favicons/favicon-16x16.png b/public/favicons/favicon-16x16.png index 0a399fc..f2f1f88 100644 Binary files a/public/favicons/favicon-16x16.png and b/public/favicons/favicon-16x16.png differ diff --git a/public/favicons/favicon-32x32.png b/public/favicons/favicon-32x32.png index a20c6d1..2bea3ef 100644 Binary files a/public/favicons/favicon-32x32.png and b/public/favicons/favicon-32x32.png differ diff --git a/public/favicons/favicon.ico b/public/favicons/favicon.ico index b48618a..9069131 100644 Binary files a/public/favicons/favicon.ico and b/public/favicons/favicon.ico differ diff --git a/public/favicons/mstile-144x144.png b/public/favicons/mstile-144x144.png deleted file mode 100644 index 25db9a0..0000000 Binary files a/public/favicons/mstile-144x144.png and /dev/null differ diff --git a/public/favicons/mstile-150x150.png b/public/favicons/mstile-150x150.png index 85e4e1f..1e7c379 100644 Binary files a/public/favicons/mstile-150x150.png and b/public/favicons/mstile-150x150.png differ diff --git a/public/favicons/mstile-310x150.png b/public/favicons/mstile-310x150.png deleted file mode 100644 index d2dda82..0000000 Binary files a/public/favicons/mstile-310x150.png and /dev/null differ diff --git a/public/favicons/mstile-310x310.png b/public/favicons/mstile-310x310.png deleted file mode 100644 index 7fda71e..0000000 Binary files a/public/favicons/mstile-310x310.png and /dev/null differ diff --git a/public/favicons/mstile-70x70.png b/public/favicons/mstile-70x70.png deleted file mode 100644 index f2b42cf..0000000 Binary files a/public/favicons/mstile-70x70.png and /dev/null differ diff --git a/public/favicons/safari-pinned-tab.svg b/public/favicons/safari-pinned-tab.svg new file mode 100644 index 0000000..344b685 --- /dev/null +++ b/public/favicons/safari-pinned-tab.svg @@ -0,0 +1,17 @@ + + + + +Created by potrace 1.14, written by Peter Selinger 2001-2017 + + + + + diff --git a/public/logo.svg b/public/logo.svg index d44df40..9eb2c24 100644 --- a/public/logo.svg +++ b/public/logo.svg @@ -1,100 +1,8 @@ - - - - - - - - - - - - - - -{{ + + + + + + + + diff --git a/src/components/experiments/ExperimentCard.tsx b/src/components/experiments/ExperimentCard.tsx new file mode 100644 index 0000000..a9f9cb7 --- /dev/null +++ b/src/components/experiments/ExperimentCard.tsx @@ -0,0 +1,76 @@ +import { + Card, + CardBody, + HStack, + Icon, + VStack, + Text, + CardHeader, + Divider, + Box, +} from "@chakra-ui/react"; +import { RiFlaskLine } from "react-icons/ri"; +import { formatTimePast } from "~/utils/dayjs"; +import { useRouter } from "next/router"; + +type ExperimentData = { + testScenarioCount: number; + promptVariantCount: number; + id: string; + label: string; + sortIndex: number; + createdAt: Date; + updatedAt: Date; +}; + +export const ExperimentCard = ({ exp }: { exp: ExperimentData }) => { + const router = useRouter(); + return ( + { + e.preventDefault(); + void router.push({ pathname: "/experiments/[id]", query: { id: exp.id } }, undefined, { + shallow: true, + }); + }} + > + + + + {exp.label} + + + + + + + + + + Created {formatTimePast(exp.createdAt)} + + Updated {formatTimePast(exp.updatedAt)} + + + + ); +}; + +const CountLabel = ({ label, count }: { label: string; count: number }) => { + return ( + + + {label} + + + {count} + + + ); +}; diff --git a/src/components/experiments/NewExperimentButton.tsx b/src/components/experiments/NewExperimentButton.tsx new file mode 100644 index 0000000..47f8864 --- /dev/null +++ b/src/components/experiments/NewExperimentButton.tsx @@ -0,0 +1,29 @@ +import { Icon, Button, Spinner, type ButtonProps } from "@chakra-ui/react"; +import { api } from "~/utils/api"; +import { useRouter } from "next/router"; +import { BsPlusSquare } from "react-icons/bs"; +import { useHandledAsyncCallback } from "~/utils/hooks"; + +export const NewExperimentButton = (props: ButtonProps) => { + const router = useRouter(); + const utils = api.useContext(); + const createMutation = api.experiments.create.useMutation(); + const [createExperiment, isLoading] = useHandledAsyncCallback(async () => { + const newExperiment = await createMutation.mutateAsync({ label: "New Experiment" }); + await utils.experiments.list.invalidate(); + await router.push({ pathname: "/experiments/[id]", query: { id: newExperiment.id } }); + }, [createMutation, router]); + + return ( + + ); +}; diff --git a/src/components/nav/AppShell.tsx b/src/components/nav/AppShell.tsx index 13ebf47..a8fe871 100644 --- a/src/components/nav/AppShell.tsx +++ b/src/components/nav/AppShell.tsx @@ -1,119 +1,82 @@ import { - Box, Heading, VStack, Icon, HStack, - type BoxProps, - forwardRef, Image, - Link, Grid, GridItem, + Divider, + Text, + Box, + type BoxProps, + type LinkProps, + Link, } from "@chakra-ui/react"; import Head from "next/head"; -import { api } from "~/utils/api"; -import { BsGithub, BsPlusSquare, BsTwitter } from "react-icons/bs"; -import { RiFlaskLine } from "react-icons/ri"; +import { BsGithub, BsTwitter } from "react-icons/bs"; import { useRouter } from "next/router"; -import NextLink from "next/link"; -import { useHandledAsyncCallback } from "~/utils/hooks"; import PublicPlaygroundWarning from "../PublicPlaygroundWarning"; +import { type IconType } from "react-icons"; +import { RiFlaskLine } from "react-icons/ri"; -const ExperimentLink = forwardRef( - ({ children, active, ...props }, ref) => ( +type IconLinkProps = BoxProps & LinkProps & { label: string; icon: IconType; href: string }; + +const IconLink = ({ icon, label, href, target, ...props }: IconLinkProps) => { + const isActive = useRouter().pathname.startsWith(href); + return ( - {children} + + + {label} + - ) -); - -ExperimentLink.displayName = "ExperimentLink"; - -const Separator = () => ; + ); +}; const NavSidebar = () => { - const experiments = api.experiments.list.useQuery(); - const router = useRouter(); - const utils = api.useContext(); - const currentId = router.query.id as string | undefined; - - const createMutation = api.experiments.create.useMutation(); - const [createExperiment] = useHandledAsyncCallback(async () => { - const newExperiment = await createMutation.mutateAsync({ label: "New Experiment" }); - await utils.experiments.list.invalidate(); - await router.push({ pathname: "/experiments/[id]", query: { id: newExperiment.id } }); - }, [createMutation, router]); - return ( - - + + OpenPipe - + - - Experiments - - - - - New Experiment - - - {experiments?.data?.map((exp) => ( - - - {exp.label} - - ))} - + - - - - + + - - - + - - - + /> + ); }; diff --git a/src/pages/experiments/[id].tsx b/src/pages/experiments/[id].tsx index 231170f..c938cd3 100644 --- a/src/pages/experiments/[id].tsx +++ b/src/pages/experiments/[id].tsx @@ -15,6 +15,7 @@ import { AlertDialogContent, AlertDialogOverlay, useDisclosure, + BreadcrumbLink, } from "@chakra-ui/react"; import { useRouter } from "next/router"; @@ -120,9 +121,11 @@ export default function Experiment() { - - Experiments - + + + Experiments + + + + + + + + Experiments + + + + + + + {experiments?.data?.map((exp) => ( + + ))} + + + + ); +} diff --git a/src/pages/index.tsx b/src/pages/index.tsx index f7e5976..99d5739 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -1,12 +1,15 @@ -import { Center } from "@chakra-ui/react"; -import AppShell from "~/components/nav/AppShell"; +import { type GetServerSideProps } from 'next'; + +// eslint-disable-next-line @typescript-eslint/require-await +export const getServerSideProps: GetServerSideProps = async (context) => { + return { + redirect: { + destination: '/experiments', + permanent: false, + }, + } +} export default function Home() { - return ( - -
-
Select an experiment from the sidebar to get started!
-
-
- ); + return null; } diff --git a/src/server/api/routers/experiments.router.ts b/src/server/api/routers/experiments.router.ts index 4c4e679..ff7b8df 100644 --- a/src/server/api/routers/experiments.router.ts +++ b/src/server/api/routers/experiments.router.ts @@ -4,11 +4,38 @@ import { prisma } from "~/server/db"; export const experimentsRouter = createTRPCRouter({ list: publicProcedure.query(async () => { - return await prisma.experiment.findMany({ + const experiments = await prisma.experiment.findMany({ orderBy: { sortIndex: "asc", }, }); + + // TODO: look for cleaner way to do this. Maybe aggregate? + const experimentsWithCounts = await Promise.all( + experiments.map(async (experiment) => { + const visibleTestScenarioCount = await prisma.testScenario.count({ + where: { + experimentId: experiment.id, + visible: true, + }, + }); + + const visiblePromptVariantCount = await prisma.promptVariant.count({ + where: { + experimentId: experiment.id, + visible: true, + }, + }); + + return { + ...experiment, + testScenarioCount: visibleTestScenarioCount, + promptVariantCount: visiblePromptVariantCount, + }; + }), + ); + + return experimentsWithCounts; }), get: publicProcedure.input(z.object({ id: z.string() })).query(async ({ input }) => { diff --git a/src/server/api/routers/promptVariants.router.ts b/src/server/api/routers/promptVariants.router.ts index 4d3c374..2e7db3e 100644 --- a/src/server/api/routers/promptVariants.router.ts +++ b/src/server/api/routers/promptVariants.router.ts @@ -76,7 +76,7 @@ export const promptVariantsRouter = createTRPCRouter({ }) ) .mutation(async ({ input }) => { - const lastScenario = await prisma.promptVariant.findFirst({ + const lastVariant = await prisma.promptVariant.findFirst({ where: { experimentId: input.experimentId, visible: true, @@ -98,16 +98,25 @@ export const promptVariantsRouter = createTRPCRouter({ }) )._max?.sortIndex ?? 0; - const newScenario = await prisma.promptVariant.create({ + const newVariant = await prisma.promptVariant.create({ data: { experimentId: input.experimentId, label: `Prompt Variant ${largestSortIndex + 2}`, - sortIndex: (lastScenario?.sortIndex ?? 0) + 1, - config: lastScenario?.config ?? {}, + sortIndex: (lastVariant?.sortIndex ?? 0) + 1, + config: lastVariant?.config ?? {}, }, }); - return newScenario; + await prisma.experiment.update({ + where: { + id: input.experimentId, + }, + data: { + updatedAt: new Date(), + }, + }); + + return newVariant; }), update: publicProcedure diff --git a/src/server/api/routers/scenarios.router.ts b/src/server/api/routers/scenarios.router.ts index 6747f44..e5df5a3 100644 --- a/src/server/api/routers/scenarios.router.ts +++ b/src/server/api/routers/scenarios.router.ts @@ -45,6 +45,15 @@ export const scenariosRouter = createTRPCRouter({ : {}, }, }); + + await prisma.experiment.update({ + where: { + id: input.experimentId, + }, + data: { + updatedAt: new Date(), + }, + }); }), hide: publicProcedure.input(z.object({ id: z.string() })).mutation(async ({ input }) => { diff --git a/src/utils/dayjs.ts b/src/utils/dayjs.ts new file mode 100644 index 0000000..705a5dc --- /dev/null +++ b/src/utils/dayjs.ts @@ -0,0 +1,20 @@ +import dayjs from "dayjs"; +import duration from "dayjs/plugin/duration"; +import relativeTime from "dayjs/plugin/relativeTime"; + +dayjs.extend(duration); +dayjs.extend(relativeTime); + +export const formatTimePast = (date: Date) => { + const now = dayjs(); + const dayDiff = Math.floor(now.diff(date, "day")); + if (dayDiff > 0) return dayjs.duration(-dayDiff, "days").humanize(true); + + const hourDiff = Math.floor(now.diff(date, "hour")); + if (hourDiff > 0) return dayjs.duration(-hourDiff, "hours").humanize(true); + + const minuteDiff = Math.floor(now.diff(date, "minute")); + if (minuteDiff > 0) return dayjs.duration(-minuteDiff, "minutes").humanize(true); + + return 'a few seconds ago' +};