muryshev commited on
Commit
893662b
·
1 Parent(s): 19eca0c
.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;