From e976d60974a7837967d03807605cbf2e7b4f3f9a Mon Sep 17 00:00:00 2001 From: Nate Sesti <33237525+sestinj@users.noreply.github.com> Date: Sat, 23 Sep 2023 13:06:00 -0700 Subject: UI Redesign and fixing many details (#496) * feat: :lipstick: start of major design upgrade * feat: :lipstick: model selection page * feat: :lipstick: use shortcut to add highlighted code as ctx * feat: :lipstick: better display of errors * feat: :lipstick: ui for learning keyboard shortcuts, more details * refactor: :construction: testing slash commands ui * Truncate continue.log * refactor: :construction: refactoring client_session, ui, more * feat: :bug: layout fixes * refactor: :lipstick: ui to enter OpenAI Key * refactor: :truck: rename MaybeProxyOpenAI -> OpenAIFreeTrial * starting help center * removing old shortcut docs * fix: :bug: fix model setting logic to avoid overwrites * feat: :lipstick: tutorial and model descriptions * refactor: :truck: rename unused -> saved * refactor: :truck: rename model roles * feat: :lipstick: edit indicator * refactor: :lipstick: move +, folder icons * feat: :lipstick: tab to clear all context * fix: :bug: context providers ui fixes * fix: :bug: fix lag when stopping step * fix: :bug: don't override system message for models * fix: :bug: fix continue button cursor * feat: :lipstick: title bar * fix: :bug: updates to code highlighting logic and more * fix: :bug: fix renaming of summarize model role * feat: :lipstick: help page and better session title * feat: :lipstick: more help page / ui improvements * feat: :lipstick: set session title * fix: :bug: small fixes for changing sessions * fix: :bug: perfecting the highlighting code and ctx interactions * style: :lipstick: sticky headers for scroll, ollama warming * fix: :bug: fix toggle bug --------- Co-authored-by: Ty Dunn --- .../src/components/UserInputContainer.tsx | 376 ++++++++++++++------- 1 file changed, 257 insertions(+), 119 deletions(-) (limited to 'extension/react-app/src/components/UserInputContainer.tsx') 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(null); + const divRef = useRef(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 ( - { - setIsHovered(true); - }} - onMouseLeave={() => { - setIsHovered(false); - }} + - {isEditing ? ( -