Project structure:

DarkDataset/
├── projects/
│   ├── 1/
│   ........
├── static/
│   ├── css/
│   │   ├── account-tab.css
│   │   ├── base.css
│   │   ├── edit_project.css
│   │   ├── home-tab.css
│   │   ├── hotbar.css
│   │   ├── join_project.css
│   │   ├── loading.css
│   │   ├── login-tab.css
│   │   ├── new_project.css
│   │   ├── tab.css
│   │   ├── windows.css
│   │   └── workspace.css
│   ├── icons/
│   │   ├── h_icon.webp
│   │   ├── profile_icon.webp
│   │   └── s_icon.webp
│   ├── js/
│   │   ├── account_manager.js
│   │   ├── account_page.js
│   │   ├── edit_project.js
│   │   ├── hotbar-manager.js
│   │   ├── join.js
│   │   ├── login.js
│   │   ├── main.js
│   │   ├── new_project.js
│   │   └── workspace.js
├── templates/
│   ├── components/
│   │   ├── account_tab.html
│   │   ├── home_tab.html
│   │   ├── hotbar.html
│   │   ├── login_tab.html
│   │   └── windows.html
│   ├── account.html
│   ├── edit_project.html
│   ├── index.html
│   ├── join.html
│   ├── login.html
│   ├── new_project.html
│   └── workspace.html
├── DatabaseManager.py
├── V2_GenerateProjectShort.py
├── database.db
├── main.py
└── requirements.txt


Project files:

DatabaseManager.py
```py
import json
import os
import sqlite3

from werkzeug.security import generate_password_hash, check_password_hash

DATABASE = 'database.db'

class DatabaseManager:
    def __init__(self, db_path=DATABASE):
        # Проверяем, существует ли база данных, если нет - создаем
        if not os.path.exists(DATABASE):
            open(DATABASE, 'w').close()

        self.db_path = db_path
        self.create_tables()

    def create_connection(self):
        conn = sqlite3.connect(self.db_path)
        # Для возврата строк как словарей
        conn.row_factory = sqlite3.Row
        return conn

    def create_tables(self):
        conn = self.create_connection()
        cursor = conn.cursor()

        # Создаем таблицу accounts
        cursor.execute('''
            CREATE TABLE IF NOT EXISTS accounts (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                username TEXT UNIQUE NOT NULL,
                password_hash TEXT NOT NULL
            )
        ''')

        # Создаем таблицу projects
        cursor.execute('''
            CREATE TABLE IF NOT EXISTS projects (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                author_id INTEGER NOT NULL,
                user_ids TEXT,
                data TEXT,
                FOREIGN KEY (author_id) REFERENCES accounts(id)
            )
        ''')

        # Создаем таблицу project_images
        cursor.execute('''
            CREATE TABLE IF NOT EXISTS project_images (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                project_id INTEGER NOT NULL,
                image_path TEXT NOT NULL,
                status TEXT NOT NULL DEFAULT 'unannotated',
                assigned_to INTEGER,
                FOREIGN KEY (project_id) REFERENCES projects(id),
                FOREIGN KEY (assigned_to) REFERENCES accounts(id)
            )
        ''')

        conn.commit()
        conn.close()

    # Методы для работы с аккаунтами
    def create_account(self, username, password):
        password_hash = generate_password_hash(password)
        conn = self.create_connection()
        cursor = conn.cursor()
        try:
            cursor.execute('''
                INSERT INTO accounts (username, password_hash) VALUES (?, ?)
            ''', (username, password_hash))
            conn.commit()
            return True
        except sqlite3.IntegrityError:
            # Пользователь с таким именем уже существует
            return False
        finally:
            conn.close()

    def get_account(self, username):
        conn = self.create_connection()
        cursor = conn.cursor()
        cursor.execute('''
            SELECT * FROM accounts WHERE username = ?
        ''', (username,))
        account = cursor.fetchone()
        conn.close()
        return account

    def get_user_by_id(self, user_id):
        conn = self.create_connection()
        cursor = conn.cursor()
        cursor.execute('''
            SELECT * FROM accounts WHERE id = ?
        ''', (user_id,))
        account = cursor.fetchone()
        conn.close()
        return account

    def get_user_by_username(self, username):
        """
        Возвращает пользователя как словарь по имени пользователя.
        """
        account = self.get_account(username)
        if account:
            return {
                'id': account['id'],
                'username': account['username']
                # Добавьте другие поля по необходимости
            }
        return None

    def verify_account(self, username, password):
        account = self.get_account(username)
        if account:
            return check_password_hash(account['password_hash'], password)
        return False

    # Методы для работы с проектами
    def create_project(self, author_id, user_ids, data, images):
        user_ids_json = json.dumps(user_ids)
        data_json = json.dumps(data)
        conn = self.create_connection()
        cursor = conn.cursor()
        cursor.execute('''
            INSERT INTO projects (author_id, user_ids, data) VALUES (?, ?, ?)
        ''', (author_id, user_ids_json, data_json))
        project_id = cursor.lastrowid

        # Добавляем изображения в таблицу project_images
        project_images = [(project_id, image_path) for image_path in images]
        cursor.executemany('''
            INSERT INTO project_images (project_id, image_path) VALUES (?, ?)
        ''', project_images)

        conn.commit()
        conn.close()
        return project_id

    def get_project(self, project_id):
        conn = self.create_connection()
        cursor = conn.cursor()
        cursor.execute('''
            SELECT * FROM projects WHERE id = ?
        ''', (project_id,))
        project = cursor.fetchone()
        conn.close()
        if project:
            project_dict = {
                'id': project['id'],
                'author_id': project['author_id'],
                'user_ids': json.loads(project['user_ids']) if project['user_ids'] else [],
                'data': json.loads(project['data']) if project['data'] else {}
            }
            return project_dict
        return None

    def update_project(self, project_id, user_ids=None, data=None):
        conn = self.create_connection()
        cursor = conn.cursor()
        if user_ids is not None:
            user_ids_json = json.dumps(user_ids)
            cursor.execute('''
                UPDATE projects SET user_ids = ? WHERE id = ?
            ''', (user_ids_json, project_id))
        if data is not None:
            data_json = json.dumps(data)
            cursor.execute('''
                UPDATE projects SET data = ? WHERE id = ?
            ''', (data_json, project_id))
        conn.commit()
        conn.close()


    # MAIN MAIN MAIN MAIN

    def get_projects_by_user(self, user_id):
        conn = self.create_connection()
        cursor = conn.cursor()

        # Get projects where the user is the author
        cursor.execute('''
            SELECT * FROM projects WHERE author_id = ?
        ''', (user_id,))
        authored_projects = cursor.fetchall()

        # Get projects where the user is a participant
        cursor.execute('''
            SELECT * FROM projects WHERE user_ids LIKE ?
        ''', (f'%{user_id}%',))
        participating_projects = cursor.fetchall()

        # Combine and remove duplicates
        projects = {project['id']: {
            'id': project['id'],
            'author_id': project['author_id'],
            'user_ids': json.loads(project['user_ids']) if project['user_ids'] else [],
            'data': json.loads(project['data']) if project['data'] else {}
        } for project in authored_projects + participating_projects}

        conn.close()
        return list(projects.values())

    def get_next_image_for_annotation(self, project_id, user_id):
        conn = self.create_connection()
        cursor = conn.cursor()
        try:
            # Start a transaction
            cursor.execute('BEGIN TRANSACTION')

            # Try to fetch an image already assigned to the user but not annotated
            cursor.execute('''
                SELECT * FROM project_images 
                WHERE project_id = ? AND status = 'unannotated' AND assigned_to = ?
                LIMIT 1
            ''', (project_id, user_id))
            image = cursor.fetchone()

            if not image:
                # Assign a new image to the user atomically
                cursor.execute('''
                    SELECT * FROM project_images 
                    WHERE project_id = ? AND status = 'unannotated' AND assigned_to IS NULL
                    LIMIT 1
                ''', (project_id,))
                image = cursor.fetchone()
                if image:
                    cursor.execute('''
                        UPDATE project_images SET assigned_to = ? WHERE id = ?
                    ''', (user_id, image['id']))
                    conn.commit()
                else:
                    # No unassigned images left
                    conn.commit()
                    return None
            else:
                conn.commit()
            return image
        except Exception as e:
            conn.rollback()
            print(f"Error in get_next_image_for_annotation: {e}")
            return None
        finally:
            conn.close()

    def get_project_progress(self, project_id):
        conn = self.create_connection()
        cursor = conn.cursor()
        cursor.execute('''
            SELECT COUNT(*) FROM project_images WHERE project_id = ?
        ''', (project_id,))
        total_images = cursor.fetchone()[0]

        cursor.execute('''
            SELECT COUNT(*) FROM project_images WHERE project_id = ? AND status = 'annotated'
        ''', (project_id,))
        annotated_images = cursor.fetchone()[0]
        conn.close()
        return total_images, annotated_images

    def get_project_image(self, image_id):
        conn = self.create_connection()
        cursor = conn.cursor()
        cursor.execute('''
            SELECT * FROM project_images WHERE id = ?
        ''', (image_id,))
        image = cursor.fetchone()
        conn.close()
        return image

    def update_image_status(self, image_id, status):
        conn = self.create_connection()
        cursor = conn.cursor()
        cursor.execute('''
            UPDATE project_images SET status = ? WHERE id = ?
        ''', (status, image_id))
        conn.commit()
        conn.close()

    def get_project_images_paginated(self, project_id, page, per_page):
        conn = self.create_connection()
        cursor = conn.cursor()
        offset = (page - 1) * per_page
        cursor.execute('''
            SELECT * FROM project_images WHERE project_id = ? LIMIT ? OFFSET ?
        ''', (project_id, per_page, offset))
        images = cursor.fetchall()
        conn.close()
        images_list = []
        for image in images:
            images_list.append({
                'id': image['id'],
                'image_path': image['image_path'],
                'status': image['status'],
                'assigned_to': image['assigned_to']
            })
        return images_list

    def get_project_images_count(self, project_id):
        conn = self.create_connection()
        cursor = conn.cursor()
        cursor.execute('''
            SELECT COUNT(*) FROM project_images WHERE project_id = ?
        ''', (project_id,))
        count = cursor.fetchone()[0]
        conn.close()
        return count

    def delete_project_image(self, image_id):
        conn = self.create_connection()
        cursor = conn.cursor()
        cursor.execute('''
            DELETE FROM project_images WHERE id = ?
        ''', (image_id,))
        conn.commit()
        conn.close()

    def delete_project(self, project_id):
        conn = self.create_connection()
        cursor = conn.cursor()
        # Get all images associated with the project
        cursor.execute('''
            SELECT * FROM project_images WHERE project_id = ?
        ''', (project_id,))
        images = cursor.fetchall()

        # Delete images from filesystem
        for image in images:
            try:
                os.remove(image['image_path'])
                annotation_path = os.path.splitext(image['image_path'])[0] + '.txt'
                if os.path.exists(annotation_path):
                    os.remove(annotation_path)
            except Exception as e:
                print(f"Error deleting image file: {e}")

        # Delete images from database
        cursor.execute('''
            DELETE FROM project_images WHERE project_id = ?
        ''', (project_id,))

        # Delete project
        cursor.execute('''
            DELETE FROM projects WHERE id = ?
        ''', (project_id,))

        conn.commit()
        conn.close()

    def get_project_progress_percent(self, project_id):
        total_images, annotated_images = self.get_project_progress(project_id)
        if total_images > 0:
            return (annotated_images / total_images) * 100
        else:
            return 0

    def unassign_user_from_project_images(self, project_id, user_id):
        conn = self.create_connection()
        cursor = conn.cursor()
        cursor.execute('''
            UPDATE project_images
            SET assigned_to = NULL
            WHERE project_id = ? AND assigned_to = ? AND status = 'unannotated'
        ''', (project_id, user_id))
        conn.commit()
        conn.close()

```


main.py
```py
import json
import os
import shutil

from flask import Flask, request, jsonify, render_template, make_response, send_file
from functools import wraps
import jwt
import datetime

from DatabaseManager import DatabaseManager

app = Flask(__name__)

# Секретный ключ для JWT
SECRET_KEY = 'your_secret_key'  # Замените на ваш секретный ключ
app.config['SECRET_KEY'] = SECRET_KEY

db_manager = DatabaseManager()

# Декоратор для защиты маршрутов, требующих аутентификации
def token_required(f):
    @wraps(f)
    def decorated(*args, **kwargs):
        token = request.cookies.get('auth_token')

        if not token:
            return jsonify({'message': 'Токен отсутствует! Пожалуйста, войдите в систему.'}), 401

        try:
            data = jwt.decode(token, app.config['SECRET_KEY'], algorithms=['HS256'])
            current_user = db_manager.get_user_by_username(data['username'])
            if not current_user:
                raise Exception('Пользователь не найден')
        except jwt.ExpiredSignatureError:
            return jsonify({'message': 'Срок действия токена истек. Пожалуйста, войдите в систему снова.'}), 401
        except Exception as e:
            return jsonify({'message': 'Токен недействителен!'}), 401

        return f(current_user, *args, **kwargs)
    return decorated

# Эндпоинты API

# Регистрация пользователя
@app.route('/api/register', methods=['POST'])
def register():
    data = request.json
    username = data.get('username')
    password = data.get('password')
    if username and password:
        success = db_manager.create_account(username, password)
        if success:
            return jsonify({'message': 'Аккаунт успешно создан'}), 201
        else:
            return jsonify({'message': 'Пользователь с таким именем уже существует'}), 400
    else:
        return jsonify({'message': 'Требуются логин и пароль'}), 400

# Аутентификация пользователя
@app.route('/api/login', methods=['POST'])
def login_api():
    data = request.json
    username = data.get('username')
    password = data.get('password')
    if username and password:
        valid = db_manager.verify_account(username, password)
        if valid:
            # Генерация JWT токена
            token = jwt.encode({
                'username': username,
                'exp': datetime.datetime.utcnow() + datetime.timedelta(hours=24)
            }, app.config['SECRET_KEY'], algorithm='HS256')

            # Создаем ответ и устанавливаем куки с токеном
            response = make_response(jsonify({'message': 'Вход выполнен успешно', 'token': token}))
            response.set_cookie('auth_token', token, httponly=True, samesite='Strict')  # Можно добавить 'secure=True' при использовании HTTPS
            return response, 200
        else:
            return jsonify({'message': 'Неверные учетные данные'}), 401
    else:
        return jsonify({'message': 'Требуются логин и пароль'}), 400

# Выход из аккаунта
@app.route('/api/logout', methods=['POST'])
def logout_api():
    response = make_response(jsonify({'message': 'Выход выполнен успешно'}))
    response.set_cookie('auth_token', '', expires=0)
    return response, 200

# Получение информации о проекте
@app.route('/api/projects/<int:project_id>', methods=['GET'])
@token_required
def get_project(current_user, project_id):
    project = db_manager.get_project(project_id)
    if project:
        # Проверка, имеет ли текущий пользователь доступ к проекту
        if project['author_id'] != current_user['id'] and current_user['id'] not in project['user_ids']:
            return jsonify({'message': 'Доступ запрещен'}), 403
        return jsonify(project), 200
    else:
        return jsonify({'message': 'Проект не найден'}), 404

# Обновление проекта
@app.route('/api/projects/<int:project_id>', methods=['PUT'])
@token_required
def update_project(current_user, project_id):
    project = db_manager.get_project(project_id)
    if not project:
        return jsonify({'message': 'Проект не найден'}), 404

    # Проверка, является ли текущий пользователь автором проекта
    if project['author_id'] != current_user['id']:
        return jsonify({'message': 'Только автор проекта может его обновлять'}), 403

    data = request.json
    user_ids = data.get('user_ids')
    project_data = data.get('data')
    db_manager.update_project(project_id, user_ids=user_ids, data=project_data)
    return jsonify({'message': 'Проект обновлен'}), 200

# Эндпоинт для получения информации об аккаунте
@app.route('/api/account', methods=['GET'])
@token_required
def get_account_info(current_user):
    print("get_account_info")
    return jsonify({
        'id': current_user['id'],
        'username': current_user['username']
        # Добавьте другие необходимые поля
    }), 200



# Главная страница
@app.route('/main')
def index():
    return render_template('index.html')

# Страница логина и регистрации
@app.route('/login')
def login():
    return render_template('login.html')

# Страница аккаунта
@app.route('/account')
def account():
    return render_template('account.html')


# MAIN MAIN MAIN MAIN

@app.route('/new_project')
@token_required
def new_project(current_user):
    return render_template('new_project.html')

@app.route('/workspace')
@token_required
def workspace(current_user):
    project_id = request.args.get('project_id', type=int)
    if not project_id:
        return "Project ID is required", 400
    return render_template('workspace.html', project_id=project_id)

# Получение списка проектов пользователя
# Создание нового проекта
@app.route('/api/projects', methods=['POST'])
@token_required
def create_project(current_user):
    # Получаем данные из формы
    title = request.form.get('title')
    path = request.form.get('path')
    classes_json = request.form.get('classes')
    images = request.files.getlist('images')

    if not title:
        return jsonify({'message': 'Title is required'}), 400

    if not images and not path:
        return jsonify({'message': 'Please upload images or specify a path'}), 400

    # Парсируем классы
    try:
        classes = json.loads(classes_json)
    except:
        return jsonify({'message': 'Invalid classes data'}), 400

    # Обрабатываем изображения
    image_paths = []

    # Директория для сохранения изображений проекта
    project_dir = os.path.join('projects', str(current_user['id']), title)
    os.makedirs(project_dir, exist_ok=True)

    # Сохраняем загруженные изображения
    for image in images:
        filename = image.filename
        image_path = os.path.join(project_dir, filename)
        image.save(image_path)
        image_paths.append(image_path)

    # Если указан путь, используем изображения из него
    if path:
        if os.path.exists(path) and os.path.isdir(path):
            for root, dirs, files in os.walk(path):
                for file in files:
                    if file.lower().endswith(('.png', '.jpg', '.jpeg', '.bmp', '.tiff')):
                        file_path = os.path.join(root, file)
                        dest_path = os.path.join(project_dir, file)
                        shutil.copy2(file_path, dest_path)
                        image_paths.append(dest_path)
        else:
            return jsonify({'message': 'Invalid path specified'}), 400

    # Подготавливаем данные проекта
    project_data = {
        'title': title,
        'classes': classes,
        # Другие необходимые данные
    }

    # Создаем проект в базе данных
    author_id = current_user['id']
    user_ids = []  # Пока что только автор
    project_id = db_manager.create_project(author_id, user_ids, project_data, image_paths)
    return jsonify({'message': 'Проект создан', 'project_id': project_id}), 201

######
# Получение изображения для аннотации
@app.route('/api/get_image', methods=['GET'])
@token_required
def get_image(current_user):
    project_id = request.args.get('project_id', type=int)
    if not project_id:
        return jsonify({'message': 'Project ID is required'}), 400

    project = db_manager.get_project(project_id)
    if not project:
        return jsonify({'message': 'Project not found'}), 404

    # Проверяем доступ пользователя к проекту
    if project['author_id'] != current_user['id'] and current_user['id'] not in project['user_ids']:
        return jsonify({'message': 'Access denied'}), 403

    # Получаем следующее неразмеченное изображение
    image_info = db_manager.get_next_image_for_annotation(project_id, current_user['id'])

    if not image_info:
        return jsonify({'message': 'No more images to annotate'}), 200

    # Получаем прогресс проекта
    total_images, annotated_images = db_manager.get_project_progress(project_id)
    progress = (annotated_images / total_images) * 100 if total_images > 0 else 0

    image_url = '/api/project_image/{}'.format(image_info['id'])

    response_data = {
        'image_id': image_info['id'],
        'image_url': image_url,
        'classes': project['data']['classes'],
        'current': annotated_images + 1,  # Предполагая, что мы собираемся аннотировать это изображение
        'total': total_images,
        'progress': progress,
        'project_title': project['data']['title']
    }
    return jsonify(response_data), 200

def convert_to_yolo_format(annotation):
    yolo_annotations = []
    for ann in annotation:
        class_id = ann['class_id']
        x_center = ann['x_center']
        y_center = ann['y_center']
        width = ann['width']
        height = ann['height']
        yolo_annotations.append(f"{class_id} {x_center} {y_center} {width} {height}")
    return '\n'.join(yolo_annotations)

# Отправка аннотации для изображения
@app.route('/api/submit_annotation', methods=['POST'])
@token_required
def submit_annotation(current_user):
    data = request.json
    project_id = data.get('project_id')
    image_id = data.get('image_id')
    annotation = data.get('annotation')

    if not project_id or not image_id or annotation is None:
        return jsonify({'message': 'project_id, image_id and annotation are required'}), 400

    project = db_manager.get_project(project_id)
    if not project:
        return jsonify({'message': 'Project not found'}), 404

    # Проверяем доступ пользователя к проекту
    if project['author_id'] != current_user['id'] and current_user['id'] not in project['user_ids']:
        return jsonify({'message': 'Access denied'}), 403

    # Сохраняем аннотацию в формате YOLO
    image_info = db_manager.get_project_image(image_id)
    if not image_info:
        return jsonify({'message': 'Image not found'}), 404

    # Проверяем, что изображение назначено текущему пользователю
    if image_info['assigned_to'] != current_user['id']:
        return jsonify({'message': 'This image is not assigned to you'}), 403

    # Путь к изображению
    image_path = image_info['image_path']
    annotation_path = os.path.splitext(image_path)[0] + '.txt'

    # Преобразуем аннотацию в формат YOLO
    yolo_annotation = convert_to_yolo_format(annotation)

    with open(annotation_path, 'w') as f:
        f.write(yolo_annotation)

    # Обновляем статус изображения на "annotated"
    db_manager.update_image_status(image_id, 'annotated')

    return jsonify({'message': 'Annotation submitted'}), 200

# Сервис для безопасной выдачи изображений
@app.route('/api/project_image/<int:image_id>')
@token_required
def project_image(current_user, image_id):
    image_info = db_manager.get_project_image(image_id)
    if not image_info:
        return "Image not found", 404

    project = db_manager.get_project(image_info['project_id'])
    if not project:
        return "Project not found", 404

    # Проверяем доступ пользователя к проекту
    if project['author_id'] != current_user['id'] and current_user['id'] not in project['user_ids']:
        return "Access denied", 403

    return send_file(image_info['image_path'])

# Получение списка проектов пользователя
@app.route('/api/projects', methods=['GET'])
@token_required
def get_user_projects(current_user):
    projects = db_manager.get_projects_by_user(current_user['id'])
    return jsonify({'projects': projects}), 200

# Update project title
@app.route('/api/projects/<int:project_id>/update_title', methods=['POST'])
@token_required
def update_project_title(current_user, project_id):
    data = request.json
    new_title = data.get('title')

    if not new_title:
        return jsonify({'message': 'Title is required'}), 400

    project = db_manager.get_project(project_id)
    if not project:
        return jsonify({'message': 'Project not found'}), 404

    if project['author_id'] != current_user['id']:
        return jsonify({'message': 'Only the author can update the project title'}), 403

    # Update the project's data
    project_data = project['data']
    project_data['title'] = new_title
    db_manager.update_project(project_id, data=project_data)

    return jsonify({'message': 'Project title updated successfully'}), 200

# Get images of a project with pagination
@app.route('/api/projects/<int:project_id>/images', methods=['GET'])
@token_required
def get_project_images(current_user, project_id):
    page = request.args.get('page', default=1, type=int)
    per_page = request.args.get('per_page', default=20, type=int)

    project = db_manager.get_project(project_id)
    if not project:
        return jsonify({'message': 'Project not found'}), 404

    if project['author_id'] != current_user['id']:
        return jsonify({'message': 'Access denied'}), 403

    images = db_manager.get_project_images_paginated(project_id, page, per_page)
    total_images = db_manager.get_project_images_count(project_id)

    return jsonify({'images': images, 'total_images': total_images}), 200

# Delete an image from a project
@app.route('/api/projects/<int:project_id>/images/<int:image_id>', methods=['DELETE'])
@token_required
def delete_project_image(current_user, project_id, image_id):
    project = db_manager.get_project(project_id)
    if not project:
        return jsonify({'message': 'Project not found'}), 404

    if project['author_id'] != current_user['id']:
        return jsonify({'message': 'Access denied'}), 403

    image_info = db_manager.get_project_image(image_id)
    if not image_info or image_info['project_id'] != project_id:
        return jsonify({'message': 'Image not found in this project'}), 404

    # Delete image file
    try:
        os.remove(image_info['image_path'])
        annotation_path = os.path.splitext(image_info['image_path'])[0] + '.txt'
        if os.path.exists(annotation_path):
            os.remove(annotation_path)
    except Exception as e:
        print(f"Error deleting image file: {e}")

    # Delete image from database
    db_manager.delete_project_image(image_id)

    return jsonify({'message': 'Image deleted successfully'}), 200

# Delete a project
@app.route('/api/projects/<int:project_id>', methods=['DELETE'])
@token_required
def delete_project(current_user, project_id):
    project = db_manager.get_project(project_id)
    if not project:
        return jsonify({'message': 'Project not found'}), 404

    if project['author_id'] != current_user['id']:
        return jsonify({'message': 'Only the author can delete the project'}), 403

    db_manager.delete_project(project_id)

    return jsonify({'message': 'Project deleted successfully'}), 200

######

@app.route('/edit_project')
@token_required
def edit_project(current_user):
    project_id = request.args.get('project_id', type=int)
    if not project_id:
        return "Project ID is required", 400

    project = db_manager.get_project(project_id)
    if not project:
        return "Project not found", 404

    if project['author_id'] != current_user['id']:
        return "Access denied", 403

    return render_template('edit_project.html', project_id=project_id)

@app.route('/join')
@token_required
def join_project_page(current_user):
    return render_template('join.html')


# Get project info for joining
@app.route('/api/projects/<int:project_id>/info', methods=['GET'])
@token_required
def get_project_info(current_user, project_id):
    project = db_manager.get_project(project_id)
    if not project:
        return jsonify({'message': 'Project not found'}), 404

    project_info = {
        'id': project['id'],
        'title': project['data'].get('title', 'Untitled'),
        'author': db_manager.get_user_by_id(project['author_id'])['username'],
        'total_images': db_manager.get_project_images_count(project_id),
        'classes': project['data'].get('classes', []),
        'progress': db_manager.get_project_progress_percent(project_id)
    }

    return jsonify({'project': project_info}), 200

# Join a project
@app.route('/api/projects/<int:project_id>/join', methods=['POST'])
@token_required
def join_project(current_user, project_id):
    project = db_manager.get_project(project_id)
    if not project:
        return jsonify({'message': 'Project not found'}), 404

    if current_user['id'] == project['author_id']:
        return jsonify({'message': 'You are the author of this project'}), 400

    if current_user['id'] in project['user_ids']:
        return jsonify({'message': 'You are already a participant in this project'}), 400

    project['user_ids'].append(current_user['id'])
    db_manager.update_project(project_id, user_ids=project['user_ids'])

    return jsonify({'message': 'You have joined the project'}), 200

# Leave a project
@app.route('/api/projects/<int:project_id>/leave', methods=['POST'])
@token_required
def leave_project(current_user, project_id):
    project = db_manager.get_project(project_id)
    if not project:
        return jsonify({'message': 'Project not found'}), 404

    if current_user['id'] == project['author_id']:
        return jsonify({'message': 'You are the author of this project and cannot leave it. Consider deleting the project if you no longer need it.'}), 400

    if current_user['id'] not in project['user_ids']:
        return jsonify({'message': 'You are not a participant in this project'}), 400

    project['user_ids'].remove(current_user['id'])
    db_manager.update_project(project_id, user_ids=project['user_ids'])

    # Optionally, unassign images assigned to the user in this project
    db_manager.unassign_user_from_project_images(project_id, current_user['id'])

    return jsonify({'message': 'You have left the project'}), 200

if __name__ == '__main__':
    app.run(debug=True, host="0.0.0.0", port=8081)

```


static/js/account_manager.js
```js
// account_manager.js

var AccountManager = (function() {

    // Приватные функции для управления куки
    function setCookie(name, value, days) {
        var expires = "";
        if (days) {
            var date = new Date();
            date.setTime(date.getTime() + (days*24*60*60*1000));
            expires = "; expires=" + date.toUTCString();
        }
        // Кодируем значение перед установкой
        document.cookie = name + "=" + encodeURIComponent(value || "")  + expires + "; path=/";
    }

    function getCookie(name) {
        var nameEQ = name + "=";
        var ca = document.cookie.split(';');
        for(var i=0;i < ca.length;i++) {
            var c = ca[i].trim(); // Используем trim() для удаления лишних пробелов
            if (c.indexOf(nameEQ) === 0) {
                // Декодируем значение перед возвратом
                return decodeURIComponent(c.substring(nameEQ.length,c.length));
            }
        }
        return null;
    }

    function eraseCookie(name) {
        document.cookie = name + '=;expires=Thu, 01 Jan 1970 00:00:01 GMT;';
    }

    // Публичные функции
    return {

        // Установка токена в куки
        setToken: function(token, days=7) {
            setCookie('auth_token', token, days);
        },

        // Получение токена из куки
        getToken: function() {
            return getCookie('auth_token');
        },

        // Выход из аккаунта
        logout: function(callback) {
            $.ajax({
                url: '/api/logout',
                type: 'POST',
                success: function(response){
                    eraseCookie('auth_token'); // Если куки доступны
                    if(callback && typeof callback === 'function') {
                        callback(null, response);
                    }
                    // Перенаправляем пользователя на страницу входа или главную страницу
                    window.location.href = '/login';
                },
                error: function(xhr){
                    if(callback && typeof callback === 'function') {
                        if(xhr.responseJSON && xhr.responseJSON.message){
                            callback(xhr.responseJSON.message, null);
                        } else {
                            callback("Произошла ошибка при выходе из аккаунта.", null);
                        }
                    }
                }
            });
        },

        // Получение информации об аккаунте
        getAccountInfo: function(callback) {
            $.ajax({
                url: '/api/account',
                type: 'GET',
                // Куки отправляются автоматически, так как тот же домен
                // Если ваш API находится на другом домене, добавьте withCredentials: true
                success: function(response){
                    if(callback && typeof callback === 'function') {
                        callback(null, response);
                    }
                },
                error: function(xhr){
                    if(callback && typeof callback === 'function') {
                        if(xhr.responseJSON && xhr.responseJSON.message){
                            callback(xhr.responseJSON.message, null);
                        } else {
                            callback("Произошла ошибка при получении информации об аккаунте.", null);
                        }
                    }
                }
            });
        },

        // Проверка, авторизован ли пользователь
        isAuthenticated: function() {
            return !!this.getToken();
        }

    };

})();
```


static/js/account_page.js
```js
$(document).ready(function() {
    // Получаем информацию об аккаунте при загрузке страницы
    AccountManager.getAccountInfo(function(error, data){
        if(error){
            // Опционально: перенаправить на страницу входа
            window.location.href = '/login';
        } else {
            $('#account-title').text(data.username);
            $('#account-title').removeClass("loading")
        }
    });

    // Обработчик кнопки выхода
    $('#logout-btn').on('click', function(){
        AccountManager.logout();
    });
});
```


static/js/edit_project.js
```js
$(document).ready(function() {
    var urlParams = new URLSearchParams(window.location.search);
    var projectId = urlParams.get('project_id');

    if (!projectId) {
        alert("Project ID is required.");
        window.location.href = '/main';
        return;
    }

    $('#project-id').val(projectId);

    // Copy project ID button
    $('#copy-project-id-btn').on('click', function() {
        var copyText = document.getElementById("project-id");
        copyText.select();
        document.execCommand("copy");
    });

    var currentPage = 1;
    var imagesPerPage = 20;
    var totalPages = 1;
    var totalImages = 0;

    // Fetch project info
    $.ajax({
        url: '/api/projects/' + projectId,
        type: 'GET',
        success: function(response) {
            $('#project-title-input').val(response.data.title || '');
            totalImages = response.total_images || 0;
            totalPages = Math.ceil(totalImages / imagesPerPage);

            loadImages();

            updatePageInfo();
        },
        error: function(xhr) {
            alert("Error fetching project info.");
            window.location.href = '/main';
        }
    });

    // Save title button handler
    $('#save-title-btn').on('click', function() {
        var newTitle = $('#project-title-input').val().trim();

        $.ajax({
            url: '/api/projects/' + projectId + '/update_title',
            type: 'POST',
            contentType: 'application/json',
            data: JSON.stringify({ title: newTitle }),
            success: function(response) {
                alert("Title updated successfully.");
            },
            error: function(xhr) {
                alert("Error updating title.");
            }
        });
    });

    // Delete project button handler
    $('#delete-project-btn').on('click', function() {
        if (confirm("Are you sure you want to delete this project? This action cannot be undone.")) {
            $.ajax({
                url: '/api/projects/' + projectId,
                type: 'DELETE',
                success: function(response) {
                    alert(response.message);
                    window.location.href = '/main';
                },
                error: function(xhr) {
                    alert("Error deleting project.");
                }
            });
        }
    });

    function loadImages() {
        $.ajax({
            url: '/api/projects/' + projectId + '/images',
            type: 'GET',
            data: { page: currentPage, per_page: imagesPerPage },
            success: function(response) {
                var imagesContainer = $('#images-container');
                imagesContainer.empty();

                response.images.forEach(function(image) {
                    var imageElement = `
                        <div class="image-item">
                            <img src="/api/project_image/${image.id}" alt="Image ${image.id}">
                            <button class="delete-image-btn" data-image-id="${image.id}">Delete</button>
                        </div>
                    `;
                    imagesContainer.append(imageElement);
                });

                // Delete image button handler
                $('.delete-image-btn').on('click', function() {
                    var imageId = $(this).data('image-id');
                    if (confirm("Are you sure you want to delete this image? This action cannot be undone.")) {
                        $.ajax({
                            url: '/api/projects/' + projectId + '/images/' + imageId,
                            type: 'DELETE',
                            success: function(response) {
                                alert(response.message);
                                // Reload images
                                loadImages();
                            },
                            error: function(xhr) {
                                alert("Error deleting image.");
                            }
                        });
                    }
                });
            },
            error: function(xhr) {
                alert("Error fetching images.");
            }
        });
    }

    // Pagination buttons
    $('#prev-page-btn').on('click', function() {
        if (currentPage > 1) {
            currentPage--;
            loadImages();
            updatePageInfo();
        }
    });

    $('#next-page-btn').on('click', function() {
        if (currentPage < totalPages) {
            currentPage++;
            loadImages();
            updatePageInfo();
        }
    });

    function updatePageInfo() {
        $('#page-info').text(`Page ${currentPage} of ${totalPages}`);
    }
});
```


static/js/hotbar-manager.js
```js
$(document).ready(function() {
    $('#home-btn').on('click', function(){
        window.location.href = '/main';
    });

    $('#profile-btn').on('click', function(){
        window.location.href = '/account';
    });
});
```


static/js/join.js
```js
$(document).ready(function() {
    var currentProjectId = null;

    $('#project-id-input').on('input', function() {
        var projectId = $(this).val().trim();
        if (projectId) {
            $.ajax({
                url: '/api/projects/' + projectId + '/info',
                type: 'GET',
                success: function(response) {
                    var project = response.project;
                    var projectInfo = `
                        <div class="project-title">${project.title}</div>
                        <div class="project-author"><strong>Author:</strong> ${project.author}</div>
                        <div class="project-add"><strong>Total Images:</strong> ${project.total_images}</div>
                        <div class="project-add"><strong>Classes:</strong> ${project.classes.map(c => c.name).join(', ')}</div>
                        <div class="project-progress"><strong>Progress:</strong> ${Math.round(project.progress)}%</div>
                    `;
                    $('#project-info').html(projectInfo).show();
                    $('#join-btn').show();
                    currentProjectId = project.id;
                },
                error: function(xhr) {
                    $('#project-info').hide();
                    $('#join-btn').hide();
                    currentProjectId = null;
                }
            });
        } else {
            $('#project-info').hide();
            $('#join-btn').hide();
            currentProjectId = null;
        }
    });

    $('#join-btn').on('click', function() {
        if (currentProjectId) {
            $.ajax({
                url: '/api/projects/' + currentProjectId + '/join',
                type: 'POST',
                success: function(response) {
                    window.location.href = '/main';
                },
                error: function(xhr) {
                    if(xhr.responseJSON && xhr.responseJSON.message){
                        alert(xhr.responseJSON.message);
                    } else {
                        alert("An error occurred. Please try again later.");
                    }
                }
            });
        }
    });
});
```


static/js/login.js
```js
// login.js

$(document).ready(function() {

    // Обработчик кнопки логина
    $('#login-btn').on('click', function(e){
        e.preventDefault(); // Предотвращаем перезагрузку страницы

        var username = $('.login-field').val().trim();
        var password = $('.password-field').val().trim();

        if(username === "" || password === "") {
            alert("Пожалуйста, введите логин и пароль.");
            return;
        }

        $.ajax({
            url: '/api/login',
            type: 'POST',
            contentType: 'application/json',
            data: JSON.stringify({username: username, password: password}),
            success: function(response){
                if(response.token){
                    // Используем AccountManager для сохранения токена
                    AccountManager.setToken(response.token, 7);
                    window.location.href = '/main';
                } else {
                    alert("Токен не получен. Повторите попытку.");
                }
            },
            error: function(xhr){
                if(xhr.responseJSON && xhr.responseJSON.message){
                    alert(xhr.responseJSON.message);
                } else {
                    alert("Произошла ошибка. Пожалуйста, попробуйте позже.");
                }
            }
        });
    });

    // Обработчик кнопки регистрации
    $('#register-btn').on('click', function(e){
        e.preventDefault(); // Предотвращаем перезагрузку страницы

        var username = $('.login-field').val().trim();
        var password = $('.password-field').val().trim();

        if(username === "" || password === "") {
            alert("Пожалуйста, введите логин и пароль.");
            return;
        }

        $.ajax({
            url: '/api/register',
            type: 'POST',
            contentType: 'application/json',
            data: JSON.stringify({username: username, password: password}),
            success: function(response){
                alert(response.message);
                // Очистка полей и предложение войти
                $('.login-field').val('');
                $('.password-field').val('');
            },
            error: function(xhr){
                if(xhr.responseJSON && xhr.responseJSON.message){
                    alert(xhr.responseJSON.message);
                } else {
                    alert("Произошла ошибка. Пожалуйста, попробуйте позже.");
                }
            }
        });
    });

});
```


static/js/main.js
```js
$(document).ready(function() {
    var currentUserId = null;

    // Fetch account info on page load
    AccountManager.getAccountInfo(function(error, data){
        if(error){
            window.location.href = '/login';
        } else {
            $('#account-title').text("Hey, " + data.username + "!");
            $('#account-title').removeClass("loading")
            currentUserId = data.id;

            // Now fetch user's projects
            $.ajax({
                url: '/api/projects',
                type: 'GET',
                success: function(response){
                    var projectsContainer = $('.projects-container');
                    projectsContainer.empty(); // Clear projects container

                    if(response.projects.length === 0){
                        projectsContainer.append('<div class="no-projects">You have no projects.</div>');
                    } else {
                        response.projects.forEach(function(project){
                            var projectElement = `
                                <div class="project-container">
                                    <div class="project-title">${project.data.title || 'Untitled'}</div>
                                    <button class="project-action-btn continue-btn" data-project-id="${project.id}">Continue</button>
                            `;

                            // Check if the user is the author
                            if(project.author_id === currentUserId){
                                projectElement += `<button class="project-action-btn edit-btn" data-project-id="${project.id}">Edit</button>`;
                            } else {
                                projectElement += `<button class="project-action-btn leave-btn" data-project-id="${project.id}">Leave</button>`;
                            }

                            projectElement += `</div>`;
                            projectsContainer.append(projectElement);
                        });

                        // Handler for "Continue" button
                        $('.continue-btn').on('click', function(){
                            var projectId = $(this).data('project-id');
                            window.location.href = '/workspace?project_id=' + projectId;
                        });

                        // Handler for "Edit" button
                        $('.edit-btn').on('click', function(){
                            var projectId = $(this).data('project-id');
                            window.location.href = '/edit_project?project_id=' + projectId;
                        });

                        // Handler for "Leave" button
                        $('.leave-btn').on('click', function(){
                            var projectId = $(this).data('project-id');
                            if(confirm("Are you sure you want to leave this project?")){
                                $.ajax({
                                    url: '/api/projects/' + projectId + '/leave',
                                    type: 'POST',
                                    success: function(response){
                                        location.reload();
                                    },
                                    error: function(xhr){
                                        if(xhr.responseJSON && xhr.responseJSON.message){
                                            alert(xhr.responseJSON.message);
                                        } else {
                                            alert("An error occurred. Please try again later.");
                                        }
                                    }
                                });
                            }
                        });
                    }
                },
                error: function(xhr){
                    console.error("Error fetching user projects.");
                    if(xhr.responseJSON && xhr.responseJSON.message){
                        alert(xhr.responseJSON.message);
                    } else {
                        alert("An error occurred. Please try again later.");
                    }
                }
            });
        }
    });

    // Handler for "Logout" button
    $('#logout-btn').on('click', function(){
        AccountManager.logout();
    });

    // Handler for "New Project" button
    $('#new-project-btn').on('click', function(){
        window.location.href = '/new_project';
    });

    // Handler for "Join Project" button
    $('#join-project-btn').on('click', function(){
        window.location.href = '/join';
    });
});
```


static/js/new_project.js
```js
$(document).ready(function() {
    // Обработчик добавления нового класса
    $('#add-class-btn').on('click', function(){
        var classItem = `
            <div class="class-item">
                <input type="text" name="class-name" placeholder="Class Name">
                <input type="color" name="class-color" value="#ff0000">
                <button type="button" class="remove-class-btn">Remove</button>
            </div>
        `;
        $('#classes-container').append(classItem);
    });

    // Обработчик удаления класса
    $(document).on('click', '.remove-class-btn', function(){
        $(this).closest('.class-item').remove();
    });

    // Обработчик формы создания проекта
    $('#new-project-form').on('submit', function(e){
        e.preventDefault();

        var title = $('#project-title').val().trim();
        var images = $('#project-images')[0].files;
        var path = $('#project-path').val().trim();

        if(title === ""){
            alert("Please enter a project title.");
            return;
        }

        if(images.length === 0 && path === ""){
            alert("Please upload images or specify a path.");
            return;
        }

        // Собираем классы
        var classes = [];
        $('.class-item').each(function(){
            var className = $(this).find('input[name="class-name"]').val().trim();
            var classColor = $(this).find('input[name="class-color"]').val();
            if(className !== ""){
                classes.push({
                    name: className,
                    color: classColor
                });
            }
        });

        if(classes.length === 0){
            alert("Please add at least one class.");
            return;
        }

        var formData = new FormData();
        formData.append('title', title);
        formData.append('path', path);
        formData.append('classes', JSON.stringify(classes));

        // Добавляем файлы изображений
        for(var i = 0; i < images.length; i++){
            formData.append('images', images[i]);
        }

        // Отправка запроса с прогрессом загрузки
        $.ajax({
            xhr: function() {
                var xhr = new window.XMLHttpRequest();
                xhr.upload.addEventListener("progress", function(evt){
                    if (evt.lengthComputable) {
                        var percentComplete = (evt.loaded / evt.total) * 100;
                        $('#upload-progress').show();
                        $('#progress-bar').val(percentComplete);
                        $('#progress-text').text(Math.round(percentComplete) + '% uploaded');
                    }
                }, false);
                return xhr;
            },
            url: '/api/projects',
            type: 'POST',
            data: formData,
            contentType: false,
            processData: false,
            success: function(response){
                alert("Project created successfully!");
                window.location.href = '/main';
            },
            error: function(xhr){
                if(xhr.responseJSON && xhr.responseJSON.message){
                    alert(xhr.responseJSON.message);
                } else {
                    alert("An error occurred. Please try again later.");
                }
            }
        });
    });
});
```


static/js/workspace.js
```js
$(document).ready(function () {
    var urlParams = new URLSearchParams(window.location.search);
    var projectId = urlParams.get('project_id');

    if (!projectId) {
        alert("Project ID is required.");
        window.location.href = '/main';
        return;
    }

    var currentImageId = null;
    var classes = [];
    var selectedClass = null;
    var isDrawing = false;
    var startX, startY;
    var annotationMode = false;

    var $imageContainer = $('.image-container');
    var imageContainerOffset;

    // Fetch account info
    AccountManager.getAccountInfo(function (error, data) {
        if (error) {
            window.location.href = '/login';
        } else {
            $('#username').text(data.username);
        }
    });

    // Fetch the image and project info
    $.ajax({
        url: '/api/get_image',
        type: 'GET',
        data: { project_id: projectId },
        success: function (response) {
            if (response.message) {
                alert(response.message);
                window.location.href = '/main';
                return;
            }

            $('#project-title').text(response.project_title || 'Project');
            $('#project-progress').val(response.progress);
            $('#progress-text').text('Image ' + response.current + ' of ' + response.total + ' (' + Math.round(response.progress) + '%)');

            // Set up classes
            classes = response.classes;
            var $classList = $('#class-list');
            $classList.empty();
            classes.forEach(function (cls, index) {
                var $classItem = $('<div class="class-item"></div>');
                $classItem.text(cls.name);
                $classItem.css('background-color', cls.color);
                $classItem.data('classIndex', index);
                $classList.append($classItem);
            });

            // Class selection handler
            $('.class-item').on('click', function () {
                selectedClass = classes[$(this).data('classIndex')];

                // Highlight selected class
                $('.class-item').removeClass('selected');
                $(this).addClass('selected');

                // Enter annotation mode
                annotationMode = true;
                $('#selected-class-name').text(selectedClass.name);
                $('#annotation-mode-indicator').removeClass('hidden');
            });

            // Load image
            currentImageId = response.image_id;
            var imageUrl = response.image_url;
            var $imageDisplay = $('#image-display');
            $imageDisplay.attr('src', imageUrl);

            // Set up image load handler
            $imageDisplay.on('load', function () {
                // Adjust image container dimensions
                $imageContainer.css({
                    'width': $imageDisplay.width(),
                    'height': $imageDisplay.height()
                });
                imageContainerOffset = $imageContainer.offset();
            });

            // Mouse events for drawing annotations
            $imageContainer.on('mousedown', function (e) {
                if (annotationMode && selectedClass) {
                    e.preventDefault();
                    isDrawing = true;
                    var offset = getMousePosition(e);
                    startX = offset.x;
                    startY = offset.y;

                    // Create a new annotation element
                    var $annotation = $('<div class="annotation"></div>');
                    $annotation.css({
                        'left': startX,
                        'top': startY,
                        'border-color': selectedClass.color,
                    });
                    $annotation.data('class', selectedClass);
                    $annotation.append('<div class="delete-handle">&times;</div>');
                    $annotation.on('mousedown', annotationMouseDown);

                    // Добавляем угловые точки ресайза
                    $annotation.append('<div class="resize-handle nw"></div>');
                    $annotation.append('<div class="resize-handle sw"></div>');
                    $annotation.append('<div class="resize-handle se"></div>');

                    // События для перетаскивания угловых точек
                    $annotation.find('.resize-handle').on('mousedown', resizeMouseDown);

                    // Add to image container
                    $imageContainer.append($annotation);

                    // Attach delete handler
                    $annotation.find('.delete-handle').on('click', function (e) {
                        e.stopPropagation();
                        $(this).parent('.annotation').remove();
                    });

                    // Store current annotation element
                    $imageContainer.data('currentAnnotation', $annotation);
                }
            });

            $(document).on('mousemove', function (e) {
                if (isDrawing) {
                    var offset = getMousePosition(e);
                    var currentX = offset.x;
                    var currentY = offset.y;
                    var width = currentX - startX;
                    var height = currentY - startY;

                    var $annotation = $imageContainer.data('currentAnnotation');
                    $annotation.css({
                        'width': Math.abs(width),
                        'height': Math.abs(height),
                        'left': width < 0 ? currentX : startX,
                        'top': height < 0 ? currentY : startY
                    });
                }
            });

            $(document).on('mouseup', function (e) {
                if (isDrawing) {
                    isDrawing = false;
                    $imageContainer.removeData('currentAnnotation');

                    // После установки маркера автоматически выходим из режима установки
                    annotationMode = false;
                    $('#annotation-mode-indicator').addClass('hidden');
                    $('.class-item').removeClass('selected');
                    selectedClass = null;
                }
            });

            /////////////
            ////////

            // Функция для ресайза
            function resizeMouseDown(e) {
                e.stopPropagation();
                e.preventDefault();

                var $handle = $(this);
                var $annotation = $handle.parent();
                var startX = e.pageX;
                var startY = e.pageY;
                var startWidth = $annotation.width();
                var startHeight = $annotation.height();
                var startLeft = parseInt($annotation.css('left'));
                var startTop = parseInt($annotation.css('top'));

                $(document).on('mousemove.resize', function (e) {
                    var dx = e.pageX - startX;
                    var dy = e.pageY - startY;

                    if ($handle.hasClass('nw')) {
                        $annotation.css({
                            'left': startLeft + dx,
                            'top': startTop + dy,
                            'width': startWidth - dx,
                            'height': startHeight - dy
                        });
                    } else if ($handle.hasClass('ne')) {
                        $annotation.css({
                            'top': startTop + dy,
                            'width': startWidth + dx,
                            'height': startHeight - dy
                        });
                    } else if ($handle.hasClass('sw')) {
                        $annotation.css({
                            'left': startLeft + dx,
                            'width': startWidth - dx,
                            'height': startHeight + dy
                        });
                    } else if ($handle.hasClass('se')) {
                        $annotation.css({
                            'width': startWidth + dx,
                            'height': startHeight + dy
                        });
                    }
                });

                $(document).on('mouseup.resize', function () {
                    $(document).off('.resize');
                });
            }
            //////
            ////////

            // Cancel annotation mode
            $('#cancel-annotation-btn').on('click', function () {
                annotationMode = false;
                $('#annotation-mode-indicator').addClass('hidden');
                $('.class-item').removeClass('selected');
                selectedClass = null;
            });
        },
        error: function (xhr) {
            alert("An error occurred while fetching the image.");
            window.location.href = '/main';
        }
    });

    // Helper to get the mouse position relative to the image container
    // Обновление функции getMousePosition
    function getMousePosition(e) {
        var parentOffset = $imageContainer.offset();
        var x = e.pageX - parentOffset.left;
        var y = e.pageY - parentOffset.top;

        return { x: x, y: y };
    }

    // Annotation movement and resizing
    function annotationMouseDown(e) {
        if ($(e.target).hasClass('delete-handle')) {
            return; // Ignore delete handle clicks
        }
        e.stopPropagation();

        var $annotation = $(this);
        var startOffset = $annotation.offset();
        var startX = e.pageX;
        var startY = e.pageY;

        $(document).on('mousemove.annotation', function (e) {
            var dx = e.pageX - startX;
            var dy = e.pageY - startY;
            $annotation.css({
                'left': startOffset.left + dx - imageContainerOffset.left,
                'top': startOffset.top + dy - imageContainerOffset.top
            });
        });

        $(document).on('mouseup.annotation', function () {
            $(document).off('.annotation');
        });
    }

    // Next button click handler
    $('#next-btn').on('click', function () {
        // Collect annotation data
        var annotations = [];
        $('.annotation').each(function () {
            var $annotation = $(this);
            var cls = $annotation.data('class');
            var left = parseInt($annotation.css('left'));
            var top = parseInt($annotation.css('top'));
            var width = $annotation.width();
            var height = $annotation.height();

            // Normalize coordinates relative to image dimensions
            var imageWidth = $imageContainer.width();
            var imageHeight = $imageContainer.height();

            var x_center = (left + width / 2) / imageWidth;
            var y_center = (top + height / 2) / imageHeight;
            var w = width / imageWidth;
            var h = height / imageHeight;

            annotations.push({
                class_id: classes.indexOf(cls),
                x_center: x_center,
                y_center: y_center,
                width: w,
                height: h
            });
        });

        // Send annotations to server
        $.ajax({
            url: '/api/submit_annotation',
            type: 'POST',
            contentType: 'application/json',
            data: JSON.stringify({
                project_id: projectId,
                image_id: currentImageId,
                annotation: annotations
            }),
            success: function (response) {
                // Reload the page for the next image
                location.reload();
            },
            error: function (xhr) {
                alert("An error occurred while submitting the annotation.");
            }
        });
    });
    // Следим за движением мыши над изображением
    $imageContainer.on('mousemove', function (e) {
        var mousePos = getMousePosition(e);

        // Для каждой аннотации проверяем расстояние до ручек ресайза
        $('.annotation').each(function () {
            var $annotation = $(this);
            var annotationOffset = $annotation.position();
            var annotationWidth = $annotation.width();
            var annotationHeight = $annotation.height();

            // Массив ручек ресайза
            var handles = [
                { name: 'nw', x: annotationOffset.left, y: annotationOffset.top },
                { name: 'sw', x: annotationOffset.left, y: annotationOffset.top + annotationHeight },
                { name: 'se', x: annotationOffset.left + annotationWidth, y: annotationOffset.top + annotationHeight }
            ];

            // Проверяем каждую ручку
            $annotation.find('.resize-handle').each(function (index) {
                var $handle = $(this);
                var handlePos = handles[index];
                var distance = Math.hypot(mousePos.x - handlePos.x, mousePos.y - handlePos.y);

                if (distance <= 20) {
                    $handle.addClass('visible');
                } else {
                    $handle.removeClass('visible');
                }
            });
        });
    });

    // Когда курсор покидает область изображения, скрываем все ручки ресайза
    $imageContainer.on('mouseleave', function () {
        $('.resize-handle').removeClass('visible');
    });
});
```


templates/account.html
```html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>DarkDataset</title>
    <link rel="preconnect" href="https://fonts.googleapis.com">
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
    <link href="https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&family=Roboto+Slab:wght@100..900&family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap" rel="stylesheet">
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
    <link rel="stylesheet" href="../static/css/loading.css">
    <link rel="stylesheet" href="../static/css/base.css">
    <link rel="stylesheet" href="../static/css/hotbar.css">

    <link rel="stylesheet" href="../static/css/tab.css">
    <link rel="stylesheet" href="../static/css/account-tab.css">
</head>
<body>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="../static/js/account_manager.js"></script>
<script src="../static/js/account_page.js"></script>

<div class="screen">
    {% include "components/hotbar.html" %}
    {% include "components/account_tab.html" %}
    <!--{% include "components/home_tab.html" %}-->
    <!--{% include "components/windows.html" %}-->
</div>
</body>
</html>
```


templates/edit_project.html
```html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Edit Project - DarkDataset</title>
    <link rel="preconnect" href="https://fonts.googleapis.com">
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
    <link href="https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&family=Roboto+Slab:wght@100..900&family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap" rel="stylesheet">
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
    <link rel="stylesheet" href="../static/css/loading.css">
    <link rel="stylesheet" href="../static/css/base.css">
    <link rel="stylesheet" href="../static/css/hotbar.css">

    <link rel="stylesheet" href="../static/css/tab.css">
    <link rel="stylesheet" href="../static/css/edit_project.css">
</head>
<body>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="../static/js/account_manager.js"></script>
<script src="../static/js/edit_project.js"></script>
<div class="screen">
    {% include "components/hotbar.html" %}
    <div class="tab edit-project-tab">
        <h2>Edit Project</h2>
        <div class="project-info">
            <div>
                <label>Project ID:</label>
                <input type="text" id="project-id" readonly>
                <button id="copy-project-id-btn">Copy</button>
            </div>
            <div>
                <label>Title:</label>
                <input type="text" id="project-title-input">
                <button id="save-title-btn">Save Title</button>
            </div>
        </div>
        <button id="delete-project-btn">Delete Project</button>
        <div class="images-container" id="images-container">
            <!-- Images will be displayed here -->
        </div>
        <div class="pagination-container">
            <button id="prev-page-btn">Previous</button>
            <span id="page-info"></span>
            <button id="next-page-btn">Next</button>
        </div>
    </div>
</div>
</body>
</html>
```


templates/index.html
```html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>DarkDataset</title>
    <link rel="preconnect" href="https://fonts.googleapis.com">
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
    <link href="https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&family=Roboto+Slab:wght@100..900&family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap" rel="stylesheet">
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
    <link rel="stylesheet" href="../static/css/loading.css">
    <link rel="stylesheet" href="../static/css/base.css">
    <link rel="stylesheet" href="../static/css/hotbar.css">
    <link rel="stylesheet" href="../static/css/windows.css">

    <link rel="stylesheet" href="../static/css/tab.css">
    <link rel="stylesheet" href="../static/css/home-tab.css">
</head>
<body>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="../static/js/account_manager.js"></script>
<script src="../static/js/main.js"></script>

<div class="screen">
    {% include "components/hotbar.html" %}
    {% include "components/home_tab.html" %}
    <!--{% include "components/windows.html" %}-->
</div>
</body>
</html>
```


templates/join.html
```html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Join Project - DarkDataset</title>
    <link rel="preconnect" href="https://fonts.googleapis.com">
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
    <link href="https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&family=Roboto+Slab:wght@100..900&family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap" rel="stylesheet">
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
    <link rel="stylesheet" href="../static/css/loading.css">
    <link rel="stylesheet" href="../static/css/base.css">
    <link rel="stylesheet" href="../static/css/hotbar.css">

    <link rel="stylesheet" href="../static/css/tab.css">
    <link rel="stylesheet" href="../static/css/join_project.css">
</head>
<body>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="../static/js/account_manager.js"></script>
<script src="../static/js/join.js"></script>
<div class="screen">
    {% include "components/hotbar.html" %}
    <div class="tab join-project-tab">
        <h2>Join a Project</h2>
        <div class="project-id-container">
            <label for="project-id-input">Project ID:</label>
            <input type="text" id="project-id-input" placeholder="Enter Project ID">
        </div>
        <div id="project-info" style="display: none;">
            <!-- Project info will be displayed here -->
        </div>
        <button id="join-btn" style="display: none;">Join</button>
    </div>
</div>
</body>
</html>
```


templates/login.html
```html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>DarkDataset</title>
    <link rel="preconnect" href="https://fonts.googleapis.com">
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
    <link href="https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&family=Roboto+Slab:wght@100..900&family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap" rel="stylesheet">
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
    <link rel="stylesheet" href="../static/css/loading.css">
    <link rel="stylesheet" href="../static/css/base.css">

    <link rel="stylesheet" href="../static/css/tab.css">
    <link rel="stylesheet" href="../static/css/login-tab.css">
</head>
<body>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="../static/js/account_manager.js"></script>
<script src="../static/js/login.js"></script>
<div class="screen">
    {% include "components/login_tab.html" %}
</div>
</body>
</html>
```


templates/new_project.html
```html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>New Project - DarkDataset</title>
    <link rel="preconnect" href="https://fonts.googleapis.com">
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
    <link href="https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&family=Roboto+Slab:wght@100..900&family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap" rel="stylesheet">
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
    <link rel="stylesheet" href="../static/css/loading.css">
    <link rel="stylesheet" href="../static/css/base.css">
    <link rel="stylesheet" href="../static/css/hotbar.css">

    <link rel="stylesheet" href="../static/css/new_project.css">
</head>
<body>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="../static/js/account_manager.js"></script>
<script src="../static/js/new_project.js"></script>
<div class="screen">
    {% include "components/hotbar.html" %}
    <div class="tab new-project-tab">
        <h2>Create New Project</h2>
        <form id="new-project-form">
            <div class="form-group">
                <label for="project-title">Project Title:</label>
                <input type="text" id="project-title" name="title" required>
            </div>
            <div class="form-group">
                <label for="project-images">Upload Images:</label>
                <input type="file" id="project-images" name="images" multiple accept="image/*">
            </div>
            <div class="form-group">
                <label for="project-path">Or specify absolute path to images folder (optional):</label>
                <input type="text" id="project-path" name="path">
            </div>
            <div class="form-group" id="classes-container">
                <label>Classes:</label>
                <div class="class-item">
                    <input type="text" name="class-name" placeholder="Class Name">
                    <input type="color" name="class-color" value="#ff0000">
                    <button type="button" class="remove-class-btn">Remove</button>
                </div>
            </div>
            <button type="button" id="add-class-btn">Add Class</button>
            <button type="submit" id="create-project-btn">Create</button>
        </form>
        <div id="upload-progress" style="display:none;">
            <progress id="progress-bar" value="0" max="100"></progress>
            <span id="progress-text"></span>
        </div>
    </div>
</div>
</body>
</html>
```


templates/workspace.html
```html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Project Workspace - DarkDataset</title>
    <link rel="preconnect" href="https://fonts.googleapis.com">
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
    <link href="https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&family=Roboto+Slab:wght@100..900&family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap" rel="stylesheet">
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
    <link rel="stylesheet" href="../static/css/loading.css">
    <link rel="stylesheet" href="../static/css/base.css">
    <link rel="stylesheet" href="../static/css/hotbar.css">

    <link rel="stylesheet" href="../static/css/workspace.css">
</head>
<body>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="../static/js/account_manager.js"></script>
<script src="../static/js/workspace.js"></script>
<div class="screen">
    {% include "components/hotbar.html" %}
    <div class="tab workspace-tab">
        <h2 id="project-title">Project Title</h2>
        <div id="username">Username</div>
        <div class="progress-container">
            <progress id="project-progress" value="0" max="100"></progress>
        </div>
        <div class="workspace-container">
            <div class="classes-list" id="class-list">
                <!-- Classes will be dynamically added here -->
            </div>
            <div class="image-container">
                <img id="image-display" alt="Annotation Image">
                <!-- Annotations will be added here as overlays -->
            </div>
        </div>
        <div id="annotation-mode-indicator" class="hidden">
            <span>Annotation Mode: <span id="selected-class-name"></span></span>
            <button id="cancel-annotation-btn">Cancel</button>
        </div>
        <button id="next-btn">Next</button>
    </div>
</div>
</body>
</html>
```


templates/components/account_tab.html
```html
<div class="tab account-tab">
    <div class="account-tile">
        <div class="account-name loading" id="account-title">.....</div>
        <div class="account-details-container">
            <div class="account-projects-in">0</div>
            <div class="account-projects-host">0</div>
        </div>
    </div>
    <div class="account-actions">
        <button id="logout-btn">Logout</button>
    </div>
</div>
```


templates/components/home_tab.html
```html
<div class="tab home-tab">
    <div class="account-title loading" id="account-title">Hey, ...!</div>
    <div class="tab-title">
        Your projects:
        <button id="new-project-btn">New Project</button>
        <button id="join-project-btn">Join Project</button>
    </div>
    <div class="projects-container">
        <!-- Projects will be displayed here -->
    </div>
</div>
```


templates/components/hotbar.html
```html
<script src="../static/js/hotbar-manager.js"></script>
<div class="hotbar">
    <button class="hotbar-button" id="home-btn">
        <img src="../../static/icons/h_icon.webp">
    </button>
    <button class="hotbar-button" id="settings-btn">
        <img src="../../static/icons/s_icon.webp">
    </button>
    <button class="hotbar-button" id="profile-btn">
        <img src="../../static/icons/profile_icon.webp">
    </button>
</div>
```


templates/components/login_tab.html
```html
<div class="tab login-tab">
    <div class="tab-title">Login or register</div>
    <div class="login-boxes-container">
        <div class="login-box">
            <input class="login-field" placeholder="login">
            <input type="password" class="password-field" placeholder="password">
        </div>
        <div class="login-buttons">
            <button class="login-btn" id="login-btn">Login</button>
            or
            <button class="login-btn" id="register-btn">Register</button>
        </div>
    </div>
</div>
```


templates/components/windows.html
```html
<div class="windows-container">
    <div class="window">
        <div class="window-controls-container">
            <div class="window-title">Home</div>
            <div class="window-controls">
                <button class="close-btn"></button>
            </div>
        </div>
    </div>
</div>
```



Сделай пожалуйста так, чтобы просмотр изображений в /edit_project работал, а то сейчас он не работает и пишет, что это 1 страница из 0. Кнопки ничего не делают.
Сделай пожалуйста, что если изображение было обработано, то в /edit_project у него был бы зеленоватый фильтр.

Также, в /edit_project добавь снизу контейнер со списком подключившихся людей, количеством изображений которое они обработали и кнопкой Kick.
Сделай так, чтобы еще при наведении на изображение в /edit_project, кроме кнопки удаления, был бы еще текст с именем того, кто его разметил.