summaryrefslogtreecommitdiff
path: root/extension/react-app/src
diff options
context:
space:
mode:
authorNate Sesti <33237525+sestinj@users.noreply.github.com>2023-09-23 13:06:00 -0700
committerGitHub <noreply@github.com>2023-09-23 13:06:00 -0700
commite976d60974a7837967d03807605cbf2e7b4f3f9a (patch)
tree5ecb19062abb162832530dd953e9d2801026c23c /extension/react-app/src
parent470711d25b44d1a545c57bc17d40d5e1fd402216 (diff)
downloadsncontinue-e976d60974a7837967d03807605cbf2e7b4f3f9a.tar.gz
sncontinue-e976d60974a7837967d03807605cbf2e7b4f3f9a.tar.bz2
sncontinue-e976d60974a7837967d03807605cbf2e7b4f3f9a.zip
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 <ty@tydunn.com>
Diffstat (limited to 'extension/react-app/src')
-rw-r--r--extension/react-app/src/App.tsx15
-rw-r--r--extension/react-app/src/components/CheckDiv.tsx3
-rw-r--r--extension/react-app/src/components/ComboBox.tsx416
-rw-r--r--extension/react-app/src/components/ContinueButton.tsx33
-rw-r--r--extension/react-app/src/components/ErrorStepContainer.tsx52
-rw-r--r--extension/react-app/src/components/HeaderButtonWithText.tsx14
-rw-r--r--extension/react-app/src/components/Layout.tsx61
-rw-r--r--extension/react-app/src/components/ModelCard.tsx122
-rw-r--r--extension/react-app/src/components/ModelSelect.tsx43
-rw-r--r--extension/react-app/src/components/ModelSettings.tsx2
-rw-r--r--extension/react-app/src/components/Onboarding.tsx130
-rw-r--r--extension/react-app/src/components/PillButton.tsx216
-rw-r--r--extension/react-app/src/components/ProgressBar.tsx9
-rw-r--r--extension/react-app/src/components/StepContainer.tsx241
-rw-r--r--extension/react-app/src/components/Suggestions.tsx228
-rw-r--r--extension/react-app/src/components/TimelineItem.tsx59
-rw-r--r--extension/react-app/src/components/UserInputContainer.tsx376
-rw-r--r--extension/react-app/src/components/dialogs/FTCDialog.tsx72
-rw-r--r--extension/react-app/src/components/dialogs/KeyboardShortcuts.tsx129
-rw-r--r--extension/react-app/src/components/index.ts7
-rw-r--r--extension/react-app/src/hooks/AbstractContinueGUIClientProtocol.ts4
-rw-r--r--extension/react-app/src/hooks/ContinueGUIClientProtocol.ts17
-rw-r--r--extension/react-app/src/index.css6
-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
-rw-r--r--extension/react-app/src/redux/slices/serverStateReducer.ts84
29 files changed, 2188 insertions, 1071 deletions
diff --git a/extension/react-app/src/App.tsx b/extension/react-app/src/App.tsx
index edcac4a0..bbb1a952 100644
--- a/extension/react-app/src/App.tsx
+++ b/extension/react-app/src/App.tsx
@@ -1,5 +1,6 @@
import GUI from "./pages/gui";
import History from "./pages/history";
+import Help from "./pages/help";
import Layout from "./components/Layout";
import { createContext, useEffect } from "react";
import useContinueGUIProtocol from "./hooks/useWebsocket";
@@ -18,6 +19,8 @@ import { postVscMessage } from "./vscode";
import { RouterProvider, createMemoryRouter } from "react-router-dom";
import ErrorPage from "./pages/error";
import SettingsPage from "./pages/settings";
+import Models from "./pages/models";
+import HelpPage from "./pages/help";
const router = createMemoryRouter([
{
@@ -38,9 +41,21 @@ const router = createMemoryRouter([
element: <History />,
},
{
+ path: "/help",
+ element: <Help />,
+ },
+ {
path: "/settings",
element: <SettingsPage />,
},
+ {
+ path: "/models",
+ element: <Models />,
+ },
+ {
+ path: "/help",
+ element: <HelpPage />,
+ },
],
},
]);
diff --git a/extension/react-app/src/components/CheckDiv.tsx b/extension/react-app/src/components/CheckDiv.tsx
index e595d70b..eaea0dc1 100644
--- a/extension/react-app/src/components/CheckDiv.tsx
+++ b/extension/react-app/src/components/CheckDiv.tsx
@@ -30,6 +30,9 @@ const StyledDiv = styled.div<{ checked: boolean }>`
margin: 0.5rem;
height: 1.4em;
+
+ overflow: hidden;
+ text-overflow: ellipsis;
`;
function CheckDiv(props: CheckDivProps) {
diff --git a/extension/react-app/src/components/ComboBox.tsx b/extension/react-app/src/components/ComboBox.tsx
index 48df368b..e63499bc 100644
--- a/extension/react-app/src/components/ComboBox.tsx
+++ b/extension/react-app/src/components/ComboBox.tsx
@@ -3,12 +3,12 @@ import React, {
useContext,
useEffect,
useImperativeHandle,
+ useLayoutEffect,
useState,
} from "react";
import { useCombobox } from "downshift";
import styled from "styled-components";
import {
- StyledTooltip,
buttonColor,
defaultBorderRadius,
lightGray,
@@ -19,53 +19,51 @@ import {
import PillButton from "./PillButton";
import HeaderButtonWithText from "./HeaderButtonWithText";
import {
- BookmarkIcon,
- DocumentPlusIcon,
- FolderArrowDownIcon,
ArrowLeftIcon,
- PlusIcon,
ArrowRightIcon,
+ MagnifyingGlassIcon,
+ TrashIcon,
} from "@heroicons/react/24/outline";
-import { ContextItem } from "../../../schema/FullState";
import { postVscMessage } from "../vscode";
import { GUIClientContext } from "../App";
import { MeiliSearch } from "meilisearch";
-import {
- setBottomMessage,
- setDialogMessage,
- setShowDialog,
-} from "../redux/slices/uiStateSlice";
+import { setBottomMessage } from "../redux/slices/uiStateSlice";
import { useDispatch, useSelector } from "react-redux";
import { RootStore } from "../redux/store";
-import SelectContextGroupDialog from "./dialogs/SelectContextGroupDialog";
-import AddContextGroupDialog from "./dialogs/AddContextGroupDialog";
+import ContinueButton from "./ContinueButton";
const SEARCH_INDEX_NAME = "continue_context_items";
// #region styled components
-const mainInputFontSize = 13;
-const EmptyPillDiv = styled.div`
- padding: 4px;
- padding-left: 8px;
- padding-right: 8px;
- border-radius: ${defaultBorderRadius};
- border: 1px dashed ${lightGray};
- color: ${lightGray};
- background-color: ${vscBackground};
- overflow: hidden;
+const HiddenHeaderButtonWithText = styled.button`
+ opacity: 0;
+ background-color: transparent;
+ border: none;
+ outline: none;
+ color: ${vscForeground};
+ cursor: pointer;
display: flex;
align-items: center;
- text-align: center;
- cursor: pointer;
- font-size: 13px;
+ justify-content: center;
+ height: 0;
+ aspect-ratio: 1;
+ padding: 0;
+ margin-left: -8px;
+
+ border-radius: ${defaultBorderRadius};
- &:hover {
- background-color: ${lightGray};
- color: ${vscBackground};
+ &:focus {
+ margin-left: 1px;
+ height: fit-content;
+ padding: 3px;
+ opacity: 1;
+ outline: 1px solid ${lightGray};
}
`;
+const mainInputFontSize = 13;
+
const MainTextInput = styled.textarea<{ inQueryForDynamicProvider: boolean }>`
resize: none;
@@ -79,20 +77,20 @@ const MainTextInput = styled.textarea<{ inQueryForDynamicProvider: boolean }>`
background-color: ${secondaryDark};
color: ${vscForeground};
z-index: 1;
- border: 1px solid
+ border: 0.5px solid
${(props) =>
props.inQueryForDynamicProvider ? buttonColor : "transparent"};
&:focus {
- outline: 1px solid
+ outline: 0.5px solid
${(props) => (props.inQueryForDynamicProvider ? buttonColor : lightGray)};
- border: 1px solid transparent;
+ border: 0.5px solid transparent;
background-color: ${(props) =>
props.inQueryForDynamicProvider ? `${buttonColor}22` : secondaryDark};
}
&::placeholder {
- color: ${lightGray}80;
+ color: ${lightGray}cc;
}
`;
@@ -110,23 +108,6 @@ const DynamicQueryTitleDiv = styled.div`
background-color: ${buttonColor};
`;
-const StyledPlusIcon = styled(PlusIcon)`
- position: absolute;
- right: 0px;
- top: 0px;
- height: fit-content;
- padding: 0;
- cursor: pointer;
- border-radius: ${defaultBorderRadius};
- z-index: 2;
-
- background-color: ${vscBackground};
-
- &:hover {
- background-color: ${secondaryDark};
- }
-`;
-
const UlMaxHeight = 300;
const Ul = styled.ul<{
hidden: boolean;
@@ -137,7 +118,7 @@ const Ul = styled.ul<{
${(props) =>
props.showAbove
? `transform: translateY(-${props.ulHeightPixels + 8}px);`
- : `transform: translateY(${5 * mainInputFontSize}px);`}
+ : `transform: translateY(${5 * mainInputFontSize - 2}px);`}
position: absolute;
background: ${vscBackground};
color: ${vscForeground};
@@ -148,7 +129,7 @@ const Ul = styled.ul<{
padding: 0;
${({ hidden }) => hidden && "display: none;"}
border-radius: ${defaultBorderRadius};
- outline: 1px solid ${lightGray};
+ outline: 0.5px solid ${lightGray};
z-index: 2;
-ms-overflow-style: none;
@@ -180,14 +161,17 @@ const Li = styled.li<{
// #endregion
+interface ComboBoxItem {
+ name: string;
+ description: string;
+ id?: string;
+ content?: string;
+}
interface ComboBoxProps {
- items: { name: string; description: string; id?: string; content?: string }[];
onInputValueChange: (inputValue: string) => void;
disabled?: boolean;
- onEnter: (e: React.KeyboardEvent<HTMLInputElement>) => void;
- selectedContextItems: ContextItem[];
+ onEnter: (e?: React.KeyboardEvent<HTMLInputElement>) => void;
onToggleAddContext: () => void;
- addingHighlightedCode: boolean;
}
const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
@@ -197,14 +181,11 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
const workspacePaths = useSelector(
(state: RootStore) => state.config.workspacePaths
);
- const savedContextGroups = useSelector(
- (state: RootStore) => state.serverState.saved_context_groups
- );
const [history, setHistory] = React.useState<string[]>([]);
// The position of the current command you are typing now, so the one that will be appended to history once you press enter
const [positionInHistory, setPositionInHistory] = React.useState<number>(0);
- const [items, setItems] = React.useState(props.items);
+ const [items, setItems] = React.useState<ComboBoxItem[]>([]);
const inputRef = React.useRef<HTMLInputElement>(null);
@@ -217,6 +198,27 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
any | undefined
>(undefined);
+ const sessionId = useSelector(
+ (state: RootStore) => state.serverState.session_info?.session_id
+ );
+ const availableSlashCommands = useSelector(
+ (state: RootStore) => state.serverState.slash_commands
+ ).map((cmd) => {
+ return {
+ name: `/${cmd.name}`,
+ description: cmd.description,
+ };
+ });
+ const selectedContextItems = useSelector(
+ (state: RootStore) => state.serverState.selected_context_items
+ );
+
+ useEffect(() => {
+ if (inputRef.current) {
+ inputRef.current.focus();
+ }
+ }, [sessionId, inputRef.current]);
+
useEffect(() => {
if (!currentlyInContextQuery) {
setNestedContextProvider(undefined);
@@ -237,7 +239,6 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
useEffect(() => {
if (!nestedContextProvider) {
- console.log("setting items", nestedContextProvider);
setItems(
contextProviders?.map((provider) => ({
name: provider.display_title,
@@ -248,6 +249,8 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
}
}, [nestedContextProvider]);
+ const [prevInputValue, setPrevInputValue] = useState("");
+
const onInputValueChangeCallback = useCallback(
({ inputValue, highlightedIndex }: any) => {
// Clear the input
@@ -257,6 +260,18 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
setCurrentlyInContextQuery(false);
return;
}
+
+ // Hacky way of stopping bug where first context provider title is injected into input
+ if (
+ prevInputValue === "" &&
+ contextProviders.some((p) => p.display_title === inputValue)
+ ) {
+ downshiftProps.setInputValue("");
+ setPrevInputValue("");
+ return;
+ }
+ setPrevInputValue(inputValue);
+
if (
inQueryForContextProvider &&
!inputValue.startsWith(`@${inQueryForContextProvider.title}`)
@@ -277,9 +292,10 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
if (nestedContextProvider && !inputValue.endsWith("@")) {
// Search only within this specific context provider
+ const spaceSegs = providerAndQuery.split(" ");
getFilteredContextItemsForProvider(
nestedContextProvider.title,
- providerAndQuery
+ spaceSegs.length > 1 ? spaceSegs[1] : ""
).then((res) => {
setItems(res);
});
@@ -316,48 +332,19 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
// Handle slash commands
setItems(
- props.items?.filter((item) =>
- item.name.toLowerCase().startsWith(inputValue.toLowerCase())
+ availableSlashCommands?.filter((slashCommand) =>
+ slashCommand.name.toLowerCase().startsWith(inputValue.toLowerCase())
) || []
);
},
[
- props.items,
+ availableSlashCommands,
currentlyInContextQuery,
nestedContextProvider,
inQueryForContextProvider,
]
);
- const onSelectedItemChangeCallback = useCallback(
- ({ selectedItem }: any) => {
- if (!selectedItem) return;
- if (selectedItem.id) {
- // Get the query from the input value
- const segs = downshiftProps.inputValue.split("@");
- const query = segs[segs.length - 1];
-
- // Tell server the context item was selected
- client?.selectContextItem(selectedItem.id, query);
- if (downshiftProps.inputValue.includes("@")) {
- const selectedNestedContextProvider = contextProviders.find(
- (provider) => provider.title === selectedItem.id
- );
- if (
- !nestedContextProvider &&
- !selectedNestedContextProvider?.dynamic
- ) {
- downshiftProps.setInputValue(`@${selectedItem.id} `);
- setNestedContextProvider(selectedNestedContextProvider);
- } else {
- downshiftProps.setInputValue("");
- }
- }
- }
- },
- [nestedContextProvider, contextProviders, client]
- );
-
const getFilteredContextItemsForProvider = async (
provider: string,
query: string
@@ -390,7 +377,6 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
};
const { getInputProps, ...downshiftProps } = useCombobox({
- onSelectedItemChange: onSelectedItemChangeCallback,
onInputValueChange: onInputValueChangeCallback,
items,
itemToString(item) {
@@ -427,7 +413,6 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
const focusedItemIndex = focusableItemsArray.findIndex(
(item) => item === document.activeElement
);
- console.log(focusedItemIndex, focusableItems);
if (focusedItemIndex === focusableItemsArray.length - 1) {
inputRef.current?.focus();
} else if (focusedItemIndex !== -1) {
@@ -457,6 +442,13 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
}
}, []);
+ useLayoutEffect(() => {
+ if (!ulRef.current) {
+ return;
+ }
+ downshiftProps.setHighlightedIndex(0);
+ }, [items, downshiftProps.setHighlightedIndex, ulRef.current]);
+
const [metaKeyPressed, setMetaKeyPressed] = useState(false);
const [focused, setFocused] = useState(false);
useEffect(() => {
@@ -476,7 +468,7 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
window.removeEventListener("keydown", handleKeyDown);
window.removeEventListener("keyup", handleKeyUp);
};
- });
+ }, []);
useEffect(() => {
if (!inputRef.current) {
@@ -489,7 +481,9 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
} else if (event.data.type === "focusContinueInputWithEdit") {
inputRef.current!.focus();
- downshiftProps.setInputValue("/edit ");
+ if (!inputRef.current?.value.startsWith("/edit")) {
+ downshiftProps.setInputValue("/edit ");
+ }
}
};
window.addEventListener("message", handler);
@@ -500,21 +494,69 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
const selectContextItemFromDropdown = useCallback(
(event: any) => {
- const newProviderName = items[downshiftProps.highlightedIndex].name;
+ const newItem = items[downshiftProps.highlightedIndex];
+ const newProviderName = newItem?.name;
const newProvider = contextProviders.find(
(provider) => provider.display_title === newProviderName
);
if (!newProvider) {
+ if (nestedContextProvider && newItem.id) {
+ // Tell server the context item was selected
+ client?.selectContextItem(newItem.id, "");
+
+ // Clear the input
+ downshiftProps.setInputValue("");
+ setCurrentlyInContextQuery(false);
+ setNestedContextProvider(undefined);
+ setInQueryForContextProvider(undefined);
+ (event.nativeEvent as any).preventDownshiftDefault = true;
+ event.preventDefault();
+ return;
+ }
+ // This is a slash command
(event.nativeEvent as any).preventDownshiftDefault = true;
+ event.preventDefault();
return;
} else if (newProvider.dynamic && newProvider.requires_query) {
+ // This is a dynamic context provider that requires a query, like URL / Search
setInQueryForContextProvider(newProvider);
downshiftProps.setInputValue(`@${newProvider.title} `);
(event.nativeEvent as any).preventDownshiftDefault = true;
event.preventDefault();
return;
} else if (newProvider.dynamic) {
+ // This is a normal dynamic context provider like Diff or Terminal
+ if (!newItem.id) return;
+
+ // Get the query from the input value
+ const segs = downshiftProps.inputValue.split("@");
+ const query = segs[segs.length - 1];
+
+ // Tell server the context item was selected
+ client?.selectContextItem(newItem.id, query);
+ if (downshiftProps.inputValue.includes("@")) {
+ const selectedNestedContextProvider = contextProviders.find(
+ (provider) => provider.title === newItem.id
+ );
+ if (
+ !nestedContextProvider &&
+ !selectedNestedContextProvider?.dynamic
+ ) {
+ downshiftProps.setInputValue(`@${newItem.id} `);
+ setNestedContextProvider(selectedNestedContextProvider);
+ } else {
+ downshiftProps.setInputValue("");
+ }
+ }
+
+ // Clear the input
+ downshiftProps.setInputValue("");
+ setCurrentlyInContextQuery(false);
+ setNestedContextProvider(undefined);
+ setInQueryForContextProvider(undefined);
+ (event.nativeEvent as any).preventDownshiftDefault = true;
+ event.preventDefault();
return;
}
@@ -531,25 +573,10 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
downshiftProps.highlightedIndex,
contextProviders,
nestedContextProvider,
+ downshiftProps.inputValue,
]
);
- const showSelectContextGroupDialog = () => {
- dispatch(setDialogMessage(<SelectContextGroupDialog />));
- dispatch(setShowDialog(true));
- };
-
- const showDialogToSaveContextGroup = () => {
- dispatch(
- setDialogMessage(
- <AddContextGroupDialog
- selectedContextItems={props.selectedContextItems}
- />
- )
- );
- dispatch(setShowDialog(true));
- };
-
const [isComposing, setIsComposing] = useState(false);
return (
@@ -558,18 +585,36 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
className="px-2 flex gap-2 items-center flex-wrap mt-2"
ref={contextItemsDivRef}
>
- {props.selectedContextItems.map((item, idx) => {
+ <HiddenHeaderButtonWithText
+ className={selectedContextItems.length > 0 ? "pill-button" : ""}
+ onClick={() => {
+ client?.deleteContextWithIds(
+ selectedContextItems.map((item) => item.description.id)
+ );
+ inputRef.current?.focus();
+ }}
+ onKeyDown={(e: any) => {
+ if (e.key === "Backspace") {
+ client?.deleteContextWithIds(
+ selectedContextItems.map((item) => item.description.id)
+ );
+ inputRef.current?.focus();
+ }
+ }}
+ >
+ <TrashIcon width="1.4em" height="1.4em" />
+ </HiddenHeaderButtonWithText>
+ {selectedContextItems.map((item, idx) => {
return (
<PillButton
- areMultipleItems={props.selectedContextItems.length > 1}
+ areMultipleItems={selectedContextItems.length > 1}
key={`${item.description.id.item_id}${idx}`}
item={item}
- warning={
- item.content.length > 4000 && item.editing
- ? "Editing such a large range may be slow"
- : undefined
+ editing={
+ item.editing &&
+ (inputRef.current as any)?.value?.startsWith("/edit")
}
- addingHighlightedCode={props.addingHighlightedCode}
+ editingAny={(inputRef.current as any)?.value?.startsWith("/edit")}
index={idx}
onDelete={() => {
client?.deleteContextWithIds([item.description.id]);
@@ -578,64 +623,16 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
/>
);
})}
- <HeaderButtonWithText
- text="Load bookmarked context"
- onClick={() => {
- showSelectContextGroupDialog();
- }}
- className="pill-button focus:outline-none focus:border-red-600 focus:border focus:border-solid"
- onKeyDown={(e: KeyboardEvent) => {
- e.preventDefault();
- if (e.key === "Enter") {
- showSelectContextGroupDialog();
- }
- }}
- >
- <FolderArrowDownIcon width="1.4em" height="1.4em" />
- </HeaderButtonWithText>
- {props.selectedContextItems.length > 0 && (
- <>
- {props.addingHighlightedCode ? (
- <EmptyPillDiv
- onClick={() => {
- props.onToggleAddContext();
- }}
- >
- Highlight code section
- </EmptyPillDiv>
- ) : (
- <HeaderButtonWithText
- text="Add more code to context"
- onClick={() => {
- props.onToggleAddContext();
- }}
- className="pill-button focus:outline-none focus:border-red-600 focus:border focus:border-solid"
- onKeyDown={(e: KeyboardEvent) => {
- e.preventDefault();
- if (e.key === "Enter") {
- props.onToggleAddContext();
- }
- }}
- >
- <DocumentPlusIcon width="1.4em" height="1.4em" />
- </HeaderButtonWithText>
- )}
- <HeaderButtonWithText
- text="Bookmark context"
- onClick={() => {
- showDialogToSaveContextGroup();
- }}
- className="pill-button focus:outline-none focus:border-red-600 focus:border focus:border-solid"
- onKeyDown={(e: KeyboardEvent) => {
- e.preventDefault();
- if (e.key === "Enter") {
- showDialogToSaveContextGroup();
- }
- }}
- >
- <BookmarkIcon width="1.4em" height="1.4em" />
- </HeaderButtonWithText>
- </>
+
+ {selectedContextItems.length > 0 && (
+ <HeaderButtonWithText
+ onClick={() => {
+ client?.showContextVirtualFile();
+ }}
+ text="View Current Context"
+ >
+ <MagnifyingGlassIcon width="1.4em" height="1.4em" />
+ </HeaderButtonWithText>
)}
</div>
<div
@@ -648,7 +645,7 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
typeof inQueryForContextProvider !== "undefined"
}
disabled={props.disabled}
- placeholder={`Ask a question, type '/' for slash commands, or '@' to add context`}
+ placeholder={`Ask a question, '/' for slash commands, '@' to add context`}
{...getInputProps({
onCompositionStart: () => setIsComposing(true),
onCompositionEnd: () => setIsComposing(false),
@@ -701,13 +698,15 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
}
}
setCurrentlyInContextQuery(false);
+ } else if (event.key === "Enter" && currentlyInContextQuery) {
+ // Handle "Enter" for Context Providers
+ selectContextItemFromDropdown(event);
} else if (
- event.key === "Enter" &&
- currentlyInContextQuery &&
- nestedContextProvider === undefined
+ event.key === "Tab" &&
+ downshiftProps.isOpen &&
+ items.length > 0 &&
+ items[downshiftProps.highlightedIndex]?.name.startsWith("/")
) {
- selectContextItemFromDropdown(event);
- } else if (event.key === "Tab" && items.length > 0) {
downshiftProps.setInputValue(items[0].name);
event.preventDefault();
} else if (event.key === "Tab") {
@@ -789,25 +788,10 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
ref: inputRef,
})}
/>
- {inQueryForContextProvider ? (
+ {inQueryForContextProvider && (
<DynamicQueryTitleDiv>
Enter {inQueryForContextProvider.display_title} Query
</DynamicQueryTitleDiv>
- ) : (
- <>
- <StyledPlusIcon
- width="1.4em"
- height="1.4em"
- data-tooltip-id="add-context-button"
- onClick={() => {
- downshiftProps.setInputValue("@");
- inputRef.current?.focus();
- }}
- />
- <StyledTooltip id="add-context-button" place="bottom">
- Add Context to Prompt
- </StyledTooltip>
- </>
)}
<Ul
@@ -816,13 +800,17 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
})}
showAbove={showAbove()}
ulHeightPixels={ulRef.current?.getBoundingClientRect().height || 0}
- hidden={!downshiftProps.isOpen || items.length === 0}
+ hidden={
+ !downshiftProps.isOpen ||
+ items.length === 0 ||
+ inputRef.current?.value === ""
+ }
>
{nestedContextProvider && (
<div
style={{
backgroundColor: secondaryDark,
- borderBottom: `1px solid ${lightGray}`,
+ borderBottom: `0.5px solid ${lightGray}`,
display: "flex",
gap: "4px",
position: "sticky",
@@ -846,27 +834,27 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
items.map((item, index) => (
<Li
style={{
- borderTop: index === 0 ? "none" : undefined,
+ borderTop: index === 0 ? "none" : `0.5px solid ${lightGray}`,
}}
key={`${item.name}${index}`}
{...downshiftProps.getItemProps({ item, index })}
highlighted={downshiftProps.highlightedIndex === index}
selected={downshiftProps.selectedItem === item}
onClick={(e) => {
- // e.stopPropagation();
- // e.preventDefault();
- // (e.nativeEvent as any).preventDownshiftDefault = true;
- // downshiftProps.selectItem(item);
selectContextItemFromDropdown(e);
- onSelectedItemChangeCallback({ selectedItem: item });
+ e.stopPropagation();
+ e.preventDefault();
+ inputRef.current?.focus();
}}
>
- <span>
+ <span className="flex justify-between w-full">
{item.name}
{" "}
<span
style={{
color: lightGray,
+ float: "right",
+ textAlign: "right",
}}
>
{item.description}
@@ -888,7 +876,7 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
))}
</Ul>
</div>
- {props.selectedContextItems.length === 0 &&
+ {selectedContextItems.length === 0 &&
(downshiftProps.inputValue?.startsWith("/edit") ||
(focused &&
metaKeyPressed &&
@@ -897,6 +885,10 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
Inserting at cursor
</div>
)}
+ <ContinueButton
+ disabled={!(inputRef.current as any)?.value}
+ onClick={() => props.onEnter(undefined)}
+ />
</>
);
});
diff --git a/extension/react-app/src/components/ContinueButton.tsx b/extension/react-app/src/components/ContinueButton.tsx
index 10ecd94a..95dde177 100644
--- a/extension/react-app/src/components/ContinueButton.tsx
+++ b/extension/react-app/src/components/ContinueButton.tsx
@@ -1,26 +1,42 @@
-import styled, { keyframes } from "styled-components";
+import styled from "styled-components";
import { Button } from ".";
import { PlayIcon } from "@heroicons/react/24/outline";
import { useSelector } from "react-redux";
import { RootStore } from "../redux/store";
import { useEffect, useState } from "react";
-let StyledButton = styled(Button)<{ color?: string | null }>`
+const StyledButton = styled(Button)<{
+ color?: string | null;
+ isDisabled: boolean;
+}>`
margin: auto;
margin-top: 8px;
margin-bottom: 16px;
display: grid;
grid-template-columns: 22px 1fr;
align-items: center;
- background: ${(props) => props.color || "#be1b55"};
+ background-color: ${(props) => props.color || "#be1b55"};
- &:hover {
- transition-property: "background";
- opacity: 0.7;
+ opacity: ${(props) => (props.isDisabled ? 0.5 : 1.0)};
+
+ cursor: ${(props) => (props.isDisabled ? "default" : "pointer")};
+
+ &:hover:enabled {
+ background-color: ${(props) => props.color || "#be1b55"};
+ ${(props) =>
+ props.isDisabled
+ ? "cursor: default;"
+ : `
+ opacity: 0.7;
+ `}
}
`;
-function ContinueButton(props: { onClick?: () => void; hidden?: boolean }) {
+function ContinueButton(props: {
+ onClick?: () => void;
+ hidden?: boolean;
+ disabled: boolean;
+}) {
const vscMediaUrl = useSelector(
(state: RootStore) => state.config.vscMediaUrl
);
@@ -49,7 +65,8 @@ function ContinueButton(props: { onClick?: () => void; hidden?: boolean }) {
hidden={props.hidden}
style={{ fontSize: "10px" }}
className="m-auto press-start-2p"
- onClick={props.onClick}
+ onClick={props.disabled ? undefined : props.onClick}
+ isDisabled={props.disabled}
>
{vscMediaUrl ? (
<img src={`${vscMediaUrl}/play_button.png`} width="16px" />
diff --git a/extension/react-app/src/components/ErrorStepContainer.tsx b/extension/react-app/src/components/ErrorStepContainer.tsx
new file mode 100644
index 00000000..e8ab7950
--- /dev/null
+++ b/extension/react-app/src/components/ErrorStepContainer.tsx
@@ -0,0 +1,52 @@
+import React from "react";
+import styled from "styled-components";
+import { HistoryNode } from "../../../schema/HistoryNode";
+import { defaultBorderRadius, vscBackground } from ".";
+import HeaderButtonWithText from "./HeaderButtonWithText";
+import {
+ MinusCircleIcon,
+ MinusIcon,
+ XMarkIcon,
+} from "@heroicons/react/24/outline";
+
+const Div = styled.div`
+ padding: 8px;
+ background-color: #ff000011;
+ border-radius: ${defaultBorderRadius};
+ border: 1px solid #cc0000;
+`;
+
+interface ErrorStepContainerProps {
+ historyNode: HistoryNode;
+ onClose: () => void;
+ onDelete: () => void;
+}
+
+function ErrorStepContainer(props: ErrorStepContainerProps) {
+ return (
+ <div style={{ backgroundColor: vscBackground, position: "relative" }}>
+ <div
+ style={{
+ position: "absolute",
+ right: "4px",
+ top: "4px",
+ display: "flex",
+ }}
+ >
+ <HeaderButtonWithText text="Collapse" onClick={() => props.onClose()}>
+ <MinusCircleIcon width="1.3em" height="1.3em" />
+ </HeaderButtonWithText>
+ <HeaderButtonWithText text="Collapse" onClick={() => props.onDelete()}>
+ <XMarkIcon width="1.3em" height="1.3em" />
+ </HeaderButtonWithText>
+ </div>
+ <Div>
+ <pre className="overflow-x-scroll">
+ {props.historyNode.observation?.error as string}
+ </pre>
+ </Div>
+ </div>
+ );
+}
+
+export default ErrorStepContainer;
diff --git a/extension/react-app/src/components/HeaderButtonWithText.tsx b/extension/react-app/src/components/HeaderButtonWithText.tsx
index 3122c287..ca359250 100644
--- a/extension/react-app/src/components/HeaderButtonWithText.tsx
+++ b/extension/react-app/src/components/HeaderButtonWithText.tsx
@@ -1,5 +1,6 @@
import React, { useState } from "react";
import { HeaderButton, StyledTooltip } from ".";
+import ReactDOM from "react-dom";
interface HeaderButtonWithTextProps {
text: string;
@@ -14,6 +15,9 @@ interface HeaderButtonWithTextProps {
const HeaderButtonWithText = (props: HeaderButtonWithTextProps) => {
const [hover, setHover] = useState(false);
+
+ const tooltipPortalDiv = document.getElementById("tooltip-portal-div");
+
return (
<>
<HeaderButton
@@ -34,9 +38,13 @@ const HeaderButtonWithText = (props: HeaderButtonWithTextProps) => {
>
{props.children}
</HeaderButton>
- <StyledTooltip id={`header_button_${props.text}`} place="bottom">
- {props.text}
- </StyledTooltip>
+ {tooltipPortalDiv &&
+ ReactDOM.createPortal(
+ <StyledTooltip id={`header_button_${props.text}`} place="bottom">
+ {props.text}
+ </StyledTooltip>,
+ tooltipPortalDiv
+ )}
</>
);
};
diff --git a/extension/react-app/src/components/Layout.tsx b/extension/react-app/src/components/Layout.tsx
index 6410db8a..9ec2e671 100644
--- a/extension/react-app/src/components/Layout.tsx
+++ b/extension/react-app/src/components/Layout.tsx
@@ -1,7 +1,6 @@
import styled from "styled-components";
import { defaultBorderRadius, secondaryDark, vscForeground } from ".";
import { Outlet } from "react-router-dom";
-import Onboarding from "./Onboarding";
import TextDialog from "./TextDialog";
import { useContext, useEffect, useState } from "react";
import { GUIClientContext } from "../App";
@@ -15,10 +14,9 @@ import {
import {
PlusIcon,
FolderIcon,
- BookOpenIcon,
- ChatBubbleOvalLeftEllipsisIcon,
SparklesIcon,
Cog6ToothIcon,
+ QuestionMarkCircleIcon,
} from "@heroicons/react/24/outline";
import HeaderButtonWithText from "./HeaderButtonWithText";
import { useNavigate, useLocation } from "react-router-dom";
@@ -62,6 +60,8 @@ const Footer = styled.footer`
align-items: center;
width: calc(100% - 16px);
height: ${FOOTER_HEIGHT};
+
+ overflow: hidden;
`;
const GridDiv = styled.div`
@@ -98,11 +98,20 @@ const Layout = () => {
(state: RootStore) => state.uiState.displayBottomMessageOnBottom
);
+ const timeline = useSelector(
+ (state: RootStore) => state.serverState.history.timeline
+ );
+
// #endregion
useEffect(() => {
const handleKeyDown = (event: any) => {
- if (event.metaKey && event.altKey && event.code === "KeyN") {
+ if (
+ event.metaKey &&
+ event.altKey &&
+ event.code === "KeyN" &&
+ timeline.filter((n) => !n.step.hide).length > 0
+ ) {
client?.loadSession(undefined);
}
if ((event.metaKey || event.ctrlKey) && event.code === "KeyC") {
@@ -121,7 +130,7 @@ const Layout = () => {
return () => {
window.removeEventListener("keydown", handleKeyDown);
};
- }, [client]);
+ }, [client, timeline]);
return (
<LayoutTopDiv>
@@ -133,7 +142,6 @@ const Layout = () => {
gridTemplateRows: "1fr auto",
}}
>
- <Onboarding />
<TextDialog
showDialog={showDialog}
onEnter={() => {
@@ -176,54 +184,26 @@ const Layout = () => {
color="yellow"
/>
)}
-
<ModelSelect />
- {defaultModel === "MaybeProxyOpenAI" &&
+ {defaultModel === "OpenAIFreeTrial" &&
(location.pathname === "/settings" ||
- parseInt(localStorage.getItem("freeTrialCounter") || "0") >=
- 125) && (
+ parseInt(localStorage.getItem("ftc") || "0") >= 125) && (
<ProgressBar
- completed={parseInt(
- localStorage.getItem("freeTrialCounter") || "0"
- )}
+ completed={parseInt(localStorage.getItem("ftc") || "0")}
total={250}
/>
)}
</div>
<HeaderButtonWithText
+ text="Help"
onClick={() => {
- client?.loadSession(undefined);
+ navigate("/help");
}}
- text="New Session (⌥⌘N)"
>
- <PlusIcon width="1.4em" height="1.4em" />
+ <QuestionMarkCircleIcon width="1.4em" height="1.4em" />
</HeaderButtonWithText>
<HeaderButtonWithText
onClick={() => {
- navigate("/history");
- }}
- text="History"
- >
- <FolderIcon width="1.4em" height="1.4em" />
- </HeaderButtonWithText>
- <a
- href="https://continue.dev/docs/how-to-use-continue"
- className="no-underline"
- >
- <HeaderButtonWithText text="Docs">
- <BookOpenIcon width="1.4em" height="1.4em" />
- </HeaderButtonWithText>
- </a>
- <a
- href="https://github.com/continuedev/continue/issues/new/choose"
- className="no-underline"
- >
- <HeaderButtonWithText text="Feedback">
- <ChatBubbleOvalLeftEllipsisIcon width="1.4em" height="1.4em" />
- </HeaderButtonWithText>
- </a>
- <HeaderButtonWithText
- onClick={() => {
navigate("/settings");
}}
text="Settings"
@@ -248,6 +228,7 @@ const Layout = () => {
{bottomMessage}
</BottomMessageDiv>
</div>
+ <div id="tooltip-portal-div" />
</LayoutTopDiv>
);
};
diff --git a/extension/react-app/src/components/ModelCard.tsx b/extension/react-app/src/components/ModelCard.tsx
new file mode 100644
index 00000000..a537c5f4
--- /dev/null
+++ b/extension/react-app/src/components/ModelCard.tsx
@@ -0,0 +1,122 @@
+import React, { useContext } from "react";
+import styled from "styled-components";
+import { buttonColor, defaultBorderRadius, lightGray, vscForeground } from ".";
+import { setShowDialog } from "../redux/slices/uiStateSlice";
+import { GUIClientContext } from "../App";
+import { useDispatch, useSelector } from "react-redux";
+import { useNavigate } from "react-router-dom";
+import { RootStore } from "../redux/store";
+import { BookOpenIcon } from "@heroicons/react/24/outline";
+import HeaderButtonWithText from "./HeaderButtonWithText";
+import ReactDOM from "react-dom";
+
+export enum ModelTag {
+ "Requires API Key" = "Requires API Key",
+ "Local" = "Local",
+ "Free" = "Free",
+ "Open-Source" = "Open-Source",
+}
+
+const MODEL_TAG_COLORS: any = {};
+MODEL_TAG_COLORS[ModelTag["Requires API Key"]] = "#FF0000";
+MODEL_TAG_COLORS[ModelTag["Local"]] = "#00bb00";
+MODEL_TAG_COLORS[ModelTag["Open-Source"]] = "#0033FF";
+MODEL_TAG_COLORS[ModelTag["Free"]] = "#ffff00";
+
+export interface ModelInfo {
+ title: string;
+ class: string;
+ args: any;
+ description: string;
+ icon?: string;
+ tags?: ModelTag[];
+}
+
+const Div = styled.div<{ color: string }>`
+ border: 1px solid ${lightGray};
+ border-radius: ${defaultBorderRadius};
+ cursor: pointer;
+ padding: 4px 8px;
+ position: relative;
+ width: 100%;
+ transition: all 0.5s;
+
+ &:hover {
+ border: 1px solid ${(props) => props.color};
+ background-color: ${(props) => props.color}22;
+ }
+`;
+
+interface ModelCardProps {
+ modelInfo: ModelInfo;
+}
+
+function ModelCard(props: ModelCardProps) {
+ const client = useContext(GUIClientContext);
+ const dispatch = useDispatch();
+ const navigate = useNavigate();
+ const vscMediaUrl = useSelector(
+ (state: RootStore) => state.config.vscMediaUrl
+ );
+
+ return (
+ <Div
+ color={buttonColor}
+ onClick={(e) => {
+ if ((e.target as any).closest("a")) {
+ return;
+ }
+ client?.addModelForRole(
+ "*",
+ props.modelInfo.class,
+ props.modelInfo.args
+ );
+ dispatch(setShowDialog(false));
+ navigate("/");
+ }}
+ >
+ <div style={{ display: "flex", alignItems: "center" }}>
+ {vscMediaUrl && (
+ <img
+ src={`${vscMediaUrl}/logos/${props.modelInfo.icon}`}
+ height="24px"
+ style={{ marginRight: "10px" }}
+ />
+ )}
+ <h3>{props.modelInfo.title}</h3>
+ </div>
+ {props.modelInfo.tags?.map((tag) => {
+ return (
+ <span
+ style={{
+ backgroundColor: `${MODEL_TAG_COLORS[tag]}55`,
+ color: "white",
+ padding: "2px 4px",
+ borderRadius: defaultBorderRadius,
+ marginRight: "4px",
+ }}
+ >
+ {tag}
+ </span>
+ );
+ })}
+ <p>{props.modelInfo.description}</p>
+
+ <a
+ style={{
+ position: "absolute",
+ right: "8px",
+ top: "8px",
+ }}
+ href={`https://continue.dev/docs/reference/Models/${props.modelInfo.class.toLowerCase()}`}
+ target="_blank"
+ >
+ <HeaderButtonWithText text="Read the docs">
+ <BookOpenIcon width="1.6em" height="1.6em" />
+ </HeaderButtonWithText>
+ </a>
+ </Div>
+ );
+}
+
+export default ModelCard;
diff --git a/extension/react-app/src/components/ModelSelect.tsx b/extension/react-app/src/components/ModelSelect.tsx
index 0b1829f1..29d9250e 100644
--- a/extension/react-app/src/components/ModelSelect.tsx
+++ b/extension/react-app/src/components/ModelSelect.tsx
@@ -10,8 +10,9 @@ import { useContext } from "react";
import { GUIClientContext } from "../App";
import { RootStore } from "../redux/store";
import { useDispatch, useSelector } from "react-redux";
-import { PlusIcon } from "@heroicons/react/24/outline";
+import { ArrowLeftIcon, PlusIcon } from "@heroicons/react/24/outline";
import { setDialogMessage, setShowDialog } from "../redux/slices/uiStateSlice";
+import { useNavigate } from "react-router-dom";
const MODEL_INFO: { title: string; class: string; args: any }[] = [
{
@@ -83,7 +84,7 @@ const MODEL_INFO: { title: string; class: string; args: any }[] = [
},
{
title: "GPT-4 limited free trial",
- class: "MaybeProxyOpenAI",
+ class: "OpenAIFreeTrial",
args: {
model: "gpt-4",
},
@@ -159,10 +160,12 @@ function ModelSelect(props: {}) {
const defaultModel = useSelector(
(state: RootStore) => (state.serverState.config as any)?.models?.default
);
- const unusedModels = useSelector(
- (state: RootStore) => (state.serverState.config as any)?.models?.unused
+ const savedModels = useSelector(
+ (state: RootStore) => (state.serverState.config as any)?.models?.saved
);
+ const navigate = useNavigate();
+
return (
<GridDiv>
<Select
@@ -173,7 +176,7 @@ function ModelSelect(props: {}) {
defaultValue={0}
onChange={(e) => {
const value = JSON.parse(e.target.value);
- if (value.t === "unused") {
+ if (value.t === "saved") {
client?.setModelForRoleFromIndex("*", value.idx);
}
}}
@@ -188,11 +191,11 @@ function ModelSelect(props: {}) {
{modelSelectTitle(defaultModel)}
</option>
)}
- {unusedModels?.map((model: any, idx: number) => {
+ {savedModels?.map((model: any, idx: number) => {
return (
<option
value={JSON.stringify({
- t: "unused",
+ t: "saved",
idx,
})}
>
@@ -206,31 +209,7 @@ function ModelSelect(props: {}) {
width="1.3em"
height="1.3em"
onClick={() => {
- dispatch(
- setDialogMessage(
- <div>
- <div className="text-lg font-bold p-2">
- Setup a new model provider
- </div>
- <br />
- {MODEL_INFO.map((model, idx) => {
- return (
- <NewProviderDiv
- onClick={() => {
- const model = MODEL_INFO[idx];
- client?.addModelForRole("*", model.class, model.args);
- dispatch(setShowDialog(false));
- }}
- >
- {model.title}
- </NewProviderDiv>
- );
- })}
- <br />
- </div>
- )
- );
- dispatch(setShowDialog(true));
+ navigate("/models");
}}
/>
</GridDiv>
diff --git a/extension/react-app/src/components/ModelSettings.tsx b/extension/react-app/src/components/ModelSettings.tsx
index 99200502..06516687 100644
--- a/extension/react-app/src/components/ModelSettings.tsx
+++ b/extension/react-app/src/components/ModelSettings.tsx
@@ -27,7 +27,7 @@ const DefaultModelOptions: {
api_key: "",
model: "gpt-4",
},
- MaybeProxyOpenAI: {
+ OpenAIFreeTrial: {
api_key: "",
model: "gpt-4",
},
diff --git a/extension/react-app/src/components/Onboarding.tsx b/extension/react-app/src/components/Onboarding.tsx
deleted file mode 100644
index 588f7298..00000000
--- a/extension/react-app/src/components/Onboarding.tsx
+++ /dev/null
@@ -1,130 +0,0 @@
-import React, { useState, useEffect } from "react";
-import styled from "styled-components";
-import { ArrowLeftIcon, ArrowRightIcon } from "@heroicons/react/24/outline";
-import { defaultBorderRadius } from ".";
-import Loader from "./Loader";
-
-const StyledDiv = styled.div`
- position: absolute;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- background-color: #1e1e1e;
- z-index: 200;
-
- color: white;
-`;
-
-const StyledSpan = styled.span`
- padding: 8px;
- border-radius: ${defaultBorderRadius};
- &:hover {
- background-color: #ffffff33;
- }
- white-space: nowrap;
-`;
-
-const Onboarding = () => {
- const [counter, setCounter] = useState(4);
- const gifs = ["intro", "highlight", "question", "help"];
- const topMessages = [
- "Welcome!",
- "Highlight code",
- "Ask a question",
- "Use /help to learn more",
- ];
-
- useEffect(() => {
- const hasVisited = localStorage.getItem("hasVisited");
- if (hasVisited) {
- setCounter(4);
- } else {
- setCounter(0);
- localStorage.setItem("hasVisited", "true");
- }
- }, []);
-
- const [loading, setLoading] = useState(true);
-
- useEffect(() => {
- setLoading(true);
- }, [counter]);
-
- return (
- <StyledDiv hidden={counter >= 4}>
- <div
- style={{
- display: "grid",
- justifyContent: "center",
- alignItems: "center",
- height: "100%",
- textAlign: "center",
- paddingLeft: "16px",
- paddingRight: "16px",
- }}
- >
- <h1>{topMessages[counter]}</h1>
- <div style={{ display: "flex", justifyContent: "center" }}>
- {loading && (
- <div style={{ margin: "auto", position: "absolute", zIndex: 0 }}>
- <Loader />
- </div>
- )}
- {counter < 4 &&
- (counter % 2 === 0 ? (
- <img
- src={`https://github.com/continuedev/continue/blob/main/media/${gifs[counter]}.gif?raw=true`}
- width="100%"
- key={"even-gif"}
- alt={topMessages[counter]}
- onLoad={() => {
- setLoading(false);
- }}
- style={{ zIndex: 1 }}
- />
- ) : (
- <img
- src={`https://github.com/continuedev/continue/blob/main/media/${gifs[counter]}.gif?raw=true`}
- width="100%"
- key={"odd-gif"}
- alt={topMessages[counter]}
- onLoad={() => {
- setLoading(false);
- }}
- style={{ zIndex: 1 }}
- />
- ))}
- </div>
- <p
- style={{
- paddingLeft: "50px",
- paddingRight: "50px",
- paddingBottom: "50px",
- textAlign: "center",
- cursor: "pointer",
- whiteSpace: "nowrap",
- }}
- >
- <StyledSpan
- hidden={counter === 0}
- onClick={() => setCounter((prev) => Math.max(prev - 1, 0))}
- >
- <ArrowLeftIcon width="18px" strokeWidth="2px" /> Previous
- </StyledSpan>
- <span hidden={counter === 0}>{" | "}</span>
- <StyledSpan onClick={() => setCounter((prev) => prev + 1)}>
- {counter === 0
- ? "Click to learn how to use Continue"
- : counter === 3
- ? "Get Started"
- : "Next"}{" "}
- <ArrowRightIcon width="18px" strokeWidth="2px" />
- </StyledSpan>
- </p>
- </div>
- </StyledDiv>
- );
-};
-
-export default Onboarding;
diff --git a/extension/react-app/src/components/PillButton.tsx b/extension/react-app/src/components/PillButton.tsx
index 1ffdeeed..4b602619 100644
--- a/extension/react-app/src/components/PillButton.tsx
+++ b/extension/react-app/src/components/PillButton.tsx
@@ -1,4 +1,4 @@
-import { useContext, useEffect, useRef, useState } from "react";
+import { useContext, useEffect, useState } from "react";
import styled from "styled-components";
import {
StyledTooltip,
@@ -15,13 +15,8 @@ import {
} from "@heroicons/react/24/outline";
import { GUIClientContext } from "../App";
import { useDispatch } from "react-redux";
-import {
- setBottomMessage,
- setBottomMessageCloseTimeout,
-} from "../redux/slices/uiStateSlice";
+import { setBottomMessage } from "../redux/slices/uiStateSlice";
import { ContextItem } from "../../../schema/FullState";
-import { ReactMarkdown } from "react-markdown/lib/react-markdown";
-import StyledMarkdownPreview from "./StyledMarkdownPreview";
const Button = styled.button`
border: none;
@@ -80,33 +75,27 @@ const CircleDiv = styled.div`
interface PillButtonProps {
onHover?: (arg0: boolean) => void;
item: ContextItem;
- warning?: string;
+ editing: boolean;
+ editingAny: boolean;
index: number;
- addingHighlightedCode?: boolean;
areMultipleItems?: boolean;
onDelete?: () => void;
}
interface StyledButtonProps {
- warning: string;
+ borderColor?: string;
editing?: boolean;
- areMultipleItems?: boolean;
}
const StyledButton = styled(Button)<StyledButtonProps>`
position: relative;
- border-color: ${(props) =>
- props.warning
- ? "red"
- : props.editing && props.areMultipleItems
- ? vscForeground
- : "transparent"};
+ border-color: ${(props) => props.borderColor || "transparent"};
border-width: 1px;
border-style: solid;
&:focus {
outline: none;
- border-color: ${vscForeground};
+ border-color: ${lightGray};
border-width: 1px;
border-style: solid;
}
@@ -116,82 +105,56 @@ const PillButton = (props: PillButtonProps) => {
const [isHovered, setIsHovered] = useState(false);
const client = useContext(GUIClientContext);
- const dispatch = useDispatch();
+ const [warning, setWarning] = useState<string | undefined>(undefined);
useEffect(() => {
- if (isHovered) {
- dispatch(setBottomMessageCloseTimeout(undefined));
- dispatch(
- setBottomMessage(
- <>
- <b>{props.item.description.name}</b>:{" "}
- {props.item.description.description}
- <pre>
- <code
- style={{
- fontSize: "12px",
- backgroundColor: "transparent",
- color: vscForeground,
- whiteSpace: "pre-wrap",
- wordWrap: "break-word",
- }}
- >
- {props.item.content}
- </code>
- </pre>
- </>
- )
- );
+ if (props.editing && props.item.content.length > 4000) {
+ setWarning("Editing such a large range may be slow");
} else {
- dispatch(
- setBottomMessageCloseTimeout(
- setTimeout(() => {
- if (!isHovered) {
- dispatch(setBottomMessage(undefined));
- }
- }, 2000)
- )
- );
+ setWarning(undefined);
}
- }, [isHovered]);
+ }, [props.editing, props.item]);
+
+ const dispatch = useDispatch();
return (
- <>
- <div style={{ position: "relative" }}>
- <StyledButton
- areMultipleItems={props.areMultipleItems}
- warning={props.warning || ""}
- editing={props.item.editing}
- onMouseEnter={() => {
- setIsHovered(true);
- if (props.onHover) {
- props.onHover(true);
- }
- }}
- onMouseLeave={() => {
- setIsHovered(false);
- if (props.onHover) {
- props.onHover(false);
- }
- }}
- className="pill-button"
- onKeyDown={(e) => {
- if (e.key === "Backspace") {
- props.onDelete?.();
- }
- }}
- >
- {isHovered && (
- <GridDiv
- style={{
- gridTemplateColumns:
- props.item.editable && props.areMultipleItems
- ? "1fr 1fr"
- : "1fr",
- backgroundColor: vscBackground,
- }}
- >
- {props.item.editable && props.areMultipleItems && (
+ <div style={{ position: "relative" }}>
+ <StyledButton
+ borderColor={props.editing ? (warning ? "red" : undefined) : undefined}
+ onMouseEnter={() => {
+ setIsHovered(true);
+ if (props.onHover) {
+ props.onHover(true);
+ }
+ }}
+ onMouseLeave={() => {
+ setIsHovered(false);
+ if (props.onHover) {
+ props.onHover(false);
+ }
+ }}
+ className="pill-button"
+ onKeyDown={(e) => {
+ if (e.key === "Backspace") {
+ props.onDelete?.();
+ }
+ }}
+ >
+ {isHovered && (
+ <GridDiv
+ style={{
+ gridTemplateColumns:
+ props.item.editable &&
+ props.areMultipleItems &&
+ props.editingAny
+ ? "1fr 1fr"
+ : "1fr",
+ backgroundColor: vscBackground,
+ }}
+ >
+ {props.editingAny &&
+ props.item.editable &&
+ props.areMultipleItems && (
<ButtonDiv
data-tooltip-id={`edit-${props.index}`}
backgroundColor={"#8800aa55"}
@@ -205,30 +168,31 @@ const PillButton = (props: PillButtonProps) => {
</ButtonDiv>
)}
- <StyledTooltip id={`pin-${props.index}`}>
- Edit this range
- </StyledTooltip>
- <ButtonDiv
- data-tooltip-id={`delete-${props.index}`}
- backgroundColor={"#cc000055"}
- onClick={() => {
- client?.deleteContextWithIds([props.item.description.id]);
- dispatch(setBottomMessage(undefined));
- }}
- >
- <TrashIcon style={{ margin: "auto" }} width="1.6em" />
- </ButtonDiv>
- </GridDiv>
- )}
- {props.item.description.name}
- </StyledButton>
- <StyledTooltip id={`edit-${props.index}`}>
- {props.item.editing
- ? "Editing this section (with entire file as context)"
- : "Edit this section"}
- </StyledTooltip>
- <StyledTooltip id={`delete-${props.index}`}>Delete</StyledTooltip>
- {props.warning && (
+ <StyledTooltip id={`pin-${props.index}`}>
+ Edit this range
+ </StyledTooltip>
+ <ButtonDiv
+ data-tooltip-id={`delete-${props.index}`}
+ backgroundColor={"#cc000055"}
+ onClick={() => {
+ client?.deleteContextWithIds([props.item.description.id]);
+ dispatch(setBottomMessage(undefined));
+ }}
+ >
+ <TrashIcon style={{ margin: "auto" }} width="1.6em" />
+ </ButtonDiv>
+ </GridDiv>
+ )}
+ {props.item.description.name}
+ </StyledButton>
+ <StyledTooltip id={`edit-${props.index}`}>
+ {props.item.editing
+ ? "Editing this section (with entire file as context)"
+ : "Edit this section"}
+ </StyledTooltip>
+ <StyledTooltip id={`delete-${props.index}`}>Delete</StyledTooltip>
+ {props.editing &&
+ (warning ? (
<>
<CircleDiv
data-tooltip-id={`circle-div-${props.item.description.name}`}
@@ -240,12 +204,32 @@ const PillButton = (props: PillButtonProps) => {
/>
</CircleDiv>
<StyledTooltip id={`circle-div-${props.item.description.name}`}>
- {props.warning}
+ {warning}
</StyledTooltip>
</>
- )}
- </div>
- </>
+ ) : (
+ <>
+ <CircleDiv
+ data-tooltip-id={`circle-div-${props.item.description.name}`}
+ style={{
+ backgroundColor: "#8800aa55",
+ border: `0.5px solid ${lightGray}`,
+ padding: "1px",
+ zIndex: 1,
+ }}
+ >
+ <PaintBrushIcon
+ style={{ margin: "auto" }}
+ width="1.0em"
+ strokeWidth={2}
+ />
+ </CircleDiv>
+ <StyledTooltip id={`circle-div-${props.item.description.name}`}>
+ Editing this range
+ </StyledTooltip>
+ </>
+ ))}
+ </div>
);
};
diff --git a/extension/react-app/src/components/ProgressBar.tsx b/extension/react-app/src/components/ProgressBar.tsx
index 4efee776..27972ffc 100644
--- a/extension/react-app/src/components/ProgressBar.tsx
+++ b/extension/react-app/src/components/ProgressBar.tsx
@@ -28,9 +28,12 @@ const GridDiv = styled.div`
const P = styled.p`
margin: 0;
margin-top: 2px;
- font-size: 12px;
+ font-size: 11.5px;
color: ${lightGray};
text-align: center;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
`;
interface ProgressBarProps {
@@ -45,7 +48,7 @@ const ProgressBar = ({ completed, total }: ProgressBarProps) => {
<>
<a
href="https://continue.dev/docs/customization/models"
- className="no-underline"
+ className="no-underline ml-2"
>
<GridDiv data-tooltip-id="usage_progress_bar">
<ProgressBarWrapper>
@@ -61,7 +64,7 @@ const ProgressBar = ({ completed, total }: ProgressBarProps) => {
/>
</ProgressBarWrapper>
<P>
- Free Usage: {completed} / {total}
+ Free Uses: {completed} / {total}
</P>
</GridDiv>
</a>
diff --git a/extension/react-app/src/components/StepContainer.tsx b/extension/react-app/src/components/StepContainer.tsx
index a05aefb0..61529227 100644
--- a/extension/react-app/src/components/StepContainer.tsx
+++ b/extension/react-app/src/components/StepContainer.tsx
@@ -1,18 +1,9 @@
-import { useContext, useEffect, useRef, useState } from "react";
-import styled, { keyframes } from "styled-components";
-import { secondaryDark, vscBackground } from ".";
-import {
- ChevronDownIcon,
- ChevronRightIcon,
- ArrowPathIcon,
- XMarkIcon,
- MagnifyingGlassIcon,
- StopCircleIcon,
-} from "@heroicons/react/24/outline";
+import { useEffect, useRef, useState } from "react";
+import styled from "styled-components";
+import { defaultBorderRadius, secondaryDark, vscBackground } from ".";
+import { ArrowPathIcon, XMarkIcon } from "@heroicons/react/24/outline";
import { HistoryNode } from "../../../schema/HistoryNode";
import HeaderButtonWithText from "./HeaderButtonWithText";
-import { getMetaKeyLabel, isMetaEquivalentKeyPressed } from "../util";
-import { GUIClientContext } from "../App";
import StyledMarkdownPreview from "./StyledMarkdownPreview";
interface StepContainerProps {
@@ -23,11 +14,10 @@ interface StepContainerProps {
onRetry: () => void;
onDelete: () => void;
open: boolean;
- onToggleAll: () => void;
- onToggle: () => void;
isFirst: boolean;
isLast: boolean;
index: number;
+ noUserInputParent: boolean;
}
// #region styled components
@@ -35,74 +25,30 @@ interface StepContainerProps {
const MainDiv = styled.div<{
stepDepth: number;
inFuture: boolean;
-}>`
- opacity: ${(props) => (props.inFuture ? 0.3 : 1)};
- overflow: hidden;
- margin-left: 0px;
- margin-right: 0px;
-`;
+}>``;
-const HeaderDiv = styled.div<{ error: boolean; loading: boolean }>`
- background-color: ${(props) => (props.error ? "#522" : vscBackground)};
- display: grid;
- grid-template-columns: 1fr auto auto;
+const ButtonsDiv = styled.div`
+ display: flex;
+ gap: 2px;
align-items: center;
- padding-right: 8px;
-`;
+ background-color: ${vscBackground};
+ box-shadow: 1px 1px 10px ${vscBackground};
+ border-radius: ${defaultBorderRadius};
-const LeftHeaderSubDiv = styled.div`
- margin: 8px;
- display: grid;
- grid-template-columns: auto 1fr;
- align-items: center;
- grid-gap: 2px;
+ position: absolute;
+ right: 0;
+ top: 0;
+ height: 0;
`;
const ContentDiv = styled.div<{ isUserInput: boolean }>`
- padding-left: 4px;
- padding-right: 2px;
+ padding: 2px;
+ padding-right: 0px;
background-color: ${(props) =>
props.isUserInput ? secondaryDark : vscBackground};
font-size: 13px;
-`;
-
-const gradient = keyframes`
- 0% {
- background-position: 0px 0;
- }
- 100% {
- background-position: 100em 0;
- }
-`;
-
-const GradientBorder = styled.div<{
- borderWidth?: number;
- borderRadius?: string;
- borderColor?: string;
- isFirst: boolean;
- isLast: boolean;
- loading: boolean;
-}>`
- border-radius: ${(props) => props.borderRadius || "0"};
- padding-top: ${(props) =>
- `${(props.borderWidth || 1) / (props.isFirst ? 1 : 2)}px`};
- padding-bottom: ${(props) =>
- `${(props.borderWidth || 1) / (props.isLast ? 1 : 2)}px`};
- background: ${(props) =>
- props.borderColor
- ? props.borderColor
- : `repeating-linear-gradient(
- 101.79deg,
- #1BBE84 0%,
- #331BBE 16%,
- #BE1B55 33%,
- #A6BE1B 55%,
- #BE1B55 67%,
- #331BBE 85%,
- #1BBE84 99%
- )`};
- animation: ${(props) => (props.loading ? gradient : "")} 6s linear infinite;
- background-size: 200% 200%;
+ border-radius: ${defaultBorderRadius};
+ overflow: hidden;
`;
// #endregion
@@ -112,7 +58,6 @@ function StepContainer(props: StepContainerProps) {
const naturalLanguageInputRef = useRef<HTMLTextAreaElement>(null);
const userInputRef = useRef<HTMLInputElement>(null);
const isUserInput = props.historyNode.step.name === "UserInputStep";
- const client = useContext(GUIClientContext);
useEffect(() => {
if (userInputRef?.current) {
@@ -139,91 +84,11 @@ function StepContainer(props: StepContainerProps) {
hidden={props.historyNode.step.hide as any}
>
<div>
- <GradientBorder
- loading={props.historyNode.active as boolean}
- isFirst={props.isFirst}
- isLast={props.isLast}
- borderColor={
- props.historyNode.observation?.error
- ? "#f005"
- : props.historyNode.active
- ? undefined
- : "transparent"
- }
- className="overflow-hidden cursor-pointer"
- onClick={(e) => {
- if (isMetaEquivalentKeyPressed(e)) {
- props.onToggleAll();
- } else {
- props.onToggle();
- }
- }}
- >
- <HeaderDiv
- loading={(props.historyNode.active as boolean) || false}
- error={props.historyNode.observation?.error ? true : false}
- >
- <LeftHeaderSubDiv
- style={
- props.historyNode.observation?.error ? { color: "white" } : {}
- }
- >
- {!isUserInput &&
- (props.open ? (
- <ChevronDownIcon width="1.4em" height="1.4em" />
- ) : (
- <ChevronRightIcon width="1.4em" height="1.4em" />
- ))}
- {props.historyNode.observation?.title ||
- (props.historyNode.step.name as any)}
- </LeftHeaderSubDiv>
- {/* <HeaderButton
- onClick={(e) => {
- e.stopPropagation();
- props.onReverse();
- }}
- >
- <Backward size="1.6em" onClick={props.onReverse}></Backward>
- </HeaderButton> */}
- {(isHovered || (props.historyNode.active as boolean)) && (
- <div className="flex gap-2 items-center">
- {(props.historyNode.logs as any)?.length > 0 && (
- <HeaderButtonWithText
- text="Logs"
- onClick={(e) => {
- e.stopPropagation();
- client?.showLogsAtIndex(props.index);
- }}
- >
- <MagnifyingGlassIcon width="1.4em" height="1.4em" />
- </HeaderButtonWithText>
- )}
- <HeaderButtonWithText
- onClick={(e) => {
- e.stopPropagation();
- props.onDelete();
- }}
- text={
- props.historyNode.active
- ? `Stop (${getMetaKeyLabel()}⌫)`
- : "Delete"
- }
- >
- {props.historyNode.active ? (
- <StopCircleIcon
- width="1.4em"
- height="1.4em"
- onClick={props.onDelete}
- />
- ) : (
- <XMarkIcon
- width="1.4em"
- height="1.4em"
- onClick={props.onDelete}
- />
- )}
- </HeaderButtonWithText>
- {props.historyNode.observation?.error ? (
+ {isHovered &&
+ (props.historyNode.observation?.error || props.noUserInputParent) && (
+ <ButtonsDiv>
+ {props.historyNode.observation?.error &&
+ ((
<HeaderButtonWithText
text="Retry"
onClick={(e) => {
@@ -237,39 +102,33 @@ function StepContainer(props: StepContainerProps) {
onClick={props.onRetry}
/>
</HeaderButtonWithText>
- ) : (
- <></>
- )}
- </div>
- )}
- </HeaderDiv>
- </GradientBorder>
- <ContentDiv hidden={!props.open} isUserInput={isUserInput}>
- {props.open && false && (
- <>
- <pre className="overflow-x-scroll">
- Step Details:
- <br />
- {JSON.stringify(props.historyNode.step, null, 2)}
- </pre>
- </>
- )}
+ ) as any)}
- {props.historyNode.observation?.error ? (
- <details>
- <summary>View Traceback</summary>
- <pre className="overflow-x-scroll">
- {props.historyNode.observation.error as string}
- </pre>
- </details>
- ) : (
- <StyledMarkdownPreview
- source={props.historyNode.step.description || ""}
- wrapperElement={{
- "data-color-mode": "dark",
- }}
- />
+ {props.noUserInputParent && (
+ <HeaderButtonWithText
+ text="Delete"
+ onClick={(e) => {
+ e.stopPropagation();
+ props.onDelete();
+ }}
+ >
+ <XMarkIcon
+ width="1.4em"
+ height="1.4em"
+ onClick={props.onRetry}
+ />
+ </HeaderButtonWithText>
+ )}
+ </ButtonsDiv>
)}
+
+ <ContentDiv hidden={!props.open} isUserInput={isUserInput}>
+ <StyledMarkdownPreview
+ source={props.historyNode.step.description || ""}
+ wrapperElement={{
+ "data-color-mode": "dark",
+ }}
+ />
</ContentDiv>
</div>
</MainDiv>
diff --git a/extension/react-app/src/components/Suggestions.tsx b/extension/react-app/src/components/Suggestions.tsx
new file mode 100644
index 00000000..1709288c
--- /dev/null
+++ b/extension/react-app/src/components/Suggestions.tsx
@@ -0,0 +1,228 @@
+import React, { useCallback, useEffect, useState } from "react";
+import styled from "styled-components";
+import {
+ StyledTooltip,
+ defaultBorderRadius,
+ lightGray,
+ secondaryDark,
+ vscForeground,
+} from ".";
+import {
+ PaperAirplaneIcon,
+ SparklesIcon,
+ XMarkIcon,
+} from "@heroicons/react/24/outline";
+import { useSelector } from "react-redux";
+import { RootStore } from "../redux/store";
+import HeaderButtonWithText from "./HeaderButtonWithText";
+
+const Div = styled.div<{ isDisabled: boolean }>`
+ border-radius: ${defaultBorderRadius};
+ cursor: ${(props) => (props.isDisabled ? "not-allowed" : "pointer")};
+ padding: 8px 8px;
+ background-color: ${secondaryDark};
+ border: 1px solid transparent;
+
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+
+ color: ${(props) => (props.isDisabled ? lightGray : vscForeground)};
+
+ &:hover {
+ border: ${(props) =>
+ props.isDisabled ? "1px solid transparent" : `1px solid ${lightGray}`};
+ }
+`;
+
+const P = styled.p`
+ font-size: 13px;
+ margin: 0;
+`;
+
+interface SuggestionsDivProps {
+ title: string;
+ description: string;
+ textInput: string;
+ onClick?: () => void;
+ disabled: boolean;
+}
+
+function SuggestionsDiv(props: SuggestionsDivProps) {
+ const [isHovered, setIsHovered] = useState(false);
+
+ return (
+ <>
+ <Div
+ data-tooltip-id={`suggestion-disabled-${props.textInput.replace(
+ " ",
+ ""
+ )}`}
+ onClick={props.onClick}
+ onMouseEnter={() => {
+ if (props.disabled) return;
+ setIsHovered(true);
+ }}
+ onMouseLeave={() => setIsHovered(false)}
+ isDisabled={props.disabled}
+ >
+ <P>{props.description}</P>
+ <PaperAirplaneIcon
+ width="1.6em"
+ height="1.6em"
+ style={{
+ opacity: isHovered ? 1 : 0,
+ backgroundColor: secondaryDark,
+ boxShadow: `1px 1px 10px ${secondaryDark}`,
+ borderRadius: defaultBorderRadius,
+ }}
+ />
+ </Div>
+ <StyledTooltip
+ id={`suggestion-disabled-${props.textInput.replace(" ", "")}`}
+ place="bottom"
+ hidden={!props.disabled}
+ >
+ Must highlight code first
+ </StyledTooltip>
+ </>
+ );
+}
+
+const stageDescriptions = [
+ <p>Ask a question</p>,
+ <ol>
+ <li>Highlight code in the editor</li>
+ <li>Press cmd+M to select the code</li>
+ <li>Ask a question</li>
+ </ol>,
+ <ol>
+ <li>Highlight code in the editor</li>
+ <li>Press cmd+shift+M to select the code</li>
+ <li>Request and edit</li>
+ </ol>,
+];
+
+const suggestionsStages: any[][] = [
+ [
+ {
+ title: stageDescriptions[0],
+ description: "How does merge sort work?",
+ textInput: "How does merge sort work?",
+ },
+ {
+ title: stageDescriptions[0],
+ description: "How do I sum over a column in SQL?",
+ textInput: "How do I sum over a column in SQL?",
+ },
+ ],
+ [
+ {
+ title: stageDescriptions[1],
+ description: "Is there any way to make this code more efficient?",
+ textInput: "Is there any way to make this code more efficient?",
+ },
+ {
+ title: stageDescriptions[1],
+ description: "What does this function do?",
+ textInput: "What does this function do?",
+ },
+ ],
+ [
+ {
+ title: stageDescriptions[2],
+ description: "/edit write comments for this code",
+ textInput: "/edit write comments for this code",
+ },
+ {
+ title: stageDescriptions[2],
+ description: "/edit make this code more efficient",
+ textInput: "/edit make this code more efficient",
+ },
+ ],
+];
+
+const TutorialDiv = styled.div`
+ margin: 4px;
+ position: relative;
+ background-color: #ff02;
+ border-radius: ${defaultBorderRadius};
+ padding: 8px 4px;
+`;
+
+function SuggestionsArea(props: { onClick: (textInput: string) => void }) {
+ const [stage, setStage] = useState(
+ parseInt(localStorage.getItem("stage") || "0")
+ );
+ const timeline = useSelector(
+ (state: RootStore) => state.serverState.history.timeline
+ );
+ const sessionId = useSelector(
+ (state: RootStore) => state.serverState.session_info?.session_id
+ );
+ const codeIsHighlighted = useSelector((state: RootStore) =>
+ state.serverState.selected_context_items.some(
+ (item) => item.description.id.provider_title === "code"
+ )
+ );
+
+ const [hide, setHide] = useState(false);
+
+ useEffect(() => {
+ setHide(false);
+ }, [sessionId]);
+
+ const [numTutorialInputs, setNumTutorialInputs] = useState(0);
+
+ const inputsAreOnlyTutorial = useCallback(() => {
+ const inputs = timeline.filter(
+ (node) => !node.step.hide && node.step.name === "User Input"
+ );
+ return inputs.length - numTutorialInputs === 0;
+ }, [timeline, numTutorialInputs]);
+
+ return (
+ <>
+ {hide || stage > 2 || !inputsAreOnlyTutorial() || (
+ <TutorialDiv>
+ <div className="flex">
+ <SparklesIcon width="1.3em" height="1.3em" color="yellow" />
+ <b className="ml-1">Tutorial</b>
+ </div>
+ <p style={{ color: lightGray }}>
+ {stage < suggestionsStages.length &&
+ suggestionsStages[stage][0]?.title}
+ </p>
+ <HeaderButtonWithText
+ className="absolute right-1 top-1 cursor-pointer"
+ text="Close Tutorial"
+ onClick={() => {
+ console.log("HIDE");
+ setHide(true);
+ }}
+ >
+ <XMarkIcon width="1.2em" height="1.2em" />
+ </HeaderButtonWithText>
+ <div className="grid grid-cols-2 gap-2 mt-2">
+ {suggestionsStages[stage]?.map((suggestion) => (
+ <SuggestionsDiv
+ disabled={stage > 0 && !codeIsHighlighted}
+ {...suggestion}
+ onClick={() => {
+ if (stage > 0 && !codeIsHighlighted) return;
+ props.onClick(suggestion.textInput);
+ setStage(stage + 1);
+ localStorage.setItem("stage", (stage + 1).toString());
+ setHide(true);
+ setNumTutorialInputs((prev) => prev + 1);
+ }}
+ />
+ ))}
+ </div>
+ </TutorialDiv>
+ )}
+ </>
+ );
+}
+
+export default SuggestionsArea;
diff --git a/extension/react-app/src/components/TimelineItem.tsx b/extension/react-app/src/components/TimelineItem.tsx
new file mode 100644
index 00000000..78568890
--- /dev/null
+++ b/extension/react-app/src/components/TimelineItem.tsx
@@ -0,0 +1,59 @@
+import React from "react";
+import { lightGray, secondaryDark, vscBackground } from ".";
+import styled from "styled-components";
+import { ChatBubbleOvalLeftIcon, PlusIcon } from "@heroicons/react/24/outline";
+
+const CollapseButton = styled.div`
+ background-color: ${vscBackground};
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ flex-shrink: 0;
+ flex-grow: 0;
+ margin-left: 5px;
+ cursor: pointer;
+`;
+
+const CollapsedDiv = styled.div`
+ margin-top: 8px;
+ margin-bottom: 8px;
+ margin-left: 8px;
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ font-size: 13px;
+ min-height: 16px;
+`;
+
+interface TimelineItemProps {
+ historyNode: any;
+ open: boolean;
+ onToggle: () => void;
+ children: any;
+ iconElement?: any;
+}
+
+function TimelineItem(props: TimelineItemProps) {
+ return props.open ? (
+ props.children
+ ) : (
+ <CollapsedDiv>
+ <CollapseButton
+ onClick={() => {
+ props.onToggle();
+ }}
+ >
+ {props.iconElement || (
+ <ChatBubbleOvalLeftIcon width="16px" height="16px" />
+ )}
+ </CollapseButton>
+ <span style={{ color: lightGray }}>
+ {props.historyNode.observation?.error
+ ? props.historyNode.observation?.title
+ : props.historyNode.step.name}
+ </span>
+ </CollapsedDiv>
+ );
+}
+
+export default TimelineItem;
diff --git a/extension/react-app/src/components/UserInputContainer.tsx b/extension/react-app/src/components/UserInputContainer.tsx
index 866fef58..76a3c615 100644
--- a/extension/react-app/src/components/UserInputContainer.tsx
+++ b/extension/react-app/src/components/UserInputContainer.tsx
@@ -1,5 +1,11 @@
-import React, { useContext, useEffect, useRef, useState } from "react";
-import styled from "styled-components";
+import React, {
+ useCallback,
+ useContext,
+ useEffect,
+ useRef,
+ useState,
+} from "react";
+import styled, { keyframes } from "styled-components";
import {
defaultBorderRadius,
lightGray,
@@ -8,69 +14,115 @@ import {
vscForeground,
} from ".";
import HeaderButtonWithText from "./HeaderButtonWithText";
-import { XMarkIcon, CheckIcon } from "@heroicons/react/24/outline";
+import {
+ XMarkIcon,
+ CheckIcon,
+ ChevronDownIcon,
+ ChevronRightIcon,
+ MagnifyingGlassIcon,
+ StopCircleIcon,
+} from "@heroicons/react/24/outline";
import { HistoryNode } from "../../../schema/HistoryNode";
import { GUIClientContext } from "../App";
+import { getMetaKeyLabel, isMetaEquivalentKeyPressed } from "../util";
+import { RootStore } from "../redux/store";
+import { useSelector } from "react-redux";
interface UserInputContainerProps {
onDelete: () => void;
children: string;
historyNode: HistoryNode;
index: number;
+ onToggle: (arg0: boolean) => void;
+ onToggleAll: (arg0: boolean) => void;
+ isToggleOpen: boolean;
+ active: boolean;
+ groupIndices: number[];
}
-const StyledDiv = styled.div`
- position: relative;
- background-color: ${secondaryDark};
- font-size: 13px;
+const gradient = keyframes`
+ 0% {
+ background-position: 0px 0;
+ }
+ 100% {
+ background-position: 100em 0;
+ }
+`;
+
+const ToggleDiv = styled.div`
display: flex;
align-items: center;
- border-bottom: 1px solid ${vscBackground};
- padding: 8px;
- padding-top: 0px;
- padding-bottom: 0px;
+ justify-content: center;
+ cursor: pointer;
- border-bottom: 0.5px solid ${lightGray};
- border-top: 0.5px solid ${lightGray};
-`;
+ height: 100%;
+ padding: 0 4px;
-const DeleteButtonDiv = styled.div`
- position: absolute;
- top: 8px;
- right: 8px;
+ &:hover {
+ background-color: ${vscBackground};
+ }
`;
-const StyledPre = styled.pre`
- margin-right: 22px;
- margin-left: 8px;
- white-space: pre-wrap;
- word-wrap: break-word;
- font-family: "Lexend", sans-serif;
- font-size: 13px;
+const GradientBorder = styled.div<{
+ borderWidth?: number;
+ borderRadius?: string;
+ borderColor?: string;
+ isFirst: boolean;
+ isLast: boolean;
+ loading: boolean;
+}>`
+ border-radius: ${(props) => props.borderRadius || "0"};
+ padding: ${(props) =>
+ `${(props.borderWidth || 1) / (props.isFirst ? 1 : 2)}px`};
+ background: ${(props) =>
+ props.borderColor
+ ? props.borderColor
+ : `repeating-linear-gradient(
+ 101.79deg,
+ #1BBE84 0%,
+ #331BBE 16%,
+ #BE1B55 33%,
+ #A6BE1B 55%,
+ #BE1B55 67%,
+ #331BBE 85%,
+ #1BBE84 99%
+ )`};
+ animation: ${(props) => (props.loading ? gradient : "")} 6s linear infinite;
+ background-size: 200% 200%;
`;
-const TextArea = styled.textarea`
- margin: 8px;
- margin-right: 22px;
- padding: 8px;
- white-space: pre-wrap;
- word-wrap: break-word;
- font-family: "Lexend", sans-serif;
+const StyledDiv = styled.div<{ editing: boolean }>`
font-size: 13px;
- width: 100%;
+ font-family: inherit;
border-radius: ${defaultBorderRadius};
- height: 100%;
- border: none;
- background-color: ${vscBackground};
- resize: none;
- outline: none;
- border: none;
+ height: auto;
+ background-color: ${secondaryDark};
color: ${vscForeground};
+ align-items: center;
+ position: relative;
+ z-index: 1;
+ overflow: hidden;
+ display: grid;
+ grid-template-columns: auto 1fr;
- &:focus {
- border: none;
- outline: none;
- }
+ outline: ${(props) => (props.editing ? `1px solid ${lightGray}` : "none")};
+ cursor: text;
+`;
+
+const DeleteButtonDiv = styled.div`
+ position: absolute;
+ top: 8px;
+ right: 8px;
+ background-color: ${secondaryDark};
+ box-shadow: 2px 2px 10px ${secondaryDark};
+ border-radius: ${defaultBorderRadius};
+`;
+
+const GridDiv = styled.div`
+ display: grid;
+ grid-template-columns: auto 1fr;
+ grid-gap: 8px;
+ align-items: center;
`;
function stringWithEllipsis(str: string, maxLen: number) {
@@ -84,108 +136,194 @@ const UserInputContainer = (props: UserInputContainerProps) => {
const [isHovered, setIsHovered] = useState(false);
const [isEditing, setIsEditing] = useState(false);
- const textAreaRef = useRef<HTMLTextAreaElement>(null);
+ const divRef = useRef<HTMLDivElement>(null);
const client = useContext(GUIClientContext);
+ const [prevContent, setPrevContent] = useState("");
+
+ const history = useSelector((state: RootStore) => state.serverState.history);
+
useEffect(() => {
- if (isEditing && textAreaRef.current) {
- textAreaRef.current.focus();
- // Select all text
- textAreaRef.current.setSelectionRange(
- 0,
- textAreaRef.current.value.length
- );
- // Change the size to match the contents (up to a max)
- textAreaRef.current.style.height = "auto";
- textAreaRef.current.style.height =
- (textAreaRef.current.scrollHeight > 500
- ? 500
- : textAreaRef.current.scrollHeight) + "px";
+ if (isEditing && divRef.current) {
+ setPrevContent(divRef.current.innerText);
+ divRef.current.focus();
+
+ if (divRef.current.innerText !== "") {
+ const range = document.createRange();
+ const sel = window.getSelection();
+ range.setStart(divRef.current, 0);
+ range.setEnd(divRef.current, 1);
+ sel?.removeAllRanges();
+ sel?.addRange(range);
+ }
}
- }, [isEditing]);
+ }, [isEditing, divRef.current]);
+
+ const onBlur = useCallback(() => {
+ setIsEditing(false);
+ if (divRef.current) {
+ divRef.current.innerText = prevContent;
+ divRef.current.blur();
+ }
+ }, [divRef.current]);
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === "Escape") {
- setIsEditing(false);
+ onBlur();
}
};
- document.addEventListener("keydown", handleKeyDown);
+ divRef.current?.addEventListener("keydown", handleKeyDown);
return () => {
- document.removeEventListener("keydown", handleKeyDown);
+ divRef.current?.removeEventListener("keydown", handleKeyDown);
};
- }, []);
+ }, [prevContent, divRef.current, isEditing, onBlur]);
const doneEditing = (e: any) => {
- if (!textAreaRef.current?.value) {
+ if (!divRef.current?.innerText) {
return;
}
- client?.editStepAtIndex(textAreaRef.current.value, props.index);
+ setPrevContent(divRef.current.innerText);
+ client?.editStepAtIndex(divRef.current.innerText, props.index);
setIsEditing(false);
e.stopPropagation();
+ divRef.current?.blur();
};
return (
- <StyledDiv
- onMouseEnter={() => {
- setIsHovered(true);
- }}
- onMouseLeave={() => {
- setIsHovered(false);
- }}
+ <GradientBorder
+ loading={props.active}
+ isFirst={false}
+ isLast={false}
+ borderColor={props.active ? undefined : vscBackground}
+ borderRadius={defaultBorderRadius}
>
- {isEditing ? (
- <TextArea
- ref={textAreaRef}
- onKeyDown={(e) => {
- if (e.key === "Enter" && !e.shiftKey) {
- e.preventDefault();
- doneEditing(e);
+ <StyledDiv
+ editing={isEditing}
+ onMouseEnter={() => {
+ setIsHovered(true);
+ }}
+ onMouseLeave={() => {
+ setIsHovered(false);
+ }}
+ onClick={() => {
+ setIsEditing(true);
+ }}
+ >
+ <GridDiv>
+ <ToggleDiv
+ onClick={
+ props.isToggleOpen
+ ? (e) => {
+ e.stopPropagation();
+ if (isMetaEquivalentKeyPressed(e)) {
+ props.onToggleAll(false);
+ } else {
+ props.onToggle(false);
+ }
+ }
+ : (e) => {
+ e.stopPropagation();
+ if (isMetaEquivalentKeyPressed(e)) {
+ props.onToggleAll(true);
+ } else {
+ props.onToggle(true);
+ }
+ }
}
- }}
- defaultValue={props.children}
- onBlur={() => {
- setIsEditing(false);
- }}
- />
- ) : (
- <StyledPre
- onClick={() => {
- setIsEditing(true);
- }}
- className="mr-6 cursor-text w-full"
- >
- {stringWithEllipsis(props.children, 600)}
- </StyledPre>
- )}
- {/* <ReactMarkdown children={props.children} className="w-fit mr-10" /> */}
- <DeleteButtonDiv>
- {(isHovered || isEditing) && (
- <div className="flex">
- {isEditing ? (
- <HeaderButtonWithText
- onClick={(e) => {
- doneEditing(e);
- }}
- text="Done"
- >
- <CheckIcon width="1.4em" height="1.4em" />
- </HeaderButtonWithText>
+ >
+ {props.isToggleOpen ? (
+ <ChevronDownIcon width="1.4em" height="1.4em" />
) : (
- <HeaderButtonWithText
- onClick={(e) => {
- props.onDelete();
- e.stopPropagation();
- }}
- text="Delete"
- >
- <XMarkIcon width="1.4em" height="1.4em" />
- </HeaderButtonWithText>
+ <ChevronRightIcon width="1.4em" height="1.4em" />
)}
+ </ToggleDiv>
+ <div
+ style={{
+ padding: "8px",
+ paddingTop: "4px",
+ paddingBottom: "4px",
+ }}
+ >
+ <div
+ ref={divRef}
+ onBlur={() => {
+ onBlur();
+ }}
+ onKeyDown={(e) => {
+ if (e.key === "Enter" && !e.shiftKey) {
+ e.preventDefault();
+ doneEditing(e);
+ }
+ }}
+ contentEditable={true}
+ suppressContentEditableWarning={true}
+ className="mr-6 ml-1 cursor-text w-full py-2 flex items-center content-center outline-none"
+ >
+ {isEditing
+ ? props.children
+ : stringWithEllipsis(props.children, 600)}
+ </div>
+ <DeleteButtonDiv>
+ {(isHovered || isEditing) && (
+ <div className="flex">
+ {isEditing ? (
+ <HeaderButtonWithText
+ onClick={(e) => {
+ doneEditing(e);
+ }}
+ text="Done"
+ >
+ <CheckIcon width="1.4em" height="1.4em" />
+ </HeaderButtonWithText>
+ ) : (
+ <>
+ {history.timeline
+ .filter(
+ (h, i: number) =>
+ props.groupIndices.includes(i) && h.logs
+ )
+ .some((h) => h.logs!.length > 0) && (
+ <HeaderButtonWithText
+ onClick={(e) => {
+ e.stopPropagation();
+ client?.showLogsAtIndex(props.groupIndices[1]);
+ }}
+ text="Context Used"
+ >
+ <MagnifyingGlassIcon width="1.4em" height="1.4em" />
+ </HeaderButtonWithText>
+ )}
+ <HeaderButtonWithText
+ onClick={(e) => {
+ e.stopPropagation();
+ if (props.active) {
+ client?.deleteAtIndex(props.groupIndices[1]);
+ } else {
+ props.onDelete();
+ }
+ }}
+ text={
+ props.active
+ ? `Stop (${getMetaKeyLabel()}⌫)`
+ : "Delete"
+ }
+ >
+ {props.active ? (
+ <StopCircleIcon width="1.4em" height="1.4em" />
+ ) : (
+ <XMarkIcon width="1.4em" height="1.4em" />
+ )}
+ </HeaderButtonWithText>
+ </>
+ )}
+ </div>
+ )}
+ </DeleteButtonDiv>
</div>
- )}
- </DeleteButtonDiv>
- </StyledDiv>
+ </GridDiv>
+ </StyledDiv>
+ </GradientBorder>
);
};
export default UserInputContainer;
diff --git a/extension/react-app/src/components/dialogs/FTCDialog.tsx b/extension/react-app/src/components/dialogs/FTCDialog.tsx
new file mode 100644
index 00000000..3ea753bc
--- /dev/null
+++ b/extension/react-app/src/components/dialogs/FTCDialog.tsx
@@ -0,0 +1,72 @@
+import React, { useContext } from "react";
+import styled from "styled-components";
+import { Button, TextInput } from "..";
+import { useNavigate } from "react-router-dom";
+import { GUIClientContext } from "../../App";
+import { useDispatch } from "react-redux";
+import { setShowDialog } from "../../redux/slices/uiStateSlice";
+
+const GridDiv = styled.div`
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ grid-gap: 8px;
+ align-items: center;
+`;
+
+function FTCDialog() {
+ const navigate = useNavigate();
+ const [apiKey, setApiKey] = React.useState("");
+ const client = useContext(GUIClientContext);
+ const dispatch = useDispatch();
+
+ return (
+ <div className="p-4">
+ <h3>Free Trial Limit Reached</h3>
+ <p>
+ 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
+ OpenAIFreeTrial object.
+ </p>
+
+ <TextInput
+ type="text"
+ placeholder="Enter your OpenAI API key"
+ value={apiKey}
+ onChange={(e) => setApiKey(e.target.value)}
+ />
+ <GridDiv>
+ <Button
+ onClick={() => {
+ navigate("/models");
+ }}
+ >
+ Select model
+ </Button>
+ <Button
+ disabled={!apiKey}
+ onClick={() => {
+ client?.addModelForRole("*", "OpenAI", {
+ model: "gpt-4",
+ api_key: apiKey,
+ title: "GPT-4",
+ });
+ dispatch(setShowDialog(false));
+ }}
+ >
+ Use my API key
+ </Button>
+ </GridDiv>
+ </div>
+ );
+}
+
+export default FTCDialog;
diff --git a/extension/react-app/src/components/dialogs/KeyboardShortcuts.tsx b/extension/react-app/src/components/dialogs/KeyboardShortcuts.tsx
new file mode 100644
index 00000000..2a7b735c
--- /dev/null
+++ b/extension/react-app/src/components/dialogs/KeyboardShortcuts.tsx
@@ -0,0 +1,129 @@
+import React from "react";
+import styled from "styled-components";
+import {
+ defaultBorderRadius,
+ lightGray,
+ secondaryDark,
+ vscForeground,
+} from "..";
+import { getPlatform } from "../../util";
+
+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;
+
+ border-top: 0.5px solid ${lightGray};
+`;
+
+const KeyDiv = styled.div`
+ border: 0.5px solid ${lightGray};
+ border-radius: ${defaultBorderRadius};
+ padding: 4px;
+ color: ${vscForeground};
+
+ width: 16px;
+ height: 16px;
+
+ display: flex;
+ justify-content: center;
+ align-items: center;
+`;
+
+interface KeyboardShortcutProps {
+ mac: string;
+ windows: string;
+ description: string;
+}
+
+function KeyboardShortcut(props: KeyboardShortcutProps) {
+ const shortcut = getPlatform() === "windows" ? props.windows : props.mac;
+ return (
+ <div className="flex justify-between w-full items-center">
+ <span
+ style={{
+ color: vscForeground,
+ }}
+ >
+ {props.description}
+ </span>
+ <div className="flex gap-2 float-right">
+ {shortcut.split(" ").map((key) => {
+ return <KeyDiv>{key}</KeyDiv>;
+ })}
+ </div>
+ </div>
+ );
+}
+
+const shortcuts: KeyboardShortcutProps[] = [
+ {
+ mac: "⌘ M",
+ windows: "⌃ M",
+ description: "Ask about Highlighted Code",
+ },
+ {
+ mac: "⌘ ⇧ M",
+ windows: "⌃ ⇧ M",
+ description: "Edit Highlighted Code",
+ },
+ {
+ mac: "⌘ ⇧ ↵",
+ windows: "⌃ ⇧ ↵",
+ description: "Accept Diff",
+ },
+ {
+ mac: "⌘ ⇧ ⌫",
+ windows: "⌃ ⇧ ⌫",
+ description: "Reject Diff",
+ },
+ {
+ mac: "⌘ ⇧ L",
+ windows: "⌃ ⇧ L",
+ description: "Quick Text Entry",
+ },
+ {
+ mac: "⌥ ⌘ M",
+ windows: "⌥ ⌃ M",
+ description: "Toggle Auxiliary Bar",
+ },
+ {
+ mac: "⌘ ⇧ R",
+ windows: "⌃ ⇧ R",
+ description: "Debug Terminal",
+ },
+ {
+ mac: "⌥ ⌘ N",
+ windows: "⌥ ⌃ N",
+ description: "New Session",
+ },
+ {
+ mac: "⌘ ⌫",
+ windows: "⌃ ⌫",
+ description: "Stop Active Step",
+ },
+];
+
+function KeyboardShortcutsDialog() {
+ return (
+ <div className="p-2">
+ <h3 className="my-3 mx-auto text-center">Keyboard Shortcuts</h3>
+ <GridDiv>
+ {shortcuts.map((shortcut) => {
+ return (
+ <KeyboardShortcut
+ mac={shortcut.mac}
+ windows={shortcut.windows}
+ description={shortcut.description}
+ />
+ );
+ })}
+ </GridDiv>
+ </div>
+ );
+}
+
+export default KeyboardShortcutsDialog;
diff --git a/extension/react-app/src/components/index.ts b/extension/react-app/src/components/index.ts
index 1f418c94..6f5a2f37 100644
--- a/extension/react-app/src/components/index.ts
+++ b/extension/react-app/src/components/index.ts
@@ -7,7 +7,7 @@ export const lightGray = "#646464";
// export const vscBackground = "rgb(30 30 30)";
export const vscBackgroundTransparent = "#1e1e1ede";
export const buttonColor = "#1bbe84";
-export const buttonColorHover = "1bbe84a8";
+export const buttonColorHover = "#1bbe84a8";
export const secondaryDark = "var(--vscode-list-hoverBackground)";
export const vscBackground = "var(--vscode-editor-background)";
@@ -17,7 +17,6 @@ export const Button = styled.button`
padding: 10px 12px;
margin: 8px 0;
border-radius: ${defaultBorderRadius};
- cursor: pointer;
border: none;
color: white;
@@ -28,7 +27,7 @@ export const Button = styled.button`
}
&:hover:enabled {
- background-color: ${buttonColorHover};
+ cursor: pointer;
}
`;
@@ -56,6 +55,8 @@ export const TextArea = styled.textarea`
z-index: 1;
border: 1px solid transparent;
+ resize: vertical;
+
&:focus {
outline: 1px solid ${lightGray};
border: 1px solid transparent;
diff --git a/extension/react-app/src/hooks/AbstractContinueGUIClientProtocol.ts b/extension/react-app/src/hooks/AbstractContinueGUIClientProtocol.ts
index 9944f221..d71186d7 100644
--- a/extension/react-app/src/hooks/AbstractContinueGUIClientProtocol.ts
+++ b/extension/react-app/src/hooks/AbstractContinueGUIClientProtocol.ts
@@ -29,6 +29,8 @@ abstract class AbstractContinueGUIClientProtocol {
abstract showLogsAtIndex(index: number): void;
+ abstract showContextVirtualFile(): void;
+
abstract selectContextItem(id: string, query: string): void;
abstract loadSession(session_id?: string): void;
@@ -52,6 +54,8 @@ abstract class AbstractContinueGUIClientProtocol {
abstract selectContextGroup(id: string): void;
abstract deleteContextGroup(id: string): void;
+
+ abstract setCurrentSessionTitle(title: string): void;
}
export default AbstractContinueGUIClientProtocol;
diff --git a/extension/react-app/src/hooks/ContinueGUIClientProtocol.ts b/extension/react-app/src/hooks/ContinueGUIClientProtocol.ts
index fe1b654b..8205a629 100644
--- a/extension/react-app/src/hooks/ContinueGUIClientProtocol.ts
+++ b/extension/react-app/src/hooks/ContinueGUIClientProtocol.ts
@@ -23,12 +23,8 @@ class ContinueGUIClientProtocol extends AbstractContinueGUIClientProtocol {
? new VscodeMessenger(serverUrlWithSessionId)
: new WebsocketMessenger(serverUrlWithSessionId);
- this.messenger.onClose(() => {
- console.log("GUI -> IDE websocket closed");
- });
- this.messenger.onError((error) => {
- console.log("GUI -> IDE websocket error", error);
- });
+ this.messenger.onClose(() => {});
+ this.messenger.onError((error) => {});
this.messenger.onMessageType("reconnect_at_session", (data: any) => {
if (data.session_id) {
@@ -52,6 +48,7 @@ class ContinueGUIClientProtocol extends AbstractContinueGUIClientProtocol {
}
onReconnectAtSession(session_id: string): void {
+ console.log("Reconnecting at session: ", session_id);
this.connectMessenger(
`${this.serverUrlWithSessionId.split("?")[0]}?session_id=${session_id}`,
this.useVscodeMessagePassing
@@ -122,6 +119,10 @@ class ContinueGUIClientProtocol extends AbstractContinueGUIClientProtocol {
this.messenger?.send("show_logs_at_index", { index });
}
+ showContextVirtualFile(): void {
+ this.messenger?.send("show_context_virtual_file", {});
+ }
+
selectContextItem(id: string, query: string): void {
this.messenger?.send("select_context_item", { id, query });
}
@@ -163,6 +164,10 @@ class ContinueGUIClientProtocol extends AbstractContinueGUIClientProtocol {
deleteContextGroup(id: string): void {
this.messenger?.send("delete_context_group", { id });
}
+
+ setCurrentSessionTitle(title: string): void {
+ this.messenger?.send("set_current_session_title", { title });
+ }
}
export default ContinueGUIClientProtocol;
diff --git a/extension/react-app/src/index.css b/extension/react-app/src/index.css
index 269da69a..3ecef025 100644
--- a/extension/react-app/src/index.css
+++ b/extension/react-app/src/index.css
@@ -11,7 +11,7 @@
--vscode-editor-background: rgb(30, 30, 30);
--vscode-editor-foreground: rgb(197, 200, 198);
- --vscode-textBlockQuote-background: rgba(255, 255, 255, 0.05);
+ --vscode-textBlockQuote-background: rgba(255, 255, 255, 1);
}
html,
@@ -33,3 +33,7 @@ body {
.press-start-2p {
font-family: "Press Start 2P", "Lexend", sans-serif;
}
+
+a:focus {
+ outline: none;
+} \ No newline at end of file
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">
diff --git a/extension/react-app/src/redux/slices/serverStateReducer.ts b/extension/react-app/src/redux/slices/serverStateReducer.ts
index 904b0e76..3a2e455a 100644
--- a/extension/react-app/src/redux/slices/serverStateReducer.ts
+++ b/extension/react-app/src/redux/slices/serverStateReducer.ts
@@ -1,6 +1,74 @@
import { createSlice } from "@reduxjs/toolkit";
import { FullState } from "../../../../schema/FullState";
+const TEST_TIMELINE = [
+ {
+ step: {
+ description: "Hi, please write bubble sort in python",
+ name: "User Input",
+ },
+ },
+ {
+ step: {
+ description: `\`\`\`python
+def bubble_sort(arr):
+ n = len(arr)
+ for i in range(n):
+ for j in range(0, n - i - 1):
+ if arr[j] > arr[j + 1]:
+ arr[j], arr[j + 1] = arr[j + 1], arr[j]
+ return arr
+\`\`\``,
+ name: "Bubble Sort in Python",
+ },
+ },
+ {
+ step: {
+ description: "Now write it in Rust",
+ name: "User Input",
+ },
+ },
+ {
+ step: {
+ description: "Hello! This is a test...\n\n1, 2, 3, testing...",
+ name: "Testing",
+ },
+ },
+ {
+ step: {
+ description: `Sure, here's bubble sort written in rust: \n\`\`\`rust
+fn bubble_sort<T: Ord>(values: &mut[T]) {
+ let len = values.len();
+ for i in 0..len {
+ for j in 0..(len - i - 1) {
+ if values[j] > values[j + 1] {
+ values.swap(j, j + 1);
+ }
+ }
+ }
+}
+\`\`\`\nIs there anything else I can answer?`,
+ name: "Rust Bubble Sort",
+ },
+ active: true,
+ },
+];
+
+const TEST_SLASH_COMMANDS = [
+ {
+ name: "edit",
+ description: "Edit the code",
+ },
+ {
+ name: "cmd",
+ description: "Generate a command",
+ },
+ {
+ name: "help",
+ description: "Get help using Continue",
+ },
+];
+
const initialState: FullState = {
history: {
timeline: [],
@@ -30,9 +98,21 @@ export const serverStateSlice = createSlice({
temporarilyPushToUserInputQueue: (state, action) => {
state.user_input_queue = [...state.user_input_queue, action.payload];
},
+ temporarilyClearSession: (state) => {
+ state.history.timeline = [];
+ state.selected_context_items = [];
+ state.session_info = {
+ title: "Loading session...",
+ session_id: "",
+ date_created: "",
+ };
+ },
},
});
-export const { setServerState, temporarilyPushToUserInputQueue } =
- serverStateSlice.actions;
+export const {
+ setServerState,
+ temporarilyPushToUserInputQueue,
+ temporarilyClearSession,
+} = serverStateSlice.actions;
export default serverStateSlice.reducer;