summaryrefslogtreecommitdiff
path: root/extension/react-app/src/pages
diff options
context:
space:
mode:
Diffstat (limited to 'extension/react-app/src/pages')
-rw-r--r--extension/react-app/src/pages/gui.tsx438
-rw-r--r--extension/react-app/src/pages/help.tsx98
-rw-r--r--extension/react-app/src/pages/history.tsx172
-rw-r--r--extension/react-app/src/pages/models.tsx167
-rw-r--r--extension/react-app/src/pages/settings.tsx45
5 files changed, 716 insertions, 204 deletions
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<boolean[]>([
- 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(
- <div className="p-4">
- <h3>Free Trial Limit Reached</h3>
- 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{" "}
- <a
- href="https://continue.dev/docs/customization/models"
- target="_blank"
- >
- documentation
- </a>
- . 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.
- </div>
- )
- );
+ dispatch(setDialogMessage(<FTCDialog />));
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<string>(
+ sessionTitle || "New Session"
+ );
+
return (
<TopGuiDiv
ref={topGuiDivRef}
@@ -409,6 +505,51 @@ function GUI(props: GUIProps) {
}
}}
>
+ <GUIHeaderDiv>
+ <TitleTextInput
+ onClick={(e) => {
+ // 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();
+ }
+ }}
+ />
+ <div className="flex">
+ {history.timeline.filter((n) => !n.step.hide).length > 0 && (
+ <HeaderButtonWithText
+ onClick={() => {
+ if (history.timeline.filter((n) => !n.step.hide).length > 0) {
+ client?.loadSession(undefined);
+ }
+ }}
+ text="New Session (⌥⌘N)"
+ >
+ <PlusIcon width="1.4em" height="1.4em" />
+ </HeaderButtonWithText>
+ )}
+
+ <HeaderButtonWithText
+ onClick={() => {
+ navigate("/history");
+ }}
+ text="History"
+ >
+ <FolderIcon width="1.4em" height="1.4em" />
+ </HeaderButtonWithText>
+ </div>
+ </GUIHeaderDiv>
{showLoading && typeof client === "undefined" && (
<>
<RingLoader />
@@ -478,63 +619,128 @@ function GUI(props: GUIProps) {
</u>
</p>
</div>
-
- <div className="w-3/4 m-auto text-center text-xs">
- {/* 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 */}
- </div>
</>
)}
- {history?.timeline.map((node: HistoryNode, index: number) => {
- return node.step.name === "User Input" ? (
- node.step.hide || (
- <UserInputContainer
- index={index}
- onDelete={() => {
- client?.deleteAtIndex(index);
- }}
- historyNode={node}
- >
- {node.step.description as string}
- </UserInputContainer>
- )
- ) : (
- <StepContainer
- index={index}
- isLast={index === history.timeline.length - 1}
- isFirst={index === 0}
- open={stepsOpen[index]}
- onToggle={() => {
- 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 && <Loader />}
+ <br />
+ <SuggestionsArea
+ onClick={(textInput) => {
+ client?.sendMainInput(textInput);
+ }}
+ />
+ <StepsDiv>
+ {history?.timeline.map((node: HistoryNode, index: number) => {
+ if (node.step.hide) return null;
+ return (
+ <>
+ {node.step.name === "User Input" ? (
+ node.step.hide || (
+ <UserInputContainer
+ active={getStepsInUserInputGroup(index).some((i) => {
+ 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}
+ </UserInputContainer>
+ )
+ ) : (
+ <TimelineItem
+ historyNode={node}
+ iconElement={
+ node.step.class_name === "DefaultModelEditCodeStep" ? (
+ <CodeBracketSquareIcon width="16px" height="16px" />
+ ) : node.observation?.error ? (
+ <ExclamationTriangleIcon
+ width="16px"
+ height="16px"
+ color="red"
+ />
+ ) : (
+ <ChatBubbleOvalLeftIcon width="16px" height="16px" />
+ )
+ }
+ open={
+ typeof stepsOpen[index] === "undefined"
+ ? node.observation?.error
+ ? false
+ : true
+ : stepsOpen[index]!
+ }
+ onToggle={() => onToggleAtIndex(index)}
+ >
+ {node.observation?.error ? (
+ <ErrorStepContainer
+ onClose={() => onToggleAtIndex(index)}
+ historyNode={node}
+ onDelete={() => client?.deleteAtIndex(index)}
+ />
+ ) : (
+ <StepContainer
+ index={index}
+ isLast={index === history.timeline.length - 1}
+ isFirst={index === 0}
+ open={
+ typeof stepsOpen[index] === "undefined"
+ ? true
+ : stepsOpen[index]!
+ }
+ 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);
+ }}
+ noUserInputParent={
+ getStepsInUserInputGroup(index).length === 0
+ }
+ />
+ )}
+ </TimelineItem>
+ )}
+ {/* <div className="h-2"></div> */}
+ </>
+ );
+ })}
+ </StepsDiv>
<div>
{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}
/>
- <ContinueButton onClick={onMainTextInput} />
</TopGuiDiv>
);
}
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 (
+ <div className="overflow-scroll">
+ <div
+ className="items-center flex m-0 p-0 sticky top-0"
+ style={{
+ borderBottom: `0.5px solid ${lightGray}`,
+ backgroundColor: vscBackground,
+ }}
+ >
+ <ArrowLeftIcon
+ width="1.2em"
+ height="1.2em"
+ onClick={() => navigate("/")}
+ className="inline-block ml-4 cursor-pointer"
+ />
+ <h3 className="text-lg font-bold m-2 inline-block">Help Center</h3>
+ </div>
+
+ <div className="grid grid-cols-2 grid-rows-2">
+ <IconDiv backgroundColor="rgb(234, 51, 35)">
+ <a href="https://youtu.be/3Ocrc-WX4iQ?si=eDLYtkc6CXQoHsEc">
+ <svg
+ xmlns="http://www.w3.org/2000/svg"
+ viewBox="-5.2 -4.5 60 60"
+ fill="white"
+ className="w-full h-full"
+ >
+ <path d="M 44.898438 14.5 C 44.5 12.300781 42.601563 10.699219 40.398438 10.199219 C 37.101563 9.5 31 9 24.398438 9 C 17.800781 9 11.601563 9.5 8.300781 10.199219 C 6.101563 10.699219 4.199219 12.199219 3.800781 14.5 C 3.398438 17 3 20.5 3 25 C 3 29.5 3.398438 33 3.898438 35.5 C 4.300781 37.699219 6.199219 39.300781 8.398438 39.800781 C 11.898438 40.5 17.898438 41 24.5 41 C 31.101563 41 37.101563 40.5 40.601563 39.800781 C 42.800781 39.300781 44.699219 37.800781 45.101563 35.5 C 45.5 33 46 29.398438 46.101563 25 C 45.898438 20.5 45.398438 17 44.898438 14.5 Z M 19 32 L 19 18 L 31.199219 25 Z"></path>
+ </svg>
+ </a>
+ </IconDiv>
+ <IconDiv backgroundColor={buttonColor}>
+ <a href="https://continue.dev/docs/how-to-use-continue">
+ <svg
+ xmlns="http://www.w3.org/2000/svg"
+ viewBox="-2.2 -2 28 28"
+ fill="white"
+ className="w-full h-full flex items-center justify-center"
+ >
+ <path d="M11.25 4.533A9.707 9.707 0 006 3a9.735 9.735 0 00-3.25.555.75.75 0 00-.5.707v14.25a.75.75 0 001 .707A8.237 8.237 0 016 18.75c1.995 0 3.823.707 5.25 1.886V4.533zM12.75 20.636A8.214 8.214 0 0118 18.75c.966 0 1.89.166 2.75.47a.75.75 0 001-.708V4.262a.75.75 0 00-.5-.707A9.735 9.735 0 0018 3a9.707 9.707 0 00-5.25 1.533v16.103z" />
+ </svg>
+ </a>
+ </IconDiv>
+ <IconDiv backgroundColor="rgb(88, 98, 227)">
+ <a href="https://discord.gg/vapESyrFmJ">
+ <svg
+ xmlns="http://www.w3.org/2000/svg"
+ viewBox="-5 -5.5 60 60"
+ fill="white"
+ className="w-full h-full"
+ >
+ <path d="M 41.625 10.769531 C 37.644531 7.566406 31.347656 7.023438 31.078125 7.003906 C 30.660156 6.96875 30.261719 7.203125 30.089844 7.589844 C 30.074219 7.613281 29.9375 7.929688 29.785156 8.421875 C 32.417969 8.867188 35.652344 9.761719 38.578125 11.578125 C 39.046875 11.867188 39.191406 12.484375 38.902344 12.953125 C 38.710938 13.261719 38.386719 13.429688 38.050781 13.429688 C 37.871094 13.429688 37.6875 13.378906 37.523438 13.277344 C 32.492188 10.15625 26.210938 10 25 10 C 23.789063 10 17.503906 10.15625 12.476563 13.277344 C 12.007813 13.570313 11.390625 13.425781 11.101563 12.957031 C 10.808594 12.484375 10.953125 11.871094 11.421875 11.578125 C 14.347656 9.765625 17.582031 8.867188 20.214844 8.425781 C 20.0625 7.929688 19.925781 7.617188 19.914063 7.589844 C 19.738281 7.203125 19.34375 6.960938 18.921875 7.003906 C 18.652344 7.023438 12.355469 7.566406 8.320313 10.8125 C 6.214844 12.761719 2 24.152344 2 34 C 2 34.175781 2.046875 34.34375 2.132813 34.496094 C 5.039063 39.605469 12.972656 40.941406 14.78125 41 C 14.789063 41 14.800781 41 14.8125 41 C 15.132813 41 15.433594 40.847656 15.621094 40.589844 L 17.449219 38.074219 C 12.515625 36.800781 9.996094 34.636719 9.851563 34.507813 C 9.4375 34.144531 9.398438 33.511719 9.765625 33.097656 C 10.128906 32.683594 10.761719 32.644531 11.175781 33.007813 C 11.234375 33.0625 15.875 37 25 37 C 34.140625 37 38.78125 33.046875 38.828125 33.007813 C 39.242188 32.648438 39.871094 32.683594 40.238281 33.101563 C 40.601563 33.515625 40.5625 34.144531 40.148438 34.507813 C 40.003906 34.636719 37.484375 36.800781 32.550781 38.074219 L 34.378906 40.589844 C 34.566406 40.847656 34.867188 41 35.1875 41 C 35.199219 41 35.210938 41 35.21875 41 C 37.027344 40.941406 44.960938 39.605469 47.867188 34.496094 C 47.953125 34.34375 48 34.175781 48 34 C 48 24.152344 43.785156 12.761719 41.625 10.769531 Z M 18.5 30 C 16.566406 30 15 28.210938 15 26 C 15 23.789063 16.566406 22 18.5 22 C 20.433594 22 22 23.789063 22 26 C 22 28.210938 20.433594 30 18.5 30 Z M 31.5 30 C 29.566406 30 28 28.210938 28 26 C 28 23.789063 29.566406 22 31.5 22 C 33.433594 22 35 23.789063 35 26 C 35 28.210938 33.433594 30 31.5 30 Z"></path>
+ </svg>
+ </a>
+ </IconDiv>
+ <IconDiv>
+ <a href="https://github.com/continuedev/continue/issues/new/choose">
+ <svg
+ xmlns="http://www.w3.org/2000/svg"
+ viewBox="-1.2 -1.2 32 32"
+ fill="white"
+ className="w-full h-full"
+ >
+ <path d="M15,3C8.373,3,3,8.373,3,15c0,5.623,3.872,10.328,9.092,11.63C12.036,26.468,12,26.28,12,26.047v-2.051 c-0.487,0-1.303,0-1.508,0c-0.821,0-1.551-0.353-1.905-1.009c-0.393-0.729-0.461-1.844-1.435-2.526 c-0.289-0.227-0.069-0.486,0.264-0.451c0.615,0.174,1.125,0.596,1.605,1.222c0.478,0.627,0.703,0.769,1.596,0.769 c0.433,0,1.081-0.025,1.691-0.121c0.328-0.833,0.895-1.6,1.588-1.962c-3.996-0.411-5.903-2.399-5.903-5.098 c0-1.162,0.495-2.286,1.336-3.233C9.053,10.647,8.706,8.73,9.435,8c1.798,0,2.885,1.166,3.146,1.481C13.477,9.174,14.461,9,15.495,9 c1.036,0,2.024,0.174,2.922,0.483C18.675,9.17,19.763,8,21.565,8c0.732,0.731,0.381,2.656,0.102,3.594 c0.836,0.945,1.328,2.066,1.328,3.226c0,2.697-1.904,4.684-5.894,5.097C18.199,20.49,19,22.1,19,23.313v2.734 c0,0.104-0.023,0.179-0.035,0.268C23.641,24.676,27,20.236,27,15C27,8.373,21.627,3,15,3z"></path>
+ </svg>
+ </a>
+ </IconDiv>
+ </div>
+
+ <KeyboardShortcutsDialog></KeyboardShortcutsDialog>
+ </div>
+ );
+}
+
+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<SessionInfo[]>([]);
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 (
- <div className="w-full">
- <div className="items-center flex">
- <ArrowLeftIcon
- width="1.4em"
- height="1.4em"
- onClick={() => navigate("/")}
- className="inline-block ml-4 cursor-pointer"
- />
- <h1 className="text-xl font-bold m-4 inline-block">History</h1>
+ <div className="overflow-y-scroll">
+ <div className="sticky top-0" style={{ backgroundColor: vscBackground }}>
+ <div
+ className="items-center flex m-0 p-0"
+ style={{
+ borderBottom: `0.5px solid ${lightGray}`,
+ }}
+ >
+ <ArrowLeftIcon
+ width="1.2em"
+ height="1.2em"
+ onClick={() => navigate("/")}
+ className="inline-block ml-4 cursor-pointer"
+ />
+ <h3 className="text-lg font-bold m-2 inline-block">History</h3>
+ </div>
+ {workspacePaths && workspacePaths.length > 0 && (
+ <CheckDiv
+ checked={filteringByWorkspace}
+ onClick={() => setFilteringByWorkspace((prev) => !prev)}
+ title={`Show only sessions from ${lastPartOfPath(
+ workspacePaths[workspacePaths.length - 1]
+ )}/`}
+ />
+ )}
</div>
- {workspacePaths && workspacePaths.length > 0 && (
- <CheckDiv
- checked={filteringByWorkspace}
- onClick={() => 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 && (
+ <div className="text-center my-4">
+ No past sessions found. To start a new session, either click the "+"
+ button or use the keyboard shortcut: <b>Option + Command + N</b>
+ </div>
)}
- <table className="w-full">
- <tbody>
- {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) => (
- <Tr key={index}>
- <td>
- <TdDiv
- onClick={() => {
- client?.loadSession(session.session_id);
- navigate("/");
- }}
- >
- <div className="text-md">{session.title}</div>
- <div className="text-gray-400">
- {parseDate(session.date_created).toLocaleString("en-US", {
- weekday: "short",
- year: "numeric",
- month: "long",
- day: "numeric",
- hour: "numeric",
- minute: "numeric",
- })}
- {" | "}
- {lastPartOfPath(session.workspace_directory || "")}/
- </div>
- </TdDiv>
- </td>
- </Tr>
- ))}
- </tbody>
- </table>
- <br />
- <i className="text-sm ml-4">
- All session data is saved in ~/.continue/sessions
- </i>
+
+ <div>
+ <table className="w-full">
+ <tbody>
+ {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) => (
+ <Tr key={index}>
+ <td>
+ <TdDiv
+ onClick={() => {
+ client?.loadSession(session.session_id);
+ dispatch(temporarilyClearSession());
+ navigate("/");
+ }}
+ >
+ <div className="text-md">{session.title}</div>
+ <div className="text-gray-400">
+ {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 || "")}/
+ </div>
+ </TdDiv>
+ </td>
+ </Tr>
+ ))}
+ </tbody>
+ </table>
+ <br />
+ <i className="text-sm ml-4">
+ All session data is saved in ~/.continue/sessions
+ </i>
+ </div>
</div>
);
}
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: "<ANTHROPIC_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: "<TOGETHER_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: "<REPLICATE_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: "<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 (
+ <div className="overflow-y-scroll">
+ <div
+ className="items-center flex m-0 p-0 sticky top-0"
+ style={{
+ borderBottom: `0.5px solid ${lightGray}`,
+ backgroundColor: vscBackground,
+ }}
+ >
+ <ArrowLeftIcon
+ width="1.2em"
+ height="1.2em"
+ onClick={() => navigate("/")}
+ className="inline-block ml-4 cursor-pointer"
+ />
+ <h3 className="text-lg font-bold m-2 inline-block">Add a new model</h3>
+ </div>
+ <GridDiv>
+ {MODEL_INFO.map((model) => (
+ <ModelCard modelInfo={model} />
+ ))}
+ </GridDiv>
+ </div>
+ );
+}
+
+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<ContinueConfig>();
@@ -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 (
<FormProvider {...formMethods}>
- <div className="w-full">
+ <div className="overflow-scroll">
+ <div
+ className="items-center flex sticky top-0"
+ style={{
+ borderBottom: `0.5px solid ${lightGray}`,
+ backgroundColor: vscBackground,
+ }}
+ >
+ <ArrowLeftIcon
+ width="1.2em"
+ height="1.2em"
+ onClick={submitAndLeave}
+ className="inline-block ml-4 cursor-pointer"
+ />
+ <h3 className="text-lg font-bold m-2 inline-block">Settings</h3>
+ </div>
<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">