diff --git a/package.json b/package.json index 075dcd9..14e4d6d 100644 --- a/package.json +++ b/package.json @@ -61,6 +61,7 @@ "react": "18.2.0", "react-dom": "18.2.0", "react-icons": "^4.10.1", + "react-select": "^5.7.4", "react-syntax-highlighter": "^15.5.0", "react-textarea-autosize": "^8.5.0", "socket.io": "^4.7.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 22c5766..3472e54 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,4 +1,4 @@ -lockfileVersion: '6.1' +lockfileVersion: '6.0' settings: autoInstallPeers: true @@ -128,6 +128,9 @@ dependencies: react-icons: specifier: ^4.10.1 version: 4.10.1(react@18.2.0) + react-select: + specifier: ^5.7.4 + version: 5.7.4(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0) react-syntax-highlighter: specifier: ^15.5.0 version: 15.5.0(react@18.2.0) @@ -2248,6 +2251,16 @@ packages: engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dev: true + /@floating-ui/core@1.3.1: + resolution: {integrity: sha512-Bu+AMaXNjrpjh41znzHqaz3r2Nr8hHuHZT6V2LBKMhyMl0FgKA62PNYbqnfgmzOhoWZj70Zecisbo4H1rotP5g==} + dev: false + + /@floating-ui/dom@1.4.5: + resolution: {integrity: sha512-96KnRWkRnuBSSFbj0sFGwwOUd8EkiecINVl0O9wiZlZ64EkpyAOG3Xc2vKKNJmru0Z7RqWNymA+6b8OZqjgyyw==} + dependencies: + '@floating-ui/core': 1.3.1 + dev: false + /@graphile/logger@0.2.0: resolution: {integrity: sha512-jjcWBokl9eb1gVJ85QmoaQ73CQ52xAaOCF29ukRbYNl6lY+ts0ErTaDYOBlejcbUs2OpaiqYLO5uDhyLFzWw4w==} dev: false @@ -2832,6 +2845,12 @@ packages: '@types/react': 18.2.6 dev: true + /@types/react-transition-group@4.4.6: + resolution: {integrity: sha512-VnCdSxfcm08KjsJVQcfBmhEQAPnLB8G08hAxn39azX1qYBQ/5RVQuoHuKIcfKOdncuaUvEpFKFzEvbtIMsfVew==} + dependencies: + '@types/react': 18.2.6 + dev: false + /@types/react@18.2.6: resolution: {integrity: sha512-wRZClXn//zxCFW+ye/D2qY65UsYP1Fpex2YXorHc8awoNamkMZSvBxwxdYVInsHOZZd2Ppq8isnSzJL5Mpf8OA==} dependencies: @@ -3896,6 +3915,13 @@ packages: esutils: 2.0.3 dev: true + /dom-helpers@5.2.1: + resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==} + dependencies: + '@babel/runtime': 7.22.6 + csstype: 3.1.2 + dev: false + /dotenv@16.3.1: resolution: {integrity: sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==} engines: {node: '>=12'} @@ -5451,6 +5477,10 @@ packages: engines: {node: '>= 0.6'} dev: false + /memoize-one@6.0.0: + resolution: {integrity: sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==} + dev: false + /merge-descriptors@1.0.1: resolution: {integrity: sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==} dev: false @@ -6370,6 +6400,27 @@ packages: use-sidecar: 1.1.2(@types/react@18.2.6)(react@18.2.0) dev: false + /react-select@5.7.4(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-NhuE56X+p9QDFh4BgeygHFIvJJszO1i1KSkg/JPcIJrbovyRtI+GuOEa4XzFCEpZRAEoEI8u/cAHK+jG/PgUzQ==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + dependencies: + '@babel/runtime': 7.22.6 + '@emotion/cache': 11.11.0 + '@emotion/react': 11.11.1(@types/react@18.2.6)(react@18.2.0) + '@floating-ui/dom': 1.4.5 + '@types/react-transition-group': 4.4.6 + memoize-one: 6.0.0 + prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-transition-group: 4.4.5(react-dom@18.2.0)(react@18.2.0) + use-isomorphic-layout-effect: 1.1.2(@types/react@18.2.6)(react@18.2.0) + transitivePeerDependencies: + - '@types/react' + dev: false + /react-ssr-prepass@1.5.0(react@18.2.0): resolution: {integrity: sha512-yFNHrlVEReVYKsLI5lF05tZoHveA5pGzjFbFJY/3pOqqjGOmMmqx83N4hIjN2n6E1AOa+eQEUxs3CgRnPmT0RQ==} peerDependencies: @@ -6422,6 +6473,20 @@ packages: - '@types/react' dev: false + /react-transition-group@4.4.5(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==} + peerDependencies: + react: '>=16.6.0' + react-dom: '>=16.6.0' + dependencies: + '@babel/runtime': 7.22.6 + dom-helpers: 5.2.1 + loose-envify: 1.4.0 + prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /react@18.2.0: resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==} engines: {node: '>=0.10.0'} diff --git a/src/components/OutputsTable/OutputCell/OutputCell.tsx b/src/components/OutputsTable/OutputCell/OutputCell.tsx index a398064..0f02d4b 100644 --- a/src/components/OutputsTable/OutputCell/OutputCell.tsx +++ b/src/components/OutputsTable/OutputCell/OutputCell.tsx @@ -106,7 +106,7 @@ export default function OutputCell({ h="100%" fontSize="xs" flexWrap="wrap" - overflowX="auto" + overflowX="hidden" justifyContent="space-between" > diff --git a/src/components/OutputsTable/index.tsx b/src/components/OutputsTable/index.tsx index d35e327..00bd471 100644 --- a/src/components/OutputsTable/index.tsx +++ b/src/components/OutputsTable/index.tsx @@ -4,7 +4,7 @@ import NewScenarioButton from "./NewScenarioButton"; import NewVariantButton from "./NewVariantButton"; import ScenarioRow from "./ScenarioRow"; import VariantEditor from "./VariantEditor"; -import VariantHeader from "./VariantHeader"; +import VariantHeader from "../VariantHeader/VariantHeader"; import VariantStats from "./VariantStats"; import { ScenariosHeader } from "./ScenariosHeader"; import { stickyHeaderStyle } from "./styles"; diff --git a/src/components/SelectModelModal/ModelStatsCard.tsx b/src/components/SelectModelModal/ModelStatsCard.tsx new file mode 100644 index 0000000..282a021 --- /dev/null +++ b/src/components/SelectModelModal/ModelStatsCard.tsx @@ -0,0 +1,83 @@ +import { + Heading, + VStack, + Text, + HStack, + type StackProps, + Icon, + Button, + GridItem, + SimpleGrid, +} from "@chakra-ui/react"; +import { BsChevronRight } from "react-icons/bs"; +import { modelStats } from "~/server/modelStats"; +import { type SupportedModel } from "~/server/types"; + +export const ModelStatsCard = ({ label, model }: { label: string; model: SupportedModel }) => { + const stats = modelStats[model]; + return ( + + + + {label} + + + + + + {model} + + {stats.provider} + + + + + ${(stats.promptTokenPrice * 1000).toFixed(3)} + / 1K tokens + + } + /> + + ${(stats.promptTokenPrice * 1000).toFixed(3)} + / 1K tokens + + } + /> + {stats.speed}} /> + + + ); +}; + +const SelectedModelLabeledInfo = ({ + label, + info, + ...props +}: { + label: string; + info: string | number | React.ReactElement; +} & StackProps) => ( + + + {label} + {info} + + +); diff --git a/src/components/SelectModelModal/SelectModelModal.tsx b/src/components/SelectModelModal/SelectModelModal.tsx new file mode 100644 index 0000000..1708e7d --- /dev/null +++ b/src/components/SelectModelModal/SelectModelModal.tsx @@ -0,0 +1,77 @@ +import { + Button, + Modal, + ModalBody, + ModalCloseButton, + ModalContent, + ModalFooter, + ModalHeader, + ModalOverlay, + VStack, + Text, + Spinner, +} from "@chakra-ui/react"; +import { useState } from "react"; +import { type SupportedModel } from "~/server/types"; +import { ModelStatsCard } from "./ModelStatsCard"; +import { SelectModelSearch } from "./SelectModelSearch"; +import { api } from "~/utils/api"; +import { useExperiment, useHandledAsyncCallback } from "~/utils/hooks"; + +export const SelectModelModal = ({ + originalModel, + variantId, + onClose, +}: { + originalModel: SupportedModel; + variantId: string; + onClose: () => void; +}) => { + const [selectedModel, setSelectedModel] = useState(originalModel); + const utils = api.useContext(); + + const experiment = useExperiment(); + + const duplicateMutation = api.promptVariants.create.useMutation(); + + const [createNewVariant, creationInProgress] = useHandledAsyncCallback(async () => { + if (!experiment?.data?.id) return; + await duplicateMutation.mutateAsync({ + experimentId: experiment?.data?.id, + variantId, + newModel: selectedModel, + }); + await utils.promptVariants.list.invalidate(); + onClose(); + }, [duplicateMutation, experiment?.data?.id, variantId, onClose]); + + return ( + + + + Select a New Model + + + + + {originalModel !== selectedModel && ( + + )} + + + + + + + + + + ); +}; diff --git a/src/components/SelectModelModal/SelectModelSearch.tsx b/src/components/SelectModelModal/SelectModelSearch.tsx new file mode 100644 index 0000000..5b79af8 --- /dev/null +++ b/src/components/SelectModelModal/SelectModelSearch.tsx @@ -0,0 +1,47 @@ +import { VStack, Text } from "@chakra-ui/react"; +import { type LegacyRef, useCallback } from "react"; +import Select, { type SingleValue } from "react-select"; +import { type SupportedModel } from "~/server/types"; +import { useElementDimensions } from "~/utils/hooks"; + +const modelOptions: { value: SupportedModel; label: string }[] = [ + { value: "gpt-3.5-turbo", label: "gpt-3.5-turbo" }, + { value: "gpt-3.5-turbo-0613", label: "gpt-3.5-turbo-0613" }, + { value: "gpt-3.5-turbo-16k", label: "gpt-3.5-turbo-16k" }, + { value: "gpt-3.5-turbo-16k-0613", label: "gpt-3.5-turbo-16k-0613" }, + { value: "gpt-4", label: "gpt-4" }, + { value: "gpt-4-0613", label: "gpt-4-0613" }, + { value: "gpt-4-32k", label: "gpt-4-32k" }, + { value: "gpt-4-32k-0613", label: "gpt-4-32k-0613" }, +]; + +export const SelectModelSearch = ({ + selectedModel, + setSelectedModel, +}: { + selectedModel: SupportedModel; + setSelectedModel: (model: SupportedModel) => void; +}) => { + const handleSelection = useCallback( + (option: SingleValue<{ value: SupportedModel; label: string }>) => { + if (!option) return; + setSelectedModel(option.value); + }, + [setSelectedModel], + ); + const selectedOption = modelOptions.find((option) => option.value === selectedModel); + + const [containerRef, containerDimensions] = useElementDimensions(); + + return ( + } w="full"> + Browse Models +