Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#1036 Conversation Input Document Selector With Hot Key #1037

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions frontend/src/components/PopupSelector.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import React, { useRef } from 'react';
import { useOutsideAlerter } from '../hooks';

export function PopupSelector<SelectionItems extends { name: string }>({
isOpen = false,
setIsPopupOpen,
positionTop = 0,
positionLeft = 0,
headerText,
onSelect,
selectionItems,
}: TPopupSelector<SelectionItems>) {
const popupRef = useRef<HTMLDivElement>(null);
useOutsideAlerter(popupRef, () => setIsPopupOpen(false), [], true);

return isOpen ? (
<div
className={`absolute h-32 w-128 top=${positionTop} left-${positionLeft} z-20 -mt-52 overflow-y-auto rounded-b-xl rounded-t-xl border-2 border-silver bg-white shadow-lg dark:border-silver/40 dark:bg-dark-charcoal`}
ref={popupRef}
>
{headerText && (
<div className="mt-2 flex items-center justify-between">
<span className="ml-4 flex-1 text-gray-500">{headerText}</span>
</div>
)}
{selectionItems.map((item, idx) => {
return (
<div
key={idx}
className="flex cursor-pointer items-center justify-between hover:bg-gray-100 dark:text-bright-gray dark:hover:bg-purple-taupe"
onClick={() => onSelect(item)}
>
<span className="ml-4 flex-1 overflow-hidden overflow-ellipsis whitespace-nowrap py-3">
{item.name}
</span>
</div>
);
})}
</div>
) : (
<div></div>
);
}

type TPopupSelector<SelectionItem> = {
isOpen: boolean;
setIsPopupOpen: React.Dispatch<React.SetStateAction<boolean>>;
onSelect: (item: SelectionItem) => void;
selectionItems: SelectionItem[];
positionTop: number;
positionLeft: number;
handleOutsideClick?: () => void;
headerText?: string;
};
44 changes: 8 additions & 36 deletions frontend/src/conversation/Conversation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,12 @@ import {
selectStatus,
updateQuery,
} from './conversationSlice';
import { selectConversationId } from '../preferences/preferenceSlice';
import Send from './../assets/send.svg';
import SendDark from './../assets/send_dark.svg';
import Spinner from './../assets/spinner.svg';
import SpinnerDark from './../assets/spinner-dark.svg';
import { FEEDBACK, Query } from './conversationModels';
import { sendFeedback } from './conversationApi';
import { useTranslation } from 'react-i18next';
import ArrowDown from './../assets/arrow-down.svg';
import RetryIcon from '../components/RetryIcon';
import { ConversationInputBox } from './ConversationInputBox';
import ShareIcon from '../assets/share.svg';
import { ShareConversationModal } from '../modals/ShareConversationModal';

Expand Down Expand Up @@ -266,37 +262,13 @@ export default function Conversation() {
</div>

<div className="flex w-11/12 flex-col items-end self-center rounded-2xl bg-opacity-0 pb-1 sm:w-6/12">
<div className="flex h-full w-full items-center rounded-[40px] border border-silver bg-white py-1 dark:bg-raisin-black">
<div
id="inputbox"
ref={inputRef}
tabIndex={1}
placeholder={t('inputPlaceholder')}
contentEditable
onPaste={handlePaste}
className={`inputbox-style max-h-24 w-full overflow-y-auto overflow-x-hidden whitespace-pre-wrap rounded-full bg-white pt-5 pb-[22px] text-base leading-tight opacity-100 focus:outline-none dark:bg-raisin-black dark:text-bright-gray`}
onKeyDown={(e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
handleQuestionSubmission();
}
}}
></div>
{status === 'loading' ? (
<img
src={isDarkTheme ? SpinnerDark : Spinner}
className="relative right-[38px] bottom-[24px] -mr-[30px] animate-spin cursor-pointer self-end bg-transparent"
></img>
) : (
<div className="mx-1 cursor-pointer rounded-full p-4 text-center hover:bg-gray-3000">
<img
className="w-6 text-white "
onClick={handleQuestionSubmission}
src={isDarkTheme ? SendDark : Send}
></img>
</div>
)}
</div>
<ConversationInputBox
inputRef={inputRef}
onSubmit={handleQuestionSubmission}
handlePaste={handlePaste}
isDarkTheme={isDarkTheme}
status={status}
/>

<p className="text-gray-595959 hidden w-[100vw] self-center bg-white bg-transparent py-2 text-center text-xs dark:bg-raisin-black dark:text-bright-gray md:inline md:w-full">
{t('tagline')}
Expand Down
124 changes: 124 additions & 0 deletions frontend/src/conversation/ConversationInputBox.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import React, {
ClipboardEvent,
KeyboardEvent,
useState,
RefObject,
useLayoutEffect,
} from 'react';
import Spinner from './../assets/spinner.svg';
import SpinnerDark from './../assets/spinner-dark.svg';
import Send from './../assets/send.svg';
import SendDark from './../assets/send_dark.svg';
import { useTranslation } from 'react-i18next';
import { PopupSelector } from '../components/PopupSelector';
import { useDispatch, useSelector } from 'react-redux';
import {
selectSelectedDocs,
selectSourceDocs,
setSelectedDocs,
} from '../preferences/preferenceSlice';
import { Doc } from '../models/misc';
import { ConversationSourceList } from './ConversationSourceList';

export function ConversationInputBox({
inputRef,
onSubmit,
handlePaste,
isDarkTheme,
status,
}: TConversationInputBox) {
const [isPopupOpen, setIsPopupOpen] = useState(false);
const [popupPosition, setPopupPosition] = useState({ top: 0, left: 0 });
const docs = useSelector(selectSourceDocs);
const selectedDocs = useSelector(selectSelectedDocs);
const dispatch = useDispatch();
const { t } = useTranslation();

useLayoutEffect(() => {
if (inputRef.current) {
const rect = inputRef.current.getBoundingClientRect();
setPopupPosition({
top: rect.bottom + window.scrollY,
left: rect.left + window.scrollX,
});
}
}, []);

const onPopupSelection = (selectedDocument: Doc) => {
dispatch(setSelectedDocs(selectedDocument));
setIsPopupOpen(false);
};

const onKeyDown = (e: KeyboardEvent) => {
if (e.ctrlKey === true && e.key === 'd') {
setIsPopupOpen(!isPopupOpen);
} else {
setIsPopupOpen(false);
}
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
onSubmit();
}
};

return (
<div className="flex h-full w-full flex-col">
<div className="mb-2 h-full w-full">
<ConversationSourceList
docs={isDoc(selectedDocs) ? [selectedDocs] : []}
/>
</div>
<div className="flex h-full w-full flex-row items-center rounded-[40px] border border-silver bg-white py-1 dark:bg-raisin-black">
<PopupSelector
isOpen={isPopupOpen}
setIsPopupOpen={setIsPopupOpen}
selectionItems={docs || []}
positionTop={popupPosition.top}
positionLeft={popupPosition.left}
onSelect={onPopupSelection}
headerText={t('selectADocument')}
/>
<div
id="inputbox"
ref={inputRef}
tabIndex={1}
placeholder={t('inputPlaceholder')}
contentEditable
onPaste={handlePaste}
className={`inputbox-style max-h-24 w-full overflow-y-auto overflow-x-hidden whitespace-pre-wrap rounded-full bg-white pt-5 pb-[22px] text-base leading-tight opacity-100 focus:outline-none dark:bg-raisin-black dark:text-bright-gray`}
onKeyDown={onKeyDown}
></div>
{status === 'loading' ? (
<img
src={isDarkTheme ? SpinnerDark : Spinner}
className="relative right-[38px] bottom-[24px] -mr-[30px] animate-spin cursor-pointer self-end bg-transparent"
></img>
) : (
<div className="mx-1 cursor-pointer rounded-full p-4 text-center hover:bg-gray-3000">
<img
className="w-6 text-white "
onClick={onSubmit}
src={isDarkTheme ? SendDark : Send}
></img>
</div>
)}
</div>
</div>
);
}

//TODO: There may be a bug where if page is loaded with "None" source docs selected then the initial value of
//selectedDocs in the global data store is an array. This case is not caught as a TS error this array is passed
//unexpectedly even though it has a type set to Doc. This type check is used to counteract this unexpected behavior in
//the mean time.
function isDoc(doc: Doc | unknown): doc is Doc {
return !!doc && (doc as Doc).name !== undefined;
}

type TConversationInputBox = {
inputRef: RefObject<HTMLDivElement>;
handlePaste: (e: ClipboardEvent) => void;
onSubmit: () => void;
isDarkTheme?: boolean;
status: string;
};
34 changes: 34 additions & 0 deletions frontend/src/conversation/ConversationSourceList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import React from 'react';
import { Doc } from '../models/misc';
import Exit from '../assets/exit.svg';
import { useDispatch } from 'react-redux';
import { setSelectedDocs } from '../preferences/preferenceSlice';

export function ConversationSourceList({ docs }: TConversationSourceListProps) {
const dispatch = useDispatch();
return (
<div className="flex flex-row">
{docs &&
docs.map((doc, idx) => {
return (
<div
className={`flex max-w-xs flex-row rounded-[28px] bg-[#D7EBFD] px-4 py-1 sm:max-w-sm md:max-w-md`}
key={idx}
>
<img
src={Exit}
alt="Remove"
className="mr-2 mt-1 h-3 w-3 cursor-pointer hover:opacity-50"
onClick={() => dispatch(setSelectedDocs(null))}
/>
{doc.name}
</div>
);
})}
</div>
);
}

type TConversationSourceListProps = {
docs: Doc[];
};
1 change: 1 addition & 0 deletions frontend/src/locale/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"sourceDocs": "Source Docs",
"none": "None",
"cancel": "Cancel",
"selectADocument": "Select A Document",
"demo": [
{
"header": "Learn about DocsGPT",
Expand Down
Loading