From e976d60974a7837967d03807605cbf2e7b4f3f9a Mon Sep 17 00:00:00 2001
From: Nate Sesti <33237525+sestinj@users.noreply.github.com>
Date: Sat, 23 Sep 2023 13:06:00 -0700
Subject: UI Redesign and fixing many details (#496)
* feat: :lipstick: start of major design upgrade
* feat: :lipstick: model selection page
* feat: :lipstick: use shortcut to add highlighted code as ctx
* feat: :lipstick: better display of errors
* feat: :lipstick: ui for learning keyboard shortcuts, more details
* refactor: :construction: testing slash commands ui
* Truncate continue.log
* refactor: :construction: refactoring client_session, ui, more
* feat: :bug: layout fixes
* refactor: :lipstick: ui to enter OpenAI Key
* refactor: :truck: rename MaybeProxyOpenAI -> OpenAIFreeTrial
* starting help center
* removing old shortcut docs
* fix: :bug: fix model setting logic to avoid overwrites
* feat: :lipstick: tutorial and model descriptions
* refactor: :truck: rename unused -> saved
* refactor: :truck: rename model roles
* feat: :lipstick: edit indicator
* refactor: :lipstick: move +, folder icons
* feat: :lipstick: tab to clear all context
* fix: :bug: context providers ui fixes
* fix: :bug: fix lag when stopping step
* fix: :bug: don't override system message for models
* fix: :bug: fix continue button cursor
* feat: :lipstick: title bar
* fix: :bug: updates to code highlighting logic and more
* fix: :bug: fix renaming of summarize model role
* feat: :lipstick: help page and better session title
* feat: :lipstick: more help page / ui improvements
* feat: :lipstick: set session title
* fix: :bug: small fixes for changing sessions
* fix: :bug: perfecting the highlighting code and ctx interactions
* style: :lipstick: sticky headers for scroll, ollama warming
* fix: :bug: fix toggle bug
---------
Co-authored-by: Ty Dunn
---
extension/react-app/src/pages/gui.tsx | 438 +++++++++++++++++++++--------
extension/react-app/src/pages/help.tsx | 98 +++++++
extension/react-app/src/pages/history.tsx | 172 ++++++-----
extension/react-app/src/pages/models.tsx | 167 +++++++++++
extension/react-app/src/pages/settings.tsx | 45 ++-
5 files changed, 716 insertions(+), 204 deletions(-)
create mode 100644 extension/react-app/src/pages/help.tsx
create mode 100644 extension/react-app/src/pages/models.tsx
(limited to 'extension/react-app/src/pages')
diff --git a/extension/react-app/src/pages/gui.tsx b/extension/react-app/src/pages/gui.tsx
index 9f58c505..78b7a970 100644
--- a/extension/react-app/src/pages/gui.tsx
+++ b/extension/react-app/src/pages/gui.tsx
@@ -1,7 +1,5 @@
import styled from "styled-components";
-import { defaultBorderRadius } from "../components";
-import Loader from "../components/Loader";
-import ContinueButton from "../components/ContinueButton";
+import { TextInput, defaultBorderRadius, lightGray } from "../components";
import { FullState } from "../../../schema/FullState";
import {
useEffect,
@@ -9,6 +7,7 @@ import {
useState,
useContext,
useLayoutEffect,
+ useCallback,
} from "react";
import { HistoryNode } from "../../../schema/HistoryNode";
import StepContainer from "../components/StepContainer";
@@ -32,6 +31,19 @@ import {
setServerState,
temporarilyPushToUserInputQueue,
} from "../redux/slices/serverStateReducer";
+import TimelineItem from "../components/TimelineItem";
+import ErrorStepContainer from "../components/ErrorStepContainer";
+import {
+ ChatBubbleOvalLeftIcon,
+ CodeBracketSquareIcon,
+ ExclamationTriangleIcon,
+ FolderIcon,
+ PlusIcon,
+} from "@heroicons/react/24/outline";
+import FTCDialog from "../components/dialogs/FTCDialog";
+import HeaderButtonWithText from "../components/HeaderButtonWithText";
+import { useNavigate } from "react-router-dom";
+import SuggestionsArea from "../components/Suggestions";
const TopGuiDiv = styled.div`
overflow-y: scroll;
@@ -44,6 +56,44 @@ const TopGuiDiv = styled.div`
}
`;
+const TitleTextInput = styled(TextInput)`
+ border: none;
+ outline: none;
+
+ font-size: 16px;
+ font-weight: bold;
+ margin: 0;
+ margin-right: 8px;
+ padding-top: 6px;
+ padding-bottom: 6px;
+
+ &:focus {
+ outline: 1px solid ${lightGray};
+ }
+`;
+
+const StepsDiv = styled.div`
+ position: relative;
+ background-color: transparent;
+ padding-left: 8px;
+ padding-right: 8px;
+
+ & > * {
+ z-index: 1;
+ position: relative;
+ }
+
+ &::before {
+ content: "";
+ position: absolute;
+ height: calc(100% - 24px);
+ border-left: 2px solid ${lightGray};
+ left: 28px;
+ z-index: 0;
+ bottom: 24px;
+ }
+`;
+
const UserInputQueueItem = styled.div`
border-radius: ${defaultBorderRadius};
color: gray;
@@ -52,6 +102,16 @@ const UserInputQueueItem = styled.div`
text-align: center;
`;
+const GUIHeaderDiv = styled.div`
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 4px;
+ padding-left: 8px;
+ padding-right: 8px;
+ border-bottom: 0.5px solid ${lightGray};
+`;
+
interface GUIProps {
firstObservation?: any;
}
@@ -61,6 +121,7 @@ function GUI(props: GUIProps) {
const client = useContext(GUIClientContext);
const posthog = usePostHog();
const dispatch = useDispatch();
+ const navigate = useNavigate();
// #endregion
@@ -73,26 +134,16 @@ function GUI(props: GUIProps) {
const user_input_queue = useSelector(
(state: RootStore) => state.serverState.user_input_queue
);
- const adding_highlighted_code = useSelector(
- (state: RootStore) => state.serverState.adding_highlighted_code
- );
- const selected_context_items = useSelector(
- (state: RootStore) => state.serverState.selected_context_items
+
+ const sessionTitle = useSelector(
+ (state: RootStore) => state.serverState.session_info?.title
);
// #endregion
// #region State
const [waitingForSteps, setWaitingForSteps] = useState(false);
- const [availableSlashCommands, setAvailableSlashCommands] = useState<
- { name: string; description: string }[]
- >([]);
- const [stepsOpen, setStepsOpen] = useState([
- true,
- true,
- true,
- true,
- ]);
+ const [stepsOpen, setStepsOpen] = useState<(boolean | undefined)[]>([]);
const [waitingForClient, setWaitingForClient] = useState(true);
const [showLoading, setShowLoading] = useState(false);
@@ -150,7 +201,7 @@ function GUI(props: GUIProps) {
topGuiDivRef.current?.scrollTo({
top: topGuiDivRef.current?.scrollHeight,
- behavior: "smooth" as any,
+ behavior: "instant" as any,
});
}, [topGuiDivRef.current?.scrollHeight, history.timeline]);
@@ -160,6 +211,7 @@ function GUI(props: GUIProps) {
if (
e.key === "Backspace" &&
isMetaEquivalentKeyPressed(e) &&
+ !e.shiftKey &&
typeof history?.current_index !== "undefined" &&
history.timeline[history.current_index]?.active
) {
@@ -188,14 +240,6 @@ function GUI(props: GUIProps) {
dispatch(setServerState(state));
setWaitingForSteps(waitingForSteps);
- setAvailableSlashCommands(
- state.slash_commands.map((c: any) => {
- return {
- name: `/${c.name}`,
- description: c.description,
- };
- })
- );
setStepsOpen((prev) => {
const nextStepsOpen = [...prev];
for (
@@ -203,7 +247,7 @@ function GUI(props: GUIProps) {
i < state.history.timeline.length;
i++
) {
- nextStepsOpen.push(true);
+ nextStepsOpen.push(undefined);
}
return nextStepsOpen;
});
@@ -214,7 +258,6 @@ function GUI(props: GUIProps) {
useEffect(() => {
if (client && waitingForClient) {
- console.log("sending user input queue, ", user_input_queue);
setWaitingForClient(false);
for (const input of user_input_queue) {
client.sendMainInput(input);
@@ -244,43 +287,22 @@ function GUI(props: GUIProps) {
return;
}
- // Increment localstorage counter for usage of free trial
if (
- defaultModel === "MaybeProxyOpenAI" &&
+ defaultModel === "OpenAIFreeTrial" &&
(!input.startsWith("/") || input.startsWith("/edit"))
) {
- const freeTrialCounter = localStorage.getItem("freeTrialCounter");
- if (freeTrialCounter) {
- const usages = parseInt(freeTrialCounter);
- localStorage.setItem("freeTrialCounter", (usages + 1).toString());
+ const ftc = localStorage.getItem("ftc");
+ if (ftc) {
+ const u = parseInt(ftc);
+ localStorage.setItem("ftc", (u + 1).toString());
- if (usages >= 250) {
- console.log("Free trial limit reached");
+ if (u >= 250) {
dispatch(setShowDialog(true));
- dispatch(
- setDialogMessage(
-
-
Free Trial Limit Reached
- You've reached the free trial limit of 250 free inputs with
- Continue's OpenAI API key. To keep using Continue, you can
- either use your own API key, or use a local LLM. To read more
- about the options, see our{" "}
-
- documentation
-
- . If you're just looking for fastest way to keep going, type
- '/config' to open your Continue config file and paste your API
- key into the MaybeProxyOpenAI object.
-
- )
- );
+ dispatch(setDialogMessage());
return;
}
} else {
- localStorage.setItem("freeTrialCounter", "1");
+ localStorage.setItem("ftc", "1");
}
}
@@ -391,6 +413,69 @@ function GUI(props: GUIProps) {
client.sendStepUserInput(input, index);
};
+ const getStepsInUserInputGroup = useCallback(
+ (index: number): number[] => {
+ // index is the index in the entire timeline, hidden steps included
+ const stepsInUserInputGroup: number[] = [];
+
+ // First find the closest above UserInputStep
+ let userInputIndex = -1;
+ for (let i = index; i >= 0; i--) {
+ if (
+ history?.timeline.length > i &&
+ history.timeline[i].step.name === "User Input" &&
+ history.timeline[i].step.hide === false
+ ) {
+ stepsInUserInputGroup.push(i);
+ userInputIndex = i;
+ break;
+ }
+ }
+ if (stepsInUserInputGroup.length === 0) return [];
+
+ for (let i = userInputIndex + 1; i < history?.timeline.length; i++) {
+ if (
+ history?.timeline.length > i &&
+ history.timeline[i].step.name === "User Input" &&
+ history.timeline[i].step.hide === false
+ ) {
+ break;
+ }
+ stepsInUserInputGroup.push(i);
+ }
+ return stepsInUserInputGroup;
+ },
+ [history.timeline]
+ );
+
+ const onToggleAtIndex = useCallback(
+ (index: number) => {
+ // Check if all steps after the User Input are closed
+ const groupIndices = getStepsInUserInputGroup(index);
+ const userInputIndex = groupIndices[0];
+ setStepsOpen((prev) => {
+ const nextStepsOpen = [...prev];
+ nextStepsOpen[index] = !nextStepsOpen[index];
+ const allStepsAfterUserInputAreClosed = !groupIndices.some(
+ (i, j) => j > 0 && nextStepsOpen[i]
+ );
+ if (allStepsAfterUserInputAreClosed) {
+ nextStepsOpen[userInputIndex] = false;
+ } else {
+ const allStepsAfterUserInputAreOpen = !groupIndices.some(
+ (i, j) => j > 0 && !nextStepsOpen[i]
+ );
+ if (allStepsAfterUserInputAreOpen) {
+ nextStepsOpen[userInputIndex] = true;
+ }
+ }
+
+ return nextStepsOpen;
+ });
+ },
+ [getStepsInUserInputGroup]
+ );
+
useEffect(() => {
const timeout = setTimeout(() => {
setShowLoading(true);
@@ -400,6 +485,17 @@ function GUI(props: GUIProps) {
clearTimeout(timeout);
};
}, []);
+
+ useEffect(() => {
+ if (sessionTitle) {
+ setSessionTitleInput(sessionTitle);
+ }
+ }, [sessionTitle]);
+
+ const [sessionTitleInput, setSessionTitleInput] = useState(
+ sessionTitle || "New Session"
+ );
+
return (
+
+ {
+ // Select all text
+ (e.target as any).setSelectionRange(
+ 0,
+ (e.target as any).value.length
+ );
+ }}
+ value={sessionTitleInput}
+ onChange={(e) => setSessionTitleInput(e.target.value)}
+ onBlur={(e) => {
+ client?.setCurrentSessionTitle(e.target.value);
+ }}
+ onKeyDown={(e) => {
+ if (e.key === "Enter") {
+ e.preventDefault();
+ (e.target as any).blur();
+ }
+ }}
+ />
+
+ {history.timeline.filter((n) => !n.step.hide).length > 0 && (
+
{
+ if (history.timeline.filter((n) => !n.step.hide).length > 0) {
+ client?.loadSession(undefined);
+ }
+ }}
+ text="New Session (⌥⌘N)"
+ >
+
+
+ )}
+
+
{
+ navigate("/history");
+ }}
+ text="History"
+ >
+
+
+
+
{showLoading && typeof client === "undefined" && (
<>
@@ -478,63 +619,128 @@ function GUI(props: GUIProps) {
-
-
- {/* Tip: Drag the Continue logo from the far left of the window to the
- right, then toggle Continue using option/alt+command+m. */}
- {/* Tip: If there is an error in the terminal, use COMMAND+D to
- automatically debug */}
-
>
)}
- {history?.timeline.map((node: HistoryNode, index: number) => {
- return node.step.name === "User Input" ? (
- node.step.hide || (
- {
- client?.deleteAtIndex(index);
- }}
- historyNode={node}
- >
- {node.step.description as string}
-
- )
- ) : (
- {
- const nextStepsOpen = [...stepsOpen];
- nextStepsOpen[index] = !nextStepsOpen[index];
- setStepsOpen(nextStepsOpen);
- }}
- onToggleAll={() => {
- const shouldOpen = !stepsOpen[index];
- setStepsOpen((prev) => prev.map(() => shouldOpen));
- }}
- key={index}
- onUserInput={(input: string) => {
- onStepUserInput(input, index);
- }}
- inFuture={index > history?.current_index}
- historyNode={node}
- onReverse={() => {
- client?.reverseToIndex(index);
- }}
- onRetry={() => {
- client?.retryAtIndex(index);
- setWaitingForSteps(true);
- }}
- onDelete={() => {
- client?.deleteAtIndex(index);
- }}
- />
- );
- })}
- {waitingForSteps && }
+
+ {
+ client?.sendMainInput(textInput);
+ }}
+ />
+
+ {history?.timeline.map((node: HistoryNode, index: number) => {
+ if (node.step.hide) return null;
+ return (
+ <>
+ {node.step.name === "User Input" ? (
+ node.step.hide || (
+ {
+ return history.timeline[i].active;
+ })}
+ groupIndices={getStepsInUserInputGroup(index)}
+ onToggle={(isOpen: boolean) => {
+ // Collapse all steps in the section
+ setStepsOpen((prev) => {
+ const nextStepsOpen = [...prev];
+ getStepsInUserInputGroup(index).forEach((i) => {
+ nextStepsOpen[i] = isOpen;
+ });
+ return nextStepsOpen;
+ });
+ }}
+ onToggleAll={(isOpen: boolean) => {
+ // Collapse _all_ steps
+ setStepsOpen((prev) => {
+ return prev.map((_) => isOpen);
+ });
+ }}
+ isToggleOpen={
+ typeof stepsOpen[index] === "undefined"
+ ? true
+ : stepsOpen[index]!
+ }
+ index={index}
+ onDelete={() => {
+ // Delete the input and all steps until the next user input
+ getStepsInUserInputGroup(index).forEach((i) => {
+ client?.deleteAtIndex(i);
+ });
+ }}
+ historyNode={node}
+ >
+ {node.step.description as string}
+
+ )
+ ) : (
+
+ ) : node.observation?.error ? (
+
+ ) : (
+
+ )
+ }
+ open={
+ typeof stepsOpen[index] === "undefined"
+ ? node.observation?.error
+ ? false
+ : true
+ : stepsOpen[index]!
+ }
+ onToggle={() => onToggleAtIndex(index)}
+ >
+ {node.observation?.error ? (
+ onToggleAtIndex(index)}
+ historyNode={node}
+ onDelete={() => client?.deleteAtIndex(index)}
+ />
+ ) : (
+ {
+ onStepUserInput(input, index);
+ }}
+ inFuture={index > history?.current_index}
+ historyNode={node}
+ onReverse={() => {
+ client?.reverseToIndex(index);
+ }}
+ onRetry={() => {
+ client?.retryAtIndex(index);
+ setWaitingForSteps(true);
+ }}
+ onDelete={() => {
+ client?.deleteAtIndex(index);
+ }}
+ noUserInputParent={
+ getStepsInUserInputGroup(index).length === 0
+ }
+ />
+ )}
+
+ )}
+ {/* */}
+ >
+ );
+ })}
+
{user_input_queue?.map?.((input) => {
@@ -547,18 +753,14 @@ function GUI(props: GUIProps) {
ref={mainTextInputRef}
onEnter={(e) => {
onMainTextInput(e);
- e.stopPropagation();
- e.preventDefault();
+ e?.stopPropagation();
+ e?.preventDefault();
}}
onInputValueChange={() => {}}
- items={availableSlashCommands}
- selectedContextItems={selected_context_items}
onToggleAddContext={() => {
client?.toggleAddingHighlightedCode();
}}
- addingHighlightedCode={adding_highlighted_code}
/>
-
);
}
diff --git a/extension/react-app/src/pages/help.tsx b/extension/react-app/src/pages/help.tsx
new file mode 100644
index 00000000..3e2e93d2
--- /dev/null
+++ b/extension/react-app/src/pages/help.tsx
@@ -0,0 +1,98 @@
+import { useNavigate } from "react-router-dom";
+import { ArrowLeftIcon } from "@heroicons/react/24/outline";
+import KeyboardShortcutsDialog from "../components/dialogs/KeyboardShortcuts";
+import { buttonColor, lightGray, vscBackground } from "../components";
+import styled from "styled-components";
+
+const IconDiv = styled.div<{ backgroundColor?: string }>`
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+
+ height: 100%;
+ padding: 0 4px;
+
+ &:hover {
+ background-color: ${(props) => props.backgroundColor || lightGray};
+ }
+`;
+
+function HelpPage() {
+ const navigate = useNavigate();
+
+ return (
+
+
+
navigate("/")}
+ className="inline-block ml-4 cursor-pointer"
+ />
+ Help Center
+
+
+
+
+
+
+ );
+}
+
+export default HelpPage;
diff --git a/extension/react-app/src/pages/history.tsx b/extension/react-app/src/pages/history.tsx
index b901dd55..b6de0520 100644
--- a/extension/react-app/src/pages/history.tsx
+++ b/extension/react-app/src/pages/history.tsx
@@ -1,13 +1,14 @@
import React, { useContext, useEffect, useState } from "react";
import { SessionInfo } from "../../../schema/SessionInfo";
import { GUIClientContext } from "../App";
-import { useSelector } from "react-redux";
+import { useDispatch, useSelector } from "react-redux";
import { RootStore } from "../redux/store";
import { useNavigate } from "react-router-dom";
-import { secondaryDark, vscBackground } from "../components";
+import { lightGray, secondaryDark, vscBackground } from "../components";
import styled from "styled-components";
import { ArrowLeftIcon } from "@heroicons/react/24/outline";
import CheckDiv from "../components/CheckDiv";
+import { temporarilyClearSession } from "../redux/slices/serverStateReducer";
const Tr = styled.tr`
&:hover {
@@ -41,6 +42,7 @@ function lastPartOfPath(path: string): string {
function History() {
const navigate = useNavigate();
+ const dispatch = useDispatch();
const [sessions, setSessions] = useState
([]);
const client = useContext(GUIClientContext);
const apiUrl = useSelector((state: RootStore) => state.config.apiUrl);
@@ -67,78 +69,106 @@ function History() {
fetchSessions();
}, [client]);
- console.log(sessions.map((session) => session.date_created));
-
return (
-
-
-
navigate("/")}
- className="inline-block ml-4 cursor-pointer"
- />
- History
+
+
+
+
navigate("/")}
+ className="inline-block ml-4 cursor-pointer"
+ />
+ History
+
+ {workspacePaths && workspacePaths.length > 0 && (
+
setFilteringByWorkspace((prev) => !prev)}
+ title={`Show only sessions from ${lastPartOfPath(
+ workspacePaths[workspacePaths.length - 1]
+ )}/`}
+ />
+ )}
- {workspacePaths && workspacePaths.length > 0 && (
-
setFilteringByWorkspace((prev) => !prev)}
- title={`Show only sessions from ${lastPartOfPath(
- workspacePaths[workspacePaths.length - 1]
- )}/`}
- />
+
+ {sessions.filter((session) => {
+ if (
+ !filteringByWorkspace ||
+ typeof workspacePaths === "undefined" ||
+ typeof session.workspace_directory === "undefined"
+ ) {
+ return true;
+ }
+ return workspacePaths.includes(session.workspace_directory);
+ }).length === 0 && (
+
+ No past sessions found. To start a new session, either click the "+"
+ button or use the keyboard shortcut: Option + Command + N
+
)}
-
-
- {sessions
- .filter((session) => {
- if (
- !filteringByWorkspace ||
- typeof workspacePaths === "undefined" ||
- typeof session.workspace_directory === "undefined"
- ) {
- return true;
- }
- return workspacePaths.includes(session.workspace_directory);
- })
- .sort(
- (a, b) =>
- parseDate(b.date_created).getTime() -
- parseDate(a.date_created).getTime()
- )
- .map((session, index) => (
-
-
- {
- client?.loadSession(session.session_id);
- navigate("/");
- }}
- >
- {session.title}
-
- {parseDate(session.date_created).toLocaleString("en-US", {
- weekday: "short",
- year: "numeric",
- month: "long",
- day: "numeric",
- hour: "numeric",
- minute: "numeric",
- })}
- {" | "}
- {lastPartOfPath(session.workspace_directory || "")}/
-
-
- |
-
- ))}
-
-
-
-
- All session data is saved in ~/.continue/sessions
-
+
+
+
+
+ {sessions
+ .filter((session) => {
+ if (
+ !filteringByWorkspace ||
+ typeof workspacePaths === "undefined" ||
+ typeof session.workspace_directory === "undefined"
+ ) {
+ return true;
+ }
+ return workspacePaths.includes(session.workspace_directory);
+ })
+ .sort(
+ (a, b) =>
+ parseDate(b.date_created).getTime() -
+ parseDate(a.date_created).getTime()
+ )
+ .map((session, index) => (
+
+
+ {
+ client?.loadSession(session.session_id);
+ dispatch(temporarilyClearSession());
+ navigate("/");
+ }}
+ >
+ {session.title}
+
+ {parseDate(session.date_created).toLocaleString(
+ "en-US",
+ {
+ year: "2-digit",
+ month: "2-digit",
+ day: "2-digit",
+ hour: "numeric",
+ minute: "2-digit",
+ hour12: true,
+ }
+ )}
+ {" | "}
+ {lastPartOfPath(session.workspace_directory || "")}/
+
+
+ |
+
+ ))}
+
+
+
+
+ All session data is saved in ~/.continue/sessions
+
+
);
}
diff --git a/extension/react-app/src/pages/models.tsx b/extension/react-app/src/pages/models.tsx
new file mode 100644
index 00000000..1a6f275b
--- /dev/null
+++ b/extension/react-app/src/pages/models.tsx
@@ -0,0 +1,167 @@
+import React from "react";
+import ModelCard, { ModelInfo, ModelTag } from "../components/ModelCard";
+import styled from "styled-components";
+import { ArrowLeftIcon } from "@heroicons/react/24/outline";
+import { lightGray, vscBackground } from "../components";
+import { useNavigate } from "react-router-dom";
+
+const MODEL_INFO: ModelInfo[] = [
+ {
+ title: "OpenAI",
+ class: "OpenAI",
+ description: "Use gpt-4, gpt-3.5-turbo, or any other OpenAI model",
+ args: {
+ model: "gpt-4",
+ api_key: "",
+ title: "OpenAI",
+ },
+ icon: "openai.svg",
+ tags: [ModelTag["Requires API Key"]],
+ },
+ {
+ title: "Anthropic",
+ class: "AnthropicLLM",
+ description:
+ "Claude-2 is a highly capable model with a 100k context length",
+ args: {
+ model: "claude-2",
+ api_key: "",
+ title: "Anthropic",
+ },
+ icon: "anthropic.png",
+ tags: [ModelTag["Requires API Key"]],
+ },
+ {
+ title: "Ollama",
+ class: "Ollama",
+ description:
+ "One of the fastest ways to get started with local models on Mac",
+ args: {
+ model: "codellama",
+ title: "Ollama",
+ },
+ icon: "ollama.png",
+ tags: [ModelTag["Local"], ModelTag["Open-Source"]],
+ },
+ {
+ title: "TogetherAI",
+ class: "TogetherLLM",
+ description:
+ "Use the TogetherAI API for extremely fast streaming of open-source models",
+ args: {
+ model: "togethercomputer/CodeLlama-13b-Instruct",
+ api_key: "",
+ title: "TogetherAI",
+ },
+ icon: "together.png",
+ tags: [ModelTag["Requires API Key"], ModelTag["Open-Source"]],
+ },
+ {
+ title: "LM Studio",
+ class: "GGML",
+ description:
+ "One of the fastest ways to get started with local models on Mac or Windows",
+ args: {
+ server_url: "http://localhost:1234",
+ title: "LM Studio",
+ },
+ icon: "lmstudio.png",
+ tags: [ModelTag["Local"], ModelTag["Open-Source"]],
+ },
+ {
+ title: "Replicate",
+ class: "ReplicateLLM",
+ description: "Use the Replicate API to run open-source models",
+ args: {
+ model:
+ "replicate/llama-2-70b-chat:58d078176e02c219e11eb4da5a02a7830a283b14cf8f94537af893ccff5ee781",
+ api_key: "",
+ title: "Replicate",
+ },
+ icon: "replicate.png",
+ tags: [ModelTag["Requires API Key"], ModelTag["Open-Source"]],
+ },
+ {
+ title: "llama.cpp",
+ class: "LlamaCpp",
+ description: "If you are running the llama.cpp server from source",
+ args: {
+ title: "llama.cpp",
+ },
+ icon: "llamacpp.png",
+ tags: [ModelTag.Local, ModelTag["Open-Source"]],
+ },
+ {
+ title: "HuggingFace TGI",
+ class: "HuggingFaceTGI",
+ description:
+ "HuggingFace Text Generation Inference is an advanced, highly performant option for serving open-source models to multiple people",
+ args: {
+ title: "HuggingFace TGI",
+ },
+ icon: "hf.png",
+ tags: [ModelTag.Local, ModelTag["Open-Source"]],
+ },
+ {
+ title: "Other OpenAI-compatible API",
+ class: "GGML",
+ description:
+ "If you are using any other OpenAI-compatible API, for example text-gen-webui, FastChat, LocalAI, or llama-cpp-python, you can simply enter your server URL",
+ args: {
+ server_url: "",
+ },
+ icon: "openai.svg",
+ tags: [ModelTag.Local, ModelTag["Open-Source"]],
+ },
+ {
+ title: "GPT-4 limited free trial",
+ class: "OpenAIFreeTrial",
+ description:
+ "New users can try out Continue with GPT-4 using a proxy server that securely makes calls to OpenAI using our API key",
+ args: {
+ model: "gpt-4",
+ title: "GPT-4 Free Trial",
+ },
+ icon: "openai.svg",
+ tags: [ModelTag.Free],
+ },
+];
+
+const GridDiv = styled.div`
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
+ grid-gap: 2rem;
+ padding: 1rem;
+ justify-items: center;
+ align-items: center;
+`;
+
+function Models() {
+ const navigate = useNavigate();
+ return (
+
+
+
navigate("/")}
+ className="inline-block ml-4 cursor-pointer"
+ />
+ Add a new model
+
+
+ {MODEL_INFO.map((model) => (
+
+ ))}
+
+
+ );
+}
+
+export default Models;
diff --git a/extension/react-app/src/pages/settings.tsx b/extension/react-app/src/pages/settings.tsx
index 8b3d9c5b..4bd51163 100644
--- a/extension/react-app/src/pages/settings.tsx
+++ b/extension/react-app/src/pages/settings.tsx
@@ -1,15 +1,23 @@
-import React, { useContext, useEffect, useState } from "react";
+import React, { useContext } from "react";
import { GUIClientContext } from "../App";
-import { useSelector } from "react-redux";
+import { useDispatch, useSelector } from "react-redux";
import { RootStore } from "../redux/store";
import { useNavigate } from "react-router-dom";
import { ContinueConfig } from "../../../schema/ContinueConfig";
-import { Button, TextArea, lightGray, secondaryDark } from "../components";
+import {
+ Button,
+ TextArea,
+ lightGray,
+ secondaryDark,
+ vscBackground,
+} from "../components";
import styled from "styled-components";
-import { ArrowLeftIcon } from "@heroicons/react/24/outline";
+import { ArrowLeftIcon, Squares2X2Icon } from "@heroicons/react/24/outline";
import Loader from "../components/Loader";
import InfoHover from "../components/InfoHover";
import { FormProvider, useForm } from "react-hook-form";
+import { setDialogMessage, setShowDialog } from "../redux/slices/uiStateSlice";
+import KeyboardShortcutsDialog from "../components/dialogs/KeyboardShortcuts";
const Hr = styled.hr`
border: 0.5px solid ${lightGray};
@@ -70,7 +78,7 @@ const Slider = styled.input.attrs({ type: "range" })`
border: none;
}
`;
-const ALL_MODEL_ROLES = ["default", "small", "medium", "large", "edit", "chat"];
+const ALL_MODEL_ROLES = ["default", "summarize", "edit", "chat"];
function Settings() {
const formMethods = useForm();
@@ -79,6 +87,7 @@ function Settings() {
const navigate = useNavigate();
const client = useContext(GUIClientContext);
const config = useSelector((state: RootStore) => state.serverState.config);
+ const dispatch = useDispatch();
const submitChanges = () => {
if (!client) return;
@@ -106,17 +115,23 @@ function Settings() {
return (
-