Skip to content

AI Assistant ​

The assistant lives inside the editor. It writes a page (or a whole site) from a short brief, and it applies plain-language requests — "change my main color to deep teal", "add a FAQ about shipping after the features", "make this section punchier" — as ordinary, undoable edits to the draft. Nothing is published; everything lands in the draft the editor already shows.

Open it from the Assistant button in the topbar, with ⌘J, or from the command palette.

The editor topbar with the Assistant button and its ⌘J shortcut next to undo, Discard, Save and Publish
The Assistant button sits in the topbar, next to undo.

A selected section adds two shortcuts. The sparkle button in its header opens the Ask tab aimed at that section. The Ask AI to change this section… bar at the top of its settings panel sends the request straight away and closes the slide-over again once the change is applied.

The Hero section's settings panel with the Ask AI bar filled in with “Make this warmer”, above the Eyebrow, Heading and Subheading fields
Asking for a change to the selected section from its settings panel.

Installation ​

The assistant runs on the official laravel/ai SDK, which is an optional dependency (it needs PHP 8.3 and Laravel 12):

bash
composer require laravel/ai
php artisan vendor:publish --tag=filamentcraft-migrations
php artisan migrate

Then set a provider key. The default provider is Gemini:

ini
GEMINI_API_KEY=...

Without the package or a key the editor simply doesn't show the Assistant button. Run php artisan filamentcraft:doctor if you expect it and it is missing.

Generate a page ​

The Generate tab is a three-step flow: a brief, a plan you can edit, then the written page.

1. Brief. Describe the business, who it is for and, optionally, what this page should do. Pick a tone and one of the four style families. This page plans the current page; Whole site plans the pages the site needs and writes each one. Leave Also design the site on to have the assistant choose colors, fonts and the look of every section too, and choose whether the new sections replace the page's current ones or go below them.

The Generate tab's brief step: a business description, audience and page goal fields, tone chips, four style cards with palettes, This page and Whole site options, the Also design the site checkbox and the replace or keep choice
The brief. The footer shows how many requests the next step costs.

2. Sections. The plan lists every section with a one-line intent. Reorder, drop or add sections and retype any intent before copy is written. With design on, a card at the top shows the proposed palette, type pairing and look; its switch skips the design and keeps the site as it is. The design arrives a moment after the plan, and the write button waits for it. If the design call fails, the card says why and the pages are written on the site's current look.

The Sections step for Cascade Coffee: a design card reading Fir green and rust, printed-label restraint, after a 1960s Oregon seed-packet label, with its palette swatches, Merriweather and the look knobs, followed by six planned sections in one list: Hero, Image with text, Gallery, Locations, FAQ and Contact form, each with an intent line, and Back, Plan again and Write 6 sections buttons
The plan and the design card, with the reference the look borrows from.

3. Done. The sections are written into the draft and the canvas shows them straight away. The summary lists what changed. Images keep their placeholders, and ⌘Z restores the page as it was.

The Sections step while the design is still being chosen: a card reading Choosing colors and fonts above the six planned sections, with the Write button disabled
The design arrives as its own step; the write button waits for it.

A whole-site run writes one page per step and needs the editor tab to stay open. If the tab closes or a step fails, the pages already written stay as drafts; a failed page shows a Retry button.

The Done step as a list of changes: 6 sections written into this page, a site palette authored around #1f4d3a, one theme setting and three look knobs, with Start over and Review the page buttons
The finished page, still a draft until you save and publish.

Ask for changes ​

The Ask tab takes plain-language requests and keeps them as a log of edits: your request, the assistant's one-line reply, then one row per change. A row for a section selects that section in the canvas when you click it. The last three exchanges go with each request, so a follow-up such as "shorter" knows what it refers to, and Undo these changes reverts the page edits of the latest one. Edits you make in the settings panel while a request is running are kept.

While a request runs, the entry shows how many seconds it has taken, and after eight seconds says the provider is slow. A failed request stays in the log with Try again.

The Ask tab as an edit log: the request Make the hero about the Saturday cupping and add a FAQ about shipping, the reply, and three change rows: Updated Hero (2 fields), Added FAQ and Set the site's primary color to #1f4d3a, followed by Undo these changes
One request, three changes. Section rows jump to that section in the canvas.

How generation stays cheap ​

A page is written in three small calls instead of one large one:

  1. Plan — the model sees the section catalog as one purpose line per type and answers with an ordered list of types plus a twelve-word intent per section. The editor reviews the plan, can reorder, drop or add sections, and can retype any intent before copy is written.
  2. Design — the model sees the site's own schemes, its theme's settings (fonts included) and the style knobs the sections share, and answers with one direction: an existing scheme or a custom palette (background, surface, primary, accent — text colors are derived for contrast), a type pairing, theme values such as radii and sizes, and the look every section starts from. The editor sees it as a card (swatches, fonts, note) with a switch to skip it.
  3. Write — the prompt carries only the copy fields of the chosen types (headings, paragraphs, links, block text). Icons and images come from the preset of the style the editor picked; the model never sees or writes them.

A six-section homepage costs roughly 500–700 prompt tokens per step on a fast model. A whole-site generation adds one page-list call, then runs plan + write per page. Every model call of a generation is its own web request, the design call included, so progress is visible and no proxy timeout is hit. A lost response is safe: a retried tick finds the page it already wrote instead of creating it twice. The design is applied once, before the first page is written.

A custom palette is authored as a site scheme named ai-brand and made active; regenerating overwrites that one scheme instead of adding another. A model that is overloaded, rate-limited or silent hands the request to the next tier down (Advanced → Balanced → Fast, Fast → Balanced) within the same time budget, and is skipped for two minutes; so is a model that took more than 12 seconds to answer. The usage row records which model actually ran.

Requests ("Ask") are one call. The prompt opens with the site's name and the last brief written for it (business, audience, tone), so rewritten copy stays on-brand without re-entering it. The page follows as one line per section (id, type, writable field keys, its heading or title), with full current copy only for the section the editor has selected, then the last three exchanges. The model returns operations — update fields, add/move/hide/remove sections, switch the color scheme, recolor a token, change a theme setting — which are validated against the real field definitions before they touch the draft. Values that don't fit (an unknown enum member, a bad URL) are dropped, never stored. Adding sections triggers one extra write call, however many are added.

Images are left as placeholders for the editor to fill; the media library is one click away.

How it avoids generic output ​

Generated sites tend to look alike: a cream background, Inter and Playfair, an eyebrow over every heading, a stats row and three testimonials nobody gave. The prompts and the code behind them push the other way.

  • Design starts from a scene, not a category. The model first writes who visits and where, names a real object or publication the look borrows from, and picks a colour strategy (restrained, committed, full or drenched) before choosing any colour. The reference appears on the design card.
The design card: palette swatches in off-white, fir green and rust, the note Fir green and rust, printed-label restraint, the line After a 1960s Oregon seed-packet label, and Merriweather with minimal, sharp and comfortable look knobs
The design names what it borrows from, so you can judge the direction before anything changes.
  • Fonts are chosen by voice. The model picks from a curated set of distinctive faces, each described by character ("sturdy newspaper grotesque", "chunky soft serif"), and the faces every generated site already uses are left out. A brand kit's font allow-list still wins, and monospace never reaches body text.
  • Cream backgrounds are corrected. A background or surface in the cream, sand or beige band is moved to a near-neutral leaning toward the brand's own colour, keeping its lightness.
  • Nothing is invented. Sections that need real numbers, quotes, clients or people are only planned when the brief gives them. Otherwise the copy uses bracketed placeholders such as [Phone] or [Client name] for you to replace, and a link to an external site survives only when the brief names that site.
  • One eyebrow per page at most, feature cards get a real icon, and banned marketing phrases ("unlock", "seamless", "Learn more") stay out of the copy.

Your own prompts ​

Everything above is the default. You can add your own rules, replace the copywriting voice, or take the last word over every request, without forking anything.

House rules. Add rules for every task, or for one. They are appended after the built-in instructions under a "House rules from the site owner" heading, and the model is told they win:

php
use FilamentCraft\Ai\Enums\AiTask;
use FilamentCraft\Models\Site;

FilamentCraftPlugin::make()
    ->aiInstructions('Write British English. Never mention prices.')
    ->aiInstructions('Stay within our brand blues and never use a dark background.', AiTask::PlanDesign)
    ->aiInstructions(fn (Site $site, AiTask $task): ?string => $site->owner?->industry === 'health'
        ? 'Never make medical claims.'
        : null);

A closure receives the site and the task, so rules can follow a tenant's plan, industry or locale. Return null to add nothing. The tasks are PlanSite, PlanPage, PlanDesign, FillSections and Command (the Ask tab).

Your voice. Replace the built-in copywriting style with your own. The rules against invented facts and the output language always stay, so a custom voice cannot switch them off:

php
FilamentCraftPlugin::make()->aiVoice(<<<'VOICE'
    Voice: warm, plain and a little dry, like a good shopkeeper. Short sentences.
    Buttons say what happens next. No exclamation marks.
    VOICE);

// or per site, with the language the copy is written in
FilamentCraftPlugin::make()->aiVoice(fn (Site $site, string $language): string => $site->settings_json['ai_voice'] ?? 'Voice: plain and direct.');

The last word. aiPromptUsing() receives every finished request before it is sent and returns the one to send, so you can rewrite the instructions or prompt, or send a task to another tier:

php
use FilamentCraft\Ai\AiRequest;
use FilamentCraft\Ai\Enums\AiTier;

FilamentCraftPlugin::make()->aiPromptUsing(fn (AiRequest $request): AiRequest => $request->task === AiTask::PlanDesign
    ? new AiRequest($request->task, $request->instructions, $request->prompt, $request->schema, AiTier::Advanced, $request->site, $request->template)
    : $request);

Rules and a voice that don't need a closure can live in config instead:

php
'ai' => [
    'prompts' => [
        'voice' => 'Voice: dry, deadpan, Scandinavian.',
        'instructions' => [
            '*' => 'Write British English.',
            'plan_design' => 'Stay within our brand blues.',
            'fill_sections' => ['Mention free delivery over €40.', 'Sign off as "the Hearth team".'],
        ],
    ],
],

Config and plugin rules add up; a plugin voice wins over a config voice. Your customisation also reaches FakeAiRunner, so a test can assert on the exact prompt your rules produce:

php
FilamentCraftPlugin::make()->aiInstructions('Write British English.');
FakeAiRunner::install()->on(AiTask::PlanPage, ['sections' => []]);

// ... run a generation ...

expect(FakeAiRunner::current()->lastRequest()->instructions)->toContain('- Write British English.');

Configuration ​

php
'ai' => [
    'enabled' => env('FILAMENTCRAFT_AI_ENABLED', true),
    'provider' => env('FILAMENTCRAFT_AI_PROVIDER', 'gemini'),
    'models' => [
        'fast' => env('FILAMENTCRAFT_AI_MODEL_FAST', 'gemini-3.5-flash-lite'),
        'balanced' => env('FILAMENTCRAFT_AI_MODEL_BALANCED', 'gemini-3.6-flash'),
        'advanced' => env('FILAMENTCRAFT_AI_MODEL_ADVANCED', 'gemini-pro-latest'),
    ],
    'default_tier' => env('FILAMENTCRAFT_AI_DEFAULT_TIER', 'fast'),
    'allow_tier_choice' => true,
    'provider_options' => ['thinkingConfig' => ['thinkingLevel' => 'low']],
    'site_keys' => [
        'enabled' => env('FILAMENTCRAFT_AI_SITE_KEYS', false),
        'required' => env('FILAMENTCRAFT_AI_SITE_KEYS_REQUIRED', false),
    ],
    'usage' => ['track' => true, 'show' => true],
    'limits' => [
        'sections_per_page' => 9,
        'pages_per_site' => 6,
        'requests_per_minute' => 30,
        'monthly_tokens' => env('FILAMENTCRAFT_AI_MONTHLY_TOKENS'), // per site; null = no budget
    ],
    'timeout' => 45, // seconds of model time per web request, retries included — keep it under your web server's timeout
],

Every key also has a fluent counterpart on the plugin, so a panel provider can own it:

php
FilamentCraftPlugin::make()
    ->aiProvider('gemini')
    ->aiModels(fast: 'gemini-3.5-flash-lite', balanced: 'gemini-3.6-flash', advanced: 'gemini-pro-latest')
    ->aiDefaultTier(AiTier::Balanced, allowChoice: true)
    ->aiSiteKeys(enabled: true, required: false)
    ->aiUsage(track: true, show: false)
    ->aiProviderOptions(['thinkingConfig' => ['thinkingLevel' => 'low']])
    ->aiLimits(sectionsPerPage: 8, pagesPerSite: 5, requestsPerMinute: 20, monthlyTokens: 500_000)
    ->aiAccess(fn (?User $user, Site $site): bool => $user?->can('use-ai', $site) ?? false);

aiModel(AiTier::Advanced, '…') sets one tier, and ai(false) turns the assistant off.

  • Tiers, not models. Editors only ever see Fast, Balanced and Advanced. Which model sits behind each tier is yours to decide; set allow_tier_choice to false to hide the picker and always use default_tier. The tier an editor picks is remembered per site.
  • Provider. Any text provider laravel/ai supports works — set provider to its config/ai.php key and point the tier models at that provider's model names. provider_options is merged into every request's generation config; the default keeps Gemini's reasoning tokens off the bill.
  • Site keys. With site_keys.enabled, each site can store its own provider key from the assistant's settings panel. Keys are encrypted at rest on the Site row and never read back into a form; the panel shows only the last four characters. The host key stays as a fallback unless site_keys.required is on, in which case a site without a key sees a prompt to add one. Because the key lives on the Site, it follows whatever tenancy the site already has.
  • Usage. With usage.track, every call writes one row to filamentcraft_ai_usages (site, template, user, task, tier, model, tokens, duration). usage.show surfaces the month's totals inside the assistant; turn it off to keep the numbers to yourself while still recording them. The FilamentCraft\Models\AiUsage model has forSite() and thisMonth() scopes for your own reporting.
The assistant's settings panel open above the Ask tab, reading This month: 18 calls and 27,880 tokens
With usage.show on, the settings panel shows the site's calls and tokens this month.

Access and spending ​

By default every editor who can open the builder can use the assistant, and every call is paid with the host key unless the site has its own. Three controls keep that in check:

  • Who. aiAccess() takes a boolean or a closure that receives the user and the site being edited. Return false and the Assistant button, the section shortcuts and the key settings disappear; direct calls are refused too. Use it for a Gate ability, a role or a plan entitlement.
  • How often. limits.requests_per_minute caps the model calls one user can start per minute (30 by default). A whole-site generation makes about two calls per page, so keep it above that.
  • How much. limits.monthly_tokens is a token budget per site per calendar month. Once a site reaches it, the assistant shows a message instead of the forms until the month rolls over. The budget is counted from the usage table, so it needs usage.track on; filamentcraft:doctor warns when it is not.

For per-plan budgets, leave monthly_tokens unset and decide in aiAccess() with the AiUsage::query()->forSite($site)->thisMonth() totals.

Privacy ​

Each call sends the brief, the site name, the page's section types and copy (full copy only for the selected section) and, for Ask, the last three exchanges to the configured provider. Images, media files, form submissions and customer data are never sent. Mention the provider in your own privacy notice if your editors are not your own team.

Undo ​

Page edits — generated sections, rewritten copy, added or moved sections — are one undo step (⌘Z, or the Undo these changes button under the reply). Site-wide changes (the authored palette, color scheme, scheme tokens, fonts and theme settings) apply to the site immediately, the same way the settings panel does, and are not part of the page undo stack.

Testing your own integration ​

Bind FilamentCraft\Ai\Testing\FakeAiRunner in tests and queue answers per task:

php
use FilamentCraft\Ai\Enums\AiTask;
use FilamentCraft\Ai\Testing\FakeAiRunner;

FakeAiRunner::install()->on(AiTask::Command, [
    'reply' => 'Done.',
    'ops' => [['op' => 'set_color_scheme', 'scheme' => 'dark']],
]);

No test ever reaches the SDK; the fake also records every request so you can assert on the prompt that was built.