diff options
Diffstat (limited to 'extension/react-app/src')
-rw-r--r-- | extension/react-app/src/App.tsx | 5 | ||||
-rw-r--r-- | extension/react-app/src/components/HeaderButtonWithText.tsx | 4 | ||||
-rw-r--r-- | extension/react-app/src/components/InfoHover.tsx | 19 | ||||
-rw-r--r-- | extension/react-app/src/components/Layout.tsx | 9 | ||||
-rw-r--r-- | extension/react-app/src/components/ModelSettings.tsx | 107 | ||||
-rw-r--r-- | extension/react-app/src/components/index.ts | 50 | ||||
-rw-r--r-- | extension/react-app/src/hooks/AbstractContinueGUIClientProtocol.ts | 10 | ||||
-rw-r--r-- | extension/react-app/src/hooks/ContinueGUIClientProtocol.ts | 12 | ||||
-rw-r--r-- | extension/react-app/src/pages/settings.tsx | 229 | ||||
-rw-r--r-- | extension/react-app/src/redux/slices/serverStateReducer.ts | 4 |
10 files changed, 436 insertions, 13 deletions
diff --git a/extension/react-app/src/App.tsx b/extension/react-app/src/App.tsx index 05b322ff..65ad1ddd 100644 --- a/extension/react-app/src/App.tsx +++ b/extension/react-app/src/App.tsx @@ -17,6 +17,7 @@ import { setHighlightedCode } from "./redux/slices/miscSlice"; import { postVscMessage } from "./vscode"; import { createBrowserRouter, RouterProvider } from "react-router-dom"; import ErrorPage from "./pages/error"; +import SettingsPage from "./pages/settings"; const router = createBrowserRouter([ { @@ -36,6 +37,10 @@ const router = createBrowserRouter([ path: "/history", element: <History />, }, + { + path: "/settings", + element: <SettingsPage />, + }, ], }, ]); diff --git a/extension/react-app/src/components/HeaderButtonWithText.tsx b/extension/react-app/src/components/HeaderButtonWithText.tsx index bcd36972..3122c287 100644 --- a/extension/react-app/src/components/HeaderButtonWithText.tsx +++ b/extension/react-app/src/components/HeaderButtonWithText.tsx @@ -1,7 +1,5 @@ import React, { useState } from "react"; -import { Tooltip } from "react-tooltip"; -import styled from "styled-components"; -import { HeaderButton, StyledTooltip, defaultBorderRadius } from "."; +import { HeaderButton, StyledTooltip } from "."; interface HeaderButtonWithTextProps { text: string; diff --git a/extension/react-app/src/components/InfoHover.tsx b/extension/react-app/src/components/InfoHover.tsx new file mode 100644 index 00000000..2cb8ad71 --- /dev/null +++ b/extension/react-app/src/components/InfoHover.tsx @@ -0,0 +1,19 @@ +import { InformationCircleIcon } from "@heroicons/react/24/outline"; +import { StyledTooltip } from "."; + +const InfoHover = ({ msg }: { msg: string }) => { + const id = "info-hover"; + + return ( + <> + <InformationCircleIcon + data-tooltip-id={id} + data-tooltip-content={msg} + className="h-5 w-5 text-gray-500 cursor-help" + /> + <StyledTooltip id={id} place="bottom" /> + </> + ); +}; + +export default InfoHover; diff --git a/extension/react-app/src/components/Layout.tsx b/extension/react-app/src/components/Layout.tsx index cec3f8e1..c0f0929b 100644 --- a/extension/react-app/src/components/Layout.tsx +++ b/extension/react-app/src/components/Layout.tsx @@ -18,6 +18,7 @@ import { BookOpenIcon, ChatBubbleOvalLeftEllipsisIcon, SparklesIcon, + Cog6ToothIcon, } from "@heroicons/react/24/outline"; import HeaderButtonWithText from "./HeaderButtonWithText"; import { useNavigate } from "react-router-dom"; @@ -193,6 +194,14 @@ const Layout = () => { <ChatBubbleOvalLeftEllipsisIcon width="1.4em" height="1.4em" /> </HeaderButtonWithText> </a> + <HeaderButtonWithText + onClick={() => { + navigate("/settings"); + }} + text="Settings" + > + <Cog6ToothIcon width="1.4em" height="1.4em" /> + </HeaderButtonWithText> </Footer> </div> </LayoutTopDiv> diff --git a/extension/react-app/src/components/ModelSettings.tsx b/extension/react-app/src/components/ModelSettings.tsx new file mode 100644 index 00000000..99200502 --- /dev/null +++ b/extension/react-app/src/components/ModelSettings.tsx @@ -0,0 +1,107 @@ +import styled from "styled-components"; +import { LLM } from "../../../schema/LLM"; +import { + Label, + Select, + TextInput, + defaultBorderRadius, + lightGray, + vscForeground, +} from "."; +import { useState } from "react"; +import { useFormContext } from "react-hook-form"; + +const Div = styled.div<{ dashed: boolean }>` + border: 1px ${(props) => (props.dashed ? "dashed" : "solid")} ${lightGray}; + border-radius: ${defaultBorderRadius}; + padding: 8px; + margin-bottom: 16px; +`; + +type ModelOption = "api_key" | "model" | "context_length"; + +const DefaultModelOptions: { + [key: string]: { [key in ModelOption]?: string }; +} = { + OpenAI: { + api_key: "", + model: "gpt-4", + }, + MaybeProxyOpenAI: { + api_key: "", + model: "gpt-4", + }, + Anthropic: { + api_key: "", + model: "claude-2", + }, + default: { + api_key: "", + model: "gpt-4", + }, +}; + +function ModelSettings(props: { llm: any | undefined; role: string }) { + const [modelOptions, setModelOptions] = useState<{ + [key in ModelOption]?: string; + }>(DefaultModelOptions[props.llm?.class_name || "default"]); + + const { register, setValue, getValues } = useFormContext(); + + return ( + <Div dashed={typeof props.llm === undefined}> + {props.llm ? ( + <> + <b>{props.role}</b>: <b> {props.llm.class_name || "gpt-4"}</b> + <form> + {typeof modelOptions.api_key !== undefined && ( + <> + <Label>API Key</Label> + <TextInput + type="text" + defaultValue={props.llm.api_key} + placeholder="API Key" + {...register(`models.${props.role}.api_key`)} + /> + </> + )} + {modelOptions.model && ( + <> + <Label>Model</Label> + <TextInput + type="text" + defaultValue={props.llm.model} + placeholder="Model" + {...register(`models.${props.role}.model`)} + /> + </> + )} + </form> + </> + ) : ( + <div> + <b>Add Model</b> + <div className="my-4"> + <Select + defaultValue="" + onChange={(e) => { + if (e.target.value) { + e.target.value = ""; + } + }} + > + <option disabled value=""> + Select Model Type + </option> + <option value="newModel1">New Model 1</option> + <option value="newModel2">New Model 2</option> + <option value="newModel3">New Model 3</option> + </Select> + </div> + </div> + )} + </Div> + ); +} + +export default ModelSettings; diff --git a/extension/react-app/src/components/index.ts b/extension/react-app/src/components/index.ts index 6705ceb2..f2e154bc 100644 --- a/extension/react-app/src/components/index.ts +++ b/extension/react-app/src/components/index.ts @@ -40,21 +40,29 @@ export const StyledTooltip = styled(Tooltip)` padding-left: 12px; padding-right: 12px; z-index: 100; + + max-width: 80vw; `; export const TextArea = styled.textarea` - width: 100%; + padding: 8px; + font-family: inherit; border-radius: ${defaultBorderRadius}; - border: none; + margin: 16px auto; + height: auto; + width: calc(100% - 32px); background-color: ${secondaryDark}; - resize: vertical; - - padding: 4px; - caret-color: ${vscForeground}; - color: #{vscForeground}; + color: ${vscForeground}; + z-index: 1; + border: 1px solid transparent; &:focus { - outline: 1px solid ${buttonColor}; + outline: 1px solid ${lightGray}; + border: 1px solid transparent; + } + + &::placeholder { + color: ${lightGray}80; } `; @@ -84,11 +92,33 @@ export const H3 = styled.h3` export const TextInput = styled.input.attrs({ type: "text" })` width: 100%; - padding: 12px 20px; + padding: 8px 12px; + margin: 8px 0; + box-sizing: border-box; + border-radius: ${defaultBorderRadius}; + outline: 1px solid ${lightGray}; + border: none; + background-color: ${vscBackground}; + color: ${vscForeground}; + + &:focus { + background: ${secondaryDark}; + } +`; + +export const Select = styled.select` + padding: 8px 12px; margin: 8px 0; box-sizing: border-box; border-radius: ${defaultBorderRadius}; - border: 2px solid gray; + outline: 1px solid ${lightGray}; + border: none; + background-color: ${vscBackground}; + color: ${vscForeground}; +`; + +export const Label = styled.label` + font-size: 13px; `; const spin = keyframes` diff --git a/extension/react-app/src/hooks/AbstractContinueGUIClientProtocol.ts b/extension/react-app/src/hooks/AbstractContinueGUIClientProtocol.ts index c9e7def2..f8c11527 100644 --- a/extension/react-app/src/hooks/AbstractContinueGUIClientProtocol.ts +++ b/extension/react-app/src/hooks/AbstractContinueGUIClientProtocol.ts @@ -37,6 +37,16 @@ abstract class AbstractContinueGUIClientProtocol { abstract editStepAtIndex(userInput: string, index: number): void; + abstract setSystemMessage(message: string): void; + + abstract setTemperature(temperature: number): void; + + abstract setModelForRole( + role: string, + model_class: string, + model: string + ): void; + abstract saveContextGroup(title: string, contextItems: ContextItem[]): void; abstract selectContextGroup(id: string): void; diff --git a/extension/react-app/src/hooks/ContinueGUIClientProtocol.ts b/extension/react-app/src/hooks/ContinueGUIClientProtocol.ts index b3ac2570..ce9b2a0a 100644 --- a/extension/react-app/src/hooks/ContinueGUIClientProtocol.ts +++ b/extension/react-app/src/hooks/ContinueGUIClientProtocol.ts @@ -133,6 +133,18 @@ class ContinueGUIClientProtocol extends AbstractContinueGUIClientProtocol { }); } + setSystemMessage(message: string): void { + this.messenger?.send("set_system_message", { message }); + } + + setTemperature(temperature: number): void { + this.messenger?.send("set_temperature", { temperature }); + } + + setModelForRole(role: string, model_class: string, model: any): void { + this.messenger?.send("set_model_for_role", { role, model, model_class }); + } + saveContextGroup(title: string, contextItems: ContextItem[]): void { this.messenger?.send("save_context_group", { context_items: contextItems, diff --git a/extension/react-app/src/pages/settings.tsx b/extension/react-app/src/pages/settings.tsx new file mode 100644 index 00000000..8fd91ff5 --- /dev/null +++ b/extension/react-app/src/pages/settings.tsx @@ -0,0 +1,229 @@ +import React, { useContext, useEffect, useState } from "react"; +import { GUIClientContext } from "../App"; +import { useSelector } from "react-redux"; +import { RootStore } from "../redux/store"; +import { useNavigate } from "react-router-dom"; +import { ContinueConfig } from "../../../schema/ContinueConfig"; +import { + Button, + Select, + TextArea, + lightGray, + secondaryDark, +} from "../components"; +import styled from "styled-components"; +import { ArrowLeftIcon } from "@heroicons/react/24/outline"; +import Loader from "../components/Loader"; +import InfoHover from "../components/InfoHover"; +import { FormProvider, useForm } from "react-hook-form"; +import ModelSettings from "../components/ModelSettings"; + +const Hr = styled.hr` + border: 0.5px solid ${lightGray}; +`; + +const CancelButton = styled(Button)` + background-color: transparent; + color: ${lightGray}; + border: 1px solid ${lightGray}; + &:hover { + background-color: ${lightGray}; + color: black; + } +`; + +const SaveButton = styled(Button)` + &:hover { + opacity: 0.8; + } +`; + +const Slider = styled.input.attrs({ type: "range" })` + --webkit-appearance: none; + width: 100%; + background-color: ${secondaryDark}; + outline: none; + border: none; + opacity: 0.7; + -webkit-transition: 0.2s; + transition: opacity 0.2s; + &:hover { + opacity: 1; + } + &::-webkit-slider-runnable-track { + width: 100%; + height: 8px; + cursor: pointer; + background: ${lightGray}; + border-radius: 4px; + } + &::-webkit-slider-thumb { + -webkit-appearance: none; + appearance: none; + width: 8px; + height: 8px; + cursor: pointer; + margin-top: -3px; + } + &::-moz-range-thumb { + width: 8px; + height: 8px; + cursor: pointer; + margin-top: -3px; + } + + &:focus { + outline: none; + border: none; + } +`; +const ALL_MODEL_ROLES = ["default", "small", "medium", "large", "edit", "chat"]; + +function Settings() { + const formMethods = useForm<ContinueConfig>(); + const onSubmit = (data: ContinueConfig) => console.log(data); + + const navigate = useNavigate(); + const client = useContext(GUIClientContext); + const config = useSelector((state: RootStore) => state.serverState.config); + + const submitChanges = () => { + if (!client) return; + + const systemMessage = formMethods.watch("system_message"); + const temperature = formMethods.watch("temperature"); + // const models = formMethods.watch("models"); + + if (systemMessage) client.setSystemMessage(systemMessage); + if (temperature) client.setTemperature(temperature); + + // if (models) { + // for (const role of ALL_MODEL_ROLES) { + // if (models[role]) { + // client.setModelForRole(role, models[role] as string, models[role]); + // } + // } + // } + }; + + const submitAndLeave = () => { + submitChanges(); + navigate("/"); + }; + + return ( + <FormProvider {...formMethods}> + <div className="w-full"> + <form onSubmit={formMethods.handleSubmit(onSubmit)}> + <div className="items-center flex"> + <ArrowLeftIcon + width="1.4em" + height="1.4em" + onClick={submitAndLeave} + className="inline-block ml-4 cursor-pointer" + /> + <h1 className="text-2xl font-bold m-4 inline-block">Settings</h1> + </div> + {config ? ( + <div className="p-2"> + <h3 className="flex gap-1"> + System Message + <InfoHover + msg={`Set a system message with information that the LLM should always + keep in mind (e.g. "Please give concise answers. Always respond in + Spanish.")`} + /> + </h3> + <TextArea + placeholder="Enter system message" + {...formMethods.register("system_message")} + defaultValue={config.system_message} + /> + + <Hr /> + <h3 className="flex gap-1"> + Temperature + <InfoHover + msg={`Set temperature to any value between 0 and 1. Higher values will + make the LLM more creative, while lower values will make it more + predictable.`} + /> + </h3> + <div className="flex justify-between mx-16 gap-1"> + <p>0</p> + <Slider + type="range" + min="0" + max="1" + step="0.01" + defaultValue={config.temperature} + {...formMethods.register("temperature")} + /> + <p>1</p> + </div> + <div className="text-center" style={{ marginTop: "-25px" }}> + <p className="text-sm text-gray-500"> + {formMethods.watch("temperature") || + config.temperature || + "-"} + </p> + </div> + <Hr /> + + {/** + <h3 className="flex gap-1">Models</h3> + {ALL_MODEL_ROLES.map((role) => { + return ( + <> + <h4>{role}</h4> + + <ModelSettings + role={role} + llm={(config.models as any)[role]} + /> + </> + ); + })} + + <Hr /> + + <h3 className="flex gap-1"> + Custom Commands + <InfoHover + msg={`Custom commands let you map a prompt to a shortened slash command. + They are like slash commands, but more easily defined - write just a + prompt instead of a Step class. Their output will always be in chat + form`} + /> + </h3> + <Hr /> + + <h3 className="flex gap-1"> + Context Providers + <InfoHover + msg={`Context Providers let you type '@' and quickly reference sources of information, like files, GitHub Issues, webpages, and more.`} + /> + </h3> + */} + </div> + ) : ( + <Loader /> + )} + </form> + + <div className="flex gap-2 justify-end px-4"> + <CancelButton + onClick={() => { + navigate("/"); + }} + > + Cancel + </CancelButton> + <SaveButton onClick={submitAndLeave}>Save</SaveButton> + </div> + </div> + </FormProvider> + ); +} + +export default Settings; diff --git a/extension/react-app/src/redux/slices/serverStateReducer.ts b/extension/react-app/src/redux/slices/serverStateReducer.ts index a20476b2..cf26f094 100644 --- a/extension/react-app/src/redux/slices/serverStateReducer.ts +++ b/extension/react-app/src/redux/slices/serverStateReducer.ts @@ -29,6 +29,10 @@ const initialState: FullState = { slash_commands: [], adding_highlighted_code: false, selected_context_items: [], + config: { + system_message: "", + temperature: 0.5, + }, }; export const serverStateSlice = createSlice({ |