From aa106dc88ced79433aab74c333f2e24c75942713 Mon Sep 17 00:00:00 2001 From: smcga Date: Wed, 8 Apr 2026 01:03:31 +0100 Subject: [PATCH] Show community effect params during generation carousel --- README.md | 2 +- index.html | 5 +++ src/main.ts | 90 +++++++++++++++++++++++++++++++++++++++++++++-- src/style.css | 24 +++++++++++++ src/style.test.ts | 2 ++ 5 files changed, 120 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 4bc47a7..2264424 100644 --- a/README.md +++ b/README.md @@ -470,7 +470,7 @@ Optional fallback token behavior: - Server-side effect moderation/generation endpoints treat submitted/generated code as inert text only (validated/stored/returned), and never execute that code in the server environment. This keeps server secrets (for example `process.env` values) out of reach of model-generated effects. - Before any generated runtime code is compiled for preview/review, the client applies a safety scanner that rejects obviously dangerous primitives (`process.env`, `document.cookie`, browser storage APIs, network APIs like `fetch`/`XMLHttpRequest`/`sendBeacon`, and dynamic code loaders such as `eval`/`Function`/`import()`/`require()`). - Generated effect payloads now also include optional parameter control metadata (`params`) and concise docs (`docs`). After approval, these generated controls are exposed in the debug panel + editor parameter pickers just like built-in effects. -- The effect generator modal now keeps the initial prompt editor focused and roomy, then reveals preview/code/param sections only after a successful generation. While generating, it shows approved community effects in a carousel with previous/next controls, and the eventual submission uses the current preview param values as defaults automatically (no separate “set defaults” step). +- The effect generator modal now keeps the initial prompt editor focused and roomy, then reveals preview/code/param sections only after a successful generation. While generating, it shows approved community effects in a carousel with previous/next controls plus live parameter controls so users can tweak those community effects while waiting, and the eventual submission uses the current preview param values as defaults automatically (no separate “set defaults” step). - Effect generation and moderation preview now use `public/songloop.ogg` as a dedicated seamless background loop (low volume), while a synthetic preview timeline keeps generated effects animating continuously even as the audio loops. - After approving or denying from `/effect-review.html`, the page now offers a **Next effect** button whenever additional pending effects are still in the moderation queue. - The client also normalizes escaped code payloads (for example strings containing literal `\\n`) before compiling preview/runtime effects. diff --git a/index.html b/index.html index 2ff990d..003e057 100644 --- a/index.html +++ b/index.html @@ -105,6 +105,11 @@
Generating your effect…
Enjoy approved community effects while Codex cooks your idea.
+ diff --git a/src/main.ts b/src/main.ts index 948b424..3921804 100644 --- a/src/main.ts +++ b/src/main.ts @@ -155,6 +155,9 @@ const effectIdeaBusyModal = document.querySelector("#effect-idea const effectIdeaBusySpinner = document.querySelector("#effect-idea-busy-spinner"); const effectIdeaBusyTitle = document.querySelector("#effect-idea-busy-title"); const effectIdeaBusyCopy = document.querySelector("#effect-idea-busy-copy"); +const effectIdeaBusyControls = document.querySelector("#effect-idea-busy-controls"); +const effectIdeaBusyControlsGrid = document.querySelector("#effect-idea-busy-controls-grid"); +const effectIdeaBusyControlsEmpty = document.querySelector("#effect-idea-busy-controls-empty"); const effectIdeaRetryButton = document.querySelector("#effect-idea-retry"); const effectIdeaGeneratedSections = document.querySelector("#effect-idea-generated-sections"); const effectIdeaControls = document.querySelector("#effect-idea-controls"); @@ -222,7 +225,12 @@ let effectIdeaPreviewFrame = 0; let effectIdeaPreviewEffect: ReturnType | null = null; let effectIdeaPreviewParams: Record = {}; let effectIdeaCountdownTimer = 0; -let effectIdeaCarouselEntries: Array<{ name: string; effect: ReturnType; params: Record }> = []; +let effectIdeaCarouselEntries: Array<{ + name: string; + effect: ReturnType; + params: Record; + controls: GeneratedEffectParam[]; +}> = []; let effectIdeaCarouselIndex = 0; let effectIdeaAudioPreview = new EffectPreviewAudioController(EFFECT_PREVIEW_AUDIO_SRC); let availableEffectNames = getEffectRegistryKeys(); @@ -595,11 +603,13 @@ function setEffectIdeaBusyState(mode: "hidden" | "busy" | "error", message?: str if (mode === "busy") { effectIdeaBusyTitle.textContent = "Generating your effect…"; effectIdeaBusyCopy.textContent = "Enjoy approved community effects while Codex cooks your idea."; + effectIdeaBusyControls?.classList.remove("hidden"); return; } if (mode === "error") { effectIdeaBusyTitle.textContent = "Generation hiccup"; effectIdeaBusyCopy.textContent = message ?? "That attempt did not compile cleanly. Retry to generate a fresh variation."; + effectIdeaBusyControls?.classList.add("hidden"); } } @@ -626,6 +636,7 @@ function applyEffectIdeaCarouselEntry(): void { } effectIdeaPreviewEffect = entry.effect; effectIdeaPreviewParams = { ...entry.params }; + renderEffectIdeaBusyControls(entry.controls); stopEffectIdeaPreview(); previewGeneratedIdea(); } @@ -645,7 +656,8 @@ async function hydrateEffectIdeaCarousel(): Promise { return [{ name: entry.name, effect: compileRuntimeEffect(entry.runtimeCode), - params: getGeneratedEffectDefaultParams(entry.params) + params: getGeneratedEffectDefaultParams(entry.params), + controls: entry.params ?? [] }]; } catch { return []; @@ -659,6 +671,79 @@ async function hydrateEffectIdeaCarousel(): Promise { effectIdeaCarouselIndex = 0; } +function renderEffectIdeaBusyControls(params: GeneratedEffectParam[]): void { + if (!effectIdeaBusyControls || !effectIdeaBusyControlsGrid || !effectIdeaBusyControlsEmpty) { + return; + } + const hasControls = params.length > 0; + effectIdeaBusyControls.classList.toggle("hidden", !hasControls); + effectIdeaBusyControlsGrid.innerHTML = ""; + effectIdeaBusyControlsEmpty.classList.toggle("hidden", hasControls); + if (!hasControls) { + return; + } + + params.forEach((param) => { + const field = document.createElement("label"); + field.classList.add("debug-field"); + const label = document.createElement("span"); + label.textContent = param.label; + field.appendChild(label); + + if (param.type === "select") { + const select = document.createElement("select"); + select.dataset.effectIdeaParam = param.key; + (param.options ?? []).forEach((option) => { + const optionEl = document.createElement("option"); + optionEl.value = option.value; + optionEl.textContent = option.label; + select.appendChild(optionEl); + }); + select.value = String(effectIdeaPreviewParams[param.key] ?? ""); + select.addEventListener("change", () => { + effectIdeaPreviewParams[param.key] = select.value; + }); + field.appendChild(select); + effectIdeaBusyControlsGrid.appendChild(field); + return; + } + + const input = document.createElement("input"); + input.dataset.effectIdeaParam = param.key; + if (param.type === "toggle") { + input.type = "checkbox"; + input.checked = Number(effectIdeaPreviewParams[param.key]) !== 0; + input.addEventListener("change", () => { + effectIdeaPreviewParams[param.key] = input.checked ? 1 : 0; + }); + field.appendChild(input); + effectIdeaBusyControlsGrid.appendChild(field); + return; + } + + input.type = "number"; + if (param.min !== undefined) { + input.min = String(param.min); + } + if (param.max !== undefined) { + input.max = String(param.max); + } + if (param.step !== undefined) { + input.step = String(param.step); + } + input.value = String(effectIdeaPreviewParams[param.key] ?? 0); + input.addEventListener("input", () => { + const numeric = Number(input.value); + if (!Number.isFinite(numeric)) { + return; + } + effectIdeaPreviewParams[param.key] = numeric; + }); + field.appendChild(input); + effectIdeaBusyControlsGrid.appendChild(field); + }); +} + function runEffectIdeaSuccessCountdown(onDone: () => void): void { if (!effectIdeaCountdown) { onDone(); @@ -824,6 +909,7 @@ function setEffectIdeaModalVisible(visible: boolean): void { effectIdeaPreviewEffect = null; effectIdeaCarouselEntries = []; effectIdeaCarouselIndex = 0; + renderEffectIdeaBusyControls([]); if (effectIdeaNameInput) { effectIdeaNameInput.value = ""; } diff --git a/src/style.css b/src/style.css index 0f0dd8b..48144c6 100644 --- a/src/style.css +++ b/src/style.css @@ -1474,6 +1474,30 @@ body, text-align: center; } +.effect-idea-busy-controls { + width: 100%; + display: grid; + gap: 0.35rem; + margin-top: 0.2rem; +} + +.effect-idea-busy-controls-label { + color: rgba(232, 247, 255, 0.86); + font-size: 0.66rem; + letter-spacing: 0.07em; + text-transform: uppercase; +} + +#effect-idea-busy-controls-grid { + grid-template-columns: repeat(auto-fit, minmax(8rem, 1fr)); + gap: 0.4rem; +} + +#effect-idea-busy-controls-grid .debug-field { + background: rgba(4, 10, 18, 0.65); + padding: 0.3rem 0.4rem; +} + #effect-idea-name { border: 1px solid rgba(142, 249, 255, 0.22); background: rgba(6, 12, 20, 0.9); diff --git a/src/style.test.ts b/src/style.test.ts index 03703ca..61d8b2e 100644 --- a/src/style.test.ts +++ b/src/style.test.ts @@ -26,9 +26,11 @@ describe("effect idea modal styling", () => { expect(indexHtml).toContain('id="effect-idea-generated-sections" class="hidden"'); expect(indexHtml).toContain('id="effect-idea-busy-modal"'); + expect(indexHtml).toContain('id="effect-idea-busy-controls-grid"'); expect(indexHtml).toContain('id="effect-idea-input" rows="8"'); expect(css).toContain("#effect-idea-input"); expect(css).toContain("min-height: 10.5rem;"); + expect(css).toContain(".effect-idea-busy-controls"); }); });