git add . && git commit -m
This commit is contained in:
parent
ee0e62160f
commit
d2fbee3a12
103
app/assistant.py
Normal file
103
app/assistant.py
Normal 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 "Извините, я пока не понимаю этот вопрос. Попробуйте спросить: 'Где сейчас?', 'Когда следующая пара?', 'Что сегодня?' или 'Как связаться?'."
|
||||
15
app/main.py
15
app/main.py
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user