From 52ab7e1ac47bc8adc1e8fb86918bf6d198aa3bd1 Mon Sep 17 00:00:00 2001 From: grigo Date: Thu, 11 Jun 2026 09:09:51 +0300 Subject: [PATCH] fixed timer --- backend/app/chat/notice_inbox.py | 35 +++++++++++++++++----- backend/app/chat/service.py | 3 +- backend/app/pomodoro/completion.py | 47 +++++++++++++++++++++++++++--- frontend/src/pages/Chat.tsx | 10 +++++-- 4 files changed, 80 insertions(+), 15 deletions(-) diff --git a/backend/app/chat/notice_inbox.py b/backend/app/chat/notice_inbox.py index 5676c8b..6a3494b 100644 --- a/backend/app/chat/notice_inbox.py +++ b/backend/app/chat/notice_inbox.py @@ -5,21 +5,40 @@ from sqlalchemy import select from app.db.base import SessionLocal from app.db.models import ChatSession, Message +DISPLAY_ONLY_ROLES = frozenset({"notice", "character"}) + + +def _latest_chat_session(db) -> ChatSession: + session = db.scalar( + select(ChatSession).order_by(ChatSession.updated_at.desc()).limit(1) + ) + if not session: + session = ChatSession(title="Уведомления") + db.add(session) + db.commit() + db.refresh(session) + return session + def post_notice_to_latest_chat(content: str) -> int | None: """Сохраняет notice в последний активный чат. Возвращает session_id.""" db = SessionLocal() try: - session = db.scalar( - select(ChatSession).order_by(ChatSession.updated_at.desc()).limit(1) - ) - if not session: - session = ChatSession(title="Уведомления") - db.add(session) - db.commit() - db.refresh(session) + session = _latest_chat_session(db) db.add(Message(session_id=session.id, role="notice", content=content)) db.commit() return session.id finally: db.close() + + +def post_character_comment_to_latest_chat(content: str) -> int | None: + """Реплика персонажа в UI; не попадает в контекст LLM (в отличие от assistant).""" + db = SessionLocal() + try: + session = _latest_chat_session(db) + db.add(Message(session_id=session.id, role="character", content=content)) + db.commit() + return session.id + finally: + db.close() diff --git a/backend/app/chat/service.py b/backend/app/chat/service.py index 2876ce3..3a0cda2 100644 --- a/backend/app/chat/service.py +++ b/backend/app/chat/service.py @@ -12,6 +12,7 @@ from app.config import get_settings from app.db.base import SessionLocal from app.character.service import CharacterService from app.chat.history import sanitize_openai_messages, strip_historical_reasoning +from app.chat.notice_inbox import DISPLAY_ONLY_ROLES from app.chat.notices import ( POMODORO_TOOL_NAMES, format_pomodoro_context, @@ -112,7 +113,7 @@ class ChatService: def _build_messages(self, session: ChatSession) -> list[dict[str, Any]]: system_prompt = self._build_system_prompt(session.id) - all_chat = [m for m in session.messages if m.role != "notice"] + all_chat = [m for m in session.messages if m.role not in DISPLAY_ONLY_ROLES] last_user = next((m.content for m in reversed(all_chat) if m.role == "user"), "") if last_user: memory_snapshot = get_memory_snapshot(self.db, session.id) diff --git a/backend/app/pomodoro/completion.py b/backend/app/pomodoro/completion.py index b7d3891..1d2fa42 100644 --- a/backend/app/pomodoro/completion.py +++ b/backend/app/pomodoro/completion.py @@ -2,9 +2,11 @@ import logging from sqlalchemy.orm import Session -from app.chat.notice_inbox import post_notice_to_latest_chat +from app.character.service import CharacterService +from app.chat.notice_inbox import post_character_comment_to_latest_chat, post_notice_to_latest_chat from app.chat.notices import format_phase_completed_notice from app.db.models import PomodoroSession +from app.llm.client import LLMClient from app.pomodoro.cycle import PHASE_LONG_BREAK, PHASE_SHORT_BREAK, PHASE_WORK, CycleManager from app.pomodoro.service import PomodoroService @@ -22,6 +24,38 @@ class PomodoroCompletionHandler: self.db = db self.pomodoro = PomodoroService(db) self.cycle = CycleManager(db) + self.llm = LLMClient() + self.character = CharacterService() + + async def _generate_llm_comment( + self, + session: PomodoroSession, + next_phase: str | None, + ) -> str: + cycle = self.cycle.to_dict() + phase_label = PHASE_LABELS.get(session.phase, session.phase) + next_label = PHASE_LABELS.get(next_phase, "пауза") if next_phase else "отдых, цикл сброшен" + work_done = cycle["completed_work_sessions"] + if session.phase == PHASE_WORK: + work_done += 1 + + system = self.character.get_system_prompt() + user_prompt = f"""Фаза помидоро «{phase_label}» только что завершилась. +Задача: {session.task_note or 'без описания'} +Прогресс цикла: {work_done}/{cycle['sessions_until_long_break']} работ. +Следующая фаза: {next_label}. + +Напиши пользователю короткое сообщение (2-4 предложения) на русском: поздравь, поддержи или предложи отдохнуть. Без markdown и без эмодзи.""" + + result = await self.llm.complete( + [ + {"role": "system", "content": system}, + {"role": "user", "content": user_prompt}, + ], + temperature=0.8, + visible_reply=True, + ) + return (result.get("content") or "").strip() or "Фаза завершена. Хорошая работа." def _resolve_next_phase(self, session: PomodoroSession) -> str | None: phase = session.phase @@ -42,11 +76,16 @@ class PomodoroCompletionHandler: next_phase = self._resolve_next_phase(session) notice = format_phase_completed_notice(session, next_phase) - - # Только notice — role=assistant ломает tool/reasoning цепочки OpenRouter. post_notice_to_latest_chat(notice) + try: + comment = await self._generate_llm_comment(session, next_phase) + if comment: + post_character_comment_to_latest_chat(comment) + except Exception: + logger.exception("Pomodoro LLM comment failed (phase=%s)", session.phase) + self.cycle.bump_notify_seq() self.pomodoro.mark_notified(session) self.pomodoro.advance_after_completion(session) - logger.info("Pomodoro phase completed notice posted (phase=%s)", session.phase) + logger.info("Pomodoro phase completed (phase=%s, next=%s)", session.phase, next_phase) diff --git a/frontend/src/pages/Chat.tsx b/frontend/src/pages/Chat.tsx index 9edaa75..a042c3f 100644 --- a/frontend/src/pages/Chat.tsx +++ b/frontend/src/pages/Chat.tsx @@ -27,10 +27,16 @@ function noticeLabel(content: string): string { function roleLabel(role: string, content = ""): string { if (role === "notice") return noticeLabel(content); + if (role === "character") return "assistant"; if (role === "user") return "вы"; return role; } +function messageClassName(role: string): string { + if (role === "character") return "assistant"; + return role; +} + export default function Chat() { const [sessions, setSessions] = useState([]); const [activeId, setActiveId] = useState(null); @@ -273,10 +279,10 @@ export default function Chat() { onClick={dismissKeyboard} > {visibleMessages.map((msg) => ( -
+
{roleLabel(msg.role, msg.content)}
- {msg.role === "assistant" || msg.role === "notice" ? ( + {msg.role === "assistant" || msg.role === "notice" || msg.role === "character" ? ( {msg.content} ) : ( msg.content