feat(ai-create): global 'AI is generating…' bar that persists across navigation#130
feat(ai-create): global 'AI is generating…' bar that persists across navigation#130dantaspaulo wants to merge 4 commits into
Conversation
|
Thanks for this — the core design is really nice. Making a singleton composable the sole owner of the subscription, persisting the state across Inertia navigations, hydrating from Before we merge, though, there's one case I don't think it handles yet, and it's a realistic one: a user can have more than one generation running at the same time (start one, navigate away, start another). The composable is built for that on the loading side —
Concrete scenario: generate A → leave → generate B. A finishes, but the bar still says "generating 1 post" (B); 12s later A is auto-removed; B finishes → the bar shows only "B ready". A's completion bar never appears. So "N generating" works, but concurrent completions don't — I don't think we should merge it in this state. Could you think through a design for the multi-generation case before we take this further? A couple of directions to consider (your call on the approach): surface each completion as it happens even while others are still running (queue/stack them), and/or a bar that can represent "N ready" with access to each post. Happy to pair on it if useful. |
|
You were right on all three counts, and the second one is the sharp edge: Reworked it in two parts. The TTL only starts once the bar has actually surfaced a generation. Terminal states became collections and the strips coexist instead of competing for one slot. Tests. This is pure composable logic driven by a WebSocket event, so a browser test cannot reach it: without Reverb in CI, Echo never delivers the event. I added Vitest (+ jsdom) and unit-tested the composable with Echo mocked, capturing the per-channel listener so a test delivers a completion exactly the way the server does. Five cases, one per defect, plus oldest-first opening and bulk error dismissal. I verified the suite actually catches the bug — dropping the Two things worth flagging, both your call:
One thing I did not do: run Prettier over the touched files. The existing ones predate this PR and do not match |
…navigation
AI generation runs on the backend (the StreamPostCreation job broadcasts
.ai.creation.completed on the private user.{id}.ai-creation.{creationId}
channel), but only the dedicated loading screen listened to that channel. As
soon as the user navigated away, they lost any visual sign that a generation
was still running — only the final bell notification told them.
Introduce a global singleton composable (useAiGeneration) that is the sole owner
of the Echo subscription. Its module-level state persists across Inertia
navigations (SPA) and survives a hard reload via sessionStorage (it re-subscribes
to still-running generations, with a safety timeout). A thin bar at the top of
the content shows 'AI is generating your post…'; on completion it turns into a
clickable 'Your post is ready — View post', and on failure into a dismissible
error.
The loading screen no longer subscribes to Echo directly — it observes the
composable and redirects when its generation is done, which also removes the
two-owners conflict of both calling leave() on the same channel.
- useAiGeneration.ts (new): owns the subscription + state.
- AiGenerationBar.vue (new): the bar, mounted in AppSidebarLayout.
- Loading.vue: observes the composable instead of Echo directly.
- lang/{en,es,pt-BR}/posts.php: bar_* strings.
Review feedback: the bar handled N concurrent generations on the loading side, but the terminal (done/error) side was effectively single-slot, so concurrent completions were delivered unreliably. Three concrete defects followed from that: 1. A finished generation stayed hidden while another was still running, because `isGenerating` took priority over `doneGeneration`. 2. Worse, the 12s TTL ran while hidden. If the second generation took longer than that, the first was removed without EVER being shown — the user only learned about it from the bell notification. 3. A success masked a failure, since both read the first match and `done` was checked before `error` in the template. The fix has two parts: - The TTL only starts once the bar has actually surfaced the generation (`markSeen`), so nothing is discarded unseen. Until surfaced, the generation's own lifetime cap applies, so nothing gets stuck either. - Terminal states become COLLECTIONS and the strips coexist instead of competing for one slot: error, done and "generating" can all show at once. With more than one done, the strip reads "N posts are ready" and each click opens the oldest, revealing the next.
The overlapping-generations bug is pure composable logic driven by a WebSocket event, so a browser test cannot reach it — without Reverb in CI, Echo never delivers the event. Vitest runs the unit in milliseconds with Echo mocked, capturing the per-channel listener so a test can deliver a completion exactly the way the server does. Five cases, one per defect fixed: a completion surfaces while another is still running; an unseen completion outlives the done TTL; a failure is not masked by a success; clicks open oldest-first; errors dismiss together. Verified the suite catches the bug: dropping the `seenAt` gate fails the TTL case. `resources/js/routes` is generated by Wayfinder and gitignored, so `@/routes/*` resolves to a stub — the suite runs with no artisan step. New dev dependencies: vitest and jsdom. `npm run test:js` runs the suite, wired into the quality workflow.
The parity test requires every locale to carry the same keys as `en`, and the bar's `bar_*` strings only existed in en/es/pt-BR. Adds them to the twelve locales that landed since this branch was cut, following each file's own wording for "post" and its formal/informal register.
2280a85 to
354acae
Compare
Problem
AI generation runs on the backend — the
StreamPostCreationjob broadcasts.ai.creation.completedon the privateuser.{id}.ai-creation.{creationId}channel — but only the dedicated loading screen listened to that channel. As
soon as the user navigated away, they lost any visual sign that a generation was
still running; only the final bell notification told them.
Change
A global singleton composable,
useAiGeneration, becomes the sole owner ofthe Echo subscription. Its module-level state persists across Inertia
navigations (SPA) and survives a hard reload via
sessionStorage— itre-subscribes to still-running generations, with a safety timeout so the bar
never gets stuck.
A thin bar at the top of the content shows "AI is generating your post…". On
completion it turns into a clickable "Your post is ready — View post"; on failure
it becomes a dismissible error.
The loading screen no longer subscribes to Echo directly — it now observes
the composable and redirects when its generation is done. That also removes the
two-owners conflict of both the screen and the composable calling
leave()onthe same channel.
How
useAiGeneration.ts(new) — owns the channel subscription and the sharedstate (
track,hydrate,complete, per-idfind).AiGenerationBar.vue(new) — the bar, mounted at the top ofSidebarInsetinAppSidebarLayout.Loading.vue— observes the composable instead of Echo; owns no subscription.lang/{en,es,pt-BR}/posts.php—bar_*strings.The channel name, event (
.ai.creation.completed) and payload(
{ post_id, error }) are unchanged — the same contract the loading screenalready used.