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.tsx446
-rw-r--r--extension/react-app/src/components/EditableDiv.tsx84
-rw-r--r--extension/react-app/src/components/Layout.tsx22
-rw-r--r--extension/react-app/src/components/ProgressBar.tsx77
-rw-r--r--extension/react-app/src/components/index.ts4
5 files changed, 550 insertions, 83 deletions
diff --git a/extension/react-app/src/components/ComboBox.tsx b/extension/react-app/src/components/ComboBox.tsx
index 41b44684..c216e7d1 100644
--- a/extension/react-app/src/components/ComboBox.tsx
+++ b/extension/react-app/src/components/ComboBox.tsx
@@ -1,4 +1,5 @@
import React, {
+ useCallback,
useContext,
useEffect,
useImperativeHandle,
@@ -7,6 +8,8 @@ import React, {
import { useCombobox } from "downshift";
import styled from "styled-components";
import {
+ StyledTooltip,
+ buttonColor,
defaultBorderRadius,
lightGray,
secondaryDark,
@@ -19,6 +22,9 @@ import {
BookmarkIcon,
DocumentPlusIcon,
FolderArrowDownIcon,
+ ArrowLeftIcon,
+ PlusIcon,
+ ArrowRightIcon,
} from "@heroicons/react/24/outline";
import { ContextItem } from "../../../schema/FullState";
import { postVscMessage } from "../vscode";
@@ -60,7 +66,7 @@ const EmptyPillDiv = styled.div`
}
`;
-const MainTextInput = styled.textarea`
+const MainTextInput = styled.textarea<{ inQueryForDynamicProvider: boolean }>`
resize: none;
padding: 8px;
@@ -73,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 {
@@ -85,6 +96,37 @@ 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;
+ top: 0px;
+ height: fit-content;
+ padding: 0;
+ cursor: pointer;
+ border-radius: ${defaultBorderRadius};
+ z-index: 2;
+
+ background-color: ${vscBackground};
+
+ &:hover {
+ background-color: ${secondaryDark};
+ }
+`;
+
const UlMaxHeight = 300;
const Ul = styled.ul<{
hidden: boolean;
@@ -129,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;
@@ -164,37 +207,66 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
const [items, setItems] = React.useState(props.items);
const inputRef = React.useRef<HTMLInputElement>(null);
- const [inputBoxHeight, setInputBoxHeight] = useState<string | undefined>(
- undefined
- );
// Whether the current input follows an '@' and should be treated as context query
const [currentlyInContextQuery, setCurrentlyInContextQuery] = useState(false);
+ const [nestedContextProvider, setNestedContextProvider] = useState<
+ any | undefined
+ >(undefined);
+ const [inQueryForContextProvider, setInQueryForContextProvider] = useState<
+ any | undefined
+ >(undefined);
- const { getInputProps, ...downshiftProps } = useCombobox({
- 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("@");
+ useEffect(() => {
+ if (!currentlyInContextQuery) {
+ setNestedContextProvider(undefined);
+ setInQueryForContextProvider(undefined);
+ }
+ }, [currentlyInContextQuery]);
- // Tell server the context item was selected
- client?.selectContextItem(selectedItem.id, query);
+ const contextProviders = useSelector(
+ (state: RootStore) => state.serverState.context_providers
+ ) as any[];
- // Remove the '@' and the context query from the input
- if (downshiftProps.inputValue.includes("@")) {
- downshiftProps.setInputValue(restOfInput);
- }
- }
- },
- onInputValueChange({ inputValue, highlightedIndex }) {
+ const goBackToContextProviders = () => {
+ setCurrentlyInContextQuery(false);
+ setNestedContextProvider(undefined);
+ setInQueryForContextProvider(undefined);
+ downshiftProps.setInputValue("@");
+ };
+
+ useEffect(() => {
+ if (!nestedContextProvider) {
+ console.log("setting items", nestedContextProvider);
+ setItems(
+ contextProviders?.map((provider) => ({
+ name: provider.display_title,
+ description: provider.description,
+ id: provider.title,
+ })) || []
+ );
+ }
+ }, [nestedContextProvider]);
+
+ const onInputValueChangeCallback = useCallback(
+ ({ inputValue, highlightedIndex }: any) => {
+ // Clear the input
if (!inputValue) {
setItems([]);
+ setNestedContextProvider(undefined);
+ setCurrentlyInContextQuery(false);
return;
}
+ if (
+ inQueryForContextProvider &&
+ !inputValue.startsWith(`@${inQueryForContextProvider.title}`)
+ ) {
+ setInQueryForContextProvider(undefined);
+ }
+
props.onInputValueChange(inputValue);
+ // Handle context selection
if (inputValue.endsWith("@") || currentlyInContextQuery) {
const segs = inputValue?.split("@") || [];
@@ -202,46 +274,124 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
// Get search results and return
setCurrentlyInContextQuery(true);
const providerAndQuery = segs[segs.length - 1] || "";
- // Only return context items from the current workspace - the index is currently shared between all sessions
- const workspaceFilter =
- workspacePaths && workspacePaths.length > 0
- ? `workspace_dir IN [ ${workspacePaths
- .map((path) => `"${path}"`)
- .join(", ")} ]`
- : undefined;
- searchClient
- .index(SEARCH_INDEX_NAME)
- .search(providerAndQuery, {
- filter: workspaceFilter,
- })
- .then((res) => {
- setItems(
- res.hits.map((hit) => {
- return {
- name: hit.name,
- description: hit.description,
- id: hit.id,
- content: hit.content,
- };
- })
- );
- })
- .catch(() => {
- // Swallow errors, because this simply is not supported on Windows at the moment
+
+ if (nestedContextProvider && !inputValue.endsWith("@")) {
+ // Search only within this specific context provider
+ getFilteredContextItemsForProvider(
+ nestedContextProvider.title,
+ providerAndQuery
+ ).then((res) => {
+ setItems(res);
});
+ } else {
+ // Search through the list of context providers
+ const filteredItems =
+ contextProviders
+ ?.filter(
+ (provider) =>
+ `@${provider.title}`
+ .toLowerCase()
+ .startsWith(inputValue.toLowerCase()) ||
+ `@${provider.display_title}`
+ .toLowerCase()
+ .startsWith(inputValue.toLowerCase())
+ )
+ .map((provider) => ({
+ name: provider.display_title,
+ description: provider.description,
+ id: provider.title,
+ })) || [];
+ setItems(filteredItems);
+ setCurrentlyInContextQuery(true);
+ }
return;
} else {
// Exit the '@' context menu
setCurrentlyInContextQuery(false);
- setItems;
+ setNestedContextProvider(undefined);
}
}
+
+ setNestedContextProvider(undefined);
+
+ // Handle slash commands
setItems(
props.items.filter((item) =>
item.name.toLowerCase().startsWith(inputValue.toLowerCase())
)
);
},
+ [
+ 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 (
+ provider: string,
+ query: string
+ ) => {
+ // Only return context items from the current workspace - the index is currently shared between all sessions
+ const workspaceFilter =
+ workspacePaths && workspacePaths.length > 0
+ ? `workspace_dir IN [ ${workspacePaths
+ .map((path) => `"${path}"`)
+ .join(", ")} ] AND provider_name = '${provider}'`
+ : undefined;
+ try {
+ const res = await searchClient.index(SEARCH_INDEX_NAME).search(query, {
+ filter: workspaceFilter,
+ });
+ return (
+ res?.hits.map((hit) => {
+ return {
+ name: hit.name,
+ description: hit.description,
+ id: hit.id,
+ content: hit.content,
+ };
+ }) || []
+ );
+ } catch (e) {
+ console.log("Error searching context items", e);
+ return [];
+ }
+ };
+
+ const { getInputProps, ...downshiftProps } = useCombobox({
+ onSelectedItemChange: onSelectedItemChangeCallback,
+ onInputValueChange: onInputValueChangeCallback,
items,
itemToString(item) {
return item ? item.name : "";
@@ -348,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));
@@ -409,21 +595,6 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
</HeaderButtonWithText>
{props.selectedContextItems.length > 0 && (
<>
- <HeaderButtonWithText
- text="Bookmark context"
- onClick={() => {
- showDialogToSaveContextGroup();
- }}
- className="pill-button focus:outline-none focus:border-red-600 focus:border focus:border-solid"
- onKeyDown={(e: KeyboardEvent) => {
- e.preventDefault();
- if (e.key === "Enter") {
- showDialogToSaveContextGroup();
- }
- }}
- >
- <BookmarkIcon width="1.4em" height="1.4em" />
- </HeaderButtonWithText>
{props.addingHighlightedCode ? (
<EmptyPillDiv
onClick={() => {
@@ -449,11 +620,33 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
<DocumentPlusIcon width="1.4em" height="1.4em" />
</HeaderButtonWithText>
)}
+ <HeaderButtonWithText
+ text="Bookmark context"
+ onClick={() => {
+ showDialogToSaveContextGroup();
+ }}
+ className="pill-button focus:outline-none focus:border-red-600 focus:border focus:border-solid"
+ onKeyDown={(e: KeyboardEvent) => {
+ e.preventDefault();
+ if (e.key === "Enter") {
+ showDialogToSaveContextGroup();
+ }
+ }}
+ >
+ <BookmarkIcon width="1.4em" height="1.4em" />
+ </HeaderButtonWithText>
</>
)}
</div>
- <div className="flex px-2" ref={divRef} hidden={!downshiftProps.isOpen}>
+ <div
+ className="flex px-2 relative"
+ ref={divRef}
+ 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({
@@ -467,7 +660,6 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
target.scrollHeight,
300
).toString()}px`;
- setInputBoxHeight(target.style.height);
// setShowContextDropdown(target.value.endsWith("@"));
},
@@ -487,17 +679,34 @@ 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 (
+ event.key === "Enter" &&
+ currentlyInContextQuery &&
+ nestedContextProvider === undefined
+ ) {
+ selectContextItemFromDropdown(event);
} else if (event.key === "Tab" && items.length > 0) {
downshiftProps.setInputValue(items[0].name);
event.preventDefault();
@@ -545,6 +754,16 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
);
setCurrentlyInContextQuery(false);
} else if (event.key === "Escape") {
+ if (nestedContextProvider) {
+ goBackToContextProviders();
+ (event.nativeEvent as any).preventDownshiftDefault = true;
+ return;
+ } else if (inQueryForContextProvider) {
+ goBackToContextProviders();
+ (event.nativeEvent as any).preventDownshiftDefault = true;
+ return;
+ }
+
setCurrentlyInContextQuery(false);
if (downshiftProps.isOpen && items.length > 0) {
downshiftProps.closeMenu();
@@ -570,6 +789,27 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
ref: inputRef,
})}
/>
+ {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,
@@ -578,20 +818,72 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
ulHeightPixels={ulRef.current?.getBoundingClientRect().height || 0}
hidden={!downshiftProps.isOpen || items.length === 0}
>
+ {nestedContextProvider && (
+ <div
+ style={{
+ backgroundColor: secondaryDark,
+ borderBottom: `1px solid ${lightGray}`,
+ display: "flex",
+ gap: "4px",
+ position: "sticky",
+ top: "0px",
+ }}
+ className="py-2 px-4 my-0"
+ >
+ <ArrowLeftIcon
+ width="1.4em"
+ height="1.4em"
+ className="cursor-pointer"
+ onClick={() => {
+ goBackToContextProviders();
+ }}
+ />
+ {nestedContextProvider.display_title} -{" "}
+ {nestedContextProvider.description}
+ </div>
+ )}
{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);
+ 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>
diff --git a/extension/react-app/src/components/EditableDiv.tsx b/extension/react-app/src/components/EditableDiv.tsx
new file mode 100644
index 00000000..a86bd692
--- /dev/null
+++ b/extension/react-app/src/components/EditableDiv.tsx
@@ -0,0 +1,84 @@
+import styled from "styled-components";
+import {
+ defaultBorderRadius,
+ lightGray,
+ secondaryDark,
+ vscForeground,
+} from ".";
+
+const Div = styled.div`
+ resize: none;
+
+ padding: 8px;
+ font-size: 13px;
+ font-family: inherit;
+ border-radius: ${defaultBorderRadius};
+ margin: 8px auto;
+ height: auto;
+ width: 100%;
+ background-color: ${secondaryDark};
+ color: ${vscForeground};
+ z-index: 1;
+ border: 1px solid transparent;
+
+ &:focus {
+ outline: 1px solid ${lightGray};
+ border: 1px solid transparent;
+ }
+
+ &::placeholder {
+ color: ${lightGray}80;
+ }
+`;
+
+const Span = styled.span<{ color?: string }>`
+ background-color: ${(props) => props.color || "#2cf8"};
+ border-radius: ${defaultBorderRadius};
+ padding: 2px 4px;
+`;
+
+interface EditableDivProps {
+ onChange: (e: any) => void;
+ value?: string;
+}
+
+function EditableDiv(props: EditableDivProps) {
+ return (
+ <Div
+ suppressContentEditableWarning={true}
+ contentEditable={true}
+ 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("@"));
+ props.onChange(e);
+ }}
+ onKeyDown={(e) => {
+ // if (e.key === "Delete") {
+ // // Delete spans if they are last child
+ // const selection = window.getSelection();
+ // const range = selection?.getRangeAt(0);
+ // const node = range?.startContainer;
+ // console.log("Del");
+ // if (node?.nodeName === "SPAN") {
+ // console.log("span");
+ // const parent = node.parentNode;
+ // if (parent?.childNodes.length === 1) {
+ // parent.removeChild(node);
+ // }
+ // }
+ // }
+ }}
+ >
+ {props.value ? props.value : <Span contentEditable={false}>testing</Span>}
+ </Div>
+ );
+}
+
+export default EditableDiv;
diff --git a/extension/react-app/src/components/Layout.tsx b/extension/react-app/src/components/Layout.tsx
index 17100c7f..6410db8a 100644
--- a/extension/react-app/src/components/Layout.tsx
+++ b/extension/react-app/src/components/Layout.tsx
@@ -21,8 +21,9 @@ import {
Cog6ToothIcon,
} from "@heroicons/react/24/outline";
import HeaderButtonWithText from "./HeaderButtonWithText";
-import { useNavigate } from "react-router-dom";
+import { useNavigate, useLocation } from "react-router-dom";
import ModelSelect from "./ModelSelect";
+import ProgressBar from "./ProgressBar";
// #region Styled Components
const FOOTER_HEIGHT = "1.8em";
@@ -74,6 +75,7 @@ const GridDiv = styled.div`
const Layout = () => {
const navigate = useNavigate();
+ const location = useLocation();
const client = useContext(GUIClientContext);
const dispatch = useDispatch();
const dialogMessage = useSelector(
@@ -82,10 +84,11 @@ const Layout = () => {
const showDialog = useSelector(
(state: RootStore) => state.uiState.showDialog
);
- const dialogEntryOn = useSelector(
- (state: RootStore) => state.uiState.dialogEntryOn
- );
+ const defaultModel = useSelector(
+ (state: RootStore) =>
+ (state.serverState.config as any).models?.default?.class_name
+ );
// #region Selectors
const bottomMessage = useSelector(
@@ -175,6 +178,17 @@ const Layout = () => {
)}
<ModelSelect />
+ {defaultModel === "MaybeProxyOpenAI" &&
+ (location.pathname === "/settings" ||
+ parseInt(localStorage.getItem("freeTrialCounter") || "0") >=
+ 125) && (
+ <ProgressBar
+ completed={parseInt(
+ localStorage.getItem("freeTrialCounter") || "0"
+ )}
+ total={250}
+ />
+ )}
</div>
<HeaderButtonWithText
onClick={() => {
diff --git a/extension/react-app/src/components/ProgressBar.tsx b/extension/react-app/src/components/ProgressBar.tsx
new file mode 100644
index 00000000..b4a2efc9
--- /dev/null
+++ b/extension/react-app/src/components/ProgressBar.tsx
@@ -0,0 +1,77 @@
+import React from "react";
+import styled from "styled-components";
+import { StyledTooltip, lightGray, vscForeground } from ".";
+
+const ProgressBarWrapper = styled.div`
+ width: 100px;
+ height: 6px;
+ border-radius: 6px;
+ border: 0.5px solid ${lightGray};
+ margin-top: 6px;
+`;
+
+const ProgressBarFill = styled.div<{ completed: number; color?: string }>`
+ height: 100%;
+ background-color: ${(props) => props.color || vscForeground};
+ border-radius: inherit;
+ transition: width 0.2s ease-in-out;
+ width: ${(props) => props.completed}%;
+`;
+
+const GridDiv = styled.div`
+ display: grid;
+ grid-template-rows: 1fr auto;
+ align-items: center;
+ justify-items: center;
+`;
+
+const P = styled.p`
+ margin: 0;
+ margin-top: 2px;
+ font-size: 12px;
+ color: ${lightGray};
+ text-align: center;
+`;
+
+interface ProgressBarProps {
+ completed: number;
+ total: number;
+}
+
+const ProgressBar = ({ completed, total }: ProgressBarProps) => {
+ const fillPercentage = Math.min(100, Math.max(0, (completed / total) * 100));
+
+ return (
+ <>
+ <a
+ href="https://continue.dev/docs/customization"
+ className="no-underline"
+ >
+ <GridDiv data-tooltip-id="usage_progress_bar">
+ <ProgressBarWrapper>
+ <ProgressBarFill
+ completed={fillPercentage}
+ color={
+ completed / total > 0.75
+ ? completed / total > 0.95
+ ? "#f00"
+ : "#fc0"
+ : undefined
+ }
+ />
+ </ProgressBarWrapper>
+ <P>
+ Free Usage: {completed} / {total}
+ </P>
+ </GridDiv>
+ </a>
+ <StyledTooltip id="usage_progress_bar" place="bottom">
+ {
+ "Continue allows you to use our OpenAI API key for up to 250 inputs. After this, you can either use your own API key, or use a local LLM. Click the progress bar to go to the docs and learn more."
+ }
+ </StyledTooltip>
+ </>
+ );
+};
+
+export default ProgressBar;
diff --git a/extension/react-app/src/components/index.ts b/extension/react-app/src/components/index.ts
index 25e35dd1..1f418c94 100644
--- a/extension/react-app/src/components/index.ts
+++ b/extension/react-app/src/components/index.ts
@@ -6,8 +6,8 @@ export const lightGray = "#646464";
// export const secondaryDark = "rgb(45 45 45)";
// export const vscBackground = "rgb(30 30 30)";
export const vscBackgroundTransparent = "#1e1e1ede";
-export const buttonColor = "rgb(27 190 132)";
-export const buttonColorHover = "rgb(27 190 132 0.67)";
+export const buttonColor = "#1bbe84";
+export const buttonColorHover = "1bbe84a8";
export const secondaryDark = "var(--vscode-list-hoverBackground)";
export const vscBackground = "var(--vscode-editor-background)";