From 712b110ac67e7bff0729b903512bda7f4c77c8d9 Mon Sep 17 00:00:00 2001 From: Nate Sesti Date: Sun, 6 Aug 2023 16:16:50 -0700 Subject: refactor: :recycle: refactor bottom bar into Layout --- extension/react-app/src/components/Layout.tsx | 183 ++++++- extension/react-app/src/pages/gui.tsx | 555 +++++++-------------- .../react-app/src/redux/slices/uiStateSlice.ts | 26 +- extension/react-app/src/redux/store.ts | 4 + 4 files changed, 403 insertions(+), 365 deletions(-) (limited to 'extension/react-app') diff --git a/extension/react-app/src/components/Layout.tsx b/extension/react-app/src/components/Layout.tsx index 47ed9cc3..30cb5df2 100644 --- a/extension/react-app/src/components/Layout.tsx +++ b/extension/react-app/src/components/Layout.tsx @@ -1,6 +1,29 @@ import styled from "styled-components"; -import { defaultBorderRadius } from "."; +import { defaultBorderRadius, secondaryDark, vscForeground } from "."; import { Outlet } from "react-router-dom"; +import Onboarding from "./Onboarding"; +import TextDialog from "./TextDialog"; +import { useContext } from "react"; +import { GUIClientContext } from "../App"; +import { useDispatch, useSelector } from "react-redux"; +import { RootStore } from "../redux/store"; +import { + setBottomMessage, + setBottomMessageCloseTimeout, + setDialogEntryOn, + setDialogMessage, + setShowDialog, +} from "../redux/slices/uiStateSlice"; +import { + PlusIcon, + FolderIcon, + BookOpenIcon, + ChatBubbleOvalLeftEllipsisIcon, +} from "@heroicons/react/24/outline"; +import HeaderButtonWithText from "./HeaderButtonWithText"; +import { useNavigate } from "react-router-dom"; + +// #region Styled Components const LayoutTopDiv = styled.div` height: 100%; @@ -9,7 +32,56 @@ const LayoutTopDiv = styled.div` scrollbar-width: thin; `; +const BottomMessageDiv = styled.div<{ displayOnBottom: boolean }>` + position: fixed; + bottom: ${(props) => (props.displayOnBottom ? "50px" : undefined)}; + top: ${(props) => (props.displayOnBottom ? undefined : "50px")}; + left: 0; + right: 0; + margin: 8px; + margin-top: 0; + background-color: ${secondaryDark}; + color: ${vscForeground}; + border-radius: ${defaultBorderRadius}; + padding: 12px; + z-index: 100; + box-shadow: 0px 0px 2px 0px ${vscForeground}; + max-height: 50vh; + overflow: scroll; +`; + +const Footer = styled.footer` + display: flex; + flex-direction: row; + gap: 8px; + justify-content: right; + padding: 8px; + align-items: center; + margin-top: 8px; + border-top: 0.1px solid gray; +`; + +// #endregion + const Layout = () => { + const navigate = useNavigate(); + const client = useContext(GUIClientContext); + const dispatch = useDispatch(); + const { showDialog, dialogEntryOn, dialogMessage } = useSelector( + (state: RootStore) => state.uiState + ); + + // #region Selectors + const vscMediaUrl = useSelector( + (state: RootStore) => state.config.vscMediaUrl + ); + + const { bottomMessage, displayBottomMessageOnBottom } = useSelector( + (state: RootStore) => state.uiState + ); + + // #endregion + return (
{ gridTemplateRows: "1fr auto", }} > + + { + client?.sendMainInput(`/feedback ${text}`); + dispatch(setShowDialog(false)); + }} + onClose={() => { + dispatch(setShowDialog(false)); + }} + message={dialogMessage} + entryOn={dialogEntryOn} + /> + + { + dispatch(setBottomMessageCloseTimeout(undefined)); + }} + onMouseLeave={(e) => { + if (!e.buttons) { + dispatch(setBottomMessage(undefined)); + } + }} + hidden={!bottomMessage} + > + {bottomMessage} + +
+ {vscMediaUrl && ( + + + + )} + { + // Show the dialog + dispatch( + setDialogMessage(`Continue uses GPT-4 by default, but works with any model. If you'd like to keep your code completely private, there are few options: + + Run a local model with ggml: [5 minute quickstart](https://github.com/continuedev/ggml-server-example) + + Use Azure OpenAI service, which is GDPR and HIPAA compliant: [Tutorial](https://continue.dev/docs/customization#azure-openai-service) + + If you already have an LLM deployed on your own infrastructure, or would like to do so, please contact us at hi@continue.dev. + `) + ); + dispatch(setDialogEntryOn(false)); + dispatch(setShowDialog(true)); + }} + text={"Use Private Model"} + > +
+ 🔒 +
+
+ { + client?.loadSession(undefined); + }} + text="New Session" + > + + + { + navigate("/history"); + }} + text="History" + > + + + + + + + + { + // Set dialog open + dispatch( + setDialogMessage( + "Having trouble using Continue? Want a new feature? Let us know! This box is anonymous, but we will promptly address your feedback." + ) + ); + dispatch(setDialogEntryOn(true)); + dispatch(setShowDialog(true)); + }} + text="Feedback" + > + + +
); diff --git a/extension/react-app/src/pages/gui.tsx b/extension/react-app/src/pages/gui.tsx index dab429b5..e2e3431a 100644 --- a/extension/react-app/src/pages/gui.tsx +++ b/extension/react-app/src/pages/gui.tsx @@ -1,9 +1,5 @@ import styled from "styled-components"; -import { - defaultBorderRadius, - secondaryDark, - vscForeground, -} from "../components"; +import { defaultBorderRadius } from "../components"; import Loader from "../components/Loader"; import ContinueButton from "../components/ContinueButton"; import { ContextItem, FullState } from "../../../schema/FullState"; @@ -12,51 +8,21 @@ import { History } from "../../../schema/History"; import { HistoryNode } from "../../../schema/HistoryNode"; import StepContainer from "../components/StepContainer"; import { GUIClientContext } from "../App"; -import { - BookOpenIcon, - ChatBubbleOvalLeftEllipsisIcon, - TrashIcon, - PlusIcon, - FolderIcon, -} from "@heroicons/react/24/outline"; import ComboBox from "../components/ComboBox"; -import TextDialog from "../components/TextDialog"; -import HeaderButtonWithText from "../components/HeaderButtonWithText"; import { usePostHog } from "posthog-js/react"; import { useDispatch, useSelector } from "react-redux"; import { RootStore } from "../redux/store"; import { postVscMessage } from "../vscode"; import UserInputContainer from "../components/UserInputContainer"; -import Onboarding from "../components/Onboarding"; import { isMetaEquivalentKeyPressed } from "../util"; import { setBottomMessage, - setBottomMessageCloseTimeout, + setDialogEntryOn, + setDialogMessage, + setDisplayBottomMessageOnBottom, + setShowDialog, } from "../redux/slices/uiStateSlice"; import RingLoader from "../components/RingLoader"; -import { useNavigate } from "react-router-dom"; - -const TopGUIDiv = styled.div` - overflow: hidden; -`; - -const BottomMessageDiv = styled.div<{ displayOnBottom: boolean }>` - position: fixed; - bottom: ${(props) => (props.displayOnBottom ? "50px" : undefined)}; - top: ${(props) => (props.displayOnBottom ? undefined : "50px")}; - left: 0; - right: 0; - margin: 8px; - margin-top: 0; - background-color: ${secondaryDark}; - color: ${vscForeground}; - border-radius: ${defaultBorderRadius}; - padding: 12px; - z-index: 100; - box-shadow: 0px 0px 2px 0px ${vscForeground}; - max-height: 50vh; - overflow: scroll; -`; const UserInputQueueItem = styled.div` border-radius: ${defaultBorderRadius}; @@ -66,43 +32,19 @@ const UserInputQueueItem = styled.div` text-align: center; `; -const Footer = styled.footer<{ dataSwitchChecked: boolean }>` - display: flex; - flex-direction: row; - gap: 8px; - justify-content: right; - padding: 8px; - align-items: center; - margin-top: 8px; - border-top: 0.1px solid gray; - background-color: ${(props) => - props.dataSwitchChecked ? "#12887a33" : "transparent"}; -`; - interface GUIProps { firstObservation?: any; } function GUI(props: GUIProps) { - const navigate = useNavigate(); - + // #region Hooks const client = useContext(GUIClientContext); const posthog = usePostHog(); + const dispatch = useDispatch(); - const vscMachineId = useSelector( - (state: RootStore) => state.config.vscMachineId - ); - const [dataSwitchChecked, setDataSwitchChecked] = useState(false); - const dataSwitchOn = useSelector( - (state: RootStore) => state.config.dataSwitchOn - ); - - useEffect(() => { - if (typeof dataSwitchOn !== "undefined") { - setDataSwitchChecked(dataSwitchOn); - } - }, [dataSwitchOn]); + // #endregion + // #region State const [waitingForSteps, setWaitingForSteps] = useState(false); const [userInputQueue, setUserInputQueue] = useState([]); const [addingHighlightedCode, setAddingHighlightedCode] = useState(false); @@ -139,41 +81,33 @@ function GUI(props: GUIProps) { ], current_index: 3, } as any); + const [waitingForClient, setWaitingForClient] = useState(true); + const [showLoading, setShowLoading] = useState(false); - const vscMediaUrl = useSelector( - (state: RootStore) => state.config.vscMediaUrl - ); - const [showFeedbackDialog, setShowFeedbackDialog] = useState(false); - const [feedbackDialogMessage, setFeedbackDialogMessage] = useState< - string | JSX.Element - >(""); - const [feedbackEntryOn, setFeedbackEntryOn] = useState(true); - - const dispatch = useDispatch(); - const bottomMessage = useSelector( - (state: RootStore) => state.uiState.bottomMessage - ); + // #endregion - const [displayBottomMessageOnBottom, setDisplayBottomMessageOnBottom] = - useState(true); + // #region Refs const mainTextInputRef = useRef(null); + const topGuiDivRef = useRef(null); + // #endregion - const aboveComboBoxDivRef = useRef(null); + // #region Effects + // Set displayBottomMessageOnBottom + const aboveComboBoxDivRef = useRef(null); + const bottomMessage = useSelector( + (state: RootStore) => state.uiState.bottomMessage + ); useEffect(() => { if (!aboveComboBoxDivRef.current) return; - if ( - aboveComboBoxDivRef.current.getBoundingClientRect().top > - window.innerHeight / 2 - ) { - setDisplayBottomMessageOnBottom(false); - } else { - setDisplayBottomMessageOnBottom(true); - } + dispatch( + setDisplayBottomMessageOnBottom( + aboveComboBoxDivRef.current.getBoundingClientRect().top < + window.innerHeight / 2 + ) + ); }, [bottomMessage, aboveComboBoxDivRef.current]); - const topGuiDivRef = useRef(null); - const [scrollTimeout, setScrollTimeout] = useState( null ); @@ -264,7 +198,8 @@ function GUI(props: GUIProps) { scrollToBottom(); }, [waitingForSteps]); - const [waitingForClient, setWaitingForClient] = useState(true); + // #endregion + useEffect(() => { if (client && waitingForClient) { setWaitingForClient(false); @@ -327,62 +262,66 @@ function GUI(props: GUIProps) { (currentCount + 1).toString() ); if (currentCount === 25) { - setFeedbackDialogMessage( -
- 👋 Thanks for using Continue. We are a beta product and love - working closely with our first users. If you're interested in - speaking, enter your name and email. We won't use this information - for anything other than reaching out. -
-
-
{ - e.preventDefault(); - posthog?.capture("user_interest_form", { - name: e.target.elements[0].value, - email: e.target.elements[1].value, - }); - setFeedbackDialogMessage( -
- Thanks! We'll be in touch soon. -
- ); - }} - style={{ - display: "flex", - flexDirection: "column", - gap: "10px", - }} - > - - - -
-
+ + + + + + ) ); - setFeedbackEntryOn(false); - setShowFeedbackDialog(true); + dispatch(setDialogEntryOn(false)); + dispatch(setShowDialog(true)); } } else { localStorage.setItem("mainTextEntryCounter", "1"); @@ -395,7 +334,6 @@ function GUI(props: GUIProps) { client.sendStepUserInput(input, index); }; - const [showLoading, setShowLoading] = useState(false); useEffect(() => { const timeout = setTimeout(() => { setShowLoading(true); @@ -405,228 +343,121 @@ function GUI(props: GUIProps) { clearTimeout(timeout); }; }, []); - - // const iterations = useSelector(selectIterations); return ( - <> - - { - client?.sendMainInput(`/feedback ${text}`); - setShowFeedbackDialog(false); - }} - onClose={() => { - setShowFeedbackDialog(false); - }} - message={feedbackDialogMessage} - entryOn={feedbackEntryOn} - /> - - { - if (e.key === "Enter" && e.ctrlKey) { - onMainTextInput(); - } - }} - > - {showLoading && typeof client === "undefined" && ( - <> - -

- Continue Server Starting -

-

{ - postVscMessage("toggleDevTools", {}); - }} - > - Click to view logs -

-
- Tip: Drag the Continue logo from the far left of the window to the - right, then toggle Continue using option/alt+command+m. -
- - )} - {history?.timeline.map((node: HistoryNode, index: number) => { - return node.step.name === "User Input" ? ( - node.step.hide || ( - { - client?.deleteAtIndex(index); - }} - historyNode={node} - > - {node.step.description as string} - - ) - ) : ( - { - const nextStepsOpen = [...stepsOpen]; - nextStepsOpen[index] = !nextStepsOpen[index]; - setStepsOpen(nextStepsOpen); - }} - onToggleAll={() => { - const shouldOpen = !stepsOpen[index]; - setStepsOpen((prev) => prev.map(() => shouldOpen)); - }} - key={index} - onUserInput={(input: string) => { - onStepUserInput(input, index); - }} - inFuture={index > history?.current_index} - historyNode={node} - onReverse={() => { - client?.reverseToIndex(index); - }} - onRetry={() => { - client?.retryAtIndex(index); - setWaitingForSteps(true); - }} +
{ + if (e.key === "Enter" && e.ctrlKey) { + onMainTextInput(); + } + }} + > + {showLoading && typeof client === "undefined" && ( + <> + +

+ Continue Server Starting +

+

{ + postVscMessage("toggleDevTools", {}); + }} + > + Click to view logs +

+
+ Tip: Drag the Continue logo from the far left of the window to the + right, then toggle Continue using option/alt+command+m. +
+ + )} + {history?.timeline.map((node: HistoryNode, index: number) => { + return node.step.name === "User Input" ? ( + node.step.hide || ( + { client?.deleteAtIndex(index); }} - /> - ); - })} - {waitingForSteps && } - -
- {userInputQueue.map((input) => { - return {input}; - })} -
+ historyNode={node} + > + {node.step.description as string} +
+ ) + ) : ( + { + const nextStepsOpen = [...stepsOpen]; + nextStepsOpen[index] = !nextStepsOpen[index]; + setStepsOpen(nextStepsOpen); + }} + onToggleAll={() => { + const shouldOpen = !stepsOpen[index]; + setStepsOpen((prev) => prev.map(() => shouldOpen)); + }} + key={index} + onUserInput={(input: string) => { + onStepUserInput(input, index); + }} + inFuture={index > history?.current_index} + historyNode={node} + onReverse={() => { + client?.reverseToIndex(index); + }} + onRetry={() => { + client?.retryAtIndex(index); + setWaitingForSteps(true); + }} + onDelete={() => { + client?.deleteAtIndex(index); + }} + /> + ); + })} + {waitingForSteps && } -
- { - onMainTextInput(e); - e.stopPropagation(); - e.preventDefault(); - }} - onInputValueChange={() => {}} - items={availableSlashCommands} - selectedContextItems={selectedContextItems} - onToggleAddContext={() => { - client?.toggleAddingHighlightedCode(); - }} - addingHighlightedCode={addingHighlightedCode} - /> - - - { - dispatch(setBottomMessageCloseTimeout(undefined)); +
+ {userInputQueue.map((input) => { + return {input}; + })} +
+ +
+ { + onMainTextInput(e); + e.stopPropagation(); + e.preventDefault(); }} - onMouseLeave={(e) => { - if (!e.buttons) { - dispatch(setBottomMessage(undefined)); - } + onInputValueChange={() => {}} + items={availableSlashCommands} + selectedContextItems={selectedContextItems} + onToggleAddContext={() => { + client?.toggleAddingHighlightedCode(); }} - hidden={!bottomMessage} - > - {bottomMessage} - -
- {vscMediaUrl && ( - - - - )} - { - // Show the dialog - setFeedbackDialogMessage( - `Continue uses GPT-4 by default, but works with any model. If you'd like to keep your code completely private, there are few options: - -Run a local model with ggml: [5 minute quickstart](https://github.com/continuedev/ggml-server-example) - -Use Azure OpenAI service, which is GDPR and HIPAA compliant: [Tutorial](https://continue.dev/docs/customization#azure-openai-service) - -If you already have an LLM deployed on your own infrastructure, or would like to do so, please contact us at hi@continue.dev. - ` - ); - setFeedbackEntryOn(false); - setShowFeedbackDialog(true); - }} - text={"Use Private Model"} - > -
- 🔒 -
-
- { - client?.loadSession(undefined); - }} - text="New Session" - > - - - { - // Go to /history page - navigate("/history"); - }} - text="History" - > - - - - - - - - { - // Set dialog open - setFeedbackDialogMessage( - "Having trouble using Continue? Want a new feature? Let us know! This box is anonymous, but we will promptly address your feedback." - ); - setFeedbackEntryOn(true); - setShowFeedbackDialog(true); - }} - text="Feedback" - > - - -
- + addingHighlightedCode={addingHighlightedCode} + /> + +
); } diff --git a/extension/react-app/src/redux/slices/uiStateSlice.ts b/extension/react-app/src/redux/slices/uiStateSlice.ts index 837d19e9..d34596c9 100644 --- a/extension/react-app/src/redux/slices/uiStateSlice.ts +++ b/extension/react-app/src/redux/slices/uiStateSlice.ts @@ -5,6 +5,10 @@ export const uiStateSlice = createSlice({ initialState: { bottomMessage: undefined, bottomMessageCloseTimeout: undefined, + showDialog: false, + dialogMessage: "", + dialogEntryOn: false, + displayBottomMessageOnBottom: true, }, reducers: { setBottomMessage: (state, action) => { @@ -16,9 +20,27 @@ export const uiStateSlice = createSlice({ } state.bottomMessageCloseTimeout = action.payload; }, + setDialogMessage: (state, action) => { + state.dialogMessage = action.payload; + }, + setDialogEntryOn: (state, action) => { + state.dialogEntryOn = action.payload; + }, + setShowDialog: (state, action) => { + state.showDialog = action.payload; + }, + setDisplayBottomMessageOnBottom: (state, action) => { + state.displayBottomMessageOnBottom = action.payload; + }, }, }); -export const { setBottomMessage, setBottomMessageCloseTimeout } = - uiStateSlice.actions; +export const { + setBottomMessage, + setBottomMessageCloseTimeout, + setDialogMessage, + setDialogEntryOn, + setShowDialog, + setDisplayBottomMessageOnBottom, +} = uiStateSlice.actions; export default uiStateSlice.reducer; diff --git a/extension/react-app/src/redux/store.ts b/extension/react-app/src/redux/store.ts index d49513e5..aa8f5e7b 100644 --- a/extension/react-app/src/redux/store.ts +++ b/extension/react-app/src/redux/store.ts @@ -35,6 +35,10 @@ export interface RootStore { uiState: { bottomMessage: JSX.Element | undefined; bottomMessageCloseTimeout: NodeJS.Timeout | undefined; + displayBottomMessageOnBottom: boolean; + showDialog: boolean; + dialogMessage: string | JSX.Element; + dialogEntryOn: boolean; }; } -- cgit v1.2.3-70-g09d2