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.tsx438
1 files changed, 320 insertions, 118 deletions
diff --git a/extension/react-app/src/pages/gui.tsx b/extension/react-app/src/pages/gui.tsx
index 9f58c505..78b7a970 100644
--- a/extension/react-app/src/pages/gui.tsx
+++ b/extension/react-app/src/pages/gui.tsx
@@ -1,7 +1,5 @@
import styled from "styled-components";
-import { defaultBorderRadius } from "../components";
-import Loader from "../components/Loader";
-import ContinueButton from "../components/ContinueButton";
+import { TextInput, defaultBorderRadius, lightGray } from "../components";
import { FullState } from "../../../schema/FullState";
import {
useEffect,
@@ -9,6 +7,7 @@ import {
useState,
useContext,
useLayoutEffect,
+ useCallback,
} from "react";
import { HistoryNode } from "../../../schema/HistoryNode";
import StepContainer from "../components/StepContainer";
@@ -32,6 +31,19 @@ import {
setServerState,
temporarilyPushToUserInputQueue,
} from "../redux/slices/serverStateReducer";
+import TimelineItem from "../components/TimelineItem";
+import ErrorStepContainer from "../components/ErrorStepContainer";
+import {
+ ChatBubbleOvalLeftIcon,
+ CodeBracketSquareIcon,
+ ExclamationTriangleIcon,
+ FolderIcon,
+ PlusIcon,
+} from "@heroicons/react/24/outline";
+import FTCDialog from "../components/dialogs/FTCDialog";
+import HeaderButtonWithText from "../components/HeaderButtonWithText";
+import { useNavigate } from "react-router-dom";
+import SuggestionsArea from "../components/Suggestions";
const TopGuiDiv = styled.div`
overflow-y: scroll;
@@ -44,6 +56,44 @@ const TopGuiDiv = styled.div`
}
`;
+const TitleTextInput = styled(TextInput)`
+ border: none;
+ outline: none;
+
+ font-size: 16px;
+ font-weight: bold;
+ margin: 0;
+ margin-right: 8px;
+ padding-top: 6px;
+ padding-bottom: 6px;
+
+ &:focus {
+ outline: 1px solid ${lightGray};
+ }
+`;
+
+const StepsDiv = styled.div`
+ position: relative;
+ background-color: transparent;
+ padding-left: 8px;
+ padding-right: 8px;
+
+ & > * {
+ z-index: 1;
+ position: relative;
+ }
+
+ &::before {
+ content: "";
+ position: absolute;
+ height: calc(100% - 24px);
+ border-left: 2px solid ${lightGray};
+ left: 28px;
+ z-index: 0;
+ bottom: 24px;
+ }
+`;
+
const UserInputQueueItem = styled.div`
border-radius: ${defaultBorderRadius};
color: gray;
@@ -52,6 +102,16 @@ const UserInputQueueItem = styled.div`
text-align: center;
`;
+const GUIHeaderDiv = styled.div`
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 4px;
+ padding-left: 8px;
+ padding-right: 8px;
+ border-bottom: 0.5px solid ${lightGray};
+`;
+
interface GUIProps {
firstObservation?: any;
}
@@ -61,6 +121,7 @@ function GUI(props: GUIProps) {
const client = useContext(GUIClientContext);
const posthog = usePostHog();
const dispatch = useDispatch();
+ const navigate = useNavigate();
// #endregion
@@ -73,26 +134,16 @@ function GUI(props: GUIProps) {
const user_input_queue = useSelector(
(state: RootStore) => state.serverState.user_input_queue
);
- const adding_highlighted_code = useSelector(
- (state: RootStore) => state.serverState.adding_highlighted_code
- );
- const selected_context_items = useSelector(
- (state: RootStore) => state.serverState.selected_context_items
+
+ const sessionTitle = useSelector(
+ (state: RootStore) => state.serverState.session_info?.title
);
// #endregion
// #region State
const [waitingForSteps, setWaitingForSteps] = useState(false);
- const [availableSlashCommands, setAvailableSlashCommands] = useState<
- { name: string; description: string }[]
- >([]);
- const [stepsOpen, setStepsOpen] = useState<boolean[]>([
- true,
- true,
- true,
- true,
- ]);
+ const [stepsOpen, setStepsOpen] = useState<(boolean | undefined)[]>([]);
const [waitingForClient, setWaitingForClient] = useState(true);
const [showLoading, setShowLoading] = useState(false);
@@ -150,7 +201,7 @@ function GUI(props: GUIProps) {
topGuiDivRef.current?.scrollTo({
top: topGuiDivRef.current?.scrollHeight,
- behavior: "smooth" as any,
+ behavior: "instant" as any,
});
}, [topGuiDivRef.current?.scrollHeight, history.timeline]);
@@ -160,6 +211,7 @@ function GUI(props: GUIProps) {
if (
e.key === "Backspace" &&
isMetaEquivalentKeyPressed(e) &&
+ !e.shiftKey &&
typeof history?.current_index !== "undefined" &&
history.timeline[history.current_index]?.active
) {
@@ -188,14 +240,6 @@ function GUI(props: GUIProps) {
dispatch(setServerState(state));
setWaitingForSteps(waitingForSteps);
- setAvailableSlashCommands(
- state.slash_commands.map((c: any) => {
- return {
- name: `/${c.name}`,
- description: c.description,
- };
- })
- );
setStepsOpen((prev) => {
const nextStepsOpen = [...prev];
for (
@@ -203,7 +247,7 @@ function GUI(props: GUIProps) {
i < state.history.timeline.length;
i++
) {
- nextStepsOpen.push(true);
+ nextStepsOpen.push(undefined);
}
return nextStepsOpen;
});
@@ -214,7 +258,6 @@ function GUI(props: GUIProps) {
useEffect(() => {
if (client && waitingForClient) {
- console.log("sending user input queue, ", user_input_queue);
setWaitingForClient(false);
for (const input of user_input_queue) {
client.sendMainInput(input);
@@ -244,43 +287,22 @@ function GUI(props: GUIProps) {
return;
}
- // Increment localstorage counter for usage of free trial
if (
- defaultModel === "MaybeProxyOpenAI" &&
+ defaultModel === "OpenAIFreeTrial" &&
(!input.startsWith("/") || input.startsWith("/edit"))
) {
- const freeTrialCounter = localStorage.getItem("freeTrialCounter");
- if (freeTrialCounter) {
- const usages = parseInt(freeTrialCounter);
- localStorage.setItem("freeTrialCounter", (usages + 1).toString());
+ const ftc = localStorage.getItem("ftc");
+ if (ftc) {
+ const u = parseInt(ftc);
+ localStorage.setItem("ftc", (u + 1).toString());
- if (usages >= 250) {
- console.log("Free trial limit reached");
+ if (u >= 250) {
dispatch(setShowDialog(true));
- dispatch(
- setDialogMessage(
- <div className="p-4">
- <h3>Free Trial Limit Reached</h3>
- 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 MaybeProxyOpenAI object.
- </div>
- )
- );
+ dispatch(setDialogMessage(<FTCDialog />));
return;
}
} else {
- localStorage.setItem("freeTrialCounter", "1");
+ localStorage.setItem("ftc", "1");
}
}
@@ -391,6 +413,69 @@ function GUI(props: GUIProps) {
client.sendStepUserInput(input, index);
};
+ const getStepsInUserInputGroup = useCallback(
+ (index: number): number[] => {
+ // index is the index in the entire timeline, hidden steps included
+ const stepsInUserInputGroup: number[] = [];
+
+ // First find the closest above UserInputStep
+ let userInputIndex = -1;
+ for (let i = index; i >= 0; i--) {
+ if (
+ history?.timeline.length > i &&
+ history.timeline[i].step.name === "User Input" &&
+ history.timeline[i].step.hide === false
+ ) {
+ stepsInUserInputGroup.push(i);
+ userInputIndex = i;
+ break;
+ }
+ }
+ if (stepsInUserInputGroup.length === 0) return [];
+
+ for (let i = userInputIndex + 1; i < history?.timeline.length; i++) {
+ if (
+ history?.timeline.length > i &&
+ history.timeline[i].step.name === "User Input" &&
+ history.timeline[i].step.hide === false
+ ) {
+ break;
+ }
+ stepsInUserInputGroup.push(i);
+ }
+ return stepsInUserInputGroup;
+ },
+ [history.timeline]
+ );
+
+ const onToggleAtIndex = useCallback(
+ (index: number) => {
+ // Check if all steps after the User Input are closed
+ const groupIndices = getStepsInUserInputGroup(index);
+ const userInputIndex = groupIndices[0];
+ setStepsOpen((prev) => {
+ const nextStepsOpen = [...prev];
+ nextStepsOpen[index] = !nextStepsOpen[index];
+ const allStepsAfterUserInputAreClosed = !groupIndices.some(
+ (i, j) => j > 0 && nextStepsOpen[i]
+ );
+ if (allStepsAfterUserInputAreClosed) {
+ nextStepsOpen[userInputIndex] = false;
+ } else {
+ const allStepsAfterUserInputAreOpen = !groupIndices.some(
+ (i, j) => j > 0 && !nextStepsOpen[i]
+ );
+ if (allStepsAfterUserInputAreOpen) {
+ nextStepsOpen[userInputIndex] = true;
+ }
+ }
+
+ return nextStepsOpen;
+ });
+ },
+ [getStepsInUserInputGroup]
+ );
+
useEffect(() => {
const timeout = setTimeout(() => {
setShowLoading(true);
@@ -400,6 +485,17 @@ function GUI(props: GUIProps) {
clearTimeout(timeout);
};
}, []);
+
+ useEffect(() => {
+ if (sessionTitle) {
+ setSessionTitleInput(sessionTitle);
+ }
+ }, [sessionTitle]);
+
+ const [sessionTitleInput, setSessionTitleInput] = useState<string>(
+ sessionTitle || "New Session"
+ );
+
return (
<TopGuiDiv
ref={topGuiDivRef}
@@ -409,6 +505,51 @@ function GUI(props: GUIProps) {
}
}}
>
+ <GUIHeaderDiv>
+ <TitleTextInput
+ onClick={(e) => {
+ // Select all text
+ (e.target as any).setSelectionRange(
+ 0,
+ (e.target as any).value.length
+ );
+ }}
+ value={sessionTitleInput}
+ onChange={(e) => setSessionTitleInput(e.target.value)}
+ onBlur={(e) => {
+ client?.setCurrentSessionTitle(e.target.value);
+ }}
+ onKeyDown={(e) => {
+ if (e.key === "Enter") {
+ e.preventDefault();
+ (e.target as any).blur();
+ }
+ }}
+ />
+ <div className="flex">
+ {history.timeline.filter((n) => !n.step.hide).length > 0 && (
+ <HeaderButtonWithText
+ onClick={() => {
+ if (history.timeline.filter((n) => !n.step.hide).length > 0) {
+ client?.loadSession(undefined);
+ }
+ }}
+ text="New Session (⌥⌘N)"
+ >
+ <PlusIcon width="1.4em" height="1.4em" />
+ </HeaderButtonWithText>
+ )}
+
+ <HeaderButtonWithText
+ onClick={() => {
+ navigate("/history");
+ }}
+ text="History"
+ >
+ <FolderIcon width="1.4em" height="1.4em" />
+ </HeaderButtonWithText>
+ </div>
+ </GUIHeaderDiv>
{showLoading && typeof client === "undefined" && (
<>
<RingLoader />
@@ -478,63 +619,128 @@ function GUI(props: GUIProps) {
</u>
</p>
</div>
-
- <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. */}
- {/* Tip: If there is an error in the terminal, use COMMAND+D to
- automatically debug */}
- </div>
</>
)}
- {history?.timeline.map((node: HistoryNode, index: number) => {
- return node.step.name === "User Input" ? (
- node.step.hide || (
- <UserInputContainer
- index={index}
- 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 />}
+ <br />
+ <SuggestionsArea
+ onClick={(textInput) => {
+ client?.sendMainInput(textInput);
+ }}
+ />
+ <StepsDiv>
+ {history?.timeline.map((node: HistoryNode, index: number) => {
+ if (node.step.hide) return null;
+ return (
+ <>
+ {node.step.name === "User Input" ? (
+ node.step.hide || (
+ <UserInputContainer
+ active={getStepsInUserInputGroup(index).some((i) => {
+ return history.timeline[i].active;
+ })}
+ groupIndices={getStepsInUserInputGroup(index)}
+ onToggle={(isOpen: boolean) => {
+ // Collapse all steps in the section
+ setStepsOpen((prev) => {
+ const nextStepsOpen = [...prev];
+ getStepsInUserInputGroup(index).forEach((i) => {
+ nextStepsOpen[i] = isOpen;
+ });
+ return nextStepsOpen;
+ });
+ }}
+ onToggleAll={(isOpen: boolean) => {
+ // Collapse _all_ steps
+ setStepsOpen((prev) => {
+ return prev.map((_) => isOpen);
+ });
+ }}
+ isToggleOpen={
+ typeof stepsOpen[index] === "undefined"
+ ? true
+ : stepsOpen[index]!
+ }
+ index={index}
+ onDelete={() => {
+ // Delete the input and all steps until the next user input
+ getStepsInUserInputGroup(index).forEach((i) => {
+ client?.deleteAtIndex(i);
+ });
+ }}
+ historyNode={node}
+ >
+ {node.step.description as string}
+ </UserInputContainer>
+ )
+ ) : (
+ <TimelineItem
+ historyNode={node}
+ iconElement={
+ node.step.class_name === "DefaultModelEditCodeStep" ? (
+ <CodeBracketSquareIcon width="16px" height="16px" />
+ ) : node.observation?.error ? (
+ <ExclamationTriangleIcon
+ width="16px"
+ height="16px"
+ color="red"
+ />
+ ) : (
+ <ChatBubbleOvalLeftIcon width="16px" height="16px" />
+ )
+ }
+ open={
+ typeof stepsOpen[index] === "undefined"
+ ? node.observation?.error
+ ? false
+ : true
+ : stepsOpen[index]!
+ }
+ onToggle={() => onToggleAtIndex(index)}
+ >
+ {node.observation?.error ? (
+ <ErrorStepContainer
+ onClose={() => onToggleAtIndex(index)}
+ historyNode={node}
+ onDelete={() => client?.deleteAtIndex(index)}
+ />
+ ) : (
+ <StepContainer
+ index={index}
+ isLast={index === history.timeline.length - 1}
+ isFirst={index === 0}
+ open={
+ typeof stepsOpen[index] === "undefined"
+ ? true
+ : stepsOpen[index]!
+ }
+ 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);
+ }}
+ noUserInputParent={
+ getStepsInUserInputGroup(index).length === 0
+ }
+ />
+ )}
+ </TimelineItem>
+ )}
+ {/* <div className="h-2"></div> */}
+ </>
+ );
+ })}
+ </StepsDiv>
<div>
{user_input_queue?.map?.((input) => {
@@ -547,18 +753,14 @@ function GUI(props: GUIProps) {
ref={mainTextInputRef}
onEnter={(e) => {
onMainTextInput(e);
- e.stopPropagation();
- e.preventDefault();
+ e?.stopPropagation();
+ e?.preventDefault();
}}
onInputValueChange={() => {}}
- items={availableSlashCommands}
- selectedContextItems={selected_context_items}
onToggleAddContext={() => {
client?.toggleAddingHighlightedCode();
}}
- addingHighlightedCode={adding_highlighted_code}
/>
- <ContinueButton onClick={onMainTextInput} />
</TopGuiDiv>
);
}