From 5eec484dc79bb56dabf9a56af0dbe6bc95227d39 Mon Sep 17 00:00:00 2001 From: Nate Sesti <33237525+sestinj@users.noreply.github.com> Date: Tue, 22 Aug 2023 13:12:58 -0700 Subject: Config UI (#399) * feat: :sparkles: UI for config! * feat: :sparkles: (latent) edit models in settings --- extension/react-app/package-lock.json | 23 ++- extension/react-app/package.json | 3 +- extension/react-app/src/App.tsx | 5 + .../src/components/HeaderButtonWithText.tsx | 4 +- extension/react-app/src/components/InfoHover.tsx | 19 ++ extension/react-app/src/components/Layout.tsx | 9 + .../react-app/src/components/ModelSettings.tsx | 107 ++++++++++ extension/react-app/src/components/index.ts | 50 ++++- .../src/hooks/AbstractContinueGUIClientProtocol.ts | 10 + .../src/hooks/ContinueGUIClientProtocol.ts | 12 ++ extension/react-app/src/pages/settings.tsx | 229 +++++++++++++++++++++ .../src/redux/slices/serverStateReducer.ts | 4 + extension/schema/ContinueConfig.d.ts | 175 ++++++++++++++++ extension/schema/FullState.d.ts | 8 + extension/schema/LLM.d.ts | 20 ++ extension/schema/Models.d.ts | 36 ++++ 16 files changed, 699 insertions(+), 15 deletions(-) create mode 100644 extension/react-app/src/components/InfoHover.tsx create mode 100644 extension/react-app/src/components/ModelSettings.tsx create mode 100644 extension/react-app/src/pages/settings.tsx create mode 100644 extension/schema/ContinueConfig.d.ts create mode 100644 extension/schema/LLM.d.ts create mode 100644 extension/schema/Models.d.ts (limited to 'extension') diff --git a/extension/react-app/package-lock.json b/extension/react-app/package-lock.json index c2265d15..fb68081c 100644 --- a/extension/react-app/package-lock.json +++ b/extension/react-app/package-lock.json @@ -6,7 +6,6 @@ "packages": { "": { "name": "react-app", - "version": "0.0.0", "dependencies": { "@heroicons/react": "^2.0.18", "@types/vscode-webview": "^1.57.1", @@ -17,6 +16,7 @@ "prismjs": "^1.29.0", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-hook-form": "^7.45.4", "react-redux": "^8.0.5", "react-router-dom": "^6.14.2", "react-switch": "^7.0.0", @@ -3748,6 +3748,21 @@ "react": "^18.2.0" } }, + "node_modules/react-hook-form": { + "version": "7.45.4", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.45.4.tgz", + "integrity": "sha512-HGDV1JOOBPZj10LB3+OZgfDBTn+IeEsNOKiq/cxbQAIbKaiJUe/KV8DBUzsx0Gx/7IG/orWqRRm736JwOfUSWQ==", + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18" + } + }, "node_modules/react-is": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", @@ -7335,6 +7350,12 @@ "scheduler": "^0.23.0" } }, + "react-hook-form": { + "version": "7.45.4", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.45.4.tgz", + "integrity": "sha512-HGDV1JOOBPZj10LB3+OZgfDBTn+IeEsNOKiq/cxbQAIbKaiJUe/KV8DBUzsx0Gx/7IG/orWqRRm736JwOfUSWQ==", + "requires": {} + }, "react-is": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", diff --git a/extension/react-app/package.json b/extension/react-app/package.json index 23cdf9bb..b9f70645 100644 --- a/extension/react-app/package.json +++ b/extension/react-app/package.json @@ -17,6 +17,7 @@ "prismjs": "^1.29.0", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-hook-form": "^7.45.4", "react-redux": "^8.0.5", "react-router-dom": "^6.14.2", "react-switch": "^7.0.0", @@ -38,4 +39,4 @@ "typescript": "^4.9.3", "vite": "^4.1.0" } -} \ No newline at end of file +} 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: , }, + { + path: "/settings", + element: , + }, ], }, ]); 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 ( + <> + + + + ); +}; + +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 = () => { + { + navigate("/settings"); + }} + text="Settings" + > + + 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 ( +
+ {props.llm ? ( + <> + {props.role}: {props.llm.class_name || "gpt-4"} +
+ {typeof modelOptions.api_key !== undefined && ( + <> + + + + )} + {modelOptions.model && ( + <> + + + + )} + + + ) : ( +
+ Add Model +
+ +
+
+ )} +
+ ); +} + +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(); + 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 ( + +
+
+
+ +

Settings

+
+ {config ? ( +
+

+ System Message + +

+