import styled from "styled-components"; import { defaultBorderRadius, secondaryDark, vscForeground, } from "../components"; import Loader from "../components/Loader"; import ContinueButton from "../components/ContinueButton"; import { ContextItem, FullState } from "../../../schema/FullState"; import { useCallback, useEffect, useRef, useState, useContext } from "react"; import { History } from "../../../schema/History"; import { HistoryNode } from "../../../schema/HistoryNode"; import StepContainer from "../components/StepContainer"; import { GUIClientContext } from "../App"; import { BookOpenIcon, ChatBubbleOvalLeftEllipsisIcon, TrashIcon, } from "@heroicons/react/24/outline"; import ComboBox from "../components/ComboBox"; import TextDialog from "../components/TextDialog"; import HeaderButtonWithText from "../components/HeaderButtonWithText"; import ReactSwitch from "react-switch"; 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, } from "../redux/slices/uiStateSlice"; import RingLoader from "../components/RingLoader"; 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}; color: gray; padding: 8px; margin: 8px; 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 client = useContext(GUIClientContext); const posthog = usePostHog(); 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]); const [waitingForSteps, setWaitingForSteps] = useState(false); const [userInputQueue, setUserInputQueue] = useState<string[]>([]); const [addingHighlightedCode, setAddingHighlightedCode] = useState(false); const [selectedContextItems, setSelectedContextItems] = useState< ContextItem[] >([]); const [availableSlashCommands, setAvailableSlashCommands] = useState< { name: string; description: string }[] >([]); const [stepsOpen, setStepsOpen] = useState<boolean[]>([ true, true, true, true, ]); const [history, setHistory] = useState<History | undefined>({ timeline: [ { step: { name: "Welcome to Continue", hide: false, description: `- Highlight code section and ask a question or give instructions - Use \`cmd+m\` (Mac) / \`ctrl+m\` (Windows) to open Continue - Use \`/help\` to ask questions about how to use Continue`, system_message: null, chat_context: [], manage_own_chat_context: false, message: "", }, depth: 0, deleted: false, active: false, }, ], current_index: 3, } as any); 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 ); const [displayBottomMessageOnBottom, setDisplayBottomMessageOnBottom] = useState<boolean>(true); const mainTextInputRef = useRef<HTMLInputElement>(null); const aboveComboBoxDivRef = useRef<HTMLDivElement>(null); useEffect(() => { if (!aboveComboBoxDivRef.current) return; if ( aboveComboBoxDivRef.current.getBoundingClientRect().top > window.innerHeight / 2 ) { setDisplayBottomMessageOnBottom(false); } else { setDisplayBottomMessageOnBottom(true); } }, [bottomMessage, aboveComboBoxDivRef.current]); const topGuiDivRef = useRef<HTMLDivElement>(null); const [scrollTimeout, setScrollTimeout] = useState<NodeJS.Timeout | null>( null ); const scrollToBottom = useCallback(() => { if (scrollTimeout) { clearTimeout(scrollTimeout); } // Debounced smooth scroll to bottom of screen if (topGuiDivRef.current) { const timeout = setTimeout(() => { window.scrollTo({ top: topGuiDivRef.current!.offsetHeight, behavior: "smooth", }); }, 200); setScrollTimeout(timeout); } }, [topGuiDivRef.current, scrollTimeout]); useEffect(() => { // Cmd + Backspace to delete current step const listener = (e: any) => { if ( e.key === "Backspace" && isMetaEquivalentKeyPressed(e) && typeof history?.current_index !== "undefined" && history.timeline[history.current_index]?.active ) { client?.deleteAtIndex(history.current_index); } else if (e.key === "Escape") { dispatch(setBottomMessage(undefined)); } }; window.addEventListener("keydown", listener); return () => { window.removeEventListener("keydown", listener); }; }, [client, history]); useEffect(() => { client?.onStateUpdate((state: FullState) => { // Scroll only if user is at very bottom of the window. const shouldScrollToBottom = topGuiDivRef.current && topGuiDivRef.current?.offsetHeight - window.scrollY < 100; const waitingForSteps = state.active && state.history.current_index < state.history.timeline.length && state.history.timeline[state.history.current_index] && state.history.timeline[ state.history.current_index ].step.description?.trim() === ""; setWaitingForSteps(waitingForSteps); setHistory(state.history); setSelectedContextItems(state.selected_context_items || []); setUserInputQueue(state.user_input_queue); setAddingHighlightedCode(state.adding_highlighted_code); setAvailableSlashCommands( state.slash_commands.map((c: any) => { return { name: `/${c.name}`, description: c.description, }; }) ); setStepsOpen((prev) => { const nextStepsOpen = [...prev]; for ( let i = nextStepsOpen.length; i < state.history.timeline.length; i++ ) { nextStepsOpen.push(true); } return nextStepsOpen; }); if (shouldScrollToBottom) { scrollToBottom(); } }); }, [client]); useEffect(() => { scrollToBottom(); }, [waitingForSteps]); const [waitingForClient, setWaitingForClient] = useState(true); useEffect(() => { if (client && waitingForClient) { setWaitingForClient(false); for (const input of userInputQueue) { client.sendMainInput(input); } } }, [client, userInputQueue, waitingForClient]); const onMainTextInput = (event?: any) => { if (mainTextInputRef.current) { let input = (mainTextInputRef.current as any).inputValue; // cmd+enter to /edit if (isMetaEquivalentKeyPressed(event)) { input = `/edit ${input}`; } (mainTextInputRef.current as any).setInputValue(""); if (!client) { setUserInputQueue((queue) => { return [...queue, input]; }); return; } setWaitingForSteps(true); if ( history && history.current_index >= 0 && history.current_index < history.timeline.length ) { if ( history.timeline[history.current_index]?.step.name === "Waiting for user input" ) { if (input.trim() === "") return; onStepUserInput(input, history!.current_index); return; } else if ( history.timeline[history.current_index]?.step.name === "Waiting for user confirmation" ) { onStepUserInput("ok", history!.current_index); return; } } if (input.trim() === "") return; client.sendMainInput(input); setUserInputQueue((queue) => { return [...queue, input]; }); // Increment localstorage counter const counter = localStorage.getItem("mainTextEntryCounter"); if (counter) { let currentCount = parseInt(counter); localStorage.setItem( "mainTextEntryCounter", (currentCount + 1).toString() ); if (currentCount === 25) { setFeedbackDialogMessage( <div className="text-center"> 👋 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. <br /> <br /> <form onSubmit={(e: any) => { e.preventDefault(); posthog?.capture("user_interest_form", { name: e.target.elements[0].value, email: e.target.elements[1].value, }); setFeedbackDialogMessage( <div className="text-center"> Thanks! We'll be in touch soon. </div> ); }} style={{ display: "flex", flexDirection: "column", gap: "10px", }} > <input style={{ padding: "10px", borderRadius: "5px" }} type="text" name="name" placeholder="Name" required /> <input style={{ padding: "10px", borderRadius: "5px" }} type="email" name="email" placeholder="Email" required /> <button style={{ padding: "10px", borderRadius: "5px", cursor: "pointer", }} type="submit" > Submit </button> </form> </div> ); setFeedbackEntryOn(false); setShowFeedbackDialog(true); } } else { localStorage.setItem("mainTextEntryCounter", "1"); } } }; const onStepUserInput = (input: string, index: number) => { if (!client) return; client.sendStepUserInput(input, index); }; const [showLoading, setShowLoading] = useState(false); useEffect(() => { const timeout = setTimeout(() => { setShowLoading(true); }, 3000); return () => { clearTimeout(timeout); }; }, []); // const iterations = useSelector(selectIterations); return ( <> <Onboarding /> <TextDialog showDialog={showFeedbackDialog} onEnter={(text) => { client?.sendMainInput(`/feedback ${text}`); setShowFeedbackDialog(false); }} onClose={() => { setShowFeedbackDialog(false); }} message={feedbackDialogMessage} entryOn={feedbackEntryOn} /> <TopGUIDiv ref={topGuiDivRef} onKeyDown={(e) => { if (e.key === "Enter" && e.ctrlKey) { onMainTextInput(); } }} > {showLoading && typeof client === "undefined" && ( <> <RingLoader /> <p style={{ textAlign: "center", margin: "0px", fontSize: "14px", }} > Continue Server Starting </p> <p style={{ margin: "auto", textAlign: "center", marginTop: "4px", fontSize: "12px", cursor: "pointer", opacity: 0.7, }} onClick={() => { postVscMessage("toggleDevTools", {}); }} > <u>Click to view logs</u> </p> <div className="w-3/4 m-auto text-center text-xs"> Tip: Drag the Continue logo from the far left of the window to the right, then toggle Continue using option/alt+command+m. </div> </> )} {history?.timeline.map((node: HistoryNode, index: number) => { return node.step.name === "User Input" ? ( node.step.hide || ( <UserInputContainer onDelete={() => { client?.deleteAtIndex(index); }} historyNode={node} > {node.step.description as string} </UserInputContainer> ) ) : ( <StepContainer index={index} isLast={index === history.timeline.length - 1} isFirst={index === 0} open={stepsOpen[index]} onToggle={() => { const nextStepsOpen = [...stepsOpen]; nextStepsOpen[index] = !nextStepsOpen[index]; setStepsOpen(nextStepsOpen); }} onToggleAll={() => { const shouldOpen = !stepsOpen[index]; setStepsOpen((prev) => prev.map(() => shouldOpen)); }} key={index} onUserInput={(input: string) => { onStepUserInput(input, index); }} inFuture={index > history?.current_index} historyNode={node} onReverse={() => { client?.reverseToIndex(index); }} onRetry={() => { client?.retryAtIndex(index); setWaitingForSteps(true); }} onDelete={() => { client?.deleteAtIndex(index); }} /> ); })} {waitingForSteps && <Loader></Loader>} <div> {userInputQueue.map((input) => { return <UserInputQueueItem>{input}</UserInputQueueItem>; })} </div> <div ref={aboveComboBoxDivRef} /> <ComboBox ref={mainTextInputRef} onEnter={(e) => { onMainTextInput(e); e.stopPropagation(); e.preventDefault(); }} onInputValueChange={() => {}} items={availableSlashCommands} selectedContextItems={selectedContextItems} onToggleAddContext={() => { client?.toggleAddingHighlightedCode(); }} addingHighlightedCode={addingHighlightedCode} /> <ContinueButton onClick={onMainTextInput} /> </TopGUIDiv> <BottomMessageDiv displayOnBottom={displayBottomMessageOnBottom} onMouseEnter={() => { dispatch(setBottomMessageCloseTimeout(undefined)); }} onMouseLeave={(e) => { if (!e.buttons) { dispatch(setBottomMessage(undefined)); } }} hidden={!bottomMessage} > {bottomMessage} </BottomMessageDiv> <Footer dataSwitchChecked={dataSwitchChecked}> {vscMediaUrl && ( <a href="https://github.com/continuedev/continue" style={{ marginRight: "auto" }} > <img src={`${vscMediaUrl}/continue-dev-square.png`} width="22px" style={{ backgroundColor: "black", color: "red" }} /> </a> )} <HeaderButtonWithText onClick={() => { // 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"} > <div style={{ fontSize: "18px", marginLeft: "2px", marginRight: "2px" }} > 🔒 </div> </HeaderButtonWithText> <HeaderButtonWithText onClick={() => { client?.sendClear(); }} text="Clear" > <TrashIcon 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> <HeaderButtonWithText onClick={() => { // 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" > <ChatBubbleOvalLeftEllipsisIcon width="1.4em" height="1.4em" /> </HeaderButtonWithText> </Footer> </> ); } export default GUI;