Change website layout (#18)
* Add basic experiments page * Isolate experiment components * Fix grid on small screens * Change nav bar * Add padding to logo * Fix linking * Remove right margin on ExperimentCard flask * Change favicon * Use humanize in formatTimePast * Add TODO
1
@types/nextjs-routes.d.ts
vendored
@@ -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<Pathname> {
|
||||
|
||||
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 738 B |
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 828 B |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 7.9 KiB |
|
Before Width: | Height: | Size: 7.9 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 8.5 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 5.4 KiB |
17
public/favicons/safari-pinned-tab.svg
Normal file
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
|
||||
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
|
||||
width="512.000000pt" height="512.000000pt" viewBox="0 0 512.000000 512.000000"
|
||||
preserveAspectRatio="xMidYMid meet">
|
||||
<metadata>
|
||||
Created by potrace 1.14, written by Peter Selinger 2001-2017
|
||||
</metadata>
|
||||
<g transform="translate(0.000000,512.000000) scale(0.100000,-0.100000)"
|
||||
fill="#000000" stroke="none">
|
||||
<path d="M7 5113 c-4 -3 -7 -352 -7 -774 0 -579 3 -770 12 -775 6 -4 226 -8
|
||||
487 -8 l476 -1 1 -1760 c1 -968 2 -1768 3 -1778 1 -16 81 -17 1584 -15 l1582
|
||||
3 0 1775 0 1775 460 2 c253 1 472 2 488 2 l27 1 -2 778 -3 777 -2551 3 c-1403
|
||||
1 -2554 -1 -2557 -5z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 770 B |
108
public/logo.svg
@@ -1,100 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Generator: Adobe Illustrator 24.1.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
|
||||
<svg
|
||||
version="1.1"
|
||||
id="Layer_1"
|
||||
x="0px"
|
||||
y="0px"
|
||||
viewBox="0 0 512 512"
|
||||
style="enable-background:new 0 0 512 512;"
|
||||
xml:space="preserve"
|
||||
sodipodi:docname="logo.svg"
|
||||
inkscape:version="1.2.2 (b0a8486, 2022-12-01)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"><defs
|
||||
id="defs144" /><sodipodi:namedview
|
||||
id="namedview142"
|
||||
pagecolor="#505050"
|
||||
bordercolor="#eeeeee"
|
||||
borderopacity="1"
|
||||
inkscape:showpageshadow="0"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#505050"
|
||||
showgrid="false"
|
||||
inkscape:zoom="0.4609375"
|
||||
inkscape:cx="304.81356"
|
||||
inkscape:cy="-29.288136"
|
||||
inkscape:window-width="1440"
|
||||
inkscape:window-height="847"
|
||||
inkscape:window-x="1080"
|
||||
inkscape:window-y="1105"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="g429" />
|
||||
<style
|
||||
type="text/css"
|
||||
id="style132">
|
||||
.st0{fill:#F39C12;}
|
||||
.st1{fill:#F1C40F;}
|
||||
</style>
|
||||
<g
|
||||
id="g429"
|
||||
transform="rotate(-31.952357,237.34973,219.54994)"><g
|
||||
id="XMLID_1_"
|
||||
transform="matrix(0.75514475,0.02046743,-0.01995659,0.75644772,46.808592,36.430117)">
|
||||
<path
|
||||
id="XMLID_5_"
|
||||
class="st0"
|
||||
d="M 463.2,68.4 C 399.4,4.6 297.8,3 231.6,68.4 c -63.8,63.8 -63.8,167.8 0,231.6 63.8,63.8 167.8,63.8 231.6,0 66.2,-65.4 63.9,-167.8 0,-231.6 z M 424.6,107 c 21.3,21.3 21.3,55.9 0,77.2 -21.3,21.3 -55.9,21.3 -77.2,0 -21.3,-21.3 -21.3,-55.9 0,-77.2 21.3,-21.3 56,-21.3 77.2,0 z" />
|
||||
<path
|
||||
id="XMLID_8_"
|
||||
class="st1"
|
||||
d="m 462.5,48.7 c -63.8,-63.8 -167.8,-63.8 -231.6,0 -63.8,63.8 -63.8,167.8 0,231.6 63.8,63.8 167.8,63.8 231.6,0 63.8,-64.6 63.8,-167.8 0,-231.6 z m -38.6,38.6 c 21.3,21.3 21.3,55.9 0,77.2 -21.3,21.3 -55.9,21.3 -77.2,0 -21.3,-21.3 -21.3,-55.9 0,-77.2 22,-21.3 55.9,-21.3 77.2,0 z" />
|
||||
|
||||
<rect
|
||||
id="XMLID_9_"
|
||||
x="204.10001"
|
||||
y="197.7"
|
||||
transform="matrix(0.7071,-0.7071,0.7071,0.7071,-130.0056,245.4256)"
|
||||
class="st1"
|
||||
width="54.400002"
|
||||
height="163.89999" />
|
||||
<polygon
|
||||
id="XMLID_10_"
|
||||
class="st1"
|
||||
points="193,395.3 250.5,337.8 173.3,260.6 0,434 0,511.2 77.2,511.2 96.1,492.3 77.2,434 134.7,453.6 154.4,434 154.4,395.3 " />
|
||||
<path
|
||||
id="XMLID_14_"
|
||||
class="st0"
|
||||
d="m 231.6,318.1 -77.2,77.2 H 193 l 57.5,-57.5 z m -77.2,77.2 -48.1,48.1 29.1,9.5 18.9,-18.9 c 0.1,0 0.1,-38.7 0.1,-38.7 z m -67.7,67.8 -48.1,48.1 h 38.6 l 18.9,-18.9 z" />
|
||||
<polygon
|
||||
id="XMLID_15_"
|
||||
class="st0"
|
||||
points="193,279.5 0,472.6 0,511.2 77.2,434 211.9,299.2 " />
|
||||
</g></g>
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-style:normal;font-weight:normal;font-size:424.372px;line-height:1.25;font-family:sans-serif;fill:#1a202c;fill-opacity:1;stroke:none;stroke-width:1.06093"
|
||||
x="-51.142395"
|
||||
y="363.65485"
|
||||
id="text475"
|
||||
transform="scale(0.8254529,1.2114562)"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan473"
|
||||
x="-51.142395"
|
||||
y="363.65485"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:424.372px;font-family:'Fira Code';-inkscape-font-specification:'Fira Code';stroke-width:1.06093;fill:#1a202c;fill-opacity:1">{</tspan></text><text
|
||||
xml:space="preserve"
|
||||
style="font-style:normal;font-weight:normal;font-size:424.373px;line-height:1.25;font-family:sans-serif;fill:#1a202c;fill-opacity:1;stroke:none;stroke-width:1.06093"
|
||||
x="-671.40875"
|
||||
y="363.655"
|
||||
id="text475-6"
|
||||
transform="scale(-0.82545186,1.2114577)"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan473-2"
|
||||
x="-671.40875"
|
||||
y="363.655"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:424.373px;font-family:'Fira Code';-inkscape-font-specification:'Fira Code';stroke-width:1.06093;fill:#1a202c;fill-opacity:1">{</tspan></text></svg>
|
||||
<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M97.5238 147.526H414.476V510C414.476 511.105 413.581 512 412.476 512H99.5238C98.4192 512 97.5238 511.105 97.5238 510V147.526Z" fill="black"/>
|
||||
<rect width="512" height="156.203" rx="2" fill="black"/>
|
||||
<rect x="109.714" y="156.203" width="292.571" height="347.119" fill="#FF5733"/>
|
||||
<rect x="12.1905" y="8.67792" width="487.619" height="138.847" fill="#FF5733"/>
|
||||
<rect x="164.571" y="156.203" width="48.7619" height="347.119" fill="white"/>
|
||||
<rect x="128" y="8.67792" width="48.7619" height="138.847" fill="white"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 627 B |
76
src/components/experiments/ExperimentCard.tsx
Normal file
@@ -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 (
|
||||
<Box
|
||||
as={Card}
|
||||
variant="elevated"
|
||||
bg="gray.50"
|
||||
_hover={{ bg: "gray.100" }}
|
||||
transition="background 0.2s"
|
||||
cursor="pointer"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
void router.push({ pathname: "/experiments/[id]", query: { id: exp.id } }, undefined, {
|
||||
shallow: true,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<CardHeader>
|
||||
<HStack w="full" color="gray.700">
|
||||
<Icon as={RiFlaskLine} boxSize={4} />
|
||||
<Text fontWeight="bold">{exp.label}</Text>
|
||||
</HStack>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<HStack w="full" mb={8} spacing={4}>
|
||||
<CountLabel label="Variants" count={exp.promptVariantCount} />
|
||||
<Divider h={12} orientation="vertical" />
|
||||
<CountLabel label="Scenarios" count={exp.testScenarioCount} />
|
||||
</HStack>
|
||||
<HStack w="full" color="gray.500" fontSize="xs">
|
||||
<Text>Created {formatTimePast(exp.createdAt)}</Text>
|
||||
<Divider h={4} orientation="vertical" />
|
||||
<Text>Updated {formatTimePast(exp.updatedAt)}</Text>
|
||||
</HStack>
|
||||
</CardBody>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const CountLabel = ({ label, count }: { label: string; count: number }) => {
|
||||
return (
|
||||
<VStack alignItems="flex-start">
|
||||
<Text color="gray.500" fontWeight="bold">
|
||||
{label}
|
||||
</Text>
|
||||
<Text fontSize="sm" color="gray.500">
|
||||
{count}
|
||||
</Text>
|
||||
</VStack>
|
||||
);
|
||||
};
|
||||
29
src/components/experiments/NewExperimentButton.tsx
Normal file
@@ -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 (
|
||||
<Button
|
||||
onClick={createExperiment}
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
variant="ghost"
|
||||
{...props}
|
||||
>
|
||||
<Icon as={isLoading ? Spinner : BsPlusSquare} boxSize={4} mr={2} />
|
||||
New Experiment
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
@@ -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<BoxProps & { active: boolean | undefined }, "a">(
|
||||
({ 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 (
|
||||
<Box
|
||||
ref={ref}
|
||||
bgColor={active ? "gray.300" : "transparent"}
|
||||
as={Link}
|
||||
href={href}
|
||||
target={target}
|
||||
w="full"
|
||||
bgColor={isActive ? "gray.300" : "transparent"}
|
||||
_hover={{ bgColor: "gray.300" }}
|
||||
borderRadius={4}
|
||||
px={4}
|
||||
py={2}
|
||||
py={4}
|
||||
justifyContent="start"
|
||||
cursor="pointer"
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<HStack w="full" px={4} color="gray.700">
|
||||
<Icon as={icon} boxSize={6} mr={2} />
|
||||
<Text fontWeight="bold">{label}</Text>
|
||||
</HStack>
|
||||
</Box>
|
||||
)
|
||||
);
|
||||
|
||||
ExperimentLink.displayName = "ExperimentLink";
|
||||
|
||||
const Separator = () => <Box h="1px" bgColor="gray.300" />;
|
||||
);
|
||||
};
|
||||
|
||||
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 (
|
||||
<VStack align="stretch" bgColor="gray.100" p={2} pb={0} height="100%">
|
||||
<HStack spacing={0}>
|
||||
<VStack align="stretch" bgColor="gray.100" py={2} pb={0} height="100%">
|
||||
<HStack spacing={0} pl="4">
|
||||
<Image src="/logo.svg" alt="" w={6} h={6} />
|
||||
<Heading size="md" p={2}>
|
||||
OpenPipe
|
||||
</Heading>
|
||||
</HStack>
|
||||
<Separator />
|
||||
<Divider />
|
||||
<VStack spacing={0} align="flex-start" overflowY="auto" flex={1}>
|
||||
<Heading size="xs" textAlign="center" p={2}>
|
||||
Experiments
|
||||
</Heading>
|
||||
<VStack spacing={1} align="stretch" flex={1}>
|
||||
<ExperimentLink
|
||||
onClick={createExperiment}
|
||||
active={false}
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
>
|
||||
<Icon as={BsPlusSquare} boxSize={4} mr={2} />
|
||||
New Experiment
|
||||
</ExperimentLink>
|
||||
|
||||
{experiments?.data?.map((exp) => (
|
||||
<ExperimentLink
|
||||
key={exp.id}
|
||||
as={NextLink}
|
||||
active={exp.id === currentId}
|
||||
href={{ pathname: "/experiments/[id]", query: { id: exp.id } }}
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
>
|
||||
<Icon as={RiFlaskLine} boxSize={4} mr={2} />
|
||||
{exp.label}
|
||||
</ExperimentLink>
|
||||
))}
|
||||
</VStack>
|
||||
<IconLink icon={RiFlaskLine} label="Experiments" href="/experiments" />
|
||||
</VStack>
|
||||
|
||||
<Separator />
|
||||
<HStack align="center" justify="center" spacing={4} p={2}>
|
||||
<Link
|
||||
<Divider />
|
||||
<VStack align="center" spacing={4} p={2}>
|
||||
<IconLink
|
||||
icon={BsGithub}
|
||||
label="GitHub"
|
||||
href="https://github.com/corbt/openpipe"
|
||||
target="_blank"
|
||||
color="gray.500"
|
||||
_hover={{ color: "gray.800" }}
|
||||
>
|
||||
<Icon as={BsGithub} boxSize={8} />
|
||||
</Link>
|
||||
<Link
|
||||
/>
|
||||
<IconLink
|
||||
icon={BsTwitter}
|
||||
label="Twitter"
|
||||
href="https://twitter.com/corbtt"
|
||||
target="_blank"
|
||||
color="gray.500"
|
||||
_hover={{ color: "gray.800" }}
|
||||
>
|
||||
<Icon as={BsTwitter} boxSize={8} />
|
||||
</Link>
|
||||
</HStack>
|
||||
/>
|
||||
</VStack>
|
||||
</VStack>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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() {
|
||||
<HStack px={4} py={2}>
|
||||
<Breadcrumb flex={1}>
|
||||
<BreadcrumbItem>
|
||||
<Flex alignItems="center">
|
||||
<Icon as={RiFlaskLine} boxSize={4} mr={2} /> Experiments
|
||||
</Flex>
|
||||
<BreadcrumbLink href="/experiments">
|
||||
<Flex alignItems="center">
|
||||
<Icon as={RiFlaskLine} boxSize={4} mr={2} /> Experiments
|
||||
</Flex>
|
||||
</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbItem isCurrentPage>
|
||||
<Input
|
||||
|
||||
40
src/pages/experiments/index.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import {
|
||||
SimpleGrid,
|
||||
HStack,
|
||||
Icon,
|
||||
VStack,
|
||||
Breadcrumb,
|
||||
BreadcrumbItem,
|
||||
Flex,
|
||||
} from "@chakra-ui/react";
|
||||
import { RiFlaskLine } from "react-icons/ri";
|
||||
import AppShell from "~/components/nav/AppShell";
|
||||
import { api } from "~/utils/api";
|
||||
import { NewExperimentButton } from "~/components/experiments/NewExperimentButton";
|
||||
import { ExperimentCard } from "~/components/experiments/ExperimentCard";
|
||||
|
||||
export default function ExperimentsPage() {
|
||||
const experiments = api.experiments.list.useQuery();
|
||||
|
||||
return (
|
||||
<AppShell>
|
||||
<VStack alignItems={"flex-start"} m={4}>
|
||||
<HStack w="full" justifyContent="space-between" mb={4}>
|
||||
<Breadcrumb flex={1}>
|
||||
<BreadcrumbItem>
|
||||
<Flex alignItems="center">
|
||||
<Icon as={RiFlaskLine} boxSize={4} mr={2} /> Experiments
|
||||
</Flex>
|
||||
</BreadcrumbItem>
|
||||
</Breadcrumb>
|
||||
<NewExperimentButton mr={4} borderRadius={8} />
|
||||
</HStack>
|
||||
<SimpleGrid w="full" columns={{ sm: 1, md: 2, lg: 3, xl: 4 }} spacing={8} p="4">
|
||||
{experiments?.data?.map((exp) => (
|
||||
<ExperimentCard key={exp.id} exp={exp} />
|
||||
))}
|
||||
</SimpleGrid>
|
||||
</VStack>
|
||||
</AppShell>
|
||||
);
|
||||
}
|
||||
@@ -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 (
|
||||
<AppShell>
|
||||
<Center h="100%">
|
||||
<div>Select an experiment from the sidebar to get started!</div>
|
||||
</Center>
|
||||
</AppShell>
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -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 }) => {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 }) => {
|
||||
|
||||
20
src/utils/dayjs.ts
Normal file
@@ -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'
|
||||
};
|
||||