From a2ef45e13f57976f84887f7825afbd49dc0b6440 Mon Sep 17 00:00:00 2001 From: jayantp2003 Date: Fri, 11 Oct 2024 17:08:04 +0530 Subject: [PATCH 001/354] Fix #859: Resolve issue with large zip breaking stream endpoint --- application/parser/file/rst_parser.py | 53 ++++++++++++++++++++++++++- 1 file changed, 52 insertions(+), 1 deletion(-) diff --git a/application/parser/file/rst_parser.py b/application/parser/file/rst_parser.py index 633ec844..eb9043b2 100644 --- a/application/parser/file/rst_parser.py +++ b/application/parser/file/rst_parser.py @@ -91,6 +91,48 @@ class RstParser(BaseParser): ] return rst_tups + def chunk_by_token_count(self, text: str, max_tokens: int = 100) -> List[str]: + """Chunk text by token count.""" + # words = text.split() + # chunks = [] + # current_chunk = [] + # current_token_count = 0 + + # for word in words: + # word_token_len = len(word.split()) # Token count + # if current_token_count + word_token_len > max_tokens: + # chunks.append(" ".join(current_chunk)) + # current_chunk = [] + # current_token_count = 0 + # current_chunk.append(word) + # current_token_count += word_token_len + + # if current_chunk: + # chunks.append(" ".join(current_chunk)) + + # return chunks + + + avg_token_length = 5 + + # Calculate approximate chunk size in characters + chunk_size = max_tokens * avg_token_length + + # Split text into chunks + chunks = [] + for i in range(0, len(text), chunk_size): + chunk = text[i:i+chunk_size] + + # Adjust chunk to end at a word boundary + if i + chunk_size < len(text): + last_space = chunk.rfind(' ') + if last_space != -1: + chunk = chunk[:last_space] + + chunks.append(chunk.strip()) + + return chunks + def remove_images(self, content: str) -> str: pattern = r"\.\. image:: (.*)" content = re.sub(pattern, "", content) @@ -136,7 +178,7 @@ class RstParser(BaseParser): return {} def parse_tups( - self, filepath: Path, errors: str = "ignore" + self, filepath: Path, errors: str = "ignore",max_tokens: Optional[int] = 1000 ) -> List[Tuple[Optional[str], str]]: """Parse file into tuples.""" with open(filepath, "r") as f: @@ -156,6 +198,15 @@ class RstParser(BaseParser): rst_tups = self.remove_whitespaces_excess(rst_tups) if self._remove_characters_excess: rst_tups = self.remove_characters_excess(rst_tups) + + # Apply chunking if max_tokens is provided + if max_tokens is not None: + chunked_tups = [] + for header, text in rst_tups: + chunks = self.chunk_by_token_count(text, max_tokens) + for idx, chunk in enumerate(chunks): + chunked_tups.append((f"{header} - Chunk {idx + 1}", chunk)) + return chunked_tups return rst_tups def parse_file( From 3db07f3a26c56d29e6d5a7d99c097206ac64cdce Mon Sep 17 00:00:00 2001 From: jayantp2003 Date: Fri, 11 Oct 2024 17:10:12 +0530 Subject: [PATCH 002/354] Fix #859: Resolve issue with large zip breaking stream endpoint --- application/parser/file/rst_parser.py | 25 +------------------------ 1 file changed, 1 insertion(+), 24 deletions(-) diff --git a/application/parser/file/rst_parser.py b/application/parser/file/rst_parser.py index eb9043b2..d39a0837 100644 --- a/application/parser/file/rst_parser.py +++ b/application/parser/file/rst_parser.py @@ -93,37 +93,14 @@ class RstParser(BaseParser): def chunk_by_token_count(self, text: str, max_tokens: int = 100) -> List[str]: """Chunk text by token count.""" - # words = text.split() - # chunks = [] - # current_chunk = [] - # current_token_count = 0 - - # for word in words: - # word_token_len = len(word.split()) # Token count - # if current_token_count + word_token_len > max_tokens: - # chunks.append(" ".join(current_chunk)) - # current_chunk = [] - # current_token_count = 0 - # current_chunk.append(word) - # current_token_count += word_token_len - - # if current_chunk: - # chunks.append(" ".join(current_chunk)) - - # return chunks - avg_token_length = 5 - # Calculate approximate chunk size in characters chunk_size = max_tokens * avg_token_length - - # Split text into chunks + chunks = [] for i in range(0, len(text), chunk_size): chunk = text[i:i+chunk_size] - - # Adjust chunk to end at a word boundary if i + chunk_size < len(text): last_space = chunk.rfind(' ') if last_space != -1: From fe18d6e638a255499b41ed70e8d82a15293007b2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Nov 2024 20:41:57 +0000 Subject: [PATCH 003/354] build(deps): bump react-dropzone from 14.2.3 to 14.3.5 in /frontend Bumps [react-dropzone](https://github.com/react-dropzone/react-dropzone) from 14.2.3 to 14.3.5. - [Release notes](https://github.com/react-dropzone/react-dropzone/releases) - [Commits](https://github.com/react-dropzone/react-dropzone/compare/v14.2.3...v14.3.5) --- updated-dependencies: - dependency-name: react-dropzone dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- frontend/package-lock.json | 50 ++++++++++++-------------------------- frontend/package.json | 2 +- 2 files changed, 17 insertions(+), 35 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 9973bb9e..9f25e198 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -17,7 +17,7 @@ "react-chartjs-2": "^5.2.0", "react-copy-to-clipboard": "^5.1.0", "react-dom": "^18.3.1", - "react-dropzone": "^14.2.3", + "react-dropzone": "^14.3.5", "react-i18next": "^15.0.2", "react-markdown": "^9.0.1", "react-redux": "^8.0.5", @@ -2371,9 +2371,9 @@ } }, "node_modules/attr-accept": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.2.tgz", - "integrity": "sha512-7prDjvt9HmqiZ0cl5CRjtS84sEyhsHP2coDkaZKRKVfCDo9s7iw7ChVmar78Gu9pC4SoR/28wFu/G5JJhTnqEg==", + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.4.tgz", + "integrity": "sha512-2pA6xFIbdTUDCAwjN8nQwI+842VwzbDUXO2IYlpPXQIORgKnavorcr4Ce3rwh+zsNg9zK7QPsdvDj3Lum4WX4w==", "engines": { "node": ">=4" } @@ -3075,24 +3075,6 @@ "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", "dev": true }, - "node_modules/easy-speech": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/easy-speech/-/easy-speech-2.4.0.tgz", - "integrity": "sha512-wpMv29DEoeP/eyXr4aXpDqd9DvlXl7aQs7BgfKbjGVxqkmQPgNmpbF5YULaTH5bc/5qrteg5MDfCD2Zd0qr4rQ==", - "funding": [ - { - "type": "GitHub", - "url": "https://github.com/sponsors/jankapunkt" - }, - { - "type": "PayPal", - "url": "https://paypal.me/kuesterjan" - } - ], - "engines": { - "node": ">= 14.x" - } - }, "node_modules/electron-to-chromium": { "version": "1.5.11", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.11.tgz", @@ -4172,20 +4154,20 @@ } }, "node_modules/file-selector": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/file-selector/-/file-selector-0.6.0.tgz", - "integrity": "sha512-QlZ5yJC0VxHxQQsQhXvBaC7VRJ2uaxTf+Tfpu4Z/OcVQJVpZO+DGU0rkoVW5ce2SccxugvpBJoMvUs59iILYdw==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/file-selector/-/file-selector-2.1.0.tgz", + "integrity": "sha512-ZuXAqGePcSPz4JuerOY06Dzzq0hrmQ6VGoXVzGyFI1npeOfBgqGIKKpznfYWRkSLJlXutkqVC5WvGZtkFVhu9Q==", "dependencies": { - "tslib": "^2.4.0" + "tslib": "^2.7.0" }, "engines": { "node": ">= 12" } }, "node_modules/file-selector/node_modules/tslib": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", - "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==" + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" }, "node_modules/fill-range": { "version": "7.1.1", @@ -7847,12 +7829,12 @@ } }, "node_modules/react-dropzone": { - "version": "14.2.3", - "resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-14.2.3.tgz", - "integrity": "sha512-O3om8I+PkFKbxCukfIR3QAGftYXDZfOE2N1mr/7qebQJHs7U+/RSL/9xomJNpRg9kM5h9soQSdf0Gc7OHF5Fug==", + "version": "14.3.5", + "resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-14.3.5.tgz", + "integrity": "sha512-9nDUaEEpqZLOz5v5SUcFA0CjM4vq8YbqO0WRls+EYT7+DvxUdzDPKNCPLqGfj3YL9MsniCLCD4RFA6M95V6KMQ==", "dependencies": { - "attr-accept": "^2.2.2", - "file-selector": "^0.6.0", + "attr-accept": "^2.2.4", + "file-selector": "^2.1.0", "prop-types": "^15.8.1" }, "engines": { diff --git a/frontend/package.json b/frontend/package.json index 83d531d6..a9afb3c1 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -28,7 +28,7 @@ "react-chartjs-2": "^5.2.0", "react-copy-to-clipboard": "^5.1.0", "react-dom": "^18.3.1", - "react-dropzone": "^14.2.3", + "react-dropzone": "^14.3.5", "react-i18next": "^15.0.2", "react-markdown": "^9.0.1", "react-redux": "^8.0.5", From 0ef232f7317b96f7046505290deb8938d93bbc5b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Nov 2024 21:01:52 +0000 Subject: [PATCH 004/354] build(deps): bump werkzeug from 3.0.4 to 3.1.3 in /application Bumps [werkzeug](https://github.com/pallets/werkzeug) from 3.0.4 to 3.1.3. - [Release notes](https://github.com/pallets/werkzeug/releases) - [Changelog](https://github.com/pallets/werkzeug/blob/main/CHANGES.rst) - [Commits](https://github.com/pallets/werkzeug/compare/3.0.4...3.1.3) --- updated-dependencies: - dependency-name: werkzeug dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- application/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/requirements.txt b/application/requirements.txt index 2f28c2ea..36a008a7 100644 --- a/application/requirements.txt +++ b/application/requirements.txt @@ -85,5 +85,5 @@ tzdata==2024.2 urllib3==2.2.3 vine==5.1.0 wcwidth==0.2.13 -werkzeug==3.0.4 +werkzeug==3.1.3 yarl==1.11.1 \ No newline at end of file From 58af393968a8bd5f6c74832838927fafc69fbfe1 Mon Sep 17 00:00:00 2001 From: rohittcodes Date: Wed, 13 Nov 2024 02:15:05 +0530 Subject: [PATCH 005/354] feat: wrapper modal --- frontend/src/Navigation.tsx | 17 +- frontend/src/modals/ConfirmationModal.tsx | 76 ++++---- frontend/src/modals/CreateAPIKeyModal.tsx | 163 +++++++++--------- .../src/modals/ShareConversationModal.tsx | 145 ++++++++-------- frontend/src/modals/WrapperModal.tsx | 48 ++++++ frontend/src/modals/types/index.ts | 4 + frontend/src/settings/Documents.tsx | 2 +- frontend/src/upload/Upload.tsx | 37 ++-- 8 files changed, 254 insertions(+), 238 deletions(-) create mode 100644 frontend/src/modals/WrapperModal.tsx create mode 100644 frontend/src/modals/types/index.ts diff --git a/frontend/src/Navigation.tsx b/frontend/src/Navigation.tsx index 324d5aa0..f5eee00e 100644 --- a/frontend/src/Navigation.tsx +++ b/frontend/src/Navigation.tsx @@ -32,7 +32,6 @@ import { selectConversations, selectModalStateDeleteConv, selectSelectedDocs, - selectSelectedDocsStatus, selectSourceDocs, selectPaginatedDocuments, setConversations, @@ -85,10 +84,6 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) { const [apiKeyModalState, setApiKeyModalState] = useState('INACTIVE'); - const isSelectedDocsSet = useSelector(selectSelectedDocsStatus); - const [selectedDocsModalState, setSelectedDocsModalState] = - useState(isSelectedDocsSet ? 'INACTIVE' : 'ACTIVE'); - const [uploadModalState, setUploadModalState] = useState('INACTIVE'); @@ -491,11 +486,13 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) { setModalState={setModalStateDeleteConv} handleDeleteAllConv={handleDeleteAllConversations} /> - + {uploadModalState === 'ACTIVE' && ( + setUploadModalState('INACTIVE')} + > + )} ); } diff --git a/frontend/src/modals/ConfirmationModal.tsx b/frontend/src/modals/ConfirmationModal.tsx index 0b39440b..c69dcedd 100644 --- a/frontend/src/modals/ConfirmationModal.tsx +++ b/frontend/src/modals/ConfirmationModal.tsx @@ -1,6 +1,6 @@ -import Exit from '../assets/exit.svg'; import { ActiveState } from '../models/misc'; import { useTranslation } from 'react-i18next'; +import WrapperModal from './WrapperModal'; function ConfirmationModal({ message, modalState, @@ -20,49 +20,43 @@ function ConfirmationModal({ }) { const { t } = useTranslation(); return ( -
-
-
- -
-

- {message} -

-
-
- - + <> + {modalState === 'ACTIVE' && ( + { + setModalState('INACTIVE'); + handleCancel && handleCancel(); + }} + > +
+
+

+ {message} +

+
+
+ + +
-
-
-
+ + )} + ); } diff --git a/frontend/src/modals/CreateAPIKeyModal.tsx b/frontend/src/modals/CreateAPIKeyModal.tsx index eb085a28..5c8c75b8 100644 --- a/frontend/src/modals/CreateAPIKeyModal.tsx +++ b/frontend/src/modals/CreateAPIKeyModal.tsx @@ -3,11 +3,11 @@ 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'; +import WrapperModal from './WrapperModal'; const embeddingsName = import.meta.env.VITE_EMBEDDINGS_NAME || @@ -73,91 +73,82 @@ export default function CreateAPIKeyModal({ 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" - /> -
- + +
+ + {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/ShareConversationModal.tsx b/frontend/src/modals/ShareConversationModal.tsx index fbb49468..3f87839e 100644 --- a/frontend/src/modals/ShareConversationModal.tsx +++ b/frontend/src/modals/ShareConversationModal.tsx @@ -10,7 +10,6 @@ import { 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 || @@ -19,6 +18,7 @@ const embeddingsName = type StatusType = 'loading' | 'idle' | 'fetched' | 'failed'; import conversationService from '../api/services/conversationService'; +import WrapperModal from './WrapperModal'; export const ShareConversationModal = ({ close, @@ -99,85 +99,78 @@ export const ShareConversationModal = ({ }; return ( -
-
- -
-

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

-

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

-
- {t('modals.shareConv.option')} - -
- {allowPrompt && ( -
- - setSourcePath(selection) - } - options={extractDocPaths(sourceDocs ?? [])} - size="w-full" - rounded="xl" + +
+

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

+

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

+
+ {t('modals.shareConv.option')} +
-
+
); }; diff --git a/frontend/src/modals/WrapperModal.tsx b/frontend/src/modals/WrapperModal.tsx new file mode 100644 index 00000000..1a012871 --- /dev/null +++ b/frontend/src/modals/WrapperModal.tsx @@ -0,0 +1,48 @@ +import React, { useEffect, useRef } from 'react'; +import { WrapperModalProps } from './types'; +import Exit from '../assets/exit.svg'; + +const WrapperModal: React.FC = ({ children, close }) => { + const modalRef = useRef(null); + + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if ( + modalRef.current && + !modalRef.current.contains(event.target as Node) + ) { + close(); + } + }; + + const handleEscapePress = (event: KeyboardEvent) => { + if (event.key === 'Escape') { + close(); + } + }; + + document.addEventListener('mousedown', handleClickOutside); + document.addEventListener('keydown', handleEscapePress); + + return () => { + document.removeEventListener('mousedown', handleClickOutside); + document.removeEventListener('keydown', handleEscapePress); + }; + }, [close]); + + return ( +
+
+ + {children} +
+
+ ); +}; + +export default WrapperModal; diff --git a/frontend/src/modals/types/index.ts b/frontend/src/modals/types/index.ts new file mode 100644 index 00000000..2010bbb9 --- /dev/null +++ b/frontend/src/modals/types/index.ts @@ -0,0 +1,4 @@ +export type WrapperModalProps = { + children?: React.ReactNode; + close: () => void; +}; diff --git a/frontend/src/settings/Documents.tsx b/frontend/src/settings/Documents.tsx index f91a3355..db2e5855 100644 --- a/frontend/src/settings/Documents.tsx +++ b/frontend/src/settings/Documents.tsx @@ -255,9 +255,9 @@ const Documents: React.FC = ({
{/* Your Upload component */} setModalState('INACTIVE')} />
diff --git a/frontend/src/upload/Upload.tsx b/frontend/src/upload/Upload.tsx index 4fee88f6..612456d5 100644 --- a/frontend/src/upload/Upload.tsx +++ b/frontend/src/upload/Upload.tsx @@ -4,7 +4,6 @@ import { useTranslation } from 'react-i18next'; import { useDispatch, useSelector } from 'react-redux'; import userService from '../api/services/userService'; -import Exit from '../assets/exit.svg'; import ArrowLeft from '../assets/arrow-left.svg'; import FileUpload from '../assets/file_upload.svg'; import WebsiteCollect from '../assets/website_collect.svg'; @@ -17,15 +16,16 @@ import { setSourceDocs, selectSourceDocs, } from '../preferences/preferenceSlice'; +import WrapperModal from '../modals/WrapperModal'; function Upload({ - modalState, setModalState, isOnboarding, + close, }: { - modalState: ActiveState; setModalState: (state: ActiveState) => void; isOnboarding: boolean; + close: () => void; }) { const [docName, setDocName] = useState(''); const [urlName, setUrlName] = useState(''); @@ -626,28 +626,17 @@ function Upload({ } return ( -
{ + close(); + setDocName(''); + setfiles([]); + setModalState('INACTIVE'); + setActiveTab(null); + }} > -
- {!isOnboarding && !progress && ( - - )} - {view} -
-
+ {view} + ); } From 541a6417b707f6a19527f7b0330a9e95c864323a Mon Sep 17 00:00:00 2001 From: ManishMadan2882 Date: Thu, 14 Nov 2024 04:51:26 +0530 Subject: [PATCH 006/354] (refactor): separate widget core --- .../src/components/DocsGPTWidget.tsx | 110 +++++++++++------- 1 file changed, 65 insertions(+), 45 deletions(-) diff --git a/extensions/react-widget/src/components/DocsGPTWidget.tsx b/extensions/react-widget/src/components/DocsGPTWidget.tsx index 1baa4c62..355056ac 100644 --- a/extensions/react-widget/src/components/DocsGPTWidget.tsx +++ b/extensions/react-widget/src/components/DocsGPTWidget.tsx @@ -1,9 +1,9 @@ "use client"; import React, { useRef } from 'react' import DOMPurify from 'dompurify'; -import styled, { keyframes, createGlobalStyle } from 'styled-components'; +import styled, { keyframes } from 'styled-components'; import { PaperPlaneIcon, RocketIcon, ExclamationTriangleIcon, Cross2Icon } from '@radix-ui/react-icons'; -import { FEEDBACK, MESSAGE_TYPE, Query, Status, WidgetProps } from '../types/index'; +import { FEEDBACK, MESSAGE_TYPE, Query, Status, WidgetCoreProps, WidgetProps } from '../types/index'; import { fetchAnswerStreaming, sendFeedback } from '../requests/streamingApi'; import { ThemeProvider } from 'styled-components'; import Like from "../assets/like.svg" @@ -66,13 +66,14 @@ const WidgetContainer = styled.div<{ modal?: boolean, isOpen?: boolean }>` right: ${props => props.modal ? '50%' : '10px'}; bottom: ${props => props.modal ? '50%' : '10px'}; z-index: 1000; - display: none; transform-origin:100% 100%; &.open { animation: createBox 250ms cubic-bezier(0.25, 0.1, 0.25, 1) forwards; + display: block; } &.close { animation: closeBox 250ms cubic-bezier(0.25, 0.1, 0.25, 1) forwards; + display: none; } ${props => props.modal && "transform : translate(50%,50%);" @@ -119,11 +120,11 @@ const StyledContainer = styled.div<{ isOpen: boolean }>` box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05), 0 2px 4px rgba(0, 0, 0, 0.1); padding: 26px 26px 0px 26px; animation: ${({ isOpen, theme }) => - theme.dimensions.size === 'large' - ? isOpen - ? 'fadeIn 150ms ease-in forwards' - : 'fadeOut 150ms ease-in forwards' - : isOpen + theme.dimensions.size === 'large' + ? isOpen + ? 'fadeIn 150ms ease-in forwards' + : 'fadeOut 150ms ease-in forwards' + : isOpen ? 'openContainer 150ms ease-in forwards' : 'closeContainer 250ms ease-in forwards'}; @keyframes openContainer { @@ -478,7 +479,49 @@ const Hero = ({ title, description, theme }: { title: string, description: strin ); }; -export const DocsGPTWidget = ({ +export const DocsGPTWidget = (props: WidgetProps) => { + + const { + buttonIcon = 'https://d3dg1063dc54p9.cloudfront.net/widget/chat.svg', + buttonText = 'Ask a question', + buttonBg = 'linear-gradient(to bottom right, #5AF0EC, #E80D9D)', + defaultOpen = false, + ...coreProps + } = props + + const [open, setOpen] = React.useState(defaultOpen); + const [isAnimatingButton, setIsAnimatingButton] = React.useState(false); + const [isFloatingButtonVisible, setIsFloatingButtonVisible] = React.useState(true); + const widgetRef = useRef(null) + + + const handleClose = () => { + setOpen(false); + setTimeout(() => { + if (widgetRef.current) + widgetRef.current.style.display = "none"; + setIsFloatingButtonVisible(true); + setIsAnimatingButton(true); + setTimeout(() => setIsAnimatingButton(false), 200); + }, 250) + }; + const handleOpen = () => { + setOpen(true); + setIsFloatingButtonVisible(false); + if (widgetRef.current) + widgetRef.current.style.display = 'block' + } + return ( + <> + + + + ) +} +export const WidgetCore = ({ apiHost = 'https://gptcloud.arc53.com', apiKey = '82962c9a-aa77-4152-94e5-a4f84fd44c6a', avatar = 'https://d3dg1063dc54p9.cloudfront.net/cute-docsgpt.png', @@ -488,22 +531,19 @@ export const DocsGPTWidget = ({ heroDescription = 'This chatbot is built with DocsGPT and utilises GenAI, please review important information using sources.', size = 'small', theme = 'dark', - buttonIcon = 'https://d3dg1063dc54p9.cloudfront.net/widget/chat.svg', - buttonText = 'Ask a question', - buttonBg = 'linear-gradient(to bottom right, #5AF0EC, #E80D9D)', collectFeedback = true, - deafultOpen = false -}: WidgetProps) => { - const [prompt, setPrompt] = React.useState(''); + widgetRef = null, + isOpen = false, + prefilledQuery = "", + handleClose +}: WidgetCoreProps) => { + const [prompt, setPrompt] = React.useState(prefilledQuery); const [status, setStatus] = React.useState('idle'); - const [queries, setQueries] = React.useState([]) - const [conversationId, setConversationId] = React.useState(null) - const [open, setOpen] = React.useState(deafultOpen) + const [queries, setQueries] = React.useState([]); + const [conversationId, setConversationId] = React.useState(null); const [eventInterrupt, setEventInterrupt] = React.useState(false); //click or scroll by user while autoScrolling - const [isAnimatingButton, setIsAnimatingButton] = React.useState(false); - const [isFloatingButtonVisible, setIsFloatingButtonVisible] = React.useState(true); - const isBubbleHovered = useRef(false) - const widgetRef = useRef(null) + + const isBubbleHovered = useRef(false); const endMessageRef = React.useRef(null); const md = new MarkdownIt(); @@ -615,37 +655,17 @@ export const DocsGPTWidget = ({ const handleImageError = (event: React.SyntheticEvent) => { event.currentTarget.src = "https://d3dg1063dc54p9.cloudfront.net/cute-docsgpt.png"; }; - const handleClose = () => { - setOpen(false); - setTimeout(() => { - if (widgetRef.current) widgetRef.current.style.display = "none"; - setIsFloatingButtonVisible(true); - setIsAnimatingButton(true); - setTimeout(() => setIsAnimatingButton(false), 200); - }, 250) - }; - const handleOpen = () => { - setOpen(true); - setIsFloatingButtonVisible(false); - if (widgetRef.current) - widgetRef.current.style.display = 'block' - } const dimensions = typeof size === 'object' && 'custom' in size ? sizesConfig.getCustom(size.custom) : sizesConfig[size]; - return ( - {open && size === 'large' && + {isOpen && size === 'large' && } - - - { + + {
From 9409e4498f7f4579d25f5ab8d4a0e5a7ef903180 Mon Sep 17 00:00:00 2001 From: ManishMadan2882 Date: Thu, 14 Nov 2024 04:52:15 +0530 Subject: [PATCH 007/354] (feat:search bar) initiating seach bar --- extensions/react-widget/package.json | 2 +- extensions/react-widget/src/App.tsx | 8 +- .../react-widget/src/components/SearchBar.tsx | 177 ++++++++++++++++++ extensions/react-widget/src/index.html | 6 +- extensions/react-widget/src/index.ts | 3 +- extensions/react-widget/src/main.tsx | 21 ++- .../react-widget/src/requests/searchAPI.ts | 34 ++++ extensions/react-widget/src/types/index.ts | 21 ++- 8 files changed, 257 insertions(+), 15 deletions(-) create mode 100644 extensions/react-widget/src/components/SearchBar.tsx create mode 100644 extensions/react-widget/src/requests/searchAPI.ts diff --git a/extensions/react-widget/package.json b/extensions/react-widget/package.json index a1097403..554b2438 100644 --- a/extensions/react-widget/package.json +++ b/extensions/react-widget/package.json @@ -32,7 +32,7 @@ "scripts": { "build": "parcel build src/main.tsx --public-url ./", "build:react": "parcel build src/index.ts", - "dev": "parcel src/index.html -p 3000", + "dev": "parcel -p 3000", "test": "jest", "lint": "eslint", "check": "tsc --noEmit", diff --git a/extensions/react-widget/src/App.tsx b/extensions/react-widget/src/App.tsx index ec9de47b..4bb24bae 100644 --- a/extensions/react-widget/src/App.tsx +++ b/extensions/react-widget/src/App.tsx @@ -1,11 +1,11 @@ import React from "react" import {DocsGPTWidget} from "./components/DocsGPTWidget" -const App = () => { +import {SearchBar} from "./components/SearchBar" +export const App = () => { return (
+
) -} - -export default App \ No newline at end of file +} \ No newline at end of file diff --git a/extensions/react-widget/src/components/SearchBar.tsx b/extensions/react-widget/src/components/SearchBar.tsx new file mode 100644 index 00000000..f16271a3 --- /dev/null +++ b/extensions/react-widget/src/components/SearchBar.tsx @@ -0,0 +1,177 @@ +import React from 'react' +import styled, { keyframes, createGlobalStyle } from 'styled-components'; +import { WidgetCore } from './DocsGPTWidget'; +import { SearchBarProps } from '@/types'; +import { getSearchResults } from '../requests/searchAPI' +import { Result } from '@/types'; +import MarkdownIt from 'markdown-it'; +import DOMPurify from 'dompurify'; +const Main = styled.div` + font-family: sans-serif; +` +const TextField = styled.input` + padding: 8px; + border-radius: 8px; + display: inline; + color: rgb(107 114 128); + outline: none; + border: 2px solid transparent; + background-color: rgba(0, 0, 0, .05);; + &:focus { + outline: #467f95; /* remove default outline */ + border:2px solid skyblue; /* change border color on focus */ + box-shadow: 0px 0px 2px skyblue; /* add a red box shadow on focus */ + } +` + +const Container = styled.div` + position: relative; + display: inline-block; +` +const SearchResults = styled.div` + position: absolute; + background-color: white; + opacity: 90%; + border: 1px solid rgba(0, 0, 0, .1); + border-radius: 12px; + padding: 20px; + width: 576px; + z-index: 100; + height: 25vh; + overflow-y: auto; + top: 45px; + scrollbar-color: lab(48.438 0 0 / 0.4) rgba(0, 0, 0, 0); + scrollbar-gutter: stable; + scrollbar-width: thin; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05), 0 2px 4px rgba(0, 0, 0, 0.1); + backdrop-filter: blur(16px); +` +const Title = styled.h3` + font-size: 12px; + color: rgb(107, 114, 128); + padding-bottom: 4px; + font-weight: 600; + text-transform: uppercase; + border-bottom: 1px solid rgba(0, 0, 0, 0.1); + +` + +const Content = styled.div` + font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; +` +const Markdown = styled.div` + +font-size: 12px; + pre { + padding: 8px; + width: 90%; + font-size: 12px; + border-radius: 6px; + overflow-x: auto; + background-color: #1B1C1F; + color: #fff ; + } + + h1,h2 { + font-size: 16px; + font-weight: 600; + color: rgb(31, 41, 55); + } + + + h3 { + font-size: 14px; + } + + p { + margin: 0px; + line-height: 1.35rem; + } + + code:not(pre code) { + border-radius: 6px; + padding: 4px 4px; + font-size: 12px; + display: inline-block; + background-color: #646464; + color: #fff ; + } + + code { + white-space: pre-wrap ; + overflow-wrap: break-word; + word-break: break-all; + } + + ul{ + padding:0px; + list-style-position: inside; + } +` +export const SearchBar = ({ + apiKey = "79bcbf0e-3dd1-4ac3-b893-e41b3d40ec8d", + apiHost = "http://127.0.0.1:7091", + theme = "dark" +}: SearchBarProps) => { + const [input, setInput] = React.useState("") + const [isWidgetOpen, setIsWidgetOpen] = React.useState(false); + const inputRef = React.useRef(null) + const [results, setResults] = React.useState([]) + React.useEffect(() => { + input.length > 0 ? + getSearchResults(input, apiKey, apiHost) + .then((data) => setResults(data)) + .catch((err) => console.log(err)) + : + setResults([]) + }, [input]) + const handleKeyDown = (event: React.KeyboardEvent) => { + if (event.key === 'Enter') { + setIsWidgetOpen(true); + } + }; + const handleClose = () => { + setIsWidgetOpen(false); + } + const md = new MarkdownIt(); + return ( +
+ + handleKeyDown(e)} + placeholder='Search here or Ask DocsGPT' + value={input} + onChange={(e) => setInput(e.target.value)} + /> + { + results.length > 0 && ( + + {results.map((res) => ( +
+ {res.title} + + + +
+ )) + } +
+ + ) + } +
+ + +
+ ) +} \ No newline at end of file diff --git a/extensions/react-widget/src/index.html b/extensions/react-widget/src/index.html index 0f0710d5..40eaad15 100644 --- a/extensions/react-widget/src/index.html +++ b/extensions/react-widget/src/index.html @@ -9,11 +9,11 @@
- - --> diff --git a/extensions/react-widget/src/index.ts b/extensions/react-widget/src/index.ts index 1efa89a6..e29b85a5 100644 --- a/extensions/react-widget/src/index.ts +++ b/extensions/react-widget/src/index.ts @@ -1 +1,2 @@ -export { DocsGPTWidget } from "./components/DocsGPTWidget"; \ No newline at end of file +export {SearchBar} from "./components/SearchBar" +export { DocsGPTWidget } from "./components/DocsGPTWidget"; diff --git a/extensions/react-widget/src/main.tsx b/extensions/react-widget/src/main.tsx index 4fb3bbb4..a8542e26 100644 --- a/extensions/react-widget/src/main.tsx +++ b/extensions/react-widget/src/main.tsx @@ -1,12 +1,25 @@ -import React from 'react'; -import { createRoot } from 'react-dom/client'; -import { DocsGPTWidget } from './components/DocsGPTWidget'; +import { createRoot } from "react-dom/client"; +import { App } from "./App"; +import { DocsGPTWidget } from './components/DocsGPTWidget'; +import { SearchBar } from './components/SearchBar'; +import React from "react"; if (typeof window !== 'undefined') { const renderWidget = (elementId: string, props = {}) => { const root = createRoot(document.getElementById(elementId) as HTMLElement); root.render(); }; + const renderSearchBar = (elementId: string, props = {}) => { + const root = createRoot(document.getElementById(elementId) as HTMLElement); + root.render(); + }; (window as any).renderDocsGPTWidget = renderWidget; + + (window as any).renderSearchBar = renderSearchBar; } -export { DocsGPTWidget }; \ No newline at end of file +const container = document.getElementById("app") as HTMLElement; +const root = createRoot(container) +root.render(); + +export { DocsGPTWidget }; +export { SearchBar } diff --git a/extensions/react-widget/src/requests/searchAPI.ts b/extensions/react-widget/src/requests/searchAPI.ts new file mode 100644 index 00000000..18df3691 --- /dev/null +++ b/extensions/react-widget/src/requests/searchAPI.ts @@ -0,0 +1,34 @@ +import { Result } from "@/types"; + + async function getSearchResults(question: string, apiKey:string, apiHost:string): Promise { + + const payload = { + question, + api_key:apiKey + }; + + try { + const response = await fetch(`${apiHost}/api/search`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(payload), + }); + + if (!response.ok) { + throw new Error(`Error: ${response.status}`); + } + + const data: Result[] = await response.json(); + return data; + + } catch (error) { + console.error("Failed to fetch documents:", error); + throw error; + } + } + + export { + getSearchResults + } \ No newline at end of file diff --git a/extensions/react-widget/src/types/index.ts b/extensions/react-widget/src/types/index.ts index 717efd92..445260dc 100644 --- a/extensions/react-widget/src/types/index.ts +++ b/extensions/react-widget/src/types/index.ts @@ -32,5 +32,22 @@ export interface WidgetProps { buttonText?:string; buttonBg?:string; collectFeedback?:boolean; - deafultOpen?: boolean; -} \ No newline at end of file + defaultOpen?: boolean; +} +export interface WidgetCoreProps extends WidgetProps { + widgetRef?:React.RefObject | null; + handleClose?:React.MouseEventHandler | undefined; + isOpen:boolean; + prefilledQuery?: string; +} + +export interface SearchBarProps { + apiHost?: string; + apiKey?: string; + theme?:THEME +} + +export interface Result { + text:string; + title:string +} From 811a20f080151fd781ac6710b005227d4397bc78 Mon Sep 17 00:00:00 2001 From: ManishMadan2882 Date: Fri, 15 Nov 2024 05:52:46 +0530 Subject: [PATCH 008/354] search: add themes, fix css override --- .../src/components/DocsGPTWidget.tsx | 6 +- .../react-widget/src/components/SearchBar.tsx | 150 +++++++++++------- extensions/react-widget/src/types/index.ts | 3 +- 3 files changed, 99 insertions(+), 60 deletions(-) diff --git a/extensions/react-widget/src/components/DocsGPTWidget.tsx b/extensions/react-widget/src/components/DocsGPTWidget.tsx index 355056ac..6fff4f46 100644 --- a/extensions/react-widget/src/components/DocsGPTWidget.tsx +++ b/extensions/react-widget/src/components/DocsGPTWidget.tsx @@ -106,8 +106,10 @@ const WidgetContainer = styled.div<{ modal?: boolean, isOpen?: boolean }>` `; const StyledContainer = styled.div<{ isOpen: boolean }>` all: initial; - max-height: ${(props) => props.theme.dimensions.maxHeight}; - max-width: ${(props) => props.theme.dimensions.maxWidth}; + max-height: ${(props) => props.theme.dimensions.maxHeight} !important; + max-width: ${(props) => props.theme.dimensions.maxWidth} !important; + width: ${(props) => props.theme.dimensions.width} !important; + height: ${(props) => props.theme.dimensions.height} !important; position: relative; flex-direction: column; justify-content: space-between; diff --git a/extensions/react-widget/src/components/SearchBar.tsx b/extensions/react-widget/src/components/SearchBar.tsx index f16271a3..a6a48173 100644 --- a/extensions/react-widget/src/components/SearchBar.tsx +++ b/extensions/react-widget/src/components/SearchBar.tsx @@ -1,26 +1,56 @@ import React from 'react' -import styled, { keyframes, createGlobalStyle } from 'styled-components'; +import styled, { keyframes, createGlobalStyle, ThemeProvider } from 'styled-components'; import { WidgetCore } from './DocsGPTWidget'; import { SearchBarProps } from '@/types'; import { getSearchResults } from '../requests/searchAPI' import { Result } from '@/types'; import MarkdownIt from 'markdown-it'; import DOMPurify from 'dompurify'; + +const themes = { + dark: { + bg: '#222327', + text: '#fff', + primary: { + text: "#FAFAFA", + bg: '#111111' + }, + secondary: { + text: "#A1A1AA", + bg: "#38383b" + } + }, + light: { + bg: '#fff', + text: '#000', + primary: { + text: "#222327", + bg: "#fff" + }, + secondary: { + text: "#A1A1AA", + bg: "#F6F6F6" + } + } + } + const Main = styled.div` + all:initial; + font-family: sans-serif; ` const TextField = styled.input` - padding: 8px; + padding: 6px 6px; border-radius: 8px; display: inline; - color: rgb(107 114 128); + color: ${props => props.theme.primary.text}; outline: none; border: 2px solid transparent; - background-color: rgba(0, 0, 0, .05);; + background-color: ${props => props.theme.secondary.bg}; &:focus { - outline: #467f95; /* remove default outline */ - border:2px solid skyblue; /* change border color on focus */ - box-shadow: 0px 0px 2px skyblue; /* add a red box shadow on focus */ + border:2px solid #007ee6; + box-shadow: 0px 0px 2px skyblue; + background-color: ${props => props.theme.primary.bg}; } ` @@ -30,29 +60,30 @@ const Container = styled.div` ` const SearchResults = styled.div` position: absolute; - background-color: white; + background-color: ${props => props.theme.primary.bg}; opacity: 90%; border: 1px solid rgba(0, 0, 0, .1); border-radius: 12px; - padding: 20px; + padding: 15px; width: 576px; z-index: 100; height: 25vh; overflow-y: auto; top: 45px; - scrollbar-color: lab(48.438 0 0 / 0.4) rgba(0, 0, 0, 0); - scrollbar-gutter: stable; - scrollbar-width: thin; + color: ${props => props.theme.primary.text}; + scrollbar-color: lab(48.438 0 0 / 0.4) rgba(0, 0, 0, 0); + scrollbar-gutter: stable; + scrollbar-width: thin; box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05), 0 2px 4px rgba(0, 0, 0, 0.1); backdrop-filter: blur(16px); ` const Title = styled.h3` - font-size: 12px; + font-size: 14px; color: rgb(107, 114, 128); - padding-bottom: 4px; + padding-bottom: 6px; font-weight: 600; text-transform: uppercase; - border-bottom: 1px solid rgba(0, 0, 0, 0.1); + border-bottom: 1px solid ${(props) => props.theme.secondary.text}; ` @@ -60,7 +91,7 @@ const Content = styled.div` font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; ` const Markdown = styled.div` - +line-height:20px; font-size: 12px; pre { padding: 8px; @@ -86,6 +117,7 @@ font-size: 12px; p { margin: 0px; line-height: 1.35rem; + font-size: 12px; } code:not(pre code) { @@ -102,20 +134,20 @@ font-size: 12px; overflow-wrap: break-word; word-break: break-all; } - - ul{ - padding:0px; - list-style-position: inside; + a{ + color: #007ee6; } ` export const SearchBar = ({ apiKey = "79bcbf0e-3dd1-4ac3-b893-e41b3d40ec8d", apiHost = "http://127.0.0.1:7091", - theme = "dark" + theme = "dark", + placeholder = "Search or Ask AI" }: SearchBarProps) => { const [input, setInput] = React.useState("") const [isWidgetOpen, setIsWidgetOpen] = React.useState(false); const inputRef = React.useRef(null) + const widgetRef = React.useRef(null) const [results, setResults] = React.useState([]) React.useEffect(() => { input.length > 0 ? @@ -125,6 +157,9 @@ export const SearchBar = ({ : setResults([]) }, [input]) + React.useEffect(()=>{ + console.log(isWidgetOpen, input); + },[isWidgetOpen]) const handleKeyDown = (event: React.KeyboardEvent) => { if (event.key === 'Enter') { setIsWidgetOpen(true); @@ -135,43 +170,44 @@ export const SearchBar = ({ } const md = new MarkdownIt(); return ( -
- - handleKeyDown(e)} - placeholder='Search here or Ask DocsGPT' - value={input} - onChange={(e) => setInput(e.target.value)} + +
+ + handleKeyDown(e)} + placeholder={placeholder} + value={input} + onChange={(e) => setInput(e.target.value)} + /> + { + input.length>0 && results.length > 0 && ( + + {results.map((res) => ( +
+ {res.title} + + + +
+ )) + } +
+ ) + } +
+ - { - results.length > 0 && ( - - {results.map((res) => ( -
- {res.title} - - - -
- )) - } -
- ) - } - - - -
+
+ ) } \ No newline at end of file diff --git a/extensions/react-widget/src/types/index.ts b/extensions/react-widget/src/types/index.ts index 445260dc..f8fc3753 100644 --- a/extensions/react-widget/src/types/index.ts +++ b/extensions/react-widget/src/types/index.ts @@ -44,7 +44,8 @@ export interface WidgetCoreProps extends WidgetProps { export interface SearchBarProps { apiHost?: string; apiKey?: string; - theme?:THEME + theme?:THEME; + placeholder?:string; } export interface Result { From 7bd0351ee9ff12a4082726b2fb9be03a17094f65 Mon Sep 17 00:00:00 2001 From: ManishMadan2882 Date: Fri, 15 Nov 2024 06:25:24 +0530 Subject: [PATCH 009/354] (fix): mount only when open --- extensions/react-widget/package-lock.json | 9 +- .../src/components/DocsGPTWidget.tsx | 212 +++++++++--------- .../react-widget/src/components/SearchBar.tsx | 52 +++-- 3 files changed, 139 insertions(+), 134 deletions(-) diff --git a/extensions/react-widget/package-lock.json b/extensions/react-widget/package-lock.json index de4c228d..6d736c6f 100644 --- a/extensions/react-widget/package-lock.json +++ b/extensions/react-widget/package-lock.json @@ -4860,9 +4860,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001625", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001625.tgz", - "integrity": "sha512-4KE9N2gcRH+HQhpeiRZXd+1niLB/XNLAhSy4z7fI8EzcbcPoAqjNInxVHTiTwWfTIV4w096XG8OtCOCQQKPv3w==", + "version": "1.0.30001680", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001680.tgz", + "integrity": "sha512-rPQy70G6AGUMnbwS1z6Xg+RkHYPAi18ihs47GH0jcxIG7wArmPgY3XbS2sRdBbxJljp3thdT8BIqv9ccCypiPA==", "funding": [ { "type": "opencollective", @@ -4876,7 +4876,8 @@ "type": "github", "url": "https://github.com/sponsors/ai" } - ] + ], + "license": "CC-BY-4.0" }, "node_modules/chalk": { "version": "2.4.2", diff --git a/extensions/react-widget/src/components/DocsGPTWidget.tsx b/extensions/react-widget/src/components/DocsGPTWidget.tsx index 6fff4f46..7a43cb7b 100644 --- a/extensions/react-widget/src/components/DocsGPTWidget.tsx +++ b/extensions/react-widget/src/components/DocsGPTWidget.tsx @@ -65,8 +65,11 @@ const WidgetContainer = styled.div<{ modal?: boolean, isOpen?: boolean }>` position: fixed; right: ${props => props.modal ? '50%' : '10px'}; bottom: ${props => props.modal ? '50%' : '10px'}; - z-index: 1000; + z-index: 1001; transform-origin:100% 100%; + &.modal{ + transform : translate(50%,50%); + } &.open { animation: createBox 250ms cubic-bezier(0.25, 0.1, 0.25, 1) forwards; display: block; @@ -75,9 +78,6 @@ const WidgetContainer = styled.div<{ modal?: boolean, isOpen?: boolean }>` animation: closeBox 250ms cubic-bezier(0.25, 0.1, 0.25, 1) forwards; display: none; } - ${props => props.modal && - "transform : translate(50%,50%);" - } align-items: center; text-align: left; @keyframes createBox { @@ -539,7 +539,9 @@ export const WidgetCore = ({ prefilledQuery = "", handleClose }: WidgetCoreProps) => { - const [prompt, setPrompt] = React.useState(prefilledQuery); + const [prompt, setPrompt] = React.useState(prefilledQuery); + console.log("propmpt",prompt); + const [status, setStatus] = React.useState('idle'); const [queries, setQueries] = React.useState([]); const [conversationId, setConversationId] = React.useState(null); @@ -666,106 +668,110 @@ export const WidgetCore = ({ {isOpen && size === 'large' && } - - { -
- - - -
- docs-gpt - - {title} - {description} - -
-
- - { - queries.length > 0 ? queries?.map((query, index) => { - return ( - - { - query.prompt && - - {query.prompt} - - - } - { - query.response ? { isBubbleHovered.current = true }} type='ANSWER'> - - - + { + isOpen && ( + + +
+ + + +
+ docs-gpt + + {title} + {description} + +
+
+ + { + queries.length > 0 ? queries?.map((query, index) => { + return ( + + { + query.prompt && + + {query.prompt} + + + } + { + query.response ? { isBubbleHovered.current = true }} type='ANSWER'> + + + - {collectFeedback && - - handleFeedback("LIKE", index)} /> - handleFeedback("DISLIKE", index)} /> - } - - :
- { - query.error ? + {collectFeedback && + + handleFeedback("LIKE", index)} /> + handleFeedback("DISLIKE", index)} /> + } + + :
+ { + query.error ? - -
-
Network Error
- {query.error} -
-
- : - - . - . - . - - - } -
- } - ) - }) - : - } - -
- - setPrompt(event.target.value)} - type='text' placeholder="Ask your question" /> - - - - - - Powered by  - DocsGPT - -
- } - + +
+
Network Error
+ {query.error} +
+
+ : + + . + . + . + + + } +
+ } +
) + }) + : + } +
+
+ + setPrompt(event.target.value)} + type='text' placeholder="Ask your question" /> + + + + + + Powered by  + DocsGPT + +
+
+
+ ) + } ) } \ No newline at end of file diff --git a/extensions/react-widget/src/components/SearchBar.tsx b/extensions/react-widget/src/components/SearchBar.tsx index a6a48173..2589cb2f 100644 --- a/extensions/react-widget/src/components/SearchBar.tsx +++ b/extensions/react-widget/src/components/SearchBar.tsx @@ -9,30 +9,30 @@ import DOMPurify from 'dompurify'; const themes = { dark: { - bg: '#222327', - text: '#fff', - primary: { - text: "#FAFAFA", - bg: '#111111' - }, - secondary: { - text: "#A1A1AA", - bg: "#38383b" - } + bg: '#222327', + text: '#fff', + primary: { + text: "#FAFAFA", + bg: '#111111' + }, + secondary: { + text: "#A1A1AA", + bg: "#38383b" + } }, light: { - bg: '#fff', - text: '#000', - primary: { - text: "#222327", - bg: "#fff" - }, - secondary: { - text: "#A1A1AA", - bg: "#F6F6F6" - } + bg: '#fff', + text: '#000', + primary: { + text: "#222327", + bg: "#fff" + }, + secondary: { + text: "#A1A1AA", + bg: "#F6F6F6" + } } - } +} const Main = styled.div` all:initial; @@ -157,9 +157,7 @@ export const SearchBar = ({ : setResults([]) }, [input]) - React.useEffect(()=>{ - console.log(isWidgetOpen, input); - },[isWidgetOpen]) + const handleKeyDown = (event: React.KeyboardEvent) => { if (event.key === 'Enter') { setIsWidgetOpen(true); @@ -181,7 +179,7 @@ export const SearchBar = ({ onChange={(e) => setInput(e.target.value)} /> { - input.length>0 && results.length > 0 && ( + input.length > 0 && results.length > 0 && ( {results.map((res) => (
@@ -198,14 +196,14 @@ export const SearchBar = ({ ) } - + />} From d33246612dcc926f1a64a956ef1810feef1cab56 Mon Sep 17 00:00:00 2001 From: ManishMadan2882 Date: Fri, 15 Nov 2024 18:16:45 +0530 Subject: [PATCH 010/354] (widget) unmount with timeout --- .../src/components/DocsGPTWidget.tsx | 246 +++++++++--------- .../react-widget/src/components/SearchBar.tsx | 18 +- frontend/.env.development | 2 +- 3 files changed, 137 insertions(+), 129 deletions(-) diff --git a/extensions/react-widget/src/components/DocsGPTWidget.tsx b/extensions/react-widget/src/components/DocsGPTWidget.tsx index 7a43cb7b..557aa9a0 100644 --- a/extensions/react-widget/src/components/DocsGPTWidget.tsx +++ b/extensions/react-widget/src/components/DocsGPTWidget.tsx @@ -67,16 +67,15 @@ const WidgetContainer = styled.div<{ modal?: boolean, isOpen?: boolean }>` bottom: ${props => props.modal ? '50%' : '10px'}; z-index: 1001; transform-origin:100% 100%; + display: block; &.modal{ transform : translate(50%,50%); } &.open { animation: createBox 250ms cubic-bezier(0.25, 0.1, 0.25, 1) forwards; - display: block; } &.close { animation: closeBox 250ms cubic-bezier(0.25, 0.1, 0.25, 1) forwards; - display: none; } align-items: center; text-align: left; @@ -494,24 +493,20 @@ export const DocsGPTWidget = (props: WidgetProps) => { const [open, setOpen] = React.useState(defaultOpen); const [isAnimatingButton, setIsAnimatingButton] = React.useState(false); const [isFloatingButtonVisible, setIsFloatingButtonVisible] = React.useState(true); - const widgetRef = useRef(null) + React.useEffect(() => { + if(isFloatingButtonVisible) + setTimeout(() => setIsAnimatingButton(false), 400); + }, [isFloatingButtonVisible]) const handleClose = () => { + setIsFloatingButtonVisible(true); + setIsAnimatingButton(true); setOpen(false); - setTimeout(() => { - if (widgetRef.current) - widgetRef.current.style.display = "none"; - setIsFloatingButtonVisible(true); - setIsAnimatingButton(true); - setTimeout(() => setIsAnimatingButton(false), 200); - }, 250) }; const handleOpen = () => { setOpen(true); setIsFloatingButtonVisible(false); - if (widgetRef.current) - widgetRef.current.style.display = 'block' } return ( <> @@ -519,7 +514,7 @@ export const DocsGPTWidget = (props: WidgetProps) => { {buttonText} - + ) } @@ -534,14 +529,13 @@ export const WidgetCore = ({ size = 'small', theme = 'dark', collectFeedback = true, - widgetRef = null, isOpen = false, prefilledQuery = "", handleClose }: WidgetCoreProps) => { - const [prompt, setPrompt] = React.useState(prefilledQuery); - console.log("propmpt",prompt); - + const [prompt, setPrompt] = React.useState(""); + console.log("propmpt", prompt); + const [mounted, setMounted] = React.useState(false); const [status, setStatus] = React.useState('idle'); const [queries, setQueries] = React.useState([]); const [conversationId, setConversationId] = React.useState(null); @@ -551,6 +545,22 @@ export const WidgetCore = ({ const endMessageRef = React.useRef(null); const md = new MarkdownIt(); + React.useEffect(() => { + if (isOpen) { + setMounted(true); // Mount the component + setPrompt(prefilledQuery) + } else { + // Wait for animations before unmounting + const timeout = setTimeout(() => { + setMounted(false) + console.log("Unmounted syccess") + }, 250); + return () => clearTimeout(timeout); + } + }, [isOpen]); + + + const handleUserInterrupt = () => { (status === 'loading') && setEventInterrupt(true); } @@ -663,114 +673,114 @@ export const WidgetCore = ({ typeof size === 'object' && 'custom' in size ? sizesConfig.getCustom(size.custom) : sizesConfig[size]; + if (!mounted) return null; return ( {isOpen && size === 'large' && } - { - isOpen && ( - - -
- - - -
- docs-gpt - - {title} - {description} - -
-
- - { - queries.length > 0 ? queries?.map((query, index) => { - return ( - - { - query.prompt && - - {query.prompt} - - - } - { - query.response ? { isBubbleHovered.current = true }} type='ANSWER'> - - - + {( + + +
+ + + +
+ docs-gpt + + {title} + {description} + +
+
+ + { + queries.length > 0 ? queries?.map((query, index) => { + return ( + + { + query.prompt && + + {query.prompt} + + + } + { + query.response ? { isBubbleHovered.current = true }} type='ANSWER'> + + + - {collectFeedback && - - handleFeedback("LIKE", index)} /> - handleFeedback("DISLIKE", index)} /> - } - - :
- { - query.error ? + {collectFeedback && + + handleFeedback("LIKE", index)} /> + handleFeedback("DISLIKE", index)} /> + } + + :
+ { + query.error ? - -
-
Network Error
- {query.error} -
-
- : - - . - . - . - - - } -
- } - ) - }) - : - } - -
- - setPrompt(event.target.value)} - type='text' placeholder="Ask your question" /> - - - - - - Powered by  - DocsGPT - -
- - - ) + +
+
Network Error
+ {query.error} +
+
+ : + + . + . + . + + + } +
+ } +
) + }) + : + } +
+
+ + setPrompt(event.target.value)} + type='text' placeholder="Ask your question" /> + + + + + + Powered by  + DocsGPT + +
+
+
+ ) }
) diff --git a/extensions/react-widget/src/components/SearchBar.tsx b/extensions/react-widget/src/components/SearchBar.tsx index 2589cb2f..5486871b 100644 --- a/extensions/react-widget/src/components/SearchBar.tsx +++ b/extensions/react-widget/src/components/SearchBar.tsx @@ -45,11 +45,12 @@ const TextField = styled.input` display: inline; color: ${props => props.theme.primary.text}; outline: none; - border: 2px solid transparent; + border: none; background-color: ${props => props.theme.secondary.bg}; + width: 240px; &:focus { - border:2px solid #007ee6; - box-shadow: 0px 0px 2px skyblue; + outline: none; + box-shadow: 0px 0px 0px 2px rgba(0, 109, 199); background-color: ${props => props.theme.primary.bg}; } ` @@ -84,9 +85,7 @@ const Title = styled.h3` font-weight: 600; text-transform: uppercase; border-bottom: 1px solid ${(props) => props.theme.secondary.text}; - ` - const Content = styled.div` font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; ` @@ -141,7 +140,7 @@ font-size: 12px; export const SearchBar = ({ apiKey = "79bcbf0e-3dd1-4ac3-b893-e41b3d40ec8d", apiHost = "http://127.0.0.1:7091", - theme = "dark", + theme = "light", placeholder = "Search or Ask AI" }: SearchBarProps) => { const [input, setInput] = React.useState("") @@ -157,7 +156,7 @@ export const SearchBar = ({ : setResults([]) }, [input]) - + const handleKeyDown = (event: React.KeyboardEvent) => { if (event.key === 'Enter') { setIsWidgetOpen(true); @@ -196,15 +195,14 @@ export const SearchBar = ({ ) } - {isWidgetOpen && } - + /> ) diff --git a/frontend/.env.development b/frontend/.env.development index 7a87f762..9569966a 100644 --- a/frontend/.env.development +++ b/frontend/.env.development @@ -1,3 +1,3 @@ # Please put appropriate value -VITE_API_HOST=http://0.0.0.0:7091 +VITE_API_HOST=http://127.0.0.1:7091 VITE_API_STREAMING=true \ No newline at end of file From 94617c5ef72f126802f82de4a04402dd412440c9 Mon Sep 17 00:00:00 2001 From: ManishMadan2882 Date: Sat, 16 Nov 2024 02:13:40 +0530 Subject: [PATCH 011/354] (fix:animations) minor fix --- .../src/components/DocsGPTWidget.tsx | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/extensions/react-widget/src/components/DocsGPTWidget.tsx b/extensions/react-widget/src/components/DocsGPTWidget.tsx index 557aa9a0..725e829a 100644 --- a/extensions/react-widget/src/components/DocsGPTWidget.tsx +++ b/extensions/react-widget/src/components/DocsGPTWidget.tsx @@ -105,10 +105,10 @@ const WidgetContainer = styled.div<{ modal?: boolean, isOpen?: boolean }>` `; const StyledContainer = styled.div<{ isOpen: boolean }>` all: initial; - max-height: ${(props) => props.theme.dimensions.maxHeight} !important; - max-width: ${(props) => props.theme.dimensions.maxWidth} !important; - width: ${(props) => props.theme.dimensions.width} !important; - height: ${(props) => props.theme.dimensions.height} !important; + max-height: ${(props) => props.theme.dimensions.maxHeight}; + max-width: ${(props) => props.theme.dimensions.maxWidth}; + width: ${(props) => props.theme.dimensions.width}; + height: ${(props) => props.theme.dimensions.height} ; position: relative; flex-direction: column; justify-content: space-between; @@ -134,15 +134,15 @@ const StyledContainer = styled.div<{ isOpen: boolean }>` height: 100px; } 100% { - width: ${(props) => props.theme.dimensions.width}; - height: ${(props) => props.theme.dimensions.height}; + width: ${(props) => props.theme.dimensions.width} !important; + height: ${(props) => props.theme.dimensions.height} !important; border-radius: 12px; } } @keyframes closeContainer { 0% { - width: ${(props) => props.theme.dimensions.width}; - height: ${(props) => props.theme.dimensions.height}; + width: ${(props) => props.theme.dimensions.width} !important; + height: ${(props) => props.theme.dimensions.height} !important; border-radius: 12px; } 100% { @@ -495,13 +495,15 @@ export const DocsGPTWidget = (props: WidgetProps) => { const [isFloatingButtonVisible, setIsFloatingButtonVisible] = React.useState(true); React.useEffect(() => { - if(isFloatingButtonVisible) - setTimeout(() => setIsAnimatingButton(false), 400); + if (isFloatingButtonVisible) + setTimeout(() => setIsAnimatingButton(true), 250); + return () => { + setIsAnimatingButton(false) + } }, [isFloatingButtonVisible]) const handleClose = () => { setIsFloatingButtonVisible(true); - setIsAnimatingButton(true); setOpen(false); }; const handleOpen = () => { @@ -534,7 +536,6 @@ export const WidgetCore = ({ handleClose }: WidgetCoreProps) => { const [prompt, setPrompt] = React.useState(""); - console.log("propmpt", prompt); const [mounted, setMounted] = React.useState(false); const [status, setStatus] = React.useState('idle'); const [queries, setQueries] = React.useState([]); @@ -553,7 +554,6 @@ export const WidgetCore = ({ // Wait for animations before unmounting const timeout = setTimeout(() => { setMounted(false) - console.log("Unmounted syccess") }, 250); return () => clearTimeout(timeout); } From 2ad6b4fa4ea3c257af0b9cd05804f7e7e9f613e0 Mon Sep 17 00:00:00 2001 From: fadingNA Date: Fri, 15 Nov 2024 23:16:58 -0500 Subject: [PATCH 012/354] update table styling --- .../src/components/DocumentPagination.tsx | 2 +- frontend/src/index.css | 12 +- frontend/src/settings/Documents.tsx | 224 ++++++++++-------- 3 files changed, 128 insertions(+), 110 deletions(-) diff --git a/frontend/src/components/DocumentPagination.tsx b/frontend/src/components/DocumentPagination.tsx index b0532362..4000f83d 100644 --- a/frontend/src/components/DocumentPagination.tsx +++ b/frontend/src/components/DocumentPagination.tsx @@ -42,7 +42,7 @@ const Pagination: React.FC = ({ }; return ( -
+
Rows per page: setEditInputBox(e.target.value)} + value={editInputBox} + className="ml-2 mr-2 rounded-[28px] py-[14px] px-[19px] border-[1.5px] border-black" + /> + )} +
+ Edit { + setIsEditClicked(true); + setEditInputBox(message); + }} + /> +
+ {isEditClicked && ( +
+ + +
+ )}
); } else { From 63b547ea138aa9748eb6dd862461ca9b44dee9f4 Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 17 Nov 2024 12:59:34 +0000 Subject: [PATCH 016/354] fix: delete old files --- Assets/DocsGPT tee-back.jpeg | Bin 90358 -> 0 bytes Assets/DocsGPT tee-front.jpeg | Bin 21921 -> 0 bytes application/parser/java2doc.py | 66 ------------------ application/parser/js2doc.py | 70 ------------------- application/parser/py2doc.py | 121 --------------------------------- 5 files changed, 257 deletions(-) delete mode 100644 Assets/DocsGPT tee-back.jpeg delete mode 100644 Assets/DocsGPT tee-front.jpeg delete mode 100644 application/parser/java2doc.py delete mode 100644 application/parser/js2doc.py delete mode 100644 application/parser/py2doc.py diff --git a/Assets/DocsGPT tee-back.jpeg b/Assets/DocsGPT tee-back.jpeg deleted file mode 100644 index 8c0e22aa2fcb706f8b64bce6145d721eceb05315..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 90358 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' - functions[func_name] = escodegen.generate(node) - elif node.type == 'VariableDeclaration': - for declaration in node.declarations: - if declaration.init and declaration.init.type == 'FunctionExpression': - func_name = declaration.id.name if declaration.id else '' - functions[func_name] = escodegen.generate(declaration.init) - elif node.type == 'ClassDeclaration': - for subnode in node.body.body: - if subnode.type == 'MethodDefinition': - func_name = subnode.key.name - functions[func_name] = escodegen.generate(subnode.value) - elif subnode.type == 'VariableDeclaration': - for declaration in subnode.declarations: - if declaration.init and declaration.init.type == 'FunctionExpression': - func_name = declaration.id.name if declaration.id else '' - functions[func_name] = escodegen.generate(declaration.init) - return functions - - -def extract_classes(file_path): - with open(file_path, 'r') as file: - source_code = file.read() - classes = {} - tree = esprima.parseScript(source_code) - for node in tree.body: - if node.type == 'ClassDeclaration': - class_name = node.id.name - function_names = [] - for subnode in node.body.body: - if subnode.type == 'MethodDefinition': - function_names.append(subnode.key.name) - classes[class_name] = ", ".join(function_names) - return classes - - -def extract_functions_and_classes(directory): - files = find_files(directory) - functions_dict = {} - classes_dict = {} - for file in files: - functions = extract_functions(file) - if functions: - functions_dict[file] = functions - classes = extract_classes(file) - if classes: - classes_dict[file] = classes - return functions_dict, classes_dict diff --git a/application/parser/py2doc.py b/application/parser/py2doc.py deleted file mode 100644 index 3a8175d4..00000000 --- a/application/parser/py2doc.py +++ /dev/null @@ -1,121 +0,0 @@ -import ast -import os -from pathlib import Path - -import tiktoken -from langchain.llms import OpenAI -from langchain.prompts import PromptTemplate - - -def find_files(directory): - files_list = [] - for root, dirs, files in os.walk(directory): - for file in files: - if file.endswith('.py'): - files_list.append(os.path.join(root, file)) - return files_list - - -def extract_functions(file_path): - with open(file_path, 'r') as file: - source_code = file.read() - functions = {} - tree = ast.parse(source_code) - for node in ast.walk(tree): - if isinstance(node, ast.FunctionDef): - func_name = node.name - func_def = ast.get_source_segment(source_code, node) - functions[func_name] = func_def - return functions - - -def extract_classes(file_path): - with open(file_path, 'r') as file: - source_code = file.read() - classes = {} - tree = ast.parse(source_code) - for node in ast.walk(tree): - if isinstance(node, ast.ClassDef): - class_name = node.name - function_names = [] - for subnode in ast.walk(node): - if isinstance(subnode, ast.FunctionDef): - function_names.append(subnode.name) - classes[class_name] = ", ".join(function_names) - return classes - - -def extract_functions_and_classes(directory): - files = find_files(directory) - functions_dict = {} - classes_dict = {} - for file in files: - functions = extract_functions(file) - if functions: - functions_dict[file] = functions - classes = extract_classes(file) - if classes: - classes_dict[file] = classes - return functions_dict, classes_dict - - -def parse_functions(functions_dict, formats, dir): - c1 = len(functions_dict) - for i, (source, functions) in enumerate(functions_dict.items(), start=1): - print(f"Processing file {i}/{c1}") - source_w = source.replace(dir + "/", "").replace("." + formats, ".md") - subfolders = "/".join(source_w.split("/")[:-1]) - Path(f"outputs/{subfolders}").mkdir(parents=True, exist_ok=True) - for j, (name, function) in enumerate(functions.items(), start=1): - print(f"Processing function {j}/{len(functions)}") - prompt = PromptTemplate( - input_variables=["code"], - template="Code: \n{code}, \nDocumentation: ", - ) - llm = OpenAI(temperature=0) - response = llm(prompt.format(code=function)) - mode = "a" if Path(f"outputs/{source_w}").exists() else "w" - with open(f"outputs/{source_w}", mode) as f: - f.write( - f"\n\n# Function name: {name} \n\nFunction: \n```\n{function}\n```, \nDocumentation: \n{response}") - - -def parse_classes(classes_dict, formats, dir): - c1 = len(classes_dict) - for i, (source, classes) in enumerate(classes_dict.items()): - print(f"Processing file {i + 1}/{c1}") - source_w = source.replace(dir + "/", "").replace("." + formats, ".md") - subfolders = "/".join(source_w.split("/")[:-1]) - Path(f"outputs/{subfolders}").mkdir(parents=True, exist_ok=True) - for name, function_names in classes.items(): - print(f"Processing Class {i + 1}/{c1}") - prompt = PromptTemplate( - input_variables=["class_name", "functions_names"], - template="Class name: {class_name} \nFunctions: {functions_names}, \nDocumentation: ", - ) - llm = OpenAI(temperature=0) - response = llm(prompt.format(class_name=name, functions_names=function_names)) - - with open(f"outputs/{source_w}", "a" if Path(f"outputs/{source_w}").exists() else "w") as f: - f.write(f"\n\n# Class name: {name} \n\nFunctions: \n{function_names}, \nDocumentation: \n{response}") - - -def transform_to_docs(functions_dict, classes_dict, formats, dir): - docs_content = ''.join([str(key) + str(value) for key, value in functions_dict.items()]) - docs_content += ''.join([str(key) + str(value) for key, value in classes_dict.items()]) - - num_tokens = len(tiktoken.get_encoding("cl100k_base").encode(docs_content)) - total_price = ((num_tokens / 1000) * 0.02) - - print(f"Number of Tokens = {num_tokens:,d}") - print(f"Approx Cost = ${total_price:,.2f}") - - user_input = input("Price Okay? (Y/N)\n").lower() - if user_input == "y" or user_input == "": - if not Path("outputs").exists(): - Path("outputs").mkdir() - parse_functions(functions_dict, formats, dir) - parse_classes(classes_dict, formats, dir) - print("All done!") - else: - print("The API was not called. No money was spent.") From 7a0137682858fb631eae9ca75562faccadb56cba Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 17 Nov 2024 13:02:45 +0000 Subject: [PATCH 017/354] fix: remove more old files --- Readme Logo.png | Bin 23575 -> 0 bytes package-lock.json | 1716 --------------------------------------------- package.json | 7 - 3 files changed, 1723 deletions(-) delete mode 100644 Readme Logo.png delete mode 100644 package-lock.json delete mode 100644 package.json diff --git a/Readme Logo.png b/Readme Logo.png deleted file mode 100644 index aad92a7524ff2e31de6ab21a1d10a58d42652219..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 23575 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.10.0" - } - }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", - "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", - "dev": true, - "dependencies": { - "eslint-visitor-keys": "^3.3.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" - } - }, - "node_modules/@eslint-community/regexpp": { - "version": "4.10.0", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", - "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==", - "dev": true, - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "node_modules/@eslint/eslintrc": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.3.tgz", - "integrity": "sha512-yZzuIG+jnVu6hNSzFEN07e8BxF3uAzYtQb6uDkaYZLo6oYZDCq454c5kB8zxnzfCYyP4MIuyBn10L0DqwujTmA==", - "dev": true, - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^9.6.0", - "globals": "^13.19.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint/js": { - "version": "8.53.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.53.0.tgz", - "integrity": "sha512-Kn7K8dx/5U6+cT1yEhpX1w4PCSg0M+XyRILPgvwcEBjerFWCwQj5sbr3/VmxqV0JGHCBCzyd6LxypEuehypY1w==", - "dev": true, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - } - }, - "node_modules/@humanwhocodes/config-array": { - "version": "0.11.13", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz", - "integrity": "sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==", - "dev": true, - "dependencies": { - "@humanwhocodes/object-schema": "^2.0.1", - "debug": "^4.1.1", - "minimatch": "^3.0.5" - }, - "engines": { - "node": ">=10.10.0" - } - }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "dev": true, - "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/object-schema": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz", - "integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==", - "dev": true - }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@ungap/structured-clone": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", - "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", - "dev": true - }, - "node_modules/acorn": { - "version": "8.11.2", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.2.tgz", - "integrity": "sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==", - "dev": true, - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ansi-escapes": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-5.0.0.tgz", - "integrity": "sha512-5GFMVX8HqE/TB+FuBJGuO5XG0WrsA6ptUqoODaT/n9mmUaZFkqnBueB4leqGBCmrUHnCnC4PCZTCd0E7QQ83bA==", - "dev": true, - "dependencies": { - "type-fest": "^1.0.2" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true - }, - "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/chalk": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", - "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", - "dev": true, - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/cli-cursor": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-4.0.0.tgz", - "integrity": "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==", - "dev": true, - "dependencies": { - "restore-cursor": "^4.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cli-truncate": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-3.1.0.tgz", - "integrity": "sha512-wfOBkjXteqSnI59oPcJkcPl/ZmwvMMOj340qUIY1SKZCv0B9Cf4D4fAucRkIKQmsIuYK3x1rrgU7MeGRruiuiA==", - "dev": true, - "dependencies": { - "slice-ansi": "^5.0.0", - "string-width": "^5.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/colorette": { - "version": "2.0.20", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", - "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", - "dev": true - }, - "node_modules/commander": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", - "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", - "dev": true, - "engines": { - "node": ">=16" - } - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true - }, - "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "dev": true, - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true - }, - "node_modules/doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "dev": true, - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true - }, - "node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true - }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint": { - "version": "8.53.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.53.0.tgz", - "integrity": "sha512-N4VuiPjXDUa4xVeV/GC/RV3hQW9Nw+Y463lkWaKKXKYMvmRiRDAtfpuPFLN+E1/6ZhyR8J2ig+eVREnYgUsiag==", - "dev": true, - "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.3", - "@eslint/js": "8.53.0", - "@humanwhocodes/config-array": "^0.11.13", - "@humanwhocodes/module-importer": "^1.0.1", - "@nodelib/fs.walk": "^1.2.8", - "@ungap/structured-clone": "^1.2.0", - "ajv": "^6.12.4", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", - "debug": "^4.3.2", - "doctrine": "^3.0.0", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.2", - "eslint-visitor-keys": "^3.4.3", - "espree": "^9.6.1", - "esquery": "^1.4.2", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "graphemer": "^1.4.0", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", - "js-yaml": "^4.1.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3", - "strip-ansi": "^6.0.1", - "text-table": "^0.2.0" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-scope": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", - "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", - "dev": true, - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/eslint/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/eslint/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/eslint/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/espree": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", - "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", - "dev": true, - "dependencies": { - "acorn": "^8.9.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/esquery": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", - "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", - "dev": true, - "dependencies": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/eventemitter3": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", - "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", - "dev": true - }, - "node_modules/execa": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", - "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", - "dev": true, - "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^8.0.1", - "human-signals": "^5.0.0", - "is-stream": "^3.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^5.1.0", - "onetime": "^6.0.0", - "signal-exit": "^4.1.0", - "strip-final-newline": "^3.0.0" - }, - "engines": { - "node": ">=16.17" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true - }, - "node_modules/fastq": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", - "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", - "dev": true, - "dependencies": { - "reusify": "^1.0.4" - } - }, - "node_modules/file-entry-cache": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", - "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", - "dev": true, - "dependencies": { - "flat-cache": "^3.0.4" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/flat-cache": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", - "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", - "dev": true, - "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.3", - "rimraf": "^3.0.2" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, - "node_modules/flatted": { - "version": "3.2.9", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz", - "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==", - "dev": true - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true - }, - "node_modules/get-stream": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", - "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", - "dev": true, - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dev": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/globals": { - "version": "13.23.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.23.0.tgz", - "integrity": "sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA==", - "dev": true, - "dependencies": { - "type-fest": "^0.20.2" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/globals/node_modules/type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/graphemer": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/human-signals": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", - "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", - "dev": true, - "engines": { - "node": ">=16.17.0" - } - }, - "node_modules/ignore": { - "version": "5.2.4", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", - "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", - "dev": true, - "engines": { - "node": ">= 4" - } - }, - "node_modules/import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", - "dev": true, - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "dev": true, - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-fullwidth-code-point": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", - "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-path-inside": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", - "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", - "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", - "dev": true, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true - }, - "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true - }, - "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dev": true, - "dependencies": { - "json-buffer": "3.0.1" - } - }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/lilconfig": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", - "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", - "dev": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/lint-staged": { - "version": "15.1.0", - "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-15.1.0.tgz", - "integrity": "sha512-ZPKXWHVlL7uwVpy8OZ7YQjYDAuO5X4kMh0XgZvPNxLcCCngd0PO5jKQyy3+s4TL2EnHoIXIzP1422f/l3nZKMw==", - "dev": true, - "dependencies": { - "chalk": "5.3.0", - "commander": "11.1.0", - "debug": "4.3.4", - "execa": "8.0.1", - "lilconfig": "2.1.0", - "listr2": "7.0.2", - "micromatch": "4.0.5", - "pidtree": "0.6.0", - "string-argv": "0.3.2", - "yaml": "2.3.4" - }, - "bin": { - "lint-staged": "bin/lint-staged.js" - }, - "engines": { - "node": ">=18.12.0" - }, - "funding": { - "url": "https://opencollective.com/lint-staged" - } - }, - "node_modules/listr2": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/listr2/-/listr2-7.0.2.tgz", - "integrity": "sha512-rJysbR9GKIalhTbVL2tYbF2hVyDnrf7pFUZBwjPaMIdadYHmeT+EVi/Bu3qd7ETQPahTotg2WRCatXwRBW554g==", - "dev": true, - "dependencies": { - "cli-truncate": "^3.1.0", - "colorette": "^2.0.20", - "eventemitter3": "^5.0.1", - "log-update": "^5.0.1", - "rfdc": "^1.3.0", - "wrap-ansi": "^8.1.0" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true - }, - "node_modules/log-update": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/log-update/-/log-update-5.0.1.tgz", - "integrity": "sha512-5UtUDQ/6edw4ofyljDNcOVJQ4c7OjDro4h3y8e1GQL5iYElYclVHJ3zeWchylvMaKnDbDilC8irOVyexnA/Slw==", - "dev": true, - "dependencies": { - "ansi-escapes": "^5.0.0", - "cli-cursor": "^4.0.0", - "slice-ansi": "^5.0.0", - "strip-ansi": "^7.0.1", - "wrap-ansi": "^8.0.1" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true - }, - "node_modules/micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", - "dev": true, - "dependencies": { - "braces": "^3.0.2", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/mimic-fn": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", - "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true - }, - "node_modules/npm-run-path": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.1.0.tgz", - "integrity": "sha512-sJOdmRGrY2sjNTRMbSvluQqg+8X7ZK61yvzBEIDhz4f8z1TZFYABsqjjCBd/0PUNE9M6QDgHJXQkGUEm7Q+l9Q==", - "dev": true, - "dependencies": { - "path-key": "^4.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/npm-run-path/node_modules/path-key": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", - "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/onetime": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", - "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", - "dev": true, - "dependencies": { - "mimic-fn": "^4.0.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/optionator": { - "version": "0.9.3", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", - "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", - "dev": true, - "dependencies": { - "@aashutoshrathi/word-wrap": "^1.2.3", - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/pidtree": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.6.0.tgz", - "integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==", - "dev": true, - "bin": { - "pidtree": "bin/pidtree.js" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/prettier": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.1.0.tgz", - "integrity": "sha512-TQLvXjq5IAibjh8EpBIkNKxO749UEWABoiIZehEPiY4GNpVdhaFKqSTu+QrlU6D2dPAfubRmtJTi4K4YkQ5eXw==", - "dev": true, - "bin": { - "prettier": "bin/prettier.cjs" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" - } - }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/restore-cursor": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-4.0.0.tgz", - "integrity": "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==", - "dev": true, - "dependencies": { - "onetime": "^5.1.0", - "signal-exit": "^3.0.2" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/restore-cursor/node_modules/mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/restore-cursor/node_modules/onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "dev": true, - "dependencies": { - "mimic-fn": "^2.1.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/restore-cursor/node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true - }, - "node_modules/reusify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "dev": true, - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "node_modules/rfdc": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.0.tgz", - "integrity": "sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==", - "dev": true - }, - "node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/slice-ansi": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", - "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^6.0.0", - "is-fullwidth-code-point": "^4.0.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/slice-ansi?sponsor=1" - } - }, - "node_modules/string-argv": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", - "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", - "dev": true, - "engines": { - "node": ">=0.6.19" - } - }, - "node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/strip-final-newline": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", - "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", - "dev": true - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/type-fest": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz", - "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true - }, - "node_modules/yaml": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.4.tgz", - "integrity": "sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==", - "dev": true, - "engines": { - "node": ">= 14" - } - }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - } - } -} diff --git a/package.json b/package.json deleted file mode 100644 index 78820bd8..00000000 --- a/package.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "devDependencies": { - "eslint": "^8.53.0", - "lint-staged": "^15.1.0", - "prettier": "^3.1.0" - } -} From 250edf26a55ea3c37d75608329e59fc5e11e9c0e Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 17 Nov 2024 15:01:39 +0000 Subject: [PATCH 018/354] Update README.md --- README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index 99baf811..83a5bab7 100644 --- a/README.md +++ b/README.md @@ -27,9 +27,7 @@ Say goodbye to time-consuming manual searches, and let - Let's chat - +[Book a Meeting :wave:](https://cal.com/arc53/docsgpt-demo-b2b)⁠ [Send Email :email:](mailto:contact@arc53.com?subject=DocsGPT%20support%2Fsolutions) From 5ee0f15d946ea04de0433d0a9322f3c10343e96b Mon Sep 17 00:00:00 2001 From: ManishMadan2882 Date: Mon, 18 Nov 2024 03:18:39 +0530 Subject: [PATCH 019/354] (feat: search): close on click outside --- .../src/components/DocsGPTWidget.tsx | 9 +-- .../react-widget/src/components/SearchBar.tsx | 58 +++++++++++++++---- extensions/react-widget/src/types/index.ts | 1 + frontend/.env.development | 2 +- 4 files changed, 55 insertions(+), 15 deletions(-) diff --git a/extensions/react-widget/src/components/DocsGPTWidget.tsx b/extensions/react-widget/src/components/DocsGPTWidget.tsx index d471aa57..b9ee8acc 100644 --- a/extensions/react-widget/src/components/DocsGPTWidget.tsx +++ b/extensions/react-widget/src/components/DocsGPTWidget.tsx @@ -78,14 +78,14 @@ const openContainer = keyframes` height: 100px; } 100% { - width: ${(props) => props.theme.dimensions.width} !important; - height: ${(props) => props.theme.dimensions.height} !important; + width: ${(props) => props.theme.dimensions.width}; + height: ${(props) => props.theme.dimensions.height}; border-radius: 12px; }` const closeContainer = keyframes` 0% { - width: ${(props) => props.theme.dimensions.width} !important; - height: ${(props) => props.theme.dimensions.height} !important; + width: ${(props) => props.theme.dimensions.width}; + height: ${(props) => props.theme.dimensions.height}; border-radius: 12px; } 100% { @@ -671,6 +671,7 @@ export const WidgetCore = ({ const handleImageError = (event: React.SyntheticEvent) => { event.currentTarget.src = "https://d3dg1063dc54p9.cloudfront.net/cute-docsgpt.png"; }; + const dimensions = typeof size === 'object' && 'custom' in size ? sizesConfig.getCustom(size.custom) diff --git a/extensions/react-widget/src/components/SearchBar.tsx b/extensions/react-widget/src/components/SearchBar.tsx index 5486871b..a417b955 100644 --- a/extensions/react-widget/src/components/SearchBar.tsx +++ b/extensions/react-widget/src/components/SearchBar.tsx @@ -39,15 +39,16 @@ const Main = styled.div` font-family: sans-serif; ` -const TextField = styled.input` +const TextField = styled.input<{inputWidth:string}>` padding: 6px 6px; + width: ${({inputWidth}) => inputWidth}; border-radius: 8px; display: inline; color: ${props => props.theme.primary.text}; outline: none; border: none; background-color: ${props => props.theme.secondary.bg}; - width: 240px; + &:focus { outline: none; box-shadow: 0px 0px 0px 2px rgba(0, 109, 199); @@ -61,6 +62,7 @@ const Container = styled.div` ` const SearchResults = styled.div` position: absolute; + display: block; background-color: ${props => props.theme.primary.bg}; opacity: 90%; border: 1px solid rgba(0, 0, 0, .1); @@ -77,6 +79,11 @@ const SearchResults = styled.div` scrollbar-width: thin; box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05), 0 2px 4px rgba(0, 0, 0, 0.1); backdrop-filter: blur(16px); + @media only screen and (max-width: 768px) { + max-height: 100vh; + max-width: 80vw; + overflow: auto; + } ` const Title = styled.h3` font-size: 14px; @@ -137,17 +144,45 @@ font-size: 12px; color: #007ee6; } ` +const Toolkit = styled.kbd` + position: absolute; + right: 12px; + top: 4px; + background-color: ${(props) => props.theme.primary.bg}; + color: ${(props) => props.theme.secondary.text}; + font-size: 9px; + padding: 2px; + border: 1px solid ${(props) => props.theme.secondary.text}; + border-radius: 4px; +` export const SearchBar = ({ apiKey = "79bcbf0e-3dd1-4ac3-b893-e41b3d40ec8d", apiHost = "http://127.0.0.1:7091", - theme = "light", - placeholder = "Search or Ask AI" + theme = "dark", + placeholder = "Search or Ask AI...", + width="240px" }: SearchBarProps) => { - const [input, setInput] = React.useState("") + const [input, setInput] = React.useState(""); const [isWidgetOpen, setIsWidgetOpen] = React.useState(false); - const inputRef = React.useRef(null) - const widgetRef = React.useRef(null) - const [results, setResults] = React.useState([]) + const inputRef = React.useRef(null); + const resultsRef = React.useRef(null); + const [isResultVisible, setIsResultVisible] = React.useState(true); + const [results, setResults] = React.useState([]); + React.useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if ( + resultsRef.current && + !resultsRef.current.contains(event.target as Node) + ) { + setIsResultVisible(false); + } + }; + document.addEventListener('mousedown', handleClickOutside); + return () => { + resultsRef.current && (resultsRef.current.style.display = 'block') + document.removeEventListener('mousedown', handleClickOutside); + }; + }, []) React.useEffect(() => { input.length > 0 ? getSearchResults(input, apiKey, apiHost) @@ -171,6 +206,8 @@ export const SearchBar = ({

setIsResultVisible(true)} ref={inputRef} onKeyDown={(e) => handleKeyDown(e)} placeholder={placeholder} @@ -178,8 +215,8 @@ export const SearchBar = ({ onChange={(e) => setInput(e.target.value)} /> { - input.length > 0 && results.length > 0 && ( - + input.length > 0 && results.length > 0 && isResultVisible && ( + {results.map((res) => (
{res.title} @@ -194,6 +231,7 @@ export const SearchBar = ({ ) } + Enter Date: Mon, 18 Nov 2024 00:05:48 -0500 Subject: [PATCH 020/354] backend : update sources/paginated to search document by query 'name' --- application/api/user/routes.py | 13 ++++++++++++- application/celery_init.py | 10 +++++++++- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/application/api/user/routes.py b/application/api/user/routes.py index 6a2f3bea..5ebd54c8 100644 --- a/application/api/user/routes.py +++ b/application/api/user/routes.py @@ -473,11 +473,22 @@ class PaginatedSources(Resource): sort_order = request.args.get("order", "desc") # Default to 'desc' page = int(request.args.get("page", 1)) # Default to 1 rows_per_page = int(request.args.get("rows", 10)) # Default to 10 + # add .strip() to remove leading and trailing whitespaces + search_term = request.args.get( + "search", "" + ).strip() # add search for filter documents - # Prepare + # Prepare query for filtering query = {"user": user} + if search_term: + query["name"] = { + "$regex": search_term, + "$options": "i", # using case-insensitive search + } + total_documents = sources_collection.count_documents(query) total_pages = max(1, math.ceil(total_documents / rows_per_page)) + page = min(max(1, page), total_pages) # add this to make sure page inbound is within the range sort_order = 1 if sort_order == "asc" else -1 skip = (page - 1) * rows_per_page diff --git a/application/celery_init.py b/application/celery_init.py index c5838083..185cc87f 100644 --- a/application/celery_init.py +++ b/application/celery_init.py @@ -2,14 +2,22 @@ from celery import Celery from application.core.settings import settings from celery.signals import setup_logging + def make_celery(app_name=__name__): - celery = Celery(app_name, broker=settings.CELERY_BROKER_URL, backend=settings.CELERY_RESULT_BACKEND) + celery = Celery( + app_name, + broker=settings.CELERY_BROKER_URL, + backend=settings.CELERY_RESULT_BACKEND, + ) celery.conf.update(settings) return celery + @setup_logging.connect def config_loggers(*args, **kwargs): from application.core.logging_config import setup_logging + setup_logging() + celery = make_celery() From 0493352292b3d89317709c8bb79e77cdd978ac99 Mon Sep 17 00:00:00 2001 From: fadingNA Date: Mon, 18 Nov 2024 00:06:18 -0500 Subject: [PATCH 021/354] frontend: remove search on localstate, change to backend search use mongo passing searchTerm --- frontend/src/preferences/preferenceApi.ts | 3 ++- frontend/src/settings/Documents.tsx | 29 +++++++++++++---------- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/frontend/src/preferences/preferenceApi.ts b/frontend/src/preferences/preferenceApi.ts index 32cf8b17..8d21bdcd 100644 --- a/frontend/src/preferences/preferenceApi.ts +++ b/frontend/src/preferences/preferenceApi.ts @@ -25,9 +25,10 @@ export async function getDocsWithPagination( order = 'desc', pageNumber = 1, rowsPerPage = 10, + searchTerm = '', ): Promise { try { - const query = `sort=${sort}&order=${order}&page=${pageNumber}&rows=${rowsPerPage}`; + const query = `sort=${sort}&order=${order}&page=${pageNumber}&rows=${rowsPerPage}&search=${searchTerm}`; const response = await userService.getDocsWithPagination(query); const data = await response.json(); const docs: Doc[] = []; diff --git a/frontend/src/settings/Documents.tsx b/frontend/src/settings/Documents.tsx index 1e5a610e..bb45b9ff 100644 --- a/frontend/src/settings/Documents.tsx +++ b/frontend/src/settings/Documents.tsx @@ -40,25 +40,18 @@ const Documents: React.FC = ({ const { t } = useTranslation(); const dispatch = useDispatch(); // State for search input - const [searchTerm, setSearchTerm] = useState(''); + const [searchTerm, setSearchTerm] = useState(''); // State for modal: active/inactive const [modalState, setModalState] = useState('INACTIVE'); // Initialize with inactive state - const [isOnboarding, setIsOnboarding] = useState(false); // State for onboarding flag - const [loading, setLoading] = useState(false); + const [isOnboarding, setIsOnboarding] = useState(false); // State for onboarding flag + const [loading, setLoading] = useState(false); const [sortField, setSortField] = useState<'date' | 'tokens'>('date'); const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('desc'); // Pagination const [currentPage, setCurrentPage] = useState(1); const [rowsPerPage, setRowsPerPage] = useState(10); const [totalPages, setTotalPages] = useState(1); - // const [totalDocuments, setTotalDocuments] = useState(0); - // Filter documents based on the search term - const filteredDocuments = paginatedDocuments?.filter((document) => - document.name.toLowerCase().includes(searchTerm.toLowerCase()), - ); - // State for documents - const currentDocuments = filteredDocuments ?? []; - console.log('currentDocuments', currentDocuments); + const currentDocuments = paginatedDocuments ?? []; const syncOptions = [ { label: 'Never', value: 'never' }, { label: 'Daily', value: 'daily' }, @@ -84,7 +77,7 @@ const Documents: React.FC = ({ setSortOrder('desc'); } } - getDocsWithPagination(sortField, sortOrder, page, rowsPerPg) + getDocsWithPagination(sortField, sortOrder, page, rowsPerPg, searchTerm) .then((data) => { dispatch(setPaginatedDocuments(data ? data.docs : [])); setTotalPages(data ? data.totalPages : 0); @@ -130,6 +123,10 @@ const Documents: React.FC = ({ } }, [modalState, sortField, currentPage, rowsPerPage]); + useEffect(() => { + refreshDocs(sortField, 1, rowsPerPage); + }, [searchTerm]); + return (
@@ -143,7 +140,13 @@ const Documents: React.FC = ({ type="text" id="document-search-input" value={searchTerm} - onChange={(e) => setSearchTerm(e.target.value)} // Handle search input change + onChange={(e) => { + setSearchTerm(e.target.value); + setCurrentPage(1); + // refreshDocs(sortField, 1, rowsPerPage); + // do not call refreshDocs here the state is async + // so it will not have the updated value + }} // Handle search input change />
+ {/*}
{t('settings.documents.type')}
+ */} = ({ ? formatTokens(+document.tokens) : ''} + {/*} {document.type === 'remote' ? 'Pre-loaded' : 'Private'} + */}
{document.type !== 'remote' && ( From dae0942d03ea24da04df1f37a5a4ca3b45861c85 Mon Sep 17 00:00:00 2001 From: ManishMadan2882 Date: Sat, 23 Nov 2024 02:29:27 +0530 Subject: [PATCH 042/354] (refactor): separate browser ready builds --- extensions/react-widget/package-lock.json | 4 ++-- extensions/react-widget/package.json | 20 ++++------------- extensions/react-widget/src/browser.tsx | 22 +++++++++++++++++++ .../react-widget/src/components/SearchBar.tsx | 6 ++--- extensions/react-widget/src/index.ts | 1 + extensions/react-widget/src/main.tsx | 22 ++----------------- 6 files changed, 34 insertions(+), 41 deletions(-) create mode 100644 extensions/react-widget/src/browser.tsx diff --git a/extensions/react-widget/package-lock.json b/extensions/react-widget/package-lock.json index 6d736c6f..6e43997a 100644 --- a/extensions/react-widget/package-lock.json +++ b/extensions/react-widget/package-lock.json @@ -1,12 +1,12 @@ { "name": "docsgpt", - "version": "0.4.7", + "version": "0.4.8", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "docsgpt", - "version": "0.4.7", + "version": "0.4.8", "license": "Apache-2.0", "dependencies": { "@babel/plugin-transform-flow-strip-types": "^7.23.3", diff --git a/extensions/react-widget/package.json b/extensions/react-widget/package.json index 12a66939..35676fc1 100644 --- a/extensions/react-widget/package.json +++ b/extensions/react-widget/package.json @@ -1,6 +1,6 @@ { - "name": "docsgpt", - "version": "0.4.7", + "name": "docsgpt-react", + "version": "0.4.8", "private": false, "description": "DocsGPT 🦖 is an innovative open-source tool designed to simplify the retrieval of information from project documentation using advanced GPT models 🤖.", "source": "./src/index.html", @@ -11,18 +11,6 @@ "dist", "package.json" ], - "targets": { - "modern": { - "engines": { - "browsers": "Chrome 80" - } - }, - "legacy": { - "engines": { - "browsers": "> 0.5%, last 2 versions, not dead" - } - } - }, "@parcel/resolver-default": { "packageExports": true }, @@ -30,9 +18,9 @@ "styled-components": "^5" }, "scripts": { - "build": "parcel build src/main.tsx --public-url ./", + "build": "parcel build src/browser.tsx --public-url ./", "build:react": "parcel build src/index.ts", - "serve": "parcel serve -p 3000", + "serve": "parcel serve -p 3000", "dev": "parcel -p 3000", "test": "jest", "lint": "eslint", diff --git a/extensions/react-widget/src/browser.tsx b/extensions/react-widget/src/browser.tsx new file mode 100644 index 00000000..8bee7748 --- /dev/null +++ b/extensions/react-widget/src/browser.tsx @@ -0,0 +1,22 @@ +//exports browser ready methods + +import { createRoot } from "react-dom/client"; + +import { DocsGPTWidget } from './components/DocsGPTWidget'; +import { SearchBar } from './components/SearchBar'; +import React from "react"; +if (typeof window !== 'undefined') { + const renderWidget = (elementId: string, props = {}) => { + const root = createRoot(document.getElementById(elementId) as HTMLElement); + root.render(); + }; + const renderSearchBar = (elementId: string, props = {}) => { + const root = createRoot(document.getElementById(elementId) as HTMLElement); + root.render(); + }; + (window as any).renderDocsGPTWidget = renderWidget; + + (window as any).renderSearchBar = renderSearchBar; +} + +export { DocsGPTWidget, SearchBar }; diff --git a/extensions/react-widget/src/components/SearchBar.tsx b/extensions/react-widget/src/components/SearchBar.tsx index aff036d0..e88bdafd 100644 --- a/extensions/react-widget/src/components/SearchBar.tsx +++ b/extensions/react-widget/src/components/SearchBar.tsx @@ -69,7 +69,6 @@ const SearchResults = styled.div` position: absolute; display: block; background-color: ${props => props.theme.primary.bg}; - opacity: 90%; border: 1px solid rgba(0, 0, 0, .1); border-radius: 12px; padding: 8px; @@ -287,7 +286,7 @@ export const SearchBar = ({ debounceTimeout.current = setTimeout(() => { getSearchResults(input, apiKey, apiHost, abortController.signal) .then((data) => setResults(data)) - .catch((err) => console.log(err)) + .catch((err) => !abortController.signal.aborted && console.log(err)) .finally(() => setLoading(false)); }, 500); @@ -340,10 +339,11 @@ export const SearchBar = ({ {!loading ? (results.length > 0 ? - results.map((res) => { + results.map((res, key) => { const containsSource = res.source !== 'local'; return ( { if (!containsSource) return; window.open(res.source, '_blank', 'noopener, noreferrer') diff --git a/extensions/react-widget/src/index.ts b/extensions/react-widget/src/index.ts index e29b85a5..5f2e30e8 100644 --- a/extensions/react-widget/src/index.ts +++ b/extensions/react-widget/src/index.ts @@ -1,2 +1,3 @@ +//exports methods for React export {SearchBar} from "./components/SearchBar" export { DocsGPTWidget } from "./components/DocsGPTWidget"; diff --git a/extensions/react-widget/src/main.tsx b/extensions/react-widget/src/main.tsx index a8542e26..a1a47065 100644 --- a/extensions/react-widget/src/main.tsx +++ b/extensions/react-widget/src/main.tsx @@ -1,25 +1,7 @@ - +//development import { createRoot } from "react-dom/client"; import { App } from "./App"; -import { DocsGPTWidget } from './components/DocsGPTWidget'; -import { SearchBar } from './components/SearchBar'; import React from "react"; -if (typeof window !== 'undefined') { - const renderWidget = (elementId: string, props = {}) => { - const root = createRoot(document.getElementById(elementId) as HTMLElement); - root.render(); - }; - const renderSearchBar = (elementId: string, props = {}) => { - const root = createRoot(document.getElementById(elementId) as HTMLElement); - root.render(); - }; - (window as any).renderDocsGPTWidget = renderWidget; - - (window as any).renderSearchBar = renderSearchBar; -} const container = document.getElementById("app") as HTMLElement; const root = createRoot(container) -root.render(); - -export { DocsGPTWidget }; -export { SearchBar } +root.render(); \ No newline at end of file From fb4bb54aca71dfb58cc28a82503de1efd6f46360 Mon Sep 17 00:00:00 2001 From: ManishMadan2882 Date: Sat, 23 Nov 2024 02:44:06 +0530 Subject: [PATCH 043/354] (upgrade) v0.4.8 --- docs/package-lock.json | 237 +++++++++++++++++++++++++-- docs/package.json | 2 +- docs/pages/_app.mdx | 2 +- extensions/react-widget/package.json | 12 ++ 4 files changed, 240 insertions(+), 13 deletions(-) diff --git a/docs/package-lock.json b/docs/package-lock.json index 10418138..eb26e8ed 100644 --- a/docs/package-lock.json +++ b/docs/package-lock.json @@ -7,7 +7,7 @@ "license": "MIT", "dependencies": { "@vercel/analytics": "^1.1.1", - "docsgpt": "^0.4.7", + "docsgpt-react": "^0.4.8", "next": "^14.2.12", "nextra": "^2.13.2", "nextra-theme-docs": "^2.13.2", @@ -3575,10 +3575,10 @@ "node": ">=0.3.1" } }, - "node_modules/docsgpt": { - "version": "0.4.7", - "resolved": "https://registry.npmjs.org/docsgpt/-/docsgpt-0.4.7.tgz", - "integrity": "sha512-4YZzLZo6ybudFrJVUQflDFeWzFiTATRWB9myrGSpLigyuMMzax1ZAY2xFallZLuEG9VVm0mOgkx3ssWHLrXWkQ==", + "node_modules/docsgpt-react": { + "version": "0.4.8", + "resolved": "https://registry.npmjs.org/docsgpt-react/-/docsgpt-react-0.4.8.tgz", + "integrity": "sha512-A4+wZVDDtX6J84SHBl2VZpDAydy1kwKUOeGcIiq0uY+JmP+ZKst879vsfgEN1WY3ZNo8F+AC1w+3g/jOZ3Ma8g==", "license": "Apache-2.0", "dependencies": { "@babel/plugin-transform-flow-strip-types": "^7.23.3", @@ -6634,15 +6634,16 @@ }, "node_modules/npm/node_modules/@colors/colors": { "version": "1.5.0", + "extraneous": true, "inBundle": true, "license": "MIT", - "optional": true, "engines": { "node": ">=0.1.90" } }, "node_modules/npm/node_modules/@isaacs/cliui": { "version": "8.0.2", + "extraneous": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -6659,6 +6660,7 @@ }, "node_modules/npm/node_modules/@isaacs/cliui/node_modules/ansi-regex": { "version": "6.0.1", + "extraneous": true, "inBundle": true, "license": "MIT", "engines": { @@ -6670,11 +6672,13 @@ }, "node_modules/npm/node_modules/@isaacs/cliui/node_modules/emoji-regex": { "version": "9.2.2", + "extraneous": true, "inBundle": true, "license": "MIT" }, "node_modules/npm/node_modules/@isaacs/cliui/node_modules/string-width": { "version": "5.1.2", + "extraneous": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -6691,6 +6695,7 @@ }, "node_modules/npm/node_modules/@isaacs/cliui/node_modules/strip-ansi": { "version": "7.1.0", + "extraneous": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -6705,11 +6710,13 @@ }, "node_modules/npm/node_modules/@isaacs/string-locale-compare": { "version": "1.1.0", + "extraneous": true, "inBundle": true, "license": "ISC" }, "node_modules/npm/node_modules/@npmcli/agent": { "version": "2.2.1", + "extraneous": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -6725,6 +6732,7 @@ }, "node_modules/npm/node_modules/@npmcli/arborist": { "version": "7.4.1", + "extraneous": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -6771,6 +6779,7 @@ }, "node_modules/npm/node_modules/@npmcli/config": { "version": "8.2.1", + "extraneous": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -6789,6 +6798,7 @@ }, "node_modules/npm/node_modules/@npmcli/disparity-colors": { "version": "3.0.0", + "extraneous": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -6800,6 +6810,7 @@ }, "node_modules/npm/node_modules/@npmcli/disparity-colors/node_modules/ansi-styles": { "version": "4.3.0", + "extraneous": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -6814,6 +6825,7 @@ }, "node_modules/npm/node_modules/@npmcli/fs": { "version": "3.1.0", + "extraneous": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -6825,6 +6837,7 @@ }, "node_modules/npm/node_modules/@npmcli/git": { "version": "5.0.4", + "extraneous": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -6843,6 +6856,7 @@ }, "node_modules/npm/node_modules/@npmcli/installed-package-contents": { "version": "2.0.2", + "extraneous": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -6858,6 +6872,7 @@ }, "node_modules/npm/node_modules/@npmcli/map-workspaces": { "version": "3.0.4", + "extraneous": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -6872,6 +6887,7 @@ }, "node_modules/npm/node_modules/@npmcli/metavuln-calculator": { "version": "7.0.0", + "extraneous": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -6886,6 +6902,7 @@ }, "node_modules/npm/node_modules/@npmcli/name-from-folder": { "version": "2.0.0", + "extraneous": true, "inBundle": true, "license": "ISC", "engines": { @@ -6894,6 +6911,7 @@ }, "node_modules/npm/node_modules/@npmcli/node-gyp": { "version": "3.0.0", + "extraneous": true, "inBundle": true, "license": "ISC", "engines": { @@ -6902,6 +6920,7 @@ }, "node_modules/npm/node_modules/@npmcli/package-json": { "version": "5.0.0", + "extraneous": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -6919,6 +6938,7 @@ }, "node_modules/npm/node_modules/@npmcli/promise-spawn": { "version": "7.0.1", + "extraneous": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -6930,6 +6950,7 @@ }, "node_modules/npm/node_modules/@npmcli/query": { "version": "3.1.0", + "extraneous": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -6941,6 +6962,7 @@ }, "node_modules/npm/node_modules/@npmcli/redact": { "version": "1.1.0", + "extraneous": true, "inBundle": true, "license": "ISC", "engines": { @@ -6949,6 +6971,7 @@ }, "node_modules/npm/node_modules/@npmcli/run-script": { "version": "7.0.4", + "extraneous": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -6964,15 +6987,16 @@ }, "node_modules/npm/node_modules/@pkgjs/parseargs": { "version": "0.11.0", + "extraneous": true, "inBundle": true, "license": "MIT", - "optional": true, "engines": { "node": ">=14" } }, "node_modules/npm/node_modules/@sigstore/bundle": { "version": "2.2.0", + "extraneous": true, "inBundle": true, "license": "Apache-2.0", "dependencies": { @@ -6984,6 +7008,7 @@ }, "node_modules/npm/node_modules/@sigstore/core": { "version": "1.0.0", + "extraneous": true, "inBundle": true, "license": "Apache-2.0", "engines": { @@ -6992,6 +7017,7 @@ }, "node_modules/npm/node_modules/@sigstore/protobuf-specs": { "version": "0.3.0", + "extraneous": true, "inBundle": true, "license": "Apache-2.0", "engines": { @@ -7000,6 +7026,7 @@ }, "node_modules/npm/node_modules/@sigstore/sign": { "version": "2.2.3", + "extraneous": true, "inBundle": true, "license": "Apache-2.0", "dependencies": { @@ -7014,6 +7041,7 @@ }, "node_modules/npm/node_modules/@sigstore/tuf": { "version": "2.3.2", + "extraneous": true, "inBundle": true, "license": "Apache-2.0", "dependencies": { @@ -7026,6 +7054,7 @@ }, "node_modules/npm/node_modules/@sigstore/verify": { "version": "1.1.0", + "extraneous": true, "inBundle": true, "license": "Apache-2.0", "dependencies": { @@ -7039,6 +7068,7 @@ }, "node_modules/npm/node_modules/@tufjs/canonical-json": { "version": "2.0.0", + "extraneous": true, "inBundle": true, "license": "MIT", "engines": { @@ -7047,6 +7077,7 @@ }, "node_modules/npm/node_modules/@tufjs/models": { "version": "2.0.0", + "extraneous": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -7059,6 +7090,7 @@ }, "node_modules/npm/node_modules/abbrev": { "version": "2.0.0", + "extraneous": true, "inBundle": true, "license": "ISC", "engines": { @@ -7067,6 +7099,7 @@ }, "node_modules/npm/node_modules/agent-base": { "version": "7.1.1", + "extraneous": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -7078,6 +7111,7 @@ }, "node_modules/npm/node_modules/aggregate-error": { "version": "3.1.0", + "extraneous": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -7090,6 +7124,7 @@ }, "node_modules/npm/node_modules/ansi-regex": { "version": "5.0.1", + "extraneous": true, "inBundle": true, "license": "MIT", "engines": { @@ -7098,6 +7133,7 @@ }, "node_modules/npm/node_modules/ansi-styles": { "version": "6.2.1", + "extraneous": true, "inBundle": true, "license": "MIT", "engines": { @@ -7109,16 +7145,19 @@ }, "node_modules/npm/node_modules/aproba": { "version": "2.0.0", + "extraneous": true, "inBundle": true, "license": "ISC" }, "node_modules/npm/node_modules/archy": { "version": "1.0.0", + "extraneous": true, "inBundle": true, "license": "MIT" }, "node_modules/npm/node_modules/are-we-there-yet": { "version": "4.0.2", + "extraneous": true, "inBundle": true, "license": "ISC", "engines": { @@ -7127,11 +7166,13 @@ }, "node_modules/npm/node_modules/balanced-match": { "version": "1.0.2", + "extraneous": true, "inBundle": true, "license": "MIT" }, "node_modules/npm/node_modules/bin-links": { "version": "4.0.3", + "extraneous": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -7146,6 +7187,7 @@ }, "node_modules/npm/node_modules/binary-extensions": { "version": "2.3.0", + "extraneous": true, "inBundle": true, "license": "MIT", "engines": { @@ -7157,6 +7199,7 @@ }, "node_modules/npm/node_modules/brace-expansion": { "version": "2.0.1", + "extraneous": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -7165,6 +7208,7 @@ }, "node_modules/npm/node_modules/builtins": { "version": "5.0.1", + "extraneous": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -7173,6 +7217,7 @@ }, "node_modules/npm/node_modules/cacache": { "version": "18.0.2", + "extraneous": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -7195,6 +7240,7 @@ }, "node_modules/npm/node_modules/chalk": { "version": "5.3.0", + "extraneous": true, "inBundle": true, "license": "MIT", "engines": { @@ -7206,6 +7252,7 @@ }, "node_modules/npm/node_modules/chownr": { "version": "2.0.0", + "extraneous": true, "inBundle": true, "license": "ISC", "engines": { @@ -7214,6 +7261,7 @@ }, "node_modules/npm/node_modules/ci-info": { "version": "4.0.0", + "extraneous": true, "funding": [ { "type": "github", @@ -7228,6 +7276,7 @@ }, "node_modules/npm/node_modules/cidr-regex": { "version": "4.0.3", + "extraneous": true, "inBundle": true, "license": "BSD-2-Clause", "dependencies": { @@ -7239,6 +7288,7 @@ }, "node_modules/npm/node_modules/clean-stack": { "version": "2.2.0", + "extraneous": true, "inBundle": true, "license": "MIT", "engines": { @@ -7247,6 +7297,7 @@ }, "node_modules/npm/node_modules/cli-columns": { "version": "4.0.0", + "extraneous": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -7259,6 +7310,7 @@ }, "node_modules/npm/node_modules/cli-table3": { "version": "0.6.4", + "extraneous": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -7273,6 +7325,7 @@ }, "node_modules/npm/node_modules/clone": { "version": "1.0.4", + "extraneous": true, "inBundle": true, "license": "MIT", "engines": { @@ -7281,6 +7334,7 @@ }, "node_modules/npm/node_modules/cmd-shim": { "version": "6.0.2", + "extraneous": true, "inBundle": true, "license": "ISC", "engines": { @@ -7289,6 +7343,7 @@ }, "node_modules/npm/node_modules/color-convert": { "version": "2.0.1", + "extraneous": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -7300,11 +7355,13 @@ }, "node_modules/npm/node_modules/color-name": { "version": "1.1.4", + "extraneous": true, "inBundle": true, "license": "MIT" }, "node_modules/npm/node_modules/color-support": { "version": "1.1.3", + "extraneous": true, "inBundle": true, "license": "ISC", "bin": { @@ -7313,6 +7370,7 @@ }, "node_modules/npm/node_modules/columnify": { "version": "1.6.0", + "extraneous": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -7325,16 +7383,19 @@ }, "node_modules/npm/node_modules/common-ancestor-path": { "version": "1.0.1", + "extraneous": true, "inBundle": true, "license": "ISC" }, "node_modules/npm/node_modules/console-control-strings": { "version": "1.1.0", + "extraneous": true, "inBundle": true, "license": "ISC" }, "node_modules/npm/node_modules/cross-spawn": { "version": "7.0.3", + "extraneous": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -7348,6 +7409,7 @@ }, "node_modules/npm/node_modules/cross-spawn/node_modules/which": { "version": "2.0.2", + "extraneous": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -7362,6 +7424,7 @@ }, "node_modules/npm/node_modules/cssesc": { "version": "3.0.0", + "extraneous": true, "inBundle": true, "license": "MIT", "bin": { @@ -7373,6 +7436,7 @@ }, "node_modules/npm/node_modules/debug": { "version": "4.3.4", + "extraneous": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -7389,11 +7453,13 @@ }, "node_modules/npm/node_modules/debug/node_modules/ms": { "version": "2.1.2", + "extraneous": true, "inBundle": true, "license": "MIT" }, "node_modules/npm/node_modules/defaults": { "version": "1.0.4", + "extraneous": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -7405,6 +7471,7 @@ }, "node_modules/npm/node_modules/diff": { "version": "5.2.0", + "extraneous": true, "inBundle": true, "license": "BSD-3-Clause", "engines": { @@ -7413,25 +7480,28 @@ }, "node_modules/npm/node_modules/eastasianwidth": { "version": "0.2.0", + "extraneous": true, "inBundle": true, "license": "MIT" }, "node_modules/npm/node_modules/emoji-regex": { "version": "8.0.0", + "extraneous": true, "inBundle": true, "license": "MIT" }, "node_modules/npm/node_modules/encoding": { "version": "0.1.13", + "extraneous": true, "inBundle": true, "license": "MIT", - "optional": true, "dependencies": { "iconv-lite": "^0.6.2" } }, "node_modules/npm/node_modules/env-paths": { "version": "2.2.1", + "extraneous": true, "inBundle": true, "license": "MIT", "engines": { @@ -7440,16 +7510,19 @@ }, "node_modules/npm/node_modules/err-code": { "version": "2.0.3", + "extraneous": true, "inBundle": true, "license": "MIT" }, "node_modules/npm/node_modules/exponential-backoff": { "version": "3.1.1", + "extraneous": true, "inBundle": true, "license": "Apache-2.0" }, "node_modules/npm/node_modules/fastest-levenshtein": { "version": "1.0.16", + "extraneous": true, "inBundle": true, "license": "MIT", "engines": { @@ -7458,6 +7531,7 @@ }, "node_modules/npm/node_modules/foreground-child": { "version": "3.1.1", + "extraneous": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -7473,6 +7547,7 @@ }, "node_modules/npm/node_modules/fs-minipass": { "version": "3.0.3", + "extraneous": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -7484,6 +7559,7 @@ }, "node_modules/npm/node_modules/function-bind": { "version": "1.1.2", + "extraneous": true, "inBundle": true, "license": "MIT", "funding": { @@ -7492,6 +7568,7 @@ }, "node_modules/npm/node_modules/gauge": { "version": "5.0.1", + "extraneous": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -7510,6 +7587,7 @@ }, "node_modules/npm/node_modules/glob": { "version": "10.3.12", + "extraneous": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -7531,16 +7609,19 @@ }, "node_modules/npm/node_modules/graceful-fs": { "version": "4.2.11", + "extraneous": true, "inBundle": true, "license": "ISC" }, "node_modules/npm/node_modules/has-unicode": { "version": "2.0.1", + "extraneous": true, "inBundle": true, "license": "ISC" }, "node_modules/npm/node_modules/hasown": { "version": "2.0.1", + "extraneous": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -7552,6 +7633,7 @@ }, "node_modules/npm/node_modules/hosted-git-info": { "version": "7.0.1", + "extraneous": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -7563,11 +7645,13 @@ }, "node_modules/npm/node_modules/http-cache-semantics": { "version": "4.1.1", + "extraneous": true, "inBundle": true, "license": "BSD-2-Clause" }, "node_modules/npm/node_modules/http-proxy-agent": { "version": "7.0.2", + "extraneous": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -7580,6 +7664,7 @@ }, "node_modules/npm/node_modules/https-proxy-agent": { "version": "7.0.4", + "extraneous": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -7592,9 +7677,9 @@ }, "node_modules/npm/node_modules/iconv-lite": { "version": "0.6.3", + "extraneous": true, "inBundle": true, "license": "MIT", - "optional": true, "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" }, @@ -7604,6 +7689,7 @@ }, "node_modules/npm/node_modules/ignore-walk": { "version": "6.0.4", + "extraneous": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -7615,6 +7701,7 @@ }, "node_modules/npm/node_modules/imurmurhash": { "version": "0.1.4", + "extraneous": true, "inBundle": true, "license": "MIT", "engines": { @@ -7623,6 +7710,7 @@ }, "node_modules/npm/node_modules/indent-string": { "version": "4.0.0", + "extraneous": true, "inBundle": true, "license": "MIT", "engines": { @@ -7631,6 +7719,7 @@ }, "node_modules/npm/node_modules/ini": { "version": "4.1.2", + "extraneous": true, "inBundle": true, "license": "ISC", "engines": { @@ -7639,6 +7728,7 @@ }, "node_modules/npm/node_modules/init-package-json": { "version": "6.0.2", + "extraneous": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -7656,6 +7746,7 @@ }, "node_modules/npm/node_modules/ip-address": { "version": "9.0.5", + "extraneous": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -7668,11 +7759,13 @@ }, "node_modules/npm/node_modules/ip-address/node_modules/sprintf-js": { "version": "1.1.3", + "extraneous": true, "inBundle": true, "license": "BSD-3-Clause" }, "node_modules/npm/node_modules/ip-regex": { "version": "5.0.0", + "extraneous": true, "inBundle": true, "license": "MIT", "engines": { @@ -7684,6 +7777,7 @@ }, "node_modules/npm/node_modules/is-cidr": { "version": "5.0.3", + "extraneous": true, "inBundle": true, "license": "BSD-2-Clause", "dependencies": { @@ -7695,6 +7789,7 @@ }, "node_modules/npm/node_modules/is-core-module": { "version": "2.13.1", + "extraneous": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -7706,6 +7801,7 @@ }, "node_modules/npm/node_modules/is-fullwidth-code-point": { "version": "3.0.0", + "extraneous": true, "inBundle": true, "license": "MIT", "engines": { @@ -7714,16 +7810,19 @@ }, "node_modules/npm/node_modules/is-lambda": { "version": "1.0.1", + "extraneous": true, "inBundle": true, "license": "MIT" }, "node_modules/npm/node_modules/isexe": { "version": "2.0.0", + "extraneous": true, "inBundle": true, "license": "ISC" }, "node_modules/npm/node_modules/jackspeak": { "version": "2.3.6", + "extraneous": true, "inBundle": true, "license": "BlueOak-1.0.0", "dependencies": { @@ -7741,11 +7840,13 @@ }, "node_modules/npm/node_modules/jsbn": { "version": "1.1.0", + "extraneous": true, "inBundle": true, "license": "MIT" }, "node_modules/npm/node_modules/json-parse-even-better-errors": { "version": "3.0.1", + "extraneous": true, "inBundle": true, "license": "MIT", "engines": { @@ -7754,6 +7855,7 @@ }, "node_modules/npm/node_modules/json-stringify-nice": { "version": "1.1.4", + "extraneous": true, "inBundle": true, "license": "ISC", "funding": { @@ -7765,21 +7867,25 @@ "engines": [ "node >= 0.2.0" ], + "extraneous": true, "inBundle": true, "license": "MIT" }, "node_modules/npm/node_modules/just-diff": { "version": "6.0.2", + "extraneous": true, "inBundle": true, "license": "MIT" }, "node_modules/npm/node_modules/just-diff-apply": { "version": "5.5.0", + "extraneous": true, "inBundle": true, "license": "MIT" }, "node_modules/npm/node_modules/libnpmaccess": { "version": "8.0.3", + "extraneous": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -7792,6 +7898,7 @@ }, "node_modules/npm/node_modules/libnpmdiff": { "version": "6.0.8", + "extraneous": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -7811,6 +7918,7 @@ }, "node_modules/npm/node_modules/libnpmexec": { "version": "7.0.9", + "extraneous": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -7832,6 +7940,7 @@ }, "node_modules/npm/node_modules/libnpmfund": { "version": "5.0.6", + "extraneous": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -7843,6 +7952,7 @@ }, "node_modules/npm/node_modules/libnpmhook": { "version": "10.0.2", + "extraneous": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -7855,6 +7965,7 @@ }, "node_modules/npm/node_modules/libnpmorg": { "version": "6.0.3", + "extraneous": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -7867,6 +7978,7 @@ }, "node_modules/npm/node_modules/libnpmpack": { "version": "6.0.8", + "extraneous": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -7881,6 +7993,7 @@ }, "node_modules/npm/node_modules/libnpmpublish": { "version": "9.0.5", + "extraneous": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -7899,6 +8012,7 @@ }, "node_modules/npm/node_modules/libnpmsearch": { "version": "7.0.2", + "extraneous": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -7910,6 +8024,7 @@ }, "node_modules/npm/node_modules/libnpmteam": { "version": "6.0.2", + "extraneous": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -7922,6 +8037,7 @@ }, "node_modules/npm/node_modules/libnpmversion": { "version": "5.0.2", + "extraneous": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -7937,6 +8053,7 @@ }, "node_modules/npm/node_modules/lru-cache": { "version": "10.2.0", + "extraneous": true, "inBundle": true, "license": "ISC", "engines": { @@ -7945,6 +8062,7 @@ }, "node_modules/npm/node_modules/make-fetch-happen": { "version": "13.0.0", + "extraneous": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -7966,6 +8084,7 @@ }, "node_modules/npm/node_modules/minimatch": { "version": "9.0.4", + "extraneous": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -7980,6 +8099,7 @@ }, "node_modules/npm/node_modules/minipass": { "version": "7.0.4", + "extraneous": true, "inBundle": true, "license": "ISC", "engines": { @@ -7988,6 +8108,7 @@ }, "node_modules/npm/node_modules/minipass-collect": { "version": "2.0.1", + "extraneous": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -7999,6 +8120,7 @@ }, "node_modules/npm/node_modules/minipass-fetch": { "version": "3.0.4", + "extraneous": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -8015,6 +8137,7 @@ }, "node_modules/npm/node_modules/minipass-flush": { "version": "1.0.5", + "extraneous": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -8026,6 +8149,7 @@ }, "node_modules/npm/node_modules/minipass-flush/node_modules/minipass": { "version": "3.3.6", + "extraneous": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -8037,6 +8161,7 @@ }, "node_modules/npm/node_modules/minipass-json-stream": { "version": "1.0.1", + "extraneous": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -8046,6 +8171,7 @@ }, "node_modules/npm/node_modules/minipass-json-stream/node_modules/minipass": { "version": "3.3.6", + "extraneous": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -8057,6 +8183,7 @@ }, "node_modules/npm/node_modules/minipass-pipeline": { "version": "1.2.4", + "extraneous": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -8068,6 +8195,7 @@ }, "node_modules/npm/node_modules/minipass-pipeline/node_modules/minipass": { "version": "3.3.6", + "extraneous": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -8079,6 +8207,7 @@ }, "node_modules/npm/node_modules/minipass-sized": { "version": "1.0.3", + "extraneous": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -8090,6 +8219,7 @@ }, "node_modules/npm/node_modules/minipass-sized/node_modules/minipass": { "version": "3.3.6", + "extraneous": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -8101,6 +8231,7 @@ }, "node_modules/npm/node_modules/minizlib": { "version": "2.1.2", + "extraneous": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -8113,6 +8244,7 @@ }, "node_modules/npm/node_modules/minizlib/node_modules/minipass": { "version": "3.3.6", + "extraneous": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -8124,6 +8256,7 @@ }, "node_modules/npm/node_modules/mkdirp": { "version": "1.0.4", + "extraneous": true, "inBundle": true, "license": "MIT", "bin": { @@ -8135,11 +8268,13 @@ }, "node_modules/npm/node_modules/ms": { "version": "2.1.3", + "extraneous": true, "inBundle": true, "license": "MIT" }, "node_modules/npm/node_modules/mute-stream": { "version": "1.0.0", + "extraneous": true, "inBundle": true, "license": "ISC", "engines": { @@ -8148,6 +8283,7 @@ }, "node_modules/npm/node_modules/negotiator": { "version": "0.6.3", + "extraneous": true, "inBundle": true, "license": "MIT", "engines": { @@ -8156,6 +8292,7 @@ }, "node_modules/npm/node_modules/node-gyp": { "version": "10.1.0", + "extraneous": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -8179,6 +8316,7 @@ }, "node_modules/npm/node_modules/nopt": { "version": "7.2.0", + "extraneous": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -8193,6 +8331,7 @@ }, "node_modules/npm/node_modules/normalize-package-data": { "version": "6.0.0", + "extraneous": true, "inBundle": true, "license": "BSD-2-Clause", "dependencies": { @@ -8207,6 +8346,7 @@ }, "node_modules/npm/node_modules/npm-audit-report": { "version": "5.0.0", + "extraneous": true, "inBundle": true, "license": "ISC", "engines": { @@ -8215,6 +8355,7 @@ }, "node_modules/npm/node_modules/npm-bundled": { "version": "3.0.0", + "extraneous": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -8226,6 +8367,7 @@ }, "node_modules/npm/node_modules/npm-install-checks": { "version": "6.3.0", + "extraneous": true, "inBundle": true, "license": "BSD-2-Clause", "dependencies": { @@ -8237,6 +8379,7 @@ }, "node_modules/npm/node_modules/npm-normalize-package-bin": { "version": "3.0.1", + "extraneous": true, "inBundle": true, "license": "ISC", "engines": { @@ -8245,6 +8388,7 @@ }, "node_modules/npm/node_modules/npm-package-arg": { "version": "11.0.1", + "extraneous": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -8259,6 +8403,7 @@ }, "node_modules/npm/node_modules/npm-packlist": { "version": "8.0.2", + "extraneous": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -8270,6 +8415,7 @@ }, "node_modules/npm/node_modules/npm-pick-manifest": { "version": "9.0.0", + "extraneous": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -8284,6 +8430,7 @@ }, "node_modules/npm/node_modules/npm-profile": { "version": "9.0.0", + "extraneous": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -8296,6 +8443,7 @@ }, "node_modules/npm/node_modules/npm-registry-fetch": { "version": "16.2.0", + "extraneous": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -8314,6 +8462,7 @@ }, "node_modules/npm/node_modules/npm-user-validate": { "version": "2.0.0", + "extraneous": true, "inBundle": true, "license": "BSD-2-Clause", "engines": { @@ -8322,6 +8471,7 @@ }, "node_modules/npm/node_modules/npmlog": { "version": "7.0.1", + "extraneous": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -8336,6 +8486,7 @@ }, "node_modules/npm/node_modules/p-map": { "version": "4.0.0", + "extraneous": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -8350,6 +8501,7 @@ }, "node_modules/npm/node_modules/pacote": { "version": "17.0.6", + "extraneous": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -8381,6 +8533,7 @@ }, "node_modules/npm/node_modules/parse-conflict-json": { "version": "3.0.1", + "extraneous": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -8394,6 +8547,7 @@ }, "node_modules/npm/node_modules/path-key": { "version": "3.1.1", + "extraneous": true, "inBundle": true, "license": "MIT", "engines": { @@ -8402,6 +8556,7 @@ }, "node_modules/npm/node_modules/path-scurry": { "version": "1.10.2", + "extraneous": true, "inBundle": true, "license": "BlueOak-1.0.0", "dependencies": { @@ -8417,6 +8572,7 @@ }, "node_modules/npm/node_modules/postcss-selector-parser": { "version": "6.0.15", + "extraneous": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -8429,6 +8585,7 @@ }, "node_modules/npm/node_modules/proc-log": { "version": "3.0.0", + "extraneous": true, "inBundle": true, "license": "ISC", "engines": { @@ -8437,6 +8594,7 @@ }, "node_modules/npm/node_modules/promise-all-reject-late": { "version": "1.0.1", + "extraneous": true, "inBundle": true, "license": "ISC", "funding": { @@ -8445,6 +8603,7 @@ }, "node_modules/npm/node_modules/promise-call-limit": { "version": "3.0.1", + "extraneous": true, "inBundle": true, "license": "ISC", "funding": { @@ -8453,11 +8612,13 @@ }, "node_modules/npm/node_modules/promise-inflight": { "version": "1.0.1", + "extraneous": true, "inBundle": true, "license": "ISC" }, "node_modules/npm/node_modules/promise-retry": { "version": "2.0.1", + "extraneous": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -8470,6 +8631,7 @@ }, "node_modules/npm/node_modules/promzard": { "version": "1.0.1", + "extraneous": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -8481,6 +8643,7 @@ }, "node_modules/npm/node_modules/qrcode-terminal": { "version": "0.12.0", + "extraneous": true, "inBundle": true, "bin": { "qrcode-terminal": "bin/qrcode-terminal.js" @@ -8488,6 +8651,7 @@ }, "node_modules/npm/node_modules/read": { "version": "3.0.1", + "extraneous": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -8499,6 +8663,7 @@ }, "node_modules/npm/node_modules/read-cmd-shim": { "version": "4.0.0", + "extraneous": true, "inBundle": true, "license": "ISC", "engines": { @@ -8507,6 +8672,7 @@ }, "node_modules/npm/node_modules/read-package-json": { "version": "7.0.0", + "extraneous": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -8521,6 +8687,7 @@ }, "node_modules/npm/node_modules/read-package-json-fast": { "version": "3.0.2", + "extraneous": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -8533,6 +8700,7 @@ }, "node_modules/npm/node_modules/retry": { "version": "0.12.0", + "extraneous": true, "inBundle": true, "license": "MIT", "engines": { @@ -8541,12 +8709,13 @@ }, "node_modules/npm/node_modules/safer-buffer": { "version": "2.1.2", + "extraneous": true, "inBundle": true, - "license": "MIT", - "optional": true + "license": "MIT" }, "node_modules/npm/node_modules/semver": { "version": "7.6.0", + "extraneous": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -8561,6 +8730,7 @@ }, "node_modules/npm/node_modules/semver/node_modules/lru-cache": { "version": "6.0.0", + "extraneous": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -8572,11 +8742,13 @@ }, "node_modules/npm/node_modules/set-blocking": { "version": "2.0.0", + "extraneous": true, "inBundle": true, "license": "ISC" }, "node_modules/npm/node_modules/shebang-command": { "version": "2.0.0", + "extraneous": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -8588,6 +8760,7 @@ }, "node_modules/npm/node_modules/shebang-regex": { "version": "3.0.0", + "extraneous": true, "inBundle": true, "license": "MIT", "engines": { @@ -8596,6 +8769,7 @@ }, "node_modules/npm/node_modules/signal-exit": { "version": "4.1.0", + "extraneous": true, "inBundle": true, "license": "ISC", "engines": { @@ -8607,6 +8781,7 @@ }, "node_modules/npm/node_modules/sigstore": { "version": "2.2.2", + "extraneous": true, "inBundle": true, "license": "Apache-2.0", "dependencies": { @@ -8623,6 +8798,7 @@ }, "node_modules/npm/node_modules/smart-buffer": { "version": "4.2.0", + "extraneous": true, "inBundle": true, "license": "MIT", "engines": { @@ -8632,6 +8808,7 @@ }, "node_modules/npm/node_modules/socks": { "version": "2.8.0", + "extraneous": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -8645,6 +8822,7 @@ }, "node_modules/npm/node_modules/socks-proxy-agent": { "version": "8.0.2", + "extraneous": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -8658,6 +8836,7 @@ }, "node_modules/npm/node_modules/spdx-correct": { "version": "3.2.0", + "extraneous": true, "inBundle": true, "license": "Apache-2.0", "dependencies": { @@ -8667,11 +8846,13 @@ }, "node_modules/npm/node_modules/spdx-exceptions": { "version": "2.5.0", + "extraneous": true, "inBundle": true, "license": "CC-BY-3.0" }, "node_modules/npm/node_modules/spdx-expression-parse": { "version": "3.0.1", + "extraneous": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -8681,11 +8862,13 @@ }, "node_modules/npm/node_modules/spdx-license-ids": { "version": "3.0.17", + "extraneous": true, "inBundle": true, "license": "CC0-1.0" }, "node_modules/npm/node_modules/ssri": { "version": "10.0.5", + "extraneous": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -8697,6 +8880,7 @@ }, "node_modules/npm/node_modules/string-width": { "version": "4.2.3", + "extraneous": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -8711,6 +8895,7 @@ "node_modules/npm/node_modules/string-width-cjs": { "name": "string-width", "version": "4.2.3", + "extraneous": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -8724,6 +8909,7 @@ }, "node_modules/npm/node_modules/strip-ansi": { "version": "6.0.1", + "extraneous": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -8736,6 +8922,7 @@ "node_modules/npm/node_modules/strip-ansi-cjs": { "name": "strip-ansi", "version": "6.0.1", + "extraneous": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -8747,6 +8934,7 @@ }, "node_modules/npm/node_modules/supports-color": { "version": "9.4.0", + "extraneous": true, "inBundle": true, "license": "MIT", "engines": { @@ -8758,6 +8946,7 @@ }, "node_modules/npm/node_modules/tar": { "version": "6.2.1", + "extraneous": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -8774,6 +8963,7 @@ }, "node_modules/npm/node_modules/tar/node_modules/fs-minipass": { "version": "2.1.0", + "extraneous": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -8785,6 +8975,7 @@ }, "node_modules/npm/node_modules/tar/node_modules/fs-minipass/node_modules/minipass": { "version": "3.3.6", + "extraneous": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -8796,6 +8987,7 @@ }, "node_modules/npm/node_modules/tar/node_modules/minipass": { "version": "5.0.0", + "extraneous": true, "inBundle": true, "license": "ISC", "engines": { @@ -8804,16 +8996,19 @@ }, "node_modules/npm/node_modules/text-table": { "version": "0.2.0", + "extraneous": true, "inBundle": true, "license": "MIT" }, "node_modules/npm/node_modules/tiny-relative-date": { "version": "1.3.0", + "extraneous": true, "inBundle": true, "license": "MIT" }, "node_modules/npm/node_modules/treeverse": { "version": "3.0.0", + "extraneous": true, "inBundle": true, "license": "ISC", "engines": { @@ -8822,6 +9017,7 @@ }, "node_modules/npm/node_modules/tuf-js": { "version": "2.2.0", + "extraneous": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -8835,6 +9031,7 @@ }, "node_modules/npm/node_modules/unique-filename": { "version": "3.0.0", + "extraneous": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -8846,6 +9043,7 @@ }, "node_modules/npm/node_modules/unique-slug": { "version": "4.0.0", + "extraneous": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -8857,11 +9055,13 @@ }, "node_modules/npm/node_modules/util-deprecate": { "version": "1.0.2", + "extraneous": true, "inBundle": true, "license": "MIT" }, "node_modules/npm/node_modules/validate-npm-package-license": { "version": "3.0.4", + "extraneous": true, "inBundle": true, "license": "Apache-2.0", "dependencies": { @@ -8871,6 +9071,7 @@ }, "node_modules/npm/node_modules/validate-npm-package-name": { "version": "5.0.0", + "extraneous": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -8882,11 +9083,13 @@ }, "node_modules/npm/node_modules/walk-up-path": { "version": "3.0.1", + "extraneous": true, "inBundle": true, "license": "ISC" }, "node_modules/npm/node_modules/wcwidth": { "version": "1.0.1", + "extraneous": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -8895,6 +9098,7 @@ }, "node_modules/npm/node_modules/which": { "version": "4.0.0", + "extraneous": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -8909,6 +9113,7 @@ }, "node_modules/npm/node_modules/which/node_modules/isexe": { "version": "3.1.1", + "extraneous": true, "inBundle": true, "license": "ISC", "engines": { @@ -8917,6 +9122,7 @@ }, "node_modules/npm/node_modules/wide-align": { "version": "1.1.5", + "extraneous": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -8925,6 +9131,7 @@ }, "node_modules/npm/node_modules/wrap-ansi": { "version": "8.1.0", + "extraneous": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -8942,6 +9149,7 @@ "node_modules/npm/node_modules/wrap-ansi-cjs": { "name": "wrap-ansi", "version": "7.0.0", + "extraneous": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -8958,6 +9166,7 @@ }, "node_modules/npm/node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { "version": "4.3.0", + "extraneous": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -8972,6 +9181,7 @@ }, "node_modules/npm/node_modules/wrap-ansi/node_modules/ansi-regex": { "version": "6.0.1", + "extraneous": true, "inBundle": true, "license": "MIT", "engines": { @@ -8983,11 +9193,13 @@ }, "node_modules/npm/node_modules/wrap-ansi/node_modules/emoji-regex": { "version": "9.2.2", + "extraneous": true, "inBundle": true, "license": "MIT" }, "node_modules/npm/node_modules/wrap-ansi/node_modules/string-width": { "version": "5.1.2", + "extraneous": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -9004,6 +9216,7 @@ }, "node_modules/npm/node_modules/wrap-ansi/node_modules/strip-ansi": { "version": "7.1.0", + "extraneous": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -9018,6 +9231,7 @@ }, "node_modules/npm/node_modules/write-file-atomic": { "version": "5.0.1", + "extraneous": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -9030,6 +9244,7 @@ }, "node_modules/npm/node_modules/yallist": { "version": "4.0.0", + "extraneous": true, "inBundle": true, "license": "ISC" }, diff --git a/docs/package.json b/docs/package.json index cc3e786d..96de9ea8 100644 --- a/docs/package.json +++ b/docs/package.json @@ -7,7 +7,7 @@ "license": "MIT", "dependencies": { "@vercel/analytics": "^1.1.1", - "docsgpt": "^0.4.7", + "docsgpt-react": "^0.4.8", "next": "^14.2.12", "nextra": "^2.13.2", "nextra-theme-docs": "^2.13.2", diff --git a/docs/pages/_app.mdx b/docs/pages/_app.mdx index 1cb8cadd..0111cd96 100644 --- a/docs/pages/_app.mdx +++ b/docs/pages/_app.mdx @@ -1,4 +1,4 @@ -import { DocsGPTWidget } from "docsgpt"; +import { DocsGPTWidget } from "docsgpt-react"; export default function MyApp({ Component, pageProps }) { return ( diff --git a/extensions/react-widget/package.json b/extensions/react-widget/package.json index 35676fc1..c6401d30 100644 --- a/extensions/react-widget/package.json +++ b/extensions/react-widget/package.json @@ -11,6 +11,18 @@ "dist", "package.json" ], + "targets": { + "modern": { + "engines": { + "browsers": "Chrome 80" + } + }, + "legacy": { + "engines": { + "browsers": "> 0.5%, last 2 versions, not dead" + } + } + }, "@parcel/resolver-default": { "packageExports": true }, From ce975c5d93036cfdf63d96a3ccece213a6cc65ff Mon Sep 17 00:00:00 2001 From: ManishMadan2882 Date: Sat, 23 Nov 2024 03:09:03 +0530 Subject: [PATCH 044/354] (documentation): udpate with search bar --- docs/pages/Extensions/react-widget.md | 111 +++++++++++++++++++++++--- extensions/react-widget/README.md | 75 ++++++++++++++++- 2 files changed, 171 insertions(+), 15 deletions(-) diff --git a/docs/pages/Extensions/react-widget.md b/docs/pages/Extensions/react-widget.md index 1859b558..8429e377 100644 --- a/docs/pages/Extensions/react-widget.md +++ b/docs/pages/Extensions/react-widget.md @@ -29,18 +29,30 @@ Now, you can use the widget in your component like this : buttonBg = "#222327" /> ``` -To tailor the widget to your needs, you can configure the following props in your component: -1. `apiHost` — The URL of your DocsGPT API. -2. `theme` — Allows to select your specific theme (dark or light). -3. `apiKey` — Usually, it's empty. -4. `avatar`: Specifies the URL of the avatar or image representing the chatbot. -5. `title`: Sets the title text displayed in the chatbot interface. -6. `description`: Provides a brief description of the chatbot's purpose or functionality. -7. `heroTitle`: Displays a welcome title when users interact with the chatbot. -8. `heroDescription`: Provide additional introductory text or information about the chatbot's capabilities. -9. `buttonIcon`: Specifies the url of the icon image for the widget. -10. `buttonBg`: Allows to specify the Background color of the widget. -11. `size`: Sets the size of the widget ( small, medium). +### Props Table for DocsGPT Widget + +| **Prop** | **Type** | **Default Value** | **Description** | +|--------------------|------------------|-------------------------------------------------------------|-----------------------------------------------------------------------------------------------------| +| **`apiHost`** | `string` | `"https://gptcloud.arc53.com"` | The URL of your DocsGPT API for vector search and chatbot queries. | +| **`apiKey`** | `string` | `""` | Your API key for authentication. Can be left empty if authentication is not required. | +| **`avatar`** | `string` | `"https://d3dg1063dc54p9.cloudfront.net/cute-docsgpt.png"` | Specifies the URL of the avatar or image representing the chatbot. | +| **`title`** | `string` | `"Get AI assistance"` | Sets the title text displayed in the chatbot interface. | +| **`description`** | `string` | `"DocsGPT's AI Chatbot is here to help"` | Provides a brief description of the chatbot's purpose or functionality. | +| **`heroTitle`** | `string` | `"Welcome to DocsGPT !"` | Displays a welcome title when users interact with the chatbot. | +| **`heroDescription`** | `string` | `"This chatbot is built with DocsGPT and utilises GenAI, please review important information using sources."` | Provides additional introductory text or information about the chatbot's capabilities. | +| **`theme`** | `"dark" \| "light"` | `"dark"` | Allows you to select the theme for the chatbot interface. Accepts `"dark"` or `"light"`. | +| **`buttonIcon`** | `string` | `"https://your-icon"` | Specifies the URL of the icon image for the widget's launch button. | +| **`buttonBg`** | `string` | `"#222327"` | Sets the background color of the widget's launch button. | +| **`size`** | `"small" \| "medium"` | `"medium"` | Sets the size of the widget. Options are `"small"` or `"medium"`. | + +--- + +### Notes +- **Customizing Props:** All properties can be overridden when embedding the widget. For example, you can provide a unique avatar, title, or color scheme to better align with your brand. +- **Default Theme:** The widget defaults to the dark theme unless explicitly set to `"light"`. +- **API Key:** If the `apiKey` is not required for your application, leave it empty. + +This table provides a clear overview of the customization options available for tailoring the DocsGPT widget to fit your application. ### How to use DocsGPTWidget with [Nextra](https://nextra.site/) (Next.js + MDX) @@ -121,5 +133,80 @@ To link the widget to your api and your documents you can pass parameters to the ``` +# SearchBar + +The `SearchBar` component is an interactive search bar designed to provide search results based on **vector similarity search**. It also includes the capability to open the AI Chatbot, enabling users to query. + +--- + +### Importing the Component +```tsx +import { SearchBar } from "docsgpt-react"; +``` + +--- + +### Usage Example +```tsx + +``` + +--- + +## HTML embedding for Search bar + +```html + + + + + + SearchBar Embedding + + + + +
+ + + + + +``` + +### Props + +| **Prop** | **Type** | **Default Value** | **Description** | +|-----------------|-----------|-------------------------------------|--------------------------------------------------------------------------------------------------| +| **`apiKey`** | `string` | `"74039c6d-bff7-44ce-ae55-2973cbf13837"` | Your API key generated from the app. Used for authenticating requests. | +| **`apiHost`** | `string` | `"https://gptcloud.arc53.com"` | The base URL of the server hosting the vector similarity search and chatbot services. | +| **`theme`** | `"dark" \| "light"` | `"dark"` | The theme of the search bar. Accepts `"dark"` or `"light"`. | +| **`placeholder`** | `string` | `"Search or Ask AI..."` | Placeholder text displayed in the search input field. | +| **`width`** | `string` | `"256px"` | Width of the search bar. Accepts any valid CSS width value (e.g., `"300px"`, `"100%"`, `"20rem"`). | + + +Feel free to reach out if you need help customizing or extending the `SearchBar`! + +## Our github + +[DocsGPT](https://github.com/arc53/DocsGPT) + +You can find the source code in the extensions/react-widget folder. + For more information about React, refer to this [link here](https://react.dev/learn) diff --git a/extensions/react-widget/README.md b/extensions/react-widget/README.md index b4159578..5b6222d2 100644 --- a/extensions/react-widget/README.md +++ b/extensions/react-widget/README.md @@ -13,7 +13,7 @@ npm install docsgpt ### React ```javascript - import { DocsGPTWidget } from "docsgpt"; + import { DocsGPTWidget } from "docsgpt-react"; const App = () => { return ; @@ -23,11 +23,11 @@ npm install docsgpt To link the widget to your api and your documents you can pass parameters to the component. ```javascript - import { DocsGPTWidget } from "docsgpt"; + import { DocsGPTWidget } from "docsgpt-react"; const App = () => { return ``` +# SearchBar + +The `SearchBar` component is an interactive search bar designed to provide search results based on **vector similarity search**. It also includes the capability to open the AI Chatbot, enabling users to query. + +--- + +### Importing the Component +```tsx +import { SearchBar } from "docsgpt-react"; +``` + +--- + +### Usage Example +```tsx + +``` + +--- + +## HTML embedding for Search bar + +```html + + + + + + SearchBar Embedding + + + + +
+ + + + + +``` + +### Props + +| **Prop** | **Type** | **Default Value** | **Description** | +|-----------------|-----------|-------------------------------------|--------------------------------------------------------------------------------------------------| +| **`apiKey`** | `string` | `"74039c6d-bff7-44ce-ae55-2973cbf13837"` | Your API key generated from the app. Used for authenticating requests. | +| **`apiHost`** | `string` | `"https://gptcloud.arc53.com"` | The base URL of the server hosting the vector similarity search and chatbot services. | +| **`theme`** | `"dark" \| "light"` | `"dark"` | The theme of the search bar. Accepts `"dark"` or `"light"`. | +| **`placeholder`** | `string` | `"Search or Ask AI..."` | Placeholder text displayed in the search input field. | +| **`width`** | `string` | `"256px"` | Width of the search bar. Accepts any valid CSS width value (e.g., `"300px"`, `"100%"`, `"20rem"`). | + + +Feel free to reach out if you need help customizing or extending the `SearchBar`! + ## Our github [DocsGPT](https://github.com/arc53/DocsGPT) From 8a67f18cd9bd9541934759a30f8f8bbb06e6688c Mon Sep 17 00:00:00 2001 From: Niharika Goulikar Date: Sat, 23 Nov 2024 06:13:07 +0000 Subject: [PATCH 045/354] Fixed minor ui issues --- frontend/src/conversation/ConversationBubble.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/src/conversation/ConversationBubble.tsx b/frontend/src/conversation/ConversationBubble.tsx index bc131376..567b09e9 100644 --- a/frontend/src/conversation/ConversationBubble.tsx +++ b/frontend/src/conversation/ConversationBubble.tsx @@ -104,7 +104,7 @@ const ConversationBubble = forwardRef< setEditInputBox(e.target.value)} value={editInputBox} - className="ml-2 mr-2 rounded-[28px] py-[14px] px-[19px] border-[1.5px] border-black" + className="w-[85%] ml-2 mr-2 rounded-[28px] py-[12px] dark:border-[0.5px] dark:border-white dark:bg-raisin-black dark:text-white px-[18px] border-[1.5px] border-black" /> )}
{isEditClicked && ( -
+
+
+ {rowsPerPageOptions.map((option) => ( +
handleSelectRowsPerPage(option)} + className={`cursor-pointer px-4 py-2 text-xs hover:bg-gray-100 dark:hover:bg-neutral-700 ${ + rowsPerPage === option + ? 'bg-gray-100 dark:bg-neutral-700 dark:text-light-gray' + : 'bg-white dark:bg-dark-charcoal dark:text-light-gray' + }`} + > + {option} +
+ ))} +
+
+ {/* Pagination controls */}
Page {currentPage} of {totalPages}
-
@@ -85,7 +108,7 @@ const Pagination: React.FC = ({ > arrow @@ -96,7 +119,7 @@ const Pagination: React.FC = ({ > arrow @@ -107,7 +130,7 @@ const Pagination: React.FC = ({ > arrow diff --git a/frontend/src/components/DropdownMenu.tsx b/frontend/src/components/DropdownMenu.tsx index 787d3b84..2e6c922c 100644 --- a/frontend/src/components/DropdownMenu.tsx +++ b/frontend/src/components/DropdownMenu.tsx @@ -48,7 +48,7 @@ export default function DropdownMenu({
+ {/* outside scrollable area */} + { + setCurrentPage(page); + refreshDocs(undefined, page, rowsPerPage); + }} + onRowsPerPageChange={(rows) => { + setRowsPerPage(rows); + setCurrentPage(1); + refreshDocs(undefined, 1, rows); + }} + /> + {/* Conditionally render the Upload modal based on modalState */} {modalState === 'ACTIVE' && (
From e5bd194b6c7a29bd62e89f00e4a485de45d0f5a6 Mon Sep 17 00:00:00 2001 From: fadingNA Date: Sat, 23 Nov 2024 20:04:53 -0500 Subject: [PATCH 048/354] Remove dangling console log --- frontend/src/settings/Documents.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/settings/Documents.tsx b/frontend/src/settings/Documents.tsx index 074d5516..7efd9016 100644 --- a/frontend/src/settings/Documents.tsx +++ b/frontend/src/settings/Documents.tsx @@ -134,7 +134,7 @@ const Documents: React.FC = ({ }; useEffect(() => { - console.log('modalState', modalState); + // console.log('modalState', modalState); if (modalState === 'INACTIVE') { refreshDocs(sortField, currentPage, rowsPerPage); } From 2709994ede7d644afb1973b2d1b0734552bba735 Mon Sep 17 00:00:00 2001 From: fadingNA Date: Sun, 24 Nov 2024 11:18:42 -0500 Subject: [PATCH 049/354] update APIKey Table and dark styling --- frontend/src/settings/APIKeys.tsx | 85 +++++++++++++++++------------ frontend/src/settings/Documents.tsx | 7 ++- 2 files changed, 55 insertions(+), 37 deletions(-) diff --git a/frontend/src/settings/APIKeys.tsx b/frontend/src/settings/APIKeys.tsx index b039477c..6775ba87 100644 --- a/frontend/src/settings/APIKeys.tsx +++ b/frontend/src/settings/APIKeys.tsx @@ -109,41 +109,56 @@ export default function APIKeys() { {loading ? ( ) : ( - - - - - - - - - - - {!apiKeys?.length && ( - - - - )} - {apiKeys?.map((element, index) => ( - - - - - - - ))} - -
{t('settings.apiKeys.name')}{t('settings.apiKeys.sourceDoc')}{t('settings.apiKeys.key')}
- {t('settings.apiKeys.noData')} -
{element.name}{element.source}{element.key} - Delete handleDeleteKey(element.id)} - /> -
+
+
+
+ + + + + + + + + + + {!apiKeys?.length && ( + + + + )} + {Array.isArray(apiKeys) && + apiKeys?.map((element, index) => ( + + + + + + + ))} + +
{t('settings.apiKeys.name')} + {t('settings.apiKeys.sourceDoc')} + {t('settings.apiKeys.key')}
+ {t('settings.apiKeys.noData')} +
{element.name}{element.source}{element.key} + Delete handleDeleteKey(element.id)} + /> +
+
+
+
)}
diff --git a/frontend/src/settings/Documents.tsx b/frontend/src/settings/Documents.tsx index 7efd9016..4003f655 100644 --- a/frontend/src/settings/Documents.tsx +++ b/frontend/src/settings/Documents.tsx @@ -183,7 +183,7 @@ const Documents: React.FC = ({ ) : (
-
+
@@ -228,7 +228,10 @@ const Documents: React.FC = ({ {!currentDocuments?.length && ( - From b5e5fb7f10337ea2c774a9b6a5f32e3c7cb080b1 Mon Sep 17 00:00:00 2001 From: fadingNA Date: Sun, 24 Nov 2024 17:02:57 -0500 Subject: [PATCH 050/354] fix table header text wrap --- frontend/src/settings/Documents.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/settings/Documents.tsx b/frontend/src/settings/Documents.tsx index 4003f655..0fd82aa8 100644 --- a/frontend/src/settings/Documents.tsx +++ b/frontend/src/settings/Documents.tsx @@ -186,7 +186,7 @@ const Documents: React.FC = ({
+ {t('settings.documents.noData')}
- + @@ -201,7 +201,7 @@ const Documents: React.FC = ({ /> - From d89bd0941d7cda6dc0287955ab9cf1829efb2916 Mon Sep 17 00:00:00 2001 From: fadingNA Date: Mon, 25 Nov 2024 08:35:24 -0500 Subject: [PATCH 052/354] change visible to block --- frontend/src/components/DocumentPagination.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/frontend/src/components/DocumentPagination.tsx b/frontend/src/components/DocumentPagination.tsx index af849ed4..f02ef1c0 100644 --- a/frontend/src/components/DocumentPagination.tsx +++ b/frontend/src/components/DocumentPagination.tsx @@ -62,17 +62,17 @@ const Pagination: React.FC = ({ {rowsPerPage}
{rowsPerPageOptions.map((option) => (
handleSelectRowsPerPage(option)} - className={`cursor-pointer px-4 py-2 text-xs hover:bg-gray-100 dark:hover:bg-neutral-700 ${ + className={`cursor-pointer px-4 py-2 text-xs hover:bg-gray-100 dark:hover:bg-neutral-700 ${ rowsPerPage === option ? 'bg-gray-100 dark:bg-neutral-700 dark:text-light-gray' : 'bg-white dark:bg-dark-charcoal dark:text-light-gray' From e9a2b8f03a3b8e7f732abf50107934edb248641c Mon Sep 17 00:00:00 2001 From: Niharika Goulikar Date: Tue, 26 Nov 2024 12:16:21 +0000 Subject: [PATCH 053/354] Fixed the feedback issue --- application/api/user/routes.py | 24 +++++++++++-------- frontend/src/conversation/Conversation.tsx | 2 +- .../src/conversation/ConversationBubble.tsx | 20 +++++++++++++--- .../src/conversation/conversationHandlers.ts | 4 ++++ 4 files changed, 36 insertions(+), 14 deletions(-) diff --git a/application/api/user/routes.py b/application/api/user/routes.py index e305845d..0e4a9f4d 100644 --- a/application/api/user/routes.py +++ b/application/api/user/routes.py @@ -176,10 +176,12 @@ class SubmitFeedback(Resource): "FeedbackModel", { "question": fields.String( - required=True, description="The user question" + required=False, description="The user question" ), - "answer": fields.String(required=True, description="The AI answer"), + "answer": fields.String(required=False, description="The AI answer"), "feedback": fields.String(required=True, description="User feedback"), + "question_index":fields.Integer(required=True, description="The question number in that particular conversation"), + "conversation_id":fields.String(required=True, description="id of the particular conversation"), "api_key": fields.String(description="Optional API key"), }, ) @@ -189,23 +191,25 @@ class SubmitFeedback(Resource): ) def post(self): data = request.get_json() - required_fields = ["question", "answer", "feedback"] + required_fields = [ "feedback","conversation_id","question_index"] missing_fields = check_required_fields(data, required_fields) if missing_fields: return missing_fields - new_doc = { - "question": data["question"], - "answer": data["answer"], - "feedback": data["feedback"], - "timestamp": datetime.datetime.now(datetime.timezone.utc), - } if "api_key" in data: new_doc["api_key"] = data["api_key"] try: - feedback_collection.insert_one(new_doc) + conversations_collection.update_one( + {"_id": ObjectId(data["conversation_id"]), f"queries.{data["question_index"]}": {"$exists": True}}, + { + "$set": { + f"queries.{data["question_index"]}.feedback": data["feedback"] + } + } + ) + except Exception as err: return make_response(jsonify({"success": False, "error": str(err)}), 400) diff --git a/frontend/src/conversation/Conversation.tsx b/frontend/src/conversation/Conversation.tsx index ed69064a..41bc8d2f 100644 --- a/frontend/src/conversation/Conversation.tsx +++ b/frontend/src/conversation/Conversation.tsx @@ -111,7 +111,7 @@ export default function Conversation() { const handleFeedback = (query: Query, feedback: FEEDBACK, index: number) => { const prevFeedback = query.feedback; dispatch(updateQuery({ index, query: { feedback } })); - handleSendFeedback(query.prompt, query.response!, feedback).catch(() => + handleSendFeedback(query.prompt, query.response!, feedback,conversationId as string,index).catch(() => dispatch(updateQuery({ index, query: { feedback: prevFeedback } })), ); }; diff --git a/frontend/src/conversation/ConversationBubble.tsx b/frontend/src/conversation/ConversationBubble.tsx index 567b09e9..7fe1b003 100644 --- a/frontend/src/conversation/ConversationBubble.tsx +++ b/frontend/src/conversation/ConversationBubble.tsx @@ -429,6 +429,11 @@ const ConversationBubble = forwardRef< feedback === 'LIKE' || type !== 'ERROR' ? 'group-hover:lg:visible' : '' + } + ${ + feedback === 'DISLIKE' && type !== 'ERROR' + ? ' hidden' + : '' }`} >
@@ -445,11 +450,14 @@ const ConversationBubble = forwardRef< isLikeClicked || feedback === 'LIKE' ? 'fill-white-3000 stroke-purple-30 dark:fill-transparent' : 'fill-none stroke-gray-4000' - }`} + } `} onClick={() => { + if(feedback===undefined){ + console.log("liked") handleFeedback?.('LIKE'); setIsLikeClicked(true); setIsDislikeClicked(false); + } }} onMouseEnter={() => setIsLikeHovered(true)} onMouseLeave={() => setIsLikeHovered(false)} @@ -462,9 +470,13 @@ const ConversationBubble = forwardRef< !isDislikeClicked ? 'lg:invisible' : '' } ${ feedback === 'DISLIKE' || type !== 'ERROR' - ? 'group-hover:lg:visible' + ? ' group-hover:lg:visible' : '' - }`} + } ${ + feedback === 'LIKE' && type !== 'ERROR' + ? ' hidden' + : '' + } `} >
{ + if(feedback===undefined){ handleFeedback?.('DISLIKE'); setIsDislikeClicked(true); setIsLikeClicked(false); + } }} onMouseEnter={() => setIsDislikeHovered(true)} onMouseLeave={() => setIsDislikeHovered(false)} diff --git a/frontend/src/conversation/conversationHandlers.ts b/frontend/src/conversation/conversationHandlers.ts index be046bca..ea8ad6ea 100644 --- a/frontend/src/conversation/conversationHandlers.ts +++ b/frontend/src/conversation/conversationHandlers.ts @@ -202,12 +202,16 @@ export function handleSendFeedback( prompt: string, response: string, feedback: FEEDBACK, + conversation_id:string, + prompt_index:number ) { return conversationService .feedback({ question: prompt, answer: response, feedback: feedback, + conversation_id:conversation_id, + question_index:prompt_index }) .then((response) => { if (response.ok) { From faf031ce8037aac6abbbb35ad66447df98c5ff3b Mon Sep 17 00:00:00 2001 From: Niharika Goulikar Date: Tue, 26 Nov 2024 12:28:16 +0000 Subject: [PATCH 054/354] fixed linting issues --- application/api/user/routes.py | 4 --- .../src/conversation/ConversationBubble.tsx | 33 +++++++------------ 2 files changed, 12 insertions(+), 25 deletions(-) diff --git a/application/api/user/routes.py b/application/api/user/routes.py index 0e4a9f4d..96fc0e0f 100644 --- a/application/api/user/routes.py +++ b/application/api/user/routes.py @@ -196,10 +196,6 @@ class SubmitFeedback(Resource): if missing_fields: return missing_fields - - if "api_key" in data: - new_doc["api_key"] = data["api_key"] - try: conversations_collection.update_one( {"_id": ObjectId(data["conversation_id"]), f"queries.{data["question_index"]}": {"$exists": True}}, diff --git a/frontend/src/conversation/ConversationBubble.tsx b/frontend/src/conversation/ConversationBubble.tsx index 7fe1b003..03a3ab08 100644 --- a/frontend/src/conversation/ConversationBubble.tsx +++ b/frontend/src/conversation/ConversationBubble.tsx @@ -430,11 +430,7 @@ const ConversationBubble = forwardRef< ? 'group-hover:lg:visible' : '' } - ${ - feedback === 'DISLIKE' && type !== 'ERROR' - ? ' hidden' - : '' - }`} + ${feedback === 'DISLIKE' && type !== 'ERROR' ? 'hidden' : ''}`} >
{ - if(feedback===undefined){ - console.log("liked") - handleFeedback?.('LIKE'); - setIsLikeClicked(true); - setIsDislikeClicked(false); + if (feedback === undefined) { + handleFeedback?.('LIKE'); + setIsLikeClicked(true); + setIsDislikeClicked(false); } }} onMouseEnter={() => setIsLikeHovered(true)} @@ -470,13 +465,9 @@ const ConversationBubble = forwardRef< !isDislikeClicked ? 'lg:invisible' : '' } ${ feedback === 'DISLIKE' || type !== 'ERROR' - ? ' group-hover:lg:visible' + ? 'group-hover:lg:visible' : '' - } ${ - feedback === 'LIKE' && type !== 'ERROR' - ? ' hidden' - : '' - } `} + } ${feedback === 'LIKE' && type !== 'ERROR' ? ' hidden' : ''} `} >
{ - if(feedback===undefined){ - handleFeedback?.('DISLIKE'); - setIsDislikeClicked(true); - setIsLikeClicked(false); + if (feedback === undefined) { + handleFeedback?.('DISLIKE'); + setIsDislikeClicked(true); + setIsLikeClicked(false); } }} onMouseEnter={() => setIsDislikeHovered(true)} From 9d4aee5de2a9bbefc33b61f55da5d2a5cc6e11a0 Mon Sep 17 00:00:00 2001 From: Niharika Goulikar Date: Tue, 26 Nov 2024 13:05:50 +0000 Subject: [PATCH 055/354] fixed the python error --- application/api/user/routes.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/application/api/user/routes.py b/application/api/user/routes.py index 96fc0e0f..6eb57cc0 100644 --- a/application/api/user/routes.py +++ b/application/api/user/routes.py @@ -198,10 +198,10 @@ class SubmitFeedback(Resource): try: conversations_collection.update_one( - {"_id": ObjectId(data["conversation_id"]), f"queries.{data["question_index"]}": {"$exists": True}}, + {"_id": ObjectId(data["conversation_id"]), f"queries.{data['question_index']}": {"$exists": True}}, { "$set": { - f"queries.{data["question_index"]}.feedback": data["feedback"] + f"queries.{data['question_index']}.feedback": data["feedback"] } } ) From 0a7a313e5dd33e092b5dece90b66788491e9d52d Mon Sep 17 00:00:00 2001 From: ManishMadan2882 Date: Tue, 26 Nov 2024 18:57:11 +0530 Subject: [PATCH 056/354] (fix:conv) input touches viewport bottom in mobile --- frontend/src/conversation/Conversation.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/frontend/src/conversation/Conversation.tsx b/frontend/src/conversation/Conversation.tsx index ed69064a..7f06d6b1 100644 --- a/frontend/src/conversation/Conversation.tsx +++ b/frontend/src/conversation/Conversation.tsx @@ -15,7 +15,6 @@ import { useDarkTheme, useMediaQuery } from '../hooks'; import { ShareConversationModal } from '../modals/ShareConversationModal'; import { selectConversationId } from '../preferences/preferenceSlice'; import { AppDispatch } from '../store'; -import conversationService from '../api/services/conversationService'; import ConversationBubble from './ConversationBubble'; import { handleSendFeedback } from './conversationHandlers'; import { FEEDBACK, Query } from './conversationModels'; @@ -323,8 +322,8 @@ export default function Conversation() { )}
-
-
+
+
{status === 'loading' ? ( + alt={t('loading')} + /> ) : (
- handleQuestionSubmission()} - src={isDarkTheme ? SendDark : Send} - > + aria-label={t('send')} + className="flex items-center justify-center" + > + {t('send')} +
)}
diff --git a/frontend/src/locale/en.json b/frontend/src/locale/en.json index 070deba6..9f9cbb21 100644 --- a/frontend/src/locale/en.json +++ b/frontend/src/locale/en.json @@ -166,6 +166,8 @@ "copied": "Copied", "speak": "Speak", "answer": "Answer", + "send": "Send message", + "loading": "Loading response", "edit": { "placeholder": "Type the updated query..." }, diff --git a/frontend/src/preferences/PromptsModal.tsx b/frontend/src/preferences/PromptsModal.tsx index 3aa8c54c..3a1f9bfa 100644 --- a/frontend/src/preferences/PromptsModal.tsx +++ b/frontend/src/preferences/PromptsModal.tsx @@ -29,8 +29,9 @@ function AddPrompt({ setNewPromptName(''); setNewPromptContent(''); }} + aria-label="Close add prompt modal" > - + Close modal

@@ -40,7 +41,11 @@ function AddPrompt({ Add your custom prompt and save it to DocsGPT

+
+
@@ -104,8 +114,9 @@ function EditPrompt({ onClick={() => { setModalState('INACTIVE'); }} + aria-label="Close edit prompt modal" > - + Close modal

@@ -115,13 +126,17 @@ function EditPrompt({ Edit your custom prompt and save it to DocsGPT

+ setEditPromptName(e.target.value)} - > + />
Prompt Name @@ -132,10 +147,15 @@ function EditPrompt({ Prompt Text
+
diff --git a/frontend/src/settings/APIKeys.tsx b/frontend/src/settings/APIKeys.tsx index 6775ba87..038e4bbb 100644 --- a/frontend/src/settings/APIKeys.tsx +++ b/frontend/src/settings/APIKeys.tsx @@ -115,12 +115,20 @@ export default function APIKeys() {
{t('settings.documents.name')} +
{t('settings.documents.tokenUsage')} Date: Sun, 24 Nov 2024 17:05:36 -0500 Subject: [PATCH 051/354] add text center when no data --- frontend/src/settings/Documents.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/settings/Documents.tsx b/frontend/src/settings/Documents.tsx index 0fd82aa8..0987b5d7 100644 --- a/frontend/src/settings/Documents.tsx +++ b/frontend/src/settings/Documents.tsx @@ -230,7 +230,7 @@ const Documents: React.FC = ({
{t('settings.documents.noData')}
- - + - - + + @@ -146,7 +154,7 @@ export default function APIKeys() { diff --git a/frontend/src/settings/Logs.tsx b/frontend/src/settings/Logs.tsx index 0f452e97..f1ebfe72 100644 --- a/frontend/src/settings/Logs.tsx +++ b/frontend/src/settings/Logs.tsx @@ -67,9 +67,12 @@ export default function Logs() {
-

+

+ {loadingChatbots ? ( ) : ( diff --git a/frontend/src/settings/Tools.tsx b/frontend/src/settings/Tools.tsx index f58487a3..3dc31e35 100644 --- a/frontend/src/settings/Tools.tsx +++ b/frontend/src/settings/Tools.tsx @@ -1,4 +1,5 @@ import React from 'react'; +import { useTranslation } from 'react-i18next'; import userService from '../api/services/userService'; import CogwheelIcon from '../assets/cogwheel.svg'; @@ -18,6 +19,7 @@ export default function Tools() { React.useState('INACTIVE'); const [userTools, setUserTools] = React.useState([]); const [selectedTool, setSelectedTool] = React.useState(null); + const { t } = useTranslation(); const getUserTools = () => { userService @@ -70,12 +72,15 @@ export default function Tools() {
+ setSearchTerm(e.target.value)} /> @@ -86,7 +91,7 @@ export default function Tools() { setAddToolModalState('ACTIVE'); }} > - Add Tool + {t('settings.tools.addTool')}
@@ -98,10 +103,10 @@ export default function Tools() {
No tools found - No tools found + {t('settings.tools.noToolsFound')}
) : ( userTools @@ -119,15 +124,19 @@ export default function Tools() {
{`${tool.displayName} @@ -146,6 +155,11 @@ export default function Tools() { htmlFor={`toolToggle-${index}`} className="relative inline-block h-6 w-10 cursor-pointer rounded-full bg-gray-300 dark:bg-[#D2D5DA33]/20 transition [-webkit-tap-highlight-color:_transparent] has-[:checked]:bg-[#0C9D35CC] has-[:checked]:dark:bg-[#0C9D35CC]" > + + {t('settings.tools.toggleToolAria', { + toolName: tool.displayName, + })} +
Date: Mon, 13 Jan 2025 00:33:10 -0500 Subject: [PATCH 206/354] initial websocket impl --- application/tts/elevenlabs.py | 84 ++++++++++++++++++++++++++--------- 1 file changed, 64 insertions(+), 20 deletions(-) diff --git a/application/tts/elevenlabs.py b/application/tts/elevenlabs.py index e1b3db27..11b4f081 100644 --- a/application/tts/elevenlabs.py +++ b/application/tts/elevenlabs.py @@ -1,29 +1,73 @@ -from io import BytesIO +import asyncio +import websockets +import json import base64 +from io import BytesIO from application.tts.base import BaseTTS class ElevenlabsTTS(BaseTTS): - def __init__(self): - from elevenlabs.client import ElevenLabs - - self.client = ElevenLabs( - api_key="ELEVENLABS_API_KEY", - ) - + def __init__(self): + self.api_key = "ELEVENLABS_API_KEY" + self.model = "eleven_multilingual_v2" + self.voice = "Brian" def text_to_speech(self, text): - lang = "en" - audio = self.client.generate( - text=text, - model="eleven_multilingual_v2", - voice="Brian", - ) - audio_data = BytesIO() - for chunk in audio: - audio_data.write(chunk) - audio_bytes = audio_data.getvalue() - - # Encode to base64 + audio_bytes = asyncio.run(self._text_to_speech_websocket(text)) audio_base64 = base64.b64encode(audio_bytes).decode("utf-8") + lang = "en" return audio_base64, lang + + async def _text_to_speech_websocket(self, text): + uri = "wss://api.elevenlabs.io/v1/tts-stream" + headers = { + "xi-api-key": self.api_key, + "Accept": "audio/mpeg" + } + payload = { + "text": text, + "model_id": self.model, + "voice_settings": { + "voice_id": self.voice + }, + } + audio_data = BytesIO() + + async with websockets.connect(uri, extra_headers=headers) as websocket: + + await websocket.send(json.dumps(payload)) + + async for message in websocket: + if isinstance(message, bytes): + audio_data.write(message) + else: + print("Received a non-binary frame:", message) + + return audio_data.getvalue() + + +def test_elevenlabs_websocket(): + """ + Tests the ElevenlabsTTS text_to_speech method with a sample prompt. + Prints out the base64-encoded result and writes it to 'output_audio.mp3'. + """ + # Instantiate your TTS class + tts = ElevenlabsTTS() + + # Call the method with some sample text + audio_base64, lang = tts.text_to_speech("Hello from ElevenLabs WebSocket!") + + print(f"Received language: {lang}") + print(f"Base64 Audio (truncated): {audio_base64[:100]}...") + + # Optional: Save the audio to a local file for manual listening. + # We'll assume the audio is in MP3 format based on "Accept": "audio/mpeg". + audio_bytes = base64.b64decode(audio_base64) + with open("output_audio.mp3", "wb") as f: + f.write(audio_bytes) + + print("Saved audio to output_audio.mp3.") + + +if __name__ == "__main__": + test_elevenlabs_websocket() From 96ab01b0c1582d7b7ece5e1330e767327f9fa045 Mon Sep 17 00:00:00 2001 From: Ahmad Alghooneh Date: Mon, 13 Jan 2025 00:53:28 -0500 Subject: [PATCH 207/354] commited reqs --- application/requirements.txt | 3 ++- application/tts/elevenlabs.py | 15 +++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/application/requirements.txt b/application/requirements.txt index 72650c3e..01b4c59e 100644 --- a/application/requirements.txt +++ b/application/requirements.txt @@ -86,4 +86,5 @@ urllib3==2.3.0 vine==5.1.0 wcwidth==0.2.13 werkzeug==3.1.3 -yarl==1.18.3 \ No newline at end of file +yarl==1.18.3 +websockets==14.1 diff --git a/application/tts/elevenlabs.py b/application/tts/elevenlabs.py index 11b4f081..96fb1f43 100644 --- a/application/tts/elevenlabs.py +++ b/application/tts/elevenlabs.py @@ -3,12 +3,12 @@ import websockets import json import base64 from io import BytesIO -from application.tts.base import BaseTTS +from base import BaseTTS class ElevenlabsTTS(BaseTTS): def __init__(self): - self.api_key = "ELEVENLABS_API_KEY" + self.api_key = 'sk_19b72c883e8bdfcec2705be2d048f3830a40d2faa4b76b26' self.model = "eleven_multilingual_v2" self.voice = "Brian" @@ -19,21 +19,20 @@ class ElevenlabsTTS(BaseTTS): return audio_base64, lang async def _text_to_speech_websocket(self, text): - uri = "wss://api.elevenlabs.io/v1/tts-stream" - headers = { - "xi-api-key": self.api_key, - "Accept": "audio/mpeg" - } + uri = f"wss://api.elevenlabs.io/v1/text-to-speech/{self.voice}/stream-input?model_id={self.model}" + payload = { "text": text, "model_id": self.model, "voice_settings": { "voice_id": self.voice }, + "xi-api-key": self.api_key, + "Accept": "audio/mpeg" } audio_data = BytesIO() - async with websockets.connect(uri, extra_headers=headers) as websocket: + async with websockets.connect(uri) as websocket: await websocket.send(json.dumps(payload)) From 51225b18b2e520472d0f969464925dea8b5c4fc6 Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 13 Jan 2025 10:37:53 +0000 Subject: [PATCH 208/354] add google --- application/llm/google_ai.py | 41 ++++++++++++++++++++++-------------- application/tools/agent.py | 32 +++++++++++++++++++++++++++- 2 files changed, 56 insertions(+), 17 deletions(-) diff --git a/application/llm/google_ai.py b/application/llm/google_ai.py index 33ae0855..09f0d9f7 100644 --- a/application/llm/google_ai.py +++ b/application/llm/google_ai.py @@ -72,29 +72,38 @@ class GoogleLLM(BaseLLM): messages, stream=False, tools=None, + formatting="openai", **kwargs ): - import google.generativeai as genai - genai.configure(api_key=self.api_key) + from google import genai + from google.genai import types + client = genai.Client(api_key=self.api_key) + config = { } model = 'gemini-2.0-flash-exp' - - model = genai.GenerativeModel( - model_name=model, - generation_config=config, - system_instruction=messages[0]["content"], - tools=self._clean_tools_format(tools) + if formatting=="raw": + response = client.models.generate_content( + model=model, + contents=messages ) - chat_session = model.start_chat( - history=self._clean_messages_google(messages)[:-1] - ) - response = chat_session.send_message( - self._clean_messages_google(messages)[-1] - ) - logging.info(response) - return response.text + + else: + model = genai.GenerativeModel( + model_name=model, + generation_config=config, + system_instruction=messages[0]["content"], + tools=self._clean_tools_format(tools) + ) + chat_session = model.start_chat( + history=self._clean_messages_google(messages)[:-1] + ) + response = chat_session.send_message( + self._clean_messages_google(messages)[-1] + ) + logging.info(response) + return response.text def _raw_gen_stream( self, diff --git a/application/tools/agent.py b/application/tools/agent.py index d4077e45..a6aec2e9 100644 --- a/application/tools/agent.py +++ b/application/tools/agent.py @@ -1,4 +1,5 @@ import json +import logging from application.core.mongo_db import MongoDB from application.llm.llm_creator import LLMCreator @@ -79,6 +80,25 @@ class Agent: print(f"Executing tool: {action_name} with args: {call_args}") return tool.execute_action(action_name, **call_args), call_id + def _execute_tool_action_google(self, tools_dict, call): + call_args = json.loads(call.args) + tool_id = call.name.split("_")[-1] + action_name = call.name.rsplit("_", 1)[0] + + tool_data = tools_dict[tool_id] + action_data = next( + action for action in tool_data["actions"] if action["name"] == action_name + ) + + for param, details in action_data["parameters"]["properties"].items(): + if param not in call_args and "value" in details: + call_args[param] = details["value"] + + tm = ToolManager(config={}) + tool = tm.load_tool(tool_data["name"], tool_config=tool_data["config"]) + print(f"Executing tool: {action_name} with args: {call_args}") + return tool.execute_action(action_name, **call_args) + def _simple_tool_agent(self, messages): tools_dict = self._get_user_tools() self._prepare_tools(tools_dict) @@ -91,8 +111,18 @@ class Agent: if resp.message.content: yield resp.message.content return + # check if self.llm class is GoogleLLM + while self.llm.__class__.__name__ == "GoogleLLM" and resp.content.parts[0].function_call: + from google.genai import types - while resp.finish_reason == "tool_calls": + function_call_part = resp.candidates[0].content.parts[0] + tool_response = self._execute_tool_action_google(tools_dict, function_call_part.function_call) + function_response_part = types.Part.from_function_response( + name=function_call_part.function_call.name, + response=tool_response + ) + + while self.llm.__class__.__name__ == "OpenAILLM" and resp.finish_reason == "tool_calls": message = json.loads(resp.model_dump_json())["message"] keys_to_remove = {"audio", "function_call", "refusal"} filtered_data = { From 774cbbf47a69cf46c79712b5d68e71d35b12ed03 Mon Sep 17 00:00:00 2001 From: ManishMadan2882 Date: Mon, 13 Jan 2025 18:20:08 +0530 Subject: [PATCH 209/354] (fix/widget) centered the toolkit msg --- extensions/react-widget/publish.sh | 102 ++++++++++++------ .../react-widget/src/components/SearchBar.tsx | 10 +- 2 files changed, 80 insertions(+), 32 deletions(-) diff --git a/extensions/react-widget/publish.sh b/extensions/react-widget/publish.sh index c4545d85..129c4bcf 100755 --- a/extensions/react-widget/publish.sh +++ b/extensions/react-widget/publish.sh @@ -1,43 +1,85 @@ #!/bin/bash -## chmod +x publish.sh - to upgrade ownership set -e -cat package.json >> package_copy.json -cat package-lock.json >> package-lock_copy.json + +# Create backup of original files +cp package.json package_original.json +cp package-lock.json package-lock_original.json + +# Store the latest version after publishing +LATEST_VERSION="" + publish_package() { - PACKAGE_NAME=$1 - BUILD_COMMAND=$2 - # Update package name in package.json - jq --arg name "$PACKAGE_NAME" '.name=$name' package.json > temp.json && mv temp.json package.json + PACKAGE_NAME=$1 + BUILD_COMMAND=$2 + IS_REACT=$3 - # Remove 'target' key if the package name is 'docsgpt-react' - if [ "$PACKAGE_NAME" = "docsgpt-react" ]; then - jq 'del(.targets)' package.json > temp.json && mv temp.json package.json - fi + echo "Preparing to publish ${PACKAGE_NAME}..." + + # Restore original package.json state before each publish + cp package_original.json package.json + cp package-lock_original.json package-lock.json - if [ -d "dist" ]; then - echo "Deleting existing dist directory..." - rm -rf dist - fi + # Update package name in package.json + jq --arg name "$PACKAGE_NAME" '.name=$name' package.json > temp.json && mv temp.json package.json - npm version patch + # Handle targets based on package type + if [ "$IS_REACT" = "true" ]; then + echo "Removing targets for React library build..." + jq 'del(.targets)' package.json > temp.json && mv temp.json package.json + fi - npm run "$BUILD_COMMAND" + # Clean dist directory + if [ -d "dist" ]; then + echo "Cleaning dist directory..." + rm -rf dist + fi - # Publish to npm - npm publish - # Clean up - mv package_copy.json package.json - mv package-lock_copy.json package-lock.json - echo "Published ${PACKAGE_NAME}" + # update version and store it + LATEST_VERSION=$(npm version patch) + echo "New version: ${LATEST_VERSION}" + + # Build package + npm run "$BUILD_COMMAND" + + # Replace npm publish with npm pack for testing + npm publish + + echo "Successfully packaged ${PACKAGE_NAME}" + + # Log the bundle size + TARBALL="${PACKAGE_NAME}-${LATEST_VERSION#v}.tgz" + if [ -f "$TARBALL" ]; then + BUNDLE_SIZE=$(du -h "$TARBALL" | cut -f1) + echo "Bundle size for ${PACKAGE_NAME}: ${BUNDLE_SIZE}" + else + echo "Error: ${TARBALL} not found." + exit 1 + fi } -# Publish docsgpt package -publish_package "docsgpt" "build" +# First publish docsgpt (HTML bundle) +publish_package "docsgpt" "build" "false" -# Publish docsgpt-react package -publish_package "docsgpt-react" "build:react" +# Then publish docsgpt-react (React library) +publish_package "docsgpt-react" "build:react" "true" +# Restore original state but keep the updated version +cp package_original.json package.json +cp package-lock_original.json package-lock.json -rm -rf package_copy.json -rm -rf package-lock_copy.json -echo "---Process completed---" \ No newline at end of file +# Update the version in the final package.json +jq --arg version "${LATEST_VERSION#v}" '.version=$version' package.json > temp.json && mv temp.json package.json + +# Run npm install to update package-lock.json with the new version +npm install --package-lock-only + +# Cleanup backup files +rm -f package_original.json +rm -f package-lock_original.json +rm -f temp.json + +echo "---Process completed---" +echo "Final version in package.json: $(jq -r '.version' package.json)" +echo "Final version in package-lock.json: $(jq -r '.version' package-lock.json)" +echo "Generated test packages:" +ls *.tgz diff --git a/extensions/react-widget/src/components/SearchBar.tsx b/extensions/react-widget/src/components/SearchBar.tsx index c7344e43..c647991f 100644 --- a/extensions/react-widget/src/components/SearchBar.tsx +++ b/extensions/react-widget/src/components/SearchBar.tsx @@ -242,14 +242,20 @@ white-space: pre-wrap; const Toolkit = styled.kbd` position: absolute; right: 4px; - top: 4px; + top: 50%; + transform: translateY(-50%); background-color: ${(props) => props.theme.primary.bg}; color: ${(props) => props.theme.secondary.text}; font-weight: 600; font-size: 10px; - padding: 3px; + padding: 3px 6px; border: 1px solid ${(props) => props.theme.secondary.text}; border-radius: 4px; + display: flex; + align-items: center; + justify-content: center; + z-index: 1; + pointer-events: none; ` const Loader = styled.div` margin: 2rem auto; From 838525b452b3d766eee69c35595c309ce02241c0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Jan 2025 20:06:18 +0000 Subject: [PATCH 210/354] build(deps): bump tqdm from 4.66.5 to 4.67.1 in /application Bumps [tqdm](https://github.com/tqdm/tqdm) from 4.66.5 to 4.67.1. - [Release notes](https://github.com/tqdm/tqdm/releases) - [Commits](https://github.com/tqdm/tqdm/compare/v4.66.5...v4.67.1) --- updated-dependencies: - dependency-name: tqdm dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- application/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/requirements.txt b/application/requirements.txt index a54e0ddd..41d742eb 100644 --- a/application/requirements.txt +++ b/application/requirements.txt @@ -77,7 +77,7 @@ sentence-transformers==3.3.1 tiktoken==0.8.0 tokenizers==0.21.0 torch==2.5.1 -tqdm==4.66.5 +tqdm==4.67.1 transformers==4.47.1 typing-extensions==4.12.2 typing-inspect==0.9.0 From e0912f0cf0e4c49a88a264633e061c487758a943 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Jan 2025 20:06:20 +0000 Subject: [PATCH 211/354] build(deps): bump openapi3-parser from 1.1.18 to 1.1.19 in /application Bumps [openapi3-parser](https://github.com/manchenkoff/openapi3-parser) from 1.1.18 to 1.1.19. - [Release notes](https://github.com/manchenkoff/openapi3-parser/releases) - [Commits](https://github.com/manchenkoff/openapi3-parser/compare/v1.1.18...v1.1.19) --- updated-dependencies: - dependency-name: openapi3-parser dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- application/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/requirements.txt b/application/requirements.txt index a54e0ddd..5d6451cd 100644 --- a/application/requirements.txt +++ b/application/requirements.txt @@ -46,7 +46,7 @@ numpy==2.2.1 openai==1.59.5 openapi-schema-validator==0.6.2 openapi-spec-validator==0.6.0 -openapi3-parser==1.1.18 +openapi3-parser==1.1.19 orjson==3.10.14 packaging==24.1 pandas==2.2.3 From 1438fea76bd74f17ae94ddb47d8f75f8ec2e3231 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Jan 2025 20:06:27 +0000 Subject: [PATCH 212/354] build(deps): bump elastic-transport in /application Bumps [elastic-transport](https://github.com/elastic/elastic-transport-python) from 8.15.1 to 8.17.0. - [Release notes](https://github.com/elastic/elastic-transport-python/releases) - [Changelog](https://github.com/elastic/elastic-transport-python/blob/v8.17.0/CHANGELOG.md) - [Commits](https://github.com/elastic/elastic-transport-python/compare/v8.15.1...v8.17.0) --- updated-dependencies: - dependency-name: elastic-transport dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- application/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/requirements.txt b/application/requirements.txt index a54e0ddd..d750b9aa 100644 --- a/application/requirements.txt +++ b/application/requirements.txt @@ -6,7 +6,7 @@ dataclasses-json==0.6.7 docx2txt==0.8 duckduckgo-search==6.3.0 ebooklib==0.18 -elastic-transport==8.15.1 +elastic-transport==8.17.0 elasticsearch==8.17.0 escodegen==1.0.11 esprima==4.0.1 From 7369b02bf40bd4661ff41e19c53a1e113b012d72 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Jan 2025 20:06:32 +0000 Subject: [PATCH 213/354] build(deps): bump langsmith from 0.2.6 to 0.2.10 in /application Bumps [langsmith](https://github.com/langchain-ai/langsmith-sdk) from 0.2.6 to 0.2.10. - [Release notes](https://github.com/langchain-ai/langsmith-sdk/releases) - [Commits](https://github.com/langchain-ai/langsmith-sdk/compare/v0.2.6...v0.2.10) --- updated-dependencies: - dependency-name: langsmith dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- application/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/requirements.txt b/application/requirements.txt index a54e0ddd..499d6251 100644 --- a/application/requirements.txt +++ b/application/requirements.txt @@ -33,7 +33,7 @@ langchain-community==0.3.14 langchain-core==0.3.29 langchain-openai==0.3.0 langchain-text-splitters==0.3.5 -langsmith==0.2.6 +langsmith==0.2.10 lazy-object-proxy==1.10.0 lxml==5.3.0 markupsafe==3.0.2 From 5aea46c214cc584c300f947dfcdea13db1e87854 Mon Sep 17 00:00:00 2001 From: ManishMadan2882 Date: Tue, 14 Jan 2025 15:49:01 +0530 Subject: [PATCH 214/354] (feat:settings) locale lang for analytics and logs --- frontend/src/locale/en.json | 22 ++++++++++- frontend/src/settings/Analytics.tsx | 57 ++++++++++++++++++----------- frontend/src/settings/Logs.tsx | 14 ++++--- 3 files changed, 65 insertions(+), 28 deletions(-) diff --git a/frontend/src/locale/en.json b/frontend/src/locale/en.json index c6ae8023..7be4b568 100644 --- a/frontend/src/locale/en.json +++ b/frontend/src/locale/en.json @@ -69,10 +69,28 @@ "noData": "No existing Chatbots" }, "analytics": { - "label": "Analytics" + "label": "Analytics", + "filterByChatbot": "Filter by chatbot", + "selectChatbot": "Select chatbot", + "filterOptions": { + "hour": "Hour", + "last24Hours": "24 Hours", + "last7Days": "7 Days", + "last15Days": "15 Days", + "last30Days": "30 Days" + }, + "messages": "Messages", + "tokenUsage": "Token Usage", + "feedback": "Feedback", + "filterPlaceholder": "Filter", + "none": "None" }, "logs": { - "label": "Logs" + "label": "Logs", + "filterByChatbot": "Filter by chatbot", + "selectChatbot": "Select chatbot", + "none": "None", + "tableHeader": "API generated / chatbot conversations" }, "tools": { "label": "Tools" diff --git a/frontend/src/settings/Analytics.tsx b/frontend/src/settings/Analytics.tsx index 8baad361..8f3c45bb 100644 --- a/frontend/src/settings/Analytics.tsx +++ b/frontend/src/settings/Analytics.tsx @@ -1,4 +1,5 @@ import React, { useState, useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; import { BarElement, CategoryScale, @@ -28,15 +29,29 @@ ChartJS.register( Legend, ); -const filterOptions = [ - { label: 'Hour', value: 'last_hour' }, - { label: '24 Hours', value: 'last_24_hour' }, - { label: '7 Days', value: 'last_7_days' }, - { label: '15 Days', value: 'last_15_days' }, - { label: '30 Days', value: 'last_30_days' }, -]; - export default function Analytics() { + const { t } = useTranslation(); + + const filterOptions = [ + { label: t('settings.analytics.filterOptions.hour'), value: 'last_hour' }, + { + label: t('settings.analytics.filterOptions.last24Hours'), + value: 'last_24_hour', + }, + { + label: t('settings.analytics.filterOptions.last7Days'), + value: 'last_7_days', + }, + { + label: t('settings.analytics.filterOptions.last15Days'), + value: 'last_15_days', + }, + { + label: t('settings.analytics.filterOptions.last30Days'), + value: 'last_30_days', + }, + ]; + const [messagesData, setMessagesData] = useState

- Filter by chatbot + {t('settings.analytics.filterByChatbot')}

{ setSelectedChatbot( chatbots.find((item) => item.id === chatbot.value), @@ -199,12 +214,12 @@ export default function Analytics() {

- Messages + {t('settings.analytics.messages')}

- Token Usage + {t('settings.analytics.tokenUsage')}

- Feedback + {t('settings.analytics.feedback')}

item.positive, ), backgroundColor: '#7D54D1', }, { - label: 'Negative Feedback', + label: t('settings.analytics.negativeFeedback'), data: Object.values(feedbackData || {}).map( (item) => item.negative, ), diff --git a/frontend/src/settings/Logs.tsx b/frontend/src/settings/Logs.tsx index 1e248d46..df406122 100644 --- a/frontend/src/settings/Logs.tsx +++ b/frontend/src/settings/Logs.tsx @@ -1,4 +1,5 @@ import React, { useState, useEffect, useRef, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; import userService from '../api/services/userService'; import ChevronRight from '../assets/chevron-right.svg'; @@ -8,6 +9,7 @@ import { APIKeyData, LogData } from './types'; import CoppyButton from '../components/CopyButton'; export default function Logs() { + const { t } = useTranslation(); const [chatbots, setChatbots] = useState([]); const [selectedChatbot, setSelectedChatbot] = useState(); const [logs, setLogs] = useState([]); @@ -66,7 +68,7 @@ export default function Logs() {

- Filter by chatbot + {t('settings.logs.filterByChatbot')}

{loadingChatbots ? ( @@ -78,9 +80,9 @@ export default function Logs() { label: chatbot.name, value: chatbot.id, })), - { label: 'None', value: '' }, + { label: t('settings.logs.none'), value: '' }, ]} - placeholder="Select chatbot" + placeholder={t('settings.logs.selectChatbot')} onSelect={(chatbot: { label: string; value: string }) => { setSelectedChatbot( chatbots.find((item) => item.id === chatbot.value), @@ -120,6 +122,7 @@ type LogsTableProps = { }; function LogsTable({ logs, setPage }: LogsTableProps) { + const { t } = useTranslation(); const observerRef = useRef(); const firstObserver = useCallback((node: HTMLDivElement) => { if (observerRef.current) { @@ -134,7 +137,7 @@ function LogsTable({ logs, setPage }: LogsTableProps) {

- API generated / chatbot conversations + {t('settings.logs.tableHeader')}

chevron-right From cbcb717aee2e05ff37a49bbe08233e52da5d4324 Mon Sep 17 00:00:00 2001 From: ManishMadan2882 Date: Tue, 14 Jan 2025 16:52:35 +0530 Subject: [PATCH 215/354] (feat:locals) sync with en.json --- frontend/src/locale/es.json | 27 ++- frontend/src/locale/jp.json | 27 ++- frontend/src/locale/ru.json | 295 +++++++++++++++------------- frontend/src/locale/zh-TW.json | 29 ++- frontend/src/locale/zh.json | 27 ++- frontend/src/settings/Documents.tsx | 2 +- 6 files changed, 256 insertions(+), 151 deletions(-) diff --git a/frontend/src/locale/es.json b/frontend/src/locale/es.json index 79aa1b3b..d2c087db 100644 --- a/frontend/src/locale/es.json +++ b/frontend/src/locale/es.json @@ -69,10 +69,31 @@ "noData": "No hay chatbots existentes" }, "analytics": { - "label": "Analítica" + "label": "Analítica", + "filterByChatbot": "Filtrar por chatbot", + "selectChatbot": "Seleccionar chatbot", + "filterOptions": { + "hour": "Hora", + "last24Hours": "24 Horas", + "last7Days": "7 Días", + "last15Days": "15 Días", + "last30Days": "30 Días" + }, + "messages": "Mensajes", + "tokenUsage": "Uso de Tokens", + "feedback": "Retroalimentación", + "filterPlaceholder": "Filtrar", + "none": "Ninguno" }, "logs": { - "label": "Registros" + "label": "Registros", + "filterByChatbot": "Filtrar por chatbot", + "selectChatbot": "Seleccionar chatbot", + "none": "Ninguno", + "tableHeader": "Conversaciones generadas por API / chatbot" + }, + "tools": { + "label": "Herramientas" } }, "modals": { @@ -128,7 +149,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 5f572ea4..71aa1e15 100644 --- a/frontend/src/locale/jp.json +++ b/frontend/src/locale/jp.json @@ -69,10 +69,31 @@ "noData": "既存のチャットボットはありません" }, "analytics": { - "label": "分析" + "label": "分析", + "filterByChatbot": "チャットボットでフィルター", + "selectChatbot": "チャットボットを選択", + "filterOptions": { + "hour": "時間", + "last24Hours": "24時間", + "last7Days": "7日間", + "last15Days": "15日間", + "last30Days": "30日間" + }, + "messages": "メッセージ", + "tokenUsage": "トークン使用量", + "feedback": "フィードバック", + "filterPlaceholder": "フィルター", + "none": "なし" }, "logs": { - "label": "ログ" + "label": "ログ", + "filterByChatbot": "チャットボットでフィルター", + "selectChatbot": "チャットボットを選択", + "none": "なし", + "tableHeader": "API生成/チャットボットの会話" + }, + "tools": { + "label": "ツール" } }, "modals": { @@ -86,7 +107,7 @@ "start": "チャットを開始する", "name": "名前", "choose": "ファイルを選択", - "info": ".pdf, .txt, .rst, .docx, .md, .json, .pptx, .zipファイルを25MBまでアップロードしてください", + "info": "25MBまでの.pdf, .txt, .rst, .csv, .xlsx, .docx, .md, .html, .epub, .json, .pptx, .zipファイルをアップロードしてください", "uploadedFiles": "アップロードされたファイル", "cancel": "キャンセル", "train": "トレーニング", diff --git a/frontend/src/locale/ru.json b/frontend/src/locale/ru.json index d23092a1..c5e3deb8 100644 --- a/frontend/src/locale/ru.json +++ b/frontend/src/locale/ru.json @@ -1,142 +1,163 @@ { - "language": "Русский", - "chat": "Чат", - "chats": "Чаты", - "newChat": "Новый чат", - "myPlan": "Мой план", - "about": "О", - "inputPlaceholder": "Введите свое сообщение здесь...", - "tagline": "DocsGPT использует GenAI, пожалуйста, проверьте важную информацию, используя источники.", - "sourceDocs": "Источник", - "none": "Нет", - "cancel": "Отмена", - "demo": [ - { - "header": "Узнайте о DocsGPT", - "query": "Что такое DocsGPT?" - }, - { - "header": "Обобщить документацию", - "query": "Обобщить текущий контекст" - }, - { - "header": "Написать код", - "query": "Написать код для запроса API к /api/answer" - }, - { - "header": "Помощь в обучении", - "query": "Написать потенциальные вопросы для контекста" - } - ], - "settings": { - "label": "Настройки", - "general": { - "label": "Общие", - "selectTheme": "Выбрать тему", - "light": "Светлая", - "dark": "Темная", - "selectLanguage": "Выбрать язык", - "chunks": "Обработанные фрагменты на запрос", - "prompt": "Активная подсказка", - "deleteAllLabel": "Удалить все беседы", - "deleteAllBtn": "Удалить все", - "addNew": "Добавить новый", - "convHistory": "История разговоров", - "none": "Нет", - "low": "Низкий", - "medium": "Средний", - "high": "Высокий", - "unlimited": "Без ограничений", - "default": "по умолчанию" - }, - "documents": { - "label": "Документы", - "name": "Название документа", - "date": "Дата вектора", - "type": "Тип", - "tokenUsage": "Использование токена", - "noData": "Нет существующих документов" - }, - "apiKeys": { - "label": "Чат-боты", - "name": "Название", - "key": "Ключ API", - "sourceDoc": "Исходный документ", - "createNew": "Создать новый", - "noData": "Нет существующих чат-ботов" - }, - "analytics": { - "label": "Analytics" - }, - "logs": { - "label": "Журналы" - } + "language": "Русский", + "chat": "Чат", + "chats": "Чаты", + "newChat": "Новый чат", + "myPlan": "Мой план", + "about": "О", + "inputPlaceholder": "Введите свое сообщение здесь...", + "tagline": "DocsGPT использует GenAI, пожалуйста, проверьте важную информацию, используя источники.", + "sourceDocs": "Источник", + "none": "Нет", + "cancel": "Отмена", + "demo": [ + { + "header": "Узнайте о DocsGPT", + "query": "Что такое DocsGPT?" }, - "modals": { - "uploadDoc": { - "label": "Загрузить новую документацию", - "select": "Выберите способ загрузки документа в DocsGPT", - "file": "Загрузить с устройства", - "back": "Назад", - "wait": "Пожалуйста, подождите ...", - "remote": "Собрать с веб-сайта", - "start": "Начать чат", - "name": "Имя", - "choose": "Выбрать файлы", - "info": "Загрузите .pdf, .txt, .rst, .csv, .xlsx, .docx, .md, .zip с ограничением до 25 МБ", - "uploadedFiles": "Загруженные файлы", - "cancel": "Отмена", - "train": "Обучение", - "link": "Ссылка", - "urlLink": "URL-ссылка", - "repoUrl": "URL-адрес репозитория", - "reddit": { - "id": "ID клиента", - "secret": "Секрет клиента", - "agent": "Агент пользователя", - "searchQueries": "Поисковые запросы", - "numberOfPosts": "Количество сообщений" - }, - "drag": { - "title": "Загрузите исходный файл", - "description": "Перетащите сюда свой файл, чтобы добавить его в качестве источника." - } - }, - "createAPIKey": { - "label": "Создать новый ключ API", - "apiKeyName": "Имя ключа API", - "chunks": "Обработано фрагментов на запрос", - "prompt": "Выбрать активный запрос", - "sourceDoc": "Исходный документ", - "create": "Создать" - }, - "saveKey": { - "note": "Пожалуйста, сохраните свой ключ", - "disclaimer": "Это единственный раз, когда будет показан ваш ключ.", - "copy": "Копировать", - "copied": "Скопировано", - "confirm": "Я сохранил ключ" - }, - "deleteConv": { - "confirm": "Вы уверены, что хотите удалить все разговоры?", - "delete": "Удалить" - }, - "shareConv": { - "label": "Создать публичную страницу для общего доступа", - "note": "Исходный документ, личная информация и дальнейший разговор останутся конфиденциальными", - "create": "Создать", - "option": "Разрешить пользователям запрашивать дальнейшие действия" - } + { + "header": "Обобщить документацию", + "query": "Обобщить текущий контекст" }, - "sharedConv": { - "subtitle": "Создано с помощью", - "button": "Начать работу с DocsGPT", - "meta": "DocsGPT использует GenAI, пожалуйста, проверьте важную информацию с помощью источников." + { + "header": "Написать код", + "query": "Написать код для запроса API к /api/answer" }, - "convTile": { - "share": "Поделиться", - "delete": "Удалить", - "rename": "Переименовать", - "deleteWarning": "Вы уверены, что хотите удалить этот разговор?" + { + "header": "Помощь в обучении", + "query": "Написать потенциальные вопросы для контекста" } -} \ No newline at end of file + ], + "settings": { + "label": "Настройки", + "general": { + "label": "Общие", + "selectTheme": "Выбрать тему", + "light": "Светлая", + "dark": "Темная", + "selectLanguage": "Выбрать язык", + "chunks": "Обработанные фрагменты на запрос", + "prompt": "Активная подсказка", + "deleteAllLabel": "Удалить все беседы", + "deleteAllBtn": "Удалить все", + "addNew": "Добавить новый", + "convHistory": "История разговоров", + "none": "Нет", + "low": "Низкий", + "medium": "Средний", + "high": "Высокий", + "unlimited": "Без ограничений", + "default": "по умолчанию" + }, + "documents": { + "label": "Документы", + "name": "Название документа", + "date": "Дата вектора", + "type": "Тип", + "tokenUsage": "Использование токена", + "noData": "Нет существующих документов" + }, + "apiKeys": { + "label": "Чат-боты", + "name": "Название", + "key": "Ключ API", + "sourceDoc": "Исходный документ", + "createNew": "Создать новый", + "noData": "Нет существующих чат-ботов" + }, + "analytics": { + "label": "Аналитика", + "filterByChatbot": "Фильтровать по чат-боту", + "selectChatbot": "Выбрать чат-бота", + "filterOptions": { + "hour": "Час", + "last24Hours": "24 часа", + "last7Days": "7 дней", + "last15Days": "15 дней", + "last30Days": "30 дней" + }, + "messages": "Сообщения", + "tokenUsage": "Использование токенов", + "feedback": "Обратная связь", + "filterPlaceholder": "Фильтр", + "none": "Нет" + }, + "logs": { + "label": "Журналы", + "filterByChatbot": "Фильтровать по чат-боту", + "selectChatbot": "Выбрать чат-бота", + "none": "Нет", + "tableHeader": "Сгенерированные API / разговоры с чат-ботом" + }, + "tools": { + "label": "Инструменты" + } + }, + "modals": { + "uploadDoc": { + "label": "Загрузить новую документацию", + "select": "Выберите способ загрузки документа в DocsGPT", + "file": "Загрузить с устройства", + "back": "Назад", + "wait": "Пожалуйста, подождите ...", + "remote": "Собрать с веб-сайта", + "start": "Начать чат", + "name": "Имя", + "choose": "Выбрать файлы", + "info": "Загрузите .pdf, .txt, .rst, .csv, .xlsx, .docx, .md, .html, .epub, .json, .pptx, .zip с ограничением до 25 МБ", + "uploadedFiles": "Загруженные файлы", + "cancel": "Отмена", + "train": "Обучение", + "link": "Ссылка", + "urlLink": "URL-ссылка", + "repoUrl": "URL-адрес репозитория", + "reddit": { + "id": "ID клиента", + "secret": "Секрет клиента", + "agent": "Агент пользователя", + "searchQueries": "Поисковые запросы", + "numberOfPosts": "Количество постов" + }, + "drag": { + "title": "Загрузить исходный файл", + "description": "Перетащите файл сюда, чтобы добавить его как источник" + } + }, + "createAPIKey": { + "label": "Создать новый ключ API", + "apiKeyName": "Название ключа API", + "chunks": "Обработанные фрагменты на запрос", + "prompt": "Выбрать активный запрос", + "sourceDoc": "Исходный документ", + "create": "Создать" + }, + "saveKey": { + "note": "Пожалуйста, сохраните свой ключ", + "disclaimer": "Это единственный раз, когда будет показан ваш ключ", + "copy": "Копировать", + "copied": "Скопировано", + "confirm": "Я сохранил ключ" + }, + "deleteConv": { + "confirm": "Вы уверены, что хотите удалить все разговоры?", + "delete": "Удалить" + }, + "shareConv": { + "label": "Создать публичную страницу для общего доступа", + "note": "Исходный документ, личная информация и дальнейший разговор останутся конфиденциальными", + "create": "Создать", + "option": "Разрешить пользователям делать дополнительные запросы" + } + }, + "sharedConv": { + "subtitle": "Создано с помощью", + "button": "Начать работу с DocsGPT", + "meta": "DocsGPT использует GenAI, пожалуйста, проверяйте важную информацию, используя источники" + }, + "convTile": { + "share": "Поделиться", + "delete": "Удалить", + "rename": "Переименовать", + "deleteWarning": "Вы уверены, что хотите удалить этот разговор?" + } +} diff --git a/frontend/src/locale/zh-TW.json b/frontend/src/locale/zh-TW.json index d5f633d2..5e97d883 100644 --- a/frontend/src/locale/zh-TW.json +++ b/frontend/src/locale/zh-TW.json @@ -10,8 +10,8 @@ "sourceDocs": "原始文件", "none": "無", "cancel": "取消", - "help": "聯繫支援", - "emailUs": "寄送電子郵件給我們", + "help": "幫助", + "emailUs": "給我們發電郵", "documentation": "文件", "demo": [ { @@ -67,10 +67,31 @@ "createNew": "新增" }, "analytics": { - "label": "分析" + "label": "分析", + "filterByChatbot": "按聊天機器人篩選", + "selectChatbot": "選擇聊天機器人", + "filterOptions": { + "hour": "小時", + "last24Hours": "24 小時", + "last7Days": "7 天", + "last15Days": "15 天", + "last30Days": "30 天" + }, + "messages": "訊息", + "tokenUsage": "Token 使用量", + "feedback": "回饋", + "filterPlaceholder": "篩選", + "none": "無" }, "logs": { - "label": "日誌" + "label": "日誌", + "filterByChatbot": "按聊天機器人篩選", + "selectChatbot": "選擇聊天機器人", + "none": "無", + "tableHeader": "API 生成 / 聊天機器人會話" + }, + "tools": { + "label": "工具" } }, "modals": { diff --git a/frontend/src/locale/zh.json b/frontend/src/locale/zh.json index 1a0d1f94..afa15f7b 100644 --- a/frontend/src/locale/zh.json +++ b/frontend/src/locale/zh.json @@ -10,7 +10,7 @@ "sourceDocs": "源", "none": "无", "cancel": "取消", - "help": "联系支持", + "help": "帮助", "emailUs": "给我们发邮件", "documentation": "文档", "demo": [ @@ -69,10 +69,31 @@ "noData": "没有现有的聊天机器人" }, "analytics": { - "label": "分析" + "label": "分析", + "filterByChatbot": "按聊天机器人筛选", + "selectChatbot": "选择聊天机器人", + "filterOptions": { + "hour": "小时", + "last24Hours": "24 小时", + "last7Days": "7 天", + "last15Days": "15 天", + "last30Days": "30 天" + }, + "messages": "消息", + "tokenUsage": "令牌使用", + "feedback": "反馈", + "filterPlaceholder": "筛选", + "none": "无" }, "logs": { - "label": "日志" + "label": "日志", + "filterByChatbot": "按聊天机器人筛选", + "selectChatbot": "选择聊天机器人", + "none": "无", + "tableHeader": "API 生成 / 聊天机器人会话" + }, + "tools": { + "label": "工具" } }, "modals": { diff --git a/frontend/src/settings/Documents.tsx b/frontend/src/settings/Documents.tsx index 8e68f226..52a63351 100644 --- a/frontend/src/settings/Documents.tsx +++ b/frontend/src/settings/Documents.tsx @@ -181,7 +181,7 @@ const Documents: React.FC = ({ {loading ? ( ) : ( -
+
{t('settings.apiKeys.name')} + + {t('settings.apiKeys.name')} + {t('settings.apiKeys.sourceDoc')} {t('settings.apiKeys.key')} + {t('settings.apiKeys.key')} +
Delete handleDeleteKey(element.id)} diff --git a/frontend/src/settings/Analytics.tsx b/frontend/src/settings/Analytics.tsx index 8baad361..ccb5ffcb 100644 --- a/frontend/src/settings/Analytics.tsx +++ b/frontend/src/settings/Analytics.tsx @@ -164,7 +164,7 @@ export default function Analytics() {
-

+

Filter by chatbot

diff --git a/frontend/src/settings/General.tsx b/frontend/src/settings/General.tsx index 227b0a6e..216ec1e0 100644 --- a/frontend/src/settings/General.tsx +++ b/frontend/src/settings/General.tsx @@ -81,9 +81,9 @@ export default function General() { return (
-

+

+
-

+

+ @@ -115,9 +115,9 @@ export default function General() { />
-

+

+
-

+

+ ({ value: value, @@ -163,16 +163,14 @@ export default function General() { />
-

+

+
diff --git a/frontend/src/settings/Prompts.tsx b/frontend/src/settings/Prompts.tsx index 6e1810e5..611b0b90 100644 --- a/frontend/src/settings/Prompts.tsx +++ b/frontend/src/settings/Prompts.tsx @@ -168,7 +168,7 @@ export default function Prompts({ />
- {' '} + {t('settings.documents.actions')}
From 9e6f970bc400bcec3d8ffb191c6d90927f982920 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 14 Jan 2025 20:35:08 +0000 Subject: [PATCH 216/354] build(deps): bump flask from 3.0.3 to 3.1.0 in /application Bumps [flask](https://github.com/pallets/flask) from 3.0.3 to 3.1.0. - [Release notes](https://github.com/pallets/flask/releases) - [Changelog](https://github.com/pallets/flask/blob/main/CHANGES.rst) - [Commits](https://github.com/pallets/flask/compare/3.0.3...3.1.0) --- updated-dependencies: - dependency-name: flask dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- application/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/requirements.txt b/application/requirements.txt index df50d9d6..e2b3c129 100644 --- a/application/requirements.txt +++ b/application/requirements.txt @@ -11,7 +11,7 @@ elasticsearch==8.17.0 escodegen==1.0.11 esprima==4.0.1 esutils==1.0.1 -Flask==3.0.3 +Flask==3.1.0 faiss-cpu==1.9.0.post1 flask-restx==1.3.0 gTTS==2.5.4 From 850b79f459e8c722562bbce8474619f9ca224360 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 14 Jan 2025 20:35:19 +0000 Subject: [PATCH 217/354] build(deps): bump transformers from 4.47.1 to 4.48.0 in /application Bumps [transformers](https://github.com/huggingface/transformers) from 4.47.1 to 4.48.0. - [Release notes](https://github.com/huggingface/transformers/releases) - [Commits](https://github.com/huggingface/transformers/compare/v4.47.1...v4.48.0) --- updated-dependencies: - dependency-name: transformers dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- application/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/requirements.txt b/application/requirements.txt index df50d9d6..5b1a1404 100644 --- a/application/requirements.txt +++ b/application/requirements.txt @@ -78,7 +78,7 @@ tiktoken==0.8.0 tokenizers==0.21.0 torch==2.5.1 tqdm==4.67.1 -transformers==4.47.1 +transformers==4.48.0 typing-extensions==4.12.2 typing-inspect==0.9.0 tzdata==2024.2 From 13fcbe3e74dad4b6f2b37bc093f022d941be9d0b Mon Sep 17 00:00:00 2001 From: Pavel Date: Wed, 15 Jan 2025 01:08:09 +0300 Subject: [PATCH 218/354] scraper with markdownify --- application/api/user/routes.py | 2 +- application/parser/remote/crawler_loader.py | 41 +++--- application/parser/remote/crawler_markdown.py | 139 ++++++++++++++++++ application/requirements.txt | 4 +- application/worker.py | 82 ++++++----- 5 files changed, 211 insertions(+), 57 deletions(-) create mode 100644 application/parser/remote/crawler_markdown.py diff --git a/application/api/user/routes.py b/application/api/user/routes.py index 4bcbd719..e516bb71 100644 --- a/application/api/user/routes.py +++ b/application/api/user/routes.py @@ -2105,4 +2105,4 @@ class DeleteTool(Resource): except Exception as err: return {"success": False, "error": str(err)}, 400 - return {"success": True}, 200 + return {"success": True}, 200 \ No newline at end of file diff --git a/application/parser/remote/crawler_loader.py b/application/parser/remote/crawler_loader.py index 76325ae6..c2da230b 100644 --- a/application/parser/remote/crawler_loader.py +++ b/application/parser/remote/crawler_loader.py @@ -2,16 +2,16 @@ import requests from urllib.parse import urlparse, urljoin from bs4 import BeautifulSoup from application.parser.remote.base import BaseRemote +from application.parser.schema.base import Document +from langchain_community.document_loaders import WebBaseLoader class CrawlerLoader(BaseRemote): def __init__(self, limit=10): - from langchain_community.document_loaders import WebBaseLoader self.loader = WebBaseLoader # Initialize the document loader self.limit = limit # Set the limit for the number of pages to scrape def load_data(self, inputs): url = inputs - # Check if the input is a list and if it is, use the first element if isinstance(url, list) and url: url = url[0] @@ -19,24 +19,29 @@ class CrawlerLoader(BaseRemote): if not urlparse(url).scheme: url = "http://" + url - visited_urls = set() # Keep track of URLs that have been visited - base_url = urlparse(url).scheme + "://" + urlparse(url).hostname # Extract the base URL - urls_to_visit = [url] # List of URLs to be visited, starting with the initial URL - loaded_content = [] # Store the loaded content from each URL + visited_urls = set() + base_url = urlparse(url).scheme + "://" + urlparse(url).hostname + urls_to_visit = [url] + loaded_content = [] - # Continue crawling until there are no more URLs to visit while urls_to_visit: - current_url = urls_to_visit.pop(0) # Get the next URL to visit - visited_urls.add(current_url) # Mark the URL as visited + current_url = urls_to_visit.pop(0) + visited_urls.add(current_url) - # Try to load and process the content from the current URL try: - response = requests.get(current_url) # Fetch the content of the current URL - response.raise_for_status() # Raise an exception for HTTP errors - loader = self.loader([current_url]) # Initialize the document loader for the current URL - loaded_content.extend(loader.load()) # Load the content and add it to the loaded_content list + response = requests.get(current_url) + response.raise_for_status() + loader = self.loader([current_url]) + docs = loader.load() + # Convert the loaded documents to your Document schema + for doc in docs: + loaded_content.append( + Document( + doc.page_content, + extra_info=doc.metadata + ) + ) except Exception as e: - # Print an error message if loading or processing fails and continue with the next URL print(f"Error processing URL {current_url}: {e}") continue @@ -45,15 +50,15 @@ class CrawlerLoader(BaseRemote): all_links = [ urljoin(current_url, a['href']) for a in soup.find_all('a', href=True) - if base_url in urljoin(current_url, a['href']) # Ensure links are from the same domain + if base_url in urljoin(current_url, a['href']) ] # Add new links to the list of URLs to visit if they haven't been visited yet urls_to_visit.extend([link for link in all_links if link not in visited_urls]) - urls_to_visit = list(set(urls_to_visit)) # Remove duplicate URLs + urls_to_visit = list(set(urls_to_visit)) # Stop crawling if the limit of pages to scrape is reached if self.limit is not None and len(visited_urls) >= self.limit: break - return loaded_content # Return the loaded content from all visited URLs + return loaded_content \ No newline at end of file diff --git a/application/parser/remote/crawler_markdown.py b/application/parser/remote/crawler_markdown.py new file mode 100644 index 00000000..3d199332 --- /dev/null +++ b/application/parser/remote/crawler_markdown.py @@ -0,0 +1,139 @@ +import requests +from urllib.parse import urlparse, urljoin +from bs4 import BeautifulSoup +from application.parser.remote.base import BaseRemote +import re +from markdownify import markdownify +from application.parser.schema.base import Document +import tldextract + +class CrawlerLoader(BaseRemote): + def __init__(self, limit=10, allow_subdomains=False): + """ + Given a URL crawl web pages up to `self.limit`, + convert HTML content to Markdown, and returning a list of Document objects. + + :param limit: The maximum number of pages to crawl. + :param allow_subdomains: If True, crawl pages on subdomains of the base domain. + """ + self.limit = limit + self.allow_subdomains = allow_subdomains + self.session = requests.Session() + + def load_data(self, inputs): + url = inputs + if isinstance(url, list) and url: + url = url[0] + + # Ensure the URL has a scheme (if not, default to http) + if not urlparse(url).scheme: + url = "http://" + url + + # Keep track of visited URLs to avoid revisiting the same page + visited_urls = set() + + # Determine the base domain for link filtering using tldextract + base_domain = self._get_base_domain(url) + urls_to_visit = {url} + documents = [] + + while urls_to_visit: + current_url = urls_to_visit.pop() + + # Skip if already visited + if current_url in visited_urls: + continue + visited_urls.add(current_url) + + # Fetch the page content + html_content = self._fetch_page(current_url) + if html_content is None: + continue + + # Convert the HTML to Markdown for cleaner text formatting + title, language, processed_markdown = self._process_html_to_markdown(html_content, current_url) + if processed_markdown: + # Create a Document for each visited page + documents.append( + Document( + processed_markdown, # content + None, # doc_id + None, # embedding + {"source": current_url, "title": title, "language": language} # extra_info + ) + ) + + # Extract links and filter them according to domain rules + new_links = self._extract_links(html_content, current_url) + filtered_links = self._filter_links(new_links, base_domain) + + # Add any new, not-yet-visited links to the queue + urls_to_visit.update(link for link in filtered_links if link not in visited_urls) + + # If we've reached the limit, stop crawling + if self.limit is not None and len(visited_urls) >= self.limit: + break + + return documents + + def _fetch_page(self, url): + try: + response = self.session.get(url, timeout=10) + response.raise_for_status() + return response.text + except requests.exceptions.RequestException as e: + print(f"Error fetching URL {url}: {e}") + return None + + def _process_html_to_markdown(self, html_content, current_url): + soup = BeautifulSoup(html_content, 'html.parser') + title_tag = soup.find('title') + title = title_tag.text.strip() if title_tag else "No Title" + + # Extract language + language_tag = soup.find('html') + language = language_tag.get('lang', 'en') if language_tag else "en" + + markdownified = markdownify(html_content, heading_style="ATX", newline_style="BACKSLASH") + # Reduce sequences of more than two newlines to exactly three + markdownified = re.sub(r'\n{3,}', '\n\n\n', markdownified) + return title, language, markdownified + + def _extract_links(self, html_content, current_url): + soup = BeautifulSoup(html_content, 'html.parser') + links = [] + for a in soup.find_all('a', href=True): + full_url = urljoin(current_url, a['href']) + links.append((full_url, a.text.strip())) + return links + + def _get_base_domain(self, url): + extracted = tldextract.extract(url) + # Reconstruct the domain as domain.suffix + base_domain = f"{extracted.domain}.{extracted.suffix}" + return base_domain + + def _filter_links(self, links, base_domain): + """ + Filter the extracted links to only include those that match the crawling criteria: + - If allow_subdomains is True, allow any link whose domain ends with the base_domain. + - If allow_subdomains is False, only allow exact matches of the base_domain. + """ + filtered = [] + for link, _ in links: + parsed_link = urlparse(link) + if not parsed_link.netloc: + continue + + extracted = tldextract.extract(parsed_link.netloc) + link_base = f"{extracted.domain}.{extracted.suffix}" + + if self.allow_subdomains: + # For subdomains: sub.example.com ends with example.com + if link_base == base_domain or link_base.endswith("." + base_domain): + filtered.append(link) + else: + # Exact domain match + if link_base == base_domain: + filtered.append(link) + return filtered \ No newline at end of file diff --git a/application/requirements.txt b/application/requirements.txt index ca8ba68f..7b756ed3 100644 --- a/application/requirements.txt +++ b/application/requirements.txt @@ -86,4 +86,6 @@ urllib3==2.3.0 vine==5.1.0 wcwidth==0.2.13 werkzeug==3.1.3 -yarl==1.18.3 \ No newline at end of file +yarl==1.18.3 +markdownify==0.14.1 +tldextract==5.1.3 \ No newline at end of file diff --git a/application/worker.py b/application/worker.py index f4f181e5..df0bbe7d 100755 --- a/application/worker.py +++ b/application/worker.py @@ -203,53 +203,61 @@ def remote_worker( sync_frequency="never", operation_mode="upload", doc_id=None, -): +): full_path = os.path.join(directory, user, name_job) - if not os.path.exists(full_path): os.makedirs(full_path) + self.update_state(state="PROGRESS", meta={"current": 1}) - logging.info( - f"Remote job: {full_path}", - extra={"user": user, "job": name_job, "source_data": source_data}, - ) + try: + logging.info("Initializing remote loader with type: %s", loader) + remote_loader = RemoteCreator.create_loader(loader) + raw_docs = remote_loader.load_data(source_data) - remote_loader = RemoteCreator.create_loader(loader) - raw_docs = remote_loader.load_data(source_data) + chunker = Chunker( + chunking_strategy="classic_chunk", + max_tokens=MAX_TOKENS, + min_tokens=MIN_TOKENS, + duplicate_headers=False + ) + docs = chunker.chunk(documents=raw_docs) + docs = [Document.to_langchain_format(raw_doc) for raw_doc in raw_docs] + tokens = count_tokens_docs(docs) + logging.info("Total tokens calculated: %d", tokens) - chunker = Chunker( - chunking_strategy="classic_chunk", - max_tokens=MAX_TOKENS, - min_tokens=MIN_TOKENS, - duplicate_headers=False - ) - docs = chunker.chunk(documents=raw_docs) + if operation_mode == "upload": + id = ObjectId() + embed_and_store_documents(docs, full_path, id, self) + elif operation_mode == "sync": + if not doc_id or not ObjectId.is_valid(doc_id): + logging.error("Invalid doc_id provided for sync operation: %s", doc_id) + raise ValueError("doc_id must be provided for sync operation.") + id = ObjectId(doc_id) + embed_and_store_documents(docs, full_path, id, self) - tokens = count_tokens_docs(docs) - if operation_mode == "upload": - id = ObjectId() - embed_and_store_documents(docs, full_path, id, self) - elif operation_mode == "sync": - if not doc_id or not ObjectId.is_valid(doc_id): - raise ValueError("doc_id must be provided for sync operation.") - id = ObjectId(doc_id) - embed_and_store_documents(docs, full_path, id, self) - self.update_state(state="PROGRESS", meta={"current": 100}) + self.update_state(state="PROGRESS", meta={"current": 100}) - file_data = { - "name": name_job, - "user": user, - "tokens": tokens, - "retriever": retriever, - "id": str(id), - "type": loader, - "remote_data": source_data, - "sync_frequency": sync_frequency, - } - upload_index(full_path, file_data) + file_data = { + "name": name_job, + "user": user, + "tokens": tokens, + "retriever": retriever, + "id": str(id), + "type": loader, + "remote_data": source_data, + "sync_frequency": sync_frequency, + } + upload_index(full_path, file_data) - shutil.rmtree(full_path) + except Exception as e: + logging.error("Error in remote_worker task: %s", str(e), exc_info=True) + raise + finally: + if os.path.exists(full_path): + shutil.rmtree(full_path) + + logging.info("remote_worker task completed successfully") return {"urls": source_data, "name_job": name_job, "user": user, "limited": False} def sync( From 811dfecf9888ce8722563f9315897b9862371099 Mon Sep 17 00:00:00 2001 From: Siddhant Rai Date: Wed, 15 Jan 2025 16:35:26 +0530 Subject: [PATCH 219/354] refactor: tool agent for action parser and handlers --- application/llm/openai.py | 20 +++---- application/tools/agent.py | 76 +++---------------------- application/tools/llm_handler.py | 74 ++++++++++++++++++++++++ application/tools/tool_action_parser.py | 26 +++++++++ 4 files changed, 116 insertions(+), 80 deletions(-) create mode 100644 application/tools/llm_handler.py create mode 100644 application/tools/tool_action_parser.py diff --git a/application/llm/openai.py b/application/llm/openai.py index cc2285a1..b507a1da 100644 --- a/application/llm/openai.py +++ b/application/llm/openai.py @@ -1,6 +1,5 @@ -from application.llm.base import BaseLLM from application.core.settings import settings - +from application.llm.base import BaseLLM class OpenAILLM(BaseLLM): @@ -10,10 +9,7 @@ class OpenAILLM(BaseLLM): super().__init__(*args, **kwargs) if settings.OPENAI_BASE_URL: - self.client = OpenAI( - api_key=api_key, - base_url=settings.OPENAI_BASE_URL - ) + self.client = OpenAI(api_key=api_key, base_url=settings.OPENAI_BASE_URL) else: self.client = OpenAI(api_key=api_key) self.api_key = api_key @@ -27,8 +23,8 @@ class OpenAILLM(BaseLLM): stream=False, tools=None, engine=settings.AZURE_DEPLOYMENT_NAME, - **kwargs - ): + **kwargs, + ): if tools: response = self.client.chat.completions.create( model=model, messages=messages, stream=stream, tools=tools, **kwargs @@ -48,18 +44,16 @@ class OpenAILLM(BaseLLM): stream=True, tools=None, engine=settings.AZURE_DEPLOYMENT_NAME, - **kwargs - ): + **kwargs, + ): response = self.client.chat.completions.create( model=model, messages=messages, stream=stream, **kwargs ) for line in response: - # import sys - # print(line.choices[0].delta.content, file=sys.stderr) if line.choices[0].delta.content is not None: yield line.choices[0].delta.content - + def _supports_tools(self): return True diff --git a/application/tools/agent.py b/application/tools/agent.py index a6aec2e9..f4b37d9b 100644 --- a/application/tools/agent.py +++ b/application/tools/agent.py @@ -1,8 +1,7 @@ -import json -import logging - from application.core.mongo_db import MongoDB from application.llm.llm_creator import LLMCreator +from application.tools.llm_handler import get_llm_handler +from application.tools.tool_action_parser import ToolActionParser from application.tools.tool_manager import ToolManager @@ -12,6 +11,7 @@ class Agent: self.llm = LLMCreator.create_llm( llm_name, api_key=api_key, user_api_key=user_api_key ) + self.llm_handler = get_llm_handler(llm_name) self.gpt_model = gpt_model # Static tool configuration (to be replaced later) self.tools = [] @@ -61,10 +61,8 @@ class Agent: ] def _execute_tool_action(self, tools_dict, call): - call_id = call.id - call_args = json.loads(call.function.arguments) - tool_id = call.function.name.split("_")[-1] - action_name = call.function.name.rsplit("_", 1)[0] + parser = ToolActionParser(self.llm.__class__.__name__) + tool_id, action_name, call_args = parser.parse_args(call) tool_data = tools_dict[tool_id] action_data = next( @@ -78,26 +76,9 @@ class Agent: tm = ToolManager(config={}) tool = tm.load_tool(tool_data["name"], tool_config=tool_data["config"]) print(f"Executing tool: {action_name} with args: {call_args}") - return tool.execute_action(action_name, **call_args), call_id - - def _execute_tool_action_google(self, tools_dict, call): - call_args = json.loads(call.args) - tool_id = call.name.split("_")[-1] - action_name = call.name.rsplit("_", 1)[0] - - tool_data = tools_dict[tool_id] - action_data = next( - action for action in tool_data["actions"] if action["name"] == action_name - ) - - for param, details in action_data["parameters"]["properties"].items(): - if param not in call_args and "value" in details: - call_args[param] = details["value"] - - tm = ToolManager(config={}) - tool = tm.load_tool(tool_data["name"], tool_config=tool_data["config"]) - print(f"Executing tool: {action_name} with args: {call_args}") - return tool.execute_action(action_name, **call_args) + result = tool.execute_action(action_name, **call_args) + call_id = getattr(call, "id", None) + return result, call_id def _simple_tool_agent(self, messages): tools_dict = self._get_user_tools() @@ -111,47 +92,8 @@ class Agent: if resp.message.content: yield resp.message.content return - # check if self.llm class is GoogleLLM - while self.llm.__class__.__name__ == "GoogleLLM" and resp.content.parts[0].function_call: - from google.genai import types - function_call_part = resp.candidates[0].content.parts[0] - tool_response = self._execute_tool_action_google(tools_dict, function_call_part.function_call) - function_response_part = types.Part.from_function_response( - name=function_call_part.function_call.name, - response=tool_response - ) - - while self.llm.__class__.__name__ == "OpenAILLM" and resp.finish_reason == "tool_calls": - message = json.loads(resp.model_dump_json())["message"] - keys_to_remove = {"audio", "function_call", "refusal"} - filtered_data = { - k: v for k, v in message.items() if k not in keys_to_remove - } - messages.append(filtered_data) - tool_calls = resp.message.tool_calls - for call in tool_calls: - try: - tool_response, call_id = self._execute_tool_action(tools_dict, call) - messages.append( - { - "role": "tool", - "content": str(tool_response), - "tool_call_id": call_id, - } - ) - except Exception as e: - messages.append( - { - "role": "tool", - "content": f"Error executing tool: {str(e)}", - "tool_call_id": call.id, - } - ) - # Generate a new response from the LLM after processing tools - resp = self.llm.gen( - model=self.gpt_model, messages=messages, tools=self.tools - ) + resp = self.llm_handler.handle_response(self, resp, tools_dict, messages) # If no tool calls are needed, generate the final response if isinstance(resp, str): diff --git a/application/tools/llm_handler.py b/application/tools/llm_handler.py new file mode 100644 index 00000000..58fce56e --- /dev/null +++ b/application/tools/llm_handler.py @@ -0,0 +1,74 @@ +import json +from abc import ABC, abstractmethod + + +class LLMHandler(ABC): + @abstractmethod + def handle_response(self, agent, resp, tools_dict, messages, **kwargs): + pass + + +class OpenAILLMHandler(LLMHandler): + def handle_response(self, agent, resp, tools_dict, messages): + while resp.finish_reason == "tool_calls": + message = json.loads(resp.model_dump_json())["message"] + keys_to_remove = {"audio", "function_call", "refusal"} + filtered_data = { + k: v for k, v in message.items() if k not in keys_to_remove + } + messages.append(filtered_data) + + tool_calls = resp.message.tool_calls + for call in tool_calls: + try: + tool_response, call_id = agent._execute_tool_action( + tools_dict, call + ) + messages.append( + { + "role": "tool", + "content": str(tool_response), + "tool_call_id": call_id, + } + ) + except Exception as e: + messages.append( + { + "role": "tool", + "content": f"Error executing tool: {str(e)}", + "tool_call_id": call_id, + } + ) + resp = agent.llm.gen( + model=agent.gpt_model, messages=messages, tools=agent.tools + ) + return resp + + +class GoogleLLMHandler(LLMHandler): + def handle_response(self, agent, resp, tools_dict, messages): + from google.genai import types + + while resp.content.parts[0].function_call: + function_call_part = resp.candidates[0].content.parts[0] + tool_response, call_id = agent._execute_tool_action( + tools_dict, function_call_part.function_call + ) + function_response_part = types.Part.from_function_response( + name=function_call_part.function_call.name, response=tool_response + ) + + messages.append(function_call_part, function_response_part) + resp = agent.llm.gen( + model=agent.gpt_model, messages=messages, tools=agent.tools + ) + + return resp + + +def get_llm_handler(llm_type): + handlers = { + "openai": OpenAILLMHandler(), + "google": GoogleLLMHandler(), + } + return handlers.get(llm_type, OpenAILLMHandler()) diff --git a/application/tools/tool_action_parser.py b/application/tools/tool_action_parser.py new file mode 100644 index 00000000..b708992a --- /dev/null +++ b/application/tools/tool_action_parser.py @@ -0,0 +1,26 @@ +import json + + +class ToolActionParser: + def __init__(self, llm_type): + self.llm_type = llm_type + self.parsers = { + "OpenAILLM": self._parse_openai_llm, + "GoogleLLM": self._parse_google_llm, + } + + def parse_args(self, call): + parser = self.parsers.get(self.llm_type, self._parse_openai_llm) + return parser(call) + + def _parse_openai_llm(self, call): + call_args = json.loads(call.function.arguments) + tool_id = call.function.name.split("_")[-1] + action_name = call.function.name.rsplit("_", 1)[0] + return tool_id, action_name, call_args + + def _parse_google_llm(self, call): + call_args = json.loads(call.args) + tool_id = call.name.split("_")[-1] + action_name = call.name.rsplit("_", 1)[0] + return tool_id, action_name, call_args From cab6305462db0aca429fa16fb4d66975b96c4d13 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 15 Jan 2025 20:59:45 +0000 Subject: [PATCH 220/354] build(deps): bump primp from 0.9.3 to 0.10.0 in /application Bumps [primp](https://github.com/deedy5/primp) from 0.9.3 to 0.10.0. - [Release notes](https://github.com/deedy5/primp/releases) - [Commits](https://github.com/deedy5/primp/compare/v0.9.3...v0.10.0) --- updated-dependencies: - dependency-name: primp dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- application/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/requirements.txt b/application/requirements.txt index 0ad92e27..3fc6d02d 100644 --- a/application/requirements.txt +++ b/application/requirements.txt @@ -55,7 +55,7 @@ pathable==0.4.4 pillow==11.1.0 portalocker==2.10.1 prance==23.6.21.0 -primp==0.9.3 +primp==0.10.0 prompt-toolkit==3.0.48 protobuf==5.29.3 py==1.11.0 From 00b10f17c1ababdcb284063f82a9507df33e0b28 Mon Sep 17 00:00:00 2001 From: Ahmad Alghooneh Date: Thu, 16 Jan 2025 00:41:09 -0500 Subject: [PATCH 221/354] eleven labs --- application/tts/elevenlabs.py | 80 ++++++++++++++++++------------- application/tts/output_audio.mp3 | Bin 0 -> 30511 bytes 2 files changed, 46 insertions(+), 34 deletions(-) create mode 100644 application/tts/output_audio.mp3 diff --git a/application/tts/elevenlabs.py b/application/tts/elevenlabs.py index 96fb1f43..0d982d31 100644 --- a/application/tts/elevenlabs.py +++ b/application/tts/elevenlabs.py @@ -8,41 +8,62 @@ from base import BaseTTS class ElevenlabsTTS(BaseTTS): def __init__(self): - self.api_key = 'sk_19b72c883e8bdfcec2705be2d048f3830a40d2faa4b76b26' - self.model = "eleven_multilingual_v2" - self.voice = "Brian" + self.api_key = 'ELEVENLABS_API_KEY'# here you should put your api key + self.model = "eleven_flash_v2_5" + self.voice = "VOICE_ID" # this is the hash code for the voice not the name! + self.write_audio = 1 def text_to_speech(self, text): - audio_bytes = asyncio.run(self._text_to_speech_websocket(text)) - audio_base64 = base64.b64encode(audio_bytes).decode("utf-8") - lang = "en" - return audio_base64, lang + asyncio.run(self._text_to_speech_websocket(text)) async def _text_to_speech_websocket(self, text): uri = f"wss://api.elevenlabs.io/v1/text-to-speech/{self.voice}/stream-input?model_id={self.model}" - + websocket = await websockets.connect(uri) payload = { - "text": text, - "model_id": self.model, + "text": " ", "voice_settings": { - "voice_id": self.voice + "stability": 0.5, + "similarity_boost": 0.8, }, - "xi-api-key": self.api_key, - "Accept": "audio/mpeg" + "xi_api_key": self.api_key, } - audio_data = BytesIO() - async with websockets.connect(uri) as websocket: - - await websocket.send(json.dumps(payload)) - - async for message in websocket: - if isinstance(message, bytes): - audio_data.write(message) - else: - print("Received a non-binary frame:", message) + await websocket.send(json.dumps(payload)) + + async def listen(): + while 1: + try: + msg = await websocket.recv() + data = json.loads(msg) - return audio_data.getvalue() + if data.get("audio"): + print("audio received") + yield base64.b64decode(data["audio"]) + elif data.get("isFinal"): + break + except websockets.exceptions.ConnectionClosed: + print("websocket closed") + break + listen_task = asyncio.create_task(self.stream(listen())) + + await websocket.send(json.dumps({"text": text})) + # this is to signal the end of the text, either use this or flush + await websocket.send(json.dumps({"text": ""})) + + await listen_task + + async def stream(self, audio_stream): + if self.write_audio: + audio_bytes = BytesIO() + async for chunk in audio_stream: + if chunk: + audio_bytes.write(chunk) + with open("output_audio.mp3", "wb") as f: + f.write(audio_bytes.getvalue()) + + else: + async for chunk in audio_stream: + pass # depends on the streamer! def test_elevenlabs_websocket(): @@ -54,16 +75,7 @@ def test_elevenlabs_websocket(): tts = ElevenlabsTTS() # Call the method with some sample text - audio_base64, lang = tts.text_to_speech("Hello from ElevenLabs WebSocket!") - - print(f"Received language: {lang}") - print(f"Base64 Audio (truncated): {audio_base64[:100]}...") - - # Optional: Save the audio to a local file for manual listening. - # We'll assume the audio is in MP3 format based on "Accept": "audio/mpeg". - audio_bytes = base64.b64decode(audio_base64) - with open("output_audio.mp3", "wb") as f: - f.write(audio_bytes) + tts.text_to_speech("Hello from ElevenLabs WebSocket!") print("Saved audio to output_audio.mp3.") diff --git a/application/tts/output_audio.mp3 b/application/tts/output_audio.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..fd0bed293895a9cff3197d827b71c71c75e0368f GIT binary patch literal 30511 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 DsOI7j literal 0 HcmV?d00001 From 7f2cc3b232c9006103734a7315e9c290ffd2fef9 Mon Sep 17 00:00:00 2001 From: Ahmad Reza Alghooneh <51265135+aalghooneh@users.noreply.github.com> Date: Thu, 16 Jan 2025 00:45:41 -0500 Subject: [PATCH 222/354] Delete application/tts/output_audio.mp3 --- application/tts/output_audio.mp3 | Bin 30511 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 application/tts/output_audio.mp3 diff --git a/application/tts/output_audio.mp3 b/application/tts/output_audio.mp3 deleted file mode 100644 index fd0bed293895a9cff3197d827b71c71c75e0368f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 30511 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 DsOI7j From cc78ea7222cc6325508df6fce00e4b2b652bd1d2 Mon Sep 17 00:00:00 2001 From: ManishMadan2882 Date: Thu, 16 Jan 2025 18:24:27 +0530 Subject: [PATCH 223/354] (fix:locales) sync static text --- .../src/components/DocumentPagination.tsx | 16 ++- frontend/src/locale/en.json | 37 +++++- frontend/src/locale/es.json | 20 ++- frontend/src/locale/jp.json | 53 ++++++-- frontend/src/locale/ru.json | 96 +++++++++----- frontend/src/locale/zh-TW.json | 55 ++++++-- frontend/src/locale/zh.json | 37 +++++- frontend/src/modals/ConfigToolModal.tsx | 15 ++- .../src/modals/ShareConversationModal.tsx | 16 ++- frontend/src/preferences/PromptsModal.tsx | 39 +++--- frontend/src/settings/Analytics.tsx | 15 ++- frontend/src/settings/General.tsx | 17 ++- frontend/src/settings/Tools.tsx | 12 +- frontend/src/upload/Upload.tsx | 118 ++++++++---------- 14 files changed, 383 insertions(+), 163 deletions(-) diff --git a/frontend/src/components/DocumentPagination.tsx b/frontend/src/components/DocumentPagination.tsx index f02ef1c0..6958d051 100644 --- a/frontend/src/components/DocumentPagination.tsx +++ b/frontend/src/components/DocumentPagination.tsx @@ -1,4 +1,5 @@ import React, { useState } from 'react'; +import { useTranslation } from 'react-i18next'; import SingleArrowLeft from '../assets/single-left-arrow.svg'; import SingleArrowRight from '../assets/single-right-arrow.svg'; import DoubleArrowLeft from '../assets/double-arrow-left.svg'; @@ -19,6 +20,7 @@ const Pagination: React.FC = ({ onPageChange, onRowsPerPageChange, }) => { + const { t } = useTranslation(); const [isDropdownOpen, setIsDropdownOpen] = useState(false); const rowsPerPageOptions = [5, 10, 20, 50]; @@ -53,7 +55,9 @@ const Pagination: React.FC = ({
{/* Rows per page dropdown */}
- Rows per page: + + {t('pagination.rowsPerPage')}: +
@@ -108,7 +112,7 @@ const Pagination: React.FC = ({ > Previous page @@ -119,7 +123,7 @@ const Pagination: React.FC = ({ > Next page @@ -130,7 +134,7 @@ const Pagination: React.FC = ({ > Last page diff --git a/frontend/src/locale/en.json b/frontend/src/locale/en.json index 7be4b568..2f21445b 100644 --- a/frontend/src/locale/en.json +++ b/frontend/src/locale/en.json @@ -83,7 +83,9 @@ "tokenUsage": "Token Usage", "feedback": "Feedback", "filterPlaceholder": "Filter", - "none": "None" + "none": "None", + "positiveFeedback": "Positive Feedback", + "negativeFeedback": "Negative Feedback" }, "logs": { "label": "Logs", @@ -93,7 +95,12 @@ "tableHeader": "API generated / chatbot conversations" }, "tools": { - "label": "Tools" + "label": "Tools", + "searchPlaceholder": "Search...", + "addTool": "Add Tool", + "noToolsAlt": "No tools found", + "noToolsFound": "No tools found", + "selectToolSetup": "Select a tool to set up" } }, "modals": { @@ -150,6 +157,24 @@ "note": "Source document, personal information and further conversation will remain private", "create": "Create", "option": "Allow users to prompt further" + }, + "configTool": { + "title": "Tool Config", + "type": "Type", + "apiKeyLabel": "API Key / OAuth", + "apiKeyPlaceholder": "Enter API Key / OAuth", + "addButton": "Add Tool", + "closeButton": "Close" + }, + "prompts": { + "addPrompt": "Add Prompt", + "addDescription": "Add your custom prompt and save it to DocsGPT", + "editPrompt": "Edit Prompt", + "editDescription": "Edit your custom prompt and save it to DocsGPT", + "promptName": "Prompt Name", + "promptText": "Prompt Text", + "save": "Save", + "nameExists": "Name already exists" } }, "sharedConv": { @@ -162,5 +187,13 @@ "delete": "Delete", "rename": "Rename", "deleteWarning": "Are you sure you want to delete this conversation?" + }, + "pagination": { + "rowsPerPage": "Rows per page", + "pageOf": "Page {{currentPage}} of {{totalPages}}", + "firstPage": "First page", + "previousPage": "Previous page", + "nextPage": "Next page", + "lastPage": "Last page" } } diff --git a/frontend/src/locale/es.json b/frontend/src/locale/es.json index d2c087db..8d0a3273 100644 --- a/frontend/src/locale/es.json +++ b/frontend/src/locale/es.json @@ -83,7 +83,9 @@ "tokenUsage": "Uso de Tokens", "feedback": "Retroalimentación", "filterPlaceholder": "Filtrar", - "none": "Ninguno" + "none": "Ninguno", + "positiveFeedback": "Retroalimentación Positiva", + "negativeFeedback": "Retroalimentación Negativa" }, "logs": { "label": "Registros", @@ -150,6 +152,14 @@ "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" + }, + "configTool": { + "title": "Configuración de la Herramienta", + "type": "Tipo", + "apiKeyLabel": "Clave API / OAuth", + "apiKeyPlaceholder": "Ingrese la Clave API / OAuth", + "addButton": "Agregar Herramienta", + "closeButton": "Cerrar" } }, "sharedConv": { @@ -162,5 +172,13 @@ "delete": "Eliminar", "rename": "Renombrar", "deleteWarning": "¿Está seguro de que desea eliminar esta conversación?" + }, + "pagination": { + "rowsPerPage": "Filas por página", + "pageOf": "Página {{currentPage}} de {{totalPages}}", + "firstPage": "Primera página", + "previousPage": "Página anterior", + "nextPage": "Página siguiente", + "lastPage": "Última página" } } diff --git a/frontend/src/locale/jp.json b/frontend/src/locale/jp.json index 71aa1e15..e1e27503 100644 --- a/frontend/src/locale/jp.json +++ b/frontend/src/locale/jp.json @@ -58,15 +58,15 @@ "date": "ベクトル日付", "type": "タイプ", "tokenUsage": "トークン使用量", - "noData": "既存のドキュメントはありません" + "noData": "既存のドキュメントがありません" }, "apiKeys": { - "label": "チャットボット", + "label": "APIキー", "name": "名前", "key": "APIキー", "sourceDoc": "ソースドキュメント", "createNew": "新規作成", - "noData": "既存のチャットボットはありません" + "noData": "既存のAPIキーがありません" }, "analytics": { "label": "分析", @@ -74,26 +74,33 @@ "selectChatbot": "チャットボットを選択", "filterOptions": { "hour": "時間", - "last24Hours": "24時間", - "last7Days": "7日間", - "last15Days": "15日間", - "last30Days": "30日間" + "last24Hours": "過去24時間", + "last7Days": "過去7日間", + "last15Days": "過去15日間", + "last30Days": "過去30日間" }, "messages": "メッセージ", "tokenUsage": "トークン使用量", "feedback": "フィードバック", "filterPlaceholder": "フィルター", - "none": "なし" + "none": "なし", + "positiveFeedback": "肯定的なフィードバック", + "negativeFeedback": "否定的なフィードバック" }, "logs": { "label": "ログ", "filterByChatbot": "チャットボットでフィルター", "selectChatbot": "チャットボットを選択", "none": "なし", - "tableHeader": "API生成/チャットボットの会話" + "tableHeader": "API生成 / チャットボットの会話" }, "tools": { - "label": "ツール" + "label": "ツール", + "searchPlaceholder": "検索...", + "addTool": "ツールを追加", + "noToolsAlt": "ツールが見つかりません", + "noToolsFound": "ツールが見つかりません", + "selectToolSetup": "設定するツールを選択してください" } }, "modals": { @@ -150,6 +157,24 @@ "note": "ソースドキュメント、個人情報、および以降の会話は非公開のままになります", "create": "作成", "option": "ユーザーがより多くのクエリを実行できるようにします。" + }, + "configTool": { + "title": "ツール設定", + "type": "タイプ", + "apiKeyLabel": "APIキー / OAuth", + "apiKeyPlaceholder": "APIキー / OAuthを入力してください", + "addButton": "ツールを追加", + "closeButton": "閉じる" + }, + "prompts": { + "addPrompt": "プロンプトを追加", + "addDescription": "カスタムプロンプトを追加してDocsGPTに保存", + "editPrompt": "プロンプトを編集", + "editDescription": "カスタムプロンプトを編集してDocsGPTに保存", + "promptName": "プロンプト名", + "promptText": "プロンプトテキスト", + "save": "保存", + "nameExists": "名前が既に存在します" } }, "sharedConv": { @@ -162,5 +187,13 @@ "delete": "削除", "rename": "名前変更", "deleteWarning": "この会話を削除してもよろしいですか?" + }, + "pagination": { + "rowsPerPage": "1ページあたりの行数", + "pageOf": "ページ {{currentPage}} / {{totalPages}}", + "firstPage": "最初のページ", + "previousPage": "前のページ", + "nextPage": "次のページ", + "lastPage": "最後のページ" } } diff --git a/frontend/src/locale/ru.json b/frontend/src/locale/ru.json index c5e3deb8..f2eb2e87 100644 --- a/frontend/src/locale/ru.json +++ b/frontend/src/locale/ru.json @@ -10,6 +10,9 @@ "sourceDocs": "Источник", "none": "Нет", "cancel": "Отмена", + "help": "Помощь", + "emailUs": "Напишите нам", + "documentation": "Документация", "demo": [ { "header": "Узнайте о DocsGPT", @@ -34,7 +37,7 @@ "label": "Общие", "selectTheme": "Выбрать тему", "light": "Светлая", - "dark": "Темная", + "dark": "Тёмная", "selectLanguage": "Выбрать язык", "chunks": "Обработанные фрагменты на запрос", "prompt": "Активная подсказка", @@ -47,7 +50,7 @@ "medium": "Средний", "high": "Высокий", "unlimited": "Без ограничений", - "default": "по умолчанию" + "default": "По умолчанию" }, "documents": { "label": "Документы", @@ -58,12 +61,12 @@ "noData": "Нет существующих документов" }, "apiKeys": { - "label": "Чат-боты", + "label": "API ключи", "name": "Название", - "key": "Ключ API", - "sourceDoc": "Исходный документ", + "key": "API ключ", + "sourceDoc": "Источник документа", "createNew": "Создать новый", - "noData": "Нет существующих чат-ботов" + "noData": "Нет существующих API ключей" }, "analytics": { "label": "Аналитика", @@ -71,52 +74,59 @@ "selectChatbot": "Выбрать чат-бота", "filterOptions": { "hour": "Час", - "last24Hours": "24 часа", - "last7Days": "7 дней", - "last15Days": "15 дней", - "last30Days": "30 дней" + "last24Hours": "Последние 24 часа", + "last7Days": "Последние 7 дней", + "last15Days": "Последние 15 дней", + "last30Days": "Последние 30 дней" }, "messages": "Сообщения", - "tokenUsage": "Использование токенов", + "tokenUsage": "Использование токена", "feedback": "Обратная связь", "filterPlaceholder": "Фильтр", - "none": "Нет" + "none": "Нет", + "positiveFeedback": "Положительная обратная связь", + "negativeFeedback": "Отрицательная обратная связь" }, "logs": { "label": "Журналы", "filterByChatbot": "Фильтровать по чат-боту", "selectChatbot": "Выбрать чат-бота", "none": "Нет", - "tableHeader": "Сгенерированные API / разговоры с чат-ботом" + "tableHeader": "API сгенерировано / разговоры с чат-ботом" }, "tools": { - "label": "Инструменты" + "label": "Инструменты", + "searchPlaceholder": "Поиск...", + "addTool": "Добавить инструмент", + "noToolsAlt": "Инструменты не найдены", + "noToolsFound": "Инструменты не найдены", + "selectToolSetup": "Выберите инструмент для настройки" } }, "modals": { "uploadDoc": { - "label": "Загрузить новую документацию", + "label": "Загрузить новый документ", "select": "Выберите способ загрузки документа в DocsGPT", "file": "Загрузить с устройства", "back": "Назад", - "wait": "Пожалуйста, подождите ...", + "wait": "Пожалуйста, подождите...", "remote": "Собрать с веб-сайта", "start": "Начать чат", "name": "Имя", "choose": "Выбрать файлы", - "info": "Загрузите .pdf, .txt, .rst, .csv, .xlsx, .docx, .md, .html, .epub, .json, .pptx, .zip с ограничением до 25 МБ", + "info": "Пожалуйста, загрузите файлы .pdf, .txt, .rst, .csv, .xlsx, .docx, .md, .html, .epub, .json, .pptx, .zip размером до 25 МБ", "uploadedFiles": "Загруженные файлы", "cancel": "Отмена", "train": "Обучение", "link": "Ссылка", "urlLink": "URL-ссылка", - "repoUrl": "URL-адрес репозитория", + "repoUrl": "URL репозитория", "reddit": { "id": "ID клиента", "secret": "Секрет клиента", - "agent": "Агент пользователя", + "agent": "Пользовательский агент", "searchQueries": "Поисковые запросы", - "numberOfPosts": "Количество постов" + "numberOfPosts": "Количество сообщений" }, "drag": { "title": "Загрузить исходный файл", @@ -124,16 +134,16 @@ } }, "createAPIKey": { - "label": "Создать новый ключ API", - "apiKeyName": "Название ключа API", + "label": "Создать новый API ключ", + "apiKeyName": "Название API ключа", "chunks": "Обработанные фрагменты на запрос", - "prompt": "Выбрать активный запрос", - "sourceDoc": "Исходный документ", + "prompt": "Выбрать активную подсказку", + "sourceDoc": "Источник документа", "create": "Создать" }, "saveKey": { - "note": "Пожалуйста, сохраните свой ключ", - "disclaimer": "Это единственный раз, когда будет показан ваш ключ", + "note": "Пожалуйста, сохраните ваш ключ", + "disclaimer": "Ваш ключ будет показан только один раз.", "copy": "Копировать", "copied": "Скопировано", "confirm": "Я сохранил ключ" @@ -143,21 +153,47 @@ "delete": "Удалить" }, "shareConv": { - "label": "Создать публичную страницу для общего доступа", - "note": "Исходный документ, личная информация и дальнейший разговор останутся конфиденциальными", + "label": "Создать публичную страницу для совместного использования", + "note": "Исходный документ, личная информация и последующие разговоры останутся приватными", "create": "Создать", - "option": "Разрешить пользователям делать дополнительные запросы" + "option": "Позволить пользователям делать дополнительные запросы." + }, + "configTool": { + "title": "Настройка инструмента", + "type": "Тип", + "apiKeyLabel": "API ключ / OAuth", + "apiKeyPlaceholder": "Введите API ключ / OAuth", + "addButton": "Добавить инструмент", + "closeButton": "Закрыть" + }, + "prompts": { + "addPrompt": "Добавить подсказку", + "addDescription": "Добавить вашу пользовательскую подсказку и сохранить её в DocsGPT", + "editPrompt": "Редактировать подсказку", + "editDescription": "Редактировать вашу пользовательскую подсказку и сохранить её в DocsGPT", + "promptName": "Название подсказки", + "promptText": "Текст подсказки", + "save": "Сохранить", + "nameExists": "Название уже существует" } }, "sharedConv": { "subtitle": "Создано с помощью", "button": "Начать работу с DocsGPT", - "meta": "DocsGPT использует GenAI, пожалуйста, проверяйте важную информацию, используя источники" + "meta": "DocsGPT использует GenAI, пожалуйста, проверьте важную информацию, используя источники." }, "convTile": { "share": "Поделиться", "delete": "Удалить", "rename": "Переименовать", "deleteWarning": "Вы уверены, что хотите удалить этот разговор?" + }, + "pagination": { + "rowsPerPage": "Строк на странице", + "pageOf": "Страница {{currentPage}} из {{totalPages}}", + "firstPage": "Первая страница", + "previousPage": "Предыдущая страница", + "nextPage": "Следующая страница", + "lastPage": "Последняя страница" } } diff --git a/frontend/src/locale/zh-TW.json b/frontend/src/locale/zh-TW.json index 5e97d883..a2bba823 100644 --- a/frontend/src/locale/zh-TW.json +++ b/frontend/src/locale/zh-TW.json @@ -57,14 +57,16 @@ "name": "文件名稱", "date": "向量日期", "type": "類型", - "tokenUsage": "Token 使用量" + "tokenUsage": "Token 使用量", + "noData": "沒有現有的文件" }, "apiKeys": { - "label": "API 金鑰", + "label": "聊天機器人", "name": "名稱", "key": "API 金鑰", "sourceDoc": "來源文件", - "createNew": "新增" + "createNew": "新增", + "noData": "沒有現有的聊天機器人" }, "analytics": { "label": "分析", @@ -81,7 +83,9 @@ "tokenUsage": "Token 使用量", "feedback": "回饋", "filterPlaceholder": "篩選", - "none": "無" + "none": "無", + "positiveFeedback": "正向回饋", + "negativeFeedback": "負向回饋" }, "logs": { "label": "日誌", @@ -91,22 +95,32 @@ "tableHeader": "API 生成 / 聊天機器人會話" }, "tools": { - "label": "工具" + "label": "工具", + "searchPlaceholder": "搜尋...", + "addTool": "新增工具", + "noToolsAlt": "找不到工具", + "noToolsFound": "找不到工具", + "selectToolSetup": "選擇要設定的工具" } }, "modals": { "uploadDoc": { "label": "上傳新文件", + "select": "選擇如何將文件上傳到 DocsGPT", "file": "從檔案", "remote": "遠端", + "back": "返回", + "wait": "請稍候...", + "start": "開始對話", "name": "名稱", "choose": "選擇檔案", - "info": "請上傳 .pdf, .txt, .rst, .docx, .md, .json, .pptx, .zip 檔案,大小限制為 25MB", + "info": "請上傳 .pdf, .txt, .rst, .csv, .xlsx, .docx, .md, .html, .epub, .json, .pptx, .zip 檔案,大小限制為 25MB", "uploadedFiles": "已上傳的檔案", "cancel": "取消", "train": "訓練", "link": "連結", "urlLink": "URL 連結", + "repoUrl": "儲存庫 URL", "reddit": { "id": "用戶端 ID", "secret": "用戶端金鑰", @@ -141,7 +155,26 @@ "shareConv": { "label": "建立公開頁面以分享", "note": "來源文件、個人資訊和後續對話將保持私密", - "create": "建立" + "create": "建立", + "option": "允許使用者進行更多查詢" + }, + "configTool": { + "title": "工具設定", + "type": "類型", + "apiKeyLabel": "API 金鑰 / OAuth", + "apiKeyPlaceholder": "輸入 API 金鑰 / OAuth", + "addButton": "新增工具", + "closeButton": "關閉" + }, + "prompts": { + "addPrompt": "新增提示", + "addDescription": "新增自定義提示並儲存到 DocsGPT", + "editPrompt": "編輯提示", + "editDescription": "編輯自定義提示並儲存到 DocsGPT", + "promptName": "提示名稱", + "promptText": "提示文字", + "save": "儲存", + "nameExists": "名稱已存在" } }, "sharedConv": { @@ -154,5 +187,13 @@ "delete": "刪除", "rename": "重新命名", "deleteWarning": "您確定要刪除這個對話嗎?" + }, + "pagination": { + "rowsPerPage": "每頁行數", + "pageOf": "第 {{currentPage}} 頁,共 {{totalPages}} 頁", + "firstPage": "第一頁", + "previousPage": "上一頁", + "nextPage": "下一頁", + "lastPage": "最後一頁" } } diff --git a/frontend/src/locale/zh.json b/frontend/src/locale/zh.json index afa15f7b..0ca94fe2 100644 --- a/frontend/src/locale/zh.json +++ b/frontend/src/locale/zh.json @@ -83,7 +83,9 @@ "tokenUsage": "令牌使用", "feedback": "反馈", "filterPlaceholder": "筛选", - "none": "无" + "none": "无", + "positiveFeedback": "正向反馈", + "negativeFeedback": "负向反馈" }, "logs": { "label": "日志", @@ -93,7 +95,12 @@ "tableHeader": "API 生成 / 聊天机器人会话" }, "tools": { - "label": "工具" + "label": "工具", + "searchPlaceholder": "搜索...", + "addTool": "添加工具", + "noToolsAlt": "未找到工具", + "noToolsFound": "未找到工具", + "selectToolSetup": "选择要设置的工具" } }, "modals": { @@ -150,6 +157,24 @@ "note": "源文档、个人信息和后续对话将保持私密", "create": "创建", "option": "允许用户进行更多查询。" + }, + "configTool": { + "title": "工具配置", + "type": "类型", + "apiKeyLabel": "API 密钥 / OAuth", + "apiKeyPlaceholder": "输入 API 密钥 / OAuth", + "addButton": "添加工具", + "closeButton": "关闭" + }, + "prompts": { + "addPrompt": "添加提示", + "addDescription": "添加自定义提示并保存到 DocsGPT", + "editPrompt": "编辑提示", + "editDescription": "编辑自定义提示并保存到 DocsGPT", + "promptName": "提示名称", + "promptText": "提示文本", + "save": "保存", + "nameExists": "名称已存在" } }, "sharedConv": { @@ -162,5 +187,13 @@ "delete": "删除", "rename": "重命名", "deleteWarning": "您确定要删除此对话吗?" + }, + "pagination": { + "rowsPerPage": "每页行数", + "pageOf": "第 {{currentPage}} 页,共 {{totalPages}} 页", + "firstPage": "第一页", + "previousPage": "上一页", + "nextPage": "下一页", + "lastPage": "最后一页" } } diff --git a/frontend/src/modals/ConfigToolModal.tsx b/frontend/src/modals/ConfigToolModal.tsx index 96bb15be..4a8ca881 100644 --- a/frontend/src/modals/ConfigToolModal.tsx +++ b/frontend/src/modals/ConfigToolModal.tsx @@ -1,4 +1,5 @@ import React from 'react'; +import { useTranslation } from 'react-i18next'; import Exit from '../assets/exit.svg'; import Input from '../components/Input'; @@ -17,6 +18,7 @@ export default function ConfigToolModal({ tool: AvailableTool | null; getUserTools: () => void; }) { + const { t } = useTranslation(); const [authKey, setAuthKey] = React.useState(''); const handleAddTool = (tool: AvailableTool) => { @@ -52,21 +54,22 @@ export default function ConfigToolModal({

- Tool Config + {t('modals.configTool.title')}

- Type: {tool?.name} + {t('modals.configTool.type')}:{' '} + {tool?.name}

- API Key / Oauth + {t('modals.configTool.apiKeyLabel')} setAuthKey(e.target.value)} borderVariant="thin" - placeholder="Enter API Key / Oauth" + placeholder={t('modals.configTool.apiKeyPlaceholder')} >
@@ -76,7 +79,7 @@ export default function ConfigToolModal({ }} className="rounded-3xl bg-purple-30 px-5 py-2 text-sm text-white transition-all hover:bg-[#6F3FD1]" > - Add Tool + {t('modals.configTool.addButton')}
diff --git a/frontend/src/modals/ShareConversationModal.tsx b/frontend/src/modals/ShareConversationModal.tsx index 3f87839e..44156761 100644 --- a/frontend/src/modals/ShareConversationModal.tsx +++ b/frontend/src/modals/ShareConversationModal.tsx @@ -101,11 +101,17 @@ export const ShareConversationModal = ({ return (
-

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

-

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

+

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

+

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

- {t('modals.shareConv.option')} -
+ ); +} diff --git a/frontend/src/settings/Documents.tsx b/frontend/src/settings/Documents.tsx index 88590165..b5e874d5 100644 --- a/frontend/src/settings/Documents.tsx +++ b/frontend/src/settings/Documents.tsx @@ -1,24 +1,31 @@ -import React, { useState, useEffect, useCallback } from 'react'; -import PropTypes from 'prop-types'; -import userService from '../api/services/userService'; -import SyncIcon from '../assets/sync.svg'; -import Trash from '../assets/trash.svg'; -import caretSort from '../assets/caret-sort.svg'; -import DropdownMenu from '../components/DropdownMenu'; -import SkeletonLoader from '../components/SkeletonLoader'; -import Input from '../components/Input'; -import Upload from '../upload/Upload'; // Import the Upload component -import Pagination from '../components/DocumentPagination'; +import React, { useCallback, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useDispatch } from 'react-redux'; -import { Doc, DocumentsProps, ActiveState } from '../models/misc'; // Ensure ActiveState type is imported -import { getDocs, getDocsWithPagination } from '../preferences/preferenceApi'; -import { setSourceDocs } from '../preferences/preferenceSlice'; -import { setPaginatedDocuments } from '../preferences/preferenceSlice'; -import { formatDate } from '../utils/dateTimeUtils'; -import ConfirmationModal from '../modals/ConfirmationModal'; -// Utility function to format numbers +import userService from '../api/services/userService'; +import ArrowLeft from '../assets/arrow-left.svg'; +import caretSort from '../assets/caret-sort.svg'; +import NoFilesDarkIcon from '../assets/no-files-dark.svg'; +import NoFilesIcon from '../assets/no-files.svg'; +import SyncIcon from '../assets/sync.svg'; +import Trash from '../assets/trash.svg'; +import Pagination from '../components/DocumentPagination'; +import DropdownMenu from '../components/DropdownMenu'; +import Input from '../components/Input'; +import SkeletonLoader from '../components/SkeletonLoader'; +import { useDarkTheme } from '../hooks'; +import AddChunkModal from '../modals/AddChunkModal'; +import ConfirmationModal from '../modals/ConfirmationModal'; +import { ActiveState, Doc, DocumentsProps } from '../models/misc'; +import { getDocs, getDocsWithPagination } from '../preferences/preferenceApi'; +import { + setPaginatedDocuments, + setSourceDocs, +} from '../preferences/preferenceSlice'; +import Upload from '../upload/Upload'; +import { formatDate } from '../utils/dateTimeUtils'; +import { ChunkType } from './types'; + const formatTokens = (tokens: number): string => { const roundToTwoDecimals = (num: number): string => { return (Math.round((num + Number.EPSILON) * 100) / 100).toString(); @@ -35,17 +42,16 @@ const formatTokens = (tokens: number): string => { } }; -const Documents: React.FC = ({ +export default function Documents({ paginatedDocuments, handleDeleteDocument, -}) => { +}: DocumentsProps) { const { t } = useTranslation(); const dispatch = useDispatch(); - // State for search input + const [searchTerm, setSearchTerm] = useState(''); - // State for modal: active/inactive - const [modalState, setModalState] = useState('INACTIVE'); // Initialize with inactive state - const [isOnboarding, setIsOnboarding] = useState(false); // State for onboarding flag + const [modalState, setModalState] = useState('INACTIVE'); + const [isOnboarding, setIsOnboarding] = useState(false); const [loading, setLoading] = useState(false); const [sortField, setSortField] = useState<'date' | 'tokens'>('date'); const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('desc'); @@ -60,6 +66,7 @@ const Documents: React.FC = ({ { label: t('settings.documents.syncFrequency.weekly'), value: 'weekly' }, { label: t('settings.documents.syncFrequency.monthly'), value: 'monthly' }, ]; + const [showDocumentChunks, setShowDocumentChunks] = useState(); const refreshDocs = useCallback( ( @@ -159,7 +166,14 @@ const Documents: React.FC = ({ refreshDocs(undefined, 1, rowsPerPage); }, [searchTerm]); - return ( + return showDocumentChunks ? ( + { + setShowDocumentChunks(undefined); + }} + /> + ) : (
@@ -183,6 +197,7 @@ const Documents: React.FC = ({ setSearchTerm(e.target.value); setCurrentPage(1); }} + borderVariant="thin" />
+ setShowDocumentChunks(document)} + > + {children} ); }, tr({ children }) { return ( - + {children} ); }, - td({ children }) { - return ; - }, th({ children }) { return ; }, + td({ children }) { + return ; + }, }} > {preprocessLaTeX(message)} From 926ec89f4890cb1c4b94a7ed78bc4c995ef984e3 Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 11 Feb 2025 09:48:45 +0000 Subject: [PATCH 319/354] Create devc-welcome.md --- .devcontainer/devc-welcome.md | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 .devcontainer/devc-welcome.md diff --git a/.devcontainer/devc-welcome.md b/.devcontainer/devc-welcome.md new file mode 100644 index 00000000..8560a578 --- /dev/null +++ b/.devcontainer/devc-welcome.md @@ -0,0 +1,26 @@ +# Welcome to DocsGPT Devcontainer + +Welcome to the DocsGPT development environment! This guide will help you get started quickly. + +## Starting Services + +To run DocsGPT, you need to start three main services: Flask (backend), Celery (task queue), and Vite (frontend). Here are the commands to start each service within the devcontainer: + +### Vite (Frontend) + +```bash +cd frontend +npm run dev -- --host +``` + +### Flask (Backend) + +```bash +flask --app application/app.py run --host=0.0.0.0 --port=7091 +``` + +### Celery (Task Queue) + +```bash +celery -A application.app.celery worker -l INFO +``` From 5f42e4ac3f3270fbfa40705e03f4b0a8d4be6b50 Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 11 Feb 2025 09:53:26 +0000 Subject: [PATCH 320/354] fix: default file codespace --- .devcontainer/devcontainer.json | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index a86b8b66..16188e32 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -13,6 +13,12 @@ "esbenp.prettier-vscode", "dbaeumer.vscode-eslint" ] - } + }, + "codespaces": { + "openFiles": [ + ".devcontainer/devc-welcome.md", + "CONTRIBUTING.md" + ] + } } } \ No newline at end of file From 72e9fcc8950964a21ee93f2a058c22c9b3e62a9c Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 11 Feb 2025 16:08:48 +0000 Subject: [PATCH 321/354] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 0ae8ec99..ba042bd8 100644 --- a/README.md +++ b/README.md @@ -45,11 +45,11 @@ - [x] Full GoogleAI compatibility (Jan 2025) - [x] Add tools (Jan 2025) +- [x] Manually updating chunks in the app UI (Feb 2025) +- [ ] Devcontainer for easy development (Feb 2025) - [ ] Anthropic Tool compatibility - [ ] Add triggerable actions / tools (webhook) - [ ] Add OAuth 2.0 authentication for tools and sources -- [ ] Manually updating chunks in the app UI -- [ ] Devcontainer for easy development - [ ] Chatbots menu re-design to handle tools, scheduling, and more You can find our full roadmap [here](https://github.com/orgs/arc53/projects/2). Please don't hesitate to contribute or create issues, it helps us improve DocsGPT! From 385ebe234e1e1c5d3b4e731369a823c544ccaedb Mon Sep 17 00:00:00 2001 From: Pavel Date: Tue, 11 Feb 2025 19:17:59 +0300 Subject: [PATCH 322/354] setup+development-docs --- .../docker-compose.optional.ollama-cpu.yaml | 11 + .../docker-compose.optional.ollama-gpu.yaml | 16 + docs/pages/API/API-docs.md | 350 ------------ docs/pages/API/_meta.json | 10 - .../Deploying/Development-Environment.md | 78 --- .../Deploying/Development-Environment.mdx | 163 ++++++ docs/pages/Deploying/Docker-Deploying.mdx | 135 +++++ docs/pages/Deploying/DocsGPT-Settings.md | 107 ++++ ...-Deploying.md => Kubernetes-Deploying.mdx} | 8 +- docs/pages/Deploying/Railway-Deploying.md | 254 --------- docs/pages/Deploying/_meta.json | 22 +- docs/pages/Extensions/_meta.json | 18 +- .../{API => Extensions}/api-key-guide.md | 0 docs/pages/Extensions/search-widget.md | 0 docs/pages/Models/_meta.json | 14 + docs/pages/Models/cloud-providers.md | 79 +++ docs/pages/Models/embeddings.md | 0 docs/pages/Models/local-inference.md | 0 docs/pages/Tools/_meta.json | 0 docs/pages/Tools/creating.md | 0 docs/pages/Tools/main-rename.md | 0 docs/pages/Tools/requests.md | 0 docs/pages/_meta.json | 16 + .../{Deploying/Quickstart.md => changelog.md} | 3 + docs/pages/index.mdx | 15 +- docs/pages/quickstart.mdx | 84 +++ setup.sh | 533 +++++++++++++++--- 27 files changed, 1110 insertions(+), 806 deletions(-) create mode 100644 deployment/optional/docker-compose.optional.ollama-cpu.yaml create mode 100644 deployment/optional/docker-compose.optional.ollama-gpu.yaml delete mode 100644 docs/pages/API/API-docs.md delete mode 100644 docs/pages/API/_meta.json delete mode 100644 docs/pages/Deploying/Development-Environment.md create mode 100644 docs/pages/Deploying/Development-Environment.mdx create mode 100644 docs/pages/Deploying/Docker-Deploying.mdx create mode 100644 docs/pages/Deploying/DocsGPT-Settings.md rename docs/pages/Deploying/{Kubernetes-Deploying.md => Kubernetes-Deploying.mdx} (92%) delete mode 100644 docs/pages/Deploying/Railway-Deploying.md rename docs/pages/{API => Extensions}/api-key-guide.md (100%) create mode 100644 docs/pages/Extensions/search-widget.md create mode 100644 docs/pages/Models/_meta.json create mode 100644 docs/pages/Models/cloud-providers.md create mode 100644 docs/pages/Models/embeddings.md create mode 100644 docs/pages/Models/local-inference.md create mode 100644 docs/pages/Tools/_meta.json create mode 100644 docs/pages/Tools/creating.md create mode 100644 docs/pages/Tools/main-rename.md create mode 100644 docs/pages/Tools/requests.md create mode 100644 docs/pages/_meta.json rename docs/pages/{Deploying/Quickstart.md => changelog.md} (98%) create mode 100644 docs/pages/quickstart.mdx diff --git a/deployment/optional/docker-compose.optional.ollama-cpu.yaml b/deployment/optional/docker-compose.optional.ollama-cpu.yaml new file mode 100644 index 00000000..d7127314 --- /dev/null +++ b/deployment/optional/docker-compose.optional.ollama-cpu.yaml @@ -0,0 +1,11 @@ +version: "3.8" +services: + ollama: + image: ollama/ollama + ports: + - "11434:11434" + volumes: + - ollama_data:/root/.ollama + +volumes: + ollama_data: \ No newline at end of file diff --git a/deployment/optional/docker-compose.optional.ollama-gpu.yaml b/deployment/optional/docker-compose.optional.ollama-gpu.yaml new file mode 100644 index 00000000..17d79100 --- /dev/null +++ b/deployment/optional/docker-compose.optional.ollama-gpu.yaml @@ -0,0 +1,16 @@ +version: "3.8" +services: + ollama: + image: ollama/ollama + ports: + - "11434:11434" + volumes: + - ollama_data:/root/.ollama + deploy: + resources: + reservations: + devices: + - capabilities: [gpu] + +volumes: + ollama_data: \ No newline at end of file diff --git a/docs/pages/API/API-docs.md b/docs/pages/API/API-docs.md deleted file mode 100644 index a85ed6f8..00000000 --- a/docs/pages/API/API-docs.md +++ /dev/null @@ -1,350 +0,0 @@ -# API Endpoints Documentation - -*Currently, the application provides the following main API endpoints:* - - -### 1. /api/answer -**Description:** - -This endpoint is used to request answers to user-provided questions. - -**Request:** - -**Method**: `POST` - -**Headers**: Content-Type should be set to `application/json; charset=utf-8` - -**Request Body**: JSON object with the following fields: -* `question` — The user's question. -* `history` — (Optional) Previous conversation history. -* `api_key`— Your API key. -* `embeddings_key` — Your embeddings key. -* `active_docs` — The location of active documentation. - -Here is a JavaScript Fetch Request example: -```js -// answer (POST http://127.0.0.1:5000/api/answer) -fetch("http://127.0.0.1:5000/api/answer", { - "method": "POST", - "headers": { - "Content-Type": "application/json; charset=utf-8" - }, - "body": JSON.stringify({"question":"Hi","history":null,"api_key":"OPENAI_API_KEY","embeddings_key":"OPENAI_API_KEY", - "active_docs": "javascript/.project/ES2015/openai_text-embedding-ada-002/"}) -}) -.then((res) => res.text()) -.then(console.log.bind(console)) -``` - -**Response** - -In response, you will get a JSON document containing the `answer`, `query` and `result`: -```json -{ - "answer": "Hi there! How can I help you?\n", - "query": "Hi", - "result": "Hi there! How can I help you?\nSOURCES:" -} -``` - -### 2. /api/docs_check - -**Description:** - -This endpoint will make sure documentation is loaded on the server (just run it every time user is switching between libraries (documentations)). - -**Request:** - -**Method**: `POST` - -**Headers**: Content-Type should be set to `application/json; charset=utf-8` - -**Request Body**: JSON object with the field: -* `docs` — The location of the documentation: -```js -// docs_check (POST http://127.0.0.1:5000/api/docs_check) -fetch("http://127.0.0.1:5000/api/docs_check", { - "method": "POST", - "headers": { - "Content-Type": "application/json; charset=utf-8" - }, - "body": JSON.stringify({"docs":"javascript/.project/ES2015/openai_text-embedding-ada-002/"}) -}) -.then((res) => res.text()) -.then(console.log.bind(console)) -``` - -**Response:** - -In response, you will get a JSON document like this one indicating whether the documentation exists or not: -```json -{ - "status": "exists" -} -``` - - -### 3. /api/combine -**Description:** - -This endpoint provides information about available vectors and their locations with a simple GET request. - -**Request:** - -**Method**: `GET` - -**Response:** - -Response will include: -* `date` -* `description` -* `docLink` -* `fullName` -* `language` -* `location` (local or docshub) -* `model` -* `name` -* `version` - -Example of JSON in Docshub and local: - -image - -### 4. /api/upload -**Description:** - -This endpoint is used to upload a file that needs to be trained, response is JSON with task ID, which can be used to check on task's progress. - -**Request:** - -**Method**: `POST` - -**Request Body**: A multipart/form-data form with file upload and additional fields, including `user` and `name`. - -HTML example: - -```html -
- - - - - - -``` - -**Response:** - -JSON response with a status and a task ID that can be used to check the task's progress. - - -### 5. /api/task_status -**Description:** - -This endpoint is used to get the status of a task (`task_id`) from `/api/upload` - -**Request:** - -**Method**: `GET` - -**Query Parameter**: `task_id` (task ID to check) - -**Sample JavaScript Fetch Request:** -```js -// Task status (Get http://127.0.0.1:5000/api/task_status) -fetch("http://localhost:5001/api/task_status?task_id=YOUR_TASK_ID", { - "method": "GET", - "headers": { - "Content-Type": "application/json; charset=utf-8" - }, -}) -.then((res) => res.text()) -.then(console.log.bind(console)) -``` - -**Response:** - -There are two types of responses: - -1. While the task is still running, the 'current' value will show progress from 0 to 100. - ```json - { - "result": { - "current": 1 - }, - "status": "PROGRESS" - } - ``` - -2. When task is completed: - ```json - { - "result": { - "directory": "temp", - "filename": "install.rst", - "formats": [ - ".rst", - ".md", - ".pdf" - ], - "name_job": "somename", - "user": "local" - }, - "status": "SUCCESS" - } - ``` - -### 6. /api/delete_old -**Description:** - -This endpoint is used to delete old Vector Stores. - -**Request:** - -**Method**: `GET` - -**Query Parameter**: `task_id` - -**Sample JavaScript Fetch Request:** -```js -// delete_old (GET http://127.0.0.1:5000/api/delete_old) -fetch("http://localhost:5001/api/delete_old?task_id=YOUR_TASK_ID", { - "method": "GET", - "headers": { - "Content-Type": "application/json; charset=utf-8" - }, -}) -.then((res) => res.text()) -.then(console.log.bind(console)) - -``` -**Response:** - -JSON response indicating the status of the operation: - -```json -{ "status": "ok" } -``` - -### 7. /api/get_api_keys -**Description:** - -The endpoint retrieves a list of API keys for the user. - -**Request:** - -**Method**: `GET` - -**Sample JavaScript Fetch Request:** -```js -// get_api_keys (GET http://127.0.0.1:5000/api/get_api_keys) -fetch("http://localhost:5001/api/get_api_keys", { - "method": "GET", - "headers": { - "Content-Type": "application/json; charset=utf-8" - }, -}) -.then((res) => res.text()) -.then(console.log.bind(console)) - -``` -**Response:** - -JSON response with a list of created API keys: - -```json -[ - { - "id": "string", - "name": "string", - "key": "string", - "source": "string" - }, - ... - ] -``` - -### 8. /api/create_api_key - -**Description:** - -Create a new API key for the user. - -**Request:** - -**Method**: `POST` - -**Headers**: Content-Type should be set to `application/json; charset=utf-8` - -**Request Body**: JSON object with the following fields: -* `name` — A name for the API key. -* `source` — The source documents that will be used. -* `prompt_id` — The prompt ID. -* `chunks` — The number of chunks used to process an answer. - -Here is a JavaScript Fetch Request example: -```js -// create_api_key (POST http://127.0.0.1:5000/api/create_api_key) -fetch("http://127.0.0.1:5000/api/create_api_key", { - "method": "POST", - "headers": { - "Content-Type": "application/json; charset=utf-8" - }, - "body": JSON.stringify({"name":"Example Key Name", - "source":"Example Source", - "prompt_id":"creative", - "chunks":"2"}) -}) -.then((res) => res.json()) -.then(console.log.bind(console)) -``` - -**Response** - -In response, you will get a JSON document containing the `id` and `key`: -```json -{ - "id": "string", - "key": "string" -} -``` - -### 9. /api/delete_api_key - -**Description:** - -Delete an API key for the user. - -**Request:** - -**Method**: `POST` - -**Headers**: Content-Type should be set to `application/json; charset=utf-8` - -**Request Body**: JSON object with the field: -* `id` — The unique identifier of the API key to be deleted. - -Here is a JavaScript Fetch Request example: -```js -// delete_api_key (POST http://127.0.0.1:5000/api/delete_api_key) -fetch("http://127.0.0.1:5000/api/delete_api_key", { - "method": "POST", - "headers": { - "Content-Type": "application/json; charset=utf-8" - }, - "body": JSON.stringify({"id":"API_KEY_ID"}) -}) -.then((res) => res.json()) -.then(console.log.bind(console)) -``` - -**Response:** - -In response, you will get a JSON document indicating the status of the operation: -```json -{ - "status": "ok" -} -``` \ No newline at end of file diff --git a/docs/pages/API/_meta.json b/docs/pages/API/_meta.json deleted file mode 100644 index 4873d38c..00000000 --- a/docs/pages/API/_meta.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "API-docs": { - "title": "🗂️️ API-docs", - "href": "/API/API-docs" - }, - "api-key-guide": { - "title": "🔐 API Keys guide", - "href": "/API/api-key-guide" - } -} \ No newline at end of file diff --git a/docs/pages/Deploying/Development-Environment.md b/docs/pages/Deploying/Development-Environment.md deleted file mode 100644 index 6a7b010c..00000000 --- a/docs/pages/Deploying/Development-Environment.md +++ /dev/null @@ -1,78 +0,0 @@ -## Development Environments - -### Spin up Mongo and Redis - -For development, only two containers are used from [docker-compose.yaml](https://github.com/arc53/DocsGPT/blob/main/deployment/docker-compose.yaml) (by deleting all services except for Redis and Mongo). -See file [docker-compose-dev.yaml](https://github.com/arc53/DocsGPT/blob/main/deployment/docker-compose-dev.yaml). - -Run - -``` -docker compose -f deployment/docker-compose-dev.yaml build -docker compose -f deployment/docker-compose-dev.yaml up -d -``` - -### Run the Backend - -> [!Note] -> Make sure you have Python 3.12 installed. - -1. Export required environment variables or prepare a `.env` file in the project folder: - - Copy [.env-template](https://github.com/arc53/DocsGPT/blob/main/application/.env-template) and create `.env`. - -(check out [`application/core/settings.py`](application/core/settings.py) if you want to see more config options.) - -2. (optional) Create a Python virtual environment: - You can follow the [Python official documentation](https://docs.python.org/3/tutorial/venv.html) for virtual environments. - -a) On Mac OS and Linux - -```commandline -python -m venv venv -. venv/bin/activate -``` - -b) On Windows - -```commandline -python -m venv venv - venv/Scripts/activate -``` - -3. Download embedding model and save it in the `model/` folder: -You can use the script below, or download it manually from [here](https://d3dg1063dc54p9.cloudfront.net/models/embeddings/mpnet-base-v2.zip), unzip it and save it in the `model/` folder. - -```commandline -wget https://d3dg1063dc54p9.cloudfront.net/models/embeddings/mpnet-base-v2.zip -unzip mpnet-base-v2.zip -d model -rm mpnet-base-v2.zip -``` - -4. Install dependencies for the backend: - -```commandline -pip install -r application/requirements.txt -``` - -5. Run the app using `flask --app application/app.py run --host=0.0.0.0 --port=7091`. -6. Start worker with `celery -A application.app.celery worker -l INFO`. - -> [!Note] -> You can also launch the in a debugger mode in vscode by accessing SHIFT + CMD + D or SHIFT + Windows + D on windows and selecting Flask or Celery. - - -### Start Frontend - -> [!Note] -> Make sure you have Node version 16 or higher. - -1. Navigate to the [/frontend](https://github.com/arc53/DocsGPT/tree/main/frontend) folder. -2. Install the required packages `husky` and `vite` (ignore if already installed). - -```commandline -npm install husky -g -npm install vite -g -``` - -3. Install dependencies by running `npm install --include=dev`. -4. Run the app using `npm run dev`. \ No newline at end of file diff --git a/docs/pages/Deploying/Development-Environment.mdx b/docs/pages/Deploying/Development-Environment.mdx new file mode 100644 index 00000000..2852be19 --- /dev/null +++ b/docs/pages/Deploying/Development-Environment.mdx @@ -0,0 +1,163 @@ +--- +title: Setting Up a Development Environment +description: Guide to setting up a development environment for DocsGPT, including backend and frontend setup. +--- + +# Setting Up a Development Environment + +This guide will walk you through setting up a development environment for DocsGPT. This setup allows you to modify and test the application's backend and frontend components. + +## 1. Spin Up MongoDB and Redis + +For development purposes, you can quickly start MongoDB and Redis containers, which are the primary database and caching systems used by DocsGPT. We provide a dedicated Docker Compose file, `docker-compose-dev.yaml`, located in the `deployment` directory, that includes only these essential services. + +You can find the `docker-compose-dev.yaml` file [here](https://github.com/arc53/DocsGPT/blob/main/deployment/docker-compose-dev.yaml). + +**Steps to start MongoDB and Redis:** + +1. Navigate to the root directory of your DocsGPT repository in your terminal. + +2. Run the following commands to build and start the containers defined in `docker-compose-dev.yaml`: + + ```bash + docker compose -f deployment/docker-compose-dev.yaml build + docker compose -f deployment/docker-compose-dev.yaml up -d + ``` + + These commands will start MongoDB and Redis in detached mode, running in the background. + +## 2. Run the Backend + +To run the DocsGPT backend locally, you'll need to set up a Python environment and install the necessary dependencies. + +**Prerequisites:** + +* **Python 3.12:** Ensure you have Python 3.12 installed on your system. You can check your Python version by running `python --version` or `python3 --version` in your terminal. + +**Steps to run the backend:** + +1. **Configure Environment Variables:** + + DocsGPT backend settings are configured using environment variables. You can set these either in a `.env` file or directly in the `settings.py` file. For a comprehensive overview of all settings, please refer to the [DocsGPT Settings Guide](/Deploying/DocsGPT-Settings). + + * **Option 1: Using a `.env` file (Recommended):** + * If you haven't already, create a file named `.env` in the **root directory** of your DocsGPT project. + * Modify the `.env` file to adjust settings as needed. You can find a comprehensive list of configurable options in [`application/core/settings.py`](application/core/settings.py). + + * **Option 2: Exporting Environment Variables:** + * Alternatively, you can export environment variables directly in your terminal. However, using a `.env` file is generally more organized for development. + +2. **Create a Python Virtual Environment (Optional but Recommended):** + + Using a virtual environment isolates project dependencies and avoids conflicts with system-wide Python packages. + + * **macOS and Linux:** + + ```bash + python -m venv venv + . venv/bin/activate + ``` + + * **Windows:** + + ```bash + python -m venv venv + venv/Scripts/activate + ``` + +3. **Download Embedding Model:** + + The backend requires an embedding model. Download the `mpnet-base-v2` model and place it in the `model/` directory within the project root. You can use the following script: + + ```bash + wget https://d3dg1063dc54p9.cloudfront.net/models/embeddings/mpnet-base-v2.zip + unzip mpnet-base-v2.zip -d model + rm mpnet-base-v2.zip + ``` + + Alternatively, you can manually download the zip file from [here](https://d3dg1063dc54p9.cloudfront.net/models/embeddings/mpnet-base-v2.zip), unzip it, and place the extracted folder in `model/`. + +4. **Install Backend Dependencies:** + + Navigate to the root of your DocsGPT repository and install the required Python packages: + + ```bash + pip install -r application/requirements.txt + ``` + +5. **Run the Flask App:** + + Start the Flask backend application: + + ```bash + flask --app application/app.py run --host=0.0.0.0 --port=7091 + ``` + + This command will launch the backend server, making it accessible on `http://localhost:7091`. + +6. **Start the Celery Worker:** + + Open a new terminal window (and activate your virtual environment if you used one). Start the Celery worker to handle background tasks: + + ```bash + celery -A application.app.celery worker -l INFO + ``` + + This command will start the Celery worker, which processes tasks such as document parsing and vector embedding. + +**Running in Debugger (VSCode):** + +For easier debugging, you can launch the Flask app and Celery worker directly from VSCode's debugger. + +* Press Shift + Cmd + D (macOS) or Shift + Windows + D (Windows) to open the Run and Debug view. +* You should see configurations named "Flask" and "Celery". Select the desired configuration and click the "Start Debugging" button (green play icon). + +## 3. Start the Frontend + +To run the DocsGPT frontend locally, you'll need Node.js and npm (Node Package Manager). + +**Prerequisites:** + +* **Node.js version 16 or higher:** Ensure you have Node.js version 16 or greater installed. You can check your Node.js version by running `node -v` in your terminal. npm is usually bundled with Node.js. + +**Steps to start the frontend:** + +1. **Navigate to the Frontend Directory:** + + In your terminal, change the current directory to the `frontend` folder within your DocsGPT repository: + + ```bash + cd frontend + ``` + +2. **Install Global Packages (If Needed):** + + If you don't have `husky` and `vite` installed globally, you can install them: + + ```bash + npm install husky -g + npm install vite -g + ``` + You can skip this step if you already have these packages installed or prefer to use local installations (though global installation simplifies running the commands in this guide). + +3. **Install Frontend Dependencies:** + + Install the project's frontend dependencies using npm: + + ```bash + npm install --include=dev + ``` + + This command reads the `package.json` file in the `frontend` directory and installs all listed dependencies, including development dependencies. + +4. **Run the Frontend App:** + + Start the frontend development server: + + ```bash + npm run dev + ``` + + This command will start the Vite development server. The frontend application will typically be accessible at [http://localhost:5173/](http://localhost:5173/). The terminal will display the exact URL where the frontend is running. + +With both the backend and frontend running, you should now have a fully functional DocsGPT development environment. You can access the application in your browser at [http://localhost:5173/](http://localhost:5173/) and start developing! \ No newline at end of file diff --git a/docs/pages/Deploying/Docker-Deploying.mdx b/docs/pages/Deploying/Docker-Deploying.mdx new file mode 100644 index 00000000..559fa4e3 --- /dev/null +++ b/docs/pages/Deploying/Docker-Deploying.mdx @@ -0,0 +1,135 @@ +--- +title: Docker Deployment of DocsGPT +description: Deploy DocsGPT using Docker and Docker Compose for easy setup and management. +--- + +# Docker Deployment of DocsGPT + +Docker is the recommended method for deploying DocsGPT, providing a consistent and isolated environment for the application to run. This guide will walk you through deploying DocsGPT using Docker and Docker Compose. + +## Prerequisites + +* **Docker Engine:** You need to have Docker Engine installed on your system. + * **macOS:** [Docker Desktop for Mac](https://docs.docker.com/desktop/install/mac-install/) + * **Linux:** [Docker Engine Installation Guide](https://docs.docker.com/engine/install/) (follow instructions for your specific distribution) + * **Windows:** [Docker Desktop for Windows](https://docs.docker.com/desktop/install/windows-install/) (requires WSL 2 backend, see notes below) +* **Docker Compose:** Docker Compose is usually included with Docker Desktop. If you are using Docker Engine separately, ensure you have Docker Compose V2 installed. + +**Important Note for Windows Users:** Docker Desktop on Windows generally requires the WSL 2 backend to function correctly, especially when using features like host networking which are utilized in DocsGPT's Docker Compose setup. Ensure WSL 2 is enabled and configured in Docker Desktop settings. + +## Quickest Setup: Using DocsGPT Public API + +The fastest way to try out DocsGPT is by using the public API endpoint. This requires minimal configuration and no local LLM setup. + +1. **Clone the DocsGPT Repository (if you haven't already):** + + ```bash + git clone https://github.com/arc53/DocsGPT.git + cd DocsGPT + ``` + +2. **Create a `.env` file:** + + In the root directory of your DocsGPT repository, create a file named `.env`. + +3. **Add Public API Configuration to `.env`:** + + Open the `.env` file and add the following lines: + + ``` + LLM_NAME=docsgpt + VITE_API_STREAMING=true + ``` + + This minimal configuration tells DocsGPT to use the public API. For more advanced settings and other LLM options, refer to the [DocsGPT Settings Guide](/Deploying/DocsGPT-Settings). + +4. **Launch DocsGPT with Docker Compose:** + + Navigate to the root directory of the DocsGPT repository in your terminal and run: + + ```bash + docker compose -f deployment/docker-compose.yaml up -d + ``` + + The `-d` flag runs Docker Compose in detached mode (in the background). + +5. **Access DocsGPT in your browser:** + + Once the containers are running, open your web browser and go to [http://localhost:5173/](http://localhost:5173/). + +6. **Stopping DocsGPT:** + + To stop the application, navigate to the same directory in your terminal and run: + + ```bash + docker compose -f deployment/docker-compose.yaml down + ``` + +## Optional Ollama Setup (Local Models) + +DocsGPT provides optional Docker Compose files to easily integrate with [Ollama](https://ollama.com/) for running local models. These files add an official Ollama container to your Docker Compose setup. These files are located in the `deployment/optional/` directory. + +There are two Ollama optional files: + +* **`docker-compose.optional.ollama-cpu.yaml`**: For running Ollama on CPU. +* **`docker-compose.optional.ollama-gpu.yaml`**: For running Ollama on GPU (requires Docker to be configured for GPU usage). + +### Launching with Ollama and Pulling a Model + +1. **Clone the DocsGPT Repository and Create `.env` (as described above).** + +2. **Launch DocsGPT with Ollama Docker Compose:** + + Choose the appropriate Ollama Compose file (CPU or GPU) and launch DocsGPT: + + **CPU:** + ```bash + docker compose -f deployment/docker-compose.yaml -f deployment/optional/docker-compose.optional.ollama-cpu.yaml up -d + ``` + **GPU:** + ```bash + docker compose -f deployment/docker-compose.yaml -f deployment/optional/docker-compose.optional.ollama-gpu.yaml up -d + ``` + +3. **Pull the Ollama Model:** + + **Crucially, after launching with Ollama, you need to pull the desired model into the Ollama container.** Find the `MODEL_NAME` you configured in your `.env` file (e.g., `llama3.2:1b`). Then execute the following command to pull the model *inside* the running Ollama container: + + ```bash + docker compose -f deployment/docker-compose.yaml -f deployment/optional/docker-compose.optional.ollama-cpu.yaml exec -it ollama ollama pull + ``` + or (for GPU): + ```bash + docker compose -f deployment/docker-compose.yaml -f deployment/optional/docker-compose.optional.ollama-gpu.yaml exec -it ollama ollama pull + ``` + Replace `` with the actual model name from your `.env` file. + +4. **Access DocsGPT in your browser:** + + Once the model is pulled and containers are running, open your web browser and go to [http://localhost:5173/](http://localhost:5173/). + +5. **Stopping Ollama Setup:** + + To stop a DocsGPT setup launched with Ollama optional files, use `docker compose down` and include all the compose files used during the `up` command: + + ```bash + docker compose -f deployment/docker-compose.yaml -f deployment/optional/docker-compose.optional.ollama-cpu.yaml down + ``` + or + + ```bash + docker compose -f deployment/docker-compose.yaml -f deployment/optional/docker-compose.optional.ollama-gpu.yaml down + ``` + +**Important for GPU Usage:** + +* **NVIDIA Container Toolkit (for NVIDIA GPUs):** If you are using NVIDIA GPUs, you need to have the [NVIDIA Container Toolkit](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html) installed and configured on your system for Docker to access your GPU. +* **Docker GPU Configuration:** Ensure Docker is configured to utilize your GPU. Refer to the [Ollama Docker Hub page](https://hub.docker.com/r/ollama/ollama) and Docker documentation for GPU setup instructions specific to your GPU type (NVIDIA, AMD, Intel). + +## Restarting After Configuration Changes + +Whenever you modify the `.env` file or any Docker Compose files, you need to restart the Docker containers for the changes to be applied. Use the same `docker compose down` and `docker compose up -d` commands you used to launch DocsGPT, ensuring you include all relevant `-f` flags for optional files if you are using them. + +## Further Configuration + +This guide covers the basic Docker deployment of DocsGPT. For detailed information on configuring various aspects of DocsGPT, such as LLM providers, models, vector stores, and more, please refer to the comprehensive [DocsGPT Settings Guide](/Deploying/DocsGPT-Settings). \ No newline at end of file diff --git a/docs/pages/Deploying/DocsGPT-Settings.md b/docs/pages/Deploying/DocsGPT-Settings.md new file mode 100644 index 00000000..ce1e46ba --- /dev/null +++ b/docs/pages/Deploying/DocsGPT-Settings.md @@ -0,0 +1,107 @@ +--- +title: DocsGPT Settings +description: Configure your DocsGPT application by understanding the basic settings. +--- + +# DocsGPT Settings + +DocsGPT is highly configurable, allowing you to tailor it to your specific needs and preferences. You can control various aspects of the application, from choosing the Large Language Model (LLM) provider to selecting embedding models and vector stores. + +This document will guide you through the basic settings you can configure in DocsGPT. These settings determine how DocsGPT interacts with LLMs and processes your data. + +## Configuration Methods + +There are two primary ways to configure DocsGPT settings: + +### 1. Configuration via `.env` file (Recommended) + +The easiest and recommended way to configure basic settings is by using a `.env` file. This file should be located in the **root directory** of your DocsGPT project (the same directory where `setup.sh` is located). + +**Example `.env` file structure:** + +``` +LLM_NAME=openai +API_KEY=YOUR_OPENAI_API_KEY +MODEL_NAME=gpt-4o +``` + +### 2. Configuration via `settings.py` file (Advanced) + +For more advanced configurations or if you prefer to manage settings directly in code, you can modify the `settings.py` file. This file is located in the `application/core` directory of your DocsGPT project. + +While modifying `settings.py` offers more flexibility, it's generally recommended to use the `.env` file for basic settings and reserve `settings.py` for more complex adjustments or when you need to configure settings programmatically. + +**Location of `settings.py`:** `application/core/settings.py` + +## Basic Settings Explained + +Here are some of the most fundamental settings you'll likely want to configure: + +- **`LLM_NAME`**: This setting determines which Large Language Model (LLM) provider DocsGPT will use. It tells DocsGPT which API to interact with. + + - **Common values:** + - `docsgpt`: Use the DocsGPT Public API Endpoint (simple and free, as offered in `setup.sh` option 1). + - `openai`: Use OpenAI's API (requires an API key). + - `google`: Use Google's Vertex AI or Gemini models. + - `anthropic`: Use Anthropic's Claude models. + - `groq`: Use Groq's models. + - `huggingface`: Use HuggingFace Inference API. + - `azure_openai`: Use Azure OpenAI Service. + - `openai` (when using local inference engines like Ollama, Llama.cpp, TGI, etc.): This signals DocsGPT to use an OpenAI-compatible API format, even if the actual LLM is running locally. + +- **`MODEL_NAME`**: Specifies the specific model to use from the chosen LLM provider. The available models depend on the `LLM_NAME` you've selected. + + - **Examples:** + - For `LLM_NAME=openai`: `gpt-4o` + - For `LLM_NAME=google`: `gemini-2.0-flash` + - For local models (e.g., Ollama): `llama3.2:1b` (or any model name available in your setup). + +- **`EMBEDDINGS_NAME`**: This setting defines which embedding model DocsGPT will use to generate vector embeddings for your documents. Embeddings are numerical representations of text that allow DocsGPT to understand the semantic meaning of your documents for efficient search and retrieval. + + - **Default value:** `huggingface_sentence-transformers/all-mpnet-base-v2` (a good general-purpose embedding model). + - **Other options:** You can explore other embedding models from Hugging Face Sentence Transformers or other providers if needed. + +- **`API_KEY`**: Required for most cloud-based LLM providers. This is your authentication key to access the LLM provider's API. You'll need to obtain this key from your chosen provider's platform. + +- **`OPENAI_BASE_URL`**: Specifically used when `LLM_NAME` is set to `openai` but you are connecting to a local inference engine (like Ollama, Llama.cpp, etc.) that exposes an OpenAI-compatible API. This setting tells DocsGPT where to find your local LLM server. + +## Configuration Examples + +Let's look at some concrete examples of how to configure these settings in your `.env` file. + +### Example for Cloud API Provider (OpenAI) + +To use OpenAI's `gpt-4o` model, you would configure your `.env` file like this: + +``` +LLM_NAME=openai +API_KEY=YOUR_OPENAI_API_KEY # Replace with your actual OpenAI API key +MODEL_NAME=gpt-4o +``` + +Make sure to replace `YOUR_OPENAI_API_KEY` with your actual OpenAI API key. + +### Example for Local Deployment + +To use a local Ollama server with the `llama3.2:1b` model, you would configure your `.env` file like this: + +``` +LLM_NAME=openai # Using OpenAI compatible API format for local models +API_KEY=None # API Key is not needed for local Ollama +MODEL_NAME=llama3.2:1b +OPENAI_BASE_URL=http://host.docker.internal:11434/v1 # Default Ollama API URL within Docker +EMBEDDINGS_NAME=huggingface_sentence-transformers/all-mpnet-base-v2 # You can also run embeddings locally if needed +``` + +In this case, even though you are using Ollama locally, `LLM_NAME` is set to `openai` because Ollama (and many other local inference engines) are designed to be API-compatible with OpenAI. `OPENAI_BASE_URL` points DocsGPT to the local Ollama server. + +## Exploring More Settings + +These are just the basic settings to get you started. The `settings.py` file contains many more advanced options that you can explore to further customize DocsGPT, such as: + +- Vector store configuration (`VECTOR_STORE`, Qdrant, Milvus, LanceDB settings) +- Retriever settings (`RETRIEVERS_ENABLED`) +- Cache settings (`CACHE_REDIS_URL`) +- And many more! + +For a complete list of available settings and their descriptions, refer to the `settings.py` file in `application/core`. Remember to restart your Docker containers after making changes to your `.env` file or `settings.py` for the changes to take effect. \ No newline at end of file diff --git a/docs/pages/Deploying/Kubernetes-Deploying.md b/docs/pages/Deploying/Kubernetes-Deploying.mdx similarity index 92% rename from docs/pages/Deploying/Kubernetes-Deploying.md rename to docs/pages/Deploying/Kubernetes-Deploying.mdx index 7e8768cf..8f1c8f7a 100644 --- a/docs/pages/Deploying/Kubernetes-Deploying.md +++ b/docs/pages/Deploying/Kubernetes-Deploying.mdx @@ -1,4 +1,10 @@ -# Self-hosting DocsGPT on Kubernetes +--- +title: Deploying DocsGPT on Kubernetes +description: Learn how to self-host DocsGPT on a Kubernetes cluster for scalable and robust deployments. +--- + +# Self-hosting DocsGPT + on Kubernetes This guide will walk you through deploying DocsGPT on Kubernetes. diff --git a/docs/pages/Deploying/Railway-Deploying.md b/docs/pages/Deploying/Railway-Deploying.md deleted file mode 100644 index 770da2f4..00000000 --- a/docs/pages/Deploying/Railway-Deploying.md +++ /dev/null @@ -1,254 +0,0 @@ - -# Self-hosting DocsGPT on Railway - - - -Here's a step-by-step guide on how to host DocsGPT on Railway App. - - - -At first Clone and set up the project locally to run , test and Modify. - - - -### 1. Clone and GitHub SetUp - -a. Open Terminal (Windows Shell or Git bash(recommended)). - - - -b. Type `git clone https://github.com/arc53/DocsGPT.git` - - - -#### Download the package information - - - -Once it has finished cloning the repository, it is time to download the package information from all sources. To do so, simply enter the following command: - - - -`sudo apt update` - - - -#### Install Docker and Docker Compose - - - -DocsGPT backend and worker use Python, Frontend is written on React and the whole application is containerized using Docker. To install Docker and Docker Compose, enter the following commands: - - - -`sudo apt install docker.io` - - - -And now install docker-compose: - - - -`sudo apt install docker-compose` - - - -#### Access the DocsGPT Folder - - - -Enter the following command to access the folder in which the DocsGPT docker-compose file is present. - - - -`cd DocsGPT/` - - - -#### Prepare the Environment - - - -Inside the DocsGPT folder create a `.env` file and copy the contents of `.env_sample` into it. - - - -`nano .env` - - - -Make sure your `.env` file looks like this: - - - -``` - -OPENAI_API_KEY=(Your OpenAI API key) - -VITE_API_STREAMING=true - -SELF_HOSTED_MODEL=false - -``` - - - -To save the file, press CTRL+X, then Y, and then ENTER. - - - -Next, set the correct IP for the Backend by opening the docker-compose.yaml file: - - - -`nano deployment/docker-compose.yaml` - - - -And Change line 7 to: `VITE_API_HOST=http://localhost:7091` - -to this `VITE_API_HOST=http://:7091` - - - -This will allow the frontend to connect to the backend. - - - -#### Running the Application - - - -You're almost there! Now that all the necessary bits and pieces have been installed, it is time to run the application. To do so, use the following command: - - - -`sudo docker compose -f deployment/docker-compose.yaml up -d` - - - -Launching it for the first time will take a few minutes to download all the necessary dependencies and build. - - - -Once this is done you can go ahead and close the terminal window. - - - -### 2. Pushing it to your own Repository - - - -a. Create a Repository on your GitHub. - - - -b. Open Terminal in the same directory of the Cloned project. - - - -c. Type `git init` - - - -d. `git add .` - - - -e. `git commit -m "first-commit"` - - - -f. `git remote add origin ` - - - -g. `git push git push --set-upstream origin master` - -Your local files will now be pushed to your GitHub Account. :) - - -### 3. Create a Railway Account: - - - -If you haven't already, create or log in to your railway account do it by visiting [Railway](https://railway.app/) - - - -Signup via **GitHub** [Recommended]. - - - -### 4. Start New Project: - - - -a. Open Railway app and Click on "Start New Project." - - - -b. Choose any from the list of options available (Recommended "**Deploy from GitHub Repo**") - - - -c. Choose the required Repository from your GitHub. - - - -d. Configure and allow access to modify your GitHub content from the pop-up window. - - - -e. Agree to all the terms and conditions. - - - -PS: It may take a few minutes for the account setup to complete. - - - -#### You will get A free trial of $5 (use it for trial and then purchase if satisfied and needed) - - - -### 5. Connecting to Your newly Railway app with GitHub - - - -a. Choose DocsGPT repo from the list of your GitHub repository that you want to deploy now. - - - -b. Click on Deploy now. - - - -![Three Tabs will be there](/Railway-selection.png) - - - -c. Select Variables Tab. - - - -d. Upload the env file here that you used for local setup. - - - -e. Go to Settings Tab now. - - - -f. Go to "Networking" and click on Generate Domain Name, to get the URL of your hosted project. - - - -g. You can update the Root directory, build command, installation command as per need. - -*[However recommended not the disturb these options and leave them as default if not that needed.]* - - - - -Your own DocsGPT is now available at the Generated domain URl. :) diff --git a/docs/pages/Deploying/_meta.json b/docs/pages/Deploying/_meta.json index d01e1f67..e3ee7485 100644 --- a/docs/pages/Deploying/_meta.json +++ b/docs/pages/Deploying/_meta.json @@ -1,22 +1,22 @@ { - "Hosting-the-app": { - "title": "☁️ Hosting DocsGPT", - "href": "/Deploying/Hosting-the-app" + "DocsGPT-Settings": { + "title": "⚙️ App Configuration", + "href": "/Deploying/DocsGPT-Settings" }, - "Quickstart": { - "title": "⚡️Quickstart", - "href": "/Deploying/Quickstart" + "Docker-Deploying": { + "title": "🛳️ Docker Setup", + "href": "/Deploying/Docker-Deploying" }, "Development-Environment": { "title": "🛠️Development Environment", "href": "/Deploying/Development-Environment" }, - "Railway-Deploying": { - "title": "🚂Deploying on Railway", - "href": "/Deploying/Railway-Deploying" - }, "Kubernetes-Deploying": { - "title": "☸️Deploying on Kubernetes", + "title": "☸️ Deploying on Kubernetes", "href": "/Deploying/Kubernetes-Deploying" + }, + "Hosting-the-app": { + "title": "☁️ Hosting DocsGPT", + "href": "/Deploying/Hosting-the-app" } } \ No newline at end of file diff --git a/docs/pages/Extensions/_meta.json b/docs/pages/Extensions/_meta.json index 270367de..6a28a60e 100644 --- a/docs/pages/Extensions/_meta.json +++ b/docs/pages/Extensions/_meta.json @@ -1,14 +1,22 @@ { - "Chatwoot-extension": { - "title": "💬️ Chatwoot Extension", - "href": "/Extensions/Chatwoot-extension" + "api-key-guide": { + "title": "🔑 Getting API key", + "href": "/Extensions/api-key-guide" }, "react-widget": { - "title": "🏗️ Widget setup", - "href": "/Extensions/react-widget" + "title": "💬️ Chat Widget", + "href": "/Extensions/chat-widget" + }, + "search-widget": { + "title": "🔎 Search Widget", + "href": "/Extensions/search-widget" }, "Chrome-extension": { "title": "🌐 Chrome Extension", "href": "/Extensions/Chrome-extension" + }, + "Chatwoot-extension": { + "title": "🗣️ Chatwoot Extension", + "href": "/Extensions/Chatwoot-extension" } } \ No newline at end of file diff --git a/docs/pages/API/api-key-guide.md b/docs/pages/Extensions/api-key-guide.md similarity index 100% rename from docs/pages/API/api-key-guide.md rename to docs/pages/Extensions/api-key-guide.md diff --git a/docs/pages/Extensions/search-widget.md b/docs/pages/Extensions/search-widget.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/pages/Models/_meta.json b/docs/pages/Models/_meta.json new file mode 100644 index 00000000..d1256cd1 --- /dev/null +++ b/docs/pages/Models/_meta.json @@ -0,0 +1,14 @@ +{ + "cloud-providers": { + "title": "☁️ Cloud Providers", + "href": "/Models/cloud-providers" + }, + "local-inference": { + "title": "🖥️ Local Inference", + "href": "/Models/local-inference" + }, + "embeddings": { + "title": "📝 Embeddings", + "href": "/Models/embeddings" + } +} \ No newline at end of file diff --git a/docs/pages/Models/cloud-providers.md b/docs/pages/Models/cloud-providers.md new file mode 100644 index 00000000..8097c777 --- /dev/null +++ b/docs/pages/Models/cloud-providers.md @@ -0,0 +1,79 @@ +--- +title: Connecting DocsGPT to LLM Providers +description: Explore the different Large Language Model (LLM) providers you can connect to DocsGPT, from cloud APIs to local inference engines. +--- + +# Connecting DocsGPT to LLM Providers + +DocsGPT is designed to be flexible and work with a variety of Large Language Model (LLM) providers. Whether you prefer the simplicity of a public API, the power of cloud-based models, or the control of local inference engines, DocsGPT can be configured to meet your needs. + +This guide will introduce you to the LLM providers that DocsGPT natively supports and explain how to connect to them. + +## Supported LLM Providers + +DocsGPT offers built-in support for the following LLM providers, selectable during the `setup.sh` script execution: + +**Cloud API Providers:** + +* **DocsGPT Public API** +* **OpenAI** +* **Google (Vertex AI, Gemini)** +* **Anthropic (Claude)** +* **Groq** +* **HuggingFace Inference API** +* **Azure OpenAI** + +## Configuration via `.env` file + +Connecting DocsGPT to an LLM provider is primarily configured through environment variables set in the `.env` file located in the root directory of your DocsGPT project. + +**Basic Configuration Parameters:** + +* **`LLM_NAME`**: This setting is crucial and specifies the provider you want to use. The values correspond to the provider names listed above (e.g., `docsgpt`, `openai`, `google`, `ollama`, etc.). +* **`MODEL_NAME`**: Determines the specific model to be used from the chosen provider (e.g., `gpt-4o`, `gemini-2.0-flash`, `llama3.2:1b`). Refer to the provider's documentation for available model names. +* **`API_KEY`**: Required for most cloud API providers. Obtain this key from your provider's platform and set it in the `.env` file. +* **`OPENAI_BASE_URL`**: Specifically used when connecting to a local inference engine that is OpenAI API compatible. This setting points DocsGPT to the address of your local server. + +## Configuration Examples + +Here are examples of `.env` configurations for different LLM providers. + +**Example for OpenAI:** + +To use OpenAI's `gpt-4o` model, your `.env` file would look like this: + +``` +LLM_NAME=openai +API_KEY=YOUR_OPENAI_API_KEY # Replace with your actual OpenAI API key +MODEL_NAME=gpt-4o +``` + +**Example for Local Ollama:** + +To connect to a local Ollama instance running `llama3.2:1b`, configure your `.env` as follows: + +``` +LLM_NAME=openai # Using OpenAI compatible API format for local models +API_KEY=None # API Key is not needed for local Ollama +MODEL_NAME=llama3.2:1b +OPENAI_BASE_URL=http://host.docker.internal:11434/v1 # Default Ollama API URL within Docker +``` + +**Example for OpenAI-Compatible API (DeepSeek):** + +Many LLM providers offer APIs that are compatible with the OpenAI API format. DeepSeek is one such example. To connect to DeepSeek, you would still use `LLM_NAME=openai` and point `OPENAI_BASE_URL` to the DeepSeek API endpoint. + +``` +LLM_NAME=openai +API_KEY=YOUR_DEEPSEEK_API_KEY # Your DeepSeek API key +MODEL_NAME=deepseek-chat # Or your desired DeepSeek model name +OPENAI_BASE_URL=https://api.deepseek.com/v1 # DeepSeek API base URL +``` + +**Important Note:** When using OpenAI-compatible APIs, you might need to adjust other settings as well, depending on the specific API's requirements. Always consult the provider's API documentation and the [DocsGPT Settings Guide](/Deploying/DocsGPT-Settings) for detailed configuration options. + +## Exploring More Providers and Advanced Settings + +The providers listed above are those with direct support in `setup.sh`. However, DocsGPT's flexible design allows you to connect to virtually any LLM provider that offers an API, especially those compatible with the OpenAI API standard. + +For a comprehensive list of all configurable settings, including advanced options for each provider and details on how to connect to other LLMs, please refer to the [DocsGPT Settings Guide](/Deploying/DocsGPT-Settings). This guide provides in-depth information on customizing your DocsGPT setup to work with a wide range of LLM providers and tailor the application to your specific needs. \ No newline at end of file diff --git a/docs/pages/Models/embeddings.md b/docs/pages/Models/embeddings.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/pages/Models/local-inference.md b/docs/pages/Models/local-inference.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/pages/Tools/_meta.json b/docs/pages/Tools/_meta.json new file mode 100644 index 00000000..e69de29b diff --git a/docs/pages/Tools/creating.md b/docs/pages/Tools/creating.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/pages/Tools/main-rename.md b/docs/pages/Tools/main-rename.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/pages/Tools/requests.md b/docs/pages/Tools/requests.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/pages/_meta.json b/docs/pages/_meta.json new file mode 100644 index 00000000..aa2f4367 --- /dev/null +++ b/docs/pages/_meta.json @@ -0,0 +1,16 @@ + +{ + "index": "Home", + "quickstart": "Quickstart", + "Deploying": "Deploying", + "Models": "Models", + "Tools": "Tools", + "Extensions": "Extensions", + "https://gptcloud.arc53.com/": { + "title": "API", + "href": "https://gptcloud.arc53.com/", + "newWindow": true + }, + "Guides": "Guides", + "changelog": "Changelog" +} \ No newline at end of file diff --git a/docs/pages/Deploying/Quickstart.md b/docs/pages/changelog.md similarity index 98% rename from docs/pages/Deploying/Quickstart.md rename to docs/pages/changelog.md index cebf6bd7..7479c1cb 100644 --- a/docs/pages/Deploying/Quickstart.md +++ b/docs/pages/changelog.md @@ -1,3 +1,6 @@ +--- +title: 'Changelog' +--- ## Launching Web App **Note**: Make sure you have Docker installed diff --git a/docs/pages/index.mdx b/docs/pages/index.mdx index 423a0a01..4c617870 100644 --- a/docs/pages/index.mdx +++ b/docs/pages/index.mdx @@ -4,22 +4,19 @@ title: 'Home' import { Cards, Card } from 'nextra/components' import Image from 'next/image' import deployingGuides from './Deploying/_meta.json'; -import developingGuides from './API/_meta.json'; import extensionGuides from './Extensions/_meta.json'; import mainGuides from './Guides/_meta.json'; - - export const allGuides = { ...deployingGuides, - ...developingGuides, + ...extensionGuides, ...mainGuides, }; -### **DocsGPT 🦖** +## **DocsGPT 🦖** DocsGPT 🦖 is an innovative open-source tool designed to simplify the retrieval of information from project documentation using advanced GPT models 🤖. Eliminate lengthy manual searches 🔍 and enhance your documentation experience with DocsGPT, and consider contributing to its AI-powered future 🚀. @@ -33,6 +30,14 @@ DocsGPT 🦖 is an innovative open-source tool designed to simplify the retrieva Try it yourself: [https://www.docsgpt.cloud/](https://www.docsgpt.cloud/) +### Features: +- **Chat** - Ask any question on your mind and get instant answers +- **Bots** - Easily connect DocsGPT's *brain* to widgets and chatbots +- **Tools** - Build complex workflows with model performing all actions +- **RAG** - Connect any document to give model reliable knowledge +- **Open source** - Customize anything with clear codebase +- **Platform Agnostic** - Connect any LLM that suits your needs + ( diff --git a/docs/pages/quickstart.mdx b/docs/pages/quickstart.mdx new file mode 100644 index 00000000..cef1cd68 --- /dev/null +++ b/docs/pages/quickstart.mdx @@ -0,0 +1,84 @@ +--- +title: Quickstart - Launching DocsGPT Web App +description: Get started with DocsGPT quickly by launching the web application using the setup script. +--- + +# Quickstart + +**Prerequisites:** + +* **Docker:** Ensure you have Docker installed and running on your system. + +## Launching DocsGPT (macOS and Linux) + +The easiest way to launch DocsGPT is using the provided `setup.sh` script. This script automates the configuration process and offers several setup options. + +**Steps:** + +1. **Download the DocsGPT Repository:** + + First, you need to download the DocsGPT repository to your local machine. You can do this using Git: + + ```bash + git clone https://github.com/arc53/DocsGPT.git + cd DocsGPT + ``` + +2. **Run the `setup.sh` script:** + + Navigate to the DocsGPT directory in your terminal and execute the `setup.sh` script: + + ```bash + ./setup.sh + ``` + +3. **Follow the interactive setup:** + + The `setup.sh` script will guide you through an interactive menu with the following options: + + ``` + Welcome to DocsGPT Setup! + How would you like to proceed? + 1) Use DocsGPT Public API Endpoint (simple and free) + 2) Serve Local (with Ollama) + 3) Connect Local Inference Engine + 4) Connect Cloud API Provider + Choose option (1-4): + ``` + + Let's break down each option: + + * **1) Use DocsGPT Public API Endpoint (simple and free):** This is the simplest option to get started. It utilizes the DocsGPT public API, requiring no API keys or local model downloads. Choose this for a quick and easy setup. + + * **2) Serve Local (with Ollama):** This option allows you to run a Large Language Model locally using [Ollama](https://ollama.com/). You'll be prompted to choose between CPU or GPU for Ollama and select a model to download. This is a good option for local processing and experimentation. + + * **3) Connect Local Inference Engine:** If you are already running a local inference engine like Llama.cpp, Text Generation Inference (TGI), vLLM, or others, choose this option. You'll be asked to select your engine and provide the necessary connection details. This is for users with existing local LLM infrastructure. + + * **4) Connect Cloud API Provider:** This option lets you connect DocsGPT to a commercial Cloud API provider such as OpenAI, Google (Vertex AI/Gemini), Anthropic (Claude), Groq, HuggingFace Inference API, or Azure OpenAI. You will need an API key from your chosen provider. Select this if you prefer to use a powerful cloud-based LLM. + + After selecting an option and providing any required information (like API keys or model names), the script will configure your `.env` file and start DocsGPT using Docker Compose. + +4. **Access DocsGPT in your browser:** + + Once the setup is complete and Docker containers are running, navigate to [http://localhost:5173/](http://localhost:5173/) in your web browser to access the DocsGPT web application. + +5. **Stopping DocsGPT:** + + To stop DocsGPT, simply open a new terminal in the `DocsGPT` directory and run: + + ```bash + docker compose -f deployment/docker-compose.yaml down + ``` + (or the specific `docker compose` command shown at the end of the `setup.sh` execution, which may include optional compose files depending on your choices). + +## Launching DocsGPT (Windows) + +For Windows users, we recommend following the Docker deployment guide for detailed instructions. Please refer to the [Docker Deployment documentation](/Deploying/Docker-Deploying) for step-by-step instructions on setting up DocsGPT on Windows using Docker. + +**Important for Windows:** Ensure Docker Desktop is installed and running correctly on your Windows system before proceeding. + +## Advanced Configuration + +For more advanced customization of DocsGPT settings, such as configuring vector stores, embedding models, and other parameters, please refer to the [DocsGPT Settings documentation](/Deploying/DocsGPT-Settings). This guide explains how to modify the `.env` file or `settings.py` for deeper configuration. + +Enjoy using DocsGPT! \ No newline at end of file diff --git a/setup.sh b/setup.sh index 3b2c7855..7775e24e 100755 --- a/setup.sh +++ b/setup.sh @@ -1,14 +1,79 @@ #!/bin/bash -# Function to prompt the user for their choice -prompt_user() { - echo "Do you want to:" - echo "1. Use DocsGPT public API (simple and free)" - echo "2. Download the language model locally (12GB)" - echo "3. Use the OpenAI API (requires an API key)" - read -p "Enter your choice (1, 2 or 3): " choice +# Color codes +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +DEFAULT_FG='\033[39m' +RED='\033[0;31m' +NC='\033[0m' +BOLD='\033[1m' + +# Base Compose file (relative to script location) +COMPOSE_FILE="$(dirname "$(readlink -f "$0")")/deployment/docker-compose.yaml" +ENV_FILE="$(dirname "$(readlink -f "$0")")/.env" + +# Animation function +animate_dino() { + tput civis # Hide cursor + local dino_lines=( + " ######### " + " ############# " + " ##################" + " ####################" + " ######################" + " ####################### ######" + " ############################### " + " ################################## " + " ################ ############ " + " ################## ########## " + " ##################### ######## " + " ###################### ###### ### " + " ############ ########## #### ## " + " ############# ######### ##### " + " ############## ######### " + " ############## ########## " + "############ ####### " + " ###### ###### #### " + " ################ " + " ################# " + ) + + # Static DocsGPT text + local static_text=( + " ____ ____ ____ _____ " + " | _ \\ ___ ___ ___ / ___| _ \\_ _|" + " | | | |/ _ \\ / __/ __| | _| |_) || | " + " | |_| | (_) | (__\\__ \\ |_| | __/ | | " + " |____/ \\___/ \\___|___/\\____|_| |_| " + " " + ) + + # Print static text + clear + for line in "${static_text[@]}"; do + echo "$line" + done + + tput sc + + # Build-up animation + for i in "${!dino_lines[@]}"; do + tput rc + for ((j=0; j<=i; j++)); do + echo "${dino_lines[$j]}" + done + sleep 0.05 + done + + sleep 0.5 + + tput rc + tput ed + + tput cnorm } +# Check and start Docker function check_and_start_docker() { # Check if Docker is running if ! docker info > /dev/null 2>&1; then @@ -31,110 +96,394 @@ check_and_start_docker() { # Wait for Docker to be fully operational with animated dots echo -n "Waiting for Docker to start" while ! docker system info > /dev/null 2>&1; do - for _ in {1..3}; do + for i in {1..3}; do echo -n "." sleep 1 done - echo -ne "\rWaiting for Docker to start " # Reset to overwrite previous dots + echo -ne "\rWaiting for Docker to start " done echo -e "\nDocker has started!" fi } -# Function to handle the choice to download the model locally -download_locally() { - echo "LLM_NAME=llama.cpp" > .env - echo "VITE_API_STREAMING=true" >> .env - echo "EMBEDDINGS_NAME=huggingface_sentence-transformers/all-mpnet-base-v2" >> .env - echo "The .env file has been created with LLM_NAME set to llama.cpp." - - # Creating the directory if it does not exist - mkdir -p models - - # Downloading the model to the specific directory - echo "Downloading the model..." - # check if docsgpt-7b-f16.gguf does not exist - if [ ! -f models/docsgpt-7b-f16.gguf ]; then - echo "Downloading the model..." - wget -P models https://d3dg1063dc54p9.cloudfront.net/models/docsgpt-7b-f16.gguf - echo "Model downloaded to models directory." - else - echo "Model already exists." - fi - - # Call the function to check and start Docker if needed - check_and_start_docker - - docker compose -f deployment/docker-compose-local.yaml build && docker compose -f deployment/docker-compose-local.yaml up -d - #python -m venv venv - #source venv/bin/activate - pip install -r application/requirements.txt - pip install llama-cpp-python - pip install sentence-transformers - export LLM_NAME=llama.cpp - export EMBEDDINGS_NAME=huggingface_sentence-transformers/all-mpnet-base-v2 - export FLASK_APP=application/app.py - export FLASK_DEBUG=true - export CELERY_BROKER_URL=redis://localhost:6379/0 - export CELERY_RESULT_BACKEND=redis://localhost:6379/1 - echo "The application is now running on http://localhost:5173" - echo "You can stop the application by running the following command:" - echo "Ctrl + C and then" - echo "Then pkill -f 'flask run' and then" - echo "docker compose down" - flask run --host=0.0.0.0 --port=7091 & - celery -A application.app.celery worker -l INFO +# Function to prompt the user for the main menu choice +prompt_main_menu() { + echo -e "\n${DEFAULT_FG}${BOLD}Welcome to DocsGPT Setup!${NC}" + echo -e "${DEFAULT_FG}How would you like to proceed?${NC}" + echo -e "${YELLOW}1) Use DocsGPT Public API Endpoint (simple and free)${NC}" + echo -e "${YELLOW}2) Serve Local (with Ollama)${NC}" + echo -e "${YELLOW}3) Connect Local Inference Engine${NC}" + echo -e "${YELLOW}4) Connect Cloud API Provider${NC}" + echo + read -p "$(echo -e "${DEFAULT_FG}Choose option (1-4): ${NC}")" main_choice } -# Function to handle the choice to use the OpenAI API -use_openai() { - read -p "Please enter your OpenAI API key: " api_key - echo "API_KEY=$api_key" > .env - echo "LLM_NAME=openai" >> .env - echo "VITE_API_STREAMING=true" >> .env - echo "The .env file has been created with API_KEY set to your provided key." - - # Call the function to check and start Docker if needed - check_and_start_docker - - docker compose -f deployment/docker-compose.yaml build && docker compose -f deployment/docker-compose.yaml up -d - - echo "The application will run on http://localhost:5173" - echo "You can stop the application by running the following command:" - echo "docker compose down" +# Function to prompt for Local Inference Engine options +prompt_local_inference_engine_options() { + clear + echo -e "\n${DEFAULT_FG}${BOLD}Connect Local Inference Engine${NC}" + echo -e "${DEFAULT_FG}Choose your local inference engine:${NC}" + echo -e "${YELLOW}1) LLaMa.cpp${NC}" + echo -e "${YELLOW}2) Ollama${NC}" + echo -e "${YELLOW}3) Text Generation Inference (TGI)${NC}" + echo -e "${YELLOW}4) SGLang${NC}" + echo -e "${YELLOW}5) vLLM${NC}" + echo -e "${YELLOW}6) Aphrodite${NC}" + echo -e "${YELLOW}7) FriendliAI${NC}" + echo -e "${YELLOW}8) LMDeploy${NC}" + echo -e "${YELLOW}b) Back to Main Menu${NC}" + echo + read -p "$(echo -e "${DEFAULT_FG}Choose option (1-8, or b): ${NC}")" engine_choice } -use_docsgpt() { +# Function to prompt for Cloud API Provider options +prompt_cloud_api_provider_options() { + clear + echo -e "\n${DEFAULT_FG}${BOLD}Connect Cloud API Provider${NC}" + echo -e "${DEFAULT_FG}Choose your Cloud API Provider:${NC}" + echo -e "${YELLOW}1) OpenAI${NC}" + echo -e "${YELLOW}2) Google (Vertex AI, Gemini)${NC}" + echo -e "${YELLOW}3) Anthropic (Claude)${NC}" + echo -e "${YELLOW}4) Groq${NC}" + echo -e "${YELLOW}5) HuggingFace Inference API${NC}" + echo -e "${YELLOW}6) Azure OpenAI${NC}" + echo -e "${YELLOW}b) Back to Main Menu${NC}" + echo + read -p "$(echo -e "${DEFAULT_FG}Choose option (1-6, or b): ${NC}")" provider_choice +} + +# Function to prompt for Ollama CPU/GPU options +prompt_ollama_options() { + clear + echo -e "\n${DEFAULT_FG}${BOLD}Serve Local with Ollama${NC}" + echo -e "${DEFAULT_FG}Choose how to serve Ollama:${NC}" + echo -e "${YELLOW}1) CPU${NC}" + echo -e "${YELLOW}2) GPU${NC}" + echo -e "${YELLOW}b) Back to Main Menu${NC}" + echo + read -p "$(echo -e "${DEFAULT_FG}Choose option (1-2, or b): ${NC}")" ollama_choice +} + +# 1) Use DocsGPT Public API Endpoint (simple and free) +use_docs_public_api_endpoint() { + echo -e "\n${NC}Setting up DocsGPT Public API Endpoint...${NC}" echo "LLM_NAME=docsgpt" > .env echo "VITE_API_STREAMING=true" >> .env - echo "The .env file has been created with API_KEY set to your provided key." + echo -e "${GREEN}.env file configured for DocsGPT Public API.${NC}" - # Call the function to check and start Docker if needed check_and_start_docker - docker compose -f deployment/docker-compose.yaml build && docker compose -f deployment/docker-compose.yaml up -d + echo -e "\n${NC}Starting Docker Compose...${NC}" + docker compose --env-file "${ENV_FILE}" -f "${COMPOSE_FILE}" build && docker compose --env-file "${ENV_FILE}" -f "${COMPOSE_FILE}" up -d + docker_compose_status=$? # Capture exit status of docker compose - echo "The application will run on http://localhost:5173" - echo "You can stop the application by running the following command:" - echo "docker compose down" + echo "Docker Compose Exit Status: $docker_compose_status" + + if [ "$docker_compose_status" -ne 0 ]; then + echo -e "\n${RED}${BOLD}Error starting Docker Compose. Please ensure Docker Compose is installed and in your PATH.${NC}" + echo -e "${RED}Refer to Docker documentation for installation instructions: https://docs.docker.com/compose/install/${NC}" + exit 1 # Indicate failure and EXIT SCRIPT + fi + + echo -e "\n${GREEN}DocsGPT is now running on http://localhost:5173${NC}" + echo -e "${YELLOW}You can stop the application by running: docker compose -f \"${COMPOSE_FILE}\" down${NC}" } -# Prompt the user for their choice -prompt_user +# 2) Serve Local (with Ollama) +serve_local_ollama() { + local ollama_choice model_name + local docker_compose_file_suffix + local model_name_prompt + local default_model="llama3.2:1b" -# Handle the user's choice -case $choice in - 1) - use_docsgpt - ;; - 2) - download_locally - ;; - 3) - use_openai - ;; - *) - echo "Invalid choice. Please choose either 1 or 2." - ;; -esac + get_model_name_ollama() { + read -p "$(echo -e "${DEFAULT_FG}Enter Ollama Model Name (leave empty for default: ${default_model} (1.3GB)): ${NC}")" model_name_input + if [ -z "$model_name_input" ]; then + model_name="$default_model" # Set default model if input is empty + else + model_name="$model_name_input" # Use user-provided model name + fi + } + + + while true; do + clear + prompt_ollama_options + case "$ollama_choice" in + 1) # CPU + docker_compose_file_suffix="cpu" + get_model_name_ollama + break ;; + 2) # GPU + echo -e "\n${YELLOW}For this option to work correctly you need to have a supported GPU and configure Docker to utilize it.${NC}" + echo -e "${YELLOW}Refer to: https://hub.docker.com/r/ollama/ollama for more information.${NC}" + read -p "$(echo -e "${DEFAULT_FG}Continue with GPU setup? (y/b): ${NC}")" confirm_gpu + case "$confirm_gpu" in + y|Y) + docker_compose_file_suffix="gpu" + get_model_name_ollama + break ;; + b|B) clear; return ;; # Back to Main Menu + *) echo -e "\n${RED}Invalid choice. Please choose y or b.${NC}" ; sleep 1 ;; + esac + ;; + b|B) clear; return ;; # Back to Main Menu + *) echo -e "\n${RED}Invalid choice. Please choose 1-2, or b.${NC}" ; sleep 1 ;; + esac + done + + + echo -e "\n${NC}Configuring for Ollama ($(echo "$docker_compose_file_suffix" | tr '[:lower:]' '[:upper:]'))...${NC}" # Using tr for uppercase - more compatible + echo "API_KEY=xxxx" > .env # Placeholder API Key + echo "LLM_NAME=openai" >> .env + echo "MODEL_NAME=$model_name" >> .env + echo "VITE_API_STREAMING=true" >> .env + echo "OPENAI_BASE_URL=http://host.docker.internal:11434/v1" >> .env + echo "EMBEDDINGS_NAME=huggingface_sentence-transformers/all-mpnet-base-v2" >> .env + echo -e "${GREEN}.env file configured for Ollama ($(echo "$docker_compose_file_suffix" | tr '[:lower:]' '[:upper:]')${NC}${GREEN}).${NC}" + echo -e "${YELLOW}Note: MODEL_NAME is set to '${BOLD}$model_name${NC}${YELLOW}'. You can change it later in the .env file.${NC}" + + + check_and_start_docker + local compose_files=( + -f "${COMPOSE_FILE}" + -f "$(dirname "${COMPOSE_FILE}")/optional/docker-compose.optional.ollama-${docker_compose_file_suffix}.yaml" + ) + + echo -e "\n${NC}Starting Docker Compose with Ollama (${docker_compose_file_suffix})...${NC}" + docker compose --env-file "${ENV_FILE}" "${compose_files[@]}" build + docker compose --env-file "${ENV_FILE}" "${compose_files[@]}" up -d + docker_compose_status=$? + + echo "Docker Compose Exit Status: $docker_compose_status" # Debug output + + if [ "$docker_compose_status" -ne 0 ]; then + echo -e "\n${RED}${BOLD}Error starting Docker Compose. Please ensure Docker Compose is installed and in your PATH.${NC}" + echo -e "${RED}Refer to Docker documentation for installation instructions: https://docs.docker.com/compose/install/${NC}" + exit 1 # Indicate failure and EXIT SCRIPT + fi + + echo "Waiting for Ollama container to be ready..." + OLLAMA_READY=false + while ! $OLLAMA_READY; do + CONTAINER_STATUS=$(docker compose "${compose_files[@]}" ps --services --filter "status=running" --format '{{.Service}}') + if [[ "$CONTAINER_STATUS" == *"ollama"* ]]; then # Check if 'ollama' service is in running services + OLLAMA_READY=true + echo "Ollama container is running." + else + echo "Ollama container not yet ready, waiting..." + sleep 5 + fi + done + + echo "Pulling $model_name model for Ollama..." + docker compose --env-file "${ENV_FILE}" "${compose_files[@]}" exec -it ollama ollama pull "$model_name" + + + echo -e "\n${GREEN}DocsGPT is now running with Ollama (${docker_compose_file_suffix}) on http://localhost:5173${NC}" + printf -v compose_files_escaped "%q " "${compose_files[@]}" + echo -e "${YELLOW}You can stop the application by running: docker compose ${compose_files_escaped}down${NC}" +} + +# 3) Connect Local Inference Engine +connect_local_inference_engine() { + local engine_choice + local model_name_prompt model_name openai_base_url + + get_model_name() { + read -p "$(echo -e "${DEFAULT_FG}Enter Model Name (leave empty to set later as None): ${NC}")" model_name + if [ -z "$model_name" ]; then + model_name="None" + fi + } + + while true; do + clear + prompt_local_inference_engine_options + case "$engine_choice" in + 1) # LLaMa.cpp + engine_name="LLaMa.cpp" + openai_base_url="http://localhost:8000/v1" + get_model_name + break ;; + 2) # Ollama + engine_name="Ollama" + openai_base_url="http://localhost:11434/v1" + get_model_name + break ;; + 3) # TGI + engine_name="TGI" + openai_base_url="http://localhost:8080/v1" + get_model_name + break ;; + 4) # SGLang + engine_name="SGLang" + openai_base_url="http://localhost:30000/v1" + get_model_name + break ;; + 5) # vLLM + engine_name="vLLM" + openai_base_url="http://localhost:8000/v1" + get_model_name + break ;; + 6) # Aphrodite + engine_name="Aphrodite" + openai_base_url="http://localhost:2242/v1" + get_model_name + break ;; + 7) # FriendliAI + engine_name="FriendliAI" + openai_base_url="http://localhost:8997/v1" + get_model_name + break ;; + 8) # LMDeploy + engine_name="LMDeploy" + openai_base_url="http://localhost:23333/v1" + get_model_name + break ;; + b|B) clear; return ;; # Back to Main Menu + *) echo -e "\n${RED}Invalid choice. Please choose 1-8, or b.${NC}" ; sleep 1 ;; + esac + done + + echo -e "\n${NC}Configuring for Local Inference Engine: ${BOLD}${engine_name}...${NC}" + echo "API_KEY=None" > .env + echo "LLM_NAME=openai" >> .env + echo "MODEL_NAME=$model_name" >> .env + echo "VITE_API_STREAMING=true" >> .env + echo "OPENAI_BASE_URL=$openai_base_url" >> .env + echo "EMBEDDINGS_NAME=huggingface_sentence-transformers/all-mpnet-base-v2" >> .env + echo -e "${GREEN}.env file configured for ${BOLD}${engine_name}${NC}${GREEN} with OpenAI API format.${NC}" + echo -e "${YELLOW}Note: MODEL_NAME is set to '${BOLD}$model_name${NC}${YELLOW}'. You can change it later in the .env file.${NC}" + + check_and_start_docker + + echo -e "\n${NC}Starting Docker Compose...${NC}" + docker compose --env-file "${ENV_FILE}" -f "${COMPOSE_FILE}" build && docker compose -f "${COMPOSE_FILE}" up -d + docker_compose_status=$? + + echo "Docker Compose Exit Status: $docker_compose_status" # Debug output + + if [ "$docker_compose_status" -ne 0 ]; then + echo -e "\n${RED}${BOLD}Error starting Docker Compose. Please ensure Docker Compose is installed and in your PATH.${NC}" + echo -e "${RED}Refer to Docker documentation for installation instructions: https://docs.docker.com/compose/install/${NC}" + exit 1 # Indicate failure and EXIT SCRIPT + fi + + echo -e "\n${GREEN}DocsGPT is now configured to connect to ${BOLD}${engine_name}${NC}${GREEN} at ${BOLD}$openai_base_url${NC}" + echo -e "${YELLOW}Ensure your ${BOLD}${engine_name} inference server is running at that address${NC}" + echo -e "\n${GREEN}DocsGPT is running at http://localhost:5173${NC}" + echo -e "${YELLOW}You can stop the application by running: docker compose -f \"${COMPOSE_FILE}\" down${NC}" +} + + +# 4) Connect Cloud API Provider +connect_cloud_api_provider() { + local provider_choice api_key llm_name + local setup_result # Variable to store the return status + + get_api_key() { + echo -e "${YELLOW}Your API key will be stored locally in the .env file and will not be sent anywhere else${NC}" + read -p "$(echo -e "${DEFAULT_FG}Please enter your API key: ${NC}")" api_key + } + + while true; do + clear + prompt_cloud_api_provider_options + case "$provider_choice" in + 1) # OpenAI + provider_name="OpenAI" + llm_name="openai" + model_name="gpt-4o" + get_api_key + break ;; + 2) # Google + provider_name="Google (Vertex AI, Gemini)" + llm_name="google" + model_name="gemini-2.0-flash" + get_api_key + break ;; + 3) # Anthropic + provider_name="Anthropic (Claude)" + llm_name="anthropic" + model_name="claude-3-5-sonnet-latest" + get_api_key + break ;; + 4) # Groq + provider_name="Groq" + llm_name="groq" + model_name="llama-3.1-8b-instant" + get_api_key + break ;; + 5) # HuggingFace Inference API + provider_name="HuggingFace Inference API" + llm_name="huggingface" + model_name="meta-llama/Llama-3.1-8B-Instruct" + get_api_key + break ;; + 6) # Azure OpenAI + provider_name="Azure OpenAI" + llm_name="azure_openai" + model_name="gpt-4o" + get_api_key + break ;; + b|B) clear; return ;; # Clear screen and Back to Main Menu + *) echo -e "\n${RED}Invalid choice. Please choose 1-6, or b.${NC}" ; sleep 1 ;; + esac + done + + echo -e "\n${NC}Configuring for Cloud API Provider: ${BOLD}${provider_name}...${NC}" + echo "API_KEY=$api_key" > .env + echo "LLM_NAME=$llm_name" >> .env + echo "MODEL_NAME=$model_name" >> .env + echo "VITE_API_STREAMING=true" >> .env + echo -e "${GREEN}.env file configured for ${BOLD}${provider_name}${NC}${GREEN}.${NC}" + + check_and_start_docker + + echo -e "\n${NC}Starting Docker Compose...${NC}" + docker compose --env-file "${ENV_FILE}" -f "${COMPOSE_FILE}" build && docker compose -f "${COMPOSE_FILE}" up -d + docker_compose_status=$? + + echo "Docker Compose Exit Status: $docker_compose_status" # Debug output + + if [ "$docker_compose_status" -ne 0 ]; then + echo -e "\n${RED}${BOLD}Error starting Docker Compose. Please ensure Docker Compose is installed and in your PATH.${NC}" + echo -e "${RED}Refer to Docker documentation for installation instructions: https://docs.docker.com/compose/install/${NC}" + exit 1 # Indicate failure and EXIT SCRIPT + fi + + echo -e "\n${GREEN}DocsGPT is now configured to use ${BOLD}${provider_name}${NC}${GREEN} on http://localhost:5173${NC}" + echo -e "${YELLOW}You can stop the application by running: docker compose -f \"${COMPOSE_FILE}\" down${NC}" +} + + +# Main script execution +animate_dino + +while true; do # Main menu loop + clear # Clear screen before showing main menu again + prompt_main_menu + + case $main_choice in + 1) # Use DocsGPT Public API Endpoint + use_docs_public_api_endpoint + ;; + 2) # Serve Local (with Ollama) + serve_local_ollama + ;; + 3) # Connect Local Inference Engine + connect_local_inference_engine + ;; + 4) # Connect Cloud API Provider + connect_cloud_api_provider + ;; + *) + echo -e "\n${RED}Invalid choice. Please choose 1-4.${NC}" ; sleep 1 ;; + esac +done + +echo -e "\n${GREEN}${BOLD}DocsGPT Setup Complete.${NC}" + +exit 0 \ No newline at end of file From 41290b463ce4e7bde10b0a7e78ab24e76543387a Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 11 Feb 2025 17:17:12 +0000 Subject: [PATCH 323/354] feat: cards --- docs/components/DeploymentCards.jsx | 114 ++++++++++++++++++ ...Hosting-the-app.md => Hosting-the-app.mdx} | 26 +++- docs/public/digitalocean.png | Bin 0 -> 24634 bytes 3 files changed, 137 insertions(+), 3 deletions(-) create mode 100644 docs/components/DeploymentCards.jsx rename docs/pages/Deploying/{Hosting-the-app.md => Hosting-the-app.mdx} (83%) create mode 100644 docs/public/digitalocean.png diff --git a/docs/components/DeploymentCards.jsx b/docs/components/DeploymentCards.jsx new file mode 100644 index 00000000..2c896476 --- /dev/null +++ b/docs/components/DeploymentCards.jsx @@ -0,0 +1,114 @@ +import Image from 'next/image'; + +const iconMap = { + 'Deploy on Civo Compute Cloud': '/civo.png', + 'Deploy on DigitalOcean Droplet': '/digitalocean.png', + 'Deploy on Kamatera Cloud': '/kamatera.png', +}; + + +export function DeploymentCards({ items }) { + return ( + <> +
+ {items.map(({ title, link, description }) => { + const isExternal = link.startsWith('https://'); + const iconSrc = iconMap[title] || '/default-icon.png'; // Default icon if not found + + return ( + + ); + })} +
+ + + + ); +} \ No newline at end of file diff --git a/docs/pages/Deploying/Hosting-the-app.md b/docs/pages/Deploying/Hosting-the-app.mdx similarity index 83% rename from docs/pages/Deploying/Hosting-the-app.md rename to docs/pages/Deploying/Hosting-the-app.mdx index 5f527aa1..f06f0379 100644 --- a/docs/pages/Deploying/Hosting-the-app.md +++ b/docs/pages/Deploying/Hosting-the-app.mdx @@ -1,3 +1,7 @@ +import { DeploymentCards } from '../../components/DeploymentCards'; + + + # Self-hosting DocsGPT on Amazon Lightsail Here's a step-by-step guide on how to set up an Amazon Lightsail instance to host DocsGPT. @@ -105,6 +109,22 @@ Your instance is now available at your Public IP Address on port 5173. Enjoy usi ## Other Deployment Options -- [Deploy DocsGPT on Civo Compute Cloud](https://dev.to/rutamhere/deploying-docsgpt-on-civo-compute-c) -- [Deploy DocsGPT on DigitalOcean Droplet](https://dev.to/rutamhere/deploying-docsgpt-on-digitalocean-droplet-50ea) -- [Deploy DocsGPT on Kamatera Performance Cloud](https://dev.to/rutamhere/deploying-docsgpt-on-kamatera-performance-cloud-1bj) + diff --git a/docs/public/digitalocean.png b/docs/public/digitalocean.png new file mode 100644 index 0000000000000000000000000000000000000000..2d90824a6623bc6f2c48c202ecea4af41a2f5098 GIT binary patch literal 24634 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 Date: Tue, 11 Feb 2025 17:27:45 +0000 Subject: [PATCH 324/354] fix: centering --- docs/components/DeploymentCards.jsx | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/docs/components/DeploymentCards.jsx b/docs/components/DeploymentCards.jsx index 2c896476..3ff12e1a 100644 --- a/docs/components/DeploymentCards.jsx +++ b/docs/components/DeploymentCards.jsx @@ -34,7 +34,7 @@ export function DeploymentCards({ items }) {
= ({ )} ); -}; +} -Documents.propTypes = { - //documents: PropTypes.array.isRequired, - handleDeleteDocument: PropTypes.func.isRequired, -}; +function DocumentChunks({ + document, + handleGoBack, +}: { + document: Doc; + handleGoBack: () => void; +}) { + const { t } = useTranslation(); + const [isDarkTheme] = useDarkTheme(); + const [paginatedChunks, setPaginatedChunks] = useState([]); + const [page, setPage] = useState(1); + const [perPage, setPerPage] = useState(5); + const [totalChunks, setTotalChunks] = useState(0); + const [loading, setLoading] = useState(false); + const [searchTerm, setSearchTerm] = useState(''); + const [modalState, setModalState] = useState('INACTIVE'); -export default Documents; + const fetchChunks = () => { + setLoading(true); + try { + userService + .getDocumentChunks(document.id ?? '', page, perPage) + .then((response) => { + if (!response.ok) { + throw new Error('Failed to fetch chunks data'); + } + return response.json(); + }) + .then((data) => { + setPage(data.page); + setPerPage(data.per_page); + setTotalChunks(data.total); + setPaginatedChunks(data.chunks); + }); + } catch (e) { + console.log(e); + } finally { + setLoading(false); + } + }; + + const handleAddChunk = (title: string, text: string) => { + try { + userService + .addChunk({ + id: document.id ?? '', + text: text, + metadata: { + title: title, + }, + }) + .then((response) => { + if (!response.ok) { + throw new Error('Failed to add chunk'); + } + fetchChunks(); + }); + } catch (e) { + console.log(e); + } + }; + + React.useEffect(() => { + fetchChunks(); + }, [page, perPage]); + return ( +
+
+ +

Back to all documents

+
+
+
+ + { + setSearchTerm(e.target.value); + }} + borderVariant="thin" + /> +
+ +
+
+ {paginatedChunks.filter((chunk) => + chunk.metadata?.title + .toLowerCase() + .includes(searchTerm.toLowerCase()), + ).length === 0 ? ( +
+ No tools found + No chunks found +
+ ) : ( + paginatedChunks + .filter((chunk) => + chunk.metadata?.title + .toLowerCase() + .includes(searchTerm.toLowerCase()), + ) + .map((chunk, index) => ( +
+
+
+

+ {chunk.metadata?.title} +

+

+ {chunk.text} +

+
+
+
+ )) + )} +
+
+ { + setPage(page); + }} + onRowsPerPageChange={(rows) => { + setPerPage(rows); + setPage(1); + }} + /> +
+ +
+ ); +} diff --git a/frontend/src/settings/ToolConfig.tsx b/frontend/src/settings/ToolConfig.tsx index ec422819..0be3a776 100644 --- a/frontend/src/settings/ToolConfig.tsx +++ b/frontend/src/settings/ToolConfig.tsx @@ -134,7 +134,7 @@ export default function ToolConfig({ {Object.keys(tool?.config).length !== 0 && tool.name !== 'api_tool' && (
- + API Key / Oauth
- + URL
- + Description setSearchTerm(e.target.value)} + borderVariant="thin" />
-
- {paginatedChunks.filter((chunk) => + {loading ? ( +
+
+ +
+
+ ) : ( +
+ {paginatedChunks.filter((chunk) => + chunk.metadata?.title + .toLowerCase() + .includes(searchTerm.toLowerCase()), + ).length === 0 ? ( +
+ No tools found + No chunks found +
+ ) : ( + paginatedChunks + .filter((chunk) => + chunk.metadata?.title + .toLowerCase() + .includes(searchTerm.toLowerCase()), + ) + .map((chunk, index) => ( +
+
+
+ +
+
+

+ {chunk.metadata?.title} +

+

+ {chunk.text} +

+
+
+
+ )) + )} +
+ )} + {!loading && + paginatedChunks.filter((chunk) => chunk.metadata?.title .toLowerCase() .includes(searchTerm.toLowerCase()), - ).length === 0 ? ( -
- No tools found + { + setPage(page); + }} + onRowsPerPageChange={(rows) => { + setPerPage(rows); + setPage(1); + }} /> - No chunks found
- ) : ( - paginatedChunks - .filter((chunk) => - chunk.metadata?.title - .toLowerCase() - .includes(searchTerm.toLowerCase()), - ) - .map((chunk, index) => ( -
-
-
-

- {chunk.metadata?.title} -

-

- {chunk.text} -

-
-
-
- )) )} -
-
- { - setPage(page); - }} - onRowsPerPageChange={(rows) => { - setPerPage(rows); - setPage(1); - }} - /> -
+ + setDeleteModalState((prev) => ({ ...prev, state })) + } + handleSubmit={() => handleDeleteChunk(deleteModalState.chunkId ?? '')} + submitLabel="Delete" + />
From 3a5192265050fef999218e6518257ec124150412 Mon Sep 17 00:00:00 2001 From: Siddhant Rai Date: Sat, 8 Feb 2025 15:00:02 +0530 Subject: [PATCH 304/354] fix: linting error --- application/vectorstore/mongodb.py | 1 + 1 file changed, 1 insertion(+) diff --git a/application/vectorstore/mongodb.py b/application/vectorstore/mongodb.py index e036d2a8..94b757e0 100644 --- a/application/vectorstore/mongodb.py +++ b/application/vectorstore/mongodb.py @@ -146,6 +146,7 @@ class MongoDBVectorStore(BaseVectorStore): return chunks except Exception as e: + print(f"Error getting chunks: {e}") return [] def add_chunk(self, text, metadata=None): From 28a0667da65ea1499f35f03f9ce4054eb95c07e0 Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 8 Feb 2025 12:49:42 +0000 Subject: [PATCH 305/354] fix: minor logging issue --- application/api/answer/routes.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/application/api/answer/routes.py b/application/api/answer/routes.py index 31aa43ac..e877fac5 100644 --- a/application/api/answer/routes.py +++ b/application/api/answer/routes.py @@ -1,7 +1,6 @@ import asyncio import datetime import json -import logging import os import traceback @@ -19,7 +18,6 @@ from application.llm.llm_creator import LLMCreator from application.retriever.retriever_creator import RetrieverCreator from application.utils import check_required_fields, limit_chat_history -logger = logging.getLogger(__name__) mongo = MongoDB.get_client() db = mongo["docsgpt"] @@ -262,8 +260,8 @@ def complete_stream( data = json.dumps({"type": "end"}) yield f"data: {data}\n\n" except Exception as e: - logger.error(f"Error in stream: {str(e)}") - logger.error(traceback.format_exc()) + current_app.logger.error(f"Error in stream: {str(e)}") + current_app.logger.error(traceback.format_exc()) data = json.dumps( { "type": "error", From ad77fe1116fad7697f1bebee0af843910967cd38 Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 8 Feb 2025 18:07:03 +0000 Subject: [PATCH 306/354] fix: logging in submodules --- application/app.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/application/app.py b/application/app.py index d7727001..4db18e2d 100644 --- a/application/app.py +++ b/application/app.py @@ -2,12 +2,13 @@ import platform import dotenv from flask import Flask, redirect, request +from application.core.logging_config import setup_logging +setup_logging() from application.api.answer.routes import answer from application.api.internal.routes import internal from application.api.user.routes import user from application.celery_init import celery -from application.core.logging_config import setup_logging from application.core.settings import settings from application.extensions import api @@ -17,7 +18,6 @@ if platform.system() == "Windows": pathlib.PosixPath = pathlib.WindowsPath dotenv.load_dotenv() -setup_logging() app = Flask(__name__) app.register_blueprint(user) From f4cb48ed0d4f23c21179d42ceca7fa35ca16481c Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 8 Feb 2025 19:29:54 +0000 Subject: [PATCH 307/354] fix: logging --- application/app.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/application/app.py b/application/app.py index 4db18e2d..4eb40331 100644 --- a/application/app.py +++ b/application/app.py @@ -5,16 +5,16 @@ from flask import Flask, redirect, request from application.core.logging_config import setup_logging setup_logging() -from application.api.answer.routes import answer -from application.api.internal.routes import internal -from application.api.user.routes import user -from application.celery_init import celery -from application.core.settings import settings -from application.extensions import api +from application.api.answer.routes import answer # noqa: E402 +from application.api.internal.routes import internal # noqa: E402 +from application.api.user.routes import user # noqa: E402 +from application.celery_init import celery # noqa: E402 +from application.core.settings import settings # noqa: E402 +from application.extensions import api # noqa: E402 + if platform.system() == "Windows": import pathlib - pathlib.PosixPath = pathlib.WindowsPath dotenv.load_dotenv() From 68ee9743fec2c7f5a496ce2fa480652df220d5a6 Mon Sep 17 00:00:00 2001 From: ManishMadan2882 Date: Sun, 9 Feb 2025 01:31:01 +0530 Subject: [PATCH 308/354] (feat:upload) advanced fields --- frontend/src/upload/Upload.tsx | 221 +++++++++++++++----------- frontend/src/upload/types/ingestor.ts | 3 +- 2 files changed, 126 insertions(+), 98 deletions(-) diff --git a/frontend/src/upload/Upload.tsx b/frontend/src/upload/Upload.tsx index e7635f3e..bff31b10 100644 --- a/frontend/src/upload/Upload.tsx +++ b/frontend/src/upload/Upload.tsx @@ -44,106 +44,121 @@ function Upload({ const [remoteName, setRemoteName] = useState(''); const [files, setfiles] = useState(receivedFile); const [activeTab, setActiveTab] = useState(renderTab); + const [showAdvancedOptions, setShowAdvancedOptions] = useState(false); const renderFormFields = () => { const schema = IngestorFormSchemas[ingestor.type]; - return schema.map((field: FormField) => { - const isRequired = field.required ?? false; - switch (field.type) { - case 'string': - return ( - , - ) => - handleIngestorChange( - field.name as keyof IngestorConfig['config'], - e.target.value, - ) - } - borderVariant="thin" - label={field.label} - required={field.required} - colorVariant="gray" - /> - ); - case 'number': - return ( - , - ) => - handleIngestorChange( - field.name as keyof IngestorConfig['config'], - Number(e.target.value), - ) - } - borderVariant="thin" - label={field.label} - required={field.required} - colorVariant="gray" - /> - ); - case 'enum': - return ( - { - const value = - typeof selected === 'string' ? selected : selected.value; - handleIngestorChange( - field.name as keyof IngestorConfig['config'], - value, - ); - }} - size="w-full" - rounded="3xl" - placeholder={field.label} - border="border" - borderColor="gray-5000" - /> - ); - case 'boolean': - return ( - { - handleIngestorChange( - field.name as keyof IngestorConfig['config'], - checked, - ); - }} - className="mt-2" - /> - ); - default: - return null; - } - }); + if (!schema) return null; + + const generalFields = schema.filter((field) => !field.advanced); + const advancedFields = schema.filter((field) => field.advanced); + + return ( + <> + {generalFields.map((field: FormField) => renderField(field))} + + {advancedFields.length > 0 && showAdvancedOptions && ( + <> +
+ {advancedFields.map((field: FormField) => renderField(field))} + + )} + + ); + }; + + const renderField = (field: FormField) => { + const isRequired = field.required ?? false; + switch (field.type) { + case 'string': + return ( + + handleIngestorChange( + field.name as keyof IngestorConfig['config'], + e.target.value, + ) + } + borderVariant="thin" + label={field.label} + required={isRequired} + colorVariant="gray" + /> + ); + case 'number': + return ( + + handleIngestorChange( + field.name as keyof IngestorConfig['config'], + Number(e.target.value), + ) + } + borderVariant="thin" + label={field.label} + required={isRequired} + colorVariant="gray" + /> + ); + case 'enum': + return ( + + opt.value === + ingestor.config[field.name as keyof typeof ingestor.config], + ) || null + } + onSelect={(selected: { label: string; value: string }) => { + handleIngestorChange( + field.name as keyof IngestorConfig['config'], + selected.value, + ); + }} + size="w-full" + rounded="3xl" + placeholder={field.label} + border="border" + borderColor="gray-5000" + /> + ); + case 'boolean': + return ( + { + handleIngestorChange( + field.name as keyof IngestorConfig['config'], + checked, + ); + }} + className="mt-2" + /> + ); + default: + return null; + } }; // New unified ingestor state @@ -645,6 +660,18 @@ function Upload({ label="Name" /> {renderFormFields()} + {IngestorFormSchemas[ingestor.type].some( + (field) => field.advanced, + ) && ( + + )} )}
diff --git a/frontend/src/upload/types/ingestor.ts b/frontend/src/upload/types/ingestor.ts index 155e1fb9..cd709847 100644 --- a/frontend/src/upload/types/ingestor.ts +++ b/frontend/src/upload/types/ingestor.ts @@ -1,5 +1,5 @@ export interface BaseIngestorConfig { - [key: string]: string | number | boolean; + [key: string]: string | number | boolean | undefined; } export interface RedditIngestorConfig extends BaseIngestorConfig { @@ -48,6 +48,7 @@ export interface FormField { label: string; type: FieldType; required?: boolean; + advanced?: boolean; options?: { label: string; value: string }[]; } From 56f91948f81b6942c90057388c5bfd1ba4c1b1be Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 9 Feb 2025 12:05:37 +0000 Subject: [PATCH 309/354] feat: improve logging --- application/api/answer/routes.py | 31 +++++++++++++++++++++---------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/application/api/answer/routes.py b/application/api/answer/routes.py index e877fac5..2df97b0d 100644 --- a/application/api/answer/routes.py +++ b/application/api/answer/routes.py @@ -3,10 +3,11 @@ import datetime import json import os import traceback +import logging from bson.dbref import DBRef from bson.objectid import ObjectId -from flask import Blueprint, current_app, make_response, request, Response +from flask import Blueprint, make_response, request, Response from flask_restx import fields, Namespace, Resource @@ -18,6 +19,7 @@ from application.llm.llm_creator import LLMCreator from application.retriever.retriever_creator import RetrieverCreator from application.utils import check_required_fields, limit_chat_history +logger = logging.getLogger(__name__) mongo = MongoDB.get_client() db = mongo["docsgpt"] @@ -210,6 +212,15 @@ def complete_stream( ): try: + import sys + + try: + logger.info(f"Stream question, inside complete_stream: {question}") + except Exception as e: + print(f"Error in logging: {str(e)}", file=sys.stderr) + print(traceback.format_exc(), file=sys.stderr) + + response_full = "" source_log_docs = [] answer = retriever.gen() @@ -260,8 +271,8 @@ def complete_stream( data = json.dumps({"type": "end"}) yield f"data: {data}\n\n" except Exception as e: - current_app.logger.error(f"Error in stream: {str(e)}") - current_app.logger.error(traceback.format_exc()) + logger.error(f"Error in stream: {str(e)}") + logger.error(traceback.format_exc()) data = json.dumps( { "type": "error", @@ -346,7 +357,7 @@ class Stream(Resource): source = {} user_api_key = None - current_app.logger.info( + logger.info( f"/stream - request_data: {data}, source: {source}", extra={"data": json.dumps({"request_data": data, "source": source})}, ) @@ -380,14 +391,14 @@ class Stream(Resource): except ValueError: message = "Malformed request body" - current_app.logger.error(f"/stream - error: {message}") + logger.error(f"/stream - error: {message}") return Response( error_stream_generate(message), status=400, mimetype="text/event-stream", ) except Exception as e: - current_app.logger.error( + logger.error( f"/stream - error: {str(e)} - traceback: {traceback.format_exc()}", extra={"error": str(e), "traceback": traceback.format_exc()}, ) @@ -471,7 +482,7 @@ class Answer(Resource): prompt = get_prompt(prompt_id) - current_app.logger.info( + logger.info( f"/api/answer - request_data: {data}, source: {source}", extra={"data": json.dumps({"request_data": data, "source": source})}, ) @@ -526,7 +537,7 @@ class Answer(Resource): ) except Exception as e: - current_app.logger.error( + logger.error( f"/api/answer - error: {str(e)} - traceback: {traceback.format_exc()}", extra={"error": str(e), "traceback": traceback.format_exc()}, ) @@ -591,7 +602,7 @@ class Search(Resource): source = {} user_api_key = None - current_app.logger.info( + logger.info( f"/api/answer - request_data: {data}, source: {source}", extra={"data": json.dumps({"request_data": data, "source": source})}, ) @@ -629,7 +640,7 @@ class Search(Resource): doc["source"] = "None" except Exception as e: - current_app.logger.error( + logger.error( f"/api/search - error: {str(e)} - traceback: {traceback.format_exc()}", extra={"error": str(e), "traceback": traceback.format_exc()}, ) From f639b052e395c7220cdee9ba9e238a3dbf2fa171 Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 9 Feb 2025 12:08:19 +0000 Subject: [PATCH 310/354] fix: remove debugging code --- application/api/answer/routes.py | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/application/api/answer/routes.py b/application/api/answer/routes.py index 2df97b0d..a2e32eba 100644 --- a/application/api/answer/routes.py +++ b/application/api/answer/routes.py @@ -211,16 +211,7 @@ def complete_stream( question, retriever, conversation_id, user_api_key, isNoneDoc=False,index=None ): - try: - import sys - - try: - logger.info(f"Stream question, inside complete_stream: {question}") - except Exception as e: - print(f"Error in logging: {str(e)}", file=sys.stderr) - print(traceback.format_exc(), file=sys.stderr) - - + try: response_full = "" source_log_docs = [] answer = retriever.gen() From 568ab33a37a4476f27b7017b40ffd24dc733c079 Mon Sep 17 00:00:00 2001 From: Piotr Idzik <65706193+vil02@users.noreply.github.com> Date: Sun, 9 Feb 2025 23:56:52 +0100 Subject: [PATCH 311/354] style: use underscore for an unused loop variable (#1593) This addresses the SC2034 warning. --- setup.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.sh b/setup.sh index c39f1f0f..3b2c7855 100755 --- a/setup.sh +++ b/setup.sh @@ -31,7 +31,7 @@ check_and_start_docker() { # Wait for Docker to be fully operational with animated dots echo -n "Waiting for Docker to start" while ! docker system info > /dev/null 2>&1; do - for i in {1..3}; do + for _ in {1..3}; do echo -n "." sleep 1 done From 7623bde159e47e43916de2097fc20191321a0e15 Mon Sep 17 00:00:00 2001 From: Siddhant Rai Date: Mon, 10 Feb 2025 09:36:18 +0530 Subject: [PATCH 312/354] feat: add update chunk API endpoint and service method --- application/api/user/routes.py | 73 ++++++++- frontend/src/api/endpoints.ts | 1 + frontend/src/api/services/userService.ts | 2 + frontend/src/modals/AddChunkModal.tsx | 86 ---------- frontend/src/modals/ChunkModal.tsx | 193 +++++++++++++++++++++++ frontend/src/settings/Documents.tsx | 94 +++++++---- 6 files changed, 330 insertions(+), 119 deletions(-) delete mode 100644 frontend/src/modals/AddChunkModal.tsx create mode 100644 frontend/src/modals/ChunkModal.tsx diff --git a/application/api/user/routes.py b/application/api/user/routes.py index 1ecf7e6e..b25e587f 100644 --- a/application/api/user/routes.py +++ b/application/api/user/routes.py @@ -1506,7 +1506,7 @@ class GetFeedbackAnalytics(Resource): except Exception as err: current_app.logger.error(f"Error getting API key: {err}") return make_response(jsonify({"success": False}), 400) - + end_date = datetime.datetime.now(datetime.timezone.utc) if filter_option == "last_hour": @@ -2193,3 +2193,74 @@ class DeleteChunk(Resource): ) except Exception as e: return make_response(jsonify({"error": str(e)}), 500) + + +@user_ns.route("/api/update_chunk") +class UpdateChunk(Resource): + @api.expect( + api.model( + "UpdateChunkModel", + { + "id": fields.String(required=True, description="Document ID"), + "chunk_id": fields.String( + required=True, description="Chunk ID to update" + ), + "text": fields.String( + required=False, description="New text of the chunk" + ), + "metadata": fields.Raw( + required=False, + description="Updated metadata associated with the chunk", + ), + }, + ) + ) + @api.doc( + description="Updates an existing chunk in the document.", + ) + def put(self): + data = request.get_json() + required_fields = ["id", "chunk_id"] + missing_fields = check_required_fields(data, required_fields) + if missing_fields: + return missing_fields + + doc_id = data.get("id") + chunk_id = data.get("chunk_id") + text = data.get("text") + metadata = data.get("metadata") + + if not ObjectId.is_valid(doc_id): + return make_response(jsonify({"error": "Invalid doc_id"}), 400) + + try: + store = get_vector_store(doc_id) + chunks = store.get_chunks() + existing_chunk = next((c for c in chunks if c["doc_id"] == chunk_id), None) + if not existing_chunk: + return make_response(jsonify({"error": "Chunk not found"}), 404) + + deleted = store.delete_chunk(chunk_id) + if not deleted: + return make_response( + jsonify({"error": "Failed to delete existing chunk"}), 500 + ) + + new_text = text if text is not None else existing_chunk["text"] + new_metadata = ( + metadata if metadata is not None else existing_chunk["metadata"] + ) + + new_chunk_id = store.add_chunk(new_text, new_metadata) + + return make_response( + jsonify( + { + "message": "Chunk updated successfully", + "new_chunk_id": new_chunk_id, + } + ), + 200, + ) + except Exception as e: + return make_response(jsonify({"error": str(e)}), 500) diff --git a/frontend/src/api/endpoints.ts b/frontend/src/api/endpoints.ts index b117e18b..9bf659de 100644 --- a/frontend/src/api/endpoints.ts +++ b/frontend/src/api/endpoints.ts @@ -29,6 +29,7 @@ const endpoints = { ADD_CHUNK: '/api/add_chunk', DELETE_CHUNK: (docId: string, chunkId: string) => `/api/delete_chunk?id=${docId}&chunk_id=${chunkId}`, + UPDATE_CHUNK: '/api/update_chunk', }, CONVERSATION: { ANSWER: '/api/answer', diff --git a/frontend/src/api/services/userService.ts b/frontend/src/api/services/userService.ts index 76c704d9..e7f367f1 100644 --- a/frontend/src/api/services/userService.ts +++ b/frontend/src/api/services/userService.ts @@ -57,6 +57,8 @@ const userService = { apiClient.post(endpoints.USER.ADD_CHUNK, data), deleteChunk: (docId: string, chunkId: string): Promise => apiClient.delete(endpoints.USER.DELETE_CHUNK(docId, chunkId)), + updateChunk: (data: any): Promise => + apiClient.put(endpoints.USER.UPDATE_CHUNK, data), }; export default userService; diff --git a/frontend/src/modals/AddChunkModal.tsx b/frontend/src/modals/AddChunkModal.tsx deleted file mode 100644 index 6ab41ccd..00000000 --- a/frontend/src/modals/AddChunkModal.tsx +++ /dev/null @@ -1,86 +0,0 @@ -import React from 'react'; - -import Exit from '../assets/exit.svg'; -import Input from '../components/Input'; -import { ActiveState } from '../models/misc'; - -export default function AddChunkModal({ - modalState, - setModalState, - handleSubmit, -}: { - modalState: ActiveState; - setModalState: (state: ActiveState) => void; - handleSubmit: (title: string, text: string) => void; -}) { - const [title, setTitle] = React.useState(''); - const [chunkText, setChunkText] = React.useState(''); - return ( -
-
-
- -
-

- Add Chunk -

-
- - Title - - setTitle(e.target.value)} - borderVariant="thin" - placeholder={'Enter title'} - > -
-
- - Body text - - -
-
- - -
-
-
-
-
- ); -} diff --git a/frontend/src/modals/ChunkModal.tsx b/frontend/src/modals/ChunkModal.tsx new file mode 100644 index 00000000..cfeb1c76 --- /dev/null +++ b/frontend/src/modals/ChunkModal.tsx @@ -0,0 +1,193 @@ +import React from 'react'; + +import Exit from '../assets/exit.svg'; +import Input from '../components/Input'; +import { ActiveState } from '../models/misc'; +import ConfirmationModal from './ConfirmationModal'; + +export default function ChunkModal({ + type, + modalState, + setModalState, + handleSubmit, + originalTitle, + originalText, + handleDelete, +}: { + type: 'ADD' | 'EDIT'; + modalState: ActiveState; + setModalState: (state: ActiveState) => void; + handleSubmit: (title: string, text: string) => void; + originalTitle?: string; + originalText?: string; + handleDelete?: () => void; +}) { + const [title, setTitle] = React.useState(''); + const [chunkText, setChunkText] = React.useState(''); + const [deleteModal, setDeleteModal] = React.useState('INACTIVE'); + + React.useEffect(() => { + setTitle(originalTitle || ''); + setChunkText(originalText || ''); + }, [originalTitle, originalText]); + if (type === 'ADD') { + return ( +
+
+
+ +
+

+ Add Chunk +

+
+ + Title + + setTitle(e.target.value)} + borderVariant="thin" + placeholder={'Enter title'} + > +
+
+
+ + Body text + + +
+
+
+ + +
+
+
+
+
+ ); + } else { + return ( +
+
+
+ +
+

+ Edit Chunk +

+
+ + Title + + setTitle(e.target.value)} + borderVariant="thin" + placeholder={'Enter title'} + > +
+
+
+ + Body text + + +
+
+
+ +
+ + +
+
+
+
+
+ {}} + submitLabel="Delete" + /> +
+ ); + } +} diff --git a/frontend/src/settings/Documents.tsx b/frontend/src/settings/Documents.tsx index 62cf21e3..5eedba21 100644 --- a/frontend/src/settings/Documents.tsx +++ b/frontend/src/settings/Documents.tsx @@ -5,6 +5,7 @@ import { useDispatch } from 'react-redux'; import userService from '../api/services/userService'; import ArrowLeft from '../assets/arrow-left.svg'; import caretSort from '../assets/caret-sort.svg'; +import Edit from '../assets/edit.svg'; import NoFilesDarkIcon from '../assets/no-files-dark.svg'; import NoFilesIcon from '../assets/no-files.svg'; import SyncIcon from '../assets/sync.svg'; @@ -15,7 +16,7 @@ import Input from '../components/Input'; import SkeletonLoader from '../components/SkeletonLoader'; import Spinner from '../components/Spinner'; import { useDarkTheme } from '../hooks'; -import AddChunkModal from '../modals/AddChunkModal'; +import ChunkModal from '../modals/ChunkModal'; import ConfirmationModal from '../modals/ConfirmationModal'; import { ActiveState, Doc, DocumentsProps } from '../models/misc'; import { getDocs, getDocsWithPagination } from '../preferences/preferenceApi'; @@ -396,11 +397,11 @@ function DocumentChunks({ const [totalChunks, setTotalChunks] = useState(0); const [loading, setLoading] = useState(true); const [searchTerm, setSearchTerm] = useState(''); - const [deleteModalState, setDeleteModalState] = useState<{ + const [addModal, setAddModal] = useState('INACTIVE'); + const [editModal, setEditModal] = useState<{ state: ActiveState; - chunkId: string | null; - }>({ state: 'INACTIVE', chunkId: null }); - const [addModalState, setAddModalState] = useState('INACTIVE'); + chunk: ChunkType | null; + }>({ state: 'INACTIVE', chunk: null }); const fetchChunks = () => { setLoading(true); @@ -448,15 +449,39 @@ function DocumentChunks({ } }; - const handleDeleteChunk = (chunkId: string) => { + const handleUpdateChunk = (title: string, text: string, chunk: ChunkType) => { try { - userService.deleteChunk(document.id ?? '', chunkId).then((response) => { - if (!response.ok) { - throw new Error('Failed to delete chunk'); - } - setDeleteModalState({ state: 'INACTIVE', chunkId: null }); - fetchChunks(); - }); + userService + .updateChunk({ + id: document.id ?? '', + chunk_id: chunk.doc_id, + text: text, + metadata: { + title: title, + }, + }) + .then((response) => { + if (!response.ok) { + throw new Error('Failed to update chunk'); + } + fetchChunks(); + }); + } catch (e) { + console.log(e); + } + }; + + const handleDeleteChunk = (chunk: ChunkType) => { + try { + userService + .deleteChunk(document.id ?? '', chunk.doc_id) + .then((response) => { + if (!response.ok) { + throw new Error('Failed to delete chunk'); + } + setEditModal({ state: 'INACTIVE', chunk: null }); + fetchChunks(); + }); } catch (e) { console.log(e); } @@ -498,7 +523,7 @@ function DocumentChunks({ @@ -539,18 +564,18 @@ function DocumentChunks({
@@ -590,20 +615,25 @@ function DocumentChunks({ />
)} - - setDeleteModalState((prev) => ({ ...prev, state })) - } - handleSubmit={() => handleDeleteChunk(deleteModalState.chunkId ?? '')} - submitLabel="Delete" - /> - + setEditModal((prev) => ({ ...prev, state }))} + handleSubmit={(title, text) => { + handleUpdateChunk(title, text, editModal.chunk as ChunkType); + }} + originalText={editModal.chunk?.text} + originalTitle={editModal.chunk?.metadata?.title} + handleDelete={() => { + handleDeleteChunk(editModal.chunk as ChunkType); + }} + />
); } From 3b45b63d2a12fd60d625834b1d74a20c8aabe352 Mon Sep 17 00:00:00 2001 From: ManishMadan2882 Date: Mon, 10 Feb 2025 16:02:02 +0530 Subject: [PATCH 313/354] (fix:upload) ui adjust --- frontend/src/locale/en.json | 4 +++- frontend/src/locale/es.json | 4 +++- frontend/src/locale/jp.json | 4 +++- frontend/src/locale/ru.json | 4 +++- frontend/src/locale/zh-TW.json | 4 +++- frontend/src/locale/zh.json | 4 +++- frontend/src/upload/Upload.tsx | 9 +++++---- frontend/src/upload/types/ingestor.ts | 1 + 8 files changed, 24 insertions(+), 10 deletions(-) diff --git a/frontend/src/locale/en.json b/frontend/src/locale/en.json index 35058be0..df7895ed 100644 --- a/frontend/src/locale/en.json +++ b/frontend/src/locale/en.json @@ -156,7 +156,9 @@ "completed": "Training completed", "wait": "This may take several minutes", "tokenLimit": "Over the token limit, please consider uploading smaller document" - } + }, + "showAdvanced": "Show advanced options", + "hideAdvanced": "Hide advanced options" }, "createAPIKey": { "label": "Create New API Key", diff --git a/frontend/src/locale/es.json b/frontend/src/locale/es.json index d0b47874..123f9fdd 100644 --- a/frontend/src/locale/es.json +++ b/frontend/src/locale/es.json @@ -156,7 +156,9 @@ "completed": "Entrenamiento completado", "wait": "Esto puede tardar varios minutos", "tokenLimit": "Excede el límite de tokens, considere cargar un documento más pequeño" - } + }, + "showAdvanced": "Mostrar opciones avanzadas", + "hideAdvanced": "Ocultar opciones avanzadas" }, "createAPIKey": { "label": "Crear Nueva Clave de API", diff --git a/frontend/src/locale/jp.json b/frontend/src/locale/jp.json index 2fdc2c61..b4cd1531 100644 --- a/frontend/src/locale/jp.json +++ b/frontend/src/locale/jp.json @@ -155,7 +155,9 @@ "completed": "トレーニング完了", "wait": "数分かかる場合があります", "tokenLimit": "トークン制限を超えています。より小さいドキュメントをアップロードしてください" - } + }, + "showAdvanced": "詳細オプションを表示", + "hideAdvanced": "詳細オプションを非表示" }, "createAPIKey": { "label": "新しいAPIキーを作成", diff --git a/frontend/src/locale/ru.json b/frontend/src/locale/ru.json index 087b8dfd..d6bc2bdf 100644 --- a/frontend/src/locale/ru.json +++ b/frontend/src/locale/ru.json @@ -156,7 +156,9 @@ "completed": "Обучение завершено", "wait": "Это может занять несколько минут", "tokenLimit": "Превышен лимит токенов, рассмотрите возможность загрузки документа меньшего размера" - } + }, + "showAdvanced": "Показать расширенные настройки", + "hideAdvanced": "Скрыть расширенные настройки" }, "createAPIKey": { "label": "Создать новый API ключ", diff --git a/frontend/src/locale/zh-TW.json b/frontend/src/locale/zh-TW.json index f2abf1e5..8eef9e56 100644 --- a/frontend/src/locale/zh-TW.json +++ b/frontend/src/locale/zh-TW.json @@ -156,7 +156,9 @@ "completed": "訓練完成", "wait": "這可能需要幾分鐘", "tokenLimit": "超出令牌限制,請考慮上傳較小的文檔" - } + }, + "showAdvanced": "顯示進階選項", + "hideAdvanced": "隱藏進階選項" }, "createAPIKey": { "label": "建立新的 API 金鑰", diff --git a/frontend/src/locale/zh.json b/frontend/src/locale/zh.json index 56c8fbb1..eb9d1e21 100644 --- a/frontend/src/locale/zh.json +++ b/frontend/src/locale/zh.json @@ -156,7 +156,9 @@ "completed": "训练完成", "wait": "这可能需要几分钟", "tokenLimit": "超出令牌限制,请考虑上传较小的文档" - } + }, + "showAdvanced": "显示高级选项", + "hideAdvanced": "隐藏高级选项" }, "createAPIKey": { "label": "创建新的 API 密钥", diff --git a/frontend/src/upload/Upload.tsx b/frontend/src/upload/Upload.tsx index bff31b10..0e030beb 100644 --- a/frontend/src/upload/Upload.tsx +++ b/frontend/src/upload/Upload.tsx @@ -637,7 +637,7 @@ function Upload({ {activeTab === 'remote' && ( <> opt.value === ingestor.type) || null @@ -658,6 +658,7 @@ function Upload({ borderVariant="thin" placeholder="Name" label="Name" + required={true} /> {renderFormFields()} {IngestorFormSchemas[ingestor.type].some( @@ -665,11 +666,11 @@ function Upload({ ) && ( )} diff --git a/frontend/src/upload/types/ingestor.ts b/frontend/src/upload/types/ingestor.ts index cd709847..e934cf91 100644 --- a/frontend/src/upload/types/ingestor.ts +++ b/frontend/src/upload/types/ingestor.ts @@ -99,6 +99,7 @@ export const IngestorFormSchemas: Record = { label: 'Number of Posts', type: 'number', required: true, + advanced: true, }, ], github: [ From 2019f29e8cd93e63c25f8df2b71f46254abb96bc Mon Sep 17 00:00:00 2001 From: ManishMadan2882 Date: Mon, 10 Feb 2025 16:02:37 +0530 Subject: [PATCH 314/354] (clean) mock changes --- frontend/src/upload/types/ingestor.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/src/upload/types/ingestor.ts b/frontend/src/upload/types/ingestor.ts index e934cf91..cd709847 100644 --- a/frontend/src/upload/types/ingestor.ts +++ b/frontend/src/upload/types/ingestor.ts @@ -99,7 +99,6 @@ export const IngestorFormSchemas: Record = { label: 'Number of Posts', type: 'number', required: true, - advanced: true, }, ], github: [ From d85bf671036a9a33b280eda9101990d65f36ca9d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 10 Feb 2025 13:06:17 +0000 Subject: [PATCH 315/354] build(deps): bump marshmallow from 3.24.1 to 3.26.1 in /application Bumps [marshmallow](https://github.com/marshmallow-code/marshmallow) from 3.24.1 to 3.26.1. - [Changelog](https://github.com/marshmallow-code/marshmallow/blob/dev/CHANGELOG.rst) - [Commits](https://github.com/marshmallow-code/marshmallow/compare/3.24.1...3.26.1) --- updated-dependencies: - dependency-name: marshmallow dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- application/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/requirements.txt b/application/requirements.txt index 5732809b..fa4e9178 100644 --- a/application/requirements.txt +++ b/application/requirements.txt @@ -39,7 +39,7 @@ langsmith==0.2.10 lazy-object-proxy==1.10.0 lxml==5.3.0 markupsafe==3.0.2 -marshmallow==3.24.1 +marshmallow==3.26.1 mpmath==1.3.0 multidict==6.1.0 mypy-extensions==1.0.0 From 60772889d5de24962ba8ae43f2da0cabfe27a394 Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 10 Feb 2025 16:20:37 +0000 Subject: [PATCH 316/354] fix: handle bad tool name input --- application/api/user/routes.py | 12 ++++++- application/tools/agent.py | 4 +++ application/utils.py | 7 ++++ frontend/src/modals/AddActionModal.tsx | 48 ++++++++++++++++++++------ frontend/src/modals/ChunkModal.tsx | 8 ++++- 5 files changed, 66 insertions(+), 13 deletions(-) diff --git a/application/api/user/routes.py b/application/api/user/routes.py index 231ad9a4..13cab96c 100644 --- a/application/api/user/routes.py +++ b/application/api/user/routes.py @@ -19,7 +19,7 @@ from application.core.settings import settings from application.extensions import api from application.tools.tool_manager import ToolManager from application.tts.google_tts import GoogleTTS -from application.utils import check_required_fields +from application.utils import check_required_fields, validate_function_name from application.vectorstore.vector_creator import VectorCreator mongo = MongoDB.get_client() @@ -1932,6 +1932,16 @@ class UpdateTool(Resource): if "actions" in data: update_data["actions"] = data["actions"] if "config" in data: + if "actions" in data["config"]: + for action_name in list(data["config"]["actions"].keys()): + if not validate_function_name(action_name): + return make_response( + jsonify({ + "success": False, + "message": f"Invalid function name '{action_name}'. Function names must match pattern '^[a-zA-Z0-9_-]+$'.", + "param": "tools[].function.name" + }), 400 + ) update_data["config"] = data["config"] if "status" in data: update_data["status"] = data["status"] diff --git a/application/tools/agent.py b/application/tools/agent.py index de8ad725..d0743cd9 100644 --- a/application/tools/agent.py +++ b/application/tools/agent.py @@ -52,6 +52,10 @@ class Agent: }, } for tool_id, tool in tools_dict.items() + if ( + (tool["name"] == "api_tool" and "actions" in tool.get("config", {})) + or (tool["name"] != "api_tool" and "actions" in tool) + ) for action in ( tool["config"]["actions"].values() if tool["name"] == "api_tool" diff --git a/application/utils.py b/application/utils.py index 690eac5e..54d2086f 100644 --- a/application/utils.py +++ b/application/utils.py @@ -1,6 +1,7 @@ import tiktoken import hashlib from flask import jsonify, make_response +import re _encoding = None @@ -95,3 +96,9 @@ def limit_chat_history(history, max_token_limit=None, gpt_model="docsgpt"): break return trimmed_history + +def validate_function_name(function_name): + """Validates if a function name matches the allowed pattern.""" + if not re.match(r"^[a-zA-Z0-9_-]+$", function_name): + return False + return True \ No newline at end of file diff --git a/frontend/src/modals/AddActionModal.tsx b/frontend/src/modals/AddActionModal.tsx index f0ee797b..6ff88ae6 100644 --- a/frontend/src/modals/AddActionModal.tsx +++ b/frontend/src/modals/AddActionModal.tsx @@ -1,21 +1,40 @@ -import React from 'react'; +import React, { useState } from 'react'; import { useTranslation } from 'react-i18next'; import Exit from '../assets/exit.svg'; import Input from '../components/Input'; import { ActiveState } from '../models/misc'; +const isValidFunctionName = (name: string): boolean => { + const pattern = /^[a-zA-Z0-9_-]+$/; + return pattern.test(name); +}; + +interface AddActionModalProps { + modalState: ActiveState; + setModalState: (state: ActiveState) => void; + handleSubmit: (actionName: string) => void; +} + export default function AddActionModal({ modalState, setModalState, handleSubmit, -}: { - modalState: ActiveState; - setModalState: (state: ActiveState) => void; - handleSubmit: (actionName: string) => void; -}) { +}: AddActionModalProps) { const { t } = useTranslation(); const [actionName, setActionName] = React.useState(''); + const [functionNameError, setFunctionNameError] = useState(false); // New error state + + const handleAddAction = () => { + if (!isValidFunctionName(actionName)) { + setFunctionNameError(true); // Set error state if invalid + return; + } + setFunctionNameError(false); // Clear error state if valid + handleSubmit(actionName); + setModalState('INACTIVE'); + }; + return (
setActionName(e.target.value)} borderVariant="thin" placeholder={'Enter name'} - > + /> +

+ Use only letters, numbers, underscores, and hyphens (e.g., + `get_user_data`, `send-report`). +

+ {functionNameError && ( +

+ Invalid function name format. Use only letters, numbers, + underscores, and hyphens. +

+ )}
From 6e8a53a204f0c8c131e2c304106a65ad40edb4dd Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 10 Feb 2025 16:52:59 +0000 Subject: [PATCH 317/354] fix: open new tool after its added --- frontend/src/modals/AddToolModal.tsx | 17 +++++++++++++++-- frontend/src/settings/Tools.tsx | 19 +++++++++++++++++++ 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/frontend/src/modals/AddToolModal.tsx b/frontend/src/modals/AddToolModal.tsx index d4706431..ba555df4 100644 --- a/frontend/src/modals/AddToolModal.tsx +++ b/frontend/src/modals/AddToolModal.tsx @@ -13,11 +13,13 @@ export default function AddToolModal({ modalState, setModalState, getUserTools, + onToolAdded, }: { message: string; modalState: ActiveState; setModalState: (state: ActiveState) => void; getUserTools: () => void; + onToolAdded: (toolId: string) => void; }) { const [availableTools, setAvailableTools] = React.useState< AvailableToolType[] @@ -59,9 +61,20 @@ export default function AddToolModal({ }) .then((res) => { if (res.status === 200) { - getUserTools(); - setModalState('INACTIVE'); + return res.json(); + } else { + throw new Error( + `Failed to create tool, status code: ${res.status}`, + ); } + }) + .then((data) => { + getUserTools(); + setModalState('INACTIVE'); + onToolAdded(data.id); + }) + .catch((error) => { + console.error('Failed to create tool:', error); }); } else { setModalState('INACTIVE'); diff --git a/frontend/src/settings/Tools.tsx b/frontend/src/settings/Tools.tsx index 5198ad5d..0d45b9ba 100644 --- a/frontend/src/settings/Tools.tsx +++ b/frontend/src/settings/Tools.tsx @@ -58,9 +58,27 @@ export default function Tools() { getUserTools(); }; + const handleToolAdded = (toolId: string) => { + userService + .getUserTools() + .then((res) => res.json()) + .then((data) => { + const newTool = data.tools.find( + (tool: UserToolType) => tool.id === toolId, + ); + if (newTool) { + setSelectedTool(newTool); + } else { + console.error('Newly added tool not found'); + } + }) + .catch((error) => console.error('Error fetching tools:', error)); + }; + React.useEffect(() => { getUserTools(); }, []); + return (
{selectedTool ? ( @@ -185,6 +203,7 @@ export default function Tools() { modalState={addToolModalState} setModalState={setAddToolModalState} getUserTools={getUserTools} + onToolAdded={handleToolAdded} />
)} From ea0a6e413d42c32f61f8ca7acb09a30b42ef1dd8 Mon Sep 17 00:00:00 2001 From: ManishMadan2882 Date: Tue, 11 Feb 2025 02:07:54 +0530 Subject: [PATCH 318/354] (feat:bubble) formattedtable/inline code --- .../src/conversation/ConversationBubble.tsx | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/frontend/src/conversation/ConversationBubble.tsx b/frontend/src/conversation/ConversationBubble.tsx index 668b0935..bfa225cc 100644 --- a/frontend/src/conversation/ConversationBubble.tsx +++ b/frontend/src/conversation/ConversationBubble.tsx @@ -351,7 +351,7 @@ const ConversationBubble = forwardRef<
) : ( - {children} + + {children} + ); }, ul({ children }) { @@ -382,8 +384,8 @@ const ConversationBubble = forwardRef< }, table({ children }) { return ( -
- +
+
{children}
@@ -391,24 +393,24 @@ const ConversationBubble = forwardRef< }, thead({ children }) { return ( -
{children}{children}{children}