Files
photo-editor/public/js/suggest.js
Coder 401969cfdb
Some checks failed
test / test (push) Failing after 24s
v2: модульный монолит — очередь, персистентность, безопасность, SSE, тесты
Реализация целевого дизайна (docs/architecture/target-design.md):
- src/: модульная структура (config, logger, errors, http, core: auth/history/jobs/engines/comfy/codex/images)
- Очередь заданий: FIFO, кониурентность codex=1 / comfy-upscale=2, MAX_QUEUED_JOBS=5, JSONL-журнал и восстановление после рестарта (running → interrupted, upscale requeue)
- Персистентные сессии (sha256-хеши токенов), scrypt-хеш пароля, rate-limit входа, Origin-проверка, magic-byte валидация аплоадов
- Атомарная запись манифеста истории, каскадное удаление, URL с фактическим расширением + 302-алиас /image.jpg, миграция history/ → data/history
- Даунскейл sharp до 1024px перед Codex + масштабирование области референса
- SSE /api/events с фолбэком на polling, фронтенд переведён на ES-модули (public/js/)
- Тесты node:test + supertest (27), CI workflow test.yml, Docker/деплой: том kadr-data
- Документация: README, docs/architecture/
2026-08-27 12:52:27 +07:00

188 lines
5.6 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* suggest.js — Подсказки промптов из истории и генерация через Codex
*/
import { apiFetch } from "./api.js";
import { getState, setSuggestRunning } from "./state.js";
function applyPrompt(promptText) {
const promptInput = document.querySelector("#prompt");
const promptCounter = document.querySelector("#prompt-counter");
const editButton = document.querySelector("#edit-button");
if (!promptInput) return;
promptInput.value = promptText;
if (promptCounter) {
promptCounter.textContent = `${promptText.length} / 1000`;
}
const state = getState();
if (editButton) {
editButton.disabled = !state.currentVersionId || !promptText.trim() || state.isBusy;
}
}
export function renderSuggestChips(container, items) {
if (!container) return;
container.innerHTML = "";
for (const item of items) {
const text = String(item || "").trim();
if (!text) continue;
const chip = document.createElement("button");
chip.type = "button";
chip.className = "suggest-chip";
chip.textContent = text;
chip.addEventListener("click", () => applyPrompt(text));
container.appendChild(chip);
}
}
export async function loadSuggestHistoryChips() {
const container = document.querySelector("#suggest-history-chips");
if (!container) return;
try {
const res = await apiFetch("/api/suggest-prompts");
const prompts = Array.isArray(res?.prompts) ? res.prompts : [];
if (prompts.length === 0) {
container.innerHTML = '<span class="suggest-empty">В истории пока нет промптов</span>';
return;
}
renderSuggestChips(container, prompts);
} catch {
container.innerHTML = '<span class="suggest-empty">Не удалось загрузить подсказки</span>';
}
}
export function updateSuggestCodexButton() {
const suggestBtn = document.querySelector("#suggest-button");
const codexBtn = document.querySelector("#suggest-codex-button");
const state = getState();
const canSuggest = Boolean(state.currentVersionId) && !state.isBusy;
if (suggestBtn) {
suggestBtn.disabled = !canSuggest;
}
if (codexBtn) {
codexBtn.disabled = !canSuggest || state.suggestRunning;
}
}
export async function runSuggest() {
const state = getState();
if (!state.currentVersionId || state.suggestRunning || state.isBusy) {
return;
}
const statusEl = document.querySelector("#suggest-codex-status");
const chipsContainer = document.querySelector("#suggest-codex-chips");
const codexBtn = document.querySelector("#suggest-codex-button");
setSuggestRunning(true);
updateSuggestCodexButton();
if (statusEl) statusEl.textContent = "Codex анализирует кадр…";
if (chipsContainer) chipsContainer.innerHTML = "";
try {
const res = await apiFetch("/api/suggest", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ sourceId: state.currentVersionId }),
});
const jobId = res.jobId;
await pollSuggestJob(jobId);
} catch (err) {
if (statusEl) statusEl.textContent = `Ошибка: ${err.message}`;
setSuggestRunning(false);
updateSuggestCodexButton();
}
}
async function pollSuggestJob(jobId) {
const statusEl = document.querySelector("#suggest-codex-status");
const chipsContainer = document.querySelector("#suggest-codex-chips");
let failures = 0;
const poll = async () => {
try {
const data = await apiFetch(`/api/jobs/${jobId}/status`);
failures = 0;
if (data.status === "queued") {
if (statusEl) {
statusEl.textContent = data.queuePosition
? `В очереди (позиция ${data.queuePosition})…`
: "В очереди…";
}
setTimeout(poll, 1500);
return;
}
if (data.status === "running") {
if (statusEl) statusEl.textContent = "Codex генерирует варианты…";
setTimeout(poll, 1500);
return;
}
if (data.status === "done") {
if (statusEl) statusEl.textContent = "";
const suggestions = data.result?.suggestions || [];
if (suggestions.length > 0) {
renderSuggestChips(chipsContainer, suggestions);
} else {
if (statusEl) statusEl.textContent = "Codex не вернул вариантов.";
}
setSuggestRunning(false);
updateSuggestCodexButton();
return;
}
if (data.status === "error" || data.status === "interrupted") {
if (statusEl) statusEl.textContent = `Ошибка: ${data.error || "Не удалось получить подсказки."}`;
setSuggestRunning(false);
updateSuggestCodexButton();
return;
}
setTimeout(poll, 1500);
} catch (err) {
failures++;
if (failures >= 5) {
if (statusEl) statusEl.textContent = `Ошибка связи с сервером: ${err.message}`;
setSuggestRunning(false);
updateSuggestCodexButton();
return;
}
setTimeout(poll, 2000);
}
};
poll();
}
export function initSuggest() {
const suggestBtn = document.querySelector("#suggest-button");
const suggestPanel = document.querySelector("#suggest-panel");
const codexBtn = document.querySelector("#suggest-codex-button");
if (suggestBtn && suggestPanel) {
suggestBtn.addEventListener("click", () => {
const isHidden = suggestPanel.hidden;
suggestPanel.hidden = !isHidden;
if (isHidden) {
loadSuggestHistoryChips();
}
});
}
if (codexBtn) {
codexBtn.addEventListener("click", runSuggest);
}
}