/** * history-tree.js — Дерево версий, свёртка групп, выбор и удаление */ import { apiFetch } from "./api.js"; import { getState, setCurrentVersionId, setHistoryVersions } from "./state.js"; import { formatHistoryDate, versionFileName, resetResult, updateDownloadCurrent, showSourcePreview, clearSourcePreview, setJobStatus, } from "./ui.js"; import { rebuildReferenceSelect, clearReference, getReferenceState } from "./reference.js"; import { updateSuggestCodexButton } from "./suggest.js"; function historyItemBadge(version) { if (version.kind === "original") return "Исходник"; if (version.kind === "upscale") return "Апскейл"; return "Правка"; } function buildChildrenMap(versions) { const map = new Map(); for (const version of versions) { const key = version.parentId || ""; if (!map.has(key)) { map.set(key, []); } map.get(key).push(version); } return map; } function countSubtreeSize(id, childrenMap) { let count = 1; const children = childrenMap.get(id); if (children) { for (const child of children) { count += countSubtreeSize(child.id, childrenMap); } } return count; } function renderTreeNode(version, depth, childrenMap, container) { const state = getState(); const node = document.createElement("div"); node.className = "history-node"; node.style.setProperty("--depth", depth); if (version.id === state.currentVersionId) { node.classList.add("is-current"); } const main = document.createElement("button"); main.type = "button"; main.className = "history-node-main"; main.setAttribute("aria-label", version.label || "Версия"); main.addEventListener("click", () => selectHistoryVersion(version)); const thumb = document.createElement("img"); thumb.className = "history-node-thumb"; thumb.src = version.url; thumb.alt = ""; thumb.loading = "lazy"; const meta = document.createElement("span"); meta.className = "history-node-meta"; const title = document.createElement("strong"); title.className = "history-node-title"; title.textContent = version.label || "Версия"; const badge = document.createElement("span"); badge.className = "history-node-badge"; badge.textContent = historyItemBadge(version); const date = document.createElement("span"); date.className = "history-node-date"; date.textContent = formatHistoryDate(version.createdAt); meta.append(title, badge, date); if (version.kind === "edit" && version.prompt) { const prompt = document.createElement("div"); prompt.className = "history-node-prompt"; prompt.textContent = version.prompt; prompt.title = version.prompt; meta.appendChild(prompt); } main.append(thumb, meta); const actions = document.createElement("div"); actions.className = "history-node-actions"; const download = document.createElement("a"); download.className = "history-node-action history-download"; download.href = version.url; download.download = versionFileName(version); download.textContent = "Скачать"; download.addEventListener("click", (event) => event.stopPropagation()); const remove = document.createElement("button"); remove.type = "button"; remove.className = "history-node-action history-delete"; remove.textContent = "Удалить"; remove.addEventListener("click", (event) => { event.stopPropagation(); deleteVersion(version); }); actions.append(download, remove); node.append(main, actions); if (childrenMap.has(version.id)) { const body = document.createElement("div"); body.className = "history-node-children"; for (const child of childrenMap.get(version.id)) { renderTreeNode(child, depth + 1, childrenMap, body); } node.appendChild(body); } container.appendChild(node); } export function renderHistory() { const list = document.querySelector("#history-list"); const empty = document.querySelector("#history-empty"); if (!list || !empty) return; const state = getState(); const versions = state.historyVersions; list.querySelectorAll(".history-group").forEach((group) => group.remove()); empty.hidden = versions.length > 0; const childrenMap = buildChildrenMap(versions); const roots = versions .filter( (version) => version.kind === "original" || !version.parentId || !childrenMap.has(version.parentId) ) .sort((a, b) => String(b.createdAt || "").localeCompare(String(a.createdAt || ""))); rebuildReferenceSelect(); for (const root of roots) { const group = document.createElement("div"); group.className = "history-group"; group.dataset.group = root.id; const rootNode = document.createElement("div"); rootNode.className = "history-node is-root"; rootNode.style.setProperty("--depth", 0); if (root.id === state.currentVersionId) { rootNode.classList.add("is-current"); } const toggle = document.createElement("button"); toggle.type = "button"; toggle.className = "history-node-toggle"; toggle.setAttribute("aria-label", "Свернуть ветку"); const collapsed = state.collapsedGroups.has(root.id); toggle.setAttribute("aria-expanded", String(!collapsed)); toggle.textContent = collapsed ? "▸" : "▾"; toggle.addEventListener("click", (event) => { event.stopPropagation(); if (state.collapsedGroups.has(root.id)) { state.collapsedGroups.delete(root.id); } else { state.collapsedGroups.set(root.id, true); } renderHistory(); }); const rootMain = document.createElement("button"); rootMain.type = "button"; rootMain.className = "history-node-main"; rootMain.setAttribute("aria-label", root.label || "Версия"); rootMain.addEventListener("click", () => selectHistoryVersion(root)); const thumb = document.createElement("img"); thumb.className = "history-node-thumb"; thumb.src = root.url; thumb.alt = ""; thumb.loading = "lazy"; const meta = document.createElement("span"); meta.className = "history-node-meta"; const title = document.createElement("strong"); title.className = "history-node-title"; title.textContent = root.label || "Версия"; const badge = document.createElement("span"); badge.className = "history-node-badge"; badge.textContent = historyItemBadge(root); const date = document.createElement("span"); date.className = "history-node-date"; date.textContent = formatHistoryDate(root.createdAt); meta.append(title, badge, date); if (root.kind === "edit" && root.prompt) { const prompt = document.createElement("div"); prompt.className = "history-node-prompt"; prompt.textContent = root.prompt; prompt.title = root.prompt; meta.appendChild(prompt); } rootMain.append(thumb, meta); const actions = document.createElement("div"); actions.className = "history-node-actions"; const download = document.createElement("a"); download.className = "history-node-action history-download"; download.href = root.url; download.download = versionFileName(root); download.textContent = "Скачать"; download.addEventListener("click", (event) => event.stopPropagation()); const remove = document.createElement("button"); remove.type = "button"; remove.className = "history-node-action history-delete"; remove.textContent = "Удалить"; remove.addEventListener("click", (event) => { event.stopPropagation(); deleteVersion(root); }); actions.append(download, remove); rootNode.append(toggle, rootMain, actions); group.appendChild(rootNode); const count = document.createElement("span"); count.className = "history-group-count"; const subtreeSize = countSubtreeSize(root.id, childrenMap); count.textContent = subtreeSize === 1 ? "1 версия" : `${subtreeSize} версий`; group.appendChild(count); if (!collapsed) { const body = document.createElement("div"); body.className = "history-group-body"; const children = childrenMap.get(root.id); if (children) { for (const child of children) { renderTreeNode(child, 1, childrenMap, body); } } group.appendChild(body); } list.appendChild(group); } } export function selectHistoryVersion(version) { const state = getState(); if (state.isBusy) return; setCurrentVersionId(version.id); resetResult(); updateDownloadCurrent(version); showSourcePreview( version.url, version.label || "Версия", version.width && version.height ? `${version.width}×${version.height}` : "Готово к обработке" ); renderHistory(); updateSuggestCodexButton(); setJobStatus("idle", ""); const editBtn = document.querySelector("#edit-button"); const upscaleBtn = document.querySelector("#upscale-button"); const promptInput = document.querySelector("#prompt"); if (upscaleBtn) upscaleBtn.disabled = false; if (editBtn) { editBtn.disabled = !promptInput?.value?.trim(); } } export async function refreshHistory(selectLatest = false) { try { const data = await apiFetch("/api/history", { cache: "no-store" }); const versions = Array.isArray(data.versions) ? data.versions : []; setHistoryVersions(versions); if (selectLatest && versions.length > 0) { const latest = versions[versions.length - 1]; selectHistoryVersion(latest); } else { renderHistory(); updateSuggestCodexButton(); } } catch (err) { console.error("Ошибка обновления истории:", err); } } export async function deleteVersion(version) { if (!confirm("Удалить версию и все её ответвления?")) return; try { const data = await apiFetch(`/api/history/${version.id}`, { method: "DELETE", cache: "no-store", }); const versions = Array.isArray(data.versions) ? data.versions : []; setHistoryVersions(versions); const state = getState(); state.collapsedGroups.delete(version.id); const currentSurvives = versions.some((v) => v.id === state.currentVersionId); if (!currentSurvives) { const parent = version.parentId ? versions.find((v) => v.id === version.parentId) : null; if (parent) { selectHistoryVersion(parent); } else if (versions.length > 0) { setCurrentVersionId(null); resetResult(); updateDownloadCurrent(null); clearSourcePreview(); renderHistory(); } else { setCurrentVersionId(null); resetResult(); updateDownloadCurrent(null); clearSourcePreview(); renderHistory(); } } else { renderHistory(); } const refState = getReferenceState(); if (refState?.source?.type === "history" && refState.source.versionId === version.id) { clearReference(); } } catch (err) { setJobStatus("error", err.message || "Не удалось удалить версию."); } }