Spaces:
Running
Running
update
Browse files- .gitignore +2 -1
- src/api/logs/hooks.ts +16 -0
- src/api/logs/logsApi.ts +15 -0
- src/api/logs/types.ts +36 -0
- src/components/pages/logs/LogDetailsModal.scss +152 -0
- src/components/pages/logs/LogDetailsModal.tsx +116 -0
- src/components/pages/logs/Logs.scss +117 -0
- src/components/pages/logs/Logs.tsx +223 -0
.gitignore
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
# Logs
|
| 2 |
-
logs
|
| 3 |
*.log
|
| 4 |
npm-debug.log*
|
| 5 |
yarn-debug.log*
|
|
@@ -22,3 +22,4 @@ dist-ssr
|
|
| 22 |
*.njsproj
|
| 23 |
*.sln
|
| 24 |
*.sw?
|
|
|
|
|
|
| 1 |
# Logs
|
| 2 |
+
/logs
|
| 3 |
*.log
|
| 4 |
npm-debug.log*
|
| 5 |
yarn-debug.log*
|
|
|
|
| 22 |
*.njsproj
|
| 23 |
*.sln
|
| 24 |
*.sw?
|
| 25 |
+
/tsconfig.tsbuildinfo
|
src/api/logs/hooks.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useQuery } from "@tanstack/react-query";
|
| 2 |
+
import {
|
| 3 |
+
getLogs,
|
| 4 |
+
} from "./logsApi";
|
| 5 |
+
import { GetLogsRequestParams } from "./types";
|
| 6 |
+
|
| 7 |
+
export const useGetLogs = (params: GetLogsRequestParams) => {
|
| 8 |
+
return useQuery({
|
| 9 |
+
queryKey: ["logs", params],
|
| 10 |
+
queryFn: async () => {
|
| 11 |
+
const response = await getLogs(params);
|
| 12 |
+
return response;
|
| 13 |
+
},
|
| 14 |
+
enabled: !!params
|
| 15 |
+
});
|
| 16 |
+
};
|
src/api/logs/logsApi.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { query } from "@/shared/api/query";
|
| 2 |
+
import { GetLogsRequestParams, GetLogsResponseType } from "./types";
|
| 3 |
+
|
| 4 |
+
export const getLogs = async (params: GetLogsRequestParams): Promise<GetLogsResponseType> => {
|
| 5 |
+
const response = await query<GetLogsResponseType>({
|
| 6 |
+
url: "/logs",
|
| 7 |
+
method: "get",
|
| 8 |
+
params: params,
|
| 9 |
+
});
|
| 10 |
+
if ("error" in response) {
|
| 11 |
+
throw new Error(`Ошибка: ${response.error.status}`);
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
return response.data;
|
| 15 |
+
};
|
src/api/logs/types.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export type LogItemType = {
|
| 2 |
+
id: number
|
| 3 |
+
date_created: Date
|
| 4 |
+
user_request: string;
|
| 5 |
+
qe_result: string;
|
| 6 |
+
search_result: string;
|
| 7 |
+
llm_result: string;
|
| 8 |
+
llm_settings: string;
|
| 9 |
+
user_name: string;
|
| 10 |
+
error: string;
|
| 11 |
+
};
|
| 12 |
+
|
| 13 |
+
export type GetLogsRequestParams = {
|
| 14 |
+
page?: number;
|
| 15 |
+
user_name?: string;
|
| 16 |
+
date_to?: Date;
|
| 17 |
+
date_from?: Date;
|
| 18 |
+
page_size?: number;
|
| 19 |
+
sort?: { field: string; direction: SortDirections }[];
|
| 20 |
+
};
|
| 21 |
+
|
| 22 |
+
export type GetLogsResponseType = {
|
| 23 |
+
data: PaginationType & LogItemType[],
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
export type PaginationType = {
|
| 27 |
+
total: number;
|
| 28 |
+
page: number;
|
| 29 |
+
page_size: number;
|
| 30 |
+
total_pages: number;
|
| 31 |
+
};
|
| 32 |
+
|
| 33 |
+
export enum SortDirections {
|
| 34 |
+
asc = "asc",
|
| 35 |
+
desc = "desc",
|
| 36 |
+
}
|
src/components/pages/logs/LogDetailsModal.scss
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.modal-overlay {
|
| 2 |
+
position: fixed;
|
| 3 |
+
top: 0;
|
| 4 |
+
left: 0;
|
| 5 |
+
right: 0;
|
| 6 |
+
bottom: 0;
|
| 7 |
+
background-color: rgba(0, 0, 0, 0.5);
|
| 8 |
+
z-index: 1000;
|
| 9 |
+
}
|
| 10 |
+
|
| 11 |
+
.fullscreen-modal {
|
| 12 |
+
top: 0;
|
| 13 |
+
left: 0;
|
| 14 |
+
overflow: auto;
|
| 15 |
+
width: 100vw;
|
| 16 |
+
height: 100vh;
|
| 17 |
+
|
| 18 |
+
.modal-content {
|
| 19 |
+
max-height: 100vh;
|
| 20 |
+
overflow-y: auto;
|
| 21 |
+
overflow-x: auto;
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
.modal-content {
|
| 27 |
+
max-height: 80vh;
|
| 28 |
+
overflow-y: auto;
|
| 29 |
+
overflow-x: hidden;
|
| 30 |
+
|
| 31 |
+
.label {
|
| 32 |
+
display: flex;
|
| 33 |
+
justify-content: space-between;
|
| 34 |
+
align-items: center;
|
| 35 |
+
margin-bottom: 20px;
|
| 36 |
+
|
| 37 |
+
.name {
|
| 38 |
+
margin: 0;
|
| 39 |
+
font-size: 1.5rem;
|
| 40 |
+
color: #333;
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
.close-button {
|
| 44 |
+
background: none;
|
| 45 |
+
border: none;
|
| 46 |
+
cursor: pointer;
|
| 47 |
+
padding: 0;
|
| 48 |
+
color: #666;
|
| 49 |
+
|
| 50 |
+
&:hover {
|
| 51 |
+
color: #333;
|
| 52 |
+
}
|
| 53 |
+
}
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
form {
|
| 57 |
+
display: grid;
|
| 58 |
+
grid-template-columns: 1fr 2fr;
|
| 59 |
+
gap: 15px;
|
| 60 |
+
align-items: center;
|
| 61 |
+
width: 100%;
|
| 62 |
+
box-sizing: border-box;
|
| 63 |
+
|
| 64 |
+
label {
|
| 65 |
+
display: contents;
|
| 66 |
+
|
| 67 |
+
span.title {
|
| 68 |
+
font-weight: 500;
|
| 69 |
+
color: #444;
|
| 70 |
+
text-align: right;
|
| 71 |
+
padding-right: 10px;
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
input {
|
| 75 |
+
max-width: 100%;
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
input[type="text"],
|
| 79 |
+
input[type="number"] {
|
| 80 |
+
padding: 8px 12px;
|
| 81 |
+
border: 1px solid #ccc;
|
| 82 |
+
border-radius: 5px;
|
| 83 |
+
font-size: 1rem;
|
| 84 |
+
color: #333;
|
| 85 |
+
background-color: #f9f9f9;
|
| 86 |
+
transition: border-color 0.2s;
|
| 87 |
+
|
| 88 |
+
&:focus {
|
| 89 |
+
border-color: #007bff;
|
| 90 |
+
outline: none;
|
| 91 |
+
background-color: #fff;
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
&:disabled {
|
| 95 |
+
background-color: #e9ecef;
|
| 96 |
+
color: #666;
|
| 97 |
+
}
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
input[type="checkbox"] {
|
| 101 |
+
width: 20px;
|
| 102 |
+
height: 20px;
|
| 103 |
+
margin: 0;
|
| 104 |
+
cursor: pointer;
|
| 105 |
+
}
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
.button-group {
|
| 109 |
+
grid-column: span 2;
|
| 110 |
+
display: flex;
|
| 111 |
+
justify-content: flex-end;
|
| 112 |
+
gap: 10px;
|
| 113 |
+
margin-top: 20px;
|
| 114 |
+
|
| 115 |
+
button {
|
| 116 |
+
padding: 10px 20px;
|
| 117 |
+
border: none;
|
| 118 |
+
border-radius: 5px;
|
| 119 |
+
font-size: 1rem;
|
| 120 |
+
cursor: pointer;
|
| 121 |
+
transition: background-color 0.2s;
|
| 122 |
+
|
| 123 |
+
&:first-child {
|
| 124 |
+
background-color: #007bff;
|
| 125 |
+
color: white;
|
| 126 |
+
|
| 127 |
+
&:hover {
|
| 128 |
+
background-color: #0056b3;
|
| 129 |
+
}
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
&:nth-child(2) {
|
| 133 |
+
background-color: #28a745;
|
| 134 |
+
color: white;
|
| 135 |
+
|
| 136 |
+
&:hover {
|
| 137 |
+
background-color: #218838;
|
| 138 |
+
}
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
&:last-child {
|
| 142 |
+
background-color: #6c757d;
|
| 143 |
+
color: white;
|
| 144 |
+
|
| 145 |
+
&:hover {
|
| 146 |
+
background-color: #5a6268;
|
| 147 |
+
}
|
| 148 |
+
}
|
| 149 |
+
}
|
| 150 |
+
}
|
| 151 |
+
}
|
| 152 |
+
}
|
src/components/pages/logs/LogDetailsModal.tsx
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from 'react';
|
| 2 |
+
import ReactJson from '@microlink/react-json-view';
|
| 3 |
+
import Modal from 'react-modal';
|
| 4 |
+
import { GoX } from 'react-icons/go';
|
| 5 |
+
import { useQuery } from '@tanstack/react-query';
|
| 6 |
+
import './LogDetailsModal.scss';
|
| 7 |
+
import { LogItemType } from '@/api/logs/types';
|
| 8 |
+
|
| 9 |
+
interface LogDetailsModalProps {
|
| 10 |
+
isOpen: boolean;
|
| 11 |
+
onRequestClose: () => void;
|
| 12 |
+
log: LogItemType;
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
const customStyles = {
|
| 16 |
+
content: {
|
| 17 |
+
// top: '0',
|
| 18 |
+
// left: '0',
|
| 19 |
+
// overflow: 'auto',
|
| 20 |
+
// width: '100vw',
|
| 21 |
+
// height: '100vh'
|
| 22 |
+
},
|
| 23 |
+
};
|
| 24 |
+
|
| 25 |
+
const LogDetailsModal: React.FC<LogDetailsModalProps> = ({
|
| 26 |
+
isOpen,
|
| 27 |
+
onRequestClose,
|
| 28 |
+
log,
|
| 29 |
+
}) => {
|
| 30 |
+
|
| 31 |
+
let qeResult = null;
|
| 32 |
+
try {
|
| 33 |
+
if (log.qe_result) {
|
| 34 |
+
qeResult = JSON.parse(log.qe_result);
|
| 35 |
+
}
|
| 36 |
+
} catch (error) {
|
| 37 |
+
console.error('Ошибка парсинга JSON:', error);
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
return (
|
| 41 |
+
<Modal
|
| 42 |
+
isOpen={isOpen}
|
| 43 |
+
onRequestClose={onRequestClose}
|
| 44 |
+
style={customStyles}
|
| 45 |
+
overlayClassName="modal-overlay"
|
| 46 |
+
>
|
| 47 |
+
<div className="modal-content">
|
| 48 |
+
<div className="label">
|
| 49 |
+
<h3 className="name">{log.user_name} {log.date_created ? new Date(log.date_created).toLocaleDateString() : ''} {log.date_created ? new Date(log.date_created).toLocaleTimeString() : ''}</h3>
|
| 50 |
+
<button className="close-button" onClick={onRequestClose}>
|
| 51 |
+
<GoX style={{ height: '25px', width: '25px' }} />
|
| 52 |
+
</button>
|
| 53 |
+
</div>
|
| 54 |
+
<form>
|
| 55 |
+
<label>
|
| 56 |
+
<span className='title'>ID:</span>
|
| 57 |
+
<input type="text" value={log.id} disabled />
|
| 58 |
+
</label>
|
| 59 |
+
<label>
|
| 60 |
+
<span className='title'>Пользователь:</span>
|
| 61 |
+
<input type="text" value={log.user_name} readOnly={true} />
|
| 62 |
+
</label>
|
| 63 |
+
<label>
|
| 64 |
+
<span className='title'>Время:</span>
|
| 65 |
+
<input type="text" value={(log.date_created ? new Date(log.date_created).toLocaleDateString() : '') + ' ' + (log.date_created ? new Date(log.date_created).toLocaleTimeString() : '')} readOnly={true} />
|
| 66 |
+
</label>
|
| 67 |
+
{log.error ? (
|
| 68 |
+
<label>
|
| 69 |
+
<span className='title'>Ошибка:</span>
|
| 70 |
+
<textarea value={log.error} readOnly={true} />
|
| 71 |
+
</label>) : ('')}
|
| 72 |
+
<label>
|
| 73 |
+
<span className='title'>Исходный запрос пользователя:</span>
|
| 74 |
+
<textarea value={log.user_request} rows={2} />
|
| 75 |
+
</label>
|
| 76 |
+
<label>
|
| 77 |
+
<span className='title'>Результат QE:</span>
|
| 78 |
+
<div
|
| 79 |
+
style={{
|
| 80 |
+
border: '1px solid #ccc',
|
| 81 |
+
padding: '10px',
|
| 82 |
+
maxHeight: '300px',
|
| 83 |
+
overflow: 'auto',
|
| 84 |
+
background: '#f5f5f5',
|
| 85 |
+
resize: 'both'
|
| 86 |
+
}}
|
| 87 |
+
>
|
| 88 |
+
{qeResult ? (
|
| 89 |
+
<ReactJson
|
| 90 |
+
src={qeResult}
|
| 91 |
+
theme="monokai" // Тема для подсветки синтаксиса
|
| 92 |
+
collapsed={1} // Сворачивать узлы на первом уровне вложенности
|
| 93 |
+
displayDataTypes={false} // Скрыть типы данных
|
| 94 |
+
displayObjectSize={false} // Скрыть размеры объектов
|
| 95 |
+
style={{ background: 'transparent', resize: 'both' }}
|
| 96 |
+
/>
|
| 97 |
+
) : (
|
| 98 |
+
<textarea value={log.qe_result} readOnly={true} />
|
| 99 |
+
)}
|
| 100 |
+
</div>
|
| 101 |
+
</label>
|
| 102 |
+
<label>
|
| 103 |
+
<span className='title'>Результат поиска:</span>
|
| 104 |
+
<textarea value={log.search_result} rows={10} readOnly={true} />
|
| 105 |
+
</label>
|
| 106 |
+
<label>
|
| 107 |
+
<span className='title'>Ответ LLM:</span>
|
| 108 |
+
<textarea value={log.llm_result} rows={10} readOnly={true} />
|
| 109 |
+
</label>
|
| 110 |
+
</form>
|
| 111 |
+
</div>
|
| 112 |
+
</Modal>
|
| 113 |
+
);
|
| 114 |
+
};
|
| 115 |
+
|
| 116 |
+
export default LogDetailsModal;
|
src/components/pages/logs/Logs.scss
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.documents-page {
|
| 2 |
+
min-width: 700px;
|
| 3 |
+
|
| 4 |
+
.btn {
|
| 5 |
+
line-height: unset;
|
| 6 |
+
}
|
| 7 |
+
|
| 8 |
+
.docs-actions {
|
| 9 |
+
display: flex;
|
| 10 |
+
gap: 10px;
|
| 11 |
+
align-items: center;
|
| 12 |
+
justify-content: space-between;
|
| 13 |
+
margin-bottom: 10px;
|
| 14 |
+
|
| 15 |
+
.datasets-name {
|
| 16 |
+
font-weight: 700;
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
.actions {
|
| 20 |
+
display: flex;
|
| 21 |
+
gap: 10px;
|
| 22 |
+
}
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
.datasets-actions {
|
| 26 |
+
display: flex;
|
| 27 |
+
gap: 10px;
|
| 28 |
+
align-items: center;
|
| 29 |
+
margin-bottom: 50px;
|
| 30 |
+
|
| 31 |
+
.react-select {
|
| 32 |
+
&__control {
|
| 33 |
+
border-radius: 10px;
|
| 34 |
+
min-width: 200px;
|
| 35 |
+
max-width: 700px;
|
| 36 |
+
background-color: var(--secondary-color);
|
| 37 |
+
|
| 38 |
+
&:hover {
|
| 39 |
+
cursor: pointer;
|
| 40 |
+
}
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
&__option {
|
| 44 |
+
&:hover {
|
| 45 |
+
cursor: pointer;
|
| 46 |
+
}
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
&__single-value {
|
| 50 |
+
color: var(--white-color);
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
&__value-container {
|
| 54 |
+
text-align: start;
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
&__indicator {
|
| 58 |
+
color: var(--white-color);
|
| 59 |
+
}
|
| 60 |
+
}
|
| 61 |
+
}
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
.docs-modal {
|
| 65 |
+
padding-left: 20px;
|
| 66 |
+
|
| 67 |
+
p {
|
| 68 |
+
margin: 40px 0 40px 0;
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
.docs-modal-actions {
|
| 72 |
+
display: flex;
|
| 73 |
+
justify-content: end;
|
| 74 |
+
gap: 10px;
|
| 75 |
+
}
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
td.ellipsis {
|
| 79 |
+
overflow: hidden;
|
| 80 |
+
text-overflow: ellipsis;
|
| 81 |
+
white-space: nowrap;
|
| 82 |
+
max-width: 400px;
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
.date-filter {
|
| 86 |
+
display: flex;
|
| 87 |
+
gap: 20px;
|
| 88 |
+
margin-bottom: 20px;
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
.date-picker-container {
|
| 92 |
+
display: flex;
|
| 93 |
+
flex-direction: row;
|
| 94 |
+
gap: 8px;
|
| 95 |
+
align-items: center;
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
.react-datepicker__input-container input {
|
| 100 |
+
padding: 8px;
|
| 101 |
+
border: 1px solid #ccc;
|
| 102 |
+
border-radius: 4px;
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
// .react-datepicker__input-container {
|
| 106 |
+
|
| 107 |
+
// input,
|
| 108 |
+
// input:hover,
|
| 109 |
+
// input:focus {
|
| 110 |
+
// border-radius: 5px;
|
| 111 |
+
// border: 2px solid var(--border-1-color);
|
| 112 |
+
// }
|
| 113 |
+
|
| 114 |
+
// .react-datepicker__close-icon::after {
|
| 115 |
+
// background-color: var(--purple-color);
|
| 116 |
+
// }
|
| 117 |
+
// }
|
src/components/pages/logs/Logs.tsx
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { FC, useCallback, useEffect, useMemo, useState } from "react";
|
| 2 |
+
import { useSearchParams } from "react-router-dom";
|
| 3 |
+
import Select from "react-select";
|
| 4 |
+
|
| 5 |
+
import {
|
| 6 |
+
useActivateDataset,
|
| 7 |
+
useCreateDatasetsDraft,
|
| 8 |
+
useDeleteDataset,
|
| 9 |
+
useDeleteFileFromDataset,
|
| 10 |
+
useGetDatasets,
|
| 11 |
+
} from "@/api/documents/hooks";
|
| 12 |
+
import Button from "@/components/generics/button/Button";
|
| 13 |
+
import Modal from "@/components/generics/modal/Modal";
|
| 14 |
+
import { DocsList } from "@/components/views/documents/docsList/DocsList";
|
| 15 |
+
import { CreateDatasetForm } from "@/components/views/documents/createDatasetForm/CreateDatasetForm";
|
| 16 |
+
import { AddDocumentForm } from "@/components/views/documents/addDocuimentForm/AddDocumentForm";
|
| 17 |
+
|
| 18 |
+
import "./Logs.scss";
|
| 19 |
+
import { downloadDocument } from "@/api/documents/documentsApi";
|
| 20 |
+
import Input from "@/components/generics/input/Input";
|
| 21 |
+
import Spinner from "@/components/generics/spinner/Spinner";
|
| 22 |
+
import Tag from "@/components/generics/tag/Tag";
|
| 23 |
+
import Tooltip from "@/components/generics/tooltip/Tooltip";
|
| 24 |
+
import { Pagination } from "@/components/views/pagination/Pagination";
|
| 25 |
+
import { SortDirectionsTooltipMap, StatusMap } from "@/shared/constants";
|
| 26 |
+
import { GoSearch, GoDownload, GoTrash, GoTriangleUp, GoTriangleDown } from "react-icons/go";
|
| 27 |
+
import { useGetLogs } from "@/api/logs/hooks";
|
| 28 |
+
import { GetLogsRequestParams, LogItemType, SortDirections } from "@/api/logs/types";
|
| 29 |
+
import Documents from "../documentsPage/Documents";
|
| 30 |
+
import LogDetailsModal from "./LogDetailsModal";
|
| 31 |
+
import DatePicker from "react-datepicker";
|
| 32 |
+
import "react-datepicker/dist/react-datepicker.css";
|
| 33 |
+
|
| 34 |
+
const Logs: FC = () => {
|
| 35 |
+
const [page, setPage] = useState<number>(0);
|
| 36 |
+
const [pageSize, setPageSize] = useState<number>(50);
|
| 37 |
+
const [userInput, setUserInput] = useState<string | undefined>(undefined);
|
| 38 |
+
const [user, setUser] = useState<string | undefined>(undefined);
|
| 39 |
+
const [sort, setSort] = useState<undefined | { field: string; direction: SortDirections }[]>(undefined);
|
| 40 |
+
const [dateTo, setDateTo] = useState<Date | undefined>(undefined);
|
| 41 |
+
const [dateFrom, setDateFrom] = useState<Date | undefined>(undefined);
|
| 42 |
+
const { data: logsData, isLoading } = useGetLogs({
|
| 43 |
+
page, page_size: pageSize,
|
| 44 |
+
user_name: user, sort,
|
| 45 |
+
date_to: dateTo, date_from: dateFrom
|
| 46 |
+
});
|
| 47 |
+
const [isModalOpen, setIsModalOpen] = useState(false);
|
| 48 |
+
const [selectedLog, setSelectedLog] = useState<LogItemType | null>(null);
|
| 49 |
+
|
| 50 |
+
const [searchParams, setSearchParams] = useSearchParams();
|
| 51 |
+
|
| 52 |
+
// const handleFilterList = () => {
|
| 53 |
+
// const localStartDate = dateFrom ? new Date(dateFrom) : undefined;
|
| 54 |
+
// localStartDate?.setHours(0, 0, 0, 0);
|
| 55 |
+
|
| 56 |
+
// const localEndDate = dateTo ? new Date(dateTo) : undefined;
|
| 57 |
+
// localEndDate?.setHours(23, 59, 59, 999);
|
| 58 |
+
|
| 59 |
+
// setDateFrom(localStartDate ? new Date(localStartDate) : undefined);
|
| 60 |
+
// setDateTo(localEndDate ? new Date(localEndDate) : undefined);
|
| 61 |
+
// setPage(0);
|
| 62 |
+
// };
|
| 63 |
+
|
| 64 |
+
const openDetailsModal = (log: LogItemType) => {
|
| 65 |
+
setSelectedLog(log)
|
| 66 |
+
setIsModalOpen(true);
|
| 67 |
+
};
|
| 68 |
+
|
| 69 |
+
const closeModal = () => {
|
| 70 |
+
setSelectedLog(null);
|
| 71 |
+
setIsModalOpen(false);
|
| 72 |
+
};
|
| 73 |
+
|
| 74 |
+
const toggleSort = (field: string) => {
|
| 75 |
+
setSort((prevSort) => {
|
| 76 |
+
if (prevSort?.length && prevSort?.length > 0) {
|
| 77 |
+
const newSort = [...prevSort];
|
| 78 |
+
const existingFieldIndex = prevSort?.findIndex((e) => e.field === field);
|
| 79 |
+
const currentDirection = prevSort[existingFieldIndex]?.direction;
|
| 80 |
+
|
| 81 |
+
if (existingFieldIndex != null && existingFieldIndex !== -1) {
|
| 82 |
+
const newSort = [...prevSort];
|
| 83 |
+
if (currentDirection === SortDirections.asc) {
|
| 84 |
+
newSort[existingFieldIndex] = { field, direction: SortDirections.desc };
|
| 85 |
+
return newSort;
|
| 86 |
+
} else if (currentDirection === SortDirections.desc) {
|
| 87 |
+
newSort.splice(existingFieldIndex, 1);
|
| 88 |
+
return newSort;
|
| 89 |
+
}
|
| 90 |
+
} else {
|
| 91 |
+
newSort.push({ field, direction: SortDirections.asc });
|
| 92 |
+
}
|
| 93 |
+
return newSort;
|
| 94 |
+
} else {
|
| 95 |
+
return [{ field, direction: SortDirections.asc }];
|
| 96 |
+
}
|
| 97 |
+
});
|
| 98 |
+
};
|
| 99 |
+
|
| 100 |
+
const sorting = (field: string) => {
|
| 101 |
+
const currentDirection = sort?.find((e) => e.field === field)?.direction;
|
| 102 |
+
|
| 103 |
+
return (
|
| 104 |
+
<Tooltip text={SortDirectionsTooltipMap[currentDirection ?? "empty"]}>
|
| 105 |
+
<Button
|
| 106 |
+
onClick={() => toggleSort(field)}
|
| 107 |
+
icon={
|
| 108 |
+
<div className="sort-btn">
|
| 109 |
+
<GoTriangleUp viewBox="0 0 20 20" className={currentDirection === SortDirections.asc ? "active" : ""} />
|
| 110 |
+
<GoTriangleDown
|
| 111 |
+
viewBox="0 0 20 20"
|
| 112 |
+
className={currentDirection === SortDirections.desc ? "arrow-down active" : "arrow-down"}
|
| 113 |
+
/>
|
| 114 |
+
</div>
|
| 115 |
+
}
|
| 116 |
+
buttonType="link"
|
| 117 |
+
/>
|
| 118 |
+
</Tooltip>
|
| 119 |
+
);
|
| 120 |
+
};
|
| 121 |
+
|
| 122 |
+
// useEffect(() => {
|
| 123 |
+
// if (
|
| 124 |
+
// (!searchParams.get("datasetId") ||
|
| 125 |
+
// !datasetsData?.find((e) => e.id.toString() == searchParams.get("datasetId"))) &&
|
| 126 |
+
// datasetsData?.[0]?.id
|
| 127 |
+
// ) {
|
| 128 |
+
// setSearchParams({ datasetId: String(datasetsData?.[0]?.id) });
|
| 129 |
+
// }
|
| 130 |
+
// }, [datasetsData, searchParams, setSearchParams]);
|
| 131 |
+
|
| 132 |
+
return (
|
| 133 |
+
<div className="documents-page">
|
| 134 |
+
<div className="date-filter">
|
| 135 |
+
<div className="date-picker-container">
|
| 136 |
+
<label htmlFor="startDate">Показать логи с</label>
|
| 137 |
+
<DatePicker
|
| 138 |
+
id="startDate"
|
| 139 |
+
dateFormat="dd/MM/yyyy"
|
| 140 |
+
selected={dateFrom}
|
| 141 |
+
onChange={(date) => setDateFrom(date ?? undefined)}
|
| 142 |
+
selectsStart
|
| 143 |
+
isClearable={true}
|
| 144 |
+
showIcon
|
| 145 |
+
/>
|
| 146 |
+
</div>
|
| 147 |
+
<div className="date-picker-container">
|
| 148 |
+
<label htmlFor="endDate">по</label>
|
| 149 |
+
<DatePicker
|
| 150 |
+
id="endDate"
|
| 151 |
+
dateFormat="dd/MM/yyyy"
|
| 152 |
+
selected={dateTo}
|
| 153 |
+
onChange={(date) => setDateTo(date ?? undefined)}
|
| 154 |
+
selectsEnd
|
| 155 |
+
isClearable={true}
|
| 156 |
+
showIcon
|
| 157 |
+
/>
|
| 158 |
+
</div>
|
| 159 |
+
{/* <Button name={"Показать"} onClick={handleFilterList} loading={isLoading} /> */}
|
| 160 |
+
</div>
|
| 161 |
+
<div className="docs-table-container" style={{ position: "relative" }}>
|
| 162 |
+
{isLoading && (
|
| 163 |
+
<div className="loading-overlay">
|
| 164 |
+
<Spinner />
|
| 165 |
+
</div>
|
| 166 |
+
)}
|
| 167 |
+
<table className="docs-table">
|
| 168 |
+
<thead>
|
| 169 |
+
<tr>
|
| 170 |
+
<th style={{ width: "50%" }}>
|
| 171 |
+
<div className="name-with-sort">
|
| 172 |
+
<span>Время</span>
|
| 173 |
+
{sorting("date_created")}
|
| 174 |
+
</div>
|
| 175 |
+
</th>
|
| 176 |
+
<th>
|
| 177 |
+
<div className="name-with-sort">
|
| 178 |
+
<span>Пользователь</span>
|
| 179 |
+
</div>
|
| 180 |
+
</th>
|
| 181 |
+
<th>
|
| 182 |
+
<div className="name-with-sort">
|
| 183 |
+
<span>Запрос</span>
|
| 184 |
+
</div>
|
| 185 |
+
</th>
|
| 186 |
+
<th>
|
| 187 |
+
<div className="name-with-sort">
|
| 188 |
+
<span>Ответ</span>
|
| 189 |
+
</div>
|
| 190 |
+
</th>
|
| 191 |
+
<th></th>
|
| 192 |
+
</tr>
|
| 193 |
+
</thead>
|
| 194 |
+
<tbody>
|
| 195 |
+
{logsData?.data.map((log) => (
|
| 196 |
+
<tr key={log.id} onClick={() => openDetailsModal(log)}>
|
| 197 |
+
<td>{log.date_created ? new Date(log.date_created).toLocaleDateString() : ''} {log.date_created ? new Date(log.date_created).toLocaleTimeString() : ''}</td>
|
| 198 |
+
<td>{log.user_name}</td>
|
| 199 |
+
<td className="ellipsis">{log.user_request}</td>
|
| 200 |
+
<td className="ellipsis">{log.llm_result}</td>
|
| 201 |
+
</tr>
|
| 202 |
+
))}
|
| 203 |
+
</tbody>
|
| 204 |
+
</table>
|
| 205 |
+
</div>
|
| 206 |
+
<Pagination
|
| 207 |
+
total={logsData?.data.total ?? 0}
|
| 208 |
+
pageNumber={logsData?.data.page ? logsData?.data.page : page}
|
| 209 |
+
pageSize={logsData?.data.page_size ?? 50}
|
| 210 |
+
setPageSize={setPageSize}
|
| 211 |
+
setPage={setPage}
|
| 212 |
+
/>
|
| 213 |
+
{selectedLog !== null ? (<LogDetailsModal
|
| 214 |
+
isOpen={isModalOpen}
|
| 215 |
+
onRequestClose={closeModal}
|
| 216 |
+
log={selectedLog}
|
| 217 |
+
/>) : ''}
|
| 218 |
+
|
| 219 |
+
</div>
|
| 220 |
+
);
|
| 221 |
+
};
|
| 222 |
+
|
| 223 |
+
export default Logs;
|