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 .db import init_db, engine
from . import config, auth
from . import config, auth, assistant
from sqlmodel import Session, select
from .models import User, Event, GlobalStatus
from datetime import datetime, timezone
@ -156,6 +156,18 @@ def public_schedule():
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")
@require_role(["admin", "professor"])
@ -200,7 +212,6 @@ def add_event():
description = request.form.get("description")
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)
end_time = datetime.strptime(end_str, "%Y-%m-%dT%H:%M").replace(tzinfo=timezone.utc)

View File

@ -7,6 +7,7 @@
<style>
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; }
.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-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-title { font-size: 1.5rem; font-weight: bold; margin-bottom: 5px; }
.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-time { font-family: monospace; font-size: 1.1rem; color: #58a6ff; min-width: 120px; }
.event-details { flex: 1; }
@ -21,8 +23,21 @@
.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-desc { font-size: 0.95rem; margin-top: 10px; }
.back-link { display: inline-block; margin-bottom: 20px; color: #8b949e; text-decoration: none; }
.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>
</head>
<body>
@ -67,6 +82,62 @@
{% else %}
<p style="color: #8b949e;">Пока нет запланированных событий.</p>
{% 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>
<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>
</html>