Skip to content

tldraw/ai

Repository files navigation

tldraw ai module

This repo is meant to help developers build integrations between tldraw's canvas and AI tools. It contains several resources that can be used to get information out of tldraw (in order to prompt a model) and to create changes in tldraw based on some generated instructions.

The best way to get started is to clone this repository and experiment with its example project.

The module is also distributed as an NPM package, @tldraw/ai. It is meant to be used together with the tldraw SDK.

Local development

This repository is a pnpm monorepo. It has three parts:

  • /package contains the ai module package itself
  • /example/client contains the example's frontend (a Vite app)
  • /example/worker contains the example's backend (a Cloudflare Worker)
  1. Clone this repository.

  2. Enable corepack

corepack enable
  1. Install dependencies using pnpm.
pnpm i
  1. Create a /example/.dev.vars file. Add any environment variables required by the server to the /example/.dev.vars file. By default, our example project requires an OpenAI API Key so your /example/.dev.vars file should look something like this:
OPENAI_API_KEY=sk-proj-rest-of-your-key
ANY_OTHER_KEY_YOU_ARE_USING=here

If you need to use public-friendly API keys on the frontend, create a /example/.env file and put them there. See this guide for more information about environment variables in Vite.

VITE_LEAKABLE_OPENAI_API_KEY=sk-proj-rest-of-your-key
VITE_SOME_PUBLIC_KEY=sk-proj-rest-of-your-key
  1. Start the development server.
pnpm run dev
  1. Open localhost:5173 in your browser.

You can now make any changes you wish to the example project.

You can now make any changes you wish to the example project.

7. Start hacking

There are a few things you can do right away:

  • Tweak the example's system prompt at ./worker/do/openai/system-prompt.ts.
  • Make the example's system capable of creating new shapes.
  • Make the example's system capable of creating new events.

See the README in example/worker/do/openai/README.md for more information on the example's backend.

Note: If you find yourself needing to make changes to the package code, please let us know on the tldraw discord channel. Your changes would be very useful information as we continue to develop the module!

Installation

For production, install the @tldraw/ai package in a new repository, such as a fork of tldraw's Vite template. See the tldraw repository for more resources.

Install the @tldraw/ai package from NPM or your package manager of choice.

npm i @tldraw/ai

Usage

Use the useTldrawAi hook in your code.

function App() {
	return (
		<div style={{ position: 'fixed', inset: 0 }}>
			<Tldraw persistenceKey="example">
				<MyPromptUi />
			</Tldraw>
		</div>
	)
}

const TLDRAW_AI_OPTIONS = {
	transforms: [],
	generate: async ({ editor, prompt, signal }) => {
		// todo, return changes
		return []
	},
	stream: async function* ({ editor, prompt, signal }) {
		// todo, yield each change as it is ready
	},
}

function MyPromptUi() {
	const ai = useTldrawAi(TLDRAW_AI_OPTIONS)

	return (
		<div style={{ position: 'fixed', bottom: 100, left: 16 }}>
			<button onClick={() => ai.prompt('draw a unicorn')}>Unicorn</button>
		</div>
	)
}

Read on for more tips about using and configuring the hook.

Guide

Changes

The fundamental unit in tldraw's ai module is the "change". A change is an instruction to the tldraw editor to do one of the following:

  • createShape creates a shape
  • updateShape updates a shape
  • deleteShape deletes a shape
  • createBinding creates a binding
  • updateBinding updates a binding
  • deleteBinding deletes a binding

See package/src/types.ts for more about each change.

Changes should be generated by the generate or stream methods of the useTldrawAi configuration. You can do generate the changes using whichever method you wish, however the expectation is that you will send information to an LLM or other model to generate changes for you.

You may find that models are bad at generating changes directly. In our example project, we communicate with the LLM using a "simplified" format, parsing the response before sending back the actual changes expected by the ai module.

Transforms

The ai module has support for "transforms" (TldrawAiTransform). This feature allows you to modify the user's prompt (the data extracted from the canvas) and then use those modifications again later when handling changes. These are useful when preparing the data into a format that is easier for an LLM to work with.

Our example project includes several of these. When extracting data from the canvas, SimpleIds transform replaces tldraw's regular ids (which look like shape:some_long_uuid) with simplified ids (like 1 or 2). Later, when handling changes, the transform replaces the simplified ids with their original ids (or creates new ones).

To create a transform, extend the TldrawAiTransform class:

export class MyCustomTransform extends TldrawAiTransform {
	override transformPrompt = (prompt: TLAiPrompt) => {
		// modify the prompt when it's first created
	}

	override transformChange = (change: TLAiChange) => {
		// modify each change when it's being handled
	}
}

Pass your new class to the useTldrawAi hook's configuration.

const MY_STATIC_CONFIG: TldrawAiOptions = {
	transforms: [MyCustomTransform],
	...etc,
}

When the user creates a new prompt, the ai module will create a new instance of each transform to be used for that prompt only. This means that you can stash whatever data you wish on the instance. See the examples in example/client/transforms as a reference.

The hooks

TldrawAiModule

The TldrawAiModule class is responsible for

  1. Getting information about the current tldraw editor's canvas
  2. Incorporating transforms before and after changes are generated
  3. Applying "changes" to the tldraw editor's canvas

useTldrawAiModule

The package exports a hook, useTldrawAiModule, that creates an instance of the TldrawAiModule class for you to use in React. This class handles tasks such as getting information out of the tldraw canvas and applying changes to the tldraw canvas.

useTldrawAi

The useTldrawAi hook adds an extra layer of convenience around the ai module. This hook handles many of the standard behaviors for you. While we expect to expand this hook to support more configration, you may find it necessary to create your own version of this hook (based on its source code) in order to customize it further.

The hook responds with three methods: prompt, repeat, and cancel.

  • prompt accepts either a string or a configuration object with messages and stream. By default, the prompt method will call your configuration's generate method. If stream is true, then it will call your configuration's stream method.
  • cancel will cancel any currently running generation.
  • repeat will apply the same changes that were generated last time. This is useful for debugging.

Generate vs. Stream

You don't need to define both generate and stream, though you should define one of them. If you call ai.prompt with the stream flag set to true, but don't have stream implemented, then you'll get an error; likewise, if you call ai.prompt without the stream flag and without generate, then you'll get an error. Just be sure to implement one or both.

Static configuration

If you're using the useTldrawAi hook, we recommend creating a custom hook that passes static options to the useTldrawAi hook. See the useTldrawAiExample hook in our example project as a reference.

export function useMyCustomAiHook() {
	const ai = useTldrawAi(MY_STATIC_OPTIONS)
}

const MY_STATIC_OPTIONS: TldrawAiOptions = {
	transforms: [],
	generate: async ({ editor, prompt, signal }) => {
		// todo, return changes
		return []
	},
	stream: async function* ({ editor, prompt, signal }) {
		// todo, yield each change as it is ready
	},
}

If you must define the options inside of a React component, it's important that you memoize the options correctly.

export function MyPromptUi() {
	const myMemoizedOptions = useMemo<TldrawAiOptions>(() => {
		return {
			transforms: [],
			generate: async ({ editor, prompt, signal }) => {
				return []
			},
			stream: async function* ({ editor, prompt, signal }) {},
		}
	}, [])

	const ai = useTldrawAi(myMemoizedOptions)

	return <etc />
}

Calling the hook

The ai module relies on the tldraw editor at runtime. You should use the useTldrawAi hook inside of the tldraw editor's React context, or else provide it with the current editor instance as a prop.

You can do that via a child component:

function App() {
	return (
		<div style={{ position: 'fixed', inset: 0 }}>
			<Tldraw persistenceKey="example">
				<MyPromptUi />
			</Tldraw>
		</div>
	)
}

function MyPromptUi() {
	const ai = useMyCustomAiHook()
	return (
		<div style={{ position: 'fixed', bottom: 100, left: 16 }}>
			<button onClick={() => ai.prompt('draw a unicorn')}>Unicorn</button>
		</div>
	)
}

Or via the Tldraw component's components prop:

const components: TLComponents = {
	InFrontOfTheCanvas: () => {
		const ai = useMyCustomAiHook()
		return (
			<div>
				<button onClick={() => ai.prompt('draw a unicorn')}>Unicorn</button>
			</div>
		)
	},
}

function App() {
	return (
		<div style={{ position: 'fixed', inset: 0 }}>
			<Tldraw persistenceKey="example" components={components} />
		</div>
	)
}

If this is inconvenient—or if you like a challenge—you can also pass the editor as an argument to useTldrawAi. While this involves some "juggling", it may be useful when you wish to place the ai module into a global context or necessary if you need to use it in different parts of your document tree.

function App() {
	const [editor, setEditor] = useState<Editor | null>(null)

	return (
		<div style={{ position: 'fixed', inset: 0 }}>
			<TldrawBranch onEditorMount={setEditor} />
			{editor && <MyPromptUi editor={editor} />}
		</div>
	)
}

function TldrawBranch({ onMount }: { onMount: (editor: Editor) => void }) {
	return (
		<div style={{ position: 'fixed', inset: 0 }}>
			<Tldraw persistenceKey="example" onMount={onEditorMount} />
		</div>
	)
}

function MyPromptUi({ editor }: Editor) {
	const ai = useTldrawAi({ editor, ...MY_STATIC_OPTIONS })
	return (
		<div style={{ position: 'fixed', bottom: 100, left: 16 }}>
			<button onClick={() => ai.prompt('draw a unicorn')}>Unicorn</button>
		</div>
	)
}

License

This project is provided under the MIT license found here. The tldraw SDK is provided under the tldraw license.

Trademarks

Copyright (c) 2024-present tldraw Inc. The tldraw name and logo are trademarks of tldraw. Please see our trademark guidelines for info on acceptable usage.

Distributions

You can find the @tldraw/ai package on npm here. You can find tldraw on npm here.

Contribution

Found a bug? Please submit an issue.

Community

Have questions, comments or feedback? Join our discord. For the latest news and release notes, visit tldraw.dev.

Contact

Find us on Twitter/X at @tldraw or email us at mailto:hello@tldraw.com.