Skip to content

feat: AI image generation overhaul + post creation flow + realtime broadcast#22

Merged
paulocastellano merged 14 commits into
mainfrom
feature/posts-realtime-status
May 8, 2026
Merged

feat: AI image generation overhaul + post creation flow + realtime broadcast#22
paulocastellano merged 14 commits into
mainfrom
feature/posts-realtime-status

Conversation

@paulocastellano
Copy link
Copy Markdown
Contributor

@paulocastellano paulocastellano commented May 7, 2026

Summary

This branch grew well beyond the original realtime broadcast work into a full AI image generation overhaul. Key pieces:

AI image generation

  • Replace Unsplash slide pipeline with gpt-image-2 via Laravel AI SDK
  • AiImageClient builds prompts from a Blade template (prompts/post_image/generator.blade.php) seeded by:
    • ImageStyle enum (8-option visual picker: cinematic, illustration, isometric, cartoon, typographic, infographic, minimalist, mockup)
    • workspace content_language, brand_color (mapped to a human-readable name via HexColorName helper) and brand_description
  • TemplateImageGenerator simplified: every slide is Template A (full-bleed photo + bottom gradient + white/grey text). Template B, renderTemplateB, roundCorners, blendHex, ensureContrast and the closing slide all gone

Post creation flow

  • StreamPostCreation creates the Post directly and dispatches PostCreationReady with post_id
  • Wizard kills its preview step and redirects straight to the editor
  • finalize endpoint removed
  • MediaItem JSON gains optional source (ai / unsplash / giphy) and source_meta (regen recipe). Gallery picker tags Unsplash/Giphy

Brand surface

  • New Workspace.image_style field + visual picker shared by /workspaces/create and /settings/workspace/brand via single BrandForm.vue component (autofill is a prop)
  • Brand-color autofill: new CssColorFrequencyExtractor parses every hex/rgb/hsl value in the homepage CSS, clusters perceptually similar shades in CIE LAB (Delta E 76 < 12), filters neutrals and returns the most frequent cluster. Solves Tailwind/utility-CSS sites where no --primary is exposed (e.g. Clinyx)

Credits

  • gpt-image-2 metered at 15 credits/image (low quality default)
  • 1.000 / 2.000 / 5.000 / 15.000 monthly credits from Starter to Max

Layout

  • AuthSplitLayout right column now sticky h-svh so form growth doesn't stretch the marketing slider
  • Image style picker keeps a clean 4×2 grid with truncated labels (no height variance)

Carry-overs from the original PR scope

  • Workspace-scoped broadcast channel + post lifecycle events
  • useWorkspaceEcho / usePostEcho composables and realtime list refresh

Cleanup

  • Drop App\Services\Unsplash\UnsplashClient (slide-only client; gallery UnsplashService stays)
  • Drop closing slide toggle (UI + job + tests + i18n)
  • i18n: removed em-dashes per project convention

Test plan

  • Create a workspace via autofill on a Tailwind site (e.g. clinyx.com.br) → brand color extracted via CSS frequency
  • Generate a single AI post → image renders with Template A overlay, slide footer shows handle/display_name
  • Generate a 5-slide carousel → all slides Template A, all 5 images persist with source: ai + source_meta in posts.media
  • Edit image_style in workspace settings → saves and is reflected on next generation
  • Pick from Unsplash gallery in editor → media item ends up with source: unsplash + source_meta.photo_id
  • Wizard loading state hides the back button until error
  • Right-side slider stays put when the create form grows tall

Adds a private workspace channel (WorkspaceChannel, authorised by
membership) so list views can subscribe once and receive events for
every post in the workspace, instead of opening N per-post channels.

New events:
- PostCreated   (broadcast on workspace.{id})
- PostDeleted   (broadcast on workspace.{id}; carries primitive ids so
  it fires after \$post->delete())
- PostPlatformStatusUpdated now broadcasts on both post.{id} and
  workspace.{id} so focused views and lists share the same trigger.

All broadcast events migrated to the namespaced 'entity.action'
convention from Laravel's broadcasting docs and switched from
ShouldBroadcastNow to ShouldBroadcast on a dedicated 'broadcasts'
queue (added to the supervisor-1 list in config/horizon.php) to keep
HTTP responses fast:

  PostCreated                  -> post.created
  PostDeleted                  -> post.deleted
  PostPlatformStatusUpdated    -> post.platform.status.updated
  PostCommentCreated           -> post.comment.created
  NotificationCreated          -> notification.created
  PostCreationReady            -> ai.creation.completed

Drops the SubscriptionCreated event — it was broadcast on
users.{owner_id} but had no listeners (frontend or backend) and the
billing/PostHog flow already handles plan-change tracking elsewhere.

PostPlatformStatusUpdated payload trimmed to {post_id} since every
consumer (Show, Edit, Index) does router.reload({ only: [...] }) and
ignored the rich shape.
The event was dispatched by StripeEventListener but had no consumers
on either side (no Event::listen registration in PHP, no Echo listener
in JS, no subscriber on the users.{owner_id} channel). Removed along
with its tests; the listener no longer references it. Cleans up the
StripeEventListenerTest to match (drops Event::fake(SubscriptionCreated)
in five negative-path tests, replaces with Bus::assertNotDispatched(
TrackBilling) which is the actual cascade-side-effect we care about).
…fresh

Two new composables under resources/js/composables/echo/ that abstract
channel naming for the two Reverb subscription patterns we now use:

- useWorkspaceEcho — list/dashboard views: 1 subscription per workspace
  delivers events for many entities. Resolves currentWorkspace.id from
  Inertia shared props; no-ops when there is no workspace yet.
- usePostEcho — focused views (Show, Edit): per-post channel.

Posts index now subscribes via useWorkspaceEcho to the 3 lifecycle
events (post.created / post.deleted / post.platform.status.updated)
and reloads the posts prop on receipt. `reset: ['posts']` is required
on the reload — without it, InfiniteScroll's merge prop appends every
broadcast onto the existing list and produces duplicates.

Show / Edit migrated from raw useEcho to usePostEcho; AiGenerateDialog,
NotificationBell and AiPostWizard updated to the new event names from
the namespaced convention. AiStream composable moved into the same
echo/ folder for organisation; its bound event names (TextDelta /
StreamEnd / Error) come from the laravel/ai package and stay as-is.

Bonus: comments tab send button was overriding shadcn's size="icon"
(40px) with class="size-9" (36px), which left it visibly shorter than
the textarea. Removed the override and matched the textarea's
min-h to size-10 so both render at 40px.
Class names and method signatures already explain what these classes
do. Project guideline (CLAUDE.md): comments only when the WHY is
non-obvious — implementation details belong in commits, not noise on
top of every class. Kept the one comment that earns it: the (int)
cast in HasUsage::cachedPostCount, which documents the load-bearing
workaround for Laravel's Redis cache numeric optimisation.
Core changes:
- Replace Unsplash slide pipeline with gpt-image-2 via Laravel AI SDK.
  New AiImageClient builds prompts from a Blade template seeded by the
  workspace's ImageStyle enum, content language, brand color (mapped to a
  human-readable name via HexColorName helper) and brand description.
- Drop Template B from TemplateImageGenerator: every slide now renders as
  Template A (full-bleed photo + bottom gradient + white/grey overlay).
  Removes renderTemplateB, roundCorners, blendHex, ensureContrast and the
  closing-slide pipeline.
- StreamPostCreation creates the Post directly and dispatches
  PostCreationReady with post_id; the wizard kills its preview step and
  redirects straight to the post editor on completion. Finalize endpoint
  removed.
- New Workspace.image_style enum field with an 8-option visual picker
  shared by /workspaces/create and /settings/workspace/brand via a single
  BrandForm component (autofill is a prop). 8 sample webp thumbs ship
  under public/images/branding/image-styles/.
- Media items gain optional source ('ai'|'unsplash'|'giphy') and
  source_meta (recipe needed to regenerate AI images later); the gallery
  picker tags Unsplash/Giphy attachments.
- Brand-color autofill: new CssColorFrequencyExtractor parses every
  hex/rgb/hsl value in the homepage CSS, clusters perceptually similar
  shades in CIE LAB (Delta E 76 < 12), filters neutrals and returns the
  most frequent cluster. Solves Tailwind/utility-CSS sites where no
  semantic --primary variable is exposed.
- Credits: gpt-image-2 metered at 15 credits/image (low quality default).
- Layout: AuthSplitLayout right column is sticky/h-svh so the form
  textarea growth no longer stretches the marketing slider.
- i18n cleanup: localized labels follow the no-em-dash convention.
@paulocastellano paulocastellano changed the title feat: realtime post lifecycle events + namespaced broadcast convention feat: AI image generation overhaul + post creation flow + realtime broadcast May 8, 2026
- Refactor ImagePreviewDialog to expose open(url, type) / openCollection(items, idx)
  via defineExpose; drop external props/state. GalleryBrowser and
  PostEditorComposer now call lightbox.open()/openCollection() instead of
  managing previewItem/previewIndex locally.
- Lightbox closes when clicking the empty space inside the wrapper (between
  image and arrows) using @click.self. Image click also closes; video click
  triggers play/pause without bubbling to close. Arrow buttons keep
  @click.stop so navigation never closes.
- Assets gallery (uploads / Unsplash / Giphy) now opens the lightbox in
  collection mode so the user can navigate the full tab list with arrows
  or keyboard.
- Add 'delete' typed-confirmation on the post-delete dialog (Index +
  Edit pages) and on the asset-delete dialog using the new
  common.confirm_modal.delete_keyword translation (en: 'delete',
  pt-BR: 'deletar', es: 'eliminar').
…names

Convert all 'foo.'.$bar style concatenations to "foo.{$bar}" interpolation
in event broadcast channels (PostCreated, PostDeleted, PostCommentCreated,
PostPlatformStatusUpdated, NotificationCreated) and the related event
tests.

Document the convention in CLAUDE.md so new code follows the pattern.
- TemplateImageGenerator->render() now returns a typed array shape
  {path: string, source_meta: array} instead of a one-off RenderedSlide
  DTO. Single internal callsite, no need for a dedicated class.
- Drop the `?? 'en'` fallback on $workspace->content_language in 6
  callsites: the column has a NOT NULL default of 'en' at the DB level,
  so the null coalesce was dead code.
- WorkspaceFactory now seeds content_language, brand_tone, brand_font
  and image_style explicitly so make() (no DB persist) produces a
  complete model — DB defaults aren't applied until create().
@paulocastellano paulocastellano merged commit 26e7688 into main May 8, 2026
2 checks passed
@paulocastellano paulocastellano deleted the feature/posts-realtime-status branch May 8, 2026 23:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant