diff --git a/frontend/.eslintrc.cjs b/frontend/.eslintrc.cjs index 323a0d3c0..88fb220b4 100644 --- a/frontend/.eslintrc.cjs +++ b/frontend/.eslintrc.cjs @@ -18,6 +18,7 @@ module.exports = { }, plugins: ['react', 'unused-imports'], rules: { + 'react/prop-types': 'off', 'unused-imports/no-unused-imports': 'error', 'react/react-in-jsx-scope': 'off', 'prettier/prettier': [ diff --git a/frontend/src/components/SkeletonLoader.tsx b/frontend/src/components/SkeletonLoader.tsx index e9a136e47..b73c5835d 100644 --- a/frontend/src/components/SkeletonLoader.tsx +++ b/frontend/src/components/SkeletonLoader.tsx @@ -1,8 +1,14 @@ -import React, { useState, useEffect } from 'react'; +import { useState, useEffect } from 'react'; interface SkeletonLoaderProps { count?: number; - component?: 'default' | 'analysis' | 'chatbot' | 'logs'; + component?: + | 'default' + | 'analysis' + | 'logs' + | 'table' + | 'chatbot' + | 'dropdown'; } const SkeletonLoader: React.FC = ({ @@ -32,107 +38,162 @@ const SkeletonLoader: React.FC = ({ }; }, [count]); - return ( -
- {component === 'default' ? ( - [...Array(skeletonCount)].map((_, idx) => ( -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ const renderTable = () => ( + <> + {[...Array(4)].map((_, idx) => ( + + +
+ + +
+ + +
+ + +
+ + + ))} + + ); + + const renderChatbot = () => ( + <> + {[...Array(4)].map((_, idx) => ( + + +
+ + +
+ + +
+ + +
+ + + ))} + + ); + + const renderDropdown = () => ( +
+
+
+
+
+
+
+ ); + + const renderLogs = () => ( +
+ {[...Array(8)].map((_, idx) => ( +
+
+
+
+
+
+
- )) - ) : component === 'analysis' ? ( - [...Array(skeletonCount)].map((_, idx) => ( -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+ ))} +
+ ); + + const renderDefault = () => ( + <> + {[...Array(skeletonCount)].map((_, idx) => ( +
+
+
+
+
+
+
+
-
- )) - ) : component === 'chatbot' ? ( -
-
-
-
-
-
-
-
- - {[...Array(skeletonCount * 6)].map((_, idx) => ( -
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
- ))} +
+
+
+
- ) : ( - [...Array(skeletonCount)].map((_, idx) => ( -
-
-
-
-
-
-
+ ))} + + ); + + const renderAnalysis = () => ( + <> + {[...Array(skeletonCount)].map((_, idx) => ( +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
- )) - )} -
+
+ ))} + ); + + const componentMap = { + table: renderTable, + chatbot: renderChatbot, + dropdown: renderDropdown, + logs: renderLogs, + default: renderDefault, + analysis: renderAnalysis, + }; + + const render = componentMap[component] || componentMap.default; + + return <>{render()}; }; export default SkeletonLoader; diff --git a/frontend/src/conversation/ConversationBubble.tsx b/frontend/src/conversation/ConversationBubble.tsx index 668b09351..bfa225cca 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} ); }, tr({ children }) { return ( - + {children} ); }, - td({ children }) { - return {children}; - }, th({ children }) { return {children}; }, + td({ children }) { + return {children}; + }, }} > {preprocessLaTeX(message)} diff --git a/frontend/src/hooks/index.ts b/frontend/src/hooks/index.ts index b993f3da0..2247cb078 100644 --- a/frontend/src/hooks/index.ts +++ b/frontend/src/hooks/index.ts @@ -112,3 +112,23 @@ export function useDarkTheme() { return [isDarkTheme, toggleTheme, componentMounted] as const; } + +export function useLoaderState( + initialState = false, + delay = 250, +): [boolean, (value: boolean) => void] { + const [state, setState] = useState(initialState); + + const setLoaderState = (value: boolean) => { + if (value) { + setState(true); + } else { + // Only add delay when changing from true to false + setTimeout(() => { + setState(false); + }, delay); + } + }; + + return [state, setLoaderState]; +} diff --git a/frontend/src/settings/APIKeys.tsx b/frontend/src/settings/APIKeys.tsx index 44242cda2..642566f12 100644 --- a/frontend/src/settings/APIKeys.tsx +++ b/frontend/src/settings/APIKeys.tsx @@ -8,14 +8,15 @@ import SaveAPIKeyModal from '../modals/SaveAPIKeyModal'; import ConfirmationModal from '../modals/ConfirmationModal'; import { APIKeyData } from './types'; import SkeletonLoader from '../components/SkeletonLoader'; +import { useLoaderState } from '../hooks'; 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 [loading, setLoading] = useState(true); + const [isCreateModalOpen, setCreateModal] = useState(false); + const [isSaveKeyModalOpen, setSaveKeyModal] = useState(false); + const [newKey, setNewKey] = useState(''); + const [apiKeys, setApiKeys] = useState([]); + const [loading, setLoading] = useLoaderState(true); const [keyToDelete, setKeyToDelete] = useState<{ id: string; name: string; @@ -38,6 +39,7 @@ export default function APIKeys() { }; const handleDeleteKey = (id: string) => { + setLoading(true); userService .deleteAPIKey({ id }) .then((response) => { @@ -47,12 +49,16 @@ export default function APIKeys() { return response.json(); }) .then((data) => { - data.success === true && + if (data.success === true) { setApiKeys((previous) => previous.filter((elem) => elem.id !== id)); + } setKeyToDelete(null); }) .catch((error) => { console.error(error); + }) + .finally(() => { + setLoading(false); }); }; @@ -63,6 +69,7 @@ export default function APIKeys() { prompt_id: string; chunks: string; }) => { + setLoading(true); userService .createAPIKey(payload) .then((response) => { @@ -80,6 +87,9 @@ export default function APIKeys() { }) .catch((error) => { console.error(error); + }) + .finally(() => { + setLoading(false); }); }; @@ -124,73 +134,72 @@ export default function APIKeys() { )}
- {loading ? ( - - ) : ( -
-
-
- - - - - - - +
+
+
+
- {t('settings.apiKeys.name')} - - {t('settings.apiKeys.sourceDoc')} - - {t('settings.apiKeys.key')} -
+ + + + + + + + + + {loading ? ( + + ) : !apiKeys?.length ? ( + + - - - {!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')} +
- {t('settings.apiKeys.noData')} + ) : ( + Array.isArray(apiKeys) && + apiKeys.map((element, index) => ( +
{element.name}{element.source}{element.key} + {`Delete + setKeyToDelete({ + id: element.id, + name: element.name, + }) + } + />
{element.name}{element.source}{element.key} - {`Delete - setKeyToDelete({ - id: element.id, - name: element.name, - }) - } - /> -
-
+ )) + )} + +
- )} +
diff --git a/frontend/src/settings/Analytics.tsx b/frontend/src/settings/Analytics.tsx index 441f6b19f..4d3904953 100644 --- a/frontend/src/settings/Analytics.tsx +++ b/frontend/src/settings/Analytics.tsx @@ -16,6 +16,7 @@ import Dropdown from '../components/Dropdown'; import { htmlLegendPlugin } from '../utils/chartUtils'; import { formatDate } from '../utils/dateTimeUtils'; import { APIKeyData } from './types'; +import { useLoaderState } from '../hooks'; import type { ChartData } from 'chart.js'; import SkeletonLoader from '../components/SkeletonLoader'; @@ -88,9 +89,9 @@ export default function Analytics() { value: 'last_30_days', }); - const [loadingMessages, setLoadingMessages] = useState(true); - const [loadingTokens, setLoadingTokens] = useState(true); - const [loadingFeedback, setLoadingFeedback] = useState(true); + const [loadingMessages, setLoadingMessages] = useLoaderState(true); + const [loadingTokens, setLoadingTokens] = useLoaderState(true); + const [loadingFeedback, setLoadingFeedback] = useLoaderState(true); const fetchChatbots = async () => { try { diff --git a/frontend/src/settings/Documents.tsx b/frontend/src/settings/Documents.tsx index 5eedba21d..e72a6e168 100644 --- a/frontend/src/settings/Documents.tsx +++ b/frontend/src/settings/Documents.tsx @@ -15,7 +15,7 @@ import DropdownMenu from '../components/DropdownMenu'; import Input from '../components/Input'; import SkeletonLoader from '../components/SkeletonLoader'; import Spinner from '../components/Spinner'; -import { useDarkTheme } from '../hooks'; +import { useDarkTheme, useLoaderState } from '../hooks'; import ChunkModal from '../modals/ChunkModal'; import ConfirmationModal from '../modals/ConfirmationModal'; import { ActiveState, Doc, DocumentsProps } from '../models/misc'; @@ -54,7 +54,7 @@ export default function Documents({ const [searchTerm, setSearchTerm] = useState(''); const [modalState, setModalState] = useState('INACTIVE'); const [isOnboarding, setIsOnboarding] = useState(false); - const [loading, setLoading] = useState(false); + const [loading, setLoading] = useLoaderState(false); const [sortField, setSortField] = useState<'date' | 'tokens'>('date'); const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('desc'); // Pagination @@ -214,123 +214,118 @@ export default function Documents({
- {loading ? ( - - ) : ( -
- {' '} - {/* Removed overflow-auto */} -
- - - - - - - + )) + )} + +
- {t('settings.documents.name')} - -
- {t('settings.documents.date')} - refreshDocs('date')} - src={caretSort} - alt="sort" - /> -
-
-
- - {t('settings.documents.tokenUsage')} - - - {t('settings.documents.tokenUsage')} - - refreshDocs('tokens')} - src={caretSort} - alt="sort" - /> -
-
- - {t('settings.documents.actions')} +
+ {' '} +
+ + + + + + + refreshDocs('tokens')} + src={caretSort} + alt="sort" + /> + + + + + + + {loading ? ( + + ) : !currentDocuments?.length ? ( + + - - - {!currentDocuments?.length ? ( - + ) : ( + currentDocuments.map((document, index) => ( + setShowDocumentChunks(document)} + > - - ) : ( - currentDocuments.map((document, index) => ( - setShowDocumentChunks(document)} - > - - - - + + - - )) - )} - -
+ {t('settings.documents.name')} + +
+ {t('settings.documents.date')} + refreshDocs('date')} + src={caretSort} + alt="sort" + /> +
+
+
+ + {t('settings.documents.tokenUsage')} + + + {t('settings.documents.tokenUsage')} -
+ + {t('settings.documents.actions')} + +
+ {t('settings.documents.noData')} +
- {t('settings.documents.noData')} + {document.name}
- {document.name} - - {document.date ? formatDate(document.date) : ''} - - {document.tokens - ? formatTokens(+document.tokens) - : ''} - -
- {!document.syncFrequency && ( -
- )} - {document.syncFrequency && ( - { - handleManageSync(document, value); - }} - defaultValue={document.syncFrequency} - icon={SyncIcon} - /> - )} -
+ {document.date ? formatDate(document.date) : ''} + + {document.tokens ? formatTokens(+document.tokens) : ''} + +
+ {!document.syncFrequency && ( +
+ )} + {document.syncFrequency && ( + { + handleManageSync(document, value); }} - className="inline-flex items-center justify-center w-8 h-8 rounded-full hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors flex-shrink-0" - > - {t('convTile.delete')} - -
-
-
+ defaultValue={document.syncFrequency} + icon={SyncIcon} + /> + )} + +
+ +
- )} +
@@ -395,7 +390,7 @@ function DocumentChunks({ const [page, setPage] = useState(1); const [perPage, setPerPage] = useState(5); const [totalChunks, setTotalChunks] = useState(0); - const [loading, setLoading] = useState(true); + const [loading, setLoading] = useLoaderState(true); const [searchTerm, setSearchTerm] = useState(''); const [addModal, setAddModal] = useState('INACTIVE'); const [editModal, setEditModal] = useState<{ @@ -425,6 +420,7 @@ function DocumentChunks({ }); } catch (e) { console.log(e); + setLoading(false); } }; diff --git a/frontend/src/settings/Logs.tsx b/frontend/src/settings/Logs.tsx index 9ff577b3a..3f4d725b9 100644 --- a/frontend/src/settings/Logs.tsx +++ b/frontend/src/settings/Logs.tsx @@ -7,6 +7,7 @@ import Dropdown from '../components/Dropdown'; import SkeletonLoader from '../components/SkeletonLoader'; import { APIKeyData, LogData } from './types'; import CoppyButton from '../components/CopyButton'; +import { useLoaderState } from '../hooks'; export default function Logs() { const { t } = useTranslation(); @@ -15,8 +16,8 @@ export default function Logs() { 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 [loadingChatbots, setLoadingChatbots] = useLoaderState(true); + const [loadingLogs, setLoadingLogs] = useLoaderState(true); const fetchChatbots = async () => { setLoadingChatbots(true); @@ -66,16 +67,16 @@ export default function Logs() { return (
-
- - {loadingChatbots ? ( - - ) : ( + {loadingChatbots ? ( + + ) : ( +
+ - )} -
+
+ )}
- {loadingLogs ? ( - - ) : ( - - )} +
); @@ -122,9 +119,10 @@ export default function Logs() { type LogsTableProps = { logs: LogData[]; setPage: React.Dispatch>; + loading: boolean; }; -function LogsTable({ logs, setPage }: LogsTableProps) { +function LogsTable({ logs, setPage, loading }: LogsTableProps) { const { t } = useTranslation(); const observerRef = useRef(); const firstObserver = useCallback((node: HTMLDivElement) => { @@ -147,7 +145,7 @@ function LogsTable({ logs, setPage }: LogsTableProps) { ref={observerRef} className="flex flex-col items-start h-[51vh] overflow-y-auto bg-transparent flex-grow gap-px" > - {logs.map((log, index) => { + {logs?.map((log, index) => { if (index === logs.length - 1) { return (
@@ -156,6 +154,7 @@ function LogsTable({ logs, setPage }: LogsTableProps) { ); } else return ; })} + {loading && }
); diff --git a/frontend/src/upload/Upload.tsx b/frontend/src/upload/Upload.tsx index 0e030beb7..248d9f110 100644 --- a/frontend/src/upload/Upload.tsx +++ b/frontend/src/upload/Upload.tsx @@ -54,16 +54,28 @@ function Upload({ const advancedFields = schema.filter((field) => field.advanced); return ( - <> - {generalFields.map((field: FormField) => renderField(field))} +
+
+ {generalFields.map((field: FormField) => renderField(field))} +
- {advancedFields.length > 0 && showAdvancedOptions && ( - <> -
- {advancedFields.map((field: FormField) => renderField(field))} - + {advancedFields.length > 0 && ( +
+
+
+
+ {advancedFields.map((field: FormField) => renderField(field))} +
+
+
)} - +
); };