Compare commits
11 Commits
8b17bd6d44
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6dc977134b | ||
|
|
3f1ccfdbfe | ||
|
|
3928cbcd61 | ||
|
|
2eafc574b6 | ||
|
|
0870bbcba3 | ||
| e7dab371f3 | |||
|
|
ef99f5de39 | ||
|
|
a3b9ce9985 | ||
|
|
0c81c88228 | ||
|
|
435b7f3fca | ||
|
|
11082d93a5 |
25
.eslintrc.js
25
.eslintrc.js
@@ -1,25 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
parser: '@typescript-eslint/parser',
|
|
||||||
parserOptions: {
|
|
||||||
project: 'tsconfig.json',
|
|
||||||
tsconfigRootDir: __dirname,
|
|
||||||
sourceType: 'module',
|
|
||||||
},
|
|
||||||
plugins: ['@typescript-eslint/eslint-plugin'],
|
|
||||||
extends: [
|
|
||||||
'plugin:@typescript-eslint/recommended',
|
|
||||||
'plugin:prettier/recommended',
|
|
||||||
],
|
|
||||||
root: true,
|
|
||||||
env: {
|
|
||||||
node: true,
|
|
||||||
jest: true,
|
|
||||||
},
|
|
||||||
ignorePatterns: ['.eslintrc.js'],
|
|
||||||
rules: {
|
|
||||||
'@typescript-eslint/interface-name-prefix': 'off',
|
|
||||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
|
||||||
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
|
||||||
'@typescript-eslint/no-explicit-any': 'off',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
71
.gitignore
vendored
71
.gitignore
vendored
@@ -1,56 +1,21 @@
|
|||||||
# compiled output
|
/uploads
|
||||||
/dist
|
/__pycache__
|
||||||
/node_modules
|
/myenv
|
||||||
/build
|
imageboard.db
|
||||||
|
**/node_modules
|
||||||
# Logs
|
**/venv
|
||||||
logs
|
**/.git
|
||||||
|
**/.github
|
||||||
|
**/logs
|
||||||
|
**/dist
|
||||||
|
**/build
|
||||||
*.log
|
*.log
|
||||||
npm-debug.log*
|
*.md
|
||||||
pnpm-debug.log*
|
|
||||||
yarn-debug.log*
|
|
||||||
yarn-error.log*
|
|
||||||
lerna-debug.log*
|
|
||||||
|
|
||||||
# OS
|
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
# Tests
|
|
||||||
/coverage
|
|
||||||
/.nyc_output
|
|
||||||
|
|
||||||
# IDEs and editors
|
|
||||||
/.idea
|
|
||||||
.project
|
|
||||||
.classpath
|
|
||||||
.c9/
|
|
||||||
*.launch
|
|
||||||
.settings/
|
|
||||||
*.sublime-workspace
|
|
||||||
|
|
||||||
# IDE - VSCode
|
|
||||||
.vscode/*
|
|
||||||
!.vscode/settings.json
|
|
||||||
!.vscode/tasks.json
|
|
||||||
!.vscode/launch.json
|
|
||||||
!.vscode/extensions.json
|
|
||||||
|
|
||||||
# dotenv environment variable files
|
|
||||||
.env
|
.env
|
||||||
.env.development.local
|
**/__pycache__
|
||||||
.env.test.local
|
*.sqlite3
|
||||||
.env.production.local
|
*.pyc
|
||||||
.env.local
|
*.pyo
|
||||||
|
*.pyd
|
||||||
# temp directory
|
.Python
|
||||||
.temp
|
|
||||||
.tmp
|
|
||||||
|
|
||||||
# Runtime data
|
|
||||||
pids
|
|
||||||
*.pid
|
|
||||||
*.seed
|
|
||||||
*.pid.lock
|
|
||||||
|
|
||||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
|
||||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
{
|
|
||||||
"singleQuote": true,
|
|
||||||
"trailingComma": "all"
|
|
||||||
}
|
|
||||||
31
Dockerfile
31
Dockerfile
@@ -1,23 +1,18 @@
|
|||||||
# Use the official Node.js image
|
# Используем официальный образ Python
|
||||||
FROM node:18
|
FROM python:3.9-slim
|
||||||
|
|
||||||
# Set the working directory
|
# Устанавливаем системные зависимости
|
||||||
|
RUN apt-get update && \
|
||||||
|
apt-get install -y --no-install-recommends gcc python3-dev && \
|
||||||
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Рабочая директория
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Copy package.json and package-lock.json
|
# Копируем весь код и устанавливаем зависимости
|
||||||
COPY package*.json ./
|
COPY requirements.txt .
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
# Install dependencies
|
|
||||||
RUN npm install
|
|
||||||
|
|
||||||
# Copy the rest of the application code
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
# Build the application
|
# Команда для запуска
|
||||||
RUN npm run build
|
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||||
|
|
||||||
# Expose the port the app runs on
|
|
||||||
EXPOSE 3000
|
|
||||||
|
|
||||||
# Start the application
|
|
||||||
CMD ["npm", "run", "start:dev"]
|
|
||||||
|
|||||||
74
README.md
74
README.md
@@ -1,73 +1 @@
|
|||||||
<p align="center">
|
docker-compose up --build
|
||||||
<a href="http://nestjs.com/" target="blank"><img src="https://nestjs.com/img/logo-small.svg" width="200" alt="Nest Logo" /></a>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
[circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456
|
|
||||||
[circleci-url]: https://circleci.com/gh/nestjs/nest
|
|
||||||
|
|
||||||
<p align="center">A progressive <a href="http://nodejs.org" target="_blank">Node.js</a> framework for building efficient and scalable server-side applications.</p>
|
|
||||||
<p align="center">
|
|
||||||
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/v/@nestjs/core.svg" alt="NPM Version" /></a>
|
|
||||||
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/l/@nestjs/core.svg" alt="Package License" /></a>
|
|
||||||
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/dm/@nestjs/common.svg" alt="NPM Downloads" /></a>
|
|
||||||
<a href="https://circleci.com/gh/nestjs/nest" target="_blank"><img src="https://img.shields.io/circleci/build/github/nestjs/nest/master" alt="CircleCI" /></a>
|
|
||||||
<a href="https://coveralls.io/github/nestjs/nest?branch=master" target="_blank"><img src="https://coveralls.io/repos/github/nestjs/nest/badge.svg?branch=master#9" alt="Coverage" /></a>
|
|
||||||
<a href="https://discord.gg/G7Qnnhy" target="_blank"><img src="https://img.shields.io/badge/discord-online-brightgreen.svg" alt="Discord"/></a>
|
|
||||||
<a href="https://opencollective.com/nest#backer" target="_blank"><img src="https://opencollective.com/nest/backers/badge.svg" alt="Backers on Open Collective" /></a>
|
|
||||||
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://opencollective.com/nest/sponsors/badge.svg" alt="Sponsors on Open Collective" /></a>
|
|
||||||
<a href="https://paypal.me/kamilmysliwiec" target="_blank"><img src="https://img.shields.io/badge/Donate-PayPal-ff3f59.svg"/></a>
|
|
||||||
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://img.shields.io/badge/Support%20us-Open%20Collective-41B883.svg" alt="Support us"></a>
|
|
||||||
<a href="https://twitter.com/nestframework" target="_blank"><img src="https://img.shields.io/twitter/follow/nestframework.svg?style=social&label=Follow"></a>
|
|
||||||
</p>
|
|
||||||
<!--[](https://opencollective.com/nest#backer)
|
|
||||||
[](https://opencollective.com/nest#sponsor)-->
|
|
||||||
|
|
||||||
## Description
|
|
||||||
|
|
||||||
[Nest](https://github.com/nestjs/nest) framework TypeScript starter repository.
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
```bash
|
|
||||||
$ npm install
|
|
||||||
```
|
|
||||||
|
|
||||||
## Running the app
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# development
|
|
||||||
$ npm run start
|
|
||||||
|
|
||||||
# watch mode
|
|
||||||
$ npm run start:dev
|
|
||||||
|
|
||||||
# production mode
|
|
||||||
$ npm run start:prod
|
|
||||||
```
|
|
||||||
|
|
||||||
## Test
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# unit tests
|
|
||||||
$ npm run test
|
|
||||||
|
|
||||||
# e2e tests
|
|
||||||
$ npm run test:e2e
|
|
||||||
|
|
||||||
# test coverage
|
|
||||||
$ npm run test:cov
|
|
||||||
```
|
|
||||||
|
|
||||||
## Support
|
|
||||||
|
|
||||||
Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support).
|
|
||||||
|
|
||||||
## Stay in touch
|
|
||||||
|
|
||||||
- Author - [Kamil Myśliwiec](https://kamilmysliwiec.com)
|
|
||||||
- Website - [https://nestjs.com](https://nestjs.com/)
|
|
||||||
- Twitter - [@nestframework](https://twitter.com/nestframework)
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
Nest is [MIT licensed](LICENSE).
|
|
||||||
@@ -1,34 +1,58 @@
|
|||||||
version: '3.8'
|
version: "3.8"
|
||||||
services:
|
|
||||||
postgres:
|
|
||||||
image: postgres:latest
|
|
||||||
container_name: postgres
|
|
||||||
environment:
|
|
||||||
POSTGRES_USER: user
|
|
||||||
POSTGRES_PASSWORD: password
|
|
||||||
POSTGRES_DB: mydatabase
|
|
||||||
ports:
|
|
||||||
- "5432:5432"
|
|
||||||
volumes:
|
|
||||||
- postgres_data:/var/lib/postgresql/data
|
|
||||||
networks:
|
|
||||||
- mynetwork
|
|
||||||
|
|
||||||
app:
|
services:
|
||||||
build: .
|
qdrant:
|
||||||
container_name: app
|
build:
|
||||||
|
context: ./qdrant
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: qdrant
|
||||||
|
ports:
|
||||||
|
- "6333:6333"
|
||||||
|
- "6334:6334"
|
||||||
|
volumes:
|
||||||
|
- qdrant_data:/qdrant/storage
|
||||||
|
restart: unless-stopped
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "curl", "-f", "http://localhost:6333/collections"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
backend:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: backend
|
||||||
|
ports:
|
||||||
|
- "8000:8000"
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
qdrant:
|
||||||
|
condition: service_healthy
|
||||||
|
environment:
|
||||||
|
- QDRANT_HOST=qdrant
|
||||||
|
- QDRANT_PORT=6333
|
||||||
|
|
||||||
|
futa-clone:
|
||||||
|
build:
|
||||||
|
context: ./futa-clone
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: futa-clone
|
||||||
ports:
|
ports:
|
||||||
- "3000:3000"
|
- "3000:3000"
|
||||||
environment:
|
restart: unless-stopped
|
||||||
DATABASE_URL: "postgresql://user:password@postgres:5432/mydatabase"
|
|
||||||
depends_on:
|
|
||||||
- postgres
|
|
||||||
networks:
|
|
||||||
- mynetwork
|
|
||||||
|
|
||||||
networks:
|
ollama:
|
||||||
mynetwork:
|
build:
|
||||||
driver: bridge
|
context: ./ollama
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: ollama
|
||||||
|
ports:
|
||||||
|
- "11434:11434"
|
||||||
|
volumes:
|
||||||
|
- ollama_data:/root/.ollama
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
postgres_data:
|
qdrant_data:
|
||||||
|
ollama_data:
|
||||||
|
|||||||
23
futa-clone/.gitignore
vendored
Normal file
23
futa-clone/.gitignore
vendored
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
/node_modules
|
||||||
|
/.pnp
|
||||||
|
.pnp.js
|
||||||
|
|
||||||
|
# testing
|
||||||
|
/coverage
|
||||||
|
|
||||||
|
# production
|
||||||
|
/build
|
||||||
|
|
||||||
|
# misc
|
||||||
|
.DS_Store
|
||||||
|
.env.local
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
18
futa-clone/Dockerfile
Normal file
18
futa-clone/Dockerfile
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# Используем официальный Node.js образ (на примере версии 16 на базе Alpine)
|
||||||
|
FROM node:20-alpine
|
||||||
|
|
||||||
|
# Устанавливаем рабочую директорию внутри контейнера
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Копируем файлы зависимостей и устанавливаем их
|
||||||
|
COPY package*.json ./
|
||||||
|
RUN npm install
|
||||||
|
|
||||||
|
# Копируем исходный код приложения
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Если требуется, можно указать порт (например, 3000)
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
# Запускаем приложение
|
||||||
|
CMD ["npm", "run", "start"]
|
||||||
70
futa-clone/README.md
Normal file
70
futa-clone/README.md
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
# Getting Started with Create React App
|
||||||
|
|
||||||
|
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
|
||||||
|
|
||||||
|
## Available Scripts
|
||||||
|
|
||||||
|
In the project directory, you can run:
|
||||||
|
|
||||||
|
### `npm start`
|
||||||
|
|
||||||
|
Runs the app in the development mode.\
|
||||||
|
Open [http://localhost:3000](http://localhost:3000) to view it in your browser.
|
||||||
|
|
||||||
|
The page will reload when you make changes.\
|
||||||
|
You may also see any lint errors in the console.
|
||||||
|
|
||||||
|
### `npm test`
|
||||||
|
|
||||||
|
Launches the test runner in the interactive watch mode.\
|
||||||
|
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
|
||||||
|
|
||||||
|
### `npm run build`
|
||||||
|
|
||||||
|
Builds the app for production to the `build` folder.\
|
||||||
|
It correctly bundles React in production mode and optimizes the build for the best performance.
|
||||||
|
|
||||||
|
The build is minified and the filenames include the hashes.\
|
||||||
|
Your app is ready to be deployed!
|
||||||
|
|
||||||
|
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
|
||||||
|
|
||||||
|
### `npm run eject`
|
||||||
|
|
||||||
|
**Note: this is a one-way operation. Once you `eject`, you can't go back!**
|
||||||
|
|
||||||
|
If you aren't satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
|
||||||
|
|
||||||
|
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you're on your own.
|
||||||
|
|
||||||
|
You don't have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn't feel obligated to use this feature. However we understand that this tool wouldn't be useful if you couldn't customize it when you are ready for it.
|
||||||
|
|
||||||
|
## Learn More
|
||||||
|
|
||||||
|
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
|
||||||
|
|
||||||
|
To learn React, check out the [React documentation](https://reactjs.org/).
|
||||||
|
|
||||||
|
### Code Splitting
|
||||||
|
|
||||||
|
This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting)
|
||||||
|
|
||||||
|
### Analyzing the Bundle Size
|
||||||
|
|
||||||
|
This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size)
|
||||||
|
|
||||||
|
### Making a Progressive Web App
|
||||||
|
|
||||||
|
This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app)
|
||||||
|
|
||||||
|
### Advanced Configuration
|
||||||
|
|
||||||
|
This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration)
|
||||||
|
|
||||||
|
### Deployment
|
||||||
|
|
||||||
|
This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment)
|
||||||
|
|
||||||
|
### `npm run build` fails to minify
|
||||||
|
|
||||||
|
This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify)
|
||||||
18135
futa-clone/package-lock.json
generated
Normal file
18135
futa-clone/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
44
futa-clone/package.json
Normal file
44
futa-clone/package.json
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
{
|
||||||
|
"name": "futa-clone",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@emotion/react": "^11.14.0",
|
||||||
|
"@emotion/styled": "^11.14.0",
|
||||||
|
"@mui/icons-material": "^6.4.4",
|
||||||
|
"@mui/material": "^6.4.4",
|
||||||
|
"cra-template": "1.2.0",
|
||||||
|
"formik": "^2.4.6",
|
||||||
|
"react": "^19.0.0",
|
||||||
|
"react-dom": "^19.0.0",
|
||||||
|
"react-router-dom": "^7.1.5",
|
||||||
|
"react-scripts": "5.0.1",
|
||||||
|
"vis-network": "^9.1.9",
|
||||||
|
"web-vitals": "^4.2.4",
|
||||||
|
"yup": "^1.6.1"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"start": "react-scripts start",
|
||||||
|
"build": "react-scripts build",
|
||||||
|
"test": "react-scripts test",
|
||||||
|
"eject": "react-scripts eject"
|
||||||
|
},
|
||||||
|
"eslintConfig": {
|
||||||
|
"extends": [
|
||||||
|
"react-app",
|
||||||
|
"react-app/jest"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"browserslist": {
|
||||||
|
"production": [
|
||||||
|
">0.2%",
|
||||||
|
"not dead",
|
||||||
|
"not op_mini all"
|
||||||
|
],
|
||||||
|
"development": [
|
||||||
|
"last 1 chrome version",
|
||||||
|
"last 1 firefox version",
|
||||||
|
"last 1 safari version"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
futa-clone/public/favicon.ico
Normal file
BIN
futa-clone/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.8 KiB |
43
futa-clone/public/index.html
Normal file
43
futa-clone/public/index.html
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<meta name="theme-color" content="#000000" />
|
||||||
|
<meta
|
||||||
|
name="description"
|
||||||
|
content="Web site created using create-react-app"
|
||||||
|
/>
|
||||||
|
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||||
|
<!--
|
||||||
|
manifest.json provides metadata used when your web app is installed on a
|
||||||
|
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||||
|
-->
|
||||||
|
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||||
|
<!--
|
||||||
|
Notice the use of %PUBLIC_URL% in the tags above.
|
||||||
|
It will be replaced with the URL of the `public` folder during the build.
|
||||||
|
Only files inside the `public` folder can be referenced from the HTML.
|
||||||
|
|
||||||
|
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||||
|
work correctly both with client-side routing and a non-root public URL.
|
||||||
|
Learn how to configure a non-root public URL by running `npm run build`.
|
||||||
|
-->
|
||||||
|
<title>React App</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
|
<div id="root"></div>
|
||||||
|
<!--
|
||||||
|
This HTML file is a template.
|
||||||
|
If you open it directly in the browser, you will see an empty page.
|
||||||
|
|
||||||
|
You can add webfonts, meta tags, or analytics to this file.
|
||||||
|
The build step will place the bundled scripts into the <body> tag.
|
||||||
|
|
||||||
|
To begin the development, run `npm start` or `yarn start`.
|
||||||
|
To create a production bundle, use `npm run build` or `yarn build`.
|
||||||
|
-->
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
BIN
futa-clone/public/logo192.png
Normal file
BIN
futa-clone/public/logo192.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.2 KiB |
BIN
futa-clone/public/logo512.png
Normal file
BIN
futa-clone/public/logo512.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.4 KiB |
25
futa-clone/public/manifest.json
Normal file
25
futa-clone/public/manifest.json
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"short_name": "React App",
|
||||||
|
"name": "Create React App Sample",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "favicon.ico",
|
||||||
|
"sizes": "64x64 32x32 24x24 16x16",
|
||||||
|
"type": "image/x-icon"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "logo192.png",
|
||||||
|
"type": "image/png",
|
||||||
|
"sizes": "192x192"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "logo512.png",
|
||||||
|
"type": "image/png",
|
||||||
|
"sizes": "512x512"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"start_url": ".",
|
||||||
|
"display": "standalone",
|
||||||
|
"theme_color": "#000000",
|
||||||
|
"background_color": "#ffffff"
|
||||||
|
}
|
||||||
3
futa-clone/public/robots.txt
Normal file
3
futa-clone/public/robots.txt
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# https://www.robotstxt.org/robotstxt.html
|
||||||
|
User-agent: *
|
||||||
|
Disallow:
|
||||||
162
futa-clone/src/App.css
Normal file
162
futa-clone/src/App.css
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
/* App.css */
|
||||||
|
/*#network {
|
||||||
|
width: 100%;
|
||||||
|
height: 600px;
|
||||||
|
border: 1px solid #800;
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hidden { display: none; }
|
||||||
|
|
||||||
|
#greeting {
|
||||||
|
text-align: center;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.graph-container {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-color: #F0E0D6;
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
margin: 20px;
|
||||||
|
max-width: 800px;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-form {
|
||||||
|
background: #FFF;
|
||||||
|
border: 1px solid #800;
|
||||||
|
padding: 15px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post {
|
||||||
|
background: #FFF;
|
||||||
|
border: 1px solid #800;
|
||||||
|
padding: 15px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post img {
|
||||||
|
max-width: 200px;
|
||||||
|
max-height: 200px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input, textarea, button {
|
||||||
|
margin: 5px;
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-image {
|
||||||
|
max-width: 150px;
|
||||||
|
margin: 10px 0;
|
||||||
|
}*/
|
||||||
|
|
||||||
|
/* App.css */
|
||||||
|
#network {
|
||||||
|
width: 100%;
|
||||||
|
height: 600px;
|
||||||
|
border: 1px solid rgba(54, 102, 153, 0.3); /* Приглушенный синий с прозрачностью */
|
||||||
|
background: rgba(245, 249, 252, 0.9); /* Светлый голубовато-белый фон */
|
||||||
|
backdrop-filter: blur(2px);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hidden { display: none; }
|
||||||
|
|
||||||
|
#greeting {
|
||||||
|
text-align: center;
|
||||||
|
padding: 20px;
|
||||||
|
color: #2c3e50; /* Темно-синий для контраста */
|
||||||
|
text-shadow: 1px 1px 2px rgba(255, 255, 255, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.graph-container {
|
||||||
|
margin-top: 20px;
|
||||||
|
background: rgba(245, 249, 252, 0.8);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-color: #f5f9fc; /* Основной фоновый цвет - холодный бело-голубой */
|
||||||
|
font-family: 'Segoe UI', Arial, sans-serif;
|
||||||
|
margin: 20px;
|
||||||
|
max-width: 800px;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-form {
|
||||||
|
background: rgba(255, 255, 255, 0.95); /* Полупрозрачный белый */
|
||||||
|
border: 1px solid rgba(54, 102, 153, 0.2);
|
||||||
|
padding: 15px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
border-radius: 6px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post {
|
||||||
|
background: rgba(255, 255, 255, 0.95); /* Полупрозрачный белый */
|
||||||
|
border: 1px solid rgba(54, 102, 153, 0.15);
|
||||||
|
padding: 15px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
border-radius: 6px;
|
||||||
|
backdrop-filter: blur(3px);
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.post img {
|
||||||
|
max-width: 200px;
|
||||||
|
max-height: 200px;
|
||||||
|
margin-top: 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid rgba(54, 102, 153, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
input, textarea {
|
||||||
|
margin: 5px;
|
||||||
|
padding: 8px;
|
||||||
|
border: 1px solid rgba(54, 102, 153, 0.2);
|
||||||
|
border-radius: 4px;
|
||||||
|
background: rgba(255, 255, 255, 0.9);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:focus, textarea:focus {
|
||||||
|
outline: none;
|
||||||
|
box-shadow: 0 0 0 2px rgba(54, 102, 153, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
margin: 5px;
|
||||||
|
padding: 8px 16px;
|
||||||
|
background: rgba(54, 102, 153, 0.85); /* Полупрозрачный синий */
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
background: rgba(54, 102, 153, 1);
|
||||||
|
transform: scale(1.02);
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-image {
|
||||||
|
max-width: 150px;
|
||||||
|
margin: 10px 0;
|
||||||
|
border-radius: 4px;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
38
futa-clone/src/App.js
Normal file
38
futa-clone/src/App.js
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import { ThemeProvider, createTheme } from '@mui/material/styles';
|
||||||
|
import CssBaseline from '@mui/material/CssBaseline';
|
||||||
|
import logo from './logo.svg';
|
||||||
|
import './App.css';
|
||||||
|
|
||||||
|
const theme = createTheme({
|
||||||
|
palette: {
|
||||||
|
primary: {
|
||||||
|
main: '#1976d2',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
return (
|
||||||
|
<ThemeProvider theme={theme}>
|
||||||
|
<CssBaseline />
|
||||||
|
<div className="App">
|
||||||
|
<header className="App-header">
|
||||||
|
<img src={logo} className="App-logo" alt="logo" />
|
||||||
|
<p>
|
||||||
|
Edit <code>src/App.js</code> and save to reload.
|
||||||
|
</p>
|
||||||
|
<a
|
||||||
|
className="App-link"
|
||||||
|
href="https://reactjs.org"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
Learn React
|
||||||
|
</a>
|
||||||
|
</header>
|
||||||
|
</div>
|
||||||
|
</ThemeProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
||||||
8
futa-clone/src/App.test.js
Normal file
8
futa-clone/src/App.test.js
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { render, screen } from '@testing-library/react';
|
||||||
|
import App from './App';
|
||||||
|
|
||||||
|
test('renders learn react link', () => {
|
||||||
|
render(<App />);
|
||||||
|
const linkElement = screen.getByText(/learn react/i);
|
||||||
|
expect(linkElement).toBeInTheDocument();
|
||||||
|
});
|
||||||
52
futa-clone/src/components/App.jsx
Normal file
52
futa-clone/src/components/App.jsx
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import PostForm from './PostForm';
|
||||||
|
import SearchForm from './SearchForm';
|
||||||
|
import PostList from './PostList';
|
||||||
|
import Greeting from './Greeting';
|
||||||
|
import GraphContainer from './GraphContainer';
|
||||||
|
import { API_URL } from '../config';
|
||||||
|
import '../App.css';
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
const [posts, setPosts] = useState([]);
|
||||||
|
const [searchResults, setSearchResults] = useState([]);
|
||||||
|
const [userVector, setUserVector] = useState(
|
||||||
|
JSON.parse(localStorage.getItem('userVector')) || null
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadPosts();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const loadPosts = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_URL}/posts/`);
|
||||||
|
const data = await response.json();
|
||||||
|
setPosts(data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading posts:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="app">
|
||||||
|
<Greeting show={!userVector} />
|
||||||
|
|
||||||
|
{userVector && <GraphContainer vector={userVector} />}
|
||||||
|
|
||||||
|
<PostForm
|
||||||
|
onPostCreated={(vector) => {
|
||||||
|
localStorage.setItem('userVector', JSON.stringify(vector));
|
||||||
|
setUserVector(vector);
|
||||||
|
loadPosts();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SearchForm onSearch={setSearchResults} />
|
||||||
|
|
||||||
|
<PostList posts={searchResults.length > 0 ? searchResults : posts} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
||||||
94
futa-clone/src/components/GraphContainer.jsx
Normal file
94
futa-clone/src/components/GraphContainer.jsx
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
import { useState, useEffect, useRef } from 'react';
|
||||||
|
import vis from 'vis-network/standalone/umd/vis-network.min';
|
||||||
|
import { API_URL } from '../config';
|
||||||
|
|
||||||
|
function GraphContainer({ vector }) {
|
||||||
|
const [posts, setPosts] = useState([]);
|
||||||
|
const networkRef = useRef(null);
|
||||||
|
const containerRef = useRef(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const loadTree = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_URL}/posts/tree`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ vector }),
|
||||||
|
});
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
// Добавлен лог полученных данных
|
||||||
|
console.log('Данные, полученные с сервера:', data);
|
||||||
|
|
||||||
|
setPosts(data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Tree load error:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
loadTree();
|
||||||
|
}, [vector]);
|
||||||
|
|
||||||
|
// Остальной код без изменений
|
||||||
|
useEffect(() => {
|
||||||
|
if (posts.length > 0 && containerRef.current) {
|
||||||
|
const { nodes, edges } = createGraphData(posts);
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
nodes: {
|
||||||
|
borderWidth: 2,
|
||||||
|
color: { border: '#42A5F5', background: '#F0E0D6' },
|
||||||
|
font: { size: 14 }
|
||||||
|
},
|
||||||
|
edges: {
|
||||||
|
color: '#42A5F5',
|
||||||
|
width: 2,
|
||||||
|
smooth: { type: 'curvedCW' },
|
||||||
|
arrows: 'none'
|
||||||
|
},
|
||||||
|
physics: {
|
||||||
|
stabilization: true,
|
||||||
|
barnesHut: { gravitationalConstant: -2000, springLength: 200 }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
networkRef.current = new vis.Network(
|
||||||
|
containerRef.current,
|
||||||
|
{ nodes, edges },
|
||||||
|
options
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (networkRef.current) {
|
||||||
|
networkRef.current.destroy();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [posts]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="graph-container">
|
||||||
|
<h2>Ризома мыслительная</h2>
|
||||||
|
<div ref={containerRef} style={{ width: '100%', height: '600px', border: '0px solid #800' }} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createGraphData(posts) {
|
||||||
|
const nodes = posts.map(post => ({
|
||||||
|
id: post.id,
|
||||||
|
label: post.text || '[Изображение]',
|
||||||
|
image: post.image ? `${API_URL}/${post.image}` : 'https://via.placeholder.com/100',
|
||||||
|
shape: 'circularImage',
|
||||||
|
size: 25
|
||||||
|
}));
|
||||||
|
|
||||||
|
const edges = posts.slice(1).map((post, index) => ({
|
||||||
|
from: posts[index].id,
|
||||||
|
to: post.id
|
||||||
|
}));
|
||||||
|
|
||||||
|
return { nodes, edges };
|
||||||
|
}
|
||||||
|
|
||||||
|
export default GraphContainer;
|
||||||
9
futa-clone/src/components/Greeting.jsx
Normal file
9
futa-clone/src/components/Greeting.jsx
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
function Greeting({ show }) {
|
||||||
|
return show ? (
|
||||||
|
<div id="greeting">
|
||||||
|
<h2>Отправьте первое сообщение, чтобы начать</h2>
|
||||||
|
</div>
|
||||||
|
) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Greeting;
|
||||||
152
futa-clone/src/components/PostForm.jsx
Normal file
152
futa-clone/src/components/PostForm.jsx
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
import { useFormik } from 'formik';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
TextField,
|
||||||
|
CircularProgress,
|
||||||
|
InputAdornment,
|
||||||
|
IconButton,
|
||||||
|
Snackbar,
|
||||||
|
Alert,
|
||||||
|
Typography,
|
||||||
|
Paper
|
||||||
|
} from '@mui/material';
|
||||||
|
import { PhotoCamera, Close } from '@mui/icons-material';
|
||||||
|
import { API_URL } from '../config';
|
||||||
|
|
||||||
|
function PostForm({ onPostCreated }) {
|
||||||
|
const [preview, setPreview] = useState('');
|
||||||
|
const [snackbar, setSnackbar] = useState({ open: false, message: '', severity: 'success' });
|
||||||
|
|
||||||
|
const formik = useFormik({
|
||||||
|
initialValues: {
|
||||||
|
text: '',
|
||||||
|
image: null
|
||||||
|
},
|
||||||
|
onSubmit: async (values, { setSubmitting, resetForm }) => {
|
||||||
|
try {
|
||||||
|
const formData = new FormData();
|
||||||
|
if (values.text) formData.append('text', values.text);
|
||||||
|
if (values.image) formData.append('image', values.image);
|
||||||
|
|
||||||
|
const response = await fetch(`${API_URL}/posts/`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) throw new Error('Ошибка сервера');
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
onPostCreated(data.vector);
|
||||||
|
resetForm();
|
||||||
|
setPreview('');
|
||||||
|
showSnackbar('Пост успешно создан!', 'success');
|
||||||
|
} catch (error) {
|
||||||
|
showSnackbar(error.message || 'Ошибка создания поста', 'error');
|
||||||
|
} finally {
|
||||||
|
setSubmitting(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleImageChange = (e) => {
|
||||||
|
const file = e.target.files[0];
|
||||||
|
if (file) {
|
||||||
|
formik.setFieldValue('image', file);
|
||||||
|
setPreview(URL.createObjectURL(file));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const showSnackbar = (message, severity) => {
|
||||||
|
setSnackbar({ open: true, message, severity });
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Paper elevation={3} sx={{ p: 3, mb: 4, maxWidth: 600, mx: 'auto' }}>
|
||||||
|
<Typography variant="h5" gutterBottom component="div">
|
||||||
|
Новый пост
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<form onSubmit={formik.handleSubmit}>
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
multiline
|
||||||
|
minRows={4}
|
||||||
|
name="text"
|
||||||
|
value={formik.values.text}
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
placeholder="Текст поста..."
|
||||||
|
variant="outlined"
|
||||||
|
margin="normal"
|
||||||
|
InputProps={{
|
||||||
|
endAdornment: (
|
||||||
|
<InputAdornment position="end">
|
||||||
|
<input
|
||||||
|
accept="image/*"
|
||||||
|
style={{ display: 'none' }}
|
||||||
|
id="image-upload"
|
||||||
|
type="file"
|
||||||
|
onChange={handleImageChange}
|
||||||
|
/>
|
||||||
|
<label htmlFor="image-upload">
|
||||||
|
<IconButton component="span" color="primary">
|
||||||
|
<PhotoCamera />
|
||||||
|
</IconButton>
|
||||||
|
</label>
|
||||||
|
</InputAdornment>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{preview && (
|
||||||
|
<Box sx={{ mt: 2, position: 'relative' }}>
|
||||||
|
<img
|
||||||
|
src={preview}
|
||||||
|
alt="Preview"
|
||||||
|
style={{
|
||||||
|
maxWidth: '100%',
|
||||||
|
borderRadius: 4,
|
||||||
|
maxHeight: 200,
|
||||||
|
objectFit: 'cover'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<IconButton
|
||||||
|
onClick={() => {
|
||||||
|
formik.setFieldValue('image', null);
|
||||||
|
setPreview('');
|
||||||
|
}}
|
||||||
|
sx={{ position: 'absolute', right: 8, top: 8, bgcolor: 'background.paper' }}
|
||||||
|
>
|
||||||
|
<Close />
|
||||||
|
</IconButton>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Box sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end' }}>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
disabled={formik.isSubmitting || (!formik.values.text && !formik.values.image)}
|
||||||
|
startIcon={formik.isSubmitting && <CircularProgress size={20} />}
|
||||||
|
>
|
||||||
|
{formik.isSubmitting ? 'Отправка...' : 'Опубликовать'}
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<Snackbar
|
||||||
|
open={snackbar.open}
|
||||||
|
autoHideDuration={6000}
|
||||||
|
onClose={() => setSnackbar(prev => ({ ...prev, open: false }))}
|
||||||
|
>
|
||||||
|
<Alert severity={snackbar.severity} sx={{ width: '100%' }}>
|
||||||
|
{snackbar.message}
|
||||||
|
</Alert>
|
||||||
|
</Snackbar>
|
||||||
|
</Paper>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PostForm;
|
||||||
19
futa-clone/src/components/PostList.jsx
Normal file
19
futa-clone/src/components/PostList.jsx
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { API_URL } from '../config';
|
||||||
|
|
||||||
|
function PostList({ posts }) {
|
||||||
|
return (
|
||||||
|
<div id="postsContainer">
|
||||||
|
{posts.map(post => (
|
||||||
|
<div key={post.id} className="post">
|
||||||
|
<div className="post-meta">
|
||||||
|
#{post.id} - {new Date(post.created_at).toLocaleString()}
|
||||||
|
</div>
|
||||||
|
{post.text && <div className="post-text">{post.text}</div>}
|
||||||
|
{post.image && <img src={`${API_URL}/${post.image}`} alt="Post" />}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PostList;
|
||||||
31
futa-clone/src/components/SearchForm.jsx
Normal file
31
futa-clone/src/components/SearchForm.jsx
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { API_URL } from '../config';
|
||||||
|
|
||||||
|
function SearchForm({ onSearch }) {
|
||||||
|
const [query, setQuery] = useState('');
|
||||||
|
|
||||||
|
const handleSearch = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_URL}/search?text=${encodeURIComponent(query)}`);
|
||||||
|
const results = await response.json();
|
||||||
|
onSearch(results);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Search error:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="post-form">
|
||||||
|
<h2>Поиск</h2>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={query}
|
||||||
|
onChange={(e) => setQuery(e.target.value)}
|
||||||
|
placeholder="Текст для поиска"
|
||||||
|
/>
|
||||||
|
<button onClick={handleSearch}>Искать</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SearchForm;
|
||||||
1
futa-clone/src/config.js
Normal file
1
futa-clone/src/config.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export const API_URL = 'http://localhost:8000';
|
||||||
13
futa-clone/src/index.css
Normal file
13
futa-clone/src/index.css
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||||
|
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||||
|
sans-serif;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||||
|
monospace;
|
||||||
|
}
|
||||||
11
futa-clone/src/index.js
Normal file
11
futa-clone/src/index.js
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { createRoot } from 'react-dom/client';
|
||||||
|
import App from './components/App';
|
||||||
|
|
||||||
|
const container = document.getElementById('root');
|
||||||
|
const root = createRoot(container);
|
||||||
|
root.render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>
|
||||||
|
);
|
||||||
1
futa-clone/src/logo.svg
Normal file
1
futa-clone/src/logo.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>
|
||||||
|
After Width: | Height: | Size: 2.6 KiB |
13
futa-clone/src/reportWebVitals.js
Normal file
13
futa-clone/src/reportWebVitals.js
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
const reportWebVitals = onPerfEntry => {
|
||||||
|
if (onPerfEntry && onPerfEntry instanceof Function) {
|
||||||
|
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
|
||||||
|
getCLS(onPerfEntry);
|
||||||
|
getFID(onPerfEntry);
|
||||||
|
getFCP(onPerfEntry);
|
||||||
|
getLCP(onPerfEntry);
|
||||||
|
getTTFB(onPerfEntry);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default reportWebVitals;
|
||||||
5
futa-clone/src/setupTests.js
Normal file
5
futa-clone/src/setupTests.js
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
// jest-dom adds custom jest matchers for asserting on DOM nodes.
|
||||||
|
// allows you to do things like:
|
||||||
|
// expect(element).toHaveTextContent(/react/i)
|
||||||
|
// learn more: https://github.com/testing-library/jest-dom
|
||||||
|
import '@testing-library/jest-dom';
|
||||||
270
index.html
Normal file
270
index.html
Normal file
@@ -0,0 +1,270 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>FutaClone</title>
|
||||||
|
<!-- Добавляем библиотеку для визуализации графа -->
|
||||||
|
<script src="https://unpkg.com/vis-network/standalone/umd/vis-network.min.js"></script>
|
||||||
|
<style>
|
||||||
|
/* Добавляем новые стили */
|
||||||
|
#network {
|
||||||
|
width: 100%;
|
||||||
|
height: 600px;
|
||||||
|
border: 1px solid #800;
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
.hidden { display: none; }
|
||||||
|
#greeting { text-align: center; padding: 20px; }
|
||||||
|
#graphContainer { margin-top: 20px; }
|
||||||
|
/* Стили в духе традиционных имиджборд */
|
||||||
|
body {
|
||||||
|
background-color: #F0E0D6;
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
margin: 20px;
|
||||||
|
max-width: 800px;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
.post-form {
|
||||||
|
background: #FFF;
|
||||||
|
border: 1px solid #800;
|
||||||
|
padding: 15px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.post {
|
||||||
|
background: #FFF;
|
||||||
|
border: 1px solid #800;
|
||||||
|
padding: 15px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.post img {
|
||||||
|
max-width: 200px;
|
||||||
|
max-height: 200px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
input, textarea, button {
|
||||||
|
margin: 5px;
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
|
.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.preview-image {
|
||||||
|
max-width: 150px;
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!-- Добавляем новые блоки -->
|
||||||
|
<div id="greeting" class="hidden">
|
||||||
|
<h2>Отправьте первое сообщение, чтобы начать</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="graphContainer" class="hidden">
|
||||||
|
<h2>Древо мыслей</h2>
|
||||||
|
<div id="network"></div>
|
||||||
|
</div>
|
||||||
|
<!-- Форма создания поста -->
|
||||||
|
<div class="post-form">
|
||||||
|
<h2>Новый пост</h2>
|
||||||
|
<form id="postForm">
|
||||||
|
<textarea id="postText" rows="4" cols="50" placeholder="Текст поста"></textarea><br>
|
||||||
|
<input type="file" id="imageInput" accept="image/*"><br>
|
||||||
|
<div id="imagePreview" class="hidden"></div>
|
||||||
|
<button type="submit">Отправить</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Форма поиска (только текст) -->
|
||||||
|
<div class="post-form">
|
||||||
|
<h2>Поиск</h2>
|
||||||
|
<input type="text" id="searchText" placeholder="Текст для поиска">
|
||||||
|
<button onclick="searchPosts()">Искать</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Список постов -->
|
||||||
|
<div id="postsContainer"></div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Базовый URL бэкенда
|
||||||
|
const API_URL = 'http://localhost:8000';
|
||||||
|
|
||||||
|
// Обработка отправки поста (оставляем без изменений)
|
||||||
|
document.getElementById('postForm').addEventListener('submit', async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const text = document.getElementById('postText').value;
|
||||||
|
const fileInput = document.getElementById('imageInput');
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
if (text) formData.append('text', text);
|
||||||
|
if (fileInput.files[0]) formData.append('image', fileInput.files[0]);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_URL}/posts/`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const postData = await response.json();
|
||||||
|
// Сохраняем вектор в localStorage
|
||||||
|
localStorage.setItem('userVector', JSON.stringify(postData.vector));
|
||||||
|
checkUserVector();
|
||||||
|
loadPosts();
|
||||||
|
document.getElementById('postForm').reset();
|
||||||
|
document.getElementById('imagePreview').classList.add('hidden');
|
||||||
|
} else {
|
||||||
|
console.error('Error:', error);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error:', error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Превью изображения перед загрузкой
|
||||||
|
document.getElementById('imageInput').addEventListener('change', function(e) {
|
||||||
|
const preview = document.getElementById('imagePreview');
|
||||||
|
if (this.files && this.files[0]) {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = function(e) {
|
||||||
|
preview.innerHTML = `<img src="${e.target.result}" class="preview-image">`;
|
||||||
|
preview.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
reader.readAsDataURL(this.files[0]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Загрузка и отображение постов (оставляем без изменений)
|
||||||
|
async function loadPosts() {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_URL}/posts/`);
|
||||||
|
const posts = await response.json();
|
||||||
|
|
||||||
|
const container = document.getElementById('postsContainer');
|
||||||
|
container.innerHTML = posts.map(post => `
|
||||||
|
<div class="post">
|
||||||
|
<div class="post-meta">#${post.id} - ${new Date(post.created_at).toLocaleString()}</div>
|
||||||
|
${post.text ? `<div class="post-text">${post.text}</div>` : ''}
|
||||||
|
${post.image ? `<img src="${API_URL}/${post.image}" />` : ''}
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обновлённая функция поиска, использующая GET-запрос
|
||||||
|
async function searchPosts() {
|
||||||
|
const text = document.getElementById('searchText').value;
|
||||||
|
// Формируем строку запроса, если введён текст
|
||||||
|
const query = text ? `?text=${encodeURIComponent(text)}` : '';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_URL}/search/${query}`, {
|
||||||
|
method: 'GET'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Запрос поиска завершился с ошибкой');
|
||||||
|
}
|
||||||
|
|
||||||
|
const results = await response.json();
|
||||||
|
alert('Найдено постов: ' + results.length);
|
||||||
|
// Здесь можно добавить отображение результатов поиска в интерфейсе
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error during search:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Первоначальная загрузка постов
|
||||||
|
loadPosts();
|
||||||
|
// Новая функция проверки вектора
|
||||||
|
function checkUserVector() {
|
||||||
|
const hasVector = localStorage.getItem('userVector') !== null;
|
||||||
|
document.getElementById('greeting').classList.toggle('hidden', hasVector);
|
||||||
|
document.getElementById('graphContainer').classList.toggle('hidden', !hasVector);
|
||||||
|
|
||||||
|
if (hasVector) {
|
||||||
|
loadUserTree();
|
||||||
|
} else {
|
||||||
|
loadPosts();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Функция загрузки дерева
|
||||||
|
async function loadUserTree() {
|
||||||
|
const userVector = JSON.parse(localStorage.getItem('userVector'));
|
||||||
|
if (!userVector) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_URL}/posts/tree`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ vector: userVector }),
|
||||||
|
});
|
||||||
|
const posts = await response.json();
|
||||||
|
renderGraph(posts);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading tree:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Визуализация графа
|
||||||
|
function renderGraph(posts) {
|
||||||
|
const container = document.getElementById('network');
|
||||||
|
const nodes = [];
|
||||||
|
const edges = [];
|
||||||
|
|
||||||
|
posts.forEach((post, index) => {
|
||||||
|
nodes.push({
|
||||||
|
id: post.id,
|
||||||
|
label: post.text || '[Изображение]',
|
||||||
|
image: post.image ? `${API_URL}/${post.image}` : 'https://via.placeholder.com/100',
|
||||||
|
shape: 'circularImage',
|
||||||
|
size: 25
|
||||||
|
});
|
||||||
|
|
||||||
|
if (index > 0) {
|
||||||
|
edges.push({
|
||||||
|
from: posts[index-1].id,
|
||||||
|
to: post.id,
|
||||||
|
arrows: 'to',
|
||||||
|
smooth: { type: 'curvedCW' }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = { nodes, edges };
|
||||||
|
const options = {
|
||||||
|
nodes: {
|
||||||
|
borderWidth: 2,
|
||||||
|
color: {
|
||||||
|
border: '#800',
|
||||||
|
background: '#F0E0D6'
|
||||||
|
},
|
||||||
|
font: { size: 14 }
|
||||||
|
},
|
||||||
|
edges: {
|
||||||
|
color: '#800',
|
||||||
|
width: 2
|
||||||
|
},
|
||||||
|
physics: {
|
||||||
|
stabilization: true,
|
||||||
|
barnesHut: {
|
||||||
|
gravitationalConstant: -2000,
|
||||||
|
springLength: 200
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
new vis.Network(container, data, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Инициализация при загрузке страницы
|
||||||
|
checkUserVector();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
364
main.py
Normal file
364
main.py
Normal file
@@ -0,0 +1,364 @@
|
|||||||
|
# main.py
|
||||||
|
from fastapi import FastAPI, UploadFile, File, HTTPException, Form
|
||||||
|
from fastapi.staticfiles import StaticFiles
|
||||||
|
from pydantic import BaseModel
|
||||||
|
from typing import List, Optional
|
||||||
|
import uuid
|
||||||
|
import os
|
||||||
|
from datetime import datetime
|
||||||
|
import requests
|
||||||
|
from qdrant_client import QdrantClient
|
||||||
|
from qdrant_client.http import models
|
||||||
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
import time # Добавлен импорт модуля time
|
||||||
|
|
||||||
|
# Database setup
|
||||||
|
from sqlalchemy import create_engine, Column, String, DateTime, Text
|
||||||
|
from sqlalchemy.ext.declarative import declarative_base
|
||||||
|
from sqlalchemy.orm import sessionmaker
|
||||||
|
|
||||||
|
# Image processing
|
||||||
|
from PIL import Image
|
||||||
|
import io
|
||||||
|
import torch
|
||||||
|
from transformers import CLIPProcessor, CLIPModel
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from qdrant_client.http.models import VectorParams, Distance
|
||||||
|
|
||||||
|
import os
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
# Настройка базового логирования
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO,
|
||||||
|
format="%(asctime)s [%(levelname)s] %(name)s - %(message)s"
|
||||||
|
)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# Загрузить переменные окружения из файла .env
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
# Получение значений с указанием значений по умолчанию
|
||||||
|
COLLECTION_NAME = os.getenv("COLLECTION_NAME", "posts")
|
||||||
|
VECTOR_SIZE = int(os.getenv("VECTOR_SIZE", 1280))
|
||||||
|
DATABASE_URL = os.getenv("DATABASE_URL", "sqlite:///./imageboard.db")
|
||||||
|
QDRANT_URL = os.getenv("QDRANT_URL", "http://qdrant:6333")
|
||||||
|
OLLAMA_URL = os.getenv("OLLAMA_URL", "http://ollama:11435")
|
||||||
|
EMBEDDING_MODEL = os.getenv("EMBEDDING_MODEL", "nomic-embed-text")
|
||||||
|
IMAGE_MODEL = os.getenv("IMAGE_MODEL", "openai/clip-vit-base-patch32")
|
||||||
|
# IMAGE_SIZE ожидается в формате "224,224", преобразуем его в кортеж чисел
|
||||||
|
image_size_str = os.getenv("IMAGE_SIZE", "224,224")
|
||||||
|
IMAGE_SIZE = tuple(map(int, image_size_str.split(',')))
|
||||||
|
UPLOAD_DIR = os.getenv("UPLOAD_DIR", "uploads")
|
||||||
|
|
||||||
|
os.makedirs(UPLOAD_DIR, exist_ok=True)
|
||||||
|
|
||||||
|
# Инициализация компонентов
|
||||||
|
Base = declarative_base()
|
||||||
|
engine = create_engine(DATABASE_URL)
|
||||||
|
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||||
|
|
||||||
|
# Инициализация CLIP
|
||||||
|
clip_model = CLIPModel.from_pretrained(IMAGE_MODEL)
|
||||||
|
clip_processor = CLIPProcessor.from_pretrained(IMAGE_MODEL)
|
||||||
|
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
||||||
|
clip_model = clip_model.to(device)
|
||||||
|
|
||||||
|
# Функция для создания QdrantClient с повторными попытками
|
||||||
|
def create_qdrant_client():
|
||||||
|
max_attempts = 5
|
||||||
|
attempt = 0
|
||||||
|
while attempt < max_attempts:
|
||||||
|
try:
|
||||||
|
client = QdrantClient(QDRANT_URL)
|
||||||
|
# Проверка подключения
|
||||||
|
client.get_collections()
|
||||||
|
logger.info("Успешное подключение к Qdrant")
|
||||||
|
return client
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Попытка {attempt+1} подключения к Qdrant не удалась: {str(e)}")
|
||||||
|
attempt += 1
|
||||||
|
time.sleep(2)
|
||||||
|
raise RuntimeError(f"Не удалось подключиться к Qdrant после {max_attempts} попыток")
|
||||||
|
|
||||||
|
# Инициализация клиента Qdrant
|
||||||
|
try:
|
||||||
|
qdrant_client = create_qdrant_client()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Ошибка инициализации Qdrant: {str(e)}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
# Функция проверки и создания коллекции
|
||||||
|
def ensure_collection_exists():
|
||||||
|
max_attempts = 5
|
||||||
|
attempt = 0
|
||||||
|
while attempt < max_attempts:
|
||||||
|
try:
|
||||||
|
# Проверка существования коллекции
|
||||||
|
qdrant_client.get_collection(collection_name=COLLECTION_NAME)
|
||||||
|
logger.info(f"Коллекция '{COLLECTION_NAME}' существует")
|
||||||
|
return
|
||||||
|
except Exception as e:
|
||||||
|
if "not found" in str(e).lower():
|
||||||
|
logger.info(f"Создание коллекции '{COLLECTION_NAME}'...")
|
||||||
|
try:
|
||||||
|
qdrant_client.create_collection(
|
||||||
|
collection_name=COLLECTION_NAME,
|
||||||
|
vectors_config=VectorParams(
|
||||||
|
size=VECTOR_SIZE,
|
||||||
|
distance=Distance.COSINE
|
||||||
|
)
|
||||||
|
)
|
||||||
|
logger.info(f"Коллекция '{COLLECTION_NAME}' создана")
|
||||||
|
return
|
||||||
|
except Exception as create_error:
|
||||||
|
logger.error(f"Ошибка создания: {str(create_error)}")
|
||||||
|
else:
|
||||||
|
logger.error(f"Ошибка подключения: {str(e)}")
|
||||||
|
|
||||||
|
attempt += 1
|
||||||
|
time.sleep(2)
|
||||||
|
raise RuntimeError(f"Не удалось инициализировать коллекцию после {max_attempts} попыток")
|
||||||
|
|
||||||
|
# Вызов функции проверки коллекции
|
||||||
|
try:
|
||||||
|
ensure_collection_exists()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Ошибка инициализации коллекции: {str(e)}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
# Инициализация FastAPI
|
||||||
|
app = FastAPI()
|
||||||
|
app.mount("/uploads", StaticFiles(directory=UPLOAD_DIR), name="uploads")
|
||||||
|
app.add_middleware(
|
||||||
|
CORSMiddleware,
|
||||||
|
allow_origins=["*"],
|
||||||
|
allow_credentials=True,
|
||||||
|
allow_methods=["*"],
|
||||||
|
allow_headers=["*"],
|
||||||
|
)
|
||||||
|
|
||||||
|
# Модель базы данных
|
||||||
|
class Post(Base):
|
||||||
|
__tablename__ = "posts"
|
||||||
|
id = Column(String, primary_key=True, index=True)
|
||||||
|
text = Column(Text, nullable=True)
|
||||||
|
image = Column(String, nullable=True)
|
||||||
|
created_at = Column(DateTime)
|
||||||
|
|
||||||
|
Base.metadata.create_all(bind=engine)
|
||||||
|
|
||||||
|
# Pydantic model для ответа
|
||||||
|
class PostResponse(BaseModel):
|
||||||
|
id: str
|
||||||
|
text: Optional[str] = None
|
||||||
|
image: Optional[str] = None
|
||||||
|
created_at: datetime
|
||||||
|
vector: Optional[List[float]] = None
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
orm_mode = True
|
||||||
|
|
||||||
|
# Pydantic модель для запроса вектора
|
||||||
|
class VectorQuery(BaseModel):
|
||||||
|
vector: List[float]
|
||||||
|
|
||||||
|
# Utility functions
|
||||||
|
def generate_text_embedding(text: str) -> List[float]:
|
||||||
|
response = requests.post(
|
||||||
|
f"{OLLAMA_URL}/api/embeddings",
|
||||||
|
json={"model": EMBEDDING_MODEL, "prompt": text}
|
||||||
|
)
|
||||||
|
if response.status_code != 200:
|
||||||
|
raise HTTPException(status_code=500, detail="Embedding generation failed")
|
||||||
|
return response.json()["embedding"]
|
||||||
|
|
||||||
|
def generate_image_embedding(image_bytes: bytes) -> List[float]:
|
||||||
|
image = Image.open(io.BytesIO(image_bytes)).convert("RGB")
|
||||||
|
inputs = clip_processor(
|
||||||
|
images=image,
|
||||||
|
return_tensors="pt",
|
||||||
|
padding=True
|
||||||
|
).to(device)
|
||||||
|
|
||||||
|
with torch.no_grad():
|
||||||
|
features = clip_model.get_image_features(**inputs)
|
||||||
|
return features.cpu().numpy().tolist()[0]
|
||||||
|
|
||||||
|
def process_image(image_bytes: bytes) -> bytes:
|
||||||
|
img = Image.open(io.BytesIO(image_bytes))
|
||||||
|
img = img.convert("RGB")
|
||||||
|
img = img.resize(IMAGE_SIZE)
|
||||||
|
buffer = io.BytesIO()
|
||||||
|
img.save(buffer, format="JPEG")
|
||||||
|
return buffer.getvalue()
|
||||||
|
|
||||||
|
# API endpoints
|
||||||
|
# API endpoints
|
||||||
|
@app.post("/posts/", response_model=PostResponse)
|
||||||
|
async def create_post(
|
||||||
|
text: Optional[str] = Form(None),
|
||||||
|
image: Optional[UploadFile] = File(None)
|
||||||
|
):
|
||||||
|
db = SessionLocal()
|
||||||
|
try:
|
||||||
|
post_id = str(uuid.uuid4())
|
||||||
|
image_path = None
|
||||||
|
thumbnail_path = None # Placeholder for thumbnail
|
||||||
|
embeddings = []
|
||||||
|
|
||||||
|
if text:
|
||||||
|
logger.info("Генерация эмбеддинга для текста")
|
||||||
|
text_embedding = generate_text_embedding(text)
|
||||||
|
embeddings.extend(text_embedding)
|
||||||
|
|
||||||
|
if image:
|
||||||
|
logger.info("Обработка изображения")
|
||||||
|
image_bytes = await image.read()
|
||||||
|
|
||||||
|
# Save original image
|
||||||
|
image_path = f"{UPLOAD_DIR}/{post_id}.jpg"
|
||||||
|
with open(image_path, "wb") as f:
|
||||||
|
f.write(image_bytes)
|
||||||
|
|
||||||
|
# Create processed image for embeddings
|
||||||
|
processed_image = process_image(image_bytes) # Assume this resizes/image processing
|
||||||
|
|
||||||
|
# Generate thumbnail as placeholder (example implementation)
|
||||||
|
#thumbnail = generate_thumbnail(image_bytes) # Implement your thumbnail generation
|
||||||
|
#thumbnail_path = f"{UPLOAD_DIR}/{post_id}_thumbnail.jpg"
|
||||||
|
#with open(thumbnail_path, "wb") as f:
|
||||||
|
# f.write(thumbnail)
|
||||||
|
|
||||||
|
# Generate embeddings from processed image
|
||||||
|
image_embedding = generate_image_embedding(processed_image)
|
||||||
|
embeddings.extend(image_embedding)
|
||||||
|
|
||||||
|
logger.info("Сохранение данных в Qdrant")
|
||||||
|
qdrant_client.upsert(
|
||||||
|
collection_name="posts",
|
||||||
|
points=[models.PointStruct(
|
||||||
|
id=post_id,
|
||||||
|
vector=embeddings,
|
||||||
|
payload={"post_id": post_id}
|
||||||
|
)]
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info("Сохранение поста в базу данных")
|
||||||
|
db_post = Post(
|
||||||
|
id=post_id,
|
||||||
|
text=text,
|
||||||
|
image=image_path,
|
||||||
|
#thumbnail=thumbnail_path, Add thumbnail field to your Post model
|
||||||
|
created_at=datetime.now()
|
||||||
|
)
|
||||||
|
db.add(db_post)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(db_post)
|
||||||
|
|
||||||
|
response = PostResponse(
|
||||||
|
id=db_post.id,
|
||||||
|
text=db_post.text,
|
||||||
|
image=db_post.image,
|
||||||
|
#thumbnail=db_post.thumbnail, Update response model to include thumbnail
|
||||||
|
created_at=db_post.created_at,
|
||||||
|
vector=embeddings
|
||||||
|
)
|
||||||
|
logger.info("Пост успешно создан: %s", response)
|
||||||
|
return response
|
||||||
|
except Exception as e:
|
||||||
|
db.rollback()
|
||||||
|
logger.exception("Ошибка при создании поста")
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
@app.get("/search/")
|
||||||
|
async def search_posts(
|
||||||
|
text: Optional[str] = None,
|
||||||
|
image: Optional[UploadFile] = File(None)
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
query_embedding = []
|
||||||
|
|
||||||
|
if text:
|
||||||
|
logger.info("Генерация эмбеддинга для текста (поиск)")
|
||||||
|
text_embedding = generate_text_embedding(text)
|
||||||
|
query_embedding.extend(text_embedding)
|
||||||
|
|
||||||
|
if image:
|
||||||
|
logger.info("Генерация эмбеддинга для изображения (поиск)")
|
||||||
|
image_bytes = await image.read()
|
||||||
|
processed_image = process_image(image_bytes)
|
||||||
|
image_embedding = generate_image_embedding(processed_image)
|
||||||
|
query_embedding.extend(image_embedding)
|
||||||
|
|
||||||
|
logger.info("Выполнение поиска в Qdrant")
|
||||||
|
search_results = qdrant_client.search(
|
||||||
|
collection_name="posts",
|
||||||
|
query_vector=query_embedding,
|
||||||
|
limit=10
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info("Поиск завершён. Найдено результатов: %d", len(search_results))
|
||||||
|
return [result.payload for result in search_results]
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception("Ошибка при поиске постов")
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
@app.get("/posts/", response_model=List[PostResponse])
|
||||||
|
async def get_all_posts():
|
||||||
|
db = SessionLocal()
|
||||||
|
try:
|
||||||
|
posts = db.query(Post).all()
|
||||||
|
return posts
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
# Новый endpoint: получение "древа" постов по вектору пользователя
|
||||||
|
@app.post("/posts/tree", response_model=List[PostResponse])
|
||||||
|
async def get_posts_tree(query: VectorQuery):
|
||||||
|
# Выполняем поиск в Qdrant с большим лимитом, чтобы получить все посты, отсортированные по сходству
|
||||||
|
search_results = qdrant_client.search(
|
||||||
|
collection_name="posts",
|
||||||
|
query_vector=query.vector,
|
||||||
|
limit=10000 # Задайте лимит в зависимости от ожидаемого числа постов
|
||||||
|
)
|
||||||
|
print("search_results")
|
||||||
|
print(search_results)
|
||||||
|
# Извлекаем список ID постов в том порядке, в котором Qdrant вернул результаты (от ближайших к дальним)
|
||||||
|
post_ids = [result.payload.get("post_id") for result in search_results]
|
||||||
|
print("post_ids")
|
||||||
|
print(post_ids)
|
||||||
|
db = SessionLocal()
|
||||||
|
try:
|
||||||
|
# Получаем все посты из БД по списку ID
|
||||||
|
posts = db.query(Post).filter(Post.id.in_(post_ids)).all()
|
||||||
|
print("posts")
|
||||||
|
print(posts)
|
||||||
|
# Создаём словарь для сохранения соответствия post_id -> post
|
||||||
|
posts_dict = {post.id: post for post in posts}
|
||||||
|
print("posts_dict")
|
||||||
|
print(posts_dict)
|
||||||
|
# Восстанавливаем порядок, используя список post_ids
|
||||||
|
ordered_posts = [posts_dict[pid] for pid in post_ids if pid in posts_dict]
|
||||||
|
print("ordered_posts")
|
||||||
|
print(ordered_posts)
|
||||||
|
return ordered_posts
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import uvicorn
|
||||||
|
print("COLLECTION_NAME:", COLLECTION_NAME)
|
||||||
|
print("VECTOR_SIZE:", VECTOR_SIZE)
|
||||||
|
print("DATABASE_URL:", DATABASE_URL)
|
||||||
|
print("QDRANT_URL:", QDRANT_URL)
|
||||||
|
print("OLLAMA_URL:", OLLAMA_URL)
|
||||||
|
print("EMBEDDING_MODEL:", EMBEDDING_MODEL)
|
||||||
|
print("IMAGE_MODEL:", IMAGE_MODEL)
|
||||||
|
print("IMAGE_SIZE:", IMAGE_SIZE)
|
||||||
|
print("UPLOAD_DIR:", UPLOAD_DIR)
|
||||||
|
uvicorn.run(app, host="0.0.0.0", port=8000)
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "https://json.schemastore.org/nest-cli",
|
|
||||||
"collection": "@nestjs/schematics",
|
|
||||||
"sourceRoot": "src",
|
|
||||||
"compilerOptions": {
|
|
||||||
"deleteOutDir": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
8
ollama/Dockerfile
Normal file
8
ollama/Dockerfile
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
FROM ollama/ollama:latest
|
||||||
|
|
||||||
|
# Копируем скрипт запуска в контейнер
|
||||||
|
COPY entrypoint.sh /entrypoint.sh
|
||||||
|
RUN chmod +x /entrypoint.sh
|
||||||
|
|
||||||
|
# Переопределяем ENTRYPOINT, чтобы запускался наш скрипт
|
||||||
|
ENTRYPOINT ["/entrypoint.sh"]
|
||||||
34
ollama/entrypoint.sh
Normal file
34
ollama/entrypoint.sh
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "Запускаем ollama serve в фоне..."
|
||||||
|
# Запускаем сервер в фоне и сохраняем PID процесса
|
||||||
|
ollama serve &
|
||||||
|
SERVER_PID=$!
|
||||||
|
|
||||||
|
echo "Ожидаем, пока сервер станет доступным..."
|
||||||
|
# Пытаемся получить список моделей, ожидая доступность сервера
|
||||||
|
for i in $(seq 1 30); do
|
||||||
|
if ollama list >/dev/null 2>&1; then
|
||||||
|
echo "Сервер доступен."
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
echo "Сервер ещё не готов, ждём..."
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "Проверяем наличие модели nomic-embed-text..."
|
||||||
|
# Вывод списка моделей для отладки
|
||||||
|
ollama list
|
||||||
|
|
||||||
|
# Если модели нет, выполняем загрузку
|
||||||
|
if ! ollama list | grep -q 'nomic-embed-text'; then
|
||||||
|
echo "Модель nomic-embed-text не найдена. Загружаем..."
|
||||||
|
ollama pull nomic-embed-text
|
||||||
|
else
|
||||||
|
echo "Модель nomic-embed-text уже установлена."
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Сервер ollama запущен и работает. Ожидание завершения процесса..."
|
||||||
|
# Ожидаем завершения фонового процесса сервера
|
||||||
|
wait $SERVER_PID
|
||||||
8676
package-lock.json
generated
8676
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
73
package.json
73
package.json
@@ -1,73 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "vdkch",
|
|
||||||
"version": "0.0.1",
|
|
||||||
"description": "",
|
|
||||||
"author": "",
|
|
||||||
"private": true,
|
|
||||||
"license": "UNLICENSED",
|
|
||||||
"scripts": {
|
|
||||||
"build": "nest build",
|
|
||||||
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
|
||||||
"start": "nest start",
|
|
||||||
"start:dev": "nest start --watch",
|
|
||||||
"start:debug": "nest start --debug --watch",
|
|
||||||
"start:prod": "node dist/main",
|
|
||||||
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
|
||||||
"test": "jest",
|
|
||||||
"test:watch": "jest --watch",
|
|
||||||
"test:cov": "jest --coverage",
|
|
||||||
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
|
|
||||||
"test:e2e": "jest --config ./test/jest-e2e.json"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@nestjs/common": "^10.0.0",
|
|
||||||
"@nestjs/core": "^10.0.0",
|
|
||||||
"@nestjs/platform-express": "^10.4.1",
|
|
||||||
"@prisma/client": "^5.18.0",
|
|
||||||
"dotenv": "^16.4.5",
|
|
||||||
"multer": "^1.4.5-lts.1",
|
|
||||||
"reflect-metadata": "^0.2.0",
|
|
||||||
"rxjs": "^7.8.1"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@nestjs/cli": "^10.0.0",
|
|
||||||
"@nestjs/schematics": "^10.0.0",
|
|
||||||
"@nestjs/testing": "^10.0.0",
|
|
||||||
"@types/express": "^4.17.17",
|
|
||||||
"@types/jest": "^29.5.2",
|
|
||||||
"@types/node": "^20.3.1",
|
|
||||||
"@types/supertest": "^6.0.0",
|
|
||||||
"@typescript-eslint/eslint-plugin": "^7.0.0",
|
|
||||||
"@typescript-eslint/parser": "^7.0.0",
|
|
||||||
"eslint": "^8.42.0",
|
|
||||||
"eslint-config-prettier": "^9.0.0",
|
|
||||||
"eslint-plugin-prettier": "^5.0.0",
|
|
||||||
"jest": "^29.5.0",
|
|
||||||
"prettier": "^3.0.0",
|
|
||||||
"prisma": "^5.18.0",
|
|
||||||
"source-map-support": "^0.5.21",
|
|
||||||
"supertest": "^7.0.0",
|
|
||||||
"ts-jest": "^29.1.0",
|
|
||||||
"ts-loader": "^9.4.3",
|
|
||||||
"ts-node": "^10.9.1",
|
|
||||||
"tsconfig-paths": "^4.2.0",
|
|
||||||
"typescript": "^5.1.3"
|
|
||||||
},
|
|
||||||
"jest": {
|
|
||||||
"moduleFileExtensions": [
|
|
||||||
"js",
|
|
||||||
"json",
|
|
||||||
"ts"
|
|
||||||
],
|
|
||||||
"rootDir": "src",
|
|
||||||
"testRegex": ".*\\.spec\\.ts$",
|
|
||||||
"transform": {
|
|
||||||
"^.+\\.(t|j)s$": "ts-jest"
|
|
||||||
},
|
|
||||||
"collectCoverageFrom": [
|
|
||||||
"**/*.(t|j)s"
|
|
||||||
],
|
|
||||||
"coverageDirectory": "../coverage",
|
|
||||||
"testEnvironment": "node"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
datasource db {
|
|
||||||
provider = "postgresql"
|
|
||||||
url = env("DATABASE_URL")
|
|
||||||
}
|
|
||||||
|
|
||||||
generator client {
|
|
||||||
provider = "prisma-client-js"
|
|
||||||
binaryTargets = ["native", "debian-openssl-3.0.x"]
|
|
||||||
}
|
|
||||||
|
|
||||||
model Post {
|
|
||||||
id Int @id @default(autoincrement())
|
|
||||||
text String?
|
|
||||||
media String? // URL или путь к файлу
|
|
||||||
mediaType String? // "image", "video", "drawing", "code"
|
|
||||||
code String? // Код, который нужно запустить
|
|
||||||
//TODO Добавить vector для оценки близости постов
|
|
||||||
}
|
|
||||||
3
qdrant/Dockerfile
Normal file
3
qdrant/Dockerfile
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
FROM qdrant/qdrant
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
|
||||||
11
requirements.txt
Normal file
11
requirements.txt
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
# requirements.txt
|
||||||
|
fastapi
|
||||||
|
uvicorn
|
||||||
|
python-multipart
|
||||||
|
qdrant-client
|
||||||
|
sqlalchemy
|
||||||
|
requests
|
||||||
|
pillow
|
||||||
|
transformers
|
||||||
|
torch
|
||||||
|
python-dotenv
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
import { Test, TestingModule } from '@nestjs/testing';
|
|
||||||
import { AppController } from './app.controller';
|
|
||||||
import { AppService } from './app.service';
|
|
||||||
|
|
||||||
describe('AppController', () => {
|
|
||||||
let appController: AppController;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
const app: TestingModule = await Test.createTestingModule({
|
|
||||||
controllers: [AppController],
|
|
||||||
providers: [AppService],
|
|
||||||
}).compile();
|
|
||||||
|
|
||||||
appController = app.get<AppController>(AppController);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('root', () => {
|
|
||||||
it('should return "Hello World!"', () => {
|
|
||||||
expect(appController.getHello()).toBe('Hello World!');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
import { Controller, Get } from '@nestjs/common';
|
|
||||||
import { AppService } from './app.service';
|
|
||||||
|
|
||||||
@Controller()
|
|
||||||
export class AppController {
|
|
||||||
constructor(private readonly appService: AppService) {}
|
|
||||||
|
|
||||||
@Get()
|
|
||||||
getHello(): string {
|
|
||||||
return this.appService.getHello();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import { Module } from '@nestjs/common';
|
|
||||||
import { PostsModule } from './posts/posts.module';
|
|
||||||
import { PrismaService } from './prisma.service';
|
|
||||||
|
|
||||||
@Module({
|
|
||||||
imports: [PostsModule],
|
|
||||||
providers: [PrismaService],
|
|
||||||
})
|
|
||||||
export class AppModule {}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class AppService {
|
|
||||||
getHello(): string {
|
|
||||||
return 'Hello World!';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
import { NestFactory } from '@nestjs/core';
|
|
||||||
import { AppModule } from './app.module';
|
|
||||||
|
|
||||||
async function bootstrap() {
|
|
||||||
const app = await NestFactory.create(AppModule);
|
|
||||||
await app.listen(3000);
|
|
||||||
}
|
|
||||||
bootstrap();
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
export interface Post {
|
|
||||||
id?: number;
|
|
||||||
text: string;
|
|
||||||
imageUrl?: string;
|
|
||||||
videoUrl?: string;
|
|
||||||
drawing?: string;
|
|
||||||
code?: string;
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
import { Test, TestingModule } from '@nestjs/testing';
|
|
||||||
import { PostsController } from './posts.controller';
|
|
||||||
|
|
||||||
describe('PostsController', () => {
|
|
||||||
let controller: PostsController;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
const module: TestingModule = await Test.createTestingModule({
|
|
||||||
controllers: [PostsController],
|
|
||||||
}).compile();
|
|
||||||
|
|
||||||
controller = module.get<PostsController>(PostsController);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should be defined', () => {
|
|
||||||
expect(controller).toBeDefined();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
import { Controller, Post, Body, Get } from '@nestjs/common';
|
|
||||||
import { PostsService } from './posts.service';
|
|
||||||
|
|
||||||
@Controller('posts')
|
|
||||||
export class PostsController {
|
|
||||||
constructor(private readonly postsService: PostsService) {}
|
|
||||||
|
|
||||||
@Post()
|
|
||||||
async create(@Body() createPostDto: { text?: string; media?: string; mediaType?: string; code?: string }) {
|
|
||||||
return this.postsService.create(createPostDto);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Get()
|
|
||||||
async findAll() {
|
|
||||||
return this.postsService.findAll();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
import { Module } from '@nestjs/common';
|
|
||||||
import { PostsService } from './posts.service';
|
|
||||||
import { PostsController } from './posts.controller';
|
|
||||||
import { PrismaService } from '../prisma.service';
|
|
||||||
|
|
||||||
@Module({
|
|
||||||
providers: [PostsService, PrismaService],
|
|
||||||
controllers: [PostsController],
|
|
||||||
})
|
|
||||||
export class PostsModule {}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
import { Test, TestingModule } from '@nestjs/testing';
|
|
||||||
import { PostsService } from './posts.service';
|
|
||||||
|
|
||||||
describe('PostsService', () => {
|
|
||||||
let service: PostsService;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
const module: TestingModule = await Test.createTestingModule({
|
|
||||||
providers: [PostsService],
|
|
||||||
}).compile();
|
|
||||||
|
|
||||||
service = module.get<PostsService>(PostsService);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should be defined', () => {
|
|
||||||
expect(service).toBeDefined();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
import { PrismaService } from '../prisma.service';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class PostsService {
|
|
||||||
constructor(private readonly prisma: PrismaService) {}
|
|
||||||
|
|
||||||
async create(data: { text?: string; media?: string; mediaType?: string; code?: string }) {
|
|
||||||
return this.prisma.post.create({
|
|
||||||
data,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async findAll() {
|
|
||||||
return this.prisma.post.findMany();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
import 'dotenv/config';
|
|
||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
import { PrismaClient } from '@prisma/client';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class PrismaService extends PrismaClient {
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
import { Test, TestingModule } from '@nestjs/testing';
|
|
||||||
import { INestApplication } from '@nestjs/common';
|
|
||||||
import * as request from 'supertest';
|
|
||||||
import { AppModule } from './../src/app.module';
|
|
||||||
|
|
||||||
describe('AppController (e2e)', () => {
|
|
||||||
let app: INestApplication;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
const moduleFixture: TestingModule = await Test.createTestingModule({
|
|
||||||
imports: [AppModule],
|
|
||||||
}).compile();
|
|
||||||
|
|
||||||
app = moduleFixture.createNestApplication();
|
|
||||||
await app.init();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('/ (GET)', () => {
|
|
||||||
return request(app.getHttpServer())
|
|
||||||
.get('/')
|
|
||||||
.expect(200)
|
|
||||||
.expect('Hello World!');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"moduleFileExtensions": ["js", "json", "ts"],
|
|
||||||
"rootDir": ".",
|
|
||||||
"testEnvironment": "node",
|
|
||||||
"testRegex": ".e2e-spec.ts$",
|
|
||||||
"transform": {
|
|
||||||
"^.+\\.(t|j)s$": "ts-jest"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": "./tsconfig.json",
|
|
||||||
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"module": "commonjs",
|
|
||||||
"declaration": true,
|
|
||||||
"removeComments": true,
|
|
||||||
"emitDecoratorMetadata": true,
|
|
||||||
"experimentalDecorators": true,
|
|
||||||
"allowSyntheticDefaultImports": true,
|
|
||||||
"target": "ES2021",
|
|
||||||
"sourceMap": true,
|
|
||||||
"outDir": "./dist",
|
|
||||||
"baseUrl": "./",
|
|
||||||
"incremental": true,
|
|
||||||
"skipLibCheck": true,
|
|
||||||
"strictNullChecks": false,
|
|
||||||
"noImplicitAny": false,
|
|
||||||
"strictBindCallApply": false,
|
|
||||||
"forceConsistentCasingInFileNames": false,
|
|
||||||
"noFallthroughCasesInSwitch": false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user