Some checks failed
test / test (push) Failing after 24s
Реализация целевого дизайна (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/
188 lines
5.6 KiB
JavaScript
188 lines
5.6 KiB
JavaScript
/**
|
||
* 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);
|
||
}
|
||
}
|