import {
ChatBubbleOvalLeftIcon,
CodeBracketSquareIcon,
ExclamationTriangleIcon,
} from "@heroicons/react/24/outline";
import { JSONContent } from "@tiptap/react";
import { usePostHog } from "posthog-js/react";
import {
Fragment,
useCallback,
useEffect,
useLayoutEffect,
useRef,
useState,
} from "react";
import { ErrorBoundary } from "react-error-boundary";
import { useDispatch, useSelector } from "react-redux";
import { useNavigate } from "react-router-dom";
import styled from "styled-components";
import {
Button,
defaultBorderRadius,
lightGray,
vscBackground,
vscForeground,
} from "../components";
import FTCDialog from "../components/dialogs/FTCDialog";
import StepContainer from "../components/gui/StepContainer";
import TimelineItem from "../components/gui/TimelineItem";
import ContinueInputBox from "../components/mainInput/ContinueInputBox";
import useChatHandler from "../hooks/useChatHandler";
import useHistory from "../hooks/useHistory";
import { useWebviewListener } from "../hooks/useWebviewListener";
import { defaultModelSelector } from "../redux/selectors/modelSelectors";
import { newSession, setInactive } from "../redux/slices/stateSlice";
import {
setDialogEntryOn,
setDialogMessage,
setShowDialog,
} from "../redux/slices/uiStateSlice";
import { RootState } from "../redux/store";
import { getMetaKeyLabel, isMetaEquivalentKeyPressed } from "../util";
import { isJetBrains } from "../util/ide";
const TopGuiDiv = styled.div`
overflow-y: scroll;
scrollbar-width: none; /* Firefox */
/* Hide scrollbar for Chrome, Safari and Opera */
&::-webkit-scrollbar {
display: none;
}
height: 100%;
`;
const StopButton = styled.div`
width: fit-content;
margin-right: auto;
margin-left: auto;
font-size: 12px;
border: 0.5px solid ${lightGray};
border-radius: ${defaultBorderRadius};
padding: 4px 8px;
color: ${lightGray};
cursor: pointer;
`;
const StepsDiv = styled.div`
position: relative;
background-color: transparent;
& > * {
position: relative;
}
&::before {
content: "";
position: absolute;
height: calc(100% - 12px);
border-left: 2px solid ${lightGray};
left: 28px;
z-index: 0;
bottom: 12px;
}
`;
const NewSessionButton = styled.div`
width: fit-content;
margin-right: auto;
margin-left: 8px;
margin-top: 4px;
font-size: 12px;
border-radius: ${defaultBorderRadius};
padding: 2px 8px;
color: ${lightGray};
&:hover {
background-color: ${lightGray}33;
color: ${vscForeground};
}
cursor: pointer;
`;
function fallbackRender({ error, resetErrorBoundary }) {
// Call resetErrorBoundary() to reset the error boundary and retry the render.
return (
Something went wrong:
{error.message}
Restart
);
}
interface GUIProps {
firstObservation?: any;
}
function GUI(props: GUIProps) {
// #region Hooks
const posthog = usePostHog();
const dispatch = useDispatch();
const navigate = useNavigate();
// #endregion
// #region Selectors
const sessionState = useSelector((state: RootState) => state.state);
const defaultModel = useSelector(defaultModelSelector);
const active = useSelector((state: RootState) => state.state.active);
// #endregion
// #region State
const [stepsOpen, setStepsOpen] = useState<(boolean | undefined)[]>([]);
const [showLoading, setShowLoading] = useState(false);
useEffect(() => {
setTimeout(() => {
setShowLoading(true);
}, 5000);
}, []);
// #endregion
const mainTextInputRef = useRef(null);
const topGuiDivRef = useRef(null);
// #region Effects
const [userScrolledAwayFromBottom, setUserScrolledAwayFromBottom] =
useState(false);
const state = useSelector((state: RootState) => state.state);
useEffect(() => {
const handleScroll = () => {
// Scroll only if user is within 200 pixels of the bottom of the window.
const edgeOffset = -25;
const scrollPosition = topGuiDivRef.current?.scrollTop || 0;
const scrollHeight = topGuiDivRef.current?.scrollHeight || 0;
const clientHeight = window.innerHeight || 0;
if (scrollPosition + clientHeight + edgeOffset >= scrollHeight) {
setUserScrolledAwayFromBottom(false);
} else {
setUserScrolledAwayFromBottom(true);
}
};
topGuiDivRef.current?.addEventListener("scroll", handleScroll);
return () => {
window.removeEventListener("scroll", handleScroll);
};
}, [topGuiDivRef.current]);
useLayoutEffect(() => {
if (userScrolledAwayFromBottom) return;
topGuiDivRef.current?.scrollTo({
top: topGuiDivRef.current?.scrollHeight,
behavior: "instant" as any,
});
}, [topGuiDivRef.current?.scrollHeight, sessionState.history]);
useEffect(() => {
// Cmd + Backspace to delete current step
const listener = (e: any) => {
if (
e.key === "Backspace" &&
isMetaEquivalentKeyPressed(e) &&
!e.shiftKey
) {
dispatch(setInactive());
}
};
window.addEventListener("keydown", listener);
return () => {
window.removeEventListener("keydown", listener);
};
}, [active]);
// #endregion
const { streamResponse } = useChatHandler(dispatch);
const sendInput = useCallback(
(editorState: JSONContent) => {
if (defaultModel?.provider === "free-trial") {
const ftc = localStorage.getItem("ftc");
if (ftc) {
const u = parseInt(ftc);
localStorage.setItem("ftc", (u + 1).toString());
if (u >= 250) {
dispatch(setShowDialog(true));
dispatch(setDialogMessage( ));
posthog?.capture("ftc_reached");
return;
}
} else {
localStorage.setItem("ftc", "1");
}
}
streamResponse(editorState);
// Increment localstorage counter for popup
const counter = localStorage.getItem("mainTextEntryCounter");
if (counter) {
let currentCount = parseInt(counter);
localStorage.setItem(
"mainTextEntryCounter",
(currentCount + 1).toString(),
);
if (currentCount === 300) {
dispatch(
setDialogMessage(
👋 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.
,
),
);
dispatch(setDialogEntryOn(false));
dispatch(setShowDialog(true));
}
} else {
localStorage.setItem("mainTextEntryCounter", "1");
}
},
[
sessionState.history,
sessionState.contextItems,
defaultModel,
state,
streamResponse,
],
);
const { saveSession } = useHistory(dispatch);
useWebviewListener(
"newSession",
async () => {
saveSession();
mainTextInputRef.current?.focus?.();
},
[saveSession],
);
const isLastUserInput = useCallback(
(index: number): boolean => {
let foundLaterUserInput = false;
for (let i = index + 1; i < state.history.length; i++) {
if (state.history[i].message.role === "user") {
foundLaterUserInput = true;
break;
}
}
return !foundLaterUserInput;
},
[state.history],
);
return (
<>
{state.history.map((item, index: number) => {
return (
{
dispatch(newSession());
}}
>
{item.message.role === "user" ? (
{
streamResponse(editorState, index);
}}
isLastUserInput={isLastUserInput(index)}
isMainInput={false}
editorState={item.editorState}
contextItems={item.contextItems}
>
) : (
) : false ? (
) : (
)
}
open={
typeof stepsOpen[index] === "undefined"
? false
? false
: true
: stepsOpen[index]!
}
onToggle={() => {}}
>
{}}
item={item}
onReverse={() => {}}
onRetry={() => {}}
onDelete={() => {}}
/>
)}
);
})}
{active ? (
<>
>
) : state.history.length > 0 ? (
{
saveSession();
}}
className="mr-auto"
>
New Session ({getMetaKeyLabel()} {isJetBrains() ? "J" : "L"})
) : null}
{active && (
{
dispatch(setInactive());
}}
>
{getMetaKeyLabel()} ⌫ Cancel
)}
>
);
}
export default GUI;