git add . && git commit -m

This commit is contained in:
Ivan Budakov 2026-10-19 18:17:07 +03:00
parent ee0e62160f
commit d2fbee3a12
3 changed files with 187 additions and 2 deletions

103
app/assistant.py Normal file
View File

@ -0,0 +1,103 @@
import re
from datetime import datetime, timezone, timedelta
from sqlmodel import Session, select
from .models import Event, GlobalStatus
from .db import engine
def get_response(text: str) -> str:
"""Принимает вопрос, определяет интент и возвращает ответ."""
text = text.lower().strip()
with Session(engine) as db_session:
# Получаем текущий статус
current_status = db_session.exec(select(GlobalStatus)).first()
# --- ИНТЕНТ: ГДЕ СЕЙЧАС / СТАТУС ---
if re.search(r'где|статус|сейчас', text):
if current_status.status == 'on_place':
return "Никита Сергеевич сейчас на месте (в аудитории)."
elif current_status.status == 'delayed':
return f"Никита Сергеевич опаздывает на {current_status.delay_minutes} мин. Причина: {current_status.message or 'не указана'}."
elif current_status.status == 'away':
return "Никита Сергеевич вне вуза."
elif current_status.status == 'cancelled':
return f"Занятия отменены! {current_status.message or ''}"
else:
return "Никита Сергеевич сейчас свободен / вне расписания."
# --- ИНТЕНТ: ОПАЗДЫВАЕТ? ---
if re.search(r'опаздыва|задержива', text):
if current_status.status == 'delayed':
return f"Да, опаздывает на {current_status.delay_minutes} мин. Причина: {current_status.message or 'не указана'}."
else:
return "По текущему статусу, он не опаздывает."
# --- ИНТЕНТ: КОГДА СЛЕДУЮЩАЯ ПАРА / БЛИЖАЙШЕЕ ---
if re.search(r'когда|следующ|ближайш', text):
now = datetime.now(timezone.utc)
next_event = db_session.exec(
select(Event)
.where(Event.is_public == True)
.where(Event.start_time >= now)
.order_by(Event.start_time)
.limit(1)
).first()
if next_event:
local_time = next_event.start_time.strftime('%d.%m в %H:%M')
return f"Ближайшее событие: {next_event.title} ({local_time}). Место: {next_event.location or 'не указано'}."
else:
return "В ближайшее время событий в расписании нет."
# --- ИНТЕНТ: РАСПИСАНИЕ НА СЕГОДНЯ ---
if re.search(r'сегодня|седня', text):
# Берем начало сегодняшнего дня и конец сегодняшнего дня
now = datetime.now(timezone.utc)
start_of_day = now.replace(hour=0, minute=0, second=0, microsecond=0)
end_of_day = start_of_day + timedelta(days=1)
events = db_session.exec(
select(Event)
.where(Event.is_public == True)
.where(Event.start_time >= start_of_day)
.where(Event.start_time < end_of_day)
.order_by(Event.start_time)
).all()
if not events:
return "На сегодня событий в расписании нет."
res = "Расписание на сегодня:\n"
for e in events:
res += f"• {e.start_time.strftime('%H:%M')} - {e.title} ({e.location or 'без места'})\n"
return res
# --- ИНТЕНТ: РАСПИСАНИЕ НА НЕДЕЛЮ ---
if re.search(r'недел|7 дней', text):
now = datetime.now(timezone.utc)
end_of_week = now + timedelta(days=7)
events = db_session.exec(
select(Event)
.where(Event.is_public == True)
.where(Event.start_time >= now)
.where(Event.start_time <= end_of_week)
.order_by(Event.start_time)
).all()
if not events:
return "На ближайшую неделю событий нет."
res = "Расписание на неделю:\n"
for e in events[:5]: # Показываем первые 5, чтобы не спамить
res += f"• {e.start_time.strftime('%d.%m %H:%M')} - {e.title}\n"
if len(events) > 5:
res += "...и другие события (смотрите на странице)."
return res
# --- ИНТЕНТ: КОНТАКТЫ ---
if re.search(r'связаться|контакт|телеграм|почта|кабинет', text):
return "Связаться с Никитой Сергеевичем можно по почте: freezer.nik@gmail.com (или в Telegram)."
# --- ЕСЛИ НИЧЕГО НЕ ПОНЯЛ ---
return "Извините, я пока не понимаю этот вопрос. Попробуйте спросить: 'Где сейчас?', 'Когда следующая пара?', 'Что сегодня?' или 'Как связаться?'."

View File

@ -1,6 +1,6 @@
from flask import Flask, render_template, jsonify, redirect, url_for, request, session from flask import Flask, render_template, jsonify, redirect, url_for, request, session
from .db import init_db, engine from .db import init_db, engine
from . import config, auth from . import config, auth, assistant
from sqlmodel import Session, select from sqlmodel import Session, select
from .models import User, Event, GlobalStatus from .models import User, Event, GlobalStatus
from datetime import datetime, timezone from datetime import datetime, timezone
@ -156,6 +156,18 @@ def public_schedule():
return render_template("schedule.html", status=current_status, events=events) return render_template("schedule.html", status=current_status, events=events)
# --- АССИСТЕНТ: API ---
@app.route("/api/assistant", methods=["POST"])
def api_assistant():
data = request.get_json()
if not data or "message" not in data:
return jsonify({"error": "Нет сообщения"}), 400
user_message = data["message"]
reply = assistant.get_response(user_message)
return jsonify({"reply": reply})
# --- РАСПИСАНИЕ: АДМИН-ПАНЕЛЬ --- # --- РАСПИСАНИЕ: АДМИН-ПАНЕЛЬ ---
@app.route("/admin/schedule") @app.route("/admin/schedule")
@require_role(["admin", "professor"]) @require_role(["admin", "professor"])
@ -200,7 +212,6 @@ def add_event():
description = request.form.get("description") description = request.form.get("description")
is_public = request.form.get("is_public") == "on" is_public = request.form.get("is_public") == "on"
# Парсим дату из input type="datetime-local"
start_time = datetime.strptime(start_str, "%Y-%m-%dT%H:%M").replace(tzinfo=timezone.utc) start_time = datetime.strptime(start_str, "%Y-%m-%dT%H:%M").replace(tzinfo=timezone.utc)
end_time = datetime.strptime(end_str, "%Y-%m-%dT%H:%M").replace(tzinfo=timezone.utc) end_time = datetime.strptime(end_str, "%Y-%m-%dT%H:%M").replace(tzinfo=timezone.utc)

View File

@ -7,6 +7,7 @@
<style> <style>
body { background: #0d1117; color: #e6edf3; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; margin: 0; padding: 40px 20px; } body { background: #0d1117; color: #e6edf3; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; margin: 0; padding: 40px 20px; }
.container { max-width: 800px; margin: 0 auto; } .container { max-width: 800px; margin: 0 auto; }
.status-banner { padding: 20px; border-radius: 10px; text-align: center; margin-bottom: 30px; } .status-banner { padding: 20px; border-radius: 10px; text-align: center; margin-bottom: 30px; }
.status-on_place { background: rgba(35, 134, 54, 0.2); border: 1px solid #238636; color: #3fb950; } .status-on_place { background: rgba(35, 134, 54, 0.2); border: 1px solid #238636; color: #3fb950; }
.status-delayed { background: rgba(210, 153, 34, 0.2); border: 1px solid #d29922; color: #d29922; } .status-delayed { background: rgba(210, 153, 34, 0.2); border: 1px solid #d29922; color: #d29922; }
@ -14,6 +15,7 @@
.status-free { background: rgba(139, 148, 158, 0.2); border: 1px solid #8b949e; color: #8b949e; } .status-free { background: rgba(139, 148, 158, 0.2); border: 1px solid #8b949e; color: #8b949e; }
.status-title { font-size: 1.5rem; font-weight: bold; margin-bottom: 5px; } .status-title { font-size: 1.5rem; font-weight: bold; margin-bottom: 5px; }
.status-message { font-size: 1.1rem; } .status-message { font-size: 1.1rem; }
.event-card { background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 20px; margin-bottom: 15px; display: flex; gap: 20px; } .event-card { background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 20px; margin-bottom: 15px; display: flex; gap: 20px; }
.event-time { font-family: monospace; font-size: 1.1rem; color: #58a6ff; min-width: 120px; } .event-time { font-family: monospace; font-size: 1.1rem; color: #58a6ff; min-width: 120px; }
.event-details { flex: 1; } .event-details { flex: 1; }
@ -21,8 +23,21 @@
.event-meta { color: #8b949e; font-size: 0.9rem; margin-bottom: 5px; } .event-meta { color: #8b949e; font-size: 0.9rem; margin-bottom: 5px; }
.event-type { display: inline-block; background: #21262d; padding: 2px 8px; border-radius: 4px; font-size: 0.8rem; text-transform: uppercase; } .event-type { display: inline-block; background: #21262d; padding: 2px 8px; border-radius: 4px; font-size: 0.8rem; text-transform: uppercase; }
.event-desc { font-size: 0.95rem; margin-top: 10px; } .event-desc { font-size: 0.95rem; margin-top: 10px; }
.back-link { display: inline-block; margin-bottom: 20px; color: #8b949e; text-decoration: none; } .back-link { display: inline-block; margin-bottom: 20px; color: #8b949e; text-decoration: none; }
.back-link:hover { color: #58a6ff; } .back-link:hover { color: #58a6ff; }
/* Стили для чата */
.chat-container { background: #161b22; border: 1px solid #30363d; border-radius: 8px; margin-top: 40px; display: flex; flex-direction: column; height: 400px; }
.chat-header { padding: 15px; border-bottom: 1px solid #30363d; background: #21262d; border-radius: 8px 8px 0 0; font-weight: bold; }
.chat-messages { flex: 1; padding: 15px; overflow-y: auto; display: flex; flex-direction: column; gap: 10px; }
.msg { max-width: 80%; padding: 10px 15px; border-radius: 8px; font-size: 0.95rem; line-height: 1.4; white-space: pre-wrap; }
.msg.bot { background: #21262d; align-self: flex-start; border: 1px solid #30363d; }
.msg.user { background: #1f6feb; color: white; align-self: flex-end; }
.chat-input-area { padding: 15px; border-top: 1px solid #30363d; display: flex; gap: 10px; }
.chat-input-area input { flex: 1; padding: 10px; border-radius: 5px; border: 1px solid #30363d; background: #0d1117; color: white; outline: none; }
.chat-input-area button { padding: 10px 20px; background: #238636; color: white; border: none; border-radius: 5px; cursor: pointer; font-weight: bold; }
.chat-input-area button:hover { background: #2ea043; }
</style> </style>
</head> </head>
<body> <body>
@ -67,6 +82,62 @@
{% else %} {% else %}
<p style="color: #8b949e;">Пока нет запланированных событий.</p> <p style="color: #8b949e;">Пока нет запланированных событий.</p>
{% endfor %} {% endfor %}
<!-- Блок Чат-бота -->
<div class="chat-container">
<div class="chat-header">🤖 Ассистент расписания</div>
<div class="chat-messages" id="chat-messages">
<div class="msg bot">Привет! Я могу подсказать статус Никиты Сергеевича, его расписание или контакты. Спроси меня о чем-нибудь.</div>
</div>
<form class="chat-input-area" id="chat-form">
<input type="text" id="chat-input" placeholder="Например: Где сейчас? Что на неделе?" autocomplete="off">
<button type="submit">Отправить</button>
</form>
</div>
</div> </div>
<script>
const chatForm = document.getElementById('chat-form');
const chatInput = document.getElementById('chat-input');
const chatMessages = document.getElementById('chat-messages');
chatForm.addEventListener('submit', async (e) => {
e.preventDefault();
const text = chatInput.value.trim();
if (!text) return;
// Добавляем сообщение пользователя
addMessage(text, 'user');
chatInput.value = '';
// Добавляем индикатор загрузки
const loadingMsg = addMessage('...', 'bot');
try {
const response = await fetch('/api/assistant', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message: text })
});
const data = await response.json();
// Заменяем индикатор загрузки на ответ бота
loadingMsg.textContent = data.reply || 'Ошибка: пустой ответ';
} catch (err) {
loadingMsg.textContent = 'Ошибка соединения с сервером.';
}
});
function addMessage(text, sender) {
const div = document.createElement('div');
div.className = `msg ${sender}`;
div.textContent = text;
chatMessages.appendChild(div);
chatMessages.scrollTop = chatMessages.scrollHeight;
return div;
}
</script>
</body> </body>
</html> </html>