added on WebSTLs (webstls.com) at 2026-07-01 22:27:02
| 1 | <style> | |
| 2 | /* Per-row action buttons in the Your experiments table. Each lives | |
| 3 | in its own form (POST + confirm) so we cannot use the standard | |
| 4 | .btn classes (those add too much weight for inline table actions). | |
| 5 | This is a smaller pill specifically tuned for table rows. */ | |
| 6 | .exp-row-action { | |
| 7 | display: inline-flex; | |
| 8 | align-items: center; | |
| 9 | gap: 4px; | |
| 10 | padding: 5px 11px; | |
| 11 | font-size: 11px; | |
| 12 | font-weight: 600; | |
| 13 | line-height: 1.2; | |
| 14 | border-radius: 6px; | |
| 15 | border: 1px solid var(--col-border); | |
| 16 | background: var(--col-surface-2); | |
| 17 | color: var(--col-text-2); | |
| 18 | text-decoration: none; | |
| 19 | cursor: pointer; | |
| 20 | font-family: inherit; | |
| 21 | transition: background 0.12s, border-color 0.12s, color 0.12s; | |
| 22 | white-space: nowrap; | |
| 23 | } | |
| 24 | .exp-row-action:hover { | |
| 25 | background: var(--col-surface-3); | |
| 26 | border-color: var(--col-accent); | |
| 27 | color: var(--col-text); | |
| 28 | } | |
| 29 | .exp-row-action.is-primary { background: rgba(124,58,237,0.15); border-color: rgba(124,58,237,0.40); color: var(--col-accent-bright); } | |
| 30 | .exp-row-action.is-primary:hover { background: rgba(124,58,237,0.25); border-color: rgba(124,58,237,0.70); } | |
| 31 | .exp-row-action.is-success { background: rgba(34,197,94,0.12); border-color: rgba(34,197,94,0.35); color: #4ade80; } | |
| 32 | .exp-row-action.is-success:hover { background: rgba(34,197,94,0.20); border-color: rgba(34,197,94,0.65); } | |
| 33 | .exp-row-action.is-warning { background: rgba(245,158,11,0.12); border-color: rgba(245,158,11,0.35); color: #fbbf24; } | |
| 34 | .exp-row-action.is-warning:hover { background: rgba(245,158,11,0.20); border-color: rgba(245,158,11,0.65); } | |
| 35 | .exp-row-action.is-danger { background: rgba(239,68,68,0.10); border-color: rgba(239,68,68,0.30); color: #f87171; } | |
| 36 | .exp-row-action.is-danger:hover { background: rgba(239,68,68,0.18); border-color: rgba(239,68,68,0.60); } | |
| 37 | /* Edit = cyan/teal accent (a different brand-family color than the | |
| 38 | primary purple so it does not compete with Details). */ | |
| 39 | .exp-row-action.is-edit { background: rgba(6,182,212,0.13); border-color: rgba(6,182,212,0.40); color: #22d3ee; } | |
| 40 | .exp-row-action.is-edit:hover { background: rgba(6,182,212,0.22); border-color: rgba(6,182,212,0.70); } | |
| 41 | /* Reset = soft amber. Same family as Pause but visually distinct | |
| 42 | because Reset is in a different position in the lifecycle. */ | |
| 43 | .exp-row-action.is-reset { background: rgba(251,191,36,0.13); border-color: rgba(251,191,36,0.40); color: #fcd34d; } | |
| 44 | .exp-row-action.is-reset:hover { background: rgba(251,191,36,0.22); border-color: rgba(251,191,36,0.70); } | |
| 45 | .exp-actions-cell { display: flex; gap: 6px; align-items: center; flex-wrap: nowrap; } | |
| 46 | ||
| 47 | /* Experiments table column sizing. Auto-layout (no fixed columns) | |
| 48 | plus a min-width on Name + nowrap on the Actions cell gives Name | |
| 49 | the leftover horizontal space while keeping Actions compact. */ | |
| 50 | .exp-table .exp-col-name { min-width: 220px; } | |
| 51 | .exp-table .exp-col-actions { width: 1%; white-space: nowrap; } | |
| 52 | .exp-table td.exp-actions-host { white-space: nowrap; } | |
| 53 | ||
| 54 | </style> | |
| 55 | ||
| 56 | <div class="page-head"> | |
| 57 | <div> | |
| 58 | <span class="page-eyebrow"><span class="dot"></span> Optimization Suite</span> | |
| 59 | <h1 class="page-title">Test and improve sales automatically</h1> | |
| 60 | <p class="page-subtitle">A/B + MVT, automated price testing, buyer pricing surveys, promotional rules. Same toolkit DTC (direct-to-consumer) brands use — without the consultant invoice.</p> | |
| 61 | <span class="opt-explainer" style="display:inline-flex;align-items:center;gap:6px;padding:6px 12px;margin-top:10px;background:var(--col-surface-2);border:1px solid var(--col-border);border-radius:6px;font-size:12px;color:var(--col-text-2);font-weight:600" data-kpi-detail='{ | |
| 62 | "eyebrow":"A/B and MVT . what these terms mean", | |
| 63 | "description":"Two related ways to test ideas on your storefront before committing to one. <strong>A/B</strong> shows half of your visitors the current version and half a new version of one thing -- a headline, a button color, a price. The winner is whichever variant converts better. <strong>MVT</strong> (multivariate testing) tests several things at once, in combination -- different headlines paired with different images paired with different button copy -- and finds the best combination instead of testing each in isolation.", | |
| 64 | "methodology":"<strong>A/B test:</strong> visitors are bucketed by a sticky cookie at first page load, 50/50. We track conversion per bucket and run a Bayesian model to estimate the probability that B beats A. When that probability crosses 95%, we surface a Call winner pill so you can ship the winning variant. <strong>MVT:</strong> visitors are bucketed across every combination of the variables you are testing. With 3 variables and 2 options each, that is 8 cells. We need ~250 conversions per cell for reliable results, so MVT requires more traffic than a single A/B.", | |
| 65 | "kpis":[ | |
| 66 | {"label":"A/B","value":"Two variants","sub":"50/50 split, one thing changes","color":"#a78bfa"}, | |
| 67 | {"label":"MVT","value":"Many combinations","sub":"finds best mix of changes","color":"#a78bfa"}, | |
| 68 | {"label":"When to use","value":"A/B for clear hypotheses, MVT for fishing","sub":"different tools, same idea"} | |
| 69 | ], | |
| 70 | "interpretation":"Rule of thumb: A/B when you have a strong belief about the change (new headline should beat the old one) and want to confirm it. MVT when you are not sure which combination wins and have enough traffic to fan out across more cells. A/B finishes faster and is easier to interpret; MVT explores more of the design space but needs significantly more visitors to be conclusive.", | |
| 71 | "insights":[ | |
| 72 | {"label":"Trap","value":"Calling an A/B at less than 90% confidence usually flips with more data. The pill is conservative on purpose -- it waits until the signal is real.","tone":"warn"}, | |
| 73 | {"label":"Tip","value":"Start with A/B tests on your highest-traffic surface (usually the home hero). Move to MVT once you have a baseline and want to compound several proven changes together.","tone":"info"} | |
| 74 | ], | |
| 75 | "drilldowns":[ | |
| 76 | {"label":"Start a new experiment","href":"#ab"}, | |
| 77 | {"label":"Why we do not personalize prices","href":"#"} | |
| 78 | ] | |
| 79 | }'> | |
| 80 | <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M9.1 9a3 3 0 0 1 5.8 1c0 2-3 3-3 3"/><circle cx="12" cy="17" r="0.5" fill="currentColor"/></svg> | |
| 81 | What is A/B + MVT? | |
| 82 | </span> | |
| 83 | </div> | |
| 84 | <div class="page-actions"> | |
| 85 | [if:$mode_new]<a href="/optimization.cgi" class="btn btn-secondary">← Back</a>[/if] | |
| 86 | [if:$mode_detail]<a href="/optimization.cgi" class="btn btn-secondary">← Back to experiments</a>[/if] | |
| 87 | [if:$mode_list] | |
| 88 | [if:$not_tutorial_mode]<a href="/optimization.cgi?tutorial=1" class="btn btn-secondary"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>Sample Data View</a>[/if] | |
| 89 | [if:$tutorial_mode]<a href="/optimization.cgi" class="btn btn-secondary"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>Exit sample view</a>[/if] | |
| 90 | <a href="/optimization.cgi?new=1" class="btn btn-primary"> | |
| 91 | <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg> | |
| 92 | New experiment | |
| 93 | </a> | |
| 94 | [/if] | |
| 95 | </div> | |
| 96 | </div> | |
| 97 | ||
| 98 | [if:$mode_list][if:$tutorial_mode] | |
| 99 | <div class="tutorial-banner"> | |
| 100 | <div class="tutorial-banner-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg></div> | |
| 101 | <div class="tutorial-banner-body"> | |
| 102 | <div class="tutorial-banner-title">Tutorial · Sample data only</div> | |
| 103 | <div class="tutorial-banner-text">Every experiment, price test, suggestion and history row below is example data — none of it is from your store. Use this to see what Optimization reads like when you have real activity.</div> | |
| 104 | </div> | |
| 105 | <a href="/optimization.cgi" class="btn btn-secondary btn-sm">Exit sample view</a> | |
| 106 | </div> | |
| 107 | ||
| 108 | <div class="tutorial-intro"> | |
| 109 | <div class="tutorial-intro-eyebrow">A walkthrough of the Optimization Suite</div> | |
| 110 | <h2 class="tutorial-intro-title">A/B tests, price tests, and peer recommendations.</h2> | |
| 111 | <p class="tutorial-intro-text">The Optimization page is where you discover what actually converts buyers on your storefront. Four tabs each test something different: <strong>A/B + MVT</strong> tests headline or image variants, <strong>Price tests</strong> auto-adjust product prices to find the revenue-per-visitor sweet spot, <strong>Pricing surveys</strong> ask buyers what they would pay (Van Westendorp / Gabor-Granger), and <strong>Promotions</strong> tracks coupons and sales (full UI is at <a href="/promotions.cgi" class="text-brand fw-700">/promotions.cgi</a>).</p> | |
| 112 | <p class="tutorial-intro-text">A/B data is recorded into <code>ab_test_events</code> as visitors land on each variant; the Bayesian model computes <strong>P(B beats A)</strong> from <code>conversions / exposures</code> per variant. When confidence crosses 95% the pill flips to <em>Call winner</em> and you can promote variant B in one click. Price tests behave similarly with a Thompson-sampling bandit over <code>price_tests.test_rules</code>; they keep sampling every tier to detect drift.</p> | |
| 113 | <p class="tutorial-intro-text tutorial-intro-foot">Click <a href="/optimization.cgi" class="text-brand fw-700">Exit sample view</a> to see your real experiments, or <a href="/optimization.cgi?new=1" class="text-brand fw-700">create your first one</a>.</p> | |
| 114 | </div> | |
| 115 | [/if][/if] | |
| 116 | ||
| 117 | [if:$mode_detail] | |
| 118 | <!-- ============ EXPERIMENT DETAIL VIEW ============ | |
| 119 | Per-experiment deep dive: winner state, per-variant stats, | |
| 120 | action buttons. The CGI computes everything via | |
| 121 | MODS::WebSTLs::Experiments->compute_stats and passes named | |
| 122 | tvars so this template stays declarative. --> | |
| 123 | <div class="module mb-3"> | |
| 124 | <div class="module-head"> | |
| 125 | <div class="left"> | |
| 126 | <div class="module-icon" style="background:rgba(124,58,237,0.15);color:var(--col-violet-bright)"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M19 12a7 7 0 1 1-14 0 7 7 0 0 1 14 0Z"/></svg></div> | |
| 127 | <div> | |
| 128 | <div class="module-title">$detail_name</div> | |
| 129 | <div class="module-sub">$detail_test_type test · $detail_surface · success metric: $detail_metric_label</div> | |
| 130 | </div> | |
| 131 | </div> | |
| 132 | <div> | |
| 133 | [if:$detail_is_draft]<span class="pill">Draft</span>[/if] | |
| 134 | [if:$detail_is_running]<span class="pill brand">Running</span>[/if] | |
| 135 | [if:$detail_is_paused]<span class="pill warning">Paused</span>[/if] | |
| 136 | [if:$detail_is_complete]<span class="pill success">Complete</span>[/if] | |
| 137 | </div> | |
| 138 | </div> | |
| 139 | ||
| 140 | <div class="module-body"> | |
| 141 | ||
| 142 | <!-- Locked winner callout: only shows once the test is | |
| 143 | Completed. Takes priority over the live winner_called / | |
| 144 | too_early / below_min messages because the test is no | |
| 145 | longer bucketing new visitors. --> | |
| 146 | [if:$detail_has_locked] | |
| 147 | <div class="mb-3" style="background:linear-gradient(135deg, rgba(34,197,94,0.14), rgba(124,58,237,0.12));border:2px solid rgba(34,197,94,0.55);border-radius:14px;padding:22px 26px;display:flex;align-items:center;gap:18px;flex-wrap:wrap"> | |
| 148 | <div style="display:grid;place-items:center;width:54px;height:54px;border-radius:50%;background:var(--col-success);color:#fff;flex-shrink:0"> | |
| 149 | <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" width="28" height="28"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg> | |
| 150 | </div> | |
| 151 | <div style="flex:1;min-width:260px"> | |
| 152 | <div style="font-family:var(--font-display);font-size:24px;font-weight:800;color:var(--col-success);line-height:1.1;margin-bottom:6px;letter-spacing:0.5px"> | |
| 153 | Winner locked in: Variant $detail_locked_winner | |
| 154 | </div> | |
| 155 | <div style="font-size:14px;color:var(--col-text-2);line-height:1.6"> | |
| 156 | This experiment is <strong style="color:var(--col-text)">Completed</strong>. When you marked it complete, we compared the conversion rates from collected visitors and locked in <strong style="color:var(--col-text)">Variant $detail_locked_winner</strong> as the winner (final confidence: <strong style="color:var(--col-text)">$detail_conf_pct</strong>). It no longer buckets new visitors. To run a fresh test on this surface, click Re-run from the Completed section, or create a new experiment. | |
| 157 | </div> | |
| 158 | </div> | |
| 159 | </div> | |
| 160 | [/if] | |
| 161 | ||
| 162 | <!-- Winner callout: big, clear, color-coded by state. --> | |
| 163 | [if:!$detail_has_locked] | |
| 164 | [if:$detail_winner_called] | |
| 165 | <div class="mb-3" style="background:rgba(34,197,94,0.10);border:2px solid rgba(34,197,94,0.45);border-radius:14px;padding:22px 26px;display:flex;align-items:center;gap:18px;flex-wrap:wrap"> | |
| 166 | <div style="display:grid;place-items:center;width:48px;height:48px;border-radius:50%;background:var(--col-success);color:#fff;flex-shrink:0"> | |
| 167 | <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" width="26" height="26"><polyline points="20 6 9 17 4 12"/></svg> | |
| 168 | </div> | |
| 169 | <div style="flex:1;min-width:260px"> | |
| 170 | <div style="font-family:var(--font-display);font-size:22px;font-weight:800;color:var(--col-success);line-height:1.1;margin-bottom:6px"> | |
| 171 | Variant $detail_leader_label is winning — ship it | |
| 172 | </div> | |
| 173 | <div style="font-size:14px;color:var(--col-text-2);line-height:1.55"> | |
| 174 | Confidence is at <strong style="color:var(--col-text)">$detail_conf_pct</strong> with a <strong style="color:var(--col-text)">$detail_lift_pct</strong> lift in $detail_metric_label. The leading variant converts at <strong style="color:var(--col-text)">$detail_leader_rate</strong> vs the other at <strong style="color:var(--col-text)">$detail_loser_rate</strong>. Once you promote the winner, you can stop the experiment and start a new one on a different surface. | |
| 175 | </div> | |
| 176 | </div> | |
| 177 | </div> | |
| 178 | [/if] | |
| 179 | ||
| 180 | [if:$detail_too_early] | |
| 181 | <div class="mb-3" style="background:rgba(96,165,250,0.10);border:1px solid rgba(96,165,250,0.35);border-radius:14px;padding:22px 26px;display:flex;align-items:center;gap:18px;flex-wrap:wrap"> | |
| 182 | <div style="display:grid;place-items:center;width:48px;height:48px;border-radius:50%;background:rgba(96,165,250,0.25);color:#60a5fa;flex-shrink:0"> | |
| 183 | <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" width="26" height="26"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg> | |
| 184 | </div> | |
| 185 | <div style="flex:1;min-width:260px"> | |
| 186 | <div style="font-family:var(--font-display);font-size:20px;font-weight:700;color:#60a5fa;line-height:1.2;margin-bottom:6px"> | |
| 187 | Trending: Variant $detail_leader_label is ahead | |
| 188 | </div> | |
| 189 | <div style="font-size:13px;color:var(--col-text-2);line-height:1.55"> | |
| 190 | Confidence is at <strong style="color:var(--col-text)">$detail_conf_pct</strong> with a <strong style="color:var(--col-text)">$detail_lift_pct</strong> lift in $detail_metric_label. That is below our 95% threshold so the lead might still flip with more data — keep the test running. Variant $detail_leader_label converts at <strong style="color:var(--col-text)">$detail_leader_rate</strong>; the other at <strong style="color:var(--col-text)">$detail_loser_rate</strong>. | |
| 191 | </div> | |
| 192 | </div> | |
| 193 | </div> | |
| 194 | [/if] | |
| 195 | ||
| 196 | [if:$detail_below_min] | |
| 197 | <div class="mb-3" style="background:var(--col-surface-2);border:1px solid var(--col-border);border-radius:14px;padding:22px 26px;display:flex;align-items:center;gap:18px;flex-wrap:wrap"> | |
| 198 | <div style="display:grid;place-items:center;width:48px;height:48px;border-radius:50%;background:var(--col-surface-3);color:var(--col-text-2);flex-shrink:0"> | |
| 199 | <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="26" height="26"><path d="M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0z"/><path d="M12 8v4"/><path d="M12 16h.01"/></svg> | |
| 200 | </div> | |
| 201 | <div style="flex:1;min-width:260px"> | |
| 202 | <div style="font-family:var(--font-display);font-size:20px;font-weight:700;color:var(--col-text);line-height:1.2;margin-bottom:6px"> | |
| 203 | Not enough data yet | |
| 204 | </div> | |
| 205 | <div style="font-size:13px;color:var(--col-text-2);line-height:1.55"> | |
| 206 | You have $detail_traffic visitors split across two variants so far. We wait for at least 25 in each arm before declaring a leader — small samples produce noisy results that often flip. Keep the test running, or drive more traffic to your storefront from a campaign / social post / marketplace link. | |
| 207 | </div> | |
| 208 | </div> | |
| 209 | </div> | |
| 210 | [/if] | |
| 211 | [/if] | |
| 212 | ||
| 213 | <!-- Per-variant cards: content side-by-side, stats below each. --> | |
| 214 | <div class="dash-grid-2" style="gap:18px"> | |
| 215 | [loop:@detail_variants] | |
| 216 | <div style="background:[if:$loop1.is_locked_winner]rgba(34,197,94,0.10)[/if][if:!$loop1.is_locked_winner][if:$loop1.is_leader]rgba(34,197,94,0.06)[/if][if:!$loop1.is_leader]var(--col-surface-2)[/if][/if];border-radius:12px;border:[if:$loop1.is_locked_winner]2px[/if][if:!$loop1.is_locked_winner]1px[/if] solid [if:$loop1.is_locked_winner]rgba(34,197,94,0.55)[/if][if:!$loop1.is_locked_winner][if:$loop1.is_leader]rgba(34,197,94,0.30)[/if][if:!$loop1.is_leader]var(--col-border)[/if][/if];padding:16px"> | |
| 217 | <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:10px"> | |
| 218 | <div class="text-xs fw-700" style="letter-spacing:1.5px;text-transform:uppercase;color:[if:$loop1.is_locked_winner]var(--col-success)[/if][if:!$loop1.is_locked_winner][if:$loop1.is_leader]var(--col-success)[/if][if:!$loop1.is_leader]var(--col-text-3)[/if][/if]">$loop1.label</div> | |
| 219 | [if:$loop1.is_locked_winner]<span class="pill success">★ Winner</span>[/if] | |
| 220 | [if:!$loop1.is_locked_winner][if:$loop1.is_leader]<span class="pill success">Leading</span>[/if][/if] | |
| 221 | </div> | |
| 222 | [if:$loop1.has_image] | |
| 223 | <div style="aspect-ratio:1.6;background:url('$loop1.image_url') center/cover;border-radius:8px;margin-bottom:10px;border:1px solid var(--col-border)"></div> | |
| 224 | [/if] | |
| 225 | [if:!$loop1.has_image][if:!$loop1.has_design] | |
| 226 | <div style="aspect-ratio:1.6;background:var(--col-surface-3);border-radius:8px;margin-bottom:10px;display:grid;place-items:center;color:var(--col-text-3);font-size:12px">No image variant — text only</div> | |
| 227 | [/if][/if] | |
| 228 | [if:$loop1.has_design] | |
| 229 | <div style="background:var(--col-surface-3);border-radius:8px;margin-bottom:10px;padding:14px;display:flex;flex-direction:column;gap:8px"> | |
| 230 | [if:$loop1.has_layout_pick]<div style="display:flex;justify-content:space-between;font-size:12px"><span class="text-dim" style="letter-spacing:1px;text-transform:uppercase">Layout</span><strong style="color:var(--col-text)">$loop1.layout_name</strong></div>[/if] | |
| 231 | [if:$loop1.show_layout_inherit]<div style="display:flex;justify-content:space-between;font-size:12px"><span class="text-dim" style="letter-spacing:1px;text-transform:uppercase">Layout</span><em style="color:var(--col-text-2)">Storefront's saved layout</em></div>[/if] | |
| 232 | [if:$loop1.has_theme_pick]<div style="display:flex;justify-content:space-between;font-size:12px"><span class="text-dim" style="letter-spacing:1px;text-transform:uppercase">Theme</span><strong style="color:var(--col-text)">$loop1.theme_name</strong></div>[/if] | |
| 233 | [if:$loop1.show_theme_inherit]<div style="display:flex;justify-content:space-between;font-size:12px"><span class="text-dim" style="letter-spacing:1px;text-transform:uppercase">Theme</span><em style="color:var(--col-text-2)">Storefront's saved theme</em></div>[/if] | |
| 234 | </div> | |
| 235 | [/if] | |
| 236 | <div class="text-sm fw-600" style="color:var(--col-text);margin-bottom:10px;min-height:42px;line-height:1.4">$loop1.text</div> | |
| 237 | <div class="dash-grid-3" style="gap:10px"> | |
| 238 | <div> | |
| 239 | <div class="text-xs text-dim" style="letter-spacing:1px;text-transform:uppercase">Visitors</div> | |
| 240 | <div class="font-mono fw-700 text-sm">$loop1.exposures</div> | |
| 241 | </div> | |
| 242 | <div> | |
| 243 | <div class="text-xs text-dim" style="letter-spacing:1px;text-transform:uppercase">$detail_metric_label</div> | |
| 244 | <div class="font-mono fw-700 text-sm">$loop1.conversions</div> | |
| 245 | </div> | |
| 246 | <div> | |
| 247 | <div class="text-xs text-dim" style="letter-spacing:1px;text-transform:uppercase">Conv. rate</div> | |
| 248 | <div class="font-mono fw-700 text-sm" style="color:[if:$loop1.is_leader]var(--col-success)[/if][if:!$loop1.is_leader]var(--col-text)[/if]">$loop1.rate</div> | |
| 249 | </div> | |
| 250 | </div> | |
| 251 | </div> | |
| 252 | [/loop] | |
| 253 | </div> | |
| 254 | ||
| 255 | <!-- Summary KPIs across the bottom. --> | |
| 256 | <div class="divider"></div> | |
| 257 | <div class="dash-grid-3"> | |
| 258 | <div> | |
| 259 | <div class="text-xs text-dim" style="letter-spacing:1.5px;text-transform:uppercase">Lift</div> | |
| 260 | <div style="font-family:var(--font-display);font-size:24px;font-weight:700;color:[if:$detail_winner_called]var(--col-success)[/if][if:!$detail_winner_called]var(--col-text)[/if]">$detail_lift_pct</div> | |
| 261 | <div class="text-xs text-dim">B over A on $detail_metric_label</div> | |
| 262 | </div> | |
| 263 | <div> | |
| 264 | <div class="text-xs text-dim" style="letter-spacing:1.5px;text-transform:uppercase">Confidence</div> | |
| 265 | <div style="font-family:var(--font-display);font-size:24px;font-weight:700;color:#fff">$detail_conf_pct</div> | |
| 266 | <div class="text-xs text-dim">[if:$detail_winner_called]Above 95% threshold[/if][if:!$detail_winner_called]Still climbing toward 95%[/if]</div> | |
| 267 | </div> | |
| 268 | <div> | |
| 269 | <div class="text-xs text-dim" style="letter-spacing:1.5px;text-transform:uppercase">Total visitors</div> | |
| 270 | <div style="font-family:var(--font-display);font-size:24px;font-weight:700;color:#fff">$detail_traffic</div> | |
| 271 | <div class="text-xs text-dim">across both variants</div> | |
| 272 | </div> | |
| 273 | </div> | |
| 274 | ||
| 275 | <!-- Action bar: Start/Pause/Resume + Edit + Reset + Delete --> | |
| 276 | <div class="divider"></div> | |
| 277 | <div style="display:flex;flex-wrap:wrap;gap:10px"> | |
| 278 | [if:$detail_is_draft] | |
| 279 | <form method="POST" action="/optimization_action.cgi" style="display:inline;margin:0"> | |
| 280 | <input type="hidden" name="act" value="start"> | |
| 281 | <input type="hidden" name="id" value="$detail_id"> | |
| 282 | <button type="submit" class="btn btn-success">Start experiment</button> | |
| 283 | </form> | |
| 284 | [/if] | |
| 285 | [if:$detail_is_running] | |
| 286 | <form method="POST" action="/optimization_action.cgi" style="display:inline;margin:0"> | |
| 287 | <input type="hidden" name="act" value="pause"> | |
| 288 | <input type="hidden" name="id" value="$detail_id"> | |
| 289 | <button type="submit" class="btn btn-warning">Pause</button> | |
| 290 | </form> | |
| 291 | <form method="POST" action="/optimization_action.cgi" style="display:inline;margin:0" | |
| 292 | data-confirm-title="Mark complete" | |
| 293 | data-confirm-message="$detail_complete_confirm" | |
| 294 | data-confirm-label="Complete" | |
| 295 | data-confirm-style="success"> | |
| 296 | <input type="hidden" name="act" value="complete"> | |
| 297 | <input type="hidden" name="id" value="$detail_id"> | |
| 298 | <button type="submit" class="btn btn-success">Mark complete</button> | |
| 299 | </form> | |
| 300 | [/if] | |
| 301 | [if:$detail_is_paused] | |
| 302 | <form method="POST" action="/optimization_action.cgi" style="display:inline;margin:0"> | |
| 303 | <input type="hidden" name="act" value="start"> | |
| 304 | <input type="hidden" name="id" value="$detail_id"> | |
| 305 | <button type="submit" class="btn btn-success">Resume</button> | |
| 306 | </form> | |
| 307 | <form method="POST" action="/optimization_action.cgi" style="display:inline;margin:0" | |
| 308 | data-confirm-title="Mark complete" | |
| 309 | data-confirm-message="$detail_complete_confirm" | |
| 310 | data-confirm-label="Complete" | |
| 311 | data-confirm-style="success"> | |
| 312 | <input type="hidden" name="act" value="complete"> | |
| 313 | <input type="hidden" name="id" value="$detail_id"> | |
| 314 | <button type="submit" class="btn btn-success">Mark complete</button> | |
| 315 | </form> | |
| 316 | [/if] | |
| 317 | [if:$detail_is_complete] | |
| 318 | <form method="POST" action="/optimization_action.cgi" style="display:inline;margin:0" | |
| 319 | data-confirm-title="Archive experiment" | |
| 320 | data-confirm-message="The experiment moves to the Archived section at the bottom of the page so it does not clutter your active or completed list. All configuration, visitor data, and the winner pick are preserved. You can Unarchive it later to bring it back." | |
| 321 | data-confirm-label="Archive" | |
| 322 | data-confirm-style="primary"> | |
| 323 | <input type="hidden" name="act" value="archive"> | |
| 324 | <input type="hidden" name="id" value="$detail_id"> | |
| 325 | <button type="submit" class="btn btn-secondary">Archive</button> | |
| 326 | </form> | |
| 327 | [/if] | |
| 328 | ||
| 329 | <a href="/optimization.cgi?edit=$detail_id" class="btn btn-secondary">Edit experiment</a> | |
| 330 | ||
| 331 | <form method="POST" action="/optimization_action.cgi" style="display:inline;margin:0" | |
| 332 | data-confirm-title="Reset stats" | |
| 333 | data-confirm-message="The variants and configuration stay exactly the same, but every collected visitor exposure and conversion event is deleted. The status returns to Draft so you can deliberately re-start the test once you are ready." | |
| 334 | data-confirm-label="Reset stats" | |
| 335 | data-confirm-style="warning"> | |
| 336 | <input type="hidden" name="act" value="reset_stats"> | |
| 337 | <input type="hidden" name="id" value="$detail_id"> | |
| 338 | <button type="submit" class="btn btn-secondary">Reset stats</button> | |
| 339 | </form> | |
| 340 | ||
| 341 | <form method="POST" action="/optimization_action.cgi" style="display:inline;margin:0;margin-left:auto" | |
| 342 | data-confirm-title="Delete experiment" | |
| 343 | data-confirm-message="The experiment configuration and every visitor exposure / conversion event tied to it are removed permanently. This cannot be undone. If you might want this history later, use Archive instead -- it keeps the row out of your active list without erasing anything." | |
| 344 | data-confirm-label="Delete permanently" | |
| 345 | data-confirm-style="danger"> | |
| 346 | <input type="hidden" name="act" value="delete"> | |
| 347 | <input type="hidden" name="id" value="$detail_id"> | |
| 348 | <button type="submit" class="btn btn-danger">Delete</button> | |
| 349 | </form> | |
| 350 | </div> | |
| 351 | </div> | |
| 352 | </div> | |
| 353 | [/if] | |
| 354 | ||
| 355 | [if:$saved_flash] | |
| 356 | <div class="banner success mb-3"> | |
| 357 | <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"/></svg> | |
| 358 | <div class="text-xs">Experiment saved as a draft. Start it when you are ready to begin collecting data.</div> | |
| 359 | </div> | |
| 360 | [/if] | |
| 361 | ||
| 362 | [if:$mode_new] | |
| 363 | <!-- ============ NEW EXPERIMENT BUILDER ============ | |
| 364 | Creates a draft row in ab_tests. The draft does not collect | |
| 365 | data until the seller hits Start on the list view. Image | |
| 366 | variants upload via /upload_image.cgi (existing endpoint) | |
| 367 | which returns a URL stored in the variants JSON. --> | |
| 368 | <div class="module mb-3"> | |
| 369 | <div class="module-head"> | |
| 370 | <div class="left"> | |
| 371 | <div class="module-icon" style="background:rgba(124,58,237,0.15);color:var(--col-violet-bright)"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg></div> | |
| 372 | <div> | |
| 373 | [if:$is_edit] | |
| 374 | <div class="module-title">Edit experiment</div> | |
| 375 | <div class="module-sub">Saving changes wipes the visitor data collected so far and reverts the row to a fresh draft.</div> | |
| 376 | [/if] | |
| 377 | [if:!$is_edit] | |
| 378 | <div class="module-title">New experiment</div> | |
| 379 | <div class="module-sub">Define a control + at least one challenger. Save as draft, start it later.</div> | |
| 380 | [/if] | |
| 381 | </div> | |
| 382 | </div> | |
| 383 | </div> | |
| 384 | <div class="module-body"> | |
| 385 | [if:$is_edit] | |
| 386 | <!-- Loud warning so the seller does not edit a running test | |
| 387 | expecting the data to be preserved. --> | |
| 388 | <div class="mb-3" style="background:rgba(245,158,11,0.12);border:1px solid rgba(245,158,11,0.40);border-radius:10px;padding:14px 18px;display:flex;gap:12px;align-items:flex-start"> | |
| 389 | <svg viewBox="0 0 24 24" fill="none" stroke="#fbbf24" stroke-width="2" width="20" height="20" style="flex-shrink:0;margin-top:1px"><path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0Z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg> | |
| 390 | <div style="font-size:13px;color:var(--col-text-2);line-height:1.55"> | |
| 391 | <strong style="color:#fbbf24">Editing this experiment resets it.</strong> | |
| 392 | The variants you typed before are about to be replaced. Any visitors already bucketed and any conversion events collected will be wiped (those visitors saw the OLD variants, so the data cannot be fairly combined with the new ones). The status flips back to <strong style="color:var(--col-text)">Draft</strong> and you will need to <strong style="color:var(--col-text)">Start</strong> the test again. If you just want to clear the collected numbers without changing the variants, use <strong style="color:var(--col-text)">Reset stats</strong> on the experiment row instead. | |
| 393 | </div> | |
| 394 | </div> | |
| 395 | [/if] | |
| 396 | [if:!$has_storefront] | |
| 397 | <div class="banner warning mb-3"><div class="text-sm">You need a storefront before you can run experiments. <a href="/storefront.cgi" class="text-brand">Create one →</a></div></div> | |
| 398 | [/if] | |
| 399 | [if:$has_storefront] | |
| 400 | <form method="POST" action="/optimization_action.cgi" id="exp-form" enctype="multipart/form-data"> | |
| 401 | <input type="hidden" name="act" value="save"> | |
| 402 | [if:$is_edit]<input type="hidden" name="id" value="$edit_id">[/if] | |
| 403 | <input type="hidden" name="storefront_id" value="$store_id"> | |
| 404 | ||
| 405 | <div class="form-group mb-3"> | |
| 406 | <label class="form-label">Experiment name <span class="req">*</span></label> | |
| 407 | <input class="input" type="text" name="name" value="$edit_name" maxlength="160" required placeholder="e.g. Hero headline test"> | |
| 408 | <div class="form-help">Private name to help you find this test later. Buyers do not see it.</div> | |
| 409 | </div> | |
| 410 | ||
| 411 | <div class="dash-grid-2 mb-3" style="gap:14px"> | |
| 412 | <div class="form-group" style="margin-bottom:0"> | |
| 413 | <label class="form-label">Test type</label> | |
| 414 | <select class="select" name="test_type"> | |
| 415 | <option value="ab">A/B — two variants, one element changing</option> | |
| 416 | <option value="mvt">MVT — multiple variants, combinations of changes</option> | |
| 417 | </select> | |
| 418 | </div> | |
| 419 | <div class="form-group" style="margin-bottom:0"> | |
| 420 | <label class="form-label">What is changing?</label> | |
| 421 | <select class="select" name="surface" id="surface-select"> | |
| 422 | <optgroup label="Element-level"> | |
| 423 | <option value="hero_title">Hero headline (text)</option> | |
| 424 | <option value="hero_subtitle">Hero subtitle (text)</option> | |
| 425 | <option value="hero_image">Hero image (visual)</option> | |
| 426 | <option value="hero_video">Hero video (visual)</option> | |
| 427 | <option value="cta_label">CTA button label (text)</option> | |
| 428 | <option value="product_tile">Product tile (single model: title + image)</option> | |
| 429 | <option value="section_heading">Section heading (text)</option> | |
| 430 | <option value="custom">Custom HTML override</option> | |
| 431 | </optgroup> | |
| 432 | <optgroup label="Page design"> | |
| 433 | <option value="layout">Page layout (swap whole storefront structure)</option> | |
| 434 | <option value="theme">Color theme (swap whole palette)</option> | |
| 435 | <option value="layout_and_theme">Layout AND theme (full design MVT)</option> | |
| 436 | </optgroup> | |
| 437 | </select> | |
| 438 | <div class="form-help">Element-level tests change one piece of the page. Page-design tests compare entire storefront looks.</div> | |
| 439 | </div> | |
| 440 | </div> | |
| 441 | ||
| 442 | <!-- Product picker (only relevant when surface = product_tile). | |
| 443 | Toggled by the JS at the bottom of this form. --> | |
| 444 | <div class="form-group mb-3" id="target-model-group" style="display:none"> | |
| 445 | <label class="form-label">Product to test <span class="req">*</span></label> | |
| 446 | [if:$has_user_models] | |
| 447 | <select class="select" name="target_model_id" id="target-model-select"> | |
| 448 | [loop:@user_models]<option value="$loop1.id">$loop1.title</option>[/loop] | |
| 449 | </select> | |
| 450 | <div class="form-help">The tile for this model in the catalog grid is what variants A and B replace. Other products are unaffected.</div> | |
| 451 | [/if] | |
| 452 | [if:!$has_user_models] | |
| 453 | <div class="text-xs text-warning">You need at least one published model to run a product tile test. <a href="/upload_model.cgi" class="text-brand">Upload one →</a></div> | |
| 454 | [/if] | |
| 455 | </div> | |
| 456 | ||
| 457 | <div class="dash-grid-2 mb-3" style="gap:14px"> | |
| 458 | <div class="form-group" style="margin-bottom:0"> | |
| 459 | <label class="form-label">Success event</label> | |
| 460 | <select class="select" name="primary_metric"> | |
| 461 | <option value="conversion_rate">Conversion (paid order)</option> | |
| 462 | <option value="add_to_cart">Add to cart</option> | |
| 463 | <option value="click">Click on tested element</option> | |
| 464 | <option value="signup">Email signup</option> | |
| 465 | </select> | |
| 466 | </div> | |
| 467 | <div class="form-group" style="margin-bottom:0"> | |
| 468 | <label class="form-label">Confidence threshold</label> | |
| 469 | <select class="select" name="confidence_threshold"> | |
| 470 | <option value="0.95">95% (recommended)</option> | |
| 471 | <option value="0.90">90% (faster, slightly less reliable)</option> | |
| 472 | <option value="0.99">99% (slower, very reliable)</option> | |
| 473 | </select> | |
| 474 | </div> | |
| 475 | </div> | |
| 476 | ||
| 477 | <div class="divider"></div> | |
| 478 | ||
| 479 | <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:10px"> | |
| 480 | <div class="pe-section-label" style="font-size:11px;letter-spacing:1.5px;text-transform:uppercase;color:var(--col-text-3);font-weight:700;margin:0">Variants</div> | |
| 481 | <button type="button" id="add-variant-btn" class="btn btn-secondary" style="display:none;font-size:12px;padding:6px 12px">+ Add variant (MVT)</button> | |
| 482 | </div> | |
| 483 | ||
| 484 | <!-- Variants. Loop renders one card per variant in edit_variants | |
| 485 | (saved variants A..F come from the row; new tests start with | |
| 486 | A + B). The card body contains every input shape we need | |
| 487 | (text, image, layout, theme); the JS at the bottom toggles | |
| 488 | which inputs are visible based on the chosen surface. --> | |
| 489 | <div id="variants-container"> | |
| 490 | [loop:@edit_variants] | |
| 491 | <div class="variant-card module mb-3" data-vid="$loop1.vid" | |
| 492 | style="background:var(--col-surface-2);[if:!$loop1.is_control]border-color:rgba(124,58,237,0.30);[/if]"> | |
| 493 | <div class="module-body"> | |
| 494 | <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:10px"> | |
| 495 | <div class="text-xs mb-0" style="letter-spacing:1.5px;text-transform:uppercase;color:[if:$loop1.is_control]var(--col-text-3)[/if][if:!$loop1.is_control]var(--col-violet-bright)[/if]">$loop1.label</div> | |
| 496 | [if:!$loop1.is_control] | |
| 497 | <button type="button" class="remove-variant-btn" data-vid="$loop1.vid" style="display:none;background:none;border:1px solid var(--col-border);color:var(--col-text-3);font-size:11px;padding:4px 10px;border-radius:6px;cursor:pointer">Remove</button> | |
| 498 | [/if] | |
| 499 | </div> | |
| 500 | ||
| 501 | <!-- Optional human-readable label that shows in the dashboard. --> | |
| 502 | <div class="form-group v-name-group" style="margin-bottom:10px"> | |
| 503 | <label class="form-label">Variant name (optional)</label> | |
| 504 | <input class="input" type="text" name="$loop1.name_field" value="$loop1.name" maxlength="120" placeholder="e.g. Bigger image + bolder CTA"> | |
| 505 | </div> | |
| 506 | ||
| 507 | <!-- Text / image inputs: relevant for hero_title, hero_subtitle, | |
| 508 | hero_image, cta_label, product_tile, section_heading, custom --> | |
| 509 | <div class="v-text-group"> | |
| 510 | <div class="form-group"> | |
| 511 | <label class="form-label">Text content</label> | |
| 512 | <textarea class="input" name="$loop1.text_field" rows="3" placeholder="[if:$loop1.is_control]The current copy on this surface, or leave blank to keep what is already live[/if][if:!$loop1.is_control]The new copy you want to test against the control[/if]">$loop1.text</textarea> | |
| 513 | </div> | |
| 514 | </div> | |
| 515 | <div class="v-image-group"> | |
| 516 | [if:$loop1.has_image] | |
| 517 | <div class="form-group" style="margin-bottom:8px"> | |
| 518 | <div class="text-xs text-dim mb-1" style="letter-spacing:1px;text-transform:uppercase">Current image</div> | |
| 519 | <div style="width:120px;height:80px;background:url('$loop1.image_url') center/cover;border:1px solid var(--col-border);border-radius:6px"></div> | |
| 520 | <input type="hidden" name="$loop1.image_existing_field" value="$loop1.image_url"> | |
| 521 | </div> | |
| 522 | [/if] | |
| 523 | <div class="form-group" style="margin-bottom:0"> | |
| 524 | <label class="form-label">[if:$loop1.has_image]Replace image (optional)[/if][if:!$loop1.has_image]Image (optional)[/if]</label> | |
| 525 | <input class="input" type="file" name="$loop1.image_field" accept="image/*"> | |
| 526 | <div class="form-help">Used when the tested element is an image, product tile, or hero video.</div> | |
| 527 | </div> | |
| 528 | </div> | |
| 529 | ||
| 530 | <!-- Layout picker: shown when surface is layout / layout_and_theme. | |
| 531 | Value 0 means "use the storefront's saved layout" -- only useful | |
| 532 | for the control variant. --> | |
| 533 | <div class="v-layout-group" style="display:none"> | |
| 534 | <div class="form-group"> | |
| 535 | <label class="form-label">Page layout</label> | |
| 536 | <select class="select v-layout-select" name="$loop1.layout_id_field" data-saved="$loop1.layout_id"> | |
| 537 | <option value="0">[if:$loop1.is_control]Use my saved layout (control)[/if][if:!$loop1.is_control]— pick a layout —[/if]</option> | |
| 538 | [loop:@tvars.layout_options]<option value="$loop2.id">$loop2.name — $loop2.tagline</option>[/loop] | |
| 539 | </select> | |
| 540 | </div> | |
| 541 | </div> | |
| 542 | ||
| 543 | <!-- Theme picker: shown when surface is theme / layout_and_theme. | |
| 544 | We give built-in themes and the seller's saved custom themes | |
| 545 | in one dropdown using a "kind:id" value so the action handler | |
| 546 | can route to the right column (theme_id vs custom_theme_id). --> | |
| 547 | <div class="v-theme-group" style="display:none"> | |
| 548 | <div class="form-group"> | |
| 549 | <label class="form-label">Color theme</label> | |
| 550 | <select class="select v-theme-select" data-vid="$loop1.vid" | |
| 551 | data-saved-builtin="$loop1.theme_id" | |
| 552 | data-saved-custom="$loop1.custom_theme_id"> | |
| 553 | <option value="builtin:0">[if:$loop1.is_control]Use my saved theme (control)[/if][if:!$loop1.is_control]— pick a theme —[/if]</option> | |
| 554 | <optgroup label="Built-in themes"> | |
| 555 | [loop:@tvars.theme_options]<option value="builtin:$loop2.id">$loop2.name</option>[/loop] | |
| 556 | </optgroup> | |
| 557 | [if:$has_custom_themes] | |
| 558 | <optgroup label="My saved custom themes"> | |
| 559 | [loop:@tvars.custom_theme_options]<option value="custom:$loop2.id">$loop2.name</option>[/loop] | |
| 560 | </optgroup> | |
| 561 | [/if] | |
| 562 | </select> | |
| 563 | <!-- Hidden numeric fields that actually post. The JS keeps | |
| 564 | them synced with the unified dropdown above. --> | |
| 565 | <input type="hidden" name="$loop1.theme_id_field" class="v-theme-id-input" value="$loop1.theme_id"> | |
| 566 | <input type="hidden" name="$loop1.custom_theme_id_field" class="v-custom-theme-id-input" value="$loop1.custom_theme_id"> | |
| 567 | </div> | |
| 568 | </div> | |
| 569 | ||
| 570 | </div> | |
| 571 | </div> | |
| 572 | [/loop] | |
| 573 | </div> | |
| 574 | ||
| 575 | <div class="flex gap-2"> | |
| 576 | <button type="submit" class="btn btn-primary">Save as draft</button> | |
| 577 | <a href="/optimization.cgi" class="btn btn-secondary">Cancel</a> | |
| 578 | </div> | |
| 579 | </form> | |
| 580 | ||
| 581 | <script> | |
| 582 | (function () { | |
| 583 | // In edit mode, seed each select to the stored value before | |
| 584 | // wiring the toggle. The CGI passes $edit_* tvars that are | |
| 585 | // empty strings when creating a fresh experiment. | |
| 586 | var EDIT = { | |
| 587 | test_type: "$edit_test_type", | |
| 588 | surface: "$edit_surface", | |
| 589 | primary_metric: "$edit_primary_metric", | |
| 590 | target_model_id: "$edit_target_model_id" | |
| 591 | }; | |
| 592 | function setSelect(name, value) { | |
| 593 | if (!value) return; | |
| 594 | var el = document.querySelector('select[name="' + name + '"]'); | |
| 595 | if (el) el.value = value; | |
| 596 | } | |
| 597 | setSelect('test_type', EDIT.test_type); | |
| 598 | setSelect('surface', EDIT.surface); | |
| 599 | setSelect('primary_metric', EDIT.primary_metric); | |
| 600 | setSelect('target_model_id', EDIT.target_model_id); | |
| 601 | ||
| 602 | // Restore each variant card's saved layout + theme picks. | |
| 603 | // The hidden inputs carry the canonical numbers (theme_id / | |
| 604 | // custom_theme_id); the unified dropdown is what the seller | |
| 605 | // touches and we keep them in sync via the change handler | |
| 606 | // wired further down. | |
| 607 | document.querySelectorAll('.v-layout-select').forEach(function (sel) { | |
| 608 | var saved = sel.getAttribute('data-saved'); | |
| 609 | if (saved && saved !== '0') sel.value = saved; | |
| 610 | }); | |
| 611 | document.querySelectorAll('.v-theme-select').forEach(function (sel) { | |
| 612 | var b = sel.getAttribute('data-saved-builtin') || '0'; | |
| 613 | var c = sel.getAttribute('data-saved-custom') || '0'; | |
| 614 | if (c !== '0') sel.value = 'custom:' + c; | |
| 615 | else if (b !== '0') sel.value = 'builtin:' + b; | |
| 616 | else sel.value = 'builtin:0'; | |
| 617 | }); | |
| 618 | ||
| 619 | // Theme picker -> hidden inputs. "builtin:N" sets theme_id=N, | |
| 620 | // "custom:N" sets custom_theme_id=N; the other one is zeroed. | |
| 621 | document.querySelectorAll('.v-theme-select').forEach(function (sel) { | |
| 622 | sel.addEventListener('change', function () { | |
| 623 | var card = sel.closest('.variant-card'); | |
| 624 | if (!card) return; | |
| 625 | var binp = card.querySelector('.v-theme-id-input'); | |
| 626 | var cinp = card.querySelector('.v-custom-theme-id-input'); | |
| 627 | var parts = (sel.value || 'builtin:0').split(':'); | |
| 628 | var kind = parts[0], num = parts[1] || '0'; | |
| 629 | if (kind === 'custom') { if (binp) binp.value = '0'; if (cinp) cinp.value = num; } | |
| 630 | else { if (cinp) cinp.value = '0'; if (binp) binp.value = num; } | |
| 631 | }); | |
| 632 | }); | |
| 633 | ||
| 634 | // Show / hide field groups based on the selected surface. | |
| 635 | // - product_tile: text, image, target_model picker | |
| 636 | // - hero_image / hero_video / product_tile: image is primary | |
| 637 | // - hero_title / hero_subtitle / cta_label / section_heading / custom: text only | |
| 638 | // - layout: layout picker per variant | |
| 639 | // - theme: theme picker per variant | |
| 640 | // - layout_and_theme: both pickers per variant | |
| 641 | var sel = document.getElementById('surface-select'); | |
| 642 | var tmGrp = document.getElementById('target-model-group'); | |
| 643 | var picker = document.getElementById('target-model-select'); | |
| 644 | var tt = document.querySelector('select[name="test_type"]'); | |
| 645 | var addBtn = document.getElementById('add-variant-btn'); | |
| 646 | if (!sel) return; | |
| 647 | ||
| 648 | var TEXT_SURFACES = { hero_title:1, hero_subtitle:1, cta_label:1, section_heading:1, custom:1 }; | |
| 649 | var IMAGE_SURFACES = { hero_image:1, hero_video:1, product_tile:1 }; | |
| 650 | var LAYOUT_SURFACES = { layout:1, layout_and_theme:1 }; | |
| 651 | var THEME_SURFACES = { theme:1, layout_and_theme:1 }; | |
| 652 | ||
| 653 | function sync() { | |
| 654 | var s = sel.value; | |
| 655 | var isProductTile = (s === 'product_tile'); | |
| 656 | if (tmGrp) tmGrp.style.display = isProductTile ? '' : 'none'; | |
| 657 | if (picker) picker.required = isProductTile; | |
| 658 | ||
| 659 | // Cards: hide text/image groups for design surfaces; show | |
| 660 | // layout/theme groups for design surfaces. Product tile keeps | |
| 661 | // both text + image visible (variants tweak title + tile image). | |
| 662 | document.querySelectorAll('.variant-card').forEach(function (card) { | |
| 663 | var tg = card.querySelector('.v-text-group'); | |
| 664 | var ig = card.querySelector('.v-image-group'); | |
| 665 | var lg = card.querySelector('.v-layout-group'); | |
| 666 | var thg = card.querySelector('.v-theme-group'); | |
| 667 | if (tg) tg.style.display = (TEXT_SURFACES[s] || s === 'product_tile') ? '' : 'none'; | |
| 668 | if (ig) ig.style.display = (IMAGE_SURFACES[s]) ? '' : 'none'; | |
| 669 | if (lg) lg.style.display = LAYOUT_SURFACES[s] ? '' : 'none'; | |
| 670 | if (thg) thg.style.display = THEME_SURFACES[s] ? '' : 'none'; | |
| 671 | }); | |
| 672 | ||
| 673 | syncAddBtn(); | |
| 674 | } | |
| 675 | sel.addEventListener('change', sync); | |
| 676 | ||
| 677 | // MVT: allow up to 6 variants. The "+ Add variant" button is | |
| 678 | // visible whenever test_type === 'mvt' (or whenever the seller | |
| 679 | // has more than 2 variants already, because they MIGHT be a | |
| 680 | // MVT that the user is downgrading to A/B and the extra rows | |
| 681 | // are still loaded). | |
| 682 | function syncAddBtn() { | |
| 683 | if (!addBtn) return; | |
| 684 | var n = document.querySelectorAll('.variant-card').length; | |
| 685 | addBtn.style.display = (tt && tt.value === 'mvt' && n < 6) ? '' : 'none'; | |
| 686 | // Remove buttons only visible on MVT and not for control. | |
| 687 | document.querySelectorAll('.remove-variant-btn').forEach(function (b) { | |
| 688 | b.style.display = (tt && tt.value === 'mvt') ? '' : 'none'; | |
| 689 | }); | |
| 690 | } | |
| 691 | if (tt) tt.addEventListener('change', syncAddBtn); | |
| 692 | ||
| 693 | // Build a new variant card by cloning the last one + bumping | |
| 694 | // its vid. Cheaper than asking the server for an empty card. | |
| 695 | var LETTERS = ['A','B','C','D','E','F']; | |
| 696 | function nextVid() { | |
| 697 | for (var i = 0; i < LETTERS.length; i++) { | |
| 698 | if (!document.querySelector('.variant-card[data-vid="' + LETTERS[i] + '"]')) { | |
| 699 | return LETTERS[i]; | |
| 700 | } | |
| 701 | } | |
| 702 | return null; | |
| 703 | } | |
| 704 | if (addBtn) { | |
| 705 | addBtn.addEventListener('click', function () { | |
| 706 | var newVid = nextVid(); | |
| 707 | if (!newVid) return; | |
| 708 | var last = document.querySelector('.variant-card[data-vid="B"]') | |
| 709 | || document.querySelector('.variant-card'); | |
| 710 | if (!last) return; | |
| 711 | var clone = last.cloneNode(true); | |
| 712 | clone.setAttribute('data-vid', newVid); | |
| 713 | // Re-label. | |
| 714 | var lbl = clone.querySelector('.text-xs'); | |
| 715 | if (lbl) lbl.textContent = 'Variant ' + newVid + ' . Challenger'; | |
| 716 | // Rename every form field from variant_B_* -> variant_<NEW>_*. | |
| 717 | clone.querySelectorAll('[name]').forEach(function (el) { | |
| 718 | el.name = el.name.replace(/^variant_[A-F]_/, 'variant_' + newVid + '_'); | |
| 719 | // Reset the new card's values. | |
| 720 | if (el.tagName === 'TEXTAREA' || (el.tagName === 'INPUT' && el.type !== 'file' && el.type !== 'hidden')) { | |
| 721 | el.value = ''; | |
| 722 | } | |
| 723 | if (el.tagName === 'INPUT' && el.type === 'hidden') { | |
| 724 | el.value = '0'; | |
| 725 | } | |
| 726 | if (el.tagName === 'SELECT') { | |
| 727 | el.selectedIndex = 0; | |
| 728 | } | |
| 729 | }); | |
| 730 | // Re-attach the change listener on this clone's theme select. | |
| 731 | var ts = clone.querySelector('.v-theme-select'); | |
| 732 | if (ts) { | |
| 733 | ts.setAttribute('data-vid', newVid); | |
| 734 | ts.addEventListener('change', function () { | |
| 735 | var card = ts.closest('.variant-card'); | |
| 736 | var binp = card.querySelector('.v-theme-id-input'); | |
| 737 | var cinp = card.querySelector('.v-custom-theme-id-input'); | |
| 738 | var parts = (ts.value || 'builtin:0').split(':'); | |
| 739 | var kind = parts[0], num = parts[1] || '0'; | |
| 740 | if (kind === 'custom') { if (binp) binp.value = '0'; if (cinp) cinp.value = num; } | |
| 741 | else { if (cinp) cinp.value = '0'; if (binp) binp.value = num; } | |
| 742 | }); | |
| 743 | } | |
| 744 | // Remove the "current image" preview (only the original may have one). | |
| 745 | var preview = clone.querySelector('.v-image-group > .form-group:first-child'); | |
| 746 | if (preview && preview.querySelector('input[type=hidden]')) preview.remove(); | |
| 747 | // Wire up the new card's remove button. | |
| 748 | var rm = clone.querySelector('.remove-variant-btn'); | |
| 749 | if (rm) { | |
| 750 | rm.setAttribute('data-vid', newVid); | |
| 751 | rm.addEventListener('click', function () { | |
| 752 | clone.remove(); | |
| 753 | syncAddBtn(); | |
| 754 | }); | |
| 755 | } | |
| 756 | document.getElementById('variants-container').appendChild(clone); | |
| 757 | sync(); // make sure new card respects current surface | |
| 758 | }); | |
| 759 | } | |
| 760 | ||
| 761 | // Wire existing remove buttons (only present on challenger cards). | |
| 762 | document.querySelectorAll('.remove-variant-btn').forEach(function (rm) { | |
| 763 | rm.addEventListener('click', function () { | |
| 764 | var card = rm.closest('.variant-card'); | |
| 765 | if (card) card.remove(); | |
| 766 | syncAddBtn(); | |
| 767 | }); | |
| 768 | }); | |
| 769 | ||
| 770 | sync(); | |
| 771 | })(); | |
| 772 | </script> | |
| 773 | [/if] | |
| 774 | </div> | |
| 775 | </div> | |
| 776 | [/if] | |
| 777 | ||
| 778 | [if:$mode_list] | |
| 779 | <!-- ============ YOUR EXPERIMENTS (three lifecycle sections) =========== | |
| 780 | Active (drafts/running/paused) on top, Completed below it, | |
| 781 | Archived at the bottom. Each has its own action set fitted to | |
| 782 | the lifecycle stage. --> | |
| 783 | ||
| 784 | [if:$has_active_experiments] | |
| 785 | <div class="module mb-3"> | |
| 786 | <div class="module-head"> | |
| 787 | <div class="left"> | |
| 788 | <div class="module-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M19 12a7 7 0 1 1-14 0 7 7 0 0 1 14 0Z"/></svg></div> | |
| 789 | <div> | |
| 790 | <div class="module-title">Active experiments</div> | |
| 791 | <div class="module-sub">$active_count in flight · drafts, running, and paused</div> | |
| 792 | </div> | |
| 793 | </div> | |
| 794 | </div> | |
| 795 | <div class="table-wrap" style="border:none;border-radius:0"> | |
| 796 | <table class="table exp-table"> | |
| 797 | <thead><tr> | |
| 798 | <th class="exp-col-name">Name</th> | |
| 799 | <th>Type</th> | |
| 800 | <th>Surface</th> | |
| 801 | <th>Status</th> | |
| 802 | <th class="num">Lift</th> | |
| 803 | <th class="num">Confidence</th> | |
| 804 | <th class="num">Visitors</th> | |
| 805 | <th class="exp-col-actions">Actions</th> | |
| 806 | </tr></thead> | |
| 807 | <tbody> | |
| 808 | [loop:@experiments_active] | |
| 809 | <tr> | |
| 810 | <td> | |
| 811 | <a href="/optimization.cgi?id=$loop1.id" style="color:inherit;text-decoration:none"> | |
| 812 | <strong style="color:var(--col-accent-bright)">$loop1.name</strong> | |
| 813 | </a> | |
| 814 | <div class="text-xs text-dim">$loop1.storefront</div> | |
| 815 | </td> | |
| 816 | <td>$loop1.test_type</td> | |
| 817 | <td class="text-xs text-dim">$loop1.surface</td> | |
| 818 | <td> | |
| 819 | [if:$loop1.is_draft]<span class="pill">Draft</span>[/if] | |
| 820 | [if:$loop1.is_running]<span class="pill brand">Running</span>[/if] | |
| 821 | [if:$loop1.is_paused]<span class="pill warning">Paused</span>[/if] | |
| 822 | </td> | |
| 823 | <td class="num">$loop1.lift</td> | |
| 824 | <td class="num">$loop1.confidence</td> | |
| 825 | <td class="num text-dim">$loop1.traffic_seen</td> | |
| 826 | <td class="exp-actions-host"> | |
| 827 | <div class="exp-actions-cell"> | |
| 828 | <a href="/optimization.cgi?id=$loop1.id" class="exp-row-action is-primary">View report →</a> | |
| 829 | ||
| 830 | [if:$loop1.is_draft] | |
| 831 | <form method="POST" action="/optimization_action.cgi" style="display:inline;margin:0"> | |
| 832 | <input type="hidden" name="act" value="start"> | |
| 833 | <input type="hidden" name="id" value="$loop1.id"> | |
| 834 | <button type="submit" class="exp-row-action is-success">Start</button> | |
| 835 | </form> | |
| 836 | [/if] | |
| 837 | [if:$loop1.is_running] | |
| 838 | <form method="POST" action="/optimization_action.cgi" style="display:inline;margin:0"> | |
| 839 | <input type="hidden" name="act" value="pause"> | |
| 840 | <input type="hidden" name="id" value="$loop1.id"> | |
| 841 | <button type="submit" class="exp-row-action is-warning">Pause</button> | |
| 842 | </form> | |
| 843 | <form method="POST" action="/optimization_action.cgi" style="display:inline;margin:0" | |
| 844 | data-confirm-title="Mark complete" | |
| 845 | data-confirm-message="$loop1.complete_confirm" | |
| 846 | data-confirm-label="Complete" | |
| 847 | data-confirm-style="success"> | |
| 848 | <input type="hidden" name="act" value="complete"> | |
| 849 | <input type="hidden" name="id" value="$loop1.id"> | |
| 850 | <button type="submit" class="exp-row-action is-success">Complete</button> | |
| 851 | </form> | |
| 852 | [/if] | |
| 853 | [if:$loop1.is_paused] | |
| 854 | <form method="POST" action="/optimization_action.cgi" style="display:inline;margin:0"> | |
| 855 | <input type="hidden" name="act" value="start"> | |
| 856 | <input type="hidden" name="id" value="$loop1.id"> | |
| 857 | <button type="submit" class="exp-row-action is-success">Resume</button> | |
| 858 | </form> | |
| 859 | <form method="POST" action="/optimization_action.cgi" style="display:inline;margin:0" | |
| 860 | data-confirm-title="Mark complete" | |
| 861 | data-confirm-message="$loop1.complete_confirm" | |
| 862 | data-confirm-label="Complete" | |
| 863 | data-confirm-style="success"> | |
| 864 | <input type="hidden" name="act" value="complete"> | |
| 865 | <input type="hidden" name="id" value="$loop1.id"> | |
| 866 | <button type="submit" class="exp-row-action is-success">Complete</button> | |
| 867 | </form> | |
| 868 | [/if] | |
| 869 | ||
| 870 | <a href="/optimization.cgi?edit=$loop1.id" class="exp-row-action is-edit">Edit</a> | |
| 871 | ||
| 872 | <form method="POST" action="/optimization_action.cgi" style="display:inline;margin:0" | |
| 873 | data-confirm-title="Reset stats" | |
| 874 | data-confirm-message="The variants and configuration stay exactly the same, but every collected visitor exposure and conversion event is deleted. The status returns to Draft so you can deliberately re-start the test once you are ready." | |
| 875 | data-confirm-label="Reset stats" | |
| 876 | data-confirm-style="warning"> | |
| 877 | <input type="hidden" name="act" value="reset_stats"> | |
| 878 | <input type="hidden" name="id" value="$loop1.id"> | |
| 879 | <button type="submit" class="exp-row-action is-reset">Reset stats</button> | |
| 880 | </form> | |
| 881 | ||
| 882 | <form method="POST" action="/optimization_action.cgi" style="display:inline;margin:0" | |
| 883 | data-confirm-title="Delete experiment" | |
| 884 | data-confirm-message="The experiment configuration and every visitor exposure / conversion event tied to it are removed permanently. This cannot be undone. If you might want this history later, use Archive instead -- it keeps the row out of your active list without erasing anything." | |
| 885 | data-confirm-label="Delete permanently" | |
| 886 | data-confirm-style="danger"> | |
| 887 | <input type="hidden" name="act" value="delete"> | |
| 888 | <input type="hidden" name="id" value="$loop1.id"> | |
| 889 | <button type="submit" class="exp-row-action is-danger">Delete</button> | |
| 890 | </form> | |
| 891 | </div> | |
| 892 | </td> | |
| 893 | </tr> | |
| 894 | [/loop] | |
| 895 | </tbody> | |
| 896 | </table> | |
| 897 | </div> | |
| 898 | </div> | |
| 899 | [/if] | |
| 900 | ||
| 901 | [if:$has_completed_experiments] | |
| 902 | <div class="module mb-3"> | |
| 903 | <div class="module-head"> | |
| 904 | <div class="left"> | |
| 905 | <div class="module-icon" style="background:rgba(34,197,94,0.15);color:#4ade80"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"/></svg></div> | |
| 906 | <div> | |
| 907 | <div class="module-title">Completed experiments</div> | |
| 908 | <div class="module-sub">$completed_count tests called · archive when you no longer need them</div> | |
| 909 | </div> | |
| 910 | </div> | |
| 911 | </div> | |
| 912 | <div class="table-wrap" style="border:none;border-radius:0"> | |
| 913 | <table class="table exp-table"> | |
| 914 | <thead><tr> | |
| 915 | <th class="exp-col-name">Name</th> | |
| 916 | <th>Type</th> | |
| 917 | <th>Surface</th> | |
| 918 | <th class="num">Lift</th> | |
| 919 | <th class="num">Confidence</th> | |
| 920 | <th class="num">Visitors</th> | |
| 921 | <th class="exp-col-actions">Actions</th> | |
| 922 | </tr></thead> | |
| 923 | <tbody> | |
| 924 | [loop:@experiments_completed] | |
| 925 | <tr> | |
| 926 | <td> | |
| 927 | <a href="/optimization.cgi?id=$loop1.id" style="color:inherit;text-decoration:none"> | |
| 928 | <strong style="color:var(--col-accent-bright)">$loop1.name</strong> | |
| 929 | </a> | |
| 930 | <div class="text-xs text-dim">$loop1.storefront</div> | |
| 931 | </td> | |
| 932 | <td>$loop1.test_type</td> | |
| 933 | <td class="text-xs text-dim">$loop1.surface</td> | |
| 934 | <td class="num">$loop1.lift</td> | |
| 935 | <td class="num">$loop1.confidence</td> | |
| 936 | <td class="num text-dim">$loop1.traffic_seen</td> | |
| 937 | <td class="exp-actions-host"> | |
| 938 | <div class="exp-actions-cell"> | |
| 939 | <a href="/optimization.cgi?id=$loop1.id" class="exp-row-action is-primary">View report →</a> | |
| 940 | ||
| 941 | <form method="POST" action="/optimization_action.cgi" style="display:inline;margin:0"> | |
| 942 | <input type="hidden" name="act" value="start"> | |
| 943 | <input type="hidden" name="id" value="$loop1.id"> | |
| 944 | <button type="submit" class="exp-row-action is-success">Re-run</button> | |
| 945 | </form> | |
| 946 | ||
| 947 | <form method="POST" action="/optimization_action.cgi" style="display:inline;margin:0" | |
| 948 | data-confirm-title="Archive experiment" | |
| 949 | data-confirm-message="The experiment moves to the Archived section at the bottom of the page so it does not clutter your active or completed list. All configuration, visitor data, and the winner pick are preserved. You can Unarchive it later to bring it back." | |
| 950 | data-confirm-label="Archive" | |
| 951 | data-confirm-style="primary"> | |
| 952 | <input type="hidden" name="act" value="archive"> | |
| 953 | <input type="hidden" name="id" value="$loop1.id"> | |
| 954 | <button type="submit" class="exp-row-action">Archive</button> | |
| 955 | </form> | |
| 956 | ||
| 957 | <form method="POST" action="/optimization_action.cgi" style="display:inline;margin:0" | |
| 958 | data-confirm-title="Delete experiment" | |
| 959 | data-confirm-message="The experiment configuration and every visitor exposure / conversion event tied to it are removed permanently. This cannot be undone. If you might want this history later, use Archive instead -- it keeps the row out of your active list without erasing anything." | |
| 960 | data-confirm-label="Delete permanently" | |
| 961 | data-confirm-style="danger"> | |
| 962 | <input type="hidden" name="act" value="delete"> | |
| 963 | <input type="hidden" name="id" value="$loop1.id"> | |
| 964 | <button type="submit" class="exp-row-action is-danger">Delete</button> | |
| 965 | </form> | |
| 966 | </div> | |
| 967 | </td> | |
| 968 | </tr> | |
| 969 | [/loop] | |
| 970 | </tbody> | |
| 971 | </table> | |
| 972 | </div> | |
| 973 | </div> | |
| 974 | [/if] | |
| 975 | ||
| 976 | [if:$has_archived_experiments] | |
| 977 | <div class="module mb-3" style="opacity:0.85"> | |
| 978 | <div class="module-head"> | |
| 979 | <div class="left"> | |
| 980 | <div class="module-icon" style="background:rgba(100,116,139,0.15);color:#94a3b8"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="21 8 21 21 3 21 3 8"/><rect x="1" y="3" width="22" height="5"/><line x1="10" y1="12" x2="14" y2="12"/></svg></div> | |
| 981 | <div> | |
| 982 | <div class="module-title">Archived experiments</div> | |
| 983 | <div class="module-sub">$archived_count parked · history only, not running</div> | |
| 984 | </div> | |
| 985 | </div> | |
| 986 | </div> | |
| 987 | <div class="table-wrap" style="border:none;border-radius:0"> | |
| 988 | <table class="table exp-table"> | |
| 989 | <thead><tr> | |
| 990 | <th class="exp-col-name">Name</th> | |
| 991 | <th>Type</th> | |
| 992 | <th>Surface</th> | |
| 993 | <th class="num">Lift</th> | |
| 994 | <th class="num">Confidence</th> | |
| 995 | <th class="num">Visitors</th> | |
| 996 | <th class="exp-col-actions">Actions</th> | |
| 997 | </tr></thead> | |
| 998 | <tbody> | |
| 999 | [loop:@experiments_archived] | |
| 1000 | <tr> | |
| 1001 | <td> | |
| 1002 | <a href="/optimization.cgi?id=$loop1.id" style="color:inherit;text-decoration:none"> | |
| 1003 | <strong style="color:var(--col-text-2)">$loop1.name</strong> | |
| 1004 | </a> | |
| 1005 | <div class="text-xs text-dim">$loop1.storefront</div> | |
| 1006 | </td> | |
| 1007 | <td class="text-dim">$loop1.test_type</td> | |
| 1008 | <td class="text-xs text-dim">$loop1.surface</td> | |
| 1009 | <td class="num text-dim">$loop1.lift</td> | |
| 1010 | <td class="num text-dim">$loop1.confidence</td> | |
| 1011 | <td class="num text-dim">$loop1.traffic_seen</td> | |
| 1012 | <td class="exp-actions-host"> | |
| 1013 | <div class="exp-actions-cell"> | |
| 1014 | <a href="/optimization.cgi?id=$loop1.id" class="exp-row-action is-primary">View report →</a> | |
| 1015 | ||
| 1016 | <form method="POST" action="/optimization_action.cgi" style="display:inline;margin:0"> | |
| 1017 | <input type="hidden" name="act" value="unarchive"> | |
| 1018 | <input type="hidden" name="id" value="$loop1.id"> | |
| 1019 | <button type="submit" class="exp-row-action">Unarchive</button> | |
| 1020 | </form> | |
| 1021 | ||
| 1022 | <form method="POST" action="/optimization_action.cgi" style="display:inline;margin:0" | |
| 1023 | data-confirm-title="Delete experiment" | |
| 1024 | data-confirm-message="The experiment configuration and every visitor exposure / conversion event tied to it are removed permanently. This cannot be undone. If you might want this history later, use Archive instead -- it keeps the row out of your active list without erasing anything." | |
| 1025 | data-confirm-label="Delete permanently" | |
| 1026 | data-confirm-style="danger"> | |
| 1027 | <input type="hidden" name="act" value="delete"> | |
| 1028 | <input type="hidden" name="id" value="$loop1.id"> | |
| 1029 | <button type="submit" class="exp-row-action is-danger">Delete</button> | |
| 1030 | </form> | |
| 1031 | </div> | |
| 1032 | </td> | |
| 1033 | </tr> | |
| 1034 | [/loop] | |
| 1035 | </tbody> | |
| 1036 | </table> | |
| 1037 | </div> | |
| 1038 | </div> | |
| 1039 | [/if] | |
| 1040 | [/if] | |
| 1041 | ||
| 1042 | [if:$mode_list] | |
| 1043 | [if:!$has_experiments] | |
| 1044 | <!-- ============ ONBOARDING ============ | |
| 1045 | Shown to users who do not yet have any real experiments. The | |
| 1046 | demo content below this section is labelled "Example" so the | |
| 1047 | user understands it is a preview, not their actual data. --> | |
| 1048 | <div class="module mb-3" style="border-color:rgba(124,58,237,0.30);background:linear-gradient(135deg, rgba(124,58,237,0.06) 0%, var(--col-surface-1) 65%)"> | |
| 1049 | <div class="module-head"> | |
| 1050 | <div class="left"> | |
| 1051 | <div class="module-icon" style="background:rgba(124,58,237,0.18);color:var(--col-violet-bright)"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg></div> | |
| 1052 | <div> | |
| 1053 | <div class="module-title">Welcome to the Optimization Suite</div> | |
| 1054 | <div class="module-sub">No experiments yet · here is how to launch your first one in under 2 minutes</div> | |
| 1055 | </div> | |
| 1056 | </div> | |
| 1057 | <a href="/optimization.cgi?new=1" class="btn btn-primary"> | |
| 1058 | <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg> | |
| 1059 | Start your first experiment | |
| 1060 | </a> | |
| 1061 | </div> | |
| 1062 | <div class="module-body"> | |
| 1063 | <p class="text-sm" style="color:var(--col-text-2);line-height:1.65;margin:0 0 16px"> | |
| 1064 | An experiment shows half your visitors one version of your storefront and the other half a different version, then measures which one converts buyers better. The Suite handles the bucketing, tracking, and the statistical math so you can focus on the ideas. | |
| 1065 | </p> | |
| 1066 | ||
| 1067 | <div class="dash-grid" style="grid-template-columns:repeat(4,1fr);gap:14px"> | |
| 1068 | ||
| 1069 | <div style="background:var(--col-surface-2);border:1px solid var(--col-border);border-radius:10px;padding:14px"> | |
| 1070 | <div style="display:flex;align-items:center;gap:8px;margin-bottom:8px"> | |
| 1071 | <span style="width:24px;height:24px;border-radius:50%;background:var(--col-accent);color:#fff;display:grid;place-items:center;font-weight:700;font-size:12px">1</span> | |
| 1072 | <div class="text-xs fw-700" style="color:var(--col-text);letter-spacing:0.5px;text-transform:uppercase">Pick a target</div> | |
| 1073 | </div> | |
| 1074 | <div class="text-xs" style="color:var(--col-text-2);line-height:1.55"> | |
| 1075 | Click <strong>+ New experiment</strong>. Choose what element you want to test — hero headline, hero image, CTA button label, a specific product tile, etc. | |
| 1076 | </div> | |
| 1077 | </div> | |
| 1078 | ||
| 1079 | <div style="background:var(--col-surface-2);border:1px solid var(--col-border);border-radius:10px;padding:14px"> | |
| 1080 | <div style="display:flex;align-items:center;gap:8px;margin-bottom:8px"> | |
| 1081 | <span style="width:24px;height:24px;border-radius:50%;background:var(--col-accent);color:#fff;display:grid;place-items:center;font-weight:700;font-size:12px">2</span> | |
| 1082 | <div class="text-xs fw-700" style="color:var(--col-text);letter-spacing:0.5px;text-transform:uppercase">Write 2 variants</div> | |
| 1083 | </div> | |
| 1084 | <div class="text-xs" style="color:var(--col-text-2);line-height:1.55"> | |
| 1085 | Variant A is the control (your current version). Variant B is the challenger — the new copy or image you want to try. For image surfaces, upload a file. | |
| 1086 | </div> | |
| 1087 | </div> | |
| 1088 | ||
| 1089 | <div style="background:var(--col-surface-2);border:1px solid var(--col-border);border-radius:10px;padding:14px"> | |
| 1090 | <div style="display:flex;align-items:center;gap:8px;margin-bottom:8px"> | |
| 1091 | <span style="width:24px;height:24px;border-radius:50%;background:var(--col-accent);color:#fff;display:grid;place-items:center;font-weight:700;font-size:12px">3</span> | |
| 1092 | <div class="text-xs fw-700" style="color:var(--col-text);letter-spacing:0.5px;text-transform:uppercase">Save + Start</div> | |
| 1093 | </div> | |
| 1094 | <div class="text-xs" style="color:var(--col-text-2);line-height:1.55"> | |
| 1095 | Save as a draft, review it, then hit <strong>Start →</strong> in the experiments table. From that moment, half your visitors see B and half see A. | |
| 1096 | </div> | |
| 1097 | </div> | |
| 1098 | ||
| 1099 | <div style="background:var(--col-surface-2);border:1px solid var(--col-border);border-radius:10px;padding:14px"> | |
| 1100 | <div style="display:flex;align-items:center;gap:8px;margin-bottom:8px"> | |
| 1101 | <span style="width:24px;height:24px;border-radius:50%;background:var(--col-success);color:#fff;display:grid;place-items:center;font-weight:700;font-size:12px">4</span> | |
| 1102 | <div class="text-xs fw-700" style="color:var(--col-text);letter-spacing:0.5px;text-transform:uppercase">Ship the winner</div> | |
| 1103 | </div> | |
| 1104 | <div class="text-xs" style="color:var(--col-text-2);line-height:1.55"> | |
| 1105 | Lift and Confidence columns fill in as visitors land. When confidence crosses 95%, promote the winning variant and start the next test. | |
| 1106 | </div> | |
| 1107 | </div> | |
| 1108 | ||
| 1109 | </div> | |
| 1110 | ||
| 1111 | <div style="margin-top:16px;padding:12px 14px;background:var(--col-surface-2);border-left:3px solid var(--col-violet-bright);border-radius:6px"> | |
| 1112 | <div class="text-xs" style="color:var(--col-text-2);line-height:1.55"> | |
| 1113 | <strong style="color:var(--col-text)">Want to see what this looks like with activity?</strong> | |
| 1114 | Click the <a href="/optimization.cgi?tutorial=1" class="text-brand fw-700">Sample Data View</a> button up top to walk through a populated version of every tab (A/B, Price tests, Pricing surveys, Promotions). | |
| 1115 | </div> | |
| 1116 | </div> | |
| 1117 | </div> | |
| 1118 | </div> | |
| 1119 | ||
| 1120 | [/if] | |
| 1121 | ||
| 1122 | <div class="banner danger mb-3"> | |
| 1123 | <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10Z"/></svg> | |
| 1124 | <div class="text-xs"><strong>Pricing transparency:</strong> WebSTLs supports global price tests only. We do not offer per‑user, per‑location, or per‑device pricing. Price changes during a buyer's session are blocked by default.</div> | |
| 1125 | </div> | |
| 1126 | ||
| 1127 | [if:$tutorial_mode] | |
| 1128 | <div class="tabs" data-tab-group> | |
| 1129 | <div class="tab active" data-target="ab">A/B + MVT</div> | |
| 1130 | <div class="tab" data-target="price">Price tests</div> | |
| 1131 | <div class="tab" data-target="vw">Pricing surveys</div> | |
| 1132 | <div class="tab" data-target="promo">Promotions</div> | |
| 1133 | </div> | |
| 1134 | ||
| 1135 | <!-- A/B + MVT --> | |
| 1136 | <div data-pane="ab"> | |
| 1137 | <div class="dash-grid mb-3"> | |
| 1138 | <div class="module"> | |
| 1139 | <div class="module-head"> | |
| 1140 | <div class="left"> | |
| 1141 | <div class="module-icon" style="background:rgba(124,58,237,0.15);color:var(--col-violet-bright)"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 12h-4l-3 9L9 3l-3 9H2"/></svg></div> | |
| 1142 | <div><div class="module-title">Hero variant · Storefront home</div><div class="module-sub">2,841 visitors · 7 days running · A/B</div></div> | |
| 1143 | </div> | |
| 1144 | <span class="pill success">Call winner</span> | |
| 1145 | </div> | |
| 1146 | <div class="module-body"> | |
| 1147 | <div class="dash-grid-2" style="gap:18px"> | |
| 1148 | <div style="background:var(--col-surface-2);border-radius:10px;border:1px solid var(--col-border);padding:14px"> | |
| 1149 | <div class="text-xs text-dim mb-2" style="letter-spacing:1.5px;text-transform:uppercase">Variant A · Control</div> | |
| 1150 | <div style="aspect-ratio:1.6;background:linear-gradient(140deg,#0d1224,#1a2540);border-radius:8px;display:grid;place-items:center;margin-bottom:10px"> | |
| 1151 | <div style="text-align:center"><div style="font-family:var(--font-display);font-size:18px;color:#fff">Premium Resin Minis</div><div class="text-xs text-dim">Pre‑supported · ready to print</div></div> | |
| 1152 | </div> | |
| 1153 | <div class="flex justify-between"><span class="text-xs text-dim">Conversion</span><span class="font-mono fw-700 text-sm">3.42%</span></div> | |
| 1154 | <div class="progress mt-1"><div class="progress-fill" style="width:48%"></div></div> | |
| 1155 | <div class="flex justify-between text-xs text-dim mt-1"><span>1,419 visitors</span><span>49 purchases</span></div> | |
| 1156 | </div> | |
| 1157 | <div style="background:rgba(34,197,94,0.06);border-radius:10px;border:1px solid rgba(34,197,94,0.3);padding:14px"> | |
| 1158 | <div class="text-xs mb-2" style="letter-spacing:1.5px;text-transform:uppercase;color:var(--col-success)">Variant B · Winning ★</div> | |
| 1159 | <div style="aspect-ratio:1.6;background:linear-gradient(140deg,#1a1142,#7c3aed);border-radius:8px;display:grid;place-items:center;margin-bottom:10px;position:relative"> | |
| 1160 | <div style="position:absolute;inset:0;background-image:radial-gradient(rgba(255,255,255,0.06) 1px, transparent 1px);background-size:20px 20px"></div> | |
| 1161 | <div style="text-align:center;position:relative"><div style="font-family:var(--font-display);font-size:18px;color:#fff">Print fast. Sell faster.</div><div class="text-xs" style="color:#cbd5e1">Battle‑ready in 6 hours of cure time</div></div> | |
| 1162 | </div> | |
| 1163 | <div class="flex justify-between"><span class="text-xs text-dim">Conversion</span><span class="font-mono fw-700 text-sm text-success">5.18%</span></div> | |
| 1164 | <div class="progress mt-1"><div class="progress-fill" style="width:74%;background:var(--grad-success)"></div></div> | |
| 1165 | <div class="flex justify-between text-xs text-dim mt-1"><span>1,422 visitors</span><span>74 purchases</span></div> | |
| 1166 | </div> | |
| 1167 | </div> | |
| 1168 | ||
| 1169 | <div class="divider"></div> | |
| 1170 | ||
| 1171 | <div class="dash-grid-3"> | |
| 1172 | <div> | |
| 1173 | <div class="text-xs text-dim" style="letter-spacing:1.5px;text-transform:uppercase">Lift</div> | |
| 1174 | <div style="font-family:var(--font-display);font-size:24px;font-weight:700;color:var(--col-success)">+51.5%</div> | |
| 1175 | <div class="text-xs text-dim">B over A</div> | |
| 1176 | </div> | |
| 1177 | <div> | |
| 1178 | <div class="text-xs text-dim" style="letter-spacing:1.5px;text-transform:uppercase">Confidence</div> | |
| 1179 | <div style="font-family:var(--font-display);font-size:24px;font-weight:700;color:#fff">96.4%</div> | |
| 1180 | <div class="text-xs text-success">Above 95% threshold</div> | |
| 1181 | </div> | |
| 1182 | <div> | |
| 1183 | <div class="text-xs text-dim" style="letter-spacing:1.5px;text-transform:uppercase">Projected lift /mo</div> | |
| 1184 | <div style="font-family:var(--font-display);font-size:24px;font-weight:700;color:#fff">+\$1,184</div> | |
| 1185 | <div class="text-xs text-dim">est. at current traffic</div> | |
| 1186 | </div> | |
| 1187 | </div> | |
| 1188 | ||
| 1189 | <div class="flex gap-2 mt-3"> | |
| 1190 | <button class="btn btn-success">✓ Promote variant B</button> | |
| 1191 | <button class="btn btn-secondary">Continue running</button> | |
| 1192 | <button class="btn btn-ghost">Archive</button> | |
| 1193 | </div> | |
| 1194 | </div> | |
| 1195 | </div> | |
| 1196 | ||
| 1197 | <div class="module"> | |
| 1198 | <div class="module-head"><div class="left"><div class="module-icon" style="background:rgba(124,58,237,0.15);color:var(--col-violet-bright)"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg></div><div><div class="module-title">Quick experiment ideas</div><div class="module-sub">From peer winning patterns</div></div></div></div> | |
| 1199 | <div class="module-body" style="display:flex;flex-direction:column;gap:10px"> | |
| 1200 | <div style="padding:12px;border-radius:10px;background:var(--col-surface-2);border:1px solid var(--col-border)"> | |
| 1201 | <div class="text-sm fw-600" style="color:#fff">Add a "Free model of the week" CTA above the fold</div> | |
| 1202 | <div class="text-xs text-dim mt-1">Peers in your segment see +14% email captures</div> | |
| 1203 | <button class="btn btn-secondary btn-sm mt-2">Set up test →</button> | |
| 1204 | </div> | |
| 1205 | <div style="padding:12px;border-radius:10px;background:var(--col-surface-2);border:1px solid var(--col-border)"> | |
| 1206 | <div class="text-sm fw-600" style="color:#fff">Test 4‑col grid vs 3‑col on catalog</div> | |
| 1207 | <div class="text-xs text-dim mt-1">Higher density correlates with +6% AOV at \$5–15 price points</div> | |
| 1208 | <button class="btn btn-secondary btn-sm mt-2">Set up test →</button> | |
| 1209 | </div> | |
| 1210 | <div style="padding:12px;border-radius:10px;background:var(--col-surface-2);border:1px solid var(--col-border)"> | |
| 1211 | <div class="text-sm fw-600" style="color:#fff">Bundle suggestion on PDP</div> | |
| 1212 | <div class="text-xs text-dim mt-1">Show "Often bought with" — peer median +9% UPT</div> | |
| 1213 | <button class="btn btn-secondary btn-sm mt-2">Set up test →</button> | |
| 1214 | </div> | |
| 1215 | </div> | |
| 1216 | </div> | |
| 1217 | </div> | |
| 1218 | ||
| 1219 | <!-- Test history table --> | |
| 1220 | <div class="module"> | |
| 1221 | <div class="module-head"><div class="left"><div class="module-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg></div><div><div class="module-title">Experiment history</div></div></div></div> | |
| 1222 | <div class="table-wrap" style="border:none;border-radius:0"> | |
| 1223 | <table class="table"> | |
| 1224 | <thead><tr><th>Name</th><th>Type</th><th>Status</th><th class="num">Lift</th><th class="num">Confidence</th><th class="num">Ended</th></tr></thead> | |
| 1225 | <tbody> | |
| 1226 | <tr><td><strong>Hero variant</strong></td><td>A/B</td><td><span class="pill success">Winning</span></td><td class="num text-success">+51.5%</td><td class="num">96.4%</td><td class="num text-dim">running</td></tr> | |
| 1227 | <tr><td><strong>Bundle CTA copy</strong></td><td>A/B</td><td><span class="pill brand">Running</span></td><td class="num text-secondary">+8.4%</td><td class="num">71%</td><td class="num text-dim">running</td></tr> | |
| 1228 | <tr><td><strong>Catalog grid layout</strong></td><td>MVT</td><td><span class="pill">Complete</span></td><td class="num text-success">+12.0%</td><td class="num">98.1%</td><td class="num text-dim">12d ago</td></tr> | |
| 1229 | <tr><td><strong>Free shipping banner</strong></td><td>A/B</td><td><span class="pill">Complete</span></td><td class="num text-danger">−4.2%</td><td class="num">92%</td><td class="num text-dim">28d ago</td></tr> | |
| 1230 | <tr><td><strong>Pricing card design</strong></td><td>A/B</td><td><span class="pill">Aborted</span></td><td class="num">—</td><td class="num">—</td><td class="num text-dim">42d ago</td></tr> | |
| 1231 | </tbody> | |
| 1232 | </table> | |
| 1233 | </div> | |
| 1234 | </div> | |
| 1235 | </div> | |
| 1236 | ||
| 1237 | <!-- Price tests --> | |
| 1238 | <div data-pane="price" class="hide"> | |
| 1239 | <div class="module"> | |
| 1240 | <div class="module-head"><div class="left"><div class="module-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="1" x2="12" y2="23"/><path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/></svg></div><div><div class="module-title">Active price tests</div><div class="module-sub">Global · creator‑defined rules · audit logged</div></div></div></div> | |
| 1241 | <div class="module-body" style="display:flex;flex-direction:column;gap:14px"> | |
| 1242 | <div style="padding:16px;border-radius:12px;background:var(--col-surface-2);border:1px solid var(--col-border)"> | |
| 1243 | <div class="flex justify-between items-center mb-2"> | |
| 1244 | <div> | |
| 1245 | <div class="text-sm fw-600" style="color:#fff">Dragon Lord MK‑II</div> | |
| 1246 | <div class="text-xs text-dim">Range \$9–\$15 · Step \$1 · Adjusts every 7d if conv. moves > 5%</div> | |
| 1247 | </div> | |
| 1248 | <span class="pill brand">Auto‑adjusting</span> | |
| 1249 | </div> | |
| 1250 | <div style="height:8px;background:var(--col-surface-3);border-radius:999px;position:relative;overflow:hidden;margin:14px 0"> | |
| 1251 | <div style="position:absolute;top:0;left:30%;right:25%;bottom:0;background:var(--grad-brand);border-radius:999px"></div> | |
| 1252 | <div style="position:absolute;top:-4px;left:50%;width:16px;height:16px;border-radius:50%;background:#fff;border:3px solid var(--col-accent);transform:translateX(-50%)"></div> | |
| 1253 | </div> | |
| 1254 | <div class="flex justify-between text-xs"> | |
| 1255 | <span class="text-dim">\$9.00</span> | |
| 1256 | <span class="text-brand fw-700">Currently \$12.00</span> | |
| 1257 | <span class="text-dim">\$15.00</span> | |
| 1258 | </div> | |
| 1259 | <div class="text-xs text-dim mt-2">Last adjusted 4d ago: \$11 → \$12 after +8% conv. lift held</div> | |
| 1260 | </div> | |
| 1261 | ||
| 1262 | <div style="padding:16px;border-radius:12px;background:var(--col-surface-2);border:1px solid var(--col-border)"> | |
| 1263 | <div class="flex justify-between items-center mb-2"> | |
| 1264 | <div> | |
| 1265 | <div class="text-sm fw-600" style="color:#fff">Bridge Terrain Pack</div> | |
| 1266 | <div class="text-xs text-dim">Range \$15–\$22 · Adjustment paused (low traffic)</div> | |
| 1267 | </div> | |
| 1268 | <span class="pill warning">Paused</span> | |
| 1269 | </div> | |
| 1270 | <div style="height:8px;background:var(--col-surface-3);border-radius:999px;position:relative;overflow:hidden;margin:14px 0"> | |
| 1271 | <div style="position:absolute;top:0;left:0;width:42%;height:100%;background:var(--grad-brand);border-radius:999px;opacity:0.5"></div> | |
| 1272 | <div style="position:absolute;top:-4px;left:42%;width:16px;height:16px;border-radius:50%;background:#fff;border:3px solid var(--col-accent);transform:translateX(-50%);opacity:0.7"></div> | |
| 1273 | </div> | |
| 1274 | <div class="flex justify-between text-xs"> | |
| 1275 | <span class="text-dim">\$15.00</span> | |
| 1276 | <span class="text-secondary fw-700">Currently \$18.00</span> | |
| 1277 | <span class="text-dim">\$22.00</span> | |
| 1278 | </div> | |
| 1279 | <div class="text-xs text-dim mt-2">Resumes when traffic ≥ 50 visitors/day for 5 consecutive days</div> | |
| 1280 | </div> | |
| 1281 | </div> | |
| 1282 | </div> | |
| 1283 | </div> | |
| 1284 | ||
| 1285 | <!-- Pricing surveys (Van Westendorp methodology under the hood) --> | |
| 1286 | <div data-pane="vw" class="hide"> | |
| 1287 | <div class="dash-grid"> | |
| 1288 | <div class="module"> | |
| 1289 | <div class="module-head"><div class="left"><div class="module-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 11H5a2 2 0 0 0-2 2v7a2 2 0 0 0 2 2h4M9 11V5a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2v6m-6 0h6m4 0h2a2 2 0 0 1 2 2v7a2 2 0 0 1-2 2h-2"/></svg></div><div><div class="module-title">Survey results · Dragon Lord MK‑II</div><div class="module-sub">847 responses · sent to email + storefront</div></div></div></div> | |
| 1290 | <div class="module-body"> | |
| 1291 | <div style="background:var(--col-surface-2);border-radius:12px;padding:18px;border:1px solid var(--col-border)"> | |
| 1292 | <div class="text-sm fw-600 mb-2" style="color:#fff">Acceptable price range (PSM intersection)</div> | |
| 1293 | <div style="height:12px;background:var(--col-surface-3);border-radius:999px;position:relative;overflow:hidden"> | |
| 1294 | <div style="position:absolute;top:0;left:30%;right:38%;bottom:0;background:var(--grad-success);border-radius:999px"></div> | |
| 1295 | </div> | |
| 1296 | <div class="flex justify-between text-xs text-dim mt-1"><span>\$5</span><span>\$10</span><span>\$15</span><span>\$20</span></div> | |
| 1297 | <div class="text-center mt-2" style="font-family:var(--font-display);font-size:22px;font-weight:700;color:var(--col-success)">\$11.50 – \$14.00</div> | |
| 1298 | <div class="text-xs text-dim text-center">Optimal Price Point: \$12.20 · Indifference Price Point: \$12.80</div> | |
| 1299 | </div> | |
| 1300 | ||
| 1301 | <div class="dash-grid-2 mt-3" style="gap:12px"> | |
| 1302 | <div style="padding:12px;border-radius:10px;background:var(--col-surface-2);border:1px solid var(--col-border)"> | |
| 1303 | <div class="text-xs text-dim">Too cheap (quality concern)</div> | |
| 1304 | <div class="font-mono fw-700 text-md" style="color:#fff">below \$7</div> | |
| 1305 | </div> | |
| 1306 | <div style="padding:12px;border-radius:10px;background:var(--col-surface-2);border:1px solid var(--col-border)"> | |
| 1307 | <div class="text-xs text-dim">Too expensive</div> | |
| 1308 | <div class="font-mono fw-700 text-md" style="color:#fff">above \$16</div> | |
| 1309 | </div> | |
| 1310 | </div> | |
| 1311 | </div> | |
| 1312 | </div> | |
| 1313 | ||
| 1314 | <div class="module"> | |
| 1315 | <div class="module-head"><div class="left"><div class="module-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg></div><div><div class="module-title">Run a new survey</div></div></div></div> | |
| 1316 | <div class="module-body"> | |
| 1317 | <div class="form-group"> | |
| 1318 | <label class="form-label">Product</label> | |
| 1319 | <select class="select"><option>Dragon Lord MK‑II</option><option>Skirmisher Set Vol.4</option><option>Bridge Terrain Pack</option></select> | |
| 1320 | </div> | |
| 1321 | <div class="form-group"> | |
| 1322 | <label class="form-label">Send to</label> | |
| 1323 | <label class="checkbox mb-1"><input type="checkbox" checked /> Email subscribers (4,218)</label> | |
| 1324 | <label class="checkbox mb-1"><input type="checkbox" checked /> Storefront visitors (post‑checkout)</label> | |
| 1325 | <label class="checkbox"><input type="checkbox" /> Patreon members</label> | |
| 1326 | </div> | |
| 1327 | <div class="form-group"> | |
| 1328 | <label class="form-label">Survey type</label> | |
| 1329 | <select class="select"><option>Price sensitivity / Van Westendorp (recommended)</option><option>Willingness to pay / Gabor‑Granger</option><option>Custom 5‑question</option></select> | |
| 1330 | </div> | |
| 1331 | <button class="btn btn-primary btn-block">Launch survey</button> | |
| 1332 | </div> | |
| 1333 | </div> | |
| 1334 | </div> | |
| 1335 | </div> | |
| 1336 | ||
| 1337 | <!-- Promotions --> | |
| 1338 | <div data-pane="promo" class="hide"> | |
| 1339 | <div class="module"> | |
| 1340 | <div class="module-head"> | |
| 1341 | <div class="left"><div class="module-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="m20.59 13.41-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82Z"/><line x1="7" y1="7" x2="7.01" y2="7"/></svg></div><div><div class="module-title">Active promotions</div><div class="module-sub">Manual + auto‑triggered when sales velocity drops</div></div></div> | |
| 1342 | <button class="btn btn-secondary btn-sm">New promotion</button> | |
| 1343 | </div> | |
| 1344 | <div class="table-wrap" style="border:none;border-radius:0"> | |
| 1345 | <table class="table"> | |
| 1346 | <thead><tr><th>Name</th><th>Trigger</th><th>Discount</th><th>Uses</th><th>Status</th><th>Ends</th></tr></thead> | |
| 1347 | <tbody> | |
| 1348 | <tr><td><strong>SUMMER10</strong><div class="text-xs text-dim">Code · whole store</div></td><td>Manual</td><td>10% off</td><td>184 / 500</td><td><span class="pill success">Active</span></td><td class="text-dim">in 12d</td></tr> | |
| 1349 | <tr><td><strong>Bridge Terrain rescue</strong><div class="text-xs text-dim">Auto · single product</div></td><td>Velocity <3 sales/wk</td><td>15% off</td><td>22 / ∞</td><td><span class="pill brand">Auto‑triggered</span></td><td class="text-dim">7d after trigger</td></tr> | |
| 1350 | <tr><td><strong>Newsletter signup</strong><div class="text-xs text-dim">Code · gated</div></td><td>Email opt‑in</td><td>\$5 off first order</td><td>412 / ∞</td><td><span class="pill success">Active</span></td><td class="text-dim">never</td></tr> | |
| 1351 | <tr><td><strong>BUNDLE3</strong><div class="text-xs text-dim">Auto · cart</div></td><td>Cart ≥ 3 items</td><td>BOGO 50%</td><td>78 / ∞</td><td><span class="pill success">Active</span></td><td class="text-dim">30d after sale</td></tr> | |
| 1352 | </tbody> | |
| 1353 | </table> | |
| 1354 | </div> | |
| 1355 | </div> | |
| 1356 | </div> | |
| 1357 | [/if] | |
| 1358 | ||
| 1359 | [if:!$tutorial_mode] | |
| 1360 | <!-- Real-mode footer: pointers to the other optimization surfaces | |
| 1361 | whose full UIs live on their own pages. Replaces the hardcoded | |
| 1362 | sample tabs that used to render here. --> | |
| 1363 | <div class="dash-grid mb-3"> | |
| 1364 | <div class="module"> | |
| 1365 | <div class="module-head"> | |
| 1366 | <div class="left"> | |
| 1367 | <div class="module-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20.59 13.41 13.42 20.58a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"/><line x1="7" y1="7" x2="7.01" y2="7"/></svg></div> | |
| 1368 | <div> | |
| 1369 | <div class="module-title">Promotions</div> | |
| 1370 | <div class="module-sub">Coupons, sales, bundles · live UI</div> | |
| 1371 | </div> | |
| 1372 | </div> | |
| 1373 | <a href="/promotions.cgi" class="btn btn-secondary btn-sm">Open promotions →</a> | |
| 1374 | </div> | |
| 1375 | <div class="module-body"> | |
| 1376 | <div class="text-xs text-dim" style="line-height:1.55">Create coupon codes, time-limited sales, bundles and shareable promo links on the Promotions page. Caps by redemptions, by date, or by per-buyer limit.</div> | |
| 1377 | </div> | |
| 1378 | </div> | |
| 1379 | <div class="module"> | |
| 1380 | <div class="module-head"> | |
| 1381 | <div class="left"> | |
| 1382 | <div class="module-icon" style="background:rgba(245,158,11,0.15);color:#fbbf24"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="1" x2="12" y2="23"/><path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/></svg></div> | |
| 1383 | <div> | |
| 1384 | <div class="module-title">Price tests & surveys</div> | |
| 1385 | <div class="module-sub">Bandit-style price tests + Van Westendorp Price Sensitivity Meter surveys</div> | |
| 1386 | </div> | |
| 1387 | </div> | |
| 1388 | </div> | |
| 1389 | <div class="module-body"> | |
| 1390 | <div class="text-xs text-dim" style="line-height:1.55">Use <strong>price tests</strong> to explore a price range automatically (per-model bandit; current round is rule-based, full Thompson sampling lands with the price-test worker). Use <strong>surveys</strong> to ask buyers directly via Van Westendorp's four-question Price Sensitivity Meter and see the four canonical intersection points (PMC, PME, OPP, IPP).</div> | |
| 1391 | </div> | |
| 1392 | </div> | |
| 1393 | </div> | |
| 1394 | ||
| 1395 | [if:$has_opt_flash] | |
| 1396 | <div class="banner $opt_flash_kind mb-3"> | |
| 1397 | <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/></svg> | |
| 1398 | <div class="text-sm fw-600">$opt_flash_msg</div> | |
| 1399 | </div> | |
| 1400 | [/if] | |
| 1401 | ||
| 1402 | [if:$px_not_ready] | |
| 1403 | <div class="banner warn mb-3" id="price"> | |
| 1404 | <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/></svg> | |
| 1405 | <div class="text-sm fw-600">Price tests + surveys require a schema migration. Run the migration in installation_instructions.html (section "Pricing experiments") and reload this page.</div> | |
| 1406 | </div> | |
| 1407 | [/if] | |
| 1408 | ||
| 1409 | [if:$px_ready] | |
| 1410 | <!-- =========================== PRICE TESTS =========================== --> | |
| 1411 | <h2 id="price" style="font-size:18px;margin:24px 0 14px;color:var(--col-text)">Price tests</h2> | |
| 1412 | <div class="module mb-3"> | |
| 1413 | <div class="module-body"> | |
| 1414 | [if:$has_price_tests] | |
| 1415 | <table class="bil-itable" style="width:100%;margin-bottom:14px"> | |
| 1416 | <thead><tr> | |
| 1417 | <th>Model</th><th class="num">Current</th><th class="num">Range</th><th class="num">Step</th><th>Status</th><th></th> | |
| 1418 | </tr></thead> | |
| 1419 | <tbody> | |
| 1420 | [loop:@price_tests] | |
| 1421 | <tr> | |
| 1422 | <td>$loop1.model_title</td> | |
| 1423 | <td class="num">$loop1.current_display</td> | |
| 1424 | <td class="num">$loop1.min_display — $loop1.max_display</td> | |
| 1425 | <td class="num">$loop1.step_display</td> | |
| 1426 | <td> | |
| 1427 | [if:$loop1.is_draft]<span class="pill open">Draft</span>[/if] | |
| 1428 | [if:$loop1.is_running]<span class="pill paid">Running</span>[/if] | |
| 1429 | [if:$loop1.is_paused]<span class="pill failed">Paused</span>[/if] | |
| 1430 | [if:$loop1.is_complete]<span class="pill refunded">Complete</span>[/if] | |
| 1431 | </td> | |
| 1432 | <td style="text-align:right"> | |
| 1433 | [if:$loop1.is_draft] | |
| 1434 | <form method="POST" action="/optimization_action.cgi" style="display:inline"> | |
| 1435 | <input type="hidden" name="act" value="set_test_status"> | |
| 1436 | <input type="hidden" name="test_id" value="$loop1.id"> | |
| 1437 | <input type="hidden" name="status" value="running"> | |
| 1438 | <button type="submit" class="btn btn-primary btn-sm" style="font-size:11px;padding:4px 9px">Start</button> | |
| 1439 | </form> | |
| 1440 | [/if] | |
| 1441 | [if:$loop1.is_running] | |
| 1442 | <form method="POST" action="/optimization_action.cgi" style="display:inline"> | |
| 1443 | <input type="hidden" name="act" value="set_test_status"> | |
| 1444 | <input type="hidden" name="test_id" value="$loop1.id"> | |
| 1445 | <input type="hidden" name="status" value="paused"> | |
| 1446 | <button type="submit" class="btn btn-secondary btn-sm" style="font-size:11px;padding:4px 9px">Pause</button> | |
| 1447 | </form> | |
| 1448 | [/if] | |
| 1449 | [if:$loop1.is_paused] | |
| 1450 | <form method="POST" action="/optimization_action.cgi" style="display:inline"> | |
| 1451 | <input type="hidden" name="act" value="set_test_status"> | |
| 1452 | <input type="hidden" name="test_id" value="$loop1.id"> | |
| 1453 | <input type="hidden" name="status" value="running"> | |
| 1454 | <button type="submit" class="btn btn-primary btn-sm" style="font-size:11px;padding:4px 9px">Resume</button> | |
| 1455 | </form> | |
| 1456 | [/if] | |
| 1457 | <form method="POST" action="/optimization_action.cgi" style="display:inline" | |
| 1458 | data-confirm-title="Delete this price test?" | |
| 1459 | data-confirm-message="The test and its full history will be removed. The model returns to its base price." | |
| 1460 | data-confirm-label="Delete test" | |
| 1461 | data-confirm-style="danger"> | |
| 1462 | <input type="hidden" name="act" value="delete_price_test"> | |
| 1463 | <input type="hidden" name="test_id" value="$loop1.id"> | |
| 1464 | <button type="submit" class="btn btn-danger btn-sm" style="font-size:11px;padding:4px 9px">Delete</button> | |
| 1465 | </form> | |
| 1466 | </td> | |
| 1467 | </tr> | |
| 1468 | [/loop] | |
| 1469 | </tbody> | |
| 1470 | </table> | |
| 1471 | [/if] | |
| 1472 | [if:!$has_price_tests] | |
| 1473 | <div class="text-sm text-dim" style="padding:8px 0">No price tests yet. Create one below to start exploring a price range automatically.</div> | |
| 1474 | [/if] | |
| 1475 | ||
| 1476 | <details> | |
| 1477 | <summary style="cursor:pointer;font-weight:600;color:var(--col-accent);user-select:none">+ Create a price test</summary> | |
| 1478 | <form method="POST" action="/optimization_action.cgi" style="margin-top:14px;padding:14px;border:1px solid var(--col-border);border-radius:8px;background:rgba(255,255,255,0.02);display:flex;flex-direction:column;gap:10px"> | |
| 1479 | <input type="hidden" name="act" value="create_price_test"> | |
| 1480 | <label class="text-xs text-dim"> | |
| 1481 | Model | |
| 1482 | <select class="input" name="model_id" required style="margin-top:4px"> | |
| 1483 | <option value="">Pick a model...</option> | |
| 1484 | [loop:@user_models] | |
| 1485 | <option value="$loop1.id">$loop1.title</option> | |
| 1486 | [/loop] | |
| 1487 | </select> | |
| 1488 | </label> | |
| 1489 | <div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:10px"> | |
| 1490 | <label class="text-xs text-dim"> | |
| 1491 | Min price (USD) | |
| 1492 | <input class="input" type="number" name="min_price_dollars" min="0.01" step="0.01" required style="margin-top:4px"> | |
| 1493 | </label> | |
| 1494 | <label class="text-xs text-dim"> | |
| 1495 | Max price (USD) | |
| 1496 | <input class="input" type="number" name="max_price_dollars" min="0.01" step="0.01" required style="margin-top:4px"> | |
| 1497 | </label> | |
| 1498 | <label class="text-xs text-dim"> | |
| 1499 | Step (USD) | |
| 1500 | <input class="input" type="number" name="step_dollars" min="0.50" step="0.50" value="1.00" style="margin-top:4px"> | |
| 1501 | </label> | |
| 1502 | </div> | |
| 1503 | <div class="text-xs text-dim">Created in draft status. Click Start when you are ready. The runner adjusts price within the range based on conversion telemetry.</div> | |
| 1504 | <div> | |
| 1505 | <button type="submit" class="btn btn-primary btn-sm">Create test</button> | |
| 1506 | </div> | |
| 1507 | </form> | |
| 1508 | </details> | |
| 1509 | </div> | |
| 1510 | </div> | |
| 1511 | ||
| 1512 | <!-- =========================== SURVEYS =========================== --> | |
| 1513 | <h2 id="survey" style="font-size:18px;margin:24px 0 14px;color:var(--col-text)">Van Westendorp surveys</h2> | |
| 1514 | <div class="module mb-3"> | |
| 1515 | <div class="module-body"> | |
| 1516 | [if:$has_surveys] | |
| 1517 | <table class="bil-itable" style="width:100%;margin-bottom:14px"> | |
| 1518 | <thead><tr> | |
| 1519 | <th>Survey</th><th>Model</th><th class="num">Responses</th><th>Status</th><th>Public URL</th><th></th> | |
| 1520 | </tr></thead> | |
| 1521 | <tbody> | |
| 1522 | [loop:@surveys] | |
| 1523 | <tr> | |
| 1524 | <td>$loop1.title</td> | |
| 1525 | <td class="text-xs">$loop1.model_title</td> | |
| 1526 | <td class="num">$loop1.response_count</td> | |
| 1527 | <td> | |
| 1528 | [if:$loop1.is_open]<span class="pill paid">Open</span>[/if] | |
| 1529 | [if:$loop1.is_closed]<span class="pill void">Closed</span>[/if] | |
| 1530 | </td> | |
| 1531 | <td class="text-xs" style="word-break:break-all;font-family:var(--font-mono,monospace);color:var(--col-text-2);max-width:300px"> | |
| 1532 | <a href="$loop1.public_url" target="_blank" rel="noopener" class="text-brand">$loop1.public_url</a> | |
| 1533 | </td> | |
| 1534 | <td style="text-align:right;white-space:nowrap"> | |
| 1535 | <a href="$loop1.results_href" class="btn btn-secondary btn-sm" style="font-size:11px;padding:4px 9px">Results</a> | |
| 1536 | [if:$loop1.is_open] | |
| 1537 | <form method="POST" action="/optimization_action.cgi" style="display:inline" | |
| 1538 | data-confirm-title="Close this survey?" | |
| 1539 | data-confirm-message="Existing responses are kept, but the public URL stops accepting new submissions." | |
| 1540 | data-confirm-label="Close survey" | |
| 1541 | data-confirm-style="warning"> | |
| 1542 | <input type="hidden" name="act" value="close_survey"> | |
| 1543 | <input type="hidden" name="survey_id" value="$loop1.id"> | |
| 1544 | <button type="submit" class="btn btn-secondary btn-sm" style="font-size:11px;padding:4px 9px">Close</button> | |
| 1545 | </form> | |
| 1546 | [/if] | |
| 1547 | <form method="POST" action="/optimization_action.cgi" style="display:inline" | |
| 1548 | data-confirm-title="Delete this survey?" | |
| 1549 | data-confirm-message="The survey and ALL of its responses are removed. This cannot be undone." | |
| 1550 | data-confirm-label="Delete survey" | |
| 1551 | data-confirm-style="danger"> | |
| 1552 | <input type="hidden" name="act" value="delete_survey"> | |
| 1553 | <input type="hidden" name="survey_id" value="$loop1.id"> | |
| 1554 | <button type="submit" class="btn btn-danger btn-sm" style="font-size:11px;padding:4px 9px">Delete</button> | |
| 1555 | </form> | |
| 1556 | </td> | |
| 1557 | </tr> | |
| 1558 | [/loop] | |
| 1559 | </tbody> | |
| 1560 | </table> | |
| 1561 | [/if] | |
| 1562 | [if:!$has_surveys] | |
| 1563 | <div class="text-sm text-dim" style="padding:8px 0">No surveys yet. Van Westendorp's four-question Price Sensitivity Meter is the fastest way to find a defensible price band for a new model.</div> | |
| 1564 | [/if] | |
| 1565 | ||
| 1566 | [if:$show_vw_results] | |
| 1567 | <div style="margin:14px 0 18px;padding:14px;border:1px solid var(--col-border);border-radius:8px;background:rgba(124,58,237,0.05)"> | |
| 1568 | <div class="text-xs text-dim" style="letter-spacing:1.5px;text-transform:uppercase;margin-bottom:8px">Results · $vw_n responses</div> | |
| 1569 | <div style="display:grid;grid-template-columns:repeat(4, minmax(0, 1fr));gap:12px"> | |
| 1570 | <div> | |
| 1571 | <div class="text-xs text-dim">PMC (point of marginal cheapness)</div> | |
| 1572 | <div style="font-size:20px;font-weight:700;color:#fff;font-family:var(--font-mono,monospace);margin-top:4px">$vw_pmc</div> | |
| 1573 | <div class="text-xs text-dim" style="margin-top:2px">below this = too cheap doubts</div> | |
| 1574 | </div> | |
| 1575 | <div> | |
| 1576 | <div class="text-xs text-dim">OPP (optimum price)</div> | |
| 1577 | <div style="font-size:20px;font-weight:700;color:#4ade80;font-family:var(--font-mono,monospace);margin-top:4px">$vw_opp</div> | |
| 1578 | <div class="text-xs text-dim" style="margin-top:2px">cheap = too-expensive crossover</div> | |
| 1579 | </div> | |
| 1580 | <div> | |
| 1581 | <div class="text-xs text-dim">IPP (indifference price)</div> | |
| 1582 | <div style="font-size:20px;font-weight:700;color:#a78bfa;font-family:var(--font-mono,monospace);margin-top:4px">$vw_ipp</div> | |
| 1583 | <div class="text-xs text-dim" style="margin-top:2px">bargain = expensive crossover</div> | |
| 1584 | </div> | |
| 1585 | <div> | |
| 1586 | <div class="text-xs text-dim">PME (point of marginal expensiveness)</div> | |
| 1587 | <div style="font-size:20px;font-weight:700;color:#fff;font-family:var(--font-mono,monospace);margin-top:4px">$vw_pme</div> | |
| 1588 | <div class="text-xs text-dim" style="margin-top:2px">above this = won't consider</div> | |
| 1589 | </div> | |
| 1590 | </div> | |
| 1591 | <div class="text-xs text-dim" style="margin-top:12px;line-height:1.55">The acceptable price band is roughly PMC to PME. OPP is the most defensible single price; IPP is where the typical buyer's "bargain" perception meets their "expensive" perception — many sellers anchor list price at OPP and offer discounts down to IPP.</div> | |
| 1592 | </div> | |
| 1593 | [/if] | |
| 1594 | ||
| 1595 | <details> | |
| 1596 | <summary style="cursor:pointer;font-weight:600;color:var(--col-accent);user-select:none">+ Create a survey</summary> | |
| 1597 | <form method="POST" action="/optimization_action.cgi" style="margin-top:14px;padding:14px;border:1px solid var(--col-border);border-radius:8px;background:rgba(255,255,255,0.02);display:flex;flex-direction:column;gap:10px"> | |
| 1598 | <input type="hidden" name="act" value="create_survey"> | |
| 1599 | <input type="hidden" name="survey_type" value="van_westendorp"> | |
| 1600 | <label class="text-xs text-dim"> | |
| 1601 | Model | |
| 1602 | <select class="input" name="model_id" required style="margin-top:4px"> | |
| 1603 | <option value="">Pick a model...</option> | |
| 1604 | [loop:@user_models] | |
| 1605 | <option value="$loop1.id">$loop1.title</option> | |
| 1606 | [/loop] | |
| 1607 | </select> | |
| 1608 | </label> | |
| 1609 | <label class="text-xs text-dim"> | |
| 1610 | Survey title (shown to respondents) | |
| 1611 | <input class="input" type="text" name="title" maxlength="191" placeholder="What would you pay for [model]?" required style="margin-top:4px"> | |
| 1612 | </label> | |
| 1613 | <div class="text-xs text-dim">The four standard Van Westendorp questions are pre-filled. After creating, you will get a public URL to share with buyers (email, storefront link, social).</div> | |
| 1614 | <div> | |
| 1615 | <button type="submit" class="btn btn-primary btn-sm">Create survey</button> | |
| 1616 | </div> | |
| 1617 | </form> | |
| 1618 | </details> | |
| 1619 | </div> | |
| 1620 | </div> | |
| 1621 | [/if] | |
| 1622 | ||
| 1623 | [/if] | |
| 1624 | [/if] | |
| 1625 | ||
| 1626 | <!-- Confirm popups for any form with data-confirm-title are handled | |
| 1627 | globally by /assets/javascript/confirm.js (loaded in the wrapper). --> | |
| 1628 | ||
| 1629 | <div class="opt-foot-spacer" data-end="optimization"> </div> |