Spaces:
Sleeping
Sleeping
init
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- .dockerignore +6 -0
- .gitignore +24 -0
- Dockerfile +25 -0
- README.md +3 -3
- devops/nginx.default.conf +23 -0
- eslint.config.js +28 -0
- index.html +12 -0
- package-lock.json +0 -0
- package.json +49 -0
- public/pdf.worker.mjs +0 -0
- src/App.css +13 -0
- src/App.tsx +33 -0
- src/api/documents/documentsApi.ts +170 -0
- src/api/documents/hooks.ts +121 -0
- src/api/documents/types.ts +67 -0
- src/api/llmConfigs/hooks.ts +49 -0
- src/api/llmConfigs/llmConfigApi.ts +79 -0
- src/api/llmConfigs/types.ts +14 -0
- src/api/llmPrompts/hooks.ts +49 -0
- src/api/llmPrompts/llmPromptApi.ts +79 -0
- src/api/llmPrompts/types.ts +9 -0
- src/api/predictions/hooks.ts +143 -0
- src/api/predictions/predictionsApi.ts +159 -0
- src/api/predictions/types.ts +159 -0
- src/components/common/navbar/Navbar.scss +36 -0
- src/components/common/navbar/Navbar.tsx +46 -0
- src/components/generics/button/Button.interface.ts +11 -0
- src/components/generics/button/Button.scss +70 -0
- src/components/generics/button/Button.tsx +23 -0
- src/components/generics/collapse/Collapse.inreface.ts +8 -0
- src/components/generics/collapse/Collapse.scss +40 -0
- src/components/generics/collapse/Collapse.tsx +22 -0
- src/components/generics/dropdown/Dropdown.interface.ts +4 -0
- src/components/generics/dropdown/Dropdown.scss +21 -0
- src/components/generics/dropdown/Dropdown.tsx +14 -0
- src/components/generics/editable/Editable.interface.ts +10 -0
- src/components/generics/editable/Editable.scss +31 -0
- src/components/generics/editable/Editable.tsx +112 -0
- src/components/generics/input/Input.interface.ts +13 -0
- src/components/generics/input/Input.scss +34 -0
- src/components/generics/input/Input.tsx +77 -0
- src/components/generics/loading/Loading.tsx +23 -0
- src/components/generics/modal/Modal.interface.ts +6 -0
- src/components/generics/modal/Modal.scss +9 -0
- src/components/generics/modal/Modal.tsx +34 -0
- src/components/generics/sizeChanger/SizeChanger.scss +31 -0
- src/components/generics/sizeChanger/SizeChanger.tsx +38 -0
- src/components/generics/sizeChanger/SizeChangerProps.ts +6 -0
- src/components/generics/spinner/Spinner.scss +14 -0
- 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:
|
5 |
-
colorTo:
|
6 |
sdk: docker
|
7 |
-
pinned:
|
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;
|