/
kreyndelHam
/
Konstructorium
Обзор
Документация
Войти
/
kreyndelHam
/
Konstructorium
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
1
CI/CD
Аналитика
Безопасность
dev
frontend/src/components/admin/AdminGigaChatLogsPanel.jsx
246 строк
10 KB
MihahamYT
New models added
25 май 2026, 17:43
25 май 2026, 17:43
1d72a4d
Код
Авторство
О чём код?
import { Fragment, useEffect, useState } from 'react' import { useQuery } from 'react-query' import { Link } from 'react-router-dom' import { adminService } from '../../services/api' import { useTranslation } from '../../hooks/useTranslation' /** * Extract list payload from paginated or plain API responses. * * @param {unknown} payload * @returns {Array} */ function extractList(payload) { if (!payload) { return [] } if (Array.isArray(payload)) { return payload } if (Array.isArray(payload.results)) { return payload.results } if (Array.isArray(payload.data)) { return payload.data } return [] } /** * Admin panel with full GigaChat request/response audit logs. * * @param {object} props * @param {boolean} [props.embedded=false] Hide page title when embedded in another admin page. * @param {string} [props.initialConversationFilter=''] * @returns {JSX.Element} */ function AdminGigaChatLogsPanel({ embedded = false, initialConversationFilter = '' }) { const { t } = useTranslation() const [purposeFilter, setPurposeFilter] = useState('') const [successFilter, setSuccessFilter] = useState('') const [conversationFilter, setConversationFilter] = useState(initialConversationFilter) const [expandedLogId, setExpandedLogId] = useState(null) useEffect(() => { setConversationFilter(initialConversationFilter) }, [initialConversationFilter]) const { data: logsResponse, isLoading, refetch, isFetching } = useQuery( ['admin-gigachat-logs', purposeFilter, successFilter, conversationFilter], async () => { const params = {} if (purposeFilter) { params.purpose = purposeFilter } if (successFilter !== '') { params.success = successFilter } if (conversationFilter.trim()) { params.conversation = conversationFilter.trim() } const response = await adminService.getGigaChatLogs(params) return response.data }, { keepPreviousData: true } ) const logs = extractList(logsResponse) const toggleExpanded = (logId) => { setExpandedLogId((current) => (current === logId ? null : logId)) } return ( <div className={embedded ? '' : 'max-w-7xl mx-auto'}> {embedded && ( <div className="flex flex-col gap-4 mb-4 sm:flex-row sm:items-start sm:justify-between"> <div> <h2 className="text-xl font-semibold text-text-heading">{t('adminAiLogs.title')}</h2> <p className="text-sm text-text-body mt-1">{t('adminAiLogs.subtitle')}</p> </div> <button type="button" onClick={() => refetch()} disabled={isFetching} className="px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 disabled:opacity-50 text-sm shrink-0" > {t('common.refresh')} </button> </div> )} {!embedded && ( <div className="flex flex-col gap-4 mb-6 sm:flex-row sm:items-center sm:justify-between"> <div> <h1 className="text-3xl font-bold text-text-heading">{t('adminAiLogs.title')}</h1> <p className="text-text-body mt-1">{t('adminAiLogs.subtitle')}</p> </div> <button type="button" onClick={() => refetch()} disabled={isFetching} className="px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 disabled:opacity-50" > {t('common.refresh')} </button> </div> )} <div className="bg-surface rounded-lg shadow-md p-4 mb-6 grid gap-4 md:grid-cols-4"> <div> <label className="block text-sm font-medium text-text-body mb-1">{t('adminAiLogs.filterPurpose')}</label> <select value={purposeFilter} onChange={(event) => setPurposeFilter(event.target.value)} className="w-full px-3 py-2 border border-border rounded-lg bg-page text-text-body" > <option value="">{t('adminAiLogs.allPurposes')}</option> <option value="chat_reply">{t('adminAiLogs.purposeChatReply')}</option> <option value="summary">{t('adminAiLogs.purposeSummary')}</option> </select> </div> <div> <label className="block text-sm font-medium text-text-body mb-1">{t('adminAiLogs.filterSuccess')}</label> <select value={successFilter} onChange={(event) => setSuccessFilter(event.target.value)} className="w-full px-3 py-2 border border-border rounded-lg bg-page text-text-body" > <option value="">{t('adminAiLogs.allStatuses')}</option> <option value="true">{t('adminAiLogs.successOnly')}</option> <option value="false">{t('adminAiLogs.failedOnly')}</option> </select> </div> <div className="md:col-span-2"> <label className="block text-sm font-medium text-text-body mb-1">{t('adminAiLogs.filterConversation')}</label> <input type="text" value={conversationFilter} onChange={(event) => setConversationFilter(event.target.value)} placeholder={t('adminAiLogs.conversationIdPlaceholder')} className="w-full px-3 py-2 border border-border rounded-lg bg-page text-text-body placeholder:text-text-muted" /> </div> </div> {isLoading ? ( <div className="text-center py-12 text-text-muted">{t('adminAiLogs.loading')}</div> ) : logs.length === 0 ? ( <div className="text-center py-12 text-text-muted bg-surface rounded-lg shadow-md"> {t('adminAiLogs.empty')} </div> ) : ( <div className="bg-surface rounded-lg shadow-md overflow-hidden"> <div className="overflow-x-auto"> <table className="w-full text-sm"> <thead className="bg-page"> <tr> <th className="px-4 py-3 text-left">{t('adminAiLogs.colTime')}</th> <th className="px-4 py-3 text-left">{t('adminAiLogs.colUser')}</th> <th className="px-4 py-3 text-left">{t('adminAiLogs.colPurpose')}</th> <th className="px-4 py-3 text-left">{t('adminAiLogs.colStatus')}</th> <th className="px-4 py-3 text-left">{t('adminAiLogs.colLatency')}</th> <th className="px-4 py-3 text-left">{t('adminAiLogs.colConversation')}</th> <th className="px-4 py-3 text-left">{t('adminAiLogs.colDetails')}</th> </tr> </thead> <tbody className="divide-y divide-border"> {logs.map((log) => ( <Fragment key={log.id}> <tr className="hover:bg-page"> <td className="px-4 py-3 whitespace-nowrap"> {new Date(log.created_at).toLocaleString()} </td> <td className="px-4 py-3">{log.user_username || '—'}</td> <td className="px-4 py-3">{log.purpose_display || log.purpose}</td> <td className="px-4 py-3"> <span className={`px-2 py-1 rounded-full text-xs font-medium ${ log.success ? 'bg-green-100 text-green-800 dark:bg-green-950/50 dark:text-green-200' : 'bg-red-100 text-red-800 dark:bg-red-950/50 dark:text-red-200' }`} > {log.success ? t('adminAiLogs.statusOk') : t('adminAiLogs.statusFailed')} </span> </td> <td className="px-4 py-3">{log.latency_ms != null ? `${log.latency_ms} ms` : '—'}</td> <td className="px-4 py-3"> {log.conversation ? ( <Link to={`/admin/chat?conversation=${log.conversation}`} className="text-primary-600 dark:text-primary-400 hover:underline" > #{log.conversation} </Link> ) : ( '—' )} </td> <td className="px-4 py-3"> <button type="button" onClick={() => toggleExpanded(log.id)} className="text-primary-600 dark:text-primary-400 hover:underline" > {expandedLogId === log.id ? t('adminAiLogs.hideDetails') : t('adminAiLogs.showDetails')} </button> </td> </tr> {expandedLogId === log.id && ( <tr> <td colSpan={7} className="px-4 py-4 bg-page"> <div className="grid gap-4 lg:grid-cols-2"> <div> <p className="font-semibold text-text-heading mb-2">{t('adminAiLogs.requestPayload')}</p> <pre className="text-xs bg-page text-text-body border border-border rounded-lg p-3 overflow-x-auto whitespace-pre-wrap max-h-96"> {JSON.stringify(log.request_messages, null, 2)} </pre> </div> <div> <p className="font-semibold text-text-heading mb-2"> {log.success ? t('adminAiLogs.responsePayload') : t('adminAiLogs.errorPayload')} </p> <pre className="text-xs bg-page text-text-body border border-border rounded-lg p-3 overflow-x-auto whitespace-pre-wrap max-h-96"> {log.success ? (log.response_content || '—') : (log.error_message || '—')} </pre> </div> </div> <p className="text-xs text-text-muted mt-3"> {t('adminAiLogs.modelLabel')}: {log.model_name} </p> </td> </tr> )} </Fragment> ))} </tbody> </table> </div> </div> )} </div> ) } export default AdminGigaChatLogsPanel