Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion backend/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

from core.rate_limit import limiter
from database import get_db
from routers import annotate, auth, clean, collect, data, review, seed, users
from routers import annotate, auth, clean, collect, dashboard, data, review, seed, users

# Em produção, definir CORS_ORIGINS no Vercel Dashboard:
# CORS_ORIGINS=https://seu-frontend.vercel.app
Expand Down Expand Up @@ -45,6 +45,7 @@ async def rate_limit_handler(request: Request, exc: RateLimitExceeded):
app.include_router(collect.router)
app.include_router(clean.router)
app.include_router(data.router)
app.include_router(dashboard.router)
app.include_router(annotate.router)
app.include_router(review.router)
app.include_router(seed.router)
Expand Down
1 change: 1 addition & 0 deletions backend/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,4 @@ pytest-mock==3.14.0
ruff==0.6.9
bandit==1.9.4
pip-audit==2.10.0
plotly==5.24.1
96 changes: 96 additions & 0 deletions backend/routers/dashboard.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
"""Router da US-06 — Dashboard de Análise."""

from fastapi import APIRouter, Depends, Query
from sqlalchemy.orm import Session

from database import get_db
from models.user import User
from schemas.dashboard import (
BotCommentsResponse,
CriteriaEffectivenessItem,
GlobalDashboardResponse,
UserDashboardResponse,
VideoDashboardResponse,
)
from services.auth import get_current_user
from services.dashboard import (
get_bot_comments,
get_criteria_effectiveness,
get_global_dashboard,
get_user_dashboard,
get_video_dashboard,
)

router = APIRouter(prefix="/dashboard", tags=["dashboard"])


@router.get("/global", response_model=GlobalDashboardResponse)
def global_endpoint(
criteria: str | None = Query(default=None),
db: Session = Depends(get_db),
_user: User = Depends(get_current_user),
):
criteria_list = (
[c.strip() for c in criteria.split(",") if c.strip()] if criteria else None
)
return get_global_dashboard(db, criteria=criteria_list)


@router.get(
"/criteria-effectiveness",
response_model=list[CriteriaEffectivenessItem],
)
def criteria_effectiveness_endpoint(
video_id: str | None = Query(default=None),
db: Session = Depends(get_db),
_user: User = Depends(get_current_user),
):
return get_criteria_effectiveness(db, video_id=video_id)


@router.get("/video", response_model=VideoDashboardResponse)
def video_endpoint(
video_id: str = Query(),
criteria: str | None = Query(default=None),
db: Session = Depends(get_db),
_user: User = Depends(get_current_user),
):
criteria_list = (
[c.strip() for c in criteria.split(",") if c.strip()] if criteria else None
)
return get_video_dashboard(db, video_id=video_id, criteria=criteria_list)


@router.get("/user", response_model=UserDashboardResponse)
def user_endpoint(
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
return get_user_dashboard(db, user_id=current_user.id)


@router.get("/bots", response_model=BotCommentsResponse)
def bots_endpoint(
dataset_id: str | None = Query(default=None),
video_id: str | None = Query(default=None),
author: str | None = Query(default=None),
search: str | None = Query(default=None),
criteria: str | None = Query(default=None),
page: int = Query(default=1, ge=1),
page_size: int = Query(default=20, ge=1, le=100),
db: Session = Depends(get_db),
_user: User = Depends(get_current_user),
):
criteria_list = (
[c.strip() for c in criteria.split(",") if c.strip()] if criteria else None
)
return get_bot_comments(
db,
dataset_id=dataset_id,
video_id=video_id,
author=author,
search=search,
criteria_filter=criteria_list,
page=page,
page_size=page_size,
)
126 changes: 126 additions & 0 deletions backend/schemas/dashboard.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
"""Schemas Pydantic da US-06 — Dashboard de Análise."""

import uuid

from pydantic import BaseModel

# ── Visão Geral (Global) ───────────────────────────────────────────


class GlobalSummary(BaseModel):
total_datasets: int
total_comments_annotated: int
total_comments_in_datasets: int
annotation_progress: float
total_bots: int
total_humans: int
total_conflicts: int
pending_conflicts: int
agreement_rate: float


class GlobalDashboardResponse(BaseModel):
summary: GlobalSummary
active_criteria_filter: list[str]
label_distribution_chart: str
comparativo_por_dataset_chart: str
annotations_over_time_chart: str
bot_rate_by_dataset_chart: str
agreement_by_dataset_chart: str
criteria_effectiveness_chart: str


# ── Eficácia por Critério ──────────────────────────────────────────


class CriteriaEffectivenessItem(BaseModel):
criteria: str
group: str
total_datasets: int
total_comments_selected: int
total_bots: int
bot_rate: float


# ── Por Vídeo ──────────────────────────────────────────────────────


class VideoSummary(BaseModel):
total_comments_collected: int
total_comments_in_datasets: int
total_annotated: int
total_bots: int
total_humans: int
total_conflicts: int
pending_conflicts: int
agreement_rate: float


class VideoHighlight(BaseModel):
label: str
value: str
detail: str | None = None


class VideoDashboardResponse(BaseModel):
video_id: str
summary: VideoSummary
highlights: list[VideoHighlight]
active_criteria_filter: list[str]
label_distribution_chart: str
comparativo_por_dataset_chart: str
bot_rate_by_criteria_chart: str
comment_timeline_chart: str


# ── Meu Progresso ──────────────────────────────────────────────────


class UserSummary(BaseModel):
total_datasets_assigned: int
datasets_completed: int
datasets_pending: int
total_annotated: int
total_pending: int
bots: int
humans: int
conflicts_generated: int


class UserDatasetProgress(BaseModel):
dataset_id: uuid.UUID
dataset_name: str
video_id: str
total_comments: int
annotated_by_me: int
pending: int
percent_complete: float
my_bots: int
my_conflicts: int
status: str


class UserDashboardResponse(BaseModel):
summary: UserSummary
datasets: list[UserDatasetProgress]
my_label_distribution_chart: str
my_progress_by_dataset_chart: str
my_annotations_over_time_chart: str


# ── Tabela de Bots ─────────────────────────────────────────────────


class BotCommentItem(BaseModel):
dataset_name: str
author_display_name: str
text_original: str
concordance_pct: int
conflict_status: str | None = None
annotators_count: int = 0
criteria: list[str] = []


class BotCommentsResponse(BaseModel):
total: int
items: list[BotCommentItem]
Loading
Loading