История — список генераций
Просмотр всех заданий пользователя (или всей франшизы при gensvc.history.read.all). Активные jobs автообновляются, завершённые сгруппированы по статусу. Главная точка получения результата генерации.
Файл
ai-photo-studio-frontend/src/pages/HistoryPage.tsx
Роут
GET /history
Доступ
Permission gensvc.access. Без gensvc.history.read.all показывает только jobs текущего пользователя (бэк фильтрует по user_id).
Layout
┌─────────────────────────────────────────────────────────┐
│ История генераций │
│ Слева — что вы загрузили, справа — что AI сгенерировал │
├─────────────────────────────────────────────────────────┤
│ В РАБОТЕ (N) Обновляется автоматически │ ← раздел появляется только при наличии активных
│ ┌──────────────┐ ┌──────────────┐ │
│ │ Spinner │ │ Spinner │ │
│ │ Генерируется │ │ В очереди... │ │
│ │ Обычно 3-6 │ │ Обычно 3-6 │ │
│ └──────────────┘ └──────────────┘ │
├─────────────────────────────────────────────────────────┤
│ ┌──────────────┐ ┌──────────────┐ │
│ │ before/after │ │ before/after │ ← SucceededCard │
│ │ [Скачать ↓] │ │ │ │
│ └──────────────┘ └──────────────┘ │
├─────────────────────────────────────────────────────────┤
│ НЕ ПОЛУЧИЛОСЬ ───────── │
│ ┌──────────────────────────────┐ │
│ │ thumb │ Не получилось … │ ← FailedCard │
│ │ │ [Повторить] │ │
│ └──────────────────────────────┘ │
├─────────────────────────────────────────────────────────┤
│ [Показать ещё] │
└─────────────────────────────────────────────────────────┘
Секции (порядок сверху вниз)
1. Active section — «В работе (N)»
Показывается только если есть jobs со статусом pending или running.
- Карточка
ActiveCard(см. § Карточки): миниатюра входного фото + спиннер + статус-текст + подсказка «Обычно 3-6 минут». - Заголовок секции:
В работе (N) · Обновляется автоматически. - Тег
aria-live="polite"для скрин-ридеров.
2. Succeeded grid
Сетка grid-template-columns: repeat(auto-fill, minmax(380px, 1fr)).
- Карточка
SucceededCard:- Если
outputs.length > 1— мини-grid 2×2 изoutputs[0..3]. - Иначе — split before/after (input ← right output).
- Подпись: дата + SizeBadge.
- Кнопка «Скачать ↓» (см. § Download).
- Если
3. Failed section
Карточки с компактной структурой (grid-template-columns: 80px 1fr):
- Слева — миниатюра входного фото.
- Справа — заголовок «Не получилось сгенерировать»,
error_messageкурсивом, дата + SizeBadge, кнопка «Повторить с теми же настройками». - Повтор →
POST /v1/jobs/{id}/retry→ редирект/photo?job=<new_id>.
4. Pagination
nextCursor от бэка. Кнопка «Показать ещё» внизу — loadMore() дописывает страницу к существующему allJobs[].
Polling — auto-refresh active jobs
const POLL_INTERVAL_MS = 5000;
const hasActive = allJobs.some(j => j.status === "pending" || j.status === "running");
useEffect(() => {
if (!hasActive) return;
const id = setInterval(() => fetchJobs(/*silent=*/true), POLL_INTERVAL_MS);
return () => clearInterval(id);
}, [hasActive]);- Polling запускается только при наличии активных jobs, останавливается когда все settled.
silent=true— не показывать spinner на полный экран при фоновом fetch (только обновить список).setLoadingне дёргается, ошибки в silent-fetch проглатываются (уже есть данные на экране).
Notification API — push при завершении
При первом заходе /history:
if (Notification.permission === "default") void Notification.requestPermission();При появлении нового succeeded job (diff lastSucceededIdsRef ↔ текущий список):
if (Notification.permission === "granted") {
new Notification("✅ Фото готово", {
body: `Сгенерировано ${fresh.length} фото${fresh.length > 1 ? "графий" : "графия"}`,
});
}Уведомление работает из background-вкладки — пользователь не должен сидеть на странице.
Download — blob через бэкенд
<a href download> на cross-origin presigned URL не работает — браузер игнорирует download и просто открывает в новой вкладке. Поэтому:
async function downloadOutput(assetId, jobId) {
const blob = await downloadAsset(assetId); // GET /v1/assets/{id} с Bearer
const blobUrl = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = blobUrl;
a.download = `dish-${jobId.slice(-8)}.png`;
a.click();
setTimeout(() => URL.revokeObjectURL(blobUrl), 1000);
}- Имя файла:
dish-XXXXXXXX.png(последние 8 символовjob_id). - На время скачивания кнопка показывает «Скачивание…» и блокируется.
- На неуспех — fallback
window.open(presignedURL)чтобы файл всё равно достался.
Карточки — данные
Все карточки получают Job из ответа GET /v1/jobs:
{
id, kind, status, user_id, franchise_id,
preset_id, user_preset_id, extra_prompt,
size, count, created_at,
input_url, // presigned, TTL 1ч
output_url, // legacy single output
output_asset_id, // для скачивания
outputs: [{ idx, asset_id, url }], // multi-output
error_message,
}Состояния и пустые экраны
| Состояние | Что показываем |
|---|---|
loading && allJobs.length === 0 | Большой spinner по центру |
error | Красная плашка с текстом ошибки |
allJobs.length === 0 (но не loading) | Empty state: «Пока нет завершённых генераций. Загрузите фото в Студии — результат появится здесь» |
activeJobs.length > 0 && succeeded === 0 && failed === 0 | Только секция «В работе» (без empty state) |
Связанные API
GET /v1/jobs?cursor=...— список jobs текущего пользователяGET /v1/jobs?status=pending,running(если фильтр пригодится — пока берём все)GET /v1/assets/{id}— скачивание (с Bearer)POST /v1/jobs/{id}/retry— повтор failed-задания