Stream Content Command

The streamContent command is a low-level API to stream content into the editor. It supports both appending content and replacing a specified range of content. This command is useful when you need to stream something like an LLM model response into the editor.

Advanced Integration

This command is useful for advanced integrations where you need to stream content into the editor from a URL or a response body.

Parameters

range: Either a single position to insert from or an object specifying the range to replace, with from and to properties.
callback: An asynchronous function that receives a write function to stream content into the editor.

callback Arguments

getWritableStream: A function that returns a writable stream object that can be used to write chunks of data into the editor.
write: A function that takes an object with the following properties:

  • partial: The content to insert into the editor.
  • transform: An optional function that takes an object with the following properties:
    • buffer: The accumulated content of the stream.
    • partial: The current partial content.
    • editor: The editor instance.
    • defaultTransform: The default transform function. This function takes the accumulated content and inserts it into the editor.
  • appendToChain: An optional function which can be used to append commands to the chain.

Example Usage

Using the write API

This example shows the flexibility of the streamContent command by fetching a large data stream from a URL and streaming it chunk-by-chunk into the editor.

editor.commands.streamContent({ from: 0, to: 10 }, async ({ write }) => {
  const response = await fetch('https://example.com/stream')
  const reader = response.body?.getReader()
  const decoder = new TextDecoder('utf-8')

  if (!reader) {
    throw new Error('Failed to get reader from response body.')
  }

  while (true) {
    const { done, value } = await reader.read()
    if (done) break

    const chunk = decoder.decode(value, { stream: true })
    write({ partial: chunk })
  }
})

Using the getWritableStream API

This example demonstrates an alternative way to stream content using a WritableStream object which can be used to write chunks of data into the editor.

editor.commands.streamContent({ from: 0, to: 10 }, async ({ getWritableStream }) => {
  const response = await fetch('https://example.com/stream')
  // This will pipe the response body content directly into the editor
  await response.body?.pipeTo(getWritableStream())
})

Using transformations

You can also use the transform function to modify the content before streaming it into the editor. This example demonstrates how to transform the content before streaming it into the editor.

editor.commands.streamContent({ from: 0, to: 10 }, async ({ write }) => {
  const response = await fetch('https://example.com/stream')
  const reader = response.body?.getReader()
  const decoder = new TextDecoder('utf-8')

  if (!reader) {
    throw new Error('Failed to get reader from response body.')
  }

  while (true) {
    const { done, value } = await reader.read()
    if (done) break

    const chunk = decoder.decode(value, { stream: true })

    write({
      partial: transformedChunk,
      transform: ({ buffer, partial, editor, defaultTransform }) => {
        // This will use the default transform function to take the whole buffer and insert it into the editor as uppercase
        return defaultTransform(buffer.toUpperCase())
      },
    })
  }
})

Use case: Parsing markdown content from a URL and streaming it into the editor.

import { marked } from 'marked'

editor.commands.streamContent({ from: 0, to: 10 }, async ({ write }) => {
  const response = await fetch('https://example.com/stream')
  const reader = response.body?.getReader()
  const decoder = new TextDecoder('utf-8')

  if (!reader) {
    throw new Error('Failed to get reader from response body.')
  }

  while (true) {
    const { done, value } = await reader.read()
    if (done) break

    const chunk = decoder.decode(value, { stream: true })

    write({
      partial: chunk,
      transform: ({ buffer, partial, editor, defaultTransform }) => {
        // This will parse the markdown content into an HTML string and insert it into the editor
        return defaultTransform(marked.parse(buffer))
      },
    })
  }
})

Using appendToChain option

The appendToChain function allows you to append commands to the chain before it is executed. This example demonstrates how to append a command to the chain before it is executed.

import { selectionToInsertionEnd } from '@tiptap/core'

editor.commands.streamContent({ from: 0, to: 10 }, async ({ write }) => {
  write({
    partial: token,
    appendToChain: (chain) =>
      chain
        // Move the selection to the end of the inserted content
        .command(({ tr }) => {
          selectionToInsertionEnd(tr, tr.steps.length - 1, -1)
          return true
        })
        // Scroll the editor to the end of the inserted content
        .scrollIntoView(),
  })
})

Using respondInline option of streamContent

By default respondInline is true. When inserting block content into the editor, sometimes you may want to insert it as a sibling instead of as a block directly. You can use the respondInline option to insert the content at the same depth as the from position.

editor.commands.setContent('<p>123</p>')
editor.commands.streamContent(
  4,
  async ({ write }) => {
    await new Promise((resolve) => {
      setTimeout(() => resolve(), 10)
    })
    write({ partial: '<p>hello ' })
    await new Promise((resolve) => {
      setTimeout(() => resolve(), 10)
    })
    write({ partial: 'world</p><p>ok</p>' })
  },
  { respondInline: true },
)
// Output: <p>123hello world</p><p>ok</p>
// As opposed to: <p>123</p><p>hello work</p><p>ok</p> when `respondInline` is `false`

Technical details

Here is the full TypeScript definition for the streamContent command:

declare module '@tiptap/core' {
  interface Commands<ReturnType> {
    streamContent: {
      streamContent: (
        /**
         * The position to insert the content at.
         */
        position: number | Range,
        /**
         * The callback to write the content into the editor.
         */
        callback: (options: StreamContentAPI) => Promise<any>,
        /**
         * The options to pass to the `insertContentAt` command.
         */
        options?: {
          parseOptions?: NonNullable<
            Parameters<RawCommands['insertContentAt']>['2']
          >['parseOptions']
          /**
           * This will insert the content at the same depth as the `from` position.
           * Effectively, this will insert the content as a sibling of the node at the `from` position.
           * @default true
           */
          respondInline?: boolean
        },
      ) => ReturnType
    }
  }
}

type StreamContentAPI = {
  /**
   * The function to write content into the editor.
   */
  write: (ctx: {
    /**
     * The partial content of the stream to insert.
     */
    partial: string
    /**
     * This function allows you to transform the content before inserting it into the editor.
     * It must return a Prosemirror `Fragment` or `Node`.
     */
    transform?: (ctx: {
      /**
       * The accumulated content of the stream.
       */
      buffer: string
      /**
       * The current partial content of the stream.
       */
      partial: string
      editor: Editor
      /**
       * Allows you to use the default transform function.
       */
      defaultTransform: (
        /**
         * The content to insert as an HTML string.
         * @default ctx.buffer
         */
        htmlString?: string,
      ) => Fragment
    }) => Fragment | Node | Node[]
    /**
     * Allows you to append commands to the chain before it is executed.
     */
    appendToChain?: (chain: ChainedCommands) => ChainedCommands
  }) => {
    /**
     * The buffer that is being written to.
     */
    buffer: string
    /**
     * The start of the inserted content in the editor.
     */
    from: number
    /**
     * The end of the inserted content in the editor.
     */
    to: number
  }
  /**
   * A writable stream to write content into the editor.
   * @example fetch('https://example.com/stream').then(response => response.body.pipeTo(ctx.getWritableStream()))
   */
  getWritableStream: () => WritableStream
}