summaryrefslogtreecommitdiff
path: root/extension/react-app/src/pages/gui.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'extension/react-app/src/pages/gui.tsx')
-rw-r--r--extension/react-app/src/pages/gui.tsx486
1 files changed, 486 insertions, 0 deletions
diff --git a/extension/react-app/src/pages/gui.tsx b/extension/react-app/src/pages/gui.tsx
new file mode 100644
index 00000000..b6a18dc8
--- /dev/null
+++ b/extension/react-app/src/pages/gui.tsx
@@ -0,0 +1,486 @@
+import styled from "styled-components";
+import { defaultBorderRadius } from "../components";
+import Loader from "../components/Loader";
+import ContinueButton from "../components/ContinueButton";
+import { FullState, HighlightedRangeContext } 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 {
+ BookOpen,
+ ChatBubbleOvalLeftEllipsis,
+ Trash,
+} from "@styled-icons/heroicons-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 { useSelector } from "react-redux";
+import { RootStore } from "../redux/store";
+import LoadingCover from "../components/LoadingCover";
+import { postVscMessage } from "../vscode";
+import UserInputContainer from "../components/UserInputContainer";
+import Onboarding from "../components/Onboarding";
+
+const TopGUIDiv = styled.div`
+ overflow: hidden;
+`;
+
+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 [usingFastModel, setUsingFastModel] = useState(false);
+ const [waitingForSteps, setWaitingForSteps] = useState(false);
+ const [userInputQueue, setUserInputQueue] = useState<string[]>([]);
+ const [highlightedRanges, setHighlightedRanges] = useState<
+ HighlightedRangeContext[]
+ >([]);
+ const [addingHighlightedCode, setAddingHighlightedCode] = useState(false);
+ const [availableSlashCommands, setAvailableSlashCommands] = useState<
+ { name: string; description: string }[]
+ >([]);
+ const [pinned, setPinned] = useState(false);
+ const [showDataSharingInfo, setShowDataSharingInfo] = useState(false);
+ 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 and ask a question or give instructions
+- Use \`cmd+k\` (Mac) / \`ctrl+k\` (Windows) to open Continue
+- Use \`cmd+shift+e\` / \`ctrl+shift+e\` to open file Explorer
+- Add your own OpenAI API key to VS Code Settings with \`cmd+,\`
+- Use slash commands when you want fine-grained control
+- Past steps are included as part of the context by default`,
+ system_message: null,
+ chat_context: [],
+ manage_own_chat_context: false,
+ message: "",
+ },
+ depth: 0,
+ deleted: false,
+ active: false,
+ },
+ ],
+ current_index: 3,
+ } as any);
+
+ const [showFeedbackDialog, setShowFeedbackDialog] = useState(false);
+ const [feedbackDialogMessage, setFeedbackDialogMessage] = useState("");
+
+ 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(() => {
+ const listener = (e: any) => {
+ // Cmd + i to toggle fast model
+ if (e.key === "i" && e.metaKey && e.shiftKey) {
+ setUsingFastModel((prev) => !prev);
+ // Cmd + backspace to stop currently running step
+ } else if (
+ e.key === "Backspace" &&
+ e.metaKey &&
+ typeof history?.current_index !== "undefined" &&
+ history.timeline[history.current_index]?.active
+ ) {
+ client?.deleteAtIndex(history.current_index);
+ }
+ };
+ 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.
+ setUsingFastModel(state.default_model === "gpt-3.5-turbo");
+ 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);
+ setHighlightedRanges(state.highlighted_ranges);
+ 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 mainTextInputRef = useRef<HTMLInputElement>(null);
+
+ const deleteContextItems = useCallback(
+ (indices: number[]) => {
+ client?.deleteContextAtIndices(indices);
+ },
+ [client]
+ );
+
+ const onMainTextInput = (event?: any) => {
+ if (mainTextInputRef.current) {
+ let input = (mainTextInputRef.current as any).inputValue;
+ // cmd+enter to /edit
+ if (event?.metaKey) {
+ input = `/edit ${input}`;
+ }
+ (mainTextInputRef.current as any).setInputValue("");
+ if (!client) 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];
+ });
+ }
+ };
+
+ const onStepUserInput = (input: string, index: number) => {
+ if (!client) return;
+ console.log("Sending step user input", input, index);
+ client.sendStepUserInput(input, index);
+ };
+
+ // const iterations = useSelector(selectIterations);
+ return (
+ <>
+ <Onboarding></Onboarding>
+ <LoadingCover hidden={true} message="Downloading local model..." />
+ <TextDialog
+ showDialog={showFeedbackDialog}
+ onEnter={(text) => {
+ client?.sendMainInput(`/feedback ${text}`);
+ setShowFeedbackDialog(false);
+ }}
+ onClose={() => {
+ setShowFeedbackDialog(false);
+ }}
+ message={feedbackDialogMessage}
+ ></TextDialog>
+
+ <TopGUIDiv
+ ref={topGuiDivRef}
+ onKeyDown={(e) => {
+ if (e.key === "Enter" && e.ctrlKey) {
+ onMainTextInput();
+ }
+ }}
+ >
+ {typeof client === "undefined" && (
+ <>
+ <Loader />
+ <p style={{ textAlign: "center" }}>Loading Continue server...</p>
+ </>
+ )}
+ {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
+ 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>
+
+ <ComboBox
+ ref={mainTextInputRef}
+ onEnter={(e) => {
+ onMainTextInput(e);
+ e.stopPropagation();
+ e.preventDefault();
+ }}
+ onInputValueChange={() => {}}
+ items={availableSlashCommands}
+ highlightedCodeSections={highlightedRanges}
+ deleteContextItems={deleteContextItems}
+ onTogglePin={() => {
+ setPinned((prev: boolean) => !prev);
+ }}
+ onToggleAddContext={() => {
+ client?.toggleAddingHighlightedCode();
+ }}
+ addingHighlightedCode={addingHighlightedCode}
+ />
+ <ContinueButton onClick={onMainTextInput} />
+ </TopGUIDiv>
+ <div
+ style={{
+ position: "fixed",
+ bottom: "50px",
+ backgroundColor: "white",
+ color: "black",
+ borderRadius: defaultBorderRadius,
+ padding: "16px",
+ margin: "16px",
+ zIndex: 100,
+ }}
+ hidden={!showDataSharingInfo}
+ >
+ By turning on this switch, you will begin collecting accepted and
+ rejected suggestions in .continue/suggestions.json. This data is stored
+ locally on your machine and not sent anywhere.
+ <br />
+ <br />
+ <b>
+ {dataSwitchChecked
+ ? "👍 Data is being collected"
+ : "👎 No data is being collected"}
+ </b>
+ </div>
+ <Footer dataSwitchChecked={dataSwitchChecked}>
+ <div
+ style={{
+ display: "flex",
+ gap: "4px",
+ marginRight: "auto",
+ alignItems: "center",
+ }}
+ onMouseEnter={() => {
+ setShowDataSharingInfo(true);
+ }}
+ onMouseLeave={() => {
+ setShowDataSharingInfo(false);
+ }}
+ >
+ <ReactSwitch
+ height={20}
+ handleDiameter={20}
+ width={40}
+ onChange={() => {
+ posthog?.capture("data_switch_toggled", {
+ vscMachineId: vscMachineId,
+ dataSwitchChecked: !dataSwitchChecked,
+ });
+ postVscMessage("toggleDataSwitch", { on: !dataSwitchChecked });
+ setDataSwitchChecked((prev) => !prev);
+ }}
+ onColor="#12887a"
+ checked={dataSwitchChecked}
+ />
+ <span style={{ cursor: "help", fontSize: "14px" }}>Collect Data</span>
+ </div>
+ <HeaderButtonWithText
+ onClick={() => {
+ // client?.changeDefaultModel(
+ // usingFastModel ? "gpt-4" : "gpt-3.5-turbo"
+ // );
+ if (!usingFastModel) {
+ // Show the dialog
+ setFeedbackDialogMessage(
+ "We don't yet support local models, but we're working on it! If privacy is a concern of yours, please write a short note to let us know."
+ );
+ setShowFeedbackDialog(true);
+ }
+ setUsingFastModel((prev) => !prev);
+ }}
+ text={usingFastModel ? "local" : "gpt-4"}
+ >
+ <div
+ style={{ fontSize: "18px", marginLeft: "2px", marginRight: "2px" }}
+ >
+ {usingFastModel ? "🔒" : "🧠"}
+ </div>
+ </HeaderButtonWithText>
+ <HeaderButtonWithText
+ onClick={() => {
+ client?.sendClear();
+ }}
+ text="Clear"
+ >
+ <Trash size="1.6em" />
+ </HeaderButtonWithText>
+ <a
+ href="https://continue.dev/docs/how-to-use-continue"
+ className="no-underline"
+ >
+ <HeaderButtonWithText text="Docs">
+ <BookOpen size="1.6em" />
+ </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."
+ );
+ setShowFeedbackDialog(true);
+ }}
+ text="Feedback"
+ >
+ <ChatBubbleOvalLeftEllipsis size="1.6em" />
+ </HeaderButtonWithText>
+ </Footer>
+ </>
+ );
+}
+
+export default GUI;