Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 55 additions & 21 deletions app.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import posthog from "posthog-js";
import {
closeModal,
hasActiveModal,
initializeModalShells,
openModal,
setModalPending,
Expand Down Expand Up @@ -5550,7 +5549,7 @@ async function runThinkingPipeline() {
}
if (!isCurrentPipelineRun(runId)) return;

if (!(await canRunPipeline())) return;
if (!(await canRunPipeline(userInput))) return;
if (!isCurrentPipelineRun(runId)) return;

await ensureIdentityReady();
Expand Down Expand Up @@ -6547,7 +6546,7 @@ function getRunLimit() {
return TIER_LIMITS[subscriptionState.tier] ?? TIER_LIMITS.free
}

async function canRunPipeline() {
async function canRunPipeline(prompt) {
if (authState.isVerified && (!isSubscriptionResolved() || isSubscriptionLoading())) {
await fetchSubscription({ force: true })
updateSubscriptionUI()
Expand All @@ -6561,7 +6560,7 @@ async function canRunPipeline() {
const { count } = getUsage()
const limit = getRunLimit()
if (count >= limit) {
showPaywallExhausted(count, limit, { openModal: true })
showPaywallExhausted(count, limit, { prompt })
return false
}
const warningThreshold = Math.floor(limit * 0.8)
Expand Down Expand Up @@ -6618,8 +6617,17 @@ function showPaywallExhausted(count, limit, options = {}) {
const signInBtn = document.getElementById('paywall-signin-btn')
if (signInBtn) signInBtn.classList.toggle('hidden', authState.isVerified)

// The paywall answers a submitted prompt: the recap shows it beside the
// upgrade path, and Edit prompt returns to it rather than an earlier run's.
if (options.prompt != null) {
pipelineState.submittedPrompt = options.prompt
const promptEl = document.getElementById('pipeline-submitted-prompt')
if (promptEl) promptEl.textContent = options.prompt
}

// Only the screen: its View Plans button opens pricing when the user
// chooses, rather than a dialog stacked over it.
paywall.classList.remove('hidden')
if (options.openModal) openPricingModal()
}

function getEffectiveModel(selectedModel) {
Expand Down Expand Up @@ -6880,20 +6888,24 @@ function renderAccountOverview() {
function updateUsageDisplay() {
renderUsageSurfaces()

if (authState.isVerified && (isSubscriptionLoading() || !isSubscriptionResolved())) {
hidePaywallExhausted()
return
}

const { count } = getUsage()
const limit = getRunLimit()
// Auto-surfacing the paywall swaps the stage and closes the walkthrough, so
// defer it while a dialog is open; the next usage update or run attempt
// still gates on the limit.
if (count >= limit && !pipelineState.isRunning && !hasActiveModal()) {
showPaywallExhausted(count, limit)
} else {
// The paywall answers a generation attempt — canRunPipeline shows it when
// a prompt is submitted at the limit — so a usage update never surfaces it
// on its own: an out-of-runs user still lands on the composer and writes
// their prompt first. An update only clears it once the limit no longer
// applies (an upgrade, a sign-in, a new month) or while the subscription
// cannot confirm it.
const subscriptionUncertain = authState.isVerified && (isSubscriptionLoading() || !isSubscriptionResolved())
if (subscriptionUncertain || getUsage().count < getRunLimit()) {
const paywallEl = document.getElementById('paywall-exhausted')
const paywallWasVisible = !!paywallEl && !paywallEl.classList.contains('hidden')
hidePaywallExhausted()
// A correction that lands while the paywall is up (a stale stored count
// replaced by a smaller authoritative one) frees runs: with nothing else
// live in the stage, hand the composer back with its draft intact rather
// than leave an empty panel.
if (paywallWasVisible && !generationStageHasLivePanel()) {
setGenerationStageVisible(false)
}
}
}

Expand Down Expand Up @@ -8984,6 +8996,9 @@ function editPipelinePrompt() {
hidePipelineFailure();
const progress = document.getElementById("pipeline-progress");
if (progress) progress.classList.remove("visible");
// Editing abandons the gated submit as well: the paywall it opened must not
// be left live inside the stage for the next return Home to reopen.
hidePaywallExhausted();
setGenerationStageVisible(false);
const input = document.getElementById("pipeline-input");
if (input) {
Expand Down Expand Up @@ -9917,6 +9932,22 @@ function initExactShell() {
mobile.addEventListener("change", order);
}

// The stage only deserves the home surface while a panel inside it is still
// live — running progress or a failure, results, the paywall, or the ready
// state. Everything the stage can hold hides once Edit prompt returns to the
// composer, and the stage's bare latch must not reopen an empty panel.
function generationStageHasLivePanel() {
const progress = document.getElementById("pipeline-progress");
if (progress?.classList.contains("visible")) return true;
const results = document.getElementById("results-view");
if (results?.classList.contains("visible")) return true;
const paywall = document.getElementById("paywall-exhausted");
if (paywall && !paywall.classList.contains("hidden")) return true;
const ready = document.getElementById("ready-state");
if (ready && !ready.classList.contains("hidden")) return true;
return false;
}

function setGenerationStageVisible(visible) {
const stage = document.getElementById("generation-stage");
if (!stage) return;
Expand Down Expand Up @@ -10024,9 +10055,12 @@ function switchView(view, pushState = true, moveFocus = true) {
}
});

// Generation overlay only belongs to the home surface.
if (view !== "home") setGenerationStageVisible(false);
else {
// Generation overlay only belongs to the home surface — and only while a
// panel inside it is still live. Its latch marks that it took over once;
// after Edit prompt returned to the composer there is nothing to reopen.
if (view !== "home" || !generationStageHasLivePanel()) {
setGenerationStageVisible(false);
} else {
const stage = document.getElementById("main-stage-container");
if (stage && stage.classList.contains("visible")) setGenerationStageVisible(true);
}
Expand Down
1 change: 1 addition & 0 deletions dist/assets/index-BeVS7Wr2.css

Large diffs are not rendered by default.

328 changes: 0 additions & 328 deletions dist/assets/index-D2LZqiXQ.js

This file was deleted.

328 changes: 328 additions & 0 deletions dist/assets/index-DHB_cdT3.js

Large diffs are not rendered by default.

1 change: 0 additions & 1 deletion dist/assets/index-DbG5mQ4q.css

This file was deleted.

55 changes: 8 additions & 47 deletions dist/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -2857,55 +2857,18 @@
color: transparent;
}
.composer .ghost .typed { color: transparent; }
.composer .ghost .suggest {
color: oklch(80% 0.012 260);
opacity: 0;
transition: opacity var(--t-fast) var(--ease);
}
.composer.has-suggest .ghost .suggest { opacity: 1; }
/* Demo caret (prototype): a real text cursor — hard blink on a 1.05s cadence
with a soft brand afterglow. Shown while a chip fills the prompt. */
with a soft brand afterglow. Shown while the hero demo or a chip writes
the prompt; held solid while characters land (is-demo-keying), so it
only blinks while the field is idle, like a real cursor. */
@keyframes caret-blink { 0%,45% { opacity:1; } 46%,100% { opacity:0; } }
.composer.is-demo-typing .ghost .typed::after {
content:""; display:inline-block; width:2px; height:1.05em; margin-right:-2px; vertical-align:-.15em;
background:var(--accent); border-radius:1px;
box-shadow:0 0 7px 1px oklch(from var(--accent) l c h / .5);
animation:caret-blink 1.05s step-end infinite;
}
.composer .tab-hint {
position: absolute;
right: 64px;
bottom: 22px;
display: inline-flex;
align-items: center;
gap: 6px;
padding: 4px 6px;
border: 0;
background: transparent;
border-radius: 6px;
cursor: pointer;
font-family: var(--font-mono);
font-size: 11px;
letter-spacing: 0.02em;
color: var(--muted);
opacity: 0;
transform: translateY(2px);
transition: opacity var(--t-fast) var(--ease), transform var(--t-fast) var(--ease);
pointer-events: none;
}
.composer .tab-hint kbd {
font: inherit;
padding: 1px 6px;
border: 1px solid var(--border);
border-bottom-width: 2px;
border-radius: 5px;
background: var(--surface);
color: var(--fg);
}
.composer.has-suggest .tab-hint { opacity: 1; transform: none; pointer-events: auto; }
.composer .tab-hint[hidden] { display: none; }
.composer .tab-hint:hover { color: var(--fg); background: oklch(96% 0.01 260); }
.composer .tab-hint:hover kbd { border-color: oklch(80% 0.02 260); }
.composer.is-demo-keying .ghost .typed::after { animation: none; }
.composer:focus-within {
border-color: var(--accent);
box-shadow:
Expand Down Expand Up @@ -3310,8 +3273,8 @@
.send.is-busy .send-spin { animation: none; opacity: 0.6; }
}
</style>
<script type="module" crossorigin src="/assets/index-D2LZqiXQ.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DbG5mQ4q.css">
<script type="module" crossorigin src="/assets/index-DHB_cdT3.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-BeVS7Wr2.css">
</head>
<body>
<div class="shell-frame"><div class="shell-stage" id="shell-stage">
Expand Down Expand Up @@ -3350,10 +3313,8 @@ <h1 class="headline reveal">FlutterFlow custom code.<br><span class="headline-su

<div class="composer" id="composer">
<label class="sr-only" for="pipeline-input">Describe the widget or action you need</label>
<div class="ghost" aria-hidden="true"><span class="typed"></span><span class="suggest"></span></div>
<textarea id="pipeline-input" class="field" rows="3" spellcheck="false" aria-describedby="suggest-status" placeholder="Describe the widget or action you need" data-demo-prompt="A circular progress gauge with a gradient stroke"></textarea>
<span id="suggest-status" class="sr-only" aria-live="polite"></span>
<button class="tab-hint" type="button" id="tab-hint" tabindex="-1" hidden=""><kbd aria-hidden="true">Tab</kbd><span class="tab-hint-label">to accept</span></button>
<div class="ghost" aria-hidden="true"><span class="typed"></span></div>
<textarea id="pipeline-input" class="field" rows="3" spellcheck="false" placeholder="Describe the widget or action you need" data-demo-prompt="A circular progress gauge with a gradient stroke"></textarea>
<div class="tools">
<div id="prompt-image-upload" class="prompt-image-upload">
<button class="icon-btn" type="button" id="btn-add-images" aria-label="Attach reference image" onclick="document.getElementById('prompt-image-input').click()">
Expand Down
Loading
Loading