Bring-your-own API key. Operators configure their own
ai.text_provider(Anthropic Claude, OpenAI, Google Gemini, or DeepSeek) on the API Keys settings page; per-call costs are billed by the provider, not by WebProCMS. Included in the free tier — no membership required.
WebProCMS reads what the editor is naming or describing — a content type, a feature card, a row item — and asks the configured AI provider to pick the most semantically relevant icons from the bundled Heroicons set. The editor sees a small grid of pre-vetted candidates instead of scrolling through 600+ icons looking for the right "document" or "rocket".
The problem
Icon pickers are unforgiving. Heroicons ships 324 outline + 324 solid icons; ionicons ships another ~1,300. An editor naming a content type "Meeting Notes" or building a features grid for "Audit Trail" doesn't want to scroll through every option — they want the small handful that actually fit. Search-by-name only works if the editor already knows what the icon is called: typing "audit" returns nothing useful, because the icon they're after is clipboard-document-check or shield-check.
The other failure mode is mismatch. Without semantic suggestions, editors default to whatever icon they remember from another page — leading to UI that's inconsistent, generic, or outright wrong (a home icon on a logout link because that's what they grabbed last time).
The fix
A sparkles button next to every icon-picker entry asks the configured text provider to nominate ~12 candidates from the available icon set. The result is shown in a small modal — click one, the field updates, the modal closes. No prompt input, no scrolling, no guessing.
The same provider plumbing that powers AI text generation, translation, and tone rewriting drives the suggestions, so any provider already configured for those features works for icons too with no extra setup.
Where it appears
| Surface | Triggered from | Context the AI sees |
|---|---|---|
| Content type create / edit | Sparkles button next to the Icon field's "Change" button. | The plural and singular names the editor has typed (e.g. "Meeting Notes" / "Meeting Note"). |
| Page editor — feature cards, content rows, repeater items | "AI" tab in the global icon picker (alongside Quick Pick / All Icons). | The row's surrounding context — when picking an icon inside a repeater item, the AI sees that item's title/description; when picking a row-level icon, it sees the row's heading and surrounding text. Per-item suggestions reflect what each card actually says, not just the row's overall topic. |
Both surfaces share the same App\Support\AiIconSuggester::suggest() entry point, so the prompt shape, validation, and provider plumbing stay in one place. In the page editor, EditorAiActions::suggestIconsForField() gathers per-row / per-item context before calling the suggester and dispatches editor-icon-suggestions-ready (or editor-icon-suggestions-error) back to the global icon picker; the picker filters by target so a result fired for one field never leaks into another.
How a suggestion is generated
- Editor clicks the sparkles button next to an icon field.
- The Livewire action validates that an
ai.text_provideris configured and that there's enough context (a name or surrounding text) to ask about. Empty input short-circuits with a clear message rather than calling the API. - A single chat-completion request is dispatched via the same per-provider helpers (
callOpenAi/callDeepseek/callGoogle/callClaude) used elsewhere in the CMS. The system prompt names the task explicitly — "pick the 12 most semantically relevant icon names from the provided list" — and the user message includes the context plus the full list of available Heroicon names so the model can only suggest icons that exist. - The response is parsed defensively: markdown code fences are stripped, the JSON array is decoded, and every candidate is validated against the bundled
resources/heroicons/data.phpset. Anything the model invented is dropped silently. - Up to 12 valid, deduped suggestions render as a small icon grid in a modal. The editor clicks one to apply it. A "Regenerate" button asks the provider for a fresh batch.
Defensive parsing — invented names are dropped
LLMs occasionally hallucinate icon names ("user-profile-edit", "rocket-fast", "settings-gear"). The suggester array_flips the bundled icon set into a lookup table and rejects every candidate that isn't in it, so the editor never sees a broken <x-heroicon> reference. If every candidate is invalid, the modal shows "Could not get suggestions. Try again or pick an icon manually." and the manual icon picker still works.
What gets sent to the provider
The icon set is a flat list of name strings, not images — roughly 4 KB of text per request, well under any provider's token cap. There is no embedding step, no image upload, and no caching layer; every click is a fresh request scoped to the current name/context. That keeps the privacy story simple: the editor knows exactly what crossed the wire (the content type name, the row's text, and the icon-name list) and the provider gets nothing else.
Per-provider behaviour
| Provider | Model used | Notes |
|---|---|---|
| Claude | ai.claude_model |
Default for high-quality semantic matching. |
| OpenAI | ai.openai_model |
Same model that handles general text generation; honours ai.openai_reasoning_effort and ai.openai_verbosity. |
| Google Gemini | ai.google_model (default gemini-3.7-flash) |
Cheapest of the four for high-volume use; same behaviour. |
| DeepSeek | ai.deepseek_model (default deepseek-v4-flash) |
Text-only — vision is not needed here, so DeepSeek works the same as the others. |
Provider switching is at Dashboard → Settings → API Keys. The suggester reads the active provider at call time — no restart, no migration.
Failure modes — handled, never silent
| Condition | Behaviour |
|---|---|
No ai.text_provider configured |
Modal shows a clear "configure a provider" message and a link to Settings → API Keys is implied by the existing site copy. |
| Empty name / no context | Modal shows "Enter a name first so suggestions have context." — the API isn't called. |
| Provider request fails (network, auth, rate limit) | Modal shows "Could not get suggestions. Try again or pick an icon manually." The manual picker still works. |
| Model returned non-JSON or invented all names | Same fallback message — no broken icon references reach the editor. |
What lives where
| Path | Purpose |
|---|---|
app/Support/Ai/AiIconSuggester.php |
Stateless suggester. One public method (suggest($name, $singular, $limit)), per-provider helpers mirroring AiTranslator. Returns a deduped, validated list<string> or [] on failure. |
app/Concerns/EditorAiActions.php |
suggestIconsForField($rowIndex, $fieldKey, $idx, $subKey, $currentValue) — page editor entry point. Walks draft overrides + ContentOverride rows to assemble row/grid-item context, calls the suggester, dispatches editor-icon-suggestions-ready (or editor-icon-suggestions-error). |
resources/views/pages/dashboard/content-types/⚡create.blade.php |
Content type create form — sparkles button + suggestions modal next to the icon picker. |
resources/views/pages/dashboard/content-types/⚡edit.blade.php |
Content type edit form — same pattern. |
resources/views/pages/dashboard/pages/partials/editor-icon-picker.blade.php |
Page editor's global icon picker — hosts the AI tab alongside Quick Pick and All Icons; calls $wire.suggestIconsForField(...) on first open of the tab. |
resources/heroicons/data.php |
Bundled Heroicon set the suggester validates against. |
Settings reference
| Key | Default | Notes |
|---|---|---|
ai.text_provider |
'' |
Same key that drives all other AI text features. One of claude, openai, google, deepseek. |
Provider key (ai.{provider}_key) |
'' |
API key for the active provider. Reused from the AI text content feature — no separate key needed for icon suggestions. |