From e976d60974a7837967d03807605cbf2e7b4f3f9a Mon Sep 17 00:00:00 2001 From: Nate Sesti <33237525+sestinj@users.noreply.github.com> Date: Sat, 23 Sep 2023 13:06:00 -0700 Subject: UI Redesign and fixing many details (#496) * feat: :lipstick: start of major design upgrade * feat: :lipstick: model selection page * feat: :lipstick: use shortcut to add highlighted code as ctx * feat: :lipstick: better display of errors * feat: :lipstick: ui for learning keyboard shortcuts, more details * refactor: :construction: testing slash commands ui * Truncate continue.log * refactor: :construction: refactoring client_session, ui, more * feat: :bug: layout fixes * refactor: :lipstick: ui to enter OpenAI Key * refactor: :truck: rename MaybeProxyOpenAI -> OpenAIFreeTrial * starting help center * removing old shortcut docs * fix: :bug: fix model setting logic to avoid overwrites * feat: :lipstick: tutorial and model descriptions * refactor: :truck: rename unused -> saved * refactor: :truck: rename model roles * feat: :lipstick: edit indicator * refactor: :lipstick: move +, folder icons * feat: :lipstick: tab to clear all context * fix: :bug: context providers ui fixes * fix: :bug: fix lag when stopping step * fix: :bug: don't override system message for models * fix: :bug: fix continue button cursor * feat: :lipstick: title bar * fix: :bug: updates to code highlighting logic and more * fix: :bug: fix renaming of summarize model role * feat: :lipstick: help page and better session title * feat: :lipstick: more help page / ui improvements * feat: :lipstick: set session title * fix: :bug: small fixes for changing sessions * fix: :bug: perfecting the highlighting code and ctx interactions * style: :lipstick: sticky headers for scroll, ollama warming * fix: :bug: fix toggle bug --------- Co-authored-by: Ty Dunn --- extension/react-app/public/logos/anthropic.png | Bin 0 -> 13331 bytes extension/react-app/public/logos/hf.png | Bin 0 -> 185450 bytes extension/react-app/public/logos/llamacpp.png | Bin 0 -> 34267 bytes extension/react-app/public/logos/lmstudio.png | Bin 0 -> 108401 bytes extension/react-app/public/logos/meta.svg | 2 + extension/react-app/public/logos/ollama.png | Bin 0 -> 10839 bytes extension/react-app/public/logos/openai.svg | 2 + extension/react-app/public/logos/replicate.png | Bin 0 -> 6143 bytes extension/react-app/public/logos/together.png | Bin 0 -> 49102 bytes extension/react-app/src/App.tsx | 15 + extension/react-app/src/components/CheckDiv.tsx | 3 + extension/react-app/src/components/ComboBox.tsx | 416 ++++++++++--------- .../react-app/src/components/ContinueButton.tsx | 33 +- .../src/components/ErrorStepContainer.tsx | 52 +++ .../src/components/HeaderButtonWithText.tsx | 14 +- extension/react-app/src/components/Layout.tsx | 61 +-- extension/react-app/src/components/ModelCard.tsx | 122 ++++++ extension/react-app/src/components/ModelSelect.tsx | 43 +- .../react-app/src/components/ModelSettings.tsx | 2 +- extension/react-app/src/components/Onboarding.tsx | 130 ------ extension/react-app/src/components/PillButton.tsx | 216 +++++----- extension/react-app/src/components/ProgressBar.tsx | 9 +- .../react-app/src/components/StepContainer.tsx | 241 +++--------- extension/react-app/src/components/Suggestions.tsx | 228 +++++++++++ .../react-app/src/components/TimelineItem.tsx | 59 +++ .../src/components/UserInputContainer.tsx | 376 ++++++++++++------ .../react-app/src/components/dialogs/FTCDialog.tsx | 72 ++++ .../src/components/dialogs/KeyboardShortcuts.tsx | 129 ++++++ extension/react-app/src/components/index.ts | 7 +- .../src/hooks/AbstractContinueGUIClientProtocol.ts | 4 + .../src/hooks/ContinueGUIClientProtocol.ts | 17 +- extension/react-app/src/index.css | 6 +- extension/react-app/src/pages/gui.tsx | 438 +++++++++++++++------ extension/react-app/src/pages/help.tsx | 98 +++++ extension/react-app/src/pages/history.tsx | 172 ++++---- extension/react-app/src/pages/models.tsx | 167 ++++++++ extension/react-app/src/pages/settings.tsx | 45 ++- .../src/redux/slices/serverStateReducer.ts | 84 +++- 38 files changed, 2192 insertions(+), 1071 deletions(-) create mode 100644 extension/react-app/public/logos/anthropic.png create mode 100644 extension/react-app/public/logos/hf.png create mode 100644 extension/react-app/public/logos/llamacpp.png create mode 100644 extension/react-app/public/logos/lmstudio.png create mode 100644 extension/react-app/public/logos/meta.svg create mode 100644 extension/react-app/public/logos/ollama.png create mode 100644 extension/react-app/public/logos/openai.svg create mode 100644 extension/react-app/public/logos/replicate.png create mode 100644 extension/react-app/public/logos/together.png create mode 100644 extension/react-app/src/components/ErrorStepContainer.tsx create mode 100644 extension/react-app/src/components/ModelCard.tsx delete mode 100644 extension/react-app/src/components/Onboarding.tsx create mode 100644 extension/react-app/src/components/Suggestions.tsx create mode 100644 extension/react-app/src/components/TimelineItem.tsx create mode 100644 extension/react-app/src/components/dialogs/FTCDialog.tsx create mode 100644 extension/react-app/src/components/dialogs/KeyboardShortcuts.tsx create mode 100644 extension/react-app/src/pages/help.tsx create mode 100644 extension/react-app/src/pages/models.tsx (limited to 'extension/react-app') diff --git a/extension/react-app/public/logos/anthropic.png b/extension/react-app/public/logos/anthropic.png new file mode 100644 index 00000000..9adf1b71 Binary files /dev/null and b/extension/react-app/public/logos/anthropic.png differ diff --git a/extension/react-app/public/logos/hf.png b/extension/react-app/public/logos/hf.png new file mode 100644 index 00000000..49e2841d Binary files /dev/null and b/extension/react-app/public/logos/hf.png differ diff --git a/extension/react-app/public/logos/llamacpp.png b/extension/react-app/public/logos/llamacpp.png new file mode 100644 index 00000000..119087b0 Binary files /dev/null and b/extension/react-app/public/logos/llamacpp.png differ diff --git a/extension/react-app/public/logos/lmstudio.png b/extension/react-app/public/logos/lmstudio.png new file mode 100644 index 00000000..b2b73591 Binary files /dev/null and b/extension/react-app/public/logos/lmstudio.png differ diff --git a/extension/react-app/public/logos/meta.svg b/extension/react-app/public/logos/meta.svg new file mode 100644 index 00000000..ba1d38d2 --- /dev/null +++ b/extension/react-app/public/logos/meta.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/extension/react-app/public/logos/ollama.png b/extension/react-app/public/logos/ollama.png new file mode 100644 index 00000000..56ef23f4 Binary files /dev/null and b/extension/react-app/public/logos/ollama.png differ diff --git a/extension/react-app/public/logos/openai.svg b/extension/react-app/public/logos/openai.svg new file mode 100644 index 00000000..9aacd2a1 --- /dev/null +++ b/extension/react-app/public/logos/openai.svg @@ -0,0 +1,2 @@ + +OpenAI icon \ No newline at end of file diff --git a/extension/react-app/public/logos/replicate.png b/extension/react-app/public/logos/replicate.png new file mode 100644 index 00000000..f71504f5 Binary files /dev/null and b/extension/react-app/public/logos/replicate.png differ diff --git a/extension/react-app/public/logos/together.png b/extension/react-app/public/logos/together.png new file mode 100644 index 00000000..21295358 Binary files /dev/null and b/extension/react-app/public/logos/together.png differ 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([ { @@ -37,10 +40,22 @@ const router = createMemoryRouter([ path: "/history", element: , }, + { + path: "/help", + element: , + }, { path: "/settings", element: , }, + { + path: "/models", + element: , + }, + { + path: "/help", + element: , + }, ], }, ]); 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) => void; - selectedContextItems: ContextItem[]; + onEnter: (e?: React.KeyboardEvent) => 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([]); // 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(0); - const [items, setItems] = React.useState(props.items); + const [items, setItems] = React.useState([]); const inputRef = React.useRef(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()); - dispatch(setShowDialog(true)); - }; - - const showDialogToSaveContextGroup = () => { - dispatch( - setDialogMessage( - - ) - ); - 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) => { + 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(); + } + }} + > + + + {selectedContextItems.map((item, idx) => { return ( 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) => { /> ); })} - { - 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(); - } - }} - > - - - {props.selectedContextItems.length > 0 && ( - <> - {props.addingHighlightedCode ? ( - { - props.onToggleAddContext(); - }} - > - Highlight code section - - ) : ( - { - 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(); - } - }} - > - - - )} - { - 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(); - } - }} - > - - - + + {selectedContextItems.length > 0 && ( + { + client?.showContextVirtualFile(); + }} + text="View Current Context" + > + + )}
{ 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 && ( Enter {inQueryForContextProvider.display_title} Query - ) : ( - <> - { - downshiftProps.setInputValue("@"); - inputRef.current?.focus(); - }} - /> - - Add Context to Prompt - - )}
    { })} showAbove={showAbove()} ulHeightPixels={ulRef.current?.getBoundingClientRect().height || 0} - hidden={!downshiftProps.isOpen || items.length === 0} + hidden={ + !downshiftProps.isOpen || + items.length === 0 || + inputRef.current?.value === "" + } > {nestedContextProvider && (
    { items.map((item, index) => (
  • { - // 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(); }} > - + {item.name} {" "} {item.description} @@ -888,7 +876,7 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => { ))}
- {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 )} + 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 ? ( 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 ( +
+
+ props.onClose()}> + + + props.onDelete()}> + + +
+
+
+          {props.historyNode.observation?.error as string}
+        
+
+
+ ); +} + +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 ( <> { > {props.children} - - {props.text} - + {tooltipPortalDiv && + ReactDOM.createPortal( + + {props.text} + , + 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 ( @@ -133,7 +142,6 @@ const Layout = () => { gridTemplateRows: "1fr auto", }} > - { @@ -176,52 +184,24 @@ const Layout = () => { color="yellow" /> )} - - {defaultModel === "MaybeProxyOpenAI" && + {defaultModel === "OpenAIFreeTrial" && (location.pathname === "/settings" || - parseInt(localStorage.getItem("freeTrialCounter") || "0") >= - 125) && ( + parseInt(localStorage.getItem("ftc") || "0") >= 125) && ( )} { - client?.loadSession(undefined); + navigate("/help"); }} - text="New Session (⌥⌘N)" > - + - { - navigate("/history"); - }} - text="History" - > - - - - - - - - - - - - { navigate("/settings"); @@ -248,6 +228,7 @@ const Layout = () => { {bottomMessage} +
); }; 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 ( +
{ + if ((e.target as any).closest("a")) { + return; + } + client?.addModelForRole( + "*", + props.modelInfo.class, + props.modelInfo.args + ); + dispatch(setShowDialog(false)); + navigate("/"); + }} + > +
+ {vscMediaUrl && ( + + )} +

{props.modelInfo.title}

+
+ {props.modelInfo.tags?.map((tag) => { + return ( + + {tag} + + ); + })} +

{props.modelInfo.description}

+ + + + + + +
+ ); +} + +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 (