mirror of
https://github.com/arc53/DocsGPT.git
synced 2025-12-01 17:43:15 +00:00
Merge pull request #1188 from prathamesh424/main
Reusable Attractive Skeleton Loader Component is added [Fixes #1181]
This commit is contained in:
2
frontend/package-lock.json
generated
2
frontend/package-lock.json
generated
@@ -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": "*"
|
||||
}
|
||||
|
||||
138
frontend/src/components/SkeletonLoader.tsx
Normal file
138
frontend/src/components/SkeletonLoader.tsx
Normal file
@@ -0,0 +1,138 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
|
||||
interface SkeletonLoaderProps {
|
||||
count?: number;
|
||||
component?: 'default' | 'analysis' | 'chatbot' | 'logs';
|
||||
}
|
||||
|
||||
const SkeletonLoader: React.FC<SkeletonLoaderProps> = ({
|
||||
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 (
|
||||
<div className="flex flex-col space-y-4">
|
||||
{component === 'default' ? (
|
||||
[...Array(skeletonCount)].map((_, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
className={`p-6 ${skeletonCount === 1 ? 'w-full' : 'w-60'} dark:bg-raisin-black rounded-3xl animate-pulse`}
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<div className="h-4 bg-gray-600 rounded mb-2 w-3/4"></div>
|
||||
<div className="h-4 bg-gray-600 rounded mb-2 w-5/6"></div>
|
||||
<div className="h-4 bg-gray-600 rounded mb-2 w-1/2"></div>
|
||||
<div className="h-4 bg-gray-600 rounded mb-2 w-3/4"></div>
|
||||
<div className="h-4 bg-gray-600 rounded mb-2 w-full"></div>
|
||||
</div>
|
||||
<div className="border-t border-gray-600 my-4"></div>
|
||||
<div>
|
||||
<div className="h-4 bg-gray-600 rounded mb-2 w-2/3"></div>
|
||||
<div className="h-4 bg-gray-600 rounded mb-2 w-1/4"></div>
|
||||
<div className="h-4 bg-gray-600 rounded mb-2 w-full"></div>
|
||||
</div>
|
||||
<div className="border-t border-gray-600 my-4"></div>
|
||||
<div>
|
||||
<div className="h-4 bg-gray-600 rounded mb-2 w-5/6"></div>
|
||||
<div className="h-4 bg-gray-600 rounded mb-2 w-1/3"></div>
|
||||
<div className="h-4 bg-gray-600 rounded mb-2 w-2/3"></div>
|
||||
<div className="h-4 bg-gray-600 rounded mb-2 w-full"></div>
|
||||
</div>
|
||||
<div className="border-t border-gray-600 my-4"></div>
|
||||
<div className="h-4 bg-gray-600 rounded w-3/4 mb-2"></div>
|
||||
<div className="h-4 bg-gray-600 rounded w-5/6 mb-2"></div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
) : component === 'analysis' ? (
|
||||
[...Array(skeletonCount)].map((_, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
className="p-6 w-full dark:bg-raisin-black rounded-3xl animate-pulse"
|
||||
>
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-2">
|
||||
<div className="h-4 bg-gray-600 rounded w-1/3 mb-4"></div>
|
||||
<div className="grid grid-cols-6 gap-2 items-end">
|
||||
<div className="h-32 bg-gray-600 rounded"></div>
|
||||
<div className="h-24 bg-gray-600 rounded"></div>
|
||||
<div className="h-40 bg-gray-600 rounded"></div>
|
||||
<div className="h-28 bg-gray-600 rounded"></div>
|
||||
<div className="h-36 bg-gray-600 rounded"></div>
|
||||
<div className="h-20 bg-gray-600 rounded"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="h-4 bg-gray-600 rounded w-1/4 mb-4"></div>
|
||||
<div className="h-32 bg-gray-600 rounded"></div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="h-4 bg-gray-600 rounded w-full"></div>
|
||||
<div className="h-4 bg-gray-600 rounded w-full"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
) : component === 'chatbot' ? (
|
||||
<div className="space-y-2 p-6 w-full dark:bg-raisin-black rounded-3xl animate-pulse">
|
||||
<div className="grid grid-cols-4 gap-2 p-2">
|
||||
<div className="h-4 bg-gray-600 rounded w-full"></div>
|
||||
<div className="h-4 bg-gray-600 rounded w-full"></div>
|
||||
<div className="h-4 bg-gray-600 rounded w-full"></div>
|
||||
<div className="h-4 bg-gray-600 rounded w-full"></div>
|
||||
</div>
|
||||
<div className="border-t border-gray-600 my-2"></div>
|
||||
|
||||
{[...Array(skeletonCount * 6)].map((_, idx) => (
|
||||
<div key={idx} className="grid grid-cols-4 gap-2 p-2 space-x-2">
|
||||
<div className="h-4 bg-gray-500 rounded w-full"></div>
|
||||
<div className="h-4 bg-gray-500 rounded w-full"></div>
|
||||
<div className="h-4 bg-gray-500 rounded w-full"></div>
|
||||
<div className="h-4 bg-gray-500 rounded w-full"></div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
[...Array(skeletonCount)].map((_, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
className="p-6 w-full dark:bg-raisin-black rounded-3xl animate-pulse"
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<div className="h-4 bg-gray-600 rounded w-1/2"></div>
|
||||
<div className="h-4 bg-gray-600 rounded w-5/6"></div>
|
||||
<div className="h-4 bg-gray-600 rounded w-3/4"></div>
|
||||
<div className="h-4 bg-gray-600 rounded w-2/3"></div>
|
||||
<div className="h-4 bg-gray-600 rounded w-1/4"></div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SkeletonLoader;
|
||||
@@ -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",
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
"sourceDocs": "Fuente",
|
||||
"none": "Nada",
|
||||
"cancel": "Cancelar",
|
||||
"help":"Asistencia",
|
||||
"help": "Asistencia",
|
||||
"emailUs": "Envíanos un correo",
|
||||
"documentation": "documentación",
|
||||
"demo": [
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
"sourceDocs": "ソース",
|
||||
"none": "なし",
|
||||
"cancel": "キャンセル",
|
||||
"help":"ヘルプ",
|
||||
"help": "ヘルプ",
|
||||
"emailUs": "メールを送る",
|
||||
"documentation": "ドキュメント",
|
||||
"demo": [
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
"sourceDocs": "原始文件",
|
||||
"none": "無",
|
||||
"cancel": "取消",
|
||||
"help":"聯繫支援",
|
||||
"help": "聯繫支援",
|
||||
"emailUs": "寄送電子郵件給我們",
|
||||
"documentation": "文件",
|
||||
"demo": [
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
"sourceDocs": "源",
|
||||
"none": "无",
|
||||
"cancel": "取消",
|
||||
"help":"联系支持",
|
||||
"help": "联系支持",
|
||||
"emailUs": "给我们发邮件",
|
||||
"documentation": "文档",
|
||||
"demo": [
|
||||
|
||||
@@ -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<APIKeyData[]>([]);
|
||||
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 (
|
||||
<div className="mt-8">
|
||||
<div className="flex flex-col max-w-[876px]">
|
||||
@@ -100,41 +106,45 @@ export default function APIKeys() {
|
||||
)}
|
||||
<div className="mt-[27px] w-full">
|
||||
<div className="w-full overflow-x-auto">
|
||||
<table className="table-default">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{t('settings.apiKeys.name')}</th>
|
||||
<th>{t('settings.apiKeys.sourceDoc')}</th>
|
||||
<th>{t('settings.apiKeys.key')}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{!apiKeys?.length && (
|
||||
{loading ? (
|
||||
<SkeletonLoader count={1} component={'chatbot'} />
|
||||
) : (
|
||||
<table className="table-default">
|
||||
<thead>
|
||||
<tr>
|
||||
<td colSpan={4} className="!p-4">
|
||||
{t('settings.apiKeys.noData')}
|
||||
</td>
|
||||
<th>{t('settings.apiKeys.name')}</th>
|
||||
<th>{t('settings.apiKeys.sourceDoc')}</th>
|
||||
<th>{t('settings.apiKeys.key')}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
)}
|
||||
{apiKeys?.map((element, index) => (
|
||||
<tr key={index}>
|
||||
<td>{element.name}</td>
|
||||
<td>{element.source}</td>
|
||||
<td>{element.key}</td>
|
||||
<td>
|
||||
<img
|
||||
src={Trash}
|
||||
alt="Delete"
|
||||
className="h-4 w-4 cursor-pointer hover:opacity-50"
|
||||
id={`img-${index}`}
|
||||
onClick={() => handleDeleteKey(element.id)}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</thead>
|
||||
<tbody>
|
||||
{!apiKeys?.length && (
|
||||
<tr>
|
||||
<td colSpan={4} className="!p-4">
|
||||
{t('settings.apiKeys.noData')}
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
{apiKeys?.map((element, index) => (
|
||||
<tr key={index}>
|
||||
<td>{element.name}</td>
|
||||
<td>{element.source}</td>
|
||||
<td>{element.key}</td>
|
||||
<td>
|
||||
<img
|
||||
src={Trash}
|
||||
alt="Delete"
|
||||
className="h-4 w-4 cursor-pointer hover:opacity-50"
|
||||
id={`img-${index}`}
|
||||
onClick={() => handleDeleteKey(element.id)}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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<Record<
|
||||
const [messagesData, setMessagesData] = useState<Record<
|
||||
string,
|
||||
number
|
||||
> | null>(null);
|
||||
const [tokenUsageData, setTokenUsageData] = React.useState<Record<
|
||||
const [tokenUsageData, setTokenUsageData] = useState<Record<
|
||||
string,
|
||||
number
|
||||
> | null>(null);
|
||||
const [feedbackData, setFeedbackData] = React.useState<Record<
|
||||
const [feedbackData, setFeedbackData] = useState<Record<
|
||||
string,
|
||||
{
|
||||
positive: number;
|
||||
negative: number;
|
||||
}
|
||||
{ positive: number; negative: number }
|
||||
> | null>(null);
|
||||
const [chatbots, setChatbots] = React.useState<APIKeyData[]>([]);
|
||||
const [selectedChatbot, setSelectedChatbot] =
|
||||
React.useState<APIKeyData | null>();
|
||||
const [messagesFilter, setMessagesFilter] = React.useState<{
|
||||
const [chatbots, setChatbots] = useState<APIKeyData[]>([]);
|
||||
const [selectedChatbot, setSelectedChatbot] = useState<APIKeyData | null>();
|
||||
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 (
|
||||
<div className="mt-12">
|
||||
<div className="flex flex-col items-start">
|
||||
@@ -181,8 +193,10 @@ export default function Analytics() {
|
||||
border="border"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Messages Analytics */}
|
||||
<div className="mt-8 w-full flex flex-col [@media(min-width:1080px)]:flex-row gap-3">
|
||||
<div className="h-[345px] [@media(min-width:1080px)]:w-1/2 w-full px-6 py-5 border rounded-2xl border-silver dark:border-silver/40">
|
||||
<div className="h-[345px] [@media(min-width:1080px)]:w-1/2 w-full px-6 py-5 border rounded-2xl border-silver dark:border-silver/40 overflow-hidden">
|
||||
<div className="flex flex-row items-center justify-start gap-3">
|
||||
<p className="font-bold text-jet dark:text-bright-gray">
|
||||
Messages
|
||||
@@ -208,26 +222,32 @@ export default function Analytics() {
|
||||
id="legend-container-1"
|
||||
className="flex flex-row items-center justify-end"
|
||||
></div>
|
||||
<AnalyticsChart
|
||||
data={{
|
||||
labels: Object.keys(messagesData || {}).map((item) =>
|
||||
formatDate(item),
|
||||
),
|
||||
datasets: [
|
||||
{
|
||||
label: 'Messages',
|
||||
data: Object.values(messagesData || {}),
|
||||
backgroundColor: '#7D54D1',
|
||||
},
|
||||
],
|
||||
}}
|
||||
legendID="legend-container-1"
|
||||
maxTicksLimitInX={8}
|
||||
isStacked={false}
|
||||
/>
|
||||
{loadingMessages ? (
|
||||
<SkeletonLoader count={1} component={'analysis'} />
|
||||
) : (
|
||||
<AnalyticsChart
|
||||
data={{
|
||||
labels: Object.keys(messagesData || {}).map((item) =>
|
||||
formatDate(item),
|
||||
),
|
||||
datasets: [
|
||||
{
|
||||
label: 'Messages',
|
||||
data: Object.values(messagesData || {}),
|
||||
backgroundColor: '#7D54D1',
|
||||
},
|
||||
],
|
||||
}}
|
||||
legendID="legend-container-1"
|
||||
maxTicksLimitInX={8}
|
||||
isStacked={false}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="h-[345px] [@media(min-width:1080px)]:w-1/2 w-full px-6 py-5 border rounded-2xl border-silver dark:border-silver/40">
|
||||
|
||||
{/* Token Usage Analytics */}
|
||||
<div className="h-[345px] [@media(min-width:1080px)]:w-1/2 w-full px-6 py-5 border rounded-2xl border-silver dark:border-silver/40 overflow-hidden">
|
||||
<div className="flex flex-row items-center justify-start gap-3">
|
||||
<p className="font-bold text-jet dark:text-bright-gray">
|
||||
Token Usage
|
||||
@@ -253,31 +273,37 @@ export default function Analytics() {
|
||||
id="legend-container-2"
|
||||
className="flex flex-row items-center justify-end"
|
||||
></div>
|
||||
<AnalyticsChart
|
||||
data={{
|
||||
labels: Object.keys(tokenUsageData || {}).map((item) =>
|
||||
formatDate(item),
|
||||
),
|
||||
datasets: [
|
||||
{
|
||||
label: 'Tokens',
|
||||
data: Object.values(tokenUsageData || {}),
|
||||
backgroundColor: '#7D54D1',
|
||||
},
|
||||
],
|
||||
}}
|
||||
legendID="legend-container-2"
|
||||
maxTicksLimitInX={8}
|
||||
isStacked={false}
|
||||
/>
|
||||
{loadingTokens ? (
|
||||
<SkeletonLoader count={1} component={'analysis'} />
|
||||
) : (
|
||||
<AnalyticsChart
|
||||
data={{
|
||||
labels: Object.keys(tokenUsageData || {}).map((item) =>
|
||||
formatDate(item),
|
||||
),
|
||||
datasets: [
|
||||
{
|
||||
label: 'Tokens',
|
||||
data: Object.values(tokenUsageData || {}),
|
||||
backgroundColor: '#7D54D1',
|
||||
},
|
||||
],
|
||||
}}
|
||||
legendID="legend-container-2"
|
||||
maxTicksLimitInX={8}
|
||||
isStacked={false}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-8 w-full">
|
||||
<div className="h-[345px] w-full px-6 py-5 border rounded-2xl border-silver dark:border-silver/40">
|
||||
|
||||
{/* Feedback Analytics */}
|
||||
<div className="mt-8 w-full flex flex-col gap-3">
|
||||
<div className="h-[345px] w-full px-6 py-5 border rounded-2xl border-silver dark:border-silver/40 overflow-hidden">
|
||||
<div className="flex flex-row items-center justify-start gap-3">
|
||||
<p className="font-bold text-jet dark:text-bright-gray">
|
||||
User Feedback
|
||||
Feedback
|
||||
</p>
|
||||
<Dropdown
|
||||
size="w-[125px]"
|
||||
@@ -300,32 +326,36 @@ export default function Analytics() {
|
||||
id="legend-container-3"
|
||||
className="flex flex-row items-center justify-end"
|
||||
></div>
|
||||
<AnalyticsChart
|
||||
data={{
|
||||
labels: Object.keys(feedbackData || {}).map((item) =>
|
||||
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 ? (
|
||||
<SkeletonLoader count={1} component={'analysis'} />
|
||||
) : (
|
||||
<AnalyticsChart
|
||||
data={{
|
||||
labels: Object.keys(feedbackData || {}).map((item) =>
|
||||
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}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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<DocumentsProps> = ({
|
||||
}) => {
|
||||
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<DocumentsProps> = ({
|
||||
];
|
||||
|
||||
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<DocumentsProps> = ({
|
||||
.then((data) => {
|
||||
dispatch(setSourceDocs(data));
|
||||
})
|
||||
.catch((error) => console.error(error));
|
||||
.catch((error) => console.error(error))
|
||||
.finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mt-8">
|
||||
<div className="flex flex-col relative">
|
||||
<div className="z-10 w-full overflow-x-auto">
|
||||
<table className="table-default">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{t('settings.documents.name')}</th>
|
||||
<th>{t('settings.documents.date')}</th>
|
||||
<th>{t('settings.documents.tokenUsage')}</th>
|
||||
<th>{t('settings.documents.type')}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{!documents?.length && (
|
||||
{loading ? (
|
||||
<SkeletonLoader count={1} />
|
||||
) : (
|
||||
<table className="table-default">
|
||||
<thead>
|
||||
<tr>
|
||||
<td colSpan={5} className="!p-4">
|
||||
{t('settings.documents.noData')}
|
||||
</td>
|
||||
<th>{t('settings.documents.name')}</th>
|
||||
<th>{t('settings.documents.date')}</th>
|
||||
<th>{t('settings.documents.tokenUsage')}</th>
|
||||
<th>{t('settings.documents.type')}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
)}
|
||||
{documents &&
|
||||
documents.map((document, index) => (
|
||||
<tr key={index}>
|
||||
<td>{document.name}</td>
|
||||
<td>{document.date}</td>
|
||||
<td>
|
||||
{document.tokens ? formatTokens(+document.tokens) : ''}
|
||||
</td>
|
||||
<td>
|
||||
{document.type === 'remote' ? 'Pre-loaded' : 'Private'}
|
||||
</td>
|
||||
<td>
|
||||
<div className="flex flex-row items-center">
|
||||
{document.type !== 'remote' && (
|
||||
<img
|
||||
src={Trash}
|
||||
alt="Delete"
|
||||
className="h-4 w-4 cursor-pointer hover:opacity-50"
|
||||
id={`img-${index}`}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
handleDeleteDocument(index, document);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{document.syncFrequency && (
|
||||
<div className="ml-2">
|
||||
<DropdownMenu
|
||||
name="Sync"
|
||||
options={syncOptions}
|
||||
onSelect={(value: string) => {
|
||||
handleManageSync(document, value);
|
||||
}}
|
||||
defaultValue={document.syncFrequency}
|
||||
icon={SyncIcon}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</thead>
|
||||
<tbody>
|
||||
{!documents?.length && (
|
||||
<tr>
|
||||
<td colSpan={5} className="!p-4">
|
||||
{t('settings.documents.noData')}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
{documents &&
|
||||
documents.map((document, index) => (
|
||||
<tr key={index}>
|
||||
<td>{document.name}</td>
|
||||
<td>{document.date}</td>
|
||||
<td>
|
||||
{document.tokens ? formatTokens(+document.tokens) : ''}
|
||||
</td>
|
||||
<td>
|
||||
{document.type === 'remote' ? 'Pre-loaded' : 'Private'}
|
||||
</td>
|
||||
<td>
|
||||
<div className="flex flex-row items-center">
|
||||
{document.type !== 'remote' && (
|
||||
<img
|
||||
src={Trash}
|
||||
alt="Delete"
|
||||
className="h-4 w-4 cursor-pointer hover:opacity-50"
|
||||
id={`img-${index}`}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
handleDeleteDocument(index, document);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{document.syncFrequency && (
|
||||
<div className="ml-2">
|
||||
<DropdownMenu
|
||||
name="Sync"
|
||||
options={syncOptions}
|
||||
onSelect={(value: string) => {
|
||||
handleManageSync(document, value);
|
||||
}}
|
||||
defaultValue={document.syncFrequency}
|
||||
icon={SyncIcon}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Documents.propTypes = {
|
||||
documents: PropTypes.array.isRequired,
|
||||
handleDeleteDocument: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default Documents;
|
||||
|
||||
@@ -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<APIKeyData[]>([]);
|
||||
const [selectedChatbot, setSelectedChatbot] =
|
||||
React.useState<APIKeyData | null>();
|
||||
const [logs, setLogs] = React.useState<LogData[]>([]);
|
||||
const [page, setPage] = React.useState(1);
|
||||
const [hasMore, setHasMore] = React.useState(true);
|
||||
const [chatbots, setChatbots] = useState<APIKeyData[]>([]);
|
||||
const [selectedChatbot, setSelectedChatbot] = useState<APIKeyData | null>();
|
||||
const [logs, setLogs] = useState<LogData[]>([]);
|
||||
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 (
|
||||
<div className="mt-12">
|
||||
<div className="flex flex-col items-start">
|
||||
@@ -59,38 +68,47 @@ export default function Logs() {
|
||||
<p className="font-bold text-jet dark:text-bright-gray">
|
||||
Filter by chatbot
|
||||
</p>
|
||||
<Dropdown
|
||||
size="w-[55vw] sm:w-[360px]"
|
||||
options={[
|
||||
...chatbots.map((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 ? (
|
||||
<SkeletonLoader />
|
||||
) : (
|
||||
<Dropdown
|
||||
size="w-[55vw] sm:w-[360px]"
|
||||
options={[
|
||||
...chatbots.map((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"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-8">
|
||||
<LogsTable logs={logs} setPage={setPage} />
|
||||
{loadingLogs ? (
|
||||
<SkeletonLoader component={'logs'} />
|
||||
) : (
|
||||
<LogsTable logs={logs} setPage={setPage} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -102,15 +120,16 @@ type LogsTableProps = {
|
||||
};
|
||||
|
||||
function LogsTable({ logs, setPage }: LogsTableProps) {
|
||||
const observerRef = React.useRef<any>();
|
||||
const firstObserver = React.useCallback((node: HTMLDivElement) => {
|
||||
const observerRef = useRef<any>();
|
||||
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 (
|
||||
<div className="logs-table border rounded-2xl h-[55vh] w-full overflow-hidden border-silver dark:border-silver/40">
|
||||
<div className="h-8 bg-black/10 dark:bg-chinese-black flex flex-col items-start justify-center">
|
||||
|
||||
Reference in New Issue
Block a user