summaryrefslogtreecommitdiff
path: root/extension/react-app/src/pages
diff options
context:
space:
mode:
authorNate Sesti <33237525+sestinj@users.noreply.github.com>2023-08-06 22:54:51 -0700
committerGitHub <noreply@github.com>2023-08-06 22:54:51 -0700
commit74005855304412f0401e29c83c166e99a8ab0944 (patch)
tree4e8ddb61fd52068e839ca4ccab268e013405d545 /extension/react-app/src/pages
parenta0d3f29ee237484c66b0efe243c79d902f2da993 (diff)
parent8ada89b0f66f9e746394ee64591359537fe0c7f0 (diff)
downloadsncontinue-74005855304412f0401e29c83c166e99a8ab0944.tar.gz
sncontinue-74005855304412f0401e29c83c166e99a8ab0944.tar.bz2
sncontinue-74005855304412f0401e29c83c166e99a8ab0944.zip
Merge pull request #351 from continuedev/history
Session History
Diffstat (limited to 'extension/react-app/src/pages')
-rw-r--r--extension/react-app/src/pages/error.tsx17
-rw-r--r--extension/react-app/src/pages/gui.tsx609
-rw-r--r--extension/react-app/src/pages/history.tsx98
3 files changed, 336 insertions, 388 deletions
diff --git a/extension/react-app/src/pages/error.tsx b/extension/react-app/src/pages/error.tsx
new file mode 100644
index 00000000..5267c405
--- /dev/null
+++ b/extension/react-app/src/pages/error.tsx
@@ -0,0 +1,17 @@
+import { useRouteError } from "react-router-dom";
+
+export default function ErrorPage() {
+ const error: any = useRouteError();
+ console.error(error);
+
+ return (
+ <div id="error-page" className="text-center">
+ <h1>Error in Continue React App</h1>
+ <p>
+ <i>{error.statusText || error.message}</i>
+ </p>
+ <br />
+ <pre className="text-left m-4">{error.stack}</pre>
+ </div>
+ );
+}
diff --git a/extension/react-app/src/pages/gui.tsx b/extension/react-app/src/pages/gui.tsx
index d69da57e..e96ffe65 100644
--- a/extension/react-app/src/pages/gui.tsx
+++ b/extension/react-app/src/pages/gui.tsx
@@ -1,60 +1,31 @@
import styled from "styled-components";
-import {
- defaultBorderRadius,
- secondaryDark,
- vscForeground,
-} from "../components";
+import { defaultBorderRadius } from "../components";
import Loader from "../components/Loader";
import ContinueButton from "../components/ContinueButton";
-import { ContextItem, FullState } from "../../../schema/FullState";
+import { 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,
+ setDialogEntryOn,
+ setDialogMessage,
+ setDisplayBottomMessageOnBottom,
+ setShowDialog,
} 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;
-`;
+import {
+ setServerState,
+ temporarilySetUserInputQueue,
+} from "../redux/slices/serverStateReducer";
const UserInputQueueItem = styled.div`
border-radius: ${defaultBorderRadius};
@@ -64,47 +35,30 @@ const UserInputQueueItem = styled.div`
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) {
+ // #region Hooks
const client = useContext(GUIClientContext);
const posthog = usePostHog();
+ const dispatch = useDispatch();
- const vscMachineId = useSelector(
- (state: RootStore) => state.config.vscMachineId
- );
- const [dataSwitchChecked, setDataSwitchChecked] = useState(false);
- const dataSwitchOn = useSelector(
- (state: RootStore) => state.config.dataSwitchOn
- );
+ // #endregion
- useEffect(() => {
- if (typeof dataSwitchOn !== "undefined") {
- setDataSwitchChecked(dataSwitchOn);
- }
- }, [dataSwitchOn]);
+ // #region Selectors
+ const {
+ history,
+ user_input_queue,
+ adding_highlighted_code,
+ selected_context_items,
+ } = useSelector((state: RootStore) => state.serverState);
+
+ // #endregion
+ // #region State
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 }[]
>([]);
@@ -114,62 +68,33 @@ function GUI(props: GUIProps) {
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 [waitingForClient, setWaitingForClient] = useState(true);
+ const [showLoading, setShowLoading] = useState(false);
- const dispatch = useDispatch();
- const bottomMessage = useSelector(
- (state: RootStore) => state.uiState.bottomMessage
- );
+ // #endregion
- const [displayBottomMessageOnBottom, setDisplayBottomMessageOnBottom] =
- useState<boolean>(true);
+ // #region Refs
const mainTextInputRef = useRef<HTMLInputElement>(null);
+ const topGuiDivRef = useRef<HTMLDivElement>(null);
+ // #endregion
- const aboveComboBoxDivRef = useRef<HTMLDivElement>(null);
+ // #region Effects
+ // Set displayBottomMessageOnBottom
+ const aboveComboBoxDivRef = useRef<HTMLDivElement>(null);
+ const bottomMessage = useSelector(
+ (state: RootStore) => state.uiState.bottomMessage
+ );
useEffect(() => {
if (!aboveComboBoxDivRef.current) return;
- if (
- aboveComboBoxDivRef.current.getBoundingClientRect().top >
- window.innerHeight / 2
- ) {
- setDisplayBottomMessageOnBottom(false);
- } else {
- setDisplayBottomMessageOnBottom(true);
- }
+ dispatch(
+ setDisplayBottomMessageOnBottom(
+ aboveComboBoxDivRef.current.getBoundingClientRect().top <
+ window.innerHeight / 2
+ )
+ );
}, [bottomMessage, aboveComboBoxDivRef.current]);
- const topGuiDivRef = useRef<HTMLDivElement>(null);
-
const [scrollTimeout, setScrollTimeout] = useState<NodeJS.Timeout | null>(
null
);
@@ -225,11 +150,9 @@ function GUI(props: GUIProps) {
state.history.current_index
].step.description?.trim() === "";
+ dispatch(setServerState(state));
+
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 {
@@ -260,15 +183,16 @@ function GUI(props: GUIProps) {
scrollToBottom();
}, [waitingForSteps]);
- const [waitingForClient, setWaitingForClient] = useState(true);
+ // #endregion
+
useEffect(() => {
if (client && waitingForClient) {
setWaitingForClient(false);
- for (const input of userInputQueue) {
+ for (const input of user_input_queue) {
client.sendMainInput(input);
}
}
- }, [client, userInputQueue, waitingForClient]);
+ }, [client, user_input_queue, waitingForClient]);
const onMainTextInput = (event?: any) => {
if (mainTextInputRef.current) {
@@ -279,9 +203,11 @@ function GUI(props: GUIProps) {
}
(mainTextInputRef.current as any).setInputValue("");
if (!client) {
- setUserInputQueue((queue) => {
- return [...queue, input];
- });
+ dispatch(
+ temporarilySetUserInputQueue((queue: string[]) => {
+ return [...queue, input];
+ })
+ );
return;
}
@@ -310,9 +236,11 @@ function GUI(props: GUIProps) {
if (input.trim() === "") return;
client.sendMainInput(input);
- setUserInputQueue((queue) => {
- return [...queue, input];
- });
+ dispatch(
+ temporarilySetUserInputQueue((queue: string[]) => {
+ return [...queue, input];
+ })
+ );
// Increment localstorage counter
const counter = localStorage.getItem("mainTextEntryCounter");
@@ -323,62 +251,66 @@ function GUI(props: GUIProps) {
(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
+ dispatch(
+ setDialogMessage(
+ <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,
+ });
+ dispatch(
+ setDialogMessage(
+ <div className="text-center">
+ Thanks! We'll be in touch soon.
+ </div>
+ )
+ );
+ }}
style={{
- padding: "10px",
- borderRadius: "5px",
- cursor: "pointer",
+ display: "flex",
+ flexDirection: "column",
+ gap: "10px",
}}
- type="submit"
>
- Submit
- </button>
- </form>
- </div>
+ <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);
+ dispatch(setDialogEntryOn(false));
+ dispatch(setShowDialog(true));
}
} else {
localStorage.setItem("mainTextEntryCounter", "1");
@@ -391,7 +323,6 @@ function GUI(props: GUIProps) {
client.sendStepUserInput(input, index);
};
- const [showLoading, setShowLoading] = useState(false);
useEffect(() => {
const timeout = setTimeout(() => {
setShowLoading(true);
@@ -401,219 +332,121 @@ function GUI(props: GUIProps) {
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);
- }}
+ <div
+ className="overflow-hidden"
+ 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 />}
+
+ <div>
+ {user_input_queue.map((input) => {
+ return <UserInputQueueItem>{input}</UserInputQueueItem>;
})}
- {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));
+ </div>
+
+ <div ref={aboveComboBoxDivRef} />
+ <ComboBox
+ ref={mainTextInputRef}
+ onEnter={(e) => {
+ onMainTextInput(e);
+ e.stopPropagation();
+ e.preventDefault();
}}
- onMouseLeave={(e) => {
- if (!e.buttons) {
- dispatch(setBottomMessage(undefined));
- }
+ onInputValueChange={() => {}}
+ items={availableSlashCommands}
+ selectedContextItems={selected_context_items}
+ onToggleAddContext={() => {
+ client?.toggleAddingHighlightedCode();
}}
- 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>
- </>
+ addingHighlightedCode={adding_highlighted_code}
+ />
+ <ContinueButton onClick={onMainTextInput} />
+ </div>
);
}
diff --git a/extension/react-app/src/pages/history.tsx b/extension/react-app/src/pages/history.tsx
new file mode 100644
index 00000000..c3240a66
--- /dev/null
+++ b/extension/react-app/src/pages/history.tsx
@@ -0,0 +1,98 @@
+import React, { useContext, useEffect, useState } from "react";
+import { SessionInfo } from "../../../schema/SessionInfo";
+import { GUIClientContext } from "../App";
+import { useSelector } from "react-redux";
+import { RootStore } from "../redux/store";
+import { useNavigate } from "react-router-dom";
+import { secondaryDark, vscBackground } from "../components";
+import styled from "styled-components";
+import { ArrowLeftIcon } from "@heroicons/react/24/outline";
+
+const Tr = styled.tr`
+ &:hover {
+ background-color: ${secondaryDark};
+ }
+`;
+
+const TdDiv = styled.div`
+ cursor: pointer;
+ padding-left: 1rem;
+ padding-right: 1rem;
+ padding-top: 0.5rem;
+ padding-bottom: 0.5rem;
+ border-bottom: 1px solid ${secondaryDark};
+`;
+
+function History() {
+ const navigate = useNavigate();
+ const [sessions, setSessions] = useState<SessionInfo[]>([]);
+ const client = useContext(GUIClientContext);
+ const apiUrl = useSelector((state: RootStore) => state.config.apiUrl);
+
+ useEffect(() => {
+ const fetchSessions = async () => {
+ console.log("fetching sessions from: ", apiUrl);
+ if (!apiUrl) {
+ return;
+ }
+ const response = await fetch(`${apiUrl}/sessions/list`);
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+ const json = await response.json();
+ console.log(json);
+ setSessions(json);
+ };
+ fetchSessions();
+ }, [client]);
+
+ 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-2xl font-bold m-4 inline-block">History</h1>
+ </div>
+ <table className="w-full">
+ <tbody>
+ {sessions.map((session, index) => (
+ <Tr key={index}>
+ <td>
+ <TdDiv
+ onClick={() => {
+ client?.loadSession(session.session_id);
+ navigate("/");
+ }}
+ >
+ <div className="text-lg">{session.title}</div>
+ <div className="text-gray-400">
+ {new Date(
+ parseInt(session.date_created) * 1000
+ ).toLocaleString("en-US", {
+ weekday: "short",
+ year: "numeric",
+ month: "long",
+ day: "numeric",
+ hour: "numeric",
+ minute: "numeric",
+ })}
+ </div>
+ </TdDiv>
+ </td>
+ </Tr>
+ ))}
+ </tbody>
+ </table>
+ <br />
+ <i className="text-sm ml-4">
+ All session data is saved in ~/.continue/sessions
+ </i>
+ </div>
+ );
+}
+
+export default History;