// web/screens/series.jsx — детальная страница серии. // Грузим серию + список её фотографий через бэк, рендерим грид с превью. // Клик по плитке открывает боковую панель с подробностями. // // На стадии распознавания (до анализа) грид заменяется построчным списком — // как в Ревью 1: каждое фото отдельной строкой, рядом GIST + полный recognition // и поле «Поправка распознавания». Лена видит ошибку — сразу правит, и правка // учтётся в плане вариативности и анализе при «Сделать анализ» (правка 2026-06-07). // recoLooksFailed — фронт-зеркало recognitionLooksFailed из pkg/pipeline/analyse.go: // распознавание считается «битым», если в тексте маркеры overflow ИЛИ у уже // распознанного кадра нет GIST. Для таких кадров Лена описывает кадр вручную. const RECO_ERR_MARKERS = [ "context overflow", "prompt too large", "try /reset", "try /new", "превышен контекст", ]; function recoLooksFailed(photo) { const r = (photo.Recognition || "").trim().toLowerCase(); if (!r) return false; // ещё не распознано — это «в очереди», а не «битое» if (RECO_ERR_MARKERS.some((m) => r.includes(m))) return true; return !photo.Gist || !photo.Gist.trim(); } // SeriesRecognizeRow — одна строка списка на стадии распознавания. Слева превью // + имя + статус, справа GIST/recognition + поле «Поправка распознавания». // Логика автосейва правки взята из Review1PhotoBlock (upsert на blur, синк с // сервером без затирания «висящего» локального ввода). function SeriesRecognizeRow({ photo, idx, allComments, reloadComments, onChanged }) { const [_, toast] = useToasts(); const myComments = (allComments || []).filter((c) => c.photoId === photo.ID); const findText = (kind) => { const c = myComments.find((c) => c.commentType === kind); return c ? c.body : ""; }; const [fix, setFix] = React.useState(findText("fix")); // Подтягиваем серверный fix, но не затираем непустой локальный ввод (race с // blur/reload — та же защита что в Ревью 1). React.useEffect(() => { const server = findText("fix"); if (server !== fix && server !== "") setFix(server); }, [allComments]); const save = async () => { const existing = myComments.find((c) => c.commentType === "fix"); const orig = existing ? existing.body : ""; if (fix === orig) return; try { await api.feed.upsertComment(photo.ShootID, photo.ID, "fix", fix); reloadComments && reloadComments(); } catch (e) { toast("Не удалось сохранить: " + e.message, "error"); } }; // Голосовой ввод дописывает кусок и СРАЗУ сохраняет (микрофон не вызывает blur). const appendFix = (t) => { setFix((prev) => { const next = (prev.trim() ? prev.trim() + " " : "") + t; api.feed.upsertComment(photo.ShootID, photo.ID, "fix", next) .then(() => reloadComments && reloadComments()) .catch(() => toast("Не удалось сохранить", "error")); return next; }); }; const recoBad = recoLooksFailed(photo); const isQueued = photo.Stage === "queued"; const isWorking = photo.Stage === "recognizing"; const isHardFailed = photo.Stage === "failed"; const failedLike = recoBad || isHardFailed; const recognizedOK = !!photo.Recognition && !recoBad && !isHardFailed; // Поле правки показываем когда кадр уже прошёл распознавание (успешно или с ошибкой). const showFix = recognizedOK || failedLike; const reRecognize = async () => { try { await api.feed.restartPhotoRecognize(photo.ID); toast("Фотка отправлена на перераспознавание", "info"); onChanged && onChanged(); } catch (e) { toast("Не удалось: " + e.message, "error"); } }; return (
{photo.YandexPath ? {photo.Filename} : null}
{String(idx + 1).padStart(2, "0")} {photo.Filename}
{isQueued && в очереди} {isWorking && распознаётся} {recognizedOK && распознано} {failedLike && распознавание не удалось} {photo.BannerType && photo.BannerType !== "none" && ( {photo.BannerType === "horizontal" ? "21:9 баннер" : "9:16 баннер"} )}
{recognizedOK && photo.Gist && ( )} {failedLike && (
Распознавание не удалось{photo.Recognition ? " — модель вернула ошибку вместо описания" : ""}. Опиши кадр вручную в поле ниже: этот текст пойдёт в план и анализ вместо распознавания (перераспознать обычно бесполезно — снова переполнит контекст модели). {photo.Recognition && (
показать ответ модели
{photo.Recognition}
)}
)} {isWorking && (
распознаётся…
)} {isQueued && (
в очереди на распознавание
)} {showFix && (