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/auth/[...nextauth]", { "nextauth": string[] }>
|
||||||
| DynamicRoute<"/api/trpc/[trpc]", { "trpc": string }>
|
| DynamicRoute<"/api/trpc/[trpc]", { "trpc": string }>
|
||||||
| DynamicRoute<"/experiments/[id]", { "id": string }>
|
| DynamicRoute<"/experiments/[id]", { "id": string }>
|
||||||
|
| StaticRoute<"/experiments">
|
||||||
| StaticRoute<"/">;
|
| StaticRoute<"/">;
|
||||||
|
|
||||||
interface StaticRoute<Pathname> {
|
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"?>
|
<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<!-- Generator: Adobe Illustrator 24.1.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
<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"/>
|
||||||
<svg
|
<rect x="109.714" y="156.203" width="292.571" height="347.119" fill="#FF5733"/>
|
||||||
version="1.1"
|
<rect x="12.1905" y="8.67792" width="487.619" height="138.847" fill="#FF5733"/>
|
||||||
id="Layer_1"
|
<rect x="164.571" y="156.203" width="48.7619" height="347.119" fill="white"/>
|
||||||
x="0px"
|
<rect x="128" y="8.67792" width="48.7619" height="138.847" fill="white"/>
|
||||||
y="0px"
|
</svg>
|
||||||
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>
|
|
||||||
|
|||||||
|
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 {
|
import {
|
||||||
Box,
|
|
||||||
Heading,
|
Heading,
|
||||||
VStack,
|
VStack,
|
||||||
Icon,
|
Icon,
|
||||||
HStack,
|
HStack,
|
||||||
type BoxProps,
|
|
||||||
forwardRef,
|
|
||||||
Image,
|
Image,
|
||||||
Link,
|
|
||||||
Grid,
|
Grid,
|
||||||
GridItem,
|
GridItem,
|
||||||
|
Divider,
|
||||||
|
Text,
|
||||||
|
Box,
|
||||||
|
type BoxProps,
|
||||||
|
type LinkProps,
|
||||||
|
Link,
|
||||||
} from "@chakra-ui/react";
|
} from "@chakra-ui/react";
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import { api } from "~/utils/api";
|
import { BsGithub, BsTwitter } from "react-icons/bs";
|
||||||
import { BsGithub, BsPlusSquare, BsTwitter } from "react-icons/bs";
|
|
||||||
import { RiFlaskLine } from "react-icons/ri";
|
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import NextLink from "next/link";
|
|
||||||
import { useHandledAsyncCallback } from "~/utils/hooks";
|
|
||||||
import PublicPlaygroundWarning from "../PublicPlaygroundWarning";
|
import PublicPlaygroundWarning from "../PublicPlaygroundWarning";
|
||||||
|
import { type IconType } from "react-icons";
|
||||||
|
import { RiFlaskLine } from "react-icons/ri";
|
||||||
|
|
||||||
const ExperimentLink = forwardRef<BoxProps & { active: boolean | undefined }, "a">(
|
type IconLinkProps = BoxProps & LinkProps & { label: string; icon: IconType; href: string };
|
||||||
({ children, active, ...props }, ref) => (
|
|
||||||
|
const IconLink = ({ icon, label, href, target, ...props }: IconLinkProps) => {
|
||||||
|
const isActive = useRouter().pathname.startsWith(href);
|
||||||
|
return (
|
||||||
<Box
|
<Box
|
||||||
ref={ref}
|
as={Link}
|
||||||
bgColor={active ? "gray.300" : "transparent"}
|
href={href}
|
||||||
|
target={target}
|
||||||
|
w="full"
|
||||||
|
bgColor={isActive ? "gray.300" : "transparent"}
|
||||||
_hover={{ bgColor: "gray.300" }}
|
_hover={{ bgColor: "gray.300" }}
|
||||||
borderRadius={4}
|
py={4}
|
||||||
px={4}
|
|
||||||
py={2}
|
|
||||||
justifyContent="start"
|
justifyContent="start"
|
||||||
cursor="pointer"
|
cursor="pointer"
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
<HStack w="full" px={4} color="gray.700">
|
||||||
|
<Icon as={icon} boxSize={6} mr={2} />
|
||||||
|
<Text fontWeight="bold">{label}</Text>
|
||||||
|
</HStack>
|
||||||
</Box>
|
</Box>
|
||||||
)
|
);
|
||||||
);
|
};
|
||||||
|
|
||||||
ExperimentLink.displayName = "ExperimentLink";
|
|
||||||
|
|
||||||
const Separator = () => <Box h="1px" bgColor="gray.300" />;
|
|
||||||
|
|
||||||
const NavSidebar = () => {
|
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 (
|
return (
|
||||||
<VStack align="stretch" bgColor="gray.100" p={2} pb={0} height="100%">
|
<VStack align="stretch" bgColor="gray.100" py={2} pb={0} height="100%">
|
||||||
<HStack spacing={0}>
|
<HStack spacing={0} pl="4">
|
||||||
<Image src="/logo.svg" alt="" w={6} h={6} />
|
<Image src="/logo.svg" alt="" w={6} h={6} />
|
||||||
<Heading size="md" p={2}>
|
<Heading size="md" p={2}>
|
||||||
OpenPipe
|
OpenPipe
|
||||||
</Heading>
|
</Heading>
|
||||||
</HStack>
|
</HStack>
|
||||||
<Separator />
|
<Divider />
|
||||||
<VStack spacing={0} align="flex-start" overflowY="auto" flex={1}>
|
<VStack spacing={0} align="flex-start" overflowY="auto" flex={1}>
|
||||||
<Heading size="xs" textAlign="center" p={2}>
|
<IconLink icon={RiFlaskLine} label="Experiments" href="/experiments" />
|
||||||
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>
|
|
||||||
</VStack>
|
</VStack>
|
||||||
|
<Divider />
|
||||||
<Separator />
|
<VStack align="center" spacing={4} p={2}>
|
||||||
<HStack align="center" justify="center" spacing={4} p={2}>
|
<IconLink
|
||||||
<Link
|
icon={BsGithub}
|
||||||
|
label="GitHub"
|
||||||
href="https://github.com/corbt/openpipe"
|
href="https://github.com/corbt/openpipe"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
color="gray.500"
|
color="gray.500"
|
||||||
_hover={{ color: "gray.800" }}
|
_hover={{ color: "gray.800" }}
|
||||||
>
|
/>
|
||||||
<Icon as={BsGithub} boxSize={8} />
|
<IconLink
|
||||||
</Link>
|
icon={BsTwitter}
|
||||||
<Link
|
label="Twitter"
|
||||||
href="https://twitter.com/corbtt"
|
href="https://twitter.com/corbtt"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
color="gray.500"
|
color="gray.500"
|
||||||
_hover={{ color: "gray.800" }}
|
_hover={{ color: "gray.800" }}
|
||||||
>
|
/>
|
||||||
<Icon as={BsTwitter} boxSize={8} />
|
</VStack>
|
||||||
</Link>
|
|
||||||
</HStack>
|
|
||||||
</VStack>
|
</VStack>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
AlertDialogContent,
|
AlertDialogContent,
|
||||||
AlertDialogOverlay,
|
AlertDialogOverlay,
|
||||||
useDisclosure,
|
useDisclosure,
|
||||||
|
BreadcrumbLink,
|
||||||
} from "@chakra-ui/react";
|
} from "@chakra-ui/react";
|
||||||
|
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
@@ -120,9 +121,11 @@ export default function Experiment() {
|
|||||||
<HStack px={4} py={2}>
|
<HStack px={4} py={2}>
|
||||||
<Breadcrumb flex={1}>
|
<Breadcrumb flex={1}>
|
||||||
<BreadcrumbItem>
|
<BreadcrumbItem>
|
||||||
<Flex alignItems="center">
|
<BreadcrumbLink href="/experiments">
|
||||||
<Icon as={RiFlaskLine} boxSize={4} mr={2} /> Experiments
|
<Flex alignItems="center">
|
||||||
</Flex>
|
<Icon as={RiFlaskLine} boxSize={4} mr={2} /> Experiments
|
||||||
|
</Flex>
|
||||||
|
</BreadcrumbLink>
|
||||||
</BreadcrumbItem>
|
</BreadcrumbItem>
|
||||||
<BreadcrumbItem isCurrentPage>
|
<BreadcrumbItem isCurrentPage>
|
||||||
<Input
|
<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 { type GetServerSideProps } from 'next';
|
||||||
import AppShell from "~/components/nav/AppShell";
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/require-await
|
||||||
|
export const getServerSideProps: GetServerSideProps = async (context) => {
|
||||||
|
return {
|
||||||
|
redirect: {
|
||||||
|
destination: '/experiments',
|
||||||
|
permanent: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
return (
|
return null;
|
||||||
<AppShell>
|
|
||||||
<Center h="100%">
|
|
||||||
<div>Select an experiment from the sidebar to get started!</div>
|
|
||||||
</Center>
|
|
||||||
</AppShell>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,11 +4,38 @@ import { prisma } from "~/server/db";
|
|||||||
|
|
||||||
export const experimentsRouter = createTRPCRouter({
|
export const experimentsRouter = createTRPCRouter({
|
||||||
list: publicProcedure.query(async () => {
|
list: publicProcedure.query(async () => {
|
||||||
return await prisma.experiment.findMany({
|
const experiments = await prisma.experiment.findMany({
|
||||||
orderBy: {
|
orderBy: {
|
||||||
sortIndex: "asc",
|
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 }) => {
|
get: publicProcedure.input(z.object({ id: z.string() })).query(async ({ input }) => {
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ export const promptVariantsRouter = createTRPCRouter({
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
const lastScenario = await prisma.promptVariant.findFirst({
|
const lastVariant = await prisma.promptVariant.findFirst({
|
||||||
where: {
|
where: {
|
||||||
experimentId: input.experimentId,
|
experimentId: input.experimentId,
|
||||||
visible: true,
|
visible: true,
|
||||||
@@ -98,16 +98,25 @@ export const promptVariantsRouter = createTRPCRouter({
|
|||||||
})
|
})
|
||||||
)._max?.sortIndex ?? 0;
|
)._max?.sortIndex ?? 0;
|
||||||
|
|
||||||
const newScenario = await prisma.promptVariant.create({
|
const newVariant = await prisma.promptVariant.create({
|
||||||
data: {
|
data: {
|
||||||
experimentId: input.experimentId,
|
experimentId: input.experimentId,
|
||||||
label: `Prompt Variant ${largestSortIndex + 2}`,
|
label: `Prompt Variant ${largestSortIndex + 2}`,
|
||||||
sortIndex: (lastScenario?.sortIndex ?? 0) + 1,
|
sortIndex: (lastVariant?.sortIndex ?? 0) + 1,
|
||||||
config: lastScenario?.config ?? {},
|
config: lastVariant?.config ?? {},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return newScenario;
|
await prisma.experiment.update({
|
||||||
|
where: {
|
||||||
|
id: input.experimentId,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
updatedAt: new Date(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return newVariant;
|
||||||
}),
|
}),
|
||||||
|
|
||||||
update: publicProcedure
|
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 }) => {
|
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'
|
||||||
|
};
|
||||||