/* global React, CR_DATA, CR_UI */ const { useState } = React; const { Icons, Btn, Tag, EmptyState, SourceChip, formatRelative } = CR_UI; // ========================================================================= // Queue Screen // ========================================================================= const TopicCardPending = ({ topic, onApprove, onReject }) => (

{topic.title}

{topic.summary}

{topic.sources.slice(0, 3).map((s) => )} {topic.sources.length > 3 && +{topic.sources.length - 3}}
{topic.keywords.map((k) => {k})} · {formatRelative(topic.found_at)}
} onClick={() => onApprove(topic)}>Одобрить
} onClick={() => onReject(topic)}>Отклонить
); const TopicCardApproved = ({ topic, onOpen }) => (

{topic.title}

}>Одобрено

{topic.summary}

{topic.sources.slice(0, 3).map((s) => )}
{topic.keywords.slice(0, 3).map((k) => {k})} · одобрено {formatRelative(topic.found_at)}
} onClick={() => onOpen(topic)}>Открыть в Статьях
); const TopicCardRejected = ({ topic, onRestore }) => (

{topic.title}

}>Отклонено

{topic.summary}

{topic.reason && (
«{topic.reason}»
)}
отклонено {formatRelative(topic.rejected_at)}
} onClick={() => onRestore(topic)}>Восстановить
); const SkeletonCard = () => (
); const QueueScreen = ({ state, onOpenJobLog, onApprove, onReject, onRestore, onOpenArticle, onFindNew, onRefresh, topicsOverride, brand, runningJobKind, runningJobElapsed }) => { const [tab, setTab] = useState("pending"); // Real data from backend only — no CR_DATA fallback. An empty backend // state is shown as an honest empty state, not as design-preview mocks. const data = topicsOverride || { pending: [], approved: [], rejected: [] }; const { pending, approved, rejected } = data; return (
Триаж · {brand || "—"}

Очередь — Новые {pending.length}

Одобрить → запустится ресерч. Отклонить → причина сохраняется в истории. {" · "} A одобрить {" "} R отклонить {" "} следующая
} onClick={onRefresh}>Обновить } onClick={onFindNew} disabled={!brand || state === "loading"}> {state === "loading" ? "Ищу…" : "Найти новые темы"}
{state === "loading" && runningJobKind === "find_topics" && (
Поиск новых тем {runningJobElapsed != null && ( {Math.floor(runningJobElapsed / 60)}:{String(runningJobElapsed % 60).padStart(2, "0")} )}
Журнал →
)} {state === "error" && (
Поиск не удался
Последний job find_topics завершился с ошибкой. Откройте журнал — там стек/лог.
} onClick={onFindNew}>Повторить Журнал →
)} {tab === "pending" && pending.length === 0 && state !== "loading" && ( } title="Очередь пуста" body={<>Нажмите Найти новые темы, чтобы Claude собрал свежие новости по конфигу бренда. Если бренд только создан — сначала прогоните «Прочитать 100 постов» в Настройках.} actions={} onClick={onFindNew} disabled={!brand}>Найти новые темы} /> )} {tab === "pending" && pending.length > 0 && state === "loading" && (
{pending.map((t) => )}
)} {tab === "pending" && pending.length > 0 && state !== "loading" && (
{pending.map((t) => )}
)} {tab === "approved" && ( approved.length === 0 ? ( } title="Нет одобренных тем" body="Одобрите тему во вкладке «Новые» — она автоматически появится здесь и попадёт в Статьи." /> ) : (
{approved.map((t) => )}
) )} {tab === "rejected" && ( rejected.length === 0 ? ( } title="Нет отклонённых тем" body="Отклонённые темы хранятся здесь — на случай, если решите вернуть тему обратно в очередь." /> ) : (
{rejected.map((t) => )}
) )}
); }; window.CR_QUEUE = { QueueScreen };