From f9a84bd2d65b3142cbcfcdd8e1e9394c9d4b458e Mon Sep 17 00:00:00 2001 From: Nate Sesti Date: Sun, 24 Sep 2023 01:00:42 -0700 Subject: feat: :lipstick: more ui improvements --- .../src/components/HeaderButtonWithText.tsx | 8 +- extension/react-app/src/components/Layout.tsx | 6 +- extension/react-app/src/components/PillButton.tsx | 1 + extension/react-app/src/components/Suggestions.tsx | 30 ++--- .../src/components/UserInputContainer.tsx | 35 +++++- extension/react-app/src/components/index.ts | 2 +- extension/react-app/src/pages/gui.tsx | 19 +++- extension/react-app/src/pages/history.tsx | 124 ++++++++++++++------- extension/react-app/src/pages/models.tsx | 1 + .../src/redux/slices/serverStateReducer.ts | 4 +- 10 files changed, 159 insertions(+), 71 deletions(-) (limited to 'extension/react-app/src') diff --git a/extension/react-app/src/components/HeaderButtonWithText.tsx b/extension/react-app/src/components/HeaderButtonWithText.tsx index ca359250..84e6118c 100644 --- a/extension/react-app/src/components/HeaderButtonWithText.tsx +++ b/extension/react-app/src/components/HeaderButtonWithText.tsx @@ -13,7 +13,10 @@ interface HeaderButtonWithTextProps { onKeyDown?: (e: any) => void; } -const HeaderButtonWithText = (props: HeaderButtonWithTextProps) => { +const HeaderButtonWithText = React.forwardRef< + HTMLButtonElement, + HeaderButtonWithTextProps +>((props: HeaderButtonWithTextProps, ref) => { const [hover, setHover] = useState(false); const tooltipPortalDiv = document.getElementById("tooltip-portal-div"); @@ -35,6 +38,7 @@ const HeaderButtonWithText = (props: HeaderButtonWithTextProps) => { onClick={props.onClick} onKeyDown={props.onKeyDown} className={props.className} + ref={ref} > {props.children} @@ -47,6 +51,6 @@ const HeaderButtonWithText = (props: HeaderButtonWithTextProps) => { )} ); -}; +}); export default HeaderButtonWithText; diff --git a/extension/react-app/src/components/Layout.tsx b/extension/react-app/src/components/Layout.tsx index 9ec2e671..6dbd348f 100644 --- a/extension/react-app/src/components/Layout.tsx +++ b/extension/react-app/src/components/Layout.tsx @@ -2,7 +2,7 @@ import styled from "styled-components"; import { defaultBorderRadius, secondaryDark, vscForeground } from "."; import { Outlet } from "react-router-dom"; import TextDialog from "./TextDialog"; -import { useContext, useEffect, useState } from "react"; +import { useContext, useEffect } from "react"; import { GUIClientContext } from "../App"; import { useDispatch, useSelector } from "react-redux"; import { RootStore } from "../redux/store"; @@ -12,8 +12,6 @@ import { setShowDialog, } from "../redux/slices/uiStateSlice"; import { - PlusIcon, - FolderIcon, SparklesIcon, Cog6ToothIcon, QuestionMarkCircleIcon, @@ -22,6 +20,7 @@ import HeaderButtonWithText from "./HeaderButtonWithText"; import { useNavigate, useLocation } from "react-router-dom"; import ModelSelect from "./ModelSelect"; import ProgressBar from "./ProgressBar"; +import { temporarilyClearSession } from "../redux/slices/serverStateReducer"; // #region Styled Components const FOOTER_HEIGHT = "1.8em"; @@ -112,6 +111,7 @@ const Layout = () => { event.code === "KeyN" && timeline.filter((n) => !n.step.hide).length > 0 ) { + dispatch(temporarilyClearSession(false)); client?.loadSession(undefined); } if ((event.metaKey || event.ctrlKey) && event.code === "KeyC") { diff --git a/extension/react-app/src/components/PillButton.tsx b/extension/react-app/src/components/PillButton.tsx index 4b602619..4e13428d 100644 --- a/extension/react-app/src/components/PillButton.tsx +++ b/extension/react-app/src/components/PillButton.tsx @@ -196,6 +196,7 @@ const PillButton = (props: PillButtonProps) => { <> Ask a question

, -
    -
  1. Highlight code in the editor
  2. -
  3. Press cmd+M to select the code
  4. -
  5. Ask a question
  6. -
, -
    -
  1. Highlight code in the editor
  2. -
  3. Press cmd+shift+M to select the code
  4. -
  5. Request and edit
  6. -
, +

+ 1. Highlight code in the editor +
+ 2. Press cmd+M to select the code +
+ 3. Ask a question +

, +

+ 1. Highlight code in the editor +
+ 2. Press cmd+shift+M to select the code +
+ 3. Request an edit +

, ]; const suggestionsStages: any[][] = [ @@ -178,7 +182,7 @@ function SuggestionsArea(props: { onClick: (textInput: string) => void }) { const inputs = timeline.filter( (node) => !node.step.hide && node.step.name === "User Input" ); - return inputs.length - numTutorialInputs === 0; + return inputs.length - numTutorialInputs <= 0; }, [timeline, numTutorialInputs]); return ( @@ -187,9 +191,9 @@ function SuggestionsArea(props: { onClick: (textInput: string) => void }) {
- Tutorial + Tutorial ({stage + 1}/3)
-

+

{stage < suggestionsStages.length && suggestionsStages[stage][0]?.title}

diff --git a/extension/react-app/src/components/UserInputContainer.tsx b/extension/react-app/src/components/UserInputContainer.tsx index 76a3c615..15f1752f 100644 --- a/extension/react-app/src/components/UserInputContainer.tsx +++ b/extension/react-app/src/components/UserInputContainer.tsx @@ -103,7 +103,7 @@ const StyledDiv = styled.div<{ editing: boolean }>` z-index: 1; overflow: hidden; display: grid; - grid-template-columns: auto 1fr; + grid-template-columns: 1fr auto; outline: ${(props) => (props.editing ? `1px solid ${lightGray}` : "none")}; cursor: text; @@ -114,7 +114,7 @@ const DeleteButtonDiv = styled.div` top: 8px; right: 8px; background-color: ${secondaryDark}; - box-shadow: 2px 2px 10px ${secondaryDark}; + box-shadow: 4px 4px 10px ${secondaryDark}; border-radius: ${defaultBorderRadius}; `; @@ -123,6 +123,7 @@ const GridDiv = styled.div` grid-template-columns: auto 1fr; grid-gap: 8px; align-items: center; + width: 100%; `; function stringWithEllipsis(str: string, maxLen: number) { @@ -165,7 +166,7 @@ const UserInputContainer = (props: UserInputContainerProps) => { divRef.current.innerText = prevContent; divRef.current.blur(); } - }, [divRef.current]); + }, [divRef.current, prevContent]); useEffect(() => { const handleKeyDown = (event: KeyboardEvent) => { @@ -190,6 +191,10 @@ const UserInputContainer = (props: UserInputContainerProps) => { divRef.current?.blur(); }; + const [divTextContent, setDivTextContent] = useState(""); + + const checkButtonRef = useRef(null); + return ( { padding: "8px", paddingTop: "4px", paddingBottom: "4px", + width: "100%", }} >
{ - onBlur(); + onBlur={(e) => { + if (e.relatedTarget !== checkButtonRef.current) onBlur(); }} onKeyDown={(e) => { if (e.key === "Enter" && !e.shiftKey) { @@ -256,6 +262,9 @@ const UserInputContainer = (props: UserInputContainerProps) => { doneEditing(e); } }} + onInput={(e) => { + setDivTextContent((e.target as any).innerText); + }} contentEditable={true} suppressContentEditableWarning={true} className="mr-6 ml-1 cursor-text w-full py-2 flex items-center content-center outline-none" @@ -271,10 +280,24 @@ const UserInputContainer = (props: UserInputContainerProps) => { { doneEditing(e); + e.stopPropagation(); }} text="Done" + disabled={ + divTextContent === "" || divTextContent === prevContent + } + ref={checkButtonRef} > - + ) : ( <> diff --git a/extension/react-app/src/components/index.ts b/extension/react-app/src/components/index.ts index 6f5a2f37..510740f8 100644 --- a/extension/react-app/src/components/index.ts +++ b/extension/react-app/src/components/index.ts @@ -178,7 +178,7 @@ export const HeaderButton = styled.button<{ inverted: boolean | undefined }>` border: none; border-radius: ${defaultBorderRadius}; - cursor: pointer; + cursor: ${({ disabled }) => (disabled ? "default" : "pointer")}; &:hover { background-color: ${({ inverted }) => diff --git a/extension/react-app/src/pages/gui.tsx b/extension/react-app/src/pages/gui.tsx index 78b7a970..f6a09bbc 100644 --- a/extension/react-app/src/pages/gui.tsx +++ b/extension/react-app/src/pages/gui.tsx @@ -29,6 +29,7 @@ import { import RingLoader from "../components/RingLoader"; import { setServerState, + temporarilyClearSession, temporarilyPushToUserInputQueue, } from "../redux/slices/serverStateReducer"; import TimelineItem from "../components/TimelineItem"; @@ -68,7 +69,7 @@ const TitleTextInput = styled(TextInput)` padding-bottom: 6px; &:focus { - outline: 1px solid ${lightGray}; + outline: none; } `; @@ -86,11 +87,11 @@ const StepsDiv = styled.div` &::before { content: ""; position: absolute; - height: calc(100% - 24px); + height: calc(100% - 12px); border-left: 2px solid ${lightGray}; left: 28px; z-index: 0; - bottom: 24px; + bottom: 12px; } `; @@ -517,20 +518,30 @@ function GUI(props: GUIProps) { value={sessionTitleInput} onChange={(e) => setSessionTitleInput(e.target.value)} onBlur={(e) => { + if ( + e.target.value === sessionTitle || + (typeof sessionTitle === "undefined" && + e.target.value === "New Session") + ) + return; client?.setCurrentSessionTitle(e.target.value); }} onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault(); (e.target as any).blur(); + } else if (e.key === "Escape") { + setSessionTitleInput(sessionTitle || "New Session"); + (e.target as any).blur(); } }} /> -
+
{history.timeline.filter((n) => !n.step.hide).length > 0 && ( { if (history.timeline.filter((n) => !n.step.hide).length > 0) { + dispatch(temporarilyClearSession(false)); client?.loadSession(undefined); } }} diff --git a/extension/react-app/src/pages/history.tsx b/extension/react-app/src/pages/history.tsx index b6de0520..b4f80d70 100644 --- a/extension/react-app/src/pages/history.tsx +++ b/extension/react-app/src/pages/history.tsx @@ -26,6 +26,17 @@ const parseDate = (date: string): Date => { return dateObj; }; +const SectionHeader = styled.tr` + padding: 4px; + padding-left: 16px; + padding-right: 16px; + background-color: ${secondaryDark}; + width: 100%; + font-weight: bold; + text-align: center; + margin: 0; +`; + const TdDiv = styled.div` cursor: pointer; padding-left: 1rem; @@ -44,6 +55,9 @@ function History() { const navigate = useNavigate(); const dispatch = useDispatch(); const [sessions, setSessions] = useState([]); + const [filteredAndSortedSessions, setFilteredAndSortedSessions] = useState< + SessionInfo[] + >([]); const client = useContext(GUIClientContext); const apiUrl = useSelector((state: RootStore) => state.config.apiUrl); const workspacePaths = useSelector( @@ -69,6 +83,32 @@ function History() { fetchSessions(); }, [client]); + useEffect(() => { + setFilteredAndSortedSessions( + 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() + ) + ); + }, [filteringByWorkspace, sessions]); + + const yesterday = new Date(Date.now() - 1000 * 60 * 60 * 24); + const lastWeek = new Date(Date.now() - 1000 * 60 * 60 * 24 * 7); + const lastMonth = new Date(Date.now() - 1000 * 60 * 60 * 24 * 30); + const earlier = new Date(0); + return (
@@ -116,52 +156,56 @@ function History() {
- {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) => ( - - + - - ))} + })} + {" | "} + {lastPartOfPath(session.workspace_directory || "")}/ + + + + + + ); + })}
- { - client?.loadSession(session.session_id); - dispatch(temporarilyClearSession()); - navigate("/"); - }} - > -
{session.title}
-
- {parseDate(session.date_created).toLocaleString( - "en-US", - { + {filteredAndSortedSessions.map((session, index) => { + const prevDate = + index > 0 + ? parseDate(filteredAndSortedSessions[index - 1].date_created) + : earlier; + const date = parseDate(session.date_created); + return ( + <> + {index === 0 && date > yesterday && ( + Today + )} + {date < yesterday && + date > lastWeek && + prevDate > yesterday && ( + This Week + )} + {date < lastWeek && + date > lastMonth && + prevDate > lastWeek && ( + This Month + )} + +
+ { + client?.loadSession(session.session_id); + dispatch(temporarilyClearSession(true)); + navigate("/"); + }} + > +
{session.title}
+
+ {date.toLocaleString("en-US", { year: "2-digit", month: "2-digit", day: "2-digit", hour: "numeric", minute: "2-digit", hour12: true, - } - )} - {" | "} - {lastPartOfPath(session.workspace_directory || "")}/ -
-
-

diff --git a/extension/react-app/src/pages/models.tsx b/extension/react-app/src/pages/models.tsx index 1a6f275b..ea8e28d6 100644 --- a/extension/react-app/src/pages/models.tsx +++ b/extension/react-app/src/pages/models.tsx @@ -145,6 +145,7 @@ function Models() { style={{ borderBottom: `0.5px solid ${lightGray}`, backgroundColor: vscBackground, + zIndex: 2, }} > { state.user_input_queue = [...state.user_input_queue, action.payload]; }, - temporarilyClearSession: (state) => { + temporarilyClearSession: (state, action) => { state.history.timeline = []; state.selected_context_items = []; state.session_info = { - title: "Loading session...", + title: action.payload ? "Loading session..." : "New Session", session_id: "", date_created: "", }; -- cgit v1.2.3-70-g09d2