Files
voidea/app/services/presentation_service.py

185 lines
6.2 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Presentation service — generates HTML slides from idea + analysis reports."""
import json
import logging
from datetime import datetime, timezone
from typing import Any, Optional
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.agents.models import AgentReport
from app.models.idea import Idea
from app.services.disk_service import DiskService
logger = logging.getLogger(__name__)
ROLE_EMOJI = {
"coordinator": "🎯",
"task_organizer": "📋",
"business_analyst": "💼",
"legal_expert": "⚖️",
"financial_consultant": "💰",
"solution_architect": "🏗️",
"tester": "🧪",
"ui_designer": "🎨",
"smm_specialist": "📱",
"life_coach": "🌟",
"accessibility_expert": "",
}
ROLE_LABELS = {
"coordinator": "Координатор",
"task_organizer": "Организатор задач",
"business_analyst": "Бизнес-аналитик",
"legal_expert": "Юрист",
"financial_consultant": "Финансовый консультант",
"solution_architect": "Архитектор решения",
"tester": "Тестировщик",
"ui_designer": "UI/Дизайнер",
"smm_specialist": "SMM-специалист",
"life_coach": "Лайф-коуч",
"accessibility_expert": "Эксперт доступности",
}
REVEAL_HTML_TEMPLATE = """<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{title} — VoIdea Презентация</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/reveal.js@5.1.0/dist/reveal.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/reveal.js@5.1.0/dist/theme/white.css">
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600;700&display=swap');
* {{ font-family: 'Inter', sans-serif; }}
.reveal {{ font-size: 28px; }}
.reveal h1 {{ color: #2563EB; font-weight: 700; }}
.reveal h2 {{ color: #1E293B; font-weight: 600; }}
.reveal h3 {{ color: #475569; font-weight: 600; }}
.reveal .slides section {{ padding: 40px; }}
.slide-title {{ text-align: center; padding-top: 15vh; }}
.slide-title h1 {{ font-size: 3em; margin-bottom: 0.3em; }}
.slide-title .meta {{ color: #94A3B8; font-size: 0.5em; }}
.slide-role {{ text-align: left; }}
.slide-role h2 {{ font-size: 1.2em; margin-bottom: 0.5em; }}
.slide-role .emoji {{ font-size: 1.5em; margin-right: 0.3em; }}
.slide-role .content {{ font-size: 0.7em; line-height: 1.6; color: #334155; }}
.slide-exec {{ text-align: left; background: #EFF6FF; border-radius: 12px; padding: 30px !important; }}
.footer {{ position: fixed; bottom: 12px; left: 0; right: 0; text-align: center; font-size: 12px; color: #94A3B8; }}
</style>
</head>
<body>
<div class="reveal"><div class="slides">
<section class="slide-title">
<h1>{title}</h1>
<div class="meta">
{author} · {status_label} · {date}
</div>
</section>
{executive_summary}
{role_slides}
<section>
<h2>Спасибо за внимание!</h2>
<p style="color:#94A3B8;font-size:0.6em;">Создано в VoIdea · {date}</p>
</section>
</div></div>
<div class="footer">VoIdea · {date}</div>
<script src="https://cdn.jsdelivr.net/npm/reveal.js@5.1.0/dist/reveal.js"></script>
<script>Reveal.initialize({{ controls: true, progress: true, hash: true }});</script>
</body>
</html>"""
class PresentationService:
def __init__(self, db: AsyncSession):
self.db = db
async def generate(
self,
idea_id: str,
roles: Optional[list[str]] = None,
user_name: str = "",
) -> Optional[str]:
"""Generate HTML presentation for an idea.
Args:
idea_id: UUID of the idea
roles: List of roles to include (None = all available)
user_name: Author display name
Returns:
HTML string or None if idea not found
"""
result = await self.db.execute(
select(Idea).where(Idea.id == idea_id)
)
idea = result.scalar_one_or_none()
if not idea:
return None
result = await self.db.execute(
select(AgentReport).where(
AgentReport.details["idea_id"].as_string() == idea_id
).order_by(AgentReport.created_at.asc())
)
reports = result.scalars().all()
if roles is not None:
reports = [
r for r in reports
if r.agent_id.replace("ai_", "") in roles
]
funnel_labels = {
"raw": "Сырая",
"validated": "Подтверждена",
"backlog": "Бэклог",
"in_progress": "В работе",
"launched": "Запущена",
"retrospective": "Ретроспектива",
}
status_label = funnel_labels.get(
idea.funnel_status or "", idea.funnel_status or "Без статуса"
)
date_str = datetime.now(timezone.utc).strftime("%d.%m.%Y")
role_slides = []
for r in reports:
role_name = r.agent_id.replace("ai_", "")
emoji = ROLE_EMOJI.get(role_name, "📄")
label = ROLE_LABELS.get(role_name, role_name)
content = r.message or "Нет данных"
role_slides.append(f"""
<section class="slide-role">
<h2><span class="emoji">{emoji}</span> {label}</h2>
<div class="content">{content}</div>
</section>""")
success_count = sum(1 for r in reports if r.success)
if role_slides:
exec_summary = f"""
<section class="slide-exec">
<h2>📊 Executive Summary</h2>
<p>Проанализировано ролей: <strong>{len(reports)}</strong></p>
<p>Успешных анализов: <strong>{success_count}</strong></p>
</section>"""
else:
exec_summary = ""
return REVEAL_HTML_TEMPLATE.format(
title=idea.title,
author=user_name or "Пользователь",
status_label=status_label,
date=date_str,
executive_summary=exec_summary,
role_slides="".join(role_slides),
)