Search by

concept24 / nova-ai-pagebuilder

mihaictinescu

AI Page Builder tool for Laravel Nova — conversational agent that builds landing pages (Page + blockText bands) with product selection, generated copy and images.

Package info

bitbucket.org/concept24/nova-ai-pagebuilder

pkg:composer/concept24/nova-ai-pagebuilder

Statistics

Installs: 29

Dependents: 0

Suggesters: 0

v0.9.1 2026-10-18 17:03 UTC

This package is auto-updated.

Last update: 2026-10-19 09:59:01 UTC


README

AI Page Builder tool for Concept24 CMS — conversational multi-model agent that turns a natural-language brief into a complete draft landing page (Page + blockText bands, product selection, multi-locale copy, generated images), built on Laravel Nova.

Requirements

  • PHP >= 8.4
  • Laravel 12 or 13
  • Laravel Nova 4.12+ / 5.x
  • OpenRouter API key
  • A C24CMS application — the package writes into the host app's Page / BlockText models and product catalog (not portable to other Laravel apps, same tradeoff as nova-ai-report)

Installation

composer require concept24/nova-ai-pagebuilder

The service provider is auto-discovered. Migrations run automatically via loadMigrationsFrom. Publish config manually:

php artisan vendor:publish --tag=nova-ai-pagebuilder-config

Host columns. The builder writes into the host's site_block_texts.html / .css, site_block_texts_trans.strings and site_pages.campaign_skin. The package ships database/migrations/2026_10_13_120000_sync_pagebuilder_freeform_schema.php, which adds each of these columns only if it is missing — so an older shop forked before the freeform work gets them with a plain php artisan migrate, and an up-to-date one is untouched. To copy the package migrations into the project's own database/migrations: php artisan vendor:publish --tag=nova-ai-pagebuilder-migrations (same file names, so they never run twice).

Register the tool in NovaServiceProvider::tools() and add a MenuSection pointing to /nova-ai-pagebuilder.

Access. The tool page and every nova-vendor/nova-ai-pagebuilder/* route require a Nova login, the viewNova gate and the tool's own canSee — restrict it when registering, e.g. (new NovaAiPagebuilder)->canSee(fn ($request) => (bool) $request->user()?->hasRole('Super Admin')). Guests get a redirect to the Nova login (page) or 401 (API); Nova users failing canSee get 403. tests/Feature/RouteAccessTest.php walks all registered routes, so new routes are covered automatically.

Configuration

config/nova-ai-pagebuilder.php:

KeyEnv varDefault
openrouter.api_keyOPENROUTER_API_KEY—
models.coordinatorNOVA_AI_PB_MODEL_COORDINATORgoogle/gemini-3.8-flash
models.sqlNOVA_AI_PB_MODEL_SQLgoogle/gemini-3.8-flash
models.copyNOVA_AI_PB_MODEL_COPY~openai/gpt-astra-latest
models.htmlNOVA_AI_PB_MODEL_HTML~anthropic/claude-opus-latest
models.coordinator_fallback—~anthropic/claude-sonnet-latest
models.video / video.modelNOVA_AI_PB_MODEL_VIDEObytedance/seedance-2.5
model_labels—dropdown labels for the ~…-latest aliases (not in the live /models catalog)
freeform.visual_qaNOVA_AI_PB_FREEFORM_VISUAL_QAtrue
models.imageNOVA_AI_PB_MODEL_IMAGEgoogle/gemini-3.1-flash-image
model_options.*—per-role allowlists for the UI dropdowns. Text roles carry the OpenRouter latest aliases (~anthropic/claude-{sonnet,opus,fable}-latest, ~openai/gpt-{astra,sol,terra,mini}-latest) next to Gemini; video lists Seedance 2.5 / 2.0 / 2.0-fast and Wan 2.6 / 3.0
max_tool_iterations—30
page_defaults—draft (is_public=0, is_active=1), layout_default
locales—null → config('app.allowed_languages')
layouts.exclude—non-content bands excluded from the AI palette
images.max_per_page—12
rate_limitNOVA_AI_PB_RATE_LIMIT30,1

Minimum .env:

OPENROUTER_API_KEY=sk-or-...

Features

  • Clarify-first gate: for complex or ambiguous requests the coordinator asks the questions it needs (max 4, with proposed defaults) and makes no tool call until the user answers; simple, complete questions are answered directly.
  • BI consultant stage (select_products in research mode): bestsellers / worst performers, FĂRĂ MIȘCARE (dead stock with immobilised value), MIȘCARE LENTĂ, SUPRASTOC / STOC CRITIC (days of cover), bought-together pairs, bundle proposals (slow mover + bestseller, both in stock, no invented discounts) and campaign product proposals that flow into propose_structure. The SQL agent receives the real orders interval and the excluded (cancelled/returned) status ids computed from the DB, so an empty calendar window is reported honestly and analysed on the last days of activity; one time window per analysis.
  • Conversion skeleton for proposals: hero + image context band → bestsellers grid → benefits/guide → secondary products on an image layout (catalog_09/catalog_08) → FAQ → final CTA band; generate_image is dropped server-side on layouts that cannot render a generated image.
  • Conversational flow with consent gates: short interview → live layout catalog → proposed band structure (requires explicit user approval) → generation → data entry → links.
  • Per-agent model dropdowns in the chat UI (Coordinator / DB queries / Copy / Images), config defaults + per-conversation override stored on the conversation (models json), validated server-side against per-role allowlists.
  • Product selection agent (select_products): read-only SQL sub-agent (sanitized SELECT/WITH only, LIMIT cap, sensitive-column masking, session execution timeout). Supports curated pinned SKU selections (incl. best/worst-sellers via site_orders_items) and dynamic filter specs (categories/brands/in_promo/last_added) consumed by the existing BlocktextRepository pipeline.
  • Copy generation in all active locales (native per-locale writing, strict JSON, SEO meta limits enforced).
  • Image generation via OpenRouter multimodal chat (modalities: ['image','text']), persisted per-conversation to tmp storage and attached to bands via Spatie MediaLibrary by the applier.
  • Deterministic data entry (LandingPageApplier): validates the full plan before any write (layouts on disk, min/max items, image refs, SKU existence), then creates the draft Page + bands + translations + media in one pass and flushes catalog/media cache tags. Draft pages are previewable on the frontend by logged-in admins (Page::scopePublic).
  • Composition canvas (drag & drop): split view next to the chat — a layout palette (thumbnails + badges), band cards with per-band instructions, image toggle, variant select and product drop-zones; product chips from chat research are draggable onto bands; the agent's numbered structure proposals render as an importable card (layout choice stays with the user); "Generate page from composition" builds exactly the user's canvas (1:1 validation in the applier, pinned-product specs auto-filled from the canvas).
  • Proposal → canvas handoff: propose_structure carries the SKUs already chosen with select_products (validated server-side against active products), the per-band generated-image flag and the page-hero flag; "Import into the composition" pins them on the bands, so generation runs with no second product selection.
  • Server-side plan assembly: create_landing_page is called WITHOUT a plan — PlanAssembler builds it from the canvas composition, the last successful generate_copy (band refs in canvas order) and the generated images (band image ref = band copy ref, hero, <ref>_<n> per item); the coordinator only sends products_by_band for product bands without pinned products. The explicit plan argument remains supported.
  • Live progress: sub-agents report steps over SSE (tool_progress: each SQL query with its purpose and row count, copy attempts, parallel image batch) and running tool cards show an elapsed-time counter.
  • Parallel images: all generate_image calls of one coordinator turn run as one HTTP pool (ImageGenerator::generateMany).
  • Product-aware copy: the copy agent receives the title, price and stock of the products pinned on each band and may name products/prices only from that list.
  • Freeform bands: the freeform layout (free HTML + Tailwind + data-anim, live product data) sits in the palette next to the classic bands and can be mixed with them. The coordinator calls generate_freeform_band(nr, brief, products?, feedback?) per band after generate_copy; the html model (default Claude Opus 5.5, own dropdown) returns HTML + per-locale strings, FreeformHtmlValidator REJECTS dangerous markup, hardcoded visible text, unknown c24-each sources / data-anim names and missing string keys (retry with feedback), PlanAssembler carries html/strings/button/products into the plan and LandingPageApplier writes them through the model (sanitised, CSS job fired). After create_landing_page, FreeformVisualQa screenshots the freeform bands (desktop + mobile, scripts/band-shots.cjs, throwaway admin session for the draft, destroyed right after; since 0.9.0 its cookie reaches the script through the NAPB_QA_COOKIE environment variable, never the command line, which every OS user can read in ps) and runs App\Ai\Agents\BandsVisualCritic; qa.problems drive one revision round. QA compared with the site (0.8.1): the script also screenshots a reference page of the site (freeform.qa.reference_url / NOVA_AI_PB_QA_REFERENCE_URL, default: first category with products) which the critic receives next to each band („banda trebuie să arate ca parte din acest site”), and measures each band — biggest font (> 1.2 × the reference h1), product image width (> 320px), backdrop behind product images (not white-ish), page background (not light unless the brief asked for dark) and, with motion ON and software GL, text covered by the WebGL canvas — turning the numbers into deterministic problems merged with the critic's; a band still covered after the revision round loses its data-gl* hooks (qa.repaired). Edit mode (0.6.0): FreeformBandGenerator::edit(html, strings, instruction, scope?, model?, sources, userId) applies an instruction to an existing band (diff-style prompt: everything not asked for stays identical; with scope only that element changes; new copy goes into strings for ALL locales), validated by the same validator with retry-with-feedback; logged as phase html with no conversation. Used by the host app's Nova preview chat. Needs PHP 8.4 (Dom\HTMLDocument) and puppeteer in the host app. Blog/categories/brands/benefits sources are linked manually in Nova.
  • Background runs: a chat run no longer dies when the tab is closed or the user navigates to another Nova menu — the stream ignores client disconnects, so the page, the visual QA and the final message are saved anyway. While it runs, a cache marker makes GET conversations/{id} return running: true; reopening the conversation shows it live and re-polls every 4s until it ends. Stop posts POST conversations/{id}/cancel, checked before each coordinator turn (the tool in flight still finishes and saves its result). A tool call left without a result by a dead run shows as „întrerupt", not as an endless spinner.
  • Page preview drawer (0.7.0): a "Previzualizare pagină" button (chat toolbar and the page-result card) opens a 98vw admin-themed drawer (Esc/×, focus trap, scroll lock) with an iframe of the real draft page (Desktop/Tabletă/Mobil + Reîncarcă) and the page's bands in order (layout, title, "Editează în Nova", click scrolls the iframe to the band). It reloads after every create/update in the chat. Data: GET conversations/{id}/page-preview. Scrolling needs the host app's data-block-id marker (rendered only for viewers who can open Nova).
  • Pick an element → edit (0.7.0): "Alege element" in the page preview resolves the clicked element to its band (nearest preceding data-block-id marker) and, for freeform bands, to the stored element (data-pv, also admin-only). Freeform band → POST conversations/{id}/band-edit (BandEditService::proposeFreeform → FreeformBandGenerator::edit, nothing saved) shown as a proposal in the drawer; Acceptă → POST conversations/{id}/band-apply (applyFreeform: re-validated, written through the model → sanitised html, CSS job, strings per locale, caches flushed). Classic band → an [EDITARE BANDĂ] block_id=… message in the chat; the coordinator calls generate_copy for that single band and the new tool apply_band_copy(block_id, ref) (BandEditService::applyCopy), which writes only that band's texts (and item texts, same count) without regenerating the page. Both refuse bands that don't belong to the conversation's page.
  • data-anim="carousel" (0.7.0): the freeform vocabulary (validator ANIMATIONS + prompt) gained carousel — a wrapper with a [data-anim-track] of slides (typically one product per slide via c24-each), optional [data-carousel-prev|next], [data-carousel-dots] and data-autoplay; native scroll-snap without JS, the host app's motion layer adds buttons/dots/keyboard/autoplay.
  • Design-system rule for freeform HTML (0.7.0): the HTML agent inherits the site's design system. FreeformTheme reads the token names/values and the site font from the host app's resources/css/theme.css at runtime; the prompt shows a semantic palette (token → role) and the rule "colours only through theme utilities (bg-brand, text-ink, from-surface-dark, /opacity and variants OK), site font only, radius from the scale". FreeformHtmlValidator rejects arbitrary colour values (bg-[#…], rgb(/hsl(/oklch(/color-mix( in any colour utility, incl. variants), Tailwind default-palette families (bg-red-500, text-gray-900), font-[…], inline style colour/font declarations and literal SVG fill/stroke; white/black/transparent/current, currentColor and none stay allowed. The feedback lists the offenders and the allowed tokens for the retry. In edit/apply of an existing band, violations already present in its html are tolerated — only NEW ones are rejected.
  • Art direction for freeform campaigns (0.7.0): coordinator tool design_campaign (ArtDirectionGenerator, html model, called once before any band) returns concept, a 6-colour palette (bg/surface/ink/accent/accent2/muted, valid hex, WCAG ink-on-bg and ink-on-surface >= 4.5), a display font from the host's config('campaign.display_fonts'), 2-3 motifs, a motion signature and a band rhythm plan (closed set of composition types, never 3 identical in a row), validated with retry-with-feedback. PlanAssembler turns it into the page's campaign_skin ({palette, display_font}, written by LandingPageApplier when site_pages.campaign_skin exists) and every generate_freeform_band / edit call receives it as a shared brief with the band's own rhythm slot. The HTML prompt gained editorial composition rules, product imagery treatment (mix-blend-multiply on campaign surfaces), generated full-bleed images, the campaign effects vocabulary (intro, reveal-image, story, curtain, scramble, zoom, data-cursor) and the campaign tokens (campaign-*, font-display); the validator knows the new names and enforces one intro per band and 2+ data-story-step per story. Shipped examples rewritten to this level.
  • Multi-conversation chat UI (SSE streaming, tool-call cards with image thumbnails, final page-result card with frontend + Nova links), per-phase cost logging (coordinator/sql/copy/image).
  • Generated video + voice-over (0.7.0): coordinator tools generate_video (Seedance via OpenRouter /videos, real product pictures as input_references, generate_audio=false; optional ElevenLabs voice via /audio/speech, script written by the copy model in <= 12 words or taken from the canvas) and apply_band_video. ffmpeg composes a voice-over mp4 (-16 LUFS, +faststart), a muted web loop (<= 6 MB) and a poster; files are staged per conversation (VideoStaging) and attached to the band as video_loop / video_full / video_poster (freeform html reads {{video.loop|full|poster}}). New model roles video / voice, a canvas panel (VideoOptionsPanel: toggle, per-band clips, duration, voice + slogan) stored in composition.video, prompt partial 09_video, log phase video (migration 2026_10_10_000002) and a video config block (presets hero 10 s/1080p, accent 6 s/720p, ffmpeg paths). Requires ffmpeg/ffprobe on the host. Real cost: about $3.76 for a 10 s 1080p hero with voice, about $0.91 per 6 s 720p clip.
  • Video models and capability fitting: bytedance/seedance-2.5 (default), bytedance/seedance-2.0, bytedance/seedance-2.0-fast, alibaba/wan-2.6, alibaba/wan-3.0, selectable per conversation. Models differ in resolutions and durations (e.g. Seedance 2.5: 480p/720p, 4-30 s; Wan 2.6: 720p/1080p, 5 or 10 s; Wan 3.0: 480p-1080p, 2-30 s), so Video\VideoModelCapabilities reads GET /videos/models (cached 24 h, key napb:video-models) and adapts each request before submit: highest supported resolution not above the requested one (else the lowest), nearest supported duration (ties go to the shorter). Without catalog data the request is sent unchanged. generate_video reports the effective video_duration / video_resolution. Presets (hero 10 s/1080p, accent 6 s/720p) are therefore only targets.
  • Background music + voice style (0.9.0): generate_video accepts music: {enabled, prompt?} (hero only) and voice.style. Video\MusicClient calls Google Lyria 3 (models.music, default google/lyria-3-clip-preview, also …-pro-preview; own dropdown role) through OpenRouter POST /chat/completions with stream: true, modalities: [text, audio], audio.format: mp3 and joins the base64 delta.audio.data pieces into an mp3 (~30 s stereo 44.1 kHz, ~$0.04 from the last chunk's usage.cost); logged in the usage table with phase music (migration 2026_10_16_000001 adds it to the enum — run php artisan migrate). MusicPromptWriter: the user's description (canvas video.music.prompt or an explicit chat request) is the style VERBATIM, otherwise it is derived from the campaign theme (art direction concept + motifs + the clip scene); it always appends instrumental / no vocals / commercial bed / clip length. CampaignVideoComposer mixes the bed into the FULL version only (the muted web loop stays silent): trimmed or looped to the clip length, fade in 0.4 s / out 1 s, two-pass loudnorm to -16 LUFS alone, -26 LUFS under a voice-over with sidechain ducking (video.music.* in the config). A band has {{video.full}} + the sound button when it has voice OR music (the HTML agent is told which per band); if the music call fails the clip is kept without it and the result carries music_error. Voice style: video.voice.style (free text: warm, feminine, mysterious, whispered…) is mapped by the copy model to one of the voices in video.speech.voices (the ids OpenRouter accepts for the ElevenLabs models, validated against the list; unknown → default brian) plus up to two inline audio tags ([whispers]) only for models in video.speech.tag_models (v3/v4 — other models would read the tags aloud). Canvas: "Muzică de fundal" switch + melody textarea, voice "how should it sound" textarea.
  • Site design system (0.8.0): the palette comes from the host theme tokens (palette_mode site | free), the heading font from the site, and FreeformTheme is now a contract reader (semantic tokens + @role annotations in the host theme.css); design_campaign derives the art direction from them.
  • Motion intensity (0.8.0): calm | balanced | dynamic — chosen per conversation, steers the art direction, the animation density in freeform bands and the WebGL scene limits per page.
  • WebGL in freeform bands (0.8.0): hook vocabulary data-gl="distort|reveal|scroll-wave|slides" and data-gl-scene="gradient|blob|particles", validated by the freeform validator, taught in the HTML prompt (WebGL + intensity sections), with page scene limits per intensity. The WebGL runtime (motion/gl) lives in the host app, not in the package.
  • Model aliases "latest" (0.8.0): model selectors accept latest aliases resolved at call time; new video models Seedance 2.5 (default) / 2.0 / 2.0-fast and Wan 2.6 / 3.0 with capability fitting (resolution/duration clamped to what the model supports).
  • Design mode Site / Creativ (0.8.1): per conversation, composition.design_mode = site (default) | creative (Conversation::designMode()). Site: the page follows the site's design system — type scale (text-h1…text-body, nothing above text-4xl, validated), products only through <c24-product-card>, light palette (dark bands only as accents), campaign font only on request, WebGL only when the brief asks, no mix-blend-* on images. Creativ: the pagebuilder 0.7 style — the art director asks for the campaign's own free palette (palette_mode: "free", #rrggbb; site tokens are only inspiration) and a campaign display font from the list by default, plus the 0.8.0 editorial behaviour (big editorial type, wide images, dark pages allowed). WebGL only on explicit request in BOTH modes (gl_signature is forced to none/none unless the brief asks; FreeformBandGenerator::webglAllowed, the PlanAssembler backstop and the prompt's WebGL section all follow the signature). The run lock (napb:run:{id}) has a 300s TTL refreshed at every coordinator turn and emitted event, so a killed run frees the conversation in at most 5 minutes. The HTML prompt is the 0.8.0 text plus the common rules, with its own examples (freeform_examples/site|creative). Both modes: no outline-only text (text-transparent + text-stroke), muted text contrast >= 4.5 on bg/surface, no mix-blend-* on images, data-video / {{video.*}} only on a band that has a staged/attached video. The mode is read by the art direction (a direction made in the other mode is ignored and design_campaign re-runs), the band generator (create + edit), the validator (also at apply time), PlanAssembler and the visual QA. QA by mode: the site reference screenshot and the fidelity metrics (font cap, product image size, light page) run in Site only; covered-by-WebGL, product image backdrop and horizontal overflow run in both, and a failed reference capture no longer stops the QA (the band is judged without the comparison).
  • Band variety (0.9.0): ArtDirectionGenerator::BAND_TYPES has 14 compositions (the original 8 + countdown-offer, tabbed-cabinet, card-stack, ledger, legend, poster; each described in one line in the art-director prompt, countdown-offer only when the brief mentions a deadline). Rhythm validation: no two consecutive identical types, a type at most twice per page, >= 4 distinct types from 4 bands. The HTML agent gets, per band, the overview of all types, the recipe (composition + 1-2 own effects) and the single example of ITS rhythm type (freeform_examples/{site,creative}/<type>.html, FreeformPrompt::exampleNames(); hero band of another type also gets full-bleed; types without a file fall back to product-spotlight). New hooks, validated: data-anim="view-reveal" (CSS-only scroll reveal), data-countdown="<ISO 8601 with timezone>" (+ optional [data-countdown-days|hours|minutes|seconds|expired] children, which must sit inside it), data-tabs (every <button type="button" data-tab="k"> pairs with a [data-tab-panel="k"]), and <c24-each offset="N"> (integer >= 0, skips the first N items). Runtime for these lives in the host app. Global rules: split-lines only on the hero + at most one other band; the numbered section kicker only for editorial-split / statement.
  • Effect controls (0.9.0): a collapsible «Efecte» section in the canvas (EffectsPanel) stores in the composition effects (the ALLOWED family keys; a missing key = all, so old conversations keep working), motion_intensity (auto | calm | balanced | dynamic) and video.accentClips (0-4, default 2, shown next to «clips on category bands»; replaces the old fixed "max 2" in prompt 09_video). Services\EffectFamilies is the single map family → data-anim values / data-micro values (lift, tilt only: buttons always keep fill press) / data-* attributes / band types: titles (split-lines, reveal-text, scramble), scroll_reveals (fade-up, stagger, view-reveal, reveal-image, zoom, reveal-mask), parallax_drawings (parallax, draw-svg, morph-svg), moving_bands (marquee, carousel, pin-scrub; band types marquee-strip, carousel), story (story; sticky-story), counters (counter, data-countdown; countdown-offer), tabs (data-tab; tabbed-cabinet), card_micro, cursor (data-cursor), webgl (data-gl; the direction's gl_signature is forced to none/none), smooth_scroll (runtime, no HTML hook; video keeps its own video.enabled toggle). Enforced in four places: FreeformPrompt teaches only the allowed vocabulary and lists the forbidden hooks (recipes of band types that need a disabled family are dropped); FreeformHtmlValidator::validate(..., ?array $effects = null) (null = all, hooks already in the stored band are tolerated) rejects hooks of a disabled family with a Romanian message — threaded from the conversation in FreeformBandGenerator generate/edit, BandEditService and LandingPageApplier; ArtDirectionGenerator removes the dependent band types from the prompt and the validation (the "min 4 distinct types" rule shrinks to the types that remain) and a non-auto motion_intensity replaces the director's (also re-applied when the stored direction is read, PlanAssembler::artDirection); smooth_scroll off writes smooth_scroll: false into the page's campaign_skin (also on pages without art direction, where the skin carries only that flag), which the host's campaign-skin partial turns into <meta name="smooth-scroll" content="off"> so the motion index skips Lenis. Endpoint: PUT /conversations/{id}/composition validates effects.* (in the family keys), motion_intensity, video.accentClips (0-4).
  • Header + footer in the campaign colours (0.9.0): a switch «Header and footer in the campaign colours» in the Style tab, right under the design-mode choice (hint: only on the campaign page; the rest of the site stays unchanged), plus a «Coloured header» chip in the summary strip. Stored in composition.campaign_chrome (nullable boolean, validated by updateComposition): missing/null = follows the design mode (creative on, site off, Conversation::campaignChrome()); an explicit value wins, and the canvas only sends one after the user touched the switch, so switching the mode moves an untouched switch with it. PlanAssembler writes campaign_skin.chrome = true only when enabled (absent otherwise, like smooth_scroll; a skin made of flags alone skips the palette checks in LandingPageApplier). The host app renders it: data-campaign-chrome on the site header and footer, remapping the site tokens to the campaign palette (contract in the app's docs/CLAUDE-FRONTEND.md). The host's new Logo Dark setting (APP_LOGO_FRONT_DARK) is the logo used on a dark campaign header; without it the Light logo sits on a light island.
  • Workspace UX, step 1 (0.9.0): a campaign header above the workspace (CampaignHeader: title, status pill Draft / Running / Page created / Page deleted from has_page / page_deleted / running, mode, consumed cost, and the actions Preview, Page on site, Nova from GET page-preview; the old «Previzualizare pagină» button above the chat is gone) and a composition panel with a summary strip (bands, mode, motion intensity, video / music / voice, effects x/11) plus four tabs remembered per conversation in localStorage: Structure (compact BandRows, one BandCard editor open at a time, drag & drop unchanged, «+ Add band» opens the LayoutPalette sheet), Style (StylePanel: design mode, motion intensity chips, EffectsPanel as a grid of switches), Media (MediaPanel: hero image, video, music, voice cards with SwitchToggles; music and voice need the video) and Advanced (AgentModelsPanel, moved from under the chat). The former Products tab is gone, see «Products on the canvas». VideoOptionsPanel is replaced by MediaPanel. The bottom bar shows the estimated cost of generating the page: GET /conversations/{id}/estimate (Services\CostEstimator, read-only, from the stored composition) = bands x median html call + median coordinator+copy per conversation (last estimate.window_days days of the log table, estimate.* config fallbacks below min_samples) + images x estimate.image + video seconds x video.price_per_second[model] (null = unknown, listed instead of guessed) + estimate.music / estimate.voice. Fix: a drop on a band no longer also fires the canvas-level append/move-to-end handler (reorder and palette drops used to apply twice).
  • Workspace UX, step 2 (0.9.0): the tool opens on a campaigns screen (CampaignsScreen / CampaignCard): a card grid with the page thumbnail (hero video poster, else the first image of band 1, else the page banner, else a gradient of the campaign palette with the title over it), status pill (Draft / Running / Page exists / Page deleted / Error — Error = the latest coordinator log row failed; a user stop is a warning, not an error), mode, band count, cost, relative date, author, a «+ New campaign» card, title search, filters (All / Mine / With page / In progress / Creative / Site) and a ⋯ menu per card (Open, Rename inline, Delete with an inline confirmation). The left conversation column and the conversation tabs are gone: a campaign opens full width with a «← Campaigns» header button and ?c=<id> in the URL (?c=new = empty workspace, the conversation is still created on the first message; Back / Forward / reload restore it; Back remounts the tool through Inertia, a run in progress continues on the server and its card shows Running). Ctrl/Cmd+K opens a quick switcher (CampaignSwitcher: combobox + listbox, arrows / Enter / Esc, focus kept inside), Ctrl/Cmd+N a new campaign. GET /conversations keeps its fields and adds, batched in Services\CampaignSummaries (a fixed number of queries whatever the number of conversations, capped at 500 rows): has_page, page_deleted, running, last_run_status, campaign_status (running > error > created > deleted > draft), page_id, design_mode, band_count (page bands, else the composition's), thumbnail_url, palette (bg, surface, ink, accent as hex — site-palette tokens are resolved from theme.css) and meta.user_id (for the «Mine» filter).
  • Workspace UX, step 3 (0.9.0): the tool calls of one assistant turn (consecutive tool-call / tool-result messages) render as ONE «Steps» card (StepsCard, labels in resources/js/steps.js): collapsed it shows the overall status (✓ / spinner with the running step's label and live progress / ⚠), the step count, the total elapsed time, the error count with the failed steps' one-line messages, or «N products selected»; expanded it lists the steps (failed first, each with its duration) and each opens the old ToolCallCard body (embedded), where the select_products list stays draggable onto bands (it opens with the card). Page-created and structure-proposal results stay outside the card; the interrupted-call logic (hasLaterTurn) is unchanged. A long brief is clamped to ~4 lines with a «More / Less» toggle. Responsive: BuilderTab measures the width of the tool itself (data-bp = wide / tablet < 1180px / phone < 760px, no container queries, so fixed overlays are unaffected): tablet and phone show a «Chat | Composition (N)» switch (both halves stay mounted, so the input and the composition state survive), the header folds its actions into a ⋯ menu on phone with an icon-only back button, campaigns are 2 columns on tablet / 1 on phone with one-row scrolling filters, the quick switcher is full width and the preview drawer is full screen on phone.
  • Editing in the page preview (0.9.0): on a freeform band, a click on a text makes it editable in place (Enter saves, Esc cancels) and POST /conversations/{id}/band-text writes it at once ({block_id, locale, key|field, text}). «Edit with AI» on a picked element now asks the HTML model for that element only and splices it back with the host's FreeformPreview::replaceAt (elementAt returns its index n); the validator compares against the problems the band already had (baseline), so an old issue elsewhere no longer blocks a small edit. Measured: shortening a paragraph went from 77 s / $0.09 to 3.3 s / $0.002. The proposal is swapped into the live preview iframe (page CSS kept) with Accept / Discard; the drawer header links to the page on the site next to Nova.
  • Stop stops (0.9.0): every long step reports progress through one callback that checks the cancel flag and throws RunCancelledException (sub-agents, video polling, visual QA), so a run stops within about a second, mid video or mid QA; the tool result carries cancelled: true.
  • Motion rules in the generated HTML (0.9.0): buttons and CTA links never move on hover — the validator rejects data-micro="magnetic", lift / tilt on a button or CTA link, and lift / tilt on a card that contains a button (it would carry the button along); buttons use fill press. The intro and curtain animations are no longer in the vocabulary (no colour mask over the hero or a band). The legend recipe and the prompt forbid invented facts (stock, percentages, bundle prices, reviews not coming from the data). Carousel controls (data-carousel-prev|next|dots) may sit anywhere in the band, e.g. arrows in the band header, when the band has one carousel (the host runtime looks them up band-wide).
  • Products on the canvas (0.9.0): after every generation LandingPageApplier::syncCompositionProducts() mirrors the SKUs the agent put on each page band into composition.bands[i].products as {sku, title, source: "agent"} (titles from the catalog, order kept — product tabs slice by it), so the canvas shows what the page holds and a regeneration keeps the same products. Bands the user pinned are never overwritten; dynamic selections carry no SKUs and are not listed; nothing is copied when page and canvas no longer have the same number of bands (edited in Nova). Pages made before 0.9.0 are backfilled once when the composition is opened (composition.products_synced), so products the user cleared stay cleared. apply_band_products writes the same rows. In the band editor the agent's products show as outlined chips with a note, «Clear» lets the agent choose again, and «+ Add product» opens the catalog search inside the band (click or Enter adds; dragging from the chat still works). PUT …/composition accepts products.*.source (user | agent).
  • Coloured header on an existing page (0.9.0): switching «Header and footer in the campaign colours» on a campaign that already has a page writes campaign_skin.chrome straight onto that page (LandingPageApplier::applyChrome(), no regeneration, no cost); the coordinator has the same as tool set_campaign_chrome(enabled) and is told never to claim it has no access to the header. The canvas re-reads the choice after a run.
  • Safe chat markdown (0.9.0): assistant replies go through resources/js/markdown.js (marked with an HTML renderer that escapes raw HTML and a link/image renderer that drops javascript:, data: and other non-http(s)/mailto schemes) before v-html.
  • Campaign intro and tooltips (0.9.0): a new campaign opens on an animated AI orb (ChatIntro: breathes while idle, lights up on click, pulses while typing) with four starter prompts that fill the input without sending. The ⓘ tooltips are drawn as one fixed bubble on <body> kept inside the viewport (resources/js/tooltips.js), so the scrolling panes no longer clip them. The band instructions field is taller (6 rows).
  • Host-columns migration (0.8.0): an idempotent migration adds the freeform/pagebuilder columns to older host projects; auto-loaded by the service provider and publishable. Run php artisan migrate.
  • New pages default to layout_landing (0.8.0).

Architecture

  • PageBuilderOrchestrator — coordinator tool loop (OpenAI-style function calling over OpenRouter, SSE).
  • Tools: get_layout_catalog, select_products (modes: research/pinned/dynamic), generate_copy, generate_image (img2img with real product photos via product_skus), create_landing_page, get_page_links, get_composition, propose_structure.
  • LayoutCatalog — bands discovered from resources/views/layouts_text/ intersected with curated metadata (layout_catalog_meta.php).
  • SqlToolRunner + QuerySanitizer — read-only SQL sub-loop.
  • CopyGenerator, ImageGenerator — single-call sub-services with their own cost logging.
  • LandingPageApplier — validation + transactional Page/BlockText creation.
  • Tables: nova_ai_pagebuilder_conversations (incl. composition json — the canvas state, shared across users), nova_ai_pagebuilder_messages, nova_ai_pagebuilder_logs.

Changelog

VersionDateNotes
0.9.12026-10-18Laravel 13: laravel/framework ^12.0\|^13.0 (Laravel 12 stays the minimum). Checked on a fresh Laravel 13.37 app with Nova 5.11: dependencies resolve, the provider and config load, the 22 routes register and refuse a guest (401), the system prompt and validator load. README requirements corrected to PHP >= 8.4 (as in composer.json). No code change, no migration
0.9.02026-10-18Campaign workspace: campaigns screen (cards with thumbnail, status, cost, filters, Ctrl/Cmd+K switcher, ?c=<id> URLs), campaign header with Preview / Page on site / Nova, composition summary + tabs Structure / Style / Media / Advanced, «Steps» card per turn, responsive tablet/phone, animated orb intro, estimated cost (GET …/estimate). Band variety: 14 band types with one example each, rhythm rules, new validated hooks view-reveal, data-countdown, data-tabs, <c24-each offset>. Effect controls: 11 families enforced in prompt, validator and art direction, motion intensity override, accent clips 0-4. Media: Lyria 3 background music (mixed under the voice with ducking) and voice style → ElevenLabs voice. Header + footer in the campaign colours (campaign_skin.chrome, applied at once on an existing page, tool set_campaign_chrome). Editing: inline text edit in the preview, element-scoped AI edit (77 s → 3.3 s), proposal shown in the live page. Products on the canvas: the agent's products mirrored into the composition and kept on regeneration; Products tab replaced by search inside the band. Rules: buttons never move on hover, no curtain/intro masks, no invented facts. Stop interrupts any step. Security: the visual QA passes its throwaway session cookie to the capture script through the environment (NAPB_QA_COOKIE) instead of argv, so it no longer shows in the server's process list; chat markdown (assistant replies, streamed or stored) no longer renders raw HTML — it is printed as text, and links/images keep only http(s), mailto, relative and #anchor URLs (resources/js/markdown.js); before, an <img onerror> echoed by the model or typed into a shared campaign ran in Nova. Migration 2026_10_16_000001 (log phase music) — run php artisan migrate. The host app provides the runtime for countdown / tabs / carousel controls, the campaign chrome and the Logo Dark setting. 362 feature tests
0.8.12026-10-14Design mode Site / Creativ per conversation (composition.design_mode): Site follows the site's design system (type scale capped at text-4xl and validated, products only via <c24-product-card>, light palette, campaign font only on request); Creativ keeps the 0.7 style (free #rrggbb palette + campaign display font). Visible switch in the canvas + badge in the conversation list; mode kept on partial PUT. WebGL only on explicit request in both modes. Visual QA compared with the site: reference page screenshot for the critic (freeform.qa.reference_url / NOVA_AI_PB_QA_REFERENCE_URL, default first category with products) + deterministic metrics (font cap, product image width/backdrop, light page, text covered by WebGL, horizontal overflow) in Site mode; capture no longer waits for networkidle, retries and continues on the other viewport. Freeform fidelity: stricter validator and prompt on the site's proportions. Run lock with 300s TTL refreshed every turn. Fixes: a new-chat tab no longer wipes an existing composition, QA repair works without the canvas. No migration. 274 feature tests
0.8.02026-10-13Site design system: palette from the host theme tokens (palette_mode site/free), heading font, FreeformTheme contract reader. Motion intensity calm/balanced/dynamic. WebGL in freeform: data-gl (distort/reveal/scroll-wave/slides) + data-gl-scene (gradient/blob/particles) validated and taught in the prompt, scene limits per intensity. Models: latest aliases, video Seedance 2.5 (default)/2.0/2.0-fast and Wan 2.6/3.0 with capability fitting. New pages default to layout_landing. Migration: idempotent host-columns migration (auto-loaded, publishable) — run php artisan migrate. The host app must provide the theme contract (semantic tokens + @role) and the WebGL runtime (motion/gl); they live in the host app, not in the package. 191 feature tests
0.7.12026-10-12Background runs: the chat run no longer dies when the tab is closed or the user navigates away — the stream ignores client disconnects, so the page, the visual QA and the final message are saved anyway; GET conversations/{id} returns running (cache marker) and a reopened conversation shows the run live, re-polling every 4s; Stop now posts POST conversations/{id}/cancel, checked before each coordinator turn. A tool call left without a result by a dead run shows as „întrerupt” instead of an endless spinner. Security: the tool page required no login (Nova::router() applied only nova.middleware) — page and API now require Nova Authenticate + viewNova + the tool's canSee (API on nova:api); RouteAccessTest walks every route as guest / Nova user without permission / Super Admin. No migration. 146 feature tests
0.7.02026-10-10Campaign art direction (design_campaign: concept, WCAG-checked 6-colour palette, display font from the host config, motifs, motion signature, band rhythm → campaign_skin on the page + shared brief per band). Design-system rule for freeform HTML (palette read from the host theme.css, validator rejects arbitrary/default-palette colours, font-[…], inline colour styles, literal SVG colours). Editorial prompt + new effects vocabulary (intro, reveal-image, story (sticky), curtain, scramble, zoom, data-cursor, carousel, data-video). Generated video (generate_video / apply_band_video: Seedance 2.0 with product references, optional ElevenLabs voice-over, ffmpeg loop/full/poster; canvas options + video/voice model roles; migration 2026_10_10_000002, needs ffmpeg). Page preview drawer + pick an element → AI edit (freeform: propose/accept; classic: apply_band_copy), apply_band_products (+ copy review), apply_band_image (multi-ref), placeholder-image filter in product selection, per-band visual QA with clean captures, story media validation. UI: preview button in the chat toolbar, one page-state source (has_page / page_deleted), confirmation before „Actualizează pagina” overwrites edited bands. Run php artisan migrate. 142 feature tests
0.6.02026-10-10Freeform edit mode: FreeformBandGenerator::edit(html, strings, instruction, scope?, model?, sources, userId) — diff-style prompt (everything not asked for stays identical; with scope only that element changes), new copy translated into all locales, same validator with retry-with-feedback (max 2), logged as phase html without a conversation. Used by the host app's Nova live preview + AI chat. No migration
0.5.02026-10-10Requires PHP ≥ 8.4 (Dom\HTMLDocument). Freeform bands: coordinator tool generate_freeform_band (HTML + per-locale strings + product source; prompt with DS tokens, {{…}}/<c24-each> syntax, data-anim/data-micro vocabulary, N1 bans, no hardcoded visible text), new html model role (default anthropic/claude-opus-5.5, env NOVA_AI_PB_MODEL_HTML) + dropdown, FreeformHtmlValidator (rejects literal text, banned elements/attributes, unknown sources, missing keys; 2 retries with feedback; re-validated at apply), pages can mix classic and freeform bands, FreeformVisualQa + BandsVisualCritic before the result. Final chat message always carries the preview/Nova links (fallback from the create_landing_page result); band url ignores the copywriter's #… placeholders. Migration adds the html log phase — run php artisan migrate. E2E: one generation with a freeform band, ~$0.19, QA approved. 82 feature tests
0.4.02026-09-26Server-side plan assembly (PlanAssembler; create_landing_page without plan, optional products_by_band), live sub-agent progress (tool_progress SSE + elapsed timer), parallel image generation (generateMany, one HTTP pool per turn), product-aware copy (pinned products' title/price/stock per band), PHP time limit raised only during the chat stream and restored after (request_time_limit, env NOVA_AI_PB_REQUEST_TIME_LIMIT, default 900s — covers Octane's per-worker set_time_limit). Measured on a full Black Friday generation: 143s and $0.54 end-to-end, 0 rejected plans, 3 images in 17s. 61 feature tests
0.3.12026-09-26Defaults from a benchmark on real sales data (16 conversations, verified against the DB): sql → google/gemini-3.8-flash, copy → openai/gpt-6-astra, coordinator stays google/gemini-3.8-flash (Opus 5.5 / Sonnet 5 / Astra Pro remain selectable). Coordinator: clarify-first gate (no tool call before the user answers), BI consultant stage with terminology and delivery format aligned to nova-ai-report, single time window per analysis, conversion skeleton for propose_structure. SQL agent: analysis recipes (velocity, days of cover, dead stock, bought-together pairs, bundles), real orders interval + excluded status ids injected from the DB, rationale surfaced on empty results. propose_structure drops generate_image on layouts without an image slot. Tests: applier test no longer depends on the host app's active locales
0.3.02026-09-25Cost/latency: Anthropic prompt caching (cache_control on system + last message) in OpenRouterClient, layout catalog no longer duplicated in the system prompt (tool-only), SQL agent told to answer in 2-4 queries, 429 admission-control responses retried (max 2, Retry-After capped at 10s). Quality: SQL agent selects only in-stock products, treats expired price_promo as no discount and varies models (max 2 variants per model); copy agent receives each layout's notes (FAQ JSON body, stats numbers) and is forbidden to invent commercial claims (discounts, warranties, delivery times, customer counts) — same rule for the coordinator; image prompts get a server-side "no text/numbers/logos" suffix. Flow: propose_structure accepts product_skus, generate_image, page_hero and the canvas import pins them; general page instructions field removed from the canvas (tone/audience/goal come from the chat); empty SQL research results surface the agent's rationale. 55 feature tests (package tests/ now versioned)
0.2.02026-09-25Model allowlists refreshed from the live OpenRouter catalog: coordinator default google/gemini-3.8-flash (+ GPT-6 Astra / Astra Pro, Claude Opus 5.5, Fable 5.1, Sonnet 5), SQL agent adds Opus 5.5 / Fable 5.1 / GPT-6 Astra, copy default openai/gpt-6-astra-pro, image adds Nano Banana Pro (google/gemini-3-pro-image) and GPT Image 2 (openai/gpt-5.4-image-2). Also: update an existing page from the composition + generate only from the composition, page-level hero, visible ⓘ tooltips, product search with drag & drop into the canvas; SQL fixes (tool-less closing call when iterations run out, explicit category-search guidance, error listing rejected SKUs)
0.1.12026-08-26No functional changes — release de verificare pentru auto-update-ul Packagist (webhook Bitbucket)
0.1.02026-08-26Composition canvas: split view with layout palette, drag&drop band cards (instructions, image kind, variants, product drop-zones), draggable product chips from chat research, importable numbered structure proposals (propose_structure), get_composition tool + MODUL COMPOZIȚIE prompt stage, GET/PUT conversations/{id}/composition, 1:1 canvas↔plan validation with auto-filled pinned-product specs, single-hero guard (page banner dropped when the first band is a hero with image). Fixes: SSE client dropped streaming tokens equal to "0" (PHP empty()), copy generator delta retry for missing band keys, interrupted tool-calls get synthetic results on replay (conversations stay resumable). Coordinator default moved to GPT (gpt-5.4, gpt-5.5 in allowlists); 45 feature tests
0.0.12026-08-25Initial release: conversational landing-page agent with a two-stage flow — research/analysis turns (analytical product questions answered as markdown tables via mode=research, iterative proposals) and build stage (interview → structure consent → generation → data entry → links); per-agent model dropdowns (Coordinator/SQL/Copy/Image) with per-conversation override; read-only SQL product selection (pinned SKU + dynamic filters, best-sellers); multi-locale copy; image generation with img2img composition integrating the selected products' real photos (media read through the media disk, S3/spaces included); deterministic applier with full pre-write validation; draft-only pages with admin frontend preview; conversations shared across all tool users (owner shown in sidebar); per-phase cost logging; 31 feature tests