Menu

Techniques

Undo

Copilot.js integrates seamlessly with your app's undo logic, regardless of the library you use: Redux Undo, Zundo, or any other. In this section, we use Zundo in all examples. Try the live demo to see how the undo integration works.

Group assistant actions in single undo step

It is best UX practice to let your users undo the actions performed by the assistant all at once, rather than individually. For example:

Group assistant actions in single undo step

To achieve this behavior, configure your app to work with Copilot as follows:

  1. Enrich your application's store with the copilotSnapshot.
// store.js
import { create } from 'zustand'
import { temporal } from 'zundo'
import { temporalOptions } from './options'

const useDrawStore = create(
  temporal(
    (set) => ({
      shapes: [], // Your application's state.
      copilotSnapshot: { isRunning: false }, // Add the `copilotSnapshot` with an initial value.
      setCopilotSnapshot: (newSnapshot) => set({ copilotSnapshot: newSnapshot }),
    }),
    temporalOptions,
  ),
)
  1. Keep the copilotSnapshot up-to-date inside your store, using the snapshotChange event.
// App.jsx
import { CopilotProvider, CopilotChat } from '@copilotjs/react'
import '@copilotjs/styles/default.css'

export default function App() {
  const { setCopilotSnapshot } = useDrawStore()
  return (
    <CopilotProvider
      // When the Copilot snapshot changes, update `copilotSnapshot` in your store.
      onSnapshotChange={(event) => setCopilotSnapshot(event.newSnapshot)}
      // ...
    >
      <CopilotChat />
    </CopilotProvider>
  )
}
  1. Save the app state when the Copilot transitions to running state. This lets the user undo all subsequent actions performed by the assistant, in a single undo step.
// options.js
export const temporalOptions = {
  // Save the state when Copilot changes to 'running' state.
  equality: (pastState, currentState) => {
    const isDifferent =
      (!pastState.copilotSnapshot.isRunning && currentState.copilotSnapshot.isRunning) ||
      (!currentState.copilotSnapshot.isRunning && pastState.shapes !== currentState.shapes)
    return !isDifferent
  },
  // Include `copilotSnapshot` in undo history.
  partialize: (state) => {
    const { shapes, copilotSnapshot } = state
    return { shapes, copilotSnapshot }
  },
}

Enable undo directly in chat UI

By default, Copilot defers the "UX of undo" to your app. This means using the buttons and shortcuts already provided by your app to undo actions performed by the assistant.

However, it can be more intuitive for the user to undo an action directly in the chat UI:

Enable undo directly in chat UI

To enable this feature:

  1. Enrich your application's store with the copilotSnapshot, as done in the previous section.

  2. Set the props of CopilotProvider and CopilotChat as follows:

// App.jsx
import { useDrawStore, useTemporalStore } from './store'
import { temporalOptions } from './options'
import { CopilotProvider, CopilotChat } from '@copilotjs/react'
import '@copilotjs/styles/default.css'

export default function App() {
  const drawStore = useDrawStore()
  const { pastStates, futureStates, undo, redo } = useTemporalStore((state) => state)
  return (
    <CopilotProvider
      // Pass your app's undo history to Copilot.
      context={{
        undoHistory: {
          past: pastStates,
          present: temporalOptions.partialize(drawStore),
          future: futureStates,
        },
      }}
      // ...
    >
      <CopilotChat
        // Show the undo & redo buttons in the Copilot chat UI.
        appearance={{
          enableUndo: true,
        }}
        // When the user presses the undo & redo buttons in the Copilot chat UI,
        // invoke the corresponding functions in your undo library.
        onUndo={() => undo()}
        onRedo={() => redo()}
      />
    </CopilotProvider>
  )
}

Note that unlike the context.state, which must be kept small, the context.undoHistory can be as large as needed, because it is not sent to the external AI provider.

Previous
State