summaryrefslogtreecommitdiff
path: root/extension/react-app/src
diff options
context:
space:
mode:
Diffstat (limited to 'extension/react-app/src')
-rw-r--r--extension/react-app/src/App.tsx5
-rw-r--r--extension/react-app/src/components/HeaderButtonWithText.tsx4
-rw-r--r--extension/react-app/src/components/InfoHover.tsx19
-rw-r--r--extension/react-app/src/components/Layout.tsx9
-rw-r--r--extension/react-app/src/components/ModelSettings.tsx107
-rw-r--r--extension/react-app/src/components/index.ts50
-rw-r--r--extension/react-app/src/hooks/AbstractContinueGUIClientProtocol.ts10
-rw-r--r--extension/react-app/src/hooks/ContinueGUIClientProtocol.ts12
-rw-r--r--extension/react-app/src/pages/settings.tsx229
-rw-r--r--extension/react-app/src/redux/slices/serverStateReducer.ts4
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({