summaryrefslogtreecommitdiff
path: root/extension/react-app/src/components
diff options
context:
space:
mode:
Diffstat (limited to 'extension/react-app/src/components')
-rw-r--r--extension/react-app/src/components/ComboBox.tsx176
-rw-r--r--extension/react-app/src/components/PillButton.tsx94
-rw-r--r--extension/react-app/src/components/StepContainer.tsx29
-rw-r--r--extension/react-app/src/components/StyledMarkdownPreview.tsx37
-rw-r--r--extension/react-app/src/components/TextDialog.tsx2
5 files changed, 214 insertions, 124 deletions
diff --git a/extension/react-app/src/components/ComboBox.tsx b/extension/react-app/src/components/ComboBox.tsx
index bf07cb93..4a1cdbc0 100644
--- a/extension/react-app/src/components/ComboBox.tsx
+++ b/extension/react-app/src/components/ComboBox.tsx
@@ -1,4 +1,9 @@
-import React, { useEffect, useImperativeHandle, useState } from "react";
+import React, {
+ useContext,
+ useEffect,
+ useImperativeHandle,
+ useState,
+} from "react";
import { useCombobox } from "downshift";
import styled from "styled-components";
import {
@@ -8,13 +13,17 @@ import {
vscBackground,
vscForeground,
} from ".";
-import CodeBlock from "./CodeBlock";
import PillButton from "./PillButton";
import HeaderButtonWithText from "./HeaderButtonWithText";
import { DocumentPlus } from "@styled-icons/heroicons-outline";
-import { HighlightedRangeContext } from "../../../schema/FullState";
+import { ContextItem } from "../../../schema/FullState";
import { postVscMessage } from "../vscode";
-import { getMetaKeyLabel } from "../util";
+import { GUIClientContext } from "../App";
+import { MeiliSearch } from "meilisearch";
+import { setBottomMessageCloseTimeout } from "../redux/slices/uiStateSlice";
+import { useDispatch } from "react-redux";
+
+const SEARCH_INDEX_NAME = "continue_context_items";
// #region styled components
const mainInputFontSize = 13;
@@ -64,6 +73,7 @@ const Ul = styled.ul<{
hidden: boolean;
showAbove: boolean;
ulHeightPixels: number;
+ inputBoxHeight?: string;
}>`
${(props) =>
props.showAbove
@@ -104,35 +114,79 @@ const Li = styled.li<{
// #endregion
interface ComboBoxProps {
- items: { name: string; description: string }[];
+ items: { name: string; description: string; id?: string }[];
onInputValueChange: (inputValue: string) => void;
disabled?: boolean;
onEnter: (e: React.KeyboardEvent<HTMLInputElement>) => void;
- highlightedCodeSections: HighlightedRangeContext[];
- deleteContextItems: (indices: number[]) => void;
- onTogglePin: () => void;
+ selectedContextItems: ContextItem[];
onToggleAddContext: () => void;
addingHighlightedCode: boolean;
}
const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
+ const searchClient = new MeiliSearch({ host: "http://127.0.0.1:7700" });
+ const client = useContext(GUIClientContext);
+ const dispatch = useDispatch();
+
const [history, setHistory] = React.useState<string[]>([]);
// 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 [highlightedCodeSections, setHighlightedCodeSections] = React.useState(
- props.highlightedCodeSections || []
- );
+
const inputRef = React.useRef<HTMLInputElement>(null);
+ const [inputBoxHeight, setInputBoxHeight] = useState<string | undefined>(
+ undefined
+ );
- useEffect(() => {
- setHighlightedCodeSections(props.highlightedCodeSections || []);
- }, [props.highlightedCodeSections]);
+ // Whether the current input follows an '@' and should be treated as context query
+ const [currentlyInContextQuery, setCurrentlyInContextQuery] = useState(false);
const { getInputProps, ...downshiftProps } = useCombobox({
- onInputValueChange({ inputValue }) {
+ onSelectedItemChange: ({ selectedItem }) => {
+ if (selectedItem?.id) {
+ // Get the query from the input value
+ const segs = downshiftProps.inputValue.split("@");
+ const query = segs[segs.length - 1];
+ const restOfInput = segs.splice(0, segs.length - 1).join("@");
+
+ // Tell server the context item was selected
+ client?.selectContextItem(selectedItem.id, query);
+
+ // Remove the '@' and the context query from the input
+ if (downshiftProps.inputValue.includes("@")) {
+ downshiftProps.setInputValue(restOfInput);
+ }
+ }
+ },
+ onInputValueChange({ inputValue, highlightedIndex }) {
if (!inputValue) return;
props.onInputValueChange(inputValue);
+
+ if (inputValue.endsWith("@") || currentlyInContextQuery) {
+ setCurrentlyInContextQuery(true);
+
+ const segs = inputValue.split("@");
+ const providerAndQuery = segs[segs.length - 1];
+ const [provider, query] = providerAndQuery.split(" ");
+ searchClient
+ .index(SEARCH_INDEX_NAME)
+ .search(providerAndQuery)
+ .then((res) => {
+ setItems(
+ res.hits.map((hit) => {
+ return {
+ name: hit.name,
+ description: hit.description,
+ id: hit.id,
+ };
+ })
+ );
+ })
+ .catch(() => {
+ // Swallow errors, because this simply is not supported on Windows at the moment
+ });
+ return;
+ }
setItems(
props.items.filter((item) =>
item.name.toLowerCase().startsWith(inputValue.toLowerCase())
@@ -145,6 +199,18 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
},
});
+ useEffect(() => {
+ if (downshiftProps.highlightedIndex < 0) {
+ downshiftProps.setHighlightedIndex(0);
+ }
+ }, [downshiftProps.inputValue]);
+
+ const divRef = React.useRef<HTMLDivElement>(null);
+ const ulRef = React.useRef<HTMLUListElement>(null);
+ const showAbove = () => {
+ return (divRef.current?.getBoundingClientRect().top || 0) > UlMaxHeight;
+ };
+
useImperativeHandle(ref, () => downshiftProps, [downshiftProps]);
const [metaKeyPressed, setMetaKeyPressed] = useState(false);
@@ -184,59 +250,25 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
};
}, [inputRef.current]);
- const divRef = React.useRef<HTMLDivElement>(null);
- const ulRef = React.useRef<HTMLUListElement>(null);
- const showAbove = () => {
- return (divRef.current?.getBoundingClientRect().top || 0) > UlMaxHeight;
- };
-
return (
<>
<div className="px-2 flex gap-2 items-center flex-wrap mt-2">
- {/* {highlightedCodeSections.length > 1 && (
- <>
- <HeaderButtonWithText
- text="Clear Context"
- onClick={() => {
- props.deleteContextItems(
- highlightedCodeSections.map((_, idx) => idx)
- );
- }}
- >
- <Trash size="1.6em" />
- </HeaderButtonWithText>
- </>
- )} */}
- {highlightedCodeSections.map((section, idx) => (
- <PillButton
- warning={
- section.range.contents.length > 4000 && section.editing
- ? "Editing such a large range may be slow"
- : undefined
- }
- onlyShowDelete={
- highlightedCodeSections.length <= 1 || section.editing
- }
- editing={section.editing}
- pinned={section.pinned}
- index={idx}
- key={`${section.display_name}${idx}`}
- title={`${section.display_name} (${
- section.range.range.start.line + 1
- }-${section.range.range.end.line + 1})`}
- onDelete={() => {
- if (props.deleteContextItems) {
- props.deleteContextItems([idx]);
+ {props.selectedContextItems.map((item, idx) => {
+ return (
+ <PillButton
+ key={`${item.description.id.item_id}${idx}`}
+ item={item}
+ warning={
+ item.content.length > 4000 && item.editing
+ ? "Editing such a large range may be slow"
+ : undefined
}
- setHighlightedCodeSections((prev) => {
- const newSections = [...prev];
- newSections.splice(idx, 1);
- return newSections;
- });
- }}
- />
- ))}
- {props.highlightedCodeSections.length > 0 &&
+ addingHighlightedCode={props.addingHighlightedCode}
+ index={idx}
+ />
+ );
+ })}
+ {props.selectedContextItems.length > 0 &&
(props.addingHighlightedCode ? (
<EmptyPillDiv
onClick={() => {
@@ -259,7 +291,7 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
<div className="flex px-2" ref={divRef} hidden={!downshiftProps.isOpen}>
<MainTextInput
disabled={props.disabled}
- placeholder={`Ask a question, give instructions, or type '/' to see slash commands`}
+ placeholder={`Ask a question, give instructions, type '/' for slash commands, or '@' to add context`}
{...getInputProps({
onChange: (e) => {
const target = e.target as HTMLTextAreaElement;
@@ -269,11 +301,13 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
target.scrollHeight,
300
).toString()}px`;
+ setInputBoxHeight(target.style.height);
// setShowContextDropdown(target.value.endsWith("@"));
},
onFocus: (e) => {
setFocused(true);
+ dispatch(setBottomMessageCloseTimeout(undefined));
},
onBlur: (e) => {
setFocused(false);
@@ -283,6 +317,7 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
if (event.key === "Enter" && event.shiftKey) {
// Prevent Downshift's default 'Enter' behavior.
(event.nativeEvent as any).preventDownshiftDefault = true;
+ setCurrentlyInContextQuery(false);
} else if (
event.key === "Enter" &&
(!downshiftProps.isOpen || items.length === 0)
@@ -296,6 +331,7 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
(event.nativeEvent as any).preventDownshiftDefault = true;
if (props.onEnter) props.onEnter(event);
+ setCurrentlyInContextQuery(false);
} else if (event.key === "Tab" && items.length > 0) {
downshiftProps.setInputValue(items[0].name);
event.preventDefault();
@@ -315,6 +351,7 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
}
downshiftProps.setInputValue(history[positionInHistory - 1]);
setPositionInHistory((prev) => prev - 1);
+ setCurrentlyInContextQuery(false);
} else if (event.key === "ArrowDown") {
if (positionInHistory < history.length) {
downshiftProps.setInputValue(history[positionInHistory + 1]);
@@ -322,8 +359,12 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
setPositionInHistory((prev) =>
Math.min(prev + 1, history.length)
);
+ setCurrentlyInContextQuery(false);
}
},
+ onClick: () => {
+ dispatch(setBottomMessageCloseTimeout(undefined));
+ },
ref: inputRef,
})}
/>
@@ -345,13 +386,14 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
selected={downshiftProps.selectedItem === item}
>
<span>
- {item.name}: {item.description}
+ {item.name}:{" "}
+ <span style={{ color: lightGray }}>{item.description}</span>
</span>
</Li>
))}
</Ul>
</div>
- {highlightedCodeSections.length === 0 &&
+ {props.selectedContextItems.length === 0 &&
(downshiftProps.inputValue?.startsWith("/edit") ||
(focused &&
metaKeyPressed &&
diff --git a/extension/react-app/src/components/PillButton.tsx b/extension/react-app/src/components/PillButton.tsx
index 5929d06a..548fdf9d 100644
--- a/extension/react-app/src/components/PillButton.tsx
+++ b/extension/react-app/src/components/PillButton.tsx
@@ -1,8 +1,9 @@
-import { useContext, useState } from "react";
+import { useContext, useEffect, useRef, useState } from "react";
import styled from "styled-components";
import {
StyledTooltip,
defaultBorderRadius,
+ lightGray,
secondaryDark,
vscBackground,
vscForeground,
@@ -13,6 +14,14 @@ import {
ExclamationTriangle,
} from "@styled-icons/heroicons-outline";
import { GUIClientContext } from "../App";
+import { useDispatch } from "react-redux";
+import {
+ setBottomMessage,
+ setBottomMessageCloseTimeout,
+} from "../redux/slices/uiStateSlice";
+import { ContextItem } from "../../../schema/FullState";
+import { ReactMarkdown } from "react-markdown/lib/react-markdown";
+import StyledMarkdownPreview from "./StyledMarkdownPreview";
const Button = styled.button`
border: none;
@@ -68,19 +77,55 @@ const CircleDiv = styled.div`
interface PillButtonProps {
onHover?: (arg0: boolean) => void;
- onDelete?: () => void;
- title: string;
- index: number;
- editing: boolean;
- pinned: boolean;
+ item: ContextItem;
warning?: string;
- onlyShowDelete?: boolean;
+ index: number;
+ addingHighlightedCode?: boolean;
}
const PillButton = (props: PillButtonProps) => {
const [isHovered, setIsHovered] = useState(false);
const client = useContext(GUIClientContext);
+ const dispatch = useDispatch();
+
+ useEffect(() => {
+ if (isHovered) {
+ dispatch(setBottomMessageCloseTimeout(undefined));
+ dispatch(
+ setBottomMessage(
+ <>
+ <b>{props.item.description.name}</b>:{" "}
+ {props.item.description.description}
+ <pre>
+ <code
+ style={{
+ fontSize: "11px",
+ backgroundColor: vscBackground,
+ color: vscForeground,
+ whiteSpace: "pre-wrap",
+ wordWrap: "break-word",
+ }}
+ >
+ {props.item.content}
+ </code>
+ </pre>
+ </>
+ )
+ );
+ } else {
+ dispatch(
+ setBottomMessageCloseTimeout(
+ setTimeout(() => {
+ if (!isHovered) {
+ dispatch(setBottomMessage(undefined));
+ }
+ }, 2000)
+ )
+ );
+ }
+ }, [isHovered]);
+
return (
<>
<div style={{ position: "relative" }}>
@@ -89,10 +134,8 @@ const PillButton = (props: PillButtonProps) => {
position: "relative",
borderColor: props.warning
? "red"
- : props.editing
+ : props.item.editing
? "#8800aa"
- : props.pinned
- ? "#ffff0099"
: "transparent",
borderWidth: "1px",
borderStyle: "solid",
@@ -113,11 +156,14 @@ const PillButton = (props: PillButtonProps) => {
{isHovered && (
<GridDiv
style={{
- gridTemplateColumns: props.onlyShowDelete ? "1fr" : "1fr 1fr",
+ gridTemplateColumns:
+ props.item.editable && props.addingHighlightedCode
+ ? "1fr 1fr"
+ : "1fr",
backgroundColor: vscBackground,
}}
>
- {props.onlyShowDelete || (
+ {props.item.editable && props.addingHighlightedCode && (
<ButtonDiv
data-tooltip-id={`edit-${props.index}`}
backgroundColor={"#8800aa55"}
@@ -132,15 +178,6 @@ const PillButton = (props: PillButtonProps) => {
</ButtonDiv>
)}
- {/* <ButtonDiv
- data-tooltip-id={`pin-${props.index}`}
- backgroundColor={"#ffff0055"}
- onClick={() => {
- client?.setPinnedAtIndices([props.index]);
- }}
- >
- <MapPin style={{ margin: "auto" }} width="1.6em"></MapPin>
- </ButtonDiv> */}
<StyledTooltip id={`pin-${props.index}`}>
Edit this range
</StyledTooltip>
@@ -148,33 +185,34 @@ const PillButton = (props: PillButtonProps) => {
data-tooltip-id={`delete-${props.index}`}
backgroundColor={"#cc000055"}
onClick={() => {
- if (props.onDelete) {
- props.onDelete();
- }
+ client?.deleteContextWithIds([props.item.description.id]);
+ dispatch(setBottomMessageCloseTimeout(undefined));
}}
>
<Trash style={{ margin: "auto" }} width="1.6em"></Trash>
</ButtonDiv>
</GridDiv>
)}
- {props.title}
+ {props.item.description.name}
</Button>
<StyledTooltip id={`edit-${props.index}`}>
- {props.editing
+ {props.item.editing
? "Editing this section (with entire file as context)"
: "Edit this section"}
</StyledTooltip>
<StyledTooltip id={`delete-${props.index}`}>Delete</StyledTooltip>
{props.warning && (
<>
- <CircleDiv data-tooltip-id={`circle-div-${props.title}`}>
+ <CircleDiv
+ data-tooltip-id={`circle-div-${props.item.description.name}`}
+ >
<ExclamationTriangle
style={{ margin: "auto" }}
width="1.0em"
strokeWidth={2}
/>
</CircleDiv>
- <StyledTooltip id={`circle-div-${props.title}`}>
+ <StyledTooltip id={`circle-div-${props.item.description.name}`}>
{props.warning}
</StyledTooltip>
</>
diff --git a/extension/react-app/src/components/StepContainer.tsx b/extension/react-app/src/components/StepContainer.tsx
index bc8665fd..2cfe7ecd 100644
--- a/extension/react-app/src/components/StepContainer.tsx
+++ b/extension/react-app/src/components/StepContainer.tsx
@@ -18,9 +18,9 @@ import {
import { StopCircle } from "@styled-icons/heroicons-solid";
import { HistoryNode } from "../../../schema/HistoryNode";
import HeaderButtonWithText from "./HeaderButtonWithText";
-import MarkdownPreview from "@uiw/react-markdown-preview";
import { getMetaKeyLabel, isMetaEquivalentKeyPressed } from "../util";
import { GUIClientContext } from "../App";
+import StyledMarkdownPreview from "./StyledMarkdownPreview";
interface StepContainerProps {
historyNode: HistoryNode;
@@ -109,33 +109,6 @@ const GradientBorder = styled.div<{
background-size: 200% 200%;
`;
-const StyledMarkdownPreview = styled(MarkdownPreview)`
- pre {
- background-color: ${secondaryDark};
- padding: 1px;
- border-radius: ${defaultBorderRadius};
- border: 0.5px solid white;
- }
-
- code {
- color: #f78383;
- word-wrap: break-word;
- border-radius: ${defaultBorderRadius};
- background-color: ${secondaryDark};
- }
-
- pre > code {
- background-color: ${secondaryDark};
- color: ${vscForeground};
- }
-
- background-color: ${vscBackground};
- font-family: "Lexend", sans-serif;
- font-size: 13px;
- padding: 8px;
- color: ${vscForeground};
-`;
-
// #endregion
function StepContainer(props: StepContainerProps) {
diff --git a/extension/react-app/src/components/StyledMarkdownPreview.tsx b/extension/react-app/src/components/StyledMarkdownPreview.tsx
new file mode 100644
index 00000000..9c2ecb62
--- /dev/null
+++ b/extension/react-app/src/components/StyledMarkdownPreview.tsx
@@ -0,0 +1,37 @@
+import styled from "styled-components";
+import {
+ defaultBorderRadius,
+ secondaryDark,
+ vscBackground,
+ vscForeground,
+} from ".";
+import MarkdownPreview from "@uiw/react-markdown-preview";
+
+const StyledMarkdownPreview = styled(MarkdownPreview)`
+ pre {
+ background-color: ${secondaryDark};
+ padding: 1px;
+ border-radius: ${defaultBorderRadius};
+ border: 0.5px solid white;
+ }
+
+ code {
+ color: #f78383;
+ word-wrap: break-word;
+ border-radius: ${defaultBorderRadius};
+ background-color: ${secondaryDark};
+ }
+
+ pre > code {
+ background-color: ${secondaryDark};
+ color: ${vscForeground};
+ }
+
+ background-color: ${vscBackground};
+ font-family: "Lexend", sans-serif;
+ font-size: 13px;
+ padding: 8px;
+ color: ${vscForeground};
+`;
+
+export default StyledMarkdownPreview;
diff --git a/extension/react-app/src/components/TextDialog.tsx b/extension/react-app/src/components/TextDialog.tsx
index 9597b578..7d8e9920 100644
--- a/extension/react-app/src/components/TextDialog.tsx
+++ b/extension/react-app/src/components/TextDialog.tsx
@@ -6,7 +6,7 @@ import { isMetaEquivalentKeyPressed } from "../util";
import { ReactMarkdown } from "react-markdown/lib/react-markdown";
const ScreenCover = styled.div`
- position: absolute;
+ position: fixed;
width: 100%;
height: 100%;
background-color: rgba(168, 168, 168, 0.5);