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/frontend/src/components/SkeletonLoader.tsx b/frontend/src/components/SkeletonLoader.tsx new file mode 100644 index 00000000..e9a136e4 --- /dev/null +++ b/frontend/src/components/SkeletonLoader.tsx @@ -0,0 +1,138 @@ +import React, { useState, useEffect } from 'react'; + +interface SkeletonLoaderProps { + count?: number; + component?: 'default' | 'analysis' | 'chatbot' | 'logs'; +} + +const SkeletonLoader: React.FC = ({ + count = 1, + component = 'default', +}) => { + 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 ( +
+ {component === 'default' ? ( + [...Array(skeletonCount)].map((_, idx) => ( +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )) + ) : component === 'analysis' ? ( + [...Array(skeletonCount)].map((_, idx) => ( +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )) + ) : component === 'chatbot' ? ( +
+
+
+
+
+
+
+
+ + {[...Array(skeletonCount * 6)].map((_, idx) => ( +
+
+
+
+
+
+ ))} +
+ ) : ( + [...Array(skeletonCount)].map((_, idx) => ( +
+
+
+
+
+
+
+
+
+ )) + )} +
+ ); +}; + +export default SkeletonLoader; 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": [ diff --git a/frontend/src/settings/APIKeys.tsx b/frontend/src/settings/APIKeys.tsx index 4517e647..e230f272 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 '../components/SkeletonLoader'; 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 5ddab2cb..8baad361 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,8 @@ import { formatDate } from '../utils/dateTimeUtils'; import { APIKeyData } from './types'; import type { ChartData } from 'chart.js'; +import SkeletonLoader from '../components/SkeletonLoader'; + ChartJS.register( CategoryScale, LinearScale, @@ -35,37 +37,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); + const [loadingTokens, setLoadingTokens] = useState(true); + const [loadingFeedback, setLoadingFeedback] = useState(true); + const fetchChatbots = async () => { try { const response = await userService.getAPIKeys(); @@ -80,6 +82,7 @@ export default function Analytics() { }; const fetchMessagesData = async (chatbot_id?: string, filter?: string) => { + setLoadingMessages(true); try { const response = await userService.getMessageAnalytics({ api_key_id: chatbot_id, @@ -92,10 +95,13 @@ export default function Analytics() { setMessagesData(data.messages); } catch (error) { console.error(error); + } finally { + setLoadingMessages(false); } }; const fetchTokenData = async (chatbot_id?: string, filter?: string) => { + setLoadingTokens(true); try { const response = await userService.getTokenAnalytics({ api_key_id: chatbot_id, @@ -108,10 +114,13 @@ export default function Analytics() { setTokenUsageData(data.token_usage); } catch (error) { console.error(error); + } finally { + setLoadingTokens(false); } }; const fetchFeedbackData = async (chatbot_id?: string, filter?: string) => { + setLoadingFeedback(true); try { const response = await userService.getFeedbackAnalytics({ api_key_id: chatbot_id, @@ -124,30 +133,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,8 +193,10 @@ export default function Analytics() { border="border" />
+ + {/* Messages Analytics */}
-
+

Messages @@ -208,26 +222,32 @@ 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 */} +

Token Usage @@ -253,31 +273,37 @@ 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 */} +
+

- User Feedback + Feedback

- - 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 Feedback', + data: Object.values(feedbackData || {}).map( + (item) => item.positive, + ), + backgroundColor: '#7D54D1', + }, + { + label: 'Negative Feedback', + data: Object.values(feedbackData || {}).map( + (item) => item.negative, + ), + backgroundColor: '#FF6384', + }, + ], + }} + legendID="legend-container-3" + maxTicksLimitInX={8} + isStacked={false} + /> + )}
diff --git a/frontend/src/settings/Documents.tsx b/frontend/src/settings/Documents.tsx index b45c968d..867fc9de 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 '../components/SkeletonLoader'; 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; diff --git a/frontend/src/settings/Logs.tsx b/frontend/src/settings/Logs.tsx index 58ab930d..1e248d46 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 '../components/SkeletonLoader'; 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 (