summaryrefslogtreecommitdiff
path: root/extension/react-app/src/components/ComboBox.tsx
diff options
context:
space:
mode:
authorNate Sesti <sestinj@gmail.com>2023-09-09 13:43:24 -0700
committerNate Sesti <sestinj@gmail.com>2023-09-09 13:43:24 -0700
commit4362a51214a683bfe1efd424ddb226d4e636eeed (patch)
tree3f9a12c6dd6d09e3f2968eb0c9374fea0d3c2d2d /extension/react-app/src/components/ComboBox.tsx
parent2b35e5f5cff948ca7d4f207b23db68f0da248a95 (diff)
downloadsncontinue-4362a51214a683bfe1efd424ddb226d4e636eeed.tar.gz
sncontinue-4362a51214a683bfe1efd424ddb226d4e636eeed.tar.bz2
sncontinue-4362a51214a683bfe1efd424ddb226d4e636eeed.zip
feat: :lipstick: query input indicator for ctx provs
Diffstat (limited to 'extension/react-app/src/components/ComboBox.tsx')
-rw-r--r--extension/react-app/src/components/ComboBox.tsx263
1 files changed, 187 insertions, 76 deletions
diff --git a/extension/react-app/src/components/ComboBox.tsx b/extension/react-app/src/components/ComboBox.tsx
index 91c5facc..c216e7d1 100644
--- a/extension/react-app/src/components/ComboBox.tsx
+++ b/extension/react-app/src/components/ComboBox.tsx
@@ -9,6 +9,7 @@ import { useCombobox } from "downshift";
import styled from "styled-components";
import {
StyledTooltip,
+ buttonColor,
defaultBorderRadius,
lightGray,
secondaryDark,
@@ -23,6 +24,7 @@ import {
FolderArrowDownIcon,
ArrowLeftIcon,
PlusIcon,
+ ArrowRightIcon,
} from "@heroicons/react/24/outline";
import { ContextItem } from "../../../schema/FullState";
import { postVscMessage } from "../vscode";
@@ -64,7 +66,7 @@ const EmptyPillDiv = styled.div`
}
`;
-const MainTextInput = styled.textarea`
+const MainTextInput = styled.textarea<{ inQueryForDynamicProvider: boolean }>`
resize: none;
padding: 8px;
@@ -77,11 +79,16 @@ const MainTextInput = styled.textarea`
background-color: ${secondaryDark};
color: ${vscForeground};
z-index: 1;
- border: 1px solid transparent;
+ border: 1px solid
+ ${(props) =>
+ props.inQueryForDynamicProvider ? buttonColor : "transparent"};
&:focus {
- outline: 1px solid ${lightGray};
+ outline: 1px solid
+ ${(props) => (props.inQueryForDynamicProvider ? buttonColor : lightGray)};
border: 1px solid transparent;
+ background-color: ${(props) =>
+ props.inQueryForDynamicProvider ? `${buttonColor}22` : secondaryDark};
}
&::placeholder {
@@ -89,6 +96,20 @@ const MainTextInput = styled.textarea`
}
`;
+const DynamicQueryTitleDiv = styled.div`
+ position: absolute;
+ right: 0px;
+ top: 0px;
+ height: fit-content;
+ padding: 2px 4px;
+ border-radius: ${defaultBorderRadius};
+ z-index: 2;
+ color: white;
+ font-size: 12px;
+
+ background-color: ${buttonColor};
+`;
+
const StyledPlusIcon = styled(PlusIcon)`
position: absolute;
right: 0px;
@@ -150,7 +171,8 @@ const Li = styled.li<{
${({ selected }) => selected && "font-weight: bold;"}
padding: 0.5rem 0.75rem;
display: flex;
- flex-direction: column;
+ flex-direction: row;
+ align-items: center;
${({ isLastItem }) => isLastItem && "border-bottom: 1px solid gray;"}
/* border-top: 1px solid gray; */
cursor: pointer;
@@ -191,10 +213,14 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
const [nestedContextProvider, setNestedContextProvider] = useState<
any | undefined
>(undefined);
+ const [inQueryForContextProvider, setInQueryForContextProvider] = useState<
+ any | undefined
+ >(undefined);
useEffect(() => {
if (!currentlyInContextQuery) {
setNestedContextProvider(undefined);
+ setInQueryForContextProvider(undefined);
}
}, [currentlyInContextQuery]);
@@ -205,6 +231,7 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
const goBackToContextProviders = () => {
setCurrentlyInContextQuery(false);
setNestedContextProvider(undefined);
+ setInQueryForContextProvider(undefined);
downshiftProps.setInputValue("@");
};
@@ -227,8 +254,16 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
if (!inputValue) {
setItems([]);
setNestedContextProvider(undefined);
+ setCurrentlyInContextQuery(false);
return;
}
+ if (
+ inQueryForContextProvider &&
+ !inputValue.startsWith(`@${inQueryForContextProvider.title}`)
+ ) {
+ setInQueryForContextProvider(undefined);
+ }
+
props.onInputValueChange(inputValue);
// Handle context selection
@@ -286,7 +321,41 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
)
);
},
- [props.items, currentlyInContextQuery, nestedContextProvider]
+ [
+ props.items,
+ currentlyInContextQuery,
+ nestedContextProvider,
+ inQueryForContextProvider,
+ ]
+ );
+
+ const onSelectedItemChangeCallback = useCallback(
+ ({ selectedItem }: any) => {
+ if (!selectedItem) return;
+ if (selectedItem.id) {
+ // Get the query from the input value
+ const segs = downshiftProps.inputValue.split("@");
+ const query = segs[segs.length - 1];
+
+ // Tell server the context item was selected
+ client?.selectContextItem(selectedItem.id, query);
+ if (downshiftProps.inputValue.includes("@")) {
+ const selectedNestedContextProvider = contextProviders.find(
+ (provider) => provider.title === selectedItem.id
+ );
+ if (
+ !nestedContextProvider &&
+ !selectedNestedContextProvider?.dynamic
+ ) {
+ downshiftProps.setInputValue(`@${selectedItem.id} `);
+ setNestedContextProvider(selectedNestedContextProvider);
+ } else {
+ downshiftProps.setInputValue("");
+ }
+ }
+ }
+ },
+ [nestedContextProvider, contextProviders, client]
);
const getFilteredContextItemsForProvider = async (
@@ -321,31 +390,7 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
};
const { getInputProps, ...downshiftProps } = useCombobox({
- onSelectedItemChange: ({ selectedItem }) => {
- if (!selectedItem) return;
- if (selectedItem.id) {
- // Get the query from the input value
- const segs = downshiftProps.inputValue.split("@");
- const query = segs[segs.length - 1];
-
- // Tell server the context item was selected
- client?.selectContextItem(selectedItem.id, query);
- if (downshiftProps.inputValue.includes("@")) {
- const selectedNestedContextProvider = contextProviders.find(
- (provider) => provider.title === selectedItem.id
- );
- if (
- !nestedContextProvider &&
- !selectedNestedContextProvider?.dynamic
- ) {
- downshiftProps.setInputValue(`@${selectedItem.id} `);
- setNestedContextProvider(selectedNestedContextProvider);
- } else {
- downshiftProps.setInputValue("");
- }
- }
- }
- },
+ onSelectedItemChange: onSelectedItemChangeCallback,
onInputValueChange: onInputValueChangeCallback,
items,
itemToString(item) {
@@ -453,6 +498,42 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
};
}, [inputRef.current]);
+ const selectContextItemFromDropdown = useCallback(
+ (event: any) => {
+ const newProviderName = items[downshiftProps.highlightedIndex].name;
+ const newProvider = contextProviders.find(
+ (provider) => provider.display_title === newProviderName
+ );
+
+ if (!newProvider) {
+ (event.nativeEvent as any).preventDownshiftDefault = true;
+ return;
+ } else if (newProvider.dynamic && newProvider.requires_query) {
+ setInQueryForContextProvider(newProvider);
+ downshiftProps.setInputValue(`@${newProvider.title} `);
+ (event.nativeEvent as any).preventDownshiftDefault = true;
+ event.preventDefault();
+ return;
+ } else if (newProvider.dynamic) {
+ return;
+ }
+
+ setNestedContextProvider(newProvider);
+ downshiftProps.setInputValue(`@${newProvider.title} `);
+ (event.nativeEvent as any).preventDownshiftDefault = true;
+ event.preventDefault();
+ getFilteredContextItemsForProvider(newProvider.title, "").then((items) =>
+ setItems(items)
+ );
+ },
+ [
+ items,
+ downshiftProps.highlightedIndex,
+ contextProviders,
+ nestedContextProvider,
+ ]
+ );
+
const showSelectContextGroupDialog = () => {
dispatch(setDialogMessage(<SelectContextGroupDialog />));
dispatch(setShowDialog(true));
@@ -563,6 +644,9 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
hidden={!downshiftProps.isOpen}
>
<MainTextInput
+ inQueryForDynamicProvider={
+ typeof inQueryForContextProvider !== "undefined"
+ }
disabled={props.disabled}
placeholder={`Ask a question, give instructions, type '/' for slash commands, or '@' to add context`}
{...getInputProps({
@@ -595,15 +679,26 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
!isComposing
) {
const value = downshiftProps.inputValue;
- if (value !== "") {
- setPositionInHistory(history.length + 1);
- setHistory([...history, value]);
- }
- // Prevent Downshift's default 'Enter' behavior.
- (event.nativeEvent as any).preventDownshiftDefault = true;
+ if (inQueryForContextProvider) {
+ const segs = value.split("@");
+ client?.selectContextItem(
+ inQueryForContextProvider.title,
+ segs[segs.length - 1]
+ );
+ setCurrentlyInContextQuery(false);
+ downshiftProps.setInputValue("");
+ return;
+ } else {
+ if (value !== "") {
+ setPositionInHistory(history.length + 1);
+ setHistory([...history, value]);
+ }
+ // Prevent Downshift's default 'Enter' behavior.
+ (event.nativeEvent as any).preventDownshiftDefault = true;
- if (props.onEnter) {
- props.onEnter(event);
+ if (props.onEnter) {
+ props.onEnter(event);
+ }
}
setCurrentlyInContextQuery(false);
} else if (
@@ -611,26 +706,7 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
currentlyInContextQuery &&
nestedContextProvider === undefined
) {
- const newProviderName =
- items[downshiftProps.highlightedIndex].name;
- const newProvider = contextProviders.find(
- (provider) => provider.display_title === newProviderName
- );
-
- if (!newProvider) {
- (event.nativeEvent as any).preventDownshiftDefault = true;
- return;
- } else if (newProvider.dynamic) {
- return;
- }
-
- setNestedContextProvider(newProvider);
- downshiftProps.setInputValue(`@${newProvider.title} `);
- (event.nativeEvent as any).preventDownshiftDefault = true;
- event.preventDefault();
- getFilteredContextItemsForProvider(newProvider.title, "").then(
- (items) => setItems(items)
- );
+ selectContextItemFromDropdown(event);
} else if (event.key === "Tab" && items.length > 0) {
downshiftProps.setInputValue(items[0].name);
event.preventDefault();
@@ -682,6 +758,10 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
goBackToContextProviders();
(event.nativeEvent as any).preventDownshiftDefault = true;
return;
+ } else if (inQueryForContextProvider) {
+ goBackToContextProviders();
+ (event.nativeEvent as any).preventDownshiftDefault = true;
+ return;
}
setCurrentlyInContextQuery(false);
@@ -709,18 +789,27 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
ref: inputRef,
})}
/>
- <StyledPlusIcon
- width="1.4em"
- height="1.4em"
- data-tooltip-id="add-context-button"
- onClick={() => {
- downshiftProps.setInputValue("@");
- inputRef.current?.focus();
- }}
- />
- <StyledTooltip id="add-context-button" place="bottom">
- Add Context to Prompt
- </StyledTooltip>
+ {inQueryForContextProvider ? (
+ <DynamicQueryTitleDiv>
+ Enter {inQueryForContextProvider.display_title} Query
+ </DynamicQueryTitleDiv>
+ ) : (
+ <>
+ <StyledPlusIcon
+ width="1.4em"
+ height="1.4em"
+ data-tooltip-id="add-context-button"
+ onClick={() => {
+ downshiftProps.setInputValue("@");
+ inputRef.current?.focus();
+ }}
+ />
+ <StyledTooltip id="add-context-button" place="bottom">
+ Add Context to Prompt
+ </StyledTooltip>
+ </>
+ )}
+
<Ul
{...downshiftProps.getMenuProps({
ref: ulRef,
@@ -756,23 +845,45 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
{downshiftProps.isOpen &&
items.map((item, index) => (
<Li
- style={{ borderTop: index === 0 ? "none" : undefined }}
+ style={{
+ borderTop: index === 0 ? "none" : undefined,
+ }}
key={`${item.name}${index}`}
{...downshiftProps.getItemProps({ item, index })}
highlighted={downshiftProps.highlightedIndex === index}
selected={downshiftProps.selectedItem === item}
onClick={(e) => {
- e.stopPropagation();
- e.preventDefault();
- (e.nativeEvent as any).preventDownshiftDefault = true;
- downshiftProps.selectItem(item);
+ // e.stopPropagation();
+ // e.preventDefault();
+ // (e.nativeEvent as any).preventDownshiftDefault = true;
+ // downshiftProps.selectItem(item);
+ selectContextItemFromDropdown(e);
+ onSelectedItemChangeCallback({ selectedItem: item });
}}
>
<span>
{item.name}
{" "}
- <span style={{ color: lightGray }}>{item.description}</span>
+ <span
+ style={{
+ color: lightGray,
+ }}
+ >
+ {item.description}
+ </span>
</span>
+ {contextProviders
+ .filter(
+ (provider) => !provider.dynamic || provider.requires_query
+ )
+ .find((provider) => provider.title === item.id) && (
+ <ArrowRightIcon
+ width="1.2em"
+ height="1.2em"
+ color={lightGray}
+ className="ml-2"
+ />
+ )}
</Li>
))}
</Ul>