muryshev commited on
Commit
79278ec
·
1 Parent(s): 3c616c4
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .dockerignore +6 -0
  2. .gitignore +24 -0
  3. Dockerfile +25 -0
  4. README.md +3 -3
  5. devops/nginx.default.conf +23 -0
  6. eslint.config.js +28 -0
  7. index.html +12 -0
  8. package-lock.json +0 -0
  9. package.json +49 -0
  10. public/pdf.worker.mjs +0 -0
  11. src/App.css +13 -0
  12. src/App.tsx +33 -0
  13. src/api/documents/documentsApi.ts +170 -0
  14. src/api/documents/hooks.ts +121 -0
  15. src/api/documents/types.ts +67 -0
  16. src/api/llmConfigs/hooks.ts +49 -0
  17. src/api/llmConfigs/llmConfigApi.ts +79 -0
  18. src/api/llmConfigs/types.ts +14 -0
  19. src/api/llmPrompts/hooks.ts +49 -0
  20. src/api/llmPrompts/llmPromptApi.ts +79 -0
  21. src/api/llmPrompts/types.ts +9 -0
  22. src/api/predictions/hooks.ts +143 -0
  23. src/api/predictions/predictionsApi.ts +159 -0
  24. src/api/predictions/types.ts +159 -0
  25. src/components/common/navbar/Navbar.scss +36 -0
  26. src/components/common/navbar/Navbar.tsx +46 -0
  27. src/components/generics/button/Button.interface.ts +11 -0
  28. src/components/generics/button/Button.scss +70 -0
  29. src/components/generics/button/Button.tsx +23 -0
  30. src/components/generics/collapse/Collapse.inreface.ts +8 -0
  31. src/components/generics/collapse/Collapse.scss +40 -0
  32. src/components/generics/collapse/Collapse.tsx +22 -0
  33. src/components/generics/dropdown/Dropdown.interface.ts +4 -0
  34. src/components/generics/dropdown/Dropdown.scss +21 -0
  35. src/components/generics/dropdown/Dropdown.tsx +14 -0
  36. src/components/generics/editable/Editable.interface.ts +10 -0
  37. src/components/generics/editable/Editable.scss +31 -0
  38. src/components/generics/editable/Editable.tsx +112 -0
  39. src/components/generics/input/Input.interface.ts +13 -0
  40. src/components/generics/input/Input.scss +34 -0
  41. src/components/generics/input/Input.tsx +77 -0
  42. src/components/generics/loading/Loading.tsx +23 -0
  43. src/components/generics/modal/Modal.interface.ts +6 -0
  44. src/components/generics/modal/Modal.scss +9 -0
  45. src/components/generics/modal/Modal.tsx +34 -0
  46. src/components/generics/sizeChanger/SizeChanger.scss +31 -0
  47. src/components/generics/sizeChanger/SizeChanger.tsx +38 -0
  48. src/components/generics/sizeChanger/SizeChangerProps.ts +6 -0
  49. src/components/generics/spinner/Spinner.scss +14 -0
  50. src/components/generics/spinner/Spinner.tsx +7 -0
.dockerignore ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ .env
2
+ desktop.ini
3
+ *.bat
4
+ /logs
5
+ /build
6
+ /node_modules
.gitignore ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Logs
2
+ logs
3
+ *.log
4
+ npm-debug.log*
5
+ yarn-debug.log*
6
+ yarn-error.log*
7
+ pnpm-debug.log*
8
+ lerna-debug.log*
9
+
10
+ node_modules
11
+ dist
12
+ dist-ssr
13
+ *.local
14
+
15
+ # Editor directories and files
16
+ .vscode/*
17
+ !.vscode/extensions.json
18
+ .idea
19
+ .DS_Store
20
+ *.suo
21
+ *.ntvs*
22
+ *.njsproj
23
+ *.sln
24
+ *.sw?
Dockerfile ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Build
2
+ FROM node:20-alpine AS builder
3
+ WORKDIR /app
4
+
5
+ COPY package.json ./
6
+ COPY package-lock.json* ./
7
+
8
+ RUN npm ci
9
+
10
+ COPY . .
11
+
12
+ ARG VITE_API_BASE_URL=https://muryshev-generic-chatbot-backend.hf.space
13
+ ENV VITE_API_BASE_URL=$VITE_API_BASE_URL
14
+ RUN npm run build
15
+
16
+ # Production
17
+ FROM nginx:alpine
18
+ WORKDIR /usr/share/nginx/html
19
+
20
+ COPY --from=builder /app/dist .
21
+ COPY devops/nginx.default.conf /etc/nginx/conf.d/default.conf
22
+
23
+ ENV PORT=7860
24
+ EXPOSE ${PORT}
25
+ CMD ["nginx", "-g", "daemon off;"]
README.md CHANGED
@@ -1,10 +1,10 @@
1
  ---
2
  title: Generic Chatbot Admin
3
  emoji: 😻
4
- colorFrom: gray
5
- colorTo: red
6
  sdk: docker
7
- pinned: false
8
  ---
9
 
10
  Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
1
  ---
2
  title: Generic Chatbot Admin
3
  emoji: 😻
4
+ colorFrom: red
5
+ colorTo: green
6
  sdk: docker
7
+ pinned: true
8
  ---
9
 
10
  Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
devops/nginx.default.conf ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ server {
2
+ listen 80 default_server;
3
+ server_name _;
4
+
5
+ root /usr/share/nginx/html;
6
+ index index.html;
7
+
8
+ location / {
9
+ try_files $uri $uri/ /index.html;
10
+ }
11
+
12
+ location = /favicon.ico {
13
+ log_not_found off;
14
+ access_log off;
15
+ }
16
+
17
+ location = /robots.txt {
18
+ log_not_found off;
19
+ access_log off;
20
+ }
21
+
22
+ error_page 404 /index.html;
23
+ }
eslint.config.js ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import js from '@eslint/js'
2
+ import globals from 'globals'
3
+ import reactHooks from 'eslint-plugin-react-hooks'
4
+ import reactRefresh from 'eslint-plugin-react-refresh'
5
+ import tseslint from 'typescript-eslint'
6
+
7
+ export default tseslint.config(
8
+ { ignores: ['dist'] },
9
+ {
10
+ extends: [js.configs.recommended, ...tseslint.configs.recommended],
11
+ files: ['**/*.{ts,tsx}'],
12
+ languageOptions: {
13
+ ecmaVersion: 2020,
14
+ globals: globals.browser,
15
+ },
16
+ plugins: {
17
+ 'react-hooks': reactHooks,
18
+ 'react-refresh': reactRefresh,
19
+ },
20
+ rules: {
21
+ ...reactHooks.configs.recommended.rules,
22
+ 'react-refresh/only-export-components': [
23
+ 'warn',
24
+ { allowConstantExport: true },
25
+ ],
26
+ },
27
+ },
28
+ )
index.html ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Чат бот</title>
7
+ </head>
8
+ <body>
9
+ <div id="root"></div>
10
+ <script type="module" src="/src/main.tsx"></script>
11
+ </body>
12
+ </html>
package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
package.json ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "nmd-front",
3
+ "private": true,
4
+ "version": "0.0.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "tsc -b && vite build",
9
+ "lint": "eslint .",
10
+ "preview": "vite preview"
11
+ },
12
+ "dependencies": {
13
+ "@fontsource/fira-mono": "^5.0.14",
14
+ "@reduxjs/toolkit": "^2.3.0",
15
+ "@tanstack/react-query": "^5.54.1",
16
+ "@tanstack/react-query-devtools": "^5.54.1",
17
+ "axios": "^1.7.4",
18
+ "react": "^18.3.1",
19
+ "react-contenteditable": "^3.3.7",
20
+ "react-datepicker": "^7.3.0",
21
+ "react-dom": "^18.3.1",
22
+ "react-flip-move": "^3.0.5",
23
+ "react-icons": "^5.3.0",
24
+ "react-modal": "^3.16.1",
25
+ "react-pdf": "^9.2.1",
26
+ "react-redux": "^9.1.2",
27
+ "react-router-dom": "^6.26.2",
28
+ "react-select": "^5.9.0",
29
+ "sanitize-html": "^2.13.1",
30
+ "xlsx": "^0.18.5"
31
+ },
32
+ "devDependencies": {
33
+ "@eslint/js": "^9.9.0",
34
+ "@types/node": "^22.4.1",
35
+ "@types/react": "^18.3.3",
36
+ "@types/react-dom": "^18.3.0",
37
+ "@types/react-modal": "^3.16.3",
38
+ "@types/sanitize-html": "^2.13.0",
39
+ "@vitejs/plugin-react": "^4.3.1",
40
+ "eslint": "^9.9.0",
41
+ "eslint-plugin-react-hooks": "^5.1.0-rc.0",
42
+ "eslint-plugin-react-refresh": "^0.4.9",
43
+ "globals": "^15.9.0",
44
+ "sass-embedded": "^1.77.8",
45
+ "typescript": "^5.5.3",
46
+ "typescript-eslint": "^8.0.1",
47
+ "vite": "^5.4.1"
48
+ }
49
+ }
public/pdf.worker.mjs ADDED
The diff for this file is too large to render. See raw diff
 
src/App.css ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #root {
2
+ max-width: 1280px;
3
+ margin: 0 auto;
4
+ padding: 2rem;
5
+ text-align: center;
6
+ }
7
+
8
+ .header {
9
+ font-size: 20px;
10
+ text-align: left;
11
+ padding-left: 10px;
12
+ font-weight: 600;
13
+ }
src/App.tsx ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import "./App.css";
2
+ import { Routes, Route, BrowserRouter as Router, Navigate } from "react-router-dom";
3
+ import Navbar from '@/components/common/navbar/Navbar';
4
+ import Page from "@/components/pages/main/Page";
5
+ import Logs from "@/components/pages/logsPage/Logs";
6
+ import Vectorization from "./components/pages/vectorizationPage/Vectorization";
7
+ import { pdfjs } from "react-pdf";
8
+ import LLMConfigList from "./components/pages/llmConfigs/LlmConfigList";
9
+ import LlmPromptList from "./components/pages/llmPrompts/LlmPromptList";
10
+
11
+ pdfjs.GlobalWorkerOptions.workerSrc = new URL(
12
+ "pdfjs-dist/build/pdf.worker.min.mjs",
13
+ import.meta.url
14
+ ).toString();
15
+
16
+ function App() {
17
+ return (
18
+ <Router>
19
+ <Navbar />
20
+ <Routes>
21
+ {/* <Route path="" element={<Page />} /> */}
22
+ <Route path="/logs" element={<Logs />} />
23
+ {/* <Route path="/test" element={<TestPage />} /> */}
24
+ <Route path="/docs" element={<Vectorization />} />
25
+ <Route path="/llmconfig" element={<LLMConfigList />} />
26
+ <Route path="/llmprompt" element={<LlmPromptList />} />
27
+ {/* <Route path="/" element={<Navigate to="/logs" />} /> */}
28
+ </Routes>
29
+ </Router>
30
+ );
31
+ }
32
+
33
+ export default App;
src/api/documents/documentsApi.ts ADDED
@@ -0,0 +1,170 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import {
2
+ DatasetDataType,
3
+ DatasetInfoDataType,
4
+ DatasetsResponseType,
5
+ GetDatasetRequestParams,
6
+ GetProcessingRequest,
7
+ } from "./types";
8
+
9
+ import { downloadFile } from "@/shared/utils/downloadFile";
10
+ import { query } from "@/shared/api/query";
11
+
12
+ export const getDatasets = async (): Promise<DatasetsResponseType> => {
13
+ const response = await query<DatasetsResponseType>({
14
+ url: "/datasets/",
15
+ method: "get",
16
+ });
17
+ if ("error" in response) {
18
+ throw new Error(`Ошибка: ${response.error.status}`);
19
+ }
20
+
21
+ return response.data;
22
+ };
23
+
24
+ export const createDataset = async (data: FormData): Promise<DatasetDataType> => {
25
+ const response = await query<DatasetDataType>({
26
+ url: "/datasets/",
27
+ method: "post",
28
+ data,
29
+ });
30
+ if ("error" in response) {
31
+ throw new Error(`Ошибка: ${response.error.status}`);
32
+ }
33
+
34
+ return response.data;
35
+ };
36
+
37
+ export const activateDataset = async (id: number): Promise<DatasetDataType> => {
38
+ const response = await query<DatasetDataType>({
39
+ url: `/datasets/${id}`,
40
+ method: "post",
41
+ });
42
+ if ("error" in response) {
43
+ throw new Error(`Ошибка: ${response.error.status}`);
44
+ }
45
+
46
+ return response.data;
47
+ };
48
+
49
+ export const getDataset = async (id: number, params: GetDatasetRequestParams): Promise<DatasetDataType> => {
50
+ const requestParams = { ...params, sort: undefined };
51
+ const sortParam: string[] = [];
52
+
53
+ if (params.sort) {
54
+ params.sort.forEach((element) => {
55
+ if (element.field && element.direction) {
56
+ sortParam.push(`${element.field}:${element.direction}`);
57
+ }
58
+ });
59
+ }
60
+
61
+ const response = await query<DatasetDataType>({
62
+ url: `/datasets/${id}`,
63
+ method: "get",
64
+ params: { ...requestParams, sort: sortParam.length > 0 ? sortParam.join(",") : undefined },
65
+ });
66
+ if ("error" in response) {
67
+ throw new Error(`Ошибка: ${response.error.status}`);
68
+ }
69
+
70
+ return response.data;
71
+ };
72
+
73
+ export const createDatasetsDraft = async (id: number): Promise<DatasetInfoDataType> => {
74
+ const response = await query<DatasetInfoDataType>({
75
+ url: `/datasets/${id}/edit`,
76
+ method: "post",
77
+ });
78
+ if ("error" in response) {
79
+ throw new Error(`Ошибка: ${response.error.status}`);
80
+ }
81
+
82
+ return response.data;
83
+ };
84
+
85
+ export const deleteDataset = async (id: number): Promise<void> => {
86
+ const response = await query<void>({
87
+ url: `/datasets/${id}`,
88
+ method: "delete",
89
+ });
90
+ if ("error" in response) {
91
+ throw new Error(`Ошибка: ${response.error.status}`);
92
+ }
93
+
94
+ return response.data;
95
+ };
96
+
97
+ export const addFileToDataset = async (id: number, data: FormData): Promise<void> => {
98
+ const response = await query<void>({
99
+ url: `/datasets/${id}/documents/`,
100
+ method: "post",
101
+ data,
102
+ });
103
+ if ("error" in response) {
104
+ throw new Error(`Ошибка: ${response.error.status}`);
105
+ }
106
+
107
+ return response.data;
108
+ };
109
+
110
+ export const deleteFileFromDataset = async (datasetId: number, fileId: number): Promise<void> => {
111
+ const response = await query<void>({
112
+ url: `/datasets/${datasetId}/documents/${fileId}`,
113
+ method: "delete",
114
+ });
115
+ if ("error" in response) {
116
+ throw new Error(`Ошибка: ${response.error.status}`);
117
+ }
118
+
119
+ return response.data;
120
+ };
121
+
122
+ export const getProcessing = async (): Promise<GetProcessingRequest> => {
123
+ const response = await query<GetProcessingRequest>({
124
+ url: "/datasets/processing",
125
+ method: "get",
126
+ });
127
+ if ("error" in response) {
128
+ throw new Error(`Ошибка: ${response.error.status}`);
129
+ }
130
+
131
+ return response.data;
132
+ };
133
+
134
+ export const downloadDocument = async (
135
+ datasetId: number | null,
136
+ fileId: string,
137
+ filename: string
138
+ ): Promise<Blob> => {
139
+ const response = await query<Blob>({
140
+ url: `/datasets/${datasetId}/documents/${fileId}`,
141
+ method: "get",
142
+ responseType: "blob",
143
+ });
144
+
145
+ if ("error" in response) {
146
+ throw new Error(`Ошибка: ${response.error.status}`);
147
+ }
148
+ downloadFile(response, filename);
149
+
150
+ return response.data;
151
+ };
152
+
153
+
154
+ export const getDocumentPdf = async (
155
+ datasetId: number | null,
156
+ fileId: string,
157
+ filename: string
158
+ ): Promise<{ file: Blob; filename: string }> => {
159
+ const response = await query<Blob>({
160
+ url: `/datasets/${datasetId}/documents/${fileId}/pdf`,
161
+ method: "get",
162
+ responseType: "blob",
163
+ });
164
+
165
+ if ("error" in response) {
166
+ throw new Error(`Ошибка: ${response.error.status}`);
167
+ }
168
+
169
+ return { file: response.data, filename };
170
+ };
src/api/documents/hooks.ts ADDED
@@ -0,0 +1,121 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useMutation, useQuery } from "@tanstack/react-query";
2
+ import {
3
+ activateDataset,
4
+ addFileToDataset,
5
+ createDataset,
6
+ createDatasetsDraft,
7
+ deleteDataset,
8
+ deleteFileFromDataset,
9
+ getDataset,
10
+ getDatasets,
11
+ getProcessing,
12
+ } from "./documentsApi";
13
+ import { GetDatasetRequestParams } from "./types";
14
+ import { queryClient } from "@/main";
15
+
16
+ export const useGetDatasets = () => {
17
+ return useQuery({
18
+ queryKey: ["datasets"],
19
+ queryFn: async () => {
20
+ const response = await getDatasets();
21
+ return response;
22
+ },
23
+ });
24
+ };
25
+
26
+ export const useCreateDataset = () => {
27
+ return useMutation({
28
+ mutationFn: async (data: FormData) => {
29
+ const response = await createDataset(data);
30
+ return response;
31
+ },
32
+ onSuccess: () => {
33
+ queryClient.invalidateQueries({ queryKey: ["datasets"] });
34
+ },
35
+ });
36
+ };
37
+
38
+ export const useActivateDataset = () => {
39
+ return useMutation({
40
+ mutationFn: async (id: number) => {
41
+ const response = await activateDataset(id);
42
+ return response;
43
+ },
44
+ onSuccess: (data) => {
45
+ queryClient.invalidateQueries({ queryKey: ["processing"] });
46
+ queryClient.invalidateQueries({ queryKey: ["datasets"] });
47
+ queryClient.invalidateQueries({ queryKey: ["dataset", data.id] });
48
+ },
49
+ });
50
+ };
51
+
52
+ export const useGetDataset = (id: number, params: GetDatasetRequestParams) => {
53
+ return useQuery({
54
+ queryKey: ["dataset", id, params],
55
+ queryFn: async () => {
56
+ const response = await getDataset(id, params);
57
+ return response;
58
+ },
59
+ });
60
+ };
61
+
62
+ export const useCreateDatasetsDraft = () => {
63
+ return useMutation({
64
+ mutationFn: async (id: number) => {
65
+ const response = await createDatasetsDraft(id);
66
+ return response;
67
+ },
68
+ onSuccess: () => {
69
+ queryClient.invalidateQueries({ queryKey: ["datasets"] });
70
+ queryClient.invalidateQueries({ queryKey: ["dataset"] });
71
+ },
72
+ });
73
+ };
74
+
75
+ export const useDeleteDataset = () => {
76
+ return useMutation({
77
+ mutationFn: async (id: number) => {
78
+ const response = await deleteDataset(id);
79
+ return response;
80
+ },
81
+ onSuccess: () => {
82
+ queryClient.invalidateQueries({ queryKey: ["datasets"] });
83
+ },
84
+ });
85
+ };
86
+
87
+ export const useAddFileToDataset = () => {
88
+ return useMutation({
89
+ mutationFn: async ({ id, data }: { id: number; data: FormData }) => {
90
+ const response = await addFileToDataset(id, data);
91
+ return response;
92
+ },
93
+ onSuccess: () => {
94
+ queryClient.invalidateQueries({ queryKey: ["datasets"] });
95
+ queryClient.invalidateQueries({ queryKey: ["dataset"] });
96
+ },
97
+ });
98
+ };
99
+
100
+ export const useDeleteFileFromDataset = () => {
101
+ return useMutation({
102
+ mutationFn: async ({ datasetId, fileId }: { datasetId: number; fileId: number }) => {
103
+ const response = await deleteFileFromDataset(datasetId, fileId);
104
+ return response;
105
+ },
106
+ onSuccess: () => {
107
+ queryClient.invalidateQueries({ queryKey: ["datasets"] });
108
+ queryClient.invalidateQueries({ queryKey: ["dataset"] });
109
+ },
110
+ });
111
+ };
112
+
113
+ export const useGetProcessing = () => {
114
+ return useQuery({
115
+ queryKey: ["processing"],
116
+ queryFn: async () => {
117
+ const response = await getProcessing();
118
+ return response;
119
+ },
120
+ });
121
+ };
src/api/documents/types.ts ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export type DatasetInfoDataType = {
2
+ id: number;
3
+ dateCreated: string; //2024-12-02
4
+ name: string;
5
+ isDraft: boolean;
6
+ isActive: boolean;
7
+ };
8
+
9
+ export type DatasetsResponseType = DatasetInfoDataType[];
10
+
11
+ export type CreateDatasetRequestType = {
12
+ zip: File; //проверить
13
+ };
14
+
15
+ export type DatasetDataType = DatasetInfoDataType & {
16
+ data: PaginationType & {
17
+ page: FileDataType[];
18
+ };
19
+ };
20
+
21
+ export type PaginationType = {
22
+ total: number;
23
+ pageNumber: number;
24
+ pageSize: number;
25
+ };
26
+
27
+ export enum FileStatus {
28
+ "Актуален" = "Актуален",
29
+ "Требует актуализации" = "Требует актуализации",
30
+ "Упразднён" = "Упразднён",
31
+ }
32
+
33
+ export type FileDataType = {
34
+ id: number;
35
+ name: string;
36
+ owner: string;
37
+ status: FileStatus;
38
+ };
39
+
40
+ export type AddDocumentRequest = {
41
+ document: File;
42
+ };
43
+
44
+ export enum SortDirections {
45
+ asc = "asc",
46
+ desc = "desc",
47
+ }
48
+
49
+ export type GetDatasetRequestParams = {
50
+ page: number;
51
+ search?: string;
52
+ page_size?: number;
53
+ sort?: { field: string; direction: SortDirections }[];
54
+ };
55
+
56
+ export enum ProcessingStatus {
57
+ ready = "ready",
58
+ in_progress = "in_progress",
59
+ }
60
+
61
+ export type GetProcessingRequest = {
62
+ status: ProcessingStatus;
63
+ datasetName: string;
64
+ current: number;
65
+ total: number;
66
+ ready: number;
67
+ };
src/api/llmConfigs/hooks.ts ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
2
+ import { fetchLLMConfigs, createLLMConfig, updateLLMConfig, setDefaultLLMConfig, deleteLLMConfig } from './llmConfigApi';
3
+
4
+ export const useLLMConfigs = () => {
5
+ const queryClient = useQueryClient();
6
+
7
+ const { data: configs = [], isLoading, error } = useQuery({
8
+ queryKey: ['llmConfigs'],
9
+ queryFn: fetchLLMConfigs,
10
+ });
11
+
12
+ const createMutation = useMutation({
13
+ mutationFn: createLLMConfig,
14
+ onSuccess: () => {
15
+ queryClient.invalidateQueries({ queryKey: ['llmConfigs'] });
16
+ },
17
+ });
18
+
19
+ const updateMutation = useMutation({
20
+ mutationFn: updateLLMConfig,
21
+ onSuccess: () => {
22
+ queryClient.invalidateQueries({ queryKey: ['llmConfigs'] });
23
+ },
24
+ });
25
+
26
+ const setDefaultMutation = useMutation({
27
+ mutationFn: setDefaultLLMConfig,
28
+ onSuccess: () => {
29
+ queryClient.invalidateQueries({ queryKey: ['llmConfigs'] });
30
+ },
31
+ });
32
+
33
+ const deleteMutation = useMutation({
34
+ mutationFn: deleteLLMConfig,
35
+ onSuccess: () => {
36
+ queryClient.invalidateQueries({ queryKey: ['llmConfigs'] });
37
+ },
38
+ });
39
+
40
+ return {
41
+ configs,
42
+ isLoading,
43
+ error: error ? (error instanceof Error ? error.message : 'Failed to fetch configurations') : null,
44
+ createConfig: createMutation.mutateAsync,
45
+ updateConfig: updateMutation.mutateAsync,
46
+ setAsDefaultConfig: setDefaultMutation.mutateAsync,
47
+ deleteConfig: deleteMutation.mutateAsync,
48
+ };
49
+ };
src/api/llmConfigs/llmConfigApi.ts ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { query } from '@/shared/api/query';
2
+ import { LLMConfig } from './types';
3
+
4
+ export const fetchLLMConfigs = async (): Promise<LLMConfig[]> => {
5
+ const response = await query<LLMConfig[]>({
6
+ url: '/llm_config/',
7
+ method: 'get',
8
+ });
9
+ if ('error' in response) {
10
+ throw new Error(`Ошибка получения конфигураций: ${response.error.status}`);
11
+ }
12
+ return response.data;
13
+ };
14
+
15
+ export const fetchLLMConfigById = async (id: number): Promise<LLMConfig> => {
16
+ const response = await query<LLMConfig>({
17
+ url: `/llm_config/${id}`,
18
+ method: 'get',
19
+ });
20
+ if ('error' in response) {
21
+ throw new Error(`Ошибка получения конфигурации: ${response.error.status}`);
22
+ }
23
+ return response.data;
24
+ };
25
+
26
+ export const createLLMConfig = async (config: Omit<LLMConfig, 'id' | 'created_at'>): Promise<LLMConfig> => {
27
+ const response = await query<LLMConfig>({
28
+ url: '/llm_config/',
29
+ method: 'post',
30
+ data: config,
31
+ });
32
+ if ('error' in response) {
33
+ throw new Error(`Ошибка создания конфигурации: ${response.error.status}`);
34
+ }
35
+ return response.data;
36
+ };
37
+
38
+ export const updateLLMConfig = async (config: LLMConfig): Promise<void> => {
39
+ const response = await query<void>({
40
+ url: `/llm_config/${config.id}`,
41
+ method: 'put',
42
+ data: config,
43
+ });
44
+ if ('error' in response) {
45
+ throw new Error(`Ошибка обновления конфигурации: ${response.error.status}`);
46
+ }
47
+ };
48
+
49
+ export const setDefaultLLMConfig = async (id: number): Promise<void> => {
50
+ const response = await query<void>({
51
+ url: `/llm_config/default/${id}`,
52
+ method: 'put',
53
+ });
54
+ if ('error' in response) {
55
+ throw new Error(`Ошибка установки конфигурации по умолчанию: ${response.error.status}`);
56
+ }
57
+ };
58
+
59
+ export const deleteLLMConfig = async (id: number): Promise<void> => {
60
+ const response = await query<void>({
61
+ url: `/llm_config/${id}`,
62
+ method: 'delete',
63
+ });
64
+ if ('error' in response) {
65
+ throw new Error(`Ошибка удаления конфигурации: ${response.error.status}`);
66
+ }
67
+ };
68
+
69
+ export const fetchDefaultLLMConfig = async (): Promise<LLMConfig | null> => {
70
+ const response = await query<LLMConfig>({
71
+ url: '/llm_config/default',
72
+ method: 'get',
73
+ });
74
+ if ('error' in response) {
75
+ if (response.error.status === 404) return null; // Если дефолтной записи нет
76
+ throw new Error(`Ошибка получения дефолтной конфигурации: ${response.error.status}`);
77
+ }
78
+ return response.data;
79
+ };
src/api/llmConfigs/types.ts ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // src/api/llm_configs/types.ts
2
+ export interface LLMConfig {
3
+ id: number;
4
+ is_default: boolean;
5
+ model: string;
6
+ temperature: number;
7
+ top_p: number;
8
+ min_p: number;
9
+ frequency_penalty: number;
10
+ presence_penalty: number;
11
+ n_predict: number;
12
+ seed: number;
13
+ created_at?: string;
14
+ }
src/api/llmPrompts/hooks.ts ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
2
+ import { fetchLlmPrompts, createLlmPrompt, updateLlmPrompt, setDefaultLlmPrompt, deleteLlmPrompt } from './llmPromptApi';
3
+
4
+ export const useLlmPrompts = () => {
5
+ const queryClient = useQueryClient();
6
+
7
+ const { data: prompts = [], isLoading, error } = useQuery({
8
+ queryKey: ['llmPrompts'],
9
+ queryFn: fetchLlmPrompts,
10
+ });
11
+
12
+ const createMutation = useMutation({
13
+ mutationFn: createLlmPrompt,
14
+ onSuccess: () => {
15
+ queryClient.invalidateQueries({ queryKey: ['llmPrompts'] });
16
+ },
17
+ });
18
+
19
+ const updateMutation = useMutation({
20
+ mutationFn: updateLlmPrompt,
21
+ onSuccess: () => {
22
+ queryClient.invalidateQueries({ queryKey: ['llmPrompts'] });
23
+ },
24
+ });
25
+
26
+ const setDefaultMutation = useMutation({
27
+ mutationFn: setDefaultLlmPrompt,
28
+ onSuccess: () => {
29
+ queryClient.invalidateQueries({ queryKey: ['llmPrompts'] });
30
+ },
31
+ });
32
+
33
+ const deleteMutation = useMutation({
34
+ mutationFn: deleteLlmPrompt,
35
+ onSuccess: () => {
36
+ queryClient.invalidateQueries({ queryKey: ['llmPrompts'] });
37
+ },
38
+ });
39
+
40
+ return {
41
+ prompts,
42
+ isLoading,
43
+ error: error ? (error instanceof Error ? error.message : 'Failed to fetch prompturations') : null,
44
+ createPrompt: createMutation.mutateAsync,
45
+ updatePrompt: updateMutation.mutateAsync,
46
+ setAsDefaultPrompt: setDefaultMutation.mutateAsync,
47
+ deletePrompt: deleteMutation.mutateAsync,
48
+ };
49
+ };
src/api/llmPrompts/llmPromptApi.ts ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { query } from '@/shared/api/query';
2
+ import { LlmPrompt } from './types';
3
+
4
+ export const fetchLlmPrompts = async (): Promise<LlmPrompt[]> => {
5
+ const response = await query<LlmPrompt[]>({
6
+ url: '/llm_prompt/',
7
+ method: 'get',
8
+ });
9
+ if ('error' in response) {
10
+ throw new Error(`Ошибка получения промптов: ${response.error.status}`);
11
+ }
12
+ return response.data;
13
+ };
14
+
15
+ export const fetchLlmPromptById = async (id: number): Promise<LlmPrompt> => {
16
+ const response = await query<LlmPrompt>({
17
+ url: `/llm_prompt/${id}`,
18
+ method: 'get',
19
+ });
20
+ if ('error' in response) {
21
+ throw new Error(`Ошибка получения промпта: ${response.error.status}`);
22
+ }
23
+ return response.data;
24
+ };
25
+
26
+ export const createLlmPrompt = async (config: Omit<LlmPrompt, 'id' | 'created_at'>): Promise<LlmPrompt> => {
27
+ const response = await query<LlmPrompt>({
28
+ url: '/llm_prompt/',
29
+ method: 'post',
30
+ data: config,
31
+ });
32
+ if ('error' in response) {
33
+ throw new Error(`Ошибка создания промпта: ${response.error.status}`);
34
+ }
35
+ return response.data;
36
+ };
37
+
38
+ export const updateLlmPrompt = async (config: LlmPrompt): Promise<void> => {
39
+ const response = await query<void>({
40
+ url: `/llm_prompt/${config.id}`,
41
+ method: 'put',
42
+ data: config,
43
+ });
44
+ if ('error' in response) {
45
+ throw new Error(`Ошибка обновления промпта: ${response.error.status}`);
46
+ }
47
+ };
48
+
49
+ export const setDefaultLlmPrompt = async (id: number): Promise<void> => {
50
+ const response = await query<void>({
51
+ url: `/llm_prompt/default/${id}`,
52
+ method: 'put',
53
+ });
54
+ if ('error' in response) {
55
+ throw new Error(`Ошибка установки промпта по умолчанию: ${response.error.status}`);
56
+ }
57
+ };
58
+
59
+ export const deleteLlmPrompt = async (id: number): Promise<void> => {
60
+ const response = await query<void>({
61
+ url: `/llm_prompt/${id}`,
62
+ method: 'delete',
63
+ });
64
+ if ('error' in response) {
65
+ throw new Error(`Ошибка удаления промпта: ${response.error.status}`);
66
+ }
67
+ };
68
+
69
+ export const fetchDefaultLlmPrompt = async (): Promise<LlmPrompt | null> => {
70
+ const response = await query<LlmPrompt>({
71
+ url: '/llm_prompt/default',
72
+ method: 'get',
73
+ });
74
+ if ('error' in response) {
75
+ if (response.error.status === 404) return null; // Если дефолтной записи нет
76
+ throw new Error(`Ошибка получения дефолтной промпта: ${response.error.status}`);
77
+ }
78
+ return response.data;
79
+ };
src/api/llmPrompts/types.ts ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ // src/api/llm_configs/types.ts
2
+ export interface LlmPrompt {
3
+ id: number;
4
+ is_default: boolean;
5
+ text: string;
6
+ name: string;
7
+ type: string;
8
+ created_at?: string;
9
+ }
src/api/predictions/hooks.ts ADDED
@@ -0,0 +1,143 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useMutation, UseMutationResult, useQuery, useQueryClient } from "@tanstack/react-query";
2
+
3
+ import { useRef } from "react";
4
+ import {
5
+ LlmResponseType,
6
+ LlmRequestType,
7
+ ChunksResponseType,
8
+ ChunksArrayItem,
9
+ ChunkRequestType,
10
+ FeedbackRequestType,
11
+ LogRequestType,
12
+ } from "./types";
13
+ import { getLlmResponse, getChunks, postFeedback, getLogs, getAbbreviations } from "./predictionsApi";
14
+
15
+ export function useGetLlmResponse() {
16
+ const queryClient = useQueryClient();
17
+ const abortControllerRef = useRef<AbortController | null>(null);
18
+
19
+ const cancelQuery = () => {
20
+ if (abortControllerRef.current) {
21
+ abortControllerRef.current.abort();
22
+ abortControllerRef.current = null;
23
+ }
24
+ queryClient.cancelQueries({ queryKey: ["llm"] });
25
+ };
26
+
27
+ const mutation: UseMutationResult<LlmResponseType | undefined, Error, LlmRequestType, unknown> = useMutation({
28
+ mutationFn: async (data: LlmRequestType) => {
29
+ // Отменить предыдущий запрос
30
+ if (abortControllerRef.current) {
31
+ abortControllerRef.current.abort();
32
+ }
33
+
34
+ const abortController = new AbortController();
35
+ abortControllerRef.current = abortController;
36
+
37
+ try {
38
+ const response = await getLlmResponse(data, abortController.signal);
39
+ return response;
40
+ } catch (error) {
41
+ if (error instanceof Error && error.name === "AbortError") {
42
+ console.log("Запрос был отменен");
43
+ return undefined;
44
+ } else {
45
+ throw error;
46
+ }
47
+ } finally {
48
+ if (abortControllerRef.current === abortController) {
49
+ abortControllerRef.current = null;
50
+ }
51
+ }
52
+ },
53
+ onSuccess: () => {},
54
+ onError: () => {},
55
+ });
56
+
57
+ return { mutation, cancelQuery };
58
+ }
59
+
60
+ export function useGetChunks() {
61
+ const queryClient = useQueryClient();
62
+ const abortControllerRef = useRef<AbortController | null>(null);
63
+
64
+ const cancelQuery = () => {
65
+ if (abortControllerRef.current) {
66
+ abortControllerRef.current.abort();
67
+ abortControllerRef.current = null;
68
+ }
69
+ queryClient.cancelQueries({ queryKey: ["predictions"] });
70
+ };
71
+
72
+ const mutation: UseMutationResult<
73
+ { chunks: ChunksResponseType; chunksArray: ChunksArrayItem[] } | undefined,
74
+ Error,
75
+ ChunkRequestType,
76
+ unknown
77
+ > = useMutation({
78
+ mutationFn: async (data: ChunkRequestType) => {
79
+ // Отменить предыдущий запрос
80
+ if (abortControllerRef.current) {
81
+ abortControllerRef.current.abort();
82
+ }
83
+
84
+ const abortController = new AbortController();
85
+ abortControllerRef.current = abortController;
86
+
87
+ try {
88
+ const response = await getChunks(data, abortController.signal);
89
+ return response;
90
+ } catch (error) {
91
+ if (error instanceof Error && error.name === "AbortError") {
92
+ console.log("Запрос был отменен");
93
+ return undefined;
94
+ } else {
95
+ throw error;
96
+ }
97
+ } finally {
98
+ if (abortControllerRef.current === abortController) {
99
+ abortControllerRef.current = null;
100
+ }
101
+ }
102
+ },
103
+ onSuccess: () => {
104
+ // if (chunksData) {
105
+ // llmMutation.mutate({ query: query, chunks: chunksData });
106
+ // }
107
+ },
108
+ onError: () => {},
109
+ });
110
+
111
+ return { mutation, cancelQuery };
112
+ }
113
+
114
+ export const usePostFeedback = () => {
115
+ return useMutation({
116
+ mutationFn: async (data: FeedbackRequestType) => {
117
+ const response = await postFeedback(data);
118
+ return response;
119
+ },
120
+ onSuccess: () => {},
121
+ onError: () => {},
122
+ });
123
+ };
124
+
125
+ export const useGetLogs = () => {
126
+ return useMutation({
127
+ mutationKey: ["logs"],
128
+ mutationFn: async (data: LogRequestType | undefined) => {
129
+ const response = await getLogs(data);
130
+ return response;
131
+ },
132
+ });
133
+ };
134
+
135
+ export const useGetAbbreviations = () => {
136
+ return useQuery({
137
+ queryKey: ["abbreviations"],
138
+ queryFn: async () => {
139
+ const response = await getAbbreviations();
140
+ return response;
141
+ },
142
+ });
143
+ };
src/api/predictions/predictionsApi.ts ADDED
@@ -0,0 +1,159 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { query } from "@/shared/api/query";
2
+
3
+ import axios from "axios";
4
+ import { parseDocumetnsInLlmAnswer } from "@/shared/utils/parseDocumnetsInLlmAnswer";
5
+ import {
6
+ ChunkRequestType,
7
+ ChunksResponseType,
8
+ ChunksArrayItem,
9
+ ChunkType,
10
+ LlmRequestType,
11
+ LlmResponseType,
12
+ FeedbackRequestType,
13
+ FeedbackResponseType,
14
+ LogRequestType,
15
+ LogResponseType,
16
+ AbbreviationsDataType,
17
+ AbbreviationsResponseDataType,
18
+ } from "./types";
19
+
20
+ export const getChunks = async (
21
+ data: ChunkRequestType,
22
+ signal: AbortSignal
23
+ ): Promise<{ chunks: ChunksResponseType; chunksArray: ChunksArrayItem[] }> => {
24
+ const response = await query<ChunksResponseType>({
25
+ url: "/chunks",
26
+ method: "post",
27
+ data,
28
+ signal,
29
+ });
30
+
31
+ if ("error" in response) {
32
+ if (axios.isCancel(response.error)) {
33
+ console.log("Запроса был отменен");
34
+ }
35
+ throw new Error(`Ошибка: ${response.error.status}`);
36
+ }
37
+ const chunksArray: ChunksArrayItem[] = [];
38
+
39
+ // Проверки на null добавлены для корретной сквозной нумерации результатов в случае,
40
+ // если чанков одного типа не пришло с бэка
41
+ if (response.data.doc_chunks !== null) {
42
+ response.data.doc_chunks?.forEach((element) => {
43
+ chunksArray.push({
44
+ type: ChunkType.doc_chunks,
45
+ number: chunksArray.length,
46
+ chunk: { doc_chunks: element },
47
+ });
48
+ });
49
+ }
50
+
51
+ if (response.data.people_search !== null) {
52
+ response.data.people_search?.forEach((element) => {
53
+ chunksArray.push({
54
+ type: ChunkType.people_search,
55
+ number: chunksArray.length,
56
+ chunk: { people_search: element },
57
+ });
58
+ });
59
+ }
60
+
61
+ if (response.data.groups_search !== null) {
62
+ chunksArray.push({
63
+ type: ChunkType.groups_search,
64
+ number: chunksArray.length,
65
+ chunk: { groups_search: response.data.groups_search },
66
+ });
67
+ }
68
+
69
+ if (response.data.rocks_nn_search !== null) {
70
+ chunksArray.push({
71
+ type: ChunkType.rocks_nn_search,
72
+ number: chunksArray.length,
73
+ chunk: { rocks_nn_search: response.data.rocks_nn_search },
74
+ });
75
+ }
76
+
77
+ if (response.data.segmentation_search !== null) {
78
+ chunksArray.push({
79
+ type: ChunkType.segmentation_search,
80
+ number: chunksArray.length,
81
+ chunk: { segmentation_search: response.data.segmentation_search },
82
+ });
83
+ }
84
+
85
+ return { chunks: response.data, chunksArray };
86
+ };
87
+
88
+ export const getLlmResponse = async (
89
+ data: LlmRequestType,
90
+ signal: AbortSignal
91
+ ): Promise<LlmResponseType> => {
92
+ const response = await query<LlmResponseType>({
93
+ url: "/answer_llm",
94
+ method: "post",
95
+ data: { query: data.query, chunks: data.chunks },
96
+ signal,
97
+ });
98
+
99
+ if ("error" in response) {
100
+ if (axios.isCancel(response.error)) {
101
+ console.log("Запроса был отменен");
102
+ }
103
+ throw new Error(`Ошибка: ${response.error.status}`);
104
+ }
105
+ const { matches, resultString } = parseDocumetnsInLlmAnswer(
106
+ response.data.answer_llm,
107
+ data.chunksArray
108
+ );
109
+
110
+ return {
111
+ answer_llm: resultString,
112
+ log_id: response.data.log_id,
113
+ documentsNumbers: matches,
114
+ };
115
+ };
116
+
117
+ export const postFeedback = async (
118
+ data: FeedbackRequestType
119
+ ): Promise<FeedbackResponseType> => {
120
+ const response = await query<FeedbackResponseType>({
121
+ url: "/feedback",
122
+ method: "post",
123
+ data,
124
+ });
125
+
126
+ if ("error" in response) {
127
+ throw new Error(`Ошибка: ${response.error.status}`);
128
+ }
129
+
130
+ return response.data;
131
+ };
132
+
133
+ export const getLogs = async (
134
+ data: LogRequestType | undefined
135
+ ): Promise<LogResponseType[]> => {
136
+ const response = await query<LogResponseType[]>({
137
+ url: "/logs",
138
+ method: "get",
139
+ params: data,
140
+ });
141
+ if ("error" in response) {
142
+ throw new Error(`Ошибка: ${response.error.status}`);
143
+ }
144
+
145
+ return response.data;
146
+ };
147
+
148
+ export const getAbbreviations = async (): Promise<AbbreviationsDataType> => {
149
+ const response = await query<AbbreviationsResponseDataType>({
150
+ url: "/collection/default",
151
+ method: "get",
152
+ });
153
+ if ("error" in response) {
154
+ throw new Error(`Ошибка: ${response.error.status}`);
155
+ }
156
+
157
+ return response.data.acronyms;
158
+ };
159
+
src/api/predictions/types.ts ADDED
@@ -0,0 +1,159 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export type UserScore = 1 | 2 | 3 | 4 | 5;
2
+ export interface DocumentDataType {
3
+ file_xml_name: string;
4
+ filename: string;
5
+ id: string | null;
6
+ title: string | null;
7
+ chunks: ChunkDataType[];
8
+ }
9
+ export interface ChunkDataType {
10
+ index_answer: number;
11
+ doc_name: string;
12
+ title: string;
13
+ text_answer: string;
14
+ other_info: string[];
15
+ start_index_paragraph: number;
16
+ }
17
+
18
+ export interface BusinessCuratorDataType {
19
+ division: string;
20
+ company_name: string;
21
+ }
22
+
23
+ export interface BusinessProcessDataType {
24
+ production_activities_section: string;
25
+ processes_name: string;
26
+ level_process: string;
27
+ }
28
+
29
+ export interface GroupDataType {
30
+ group_name: string;
31
+ position_in_group: string;
32
+ block: string;
33
+ }
34
+
35
+ export interface OrganizatinalStructureDataType {
36
+ position: string;
37
+ leads:
38
+ | {
39
+ person: string;
40
+ leads: string;
41
+ }[]
42
+ | null;
43
+ subordinates: {
44
+ person_name: string | undefined;
45
+ position: string | undefined;
46
+ } | null;
47
+ }
48
+ export interface PeopleDataType {
49
+ person_name: string;
50
+ business_curator: BusinessCuratorDataType[] | null;
51
+ business_processes: BusinessProcessDataType[] | null;
52
+ groups: GroupDataType[] | null;
53
+ organizatinal_structure: OrganizatinalStructureDataType[] | null;
54
+ }
55
+
56
+ export interface RocksNNDataType {
57
+ division: string;
58
+ company_name: string[];
59
+ }
60
+
61
+ export interface ChunksResponseType {
62
+ doc_chunks: DocumentDataType[] | null;
63
+ people_search: PeopleDataType[] | null;
64
+ groups_search: GroupSearchDataType | null;
65
+ rocks_nn_search: RocksNNDataType | null;
66
+ segmentation_search: SegmentationModelDataType | null;
67
+ }
68
+
69
+ export enum ChunkType {
70
+ doc_chunks = "doc_chunks",
71
+ people_search = "people_search",
72
+ groups_search = "groups_search",
73
+ rocks_nn_search = "rocks_nn_search",
74
+ segmentation_search = "segmentation_search",
75
+ }
76
+
77
+ export interface ChunksArrayItem {
78
+ type: ChunkType;
79
+ number: number;
80
+ chunk: Partial<{
81
+ doc_chunks: DocumentDataType | null;
82
+ people_search: PeopleDataType | null;
83
+ groups_search: GroupSearchDataType | null;
84
+ rocks_nn_search: RocksNNDataType | null;
85
+ segmentation_search: SegmentationModelDataType | null;
86
+ }>;
87
+ }
88
+
89
+ export interface LlmResponseType {
90
+ text_chunks?: string; //TODO: убрать, тк созданно для тестирования
91
+ documentsNumbers?: number[];
92
+ answer_llm: string;
93
+ log_id: number;
94
+ }
95
+ export interface ChunkRequestType {
96
+ userName: string;
97
+ query: string;
98
+ query_abbreviation: string;
99
+ abbreviations_replaced: string[];
100
+ }
101
+
102
+ export interface LlmRequestType {
103
+ query: ChunkRequestType;
104
+ chunks: ChunksResponseType;
105
+ chunksArray: ChunksArrayItem[];
106
+ }
107
+
108
+ export interface FeedbackRequestType {
109
+ log_id: number;
110
+ userComment: string;
111
+ userScore: UserScore;
112
+ manualEstimate: number;
113
+ llmEstimate: number;
114
+ }
115
+
116
+ export interface FeedbackResponseType extends FeedbackRequestType {
117
+ feedback_id: number;
118
+ }
119
+
120
+ export interface LogResponseType {
121
+ log_id: number;
122
+ llmPrompt: string;
123
+ llmResponse: string;
124
+ llm_classifier: string;
125
+ dateCreated: string;
126
+ userRequest: string;
127
+ feedback_id: number | null;
128
+ userComment: string | null;
129
+ userScore: UserScore | null;
130
+ }
131
+
132
+ export interface LogRequestType {
133
+ date_start?: string;
134
+ date_end?: string;
135
+ }
136
+
137
+ export interface GroupSearchDataType {
138
+ group_name: string;
139
+ group_composition: {
140
+ person_name: string;
141
+ position_in_group: string;
142
+ }[];
143
+ }
144
+
145
+ export interface AbbreviationsDataType {
146
+ [key: string]: string[];
147
+ }
148
+
149
+ export interface AbbreviationsResponseDataType {
150
+ collection_name: string;
151
+ collection_filename: string;
152
+ updated_at: string;
153
+ acronyms: AbbreviationsDataType;
154
+ }
155
+
156
+ export interface SegmentationModelDataType {
157
+ segmentation_model: string;
158
+ company_name: string[];
159
+ }
src/components/common/navbar/Navbar.scss ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .navbar {
2
+ position: fixed;
3
+ top: 0;
4
+ left: 0;
5
+ width: 100%;
6
+ background-color: #333;
7
+ padding: 10px 0;
8
+ z-index: 1000;
9
+
10
+ ul {
11
+ list-style: none;
12
+ display: flex;
13
+ justify-content: center;
14
+ margin: 0;
15
+ padding: 0;
16
+
17
+ li {
18
+ margin: 0 20px;
19
+
20
+ a {
21
+ color: white;
22
+ text-decoration: none;
23
+ font-size: 16px;
24
+
25
+ &.active {
26
+ font-weight: bold;
27
+ border-bottom: 2px solid white;
28
+ }
29
+
30
+ &:hover {
31
+ color: #ddd;
32
+ }
33
+ }
34
+ }
35
+ }
36
+ }
src/components/common/navbar/Navbar.tsx ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import { NavLink } from 'react-router-dom';
3
+ import './Navbar.scss';
4
+
5
+ const Navbar: React.FC = () => {
6
+ return (
7
+ <nav className="navbar">
8
+ <ul>
9
+ <li>
10
+ <NavLink
11
+ to="/logs"
12
+ className={({ isActive }) => (isActive ? 'active' : '')}
13
+ >
14
+ Логи
15
+ </NavLink>
16
+ </li>
17
+ <li>
18
+ <NavLink
19
+ to="/docs"
20
+ className={({ isActive }) => (isActive ? 'active' : '')}
21
+ >
22
+ Датасеты
23
+ </NavLink>
24
+ </li>
25
+ <li>
26
+ <NavLink
27
+ to="/llmconfig"
28
+ className={({ isActive }) => (isActive ? 'active' : '')}
29
+ >
30
+ Настройки LLM
31
+ </NavLink>
32
+ </li>
33
+ <li>
34
+ <NavLink
35
+ to="/llmprompt"
36
+ className={({ isActive }) => (isActive ? 'active' : '')}
37
+ >
38
+ Системные промпты
39
+ </NavLink>
40
+ </li>
41
+ </ul>
42
+ </nav>
43
+ );
44
+ };
45
+
46
+ export default Navbar;
src/components/generics/button/Button.interface.ts ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { ReactNode } from "react";
2
+
3
+ export type ButtonProps = {
4
+ name?: string;
5
+ onClick: () => void;
6
+ buttonType?: "primary" | "outlined" | "secondary" | "link";
7
+ disabled?: boolean;
8
+ loading?: boolean;
9
+ icon?: ReactNode;
10
+ className?: string;
11
+ } & React.DetailedHTMLProps<React.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>;
src/components/generics/button/Button.scss ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .btn {
2
+ border-radius: 15px;
3
+ font-size: 1rem;
4
+ line-height: 1.5rem;
5
+ padding-left: 1.25rem;
6
+ padding-right: 1.25rem;
7
+ padding-top: 9px;
8
+ padding-bottom: 9px;
9
+ font-family: inherit;
10
+ display: flex;
11
+ gap: 10px;
12
+ }
13
+
14
+ .btn:hover {
15
+ cursor: pointer;
16
+ background-color: var(--hover-bg-color);
17
+ }
18
+
19
+ .primary {
20
+ background-color: var(--primary-color);
21
+ color: var(--text-1-color);
22
+ outline: none;
23
+ border: 2px solid transparent;
24
+ }
25
+
26
+ .outlined {
27
+ border: 2px solid var(--primary-color);
28
+ color: var(--text-0-color);
29
+ background-color: transparent;
30
+ }
31
+
32
+ .secondary {
33
+ background-color: var(--secondary-color);
34
+ color: var(--white-color);
35
+ outline: none;
36
+ border: 2px solid transparent;
37
+
38
+ &:hover {
39
+ background-color: var(--purple-color);
40
+ }
41
+ }
42
+
43
+ .link {
44
+ text-align: left;
45
+ display: flex;
46
+ background: none !important;
47
+ border: none;
48
+ padding: 0 !important;
49
+ word-wrap: break-word;
50
+ margin: 0;
51
+ margin-bottom: 3px;
52
+ color: var(--purple-color);
53
+ font-weight: 400;
54
+ text-decoration: none;
55
+ cursor: pointer;
56
+
57
+ &:hover {
58
+ color: var(--secondary-color);
59
+ }
60
+ }
61
+
62
+ button:disabled {
63
+ background-color: light-dark(rgba(239, 239, 239, 0.3), rgba(19, 1, 1, 0.3));
64
+ color: light-dark(rgba(16, 16, 16, 0.3), rgba(255, 255, 255, 0.3));
65
+ &:hover {
66
+ cursor: default;
67
+ background-color: light-dark(rgba(239, 239, 239, 0.3), rgba(19, 1, 1, 0.3));
68
+ color: light-dark(rgba(16, 16, 16, 0.3), rgba(255, 255, 255, 0.3));
69
+ }
70
+ }
src/components/generics/button/Button.tsx ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import Spinner from "../spinner/Spinner";
2
+ import { ButtonProps } from "./Button.interface";
3
+ import "./Button.scss";
4
+
5
+ const Button = ({
6
+ name = "",
7
+ onClick,
8
+ buttonType = "primary",
9
+ disabled,
10
+ loading = false,
11
+ icon = null,
12
+ className,
13
+ }: ButtonProps) => {
14
+ return (
15
+ <button onClick={onClick} className={`btn ${buttonType} ${className}`} disabled={disabled}>
16
+ {name}
17
+ {icon}
18
+ {loading && <Spinner />}
19
+ </button>
20
+ );
21
+ };
22
+
23
+ export default Button;
src/components/generics/collapse/Collapse.inreface.ts ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ import { ReactNode } from "react";
2
+
3
+ interface CollapseProps {
4
+ name: string;
5
+ content: ReactNode;
6
+ }
7
+
8
+ export default CollapseProps;
src/components/generics/collapse/Collapse.scss ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .collapsible {
2
+ background-color: var(--white-color);
3
+ color: var(--text-2-color);
4
+ cursor: pointer;
5
+ padding: 10px;
6
+ width: 100%;
7
+ border: 2px solid var(--border-0-color);
8
+ text-align: left;
9
+ outline: none;
10
+ border-radius: 15px;
11
+ }
12
+
13
+ .collapsible:hover {
14
+ background-color: var(--primary-color);
15
+ }
16
+
17
+ .collapsible:not(:has(+ .hidden)) {
18
+ border-bottom-left-radius: 0;
19
+ border-bottom-right-radius: 0;
20
+ }
21
+
22
+ .content {
23
+ padding: 0 18px;
24
+ overflow: hidden;
25
+ display: block;
26
+ border-bottom-left-radius: 15px;
27
+ border-bottom-right-radius: 15px;
28
+ background-color: var(--bg-color);
29
+ }
30
+
31
+ .content.hidden {
32
+ display: none;
33
+ }
34
+
35
+ .collapsible:after {
36
+ content: "\02C5";
37
+ color: var(--text-0-color);
38
+ float: right;
39
+ margin-left: 5px;
40
+ }
src/components/generics/collapse/Collapse.tsx ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState } from "react";
2
+ import "./Collapse.scss";
3
+ import CollapseProps from "./Collapse.inreface";
4
+
5
+ const Collapse = ({ name, content }: CollapseProps) => {
6
+ const [collapsed, setCollapsed] = useState<boolean>(false);
7
+
8
+ return (
9
+ <>
10
+ <button
11
+ type="button"
12
+ className="collapsible"
13
+ onClick={() => setCollapsed(!collapsed)}
14
+ >
15
+ {name}
16
+ </button>
17
+ <div className={`content ${!collapsed && "hidden"}`}>{content}</div>
18
+ </>
19
+ );
20
+ };
21
+
22
+ export default Collapse;
src/components/generics/dropdown/Dropdown.interface.ts ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ export interface DropdownProps {
2
+ name: string;
3
+ content: string;
4
+ }
src/components/generics/dropdown/Dropdown.scss ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .dropdown {
2
+ position: relative;
3
+ display: inline-block;
4
+ }
5
+
6
+ .dropdown-content {
7
+ display: none;
8
+ position: absolute;
9
+ background-color: #f9f9f9;
10
+ box-shadow: 0px 8px 16px 0px rgba(0, 0, 0, 0.2);
11
+ padding: 10px;
12
+ z-index: 1;
13
+ border-radius: 15px;
14
+ p {
15
+ margin: 0;
16
+ }
17
+ }
18
+
19
+ .dropdown:hover .dropdown-content {
20
+ display: block;
21
+ }
src/components/generics/dropdown/Dropdown.tsx ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { DropdownProps } from "./Dropdown.interface";
2
+ import "./Dropdown.scss";
3
+
4
+ const Dropdown = ({ name, content }: DropdownProps) => {
5
+ return (
6
+ <div className="dropdown">
7
+ <span>{name}</span>
8
+ <div className="dropdown-content">
9
+ <p>{content}</p>
10
+ </div>
11
+ </div>
12
+ );
13
+ };
14
+ export default Dropdown;
src/components/generics/editable/Editable.interface.ts ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ import { MouseEventHandler } from "react";
2
+
3
+ export interface EditableProps {
4
+ content: string;
5
+ className?: string;
6
+ placeholder: string;
7
+ onContentChange: (content: string) => void;
8
+ handleClick?: MouseEventHandler<HTMLDivElement>;
9
+ disabled?: boolean;
10
+ }
src/components/generics/editable/Editable.scss ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .editable {
2
+ box-sizing: border-box;
3
+ width: 100%;
4
+ font-size: 16px;
5
+ border: 0;
6
+ border-radius: 15px;
7
+ resize: none;
8
+ overflow-y: auto;
9
+ min-height: 1rem;
10
+ padding: 16px;
11
+ background-color: var(--bg-color);
12
+ outline: none;
13
+ text-align: left;
14
+ }
15
+
16
+ .editable:empty::before {
17
+ content: attr(data-placeholder);
18
+ color: var(--text-2-color);
19
+ display: block;
20
+ pointer-events: none;
21
+ opacity: 0.7;
22
+ }
23
+
24
+ .with_options {
25
+ border-bottom-left-radius: 0;
26
+ border-bottom-right-radius: 0;
27
+ }
28
+
29
+ .with_shadow {
30
+ box-shadow: 0px 8px 16px 0px rgba(0, 0, 0, 0.2);
31
+ }
src/components/generics/editable/Editable.tsx ADDED
@@ -0,0 +1,112 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useRef, useEffect, useCallback, MouseEventHandler } from "react";
2
+ import ContentEditable, { ContentEditableEvent } from "react-contenteditable";
3
+ import "./Editable.scss";
4
+ import { EditableProps } from "./Editable.interface";
5
+
6
+ const Editable: React.FC<EditableProps> = ({
7
+ className,
8
+ placeholder,
9
+ content,
10
+ onContentChange,
11
+ handleClick: handleContentClick,
12
+ }) => {
13
+ const editableRef = useRef<HTMLElement>(null);
14
+ const lastCaretPosition = useRef<number>(0);
15
+
16
+ const getCaretPosition = (el: HTMLElement): number => {
17
+ let caretPosition = 0;
18
+ const selection = window.getSelection();
19
+
20
+ if (selection && selection.rangeCount > 0) {
21
+ const range = selection.getRangeAt(0);
22
+ const preRange = range.cloneRange();
23
+ preRange.selectNodeContents(el);
24
+ preRange.setEnd(range.endContainer, range.endOffset);
25
+ caretPosition = preRange.toString().length;
26
+ }
27
+
28
+ return caretPosition;
29
+ };
30
+
31
+ const setCaretPosition = useCallback((offset: number) => {
32
+ const selection = window.getSelection();
33
+ const range = document.createRange();
34
+ const el = editableRef.current;
35
+ let localOffset = offset;
36
+
37
+ el?.childNodes.forEach((element) => {
38
+ if (!element.textContent?.length) {
39
+ element.textContent = "";
40
+ }
41
+ if (localOffset <= element.textContent?.length && localOffset >= 0) {
42
+ range.setStart(element.childNodes[0] ?? element, localOffset);
43
+ range.collapse(true);
44
+ selection?.removeAllRanges();
45
+ selection?.addRange(range);
46
+ localOffset -= element.textContent?.length;
47
+ return;
48
+ } else if (localOffset >= 0) {
49
+ localOffset -= element.textContent?.length;
50
+ }
51
+ });
52
+ }, []);
53
+
54
+ const handleChange = (event: ContentEditableEvent) => {
55
+ const newText = event.currentTarget.innerText;
56
+ const currentCaretPosition = getCaretPosition(editableRef.current!);
57
+
58
+ if (newText.length <= 500) {
59
+ onContentChange(newText);
60
+ lastCaretPosition.current = currentCaretPosition;
61
+ } else {
62
+ const trimmedText = newText.substring(0, 500);
63
+ event.currentTarget.innerText = trimmedText;
64
+ onContentChange(trimmedText);
65
+ lastCaretPosition.current = currentCaretPosition;
66
+ setCaretPosition(500);
67
+ }
68
+ };
69
+
70
+ const handleClick: MouseEventHandler<HTMLDivElement> = (event) => {
71
+ lastCaretPosition.current = getCaretPosition(editableRef.current!);
72
+ if (handleContentClick) handleContentClick(event);
73
+ };
74
+
75
+ useEffect(() => {
76
+ const contentEditable = editableRef.current;
77
+
78
+ const observer = new MutationObserver((mutations) => {
79
+ mutations.forEach((mutation) => {
80
+ if (mutation.type === "childList") {
81
+ mutation.removedNodes.forEach((node) => {
82
+ if (node instanceof HTMLElement && node.tagName === "A") {
83
+ setCaretPosition(lastCaretPosition.current);
84
+ }
85
+ });
86
+ }
87
+ });
88
+ });
89
+
90
+ if (contentEditable) {
91
+ observer.observe(contentEditable, { childList: true, subtree: true });
92
+ }
93
+
94
+ return () => {
95
+ observer.disconnect();
96
+ };
97
+ }, [setCaretPosition]);
98
+
99
+ return (
100
+ <ContentEditable
101
+ innerRef={editableRef}
102
+ className={`editable ${className}`}
103
+ onChange={handleChange}
104
+ onBlur={() => {}}
105
+ html={content.replaceAll("@@@", "")}
106
+ data-placeholder={placeholder}
107
+ onClick={handleClick}
108
+ />
109
+ );
110
+ };
111
+
112
+ export default Editable;
src/components/generics/input/Input.interface.ts ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { DetailedHTMLProps, ReactNode } from "react";
2
+
3
+ export type InputProps = {
4
+ name: string;
5
+ label?: ReactNode;
6
+ placeholder?: string;
7
+ rules?: { rule: (value: string) => boolean; errorMessage: string }[];
8
+ value?: string;
9
+ onSetValue: (value: string) => void;
10
+ error?: string | null;
11
+ onSetError?: (value: string | null) => void;
12
+ extra?: ReactNode;
13
+ } & DetailedHTMLProps<React.InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>;
src/components/generics/input/Input.scss ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .input_container {
2
+ .with_icon_container {
3
+ display: flex;
4
+ align-items: center;
5
+ border-radius: 10px;
6
+ width: 100%;
7
+ padding: 0;
8
+ box-sizing: border-box;
9
+ border: 1px solid var(--border-1-color);
10
+ &:focus,
11
+ &:active {
12
+ border: 1px solid var(--border-1-color);
13
+ outline: none;
14
+ }
15
+ }
16
+ .input {
17
+ border: 0;
18
+ margin: 8px;
19
+ padding: 0;
20
+ width: 100%;
21
+ outline: none;
22
+ }
23
+
24
+ .error {
25
+ margin-top: 10px;
26
+ color: var(--error-color);
27
+ word-wrap: break-word;
28
+ }
29
+ }
30
+
31
+ input[type="radio"] {
32
+ accent-color: var(--secondary-color);
33
+ transform: scale(1.4);
34
+ }
src/components/generics/input/Input.tsx ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useEffect } from "react";
2
+ import { InputProps } from "./Input.interface";
3
+
4
+ import "./Input.scss";
5
+
6
+ const Input = ({
7
+ name,
8
+ placeholder,
9
+ rules,
10
+ value = "",
11
+ onSetValue,
12
+ label,
13
+ error,
14
+ onSetError,
15
+ extra,
16
+ ...props
17
+ }: InputProps) => {
18
+ const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
19
+ if (props.type == "number") {
20
+ const { value, max, min } = event.target;
21
+ if (Number(value) > Number(max)) {
22
+ onSetValue(max);
23
+ } else if (Number(value) > Number(min)) {
24
+ onSetValue(value);
25
+ } else {
26
+ onSetValue("");
27
+ }
28
+ } else {
29
+ onSetValue(event.target.value);
30
+ }
31
+ };
32
+
33
+ const handleKeyPress = (event: React.KeyboardEvent<HTMLInputElement>) => {
34
+ if (props.type === "number") {
35
+ if (!/[0-9]/.test(event.key) && event.key !== "Backspace") {
36
+ event.preventDefault();
37
+ }
38
+ }
39
+ };
40
+
41
+ useEffect(() => {
42
+ if (value.length > 0) {
43
+ rules?.some((rule) => {
44
+ if (!rule.rule(value)) {
45
+ // console.log(value);
46
+ onSetError?.(rule.errorMessage);
47
+ return true;
48
+ }
49
+ // console.log(value);
50
+ onSetError?.(null);
51
+
52
+ return false;
53
+ });
54
+ }
55
+ }, [onSetError, rules, value]);
56
+
57
+ return (
58
+ <div className={"input_container"}>
59
+ {label}
60
+ <div className="with_icon_container">
61
+ <input
62
+ className={"input"}
63
+ name={name}
64
+ placeholder={placeholder}
65
+ value={value}
66
+ onChange={handleChange}
67
+ onKeyPress={handleKeyPress}
68
+ {...props}
69
+ />
70
+ {extra}
71
+ </div>
72
+ {error && <div className={"error"}>{error}</div>}
73
+ </div>
74
+ );
75
+ };
76
+
77
+ export default Input;
src/components/generics/loading/Loading.tsx ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useEffect, useState } from "react";
2
+
3
+ interface LoadingProps {
4
+ loading: boolean;
5
+ }
6
+
7
+ const Loading: React.FC<LoadingProps> = ({ loading }) => {
8
+ const [dots, setDots] = useState<string>("");
9
+
10
+ useEffect(() => {
11
+ if (loading) {
12
+ const interval = setInterval(() => {
13
+ setDots((prev) => (prev.length < 3 ? prev + "." : ""));
14
+ }, 500);
15
+
16
+ return () => clearInterval(interval);
17
+ }
18
+ }, [loading]);
19
+
20
+ return <div className="loading">Загрузка{dots}</div>;
21
+ };
22
+
23
+ export default Loading;
src/components/generics/modal/Modal.interface.ts ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ export interface ModalWindowProps {
2
+ label?: string;
3
+ isOpen: boolean;
4
+ closeModal: () => void;
5
+ children: React.ReactNode;
6
+ }
src/components/generics/modal/Modal.scss ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ .modal-content {
2
+ padding: 0 10px 0 10px;
3
+ .label {
4
+ display: flex;
5
+ align-items: center;
6
+ justify-content: space-between;
7
+ gap: 10px;
8
+ }
9
+ }
src/components/generics/modal/Modal.tsx ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { FC } from "react";
2
+ import Modal from "react-modal";
3
+ import { GoX } from "react-icons/go";
4
+ import { ModalWindowProps } from "./Modal.interface";
5
+ import Button from "../button/Button";
6
+ import "./Modal.scss";
7
+
8
+ const customStyles = {
9
+ content: {
10
+ top: "40%",
11
+ left: "50%",
12
+ right: "auto",
13
+ bottom: "auto",
14
+ transform: "translate(-50%, -50%)",
15
+ borderRadius: "15px",
16
+ width: "600px",
17
+ },
18
+ };
19
+
20
+ const ModalWindow: FC<ModalWindowProps> = ({ isOpen, closeModal, children, label = "" }) => {
21
+ return (
22
+ <Modal style={customStyles} isOpen={isOpen} onRequestClose={closeModal}>
23
+ <div className="modal-content">
24
+ <div className="label">
25
+ <h3 className="name">{label}</h3>
26
+ <Button onClick={closeModal} icon={<GoX style={{ height: "25px", width: "25px" }} />} buttonType="link" />
27
+ </div>
28
+ {children}
29
+ </div>
30
+ </Modal>
31
+ );
32
+ };
33
+
34
+ export default ModalWindow;
src/components/generics/sizeChanger/SizeChanger.scss ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .resizable-container {
2
+ display: flex;
3
+ width: 100%;
4
+ margin-top: 10px;
5
+ overflow: hidden;
6
+ }
7
+
8
+ .left-pane {
9
+ min-width: 345px;
10
+ }
11
+
12
+ .right-pane {
13
+ min-width: 260px;
14
+ flex-grow: 1;
15
+ }
16
+
17
+ .resizer {
18
+ width: 2px;
19
+ background-color: var(--bg-color);
20
+ cursor: ew-resize;
21
+ position: relative;
22
+ margin: 10px;
23
+ transition: width 0.3s ease;
24
+ pointer-events: auto;
25
+ }
26
+
27
+ .resizer:hover {
28
+ width: 3px;
29
+ border: 2px solid var(--border-0-color);
30
+ background-color: var(--border-0-color);
31
+ }
src/components/generics/sizeChanger/SizeChanger.tsx ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState } from "react";
2
+ import "./SizeChanger.scss";
3
+ import { SizeChangerProps } from "./SizeChangerProps";
4
+
5
+ const SizeChanger = ({ left, right }: SizeChangerProps) => {
6
+ const [leftWidth, setLeftWidth] = useState(600); // Начальная ширина левого блока
7
+
8
+ const handleMouseDown = (e: React.MouseEvent) => {
9
+ e.preventDefault();
10
+
11
+ const startX = e.clientX;
12
+
13
+ const onMouseMove = (e: MouseEvent) => {
14
+ const newWidth = leftWidth + e.clientX - startX;
15
+ setLeftWidth(newWidth);
16
+ };
17
+
18
+ const onMouseUp = () => {
19
+ document.removeEventListener("mousemove", onMouseMove);
20
+ document.removeEventListener("mouseup", onMouseUp);
21
+ };
22
+
23
+ document.addEventListener("mousemove", onMouseMove);
24
+ document.addEventListener("mouseup", onMouseUp);
25
+ };
26
+
27
+ return (
28
+ <div className="resizable-container">
29
+ <div className="left-pane" style={{ width: `${leftWidth}px` }}>
30
+ {right}
31
+ </div>
32
+ <div className="resizer" onMouseDown={handleMouseDown} />
33
+ <div className="right-pane">{left}</div>
34
+ </div>
35
+ );
36
+ };
37
+
38
+ export default SizeChanger;
src/components/generics/sizeChanger/SizeChangerProps.ts ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ import { ReactNode } from "react";
2
+
3
+ export interface SizeChangerProps {
4
+ left: ReactNode;
5
+ right: ReactNode;
6
+ }
src/components/generics/spinner/Spinner.scss ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .spinner {
2
+ border: 3px solid rgba(122, 122, 122, 0.1);
3
+ border-left-color: var(--border-1-color);
4
+ border-radius: 50%;
5
+ width: 16px;
6
+ height: 16px;
7
+ animation: spin 1s linear infinite;
8
+ }
9
+
10
+ @keyframes spin {
11
+ to {
12
+ transform: rotate(360deg);
13
+ }
14
+ }
src/components/generics/spinner/Spinner.tsx ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ import "./Spinner.scss";
2
+
3
+ const Spinner = () => {
4
+ return <div className="spinner"></div>;
5
+ };
6
+
7
+ export default Spinner;