import { BlockView, BlockRegistry, // The standard library (checklist, table, code-tabs, html, tabs + the eight // dev-doc blocks) is registered once via `registerLibraryBlocks` — the SAME // shared list plan registers. Content has no app-specific blocks beyond the // library, so it only re-types the table block (see below). registerLibraryBlocks, type BlockRenderContext, type NestedBlock, } from "@agent-native/core/blocks"; import { sendToAgentChat } from "@agent-native/core/client/agent-chat"; import { useEffect, useRef, useState } from "react"; import { uploadImageFile } from "@/components/editor/image-upload"; import { Popover, PopoverContent, PopoverTrigger, } from "@/components/ui/popover"; import { cn } from "@/lib/utils"; import { builderDocsBlocks } from "./BuilderDocsBlocks"; import { ContentBlockMarkdown, ContentBlockMarkdownEditor, } from "./ContentBlockMarkdown"; import { inlineDatabaseBlock } from "./InlineDatabaseBlock"; import { sourceComponentBlock } from "./SourceComponentBlock"; /** * Content's BROWSER block registry. Registers the same structured-block library * the server NFM registry (`shared/nfm-registry.ts`) registers, but WITH the real * React `Read`/`Edit` renderers. Both registries share the identical core * `schema` + `mdx` config per block, so what the editor renders and what the * inline NFM source serializes to can never drift. App-specific blocks register * after the shared library; `inline-database` is content's first one. * * Block `type`s MUST match the server registry exactly: the NFM parser stamps a * `registryBlock` node's `blockType` from the server spec's `type`, and this * registry resolves the renderer back by that same `type`. The one place the two * diverge from the core default is the table — registered as `table-block` here * to match `nfm-registry.ts` (content already owns a Notion `table` node, so the * registry block can't reuse the bare `table` type). The core `tableBlock`'s * schema/mdx/Read/Edit are reused verbatim; only the discriminating `type` * changes. * * Mirrors `templates/plan/app/components/plan/planBlocks.tsx`. */ export const contentBlockRegistry = new BlockRegistry(); // Register the whole standard library in one shared call (the same list plan // registers). Content's only override is the table `type` rename described above; // every other block keeps its canonical core metadata, so adding a 14th library // block in core lands in content automatically. registerLibraryBlocks(contentBlockRegistry, { overrides: { table: { type: "table-block" } }, }); for (const block of builderDocsBlocks) { contentBlockRegistry.register(block); } contentBlockRegistry.register(sourceComponentBlock); contentBlockRegistry.register(inlineDatabaseBlock); type ContentBlockRenderContext = BlockRenderContext & { documentId?: string | null; canEdit?: boolean; }; /** * Build the {@link BlockRenderContext} content's registry blocks render through. * Mirrors plan's `createPlanBlockRenderContext`, adapted to content: * - `dialect: "nfm"` — content's prose dialect. * - `renderMarkdown` / `renderMarkdownEditor` — block-internal prose (endpoint * descriptions, file-tree notes, annotated-code notes) renders through a * lightweight content markdown reader/editor rather than the document editor * (block prose is small and read-mostly). * - `renderEditSurface` — `editSurface: "panel"` blocks (the dev-doc blocks) * open their editor in a shadcn Popover anchored to the corner edit button, * non-modal so the rest of the document stays interactive. * - `uploadFile` — routes block uploads through content's existing upload path. */ export function createContentBlockRenderContext(options?: { documentId?: string | null; canEdit?: boolean; }): BlockRenderContext { const ctx: ContentBlockRenderContext = { dialect: "nfm", documentId: options?.documentId, canEdit: options?.canEdit, renderMarkdown: (markdown) => , renderMarkdownEditor: ({ value, onChange, editable }) => ( ), uploadFile: async (file: File) => { const url = await uploadImageFile(file); return { url }; }, renderEditSurface: ({ title, trigger, children, open, onOpenChange, variant, blockId, blockType, blockTitle, blockSummary, blockData, }) => { const compactMenu = variant === "menu"; return ( {trigger} {compactMenu ? ( <> {children} {blockId && blockType ? (
Ask AI to edit
) : null} ) : ( <>
{title}
{children} )}
); }, }; ctx.renderBlock = ({ block, editing = false, onChange }) => renderNestedContentBlock(block, ctx, editing, onChange); return ctx; } function ContentAiBlockAction({ label, blockId, blockType, blockTitle, blockSummary, blockData, documentId, }: { label: string; blockId: string; blockType: string; blockTitle?: string; blockSummary?: string; blockData: unknown; documentId?: string | null; }) { const submitPrompt = (prompt: string) => { const trimmed = prompt.trim(); if (!trimmed) return; sendToAgentChat({ type: "content", submit: true, openSidebar: true, message: trimmed, context: [ "The user is asking the agent to edit a focused structured block from the Content document editor popover.", documentId ? `Document id: ${documentId}` : null, `Document block id: ${blockId}`, `Document block type: ${blockType}`, blockTitle ? `Block title: ${blockTitle}` : null, blockSummary ? `Block summary: ${blockSummary}` : null, "", "Current block data:", fencedBlockData(blockData), "", "Patch the document's inline NFM/MDX block with this exact id. Use the Content app document editing actions, and patch only this block unless the user's instruction explicitly asks for a broader document change. Preserve existing block fields that the user did not ask to change.", ] .filter(Boolean) .join("\n"), }); }; return ( ); } function InlinePromptField({ placeholder, ariaLabel, onSubmit, disabled, }: { placeholder: string; ariaLabel?: string; onSubmit: (text: string) => void; disabled?: boolean; }) { const [value, setValue] = useState(""); const ref = useRef(null); // Grow the field to fit wrapped lines as the user types (capped, then scrolls). useEffect(() => { const el = ref.current; if (!el) return; el.style.height = "0px"; el.style.height = `${Math.min(el.scrollHeight, 220)}px`; }, [value]); const submit = () => { const trimmed = value.trim(); if (!trimmed) return; onSubmit(trimmed); setValue(""); if (ref.current) ref.current.style.height = ""; ref.current?.blur(); }; return (