/** * Single-source-of-truth hook for the brief textarea in GenerateDialog. * * Replaces the previous tangle of state + refs + parallel effects with one * explicit state machine: * * placeholder ─► ai-loading ─► ai-ready * │ │ │ * └─────────────┴────────────┴─► user-edited ◄──► chip-applied * ▲ │ * └────keystroke───┘ * * • placeholder — deterministic string template built from doc context. * Shown the moment the popup opens; nothing async. * • ai-loading — the mount-effect kicked off `client.content.brief()`. * UI shows a spinner. briefText is still the placeholder. * • ai-ready — AI brief returned and replaced briefText. User has not * edited. * • user-edited — user typed in the textarea. Typeahead is enabled. * • chip-applied — user clicked a typeahead chip. Text was replaced with * the chip's prompt; typeahead suppressed so we don't * immediately re-fetch. Any subsequent textarea keystroke * flips back to user-edited. * * AI replacements (mount-effect) are ignored once the user has interacted * (status ∈ {user-edited, chip-applied}) — their work always wins. * * Typeahead chips are populated only when status === 'user-edited' and the * brief is long enough to be a real query (≥8 chars). One debounced fetch * per ~500ms of input idle. * * The hook owns: * - briefText state * - briefStatus state * - typeaheadChips + typeaheadLoading state * - the fire-once mount-effect ref * - the typeahead debounce timer * * The component owns: * - rendering the textarea, spinner, chips * - calling setBriefText on every textarea change * - everything else (modality, brand picker, generate button, etc.) */ import type { ContentConcept, LaminaClient } from '@uselamina/sdk'; export type BriefStatus = 'placeholder' | 'ai-loading' | 'ai-ready' | 'user-edited' | 'chip-applied'; export interface UseDocumentBriefArgs { client: LaminaClient; /** * Sanity document _id (drafts.foo or foo). Together with `fieldName`, * forms the persistence scope for the brief cache. When either is missing, * the hook still works fully — it just doesn't persist across reopens. */ documentId?: string; documentType?: string; documentTitle?: string; documentExcerpt?: string; fieldName?: string; fieldDescription?: string; /** Full Sanity document; sent to the server as `metadata.document`. */ fullDocument?: Record; /** Current modality (caller may override the auto-derived one). */ modality?: string; /** Sanity asset type from the asset-source props (`'image' | 'file'`). */ assetType?: string; /** When set, included in the brief request as brandProfileId. */ selectedBrandId?: string; /** * Whether typeahead should be enabled. Typically `true` when the dialog's * generation state is idle (not currently running). Caller passes * `(state.status === 'idle' || state.status === 'failed')` or similar. */ typeaheadEnabled: boolean; } export interface UseDocumentBriefResult { briefText: string; /** * Setter for the textarea. Wire this to `