From 937c60c9cf4e902abb2a3a94aa91611355f3aee0 Mon Sep 17 00:00:00 2001 From: utin-francis-peter Date: Sat, 29 Jun 2024 18:55:10 +0100 Subject: [PATCH 01/46] style: updated custom css class to match textInput component's --- frontend/src/index.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/index.css b/frontend/src/index.css index 519b5f74..80a1aa8e 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -413,7 +413,7 @@ template { bottom: env(safe-area-inset-bottom, 0); } -.inputbox-style[contenteditable] { +.text-input { padding-left: 36px; padding-right: 36px; } From 522e966194d8d8de7f11b8789295ba04c508b790 Mon Sep 17 00:00:00 2001 From: utin-francis-peter Date: Sat, 29 Jun 2024 18:58:13 +0100 Subject: [PATCH 02/46] refactor: custom input component is used. inputRef is also replaced with state value --- frontend/src/conversation/Conversation.tsx | 36 +++++++++++++++++----- 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/frontend/src/conversation/Conversation.tsx b/frontend/src/conversation/Conversation.tsx index 4fb34186..ee85cbbd 100644 --- a/frontend/src/conversation/Conversation.tsx +++ b/frontend/src/conversation/Conversation.tsx @@ -20,12 +20,15 @@ import { sendFeedback } from './conversationApi'; import { useTranslation } from 'react-i18next'; import ArrowDown from './../assets/arrow-down.svg'; import RetryIcon from '../components/RetryIcon'; +import TextInput from '../components/inputs/TextInput'; export default function Conversation() { const queries = useSelector(selectQueries); const status = useSelector(selectStatus); const dispatch = useDispatch(); const endMessageRef = useRef(null); - const inputRef = useRef(null); + // const inputRef = useRef(null); + // migrating to useState for managing input values and onChange + const [prompt, setPrompt] = useState(''); const [isDarkTheme] = useDarkTheme(); const [hasScrolledToLast, setHasScrolledToLast] = useState(true); const fetchStream = useRef(null); @@ -112,14 +115,14 @@ export default function Conversation() { }; const handleQuestionSubmission = () => { - if (inputRef.current?.textContent && status !== 'loading') { + if (prompt.length && status !== 'loading') { if (lastQueryReturnedErr) { // update last failed query with new prompt dispatch( updateQuery({ index: queries.length - 1, query: { - prompt: inputRef.current.textContent, + prompt, }, }), ); @@ -128,9 +131,9 @@ export default function Conversation() { isRetry: true, }); } else { - handleQuestion({ question: inputRef.current.textContent }); + handleQuestion({ question: prompt }); } - inputRef.current.textContent = ''; + setPrompt(''); } }; @@ -190,6 +193,8 @@ export default function Conversation() { document.execCommand('insertText', false, text); }; + // console.log('inputRef: ', inputRef); + return (
-
{ if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); handleQuestionSubmission(); } }} - >
+ >
*/} + setPrompt(e.target.value)} + placeholder={t('inputPlaceholder')} + onPaste={handlePaste} + onKeyDown={(e) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + handleQuestionSubmission(); + } + }} + > {status === 'loading' ? ( Date: Sat, 29 Jun 2024 20:45:33 +0100 Subject: [PATCH 03/46] style: removed custom padding and used twClasses --- frontend/src/index.css | 5 ----- 1 file changed, 5 deletions(-) diff --git a/frontend/src/index.css b/frontend/src/index.css index 80a1aa8e..ac90fc66 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -412,8 +412,3 @@ template { .bottom-safe { bottom: env(safe-area-inset-bottom, 0); } - -.text-input { - padding-left: 36px; - padding-right: 36px; -} From 7408454a7566970815c7f37d612c7526ab3c56a4 Mon Sep 17 00:00:00 2001 From: utin-francis-peter Date: Mon, 1 Jul 2024 19:54:31 +0100 Subject: [PATCH 04/46] chore: prompts input now uses useState hook for state change and inbuilt autoFocus --- frontend/src/conversation/Conversation.tsx | 26 ---------------------- 1 file changed, 26 deletions(-) diff --git a/frontend/src/conversation/Conversation.tsx b/frontend/src/conversation/Conversation.tsx index ee85cbbd..96f1eecb 100644 --- a/frontend/src/conversation/Conversation.tsx +++ b/frontend/src/conversation/Conversation.tsx @@ -26,8 +26,6 @@ export default function Conversation() { const status = useSelector(selectStatus); const dispatch = useDispatch(); const endMessageRef = useRef(null); - // const inputRef = useRef(null); - // migrating to useState for managing input values and onChange const [prompt, setPrompt] = useState(''); const [isDarkTheme] = useDarkTheme(); const [hasScrolledToLast, setHasScrolledToLast] = useState(true); @@ -43,13 +41,6 @@ export default function Conversation() { !eventInterrupt && scrollIntoView(); }, [queries.length, queries[queries.length - 1]]); - useEffect(() => { - const element = document.getElementById('inputbox') as HTMLInputElement; - if (element) { - element.focus(); - } - }, []); - useEffect(() => { return () => { if (status !== 'idle') { @@ -241,25 +232,8 @@ export default function Conversation() {
- {/*
{ - if (e.key === 'Enter' && !e.shiftKey) { - e.preventDefault(); - handleQuestionSubmission(); - } - }} - >
*/} setPrompt(e.target.value)} placeholder={t('inputPlaceholder')} From 15b0e321bdbafb66f204b4c596bca9201e3b4a93 Mon Sep 17 00:00:00 2001 From: utin-francis-peter Date: Wed, 3 Jul 2024 11:24:29 +0100 Subject: [PATCH 05/46] chore: TextArea component to replace Div contentEditable for entering prompts --- frontend/src/components/TextArea.tsx | 71 ++++++++++++++++++++++ frontend/src/conversation/Conversation.tsx | 8 +-- 2 files changed, 75 insertions(+), 4 deletions(-) create mode 100644 frontend/src/components/TextArea.tsx diff --git a/frontend/src/components/TextArea.tsx b/frontend/src/components/TextArea.tsx new file mode 100644 index 00000000..b650423f --- /dev/null +++ b/frontend/src/components/TextArea.tsx @@ -0,0 +1,71 @@ +import React, { useEffect, useRef } from 'react'; + +type Props = { + value: string | string[] | number; + isAutoFocused: boolean; + id?: string; + maxLength?: number; + name?: string; + placeholder?: string; + className?: string; + children?: React.ReactElement; + onChange: (e: React.ChangeEvent) => void; + onPaste?: (e: React.ClipboardEvent) => void; + onKeyDown?: (e: React.KeyboardEvent) => void; +}; + +const TextArea = ({ + value, + isAutoFocused, + id, + maxLength, + name, + placeholder, + className, + children, + onChange, + onPaste, + onKeyDown, +}: Props) => { + const textAreaRef = useRef(null); + + useEffect(() => { + const autoResizeTextArea = () => { + if (textAreaRef.current) { + textAreaRef.current.style.height = 'auto'; + + const maxHeight = 96; + const currentContentHeight = textAreaRef.current.scrollHeight; + + const newHeight = Math.min(maxHeight, currentContentHeight); + + textAreaRef.current.style.height = `${newHeight}px`; + } + }; + + autoResizeTextArea(); + }, [value]); + + return ( + + ); +}; + +export default TextArea; diff --git a/frontend/src/conversation/Conversation.tsx b/frontend/src/conversation/Conversation.tsx index 96f1eecb..58af6d09 100644 --- a/frontend/src/conversation/Conversation.tsx +++ b/frontend/src/conversation/Conversation.tsx @@ -20,7 +20,7 @@ import { sendFeedback } from './conversationApi'; import { useTranslation } from 'react-i18next'; import ArrowDown from './../assets/arrow-down.svg'; import RetryIcon from '../components/RetryIcon'; -import TextInput from '../components/inputs/TextInput'; +import TextArea from '../components/TextArea'; export default function Conversation() { const queries = useSelector(selectQueries); const status = useSelector(selectStatus); @@ -232,9 +232,9 @@ export default function Conversation() {
- setPrompt(e.target.value)} placeholder={t('inputPlaceholder')} onPaste={handlePaste} @@ -244,7 +244,7 @@ export default function Conversation() { handleQuestionSubmission(); } }} - > + >{' '} {status === 'loading' ? ( Date: Wed, 3 Jul 2024 11:49:49 +0100 Subject: [PATCH 06/46] chore: migrated prop type definition into a types declaration file for components. other components prop types will live here --- frontend/src/components/TextArea.tsx | 17 ++--------------- frontend/src/components/types/index.ts | 23 +++++++++++++++++++++++ 2 files changed, 25 insertions(+), 15 deletions(-) create mode 100644 frontend/src/components/types/index.ts diff --git a/frontend/src/components/TextArea.tsx b/frontend/src/components/TextArea.tsx index b650423f..311efe81 100644 --- a/frontend/src/components/TextArea.tsx +++ b/frontend/src/components/TextArea.tsx @@ -1,18 +1,5 @@ import React, { useEffect, useRef } from 'react'; - -type Props = { - value: string | string[] | number; - isAutoFocused: boolean; - id?: string; - maxLength?: number; - name?: string; - placeholder?: string; - className?: string; - children?: React.ReactElement; - onChange: (e: React.ChangeEvent) => void; - onPaste?: (e: React.ClipboardEvent) => void; - onKeyDown?: (e: React.KeyboardEvent) => void; -}; +import { TextAreaProps } from './types'; const TextArea = ({ value, @@ -26,7 +13,7 @@ const TextArea = ({ onChange, onPaste, onKeyDown, -}: Props) => { +}: TextAreaProps) => { const textAreaRef = useRef(null); useEffect(() => { diff --git a/frontend/src/components/types/index.ts b/frontend/src/components/types/index.ts new file mode 100644 index 00000000..3cebc1c2 --- /dev/null +++ b/frontend/src/components/types/index.ts @@ -0,0 +1,23 @@ +export type TextAreaProps = { + value: string | string[] | number; + isAutoFocused: boolean; + id?: string; + maxLength?: number; + name?: string; + placeholder?: string; + className?: string; + children?: React.ReactElement; + onChange: ( + e: React.ChangeEvent, + ) => void; + onPaste?: ( + e: React.ClipboardEvent, + ) => void; + onKeyDown?: ( + e: React.KeyboardEvent, + ) => void; +}; + +export type InputProps = TextAreaProps & { + type: 'text' | 'number'; +}; From b21230c4d6899d921b3d95ed0973f1b924f59fba Mon Sep 17 00:00:00 2001 From: utin-francis-peter Date: Wed, 3 Jul 2024 12:34:13 +0100 Subject: [PATCH 07/46] chore: migrated to using custom Input component to address redundant twClasses --- frontend/src/components/Input.tsx | 39 +++++++++++++++++ frontend/src/components/types/index.ts | 3 +- frontend/src/preferences/APIKeyModal.tsx | 7 +-- frontend/src/preferences/PromptsModal.tsx | 15 ++++--- frontend/src/settings/APIKeys.tsx | 8 ++-- frontend/src/upload/Upload.tsx | 53 ++++++++++++----------- 6 files changed, 87 insertions(+), 38 deletions(-) create mode 100644 frontend/src/components/Input.tsx diff --git a/frontend/src/components/Input.tsx b/frontend/src/components/Input.tsx new file mode 100644 index 00000000..fe5cdba2 --- /dev/null +++ b/frontend/src/components/Input.tsx @@ -0,0 +1,39 @@ +import { InputProps } from './types'; + +const Input = ({ + id, + name, + type, + value, + isAutoFocused = false, + placeholder, + maxLength, + className, + hasSilverBorder, + children, + onChange, + onPaste, + onKeyDown, +}: InputProps) => { + return ( + + {children} + + ); +}; + +export default Input; diff --git a/frontend/src/components/types/index.ts b/frontend/src/components/types/index.ts index 3cebc1c2..3ee65ec4 100644 --- a/frontend/src/components/types/index.ts +++ b/frontend/src/components/types/index.ts @@ -1,6 +1,6 @@ export type TextAreaProps = { value: string | string[] | number; - isAutoFocused: boolean; + isAutoFocused?: boolean; id?: string; maxLength?: number; name?: string; @@ -20,4 +20,5 @@ export type TextAreaProps = { export type InputProps = TextAreaProps & { type: 'text' | 'number'; + hasSilverBorder?: boolean; }; diff --git a/frontend/src/preferences/APIKeyModal.tsx b/frontend/src/preferences/APIKeyModal.tsx index dd7a1b88..18cf53f3 100644 --- a/frontend/src/preferences/APIKeyModal.tsx +++ b/frontend/src/preferences/APIKeyModal.tsx @@ -4,6 +4,7 @@ import { ActiveState } from '../models/misc'; import { selectApiKey, setApiKey } from './preferenceSlice'; import { useMediaQuery, useOutsideAlerter } from './../hooks'; import Modal from '../modals'; +import Input from '../components/Input'; export default function APIKeyModal({ modalState, @@ -66,14 +67,14 @@ export default function APIKeyModal({ key for llm. Currently, we support only OpenAI but soon many more. You can find it here.

- setKey(e.target.value)} - /> + > ); }} diff --git a/frontend/src/preferences/PromptsModal.tsx b/frontend/src/preferences/PromptsModal.tsx index c16ddd2c..ce634c86 100644 --- a/frontend/src/preferences/PromptsModal.tsx +++ b/frontend/src/preferences/PromptsModal.tsx @@ -1,5 +1,6 @@ import { ActiveState } from '../models/misc'; import Exit from '../assets/exit.svg'; +import Input from '../components/Input'; function AddPrompt({ setModalState, @@ -34,13 +35,14 @@ function AddPrompt({ Add your custom prompt and save it to DocsGPT

- setNewPromptName(e.target.value)} - > + >
Prompt Name @@ -105,13 +107,14 @@ function EditPrompt({ Edit your custom prompt and save it to DocsGPT

- setEditPromptName(e.target.value)} - > + >
Prompt Name diff --git a/frontend/src/settings/APIKeys.tsx b/frontend/src/settings/APIKeys.tsx index 8264af08..dda68663 100644 --- a/frontend/src/settings/APIKeys.tsx +++ b/frontend/src/settings/APIKeys.tsx @@ -10,6 +10,7 @@ import { selectSourceDocs } from '../preferences/preferenceSlice'; import Exit from '../assets/exit.svg'; import Trash from '../assets/trash.svg'; import { useTranslation } from 'react-i18next'; +import Input from '../components/Input'; const apiHost = import.meta.env.VITE_API_HOST || 'https://docsapi.arc53.com'; const embeddingsName = import.meta.env.VITE_EMBEDDINGS_NAME || @@ -237,12 +238,13 @@ const CreateAPIKeyModal: React.FC = ({ {t('modals.createAPIKey.apiKeyName')} - setAPIKeyName(e.target.value)} - /> + >
) => { + const handleChange = ( + e: React.ChangeEvent, + ) => { const { name, value } = e.target; if (name === 'search_queries' && value.length > 0) { setRedditData({ @@ -323,12 +326,12 @@ function Upload({ {activeTab === 'file' && ( <> - setDocName(e.target.value)} - > + >
{t('modals.uploadDoc.name')} @@ -373,25 +376,25 @@ function Upload({ /> {urlType.label !== 'Reddit' ? ( <> - setUrlName(e.target.value)} - > + >
{t('modals.uploadDoc.name')}
- setUrl(e.target.value)} - > + >
{t('modals.uploadDoc.link')} @@ -400,66 +403,66 @@ function Upload({ ) : ( <> - + >
{t('modals.uploadDoc.reddit.id')}
- + >
{t('modals.uploadDoc.reddit.secret')}
- + >
{t('modals.uploadDoc.reddit.agent')}
- + >
{t('modals.uploadDoc.reddit.searchQueries')}
- + >
{t('modals.uploadDoc.reddit.numberOfPosts')} From 6e3bd5e6f36dd28741d40cf2b80172836e2692f3 Mon Sep 17 00:00:00 2001 From: utin-francis-peter Date: Wed, 3 Jul 2024 13:29:34 +0100 Subject: [PATCH 08/46] fix: adjusted alignment of submit query icon within its container --- frontend/src/conversation/Conversation.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/conversation/Conversation.tsx b/frontend/src/conversation/Conversation.tsx index 4fb34186..9493024e 100644 --- a/frontend/src/conversation/Conversation.tsx +++ b/frontend/src/conversation/Conversation.tsx @@ -257,9 +257,9 @@ export default function Conversation() { className="relative right-[38px] bottom-[24px] -mr-[30px] animate-spin cursor-pointer self-end bg-transparent" > ) : ( -
+
From b4bfed2ccb80296afb64f0418dd6c057c35fdf6a Mon Sep 17 00:00:00 2001 From: utin-francis-peter Date: Wed, 3 Jul 2024 15:46:35 +0100 Subject: [PATCH 09/46] style: query submission icon centering --- frontend/src/conversation/Conversation.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/conversation/Conversation.tsx b/frontend/src/conversation/Conversation.tsx index 9493024e..ca1ae9db 100644 --- a/frontend/src/conversation/Conversation.tsx +++ b/frontend/src/conversation/Conversation.tsx @@ -257,9 +257,9 @@ export default function Conversation() { className="relative right-[38px] bottom-[24px] -mr-[30px] animate-spin cursor-pointer self-end bg-transparent" > ) : ( -
+
From a41519be637b57cf102ef126399b72558cba7b5d Mon Sep 17 00:00:00 2001 From: utin-francis-peter Date: Fri, 5 Jul 2024 11:41:12 +0100 Subject: [PATCH 10/46] fix: minor typo --- frontend/src/components/TextArea.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/TextArea.tsx b/frontend/src/components/TextArea.tsx index 311efe81..43e91e79 100644 --- a/frontend/src/components/TextArea.tsx +++ b/frontend/src/components/TextArea.tsx @@ -36,7 +36,7 @@ const TextArea = ({ return ( + >
{status === 'loading' ? ( Date: Thu, 11 Jul 2024 21:45:47 +0530 Subject: [PATCH 17/46] conversation tile: add menu, add share modal --- application/api/user/routes.py | 3 +- frontend/package-lock.json | 10 ++ frontend/src/assets/red-trash.svg | 3 + frontend/src/assets/share.svg | 3 + frontend/src/assets/three-dots.svg | 3 + .../src/conversation/ConversationTile.tsx | 154 ++++++++++++++---- .../src/modals/ShareConversationModal.tsx | 93 +++++++++++ frontend/tailwind.config.cjs | 1 + 8 files changed, 238 insertions(+), 32 deletions(-) create mode 100644 frontend/src/assets/red-trash.svg create mode 100644 frontend/src/assets/share.svg create mode 100644 frontend/src/assets/three-dots.svg create mode 100644 frontend/src/modals/ShareConversationModal.tsx diff --git a/application/api/user/routes.py b/application/api/user/routes.py index 8db821fa..12744f0f 100644 --- a/application/api/user/routes.py +++ b/application/api/user/routes.py @@ -498,6 +498,7 @@ def delete_api_key(): #route to share conversation +##isPromptable should be passed through queries @user.route("/api/share",methods=["POST"]) def share_conversation(): try: @@ -526,7 +527,7 @@ def share_conversation(): "$ref":"conversations", "$id":ObjectId(conversation_id) } , - "isPromptable":isPromptable, + "isPromptable":isPromptable, "first_n_queries":current_n_queries, "user":user }) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 83e0a930..2b868da2 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -11,6 +11,7 @@ "@reduxjs/toolkit": "^1.9.2", "@vercel/analytics": "^0.1.10", "i18next": "^23.11.5", + "i18next-browser-languagedetector": "^8.0.0", "prop-types": "^15.8.1", "react": "^18.2.0", "react-copy-to-clipboard": "^5.1.0", @@ -4194,6 +4195,15 @@ "@babel/runtime": "^7.23.2" } }, + "node_modules/i18next-browser-languagedetector": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.0.0.tgz", + "integrity": "sha512-zhXdJXTTCoG39QsrOCiOabnWj2jecouOqbchu3EfhtSHxIB5Uugnm9JaizenOy39h7ne3+fLikIjeW88+rgszw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.2" + } + }, "node_modules/ignore": { "version": "5.2.4", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", diff --git a/frontend/src/assets/red-trash.svg b/frontend/src/assets/red-trash.svg new file mode 100644 index 00000000..b3331d95 --- /dev/null +++ b/frontend/src/assets/red-trash.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/src/assets/share.svg b/frontend/src/assets/share.svg new file mode 100644 index 00000000..4699e16b --- /dev/null +++ b/frontend/src/assets/share.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/src/assets/three-dots.svg b/frontend/src/assets/three-dots.svg new file mode 100644 index 00000000..6462b942 --- /dev/null +++ b/frontend/src/assets/three-dots.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/src/conversation/ConversationTile.tsx b/frontend/src/conversation/ConversationTile.tsx index b9d2301d..94731b90 100644 --- a/frontend/src/conversation/ConversationTile.tsx +++ b/frontend/src/conversation/ConversationTile.tsx @@ -5,11 +5,14 @@ import Exit from '../assets/exit.svg'; import Message from '../assets/message.svg'; import MessageDark from '../assets/message-dark.svg'; import { useDarkTheme } from '../hooks'; +import ConfirmationModal from '../modals/ConfirmationModal'; import CheckMark2 from '../assets/checkMark2.svg'; -import Trash from '../assets/trash.svg'; - +import Trash from '../assets/red-trash.svg'; +import Share from '../assets/share.svg'; +import threeDots from '../assets/three-dots.svg'; import { selectConversationId } from '../preferences/preferenceSlice'; - +import { ActiveState } from '../models/misc'; +import { ShareConversationModal } from '../modals/ShareConversationModal'; interface ConversationProps { name: string; id: string; @@ -32,13 +35,18 @@ export default function ConversationTile({ const [isDarkTheme] = useDarkTheme(); const [isEdit, setIsEdit] = useState(false); const [conversationName, setConversationsName] = useState(''); - + const [isOpen, setOpen] = useState(false); + const [isShareModalOpen, setShareModalState] = useState(false); + const [deleteModalState, setDeleteModalState] = + useState('INACTIVE'); + const menuRef = useRef(null); useEffect(() => { setConversationsName(conversation.name); }, [conversation.name]); function handleEditConversation() { setIsEdit(true); + setOpen(false); } function handleSaveConversation(changedConversation: ConversationProps) { @@ -50,6 +58,18 @@ export default function ConversationTile({ } } + const handleClickOutside = (event: MouseEvent) => { + if (menuRef.current && !menuRef.current.contains(event.target as Node)) { + setOpen(false); + } + }; + + useEffect(() => { + document.addEventListener('mousedown', handleClickOutside); + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, []); function onClear() { setConversationsName(conversation.name); setIsEdit(false); @@ -79,7 +99,7 @@ export default function ConversationTile({ setConversationsName(e.target.value)} /> @@ -90,36 +110,108 @@ export default function ConversationTile({ )}
{conversationId === conversation.id && ( -
- Edit { - event.stopPropagation(); - isEdit - ? handleSaveConversation({ +
+ {isEdit ? ( +
+ Edit { + event.stopPropagation(); + handleSaveConversation({ id: conversationId, name: conversationName, - }) - : handleEditConversation(); - }} - /> - Exit { - event.stopPropagation(); - isEdit ? onClear() : onDeleteConversation(conversation.id); - }} - /> + }); + }} + /> + Exit { + event.stopPropagation(); + onClear(); + }} + /> +
+ ) : ( + + )} + {isOpen && ( +
+ + + +
+ )}
)} + onDeleteConversation(conversation.id)} + submitLabel="Delete" + /> + {isShareModalOpen && conversationId && ( + { + setShareModalState(false); + }} + conversationId={conversationId} + /> + )}
); } diff --git a/frontend/src/modals/ShareConversationModal.tsx b/frontend/src/modals/ShareConversationModal.tsx new file mode 100644 index 00000000..886901b5 --- /dev/null +++ b/frontend/src/modals/ShareConversationModal.tsx @@ -0,0 +1,93 @@ +import { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import Spinner from '../assets/spinner.svg'; +import Exit from '../assets/exit.svg'; +const apiHost = import.meta.env.VITE_API_HOST || 'https://docsapi.arc53.com'; + +export const ShareConversationModal = ({ + close, + conversationId, +}: { + close: () => void; + conversationId: string; +}) => { + const [identifier, setIdentifier] = useState(null); + const [isCopied, setIsCopied] = useState(false); + type StatusType = 'loading' | 'idle' | 'fetched' | 'failed'; + const [status, setStatus] = useState('idle'); + const { t } = useTranslation(); + const domain = window.location.origin; + const handleCopyKey = (url: string) => { + navigator.clipboard.writeText(url); + setIsCopied(true); + }; + const shareCoversationPublicly: (isPromptable: boolean) => void = ( + isPromptable = false, + ) => { + setStatus('loading'); + fetch(`${apiHost}/api/share?isPromptable=${isPromptable}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ conversation_id: conversationId }), + }) + .then((res) => { + console.log(res.status); + return res.json(); + }) + .then((data) => { + if (data.success && data.identifier) { + setIdentifier(data.identifier); + setStatus('fetched'); + } else setStatus('failed'); + }) + .catch((err) => setStatus('failed')); + }; + return ( +
+
+ +
+

Create a public page to share

+

+ Source document, personal information and further conversation will + remain private +

+
+ {`${domain}/shared/${ + identifier ?? '....' + }`} + {status === 'fetched' ? ( + + ) : ( + + )} +
+
+
+
+ ); +}; diff --git a/frontend/tailwind.config.cjs b/frontend/tailwind.config.cjs index 63f99513..938eacee 100644 --- a/frontend/tailwind.config.cjs +++ b/frontend/tailwind.config.cjs @@ -48,6 +48,7 @@ module.exports = { 'soap':'#D8CCF1', 'independence':'#54546D', 'philippine-yellow':'#FFC700', + 'bright-gray':'#EBEBEB' }, }, }, From 019bf013ac6990c8a7b2eb9e9ab256c014fbe0e1 Mon Sep 17 00:00:00 2001 From: ManishMadan2882 Date: Fri, 12 Jul 2024 02:51:59 +0530 Subject: [PATCH 18/46] add css class: no-scrollbar --- frontend/src/index.css | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/frontend/src/index.css b/frontend/src/index.css index c1d80714..7e0b3bda 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -22,6 +22,18 @@ background: #b1afaf; } +@layer utilities { + /* Chrome, Safari and Opera */ + .no-scrollbar::-webkit-scrollbar { + display: none; + } + + .no-scrollbar { + -ms-overflow-style: none; /* IE and Edge */ + scrollbar-width: none; /* Firefox */ + } +} + /*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */ /* Document From 02187fed4e6ecc3156c111e31e39a9d916035c7a Mon Sep 17 00:00:00 2001 From: ManishMadan2882 Date: Sun, 14 Jul 2024 03:27:53 +0530 Subject: [PATCH 19/46] add timetamp in iso, remove sources --- application/api/user/routes.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/application/api/user/routes.py b/application/api/user/routes.py index 12744f0f..4b6bba8f 100644 --- a/application/api/user/routes.py +++ b/application/api/user/routes.py @@ -547,10 +547,15 @@ def get_publicly_shared_conversations(identifier : str): # Resolve the DBRef conversation_ref = shared['conversation_id'] conversation = db.dereference(conversation_ref) - conversation_queries = conversation['queries'][:(shared["first_n_queries"])] + if(conversation is None): + return jsonify({"sucess":False,"error":"might have broken url or the conversation does not exist"}),404 + conversation_queries = conversation['queries'][:(shared["first_n_queries"])] + for query in conversation_queries: + query.pop("sources") ## avoid exposing sources else: return jsonify({"sucess":False,"error":"might have broken url or the conversation does not exist"}),404 - return jsonify(conversation_queries),200 + date = conversation["_id"].generation_time.isoformat() + return jsonify({"success":True,"queries":conversation_queries,"title":conversation["name"],"timestamp":date}),200 except Exception as err: print (err) return jsonify({"success":False,"error":str(err)}),400 From 81d7fe3fdba409806a3620e51f315bc7fb816cf5 Mon Sep 17 00:00:00 2001 From: ManishMadan2882 Date: Sun, 14 Jul 2024 03:29:06 +0530 Subject: [PATCH 20/46] refactor App, add /shared/id page --- frontend/src/App.tsx | 87 +++++++++-- .../src/conversation/SharedConversation.tsx | 146 ++++++++++++++++++ frontend/src/hooks/index.ts | 14 +- .../src/modals/ShareConversationModal.tsx | 4 +- 4 files changed, 232 insertions(+), 19 deletions(-) create mode 100644 frontend/src/conversation/SharedConversation.tsx diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 3083f1f3..2aa8a8fc 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,4 +1,5 @@ import { Routes, Route } from 'react-router-dom'; +import { ReactElement, useEffect } from 'react'; import Navigation from './Navigation'; import Conversation from './conversation/Conversation'; import About from './About'; @@ -8,29 +9,93 @@ import { useMediaQuery } from './hooks'; import { useState } from 'react'; import Setting from './settings'; import './locale/i18n'; - +import SharedConversation from './conversation/SharedConversation'; +import { useDarkTheme } from './hooks'; inject(); -export default function App() { +function MainLayout({ children }: { children: ReactElement }) { const { isMobile } = useMediaQuery(); const [navOpen, setNavOpen] = useState(!isMobile); return ( -
+ <>
- - } /> - } /> - } /> - } /> - + {children}
-
+ + ); +} + +function Layout({ children }: { children: ReactElement }) { + return ( + <> +
{children}
+ + ); +} +export default function App() { + const [isDarkTheme] = useDarkTheme(); + useEffect(() => { + localStorage.setItem('selectedTheme', isDarkTheme ? 'Dark' : 'Light'); + if (isDarkTheme) { + document + .getElementById('root') + ?.classList.add('dark', 'dark:bg-raisin-black'); + } else { + document.getElementById('root')?.classList.remove('dark'); + } + }, [isDarkTheme]); + return ( + <> + + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + {/* default page */} + + + + } + /> + + ); } diff --git a/frontend/src/conversation/SharedConversation.tsx b/frontend/src/conversation/SharedConversation.tsx new file mode 100644 index 00000000..82e6be62 --- /dev/null +++ b/frontend/src/conversation/SharedConversation.tsx @@ -0,0 +1,146 @@ +import { useState, useEffect } from 'react'; +import { useParams } from 'react-router-dom'; +import { useNavigate } from 'react-router-dom'; +import { Query } from './conversationModels'; +import ConversationBubble from './ConversationBubble'; +import { Fragment } from 'react'; +const apiHost = import.meta.env.VITE_API_HOST || 'https://docsapi.arc53.com'; +const SharedConversation = () => { + const params = useParams(); + const navigate = useNavigate(); + const { identifier } = params; //identifier is a uuid, not conversationId + const [queries, setQueries] = useState([]); + const [title, setTitle] = useState(''); + const [date, setDate] = useState(''); + + function formatISODate(isoDateStr: string) { + const date = new Date(isoDateStr); + + const monthNames = [ + 'Jan', + 'Feb', + 'Mar', + 'Apr', + 'May', + 'June', + 'July', + 'Aug', + 'Sept', + 'Oct', + 'Nov', + 'Dec', + ]; + + const month = monthNames[date.getMonth()]; + const day = date.getDate(); + const year = date.getFullYear(); + + let hours = date.getHours(); + const minutes = date.getMinutes(); + const ampm = hours >= 12 ? 'PM' : 'AM'; + + hours = hours % 12; + hours = hours ? hours : 12; + const minutesStr = minutes < 10 ? '0' + minutes : minutes; + const formattedDate = `Published ${month} ${day}, ${year} at ${hours}:${minutesStr} ${ampm}`; + return formattedDate; + } + const fetchQueris = () => { + fetch(`${apiHost}/api/shared_conversation/${identifier}`) + .then((res) => { + if (res.status === 404 || res.status === 400) navigate('/pagenotfound'); + return res.json(); + }) + .then((data) => { + if (data.success) { + setQueries(data.queries); + setTitle(data.title); + setDate(formatISODate(data.timestamp)); + } + }); + }; + + const prepResponseView = (query: Query, index: number) => { + let responseView; + if (query.response) { + responseView = ( + + ); + } else if (query.error) { + responseView = ( + + ); + } + return responseView; + }; + useEffect(() => { + fetchQueris(); + }, []); + return ( +
+
+ {queries.length > 0 && ( +
+
+
+

+ {title} +

+

+ Created with{' '} + + DocsGPT + +

+

+ {date} +

+
+
+ {queries.map((query, index) => { + return ( + + + + {prepResponseView(query, index)} + + ); + })} +
+
+
+ )} +
+ + + This is a chatbot that uses the GPT-3, Faiss and LangChain to answer + questions. + +
+
+
+ ); +}; + +export default SharedConversation; diff --git a/frontend/src/hooks/index.ts b/frontend/src/hooks/index.ts index 6248b3f8..c8258ad2 100644 --- a/frontend/src/hooks/index.ts +++ b/frontend/src/hooks/index.ts @@ -77,21 +77,23 @@ export function useDarkTheme() { // Set dark mode based on local storage preference if (savedMode === 'Dark') { setIsDarkTheme(true); - document.documentElement.classList.add('dark'); - document.documentElement.classList.add('dark:bg-raisin-black'); + document + .getElementById('root') + ?.classList.add('dark', 'dark:bg-raisin-black'); } else { // If no preference found, set to default (light mode) setIsDarkTheme(false); - document.documentElement.classList.remove('dark'); + document.getElementById('root')?.classList.remove('dark'); } }, []); useEffect(() => { localStorage.setItem('selectedTheme', isDarkTheme ? 'Dark' : 'Light'); if (isDarkTheme) { - document.documentElement.classList.add('dark'); - document.documentElement.classList.add('dark:bg-raisin-black'); + document + .getElementById('root') + ?.classList.add('dark', 'dark:bg-raisin-black'); } else { - document.documentElement.classList.remove('dark'); + document.getElementById('root')?.classList.remove('dark'); } }, [isDarkTheme]); //method to toggle theme diff --git a/frontend/src/modals/ShareConversationModal.tsx b/frontend/src/modals/ShareConversationModal.tsx index 886901b5..d32c5f90 100644 --- a/frontend/src/modals/ShareConversationModal.tsx +++ b/frontend/src/modals/ShareConversationModal.tsx @@ -57,13 +57,13 @@ export const ShareConversationModal = ({ remain private

- {`${domain}/shared/${ + {`${domain}/share/${ identifier ?? '....' }`} {status === 'fetched' ? (
)}
)} onDeleteConversation(conversation.id)} - submitLabel="Delete" + submitLabel={t('convTile.delete')} /> {isShareModalOpen && conversationId && ( { const [queries, setQueries] = useState([]); const [title, setTitle] = useState(''); const [date, setDate] = useState(''); - + const { t } = useTranslation(); function formatISODate(isoDateStr: string) { const date = new Date(isoDateStr); @@ -97,7 +98,7 @@ const SharedConversation = () => { {title}

- Created with{' '} + {t('sharedConv.subtitle')}{' '} DocsGPT @@ -131,11 +132,10 @@ const SharedConversation = () => { onClick={() => navigate('/')} className="w-fit rounded-full bg-purple-30 p-4 text-white shadow-xl transition-colors duration-200 hover:bg-purple-taupe" > - Get Started with DocsGPT + {t('sharedConv.button')} - This is a chatbot that uses the GPT-3, Faiss and LangChain to answer - questions. + {t('sharedConv.meta')}

diff --git a/frontend/src/locale/en.json b/frontend/src/locale/en.json index e59fbedc..e914693a 100644 --- a/frontend/src/locale/en.json +++ b/frontend/src/locale/en.json @@ -103,6 +103,22 @@ "deleteConv": { "confirm": "Are you sure you want to delete all the conversations?", "delete": "Delete" + }, + "shareConv": { + "label": "Create a public page to share", + "note": "Source document, personal information and further conversation will remain private", + "create": "Create" } + }, + "sharedConv": { + "subtitle": "Created with", + "button": "Get Started with DocsGPT", + "meta": "This is a chatbot that uses the GPT-3, Faiss and LangChain to answer questions." + }, + "convTile": { + "share": "Share", + "delete": "Delete", + "rename": "Rename", + "deleteWarning": "Are you sure you want to delete this conversation?" } } diff --git a/frontend/src/locale/es.json b/frontend/src/locale/es.json index 70966e4b..78a4aa29 100644 --- a/frontend/src/locale/es.json +++ b/frontend/src/locale/es.json @@ -103,6 +103,22 @@ "deleteConv": { "confirm": "¿Está seguro de que desea eliminar todas las conversaciones?", "delete": "Eliminar" + }, + "shareConv": { + "label": "Crear una página pública para compartir", + "note": "El documento original, la información personal y las conversaciones posteriores permanecerán privadas", + "create": "Crear" } + }, + "sharedConv": { + "subtitle": "Creado con", + "button": "Comienza con DocsGPT", + "meta": "Este es un chatbot que utiliza GPT-3, Faiss y LangChain para responder preguntas." + }, + "convTile": { + "share": "Compartir", + "delete": "Eliminar", + "rename": "Renombrar", + "deleteWarning": "¿Está seguro de que desea eliminar esta conversación?" } } diff --git a/frontend/src/locale/jp.json b/frontend/src/locale/jp.json index a025a17a..697b137f 100644 --- a/frontend/src/locale/jp.json +++ b/frontend/src/locale/jp.json @@ -103,6 +103,22 @@ "deleteConv": { "confirm": "すべての会話を削除してもよろしいですか?", "delete": "削除" + }, + "shareConv": { + "label": "共有ページを作成して共有する", + "note": "ソースドキュメント、個人情報、および以降の会話は非公開のままになります", + "create": "作成" } + }, + "sharedConv": { + "subtitle": "作成者", + "button": "DocsGPT を始める", + "meta": "GPT-3、Faiss、および LangChain を使用して質問に答えるチャットボットです" + }, + "convTile": { + "share": "共有", + "delete": "削除", + "rename": "名前変更", + "deleteWarning": "この会話を削除してもよろしいですか?" } } diff --git a/frontend/src/locale/zh.json b/frontend/src/locale/zh.json index f686ea20..8161f740 100644 --- a/frontend/src/locale/zh.json +++ b/frontend/src/locale/zh.json @@ -103,6 +103,22 @@ "deleteConv": { "confirm": "您确定要删除所有对话吗?", "delete": "删除" + }, + "shareConv": { + "label": "创建用于分享的公共页面", + "note": "源文档、个人信息和后续对话将保持私密", + "create": "创建" } + }, + "sharedConv": { + "subtitle": "使用创建", + "button": "开始使用 DocsGPT", + "meta": "这是一个使用 GPT-3、Faiss 和 LangChain 来回答问题的聊天机器人。" + }, + "convTile": { + "share": "分享", + "delete": "删除", + "rename": "重命名", + "deleteWarning": "您确定要删除此对话吗?" } } diff --git a/frontend/src/modals/ShareConversationModal.tsx b/frontend/src/modals/ShareConversationModal.tsx index d32c5f90..a6ab1e0f 100644 --- a/frontend/src/modals/ShareConversationModal.tsx +++ b/frontend/src/modals/ShareConversationModal.tsx @@ -51,11 +51,8 @@ export const ShareConversationModal = ({
-

Create a public page to share

-

- Source document, personal information and further conversation will - remain private -

+

{t('modals.shareConv.label')}

+

{t('modals.shareConv.note')}

{`${domain}/share/${ identifier ?? '....' @@ -76,7 +73,7 @@ export const ShareConversationModal = ({ shareCoversationPublicly(false); }} > - Create + {t('modals.shareConv.create')} {status === 'loading' && ( Date: Mon, 15 Jul 2024 02:55:38 +0530 Subject: [PATCH 22/46] feedback visible conditioned, update meta info in shared --- frontend/src/App.tsx | 6 +- .../src/conversation/ConversationBubble.tsx | 122 +++++++++--------- .../src/conversation/SharedConversation.tsx | 94 +++++++------- frontend/src/locale/en.json | 2 +- frontend/src/locale/es.json | 2 +- frontend/src/locale/jp.json | 2 +- frontend/src/locale/zh.json | 2 +- 7 files changed, 114 insertions(+), 116 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 2aa8a8fc..38694182 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -33,11 +33,7 @@ function MainLayout({ children }: { children: ReactElement }) { } function Layout({ children }: { children: ReactElement }) { - return ( - <> -
{children}
- - ); + return
{children}
; } export default function App() { const [isDarkTheme] = useDarkTheme(); diff --git a/frontend/src/conversation/ConversationBubble.tsx b/frontend/src/conversation/ConversationBubble.tsx index fc5d469e..4adfbf94 100644 --- a/frontend/src/conversation/ConversationBubble.tsx +++ b/frontend/src/conversation/ConversationBubble.tsx @@ -1,6 +1,6 @@ import { forwardRef, useState } from 'react'; import Avatar from '../components/Avatar'; -import CoppyButton from '../components/CopyButton'; +import CopyButton from '../components/CopyButton'; import remarkGfm from 'remark-gfm'; import { FEEDBACK, MESSAGE_TYPE } from './conversationModels'; import classes from './ConversationBubble.module.css'; @@ -103,7 +103,7 @@ const ConversationBubble = forwardRef< className={`absolute right-3 top-3 lg:invisible ${type !== 'ERROR' ? 'group-hover:lg:visible' : ''} `} > -
@@ -215,78 +215,82 @@ const ConversationBubble = forwardRef< ${type !== 'ERROR' ? 'group-hover:lg:visible' : ''}`} >
- +
-
-
+ {handleFeedback && ( + <>
- +
+ { - handleFeedback?.('LIKE'); - setIsLikeClicked(true); - setIsDislikeClicked(false); - }} - onMouseEnter={() => setIsLikeHovered(true)} - onMouseLeave={() => setIsLikeHovered(false)} - > + onClick={() => { + handleFeedback?.('LIKE'); + setIsLikeClicked(true); + setIsDislikeClicked(false); + }} + onMouseEnter={() => setIsLikeHovered(true)} + onMouseLeave={() => setIsLikeHovered(false)} + > +
+
-
-
-
-
- { - handleFeedback?.('DISLIKE'); - setIsDislikeClicked(true); - setIsLikeClicked(false); - }} - onMouseEnter={() => setIsDislikeHovered(true)} - onMouseLeave={() => setIsDislikeHovered(false)} - > +
+
+ { + handleFeedback?.('DISLIKE'); + setIsDislikeClicked(true); + setIsLikeClicked(false); + }} + onMouseEnter={() => setIsDislikeHovered(true)} + onMouseLeave={() => setIsDislikeHovered(false)} + > +
+
-
-
+ + )}
{sources && openSource !== null && sources[openSource] && ( diff --git a/frontend/src/conversation/SharedConversation.tsx b/frontend/src/conversation/SharedConversation.tsx index ff9ab3fb..1b9fb781 100644 --- a/frontend/src/conversation/SharedConversation.tsx +++ b/frontend/src/conversation/SharedConversation.tsx @@ -87,58 +87,56 @@ const SharedConversation = () => { useEffect(() => { fetchQueris(); }, []); - return ( -
-
- {queries.length > 0 && ( -
-
-
-

- {title} -

-

- {t('sharedConv.subtitle')}{' '} - - DocsGPT - -

-

- {date} -

-
-
- {queries.map((query, index) => { - return ( - - - {prepResponseView(query, index)} - - ); - })} -
-
+ return ( +
+
+
+
+

+ {title} +

+

+ {t('sharedConv.subtitle')}{' '} + + DocsGPT + +

+

+ {date} +

+
+
+ {queries?.map((query, index) => { + return ( + + + + {prepResponseView(query, index)} + + ); + })}
- )} -
- - - {t('sharedConv.meta')} -
+ +
+ + + {t('sharedConv.meta')} + +
); }; diff --git a/frontend/src/locale/en.json b/frontend/src/locale/en.json index e914693a..0f5aa708 100644 --- a/frontend/src/locale/en.json +++ b/frontend/src/locale/en.json @@ -113,7 +113,7 @@ "sharedConv": { "subtitle": "Created with", "button": "Get Started with DocsGPT", - "meta": "This is a chatbot that uses the GPT-3, Faiss and LangChain to answer questions." + "meta": "DocsGPT uses GenAI, please review critical information using sources." }, "convTile": { "share": "Share", diff --git a/frontend/src/locale/es.json b/frontend/src/locale/es.json index 78a4aa29..b91b67e9 100644 --- a/frontend/src/locale/es.json +++ b/frontend/src/locale/es.json @@ -113,7 +113,7 @@ "sharedConv": { "subtitle": "Creado con", "button": "Comienza con DocsGPT", - "meta": "Este es un chatbot que utiliza GPT-3, Faiss y LangChain para responder preguntas." + "meta": "DocsGPT utiliza GenAI, por favor revise la información crítica utilizando fuentes." }, "convTile": { "share": "Compartir", diff --git a/frontend/src/locale/jp.json b/frontend/src/locale/jp.json index 697b137f..75e62589 100644 --- a/frontend/src/locale/jp.json +++ b/frontend/src/locale/jp.json @@ -113,7 +113,7 @@ "sharedConv": { "subtitle": "作成者", "button": "DocsGPT を始める", - "meta": "GPT-3、Faiss、および LangChain を使用して質問に答えるチャットボットです" + "meta": "DocsGPT は GenAI を使用しています、情報源を使用して重要情報を確認してください。" }, "convTile": { "share": "共有", diff --git a/frontend/src/locale/zh.json b/frontend/src/locale/zh.json index 8161f740..e46fe254 100644 --- a/frontend/src/locale/zh.json +++ b/frontend/src/locale/zh.json @@ -113,7 +113,7 @@ "sharedConv": { "subtitle": "使用创建", "button": "开始使用 DocsGPT", - "meta": "这是一个使用 GPT-3、Faiss 和 LangChain 来回答问题的聊天机器人。" + "meta": "DocsGPT 使用 GenAI,请使用资源查看关键信息。" }, "convTile": { "share": "分享", From 7b8458b47de2cf708dac742e84972af0c582e39e Mon Sep 17 00:00:00 2001 From: ManishMadan2882 Date: Mon, 15 Jul 2024 05:00:13 +0530 Subject: [PATCH 23/46] fix layout --- frontend/src/App.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 38694182..dff27112 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -20,7 +20,7 @@ function MainLayout({ children }: { children: ReactElement }) { <>
Date: Mon, 15 Jul 2024 05:13:28 +0530 Subject: [PATCH 24/46] minor fix --- frontend/src/App.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index dff27112..99a63cbc 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -17,10 +17,10 @@ function MainLayout({ children }: { children: ReactElement }) { const { isMobile } = useMediaQuery(); const [navOpen, setNavOpen] = useState(!isMobile); return ( - <> +
{children}
- +
); } From 1107a2f2bcdb2bff164b42d6cc8a42eb703662c6 Mon Sep 17 00:00:00 2001 From: ManishMadan2882 Date: Mon, 15 Jul 2024 17:56:23 +0530 Subject: [PATCH 25/46] refactor App.tsx: better convention --- frontend/src/App.tsx | 58 ++++--------------- frontend/src/PageNotFound.tsx | 6 +- .../src/conversation/SharedConversation.tsx | 2 +- 3 files changed, 15 insertions(+), 51 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 99a63cbc..05792187 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,5 +1,5 @@ import { Routes, Route } from 'react-router-dom'; -import { ReactElement, useEffect } from 'react'; +import { useEffect } from 'react'; import Navigation from './Navigation'; import Conversation from './conversation/Conversation'; import About from './About'; @@ -9,11 +9,12 @@ import { useMediaQuery } from './hooks'; import { useState } from 'react'; import Setting from './settings'; import './locale/i18n'; +import { Outlet } from 'react-router-dom'; import SharedConversation from './conversation/SharedConversation'; import { useDarkTheme } from './hooks'; inject(); -function MainLayout({ children }: { children: ReactElement }) { +function MainLayout() { const { isMobile } = useMediaQuery(); const [navOpen, setNavOpen] = useState(!isMobile); return ( @@ -26,15 +27,12 @@ function MainLayout({ children }: { children: ReactElement }) { : 'ml-0 md:ml-16' }`} > - {children} +
); } -function Layout({ children }: { children: ReactElement }) { - return
{children}
; -} export default function App() { const [isDarkTheme] = useDarkTheme(); useEffect(() => { @@ -50,47 +48,13 @@ export default function App() { return ( <> - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> - {/* default page */} - - - - } - /> + }> + } /> + } /> + } /> + + } /> + } /> ); diff --git a/frontend/src/PageNotFound.tsx b/frontend/src/PageNotFound.tsx index eaea5cc9..0b86d7c1 100644 --- a/frontend/src/PageNotFound.tsx +++ b/frontend/src/PageNotFound.tsx @@ -2,11 +2,11 @@ import { Link } from 'react-router-dom'; export default function PageNotFound() { return ( -
-

+

+

404

The page you are looking for does not exist.

-

diff --git a/frontend/src/conversation/SharedConversation.tsx b/frontend/src/conversation/SharedConversation.tsx index 1b9fb781..e365c6f0 100644 --- a/frontend/src/conversation/SharedConversation.tsx +++ b/frontend/src/conversation/SharedConversation.tsx @@ -89,7 +89,7 @@ const SharedConversation = () => { }, []); return ( -
+
From e2b76d9c2963640a9a846f7dfe831c97d859ff1b Mon Sep 17 00:00:00 2001 From: ManishMadan2882 Date: Tue, 16 Jul 2024 02:09:36 +0530 Subject: [PATCH 26/46] feat(share): share btn above conversations --- frontend/src/conversation/Conversation.tsx | 31 +++++++++++++++++++ .../src/modals/ShareConversationModal.tsx | 2 +- 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/frontend/src/conversation/Conversation.tsx b/frontend/src/conversation/Conversation.tsx index 487e8ea7..115abcd8 100644 --- a/frontend/src/conversation/Conversation.tsx +++ b/frontend/src/conversation/Conversation.tsx @@ -11,6 +11,7 @@ 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'; @@ -20,9 +21,13 @@ import { sendFeedback } from './conversationApi'; import { useTranslation } from 'react-i18next'; import ArrowDown from './../assets/arrow-down.svg'; import RetryIcon from '../components/RetryIcon'; +import ShareIcon from '../assets/share.svg'; +import { ShareConversationModal } from '../modals/ShareConversationModal'; + export default function Conversation() { const queries = useSelector(selectQueries); const status = useSelector(selectStatus); + const conversationId = useSelector(selectConversationId); const dispatch = useDispatch(); const endMessageRef = useRef(null); const inputRef = useRef(null); @@ -31,6 +36,7 @@ export default function Conversation() { const fetchStream = useRef(null); const [eventInterrupt, setEventInterrupt] = useState(false); const [lastQueryReturnedErr, setLastQueryReturnedErr] = useState(false); + const [isShareModalOpen, setShareModalState] = useState(false); const { t } = useTranslation(); const handleUserInterruption = () => { @@ -192,6 +198,31 @@ export default function Conversation() { return (
+ {conversationId && ( + <> + + {isShareModalOpen && ( + { + setShareModalState(false); + }} + conversationId={conversationId} + /> + )} + + )}
) : (
); } @@ -164,6 +150,10 @@ function Upload({ failed: false, }, ); + setDocName(''); + setfiles([]); + setProgress(undefined); + setModalState('INACTIVE'); } } else if (data.status == 'PROGRESS') { setProgress( From b2fffb2e232641afba4fa4747a818095f1dfa80a Mon Sep 17 00:00:00 2001 From: ManishMadan2882 Date: Fri, 19 Jul 2024 03:24:35 +0530 Subject: [PATCH 32/46] feat(share): enable route to share promptable conv --- application/api/user/routes.py | 85 ++++++++++++++++++++++++++++++---- 1 file changed, 75 insertions(+), 10 deletions(-) diff --git a/application/api/user/routes.py b/application/api/user/routes.py index e67eb992..0f9f5829 100644 --- a/application/api/user/routes.py +++ b/application/api/user/routes.py @@ -502,24 +502,88 @@ def delete_api_key(): def share_conversation(): try: data = request.get_json() - user = "local" - if(hasattr(data,"user")): - user = data["user"] + user = "local" if "user" not in data else data["user"] conversation_id = data["conversation_id"] isPromptable = request.args.get("isPromptable").lower() == "true" + conversation = conversations_collection.find_one({"_id": ObjectId(conversation_id)}) current_n_queries = len(conversation["queries"]) + + ##generate binary representation of uuid + explicit_binary = Binary.from_uuid(uuid.uuid4(), UuidRepresentation.STANDARD) + + if(isPromptable): + source = "default" if "source" not in data else data["source"] + prompt_id = "default" if "prompt_id" not in data else data["prompt_id"] + chunks = "2" if "chunks" not in data else data["chunks"] + + name = conversation["name"]+"(shared)" + pre_existing_api_document = api_key_collection.find_one({ + "prompt_id":prompt_id, + "chunks":chunks, + "source":source, + "user":user + }) + api_uuid = str(uuid.uuid4()) + if(pre_existing_api_document): + api_uuid = pre_existing_api_document["key"] + pre_existing = shared_conversations_collections.find_one({ + "conversation_id":DBRef("conversations",ObjectId(conversation_id)), + "isPromptable":isPromptable, + "first_n_queries":current_n_queries, + "user":user, + "api_key":api_uuid + }) + if(pre_existing is not None): + return jsonify({"success":True, "identifier":str(pre_existing["uuid"].as_uuid())}),200 + else: + shared_conversations_collections.insert_one({ + "uuid":explicit_binary, + "conversation_id": { + "$ref":"conversations", + "$id":ObjectId(conversation_id) + } , + "isPromptable":isPromptable, + "first_n_queries":current_n_queries, + "user":user, + "api_key":api_uuid + }) + return jsonify({"success":True,"identifier":str(explicit_binary.as_uuid())}) + else: + api_key_collection.insert_one( + { + "name": name, + "key": api_uuid, + "source": source, + "user": user, + "prompt_id": prompt_id, + "chunks": chunks, + } + ) + shared_conversations_collections.insert_one({ + "uuid":explicit_binary, + "conversation_id": { + "$ref":"conversations", + "$id":ObjectId(conversation_id) + } , + "isPromptable":isPromptable, + "first_n_queries":current_n_queries, + "user":user, + "api_key":api_uuid + }) + ## Identifier as route parameter in frontend + return jsonify({"success":True, "identifier":str(explicit_binary.as_uuid())}),201 + + ##isPromptable = False pre_existing = shared_conversations_collections.find_one({ "conversation_id":DBRef("conversations",ObjectId(conversation_id)), "isPromptable":isPromptable, - "first_n_queries":current_n_queries + "first_n_queries":current_n_queries, + "user":user }) - print("pre_existing",pre_existing) if(pre_existing is not None): - explicit_binary = pre_existing["uuid"] - return jsonify({"success":True, "identifier":str(explicit_binary.as_uuid())}),200 - else: - explicit_binary = Binary.from_uuid(uuid.uuid4(), UuidRepresentation.STANDARD) + return jsonify({"success":True, "identifier":str(pre_existing["uuid"].as_uuid())}),200 + else: shared_conversations_collections.insert_one({ "uuid":explicit_binary, "conversation_id": { @@ -532,7 +596,8 @@ def share_conversation(): }) ## Identifier as route parameter in frontend return jsonify({"success":True, "identifier":str(explicit_binary.as_uuid())}),201 - except Exception as err: + except ArithmeticError as err: + print (err) return jsonify({"success":False,"error":str(err)}),400 #route to get publicly shared conversations From 0cf86d3bbc7eb985cd47ebb7aec16b2ca5188ee2 Mon Sep 17 00:00:00 2001 From: ManishMadan2882 Date: Sat, 20 Jul 2024 03:34:34 +0530 Subject: [PATCH 33/46] share api key through response --- application/api/user/routes.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/application/api/user/routes.py b/application/api/user/routes.py index 0f9f5829..a26ddc51 100644 --- a/application/api/user/routes.py +++ b/application/api/user/routes.py @@ -596,7 +596,7 @@ def share_conversation(): }) ## Identifier as route parameter in frontend return jsonify({"success":True, "identifier":str(explicit_binary.as_uuid())}),201 - except ArithmeticError as err: + except Exception as err: print (err) return jsonify({"success":False,"error":str(err)}),400 @@ -619,13 +619,15 @@ def get_publicly_shared_conversations(identifier : str): else: return jsonify({"sucess":False,"error":"might have broken url or the conversation does not exist"}),404 date = conversation["_id"].generation_time.isoformat() - return jsonify({ + res = { "success":True, "queries":conversation_queries, "title":conversation["name"], "timestamp":date - }), 200 + } + if(shared["isPromptable"] and "api_key" in shared): + res["api_key"] = shared["api_key"] + return jsonify(res), 200 except Exception as err: print (err) - return jsonify({"success":False,"error":str(err)}),400 - \ No newline at end of file + return jsonify({"success":False,"error":str(err)}),400 \ No newline at end of file From 052669a0b06d8accea9bd57615831fdb93d04ff5 Mon Sep 17 00:00:00 2001 From: ManishMadan2882 Date: Wed, 24 Jul 2024 02:01:32 +0530 Subject: [PATCH 34/46] complete_stream: no storing for api key req, fe:update shared conv --- application/api/answer/routes.py | 15 +- frontend/src/components/SourceDropdown.tsx | 2 +- frontend/src/conversation/Conversation.tsx | 2 +- .../src/conversation/SharedConversation.tsx | 68 +++++++-- frontend/src/conversation/conversationApi.ts | 73 ++++++++++ .../conversation/sharedConversationSlice.ts | 0 .../src/modals/ShareConversationModal.tsx | 135 ++++++++++++++++-- 7 files changed, 264 insertions(+), 31 deletions(-) create mode 100644 frontend/src/conversation/sharedConversationSlice.ts diff --git a/application/api/answer/routes.py b/application/api/answer/routes.py index 9559133c..7eed8434 100644 --- a/application/api/answer/routes.py +++ b/application/api/answer/routes.py @@ -189,13 +189,14 @@ def complete_stream(question, retriever, conversation_id, user_api_key): llm = LLMCreator.create_llm( settings.LLM_NAME, api_key=settings.API_KEY, user_api_key=user_api_key ) - conversation_id = save_conversation( - conversation_id, question, response_full, source_log_docs, llm - ) - - # send data.type = "end" to indicate that the stream has ended as json - data = json.dumps({"type": "id", "id": str(conversation_id)}) - yield f"data: {data}\n\n" + if(user_api_key is None): + conversation_id = save_conversation( + conversation_id, question, response_full, source_log_docs, llm + ) + # send data.type = "end" to indicate that the stream has ended as json + data = json.dumps({"type": "id", "id": str(conversation_id)}) + yield f"data: {data}\n\n" + data = json.dumps({"type": "end"}) yield f"data: {data}\n\n" except Exception as e: diff --git a/frontend/src/components/SourceDropdown.tsx b/frontend/src/components/SourceDropdown.tsx index 6983fe76..ce130b4d 100644 --- a/frontend/src/components/SourceDropdown.tsx +++ b/frontend/src/components/SourceDropdown.tsx @@ -77,7 +77,7 @@ function SourceDropdown({ /> {isDocsListOpen && ( -
+
{options ? ( options.map((option: any, index: number) => { if (option.model === embeddingsName) { diff --git a/frontend/src/conversation/Conversation.tsx b/frontend/src/conversation/Conversation.tsx index 1b53ab2f..dddd945e 100644 --- a/frontend/src/conversation/Conversation.tsx +++ b/frontend/src/conversation/Conversation.tsx @@ -193,7 +193,7 @@ export default function Conversation() { const handlePaste = (e: React.ClipboardEvent) => { e.preventDefault(); const text = e.clipboardData.getData('text/plain'); - document.execCommand('insertText', false, text); + inputRef.current && (inputRef.current.innerText = text); }; return ( diff --git a/frontend/src/conversation/SharedConversation.tsx b/frontend/src/conversation/SharedConversation.tsx index e365c6f0..9a2d8d16 100644 --- a/frontend/src/conversation/SharedConversation.tsx +++ b/frontend/src/conversation/SharedConversation.tsx @@ -1,9 +1,12 @@ -import { useState, useEffect } from 'react'; +import { useState, useEffect, useRef } from 'react'; import { useParams } from 'react-router-dom'; import { useNavigate } from 'react-router-dom'; import { Query } from './conversationModels'; import { useTranslation } from 'react-i18next'; import ConversationBubble from './ConversationBubble'; +import Send from '../assets/send.svg'; +import Spinner from '../assets/spinner.svg'; + import { Fragment } from 'react'; const apiHost = import.meta.env.VITE_API_HOST || 'https://docsapi.arc53.com'; const SharedConversation = () => { @@ -13,6 +16,8 @@ const SharedConversation = () => { const [queries, setQueries] = useState([]); const [title, setTitle] = useState(''); const [date, setDate] = useState(''); + const [apiKey, setAPIKey] = useState(null); + const inputRef = useRef(null); const { t } = useTranslation(); function formatISODate(isoDateStr: string) { const date = new Date(isoDateStr); @@ -57,10 +62,15 @@ const SharedConversation = () => { setQueries(data.queries); setTitle(data.title); setDate(formatISODate(data.timestamp)); + data.api_key && setAPIKey(data.api_key); } }); }; - + const handlePaste = (e: React.ClipboardEvent) => { + e.preventDefault(); + const text = e.clipboardData.getData('text/plain'); + inputRef.current && (inputRef.current.innerText = text); + }; const prepResponseView = (query: Query, index: number) => { let responseView; if (query.response) { @@ -126,17 +136,51 @@ const SharedConversation = () => {
-
- - - {t('sharedConv.meta')} - +
+ {apiKey ? ( +
+
{ + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + //handleQuestionSubmission(); + } + }} + >
+ {status === 'loading' ? ( + + ) : ( +
+ +
+ )} +
+ ) : ( + + )}
+ + {t('sharedConv.meta')} +
); }; diff --git a/frontend/src/conversation/conversationApi.ts b/frontend/src/conversation/conversationApi.ts index e107abc8..3010a63d 100644 --- a/frontend/src/conversation/conversationApi.ts +++ b/frontend/src/conversation/conversationApi.ts @@ -233,3 +233,76 @@ export function sendFeedback( } }); } + +export function fetchSharedAnswerSteaming( //for shared conversations + question: string, + signal: AbortSignal, + apiKey: string, + history: Array = [], + onEvent: (event: MessageEvent) => void, +): Promise { + history = history.map((item) => { + return { prompt: item.prompt, response: item.response }; + }); + + return new Promise((resolve, reject) => { + const body = { + question: question, + history: JSON.stringify(history), + apiKey: apiKey, + }; + fetch(apiHost + '/stream', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(body), + signal, + }) + .then((response) => { + if (!response.body) throw Error('No response body'); + + const reader = response.body.getReader(); + const decoder = new TextDecoder('utf-8'); + let counterrr = 0; + const processStream = ({ + done, + value, + }: ReadableStreamReadResult) => { + if (done) { + console.log(counterrr); + return; + } + + counterrr += 1; + + const chunk = decoder.decode(value); + + const lines = chunk.split('\n'); + + for (let line of lines) { + if (line.trim() == '') { + continue; + } + if (line.startsWith('data:')) { + line = line.substring(5); + } + + const messageEvent: MessageEvent = new MessageEvent('message', { + data: line, + }); + + onEvent(messageEvent); // handle each message + } + + reader.read().then(processStream).catch(reject); + }; + + reader.read().then(processStream).catch(reject); + }) + .catch((error) => { + console.error('Connection failed:', error); + reject(error); + }); + }); +} diff --git a/frontend/src/conversation/sharedConversationSlice.ts b/frontend/src/conversation/sharedConversationSlice.ts new file mode 100644 index 00000000..e69de29b diff --git a/frontend/src/modals/ShareConversationModal.tsx b/frontend/src/modals/ShareConversationModal.tsx index 37da934d..cffe477b 100644 --- a/frontend/src/modals/ShareConversationModal.tsx +++ b/frontend/src/modals/ShareConversationModal.tsx @@ -1,8 +1,22 @@ import { useState } from 'react'; import { useTranslation } from 'react-i18next'; +import { useSelector } from 'react-redux'; +import { + selectSourceDocs, + selectSelectedDocs, + selectChunks, + selectPrompt, +} from '../preferences/preferenceSlice'; +import Dropdown from '../components/Dropdown'; +import { Doc } from '../models/misc'; import Spinner from '../assets/spinner.svg'; import Exit from '../assets/exit.svg'; const apiHost = import.meta.env.VITE_API_HOST || 'https://docsapi.arc53.com'; +const embeddingsName = + import.meta.env.VITE_EMBEDDINGS_NAME || + 'huggingface_sentence-transformers/all-mpnet-base-v2'; + +type StatusType = 'loading' | 'idle' | 'fetched' | 'failed'; export const ShareConversationModal = ({ close, @@ -11,26 +25,87 @@ export const ShareConversationModal = ({ close: () => void; conversationId: string; }) => { + const { t } = useTranslation(); + + const domain = window.location.origin; + const [identifier, setIdentifier] = useState(null); const [isCopied, setIsCopied] = useState(false); - type StatusType = 'loading' | 'idle' | 'fetched' | 'failed'; const [status, setStatus] = useState('idle'); - const { t } = useTranslation(); - const domain = window.location.origin; + const [allowPrompt, setAllowPrompt] = useState(false); + + const sourceDocs = useSelector(selectSourceDocs); + const preSelectedDoc = useSelector(selectSelectedDocs); + const selectedPrompt = useSelector(selectPrompt); + const selectedChunk = useSelector(selectChunks); + + const extractDocPaths = (docs: Doc[]) => + docs + ? docs + .filter((doc) => doc.model === embeddingsName) + .map((doc: Doc) => { + let namePath = doc.name; + if (doc.language === namePath) { + namePath = '.project'; + } + let docPath = 'default'; + if (doc.location === 'local') { + docPath = 'local' + '/' + doc.name + '/'; + } else if (doc.location === 'remote') { + docPath = + doc.language + + '/' + + namePath + + '/' + + doc.version + + '/' + + doc.model + + '/'; + } + return { + label: doc.name, + value: docPath, + }; + }) + : []; + + const [sourcePath, setSourcePath] = useState<{ + label: string; + value: string; + } | null>(preSelectedDoc ? extractDocPaths([preSelectedDoc])[0] : null); + const handleCopyKey = (url: string) => { navigator.clipboard.writeText(url); setIsCopied(true); }; + + const togglePromptPermission = () => { + setAllowPrompt(!allowPrompt); + setStatus('idle'); + setIdentifier(null); + }; + const shareCoversationPublicly: (isPromptable: boolean) => void = ( isPromptable = false, ) => { setStatus('loading'); + const payload: { + conversation_id: string; + chunks?: string; + prompt_id?: string; + source?: string; + } = { conversation_id: conversationId }; + if (isPromptable) { + payload.chunks = selectedChunk; + payload.prompt_id = selectedPrompt.id; + sourcePath && (payload.source = sourcePath.value); + } fetch(`${apiHost}/api/share?isPromptable=${isPromptable}`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, - body: JSON.stringify({ conversation_id: conversationId }), + body: JSON.stringify(payload), }) .then((res) => { console.log(res.status); @@ -44,6 +119,7 @@ export const ShareConversationModal = ({ }) .catch((err) => setStatus('failed')); }; + return (
@@ -53,13 +129,52 @@ export const ShareConversationModal = ({

{t('modals.shareConv.label')}

{t('modals.shareConv.note')}

+
+ Allow users to prompt further + +
+ {allowPrompt && ( +
+ + setSourcePath(selection) + } + options={extractDocPaths(sourceDocs ?? [])} + size="w-full" + rounded="xl" + /> +
+ )}
- {`${domain}/share/${ - identifier ?? '....' - }`} + + {`${domain}/share/${identifier ?? '....'}`} + {status === 'fetched' ? ( ) : (
); -}; - -export default SharedConversation; +} diff --git a/frontend/src/conversation/conversationApi.ts b/frontend/src/conversation/conversationHandlers.ts similarity index 72% rename from frontend/src/conversation/conversationApi.ts rename to frontend/src/conversation/conversationHandlers.ts index e107abc8..52f29f82 100644 --- a/frontend/src/conversation/conversationApi.ts +++ b/frontend/src/conversation/conversationHandlers.ts @@ -1,11 +1,9 @@ -import { Answer, FEEDBACK } from './conversationModels'; +import conversationService from '../api/services/conversationService'; import { Doc } from '../preferences/preferenceApi'; - -const apiHost = import.meta.env.VITE_API_HOST || 'https://docsapi.arc53.com'; +import { Answer, FEEDBACK } from './conversationModels'; function getDocPath(selectedDocs: Doc | null): string { let docPath = 'default'; - if (selectedDocs) { let namePath = selectedDocs.name; if (selectedDocs.language === namePath) { @@ -27,10 +25,10 @@ function getDocPath(selectedDocs: Doc | null): string { docPath = selectedDocs.docLink; } } - return docPath; } -export function fetchAnswerApi( + +export function handleFetchAnswer( question: string, signal: AbortSignal, selectedDocs: Doc | null, @@ -57,27 +55,22 @@ export function fetchAnswerApi( } > { const docPath = getDocPath(selectedDocs); - //in history array remove all keys except prompt and response history = history.map((item) => { return { prompt: item.prompt, response: item.response }; }); - - return fetch(apiHost + '/api/answer', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - question: question, - history: history, - active_docs: docPath, - conversation_id: conversationId, - prompt_id: promptId, - chunks: chunks, - token_limit: token_limit, - }), - signal, - }) + return conversationService + .answer( + { + question: question, + history: history, + active_docs: docPath, + conversation_id: conversationId, + prompt_id: promptId, + chunks: chunks, + token_limit: token_limit, + }, + signal, + ) .then((response) => { if (response.ok) { return response.json(); @@ -97,7 +90,7 @@ export function fetchAnswerApi( }); } -export function fetchAnswerSteaming( +export function handleFetchAnswerSteaming( question: string, signal: AbortSignal, selectedDocs: Doc | null, @@ -109,29 +102,23 @@ export function fetchAnswerSteaming( onEvent: (event: MessageEvent) => void, ): Promise { const docPath = getDocPath(selectedDocs); - history = history.map((item) => { return { prompt: item.prompt, response: item.response }; }); - return new Promise((resolve, reject) => { - const body = { - question: question, - active_docs: docPath, - history: JSON.stringify(history), - conversation_id: conversationId, - prompt_id: promptId, - chunks: chunks, - token_limit: token_limit, - }; - fetch(apiHost + '/stream', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(body), - signal, - }) + conversationService + .answerStream( + { + question: question, + active_docs: docPath, + history: JSON.stringify(history), + conversation_id: conversationId, + prompt_id: promptId, + chunks: chunks, + token_limit: token_limit, + }, + signal, + ) .then((response) => { if (!response.body) throw Error('No response body'); @@ -179,7 +166,8 @@ export function fetchAnswerSteaming( }); }); } -export function searchEndpoint( + +export function handleSearch( question: string, selectedDocs: Doc | null, conversation_id: string | null, @@ -188,48 +176,38 @@ export function searchEndpoint( token_limit: number, ) { const docPath = getDocPath(selectedDocs); - - const body = { - question: question, - active_docs: docPath, - conversation_id, - history, - chunks: chunks, - token_limit: token_limit, - }; - return fetch(`${apiHost}/api/search`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(body), - }) + return conversationService + .search({ + question: question, + active_docs: docPath, + conversation_id, + history, + chunks: chunks, + token_limit: token_limit, + }) .then((response) => response.json()) .then((data) => { return data; }) .catch((err) => console.log(err)); } -export function sendFeedback( + +export function handleSendFeedback( prompt: string, response: string, feedback: FEEDBACK, ) { - return fetch(`${apiHost}/api/feedback`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ + return conversationService + .feedback({ question: prompt, answer: response, feedback: feedback, - }), - }).then((response) => { - if (response.ok) { - return Promise.resolve(); - } else { - return Promise.reject(); - } - }); + }) + .then((response) => { + if (response.ok) { + return Promise.resolve(); + } else { + return Promise.reject(); + } + }); } diff --git a/frontend/src/conversation/conversationSlice.ts b/frontend/src/conversation/conversationSlice.ts index 7ab9f8fe..75c457a9 100644 --- a/frontend/src/conversation/conversationSlice.ts +++ b/frontend/src/conversation/conversationSlice.ts @@ -1,10 +1,14 @@ import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit'; -import store from '../store'; -import { fetchAnswerApi, fetchAnswerSteaming } from './conversationApi'; -import { searchEndpoint } from './conversationApi'; -import { Answer, ConversationState, Query, Status } from './conversationModels'; + import { getConversations } from '../preferences/preferenceApi'; import { setConversations } from '../preferences/preferenceSlice'; +import store from '../store'; +import { + handleFetchAnswer, + handleFetchAnswerSteaming, + handleSearch, +} from './conversationHandlers'; +import { Answer, ConversationState, Query, Status } from './conversationModels'; const initialState: ConversationState = { queries: [], @@ -20,7 +24,7 @@ export const fetchAnswer = createAsyncThunk( const state = getState() as RootState; if (state.preference) { if (API_STREAMING) { - await fetchAnswerSteaming( + await handleFetchAnswerSteaming( question, signal, state.preference.selectedDocs!, @@ -45,7 +49,7 @@ export const fetchAnswer = createAsyncThunk( console.error('Failed to fetch conversations: ', error); }); - searchEndpoint( + handleSearch( //search for sources post streaming question, state.preference.selectedDocs!, @@ -89,7 +93,7 @@ export const fetchAnswer = createAsyncThunk( }, ); } else { - const answer = await fetchAnswerApi( + const answer = await handleFetchAnswer( question, signal, state.preference.selectedDocs!, diff --git a/frontend/src/modals/CreateAPIKeyModal.tsx b/frontend/src/modals/CreateAPIKeyModal.tsx new file mode 100644 index 00000000..2f67d83b --- /dev/null +++ b/frontend/src/modals/CreateAPIKeyModal.tsx @@ -0,0 +1,162 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { useSelector } from 'react-redux'; + +import userService from '../api/services/userService'; +import Exit from '../assets/exit.svg'; +import Dropdown from '../components/Dropdown'; +import Input from '../components/Input'; +import { CreateAPIKeyModalProps, Doc } from '../models/misc'; +import { selectSourceDocs } from '../preferences/preferenceSlice'; + +const embeddingsName = + import.meta.env.VITE_EMBEDDINGS_NAME || + 'huggingface_sentence-transformers/all-mpnet-base-v2'; + +export default function CreateAPIKeyModal({ + close, + createAPIKey, +}: CreateAPIKeyModalProps) { + const { t } = useTranslation(); + const docs = useSelector(selectSourceDocs); + + const [APIKeyName, setAPIKeyName] = React.useState(''); + const [sourcePath, setSourcePath] = React.useState<{ + label: string; + value: string; + } | null>(null); + const [prompt, setPrompt] = React.useState<{ + name: string; + id: string; + type: string; + } | null>(null); + const [activePrompts, setActivePrompts] = React.useState< + { name: string; id: string; type: string }[] + >([]); + const [chunk, setChunk] = React.useState('2'); + const chunkOptions = ['0', '2', '4', '6', '8', '10']; + + const extractDocPaths = () => + docs + ? docs + .filter((doc) => doc.model === embeddingsName) + .map((doc: Doc) => { + let namePath = doc.name; + if (doc.language === namePath) { + namePath = '.project'; + } + let docPath = 'default'; + if (doc.location === 'local') { + docPath = 'local' + '/' + doc.name + '/'; + } else if (doc.location === 'remote') { + docPath = + doc.language + + '/' + + namePath + + '/' + + doc.version + + '/' + + doc.model + + '/'; + } + return { + label: doc.name, + value: docPath, + }; + }) + : []; + + React.useEffect(() => { + const handleFetchPrompts = async () => { + try { + const response = await userService.getPrompts(); + if (!response.ok) { + throw new Error('Failed to fetch prompts'); + } + const promptsData = await response.json(); + setActivePrompts(promptsData); + } catch (error) { + console.error(error); + } + }; + handleFetchPrompts(); + }, []); + return ( +
+
+ +
+ + {t('modals.createAPIKey.label')} + +
+
+ + {t('modals.createAPIKey.apiKeyName')} + + setAPIKeyName(e.target.value)} + > +
+
+ + setSourcePath(selection) + } + options={extractDocPaths()} + size="w-full" + rounded="xl" + border="border" + /> +
+
+ + setPrompt(value) + } + size="w-full" + border="border" + /> +
+
+

+ {t('modals.createAPIKey.chunks')} +

+ setChunk(value)} + size="w-full" + border="border" + /> +
+ +
+
+ ); +} diff --git a/frontend/src/modals/SaveAPIKeyModal.tsx b/frontend/src/modals/SaveAPIKeyModal.tsx new file mode 100644 index 00000000..d91d0c2d --- /dev/null +++ b/frontend/src/modals/SaveAPIKeyModal.tsx @@ -0,0 +1,52 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; + +import Exit from '../assets/exit.svg'; +import { SaveAPIKeyModalProps } from '../models/misc'; + +export default function SaveAPIKeyModal({ + apiKey, + close, +}: SaveAPIKeyModalProps) { + const { t } = useTranslation(); + const [isCopied, setIsCopied] = React.useState(false); + + const handleCopyKey = () => { + navigator.clipboard.writeText(apiKey); + setIsCopied(true); + }; + return ( +
+
+ +

+ {' '} + {t('modals.saveKey.note')} +

+

+ {t('modals.saveKey.disclaimer')} +

+
+
+

API Key

+ {apiKey} +
+ +
+ +
+
+ ); +} diff --git a/frontend/src/models/misc.ts b/frontend/src/models/misc.ts index 52787932..ab8d6b85 100644 --- a/frontend/src/models/misc.ts +++ b/frontend/src/models/misc.ts @@ -21,7 +21,6 @@ export type PromptProps = { selectedPrompt: { name: string; id: string; type: string }; onSelectPrompt: (name: string, id: string, type: string) => void; setPrompts: (prompts: { name: string; id: string; type: string }[]) => void; - apiHost: string; }; export type DocumentsProps = { diff --git a/frontend/src/settings/APIKeys.tsx b/frontend/src/settings/APIKeys.tsx index d87e0abe..18904d24 100644 --- a/frontend/src/settings/APIKeys.tsx +++ b/frontend/src/settings/APIKeys.tsx @@ -1,22 +1,12 @@ import React from 'react'; -import { useSelector } from 'react-redux'; -import Dropdown from '../components/Dropdown'; -import { - Doc, - CreateAPIKeyModalProps, - SaveAPIKeyModalProps, -} from '../models/misc'; -import { selectSourceDocs } from '../preferences/preferenceSlice'; -import Exit from '../assets/exit.svg'; -import Trash from '../assets/trash.svg'; import { useTranslation } from 'react-i18next'; -import Input from '../components/Input'; -const apiHost = import.meta.env.VITE_API_HOST || 'https://docsapi.arc53.com'; -const embeddingsName = - import.meta.env.VITE_EMBEDDINGS_NAME || - 'huggingface_sentence-transformers/all-mpnet-base-v2'; -const APIKeys: React.FC = () => { +import userService from '../api/services/userService'; +import Trash from '../assets/trash.svg'; +import CreateAPIKeyModal from '../modals/CreateAPIKeyModal'; +import SaveAPIKeyModal from '../modals/SaveAPIKeyModal'; + +export default function APIKeys() { const { t } = useTranslation(); const [isCreateModalOpen, setCreateModal] = React.useState(false); const [isSaveKeyModalOpen, setSaveKeyModal] = React.useState(false); @@ -24,14 +14,23 @@ const APIKeys: React.FC = () => { const [apiKeys, setApiKeys] = React.useState< { name: string; key: string; source: string; id: string }[] >([]); + + const handleFetchKeys = async () => { + try { + const response = await userService.getAPIKeys(); + if (!response.ok) { + throw new Error('Failed to fetch API Keys'); + } + const apiKeys = await response.json(); + setApiKeys(apiKeys); + } catch (error) { + console.log(error); + } + }; + const handleDeleteKey = (id: string) => { - fetch(`${apiHost}/api/delete_api_key`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ id }), - }) + userService + .deleteAPIKey({ id }) .then((response) => { if (!response.ok) { throw new Error('Failed to delete API Key'); @@ -46,34 +45,15 @@ const APIKeys: React.FC = () => { console.error(error); }); }; - React.useEffect(() => { - fetchAPIKeys(); - }, []); - const fetchAPIKeys = async () => { - try { - const response = await fetch(`${apiHost}/api/get_api_keys`); - if (!response.ok) { - throw new Error('Failed to fetch API Keys'); - } - const apiKeys = await response.json(); - setApiKeys(apiKeys); - } catch (error) { - console.log(error); - } - }; - const createAPIKey = (payload: { + + const handleCreateKey = (payload: { name: string; source: string; prompt_id: string; chunks: string; }) => { - fetch(`${apiHost}/api/create_api_key`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(payload), - }) + userService + .createAPIKey(payload) .then((response) => { if (!response.ok) { throw new Error('Failed to create API Key'); @@ -85,12 +65,16 @@ const APIKeys: React.FC = () => { setCreateModal(false); setNewKey(data.key); setSaveKeyModal(true); - fetchAPIKeys(); + handleFetchKeys(); }) .catch((error) => { console.error(error); }); }; + + React.useEffect(() => { + handleFetchKeys(); + }, []); return (
@@ -104,8 +88,8 @@ const APIKeys: React.FC = () => {
{isCreateModalOpen && ( setCreateModal(false)} - createAPIKey={createAPIKey} /> )} {isSaveKeyModalOpen && ( @@ -155,192 +139,4 @@ const APIKeys: React.FC = () => {
); -}; - -const CreateAPIKeyModal: React.FC = ({ - close, - createAPIKey, -}) => { - const [APIKeyName, setAPIKeyName] = React.useState(''); - const [sourcePath, setSourcePath] = React.useState<{ - label: string; - value: string; - } | null>(null); - - const chunkOptions = ['0', '2', '4', '6', '8', '10']; - const [chunk, setChunk] = React.useState('2'); - const [activePrompts, setActivePrompts] = React.useState< - { name: string; id: string; type: string }[] - >([]); - const [prompt, setPrompt] = React.useState<{ - name: string; - id: string; - type: string; - } | null>(null); - const docs = useSelector(selectSourceDocs); - React.useEffect(() => { - const fetchPrompts = async () => { - try { - const response = await fetch(`${apiHost}/api/get_prompts`); - if (!response.ok) { - throw new Error('Failed to fetch prompts'); - } - const promptsData = await response.json(); - setActivePrompts(promptsData); - } catch (error) { - console.error(error); - } - }; - fetchPrompts(); - }, []); - const extractDocPaths = () => - docs - ? docs - .filter((doc) => doc.model === embeddingsName) - .map((doc: Doc) => { - let namePath = doc.name; - if (doc.language === namePath) { - namePath = '.project'; - } - let docPath = 'default'; - if (doc.location === 'local') { - docPath = 'local' + '/' + doc.name + '/'; - } else if (doc.location === 'remote') { - docPath = - doc.language + - '/' + - namePath + - '/' + - doc.version + - '/' + - doc.model + - '/'; - } - return { - label: doc.name, - value: docPath, - }; - }) - : []; - const { t } = useTranslation(); - return ( -
-
- -
- - {t('modals.createAPIKey.label')} - -
-
- - {t('modals.createAPIKey.apiKeyName')} - - setAPIKeyName(e.target.value)} - > -
-
- - setSourcePath(selection) - } - options={extractDocPaths()} - size="w-full" - rounded="xl" - /> -
-
- - setPrompt(value) - } - size="w-full" - /> -
-
-

- {t('modals.createAPIKey.chunks')} -

- setChunk(value)} - size="w-full" - /> -
- -
-
- ); -}; - -const SaveAPIKeyModal: React.FC = ({ apiKey, close }) => { - const [isCopied, setIsCopied] = React.useState(false); - const { t } = useTranslation(); - const handleCopyKey = () => { - navigator.clipboard.writeText(apiKey); - setIsCopied(true); - }; - return ( -
-
- -

- {' '} - {t('modals.saveKey.note')} -

-

- {t('modals.saveKey.disclaimer')} -

-
-
-

API Key

- {apiKey} -
- -
- -
-
- ); -}; - -export default APIKeys; +} diff --git a/frontend/src/settings/General.tsx b/frontend/src/settings/General.tsx index bf917d91..2d0c466d 100644 --- a/frontend/src/settings/General.tsx +++ b/frontend/src/settings/General.tsx @@ -1,22 +1,22 @@ import React from 'react'; -import { useSelector, useDispatch } from 'react-redux'; -import Prompts from './Prompts'; -import { useDarkTheme } from '../hooks'; import { useTranslation } from 'react-i18next'; +import { useDispatch, useSelector } from 'react-redux'; + +import userService from '../api/services/userService'; import Dropdown from '../components/Dropdown'; +import { useDarkTheme } from '../hooks'; import { - selectPrompt, - setPrompt, - setChunks, selectChunks, - setTokenLimit, + selectPrompt, selectTokenLimit, + setChunks, setModalStateDeleteConv, + setPrompt, + setTokenLimit, } from '../preferences/preferenceSlice'; +import Prompts from './Prompts'; -const apiHost = import.meta.env.VITE_API_HOST || 'https://docsapi.arc53.com'; - -const General: React.FC = () => { +export default function General() { const { t, i18n: { changeLanguage, language }, @@ -69,9 +69,9 @@ const General: React.FC = () => { const selectedPrompt = useSelector(selectPrompt); React.useEffect(() => { - const fetchPrompts = async () => { + const handleFetchPrompts = async () => { try { - const response = await fetch(`${apiHost}/api/get_prompts`); + const response = await userService.getPrompts(); if (!response.ok) { throw new Error('Failed to fetch prompts'); } @@ -81,14 +81,13 @@ const General: React.FC = () => { console.error(error); } }; - fetchPrompts(); + handleFetchPrompts(); }, []); React.useEffect(() => { localStorage.setItem('docsgpt-locale', selectedLanguage?.value as string); changeLanguage(selectedLanguage?.value); }, [selectedLanguage, changeLanguage]); - return (
@@ -171,7 +170,6 @@ const General: React.FC = () => { dispatch(setPrompt({ name: name, id: id, type: type })) } setPrompts={setPrompts} - apiHost={apiHost} />
@@ -189,6 +187,4 @@ const General: React.FC = () => {
); -}; - -export default General; +} diff --git a/frontend/src/settings/Prompts.tsx b/frontend/src/settings/Prompts.tsx index 2bae07eb..3d041555 100644 --- a/frontend/src/settings/Prompts.tsx +++ b/frontend/src/settings/Prompts.tsx @@ -1,15 +1,17 @@ import React from 'react'; -import { PromptProps, ActiveState } from '../models/misc'; -import Dropdown from '../components/Dropdown'; -import PromptsModal from '../preferences/PromptsModal'; import { useTranslation } from 'react-i18next'; -const apiHost = import.meta.env.VITE_API_HOST || 'https://docsapi.arc53.com'; -const Prompts: React.FC = ({ + +import userService from '../api/services/userService'; +import Dropdown from '../components/Dropdown'; +import { ActiveState, PromptProps } from '../models/misc'; +import PromptsModal from '../preferences/PromptsModal'; + +export default function Prompts({ prompts, selectedPrompt, onSelectPrompt, setPrompts, -}) => { +}: PromptProps) { const handleSelectPrompt = ({ name, id, @@ -37,17 +39,12 @@ const Prompts: React.FC = ({ t, i18n: { changeLanguage, language }, } = useTranslation(); + const handleAddPrompt = async () => { try { - const response = await fetch(`${apiHost}/api/create_prompt`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - name: newPromptName, - content: newPromptContent, - }), + const response = await userService.createPrompt({ + name: newPromptName, + content: newPromptContent, }); if (!response.ok) { throw new Error('Failed to add prompt'); @@ -69,18 +66,12 @@ const Prompts: React.FC = ({ const handleDeletePrompt = (id: string) => { setPrompts(prompts.filter((prompt) => prompt.id !== id)); - fetch(`${apiHost}/api/delete_prompt`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ id: id }), - }) + userService + .deletePrompt({ id }) .then((response) => { if (!response.ok) { throw new Error('Failed to delete prompt'); } - // get 1st prompt and set it as selected if (prompts.length > 0) { onSelectPrompt(prompts[0].name, prompts[0].id, prompts[0].type); } @@ -90,18 +81,9 @@ const Prompts: React.FC = ({ }); }; - const fetchPromptContent = async (id: string) => { - console.log('fetching prompt content'); + const handleFetchPromptContent = async (id: string) => { try { - const response = await fetch( - `${apiHost}/api/get_single_prompt?id=${id}`, - { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - }, - }, - ); + const response = await userService.getSinglePrompt(id); if (!response.ok) { throw new Error('Failed to fetch prompt content'); } @@ -113,17 +95,12 @@ const Prompts: React.FC = ({ }; const handleSaveChanges = (id: string, type: string) => { - fetch(`${apiHost}/api/update_prompt`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ + userService + .updatePrompt({ id: id, name: editPromptName, content: editPromptContent, - }), - }) + }) .then((response) => { if (!response.ok) { throw new Error('Failed to update prompt'); @@ -154,7 +131,6 @@ const Prompts: React.FC = ({ console.error(error); }); }; - return ( <>
@@ -183,7 +159,7 @@ const Prompts: React.FC = ({ }) => { setModalType('EDIT'); setEditPromptName(name); - fetchPromptContent(id); + handleFetchPromptContent(id); setCurrentPromptEdit({ id: id, name: name, type: type }); setModalState('ACTIVE'); }} @@ -219,6 +195,4 @@ const Prompts: React.FC = ({ /> ); -}; - -export default Prompts; +} diff --git a/frontend/src/settings/index.tsx b/frontend/src/settings/index.tsx index 3969fce6..226ebb3b 100644 --- a/frontend/src/settings/index.tsx +++ b/frontend/src/settings/index.tsx @@ -1,22 +1,22 @@ import React from 'react'; -import { useSelector, useDispatch } from 'react-redux'; -import General from './General'; -import Documents from './Documents'; -import APIKeys from './APIKeys'; -import Widgets from './Widgets'; +import { useTranslation } from 'react-i18next'; +import { useDispatch, useSelector } from 'react-redux'; + +import userService from '../api/services/userService'; +import ArrowLeft from '../assets/arrow-left.svg'; +import ArrowRight from '../assets/arrow-right.svg'; +import i18n from '../locale/i18n'; +import { Doc } from '../preferences/preferenceApi'; import { selectSourceDocs, setSourceDocs, } from '../preferences/preferenceSlice'; -import { Doc } from '../preferences/preferenceApi'; -import ArrowLeft from '../assets/arrow-left.svg'; -import ArrowRight from '../assets/arrow-right.svg'; -import { useTranslation } from 'react-i18next'; -import i18n from '../locale/i18n'; +import APIKeys from './APIKeys'; +import Documents from './Documents'; +import General from './General'; +import Widgets from './Widgets'; -const apiHost = import.meta.env.VITE_API_HOST || 'https://docsapi.arc53.com'; - -const Settings: React.FC = () => { +export default function Settings() { const dispatch = useDispatch(); const { t } = useTranslation(); const tabs = [ @@ -33,11 +33,11 @@ const Settings: React.FC = () => { const updateWidgetScreenshot = (screenshot: File | null) => { setWidgetScreenshot(screenshot); }; + const handleDeleteClick = (index: number, doc: Doc) => { const docPath = 'indexes/' + 'local' + '/' + doc.name; - fetch(`${apiHost}/api/delete_old?path=${docPath}`, { - method: 'GET', - }) + userService + .deletePath(docPath) .then((response) => { if (response.ok && documents) { const updatedDocuments = [ @@ -50,7 +50,6 @@ const Settings: React.FC = () => { .catch((error) => console.error(error)); }; - // persist active tab as the translated version of 'general' per language change React.useEffect(() => { setActiveTab(t('settings.general.label')); }, [i18n.language]); @@ -134,6 +133,4 @@ const Settings: React.FC = () => { return null; } } -}; - -export default Settings; +} From 99b649f24e38c64a749871319ccc6ccd83bc55f0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 24 Jul 2024 17:36:19 +0000 Subject: [PATCH 36/46] chore(deps): bump langchain-community from 0.0.16 to 0.2.9 in /scripts Bumps [langchain-community](https://github.com/langchain-ai/langchain) from 0.0.16 to 0.2.9. - [Release notes](https://github.com/langchain-ai/langchain/releases) - [Commits](https://github.com/langchain-ai/langchain/commits/langchain-community==0.2.9) --- updated-dependencies: - dependency-name: langchain-community dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- scripts/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/requirements.txt b/scripts/requirements.txt index c4aed5b2..4a7b26a7 100644 --- a/scripts/requirements.txt +++ b/scripts/requirements.txt @@ -7,7 +7,7 @@ faiss_cpu==1.7.4 html2text==2020.1.16 javalang==0.13.0 langchain==0.1.4 -langchain_community==0.0.16 +langchain_community==0.2.9 langchain-openai==0.0.5 nltk==3.8.1 openapi3_parser==1.1.16 From 0c062a848573590bd6ded270790b56aaea2ce251 Mon Sep 17 00:00:00 2001 From: Siddhant Rai Date: Wed, 24 Jul 2024 23:08:42 +0530 Subject: [PATCH 37/46] enhancement: implement api client in remaining places --- frontend/src/Navigation.tsx | 92 ++++++++----------- frontend/src/api/endpoints.ts | 10 ++ .../src/api/services/conversationService.ts | 15 +++ frontend/src/api/services/userService.ts | 5 + .../src/modals/ShareConversationModal.tsx | 14 +-- frontend/src/preferences/preferenceApi.ts | 26 ++---- frontend/src/upload/Upload.tsx | 15 +-- 7 files changed, 90 insertions(+), 87 deletions(-) diff --git a/frontend/src/Navigation.tsx b/frontend/src/Navigation.tsx index ebb3f88a..22ab5017 100644 --- a/frontend/src/Navigation.tsx +++ b/frontend/src/Navigation.tsx @@ -1,46 +1,48 @@ import { useEffect, useRef, useState } from 'react'; +import { useTranslation } from 'react-i18next'; import { useDispatch, useSelector } from 'react-redux'; import { NavLink, useNavigate } from 'react-router-dom'; + +import conversationService from './api/services/conversationService'; +import userService from './api/services/userService'; +import Add from './assets/add.svg'; import DocsGPT3 from './assets/cute_docsgpt3.svg'; import Discord from './assets/discord.svg'; import Expand from './assets/expand.svg'; import Github from './assets/github.svg'; -import Hamburger from './assets/hamburger.svg'; import HamburgerDark from './assets/hamburger-dark.svg'; +import Hamburger from './assets/hamburger.svg'; import Info from './assets/info.svg'; import SettingGear from './assets/settingGear.svg'; import Twitter from './assets/TwitterX.svg'; -import Add from './assets/add.svg'; import UploadIcon from './assets/upload.svg'; -import { ActiveState } from './models/misc'; -import APIKeyModal from './preferences/APIKeyModal'; -import DeleteConvModal from './modals/DeleteConvModal'; - -import { - selectApiKeyStatus, - selectSelectedDocs, - selectSelectedDocsStatus, - selectSourceDocs, - setSelectedDocs, - selectConversations, - setConversations, - selectConversationId, - selectModalStateDeleteConv, - setModalStateDeleteConv, - setSourceDocs, -} from './preferences/preferenceSlice'; +import SourceDropdown from './components/SourceDropdown'; import { setConversation, updateConversationId, } from './conversation/conversationSlice'; -import { useMediaQuery, useOutsideAlerter } from './hooks'; -import Upload from './upload/Upload'; -import { Doc, getConversations, getDocs } from './preferences/preferenceApi'; -import SelectDocsModal from './preferences/SelectDocsModal'; import ConversationTile from './conversation/ConversationTile'; -import { useDarkTheme } from './hooks'; -import SourceDropdown from './components/SourceDropdown'; -import { useTranslation } from 'react-i18next'; +import { useDarkTheme, useMediaQuery, useOutsideAlerter } from './hooks'; +import DeleteConvModal from './modals/DeleteConvModal'; +import { ActiveState } from './models/misc'; +import APIKeyModal from './preferences/APIKeyModal'; +import { Doc, getConversations, getDocs } from './preferences/preferenceApi'; +import { + selectApiKeyStatus, + selectConversationId, + selectConversations, + selectModalStateDeleteConv, + selectSelectedDocs, + selectSelectedDocsStatus, + selectSourceDocs, + setConversations, + setModalStateDeleteConv, + setSelectedDocs, + setSourceDocs, +} from './preferences/preferenceSlice'; +import SelectDocsModal from './preferences/SelectDocsModal'; +import Upload from './upload/Upload'; + interface NavigationProps { navOpen: boolean; setNavOpen: React.Dispatch>; @@ -85,7 +87,6 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) { useState('INACTIVE'); const navRef = useRef(null); - const apiHost = import.meta.env.VITE_API_HOST || 'https://docsapi.arc53.com'; const navigate = useNavigate(); @@ -106,9 +107,8 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) { } const handleDeleteAllConversations = () => { - fetch(`${apiHost}/api/delete_all_conversations`, { - method: 'POST', - }) + conversationService + .deleteAll({}) .then(() => { fetchConversations(); }) @@ -116,9 +116,8 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) { }; const handleDeleteConversation = (id: string) => { - fetch(`${apiHost}/api/delete_conversation?id=${id}`, { - method: 'POST', - }) + conversationService + .delete(id, {}) .then(() => { fetchConversations(); }) @@ -128,17 +127,9 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) { const handleDeleteClick = (doc: Doc) => { const docPath = `indexes/local/${doc.name}`; - fetch(`${apiHost}/api/delete_old?path=${docPath}`, { - method: 'GET', - }) + userService + .deletePath(docPath) .then(() => { - // remove the image element from the DOM - // const imageElement = document.querySelector( - // `#img-${index}`, - // ) as HTMLElement; - // const parentElement = imageElement.parentNode as HTMLElement; - // parentElement.parentNode?.removeChild(parentElement); - return getDocs(); }) .then((updatedDocs) => { @@ -153,10 +144,8 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) { }; const handleConversationClick = (index: string) => { - // fetch the conversation from the server and setConversation in the store - fetch(`${apiHost}/api/get_single_conversation?id=${index}`, { - method: 'GET', - }) + conversationService + .getConversation(index) .then((response) => response.json()) .then((data) => { navigate('/'); @@ -173,13 +162,8 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) { name: string; id: string; }) { - await fetch(`${apiHost}/api/update_conversation_name`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(updatedConversation), - }) + await conversationService + .update(updatedConversation) .then((response) => response.json()) .then((data) => { if (data) { diff --git a/frontend/src/api/endpoints.ts b/frontend/src/api/endpoints.ts index 790fcb12..af2fb920 100644 --- a/frontend/src/api/endpoints.ts +++ b/frontend/src/api/endpoints.ts @@ -1,5 +1,7 @@ const endpoints = { USER: { + DOCS: '/api/combine', + DOCS_CHECK: '/api/docs_check', API_KEYS: '/api/get_api_keys', CREATE_API_KEY: '/api/create_api_key', DELETE_API_KEY: '/api/delete_api_key', @@ -9,14 +11,22 @@ const endpoints = { UPDATE_PROMPT: '/api/update_prompt', SINGLE_PROMPT: (id: string) => `/api/get_single_prompt?id=${id}`, DELETE_PATH: (docPath: string) => `/api/delete_old?path=${docPath}`, + TASK_STATUS: (task_id: string) => `/api/task_status?task_id=${task_id}`, }, CONVERSATION: { ANSWER: '/api/answer', ANSWER_STREAMING: '/stream', SEARCH: '/api/search', FEEDBACK: '/api/feedback', + CONVERSATION: (id: string) => `/api/get_single_conversation?id=${id}`, + CONVERSATIONS: '/api/get_conversations', + SHARE_CONVERSATION: (isPromptable: boolean) => + `/api/share?isPromptable=${isPromptable}`, SHARED_CONVERSATION: (identifier: string) => `/api/shared_conversation/${identifier}`, + DELETE: (id: string) => `/api/delete_conversation?id=${id}`, + DELETE_ALL: '/api/delete_all_conversations', + UPDATE: '/api/update_conversation_name', }, }; diff --git a/frontend/src/api/services/conversationService.ts b/frontend/src/api/services/conversationService.ts index f41a5d31..9e31df84 100644 --- a/frontend/src/api/services/conversationService.ts +++ b/frontend/src/api/services/conversationService.ts @@ -10,8 +10,23 @@ const conversationService = { apiClient.post(endpoints.CONVERSATION.SEARCH, data), feedback: (data: any): Promise => apiClient.post(endpoints.CONVERSATION.FEEDBACK, data), + getConversation: (id: string): Promise => + apiClient.get(endpoints.CONVERSATION.CONVERSATION(id)), + getConversations: (): Promise => + apiClient.get(endpoints.CONVERSATION.CONVERSATIONS), + shareConversation: (isPromptable: boolean, data: any): Promise => + apiClient.post( + endpoints.CONVERSATION.SHARE_CONVERSATION(isPromptable), + data, + ), getSharedConversation: (identifier: string): Promise => apiClient.get(endpoints.CONVERSATION.SHARED_CONVERSATION(identifier)), + delete: (id: string, data: any): Promise => + apiClient.post(endpoints.CONVERSATION.DELETE(id), data), + deleteAll: (data: any): Promise => + apiClient.post(endpoints.CONVERSATION.DELETE_ALL, data), + update: (data: any): Promise => + apiClient.post(endpoints.CONVERSATION.UPDATE, data), }; export default conversationService; diff --git a/frontend/src/api/services/userService.ts b/frontend/src/api/services/userService.ts index 7e65e94e..193fe6ad 100644 --- a/frontend/src/api/services/userService.ts +++ b/frontend/src/api/services/userService.ts @@ -2,6 +2,9 @@ import apiClient from '../client'; import endpoints from '../endpoints'; const userService = { + getDocs: (): Promise => apiClient.get(endpoints.USER.DOCS), + checkDocs: (data: any): Promise => + apiClient.post(endpoints.USER.DOCS_CHECK, data), getAPIKeys: (): Promise => apiClient.get(endpoints.USER.API_KEYS), createAPIKey: (data: any): Promise => apiClient.post(endpoints.USER.CREATE_API_KEY, data), @@ -18,6 +21,8 @@ const userService = { apiClient.get(endpoints.USER.SINGLE_PROMPT(id)), deletePath: (docPath: string): Promise => apiClient.get(endpoints.USER.DELETE_PATH(docPath)), + getTaskStatus: (task_id: string): Promise => + apiClient.get(endpoints.USER.TASK_STATUS(task_id)), }; export default userService; diff --git a/frontend/src/modals/ShareConversationModal.tsx b/frontend/src/modals/ShareConversationModal.tsx index 37da934d..a43e4326 100644 --- a/frontend/src/modals/ShareConversationModal.tsx +++ b/frontend/src/modals/ShareConversationModal.tsx @@ -1,8 +1,9 @@ import { useState } from 'react'; import { useTranslation } from 'react-i18next'; -import Spinner from '../assets/spinner.svg'; + +import conversationService from '../api/services/conversationService'; import Exit from '../assets/exit.svg'; -const apiHost = import.meta.env.VITE_API_HOST || 'https://docsapi.arc53.com'; +import Spinner from '../assets/spinner.svg'; export const ShareConversationModal = ({ close, @@ -25,13 +26,8 @@ export const ShareConversationModal = ({ isPromptable = false, ) => { setStatus('loading'); - fetch(`${apiHost}/api/share?isPromptable=${isPromptable}`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ conversation_id: conversationId }), - }) + conversationService + .shareConversation(isPromptable, { conversation_id: conversationId }) .then((res) => { console.log(res.status); return res.json(); diff --git a/frontend/src/preferences/preferenceApi.ts b/frontend/src/preferences/preferenceApi.ts index 81fd3131..29a41645 100644 --- a/frontend/src/preferences/preferenceApi.ts +++ b/frontend/src/preferences/preferenceApi.ts @@ -1,3 +1,6 @@ +import conversationService from '../api/services/conversationService'; +import userService from '../api/services/userService'; + // not all properties in Doc are going to be present. Make some optional export type Doc = { location: string; @@ -14,10 +17,7 @@ export type Doc = { //Fetches all JSON objects from the source. We only use the objects with the "model" property in SelectDocsModal.tsx. Hopefully can clean up the source file later. export async function getDocs(): Promise { try { - const apiHost = - import.meta.env.VITE_API_HOST || 'https://docsapi.arc53.com'; - - const response = await fetch(apiHost + '/api/combine'); + const response = await userService.getDocs(); const data = await response.json(); const docs: Doc[] = []; @@ -37,10 +37,7 @@ export async function getConversations(): Promise< { name: string; id: string }[] | null > { try { - const apiHost = - import.meta.env.VITE_API_HOST || 'https://docsapi.arc53.com'; - - const response = await fetch(apiHost + '/api/get_conversations'); + const response = await conversationService.getConversations(); const data = await response.json(); const conversations: { name: string; id: string }[] = []; @@ -93,14 +90,9 @@ export function setLocalRecentDocs(doc: Doc): void { docPath = doc.language + '/' + namePath + '/' + doc.version + '/' + doc.model + '/'; } - const apiHost = import.meta.env.VITE_API_HOST || 'https://docsapi.arc53.com'; - fetch(apiHost + '/api/docs_check', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ + userService + .checkDocs({ docs: docPath, - }), - }).then((response) => response.json()); + }) + .then((response) => response.json()); } diff --git a/frontend/src/upload/Upload.tsx b/frontend/src/upload/Upload.tsx index a2c23639..3bb3e7ae 100644 --- a/frontend/src/upload/Upload.tsx +++ b/frontend/src/upload/Upload.tsx @@ -1,13 +1,14 @@ -import React, { useRef } from 'react'; -import { useCallback, useEffect, useState } from 'react'; +import React, { useCallback, useEffect, useRef, useState } from 'react'; import { useDropzone } from 'react-dropzone'; +import { useTranslation } from 'react-i18next'; import { useDispatch } from 'react-redux'; + +import userService from '../api/services/userService'; +import Dropdown from '../components/Dropdown'; +import Input from '../components/Input'; import { ActiveState } from '../models/misc'; import { getDocs } from '../preferences/preferenceApi'; import { setSelectedDocs, setSourceDocs } from '../preferences/preferenceSlice'; -import Dropdown from '../components/Dropdown'; -import { useTranslation } from 'react-i18next'; -import Input from '../components/Input'; function Upload({ modalState, @@ -111,8 +112,8 @@ function Upload({ if ((progress?.percentage ?? 0) < 100) { timeoutID = setTimeout(() => { - const apiHost = import.meta.env.VITE_API_HOST; - fetch(`${apiHost}/api/task_status?task_id=${progress?.taskId}`) + userService + .getTaskStatus(progress?.taskId as string) .then((data) => data.json()) .then((data) => { if (data.status == 'SUCCESS') { From 8276b6c9a9ef9eca1c206b4b15ad7618825521b8 Mon Sep 17 00:00:00 2001 From: ManishMadan2882 Date: Fri, 26 Jul 2024 15:19:38 +0530 Subject: [PATCH 38/46] (shared conv) centralised into redux state --- .../src/conversation/SharedConversation.tsx | 58 ++++++++++------ .../conversation/sharedConversationSlice.ts | 66 +++++++++++++++++++ frontend/src/store.ts | 2 + 3 files changed, 105 insertions(+), 21 deletions(-) diff --git a/frontend/src/conversation/SharedConversation.tsx b/frontend/src/conversation/SharedConversation.tsx index 9a2d8d16..dea1258f 100644 --- a/frontend/src/conversation/SharedConversation.tsx +++ b/frontend/src/conversation/SharedConversation.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect, useRef } from 'react'; +import { useEffect, useRef } from 'react'; import { useParams } from 'react-router-dom'; import { useNavigate } from 'react-router-dom'; import { Query } from './conversationModels'; @@ -6,19 +6,30 @@ import { useTranslation } from 'react-i18next'; import ConversationBubble from './ConversationBubble'; import Send from '../assets/send.svg'; import Spinner from '../assets/spinner.svg'; - +import { selectClientAPIKey, setClientApiKey } from './sharedConversationSlice'; +import { setIdentifier, setFetchedData } from './sharedConversationSlice'; +import { useDispatch } from 'react-redux'; +import { + selectDate, + selectTitle, + selectQueries, +} from './sharedConversationSlice'; +import { useSelector } from 'react-redux'; import { Fragment } from 'react'; const apiHost = import.meta.env.VITE_API_HOST || 'https://docsapi.arc53.com'; const SharedConversation = () => { const params = useParams(); const navigate = useNavigate(); const { identifier } = params; //identifier is a uuid, not conversationId - const [queries, setQueries] = useState([]); - const [title, setTitle] = useState(''); - const [date, setDate] = useState(''); - const [apiKey, setAPIKey] = useState(null); + + const queries = useSelector(selectQueries); + const title = useSelector(selectTitle); + const date = useSelector(selectDate); + const apiKey = useSelector(selectClientAPIKey); const inputRef = useRef(null); const { t } = useTranslation(); + const dispatch = useDispatch(); + identifier && dispatch(setIdentifier(identifier)); function formatISODate(isoDateStr: string) { const date = new Date(isoDateStr); @@ -52,19 +63,26 @@ const SharedConversation = () => { return formattedDate; } const fetchQueris = () => { - fetch(`${apiHost}/api/shared_conversation/${identifier}`) - .then((res) => { - if (res.status === 404 || res.status === 400) navigate('/pagenotfound'); - return res.json(); - }) - .then((data) => { - if (data.success) { - setQueries(data.queries); - setTitle(data.title); - setDate(formatISODate(data.timestamp)); - data.api_key && setAPIKey(data.api_key); - } - }); + identifier && + fetch(`${apiHost}/api/shared_conversation/${identifier}`) + .then((res) => { + if (res.status === 404 || res.status === 400) + navigate('/pagenotfound'); + return res.json(); + }) + .then((data) => { + if (data.success) { + dispatch( + setFetchedData({ + queries: data.queries, + title: data.title, + date: data.date, + identifier, + }), + ); + data.api_key && dispatch(setClientApiKey(data.api_key)); + } + }); }; const handlePaste = (e: React.ClipboardEvent) => { e.preventDefault(); @@ -150,7 +168,6 @@ const SharedConversation = () => { onKeyDown={(e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); - //handleQuestionSubmission(); } }} >
@@ -163,7 +180,6 @@ const SharedConversation = () => {
diff --git a/frontend/src/conversation/sharedConversationSlice.ts b/frontend/src/conversation/sharedConversationSlice.ts index e69de29b..0e7bd4bd 100644 --- a/frontend/src/conversation/sharedConversationSlice.ts +++ b/frontend/src/conversation/sharedConversationSlice.ts @@ -0,0 +1,66 @@ +import { createSlice } from '@reduxjs/toolkit'; +import type { PayloadAction } from '@reduxjs/toolkit'; +import store from '../store'; +import { Query, Status } from '../conversation/conversationModels'; + +interface SharedConversationsType { + queries: Query[]; + apiKey?: string; + identifier: string | null; + status: Status; + date?: string; + title?: string; +} + +const initialState: SharedConversationsType = { + queries: [], + identifier: null, + status: 'idle', +}; + +export const sharedConversationSlice = createSlice({ + name: 'sharedConversation', + initialState, + reducers: { + setStatus(state, action: PayloadAction) { + state.status = action.payload; + }, + setIdentifier(state, action: PayloadAction) { + state.identifier = action.payload; + }, + setFetchedData( + state, + action: PayloadAction<{ + queries: Query[]; + title: string; + date: string; + identifier: string; + }>, + ) { + const { queries, title, identifier, date } = action.payload; + state.queries = queries; + state.title = title; + state.date = date; + state.identifier = identifier; + }, + setClientApiKey(state, action: PayloadAction) { + state.apiKey = action.payload; + }, + }, +}); + +export const { setStatus, setIdentifier, setFetchedData, setClientApiKey } = + sharedConversationSlice.actions; + +export const selectStatus = (state: RootState) => state.conversation.status; +export const selectClientAPIKey = (state: RootState) => + state.sharedConversation.apiKey; +export const selectQueries = (state: RootState) => + state.sharedConversation.queries; +export const selectTitle = (state: RootState) => state.sharedConversation.title; +export const selectDate = (state: RootState) => state.sharedConversation.date; + +type RootState = ReturnType; + +sharedConversationSlice; +export default sharedConversationSlice.reducer; diff --git a/frontend/src/store.ts b/frontend/src/store.ts index 5b7e7ea1..3d1408b3 100644 --- a/frontend/src/store.ts +++ b/frontend/src/store.ts @@ -1,5 +1,6 @@ import { configureStore } from '@reduxjs/toolkit'; import { conversationSlice } from './conversation/conversationSlice'; +import { sharedConversationSlice } from './conversation/sharedConversationSlice'; import { prefListenerMiddleware, prefSlice, @@ -42,6 +43,7 @@ const store = configureStore({ reducer: { preference: prefSlice.reducer, conversation: conversationSlice.reducer, + sharedConversation: sharedConversationSlice.reducer, }, middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(prefListenerMiddleware.middleware), From db7c001076111af28fcae56c90903f5c2b6be435 Mon Sep 17 00:00:00 2001 From: ManishMadan2882 Date: Fri, 26 Jul 2024 15:47:51 +0530 Subject: [PATCH 39/46] exclude the models only in root --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index ac5ff190..f5480882 100644 --- a/.gitignore +++ b/.gitignore @@ -172,5 +172,5 @@ application/vectors/ node_modules/ .vscode/settings.json -models/ +/models/ model/ From a0dd8f8e0fdb909e511535d7c35e3aa36714d099 Mon Sep 17 00:00:00 2001 From: ManishMadan2882 Date: Sat, 27 Jul 2024 02:42:33 +0530 Subject: [PATCH 40/46] integrated stream for shared(prompt) conv --- frontend/src/App.tsx | 2 +- .../src/conversation/SharedConversation.tsx | 100 +++++++++- .../src/conversation/conversationHandlers.ts | 61 +++++-- .../conversation/sharedConversationSlice.ts | 172 +++++++++++++++++- 4 files changed, 310 insertions(+), 25 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 05792187..9bad8724 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -10,7 +10,7 @@ import { useState } from 'react'; import Setting from './settings'; import './locale/i18n'; import { Outlet } from 'react-router-dom'; -import SharedConversation from './conversation/SharedConversation'; +import { SharedConversation } from './conversation/SharedConversation'; import { useDarkTheme } from './hooks'; inject(); diff --git a/frontend/src/conversation/SharedConversation.tsx b/frontend/src/conversation/SharedConversation.tsx index bd484c4b..4b73579b 100644 --- a/frontend/src/conversation/SharedConversation.tsx +++ b/frontend/src/conversation/SharedConversation.tsx @@ -1,5 +1,5 @@ import { Query } from './conversationModels'; -import { Fragment, useEffect, useRef } from 'react'; +import { Fragment, useEffect, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useNavigate, useParams } from 'react-router-dom'; @@ -7,9 +7,19 @@ import conversationService from '../api/services/conversationService'; import ConversationBubble from './ConversationBubble'; import Send from '../assets/send.svg'; import Spinner from '../assets/spinner.svg'; -import { selectClientAPIKey, setClientApiKey } from './sharedConversationSlice'; +import { + selectClientAPIKey, + setClientApiKey, + updateQuery, + addQuery, + fetchSharedAnswer, + selectStatus, +} from './sharedConversationSlice'; import { setIdentifier, setFetchedData } from './sharedConversationSlice'; + import { useDispatch } from 'react-redux'; +import { AppDispatch } from '../store'; + import { selectDate, selectTitle, @@ -17,19 +27,39 @@ import { } from './sharedConversationSlice'; import { useSelector } from 'react-redux'; const apiHost = import.meta.env.VITE_API_HOST || 'https://docsapi.arc53.com'; -const SharedConversation = () => { - const params = useParams(); + +export const SharedConversation = () => { const navigate = useNavigate(); - const { identifier } = params; //identifier is a uuid, not conversationId + const { identifier } = useParams(); //identifier is a uuid, not conversationId const queries = useSelector(selectQueries); const title = useSelector(selectTitle); const date = useSelector(selectDate); const apiKey = useSelector(selectClientAPIKey); + const status = useSelector(selectStatus); + const inputRef = useRef(null); const { t } = useTranslation(); - const dispatch = useDispatch(); - identifier && dispatch(setIdentifier(identifier)); + const dispatch = useDispatch(); + + const [lastQueryReturnedErr, setLastQueryReturnedErr] = useState(false); + const [eventInterrupt, setEventInterrupt] = useState(false); + const endMessageRef = useRef(null); + const handleUserInterruption = () => { + if (!eventInterrupt && status === 'loading') setEventInterrupt(true); + }; + useEffect(() => { + !eventInterrupt && scrollIntoView(); + }, [queries.length, queries[queries.length - 1]]); + + useEffect(() => { + identifier && dispatch(setIdentifier(identifier)); + const element = document.getElementById('inputbox') as HTMLInputElement; + if (element) { + element.focus(); + } + }, []); + function formatISODate(isoDateStr: string) { const date = new Date(isoDateStr); @@ -62,7 +92,21 @@ const SharedConversation = () => { const formattedDate = `Published ${month} ${day}, ${year} at ${hours}:${minutesStr} ${ampm}`; return formattedDate; } - const fetchQueris = () => { + useEffect(() => { + if (queries.length) { + queries[queries.length - 1].error && setLastQueryReturnedErr(true); + queries[queries.length - 1].response && setLastQueryReturnedErr(false); //considering a query that initially returned error can later include a response property on retry + } + }, [queries[queries.length - 1]]); + + const scrollIntoView = () => { + endMessageRef?.current?.scrollIntoView({ + behavior: 'smooth', + block: 'start', + }); + }; + + const fetchQueries = () => { identifier && conversationService .getSharedConversation(identifier || '') @@ -113,8 +157,44 @@ const SharedConversation = () => { } return responseView; }; + const handleQuestionSubmission = () => { + if (inputRef.current?.textContent && status !== 'loading') { + if (lastQueryReturnedErr) { + // update last failed query with new prompt + dispatch( + updateQuery({ + index: queries.length - 1, + query: { + prompt: inputRef.current.textContent, + }, + }), + ); + handleQuestion({ + question: queries[queries.length - 1].prompt, + isRetry: true, + }); + } else { + handleQuestion({ question: inputRef.current.textContent }); + } + inputRef.current.textContent = ''; + } + }; + + const handleQuestion = ({ + question, + isRetry = false, + }: { + question: string; + isRetry?: boolean; + }) => { + question = question.trim(); + if (question === '') return; + setEventInterrupt(false); + !isRetry && dispatch(addQuery({ prompt: question })); //dispatch only new queries + dispatch(fetchSharedAnswer({ question: '' })); + }; useEffect(() => { - fetchQueris(); + fetchQueries(); }, []); return ( @@ -169,6 +249,7 @@ const SharedConversation = () => { onKeyDown={(e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); + handleQuestionSubmission(); } }} >
@@ -180,6 +261,7 @@ const SharedConversation = () => { ) : (
diff --git a/frontend/src/conversation/conversationHandlers.ts b/frontend/src/conversation/conversationHandlers.ts index 05c2db0d..90bbc0a9 100644 --- a/frontend/src/conversation/conversationHandlers.ts +++ b/frontend/src/conversation/conversationHandlers.ts @@ -212,7 +212,7 @@ export function handleSendFeedback( }); } -export function fetchSharedAnswerSteaming( //for shared conversations +export function handleFetchSharedAnswerStreaming( //for shared conversations question: string, signal: AbortSignal, apiKey: string, @@ -224,19 +224,13 @@ export function fetchSharedAnswerSteaming( //for shared conversations }); return new Promise((resolve, reject) => { - const body = { + const payload = { question: question, history: JSON.stringify(history), - apiKey: apiKey, + api_key: apiKey, }; - fetch(apiHost + '/stream', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(body), - signal, - }) + conversationService + .answerStream(payload, signal) .then((response) => { if (!response.body) throw Error('No response body'); @@ -284,3 +278,48 @@ export function fetchSharedAnswerSteaming( //for shared conversations }); }); } + +export function handleFetchSharedAnswer( + question: string, + signal: AbortSignal, + apiKey: string, +): Promise< + | { + result: any; + answer: any; + sources: any; + query: string; + } + | { + result: any; + answer: any; + sources: any; + query: string; + title: any; + } +> { + return conversationService + .answer( + { + question: question, + api_key: apiKey, + }, + signal, + ) + .then((response) => { + if (response.ok) { + return response.json(); + } else { + return Promise.reject(new Error(response.statusText)); + } + }) + .then((data) => { + const result = data.answer; + return { + answer: result, + query: question, + result, + sources: data.sources, + }; + }); +} diff --git a/frontend/src/conversation/sharedConversationSlice.ts b/frontend/src/conversation/sharedConversationSlice.ts index 0e7bd4bd..ecc45cc6 100644 --- a/frontend/src/conversation/sharedConversationSlice.ts +++ b/frontend/src/conversation/sharedConversationSlice.ts @@ -1,8 +1,14 @@ import { createSlice } from '@reduxjs/toolkit'; import type { PayloadAction } from '@reduxjs/toolkit'; import store from '../store'; -import { Query, Status } from '../conversation/conversationModels'; +import { Query, Status, Answer } from '../conversation/conversationModels'; +import { createAsyncThunk } from '@reduxjs/toolkit'; +import { + handleFetchSharedAnswer, + handleFetchSharedAnswerStreaming, +} from './conversationHandlers'; +const API_STREAMING = import.meta.env.VITE_API_STREAMING === 'true'; interface SharedConversationsType { queries: Query[]; apiKey?: string; @@ -18,6 +24,85 @@ const initialState: SharedConversationsType = { status: 'idle', }; +export const fetchSharedAnswer = createAsyncThunk( + 'shared/fetchAnswer', + async ({ question }, { dispatch, getState, signal }) => { + console.log('bulaya sahab ji ?'); + const state = getState() as RootState; + if (state.preference && state.sharedConversation.apiKey) { + if (API_STREAMING) { + await handleFetchSharedAnswerStreaming( + question, + signal, + state.sharedConversation.apiKey, + state.sharedConversation.queries, + + (event) => { + const data = JSON.parse(event.data); + // check if the 'end' event has been received + if (data.type === 'end') { + // set status to 'idle' + dispatch(sharedConversationSlice.actions.setStatus('idle')); + } else if (data.type === 'error') { + // set status to 'failed' + dispatch(sharedConversationSlice.actions.setStatus('failed')); + dispatch( + sharedConversationSlice.actions.raiseError({ + index: state.conversation.queries.length - 1, + message: data.error, + }), + ); + } else { + const result = data.answer; + dispatch( + updateStreamingQuery({ + index: state.sharedConversation.queries.length - 1, + query: { response: result }, + }), + ); + } + }, + ); + } else { + const answer = await handleFetchSharedAnswer( + question, + signal, + state.sharedConversation.apiKey, + ); + if (answer) { + let sourcesPrepped = []; + sourcesPrepped = answer.sources.map((source: { title: string }) => { + if (source && source.title) { + const titleParts = source.title.split('/'); + return { + ...source, + title: titleParts[titleParts.length - 1], + }; + } + return source; + }); + + dispatch( + updateQuery({ + index: state.sharedConversation.queries.length - 1, + query: { response: answer.answer, sources: sourcesPrepped }, + }), + ); + dispatch(sharedConversationSlice.actions.setStatus('idle')); + } + } + } + return { + conversationId: null, + title: null, + answer: '', + query: question, + result: '', + sources: [], + }; + }, +); + export const sharedConversationSlice = createSlice({ name: 'sharedConversation', initialState, @@ -38,7 +123,11 @@ export const sharedConversationSlice = createSlice({ }>, ) { const { queries, title, identifier, date } = action.payload; - state.queries = queries; + const previousQueriesStr = localStorage.getItem(identifier); + const localySavedQueries: Query[] = previousQueriesStr + ? JSON.parse(previousQueriesStr) + : []; + state.queries = [...queries, ...localySavedQueries]; state.title = title; state.date = date; state.identifier = identifier; @@ -46,11 +135,86 @@ export const sharedConversationSlice = createSlice({ setClientApiKey(state, action: PayloadAction) { state.apiKey = action.payload; }, + addQuery(state, action: PayloadAction) { + state.queries.push(action.payload); + if (state.identifier) { + const previousQueriesStr = localStorage.getItem(state.identifier); + previousQueriesStr + ? localStorage.setItem( + state.identifier, + JSON.stringify([ + ...JSON.parse(previousQueriesStr), + action.payload, + ]), + ) + : localStorage.setItem( + state.identifier, + JSON.stringify([action.payload]), + ); + if (action.payload.prompt) { + fetchSharedAnswer({ question: action.payload.prompt }); + } + } + }, + updateStreamingQuery( + state, + action: PayloadAction<{ index: number; query: Partial }>, + ) { + const { index, query } = action.payload; + if (query.response != undefined) { + state.queries[index].response = + (state.queries[index].response || '') + query.response; + } else { + state.queries[index] = { + ...state.queries[index], + ...query, + }; + } + }, + updateQuery( + state, + action: PayloadAction<{ index: number; query: Partial }>, + ) { + const { index, query } = action.payload; + state.queries[index] = { + ...state.queries[index], + ...query, + }; + }, + raiseError( + state, + action: PayloadAction<{ index: number; message: string }>, + ) { + const { index, message } = action.payload; + state.queries[index].error = message; + }, + }, + extraReducers(builder) { + builder + .addCase(fetchSharedAnswer.pending, (state) => { + state.status = 'loading'; + }) + .addCase(fetchSharedAnswer.rejected, (state, action) => { + if (action.meta.aborted) { + state.status = 'idle'; + return state; + } + state.status = 'failed'; + state.queries[state.queries.length - 1].error = + 'Something went wrong. Please check your internet connection.'; + }); }, }); -export const { setStatus, setIdentifier, setFetchedData, setClientApiKey } = - sharedConversationSlice.actions; +export const { + setStatus, + setIdentifier, + setFetchedData, + setClientApiKey, + updateQuery, + updateStreamingQuery, + addQuery, +} = sharedConversationSlice.actions; export const selectStatus = (state: RootState) => state.conversation.status; export const selectClientAPIKey = (state: RootState) => From 360d790282dd17d59fdf7604168024f7ff8e455b Mon Sep 17 00:00:00 2001 From: ManishMadan2882 Date: Sun, 28 Jul 2024 00:58:55 +0530 Subject: [PATCH 41/46] save further queries to localstorage --- .../src/conversation/SharedConversation.tsx | 5 ++- .../conversation/sharedConversationSlice.ts | 41 +++++++++---------- .../src/modals/ShareConversationModal.tsx | 1 - 3 files changed, 24 insertions(+), 23 deletions(-) diff --git a/frontend/src/conversation/SharedConversation.tsx b/frontend/src/conversation/SharedConversation.tsx index 4b73579b..78d49a79 100644 --- a/frontend/src/conversation/SharedConversation.tsx +++ b/frontend/src/conversation/SharedConversation.tsx @@ -139,6 +139,7 @@ export const SharedConversation = () => { if (query.response) { responseView = ( { } else if (query.error) { responseView = ( { if (question === '') return; setEventInterrupt(false); !isRetry && dispatch(addQuery({ prompt: question })); //dispatch only new queries - dispatch(fetchSharedAnswer({ question: '' })); + dispatch(fetchSharedAnswer({ question })); }; useEffect(() => { fetchQueries(); @@ -220,6 +222,7 @@ export const SharedConversation = () => { return ( ( 'shared/fetchAnswer', async ({ question }, { dispatch, getState, signal }) => { - console.log('bulaya sahab ji ?'); const state = getState() as RootState; + if (state.preference && state.sharedConversation.apiKey) { if (API_STREAMING) { await handleFetchSharedAnswerStreaming( @@ -43,6 +43,7 @@ export const fetchSharedAnswer = createAsyncThunk( if (data.type === 'end') { // set status to 'idle' dispatch(sharedConversationSlice.actions.setStatus('idle')); + dispatch(saveToLocalStorage()); } else if (data.type === 'error') { // set status to 'failed' dispatch(sharedConversationSlice.actions.setStatus('failed')); @@ -137,24 +138,6 @@ export const sharedConversationSlice = createSlice({ }, addQuery(state, action: PayloadAction) { state.queries.push(action.payload); - if (state.identifier) { - const previousQueriesStr = localStorage.getItem(state.identifier); - previousQueriesStr - ? localStorage.setItem( - state.identifier, - JSON.stringify([ - ...JSON.parse(previousQueriesStr), - action.payload, - ]), - ) - : localStorage.setItem( - state.identifier, - JSON.stringify([action.payload]), - ); - if (action.payload.prompt) { - fetchSharedAnswer({ question: action.payload.prompt }); - } - } }, updateStreamingQuery( state, @@ -188,6 +171,21 @@ export const sharedConversationSlice = createSlice({ const { index, message } = action.payload; state.queries[index].error = message; }, + saveToLocalStorage(state) { + const previousQueriesStr = localStorage.getItem(state.identifier); + previousQueriesStr + ? localStorage.setItem( + state.identifier, + JSON.stringify([ + ...JSON.parse(previousQueriesStr), + state.queries[state.queries.length - 1], + ]), + ) + : localStorage.setItem( + state.identifier, + JSON.stringify([state.queries[state.queries.length - 1]]), + ); + }, }, extraReducers(builder) { builder @@ -214,6 +212,7 @@ export const { updateQuery, updateStreamingQuery, addQuery, + saveToLocalStorage, } = sharedConversationSlice.actions; export const selectStatus = (state: RootState) => state.conversation.status; diff --git a/frontend/src/modals/ShareConversationModal.tsx b/frontend/src/modals/ShareConversationModal.tsx index 0015402e..870c032c 100644 --- a/frontend/src/modals/ShareConversationModal.tsx +++ b/frontend/src/modals/ShareConversationModal.tsx @@ -105,7 +105,6 @@ export const ShareConversationModal = ({ conversationService .shareConversation(isPromptable, payload) .then((res) => { - console.log(res.status); return res.json(); }) .then((data) => { From d63b5d71a1af4d2e8f3ec107ea6b54b27c50583b Mon Sep 17 00:00:00 2001 From: ManishMadan2882 Date: Sun, 28 Jul 2024 19:48:48 +0530 Subject: [PATCH 42/46] prevent event bubble, open menu onhover --- frontend/src/conversation/Conversation.tsx | 2 +- .../src/conversation/ConversationTile.tsx | 61 +++++++++++-------- .../src/conversation/SharedConversation.tsx | 2 +- frontend/src/modals/ConfirmationModal.tsx | 2 + .../src/modals/ShareConversationModal.tsx | 7 ++- 5 files changed, 46 insertions(+), 28 deletions(-) diff --git a/frontend/src/conversation/Conversation.tsx b/frontend/src/conversation/Conversation.tsx index 4ceddbff..d9defb9e 100644 --- a/frontend/src/conversation/Conversation.tsx +++ b/frontend/src/conversation/Conversation.tsx @@ -289,7 +289,7 @@ export default function Conversation() { className="relative right-[38px] bottom-[24px] -mr-[30px] animate-spin cursor-pointer self-end bg-transparent" > ) : ( -
+
(false); const [isShareModalOpen, setShareModalState] = useState(false); + const [isHovered, setIsHovered] = useState(false); const [deleteModalState, setDeleteModalState] = useState('INACTIVE'); const menuRef = useRef(null); @@ -79,20 +81,24 @@ export default function ConversationTile({ return (
{ + setOpen(false); + setIsHovered(true); + }} + onMouseLeave={() => { + setIsHovered(false); + }} onClick={() => { - selectConversation(conversation.id); + conversationId !== conversation.id && + selectConversation(conversation.id); }} className={`my-auto mx-4 mt-4 flex h-9 cursor-pointer items-center justify-between gap-4 rounded-3xl hover:bg-gray-100 dark:hover:bg-[#28292E] ${ - conversationId === conversation.id + conversationId === conversation.id || isOpen ? 'bg-gray-100 dark:bg-[#28292E]' : '' }`} > -
+
)}
- {conversationId === conversation.id && ( + {(conversationId === conversation.id || isHovered || isOpen) && (
{isEdit ? (
@@ -120,34 +126,41 @@ export default function ConversationTile({ alt="Edit" className="mr-2 h-4 w-4 cursor-pointer text-white hover:opacity-50" id={`img-${conversation.id}`} - onClick={(event) => { + onClick={(event: SyntheticEvent) => { event.stopPropagation(); handleSaveConversation({ - id: conversationId, + id: conversation.id, name: conversationName, }); }} /> Exit { + onClick={(event: SyntheticEvent) => { event.stopPropagation(); onClear(); }} />
) : ( - )} {isOpen && ( -
+
diff --git a/frontend/src/conversation/SharedConversation.tsx b/frontend/src/conversation/SharedConversation.tsx index 78d49a79..cbe80717 100644 --- a/frontend/src/conversation/SharedConversation.tsx +++ b/frontend/src/conversation/SharedConversation.tsx @@ -262,7 +262,7 @@ export const SharedConversation = () => { className="relative right-[38px] bottom-[24px] -mr-[30px] animate-spin cursor-pointer self-end bg-transparent filter dark:invert" > ) : ( -
+
event.stopPropagation()} className={`${ modalState === 'ACTIVE' ? 'visible' : 'hidden' } fixed top-0 left-0 z-30 h-screen w-screen bg-gray-alpha`} diff --git a/frontend/src/modals/ShareConversationModal.tsx b/frontend/src/modals/ShareConversationModal.tsx index 870c032c..e26cbd38 100644 --- a/frontend/src/modals/ShareConversationModal.tsx +++ b/frontend/src/modals/ShareConversationModal.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react'; +import { SyntheticEvent, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useSelector } from 'react-redux'; import { @@ -117,7 +117,10 @@ export const ShareConversationModal = ({ }; return ( -
+
event.stopPropagation()} + className="z-100 fixed top-0 left-0 flex h-screen w-screen items-center justify-center bg-gray-alpha bg-opacity-50 text-chinese-black dark:text-silver" + >
- )} - {isOpen && ( -
- - - -
+

+ {conversationName} +

)}
- )} + {(conversationId === conversation.id || isHovered || isOpen) && ( +
+ {isEdit ? ( +
+ Edit { + event.stopPropagation(); + handleSaveConversation({ + id: conversation.id, + name: conversationName, + }); + }} + /> + Exit { + event.stopPropagation(); + onClear(); + }} + /> +
+ ) : ( + + )} + {isOpen && ( +
+ + + +
+ )} +
+ )} +
)} -
+ ); } diff --git a/frontend/src/locale/en.json b/frontend/src/locale/en.json index ce44b713..c7651f8b 100644 --- a/frontend/src/locale/en.json +++ b/frontend/src/locale/en.json @@ -108,7 +108,7 @@ "label": "Create a public page to share", "note": "Source document, personal information and further conversation will remain private", "create": "Create", - "option":"Allow users to prompt further" + "option": "Allow users to prompt further" } }, "sharedConv": { diff --git a/frontend/src/locale/es.json b/frontend/src/locale/es.json index a68d440c..d368749f 100644 --- a/frontend/src/locale/es.json +++ b/frontend/src/locale/es.json @@ -108,7 +108,7 @@ "label": "Crear una página pública para compartir", "note": "El documento original, la información personal y las conversaciones posteriores permanecerán privadas", "create": "Crear", - "option":"Permitir a los usuarios realizar más consultas." + "option": "Permitir a los usuarios realizar más consultas." } }, "sharedConv": { diff --git a/frontend/src/locale/jp.json b/frontend/src/locale/jp.json index f4e2980f..350faf09 100644 --- a/frontend/src/locale/jp.json +++ b/frontend/src/locale/jp.json @@ -108,7 +108,7 @@ "label": "共有ページを作成して共有する", "note": "ソースドキュメント、個人情報、および以降の会話は非公開のままになります", "create": "作成", - "option":"ユーザーがより多くのクエリを実行できるようにします。" + "option": "ユーザーがより多くのクエリを実行できるようにします。" } }, "sharedConv": { diff --git a/frontend/src/locale/zh.json b/frontend/src/locale/zh.json index a3ff940b..db93dcfc 100644 --- a/frontend/src/locale/zh.json +++ b/frontend/src/locale/zh.json @@ -108,7 +108,7 @@ "label": "创建用于分享的公共页面", "note": "源文档、个人信息和后续对话将保持私密", "create": "创建", - "option":"允许用户进行更多查询。" + "option": "允许用户进行更多查询。" } }, "sharedConv": { diff --git a/frontend/src/modals/ConfirmationModal.tsx b/frontend/src/modals/ConfirmationModal.tsx index f1bb4549..0b39440b 100644 --- a/frontend/src/modals/ConfirmationModal.tsx +++ b/frontend/src/modals/ConfirmationModal.tsx @@ -1,4 +1,3 @@ -import { SyntheticEvent } from 'react'; import Exit from '../assets/exit.svg'; import { ActiveState } from '../models/misc'; import { useTranslation } from 'react-i18next'; @@ -22,7 +21,6 @@ function ConfirmationModal({ const { t } = useTranslation(); return (
event.stopPropagation()} className={`${ modalState === 'ACTIVE' ? 'visible' : 'hidden' } fixed top-0 left-0 z-30 h-screen w-screen bg-gray-alpha`} diff --git a/frontend/src/modals/ShareConversationModal.tsx b/frontend/src/modals/ShareConversationModal.tsx index 939a4efc..c7ef0ad6 100644 --- a/frontend/src/modals/ShareConversationModal.tsx +++ b/frontend/src/modals/ShareConversationModal.tsx @@ -1,4 +1,4 @@ -import { SyntheticEvent, useState } from 'react'; +import { useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useSelector } from 'react-redux'; import { @@ -117,10 +117,7 @@ export const ShareConversationModal = ({ }; return ( -
event.stopPropagation()} - className="z-100 fixed top-0 left-0 flex h-screen w-screen items-center justify-center bg-gray-alpha bg-opacity-50 text-chinese-black dark:text-silver" - > +
) : (