diff options
Diffstat (limited to 'extension/react-app/src/pages/gui.tsx')
-rw-r--r-- | extension/react-app/src/pages/gui.tsx | 438 |
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> ); } |