import { Listbox, Transition } from "@headlessui/react"; import { ChevronUpDownIcon, PlusIcon, TrashIcon, } from "@heroicons/react/24/outline"; import { Fragment, useEffect, useState } from "react"; import ReactDOM from "react-dom"; import { useDispatch, useSelector } from "react-redux"; import { useNavigate } from "react-router-dom"; import styled from "styled-components"; import { defaultBorderRadius, lightGray, vscBackground, vscForeground, vscInputBackground, vscListActiveBackground, vscListActiveForeground, } from ".."; import { defaultModelSelector } from "../../redux/selectors/modelSelectors"; import { setDefaultModel } from "../../redux/slices/stateSlice"; import { setDialogMessage, setShowDialog, } from "../../redux/slices/uiStateSlice"; import { RootState } from "../../redux/store"; import { getMetaKeyLabel, isMetaEquivalentKeyPressed } from "../../util"; import { postToIde } from "../../util/ide"; import HeaderButtonWithText from "../HeaderButtonWithText"; import ConfirmationDialog from "../dialogs/ConfirmationDialog"; const GridDiv = styled.div` display: grid; grid-template-columns: 1fr auto; align-items: center; border: 0.5px solid ${lightGray}; border-radius: ${defaultBorderRadius}; overflow: hidden; `; const StyledPlusIcon = styled(PlusIcon)` background-color: ${vscBackground}; cursor: pointer; margin: 0px; padding-left: 4px; padding-right: 4px; height: 100%; &:hover { background-color: ${vscInputBackground}; } border-left: 0.5px solid ${lightGray}; `; const StyledListbox = styled(Listbox)` background-color: ${vscBackground}; padding: 0; min-width: 80px; `; const StyledListboxButton = styled(Listbox.Button)` position: relative; cursor: pointer; background-color: ${vscBackground}; text-align: left; border: none; margin: 0; height: 100%; width: 100%; max-width: 180px; white-space: nowrap; overflow: hidden; display: grid; grid-template-columns: 1fr auto; align-items: center; color: ${vscForeground}; padding: 4px 8px; &:focus { outline: none; } &:hover { background-color: ${vscInputBackground}; } `; const StyledListboxOptions = styled(Listbox.Options)` background-color: ${vscInputBackground}; padding: 0; position: absolute; bottom: calc(100% - 16px); max-width: 100%; max-height: 80vh; border-radius: ${defaultBorderRadius}; overflow-y: scroll; `; const StyledListboxOption = styled(Listbox.Option)<{ selected: boolean }>` background-color: ${({ selected }) => selected ? vscListActiveBackground : vscInputBackground}; cursor: pointer; padding: 6px 8px; &:hover { background-color: ${vscListActiveBackground}; color: ${vscListActiveForeground}; } `; function ListBoxOption({ option, idx, showDelete, }: { option: Option; idx: number; showDelete?: boolean; }) { const dispatch = useDispatch(); const [hovered, setHovered] = useState(false); return ( { setHovered(true); }} onMouseLeave={() => { setHovered(false); }} > {option.title} {hovered && showDelete && ( { dispatch(setShowDialog(true)); dispatch( setDialogMessage( { postToIde("config/deleteModel", { title: option.title }); }} />, ), ); e.stopPropagation(); e.preventDefault(); }} style={{ backgroundColor: vscInputBackground }} className="absolute right-0 p-1" > )} ); } function modelSelectTitle(model: any): string { if (model?.title) return model?.title; if (model?.model !== undefined && model?.model.trim() !== "") { if (model?.class_name) { return `${model?.class_name} - ${model?.model}`; } return model?.model; } return model?.class_name; } interface Option { value: string; title: string; } function ModelSelect(props: {}) { const dispatch = useDispatch(); const defaultModel = useSelector(defaultModelSelector); const allModels = useSelector( (state: RootState) => state.state.config.models, ); const navigate = useNavigate(); const [options, setOptions] = useState([]); useEffect(() => { setOptions( allModels.map((model) => { return { value: model.title, title: modelSelectTitle(model), }; }), ); }, [allModels]); const topDiv = document.getElementById("model-select-top-div"); useEffect(() => { const handleKeyDown = (event: KeyboardEvent) => { if (event.key === "'" && isMetaEquivalentKeyPressed(event)) { const direction = event.shiftKey ? -1 : 1; const currentIndex = options.findIndex( (option) => option.value === defaultModel?.title, ); let nextIndex = (currentIndex + 1 * direction) % options.length; if (nextIndex < 0) nextIndex = options.length - 1; dispatch(setDefaultModel(options[nextIndex].value)); } }; window.addEventListener("keydown", handleKeyDown); return () => { window.removeEventListener("keydown", handleKeyDown); }; }, [options, defaultModel]); return ( <> { if (val === defaultModel?.title) return; dispatch(setDefaultModel(val)); // TODO // client?.setModelForRoleFromTitle("default", val); }} defaultValue={"GPT-4"} > {modelSelectTitle(defaultModel)} {topDiv && ReactDOM.createPortal( {options.map((option, idx) => ( 1} /> ))} {options.length === 0 && No models found} {getMetaKeyLabel()}' to toggle , topDiv, )} { navigate("/models"); }} /> > ); } export default ModelSelect;