feat([issue-4188]): synthesize a universe style guide from its linked mood board - #4247
Merged
Conversation
…oard (Phase 4) New stateless POST /api/mood-boards/:id/synthesize-style runs the board's collected content (text notes, captions, and the per-item prompt-from-media analyses) through a user-picked API LLM and returns a proposed styleNotes/influences guide plus the same reviewable diff the art-reference analyzer produces. The Universe Builder gains a Synthesize style flow next to the mood-board picker: preview the diff, then Adopt persists via a new server-side queued write (adoptStyleGuide / POST /:id/adopt-style) that re-checks field locks against the freshest persisted record — never a client wholesale influences PATCH. The StyleDiff preview is extracted from UniverseStyleReferences into a shared component so both adopt flows render identically.
…ping, context budget, busy-modal dismissal The synthesis body now remounts (keyed by universe+board) when the target changes, so a proposal generated for one universe can never be adopted into another; a mounted-ref guard drops in-flight resolutions after unmount. Adoption routes through the draft hook's applyStyleReferenceResult bookkeeping (new adoptStyleGuideFromBoard) instead of a bare updateDraft, so the saved-style snapshot and update watermark advance and styleProbeDirty clears — exactly as an art-reference adopt. The board context collector now bounds the AGGREGATE character budget (24k chars), not just the fragment count, so a large board degrades to fewer items instead of a truncated or rejected request. Modal backdrop/Escape dismissal is gated on the run state (with a force path for the post-adopt close, whose busy flag clears in the same tick).
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.
Summary
Phase 4 of #4188 — the final phase: a universe's linked mood board can now be distilled into its style guide.
POST /api/mood-boards/:id/synthesize-style(new, stateless) — feeds the board's collected content (text notes, captions, and the per-item prompt-from-media analyses Phase 3 persists) plus the universe's current style context to a user-picked API LLM, and returns{ proposed: { styleNotes, influences }, diff, rationale, llm }— the same reviewable diff shape the art-reference analyzer produces (buildStyleReferenceDiff). Context is bounded by fragment count and an aggregate character budget, with the dropped-item count reported. Locked fields keep their current values at proposal time.POST /api/universe-builder/:id/adopt-style(new) +adoptStyleGuidein crud.js — the reference-less sibling ofaddStyleReference's{ adopt }half: one server-side queued write ofstyleNotes+influencesthat re-checks field locks against the freshest persisted record (mergeInfluencesWithLocks). Never a client wholesaleinfluencesPATCH.applyStyleReferenceResultbookkeeping (newadoptStyleGuideFromBoard), so the saved-style snapshot and update watermark advance andstyleProbeDirtyclears exactly as an art-reference adopt does.StyleDiffPreviewextracted fromUniverseStyleReferencesinto a shared component so both adopt flows render the identical diff UI.All LLM calls are user-triggered with a visible provider/model picker (AI Provider Usage Policy); no batch auto-analysis.
A codex review pass surfaced one P1 and three P2s, fixed in the second commit: the synthesis modal is remounted (keyed by universe+board) so a stale proposal can never be adopted into a different universe; adoption goes through the hook's saved-state bookkeeping instead of a bare draft merge; the context collector caps aggregate characters, not just item count; and modal backdrop/Escape dismissal is gated while a run is in flight.
This completes the issue: Phase 1 (persisted universe↔board link, PR #4192), Phase 2 (gallery/upload pickers + video items, PR #4203), Phase 3 (per-item analysis, PR #4234), Phase 4 (this PR).
Closes #4188
Test plan
server/services/moodBoardStyleSynthesis.test.js— context collection (notes/captions/analyses, skip empty media items, fragment cap, aggregate character budget), empty-board 400, happy-path proposal + diff + prompt content, locked-field preservation, bad-JSON 502, no-API-provider 503.server/routes/moodBoard.test.js— synthesize route: 404 on missing board, validated pass-through to the service, strict-schema 400.server/services/universeBuilder.test.js—adoptStyleGuidepersists the pair with no reference attached; locks re-checked against the freshest record (locked notes/embrace kept, unlocked avoid adopts).client/src/components/universeBuilder/MoodBoardStyleSynthesis.test.jsx— hidden without a board / disabled until saved; synthesize sends the draft style context; diff preview; adopt hands the proposal to the caller and closes; Adopt disabled on a no-change proposal; stale proposal discarded when the target universe changes.client/src/components/universeBuilder/UniverseStyleReferences.test.jsx— unchanged and green after theStyleDiffPreviewextraction.biome checkclean.