От новичка до код-гения: Создаём ИИ-ассистента для тренировок и не скучаем

D2

Администратор
Регистрация
19 Фев 2025
Сообщения
4,380
Реакции
0

Авторство: hackeryaroslav​

Источник: xss.is​


Введение​

Всем привет, на очереди очередной проект для практики скиллов, а именно совмещения ИИ и веб разработки. В этой статье мы рассмотрим создание приложения для рекомендаций по программированию (аля челленджи), которое использует ИИ для предложения задач, соответствующих уровню навыков (да-да, даже для самых профессиональных кодеров!) и интересам пользователя. Создадим профиль, регистрацию и вход, а также лидерборд. Приложение также предоставляет обратную связь от ИИ. В этой статье мы разберем, как работает этот инструмент, как настроить модель и многое другое, надеюсь зайдет, особенно для новичков.

Демо видео проекта:


Начнем с рутины из прошлой статьи по фарму ключа от гемини Google. Сделать это довольно легко. Переходим по ссылке: https://aistudio.google.com/app/prompts/new_chat предварительно войти в свой аккаунт.

92993

Кликаем по кнопке Get API key и создаем свой ключ, дальше копируем из окошка:

92994

Вставьте свой ключ в файле .env в корне проекта:

92995

Структура проекта
Код с оформлением (BB-коды): Скопировать в буфер обмена
Код:
project/
├── app/
│   ├── __init__.py
│   ├── routes.py
│   ├── models.py
│   ├── forms.py
│   ├── recommender.py
│   ├── static/
│   │   ├── css/
│   │   │   └── styles.css
│   │   └── js/
│   │       └── app.js
│   └── templates/
│       ├── base.html
│       ├── hint.html
│       ├── leaderboard.html
│       ├── login.html
│       ├── profile.html
│       ├── register.html
│       ├── index.html
│       └── dashboard.html
├── config.py
├── .env
└── run.py

Обзор файла __init__.py

Файл __init__.py представляет собой основной файл инициализации. Он устанавливает и настраивает все необходимые компоненты и расширения для приложения, а также предоставляет функциональные возможности для создания и настройки экземпляра приложения. Давайте подробнее разберем каждую часть этого файла.

Импорт библиотек

Python: Скопировать в буфер обмена
Код:
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
from flask_login import LoginManager

В этом блоке мы импортируем ключевые модули и расширения для Flask:
  • Flask — основной класс для создания экземпляра приложения.
  • SQLAlchemy — ORM (Object-Relational Mapping) для работы с базой данных.
  • Migrate — расширение для управления миграциями базы данных.
  • LoginManager — расширение для управления аутентификацией и сессиями пользователей.

Инициализация расширений

Python: Скопировать в буфер обмена
Код:
db = SQLAlchemy()
migrate = Migrate()
login_manager = LoginManager()

Здесь мы создаем экземпляры классов для работы с базой данных, миграциями и аутентификацией. Эти экземпляры будут инициализированы позже в функции create_app.

Определение функции truncatewords

Python: Скопировать в буфер обмена
Код:
def truncatewords(value, num_words):
    words = value.split()
    if len(words) > num_words:
        return " ".join(words[:num_words]) + "..."
    return value

Функция truncatewords служит для сокращения текста до указанного количества слов, добавляя многоточие в конце, если текст был усечен. Эта функция поможет нам в дальнейшем, скоро узнаете почему.

Функция create_app

Python: Скопировать в буфер обмена
Код:
def create_app(config_class="config.Config"):
    app = Flask(__name__)
    app.config.from_object(config_class)

    db.init_app(app)
    migrate.init_app(app, db)
    login_manager.init_app(app)

    app.jinja_env.filters["truncatewords"] = truncatewords

    from app.routes import bp as routes_bp
    app.register_blueprint(routes_bp)

    return app

Функция create_app отвечает за создание и настройку экземпляра приложения. Создается новый экземпляр приложения, параметр __name__ позволяет коду определить местоположение текущего модуля. Конфигурация приложения загружается из указанного класса конфигурации. По умолчанию используется класс Config из модуля config. Дальше мы передаем экземпляр приложения в методы init_app для db, migrateи login_manager, что позволяет интегрировать их с приложением. Функция truncatewords добавляется как пользовательский фильтр в Jinja2 (шаблонизатор, используемый Flask), что позволяет использовать его в шаблонах для обработки текста. Мы импортируем и регистрируем маршруты приложения из модуля routes. Это позволит организовать маршруты в виде отдельных "синих принтов", что упрощает структуру приложения. И последнее, функция возвращает созданный и настроенный экземпляр приложения Flask.

Обзор файла forms.py

Файл forms.py содержит определения форм для обработки пользовательского ввода в нашем проекте. Мы используем библиотеку Flask-WTF, которая предоставляет удобный интерфейс для работы с формами и их валидацией в рамках Flask. Давайте рассмотрим каждую форму и её элементы подробнее.

Импорт библиотек

Python: Скопировать в буфер обмена
Код:
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, IntegerField, SubmitField
from wtforms.validators import DataRequired, Email, EqualTo, NumberRange

В этом блоке импортируются такие модули и классы:
  • FlaskForm — базовый класс для форм Flask-WTF.
  • StringField, PasswordField, IntegerField, SubmitField — классы полей формы.
  • DataRequired, Email, EqualTo, NumberRange — валидаторы для проверки данных в полях формы.

Форма регистрации (RegistrationForm)

Python: Скопировать в буфер обмена
Код:
class RegistrationForm(FlaskForm):
    username = StringField("Username", validators=[DataRequired()])
    email = StringField("Email", validators=[DataRequired(), Email()])
    password = PasswordField("Password", validators=[DataRequired()])
    confirm_password = PasswordField(
        "Confirm Password", validators=[DataRequired(), EqualTo("password")]
    )
    submit = SubmitField("Register")
Тут наш код будет регистрировать нового пользователя. Рассмотрим её поля:
  • username: Поле для ввода имени пользователя. Обязательное поле (DataRequired).
  • email: Поле для ввода электронной почты. Проверяется на обязательность и корректность формата email (DataRequired и Email).
  • password: Поле для ввода пароля. Обязательное поле (DataRequired).
  • confirm_password: Поле для подтверждения пароля. Обязательное поле, которое должно совпадать с полем password (DataRequired и EqualTo("password")).
  • submit: Кнопка отправки формы.

Форма входа (LoginForm)

Python: Скопировать в буфер обмена
Код:
class LoginForm(FlaskForm):
    email = StringField("Email", validators=[DataRequired(), Email()])
    password = PasswordField("Password", validators=[DataRequired()])
    submit = SubmitField("Login")

Как мы уже поняли, форма входа используется для аутентификации пользователя. Тоже самое:
  • email: Поле для ввода электронной почты. Проверяется на обязательность и корректность формата email (DataRequired и Email).
  • password: Поле для ввода пароля. Обязательное поле (DataRequired).
  • submit: Кнопка отправки формы.

Форма профиля (ProfileForm)

Python: Скопировать в буфер обмена
Код:
class ProfileForm(FlaskForm):
    skill_level = IntegerField(
        "Skill Level", validators=[DataRequired(), NumberRange(min=1, max=10)]
    )
    preferred_languages = StringField(
        "Preferred Programming Languages", validators=[DataRequired()]
    )
    preferred_topics = StringField("Preferred Topics", validators=[DataRequired()])
    submit = SubmitField("Update Profile")

Этот класс позволяет пользователю обновить свои предпочтения и уровень навыков. Поля формы:
  • skill_level: Поле для ввода уровня навыков. Обязательное поле, значение должно быть в пределах от 1 до 10 (DataRequired и NumberRange(min=1, max=10)).
  • preferred_languages: Поле для ввода предпочтительных языков программирования. Обязательное поле (DataRequired).
  • preferred_topics: Поле для ввода предпочтительных тем. Обязательное поле (DataRequired).
  • submit: Кнопка отправки формы.

Обзор файла recommender.py

Файл recommender.py реализует два ключевых компонента для приложения: генерацию новых задач программирования (или попросту челленджи) и оценку решений пользователей. Оба компонента используют библиотеку ИИ Google для создания и анализа контента. Рассмотрим каждую часть файла более подробно.

Импорт библиотек

Python: Скопировать в буфер обмена
Код:
import google.generativeai as genai
from app.models import CodingChallenge
from flask import current_app

В этом блоке импортируются следующие модули:
  • google.generativeai — библиотека для взаимодействия с моделью ИИ.
  • CodingChallenge из app.models — модель для хранения данных о задачах программирования.
  • current_app из flask — позволяет получить доступ к текущему экземпляру приложения Flask, в том числе к конфигурации.

Функция generate_coding_challenge

Python: Скопировать в буфер обмена
Код:
def generate_coding_challenge(user):
    genai.configure(api_key=current_app.config["GEMINI_API_KEY"])
    model = genai.GenerativeModel("gemini-1.5-flash")

    prompt = (
        f"Generate a coding challenge for a user with skill level {user.skill_level} "
        f"who is interested in {user.preferred_languages} and {user.preferred_topics}."
    )

Функция отвечает за создание новой задачи программирования, ориентированной на конкретного пользователя. Она начинается с настройки ИИ:
  1. Конфигурация модели: Используется API-ключ из конфигурации приложения для настройки модели gemini-1.5-flash.
  2. Формирование запроса: Создается запрос (prompt) для генерации задачи на основе уровня навыков пользователя и его интересов.
Python: Скопировать в буфер обмена
Код:
    try:
        response = model.generate_content(prompt, stream=True)

        challenge_data = ""
        for chunk in response:
            if hasattr(chunk, "text"):
                challenge_data += chunk.text
            else:
                print(f"Received non-text chunk: {chunk}")

        if not challenge_data.strip():
            raise ValueError("Received empty or invalid content from the model.")

        challenge_lines = challenge_data.split("\n")
        if len(challenge_lines) < 2:
            raise ValueError("Challenge data format is incorrect.")

        title = challenge_lines[0].strip()
        description = "\n".join(challenge_lines[1:]).strip()
        difficulty = user.skill_level
        programming_language = user.preferred_languages
        topic = user.preferred_topics

        challenge = CodingChallenge(
            title=title,
            description=description,
            difficulty=difficulty,
            programming_language=programming_language,
            topic=topic,
        )

        return challenge

    except Exception as e:
        print(f"An error occurred while generating the coding challenge: {e}")
        raise

3.Получение и обработка ответа: Функция обрабатывает поток ответа модели и собирает текст. Проверяется, что ответ не пустой и имеет правильный формат.
4. Форматирование данных: Ответ разбивается на строки, первая строка используется как заголовок задачи, а остальные строки составляют описание.
5. Создание и возврат задачи: Создается объект CodingChallenge с полученными данными и возвращается.
6. Обработка исключений: В случае ошибки выводится сообщение и исключение повторно возбуждается.

Функция evaluate_solution
Python: Скопировать в буфер обмена
Код:
def evaluate_solution(user_solution, challenge):
    genai.configure(api_key=current_app.config["GEMINI_API_KEY"])
    model = genai.GenerativeModel("gemini-1.5-flash")

    prompt = f"Evaluate the user's solution for the coding challenge '{challenge.title}' on a scale of 1 to 10. User's solution: {user_solution}"

Функция evaluate_solution предназначена для оценки решения пользователя по конкретной задаче:
  • Конфигурация модели: Снова используется API-ключ для настройки модели.
  • Формирование запроса: Запрос формируется для оценки решения пользователя по шкале от 1 до 10 на основе заголовка задачи и самого решения.
Python: Скопировать в буфер обмена
Код:
    try:
        response = model.generate_content(prompt, stream=True)

        evaluation_data = ""
        for chunk in response:
            if hasattr(chunk, "text"):
                evaluation_data += chunk.text
            else:
                print(f"Received non-text chunk: {chunk}")

        if not evaluation_data.strip():
            raise ValueError("Received empty response from the model.")

        if evaluation_data.strip().isdigit():
            score = int(evaluation_data.strip())
        else:
            raise ValueError("The response did not contain a valid numeric score.")

        return score

    except Exception as e:
        print(f"An error occurred while evaluating the solution: {e}")
        raise
  • Получение и обработка ответа: Как и в предыдущей функции, поток ответа обрабатывается и собирается в строку. Проверяется, что ответ не пустой.
  • Оценка результата: Проверяется, что ответ содержит корректный числовой результат, который затем возвращается как оценка.
  • Обработка исключений: В случае ошибки выводится сообщение об ошибке и исключение повторно возбуждается.

Обзор файла routes.py

Файл routes.py управляет различными маршрутами. Эти маршруты включают регистрацию пользователей, вход и выход из системы, управление профилем, получение рекомендаций по задачам и оценку решений. Давайте рассмотрим каждую часть кода.
Python: Скопировать в буфер обмена
Код:
from flask import (
    Blueprint,
    render_template,
    redirect,
    url_for,
    flash,
    request,
    jsonify,
    current_app,
)
from flask_login import login_user, logout_user, login_required, current_user
from app.forms import RegistrationForm, LoginForm, ProfileForm
from app.models import UserProfile, CodingChallenge
from app import db
from app.recommender import generate_coding_challenge
import google.generativeai as genai
import re
import logging

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

bp = Blueprint("routes", __name__)

В этом блоке кода создается экземпляр Blueprint, который группирует маршруты в модуле routes. Импортируются необходимые модули и функции для обработки запросов, работы с формами и взаимодействия с базой данных. Также настраивается логирование для отслеживания ошибок и событий.

Маршрут для главной страницы

Python: Скопировать в буфер обмена
Код:
@bp.route("/")
def index():
    return render_template("index.html")

Маршрут / рендерит главную страницу сайта, используя шаблон index.html.

Регистрация пользователя

Python: Скопировать в буфер обмена
Код:
@bp.route("/register", methods=["GET", "POST"])
def register():
form = RegistrationForm()
if form.validate_on_submit():
user = UserProfile(username=form.username.data, email=form.email.data)
user.set_password(form.password.data)
db.session.add(user)
db.session.commit()
flash("Congratulations, you are now a registered user!", "success")
return redirect(url_for("routes.login"))
return render_template("register.html", form=form)

Маршрут /register обрабатывает запросы для регистрации новых пользователей. Если форма регистрации проходит валидацию, создается новый пользователь, пароль которого хэшируется перед сохранением в базе данных. После успешной регистрации пользователя перенаправляют на страницу входа.

Вход пользователя

Python: Скопировать в буфер обмена
Код:
@bp.route("/login", methods=["GET", "POST"])
def login():
form = LoginForm()
if form.validate_on_submit():
user = UserProfile.query.filter_by(email=form.email.data).first()
if user and user.check_password(form.password.data):
login_user(user)
flash("You have been logged in.", "success")
return redirect(url_for("routes.dashboard"))
else:
flash("Invalid email or password.", "danger")
return render_template("login.html", form=form)

Маршрут /login позволяет пользователю войти в систему. При успешной проверке учетных данных пользователя перенаправляют на страницу панели (dashboard). В случае ошибки отображается сообщение об ошибке.

Выход пользователя

Python: Скопировать в буфер обмена
Код:
@bp.route("/logout")
@login_required
def logout():
logout_user()
flash("You have been logged out.", "success")
return redirect(url_for("routes.index"))

Маршрут /logout позволяет пользователю выйти из системы. После выхода пользователя перенаправляют на главную страницу с сообщением о выходе.

Панель инструментов

Python: Скопировать в буфер обмена
Код:
@bp.route("/dashboard")
@login_required
def dashboard():
user = current_user
challenges = user.completed_challenges
num_challenges = len(challenges)
return render_template(
"dashboard.html", challenges=challenges, num_challenges=num_challenges
)

Маршрут /dashboard отображает страницу панели инструментов (наших задач по кодингу) для авторизованных пользователей. Он показывает список задач, которые пользователь завершил, и общее количество завершенных задач.

Обновление профиля

Python: Скопировать в буфер обмена
Код:
@bp.route("/profile", methods=["GET", "POST"])
@login_required
def profile():
form = ProfileForm()
if form.validate_on_submit():
current_user.skill_level = form.skill_level.data
current_user.preferred_languages = form.preferred_languages.data
current_user.preferred_topics = form.preferred_topics.data
db.session.commit()
flash("Your profile has been updated.", "success")
return redirect(url_for("routes.profile"))
form.skill_level.data = current_user.skill_level
form.preferred_languages.data = current_user.preferred_languages
form.preferred_topics.data = current_user.preferred_topics
return render_template("profile.html", form=form)

Маршрут /profile позволяет пользователю обновить информацию в своем профиле, включая уровень навыков и предпочтительные языки и темы. После успешного обновления информации пользователя перенаправляют обратно на страницу профиля.

Рекомендации по задачам

Python: Скопировать в буфер обмена
Код:
@bp.route("/recommend")
@login_required
def recommend():
challenge = generate_coding_challenge(current_user)
db.session.add(challenge)
db.session.commit()

return jsonify(
id=challenge.id,
title=challenge.title,
description=challenge.description,
difficulty=challenge.difficulty,
programming_language=challenge.programming_language,
topic=challenge.topic,
)

Маршрут /recommend генерирует новую задачу программирования для текущего пользователя, используя функцию generate_coding_challenge. Новая задача сохраняется в базе данных, и информация о ней возвращается в формате JSON.

Таблица лидеров

Python: Скопировать в буфер обмена
Код:
@bp.route("/leaderboard")
def leaderboard():
users = UserProfile.query.order_by(UserProfile.skill_level.desc()).all()
num_users = len(users)
return render_template("leaderboard.html", users=users, num_users=num_users)

Маршрут /leaderboard отображает таблицу лидеров, в которой пользователи сортируются по уровню навыков. Список пользователей передается в шаблон leaderboard.html.

Оценка решений


Python: Скопировать в буфер обмена
Код:
def parse_ai_response(response_text):
lines = response_text.split("\n")
score = None
feedback = []

for line in lines:
if score is None and ("score" in line.lower() or "/10" in line):
score_match = re.search(r"(\d+)/10", line)
if score_match:
score = int(score_match.group(1))
continue
feedback.append(line)

return score, "\n".join(feedback).strip()

Функция parse_ai_response анализирует текст ответа от ИИ, извлекая оценку и обратную связь. Оценка и комментарии собираются из текста, разделенного на строки.

Python: Скопировать в буфер обмена
Код:
@bp.route("/complete/<int:challenge_id>", methods=["POST"])
@login_required
def complete_challenge(challenge_id):
challenge = CodingChallenge.query.get(challenge_id)

if challenge and challenge not in current_user.completed_challenges:
genai.configure(api_key=current_app.config["GEMINI_API_KEY"])
model = genai.GenerativeModel("gemini-1.5-flash")

user_solution = request.form.get("solution", "").strip()

if user_solution.lower() == "i don't know" or user_solution == "":
flash(
"It looks like you're having trouble with this challenge. Don't worry, that's normal! Would you like some hints?",
"info",
)
return redirect(url_for("routes.dashboard"))

prompt = f"Evaluate the user's solution for the coding challenge '{challenge.title}' on a scale of 1 to 10. Provide a brief explanation of the score. User's solution: {user_solution}"

try:
response = model.generate_content(prompt, stream=True)
evaluation_text = ""

for chunk in response:
if hasattr(chunk, "text"):
evaluation_text += chunk.text

score, feedback = parse_ai_response(evaluation_text)

if score is None:
logger.warning(
f"Could not extract numeric score from AI response: {evaluation_text}"
)
flash(
"We couldn't determine a precise score for your solution, but here's some feedback: "
+ feedback,
"info",
)
elif score >= 5:
current_user.completed_challenges.append(challenge)
db.session.commit()
flash(
f"Congratulations! You have completed the coding challenge with a score of {score}/10! "
+ feedback,
"success",
)
else:
flash(
f"Your solution scored {score}/10. Here's some feedback to help you improve: "
+ feedback,
"info",
)
except Exception as e:
logger.error(f"An error occurred while evaluating the solution: {str(e)}")
flash(
"An error occurred while evaluating your solution. Please try again later.",
"danger",
)

else:
flash(
"You are not authorized to complete this challenge or it has already been completed.",
"danger",
)

return redirect(url_for("routes.dashboard"))

Маршрут /complete/<int:challenge_id> обрабатывает отправку решения задачи. Он проверяет наличие задачи, получает решение пользователя, оценивает его с помощью модели ИИ и отображает соответствующее сообщение в зависимости от оценки.

Подсказки для задач

Python: Скопировать в буфер обмена
Код:
@bp.route("/hint/<int:challenge_id>")
@login_required
def get_hint(challenge_id):
challenge = CodingChallenge.query.get(challenge_id)
if not challenge:
flash("Challenge not found.", "danger")
return redirect(url_for("routes.dashboard"))

genai.configure(api_key=current_app.config["GEMINI_API_KEY"])
model = genai.GenerativeModel("gemini-1.5-flash")

prompt = f"Provide a hint for the coding challenge: '{challenge.title}'. The hint should guide the user without giving away the full solution."

try:
response = model.generate_content(prompt)
hint = response.text
return render_template("hint.html", challenge=challenge, hint=hint)
except Exception as e:
logger.error(f"An error occurred while generating a hint: {str(e)}")
flash(
"An error occurred while generating a hint. Please try again later.",
"danger",
)
return redirect(url_for("routes.dashboard"))

Маршрут /hint/<int:challenge_id> предоставляет пользователю подсказку для задачи. Подсказка генерируется с помощью модели ИИ и отображается на странице подсказки. Можно также ее развить, обрабатывая ответ от пользователя и спросить, к какой категории относится его ответ. Например, если бы ИИ написало, что пользователь не ответил или вообще не понимает, что пишет, мы бы автоматически генерировали подсказки. Но у меня руки не дошли до это :(

Обработка ошибок

Python: Скопировать в буфер обмена
Код:
@bp.errorhandler(404)
def page_not_found(e):
"""Handle 404 errors."""
return render_template("404.html"), 404

@bp.errorhandler(500)
def internal_server_error(e):
"""Handle 500 errors."""
return render_template("500.html"), 500

Эти обработчики ошибок отображают страницы ошибок 404 (не найдено) и 500 (внутренняя ошибка сервера), если возникают соответствующие исключения. На всякий случай, если наш пользователь кликнет не туда.

Обзор файла app.js

Файл отвечает за взаимодействие с пользователем в веб-приложении, предоставляющем наши задания из бэкэнда. Файл управляет различными аспектами пользовательского интерфейса, включая переключение форм, отправку данных, создание карточек заданий и эффектами на странице. Давайте подробнее разберем каждую часть кода.

Инициализация функций
JavaScript: Скопировать в буфер обмена
Код:
document.addEventListener("DOMContentLoaded", () => {
initializeProfileFormToggle();
initializeChallengeForms();
initializeShowMoreButtons();
initializeGenerateChallengeButton();
initializeSmoothScroll();
initializeScrollAnimations();
initializeChallengeCardHoverEffects();
initializeTypewriterEffect();
});

Функция DOMContentLoaded запускает все функции инициализации, как только HTML-документ полностью загружен и разобран.

Переключение формы профиля

JavaScript: Скопировать в буфер обмена
Код:
function initializeProfileFormToggle() {
const profileFormBtn = document.getElementById("profile-form-btn");
const profileForm = document.getElementById("profile-form");

if (profileFormBtn && profileForm) {
profileFormBtn.addEventListener("click", () => {
profileForm.classList.toggle("hidden");
});
}
}

Функция initializeProfileFormToggle добавляет обработчик событий на кнопку, которая показывает или скрывает форму профиля при нажатии. Это упрощает доступ к форме и делает интерфейс более удобным.

Обработка форм заданий

JavaScript: Скопировать в буфер обмена
Код:
function initializeChallengeForms() {
const challengeForms = document.querySelectorAll(".challenge-form");

challengeForms.forEach((form) => {
form.addEventListener("submit", async (event) => {
event.preventDefault();

const formData = new FormData(form);
const actionUrl = form.action;

try {
const response = await fetch(actionUrl, {
method: "POST",
body: formData,
});

if (response.ok) {
const jsonResponse = await response.json();
handleChallengeResponse(jsonResponse, form);
} else {
throw new Error("Error submitting your solution.");
}
} catch (error) {
handleError(error);
}
});
});
}

Функция initializeChallengeForms добавляет обработчик событий на каждую форму заданий. При отправке формы происходит асинхронный запрос на сервер с использованием fetch. В случае успешного ответа вызывается функция handleChallengeResponse, которая обновляет состояние формы. Это позволяет пользователям отправлять свои решения и получать обратную связь без перезагрузки страницы.

Обработка ответа на задание

JavaScript: Скопировать в буфер обмена
Код:
function handleChallengeResponse(jsonResponse, form) {
if (jsonResponse.status === "success") {
const button = form.previousElementSibling;
button.classList.add("disabled");
button.textContent = "Completed";
form.classList.add("hidden");
} else {
alert("Error submitting your solution. Please try again.");
}
}

Функция handleChallengeResponse обновляет кнопку и форму на основе статуса ответа от сервера. Если задача успешно завершена, кнопка становится неактивной, и форма скрывается. В противном случае пользователю показывается сообщение об ошибке.

Кнопки "Show more"

JavaScript: Скопировать в буфер обмена
Код:
function initializeShowMoreButtons() {
const showMoreButtons = document.querySelectorAll(".show-more-btn");

showMoreButtons.forEach((button) => {
button.addEventListener("click", () => {
const card = button.closest(".challenge-card");
card.classList.toggle("show-more-expanded");
button.textContent = card.classList.contains("show-more-expanded")
? "Show Less"
: "Show More";
});
});
}

Функция initializeShowMoreButtons управляет отображением дополнительной информации на карточках заданий. При нажатии на кнопку "Show More" или "Show less", карточка задания расширяется или сжимается, так мы экономим место на странице, не превращая ее в свалку.

Генерация нового задания

JavaScript: Скопировать в буфер обмена
Код:
function initializeGenerateChallengeButton() {
const generateChallengeBtn = document.getElementById("generate-challenge-btn");
const challengeContainer = document.getElementById("challenge-container");

if (generateChallengeBtn) {
generateChallengeBtn.addEventListener("click", async () => {
try {
const response = await fetch("/recommend", {
method: "GET",
});

if (response.ok) {
const challenge = await response.json();
const challengeCard = createChallengeCard(challenge);

challengeContainer.innerHTML = "";
challengeContainer.appendChild(challengeCard);

challengeCard.classList.add("fade-in");
} else {
throw new Error("Error generating a new coding challenge.");
}
} catch (error) {
handleError(error);
}
});
}
}

Функция initializeGenerateChallengeButton управляет генерацией новых заданий по нажатию на кнопку. Она отправляет запрос на сервер для получения нового задания и обновляет контейнер с заданиями. Полученное задание отображается на странице с помощью функции createChallengeCard.

Обработка ошибок

JavaScript: Скопировать в буфер обмена
Код:
function handleError(error) {
console.error("Error:", error);
alert("An error occurred. Please try again later.");
}

Функция handleError обрабатывает ошибки, которые могут возникнуть в процессе выполнения запросов или других действий. Она выводит сообщение об ошибке в консоль и отображает предупреждение пользователю.

Плавная прокрутка

JavaScript: Скопировать в буфер обмена
Код:
function initializeSmoothScroll() {
document.querySelectorAll('a[href^="#"]').forEach((anchor) => {
anchor.addEventListener("click", function (e) {
e.preventDefault();
document.querySelector(this.getAttribute("href")).scrollIntoView({
behavior: "smooth",
});
});
});
}

Функция initializeSmoothScroll обеспечивает плавную прокрутку при переходе по якорным ссылкам на странице. Это улучшает наш UX, делая навигацию более плавной и приятной.

Анимации при прокрутке

JavaScript: Скопировать в буфер обмена
Код:
function initializeScrollAnimations() {
const animateOnScroll = () => {
const elements = document.querySelectorAll(".animate-on-scroll");
elements.forEach((element) => {
const elementTop = element.getBoundingClientRect().top;
const elementBottom = element.getBoundingClientRect().bottom;
if (elementTop < window.innerHeight && elementBottom > 0) {
element.classList.add("animate");
}
});
};

window.addEventListener("scroll", animateOnScroll);
animateOnScroll();
}

Функция initializeScrollAnimations добавляет анимации элементам, когда они становятся видимыми при прокрутке страницы. Это создаёт визуально привлекательный эффект, когда элементы появляются на экране.

Эффекты наведения на карточки заданий

JavaScript: Скопировать в буфер обмена
Код:
function initializeChallengeCardHoverEffects() {
const challengeCards = document.querySelectorAll(".challenge-card");
challengeCards.forEach((card) => {
card.addEventListener("mouseenter", () => {
card.style.transform = "translateY(-5px)";
card.style.boxShadow = "0 15px 40px rgba(0, 0, 0, 0.2)";
});
card.addEventListener("mouseleave", () => {
card.style.transform = "translateY(0)";
card.style.boxShadow = "0 10px 30px rgba(0, 0, 0, 0.1)";
});
});
}

Функция initializeChallengeCardHoverEffects управляет визуальными эффектами на карточках заданий при наведении курсора. Это создаёт эффект поднятия и увеличения тени, так пользователю будет намного приятнее работать с карточками.

Эффект машинописного текста

JavaScript: Скопировать в буфер обмена
Код:
function initializeTypewriterEffect() {
const heroTitle = document.querySelector(".hero h1");
if (heroTitle) {
const text = heroTitle.textContent;
heroTitle.textContent = "";
let i = 0;
const typeWriter = () => {
if (i < text.length) {
heroTitle.textContent += text.charAt(i);
i++;
setTimeout(typeWriter, 100);
}
};
typeWriter();
}
}

Функция initializeTypewriterEffect добавляет эффект машинописного текста к заголовку на главной странице. Символы текста появляются по одному, создавая эффект печатания.

Создание карточки задания

JavaScript: Скопировать в буфер обмена
Код:
function createChallengeCard(challenge) {
const card = document.createElement("div");
card.classList.add("challenge-card");

const title = document.createElement("h3");
title.textContent = challenge.title;

const description = document.createElement("p");
description.textContent = `Description: ${challenge.description}`;

const difficulty = document.createElement("p");
difficulty.textContent = `Difficulty: ${challenge.difficulty}`;

const language = document.createElement("p");
language.textContent = `Programming Language: ${challenge.programming_language}`;

const topic = document.createElement("p");
topic.textContent = `Topic: ${challenge.topic}`;

const completeButton = document.createElement("button");
completeButton.classList.add("show-solution-form");
completeButton.dataset.challengeId = challenge.id;
completeButton.textContent = "Complete";

const form = document.createElement("form");
form.classList.add("challenge-form", "hidden");
form.dataset.challengeId = challenge.id;
form.action = `/complete/${challenge.id}`;
form.method = "post";

const textarea = document.createElement("textarea");
textarea.name = "solution";
textarea.placeholder = "Enter your solution here";
textarea.rows = "4";
textarea.cols = "50";

const submitButton = document.createElement("button");
submitButton.type = "submit";
submitButton.textContent = "Submit Solution";

form.appendChild(textarea);
form.appendChild(submitButton);

card.appendChild(title);
card.appendChild(difficulty);
card.appendChild(language);
card.appendChild(topic);
card.appendChild(description);
card.appendChild(completeButton);
card.appendChild(form);

completeButton.addEventListener("click", () => {
form.classList.toggle("hidden");
});

return card;
}

Функция createChallengeCard создаёт элемент карточки для задания. В карточке отображаются все детали задания, такие как название, описание, сложность, язык программирования и тема. Также добавляются кнопка для завершения задания и форма для отправки решения.

Быстрый обзор файлов шаблонов

1. Файл base.html

HTML: Скопировать в буфер обмена
Код:
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>{% block title %}AI code challenge{% endblock %}</title>
    <link
      rel="stylesheet"
      href="{{ url_for('static', filename='css/styles.css') }}"
    />
    <link
      href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&display=swap"
      rel="stylesheet"
    />
    <link
      rel="stylesheet"
      href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css"
    />
  </head>
  <body>
    <div class="navbar">
      <div class="navbar-content">
        <a href="{{ url_for('routes.index') }}" class="navbar-logo">AI code challenge</a>
        <div class="navbar-links">
          <a href="{{ url_for('routes.index') }}">Home</a>
          {% if current_user.is_authenticated %}
          <a href="{{ url_for('routes.dashboard') }}">Dashboard</a>
          <a href="{{ url_for('routes.profile') }}">Profile</a>
          <a href="{{ url_for('routes.leaderboard') }}">Leaderboard</a>
          <a href="{{ url_for('routes.logout') }}">Logout</a>
          {% else %}
          <a href="{{ url_for('routes.register') }}">Register</a>
          <a href="{{ url_for('routes.login') }}">Login</a>
          {% endif %}
        </div>
      </div>
    </div>

    <div class="content">
      {% with messages = get_flashed_messages(with_categories=true) %} {% if
      messages %} {% for category, message in messages %}
      <div class="alert alert-{{ category }}">{{ message }}</div>
      {% endfor %} {% endif %} {% endwith %} {% block content %}{% endblock %}
    </div>

    <div class="message">
      {% with messages = get_flashed_messages(with_categories=true) %} {% if
      messages %} {% for category, message in messages %}
      <div class="alert alert-{{ category }} fade-in">
        <i class="fas fa-info-circle"></i>
        {{ message }}
        <button
          class="close-btn"
          onclick="this.parentElement.style.display='none'"
        >
          &times;
        </button>
      </div>
      {% endfor %} {% endif %} {% endwith %}
    </div>

    <script src="{{ url_for('static', filename='js/app.js') }}"></script>
  </body>
</html>

base.html служит основой для всех страниц веб-приложения, определяя общую структуру и стили. В этом шаблоне задаются базовые элементы, такие как метаданные, подключение стилей и шрифтов, а также навигационная панель, которая включает ссылки для аутентифицированных и неаутентифицированных пользователей. Контент, который будет изменяться на разных страницах, вставляется в специально отведенные блоки.

2. Файл dashboard.html

HTML: Скопировать в буфер обмена
Код:
{% extends 'base.html' %} {% block content %}
<div class="container">
  <h1 class="text-center">Coding Challenges</h1>

  <div class="generate-challenge text-center my-4">
    <button id="generate-challenge-btn" class="btn btn-primary">
      Generate New Challenge
    </button>
    <div id="challenge-container"></div>
  </div>

  <div class="challenges-container">
    {% if challenges|length == 0 %}
    <p class="text-center">
      No coding challenges available yet. Please check back later.
    </p>
    {% else %}
    <div class="row">
      {% for challenge in challenges %}
      <div class="col-md-4 mb-4">
        <div class="challenge-card">
          <h3>{{ challenge.title }}</h3>
          <p><strong>Difficulty:</strong> {{ challenge.difficulty }}</p>
          <p>
            <strong>Programming Language:</strong> {{
            challenge.programming_language }}
          </p>
          <p><strong>Topic:</strong> {{ challenge.topic }}</p>
          <p class="challenge-description-summary">
            {{ challenge.description|truncatewords(10) }}
          </p>
          <p class="challenge-description-full hidden">
            {{ challenge.description }}
          </p>

          {% if challenge in current_user.completed_challenges %}
          <button class="complete-challenge btn btn-success disabled" disabled>
            Completed
          </button>
          {% else %}
          <button
            class="show-solution-form btn btn-primary"
            data-challenge-id="{{ challenge.id }}"
          >
            Complete
          </button>
          <form
            class="challenge-form hidden"
            data-challenge-id="{{ challenge.id }}"
            action="{{ url_for('routes.complete_challenge', challenge_id=challenge.id) }}"
            method="post"
          >
            <div class="form-group">
              <textarea
                name="solution"
                placeholder="Enter your solution here"
                rows="6"
                class="form-control mt-3"
              ></textarea>
            </div>
            <button type="submit" class="btn btn-primary">
              Submit Solution
            </button>
          </form>
          {% endif %}
          <button class="show-more-btn btn btn-secondary mt-2">
            Show More
          </button>
        </div>
      </div>
      {% endfor %}
    </div>
    {% endif %}
  </div>
</div>
{% endblock %}

dashboard.html предназначен для отображения списка кодовых заданий, доступных пользователю. Этот шаблон включает в себя функциональность для генерации новых заданий и отображения их списка. Пользователи могут видеть описание заданий, уровень сложности и язык программирования. Для каждого задания предусмотрены кнопки для его завершения и отправки решения, если задание еще не выполнено.

3. Файл hint.html
HTML: Скопировать в буфер обмена
Код:
{% extends 'base.html' %} {% block content %}
<div class="container">
  <h2>Hint for: {{ challenge.title }}</h2>
  <p>{{ hint }}</p>
  <a href="{{ url_for('routes.dashboard') }}" class="btn btn-primary">
    Back to Dashboard
  </a>
</div>
{% endblock %}

hint.html используется для отображения подсказок по конкретным заданиям. Этот шаблон отображает подсказку вместе с названием задания и предоставляет кнопку для возврата на страницу с заданиями.

4. Файл index.html
HTML: Скопировать в буфер обмена
Код:
{% extends 'base.html' %} {% block content %}
<section class="hero">
  <div class="container">
    <h1 class="animate-on-scroll">Master Your Coding Skills</h1>
    <p class="animate-on-scroll">
      Challenge yourself with personalized coding exercises and climb the
      leaderboard!
    </p>
    {% if current_user.is_authenticated %}
    <a
      href="{{ url_for('routes.dashboard') }}"
      class="cta-button animate-on-scroll"
      >Go to Dashboard</a
    >
    {% else %}
    <a
      href="{{ url_for('routes.register') }}"
      class="cta-button animate-on-scroll"
      >Get Started</a
    >
    {% endif %}
  </div>
</section>

<section class="features">
  <div class="container">
    <h2 class="animate-on-scroll">Why Choose Our Platform?</h2>
    <div class="feature-grid">
      <div class="feature-card animate-on-scroll">
        <i class="fas fa-code"></i>
        <h3>Personalized Challenges</h3>
        <p>Get coding challenges tailored to your skill level and interests.</p>
      </div>
      <div class="feature-card animate-on-scroll">
        <i class="fas fa-trophy"></i>
        <h3>Competitive Leaderboard</h3>
        <p>Compete with other coders and see where you stand.</p>
      </div>
      <div class="feature-card animate-on-scroll">
        <i class="fas fa-graduation-cap"></i>
        <h3>Skill Progression</h3>
        <p>
          Track your progress and watch your coding skills improve over time.
        </p>
      </div>
    </div>
  </div>
</section>

<section class="cta">
  <div class="container">
    <h2 class="animate-on-scroll">Ready to Start Coding?</h2>
    <p class="animate-on-scroll">
      Join our community of passionate coders and take your skills to the next
      level!
    </p>
    {% if current_user.is_authenticated %}
    <a
      href="{{ url_for('routes.dashboard') }}"
      class="cta-button animate-on-scroll"
      >View Challenges</a
    >
    {% else %}
    <a
      href="{{ url_for('routes.register') }}"
      class="cta-button animate-on-scroll"
      >Sign Up Now</a
    >
    {% endif %}
  </div>
</section>
{% endblock %}

index.html представляет собой главную страницу веб-приложения. Она приветствует новых пользователей и предлагает им начать работу с платформой. На этой странице также представлены ключевые особенности, такие как персонализированные задания, рейтинг и отслеживание прогресса. Для аутентифицированных пользователей доступна кнопка для перехода на страницу с заданиями, в то время как неаутентифицированные пользователи могут зарегистрироваться.

5. Файл leaderboard.html
HTML: Скопировать в буфер обмена
Код:
{% extends 'base.html' %} {% block content %}
<div class="leaderboard">
  <h1>Leaderboard</h1>
  <table>
    <thead>
      <tr>
        <th>Rank</th>
        <th>Username</th>
        <th>Skill Level</th>
        <th>Completed Challenges</th>
      </tr>
    </thead>
    <tbody>
      {% for user in users %}
      <tr>
        <td>{{ loop.index }}</td>
        <td>{{ user.username }}</td>
        <td>{{ user.skill_level }}</td>
        <td>{{ user.completed_challenges|length }}</td>
      </tr>
      {% endfor %}
    </tbody>
  </table>
</div>
{% endblock %}

leaderboard.html отображает таблицу с рейтингом пользователей. Здесь представлены данные о ранге, имени пользователя, уровне навыков и количестве завершенных заданий. Пользователи могут видеть свои позиции по сравнению с другими участниками.

6. Файл login.html
HTML: Скопировать в буфер обмена
Код:
{% extends 'base.html' %} {% block content %}
<div class="container">
  <h1>Login</h1>
  <form method="POST" action="{{ url_for('routes.login') }}">
    {{ form.hidden_tag() }}
    <div class="form-group">
      {{ form.email.label }} {{ form.email(class="form-control") }} {% if
      form.email.errors %} {% for error in form.email.errors %}
      <div class="alert alert-danger">{{ error }}</div>
      {% endfor %} {% endif %}
    </div>
    <div class="form-group">
      {{ form.password.label }} {{ form.password(class="form-control") }} {% if
      form.password.errors %} {% for error in form.password.errors %}
      <div class="alert alert-danger">{{ error }}</div>
      {% endfor %} {% endif %}
    </div>
    <div class="form-group">{{ form.submit(class="btn btn-primary") }}</div>
  </form>
</div>
{% endblock %}

login.html содержит форму для входа в систему. В этом шаблоне предусмотрены поля для ввода email и пароля, а также отображение ошибок валидации.

7. Файл profile.html
HTML: Скопировать в буфер обмена
Код:
{% extends 'base.html' %} {% block content %}
<div class="container">
  <h1>Profile</h1>
  <form
    id="profile-form"
    method="POST"
    action="{{ url_for('routes.profile') }}"
  >
    {{ form.hidden_tag() }}
    <div class="form-group">
      {{ form.skill_level.label }} {{ form.skill_level(class="form-control") }}
      {% if form.skill_level.errors %} {% for error in form.skill_level.errors
      %}
      <div class="alert alert-danger">{{ error }}</div>
      {% endfor %} {% endif %}
    </div>
    <div class="form-group">
      {{ form.preferred_languages.label }} {{
      form.preferred_languages(class="form-control") }} {% if
      form.preferred_languages.errors %} {% for error in
      form.preferred_languages.errors %}
      <div class="alert alert-danger">{{ error }}</div>
      {% endfor %} {% endif %}
    </div>
    <div class="form-group">
      {{ form.preferred_topics.label }} {{
      form.preferred_topics(class="form-control") }} {% if
      form.preferred_topics.errors %} {% for error in
      form.preferred_topics.errors %}
      <div class="alert alert-danger">{{ error }}</div>
      {% endfor %} {% endif %}
    </div>
    <div class="form-group">{{ form.submit(class="btn btn-primary") }}</div>
  </form>
</div>
{% endblock %}

profile.html предназначен для обновления профиля пользователя. Пользователи могут редактировать свой уровень навыков и предпочтения по языкам и темам. Шаблон включает форму с соответствующими полями и обработкой ошибок.

8. Файл register.html
HTML: Скопировать в буфер обмена
Код:
{% extends 'base.html' %} {% block content %}
<div class="container">
  <h1>Register</h1>
  <form method="POST" action="{{ url_for('routes.register') }}">
    {{ form.hidden_tag() }}
    <div class="form-group">
      {{ form.username.label }} {{ form.username(class="form-control") }} {% if
      form.username.errors %} {% for error in form.username.errors %}
      <div class="alert alert-danger">{{ error }}</div>
      {% endfor %} {% endif %}
    </div>
    <div class="form-group">
      {{ form.email.label }} {{ form.email(class="form-control") }} {% if
      form.email.errors %} {% for error in form.email.errors %}
      <div class="alert alert-danger">{{ error }}</div>
      {% endfor %} {% endif %}
    </div>
    <div class="form-group">
      {{ form.password.label }} {{ form.password(class="form-control") }} {% if
      form.password.errors %} {% for error in form.password.errors %}
      <div class="alert alert-danger">{{ error }}</div>
      {% endfor %} {% endif %}
    </div>
    <div class="form-group">
      {{ form.confirm_password.label }} {{
      form.confirm_password(class="form-control") }} {% if
      form.confirm_password.errors %} {% for error in
      form.confirm_password.errors %}
      <div class="alert alert-danger">{{ error }}</div>
      {% endfor %} {% endif %}
    </div>
    <div class="form-group">{{ form.submit(class="btn btn-primary") }}</div>
  </form>
</div>
{% endblock %}

register.html предоставляет форму для регистрации нового пользователя. В этом шаблоне пользователи вводят свои имя, email и пароль, а также подтверждают пароль. Включены проверки на ошибки ввода.

Обзор файла main.css
Спойлер: пояснение
Я не планирую его объяснять, с этим можно легко разобраться самим. Код файла можете посмотреть в зип файле ниже!

Обзор файлов config.py и run.py

Файл config.py. Он используется для конфигурации таких аспектов, как безопасность, подключение к базе данных и интеграция с внешними сервисами. Одним из важнейших параметров является SECRET_KEY, который обеспечивает защиту данных сессий и предотвращает атаки, например, CSRF. Для повышения безопасности значение ключа рекомендуется хранить в переменной окружения, чтобы его можно было легко менять в зависимости от среды (разработка, тестирование, продакшен).

Далее, параметр SQLALCHEMY_DATABASE_URI отвечает за подключение к базе данных. Здесь используется подход с приоритетом переменных окружения: если в системе задан DATABASE_URL, приложение использует его; в противном случае используется локальная база данных PostgreSQL.

Настройка SQLALCHEMY_TRACK_MODIFICATIONS отключает ненужные уведомления об изменениях в объектах базы данных, что снижает нагрузку на систему. Также предусмотрен параметр GEMINI_API_KEY, который служит для взаимодействия с ИИ.

Python: Скопировать в буфер обмена
Код:
import os


class Config:
    SECRET_KEY = os.environ.get("SECRET_KEY") or "you-will-never-guess"
    SQLALCHEMY_DATABASE_URI = (
        os.environ.get("DATABASE_URL") or "postgresql://юзер:пасс@localhost:5432/example"
    )
    SQLALCHEMY_TRACK_MODIFICATIONS = False
    GEMINI_API_KEY = os.environ.get("GEMINI_API_KEY")

Файл run.py выполняет функцию точки входа в приложение.
Python: Скопировать в буфер обмена
Код:
from app import create_app

app = create_app()

if __name__ == "__main__":
    app.run(debug=True)

Заключение
Надеюсь проект и статья понравились, за этот код особо не бейте, 70% писал ночью, а на утро поправлял и дорабатывал. Идей не так много, может сменить тему ИИ 🤔, но думаю парочку статей еще напишу. Код проекта оставил ниже. Не забудьте перед этим установить админку PgAdmin4 для удобной работы и предварительно создать таблицы, запросы можете составить ориентируясь на models.py.
 
Сверху Снизу