diff options
Diffstat (limited to 'extension/react-app/src/components')
6 files changed, 286 insertions, 200 deletions
diff --git a/extension/react-app/src/components/CodeBlock.tsx b/extension/react-app/src/components/CodeBlock.tsx index 1624b986..17f5626b 100644 --- a/extension/react-app/src/components/CodeBlock.tsx +++ b/extension/react-app/src/components/CodeBlock.tsx @@ -61,7 +61,7 @@ function CopyButton(props: { textToCopy: string; visible: boolean }) { ); } -function CodeBlock(props: { children: React.ReactNode }) { +function CodeBlock(props: { children: string; showCopy?: boolean }) { const [result, setResult] = useState<AutoHighlightResult | undefined>( undefined ); @@ -78,39 +78,36 @@ function CodeBlock(props: { children: React.ReactNode }) { setHighlightTimeout( setTimeout(() => { - const result = hljs.highlightAuto( - (props.children as any).props.children[0], - [ - "python", - "javascript", - "typescript", - "bash", - "html", - "css", - "json", - "yaml", - "markdown", - "sql", - "java", - "c", - "cpp", - "csharp", - "go", - "kotlin", - "php", - "ruby", - "rust", - "scala", - "swift", - "dart", - "haskell", - "perl", - "r", - "julia", - "objectivec", - "ocaml", - ] - ); + const result = hljs.highlightAuto(props.children, [ + "python", + "javascript", + "typescript", + "bash", + "html", + "css", + "json", + "yaml", + "markdown", + "sql", + "java", + "c", + "cpp", + "csharp", + "go", + "kotlin", + "php", + "ruby", + "rust", + "scala", + "swift", + "dart", + "haskell", + "perl", + "r", + "julia", + "objectivec", + "ocaml", + ]); setResult(result); setHighlightTimeout(undefined); }, 100) @@ -129,13 +126,11 @@ function CodeBlock(props: { children: React.ReactNode }) { > <CopyButtonDiv> <CopyButton - visible={hovered} - textToCopy={(props.children as any).props.children[0]} + visible={hovered && (props.showCopy || true)} + textToCopy={props.children} /> </CopyButtonDiv> - <StyledCode language={result?.language}> - {(props.children as any).props.children[0]} - </StyledCode> + <StyledCode language={result?.language}>{props.children}</StyledCode> </StyledPre> ); } diff --git a/extension/react-app/src/components/ComboBox.tsx b/extension/react-app/src/components/ComboBox.tsx index 3816cee8..f299c3a2 100644 --- a/extension/react-app/src/components/ComboBox.tsx +++ b/extension/react-app/src/components/ComboBox.tsx @@ -1,4 +1,4 @@ -import React, { useCallback } from "react"; +import React, { useCallback, useEffect } from "react"; import { useCombobox } from "downshift"; import styled from "styled-components"; import { @@ -7,8 +7,27 @@ import { secondaryDark, vscBackground, } from "."; +import CodeBlock from "./CodeBlock"; +import { RangeInFile } from "../../../src/client"; +import PillButton from "./PillButton"; const mainInputFontSize = 16; + +const ContextDropdown = styled.div` + position: absolute; + padding: 4px; + width: calc(100% - 16px - 8px); + background-color: ${secondaryDark}; + color: white; + border-bottom-right-radius: ${defaultBorderRadius}; + border-bottom-left-radius: ${defaultBorderRadius}; + /* border: 1px solid white; */ + border-top: none; + margin: 8px; + outline: 1px solid orange; + z-index: 5; +`; + const MainTextInput = styled.textarea` resize: none; @@ -20,6 +39,7 @@ const MainTextInput = styled.textarea` width: 100%; background-color: ${vscBackground}; color: white; + z-index: 1; &:focus { border: 1px solid transparent; @@ -49,6 +69,7 @@ const Ul = styled.ul<{ border-radius: ${defaultBorderRadius}; overflow: hidden; border: 0.5px solid gray; + z-index: 2; `; const Li = styled.li<{ @@ -71,6 +92,8 @@ interface ComboBoxProps { onInputValueChange: (inputValue: string) => void; disabled?: boolean; onEnter?: (e: React.KeyboardEvent<HTMLInputElement>) => void; + highlightedCodeSections?: (RangeInFile & { contents: string })[]; + deleteContextItem?: (idx: number) => void; } const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => { @@ -78,6 +101,26 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => { // The position of the current command you are typing now, so the one that will be appended to history once you press enter const [positionInHistory, setPositionInHistory] = React.useState<number>(0); const [items, setItems] = React.useState(props.items); + const [hoveringButton, setHoveringButton] = React.useState(false); + const [hoveringContextDropdown, setHoveringContextDropdown] = + React.useState(false); + const [highlightedCodeSections, setHighlightedCodeSections] = React.useState( + props.highlightedCodeSections || [ + { + filepath: "test.ts", + range: { + start: { line: 0, character: 0 }, + end: { line: 0, character: 0 }, + }, + contents: "import * as a from 'a';", + }, + ] + ); + + useEffect(() => { + setHighlightedCodeSections(props.highlightedCodeSections || []); + }, [props.highlightedCodeSections]); + const { isOpen, getToggleButtonProps, @@ -111,90 +154,144 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => { }; return ( - <div className="flex px-2" ref={divRef} hidden={!isOpen}> - <MainTextInput - disabled={props.disabled} - placeholder="Type '/' to see available slash commands." - {...getInputProps({ - onChange: (e) => { - const target = e.target as HTMLTextAreaElement; - // Update the height of the textarea to match the content, up to a max of 200px. - target.style.height = "auto"; - target.style.height = `${Math.min( - target.scrollHeight, - 300 - ).toString()}px`; - }, - onKeyDown: (event) => { - if (event.key === "Enter" && event.shiftKey) { - // Prevent Downshift's default 'Enter' behavior. - (event.nativeEvent as any).preventDownshiftDefault = true; - } else if ( - event.key === "Enter" && - (!isOpen || items.length === 0) - ) { - // Prevent Downshift's default 'Enter' behavior. - (event.nativeEvent as any).preventDownshiftDefault = true; - if (props.onEnter) props.onEnter(event); - setInputValue(""); - const value = event.currentTarget.value; - if (value !== "") { - setPositionInHistory(history.length + 1); - setHistory([...history, value]); + <> + <div className="flex px-2" ref={divRef} hidden={!isOpen}> + <MainTextInput + disabled={props.disabled} + placeholder="Type '/' to see available slash commands." + {...getInputProps({ + onChange: (e) => { + const target = e.target as HTMLTextAreaElement; + // Update the height of the textarea to match the content, up to a max of 200px. + target.style.height = "auto"; + target.style.height = `${Math.min( + target.scrollHeight, + 300 + ).toString()}px`; + + // setShowContextDropdown(target.value.endsWith("@")); + }, + onKeyDown: (event) => { + if (event.key === "Enter" && event.shiftKey) { + // Prevent Downshift's default 'Enter' behavior. + (event.nativeEvent as any).preventDownshiftDefault = true; + } else if ( + event.key === "Enter" && + (!isOpen || items.length === 0) + ) { + // Prevent Downshift's default 'Enter' behavior. + (event.nativeEvent as any).preventDownshiftDefault = true; + if (props.onEnter) props.onEnter(event); + setInputValue(""); + const value = event.currentTarget.value; + if (value !== "") { + setPositionInHistory(history.length + 1); + setHistory([...history, value]); + } + } else if (event.key === "Tab" && items.length > 0) { + setInputValue(items[0].name); + event.preventDefault(); + } else if ( + event.key === "ArrowUp" || + (event.key === "ArrowDown" && + event.currentTarget.value.split("\n").length > 1) + ) { + (event.nativeEvent as any).preventDownshiftDefault = true; + } else if ( + event.key === "ArrowUp" && + event.currentTarget.value.split("\n").length > 1 + ) { + if (positionInHistory == 0) return; + setInputValue(history[positionInHistory - 1]); + setPositionInHistory((prev) => prev - 1); + } else if ( + event.key === "ArrowDown" && + event.currentTarget.value.split("\n").length > 1 + ) { + if (positionInHistory < history.length - 1) { + setInputValue(history[positionInHistory + 1]); + } + setPositionInHistory((prev) => + Math.min(prev + 1, history.length) + ); + } + }, + ref: ref as any, + })} + /> + <Ul + {...getMenuProps({ + ref: ulRef, + })} + showAbove={showAbove()} + ulHeightPixels={ulRef.current?.getBoundingClientRect().height || 0} + > + {isOpen && + items.map((item, index) => ( + <Li + key={`${item.name}${index}`} + {...getItemProps({ item, index })} + highlighted={highlightedIndex === index} + selected={selectedItem === item} + > + <span> + {item.name}: {item.description} + </span> + </Li> + ))} + </Ul> + </div> + <div className="px-2 flex gap-2 items-center flex-wrap"> + {highlightedCodeSections.map((section, idx) => ( + <PillButton + title={section.filepath} + onDelete={() => { + if (props.deleteContextItem) { + props.deleteContextItem(idx); } - } else if (event.key === "Tab" && items.length > 0) { - setInputValue(items[0].name); - event.preventDefault(); - } else if ( - event.key === "ArrowUp" || - (event.key === "ArrowDown" && - event.currentTarget.value.split("\n").length > 1) - ) { - (event.nativeEvent as any).preventDownshiftDefault = true; - } else if ( - event.key === "ArrowUp" && - event.currentTarget.value.split("\n").length > 1 - ) { - if (positionInHistory == 0) return; - setInputValue(history[positionInHistory - 1]); - setPositionInHistory((prev) => prev - 1); - } else if ( - event.key === "ArrowDown" && - event.currentTarget.value.split("\n").length > 1 - ) { - if (positionInHistory < history.length - 1) { - setInputValue(history[positionInHistory + 1]); + setHighlightedCodeSections((prev) => { + const newSections = [...prev]; + newSections.splice(idx, 1); + return newSections; + }); + }} + onHover={(val: boolean) => { + if (val) { + setHoveringButton(val); + } else { + setTimeout(() => { + setHoveringButton(val); + }, 100); } - setPositionInHistory((prev) => - Math.min(prev + 1, history.length) - ); - } - }, - ref: ref as any, - })} - /> - <Ul - {...getMenuProps({ - ref: ulRef, - })} - showAbove={showAbove()} - ulHeightPixels={ulRef.current?.getBoundingClientRect().height || 0} + }} + /> + ))} + + <span className="text-trueGray-400 ml-auto mr-4 text-xs"> + Highlight code to include as context.{" "} + {highlightedCodeSections.length > 0 && + "Otherwise using entire currently open file."} + </span> + </div> + <ContextDropdown + onMouseEnter={() => { + setHoveringContextDropdown(true); + }} + onMouseLeave={() => { + setHoveringContextDropdown(false); + }} + hidden={!hoveringContextDropdown && !hoveringButton} > - {isOpen && - items.map((item, index) => ( - <Li - key={`${item.name}${index}`} - {...getItemProps({ item, index })} - highlighted={highlightedIndex === index} - selected={selectedItem === item} - > - <span> - {item.name}: {item.description} - </span> - </Li> - ))} - </Ul> - </div> + {highlightedCodeSections.map((section, idx) => ( + <> + <p>{section.filepath}</p> + <CodeBlock showCopy={false} key={idx}> + {section.contents} + </CodeBlock> + </> + ))} + </ContextDropdown> + </> ); }); diff --git a/extension/react-app/src/components/ContinueButton.tsx b/extension/react-app/src/components/ContinueButton.tsx index ef6719b7..5295799a 100644 --- a/extension/react-app/src/components/ContinueButton.tsx +++ b/extension/react-app/src/components/ContinueButton.tsx @@ -6,6 +6,7 @@ import { RootStore } from "../redux/store"; let StyledButton = styled(Button)` margin: auto; + margin-top: 8px; display: grid; grid-template-columns: 30px 1fr; align-items: center; diff --git a/extension/react-app/src/components/IterationContainer.tsx b/extension/react-app/src/components/IterationContainer.tsx deleted file mode 100644 index a0053519..00000000 --- a/extension/react-app/src/components/IterationContainer.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import { useState } from "react"; -import styled from "styled-components"; -import { - defaultBorderRadius, - MainContainerWithBorder, - secondaryDark, - vscBackground, -} from "."; -import { RangeInFile, FileEdit } from "../../../src/client"; -import CodeBlock from "./CodeBlock"; -import SubContainer from "./SubContainer"; - -import { ChevronDown, ChevronRight } from "@styled-icons/heroicons-outline"; - -export interface IterationContext { - codeSelections: RangeInFile[]; - instruction: string; - suggestedChanges: FileEdit[]; - status: "waiting" | "accepted" | "rejected"; - summary?: string; - action: string; - error?: string; -} - -interface IterationContainerProps { - iterationContext: IterationContext; -} - -const IterationContainerDiv = styled.div<{ open: boolean }>` - background-color: ${(props) => (props.open ? vscBackground : secondaryDark)}; - border-radius: ${defaultBorderRadius}; - padding: ${(props) => (props.open ? "2px" : "8px")}; -`; - -function IterationContainer(props: IterationContainerProps) { - const [open, setOpen] = useState(false); - - return ( - <MainContainerWithBorder className="m-2 overflow-hidden"> - <IterationContainerDiv open={open}> - <p - className="m-2 cursor-pointer" - onClick={() => setOpen((prev) => !prev)} - > - {open ? <ChevronDown size="1.4em" /> : <ChevronRight size="1.4em" />} - {props.iterationContext.summary || - props.iterationContext.codeSelections - .map((cs) => cs.filepath) - .join("\n")} - </p> - - {open && ( - <> - <SubContainer title="Action"> - {props.iterationContext.action} - </SubContainer> - {props.iterationContext.error && ( - <SubContainer title="Error"> - <CodeBlock>{props.iterationContext.error}</CodeBlock> - </SubContainer> - )} - {props.iterationContext.suggestedChanges.map((sc) => { - return ( - <SubContainer title="Suggested Change"> - {sc.filepath} - <CodeBlock>{sc.replacement}</CodeBlock> - </SubContainer> - ); - })} - </> - )} - </IterationContainerDiv> - </MainContainerWithBorder> - ); -} - -export default IterationContainer; diff --git a/extension/react-app/src/components/PillButton.tsx b/extension/react-app/src/components/PillButton.tsx new file mode 100644 index 00000000..33451db5 --- /dev/null +++ b/extension/react-app/src/components/PillButton.tsx @@ -0,0 +1,66 @@ +import { useState } from "react"; +import styled from "styled-components"; +import { defaultBorderRadius } from "."; + +const Button = styled.button` + border: none; + color: white; + background-color: transparent; + border: 1px solid white; + border-radius: ${defaultBorderRadius}; + padding: 3px 6px; + + &:hover { + background-color: white; + color: black; + } +`; + +interface PillButtonProps { + onHover?: (arg0: boolean) => void; + onDelete?: () => void; + title: string; +} + +const PillButton = (props: PillButtonProps) => { + const [isHovered, setIsHovered] = useState(false); + return ( + <Button + onMouseEnter={() => { + setIsHovered(true); + if (props.onHover) { + props.onHover(true); + } + }} + onMouseLeave={() => { + setIsHovered(false); + if (props.onHover) { + props.onHover(false); + } + }} + > + <div + style={{ display: "grid", gridTemplateColumns: "1fr auto", gap: "4px" }} + > + <span>{props.title}</span> + <span + style={{ + cursor: "pointer", + color: "red", + borderLeft: "1px solid black", + paddingLeft: "4px", + }} + hidden={!isHovered} + onClick={() => { + props.onDelete?.(); + props.onHover?.(false); + }} + > + X + </span> + </div> + </Button> + ); +}; + +export default PillButton; diff --git a/extension/react-app/src/components/StepContainer.tsx b/extension/react-app/src/components/StepContainer.tsx index 02c04548..cb83f20a 100644 --- a/extension/react-app/src/components/StepContainer.tsx +++ b/extension/react-app/src/components/StepContainer.tsx @@ -246,10 +246,14 @@ function StepContainer(props: StepContainerProps) { className="overflow-x-scroll" components={{ pre: ({ node, ...props }) => { - return <CodeBlock children={props.children[0]} />; + return ( + <CodeBlock + children={(props.children[0] as any).props.children[0]} + /> + ); }, code: ({ node, ...props }) => { - return <StyledCode children={props.children[0]} />; + return <StyledCode children={props.children[0] as any} />; }, }} > |