- Anotação de Comentários + Anotação de Usuários
- Classifique cada comentário como bot ou humano. Veja todos os comentários do usuário para + Classifique cada usuário como bot ou humano. Analise todos os comentários do autor para tomar uma decisão informada.
@@ -359,11 +359,12 @@ export function AnnotatePage() { }, { label: "Escolha um usuário", - description: "Veja todos os comentários do usuário agrupados para contexto.", + description: "Veja todos os comentários do autor como evidência.", }, { - label: "Classifique cada comentário", - description: 'Marque como "Bot" ou "Humano". Bot exige justificativa.', + label: "Classifique o usuário", + description: + 'Marque o autor como "Bot" ou "Humano". Bot exige justificativa.', }, ]} /> @@ -394,7 +395,7 @@ export function AnnotatePage() {Meu progresso
- {datasetProgress.annotated}/{datasetProgress.total_comments} comentários + {datasetProgress.annotated}/{datasetProgress.total_users} usuários- {item.author_display_name} -
-- {item.author_channel_id.slice(0, 20)}... -
-+ {item.author_display_name} +
++ {item.author_channel_id.slice(0, 20)}... +
+
- comment_db_id
+ entry_id
,{" "}
@@ -685,7 +687,7 @@ export function AnnotatePage() {
{p.dataset_name}
- {p.annotated}/{p.total_comments}
+ {p.annotated}/{p.total_users}
@@ -709,7 +711,7 @@ export function AnnotatePage() {
{p.dataset_name}
- {p.annotated}/{p.total_comments}
+ {p.annotated}/{p.total_users}
diff --git a/frontend/src/pages/Annotate/CommentAnnotationRow.tsx b/frontend/src/pages/Annotate/CommentAnnotationRow.tsx
deleted file mode 100644
index 7247f79..0000000
--- a/frontend/src/pages/Annotate/CommentAnnotationRow.tsx
+++ /dev/null
@@ -1,281 +0,0 @@
-import { useCallback, useEffect, useRef, useState } from "react";
-import type { CommentWithAnnotation } from "../../api/annotate";
-
-interface Props {
- comment: CommentWithAnnotation;
- focused: boolean;
- onAnnotate: (
- commentDbId: string,
- label: "bot" | "humano",
- justificativa?: string | null
- ) => Promise;
- onFocus: () => void;
- readOnly?: boolean;
-}
-
-export function CommentAnnotationRow({
- comment,
- focused,
- onAnnotate,
- onFocus,
- readOnly = false,
-}: Props) {
- const [showJustificativa, setShowJustificativa] = useState(false);
- const [justificativa, setJustificativa] = useState(comment.my_annotation?.justificativa ?? "");
- const [saving, setSaving] = useState(false);
- const rowRef = useRef(null);
-
- const currentLabel = comment.my_annotation?.label ?? null;
-
- useEffect(() => {
- if (focused && rowRef.current) {
- rowRef.current.scrollIntoView({ behavior: "smooth", block: "nearest" });
- }
- }, [focused]);
-
- const handleHumano = useCallback(async () => {
- setSaving(true);
- setShowJustificativa(false);
- await onAnnotate(comment.comment_db_id, "humano", null);
- setSaving(false);
- }, [comment.comment_db_id, onAnnotate]);
-
- const handleBotClick = useCallback(() => {
- setShowJustificativa(true);
- onFocus();
- }, [onFocus]);
-
- const handleBotConfirm = useCallback(async () => {
- if (!justificativa.trim()) return;
- setSaving(true);
- await onAnnotate(comment.comment_db_id, "bot", justificativa.trim());
- setShowJustificativa(false);
- setSaving(false);
- }, [comment.comment_db_id, justificativa, onAnnotate]);
-
- // Atalhos de teclado
- useEffect(() => {
- if (!focused) return;
- const handler = (e: KeyboardEvent) => {
- if (e.target instanceof HTMLTextAreaElement || e.target instanceof HTMLInputElement) return;
- if (e.key === "h" || e.key === "H") {
- e.preventDefault();
- void handleHumano();
- } else if (e.key === "b" || e.key === "B") {
- e.preventDefault();
- handleBotClick();
- }
- };
- window.addEventListener("keydown", handler);
- return () => window.removeEventListener("keydown", handler);
- }, [focused, handleHumano, handleBotClick]);
-
- const date = new Date(comment.published_at).toLocaleDateString("pt-BR", {
- day: "2-digit",
- month: "2-digit",
- year: "numeric",
- hour: "2-digit",
- minute: "2-digit",
- });
-
- return (
-
- {/* Texto do comentário */}
-
- {comment.text_original}
-
-
- {/* Metadados */}
-
- {date}
- {comment.like_count} curtidas
- {comment.reply_count} respostas
-
-
- {/* Badge atual + botões */}
-
- {currentLabel && (
-
- {currentLabel === "bot" ? "Bot" : "Humano"}
-
- )}
-
- {!readOnly && (
-
-
-
-
- )}
-
-
- {/* Campo justificativa (inline) */}
- {showJustificativa && (
-
-
- )}
-
- {/* Justificativa existente (readonly — pesquisador) */}
- {!readOnly &&
- currentLabel === "bot" &&
- comment.my_annotation?.justificativa &&
- !showJustificativa && (
-
- Justificativa: {comment.my_annotation.justificativa}
-
- )}
-
- {/* Admin: anotações de todos os pesquisadores */}
- {readOnly && comment.all_annotations && comment.all_annotations.length > 0 && (
-
- {comment.all_annotations.map((ann, i) => (
-
- {ann.annotator_name}:
-
- {ann.label === "bot" ? "Bot" : "Humano"}
-
- {ann.justificativa && (
- {ann.justificativa}
- )}
-
- ))}
-
- )}
-
- {/* Sem anotação (admin) */}
- {readOnly && (!comment.all_annotations || comment.all_annotations.length === 0) && (
- Nenhuma anotação ainda.
- )}
-
- );
-}
diff --git a/frontend/src/pages/Annotate/UserCommentsList.tsx b/frontend/src/pages/Annotate/UserCommentsList.tsx
index efca834..76c1746 100644
--- a/frontend/src/pages/Annotate/UserCommentsList.tsx
+++ b/frontend/src/pages/Annotate/UserCommentsList.tsx
@@ -1,12 +1,10 @@
import { useCallback, useState } from "react";
import type { UserCommentsResponse } from "../../api/annotate";
-import { ProgressBar } from "../../components/ProgressBar";
-import { CommentAnnotationRow } from "./CommentAnnotationRow";
interface Props {
data: UserCommentsResponse;
onAnnotate: (
- commentDbId: string,
+ entryId: string,
label: "bot" | "humano",
justificativa?: string | null
) => Promise;
@@ -15,34 +13,25 @@ interface Props {
}
export function UserCommentsList({ data, onAnnotate, onBack, readOnly = false }: Props) {
- const [focusedIndex, setFocusedIndex] = useState(0);
+ const [justificativa, setJustificativa] = useState("");
+ const [submitting, setSubmitting] = useState(false);
- const annotated = data.comments.filter((c) => c.my_annotation !== null).length;
- const total = data.comments.length;
- const percent = total > 0 ? Math.round((annotated / total) * 100) : 0;
+ const currentLabel = data.my_annotation?.label ?? null;
- const handleAnnotate = useCallback(
- async (commentDbId: string, label: "bot" | "humano", justificativa?: string | null) => {
- await onAnnotate(commentDbId, label, justificativa);
+ const handleClassify = useCallback(
+ async (label: "bot" | "humano") => {
+ if (submitting) return;
+ const just = label === "bot" ? justificativa : undefined;
+ if (label === "bot" && !just?.trim()) return;
+ setSubmitting(true);
+ await onAnnotate(data.entry_id, label, just);
+ setSubmitting(false);
},
- [onAnnotate]
- );
-
- // Tab navega entre comentários
- const handleKeyDown = useCallback(
- (e: React.KeyboardEvent) => {
- if (e.key === "Tab") {
- e.preventDefault();
- setFocusedIndex((prev) =>
- e.shiftKey ? Math.max(0, prev - 1) : Math.min(data.comments.length - 1, prev + 1)
- );
- }
- },
- [data.comments.length]
+ [onAnnotate, data.entry_id, justificativa, submitting]
);
return (
-
+
{/* Header */}
@@ -54,18 +43,141 @@ export function UserCommentsList({ data, onAnnotate, onBack, readOnly = false }:
{data.author_display_name}
- {data.author_channel_id} · {total} comentários
+ {data.author_channel_id} · {data.comments.length} comentários
-
-
- {annotated}/{total} anotados
+ {currentLabel && (
+
+ {currentLabel === "bot" ? "Bot" : "Humano"}
+
+ )}
+
+
+ {/* Anotação de todos os anotadores (admin) */}
+ {data.all_annotations && data.all_annotations.length > 0 && (
+
+
+ Anotações dos pesquisadores
+
+
+ {data.all_annotations.map((a, i) => (
+
+ {a.annotator_name}
+
+ {a.label}
+
+ {a.justificativa && (
+ {a.justificativa}
+ )}
+
+ ))}
+
+
+ )}
+
+ {/* Classificação do usuário (pesquisador) */}
+ {!readOnly && (
+
+ Classificar este usuário
+
+ Analise todos os comentários abaixo e classifique o autor como bot ou humano.
-
+
+ {/* Justificativa (obrigatória para bot) */}
+
+
+
+
+
+
+
+
+
+ {data.my_annotation && (
+
+ Anotado como {data.my_annotation.label}
+ {data.my_annotation.justificativa && <> — {data.my_annotation.justificativa}>}
+
+ )}
-
+ )}
- {/* Dica atalhos */}
+ {/* Dica */}
{!readOnly && (
- Atalhos:{" "}
-
- H
- {" "}
- = Humano,{" "}
-
- B
- {" "}
- = Bot,{" "}
-
- Tab
- {" "}
- = próximo comentário
+ Os comentários abaixo são evidências para fundamentar sua decisão sobre
+ o autor. A classificação é única para o usuário, não por comentário.
)}
- {/* Comentários */}
+ {/* Comentários (apenas como evidências — sem botões de anotação por comment) */}
- {data.comments.map((comment, i) => (
- (
+ setFocusedIndex(i)}
- readOnly={readOnly}
- />
+ className="bg-white rounded-xl border border-gray-200 p-4"
+ >
+ {comment.text_original}
+
+ {new Date(comment.published_at).toLocaleDateString("pt-BR")}
+ {comment.like_count} likes
+ {comment.reply_count} respostas
+
+
))}
diff --git a/frontend/src/pages/Review/ReviewPage.tsx b/frontend/src/pages/Review/ReviewPage.tsx
index dea533b..8507d31 100644
--- a/frontend/src/pages/Review/ReviewPage.tsx
+++ b/frontend/src/pages/Review/ReviewPage.tsx
@@ -3,7 +3,7 @@ import { PageHeader } from "../../components/PageHeader";
import { StepsCard } from "../../components/StepsCard";
import { useAuthContext } from "../../contexts/AuthContext";
import { useReview } from "../../hooks/useReview";
-import type { ConflictDetail, ConflictListItem, BotCommentItem } from "../../api/review";
+import type { ConflictDetail, ConflictListItem, BotUserItem } from "../../api/review";
type Tab = "conflicts" | "bots" | "import";
@@ -41,7 +41,7 @@ export function ReviewPage() {
} | null>(null);
const [conflictsPage, setConflictsPage] = useState(1);
const [botsPage, setBotsPage] = useState(1);
- const [selectedBot, setSelectedBot] = useState(null);
+ const [selectedBot, setSelectedBot] = useState(null);
const [importFile, setImportFile] = useState(null);
const [importParseError, setImportParseError] = useState(null);
@@ -120,8 +120,8 @@ export function ReviewPage() {
try {
const text = await importFile.text();
const parsed = JSON.parse(text);
- if (!parsed.video_id || !parsed.comments) {
- setImportParseError('JSON deve conter "video_id", "dataset_name" e "comments".');
+ if (!parsed.video_id || !parsed.users) {
+ setImportParseError('JSON deve conter "video_id", "dataset_name" e "users".');
return;
}
await importReview(parsed);
@@ -427,10 +427,6 @@ function ConflictStatusBadge({ status }: { status: string }) {
);
}
-function Truncate({ text, max = 80 }: { text: string; max?: number }) {
- return <>{text.length > max ? text.slice(0, max) + "…" : text}>;
-}
-
// ─── Conflicts Tab ──────────────────────────────────────────────────────────
function ConflictsTab({
@@ -467,10 +463,10 @@ function ConflictsTab({
- Comentário
+ Usuário
- Autor
+ Comentários
Dataset
@@ -488,12 +484,10 @@ function ConflictsTab({
className="border-b border-gray-50 hover:bg-gray-50 cursor-pointer"
onClick={() => onSelectConflict(c.conflict_id)}
>
-
-
-
{c.author_display_name}
+ {c.comment_count}
{c.dataset_name}
@@ -527,18 +521,18 @@ function BotsTab({
onPageChange,
onSelectBot,
}: {
- bots: BotCommentItem[];
+ bots: BotUserItem[];
loading: boolean;
page: number;
totalPages: number;
total: number;
onPageChange: (p: number) => void;
- onSelectBot: (bot: BotCommentItem) => void;
+ onSelectBot: (bot: BotUserItem) => void;
}) {
if (bots.length === 0 && !loading) {
return (
- Nenhum comentário classificado como bot com os filtros selecionados.
+ Nenhum usuário classificado como bot com os filtros selecionados.
);
}
@@ -552,10 +546,10 @@ function BotsTab({
- Comentário
+ Usuário
- Autor
+ Comentários
Dataset
@@ -571,13 +565,11 @@ function BotsTab({
const botCount = b.annotations.filter((a) => a.label === "bot").length;
const humanCount = b.annotations.filter((a) => a.label === "humano").length;
return (
-
-
-
-
+
{b.author_display_name}
+ {b.comment_count}
{b.dataset_name}
@@ -751,11 +743,6 @@ function ConflictModal({
}) {
const isPending = detail.status === "pending";
- // The comment that triggered the conflict (first in the list)
- const conflictComment = detail.comments[0];
- // Other comments from the same author (context)
- const otherComments = detail.comments.slice(1);
-
return (
- Desempate de comentário
+ Desempate de usuário
{detail.dataset_name} · {detail.author_display_name}
@@ -790,25 +777,6 @@ function ConflictModal({
- {/* The comment in conflict */}
- {conflictComment && (
-
-
- Comentário em conflito
-
-
-
- {conflictComment.text_original}
-
-
- {new Date(conflictComment.published_at).toLocaleDateString("pt-BR")}
- {conflictComment.like_count} curtidas
- {conflictComment.reply_count} respostas
-
-
-
- )}
-
{/* Side-by-side annotations */}
)}
- {/* Other comments from same author (context) */}
- {otherComments.length > 0 && (
+ {/* Comentários do autor (evidências) */}
+ {detail.comments.length > 0 && (
- Outros comentários do mesmo autor ({otherComments.length})
+ Comentários do autor ({detail.comments.length})
-
- {otherComments.map((c) => (
+
+ {detail.comments.map((c) => (
{c.text_original}
@@ -868,15 +836,49 @@ function ConflictModal({
{/* Resolve buttons */}
{isPending && (
-
-
- A decisão é irreversível e se aplica a este comentário.
+
+
+ A decisão é irreversível e se aplica a este usuário.
-
-