feat: deploy infrastructure + disk/drive, calendar, presentation, workspaces, onboarding, demo user
This commit is contained in:
@@ -0,0 +1,184 @@
|
||||
"""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),
|
||||
)
|
||||
Reference in New Issue
Block a user