summaryrefslogtreecommitdiff
path: root/extension/react-app/src/components/UserInputContainer.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'extension/react-app/src/components/UserInputContainer.tsx')
-rw-r--r--extension/react-app/src/components/UserInputContainer.tsx376
1 files changed, 257 insertions, 119 deletions
diff --git a/extension/react-app/src/components/UserInputContainer.tsx b/extension/react-app/src/components/UserInputContainer.tsx
index 866fef58..76a3c615 100644
--- a/extension/react-app/src/components/UserInputContainer.tsx
+++ b/extension/react-app/src/components/UserInputContainer.tsx
@@ -1,5 +1,11 @@
-import React, { useContext, useEffect, useRef, useState } from "react";
-import styled from "styled-components";
+import React, {
+ useCallback,
+ useContext,
+ useEffect,
+ useRef,
+ useState,
+} from "react";
+import styled, { keyframes } from "styled-components";
import {
defaultBorderRadius,
lightGray,
@@ -8,69 +14,115 @@ import {
vscForeground,
} from ".";
import HeaderButtonWithText from "./HeaderButtonWithText";
-import { XMarkIcon, CheckIcon } from "@heroicons/react/24/outline";
+import {
+ XMarkIcon,
+ CheckIcon,
+ ChevronDownIcon,
+ ChevronRightIcon,
+ MagnifyingGlassIcon,
+ StopCircleIcon,
+} from "@heroicons/react/24/outline";
import { HistoryNode } from "../../../schema/HistoryNode";
import { GUIClientContext } from "../App";
+import { getMetaKeyLabel, isMetaEquivalentKeyPressed } from "../util";
+import { RootStore } from "../redux/store";
+import { useSelector } from "react-redux";
interface UserInputContainerProps {
onDelete: () => void;
children: string;
historyNode: HistoryNode;
index: number;
+ onToggle: (arg0: boolean) => void;
+ onToggleAll: (arg0: boolean) => void;
+ isToggleOpen: boolean;
+ active: boolean;
+ groupIndices: number[];
}
-const StyledDiv = styled.div`
- position: relative;
- background-color: ${secondaryDark};
- font-size: 13px;
+const gradient = keyframes`
+ 0% {
+ background-position: 0px 0;
+ }
+ 100% {
+ background-position: 100em 0;
+ }
+`;
+
+const ToggleDiv = styled.div`
display: flex;
align-items: center;
- border-bottom: 1px solid ${vscBackground};
- padding: 8px;
- padding-top: 0px;
- padding-bottom: 0px;
+ justify-content: center;
+ cursor: pointer;
- border-bottom: 0.5px solid ${lightGray};
- border-top: 0.5px solid ${lightGray};
-`;
+ height: 100%;
+ padding: 0 4px;
-const DeleteButtonDiv = styled.div`
- position: absolute;
- top: 8px;
- right: 8px;
+ &:hover {
+ background-color: ${vscBackground};
+ }
`;
-const StyledPre = styled.pre`
- margin-right: 22px;
- margin-left: 8px;
- white-space: pre-wrap;
- word-wrap: break-word;
- font-family: "Lexend", sans-serif;
- font-size: 13px;
+const GradientBorder = styled.div<{
+ borderWidth?: number;
+ borderRadius?: string;
+ borderColor?: string;
+ isFirst: boolean;
+ isLast: boolean;
+ loading: boolean;
+}>`
+ border-radius: ${(props) => props.borderRadius || "0"};
+ padding: ${(props) =>
+ `${(props.borderWidth || 1) / (props.isFirst ? 1 : 2)}px`};
+ background: ${(props) =>
+ props.borderColor
+ ? props.borderColor
+ : `repeating-linear-gradient(
+ 101.79deg,
+ #1BBE84 0%,
+ #331BBE 16%,
+ #BE1B55 33%,
+ #A6BE1B 55%,
+ #BE1B55 67%,
+ #331BBE 85%,
+ #1BBE84 99%
+ )`};
+ animation: ${(props) => (props.loading ? gradient : "")} 6s linear infinite;
+ background-size: 200% 200%;
`;
-const TextArea = styled.textarea`
- margin: 8px;
- margin-right: 22px;
- padding: 8px;
- white-space: pre-wrap;
- word-wrap: break-word;
- font-family: "Lexend", sans-serif;
+const StyledDiv = styled.div<{ editing: boolean }>`
font-size: 13px;
- width: 100%;
+ font-family: inherit;
border-radius: ${defaultBorderRadius};
- height: 100%;
- border: none;
- background-color: ${vscBackground};
- resize: none;
- outline: none;
- border: none;
+ height: auto;
+ background-color: ${secondaryDark};
color: ${vscForeground};
+ align-items: center;
+ position: relative;
+ z-index: 1;
+ overflow: hidden;
+ display: grid;
+ grid-template-columns: auto 1fr;
- &:focus {
- border: none;
- outline: none;
- }
+ outline: ${(props) => (props.editing ? `1px solid ${lightGray}` : "none")};
+ cursor: text;
+`;
+
+const DeleteButtonDiv = styled.div`
+ position: absolute;
+ top: 8px;
+ right: 8px;
+ background-color: ${secondaryDark};
+ box-shadow: 2px 2px 10px ${secondaryDark};
+ border-radius: ${defaultBorderRadius};
+`;
+
+const GridDiv = styled.div`
+ display: grid;
+ grid-template-columns: auto 1fr;
+ grid-gap: 8px;
+ align-items: center;
`;
function stringWithEllipsis(str: string, maxLen: number) {
@@ -84,108 +136,194 @@ const UserInputContainer = (props: UserInputContainerProps) => {
const [isHovered, setIsHovered] = useState(false);
const [isEditing, setIsEditing] = useState(false);
- const textAreaRef = useRef<HTMLTextAreaElement>(null);
+ const divRef = useRef<HTMLDivElement>(null);
const client = useContext(GUIClientContext);
+ const [prevContent, setPrevContent] = useState("");
+
+ const history = useSelector((state: RootStore) => state.serverState.history);
+
useEffect(() => {
- if (isEditing && textAreaRef.current) {
- textAreaRef.current.focus();
- // Select all text
- textAreaRef.current.setSelectionRange(
- 0,
- textAreaRef.current.value.length
- );
- // Change the size to match the contents (up to a max)
- textAreaRef.current.style.height = "auto";
- textAreaRef.current.style.height =
- (textAreaRef.current.scrollHeight > 500
- ? 500
- : textAreaRef.current.scrollHeight) + "px";
+ if (isEditing && divRef.current) {
+ setPrevContent(divRef.current.innerText);
+ divRef.current.focus();
+
+ if (divRef.current.innerText !== "") {
+ const range = document.createRange();
+ const sel = window.getSelection();
+ range.setStart(divRef.current, 0);
+ range.setEnd(divRef.current, 1);
+ sel?.removeAllRanges();
+ sel?.addRange(range);
+ }
}
- }, [isEditing]);
+ }, [isEditing, divRef.current]);
+
+ const onBlur = useCallback(() => {
+ setIsEditing(false);
+ if (divRef.current) {
+ divRef.current.innerText = prevContent;
+ divRef.current.blur();
+ }
+ }, [divRef.current]);
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === "Escape") {
- setIsEditing(false);
+ onBlur();
}
};
- document.addEventListener("keydown", handleKeyDown);
+ divRef.current?.addEventListener("keydown", handleKeyDown);
return () => {
- document.removeEventListener("keydown", handleKeyDown);
+ divRef.current?.removeEventListener("keydown", handleKeyDown);
};
- }, []);
+ }, [prevContent, divRef.current, isEditing, onBlur]);
const doneEditing = (e: any) => {
- if (!textAreaRef.current?.value) {
+ if (!divRef.current?.innerText) {
return;
}
- client?.editStepAtIndex(textAreaRef.current.value, props.index);
+ setPrevContent(divRef.current.innerText);
+ client?.editStepAtIndex(divRef.current.innerText, props.index);
setIsEditing(false);
e.stopPropagation();
+ divRef.current?.blur();
};
return (
- <StyledDiv
- onMouseEnter={() => {
- setIsHovered(true);
- }}
- onMouseLeave={() => {
- setIsHovered(false);
- }}
+ <GradientBorder
+ loading={props.active}
+ isFirst={false}
+ isLast={false}
+ borderColor={props.active ? undefined : vscBackground}
+ borderRadius={defaultBorderRadius}
>
- {isEditing ? (
- <TextArea
- ref={textAreaRef}
- onKeyDown={(e) => {
- if (e.key === "Enter" && !e.shiftKey) {
- e.preventDefault();
- doneEditing(e);
+ <StyledDiv
+ editing={isEditing}
+ onMouseEnter={() => {
+ setIsHovered(true);
+ }}
+ onMouseLeave={() => {
+ setIsHovered(false);
+ }}
+ onClick={() => {
+ setIsEditing(true);
+ }}
+ >
+ <GridDiv>
+ <ToggleDiv
+ onClick={
+ props.isToggleOpen
+ ? (e) => {
+ e.stopPropagation();
+ if (isMetaEquivalentKeyPressed(e)) {
+ props.onToggleAll(false);
+ } else {
+ props.onToggle(false);
+ }
+ }
+ : (e) => {
+ e.stopPropagation();
+ if (isMetaEquivalentKeyPressed(e)) {
+ props.onToggleAll(true);
+ } else {
+ props.onToggle(true);
+ }
+ }
}
- }}
- defaultValue={props.children}
- onBlur={() => {
- setIsEditing(false);
- }}
- />
- ) : (
- <StyledPre
- onClick={() => {
- setIsEditing(true);
- }}
- className="mr-6 cursor-text w-full"
- >
- {stringWithEllipsis(props.children, 600)}
- </StyledPre>
- )}
- {/* <ReactMarkdown children={props.children} className="w-fit mr-10" /> */}
- <DeleteButtonDiv>
- {(isHovered || isEditing) && (
- <div className="flex">
- {isEditing ? (
- <HeaderButtonWithText
- onClick={(e) => {
- doneEditing(e);
- }}
- text="Done"
- >
- <CheckIcon width="1.4em" height="1.4em" />
- </HeaderButtonWithText>
+ >
+ {props.isToggleOpen ? (
+ <ChevronDownIcon width="1.4em" height="1.4em" />
) : (
- <HeaderButtonWithText
- onClick={(e) => {
- props.onDelete();
- e.stopPropagation();
- }}
- text="Delete"
- >
- <XMarkIcon width="1.4em" height="1.4em" />
- </HeaderButtonWithText>
+ <ChevronRightIcon width="1.4em" height="1.4em" />
)}
+ </ToggleDiv>
+ <div
+ style={{
+ padding: "8px",
+ paddingTop: "4px",
+ paddingBottom: "4px",
+ }}
+ >
+ <div
+ ref={divRef}
+ onBlur={() => {
+ onBlur();
+ }}
+ onKeyDown={(e) => {
+ if (e.key === "Enter" && !e.shiftKey) {
+ e.preventDefault();
+ doneEditing(e);
+ }
+ }}
+ contentEditable={true}
+ suppressContentEditableWarning={true}
+ className="mr-6 ml-1 cursor-text w-full py-2 flex items-center content-center outline-none"
+ >
+ {isEditing
+ ? props.children
+ : stringWithEllipsis(props.children, 600)}
+ </div>
+ <DeleteButtonDiv>
+ {(isHovered || isEditing) && (
+ <div className="flex">
+ {isEditing ? (
+ <HeaderButtonWithText
+ onClick={(e) => {
+ doneEditing(e);
+ }}
+ text="Done"
+ >
+ <CheckIcon width="1.4em" height="1.4em" />
+ </HeaderButtonWithText>
+ ) : (
+ <>
+ {history.timeline
+ .filter(
+ (h, i: number) =>
+ props.groupIndices.includes(i) && h.logs
+ )
+ .some((h) => h.logs!.length > 0) && (
+ <HeaderButtonWithText
+ onClick={(e) => {
+ e.stopPropagation();
+ client?.showLogsAtIndex(props.groupIndices[1]);
+ }}
+ text="Context Used"
+ >
+ <MagnifyingGlassIcon width="1.4em" height="1.4em" />
+ </HeaderButtonWithText>
+ )}
+ <HeaderButtonWithText
+ onClick={(e) => {
+ e.stopPropagation();
+ if (props.active) {
+ client?.deleteAtIndex(props.groupIndices[1]);
+ } else {
+ props.onDelete();
+ }
+ }}
+ text={
+ props.active
+ ? `Stop (${getMetaKeyLabel()}⌫)`
+ : "Delete"
+ }
+ >
+ {props.active ? (
+ <StopCircleIcon width="1.4em" height="1.4em" />
+ ) : (
+ <XMarkIcon width="1.4em" height="1.4em" />
+ )}
+ </HeaderButtonWithText>
+ </>
+ )}
+ </div>
+ )}
+ </DeleteButtonDiv>
</div>
- )}
- </DeleteButtonDiv>
- </StyledDiv>
+ </GridDiv>
+ </StyledDiv>
+ </GradientBorder>
);
};
export default UserInputContainer;