From 3683d4a7598da2be8c1fa58588aec637bba59354 Mon Sep 17 00:00:00 2001 From: Prathamesh Gursal Date: Tue, 1 Oct 2024 23:07:44 +0530 Subject: [PATCH 01/10] loader component is created --- frontend/src/utils/loader.tsx | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 frontend/src/utils/loader.tsx diff --git a/frontend/src/utils/loader.tsx b/frontend/src/utils/loader.tsx new file mode 100644 index 00000000..29e7c34e --- /dev/null +++ b/frontend/src/utils/loader.tsx @@ -0,0 +1,15 @@ +import React from 'react'; + +const Loader: React.FC = () => { + return ( +
+
+
+
+
+
+
+ ); +}; + +export default Loader; From 034dfffb85d112a382040dbafcc38ddf5cd90e6b Mon Sep 17 00:00:00 2001 From: GURSAL PRATHAMESH APPASAHEB <135983263+prathamesh424@users.noreply.github.com> Date: Fri, 4 Oct 2024 23:47:37 +0530 Subject: [PATCH 02/10] added the skeleton loader reusable component --- frontend/src/utils/loader.tsx | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/frontend/src/utils/loader.tsx b/frontend/src/utils/loader.tsx index 29e7c34e..0d579f85 100644 --- a/frontend/src/utils/loader.tsx +++ b/frontend/src/utils/loader.tsx @@ -1,15 +1,22 @@ import React from 'react'; -const Loader: React.FC = () => { +const SkeletonLoader = ({ count = 1 }) => { return ( -
-
-
-
-
-
+
+ {[...Array(count)].map((_, idx) => ( +
+
+
+
+
+
+
+ ))}
); }; -export default Loader; +export default SkeletonLoader; + +// calling function should be pass --- no. of sketeton cards +// eg . ----------->>> From 0fc48ea03f619db5b2f0921ca8f65850ca6c4c95 Mon Sep 17 00:00:00 2001 From: Prathamesh Gursal Date: Sun, 6 Oct 2024 11:13:01 +0530 Subject: [PATCH 03/10] applying loader in the settings respective components --- frontend/src/settings/APIKeys.tsx | 97 ++++++++------- frontend/src/settings/Analytics.tsx | 185 ++++++++++++++++------------ frontend/src/settings/Documents.tsx | 156 ++++++++++++----------- frontend/src/settings/Logs.tsx | 105 +++++++++------- frontend/src/utils/loader.tsx | 13 +- 5 files changed, 319 insertions(+), 237 deletions(-) diff --git a/frontend/src/settings/APIKeys.tsx b/frontend/src/settings/APIKeys.tsx index ebb32268..c108f93a 100644 --- a/frontend/src/settings/APIKeys.tsx +++ b/frontend/src/settings/APIKeys.tsx @@ -1,20 +1,23 @@ -import React from 'react'; +import React, { useState, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; import userService from '../api/services/userService'; import Trash from '../assets/trash.svg'; import CreateAPIKeyModal from '../modals/CreateAPIKeyModal'; import SaveAPIKeyModal from '../modals/SaveAPIKeyModal'; +import SkeletonLoader from '../utils/loader'; import { APIKeyData } from './types'; export default function APIKeys() { const { t } = useTranslation(); - const [isCreateModalOpen, setCreateModal] = React.useState(false); - const [isSaveKeyModalOpen, setSaveKeyModal] = React.useState(false); - const [newKey, setNewKey] = React.useState(''); - const [apiKeys, setApiKeys] = React.useState([]); + const [isCreateModalOpen, setCreateModal] = useState(false); + const [isSaveKeyModalOpen, setSaveKeyModal] = useState(false); + const [newKey, setNewKey] = useState(''); + const [apiKeys, setApiKeys] = useState([]); + const [loading, setLoading] = useState(true); const handleFetchKeys = async () => { + setLoading(true); try { const response = await userService.getAPIKeys(); if (!response.ok) { @@ -24,6 +27,8 @@ export default function APIKeys() { setApiKeys(apiKeys); } catch (error) { console.log(error); + } finally { + setLoading(false); } }; @@ -72,9 +77,10 @@ export default function APIKeys() { }); }; - React.useEffect(() => { + useEffect(() => { handleFetchKeys(); }, []); + return (
@@ -98,44 +104,51 @@ export default function APIKeys() { close={() => setSaveKeyModal(false)} /> )} -
-
- - - - - - - - - - - {apiKeys?.map((element, index) => ( - - - - - + + {loading ? ( + + ) : ( +
+
+
- {t('settings.apiKeys.name')} - - {t('settings.apiKeys.sourceDoc')} - - {t('settings.apiKeys.key')} -
{element.name}{element.source}{element.key} - Delete handleDeleteKey(element.id)} - /> -
+ + + + + + - ))} - -
+ {t('settings.apiKeys.name')} + + {t('settings.apiKeys.sourceDoc')} + + {t('settings.apiKeys.key')} +
+ + + {apiKeys?.map((element, index) => ( + + {element.name} + + {element.source} + + {element.key} + + Delete handleDeleteKey(element.id)} + /> + + + ))} + + +
-
+ )}
); diff --git a/frontend/src/settings/Analytics.tsx b/frontend/src/settings/Analytics.tsx index 5ddab2cb..2d22ab33 100644 --- a/frontend/src/settings/Analytics.tsx +++ b/frontend/src/settings/Analytics.tsx @@ -1,3 +1,4 @@ +import React, { useState, useEffect } from 'react'; import { BarElement, CategoryScale, @@ -7,7 +8,6 @@ import { Title, Tooltip, } from 'chart.js'; -import React from 'react'; import { Bar } from 'react-chartjs-2'; import userService from '../api/services/userService'; @@ -17,6 +17,7 @@ import { formatDate } from '../utils/dateTimeUtils'; import { APIKeyData } from './types'; import type { ChartData } from 'chart.js'; +import SkeletonLoader from '../utils/loader'; ChartJS.register( CategoryScale, LinearScale, @@ -35,37 +36,37 @@ const filterOptions = [ ]; export default function Analytics() { - const [messagesData, setMessagesData] = React.useState | null>(null); - const [tokenUsageData, setTokenUsageData] = React.useState | null>(null); - const [feedbackData, setFeedbackData] = React.useState | null>(null); - const [chatbots, setChatbots] = React.useState([]); - const [selectedChatbot, setSelectedChatbot] = - React.useState(); - const [messagesFilter, setMessagesFilter] = React.useState<{ + const [chatbots, setChatbots] = useState([]); + const [selectedChatbot, setSelectedChatbot] = useState(); + const [messagesFilter, setMessagesFilter] = useState<{ label: string; value: string; }>({ label: '30 Days', value: 'last_30_days' }); - const [tokenUsageFilter, setTokenUsageFilter] = React.useState<{ + const [tokenUsageFilter, setTokenUsageFilter] = useState<{ label: string; value: string; }>({ label: '30 Days', value: 'last_30_days' }); - const [feedbackFilter, setFeedbackFilter] = React.useState<{ + const [feedbackFilter, setFeedbackFilter] = useState<{ label: string; value: string; }>({ label: '30 Days', value: 'last_30_days' }); + const [loadingMessages, setLoadingMessages] = useState(true); // Loading state for messages + const [loadingTokens, setLoadingTokens] = useState(true); // Loading state for tokens + const [loadingFeedback, setLoadingFeedback] = useState(true); // Loading state for feedback + const fetchChatbots = async () => { try { const response = await userService.getAPIKeys(); @@ -80,6 +81,7 @@ export default function Analytics() { }; const fetchMessagesData = async (chatbot_id?: string, filter?: string) => { + setLoadingMessages(true); // Start loading try { const response = await userService.getMessageAnalytics({ api_key_id: chatbot_id, @@ -92,10 +94,13 @@ export default function Analytics() { setMessagesData(data.messages); } catch (error) { console.error(error); + } finally { + setLoadingMessages(false); // Stop loading } }; const fetchTokenData = async (chatbot_id?: string, filter?: string) => { + setLoadingTokens(true); // Start loading try { const response = await userService.getTokenAnalytics({ api_key_id: chatbot_id, @@ -108,10 +113,13 @@ export default function Analytics() { setTokenUsageData(data.token_usage); } catch (error) { console.error(error); + } finally { + setLoadingTokens(false); // Stop loading } }; const fetchFeedbackData = async (chatbot_id?: string, filter?: string) => { + setLoadingFeedback(true); // Start loading try { const response = await userService.getFeedbackAnalytics({ api_key_id: chatbot_id, @@ -124,30 +132,33 @@ export default function Analytics() { setFeedbackData(data.feedback); } catch (error) { console.error(error); + } finally { + setLoadingFeedback(false); } }; - React.useEffect(() => { + useEffect(() => { fetchChatbots(); }, []); - React.useEffect(() => { + useEffect(() => { const id = selectedChatbot?.id; const filter = messagesFilter; fetchMessagesData(id, filter?.value); }, [selectedChatbot, messagesFilter]); - React.useEffect(() => { + useEffect(() => { const id = selectedChatbot?.id; const filter = tokenUsageFilter; fetchTokenData(id, filter?.value); }, [selectedChatbot, tokenUsageFilter]); - React.useEffect(() => { + useEffect(() => { const id = selectedChatbot?.id; const filter = feedbackFilter; fetchFeedbackData(id, filter?.value); }, [selectedChatbot, feedbackFilter]); + return (
@@ -181,6 +192,8 @@ export default function Analytics() { border="border" />
+ + {/* Messages Analytics */}
@@ -208,25 +221,31 @@ export default function Analytics() { id="legend-container-1" className="flex flex-row items-center justify-end" >
- - formatDate(item), - ), - datasets: [ - { - label: 'Messages', - data: Object.values(messagesData || {}), - backgroundColor: '#7D54D1', - }, - ], - }} - legendID="legend-container-1" - maxTicksLimitInX={8} - isStacked={false} - /> + {loadingMessages ? ( + + ) : ( + + formatDate(item), + ), + datasets: [ + { + label: 'Messages', + data: Object.values(messagesData || {}), + backgroundColor: '#7D54D1', + }, + ], + }} + legendID="legend-container-1" + maxTicksLimitInX={8} + isStacked={false} + /> + )}
+ + {/* Token Usage Analytics */}

@@ -253,26 +272,32 @@ export default function Analytics() { id="legend-container-2" className="flex flex-row items-center justify-end" >

- - formatDate(item), - ), - datasets: [ - { - label: 'Tokens', - data: Object.values(tokenUsageData || {}), - backgroundColor: '#7D54D1', - }, - ], - }} - legendID="legend-container-2" - maxTicksLimitInX={8} - isStacked={false} - /> + {loadingTokens ? ( + + ) : ( + + formatDate(item), + ), + datasets: [ + { + label: 'Tokens', + data: Object.values(tokenUsageData || {}), + backgroundColor: '#7D54D1', + }, + ], + }} + legendID="legend-container-2" + maxTicksLimitInX={8} + isStacked={false} + /> + )}
+ + {/* Feedback Analytics */}
@@ -300,32 +325,36 @@ export default function Analytics() { id="legend-container-3" className="flex flex-row items-center justify-end" >
- - formatDate(item), - ), - datasets: [ - { - label: 'Positive', - data: Object.values(feedbackData || {}).map( - (item) => item.positive, - ), - backgroundColor: '#8BD154', - }, - { - label: 'Negative', - data: Object.values(feedbackData || {}).map( - (item) => item.negative, - ), - backgroundColor: '#D15454', - }, - ], - }} - legendID="legend-container-3" - maxTicksLimitInX={10} - isStacked={true} - /> + {loadingFeedback ? ( + + ) : ( + + formatDate(item), + ), + datasets: [ + { + label: 'Positive', + data: Object.values(feedbackData || {}).map( + (item) => item.positive, + ), + backgroundColor: '#8BD154', + }, + { + label: 'Negative', + data: Object.values(feedbackData || {}).map( + (item) => item.negative, + ), + backgroundColor: '#D15454', + }, + ], + }} + legendID="legend-container-3" + maxTicksLimitInX={10} + isStacked={true} + /> + )}
diff --git a/frontend/src/settings/Documents.tsx b/frontend/src/settings/Documents.tsx index ee88a98f..6de3311a 100644 --- a/frontend/src/settings/Documents.tsx +++ b/frontend/src/settings/Documents.tsx @@ -1,7 +1,7 @@ +import React, { useState } from 'react'; import PropTypes from 'prop-types'; import { useTranslation } from 'react-i18next'; import { useDispatch } from 'react-redux'; - import userService from '../api/services/userService'; import SyncIcon from '../assets/sync.svg'; import Trash from '../assets/trash.svg'; @@ -9,8 +9,8 @@ import DropdownMenu from '../components/DropdownMenu'; import { Doc, DocumentsProps } from '../models/misc'; import { getDocs } from '../preferences/preferenceApi'; import { setSourceDocs } from '../preferences/preferenceSlice'; +import SkeletonLoader from '../utils/loader'; -// Utility function to format numbers const formatTokens = (tokens: number): string => { const roundToTwoDecimals = (num: number): string => { return (Math.round((num + Number.EPSILON) * 100) / 100).toString(); @@ -33,6 +33,9 @@ const Documents: React.FC = ({ }) => { const { t } = useTranslation(); const dispatch = useDispatch(); + + const [loading, setLoading] = useState(false); + const syncOptions = [ { label: 'Never', value: 'never' }, { label: 'Daily', value: 'daily' }, @@ -41,6 +44,7 @@ const Documents: React.FC = ({ ]; const handleManageSync = (doc: Doc, sync_frequency: string) => { + setLoading(true); userService .manageSync({ source_id: doc.id, sync_frequency }) .then(() => { @@ -49,86 +53,96 @@ const Documents: React.FC = ({ .then((data) => { dispatch(setSourceDocs(data)); }) - .catch((error) => console.error(error)); + .catch((error) => console.error(error)) + .finally(() => setLoading(false)); }; + return (
-
-
- - - - - - - - - - - - {documents && - documents.map((document, index) => ( - - - - - - + + ))} + +
- {t('settings.documents.name')} - - {t('settings.documents.date')} - - {t('settings.documents.tokenUsage')} - - {t('settings.documents.type')} -
- {document.name} - - {document.date} - - {document.tokens ? formatTokens(+document.tokens) : ''} - - {document.type === 'remote' ? 'Pre-loaded' : 'Private'} - -
- {document.type !== 'remote' && ( - Delete { - event.stopPropagation(); - handleDeleteDocument(index, document); - }} - /> - )} - {document.syncFrequency && ( -
- { - handleManageSync(document, value); + {loading ? ( + + ) : ( +
+
+ + + + + + + + + + + + {documents && + documents.map((document, index) => ( + + + + + + - - ))} - -
+ {t('settings.documents.name')} + + {t('settings.documents.date')} + + {t('settings.documents.tokenUsage')} + + {t('settings.documents.type')} +
+ {document.name} + + {document.date} + + {document.tokens ? formatTokens(+document.tokens) : ''} + + {document.type === 'remote' ? 'Pre-loaded' : 'Private'} + +
+ {document.type !== 'remote' && ( + Delete { + event.stopPropagation(); + setLoading(true); + handleDeleteDocument(index, document); + setLoading(false); }} - defaultValue={document.syncFrequency} - icon={SyncIcon} /> -
- )} - -
+ )} + {document.syncFrequency && ( +
+ { + handleManageSync(document, value); + }} + defaultValue={document.syncFrequency} + icon={SyncIcon} + /> +
+ )} +
+
+
-
+ )} ); }; + Documents.propTypes = { documents: PropTypes.array.isRequired, handleDeleteDocument: PropTypes.func.isRequired, }; + export default Documents; diff --git a/frontend/src/settings/Logs.tsx b/frontend/src/settings/Logs.tsx index 58ab930d..32d89564 100644 --- a/frontend/src/settings/Logs.tsx +++ b/frontend/src/settings/Logs.tsx @@ -1,20 +1,23 @@ -import React from 'react'; +import React, { useState, useEffect, useRef, useCallback } from 'react'; import userService from '../api/services/userService'; import ChevronRight from '../assets/chevron-right.svg'; import Dropdown from '../components/Dropdown'; +import SkeletonLoader from '../utils/loader'; import { APIKeyData, LogData } from './types'; import CoppyButton from '../components/CopyButton'; export default function Logs() { - const [chatbots, setChatbots] = React.useState([]); - const [selectedChatbot, setSelectedChatbot] = - React.useState(); - const [logs, setLogs] = React.useState([]); - const [page, setPage] = React.useState(1); - const [hasMore, setHasMore] = React.useState(true); + const [chatbots, setChatbots] = useState([]); + const [selectedChatbot, setSelectedChatbot] = useState(); + const [logs, setLogs] = useState([]); + const [page, setPage] = useState(1); + const [hasMore, setHasMore] = useState(true); + const [loadingChatbots, setLoadingChatbots] = useState(true); + const [loadingLogs, setLoadingLogs] = useState(true); const fetchChatbots = async () => { + setLoadingChatbots(true); try { const response = await userService.getAPIKeys(); if (!response.ok) { @@ -24,10 +27,13 @@ export default function Logs() { setChatbots(chatbots); } catch (error) { console.error(error); + } finally { + setLoadingChatbots(false); } }; const fetchLogs = async () => { + setLoadingLogs(true); try { const response = await userService.getLogs({ page: page, @@ -38,20 +44,23 @@ export default function Logs() { throw new Error('Failed to fetch logs'); } const olderLogs = await response.json(); - setLogs([...logs, ...olderLogs.logs]); + setLogs((prevLogs) => [...prevLogs, ...olderLogs.logs]); setHasMore(olderLogs.has_more); } catch (error) { console.error(error); + } finally { + setLoadingLogs(false); } }; - React.useEffect(() => { + useEffect(() => { fetchChatbots(); }, []); - React.useEffect(() => { + useEffect(() => { if (hasMore) fetchLogs(); }, [page, selectedChatbot]); + return (
@@ -59,38 +68,47 @@ export default function Logs() {

Filter by chatbot

- ({ - label: chatbot.name, - value: chatbot.id, - })), - { label: 'None', value: '' }, - ]} - placeholder="Select chatbot" - onSelect={(chatbot: { label: string; value: string }) => { - setSelectedChatbot( - chatbots.find((item) => item.id === chatbot.value), - ); - setLogs([]); - setPage(1); - setHasMore(true); - }} - selectedValue={ - (selectedChatbot && { - label: selectedChatbot.name, - value: selectedChatbot.id, - }) || - null - } - rounded="3xl" - border="border" - /> + {loadingChatbots ? ( + + ) : ( + ({ + label: chatbot.name, + value: chatbot.id, + })), + { label: 'None', value: '' }, + ]} + placeholder="Select chatbot" + onSelect={(chatbot: { label: string; value: string }) => { + setSelectedChatbot( + chatbots.find((item) => item.id === chatbot.value), + ); + setLogs([]); + setPage(1); + setHasMore(true); + }} + selectedValue={ + (selectedChatbot && { + label: selectedChatbot.name, + value: selectedChatbot.id, + }) || + null + } + rounded="3xl" + border="border" + /> + )}
+
- + {loadingLogs ? ( + + ) : ( + + )}
); @@ -102,15 +120,16 @@ type LogsTableProps = { }; function LogsTable({ logs, setPage }: LogsTableProps) { - const observerRef = React.useRef(); - const firstObserver = React.useCallback((node: HTMLDivElement) => { + const observerRef = useRef(); + const firstObserver = useCallback((node: HTMLDivElement) => { if (observerRef.current) { - observerRef.current = new IntersectionObserver((enteries) => { - if (enteries[0].isIntersecting) setPage((prev) => prev + 1); + observerRef.current = new IntersectionObserver((entries) => { + if (entries[0].isIntersecting) setPage((prev) => prev + 1); }); } if (node && observerRef.current) observerRef.current.observe(node); }, []); + return (
diff --git a/frontend/src/utils/loader.tsx b/frontend/src/utils/loader.tsx index 0d579f85..bbf8ba25 100644 --- a/frontend/src/utils/loader.tsx +++ b/frontend/src/utils/loader.tsx @@ -1,10 +1,17 @@ import React from 'react'; -const SkeletonLoader = ({ count = 1 }) => { +interface SkeletonLoaderProps { + count?: number; // Optional prop to define the number of skeleton loaders +} + +const SkeletonLoader: React.FC = ({ count = 1 }) => { return (
{[...Array(count)].map((_, idx) => ( -
+
@@ -18,5 +25,5 @@ const SkeletonLoader = ({ count = 1 }) => { export default SkeletonLoader; -// calling function should be pass --- no. of sketeton cards +// calling function should be pass --- no. of sketeton cards // eg . ----------->>> From a8d371045b6bbd19f8aad7eaed8a31c13340af46 Mon Sep 17 00:00:00 2001 From: Prathamesh Gursal Date: Sun, 6 Oct 2024 11:40:58 +0530 Subject: [PATCH 04/10] updated the loader in settings --- frontend/src/settings/APIKeys.tsx | 78 +++++++++------- frontend/src/settings/Analytics.tsx | 14 +-- frontend/src/settings/Documents.tsx | 132 +++++++++++++++------------- 3 files changed, 124 insertions(+), 100 deletions(-) diff --git a/frontend/src/settings/APIKeys.tsx b/frontend/src/settings/APIKeys.tsx index 4517e647..e05f3a1f 100644 --- a/frontend/src/settings/APIKeys.tsx +++ b/frontend/src/settings/APIKeys.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useState } from 'react'; import { useTranslation } from 'react-i18next'; import userService from '../api/services/userService'; @@ -6,6 +6,7 @@ import Trash from '../assets/trash.svg'; import CreateAPIKeyModal from '../modals/CreateAPIKeyModal'; import SaveAPIKeyModal from '../modals/SaveAPIKeyModal'; import { APIKeyData } from './types'; +import SkeletonLoader from '../utils/loader'; export default function APIKeys() { const { t } = useTranslation(); @@ -13,8 +14,10 @@ export default function APIKeys() { const [isSaveKeyModalOpen, setSaveKeyModal] = React.useState(false); const [newKey, setNewKey] = React.useState(''); const [apiKeys, setApiKeys] = React.useState([]); + const [loading, setLoading] = useState(true); const handleFetchKeys = async () => { + setLoading(true); try { const response = await userService.getAPIKeys(); if (!response.ok) { @@ -24,6 +27,8 @@ export default function APIKeys() { setApiKeys(apiKeys); } catch (error) { console.log(error); + } finally { + setLoading(false); } }; @@ -75,6 +80,7 @@ export default function APIKeys() { React.useEffect(() => { handleFetchKeys(); }, []); + return (
@@ -100,41 +106,45 @@ export default function APIKeys() { )}
- - - - - - - - - - - {!apiKeys?.length && ( + {loading ? ( + + ) : ( +
{t('settings.apiKeys.name')}{t('settings.apiKeys.sourceDoc')}{t('settings.apiKeys.key')}
+ - + + + + - )} - {apiKeys?.map((element, index) => ( - - - - - - - ))} - -
- {t('settings.apiKeys.noData')} - {t('settings.apiKeys.name')}{t('settings.apiKeys.sourceDoc')}{t('settings.apiKeys.key')}
{element.name}{element.source}{element.key} - Delete handleDeleteKey(element.id)} - /> -
+ + + {!apiKeys?.length && ( + + + {t('settings.apiKeys.noData')} + + + )} + {apiKeys?.map((element, index) => ( + + {element.name} + {element.source} + {element.key} + + Delete handleDeleteKey(element.id)} + /> + + + ))} + + + )}
diff --git a/frontend/src/settings/Analytics.tsx b/frontend/src/settings/Analytics.tsx index 2d22ab33..b736f6ad 100644 --- a/frontend/src/settings/Analytics.tsx +++ b/frontend/src/settings/Analytics.tsx @@ -63,9 +63,9 @@ export default function Analytics() { value: string; }>({ label: '30 Days', value: 'last_30_days' }); - const [loadingMessages, setLoadingMessages] = useState(true); // Loading state for messages - const [loadingTokens, setLoadingTokens] = useState(true); // Loading state for tokens - const [loadingFeedback, setLoadingFeedback] = useState(true); // Loading state for feedback + const [loadingMessages, setLoadingMessages] = useState(true); + const [loadingTokens, setLoadingTokens] = useState(true); + const [loadingFeedback, setLoadingFeedback] = useState(true); const fetchChatbots = async () => { try { @@ -81,7 +81,7 @@ export default function Analytics() { }; const fetchMessagesData = async (chatbot_id?: string, filter?: string) => { - setLoadingMessages(true); // Start loading + setLoadingMessages(true); try { const response = await userService.getMessageAnalytics({ api_key_id: chatbot_id, @@ -95,7 +95,7 @@ export default function Analytics() { } catch (error) { console.error(error); } finally { - setLoadingMessages(false); // Stop loading + setLoadingMessages(false); } }; @@ -114,12 +114,12 @@ export default function Analytics() { } catch (error) { console.error(error); } finally { - setLoadingTokens(false); // Stop loading + setLoadingTokens(false); } }; const fetchFeedbackData = async (chatbot_id?: string, filter?: string) => { - setLoadingFeedback(true); // Start loading + setLoadingFeedback(true); try { const response = await userService.getFeedbackAnalytics({ api_key_id: chatbot_id, diff --git a/frontend/src/settings/Documents.tsx b/frontend/src/settings/Documents.tsx index b45c968d..a5e5efb7 100644 --- a/frontend/src/settings/Documents.tsx +++ b/frontend/src/settings/Documents.tsx @@ -1,3 +1,4 @@ +import React, { useState } from 'react'; import PropTypes from 'prop-types'; import { useTranslation } from 'react-i18next'; import { useDispatch } from 'react-redux'; @@ -6,6 +7,7 @@ import userService from '../api/services/userService'; import SyncIcon from '../assets/sync.svg'; import Trash from '../assets/trash.svg'; import DropdownMenu from '../components/DropdownMenu'; +import SkeletonLoader from '../utils/loader'; import { Doc, DocumentsProps } from '../models/misc'; import { getDocs } from '../preferences/preferenceApi'; import { setSourceDocs } from '../preferences/preferenceSlice'; @@ -33,6 +35,7 @@ const Documents: React.FC = ({ }) => { const { t } = useTranslation(); const dispatch = useDispatch(); + const [loading, setLoading] = useState(false); const syncOptions = [ { label: 'Never', value: 'never' }, { label: 'Daily', value: 'daily' }, @@ -41,6 +44,7 @@ const Documents: React.FC = ({ ]; const handleManageSync = (doc: Doc, sync_frequency: string) => { + setLoading(true); userService .manageSync({ source_id: doc.id, sync_frequency }) .then(() => { @@ -49,81 +53,91 @@ const Documents: React.FC = ({ .then((data) => { dispatch(setSourceDocs(data)); }) - .catch((error) => console.error(error)); + .catch((error) => console.error(error)) + .finally(() => { + setLoading(false); + }); }; + return (
- - - - - - - - - - - - {!documents?.length && ( + {loading ? ( + + ) : ( +
{t('settings.documents.name')}{t('settings.documents.date')}{t('settings.documents.tokenUsage')}{t('settings.documents.type')}
+ - + + + + + - )} - {documents && - documents.map((document, index) => ( - - - - - - + + {!documents?.length && ( + + - ))} - -
- {t('settings.documents.noData')} - {t('settings.documents.name')}{t('settings.documents.date')}{t('settings.documents.tokenUsage')}{t('settings.documents.type')}
{document.name}{document.date} - {document.tokens ? formatTokens(+document.tokens) : ''} - - {document.type === 'remote' ? 'Pre-loaded' : 'Private'} - -
- {document.type !== 'remote' && ( - Delete { - event.stopPropagation(); - handleDeleteDocument(index, document); - }} - /> - )} - {document.syncFrequency && ( -
- { - handleManageSync(document, value); - }} - defaultValue={document.syncFrequency} - icon={SyncIcon} - /> -
- )} -
+
+ {t('settings.documents.noData')}
+ )} + {documents && + documents.map((document, index) => ( + + {document.name} + {document.date} + + {document.tokens ? formatTokens(+document.tokens) : ''} + + + {document.type === 'remote' ? 'Pre-loaded' : 'Private'} + + +
+ {document.type !== 'remote' && ( + Delete { + event.stopPropagation(); + handleDeleteDocument(index, document); + }} + /> + )} + {document.syncFrequency && ( +
+ { + handleManageSync(document, value); + }} + defaultValue={document.syncFrequency} + icon={SyncIcon} + /> +
+ )} +
+ + + ))} + + + )}
); }; + Documents.propTypes = { documents: PropTypes.array.isRequired, handleDeleteDocument: PropTypes.func.isRequired, }; + export default Documents; From b59170078db3064db1e42e08b79e8e3ac35bef71 Mon Sep 17 00:00:00 2001 From: Prathamesh Gursal Date: Tue, 8 Oct 2024 01:19:26 +0530 Subject: [PATCH 05/10] make single loader more responsive --- frontend/src/settings/APIKeys.tsx | 2 +- frontend/src/settings/Analytics.tsx | 29 +++++++------- frontend/src/settings/Documents.tsx | 2 +- frontend/src/settings/Logs.tsx | 4 +- frontend/src/utils/loader.tsx | 62 ++++++++++++++++++++++++----- package-lock.json | 2 +- 6 files changed, 71 insertions(+), 30 deletions(-) diff --git a/frontend/src/settings/APIKeys.tsx b/frontend/src/settings/APIKeys.tsx index e05f3a1f..eb9c14e8 100644 --- a/frontend/src/settings/APIKeys.tsx +++ b/frontend/src/settings/APIKeys.tsx @@ -107,7 +107,7 @@ export default function APIKeys() {
{loading ? ( - + ) : ( diff --git a/frontend/src/settings/Analytics.tsx b/frontend/src/settings/Analytics.tsx index b736f6ad..b6948d70 100644 --- a/frontend/src/settings/Analytics.tsx +++ b/frontend/src/settings/Analytics.tsx @@ -18,6 +18,7 @@ import { APIKeyData } from './types'; import type { ChartData } from 'chart.js'; import SkeletonLoader from '../utils/loader'; + ChartJS.register( CategoryScale, LinearScale, @@ -100,7 +101,7 @@ export default function Analytics() { }; const fetchTokenData = async (chatbot_id?: string, filter?: string) => { - setLoadingTokens(true); // Start loading + setLoadingTokens(true); try { const response = await userService.getTokenAnalytics({ api_key_id: chatbot_id, @@ -195,7 +196,7 @@ export default function Analytics() { {/* Messages Analytics */}
-
+

Messages @@ -246,7 +247,7 @@ export default function Analytics() {

{/* Token Usage Analytics */} -
+

Token Usage @@ -273,7 +274,7 @@ export default function Analytics() { className="flex flex-row items-center justify-end" >

{loadingTokens ? ( - + ) : ( {/* Feedback Analytics */} -
-
+
+

- User Feedback + Feedback

{loadingFeedback ? ( - + ) : ( item.positive, ), - backgroundColor: '#8BD154', + backgroundColor: '#7D54D1', }, { - label: 'Negative', + label: 'Negative Feedback', data: Object.values(feedbackData || {}).map( (item) => item.negative, ), - backgroundColor: '#D15454', + backgroundColor: '#FF6384', }, ], }} legendID="legend-container-3" - maxTicksLimitInX={10} - isStacked={true} + maxTicksLimitInX={8} + isStacked={false} /> )}
diff --git a/frontend/src/settings/Documents.tsx b/frontend/src/settings/Documents.tsx index a5e5efb7..f1cfc0b9 100644 --- a/frontend/src/settings/Documents.tsx +++ b/frontend/src/settings/Documents.tsx @@ -64,7 +64,7 @@ const Documents: React.FC = ({
{loading ? ( - + ) : (
diff --git a/frontend/src/settings/Logs.tsx b/frontend/src/settings/Logs.tsx index 32d89564..28bca57f 100644 --- a/frontend/src/settings/Logs.tsx +++ b/frontend/src/settings/Logs.tsx @@ -69,7 +69,7 @@ export default function Logs() { Filter by chatbot

{loadingChatbots ? ( - + ) : ( {loadingLogs ? ( - + ) : ( )} diff --git a/frontend/src/utils/loader.tsx b/frontend/src/utils/loader.tsx index bbf8ba25..de9cf766 100644 --- a/frontend/src/utils/loader.tsx +++ b/frontend/src/utils/loader.tsx @@ -1,21 +1,64 @@ -import React from 'react'; +import React, { useState, useEffect } from 'react'; interface SkeletonLoaderProps { - count?: number; // Optional prop to define the number of skeleton loaders + count?: number; } const SkeletonLoader: React.FC = ({ count = 1 }) => { + const [skeletonCount, setSkeletonCount] = useState(count); + + useEffect(() => { + const handleResize = () => { + const windowWidth = window.innerWidth; + + if (windowWidth > 1024) { + setSkeletonCount(1); + } else if (windowWidth > 768) { + setSkeletonCount(count); + } else { + setSkeletonCount(Math.min(count, 2)); + } + }; + + handleResize(); + window.addEventListener('resize', handleResize); + + return () => { + window.removeEventListener('resize', handleResize); + }; + }, [count]); + return ( -
- {[...Array(count)].map((_, idx) => ( +
+ {[...Array(skeletonCount)].map((_, idx) => (
-
-
-
+
+
{' '} +
{' '} +
{' '} +
{' '} +
{' '} +
+
{' '} +
+
{' '} +
{' '} +
{' '} +
+
{' '} +
+
{' '} +
{' '} +
{' '} +
{' '} +
+
{' '} +
{' '} +
{' '}
))} @@ -24,6 +67,3 @@ const SkeletonLoader: React.FC = ({ count = 1 }) => { }; export default SkeletonLoader; - -// calling function should be pass --- no. of sketeton cards -// eg . ----------->>> diff --git a/package-lock.json b/package-lock.json index 1d8f28b5..9582c111 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,5 +1,5 @@ { - "name": "DocsGPT", + "name": "DocsGpt", "lockfileVersion": 3, "requires": true, "packages": { From 81c85113164908326f91a6ff2d3100a7b6ee1323 Mon Sep 17 00:00:00 2001 From: Prathamesh Gursal Date: Tue, 8 Oct 2024 01:20:34 +0530 Subject: [PATCH 06/10] added single loader with window responsiveness --- frontend/src/Navigation.tsx | 51 ++++++++++++++-------- frontend/src/components/SourceDropdown.tsx | 9 ++-- frontend/src/upload/Upload.tsx | 4 +- 3 files changed, 41 insertions(+), 23 deletions(-) diff --git a/frontend/src/Navigation.tsx b/frontend/src/Navigation.tsx index 0d99fd75..3a55525a 100644 --- a/frontend/src/Navigation.tsx +++ b/frontend/src/Navigation.tsx @@ -209,24 +209,29 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) { menu toggle )}
-
{ - if (isMobile) { - setNavOpen(!navOpen); - } - }}> + @@ -255,7 +261,8 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) { resetConversation(); }} className={({ isActive }) => - `${isActive ? 'bg-gray-3000 dark:bg-transparent' : '' + `${ + isActive ? 'bg-gray-3000 dark:bg-transparent' : '' } group sticky mx-4 mt-4 flex cursor-pointer gap-2.5 rounded-3xl border border-silver p-3 hover:border-rainy-gray hover:bg-gray-3000 dark:border-purple-taupe dark:text-white dark:hover:bg-transparent` } > @@ -280,7 +287,11 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) { key={conversation.id} conversation={conversation} selectConversation={(id) => handleConversationClick(id)} - onCoversationClick={() => {if (isMobile) { setNavOpen(false) }}} + onCoversationClick={() => { + if (isMobile) { + setNavOpen(false); + } + }} onDeleteConversation={(id) => handleDeleteConversation(id)} onSave={(conversation) => updateConversationName(conversation) @@ -306,7 +317,7 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) { handleDeleteClick={handleDeleteClick} handlePostDocumentSelect={(option?: string) => { if (isMobile) { - setNavOpen(!navOpen) + setNavOpen(!navOpen); } }} /> @@ -314,12 +325,12 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) { className="mt-2 h-9 w-9 hover:cursor-pointer" src={UploadIcon} onClick={() => { - setUploadModalState('ACTIVE') + setUploadModalState('ACTIVE'); if (isMobile) { setNavOpen(!navOpen); } - } - }> + }} + >

{t('sourceDocs')}

@@ -333,7 +344,8 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) { }} to="/settings" className={({ isActive }) => - `my-auto mx-4 flex h-9 cursor-pointer gap-4 rounded-3xl hover:bg-gray-100 dark:hover:bg-[#28292E] ${isActive ? 'bg-gray-3000 dark:bg-transparent' : '' + `my-auto mx-4 flex h-9 cursor-pointer gap-4 rounded-3xl hover:bg-gray-100 dark:hover:bg-[#28292E] ${ + isActive ? 'bg-gray-3000 dark:bg-transparent' : '' }` } > @@ -357,7 +369,8 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) { }} to="/about" className={({ isActive }) => - `my-auto mx-4 flex h-9 cursor-pointer gap-4 rounded-3xl hover:bg-gray-100 dark:hover:bg-[#28292E] ${isActive ? 'bg-gray-3000 dark:bg-[#28292E]' : '' + `my-auto mx-4 flex h-9 cursor-pointer gap-4 rounded-3xl hover:bg-gray-100 dark:hover:bg-[#28292E] ${ + isActive ? 'bg-gray-3000 dark:bg-[#28292E]' : '' }` } > diff --git a/frontend/src/components/SourceDropdown.tsx b/frontend/src/components/SourceDropdown.tsx index 6a348161..f92173a0 100644 --- a/frontend/src/components/SourceDropdown.tsx +++ b/frontend/src/components/SourceDropdown.tsx @@ -121,9 +121,12 @@ function SourceDropdown({ className="flex cursor-pointer items-center justify-between hover:bg-gray-100 dark:text-bright-gray dark:hover:bg-purple-taupe" onClick={handleEmptyDocumentSelect} > - { - handlePostDocumentSelect(null); - }}> + { + handlePostDocumentSelect(null); + }} + > {t('none')}
diff --git a/frontend/src/upload/Upload.tsx b/frontend/src/upload/Upload.tsx index c09bab53..9012059b 100644 --- a/frontend/src/upload/Upload.tsx +++ b/frontend/src/upload/Upload.tsx @@ -275,7 +275,9 @@ function Upload({ 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': ['.docx'], 'text/csv': ['.csv'], - 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': ['.xlsx'], + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': [ + '.xlsx', + ], }, }); From 5b5281e50c06276cbaea9d814f62685c0c0b23ee Mon Sep 17 00:00:00 2001 From: Prathamesh Gursal Date: Wed, 9 Oct 2024 01:28:54 +0530 Subject: [PATCH 07/10] added customize loader for each section --- frontend/src/settings/APIKeys.tsx | 2 +- frontend/src/settings/Analytics.tsx | 6 +- frontend/src/settings/Documents.tsx | 2 +- frontend/src/settings/Logs.tsx | 2 +- frontend/src/utils/loader.tsx | 138 +++++++++++++++++++++------- 5 files changed, 112 insertions(+), 38 deletions(-) diff --git a/frontend/src/settings/APIKeys.tsx b/frontend/src/settings/APIKeys.tsx index eb9c14e8..07370541 100644 --- a/frontend/src/settings/APIKeys.tsx +++ b/frontend/src/settings/APIKeys.tsx @@ -107,7 +107,7 @@ export default function APIKeys() {
{loading ? ( - + ) : (
diff --git a/frontend/src/settings/Analytics.tsx b/frontend/src/settings/Analytics.tsx index b6948d70..6103c35e 100644 --- a/frontend/src/settings/Analytics.tsx +++ b/frontend/src/settings/Analytics.tsx @@ -223,7 +223,7 @@ export default function Analytics() { className="flex flex-row items-center justify-end" > {loadingMessages ? ( - + ) : ( {loadingTokens ? ( - + ) : ( {loadingFeedback ? ( - + ) : ( = ({
{loading ? ( - + ) : (
diff --git a/frontend/src/settings/Logs.tsx b/frontend/src/settings/Logs.tsx index 28bca57f..1e2bd953 100644 --- a/frontend/src/settings/Logs.tsx +++ b/frontend/src/settings/Logs.tsx @@ -105,7 +105,7 @@ export default function Logs() {
{loadingLogs ? ( - + ) : ( )} diff --git a/frontend/src/utils/loader.tsx b/frontend/src/utils/loader.tsx index de9cf766..a9548d6d 100644 --- a/frontend/src/utils/loader.tsx +++ b/frontend/src/utils/loader.tsx @@ -2,9 +2,13 @@ import React, { useState, useEffect } from 'react'; interface SkeletonLoaderProps { count?: number; + component?: 'default' | 'analysis' | 'chatbot' | 'logs'; } -const SkeletonLoader: React.FC = ({ count = 1 }) => { +const SkeletonLoader: React.FC = ({ + count = 1, + component = 'default', +}) => { const [skeletonCount, setSkeletonCount] = useState(count); useEffect(() => { @@ -30,38 +34,108 @@ const SkeletonLoader: React.FC = ({ count = 1 }) => { return (
- {[...Array(skeletonCount)].map((_, idx) => ( -
-
-
-
{' '} -
{' '} -
{' '} -
{' '} -
{' '} + {component === 'default' + ? [...Array(skeletonCount)].map((_, idx) => ( +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
{' '} -
-
{' '} -
{' '} -
{' '} -
-
{' '} -
-
{' '} -
{' '} -
{' '} -
{' '} -
-
{' '} -
{' '} -
{' '} -
-
- ))} + )) + : component === 'analysis' + ? [...Array(skeletonCount)].map((_, idx) => ( +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )) + : component === 'chatbot' + ? [...Array(skeletonCount)].map((_, idx) => ( +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )) + : [...Array(skeletonCount)].map((_, idx) => ( +
+
+
+
+
+
+
+
+
+ ))}
); }; From 4402442e54450941c616932823059cf7abd91a7f Mon Sep 17 00:00:00 2001 From: Prathamesh Gursal Date: Thu, 10 Oct 2024 21:00:31 +0530 Subject: [PATCH 08/10] updated chatbot loader --- frontend/src/utils/loader.tsx | 193 +++++++++++++++++----------------- 1 file changed, 94 insertions(+), 99 deletions(-) diff --git a/frontend/src/utils/loader.tsx b/frontend/src/utils/loader.tsx index a9548d6d..e9a136e4 100644 --- a/frontend/src/utils/loader.tsx +++ b/frontend/src/utils/loader.tsx @@ -34,108 +34,103 @@ const SkeletonLoader: React.FC = ({ return (
- {component === 'default' - ? [...Array(skeletonCount)].map((_, idx) => ( -
-
-
-
-
-
-
-
+ {component === 'default' ? ( + [...Array(skeletonCount)].map((_, idx) => ( +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )) + ) : component === 'analysis' ? ( + [...Array(skeletonCount)].map((_, idx) => ( +
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
- )) - : component === 'analysis' - ? [...Array(skeletonCount)].map((_, idx) => ( -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- )) - : component === 'chatbot' - ? [...Array(skeletonCount)].map((_, idx) => ( -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- )) - : [...Array(skeletonCount)].map((_, idx) => ( -
-
-
-
-
-
-
-
-
- ))} +
+ )) + ) : component === 'chatbot' ? ( +
+
+
+
+
+
+
+
+ + {[...Array(skeletonCount * 6)].map((_, idx) => ( +
+
+
+
+
+
+ ))} +
+ ) : ( + [...Array(skeletonCount)].map((_, idx) => ( +
+
+
+
+
+
+
+
+
+ )) + )}
); }; From 973304e0d71bad4c8abd1f7619c414f25640798c Mon Sep 17 00:00:00 2001 From: Prathamesh Gursal Date: Tue, 15 Oct 2024 21:11:00 +0530 Subject: [PATCH 09/10] changes --- frontend/src/locale/en.json | 6 +++--- frontend/src/locale/es.json | 2 +- frontend/src/locale/jp.json | 2 +- frontend/src/locale/zh-TW.json | 2 +- frontend/src/locale/zh.json | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/frontend/src/locale/en.json b/frontend/src/locale/en.json index 335d1e6c..cefb99b7 100644 --- a/frontend/src/locale/en.json +++ b/frontend/src/locale/en.json @@ -10,9 +10,9 @@ "sourceDocs": "Source", "none": "None", "cancel": "Cancel", - "help":"Help", - "emailUs":"Email us", - "documentation":"documentation", + "help": "Help", + "emailUs": "Email us", + "documentation": "documentation", "demo": [ { "header": "Learn about DocsGPT", diff --git a/frontend/src/locale/es.json b/frontend/src/locale/es.json index 9358aafa..66b457e8 100644 --- a/frontend/src/locale/es.json +++ b/frontend/src/locale/es.json @@ -10,7 +10,7 @@ "sourceDocs": "Fuente", "none": "Nada", "cancel": "Cancelar", - "help":"Asistencia", + "help": "Asistencia", "emailUs": "Envíanos un correo", "documentation": "documentación", "demo": [ diff --git a/frontend/src/locale/jp.json b/frontend/src/locale/jp.json index 2adc4947..53f1da14 100644 --- a/frontend/src/locale/jp.json +++ b/frontend/src/locale/jp.json @@ -10,7 +10,7 @@ "sourceDocs": "ソース", "none": "なし", "cancel": "キャンセル", - "help":"ヘルプ", + "help": "ヘルプ", "emailUs": "メールを送る", "documentation": "ドキュメント", "demo": [ diff --git a/frontend/src/locale/zh-TW.json b/frontend/src/locale/zh-TW.json index a826baed..afcef769 100644 --- a/frontend/src/locale/zh-TW.json +++ b/frontend/src/locale/zh-TW.json @@ -10,7 +10,7 @@ "sourceDocs": "原始文件", "none": "無", "cancel": "取消", - "help":"聯繫支援", + "help": "聯繫支援", "emailUs": "寄送電子郵件給我們", "documentation": "文件", "demo": [ diff --git a/frontend/src/locale/zh.json b/frontend/src/locale/zh.json index 427bddc4..0d60a701 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": [ From a25d5d98a4678f2d72feca693efb3c1a56cbe892 Mon Sep 17 00:00:00 2001 From: Prathamesh Gursal Date: Tue, 15 Oct 2024 21:23:32 +0530 Subject: [PATCH 10/10] package installed --- frontend/package-lock.json | 2 +- package-lock.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 75f4ea8e..4087e4f5 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1675,7 +1675,7 @@ "version": "18.3.0", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.0.tgz", "integrity": "sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==", - "dev": true, + "devOptional": true, "dependencies": { "@types/react": "*" } diff --git a/package-lock.json b/package-lock.json index 9582c111..1d8f28b5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,5 +1,5 @@ { - "name": "DocsGpt", + "name": "DocsGPT", "lockfileVersion": 3, "requires": true, "packages": {