feat: user logs section in settings

This commit is contained in:
Siddhant Rai
2024-09-11 17:45:54 +05:30
parent 90309d5552
commit bea0bbfcdb
15 changed files with 349 additions and 51 deletions

View File

@@ -15,6 +15,7 @@ const endpoints = {
MESSAGE_ANALYTICS: '/api/get_message_analytics',
TOKEN_ANALYTICS: '/api/get_token_analytics',
FEEDBACK_ANALYTICS: '/api/get_feedback_analytics',
LOGS: `/api/get_user_logs`,
},
CONVERSATION: {
ANSWER: '/api/answer',

View File

@@ -29,6 +29,8 @@ const userService = {
apiClient.post(endpoints.USER.TOKEN_ANALYTICS, data),
getFeedbackAnalytics: (data: any): Promise<any> =>
apiClient.post(endpoints.USER.FEEDBACK_ANALYTICS, data),
getLogs: (data: any): Promise<any> =>
apiClient.post(endpoints.USER.LOGS, data),
};
export default userService;

View File

@@ -0,0 +1,3 @@
<svg width="7" height="12" viewBox="0 0 7 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.29154 4.88202L1.70154 0.29202C1.60896 0.199438 1.49905 0.125998 1.37808 0.0758932C1.25712 0.0257882 1.12747 -2.37536e-07 0.99654 -2.44235e-07C0.86561 -2.50934e-07 0.735961 0.0257882 0.614997 0.0758931C0.494033 0.125998 0.384122 0.199438 0.29154 0.29202C0.198958 0.384602 0.125519 0.494513 0.0754137 0.615477C0.0253086 0.736441 -0.00048069 0.86609 -0.000480695 0.99702C-0.000480701 1.12795 0.0253086 1.2576 0.0754136 1.37856C0.125519 1.49953 0.198958 1.60944 0.29154 1.70202L4.17154 5.59202L0.29154 9.47202C0.198958 9.5646 0.125518 9.67451 0.0754133 9.79548C0.0253082 9.91644 -0.000481091 10.0461 -0.000481097 10.177C-0.000481102 10.3079 0.0253082 10.4376 0.0754132 10.5586C0.125518 10.6795 0.198958 10.7894 0.29154 10.882C0.384121 10.9746 0.494032 11.048 0.614996 11.0981C0.73596 11.1483 0.865609 11.174 0.99654 11.174C1.12747 11.174 1.25712 11.1483 1.37808 11.0981C1.49905 11.048 1.60896 10.9746 1.70154 10.882L6.29154 6.29202C6.38424 6.19951 6.45779 6.08962 6.50797 5.96864C6.55815 5.84767 6.58398 5.71799 6.58398 5.58702C6.58398 5.45605 6.55815 5.32637 6.50797 5.2054C6.45779 5.08442 6.38424 4.97453 6.29154 4.88202Z" fill="#666666"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -1,16 +1,24 @@
import { useState } from 'react';
import Copy from './../assets/copy.svg?react';
import CheckMark from './../assets/checkmark.svg?react';
import copy from 'copy-to-clipboard';
import { useState } from 'react';
export default function CoppyButton({ text }: { text: string }) {
import CheckMark from '../assets/checkmark.svg?react';
import Copy from '../assets/copy.svg?react';
export default function CoppyButton({
text,
colorLight,
colorDark,
}: {
text: string;
colorLight?: string;
colorDark?: string;
}) {
const [copied, setCopied] = useState(false);
const [isCopyHovered, setIsCopyHovered] = useState(false);
const handleCopyClick = (text: string) => {
copy(text);
setCopied(true);
// Reset copied to false after a few seconds
setTimeout(() => {
setCopied(false);
}, 3000);
@@ -20,8 +28,8 @@ export default function CoppyButton({ text }: { text: string }) {
<div
className={`flex items-center justify-center rounded-full p-2 ${
isCopyHovered
? 'bg-[#EEEEEE] dark:bg-purple-taupe'
: 'bg-[#ffffff] dark:bg-transparent'
? `bg-[#EEEEEE] dark:bg-purple-taupe`
: `bg-[${colorLight ? colorLight : '#FFFFFF'}] dark:bg-[${colorDark ? colorDark : 'transparent'}]`
}`}
>
{copied ? (

View File

@@ -420,6 +420,12 @@ template {
src: url('/fonts/Inter-Variable.ttf');
}
@font-face {
font-family: 'IBMPlexMono-Medium';
font-weight: 500;
src: url('/fonts/IBMPlexMono-Medium.ttf');
}
::-webkit-scrollbar {
width: 0;
}
@@ -461,3 +467,7 @@ input:-webkit-autofill:focus {
-webkit-box-orient: vertical;
text-overflow: ellipsis;
}
.logs-table {
font-family: 'IBMPlexMono-Medium', system-ui;
}

View File

@@ -65,6 +65,9 @@
},
"analytics": {
"label": "Analytics"
},
"logs": {
"label": "Logs"
}
},
"modals": {

View File

@@ -65,6 +65,9 @@
},
"analytics": {
"label": "Analítica"
},
"logs": {
"label": "Registros"
}
},
"modals": {

View File

@@ -65,6 +65,9 @@
},
"analytics": {
"label": "分析"
},
"logs": {
"label": "ログ"
}
},
"modals": {

View File

@@ -65,6 +65,9 @@
},
"analytics": {
"label": "分析"
},
"logs": {
"label": "日志"
}
},
"modals": {

View File

@@ -0,0 +1,175 @@
import React from 'react';
import userService from '../api/services/userService';
import ChevronRight from '../assets/chevron-right.svg';
import Dropdown from '../components/Dropdown';
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 fetchChatbots = async () => {
try {
const response = await userService.getAPIKeys();
if (!response.ok) {
throw new Error('Failed to fetch Chatbots');
}
const chatbots = await response.json();
setChatbots(chatbots);
} catch (error) {
console.error(error);
}
};
const fetchLogs = async () => {
try {
const response = await userService.getLogs({
page: page,
api_key_id: selectedChatbot?.id,
page_size: 10,
});
if (!response.ok) {
throw new Error('Failed to fetch logs');
}
const olderLogs = await response.json();
setLogs([...logs, ...olderLogs.logs]);
setHasMore(olderLogs.has_more);
} catch (error) {
console.error(error);
}
};
React.useEffect(() => {
fetchChatbots();
}, []);
React.useEffect(() => {
if (hasMore) fetchLogs();
}, [page, selectedChatbot]);
return (
<div className="mt-12">
<div className="flex flex-col items-start">
<div className="flex flex-col gap-3">
<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"
/>
</div>
</div>
<div className="mt-8">
<LogsTable logs={logs} setPage={setPage} />
</div>
</div>
);
}
type LogsTableProps = {
logs: LogData[];
setPage: React.Dispatch<React.SetStateAction<number>>;
};
function LogsTable({ logs, setPage }: LogsTableProps) {
const observerRef = React.useRef<any>();
const firstObserver = React.useCallback((node: HTMLDivElement) => {
if (observerRef.current) {
observerRef.current = new IntersectionObserver((enteries) => {
if (enteries[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">
<p className="px-3 text-xs dark:text-gray-6000">
API generated / chatbot conversations
</p>
</div>
<div
ref={observerRef}
className="flex flex-col items-start h-[51vh] overflow-y-auto bg-transparent flex-grow gap-px"
>
{logs.map((log, index) => {
if (index === logs.length - 1) {
return (
<div ref={firstObserver} key={index}>
<Log log={log} />
</div>
);
} else return <Log key={index} log={log} />;
})}
</div>
</div>
);
}
function Log({ log }: { log: LogData }) {
const logLevelColor = {
info: 'text-green-500',
error: 'text-red-500',
warning: 'text-yellow-500',
};
const { id, action, timestamp, ...filteredLog } = log;
return (
<details className="group bg-transparent [&_summary::-webkit-details-marker]:hidden w-full hover:bg-[#F9F9F9] hover:dark:bg-dark-charcoal">
<summary className="flex flex-row items-center gap-2 text-gray-900 cursor-pointer p-2 group-open:bg-[#F9F9F9] dark:group-open:bg-dark-charcoal">
<img
src={ChevronRight}
alt="chevron-right"
className="w-3 h-3 transition duration-300 group-open:rotate-90"
/>
<span className="flex flex-row gap-2">
<h2 className="text-xs text-black/60 dark:text-bright-gray">{`${log.timestamp}`}</h2>
<h2 className="text-xs text-[#913400] dark:text-[#DF5200]">{`[${log.action}]`}</h2>
<h2
className={`text-xs ${logLevelColor[log.level]}`}
>{`${log.question}`}</h2>
</span>
</summary>
<div className="px-4 group-open:bg-[#F9F9F9] dark:group-open:bg-dark-charcoal">
<p className="px-2 leading-relaxed text-gray-700 dark:text-gray-400 text-xs">
{JSON.stringify(filteredLog, null, 2)}
</p>
<div className="my-px w-8">
<CoppyButton
text={JSON.stringify(filteredLog)}
colorLight="transparent"
/>
</div>
</div>
</details>
);
}

View File

@@ -15,6 +15,7 @@ import Analytics from './Analytics';
import APIKeys from './APIKeys';
import Documents from './Documents';
import General from './General';
import Logs from './Logs';
import Widgets from './Widgets';
export default function Settings() {
@@ -25,6 +26,7 @@ export default function Settings() {
t('settings.documents.label'),
t('settings.apiKeys.label'),
t('settings.analytics.label'),
t('settings.logs.label'),
];
const [activeTab, setActiveTab] = React.useState(t('settings.general.label'));
const [widgetScreenshot, setWidgetScreenshot] = React.useState<File | null>(
@@ -133,6 +135,8 @@ export default function Settings() {
return <APIKeys />;
case t('settings.analytics.label'):
return <Analytics />;
case t('settings.logs.label'):
return <Logs />;
default:
return null;
}

View File

@@ -6,3 +6,15 @@ export type APIKeyData = {
prompt_id: string;
chunks: string;
};
export type LogData = {
id: string;
action: string;
level: 'info' | 'error' | 'warning';
user: string;
question: string;
response: string;
sources: Record<string, any>[];
retriever_params: Record<string, any>;
timestamp: string;
};