feat(admin): fill the category form from prose, then change it by talking - #279
Merged
Conversation
…king
Second app on the fleet's AI-forms standard, after kivvi. evig already had AI
routes (extract, advisors), but no form here could be filled from a description,
and nothing could be revised by talking to it.
Adopts @fleet/ai-forms, which owns the merge rule that makes refinement work:
on a fill the user's typed input wins, on a refine the model wins for the fields
it returns. useAiForm infers which from whether the form is empty, so editing an
existing category refines by default and a new one gets filled — the user never
picks a mode.
- src/config/ai-forms.ts — SSOT for what the assistant may write. Colour options
come from UI_COLOR_PALETTE; nothing is retyped.
- src/app/api/ai/form-assist/route.ts — one route for every assisted form, staff
auth like the other advisors, and this app's own callWithFallback so form
assistance inherits the existing provider chain rather than adding a second
policy. Field specs stay server-side: the client names a form, never fields.
- src/components/ui/AiFormBar.tsx — generic over UseAiForm, so the next form
reuses it unchanged.
- CategoryForm's useState<CategoryFormData> becomes the shared store. The form
did not grow; the state moved.
Two details worth the review time:
The slug is aiExcluded — the model must not invent a URL segment, because
changing one silently moves where published posts live. But a name the model
writes still has to produce a slug, exactly as typing a name does, so onApplied
regenerates it via the same generateSlug(). Without that an AI-filled form would
save with an empty slug and fail the required check.
The route deliberately does NOT use the house { success, data } envelope. It has
exactly one client — useAiForm — and the hook reads the AssistResult shape
directly (result.ok, result.values). Wrapping it typechecks fine and breaks at
runtime, reporting every successful call as a failure. Found while writing it.
Translations added for all eight locales; lint:umlauts clean (ss, never ß).
Verified: tsc --noEmit clean, eslint 0 errors (48 pre-existing warnings in files
this does not touch), lint:umlauts clean. Build runs in CI.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
github-actions Bot
pushed a commit
that referenced
this pull request
Aug 7, 2026
#283) Three consecutive sweeps read #278 and #282 as MERGEABLE/CLEAN, announced the merge, and were refused: X Pull request #278 is not mergeable: the base branch policy prohibits the merge. There is no such policy. main has branch protection with every option disabled, no rulesets, no CODEOWNERS, no required checks and no required reviews. Throughout the same window the bot merged #225, #226, #279 and #280 without trouble — including #225/#226, which modify .github/workflows/, so this is not the token's workflows scope either. The refusal is gh's client-side precheck reading the mergeStateStatus that GITHUB_TOKEN sees, which is not the one a PAT sees — nothing this script can inspect its way around. So log both what we saw and delegate: native auto-merge is GitHub performing the merge itself, and does not go through that precheck. It is the escape hatch gh names in its own error message. Still one car per sweep — the fallback is followed by the same `break`, so at most one PR per sweep is handed over, and the green-base guard is unchanged. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Second app on the fleet's AI-forms standard, after kivvi#38.
evig already had AI routes (extract, the advisors), but no form could be filled from a description, and nothing could be revised by talking to it.
Adopts
@fleet/ai-forms, which owns the merge rule that makes refinement work: on a fill the user's typed input wins, on a refine the model wins for the fields it returns.useAiForminfers which from whether the form is empty — so editing an existing category refines by default, a new one gets filled, and the user never picks a mode.What's here
src/config/ai-forms.ts— SSOT for what the assistant may write. Colour options come fromUI_COLOR_PALETTE; nothing retyped.src/app/api/ai/form-assist/route.ts— one route for every assisted form. Staff auth like the other advisors, and this app's owncallWithFallback, so form assistance inherits the existing provider chain instead of adding a second policy. Field specs stay server-side: the client names a form, never fields.src/components/ui/AiFormBar.tsx— generic overUseAiForm; the next form reuses it unchanged.CategoryForm'suseState<CategoryFormData>becomes the shared store. The form didn't grow — the state moved.Two details worth the review time
The slug. It's
aiExcluded— the model must not invent a URL segment, since changing one silently moves where published posts live. But a name the model writes still has to produce a slug, exactly as typing a name does, soonAppliedregenerates it through the samegenerateSlug(). Without that, an AI-filled form saves with an empty slug and fails the required check.The envelope. This route deliberately does not use the house
{ success, data }shape. It has exactly one client —useAiForm— and the hook reads theAssistResultshape directly (result.ok,result.values). Wrapping it typechecks fine and breaks at runtime, reporting every successful call as a failure. Found while writing it, not after.Verified
tsc --noEmitclean · eslint 0 errors (48 pre-existing warnings in files this doesn't touch) ·lint:umlautsclean. Translations for all eight locales. Build runs in CI.🤖 Generated with Claude Code