diff options
Diffstat (limited to 'extension/react-app/src/pages')
-rw-r--r-- | extension/react-app/src/pages/gui.tsx | 438 | ||||
-rw-r--r-- | extension/react-app/src/pages/help.tsx | 98 | ||||
-rw-r--r-- | extension/react-app/src/pages/history.tsx | 172 | ||||
-rw-r--r-- | extension/react-app/src/pages/models.tsx | 167 | ||||
-rw-r--r-- | extension/react-app/src/pages/settings.tsx | 45 |
5 files changed, 716 insertions, 204 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> ); } diff --git a/extension/react-app/src/pages/help.tsx b/extension/react-app/src/pages/help.tsx new file mode 100644 index 00000000..3e2e93d2 --- /dev/null +++ b/extension/react-app/src/pages/help.tsx @@ -0,0 +1,98 @@ +import { useNavigate } from "react-router-dom"; +import { ArrowLeftIcon } from "@heroicons/react/24/outline"; +import KeyboardShortcutsDialog from "../components/dialogs/KeyboardShortcuts"; +import { buttonColor, lightGray, vscBackground } from "../components"; +import styled from "styled-components"; + +const IconDiv = styled.div<{ backgroundColor?: string }>` + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + + height: 100%; + padding: 0 4px; + + &:hover { + background-color: ${(props) => props.backgroundColor || lightGray}; + } +`; + +function HelpPage() { + const navigate = useNavigate(); + + return ( + <div className="overflow-scroll"> + <div + className="items-center flex m-0 p-0 sticky top-0" + style={{ + borderBottom: `0.5px solid ${lightGray}`, + backgroundColor: vscBackground, + }} + > + <ArrowLeftIcon + width="1.2em" + height="1.2em" + onClick={() => navigate("/")} + className="inline-block ml-4 cursor-pointer" + /> + <h3 className="text-lg font-bold m-2 inline-block">Help Center</h3> + </div> + + <div className="grid grid-cols-2 grid-rows-2"> + <IconDiv backgroundColor="rgb(234, 51, 35)"> + <a href="https://youtu.be/3Ocrc-WX4iQ?si=eDLYtkc6CXQoHsEc"> + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="-5.2 -4.5 60 60" + fill="white" + className="w-full h-full" + > + <path d="M 44.898438 14.5 C 44.5 12.300781 42.601563 10.699219 40.398438 10.199219 C 37.101563 9.5 31 9 24.398438 9 C 17.800781 9 11.601563 9.5 8.300781 10.199219 C 6.101563 10.699219 4.199219 12.199219 3.800781 14.5 C 3.398438 17 3 20.5 3 25 C 3 29.5 3.398438 33 3.898438 35.5 C 4.300781 37.699219 6.199219 39.300781 8.398438 39.800781 C 11.898438 40.5 17.898438 41 24.5 41 C 31.101563 41 37.101563 40.5 40.601563 39.800781 C 42.800781 39.300781 44.699219 37.800781 45.101563 35.5 C 45.5 33 46 29.398438 46.101563 25 C 45.898438 20.5 45.398438 17 44.898438 14.5 Z M 19 32 L 19 18 L 31.199219 25 Z"></path> + </svg> + </a> + </IconDiv> + <IconDiv backgroundColor={buttonColor}> + <a href="https://continue.dev/docs/how-to-use-continue"> + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="-2.2 -2 28 28" + fill="white" + className="w-full h-full flex items-center justify-center" + > + <path d="M11.25 4.533A9.707 9.707 0 006 3a9.735 9.735 0 00-3.25.555.75.75 0 00-.5.707v14.25a.75.75 0 001 .707A8.237 8.237 0 016 18.75c1.995 0 3.823.707 5.25 1.886V4.533zM12.75 20.636A8.214 8.214 0 0118 18.75c.966 0 1.89.166 2.75.47a.75.75 0 001-.708V4.262a.75.75 0 00-.5-.707A9.735 9.735 0 0018 3a9.707 9.707 0 00-5.25 1.533v16.103z" /> + </svg> + </a> + </IconDiv> + <IconDiv backgroundColor="rgb(88, 98, 227)"> + <a href="https://discord.gg/vapESyrFmJ"> + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="-5 -5.5 60 60" + fill="white" + className="w-full h-full" + > + <path d="M 41.625 10.769531 C 37.644531 7.566406 31.347656 7.023438 31.078125 7.003906 C 30.660156 6.96875 30.261719 7.203125 30.089844 7.589844 C 30.074219 7.613281 29.9375 7.929688 29.785156 8.421875 C 32.417969 8.867188 35.652344 9.761719 38.578125 11.578125 C 39.046875 11.867188 39.191406 12.484375 38.902344 12.953125 C 38.710938 13.261719 38.386719 13.429688 38.050781 13.429688 C 37.871094 13.429688 37.6875 13.378906 37.523438 13.277344 C 32.492188 10.15625 26.210938 10 25 10 C 23.789063 10 17.503906 10.15625 12.476563 13.277344 C 12.007813 13.570313 11.390625 13.425781 11.101563 12.957031 C 10.808594 12.484375 10.953125 11.871094 11.421875 11.578125 C 14.347656 9.765625 17.582031 8.867188 20.214844 8.425781 C 20.0625 7.929688 19.925781 7.617188 19.914063 7.589844 C 19.738281 7.203125 19.34375 6.960938 18.921875 7.003906 C 18.652344 7.023438 12.355469 7.566406 8.320313 10.8125 C 6.214844 12.761719 2 24.152344 2 34 C 2 34.175781 2.046875 34.34375 2.132813 34.496094 C 5.039063 39.605469 12.972656 40.941406 14.78125 41 C 14.789063 41 14.800781 41 14.8125 41 C 15.132813 41 15.433594 40.847656 15.621094 40.589844 L 17.449219 38.074219 C 12.515625 36.800781 9.996094 34.636719 9.851563 34.507813 C 9.4375 34.144531 9.398438 33.511719 9.765625 33.097656 C 10.128906 32.683594 10.761719 32.644531 11.175781 33.007813 C 11.234375 33.0625 15.875 37 25 37 C 34.140625 37 38.78125 33.046875 38.828125 33.007813 C 39.242188 32.648438 39.871094 32.683594 40.238281 33.101563 C 40.601563 33.515625 40.5625 34.144531 40.148438 34.507813 C 40.003906 34.636719 37.484375 36.800781 32.550781 38.074219 L 34.378906 40.589844 C 34.566406 40.847656 34.867188 41 35.1875 41 C 35.199219 41 35.210938 41 35.21875 41 C 37.027344 40.941406 44.960938 39.605469 47.867188 34.496094 C 47.953125 34.34375 48 34.175781 48 34 C 48 24.152344 43.785156 12.761719 41.625 10.769531 Z M 18.5 30 C 16.566406 30 15 28.210938 15 26 C 15 23.789063 16.566406 22 18.5 22 C 20.433594 22 22 23.789063 22 26 C 22 28.210938 20.433594 30 18.5 30 Z M 31.5 30 C 29.566406 30 28 28.210938 28 26 C 28 23.789063 29.566406 22 31.5 22 C 33.433594 22 35 23.789063 35 26 C 35 28.210938 33.433594 30 31.5 30 Z"></path> + </svg> + </a> + </IconDiv> + <IconDiv> + <a href="https://github.com/continuedev/continue/issues/new/choose"> + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="-1.2 -1.2 32 32" + fill="white" + className="w-full h-full" + > + <path d="M15,3C8.373,3,3,8.373,3,15c0,5.623,3.872,10.328,9.092,11.63C12.036,26.468,12,26.28,12,26.047v-2.051 c-0.487,0-1.303,0-1.508,0c-0.821,0-1.551-0.353-1.905-1.009c-0.393-0.729-0.461-1.844-1.435-2.526 c-0.289-0.227-0.069-0.486,0.264-0.451c0.615,0.174,1.125,0.596,1.605,1.222c0.478,0.627,0.703,0.769,1.596,0.769 c0.433,0,1.081-0.025,1.691-0.121c0.328-0.833,0.895-1.6,1.588-1.962c-3.996-0.411-5.903-2.399-5.903-5.098 c0-1.162,0.495-2.286,1.336-3.233C9.053,10.647,8.706,8.73,9.435,8c1.798,0,2.885,1.166,3.146,1.481C13.477,9.174,14.461,9,15.495,9 c1.036,0,2.024,0.174,2.922,0.483C18.675,9.17,19.763,8,21.565,8c0.732,0.731,0.381,2.656,0.102,3.594 c0.836,0.945,1.328,2.066,1.328,3.226c0,2.697-1.904,4.684-5.894,5.097C18.199,20.49,19,22.1,19,23.313v2.734 c0,0.104-0.023,0.179-0.035,0.268C23.641,24.676,27,20.236,27,15C27,8.373,21.627,3,15,3z"></path> + </svg> + </a> + </IconDiv> + </div> + + <KeyboardShortcutsDialog></KeyboardShortcutsDialog> + </div> + ); +} + +export default HelpPage; diff --git a/extension/react-app/src/pages/history.tsx b/extension/react-app/src/pages/history.tsx index b901dd55..b6de0520 100644 --- a/extension/react-app/src/pages/history.tsx +++ b/extension/react-app/src/pages/history.tsx @@ -1,13 +1,14 @@ import React, { useContext, useEffect, useState } from "react"; import { SessionInfo } from "../../../schema/SessionInfo"; import { GUIClientContext } from "../App"; -import { useSelector } from "react-redux"; +import { useDispatch, useSelector } from "react-redux"; import { RootStore } from "../redux/store"; import { useNavigate } from "react-router-dom"; -import { secondaryDark, vscBackground } from "../components"; +import { lightGray, secondaryDark, vscBackground } from "../components"; import styled from "styled-components"; import { ArrowLeftIcon } from "@heroicons/react/24/outline"; import CheckDiv from "../components/CheckDiv"; +import { temporarilyClearSession } from "../redux/slices/serverStateReducer"; const Tr = styled.tr` &:hover { @@ -41,6 +42,7 @@ function lastPartOfPath(path: string): string { function History() { const navigate = useNavigate(); + const dispatch = useDispatch(); const [sessions, setSessions] = useState<SessionInfo[]>([]); const client = useContext(GUIClientContext); const apiUrl = useSelector((state: RootStore) => state.config.apiUrl); @@ -67,78 +69,106 @@ function History() { fetchSessions(); }, [client]); - console.log(sessions.map((session) => session.date_created)); - return ( - <div className="w-full"> - <div className="items-center flex"> - <ArrowLeftIcon - width="1.4em" - height="1.4em" - onClick={() => navigate("/")} - className="inline-block ml-4 cursor-pointer" - /> - <h1 className="text-xl font-bold m-4 inline-block">History</h1> + <div className="overflow-y-scroll"> + <div className="sticky top-0" style={{ backgroundColor: vscBackground }}> + <div + className="items-center flex m-0 p-0" + style={{ + borderBottom: `0.5px solid ${lightGray}`, + }} + > + <ArrowLeftIcon + width="1.2em" + height="1.2em" + onClick={() => navigate("/")} + className="inline-block ml-4 cursor-pointer" + /> + <h3 className="text-lg font-bold m-2 inline-block">History</h3> + </div> + {workspacePaths && workspacePaths.length > 0 && ( + <CheckDiv + checked={filteringByWorkspace} + onClick={() => setFilteringByWorkspace((prev) => !prev)} + title={`Show only sessions from ${lastPartOfPath( + workspacePaths[workspacePaths.length - 1] + )}/`} + /> + )} </div> - {workspacePaths && workspacePaths.length > 0 && ( - <CheckDiv - checked={filteringByWorkspace} - onClick={() => setFilteringByWorkspace((prev) => !prev)} - title={`Show only sessions from ${lastPartOfPath( - workspacePaths[workspacePaths.length - 1] - )}/`} - /> + + {sessions.filter((session) => { + if ( + !filteringByWorkspace || + typeof workspacePaths === "undefined" || + typeof session.workspace_directory === "undefined" + ) { + return true; + } + return workspacePaths.includes(session.workspace_directory); + }).length === 0 && ( + <div className="text-center my-4"> + No past sessions found. To start a new session, either click the "+" + button or use the keyboard shortcut: <b>Option + Command + N</b> + </div> )} - <table className="w-full"> - <tbody> - {sessions - .filter((session) => { - if ( - !filteringByWorkspace || - typeof workspacePaths === "undefined" || - typeof session.workspace_directory === "undefined" - ) { - return true; - } - return workspacePaths.includes(session.workspace_directory); - }) - .sort( - (a, b) => - parseDate(b.date_created).getTime() - - parseDate(a.date_created).getTime() - ) - .map((session, index) => ( - <Tr key={index}> - <td> - <TdDiv - onClick={() => { - client?.loadSession(session.session_id); - navigate("/"); - }} - > - <div className="text-md">{session.title}</div> - <div className="text-gray-400"> - {parseDate(session.date_created).toLocaleString("en-US", { - weekday: "short", - year: "numeric", - month: "long", - day: "numeric", - hour: "numeric", - minute: "numeric", - })} - {" | "} - {lastPartOfPath(session.workspace_directory || "")}/ - </div> - </TdDiv> - </td> - </Tr> - ))} - </tbody> - </table> - <br /> - <i className="text-sm ml-4"> - All session data is saved in ~/.continue/sessions - </i> + + <div> + <table className="w-full"> + <tbody> + {sessions + .filter((session) => { + if ( + !filteringByWorkspace || + typeof workspacePaths === "undefined" || + typeof session.workspace_directory === "undefined" + ) { + return true; + } + return workspacePaths.includes(session.workspace_directory); + }) + .sort( + (a, b) => + parseDate(b.date_created).getTime() - + parseDate(a.date_created).getTime() + ) + .map((session, index) => ( + <Tr key={index}> + <td> + <TdDiv + onClick={() => { + client?.loadSession(session.session_id); + dispatch(temporarilyClearSession()); + navigate("/"); + }} + > + <div className="text-md">{session.title}</div> + <div className="text-gray-400"> + {parseDate(session.date_created).toLocaleString( + "en-US", + { + year: "2-digit", + month: "2-digit", + day: "2-digit", + hour: "numeric", + minute: "2-digit", + hour12: true, + } + )} + {" | "} + {lastPartOfPath(session.workspace_directory || "")}/ + </div> + </TdDiv> + </td> + </Tr> + ))} + </tbody> + </table> + <br /> + <i className="text-sm ml-4"> + All session data is saved in ~/.continue/sessions + </i> + </div> </div> ); } diff --git a/extension/react-app/src/pages/models.tsx b/extension/react-app/src/pages/models.tsx new file mode 100644 index 00000000..1a6f275b --- /dev/null +++ b/extension/react-app/src/pages/models.tsx @@ -0,0 +1,167 @@ +import React from "react"; +import ModelCard, { ModelInfo, ModelTag } from "../components/ModelCard"; +import styled from "styled-components"; +import { ArrowLeftIcon } from "@heroicons/react/24/outline"; +import { lightGray, vscBackground } from "../components"; +import { useNavigate } from "react-router-dom"; + +const MODEL_INFO: ModelInfo[] = [ + { + title: "OpenAI", + class: "OpenAI", + description: "Use gpt-4, gpt-3.5-turbo, or any other OpenAI model", + args: { + model: "gpt-4", + api_key: "", + title: "OpenAI", + }, + icon: "openai.svg", + tags: [ModelTag["Requires API Key"]], + }, + { + title: "Anthropic", + class: "AnthropicLLM", + description: + "Claude-2 is a highly capable model with a 100k context length", + args: { + model: "claude-2", + api_key: "<ANTHROPIC_API_KEY>", + title: "Anthropic", + }, + icon: "anthropic.png", + tags: [ModelTag["Requires API Key"]], + }, + { + title: "Ollama", + class: "Ollama", + description: + "One of the fastest ways to get started with local models on Mac", + args: { + model: "codellama", + title: "Ollama", + }, + icon: "ollama.png", + tags: [ModelTag["Local"], ModelTag["Open-Source"]], + }, + { + title: "TogetherAI", + class: "TogetherLLM", + description: + "Use the TogetherAI API for extremely fast streaming of open-source models", + args: { + model: "togethercomputer/CodeLlama-13b-Instruct", + api_key: "<TOGETHER_API_KEY>", + title: "TogetherAI", + }, + icon: "together.png", + tags: [ModelTag["Requires API Key"], ModelTag["Open-Source"]], + }, + { + title: "LM Studio", + class: "GGML", + description: + "One of the fastest ways to get started with local models on Mac or Windows", + args: { + server_url: "http://localhost:1234", + title: "LM Studio", + }, + icon: "lmstudio.png", + tags: [ModelTag["Local"], ModelTag["Open-Source"]], + }, + { + title: "Replicate", + class: "ReplicateLLM", + description: "Use the Replicate API to run open-source models", + args: { + model: + "replicate/llama-2-70b-chat:58d078176e02c219e11eb4da5a02a7830a283b14cf8f94537af893ccff5ee781", + api_key: "<REPLICATE_API_KEY>", + title: "Replicate", + }, + icon: "replicate.png", + tags: [ModelTag["Requires API Key"], ModelTag["Open-Source"]], + }, + { + title: "llama.cpp", + class: "LlamaCpp", + description: "If you are running the llama.cpp server from source", + args: { + title: "llama.cpp", + }, + icon: "llamacpp.png", + tags: [ModelTag.Local, ModelTag["Open-Source"]], + }, + { + title: "HuggingFace TGI", + class: "HuggingFaceTGI", + description: + "HuggingFace Text Generation Inference is an advanced, highly performant option for serving open-source models to multiple people", + args: { + title: "HuggingFace TGI", + }, + icon: "hf.png", + tags: [ModelTag.Local, ModelTag["Open-Source"]], + }, + { + title: "Other OpenAI-compatible API", + class: "GGML", + description: + "If you are using any other OpenAI-compatible API, for example text-gen-webui, FastChat, LocalAI, or llama-cpp-python, you can simply enter your server URL", + args: { + server_url: "<SERVER_URL>", + }, + icon: "openai.svg", + tags: [ModelTag.Local, ModelTag["Open-Source"]], + }, + { + title: "GPT-4 limited free trial", + class: "OpenAIFreeTrial", + description: + "New users can try out Continue with GPT-4 using a proxy server that securely makes calls to OpenAI using our API key", + args: { + model: "gpt-4", + title: "GPT-4 Free Trial", + }, + icon: "openai.svg", + tags: [ModelTag.Free], + }, +]; + +const GridDiv = styled.div` + display: grid; + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + grid-gap: 2rem; + padding: 1rem; + justify-items: center; + align-items: center; +`; + +function Models() { + const navigate = useNavigate(); + return ( + <div className="overflow-y-scroll"> + <div + className="items-center flex m-0 p-0 sticky top-0" + style={{ + borderBottom: `0.5px solid ${lightGray}`, + backgroundColor: vscBackground, + }} + > + <ArrowLeftIcon + width="1.2em" + height="1.2em" + onClick={() => navigate("/")} + className="inline-block ml-4 cursor-pointer" + /> + <h3 className="text-lg font-bold m-2 inline-block">Add a new model</h3> + </div> + <GridDiv> + {MODEL_INFO.map((model) => ( + <ModelCard modelInfo={model} /> + ))} + </GridDiv> + </div> + ); +} + +export default Models; diff --git a/extension/react-app/src/pages/settings.tsx b/extension/react-app/src/pages/settings.tsx index 8b3d9c5b..4bd51163 100644 --- a/extension/react-app/src/pages/settings.tsx +++ b/extension/react-app/src/pages/settings.tsx @@ -1,15 +1,23 @@ -import React, { useContext, useEffect, useState } from "react"; +import React, { useContext } from "react"; import { GUIClientContext } from "../App"; -import { useSelector } from "react-redux"; +import { useDispatch, useSelector } from "react-redux"; import { RootStore } from "../redux/store"; import { useNavigate } from "react-router-dom"; import { ContinueConfig } from "../../../schema/ContinueConfig"; -import { Button, TextArea, lightGray, secondaryDark } from "../components"; +import { + Button, + TextArea, + lightGray, + secondaryDark, + vscBackground, +} from "../components"; import styled from "styled-components"; -import { ArrowLeftIcon } from "@heroicons/react/24/outline"; +import { ArrowLeftIcon, Squares2X2Icon } from "@heroicons/react/24/outline"; import Loader from "../components/Loader"; import InfoHover from "../components/InfoHover"; import { FormProvider, useForm } from "react-hook-form"; +import { setDialogMessage, setShowDialog } from "../redux/slices/uiStateSlice"; +import KeyboardShortcutsDialog from "../components/dialogs/KeyboardShortcuts"; const Hr = styled.hr` border: 0.5px solid ${lightGray}; @@ -70,7 +78,7 @@ const Slider = styled.input.attrs({ type: "range" })` border: none; } `; -const ALL_MODEL_ROLES = ["default", "small", "medium", "large", "edit", "chat"]; +const ALL_MODEL_ROLES = ["default", "summarize", "edit", "chat"]; function Settings() { const formMethods = useForm<ContinueConfig>(); @@ -79,6 +87,7 @@ function Settings() { const navigate = useNavigate(); const client = useContext(GUIClientContext); const config = useSelector((state: RootStore) => state.serverState.config); + const dispatch = useDispatch(); const submitChanges = () => { if (!client) return; @@ -106,17 +115,23 @@ function Settings() { return ( <FormProvider {...formMethods}> - <div className="w-full"> + <div className="overflow-scroll"> + <div + className="items-center flex sticky top-0" + style={{ + borderBottom: `0.5px solid ${lightGray}`, + backgroundColor: vscBackground, + }} + > + <ArrowLeftIcon + width="1.2em" + height="1.2em" + onClick={submitAndLeave} + className="inline-block ml-4 cursor-pointer" + /> + <h3 className="text-lg font-bold m-2 inline-block">Settings</h3> + </div> <form onSubmit={formMethods.handleSubmit(onSubmit)}> - <div className="items-center flex"> - <ArrowLeftIcon - width="1.4em" - height="1.4em" - onClick={submitAndLeave} - className="inline-block ml-4 cursor-pointer" - /> - <h1 className="text-2xl font-bold m-4 inline-block">Settings</h1> - </div> {config ? ( <div className="p-2"> <h3 className="flex gap-1"> |