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;
|