useDocumentCreation.tsx3.0 KBView on GitHub
'use client';

import { useCallback, useRef, useState } from 'react';
import type { Editor } from '@tiptap/core';

import {
  PlaybookDocumentCreateDialog,
  type CreatedPlaybookDocument,
} from './PlaybookDocumentCreateDialog';
import type { PlaybookDocumentKind } from './HashMentionList';
import { insertDocumentRef, scopeAtCursor } from './playbookExtensions';

interface UseDocumentCreationOptions {
  /** User AOP id, for the resource-document path. */
  aopId?: string;
  /** Scope used when the cursor is not inside a scope card (single-scope editors). */
  baseScope?: 'user' | 'org';
  /** The playbook's owner, forwarded to the create mutation. */
  ownerUserId?: string | null;
  /** Persist the playbook before navigating away, so the inserted ref survives. */
  save?: () => Promise<unknown>;
}

/**
 * Wires the playbook editor's Board / Table / Document options to the create flow.
 *
 * The twin of `useSubagentCreation`, and separate from it for the reason
 * `insertDocumentRef` is separate from `insertSubagentRef`: a subagent's ref has to land
 * inside a `<trigger>` to ever fire, and a board's must not, because a trigger around a
 * board is a resource the playbook would try to run. Merging the two hooks would put that
 * difference behind a conditional in a shared `onCreated`, which is the sort of thing that
 * gets "simplified" back into one path by someone who does not know why it is two.
 *
 * It does NOT navigate away the way the subagent flow does. A subagent doc arrives empty
 * and unusable until you write its instructions, so leaving for it is the next step; a board
 * created from a template is complete on arrival, and the next step is finishing the
 * sentence you were writing in the playbook.
 */
export function useDocumentCreation({
  aopId,
  baseScope = 'user',
  ownerUserId,
  save,
}: UseDocumentCreationOptions) {
  const [open, setOpen] = useState(false);
  const [kind, setKind] = useState<PlaybookDocumentKind>('board');
  const [scope, setScope] = useState<'user' | 'org'>(baseScope);
  const editorRef = useRef<Editor | null>(null);

  const onCreateDocument = useCallback(
    (nextKind: PlaybookDocumentKind, editor: Editor) => {
      editorRef.current = editor;
      setKind(nextKind);
      setScope(scopeAtCursor(editor) ?? baseScope);
      setOpen(true);
    },
    [baseScope],
  );

  const onCreated = useCallback(
    async (result: CreatedPlaybookDocument) => {
      if (editorRef.current) insertDocumentRef(editorRef.current, result.documentId);
      try {
        await save?.();
      } catch {
        // Best-effort: the document exists either way, and the ref is in the editor. A failed
        // save is recoverable by typing anything; a swallowed create would not be.
      }
      setOpen(false);
    },
    [save],
  );

  const documentDialog = (
    <PlaybookDocumentCreateDialog
      open={open}
      onOpenChange={setOpen}
      kind={kind}
      scope={scope}
      aopId={aopId}
      targetUserId={ownerUserId ?? undefined}
      onCreated={onCreated}
    />
  );

  return { onCreateDocument, documentDialog };
}