337 lines
12 KiB
Python
337 lines
12 KiB
Python
"""Ideas API routes for VoIdea."""
|
|
|
|
from typing import Annotated
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from fastapi.responses import PlainTextResponse
|
|
|
|
from uuid import uuid4
|
|
|
|
from app.core.dependencies import get_current_user, get_db, get_optional_user
|
|
from app.models.idea import Idea
|
|
from app.models.user import User
|
|
from app.schemas.idea import (
|
|
AnalysisResultResponse,
|
|
FunnelTransitionRequest,
|
|
IdeaAnalyzeResponse,
|
|
IdeaCreate,
|
|
IdeaResponse,
|
|
IdeaUpdate,
|
|
)
|
|
from app.services.analysis_service import AnalysisService
|
|
from app.services.collaboration_service import CollaborationService
|
|
from app.services.disk_service import DiskService
|
|
from app.services.export_service import export_content
|
|
from app.services.idea_service import IdeaService
|
|
from app.services.presentation_service import PresentationService
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
def _idea_to_response(idea) -> IdeaResponse:
|
|
return IdeaResponse(
|
|
id=str(idea.id),
|
|
user_id=str(idea.user_id),
|
|
title=idea.title,
|
|
content=idea.content,
|
|
status=idea.status,
|
|
tags=idea.tags,
|
|
is_public=idea.is_public,
|
|
public_slug=idea.public_slug,
|
|
created_at=idea.created_at,
|
|
updated_at=idea.updated_at,
|
|
)
|
|
|
|
|
|
@router.get("/", response_model=list[IdeaResponse])
|
|
async def list_ideas(
|
|
user: Annotated[User, Depends(get_current_user)],
|
|
db: AsyncSession = Depends(get_db),
|
|
skip: int = Query(0, ge=0),
|
|
limit: int = Query(50, ge=1, le=100),
|
|
):
|
|
service = IdeaService(db)
|
|
ideas = await service.list_by_user(str(user.id), skip=skip, limit=limit)
|
|
return [_idea_to_response(idea) for idea in ideas]
|
|
|
|
|
|
@router.post("/", response_model=IdeaResponse, status_code=status.HTTP_201_CREATED)
|
|
async def create_idea(
|
|
body: IdeaCreate,
|
|
user: Annotated[User, Depends(get_current_user)],
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
service = IdeaService(db)
|
|
idea = await service.create(
|
|
user_id=str(user.id),
|
|
title=body.title,
|
|
content=body.content,
|
|
tags=body.tags,
|
|
is_public=body.is_public,
|
|
)
|
|
return _idea_to_response(idea)
|
|
|
|
|
|
@router.get("/{idea_id}", response_model=IdeaResponse)
|
|
async def get_idea(
|
|
idea_id: str,
|
|
user: Annotated[User, Depends(get_current_user)],
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
service = IdeaService(db)
|
|
idea = await service.get_by_id(idea_id)
|
|
if not idea or str(idea.user_id) != str(user.id):
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Idea not found")
|
|
return _idea_to_response(idea)
|
|
|
|
|
|
@router.patch("/{idea_id}", response_model=IdeaResponse)
|
|
async def update_idea(
|
|
idea_id: str,
|
|
body: IdeaUpdate,
|
|
user: Annotated[User, Depends(get_current_user)],
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
service = IdeaService(db)
|
|
idea = await service.update(
|
|
idea_id,
|
|
str(user.id),
|
|
title=body.title,
|
|
content=body.content,
|
|
status=body.status,
|
|
tags=body.tags,
|
|
is_public=body.is_public,
|
|
)
|
|
if not idea:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Idea not found")
|
|
return _idea_to_response(idea)
|
|
|
|
|
|
@router.delete("/{idea_id}", status_code=status.HTTP_204_NO_CONTENT)
|
|
async def delete_idea(
|
|
idea_id: str,
|
|
user: Annotated[User, Depends(get_current_user)],
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
service = IdeaService(db)
|
|
deleted = await service.delete(idea_id, str(user.id))
|
|
if not deleted:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Idea not found")
|
|
|
|
|
|
@router.post("/{idea_id}/analyze", response_model=IdeaAnalyzeResponse)
|
|
async def analyze_idea(
|
|
idea_id: str,
|
|
user: Annotated[User, Depends(get_current_user)],
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
idea_service = IdeaService(db)
|
|
idea = await idea_service.get_by_id(idea_id)
|
|
if not idea or str(idea.user_id) != str(user.id):
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Idea not found")
|
|
|
|
analysis_service = AnalysisService(db)
|
|
result = await analysis_service.start_analysis(idea_id)
|
|
|
|
return IdeaAnalyzeResponse(
|
|
status="started",
|
|
idea_id=idea_id,
|
|
task_count=result["task_count"],
|
|
tasks=result["tasks"],
|
|
)
|
|
|
|
|
|
@router.get("/{idea_id}/analysis", response_model=list[AnalysisResultResponse])
|
|
async def get_analysis_results(
|
|
idea_id: str,
|
|
user: Annotated[User, Depends(get_current_user)],
|
|
db: AsyncSession = Depends(get_db),
|
|
role: str = Query(None, description="Filter by agent role"),
|
|
):
|
|
idea_service = IdeaService(db)
|
|
idea = await idea_service.get_by_id(idea_id)
|
|
if not idea or str(idea.user_id) != str(user.id):
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Idea not found")
|
|
|
|
analysis_service = AnalysisService(db)
|
|
results = await analysis_service.get_analysis_results(idea_id, role=role)
|
|
|
|
return [AnalysisResultResponse(**r) for r in results]
|
|
|
|
|
|
@router.get("/{idea_id}/export")
|
|
async def export_idea(
|
|
idea_id: str,
|
|
user: Annotated[User, Depends(get_current_user)],
|
|
db: AsyncSession = Depends(get_db),
|
|
format: str = Query("md", regex="^(json|md|html)$"),
|
|
):
|
|
idea_service = IdeaService(db)
|
|
idea = await idea_service.get_by_id(idea_id)
|
|
if not idea or str(idea.user_id) != str(user.id):
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Idea not found")
|
|
|
|
metadata = {
|
|
"ID": str(idea.id),
|
|
"Статус": idea.status,
|
|
"Теги": ", ".join(idea.tags) if idea.tags else "—",
|
|
"Создано": idea.created_at.isoformat() if idea.created_at else "—",
|
|
}
|
|
body, content_type, ext = export_content(format, idea.title, idea.content, metadata)
|
|
filename = f"{idea.title[:50]}.{ext}".replace(" ", "_")
|
|
|
|
from fastapi.responses import Response
|
|
return Response(
|
|
content=body,
|
|
media_type=content_type,
|
|
headers={"Content-Disposition": f'attachment; filename="{filename}"'},
|
|
)
|
|
|
|
|
|
@router.post("/{idea_id}/presentation")
|
|
async def generate_presentation(
|
|
idea_id: str,
|
|
user: Annotated[User, Depends(get_current_user)],
|
|
db: AsyncSession = Depends(get_db),
|
|
roles: str = Query("", description="Comma-separated role names (empty=all)"),
|
|
destination: str = Query("download", regex="^(download|drive)$"),
|
|
drive_provider: str = Query("", description="Specific drive provider (google/yandex/apple)"),
|
|
):
|
|
"""Generate HTML presentation for an idea.
|
|
|
|
Args:
|
|
destination: "download" = return as file, "drive" = upload to cloud drive
|
|
roles: Comma-separated agent roles (empty = all available)
|
|
drive_provider: Specific provider to upload to (empty = auto-detect)
|
|
"""
|
|
idea_svc = IdeaService(db)
|
|
idea = await idea_svc.get_by_id(idea_id)
|
|
if not idea or str(idea.user_id) != str(user.id):
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Idea not found")
|
|
|
|
role_list = [r.strip() for r in roles.split(",") if r.strip()] if roles else None
|
|
pres_svc = PresentationService(db)
|
|
html = await pres_svc.generate(idea_id, roles=role_list, user_name=user.display_name)
|
|
if not html:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Idea not found")
|
|
|
|
filename = f"prezentaciya_{idea.title[:40].replace(' ', '_')}.html"
|
|
|
|
if destination == "drive":
|
|
disk_svc = DiskService(db)
|
|
content_bytes = html.encode("utf-8")
|
|
if drive_provider:
|
|
success = await disk_svc.upload_to_provider(
|
|
user, drive_provider, filename, content_bytes
|
|
)
|
|
if not success:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail=f"Failed to upload to {drive_provider}",
|
|
)
|
|
return {"success": True, "provider": drive_provider, "filename": filename}
|
|
else:
|
|
result = await disk_svc.upload_to_any(user, filename, content_bytes)
|
|
if not result:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail="No connected drive. Connect a cloud drive first.",
|
|
)
|
|
return {"success": True, "provider": result["provider"], "filename": filename}
|
|
|
|
from fastapi.responses import Response
|
|
return Response(
|
|
content=html,
|
|
media_type="text/html; charset=utf-8",
|
|
headers={"Content-Disposition": f'attachment; filename="{filename}"'},
|
|
)
|
|
|
|
|
|
@router.post("/{idea_id}/share", response_model=IdeaResponse)
|
|
async def toggle_idea_share(
|
|
idea_id: str,
|
|
user: Annotated[User, Depends(get_current_user)],
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
service = IdeaService(db)
|
|
idea = await service.get_by_id(idea_id)
|
|
if not idea or str(idea.user_id) != str(user.id):
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Idea not found")
|
|
|
|
idea.is_public = not idea.is_public
|
|
if idea.is_public and not idea.public_slug:
|
|
idea.public_slug = uuid4().hex[:16]
|
|
elif not idea.is_public:
|
|
idea.public_slug = None
|
|
|
|
await db.commit()
|
|
await db.refresh(idea)
|
|
return _idea_to_response(idea)
|
|
|
|
|
|
@router.post("/{idea_id}/funnel", response_model=IdeaResponse)
|
|
async def transition_funnel(
|
|
idea_id: str,
|
|
body: FunnelTransitionRequest,
|
|
user: Annotated[User, Depends(get_current_user)],
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
service = IdeaService(db)
|
|
idea = await service.get_by_id(idea_id)
|
|
if not idea or str(idea.user_id) != str(user.id):
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Idea not found")
|
|
|
|
target = body.target_status
|
|
valid_states = Idea.FUNNEL_STATES
|
|
if target not in valid_states:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail=f"Invalid funnel status. Valid: {', '.join(valid_states)}",
|
|
)
|
|
|
|
idea.funnel_status = target
|
|
await db.commit()
|
|
await db.refresh(idea)
|
|
|
|
collab = CollaborationService(db)
|
|
await collab.log_activity(
|
|
UUID(idea_id), user.id, "funnel",
|
|
{"from": idea.funnel_status, "to": target},
|
|
)
|
|
|
|
return _idea_to_response(idea)
|
|
|
|
|
|
@router.post("/demo", response_model=IdeaResponse, status_code=status.HTTP_201_CREATED)
|
|
async def create_demo_idea(
|
|
user: Annotated[User, Depends(get_current_user)],
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
service = IdeaService(db)
|
|
idea = await service.create(
|
|
user_id=str(user.id),
|
|
title="Моя первая идея (демо)",
|
|
content="Пример идеи для знакомства с VoIdeaAI. Замените этот текст на свою идею.\n\n"
|
|
"VoIdeaAI поможет проанализировать её с разных сторон: бизнес, финансы, "
|
|
"право, технологии и маркетинг. Просто нажмите «Анализ» на странице идеи.",
|
|
tags=["demo", "привет"],
|
|
is_public=False,
|
|
)
|
|
return _idea_to_response(idea)
|
|
|
|
|
|
@router.get("/shared/{slug}", response_model=IdeaResponse)
|
|
async def get_shared_idea(
|
|
slug: str,
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
result = await db.execute(
|
|
select(Idea).where(Idea.public_slug == slug, Idea.is_public == True)
|
|
)
|
|
idea = result.scalar_one_or_none()
|
|
if not idea:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Idea not found or not shared")
|
|
return _idea_to_response(idea)
|