diff --git a/.cursor/rules/single-place-functionality.mdc b/.cursor/rules/single-place-functionality.mdc new file mode 100644 index 00000000..ad27b414 --- /dev/null +++ b/.cursor/rules/single-place-functionality.mdc @@ -0,0 +1,20 @@ +--- +description: No duplicated functionality — implement each behavior in one place only +alwaysApply: true +--- + +# Single-place functionality + +Where something can be done in one place and functionality remains absolutely the same, it must be done in that single place. + +Duplicated functionality is prohibited unless strictly required for the process. + +## Examples + +- UI state that belongs on a timeline clip (e.g. "Generated with" stale-prompt hint) lives on the clip only — not repeated on the player overlay, inspector, or toolbar. +- Shared logic belongs in one module/function; views call it instead of re-implementing. +- If two call sites need the same behavior, extract once and reuse — do not copy-paste with drift. + +## When duplication is allowed + +Only when the process genuinely requires it (e.g. server validation mirroring client hints for security, or ComfyUI node vs Movie Editor entry points that cannot share a runtime). diff --git a/movie_editor/frontend/inspector.js b/movie_editor/frontend/inspector.js index 5abbf9a0..0b706394 100644 --- a/movie_editor/frontend/inspector.js +++ b/movie_editor/frontend/inspector.js @@ -164,13 +164,6 @@ const ta = el("textarea"); ta.rows = 4; ta.value = root.text || ""; ta.placeholder = "Describe this scene…"; ta.dataset.k = "sc-text"; ta.oninput = () => S.patchSceneQuiet(scene.id, { text: ta.value }); body.append(field("Prompt", ta)); - const promptMismatch = S.renderPromptMismatch ? S.renderPromptMismatch(scene.id) : null; - if (promptMismatch) { - const warn = el("div", "render-prompt-warn"); - warn.append(el("div", "render-prompt-warn-title", "Preview was generated with a different prompt")); - warn.append(el("div", "render-prompt-text", promptMismatch.rendered || "(empty)")); - body.append(warn); - } const src = el("select"); SRC.forEach(([v, label]) => { const o = el("option", null, label); o.value = v; if ((root.source?.type) === v) o.selected = true; src.append(o); }); src.onchange = () => { diff --git a/movie_editor/frontend/player.js b/movie_editor/frontend/player.js index f7fe7ce4..4c781646 100644 --- a/movie_editor/frontend/player.js +++ b/movie_editor/frontend/player.js @@ -102,7 +102,6 @@ let _slateEl = null; let _badgeEl = null; - let _promptEl = null; function _showSlate(title, sub) { if (!_slateEl) return; _slateEl.style.display = title ? "" : "none"; @@ -117,18 +116,6 @@ _badgeEl.textContent = text || ""; _badgeEl.style.display = text ? "" : "none"; } - function _showRenderPrompt(sceneId) { - if (!_promptEl) return; - const mismatch = sceneId && S.renderPromptMismatch ? S.renderPromptMismatch(sceneId) : null; - if (!mismatch) { - _promptEl.style.display = "none"; - _promptEl.textContent = ""; - return; - } - _promptEl.style.display = ""; - _promptEl.title = "Timeline prompt was edited after generation - rate against this text"; - _promptEl.textContent = `Generated with: ${mismatch.rendered}`; - } // ── video pool management ───────────────────────────────────────────────────── function _ensureVideo(url) { @@ -210,7 +197,6 @@ _currentClip = clip; _setActive(null); _showBadge(""); - _showRenderPrompt(null); _showSlate(clip.slate || "Not generated yet", clip.slateSub); if (play) _startTick(); return; @@ -221,7 +207,6 @@ _showSlate(""); const badge = clip.ghost ? (clip.slate || "Removed — preview only") : _segmentSourceBadge(clip.sceneId); _showBadge(badge); - _showRenderPrompt(clip.ghost ? null : clip.sceneId); if (_badgeEl && badge.startsWith("Mixed")) _badgeEl.classList.add("mixed-mode"); else if (_badgeEl) _badgeEl.classList.remove("mixed-mode"); const target = (clip.inSec || 0) + Math.max(0, offset); @@ -512,14 +497,11 @@ _slateEl.append(el("div", "pm-slate-sub")); _badgeEl = el("div", "pm-segment-badge"); _badgeEl.style.display = "none"; - _promptEl = el("div", "pm-render-prompt"); - _promptEl.style.display = "none"; if (_clips.length) { canvas.append(stage); // pooled videos live here; only the active one is visible canvas.append(_slateEl); canvas.append(_badgeEl); - canvas.append(_promptEl); } else if (previewAsset?.kind === "image") { canvas.append(stage); } else if (["queuing", "running", "pending"].includes(gen.state)) { diff --git a/movie_editor/frontend/styles.css b/movie_editor/frontend/styles.css index 2cd5d8f2..67a0be3d 100644 --- a/movie_editor/frontend/styles.css +++ b/movie_editor/frontend/styles.css @@ -343,7 +343,7 @@ input[type=number] { font-family: var(--font-mono); } .pm-canvas.media-previewing .pm-stage, .pm-canvas.media-previewing .pm-slate, .pm-canvas.media-previewing .pm-segment-badge, -.pm-canvas.media-previewing .pm-render-prompt { visibility: hidden; } +.pm-canvas.media-previewing .pm-segment-badge { visibility: hidden; } .pm-media-preview { position: absolute; inset: 0; z-index: 5; display: flex; align-items: center; justify-content: center; cursor: default; } .pm-media-preview-img { max-width: 100%; max-height: 100%; object-fit: contain; position: relative; z-index: 1; cursor: default; } .pm-media-preview-label { position: absolute; top: 10px; left: 10px; z-index: 2; font-size: 10.5px; font-family: var(--font-mono); color: var(--teal); background: #000a; border: 1px solid color-mix(in srgb, var(--teal) 35%, var(--line)); border-radius: 12px; padding: 3px 9px; pointer-events: none; } @@ -380,13 +380,6 @@ input[type=number] { font-family: var(--font-mono); } .pm-slate-title { font-size: 14px; color: var(--amber-2); font-family: var(--font-mono); } .pm-slate-sub { font-size: 11.5px; color: var(--faint); max-width: 280px; } .pm-segment-badge { position: absolute; top: 10px; left: 10px; z-index: 3; font-size: 10.5px; font-family: var(--font-mono); color: var(--amber-2); background: #000a; border: 1px solid color-mix(in srgb, var(--amber) 35%, var(--line)); border-radius: 12px; padding: 3px 9px; pointer-events: none; } -.pm-render-prompt { - position: absolute; left: 10px; right: 10px; bottom: 10px; z-index: 3; - font-size: 10.5px; font-family: var(--font-mono); line-height: 1.35; color: #ffd9a0; - background: #000c; border: 1px solid color-mix(in srgb, var(--amber) 40%, var(--line)); - border-radius: 10px; padding: 6px 10px; pointer-events: none; - white-space: nowrap; overflow: hidden; text-overflow: ellipsis; -} .pm-needle { position: absolute; top: 0; bottom: 0; width: 2px; background: #fff; opacity: .85; transform: translateX(-50%); pointer-events: none; z-index: 2; } /* transport bar */ @@ -563,21 +556,6 @@ input[type=number] { font-family: var(--font-mono); } .me-rating-heart:hover { background: rgba(255,120,180,0.12); border-color: rgba(255,120,180,0.6); color: #ffb3d4; } .me-rating-heart.active { background: rgba(255,120,180,0.20); border-color: rgba(255,120,180,0.8); color: #ffb3d4; } .me-rating-nuclear { padding: 8px 10px; display: grid; grid-template-columns: 1fr 1fr; gap: 5px; } -.tl-render-prompt { - font-family: var(--font-mono); font-size: 10px; color: var(--amber-2); opacity: .9; - max-width: 220px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; - border: 1px solid color-mix(in srgb, var(--amber) 30%, var(--line)); border-radius: 6px; padding: 3px 7px; - background: color-mix(in srgb, var(--amber-soft) 35%, var(--ink-2)); -} -.render-prompt-warn { - border: 1px solid color-mix(in srgb, var(--amber) 35%, var(--line)); - background: color-mix(in srgb, var(--amber-soft) 25%, var(--ink-1)); - border-radius: 8px; padding: 8px 10px; margin: -4px 0 12px; -} -.render-prompt-warn-title { font-size: 11.5px; color: var(--amber-2); margin-bottom: 6px; } -.render-prompt-prev { display: flex; flex-direction: column; gap: 3px; margin-bottom: 6px; } -.render-prompt-label { font-family: var(--font-mono); font-size: 10px; letter-spacing: .5px; text-transform: uppercase; color: var(--faint); } -.render-prompt-text { font-size: 12px; color: var(--text); line-height: 1.4; white-space: pre-wrap; word-break: break-word; } .tl-scroll { flex: 1; overflow-x: auto; overflow-y: auto; position: relative; } .tl-stage { display: flex; flex-direction: row; height: 100%; min-height: 100%; }