Initial commit: VoIdeaAI - voice-first AI idea assistant

This commit is contained in:
2026-05-13 12:51:42 +03:00
commit 688d043dad
421 changed files with 47915 additions and 0 deletions
+240
View File
@@ -0,0 +1,240 @@
"""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,
IdeaAnalyzeResponse,
IdeaCreate,
IdeaResponse,
IdeaUpdate,
)
from app.services.analysis_service import AnalysisService
from app.services.export_service import export_content
from app.services.idea_service import IdeaService
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}/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("/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)