/* global React, CR_UI, CR_API */ const { useState, useEffect, useCallback } = React; const { Icons, Btn } = CR_UI; function StatCard({ label, value, sub }) { return (
{label}
{value}
{sub &&
{sub}
}
); } function StatusDot({ status }) { const colors = { ok: "var(--status-approved-fg)", approved: "var(--status-approved-fg)", needs_revision: "var(--status-warning-fg)", failed: "var(--status-rejected-fg)", timeout: "var(--status-rejected-fg)", killed: "var(--fg-muted)", running: "var(--status-info-fg)", }; return ( ); } function CouncilCard({ review }) { const [open, setOpen] = useState(false); const verdict = review.verdict || {}; const gpt = review.reviews?.gpt || {}; const gemini = review.reviews?.gemini || {}; const decision = verdict.decision || "—"; return (
setOpen((v) => !v)} > {review.topic_id} GPT: {gpt.score ?? "—"} · Gemini: {gemini.score ?? "—"} {decision === "approved" ? "Одобрено" : decision === "failed" ? "Ошибка" : "Нужны правки"}
{open && (
{[["GPT", gpt], ["Gemini", gemini]].map(([name, r]) => (
{name} · {r.model} · Оценка: {r.score ?? "—"}
{r.error &&
Ошибка: {r.error}
} {r.summary &&
{r.summary}
} {r.issues?.length > 0 && (
    {r.issues.map((s, i) =>
  • {s}
  • )}
)}
))}
{verdict.model || "Судья"} · Финальное решение
{verdict.error &&
Ошибка: {verdict.error}
} {verdict.reasoning &&
{verdict.reasoning}
} {verdict.final_suggestions?.length > 0 && (
    {verdict.final_suggestions.map((s, i) =>
  • {s}
  • )}
)}
{review.created_at ? new Date(review.created_at).toLocaleString("ru-RU") : ""}
)}
); } const ReportScreen = ({ brand }) => { const [report, setReport] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const load = useCallback(() => { if (!brand) return; setLoading(true); setError(null); CR_API.getReport(brand) .then((r) => { setReport(r); setLoading(false); }) .catch((e) => { setError(e.message); setLoading(false); }); }, [brand]); useEffect(() => { load(); }, [load]); if (!brand) { return (
Выберите бренд
); } if (loading) { return (
Загрузка отчёта…
); } if (error) { return (
Ошибка
{error}
Повторить
); } if (!report) return null; const { pipeline = {}, jobs: jobsData = {}, council_reviews = [], bootstrap } = report; const topics = pipeline.topics || {}; const posts = pipeline.posts || {}; const council = pipeline.council || {}; const research = pipeline.research || {}; return (
Аналитика · {brand}

Отчёт по пайплайну

Статус всех этапов: от поиска тем до публикации
} onClick={load}>Обновить
{/* Pipeline stats */}
Пайплайн
{/* Bootstrap quality */} {bootstrap && (
Bootstrap
{bootstrap.channel} · {bootstrap.status} {bootstrap.quality_blockers > 0 && ( ⛔ {bootstrap.quality_blockers} блокер(а) )} {bootstrap.quality_warnings > 0 && ( ⚠️ {bootstrap.quality_warnings} предупреждение(й) )}
{bootstrap.quality_issues?.length > 0 && (() => { const CODE_LABELS = { truncated_title: "Заголовок обрезан", truncated_summary: "Описание обрезано", placeholder_glyphs: "Нетипичные эмодзи", few_new_sources: "Мало новых источников", own_channel_as_source: "Собственный канал в источниках", }; // Strip leading "filename.md: " from message const stripFile = (msg) => msg.replace(/^[^\s:]+\.md:\s*/, ""); const blockers = bootstrap.quality_issues.filter((i) => i.level === "BLOCKER"); const warnings = bootstrap.quality_issues.filter((i) => i.level !== "BLOCKER"); // Warnings: grouped by code with count const warnGroups = {}; warnings.forEach((iss) => { const key = iss.code || "other"; if (!warnGroups[key]) warnGroups[key] = 0; warnGroups[key]++; }); return (
{blockers.map((iss, i) => (
{stripFile(iss.message)}
))} {Object.entries(warnGroups).map(([code, count]) => (
⚠️ {count}×{" "} {CODE_LABELS[code] || code}
))}
); })()}
)} {/* Jobs breakdown */} {jobsData.total > 0 && (
Задачи по типам
{["Тип", "Всего", "OK", "Failed", "Timeout"].map((h) => ( ))} {Object.entries(jobsData.by_kind || {}).map(([kind, count]) => { const ks = jobsData.by_kind_status?.[kind] || {}; return ( ); })}
{h}
{kind} {count} {ks.ok ?? 0} {ks.failed ?? 0} {ks.timeout ?? 0}
)} {/* Recent jobs */} {jobsData.recent?.length > 0 && (
Последние задачи
{["Задача", "Статус", "Запущена", "Длительность", "Токены"].map((h) => ( ))} {jobsData.recent.map((j) => ( ))}
{h}
{j.kind} {j.id} {j.status} {new Date(j.started_at).toLocaleString("ru-RU")} {j.duration_s != null ? `${Math.round(j.duration_s)}с` : "—"} {j.cost_tokens != null ? j.cost_tokens.toLocaleString("ru-RU") : "—"}
)} {/* Council reviews */} {council_reviews.length > 0 && (
Консилиум · Результаты
{council_reviews.map((r) => ( ))}
)} {council_reviews.length === 0 && jobsData.total === 0 && !bootstrap && (
Нет данных. Запустите bootstrap и создайте несколько постов.
)}
); }; window.CR_REPORT = { ReportScreen };