summaryrefslogtreecommitdiff
path: root/extension/react-app/src/components
diff options
context:
space:
mode:
Diffstat (limited to 'extension/react-app/src/components')
-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
19 files changed, 1355 insertions, 858 deletions
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;