snapshot of https://github.com/tldraw/tldraw.git (main, 2026-09-23; blobs > 131072B omitted)
Some checks failed
Deploy analytics worker / Deploy analytics worker to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
i18n upload strings / Upload latest strings (push) Failing after 1s
Deploy bemo / Deploy bemo to ${{ (github.ref == 'refs/heads/bemo-production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
Deploy grafana resources / ${{ github.event_name == 'push' && 'Deploy grafana resources' || 'Validate grafana resources (dry run)' }} (push) Failing after 0s
End to end tests (dotcom) / Check for relevant changes (push) Failing after 0s
End to end tests (examples) / Check for relevant changes (push) Failing after 0s
End to end tests (mermaid readability) / Check for relevant changes (push) Failing after 1s
End to end tests (perf) / Check for relevant changes (push) Failing after 0s
Publish editor extensions / Publish editor extensions (push) Failing after 1s
Publish SDK packages / Remove the publish-packages label (push) Has been skipped
Checks / Tests & checks (push) Has been cancelled
Checks / Build all projects (push) Has been cancelled
Deploy templates / Deploy templates (push) Has been cancelled
End to end tests (dotcom) / End to end tests (dotcom) (push) Has been cancelled
End to end tests (mermaid readability) / End to end tests (mermaid readability) (push) Has been cancelled
End to end tests (perf) / End to end tests (perf) (push) Has been cancelled
Deploy .com / Deploy dotcom to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Has been cancelled
End to end tests (examples) / End to end tests (examples) (push) Has been cancelled
Publish SDK packages / Publish SDK packages (push) Has been cancelled
Publish SDK packages / Tell user released version (push) Has been cancelled
Publish SDK packages / Publishes code templates to separate repositories (push) Has been cancelled
Publish SDK packages / Deploys the template demos with the new SDK version (push) Has been cancelled
Some checks failed
Deploy analytics worker / Deploy analytics worker to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
i18n upload strings / Upload latest strings (push) Failing after 1s
Deploy bemo / Deploy bemo to ${{ (github.ref == 'refs/heads/bemo-production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
Deploy grafana resources / ${{ github.event_name == 'push' && 'Deploy grafana resources' || 'Validate grafana resources (dry run)' }} (push) Failing after 0s
End to end tests (dotcom) / Check for relevant changes (push) Failing after 0s
End to end tests (examples) / Check for relevant changes (push) Failing after 0s
End to end tests (mermaid readability) / Check for relevant changes (push) Failing after 1s
End to end tests (perf) / Check for relevant changes (push) Failing after 0s
Publish editor extensions / Publish editor extensions (push) Failing after 1s
Publish SDK packages / Remove the publish-packages label (push) Has been skipped
Checks / Tests & checks (push) Has been cancelled
Checks / Build all projects (push) Has been cancelled
Deploy templates / Deploy templates (push) Has been cancelled
End to end tests (dotcom) / End to end tests (dotcom) (push) Has been cancelled
End to end tests (mermaid readability) / End to end tests (mermaid readability) (push) Has been cancelled
End to end tests (perf) / End to end tests (perf) (push) Has been cancelled
Deploy .com / Deploy dotcom to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Has been cancelled
End to end tests (examples) / End to end tests (examples) (push) Has been cancelled
Publish SDK packages / Publish SDK packages (push) Has been cancelled
Publish SDK packages / Tell user released version (push) Has been cancelled
Publish SDK packages / Publishes code templates to separate repositories (push) Has been cancelled
Publish SDK packages / Deploys the template demos with the new SDK version (push) Has been cancelled
This commit is contained in:
769
apps/docs/content/starter-kits/agent.mdx
Normal file
769
apps/docs/content/starter-kits/agent.mdx
Normal file
@@ -0,0 +1,769 @@
|
||||
---
|
||||
title: Agent starter kit
|
||||
sidebarTitle: Agent
|
||||
status: published
|
||||
author: tldraw
|
||||
date: 9/16/2024
|
||||
order: 4
|
||||
description: The Agent Starter Kit demonstrates how to build an AI agent that can interpret and manipulate the [tldraw canvas](https://github.com/tldraw/tldraw). It features a chat panel on the righthand side of the screen where the user can communicate with the agent, add context and see chat history. It works as a foundation to create diagram generators, drawing assistants, or visual AI applications.
|
||||
embed: agent
|
||||
githubLink: https://github.com/tldraw/tldraw/tree/main/templates/agent
|
||||
keywords:
|
||||
- ai
|
||||
- agent
|
||||
- artificial intelligence
|
||||
- automation
|
||||
readability: 7
|
||||
voice: 6
|
||||
completeness: 9
|
||||
accuracy: 9
|
||||
---
|
||||
|
||||
To build with an agent starter kit, run this command in your terminal:
|
||||
|
||||
```bash
|
||||
npm create tldraw@latest -- --template agent
|
||||
```
|
||||
|
||||
Then create a `.dev.vars` file in the project root with API keys for the model providers you want to use:
|
||||
|
||||
```
|
||||
ANTHROPIC_API_KEY=<your key>
|
||||
GOOGLE_API_KEY=<your key>
|
||||
OPENAI_API_KEY=<your key>
|
||||
```
|
||||
|
||||
We recommend Anthropic for best results. Get an API key from the [Anthropic dashboard](https://console.anthropic.com/settings/keys).
|
||||
|
||||
---
|
||||
|
||||
## Use cases
|
||||
|
||||
Use the agent starter kit to build visual AI assistants that read and modify drawings, diagram generators that turn text descriptions into flowcharts or architecture diagrams, and tools that convert hand-drawn sketches into structured digital content.
|
||||
|
||||
## How it works
|
||||
|
||||
### What the agent can do
|
||||
|
||||
With its default configuration, the agent can perform the following actions:
|
||||
|
||||
- Create, update and delete shapes.
|
||||
- Draw freehand pen strokes.
|
||||
- Use higher-level operations on multiple shapes at once: rotate, resize, align, distribute, stack and reorder shapes.
|
||||
- Write out its thinking and send messages to the user.
|
||||
- Keep track of its task by writing and updating a todo list.
|
||||
- Move its viewport to look at different parts of the canvas.
|
||||
- Count shapes matching a given expression.
|
||||
- Schedule further work and reviews to be carried out in follow-up requests.
|
||||
- Call external APIs.
|
||||
|
||||
### Architecture
|
||||
|
||||

|
||||
|
||||
### 1. User input
|
||||
|
||||
To make decisions on what to do, the agent gathers information from various sources:
|
||||
|
||||
- The user's message.
|
||||
- The user's current selection of shapes.
|
||||
- What the user can currently see on their screen.
|
||||
- Any additional context the user has provided, such as specific shapes or a particular position or area on the canvas.
|
||||
- Actions the user has recently taken.
|
||||
- A screenshot of the agent's current view of the canvas.
|
||||
- A simplified format of all shapes within the agent's viewport.
|
||||
- Information about clusters of shapes outside the agent's viewport.
|
||||
- The history of the current session, including the user's messages and all of the agent's actions.
|
||||
- Lints identifying potential issues with shapes on the canvas.
|
||||
|
||||
### 2. Visual context system: Canvas understanding
|
||||
|
||||
The agent captures both visual screenshots and structured shape data from the canvas. This dual approach means the model understands both the visual layout and the underlying data structure, including spatial relationships between shapes.
|
||||
|
||||
The agent's "eyes" are defined by `PromptPartUtil` classes that gather different types of context, from user messages and canvas screenshots to shape data and interaction history.
|
||||
|
||||
### 3. Action system: Canvas manipulation
|
||||
|
||||
The agent performs canvas operations through a modular action system. Each action type handles a specific kind of canvas modification, from creating shapes to multi-step operations like aligning or distributing groups of shapes. Every action includes its own validation, execution, and chat-panel presentation logic.
|
||||
|
||||
The agent's "hands" are defined by `AgentActionUtil` classes that specify what operations the agent can perform on the canvas.
|
||||
|
||||
### 4. Mode system: Contextual capabilities
|
||||
|
||||
The agent operates in one of several modes, each with its own set of capabilities tailored to a specific kind of task. The default `working` mode has full access to the canvas. A narrower mode might strip away drawing actions and add critique-specific ones for reviewing work, or swap in different tools for a planning phase. Modes let the same agent take on different roles at different points in a task.
|
||||
|
||||
### 5. Streaming system: Real-time AI responses
|
||||
|
||||
AI responses stream in real time. Users see the agent's thinking and its canvas modifications as they happen. Shapes get created, updated, and deleted incrementally as each action finishes streaming, so the canvas stays responsive throughout long, multi-step tasks.
|
||||
|
||||
### 6. Manager architecture: State decomposition
|
||||
|
||||
The agent's state is organized into focused managers, each responsible for a single concern: chat history, model selection, contextual shapes, the todo list, mode transitions, and more. This decomposition keeps the agent's behavior easy to reason about as the template grows, and adding a new capability means adding a new manager rather than expanding a monolith.
|
||||
|
||||
### 7. Integration system: Model flexibility
|
||||
|
||||
This starter kit supports multiple AI providers (Anthropic, OpenAI, Google) with consistent interfaces. The system abstracts provider differences, so you can switch models or use different providers for different operations. Adding support for a new provider doesn't require changes to the rest of the system.
|
||||
|
||||
## Use the agent programmatically
|
||||
|
||||
Aside from using the chat panel UI, you can also prompt the agent programmatically.
|
||||
|
||||
Call the `prompt()` method to start an agentic loop. The agent will continue until it has finished the task you've given it.
|
||||
|
||||
```tsx
|
||||
// Inside a component wrapped by TldrawAgentAppProvider
|
||||
const agent = useAgent()
|
||||
agent.prompt('Draw a cat')
|
||||
```
|
||||
|
||||
You can optionally specify further details about the request in the form of an `AgentInput` object:
|
||||
|
||||
```tsx
|
||||
agent.prompt({
|
||||
message: 'Draw a cat in this area',
|
||||
bounds: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: 300,
|
||||
h: 400,
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
There are more methods on the `TldrawAgent` class that can help when building an agentic app:
|
||||
|
||||
- `agent.cancel()` - Cancel the agent's current task.
|
||||
- `agent.reset()` - Reset the agent's chat and memory.
|
||||
- `agent.request(input)` - Send a single request to the agent and handle its response _without_ entering into an agentic loop.
|
||||
|
||||
---
|
||||
|
||||
## Customize the agent
|
||||
|
||||
The agent's behavior is defined in `client/modes/AgentModeDefinitions.ts`. The `AGENT_MODE_DEFINITIONS` array contains mode definitions. Each mode has two arrays:
|
||||
|
||||
- `parts` determine what the agent can **see**.
|
||||
- `actions` determine what the agent can **do**.
|
||||
|
||||
Add, edit or remove an entry in either array to change what the agent can see or do in a given mode.
|
||||
|
||||
### The mode system
|
||||
|
||||
Modes control which parts and actions the agent has access to at any given time. They're defined in `client/modes/AgentModeDefinitions.ts`.
|
||||
|
||||
The default `working` mode includes all standard capabilities. You can create additional modes with different subsets of parts and actions.
|
||||
|
||||
Call `agent.mode.setMode(modeType)` to change modes during a prompt. To control each mode's lifecycle, implement any of the lifecycle hooks in `client/modes/AgentModeChart.ts`:
|
||||
|
||||
- `onEnter(agent, fromMode)` - runs when the agent enters a mode.
|
||||
- `onExit(agent, toMode)` - runs when the agent exits a mode.
|
||||
- `onPromptStart(agent, request)` - runs when a prompt commences.
|
||||
- `onPromptEnd(agent, request)` - runs when a prompt ends.
|
||||
- `onPromptCancel(agent, request)` - runs when a prompt is canceled.
|
||||
|
||||
### Change what the agent can see
|
||||
|
||||
**Change what the agent can see by adding, editing or removing a prompt part.**
|
||||
|
||||
Prompt parts assemble the prompt that the model receives, with each part adding a different piece of information. This includes the user's message, the model name, the system prompt, chat history and more.
|
||||
|
||||
A prompt part has two pieces: a `PromptPartUtil` class on the client that gathers data, and a `PromptPartDefinition` on the worker that turns that data into messages.
|
||||
|
||||
This example shows how to let the model see what the current time is.
|
||||
|
||||
First, define a prompt part type:
|
||||
|
||||
```tsx
|
||||
interface TimePart extends BasePromptPart<'time'> {
|
||||
time: string
|
||||
}
|
||||
```
|
||||
|
||||
Then, create a prompt part util in `client/parts/`:
|
||||
|
||||
```tsx
|
||||
export const TimePartUtil = registerPromptPartUtil(
|
||||
class TimePartUtil extends PromptPartUtil<TimePart> {
|
||||
static override type = 'time' as const
|
||||
|
||||
override getPart(): TimePart {
|
||||
return {
|
||||
type: 'time',
|
||||
time: new Date().toLocaleTimeString(),
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
Next, create the prompt part definition in `shared/schema/PromptPartDefinitions.ts`:
|
||||
|
||||
```tsx
|
||||
export const TimePartDefinition: PromptPartDefinition<TimePart> = {
|
||||
type: 'time',
|
||||
priority: -100,
|
||||
buildContent({ time }: TimePart) {
|
||||
return [`The user's current time is: ${time}`]
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
To enable the prompt part, import its util in `client/modes/AgentModeDefinitions.ts` and add `TimePartUtil.type` to a mode's `parts` array. Its methods will be used to assemble its data and send it to the model.
|
||||
|
||||
- `getPart()` - Gather any data needed to construct the prompt.
|
||||
- `buildContent()` - Turn the data into messages to send to the model.
|
||||
|
||||
There are other fields available on the `PromptPartDefinition` interface that you can override for more granular control.
|
||||
|
||||
- `priority` - Control where this prompt part will appear in the list of messages sent to the model. Higher priority appears later in the prompt.
|
||||
- `getModelName()` - Determine which AI model to use.
|
||||
- `buildMessages()` - Manually override how prompt messages are constructed from the prompt part.
|
||||
|
||||
### Change what the agent can do
|
||||
|
||||
**Change what the agent can do by adding, editing or removing an `AgentActionUtil`.**
|
||||
|
||||
Agent action utils define which actions the agent can perform. Each `AgentActionUtil` adds a different capability.
|
||||
|
||||
This example shows how to allow the agent to clear the screen.
|
||||
|
||||
First, define an agent action by creating a schema for it in `shared/schema/AgentActionSchemas.ts`:
|
||||
|
||||
```tsx
|
||||
export const ClearAction = z
|
||||
// All agent actions must have a _type field
|
||||
// The underscore encourages the model to put this field first
|
||||
.object({
|
||||
_type: z.literal('clear'),
|
||||
})
|
||||
// A title and description tell the model what the action does
|
||||
.meta({
|
||||
title: 'Clear',
|
||||
description: 'The agent deletes all shapes on the canvas.',
|
||||
})
|
||||
|
||||
// Infer the action's type
|
||||
export type ClearAction = z.infer<typeof ClearAction>
|
||||
```
|
||||
|
||||
Then, create an agent action util in `client/actions/`:
|
||||
|
||||
```tsx
|
||||
export const ClearActionUtil = registerActionUtil(
|
||||
class ClearActionUtil extends AgentActionUtil<ClearAction> {
|
||||
static override type = 'clear' as const
|
||||
|
||||
override applyAction(action: Streaming<ClearAction>) {
|
||||
// Don't do anything until the action has finished streaming
|
||||
if (!action.complete) return
|
||||
|
||||
// Get the editor
|
||||
const { editor } = this
|
||||
|
||||
// Delete all shapes on the page
|
||||
const shapes = editor.getCurrentPageShapes()
|
||||
editor.deleteShapes(shapes)
|
||||
}
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
To enable the agent action, import its util in `client/modes/AgentModeDefinitions.ts` and add `ClearActionUtil.type` to a mode's `actions` array. Its method will be used to execute the action.
|
||||
|
||||
- `applyAction()` - Execute the action.
|
||||
|
||||
There are other methods available on the `AgentActionUtil` class that you can override for more granular control.
|
||||
|
||||
- `getInfo()` - Determine how the action gets displayed in the chat panel UI.
|
||||
- `savesToHistory()` - Control whether actions get saved to chat history or not.
|
||||
- `sanitizeAction()` - Apply transformations to the action before saving it to history and applying it. More details on transformations below.
|
||||
|
||||
### Change how actions appear in chat history
|
||||
|
||||
**Configure the icon and description of an action in the chat panel UI using the `getInfo()` method.**
|
||||
|
||||
```tsx
|
||||
override getInfo() {
|
||||
return {
|
||||
icon: 'trash' as const,
|
||||
description: 'Cleared the canvas',
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
You can make an action collapsible by adding a `summary` property.
|
||||
|
||||
```tsx
|
||||
override getInfo() {
|
||||
return {
|
||||
summary: 'Cleared the canvas',
|
||||
description: 'After much consideration, the agent decided to clear the canvas',
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
To customize an action's appearance via CSS, define styles for the `agent-action-type-{TYPE}` class where `{TYPE}` is the type of the action.
|
||||
|
||||
```css
|
||||
.agent-action-type-clear {
|
||||
color: red;
|
||||
}
|
||||
```
|
||||
|
||||
### Managers
|
||||
|
||||
Managers are classes that extend `TldrawAgent` or `TldrawAgentApp` with a focused responsibility, such as chat history, model selection, or context management. They're available as properties on the agent instance, for example `agent.chat`, `agent.modelName`, and `agent.context`.
|
||||
|
||||
To add a custom manager, extend `BaseAgentManager` or `BaseAgentAppManager` and attach it to the agent in `client/agent/TldrawAgent.ts`.
|
||||
|
||||
### Registering utils
|
||||
|
||||
Utils use a self-registration pattern. When you create a new `PromptPartUtil` or `AgentActionUtil`, wrap it with a registration function:
|
||||
|
||||
```tsx
|
||||
export const MyPartUtil = registerPromptPartUtil(
|
||||
class MyPartUtil extends PromptPartUtil<MyPart> {
|
||||
// ...
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
This ensures the util is discovered automatically when its module is imported in `AgentModeDefinitions.ts`.
|
||||
|
||||
### Mode-scoped actions
|
||||
|
||||
Different modes can implement actions with the same `_type`. For example, a `team-member` mode and a `solo` mode might both have a `mark-task-done` action, but with different implementations.
|
||||
|
||||
To register a mode-specific action util, pass the `forModes` option:
|
||||
|
||||
```tsx
|
||||
export const MarkTeamMemberTaskDoneActionUtil = registerActionUtil(
|
||||
class MarkTeamMemberTaskDoneActionUtil extends AgentActionUtil<MarkTeamMemberTaskDoneAction> {
|
||||
static override type = 'mark-task-done' as const
|
||||
override applyAction(action: Streaming<MarkTeamMemberTaskDoneAction>) {
|
||||
// Team member-specific implementation
|
||||
}
|
||||
},
|
||||
{ forModes: ['team-member'] }
|
||||
)
|
||||
```
|
||||
|
||||
Default schemas are auto-registered when exported from `AgentActionSchemas.ts`. For mode-specific schemas, call `registerActionSchema` explicitly with the `forModes` option.
|
||||
|
||||
See the [template README](https://github.com/tldraw/tldraw/blob/main/templates/agent/README.md#mode-scoped-actions) for a full worked example.
|
||||
|
||||
### Schedule further work
|
||||
|
||||
You can let the agent work over multiple turns by scheduling further work using the `schedule` method as part of an action.
|
||||
|
||||
This example shows how to schedule an extra step for adding detail to the canvas.
|
||||
|
||||
```tsx
|
||||
override applyAction(action: Streaming<AddDetailAction>) {
|
||||
if (!action.complete) return
|
||||
if (!this.agent) return
|
||||
this.agent.schedule('Add more detail to the canvas.')
|
||||
}
|
||||
```
|
||||
|
||||
As with the `prompt` method, you can specify further details about the request.
|
||||
|
||||
```tsx
|
||||
agent.schedule({
|
||||
message: 'Add more detail in this area.',
|
||||
bounds: { x: 0, y: 0, w: 100, h: 100 },
|
||||
})
|
||||
```
|
||||
|
||||
You can schedule multiple things by calling the `schedule()` method more than once.
|
||||
|
||||
```tsx
|
||||
agent.schedule('Add more detail to the canvas.')
|
||||
agent.schedule('Check for spelling mistakes.')
|
||||
```
|
||||
|
||||
To interrupt the agent with a new prompt instead of waiting for the current prompt to end, use the `interrupt` method. `interrupt` also lets you specify a mode to transition into.
|
||||
|
||||
```tsx
|
||||
override applyAction(action: Streaming<EnterReviewingModeAction>) {
|
||||
if (!action.complete) return
|
||||
this.agent.interrupt({
|
||||
mode: 'reviewing',
|
||||
input: {
|
||||
message: 'Review the new area thoroughly for any mistakes',
|
||||
bounds: action.bounds,
|
||||
},
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### Retrieve data from an external API
|
||||
|
||||
To let the agent retrieve information from an external API, fetch the data within `applyAction` and schedule a follow-up request with any data you want the agent to have access to.
|
||||
|
||||
```tsx
|
||||
override async applyAction(action: Streaming<CountryInfoAction>) {
|
||||
if (!action.complete) return
|
||||
if (!this.agent) return
|
||||
|
||||
// Fetch from the external API
|
||||
const data = await fetchCountryInfo(action.code)
|
||||
|
||||
// Schedule a follow-up request with the data
|
||||
this.agent.schedule({ data: [data] })
|
||||
}
|
||||
```
|
||||
|
||||
### Sanitize data received from the model
|
||||
|
||||
The model can make mistakes. Sometimes this is due to hallucinations, and sometimes this is due to the canvas changing since the last time the model saw it. Either way, an incoming action might contain invalid data by the time you receive it.
|
||||
|
||||
To correct incoming mistakes, apply fixes in the `sanitizeAction()` method of an action util. They'll get carried out before the action is applied to the editor or saved to chat history.
|
||||
|
||||
For example, ensure that a shape ID received from the model refers to an existing shape by using the `ensureShapeIdExists()` method.
|
||||
|
||||
```tsx
|
||||
override sanitizeAction(action: Streaming<DeleteAction>, helpers: AgentHelpers) {
|
||||
if (!action.complete) return action
|
||||
|
||||
// Ensure the shape ID refers to an existing shape
|
||||
action.shapeId = helpers.ensureShapeIdExists(action.shapeId)
|
||||
|
||||
// If the shape ID doesn't refer to an existing shape, cancel the action
|
||||
if (!action.shapeId) return null
|
||||
|
||||
return action
|
||||
}
|
||||
```
|
||||
|
||||
The `AgentHelpers` class contains more helpers for sanitizing data received from the model.
|
||||
|
||||
- `ensureShapeIdExists()` - Ensure that a shape ID refers to a real shape. Useful for interacting with existing shapes.
|
||||
- `ensureShapeIdIsUnique()` - Ensure that a shape ID is unique. Useful for creating new shapes.
|
||||
- `ensureValueIsVec()`, `ensureValueIsNumber()` - Ensure that a value is a certain type. Useful for more complex actions where the model is more likely to make mistakes.
|
||||
|
||||
### Send positions to and from the model
|
||||
|
||||
By default, every position sent to the model is offset by the starting position of the current chat.
|
||||
|
||||
**To apply this offset to a position sent to the model, use the `applyOffsetToVec` method.**
|
||||
|
||||
```tsx
|
||||
override getPart(request: AgentRequest, helpers: AgentHelpers): ViewportCenterPart {
|
||||
if (!this.editor) return { part: 'user-viewport-center', center: null }
|
||||
|
||||
// Get the center of the user's viewport
|
||||
const viewportCenter = this.editor.getViewportBounds().center
|
||||
|
||||
// Apply the chat's offset to the vector
|
||||
const offsetViewportCenter = helpers.applyOffsetToVec(viewportCenter)
|
||||
|
||||
// Return the prompt part
|
||||
return {
|
||||
part: 'user-viewport-center',
|
||||
center: offsetViewportCenter,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**To remove the offset from a position received from the model, use the `removeOffsetFromVec` method.**
|
||||
|
||||
```tsx
|
||||
override applyAction(action: Streaming<MoveAction>, helpers: AgentHelpers) {
|
||||
if (!action.complete) return
|
||||
|
||||
// Remove the offset from the position
|
||||
const position = helpers.removeOffsetFromVec({ x: action.x, y: action.y })
|
||||
|
||||
// Do something with the position...
|
||||
}
|
||||
```
|
||||
|
||||
It's a good idea to round numbers before sending them to the model. If you want to be able to restore the original number later, use the `roundAndSaveNumber` and `unroundAndRestoreNumber` methods.
|
||||
|
||||
```tsx
|
||||
// In `getPart`...
|
||||
const roundedX = helpers.roundAndSaveNumber(x, 'my_key_x')
|
||||
const roundedY = helpers.roundAndSaveNumber(y, 'my_key_y')
|
||||
|
||||
// In `applyAction`...
|
||||
const unroundedX = helpers.unroundAndRestoreNumber(x, 'my_key_x')
|
||||
const unroundedY = helpers.unroundAndRestoreNumber(y, 'my_key_y')
|
||||
```
|
||||
|
||||
To round all the numbers on a shape, use the `roundShape` and `unroundShape` methods. See the [Send shapes to the model](#send-shapes-to-the-model) section below for more details.
|
||||
|
||||
### Send shapes to the model
|
||||
|
||||
By default, the agent converts tldraw shapes to various simplified formats to improve the model's understanding and performance.
|
||||
|
||||
There are three main formats used in this starter:
|
||||
|
||||
- `BlurryShape` - The format for shapes within the agent's viewport. It contains a shape's bounds, its ID, its type, and any text it contains. The "blurry" name refers to the fact that the agent can't make out the details of shapes from this format. Instead, it gives the model an overview of what it's looking at.
|
||||
- `FocusedShape` - The format for shapes that the agent is focusing on, such as shapes you've manually added to its context. The format contains most of a shape's properties, including color, fill, alignment, and any other shape-specific information. This is also the format the model outputs when creating shapes.
|
||||
- `PeripheralShapeCluster` - The format for shapes outside the agent's viewport. Nearby shapes are grouped together into clusters, each with the group's bounds and a count of how many shapes are inside it. This is the least detailed format. Its role is to give the model an awareness of shapes elsewhere on the page.
|
||||
|
||||
To send the model some shapes in one of these formats, use one of the conversion functions found within the `shared/format` folder, such as `convertTldrawShapeToFocusedShape()`.
|
||||
|
||||
This example picks one random shape on the canvas and sends it to the model in the Focused format.
|
||||
|
||||
```tsx
|
||||
override getPart(request: AgentRequest, helpers: AgentHelpers): RandomShapePart {
|
||||
const { editor } = this
|
||||
if (!this.editor) return { type: 'random-shape', shape: null}
|
||||
|
||||
// Get a random shape
|
||||
const shapes = editor.getCurrentPageShapes()
|
||||
const randomShape = shapes[Math.floor(Math.random() * shapes.length)]
|
||||
|
||||
// Convert the shape to the Focused format
|
||||
const focusedShape = convertTldrawShapeToFocusedShape(editor, randomShape)
|
||||
|
||||
// Normalize the shape's position
|
||||
const offsetShape = helpers.applyOffsetToShape(focusedShape)
|
||||
const roundedShape = helpers.roundShape(offsetShape)
|
||||
|
||||
return {
|
||||
type: 'random-shape',
|
||||
shape: roundedShape,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Change the system prompt
|
||||
|
||||
To change the default system prompt, edit the sections in `worker/prompt/sections/`. These sections are assembled by `worker/prompt/buildSystemPrompt.ts`.
|
||||
|
||||
The system prompt is rebuilt for each step in the agentic loop depending on which actions and parts are available in the agent's current mode. To give the model more detailed instructions for how to use any custom actions or parts you add, edit `worker/prompt/sections/rules-section.ts`.
|
||||
|
||||
### Change to a different model
|
||||
|
||||
You can set an agent's model by calling `setModelName` on the `modelName` manager.
|
||||
|
||||
```tsx
|
||||
agent.modelName.setModelName('gemini-3.8-flash')
|
||||
```
|
||||
|
||||
To override an agent's model, specify a different model name with a request.
|
||||
|
||||
```tsx
|
||||
agent.prompt({
|
||||
modelName: 'gemini-3.8-flash',
|
||||
message: 'Draw a diagram of a volcano.',
|
||||
})
|
||||
```
|
||||
|
||||
You can conditionally override the model name by overriding the `getModelName()` method on any `PromptPartDefinition`.
|
||||
|
||||
```tsx
|
||||
override getModelName(part: MyCustomPromptPart) {
|
||||
return part.fastMode ? 'gemini-3.8-flash' : 'claude-sonnet-5'
|
||||
}
|
||||
```
|
||||
|
||||
### Support a different model
|
||||
|
||||
To add support for a different model, add the model's definition to `AGENT_MODEL_DEFINITIONS` in the `shared/models.ts` file.
|
||||
|
||||
```tsx
|
||||
'claude-sonnet-5': {
|
||||
name: 'claude-sonnet-5',
|
||||
id: 'claude-sonnet-5',
|
||||
provider: 'anthropic',
|
||||
supportsPrefill: false,
|
||||
supportsTemperature: false,
|
||||
thinking: 'adaptive',
|
||||
effort: 'low',
|
||||
}
|
||||
```
|
||||
|
||||
If you need to add any extra setup or configuration for your provider, you can add it to the `worker/do/AgentService.ts` file.
|
||||
|
||||
### Support custom shapes
|
||||
|
||||
If your app includes [custom shapes](/docs/shapes#Custom-shapes), the agent will be able to see, move, delete, resize, rotate and arrange them with no extra setup. However, you might want to also let the agent create and edit them, and read their custom properties.
|
||||
|
||||
To support custom shapes, you have two main options:
|
||||
|
||||
1. Add an action that lets the agent create your custom shape.
|
||||
|
||||
See the [Let the agent create custom shapes with an action](#let-the-agent-create-a-custom-shape-with-an-action) section below.
|
||||
|
||||
2. Add your custom shape to the schema so that the agent read, edit and create it like any other shape.
|
||||
|
||||
See the [Add your custom shape to the schema](#add-a-custom-shape-to-the-schema) section below.
|
||||
|
||||
#### Let the agent create a custom shape with an action
|
||||
|
||||
To add partial support for a custom shape, let the agent create it with an [agent action](#change-what-the-agent-can-do). For example, this action lets the agent create a custom "sticker" shape:
|
||||
|
||||
```tsx
|
||||
// In shared/schema/AgentActionSchemas.ts
|
||||
export const StickerAction = z
|
||||
.object({
|
||||
_type: z.literal('sticker'),
|
||||
stickerType: z.enum(['❤️', '⭐']),
|
||||
x: z.number(),
|
||||
y: z.number(),
|
||||
})
|
||||
.meta({
|
||||
title: 'Sticker',
|
||||
description: 'Add a sticker to the canvas.',
|
||||
})
|
||||
|
||||
export type StickerAction = z.infer<typeof StickerAction>
|
||||
```
|
||||
|
||||
Define how the action gets applied to the canvas by creating an action util:
|
||||
|
||||
```tsx
|
||||
// In client/actions/StickerActionUtil.ts
|
||||
export const StickerActionUtil = registerActionUtil(
|
||||
class StickerActionUtil extends AgentActionUtil<StickerAction> {
|
||||
static override type = 'sticker' as const
|
||||
|
||||
// Tell the model how to display the action in chat history
|
||||
override getInfo(action: Streaming<StickerAction>) {
|
||||
return {
|
||||
icon: 'pencil' as const,
|
||||
description: 'Added a sticker',
|
||||
}
|
||||
}
|
||||
|
||||
// Execute the action
|
||||
override applyAction(action: Streaming<StickerAction>, helpers: AgentHelpers) {
|
||||
if (!action.complete) return
|
||||
if (!this.editor) return
|
||||
|
||||
// Normalize the position
|
||||
const position = helpers.removeOffsetFromVec({ x: action.x, y: action.y })
|
||||
|
||||
// Create the custom shape
|
||||
this.editor.createShape({
|
||||
type: 'sticker',
|
||||
id: createShapeId(),
|
||||
x: position.x,
|
||||
y: position.y,
|
||||
props: { stickerType: action.stickerType },
|
||||
})
|
||||
}
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
#### Add a custom shape to the schema
|
||||
|
||||
To let the agent see the custom properties of your custom shape, add it to the schema in `shared/format/FocusedShape.ts`.
|
||||
|
||||
For example, here's a schema for a custom sticker shape.
|
||||
|
||||
```tsx
|
||||
const FocusedStickerShape = z
|
||||
.object({
|
||||
// Required properties
|
||||
_type: z.literal('sticker'),
|
||||
note: z.string(),
|
||||
shapeId: z.string(),
|
||||
|
||||
// Custom properties
|
||||
stickerType: z.enum(['❤️', '⭐']),
|
||||
x: z.number(),
|
||||
y: z.number(),
|
||||
})
|
||||
.meta({
|
||||
// Information about the shape to give to the agent
|
||||
title: 'Sticker Shape',
|
||||
description:
|
||||
'A sticker shape is a small symbol stamped onto the canvas. There are two types of stickers: heart and star.',
|
||||
})
|
||||
```
|
||||
|
||||
The `_type` and `shapeId` properties are required so that the app can identify your shape. The `note` property is also required. The agent uses it to leave notes for itself.
|
||||
|
||||
For optional properties, it's worth considering how the agent should see your custom shape. You might want to leave out some properties and focus on showing the most important ones. It's also best to keep them in alphabetical order for better performance with Gemini models.
|
||||
|
||||
Enable your custom shape schema by adding it to the list of `FOCUSED_SHAPES` in the same file.
|
||||
|
||||
```tsx
|
||||
const FOCUSED_SHAPES = [
|
||||
FocusedDrawShape,
|
||||
FocusedGeoShape,
|
||||
FocusedLineShape,
|
||||
FocusedTextShape,
|
||||
FocusedArrowShape,
|
||||
FocusedNoteShape,
|
||||
FocusedUnknownShape,
|
||||
|
||||
// Our custom shape
|
||||
FocusedStickerShape,
|
||||
] as const
|
||||
```
|
||||
|
||||
Tell the app how to convert your custom shape into the `FocusedShape` format by adding a case in `shared/format/convertTldrawShapeToFocusedShape.ts`:
|
||||
|
||||
```tsx
|
||||
export function convertTldrawShapeToFocusedShape(editor: Editor, shape: TLShape): FocusedShape {
|
||||
switch (shape.type) {
|
||||
// ...
|
||||
case 'sticker':
|
||||
const bounds = getShapeBounds(shape)
|
||||
return {
|
||||
_type: 'sticker',
|
||||
note: (shape.meta.note as string) ?? '',
|
||||
shapeId: convertTldrawIdToSimpleId(shape.id),
|
||||
stickerType: shape.props.stickerType,
|
||||
x: bounds.x,
|
||||
y: bounds.y,
|
||||
}
|
||||
// ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
To allow the agent to edit your custom shape's properties, tell the app how to convert your shape from the `FocusedShape` format that the model outputs to the actual format of your shape:
|
||||
|
||||
```tsx
|
||||
export function convertFocusedShapeToTldrawShape(
|
||||
editor: Editor,
|
||||
focusedShape: FocusedShape,
|
||||
{ defaultShape }: { defaultShape: Partial<TLShape> }
|
||||
) {
|
||||
switch (focusedShape._type) {
|
||||
// ...
|
||||
case 'sticker':
|
||||
const shapeId = convertSimpleIdToTldrawId(focusedShape.shapeId)
|
||||
return {
|
||||
shape: {
|
||||
id: shapeId,
|
||||
x: focusedShape.x,
|
||||
y: focusedShape.y,
|
||||
// ...
|
||||
props: {
|
||||
// ...
|
||||
stickerType: focusedShape.stickerType,
|
||||
},
|
||||
meta: {
|
||||
note: focusedShape.note ?? '',
|
||||
},
|
||||
},
|
||||
}
|
||||
// ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Further reading
|
||||
|
||||
- **[Multiplayer starter kit](/starter-kits/multiplayer)**: Use the tldraw multiplayer sync starter kit to build multi-user agent environments.
|
||||
|
||||
- **[Shape utilities](/docs/shapes#Custom-shapes)**: Learn how to create custom shapes and extend tldraw's shape system with advanced geometry, rendering, and interaction patterns.
|
||||
|
||||
- **[Editor state management](/docs/editor)**: Learn how to work with tldraw's reactive state system, editor lifecycle, and event handling for complex canvas applications.
|
||||
|
||||
---
|
||||
|
||||
## Building with this starter kit?
|
||||
|
||||
If you build something great, please share it with us in our [#show-and-tell](https://discord.tldraw.com/?utm_source=docs&utm_medium=organic&utm_campaign=sociallink) channel on Discord. We want to see what you've built!
|
||||
109
apps/docs/content/starter-kits/branching-chat.mdx
Normal file
109
apps/docs/content/starter-kits/branching-chat.mdx
Normal file
@@ -0,0 +1,109 @@
|
||||
---
|
||||
title: Branching chat starter kit
|
||||
sidebarTitle: Branching chat
|
||||
status: published
|
||||
author: tldraw
|
||||
date: 9/16/2024
|
||||
order: 6
|
||||
description: The Branching Chat Starter Kit demonstrates how to build visual conversation interfaces where users create branching chat trees with AI integration. It features interactive dialogue nodes that connect to form conversation flows, with real-time AI responses and context-aware message handling. You can use this foundation to create interactive chatbots, conversation design tools, or storytelling applications.
|
||||
embed: branching-chat
|
||||
githubLink: https://github.com/tldraw/tldraw/tree/main/templates/branching-chat
|
||||
keywords:
|
||||
- branching
|
||||
- chat
|
||||
- conversation
|
||||
- dialogue
|
||||
- tree
|
||||
readability: 8
|
||||
voice: 6
|
||||
completeness: 8
|
||||
accuracy: 8
|
||||
notes: "'Use Cases' heading should be sentence case. Uses bolded bullet format. Good architecture explanation and file references."
|
||||
---
|
||||
|
||||
To build with a branching chat starter kit, run this command in your terminal:
|
||||
|
||||
```bash
|
||||
npm create tldraw@latest -- --template branching-chat
|
||||
```
|
||||
|
||||
Then create a `.env` file in the project root with an API key from [Google AI Studio](https://aistudio.google.com/apikey):
|
||||
|
||||
```
|
||||
GOOGLE_GENERATIVE_AI_API_KEY=<your key>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Use cases
|
||||
|
||||
Use the branching chat starter kit to build:
|
||||
|
||||
<StarterKitBento type="branching" />
|
||||
|
||||
---
|
||||
|
||||
## How it works
|
||||
|
||||

|
||||
|
||||
### 1. Visual node system: Interactive message containers
|
||||
|
||||
Each conversation message appears as a draggable node on the infinite canvas. The `NodeShapeUtil` class extends tldraw's shape system to create custom chat message containers. These containers dynamically resize based on content length and provide input fields for user messages, areas for AI responses, and connection ports for linking conversations. You can also create nodes directly from the toolbar.
|
||||
|
||||
### 2. Connection architecture: Conversation flow management
|
||||
|
||||
The `ConnectionBindingUtil` manages relationships between message nodes and creates visual lines that represent conversation flow. You create connections by dragging between node ports to branch conversations and link context. When users send a message, the system traces backwards through connected nodes to build complete conversation history. This gives AI responses the full dialogue context.
|
||||
|
||||
### 3. AI streaming integration: Real-time response handling
|
||||
|
||||
The backend uses Cloudflare Workers with the Vercel AI SDK to stream responses from Google's Gemini API. As AI text generates, the streaming fetch implementation decodes response chunks and updates the tldraw document state in real time. Users see responses appear progressively within the connected node.
|
||||
|
||||
### 4. Port-based connection system: Visual conversation linking
|
||||
|
||||
Nodes feature input and output ports that let users create branching dialogue structures by dragging connections between messages. You can build complex conversation trees where multiple messages feed into a single AI response or diverge into parallel branches. Connected nodes automatically establish context relationships across different paths in the dialogue graph.
|
||||
|
||||
---
|
||||
|
||||
## Customization
|
||||
|
||||
This starter kit is built on top of tldraw's extensible architecture. You can customize everything. The canvas renders using React DOM, so you can use familiar React patterns, components, and state management across your conversation interface. Let's have a look at some ways to change this starter kit.
|
||||
|
||||
### Adding custom node types
|
||||
|
||||
To create new types of conversation nodes beyond basic messages, you can extend the node system with custom node definitions. The system uses a pluggable architecture where each node type defines its own behavior, rendering, and port configuration.
|
||||
|
||||
See `client/nodes/types/MessageNode.tsx` as an example. This file shows how to define a complete node type with TypeScript validation, React component rendering, AI streaming integration, and dynamic sizing based on content length.
|
||||
|
||||
### Customizing AI integration
|
||||
|
||||
To integrate with different AI providers or modify response behavior, you can customize the streaming implementation and API endpoints. The system uses the Vercel AI SDK which supports multiple providers including OpenAI, Anthropic, and Google.
|
||||
|
||||
See `worker/worker.ts` as an example. This file demonstrates how to configure AI providers, handle streaming responses, and process conversation context from connected nodes.
|
||||
|
||||
### Customizing node appearance
|
||||
|
||||
To modify how conversation nodes look and behave, you can override the node rendering and styling system. Each node type has complete control over its visual presentation while maintaining integration with tldraw's interaction system.
|
||||
|
||||
See `client/nodes/NodeShapeUtil.tsx` as an example. This file shows how the shape utility defines node geometry, interaction behavior, and visual indicators including port positioning and selection bounds.
|
||||
|
||||
### Customizing connection behavior
|
||||
|
||||
To modify how conversation flows connect and interact, you can customize the connection and binding system. This controls how nodes link together, how context flows between connected messages, and how the visual connections appear.
|
||||
|
||||
See `client/connection/ConnectionBindingUtil.tsx` as an example. This file demonstrates how to define binding behavior between shapes, including automatic cleanup when nodes are deleted and visual feedback during connection creation.
|
||||
|
||||
---
|
||||
|
||||
## Further reading
|
||||
|
||||
- [Workflow starter kit](/starter-kits/workflow): Learn how to build visual programming interfaces with node-based workflows on infinite canvas.
|
||||
- [Shape utilities](/docs/shapes#Custom-shapes): Learn how to create custom shapes and extend tldraw's shape system with advanced geometry, rendering, and interaction patterns.
|
||||
- [Binding system](/sdk-features/bindings): Learn more about tldraw's binding system for creating relationships between shapes, automatic updates, and connection management.
|
||||
- [Editor state management](/docs/editor): Learn how to work with tldraw's reactive state system, editor lifecycle, and event handling for complex canvas applications.
|
||||
|
||||
---
|
||||
|
||||
## Building with this starter kit?
|
||||
|
||||
If you build something great, please share it with us in our [#show-and-tell](https://discord.tldraw.com/?utm_source=docs&utm_medium=organic&utm_campaign=sociallink) channel on Discord. We want to see what you've built!
|
||||
90
apps/docs/content/starter-kits/chat.mdx
Normal file
90
apps/docs/content/starter-kits/chat.mdx
Normal file
@@ -0,0 +1,90 @@
|
||||
---
|
||||
title: Chat starter kit
|
||||
sidebarTitle: Chat
|
||||
status: published
|
||||
author: tldraw
|
||||
date: 9/16/2024
|
||||
order: 3
|
||||
description: The Chat Starter Kit shows how to integrate tldraw's infinite canvas into an existing chat application. Built on [Vercel's useChat hook](https://ai-sdk.dev/docs/reference/ai-sdk-ui/use-chat), this template shows you how to add visual annotation capabilities to any app (in this case, a chat).
|
||||
embed: chat
|
||||
githubLink: https://github.com/tldraw/tldraw/tree/main/templates/chat
|
||||
keywords:
|
||||
- chat
|
||||
- messaging
|
||||
- collaboration
|
||||
- communication
|
||||
---
|
||||
|
||||
To build with a chat starter kit, run this command in your terminal:
|
||||
|
||||
```bash
|
||||
npm create tldraw@latest -- --template chat
|
||||
```
|
||||
|
||||
Once you've followed the instructions, you'll need to create a `.env.local` file with an API key from [Google AI Studio](https://aistudio.google.com/apikey):
|
||||
|
||||
```
|
||||
GOOGLE_GENERATIVE_AI_API_KEY=<your key>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Use cases
|
||||
|
||||
This starter kit shows how to add visual capabilities to:
|
||||
|
||||
<StarterKitBento type="chat" />
|
||||
|
||||
---
|
||||
|
||||
## How it works
|
||||
|
||||
This starter kit demonstrates how to add tldraw's visual capabilities to an existing chat application. The integration uses a modal to bring annotation tools directly into your chat UI.
|
||||
|
||||

|
||||
|
||||
### 1. Whiteboard modal
|
||||
|
||||
The whiteboard opens a modal tldraw editor whenever users want to create or edit visual content. The modal supports uploaded images, freehand drawing, and editing of images from chat history.
|
||||
|
||||
### 2. Automatic image annotation
|
||||
|
||||
When an image is uploaded for the first time, the editor places it on the canvas, centers the camera on it, and switches into cropping mode using `editor.setCurrentTool('select.crop')`. From there, users can quickly crop their image to the desired section or add annotations on top.
|
||||
|
||||
### 3. Snapshot saving
|
||||
|
||||
Once the user has finished sketching or annotating, the editor exports the work as a PNG ready to send to the LLM. At the same time, it saves a tldraw snapshot of the session. This allows users to reopen the image as a whiteboard and continue working from where they left off, without flattening the individual shapes into pixels.
|
||||
|
||||
---
|
||||
|
||||
## Customization
|
||||
|
||||
This starter kit provides a minimal integration between tldraw and a chat app. You can tailor the modal's appearance, available tools, and overall behavior.
|
||||
|
||||
### Modifying the upload workflow
|
||||
|
||||
You can customize the automatic crop behavior, add image processing steps, or extend support for additional file types to match your app's needs.
|
||||
|
||||
See `src/components/WhiteboardModal.tsx:195` for the automatic crop activation and `src/components/Chat.tsx:148` for drag-and-drop handling.
|
||||
|
||||
### Adapting to your design system
|
||||
|
||||
Since tldraw is built with React, you can adapt both the modal behavior and its appearance to integrate with your chat interface. This includes controlling when the modal opens, how it looks, and which tools are available.
|
||||
|
||||
For example, see `src/components/WhiteboardModal.tsx:92` for UI component overrides, `src/components/Chat.tsx:113` for the history image click handler, and `src/app/styles.css` for styling.
|
||||
|
||||
---
|
||||
|
||||
## Further reading
|
||||
|
||||
- **[Custom shape utilities](/docs/shapes#Custom-shapes)**: Learn how to create custom shapes with their own interactions.
|
||||
|
||||
- **[Editor API](/docs/editor)**: Learn about the tldraw Editor.
|
||||
|
||||
- **[Customize the user interface](/docs/user-interface)**: Learn how to customize the user interface of your tldraw application.
|
||||
|
||||
---
|
||||
|
||||
## Building with this starter kit?
|
||||
|
||||
If you build something great, please share it with us in our [#show-and-tell](https://discord.tldraw.com/?utm_source=docs&utm_medium=organic&utm_campaign=sociallink) channel on Discord. We want to see what you've built!
|
||||
135
apps/docs/content/starter-kits/image-pipeline.mdx
Normal file
135
apps/docs/content/starter-kits/image-pipeline.mdx
Normal file
@@ -0,0 +1,135 @@
|
||||
---
|
||||
title: Image pipeline starter kit
|
||||
sidebarTitle: Image pipeline
|
||||
status: published
|
||||
author: tldraw
|
||||
date: 2/8/2026
|
||||
order: 5
|
||||
description: Demonstrates how to build visual node-based image generation pipelines on infinite canvas. Features a type-safe port system with six data types, parallel DAG execution, AI image generation via Replicate, and a Cloudflare Worker backend. Includes 17 node types across input, processing, and output categories.
|
||||
embed: image-pipeline
|
||||
githubLink: https://github.com/tldraw/tldraw/tree/main/templates/image-pipeline
|
||||
keywords:
|
||||
- image
|
||||
- pipeline
|
||||
- generation
|
||||
- nodes
|
||||
- ai
|
||||
- comfyui
|
||||
---
|
||||
|
||||
To build with an image pipeline starter kit, run this command in your terminal:
|
||||
|
||||
```bash
|
||||
npm create tldraw@latest -- --template image-pipeline
|
||||
```
|
||||
|
||||
The template works out of the box with placeholder images. To connect real AI providers, create a `.dev.vars` file:
|
||||
|
||||
```
|
||||
REPLICATE_API_TOKEN=<your key>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Use cases
|
||||
|
||||
Use the image pipeline starter kit to build:
|
||||
|
||||
<StarterKitBento type="image-pipeline" />
|
||||
|
||||
---
|
||||
|
||||
## How it works
|
||||
|
||||
### 1. Shape system: Nodes and connections
|
||||
|
||||
Nodes are custom tldraw shapes that represent pipeline steps. Each node has typed input and output ports — there are six port data types (image, text, model, number, latent, and any) that enforce connection compatibility. Connections are also shapes, rendered as color-coded bezier curves that use tldraw's binding system to stay attached to specific ports.
|
||||
|
||||
### 2. Type-safe port system
|
||||
|
||||
Ports are color-coded by data type and enforce compatibility during connection. When you drag a connection, only compatible ports highlight as valid targets. The system also prevents cycles, replaces existing connections on single-input ports, and supports multi-input ports that accept multiple connections.
|
||||
|
||||
### 3. Pipeline execution
|
||||
|
||||
The execution engine builds a DAG (directed acyclic graph) from your node connections and resolves dependencies automatically. Independent branches execute in parallel. Each node receives its input values, runs its operation, and passes results downstream. Nodes show loading states during execution and cache their results.
|
||||
|
||||
### 4. AI image generation
|
||||
|
||||
The starter kit integrates with AI providers through a Cloudflare Worker backend. The Generate node sends prompts, model selection, and parameters (steps, CFG scale, seed) to the worker, which dispatches to providers like Replicate. When no backend is configured, the system returns placeholder SVGs so you can explore the full UI without API credentials.
|
||||
|
||||
### 5. Image processing pipeline
|
||||
|
||||
Beyond generation, the template includes processing nodes for upscaling, style transfer, ControlNet-guided generation, IP-Adapter reference images, blending, and image adjustments. These can be chained together to build complex multi-step image processing workflows.
|
||||
|
||||
---
|
||||
|
||||
## Customization
|
||||
|
||||
### Adding custom nodes
|
||||
|
||||
To add a custom node type, create a new file in `src/nodes/types/`. The easiest approach is to duplicate an existing node like `AdjustNode.tsx`.
|
||||
|
||||
Define your node's data shape:
|
||||
|
||||
```tsx
|
||||
import { T } from 'tldraw'
|
||||
|
||||
export const CustomNode = T.object({
|
||||
type: T.literal('custom'),
|
||||
// your node's parameters...
|
||||
})
|
||||
|
||||
export type CustomNode = T.TypeOf<typeof CustomNode>
|
||||
```
|
||||
|
||||
Then create a node definition:
|
||||
|
||||
```tsx
|
||||
export class CustomNodeDefinition extends NodeDefinition<CustomNode> {
|
||||
static type = 'custom'
|
||||
static validator = CustomNode
|
||||
|
||||
title = 'My custom node'
|
||||
icon = <span>🔧</span>
|
||||
category = 'process'
|
||||
|
||||
getDefault() { ... }
|
||||
getBodyHeightPx(shape, node) { ... }
|
||||
getPorts(shape, node) { ... }
|
||||
async execute(shape, node, inputs) { ... }
|
||||
getOutputInfo(shape, node, inputs) { ... }
|
||||
Component = CustomNodeComponent
|
||||
}
|
||||
```
|
||||
|
||||
Register your node in `src/nodes/nodeTypes.tsx` by adding it to `NodeDefinitions`, and add it to the sidebar in `src/components/ImagePipelineSidebar.tsx`.
|
||||
|
||||
### Adding AI providers
|
||||
|
||||
The worker uses a provider abstraction. To add a new provider, create a class implementing the `ImageProvider` interface in `worker/providers/`. The provider receives generation parameters and returns image URLs. Register it in the provider resolution logic so it can be selected from the Model node.
|
||||
|
||||
### UI customization
|
||||
|
||||
The template overrides several tldraw UI components:
|
||||
|
||||
- A sidebar node palette replaces the default toolbar, organized by category (input, process, output, utility)
|
||||
- Region overlays detect connected node groups and provide per-region play/stop controls
|
||||
- An on-canvas node picker appears when dragging a connection to empty space, filtered to show only type-compatible nodes
|
||||
- A template system lets users save and restore workflow configurations
|
||||
|
||||
To further customize the UI, read up on [customizing tldraw's UI as a whole](/docs/user-interface).
|
||||
|
||||
---
|
||||
|
||||
## Further reading
|
||||
|
||||
- **[Shape utilities](/docs/shapes)**: Learn how to create custom shapes and extend tldraw's shape system with advanced geometry, rendering, and interaction patterns.
|
||||
- **[Binding system](/sdk-features/bindings)**: Learn more about tldraw's binding system for creating relationships between shapes, automatic updates, and connection management.
|
||||
- **[Editor state management](/docs/editor)**: Learn how to work with tldraw's reactive state system, editor lifecycle, and event handling for complex canvas applications.
|
||||
- **[Customize the user interface](/docs/user-interface)**: Learn how to customize the user interface of your tldraw application.
|
||||
|
||||
---
|
||||
|
||||
## Building with this starter kit?
|
||||
|
||||
If you build something great, please share it with us in our [#show-and-tell](https://discord.tldraw.com/?utm_source=docs&utm_medium=organic&utm_campaign=sociallink) channel on Discord. We want to see what you've built!
|
||||
254
apps/docs/content/starter-kits/multiplayer.mdx
Normal file
254
apps/docs/content/starter-kits/multiplayer.mdx
Normal file
@@ -0,0 +1,254 @@
|
||||
---
|
||||
title: Multiplayer starter kit
|
||||
sidebarTitle: Multiplayer
|
||||
status: published
|
||||
author: tldraw
|
||||
date: 9/16/2024
|
||||
order: 7
|
||||
description: The Multiplayer Starter Kit demonstrates how to build self-hosted tldraw applications with real-time multiplayer collaboration using Cloudflare Durable Objects. It features production-ready backend infrastructure that handles WebSocket connections, automatic persistence, and asset management. You can use this foundation to create collaborative whiteboards, design tools, or any multi-user canvas application.
|
||||
embed: multiplayer
|
||||
githubLink: https://github.com/tldraw/tldraw/tree/main/templates/sync-cloudflare
|
||||
keywords:
|
||||
- multiplayer
|
||||
- sync
|
||||
- collaboration
|
||||
- real-time
|
||||
readability: 8
|
||||
voice: 7
|
||||
completeness: 8
|
||||
accuracy: 9
|
||||
notes: 'Revised to reflect current SQLite DO storage architecture. Code examples updated to show correct ShapeUtil patterns.'
|
||||
---
|
||||
|
||||
The multiplayer starter kit provides a production-ready foundation for building collaborative tldraw applications with real-time synchronization. It uses Cloudflare Durable Objects for room management, WebSocket connections for instant updates, and SQLite storage for automatic persistence.
|
||||
|
||||
To build with this starter kit, run this command in your terminal:
|
||||
|
||||
```bash
|
||||
npm create tldraw@latest -- --template multiplayer
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Use cases
|
||||
|
||||
This starter kit is perfect for building:
|
||||
|
||||
<StarterKitBento type="multiplayer" />
|
||||
|
||||
---
|
||||
|
||||
## How it works
|
||||
|
||||
### 1. Durable Objects: Room management
|
||||
|
||||
Each collaborative room runs in its own Cloudflare Durable Object instance. There's only ever one authoritative copy of each room's data, and all users connect to that same instance. This ensures strong consistency. Presence indicators (avatars and names) show who is currently in the room, and real-time cursors show where other users are pointing or selecting.
|
||||
|
||||
### 2. WebSocket synchronization: Real-time updates
|
||||
|
||||
Changes are synchronized instantly via WebSocket connections. When a user draws or modifies content, the change is sent to the Durable Object, applied to the in-memory document, and broadcast to all connected clients. The sync protocol keeps state consistent across all clients. If a connection drops, the client automatically reconnects and replays missed changes to restore consistency.
|
||||
|
||||
### 3. Persistent storage: SQLite
|
||||
|
||||
Room data is automatically persisted to Durable Object SQLite storage. Every change is saved immediately, so data survives restarts without manual save logic.
|
||||
|
||||
### 4. Asset management: Scalable file handling
|
||||
|
||||
Images, videos, and other files are uploaded directly to R2 storage and served through Cloudflare's global edge network. Shared assets are synchronized across all connected users. Pasted URLs automatically unfurl into link previews.
|
||||
|
||||
---
|
||||
|
||||
## Customization
|
||||
|
||||
This starter kit is built on top of tldraw's extensible architecture, which means that everything can be customized. The canvas renders using React DOM, so you can use familiar React patterns, components, and state management. Here are some ways to customize this starter kit.
|
||||
|
||||
### Custom shapes integration
|
||||
|
||||
To add custom shapes that work with multiplayer sync, you need to configure both the client-side shape utilities and the server-side schema. Both sides must know about your custom shapes so that validation, synchronization, and version compatibility work correctly.
|
||||
|
||||
See `worker/TldrawDurableObject.ts` and `client/pages/Room.tsx` as examples. The server file shows schema configuration while the client shows how to connect custom shapes to the sync client.
|
||||
|
||||
In the code example below, we add a custom sticky note shape on both client and server:
|
||||
|
||||
```tsx
|
||||
// 1. Server-side: Add custom shape schema in worker/TldrawDurableObject.ts
|
||||
import { createTLSchema, defaultShapeSchemas } from '@tldraw/tlschema'
|
||||
|
||||
const schema = createTLSchema({
|
||||
shapes: {
|
||||
...defaultShapeSchemas,
|
||||
'sticky-note': {
|
||||
props: {
|
||||
text: { type: 'string', default: '' },
|
||||
color: { type: 'string', default: 'yellow' },
|
||||
},
|
||||
migrations: {
|
||||
currentVersion: 1,
|
||||
migrators: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// 2. Client-side: Create ShapeUtil class in client/StickyNoteShapeUtil.tsx
|
||||
import { ShapeUtil, Rectangle2d, HTMLContainer } from 'tldraw'
|
||||
|
||||
class StickyNoteShapeUtil extends ShapeUtil<StickyNoteShape> {
|
||||
static override type = 'sticky-note' as const
|
||||
|
||||
getDefaultProps() {
|
||||
return { text: '', color: 'yellow' }
|
||||
}
|
||||
|
||||
getGeometry(shape: StickyNoteShape) {
|
||||
return new Rectangle2d({ width: 200, height: 200, isFilled: true })
|
||||
}
|
||||
|
||||
component(shape: StickyNoteShape) {
|
||||
return <HTMLContainer>{shape.props.text}</HTMLContainer>
|
||||
}
|
||||
|
||||
getIndicatorPath(shape: StickyNoteShape) {
|
||||
const path = new Path2D()
|
||||
path.rect(0, 0, 200, 200)
|
||||
return path
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Client-side: Pass ShapeUtil to Tldraw in client/pages/Room.tsx
|
||||
;<Tldraw store={store} shapeUtils={[StickyNoteShapeUtil]} />
|
||||
```
|
||||
|
||||
### Asset upload customization
|
||||
|
||||
To customize how assets are uploaded and served, modify the asset store configuration. You can add authentication, preprocessing, or serve different asset variants based on user permissions.
|
||||
|
||||
See `client/multiplayerAssetStore.tsx` as an example. This file shows how to upload assets to your Cloudflare Worker and retrieve them for display.
|
||||
|
||||
In the code example below, we add authentication to asset uploads:
|
||||
|
||||
```tsx
|
||||
// Custom asset store with authentication
|
||||
export const authenticatedAssetStore: TLAssetStore = {
|
||||
async upload(asset, file) {
|
||||
const id = uniqueId()
|
||||
const objectName = `${id}-${file.name}`.replace(/[^a-zA-Z0-9.]/g, '-')
|
||||
|
||||
const response = await fetch(`/api/uploads/${objectName}`, {
|
||||
method: 'POST',
|
||||
body: file,
|
||||
headers: {
|
||||
Authorization: `Bearer ${getAuthToken()}`,
|
||||
'X-User-ID': getCurrentUserId(),
|
||||
},
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Upload failed: ${response.statusText}`)
|
||||
}
|
||||
|
||||
return { src: `/api/uploads/${objectName}` }
|
||||
},
|
||||
|
||||
resolve(asset) {
|
||||
return `${asset.props.src}?token=${getAuthToken()}`
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### Deployment configuration
|
||||
|
||||
Modify the Cloudflare Worker configuration to customize your deployment. You can adjust resource limits, add custom domains, and set environment-specific variables.
|
||||
|
||||
See `wrangler.toml` as an example. This configuration file controls how your worker is deployed, including bucket names, environment variables, and routing rules.
|
||||
|
||||
In the code example below, we configure production deployment settings:
|
||||
|
||||
```toml
|
||||
# Production deployment configuration
|
||||
name = "my-multiplayer-app"
|
||||
main = "worker/worker.ts"
|
||||
compatibility_date = "2024-08-01"
|
||||
|
||||
[env.production]
|
||||
vars = { ENVIRONMENT = "production" }
|
||||
|
||||
[[env.production.r2_buckets]]
|
||||
binding = "TLDRAW_BUCKET"
|
||||
bucket_name = "my-app-production-bucket"
|
||||
|
||||
[[env.production.durable_objects.bindings]]
|
||||
name = "TLDRAW_DURABLE_OBJECT"
|
||||
class_name = "TldrawDurableObject"
|
||||
|
||||
[[env.production.routes]]
|
||||
pattern = "myapp.com/api/*"
|
||||
zone_name = "myapp.com"
|
||||
```
|
||||
|
||||
### Room persistence
|
||||
|
||||
Room data is automatically persisted via SQLite storage in the Durable Object. The `SQLiteSyncStorage` class handles all persistence logic. You don't need to implement custom save logic.
|
||||
|
||||
See `worker/TldrawDurableObject.ts` as an example. The room is created lazily and supports WebSocket hibernation:
|
||||
|
||||
```tsx
|
||||
import {
|
||||
DurableObjectSqliteSyncWrapper,
|
||||
type SessionStateSnapshot,
|
||||
SQLiteSyncStorage,
|
||||
TLSocketRoom,
|
||||
} from '@tldraw/sync-core'
|
||||
import { DurableObject } from 'cloudflare:workers'
|
||||
|
||||
export class TldrawDurableObject extends DurableObject {
|
||||
// Room is created lazily on first connection
|
||||
private room: TLSocketRoom<TLRecord, void> | null = null
|
||||
|
||||
private getOrCreateRoom(): TLSocketRoom<TLRecord, void> {
|
||||
if (!this.room) {
|
||||
const sql = new DurableObjectSqliteSyncWrapper(this.ctx.storage)
|
||||
const storage = new SQLiteSyncStorage<TLRecord>({ sql })
|
||||
|
||||
this.room = new TLSocketRoom<TLRecord, void>({
|
||||
schema,
|
||||
storage,
|
||||
// Disable idle timeout — Cloudflare handles keep-alive via auto-response
|
||||
clientTimeout: Infinity,
|
||||
// Persist session state to WebSocket attachments for hibernation recovery
|
||||
onSessionSnapshot: (sessionId, snapshot) => {
|
||||
const ws = this.sessionIdToWs.get(sessionId)
|
||||
if (ws) ws.serializeAttachment({ sessionId, snapshot })
|
||||
},
|
||||
})
|
||||
|
||||
// Resume any sessions that survived hibernation
|
||||
for (const ws of this.ctx.getWebSockets()) {
|
||||
const attachment = ws.deserializeAttachment() as SocketAttachment | null
|
||||
if (!attachment?.sessionId) continue
|
||||
if (attachment.snapshot) {
|
||||
this.room.handleSocketResume({
|
||||
sessionId: attachment.sessionId,
|
||||
socket: ws,
|
||||
snapshot: attachment.snapshot,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
return this.room
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Further reading
|
||||
|
||||
- **[Sync documentation](/docs/sync)**: Learn how to integrate tldraw sync into existing applications and customize the synchronization behavior.
|
||||
- **[Editor state management](/docs/editor)**: Learn how to work with tldraw's reactive state system, editor lifecycle, and event handling for complex canvas applications.
|
||||
|
||||
---
|
||||
|
||||
## Building with this starter kit?
|
||||
|
||||
If you build something great, please share it with us in our [#show-and-tell](https://discord.tldraw.com/?utm_source=docs&utm_medium=organic&utm_campaign=sociallink) channel on Discord. We want to see what you've built!
|
||||
136
apps/docs/content/starter-kits/overview.mdx
Normal file
136
apps/docs/content/starter-kits/overview.mdx
Normal file
@@ -0,0 +1,136 @@
|
||||
---
|
||||
title: Starter kits
|
||||
sidebarTitle: Overview
|
||||
status: published
|
||||
author: tldraw
|
||||
date: 1/30/2026
|
||||
order: 1
|
||||
keywords:
|
||||
- starter kit
|
||||
- templates
|
||||
- examples
|
||||
---
|
||||
|
||||
Starter kits provide example implementations of common canvas-based use cases. Use them to prototype ideas or as the foundation for new projects. The source code is yours to hack on.
|
||||
|
||||
---
|
||||
|
||||
## Create with tldraw
|
||||
|
||||
You can access starter kits using the `npm create tldraw@latest` command:
|
||||
|
||||
```bash
|
||||
npm create tldraw@latest
|
||||
```
|
||||
|
||||
Or by using a template repository on [GitHub](https://github.com/orgs/tldraw/repositories).
|
||||
|
||||
---
|
||||
|
||||
## Available kits
|
||||
|
||||
### [Workflow](/starter-kits/workflow)
|
||||
|
||||
Build visual tools where users drag, connect, and execute nodes on the canvas. Includes node and connection shapes, automatic connection routing, and a replaceable execution engine.
|
||||
|
||||
<StarterKitBento
|
||||
type="workflow"
|
||||
href="/starter-kits/workflow"
|
||||
img={{ src: '/images/starter-kits/workflow.png', alt: 'Workflow Starter Kit' }}
|
||||
/>
|
||||
|
||||
### [Chat](/starter-kits/chat)
|
||||
|
||||
A canvas for sketching, annotating, and marking up images before sending them to an AI chat.
|
||||
|
||||
<StarterKitBento
|
||||
type="chat"
|
||||
href="/starter-kits/chat"
|
||||
img={{ src: '/images/starter-kits/chat.png', alt: 'Chat Starter Kit' }}
|
||||
/>
|
||||
|
||||
### [Agent](/starter-kits/agent)
|
||||
|
||||
An AI agent that interprets and manipulates drawings and other elements on the canvas.
|
||||
|
||||
<StarterKitBento
|
||||
type="agent"
|
||||
href="/starter-kits/agent"
|
||||
img={{ src: '/images/starter-kits/agent.png', alt: 'Agent Starter Kit' }}
|
||||
/>
|
||||
|
||||
### [Image pipeline](/starter-kits/image-pipeline)
|
||||
|
||||
Visual node-based builder for AI image generation pipelines. Chain prompts, models, and processing steps to create complex image workflows with real-time preview.
|
||||
|
||||
<StarterKitBento
|
||||
type="image-pipeline"
|
||||
href="/starter-kits/image-pipeline"
|
||||
img={{ src: '/images/starter-kits/pipeline.png', alt: 'Image Pipeline Starter Kit' }}
|
||||
/>
|
||||
|
||||
### [Branching chat](/starter-kits/branching-chat)
|
||||
|
||||
A visual branching conversation interface for creating interactive chat trees with AI integration.
|
||||
|
||||
<StarterKitBento
|
||||
type="branching"
|
||||
href="/starter-kits/branching-chat"
|
||||
img={{ src: '/images/starter-kits/branching.png', alt: 'Branching Starter Kit' }}
|
||||
/>
|
||||
|
||||
### [Multiplayer](/starter-kits/multiplayer)
|
||||
|
||||
Self-hosted tldraw with real-time multiplayer collaboration powered by tldraw sync and Cloudflare Durable Objects.
|
||||
|
||||
<StarterKitBento
|
||||
type="multiplayer"
|
||||
href="/starter-kits/multiplayer"
|
||||
img={{ src: '/images/starter-kits/multi.png', alt: 'Multiplayer Starter Kit' }}
|
||||
/>
|
||||
|
||||
### [Shader](/starter-kits/shader)
|
||||
|
||||
WebGL shader backgrounds that respond to shapes and interactions on the canvas.
|
||||
|
||||
<StarterKitBento
|
||||
type="shader"
|
||||
href="/starter-kits/shader"
|
||||
img={{ src: '/images/starter-kits/shader.png', alt: 'Shader Starter Kit' }}
|
||||
/>
|
||||
|
||||
---
|
||||
|
||||
## Why build with starter kits
|
||||
|
||||
<CheckItem>Complete starter code built around the tldraw SDK</CheckItem>
|
||||
<CheckItem>Ready for real-time collaboration</CheckItem>
|
||||
<CheckItem>Custom shape systems and tool implementations</CheckItem>
|
||||
|
||||
<br />
|
||||
|
||||
<div className="flex flex-col gap-4">
|
||||
<Feature icon="bolt" title="Ship features in weeks, not months">
|
||||
Skip the months of research and implementation. Each kit provides tested solutions for complex canvas interactions.
|
||||
</Feature>
|
||||
|
||||
<Feature icon="cube" title="Production-ready from day one">
|
||||
Start further, prototype faster. Get extra features, patterns, and components relevant to your use-case.
|
||||
|
||||
</Feature>
|
||||
|
||||
<Feature icon="wrench" title="Built for customization">
|
||||
Use as a reference or the foundation for something new. The starter kit code is MIT licensed and free to hack with.
|
||||
|
||||
</Feature>
|
||||
|
||||
<Feature icon="shield" title="Validated by engineering teams">
|
||||
Built around common use cases. Every component uses the SDK's extensible APIs. Add custom shapes, tools, and behaviors on top.
|
||||
|
||||
</Feature>
|
||||
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
Starter kits use the same [license](/community/license) as the tldraw SDK.
|
||||
110
apps/docs/content/starter-kits/shader.mdx
Normal file
110
apps/docs/content/starter-kits/shader.mdx
Normal file
@@ -0,0 +1,110 @@
|
||||
---
|
||||
title: Shader starter kit
|
||||
sidebarTitle: Shader
|
||||
status: published
|
||||
author: tldraw
|
||||
date: 10/14/2024
|
||||
order: 8
|
||||
description: The Shader Starter Kit demonstrates how to integrate WebGL shaders with [tldraw](https://github.com/tldraw/tldraw). It creates dynamic backgrounds that respond to canvas interactions, and includes four complete examples and a reusable `WebGLManager` base class for building custom shader effects.
|
||||
embed: shader
|
||||
githubLink: https://github.com/tldraw/tldraw/tree/main/templates/shader
|
||||
keywords:
|
||||
- shader
|
||||
- webgl
|
||||
- graphics
|
||||
- visual effects
|
||||
- renderer
|
||||
---
|
||||
|
||||
To build with a shader starter kit, run this command in your terminal:
|
||||
|
||||
```bash
|
||||
npm create tldraw@latest -- --template shader
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Use cases
|
||||
|
||||
Use the shader starter kit to build:
|
||||
|
||||
<StarterKitBento type="shader" />
|
||||
|
||||
---
|
||||
|
||||
## How it works
|
||||
|
||||
### WebGLManager lifecycle
|
||||
|
||||
The reusable `WebGLManager` class ([`src/WebGLManager.ts`](https://github.com/tldraw/tldraw/tree/main/templates/shader/src/WebGLManager.ts)) creates and manages a WebGL2 context that is synchronized with the tldraw canvas. It owns the render loop and exposes lifecycle hooks—`onInitialize()`, `onUpdate()`, `onRender()`, and `onDispose()`. Each shader manager can focus on its effect-specific logic while sharing viewport coordination, resolution control, and animation timing. The Minimal, Rainbow, and Shadows examples extend this base class; the Fluid example uses a different architecture with its own simulation system.
|
||||
|
||||
### Config panel system
|
||||
|
||||
The config panel components ([`src/config-panel/`](https://github.com/tldraw/tldraw/tree/main/templates/shader/src/config-panel/)) provide ready-made UI controls for editing shader uniforms. Each panel stores settings in reactive atoms and persists them to `localStorage`, so your shader parameters survive reloads without extra wiring.
|
||||
|
||||
### Example gallery
|
||||
|
||||
The template ships with four complete demos that follow the same pattern of manager, renderer, config panel, and GLSL files:
|
||||
|
||||
- **Fluid simulation** ([`src/fluid/`](https://github.com/tldraw/tldraw/tree/main/templates/shader/src/fluid/)) — Navier-Stokes-based flow, built on Pavel Dobryakov's WebGL fluid implementation, that turns shape and pointer movement into velocity splats. Includes an in-depth [guide](https://github.com/tldraw/tldraw/tree/main/templates/shader/src/fluid/fluid.md).
|
||||
- **Rainbow** ([`src/rainbow/`](https://github.com/tldraw/tldraw/tree/main/templates/shader/src/rainbow/)) — Gradient animation that demonstrates time-based uniforms and color cycling.
|
||||
- **Shadows** ([`src/shadow/`](https://github.com/tldraw/tldraw/tree/main/templates/shader/src/shadow/)) — Raymarched shadow effect using signed distance fields derived from canvas geometry.
|
||||
- **Minimal** ([`src/minimal/`](https://github.com/tldraw/tldraw/tree/main/templates/shader/src/minimal/)) — Dark-mode-aware solid color shader that makes it easy to start your own effect, with a [step-by-step walkthrough](https://github.com/tldraw/tldraw/tree/main/templates/shader/src/minimal/minimal-example.md).
|
||||
|
||||
Switch between demos from the example menu next to the style panel to see how different managers plug into the same infrastructure.
|
||||
|
||||
---
|
||||
|
||||
## Customization
|
||||
|
||||
### Start from the minimal template
|
||||
|
||||
Copy the minimal shader to create a new effect:
|
||||
|
||||
```bash
|
||||
cp -r src/minimal src/my-shader
|
||||
```
|
||||
|
||||
Then update:
|
||||
|
||||
- `config.ts` — Define uniforms and UI controls for your shader.
|
||||
- `fragment.glsl` / `vertex.glsl` — Implement rendering logic.
|
||||
- `MyShaderManager.ts` — Extend `WebGLManager` and coordinate buffers, uniforms, and lifecycle hooks.
|
||||
- `MyRenderer.tsx` — Mount the manager from React and handle cleanup.
|
||||
- `MyConfigPanel.tsx` — Customize the controls exposed to users.
|
||||
|
||||
Finally, register the new manager in [`src/App.tsx`](https://github.com/tldraw/tldraw/tree/main/templates/shader/src/App.tsx).
|
||||
|
||||
### Extend WebGLManager hooks
|
||||
|
||||
Each manager can override lifecycle hooks to add render targets, resize behavior, or post-processing passes. Use `onInitialize()` to set up buffers, textures, and framebuffers; `onUpdate()` for per-frame uniform updates (such as time, resolution, or editor-driven state); and `onRender()` to issue draw calls. The base class also exposes helpers for handling pixel density and canvas resizes.
|
||||
|
||||
### Integrate with tldraw data
|
||||
|
||||
Every manager receives the live tldraw editor instance, which lets you:
|
||||
|
||||
- Access shapes with `editor.getCurrentPageShapes()`.
|
||||
- Subscribe to document changes through `editor.store.listen()`.
|
||||
- Read the current camera via `editor.getCamera()`.
|
||||
- Convert coordinates with `editor.pageToViewport()`.
|
||||
- Track pointer data from `editor.inputs.getCurrentScreenPoint()`.
|
||||
|
||||
See [`src/fluid/FluidManager.ts`](https://github.com/tldraw/tldraw/tree/main/templates/shader/src/fluid/FluidManager.ts) for a full example of mixing editor state with GPU simulation.
|
||||
|
||||
## Resources
|
||||
|
||||
- [WebGL2 Fundamentals](https://webgl2fundamentals.org/) - WebGL tutorials
|
||||
- [The Book of Shaders](https://thebookofshaders.com/) - GLSL shader programming
|
||||
- [Shadertoy](https://www.shadertoy.com/) - Shader examples
|
||||
|
||||
---
|
||||
|
||||
## Further reading
|
||||
|
||||
For multi-user shader environments, see the [Multiplayer starter kit](/starter-kits/multiplayer). To create custom shapes with advanced geometry and rendering, see [Shape utilities](/docs/shapes#Custom-shapes). For working with tldraw's reactive state system and event handling, see [Editor state management](/docs/editor).
|
||||
|
||||
---
|
||||
|
||||
## Building with this starter kit?
|
||||
|
||||
If you build something great, please share it with us in our [#show-and-tell](https://discord.tldraw.com/?utm_source=docs&utm_medium=organic&utm_campaign=sociallink) channel on Discord. We want to see what you've built!
|
||||
162
apps/docs/content/starter-kits/workflow.mdx
Normal file
162
apps/docs/content/starter-kits/workflow.mdx
Normal file
@@ -0,0 +1,162 @@
|
||||
---
|
||||
title: Workflow starter kit
|
||||
sidebarTitle: Workflow
|
||||
status: published
|
||||
author: tldraw
|
||||
date: 9/16/2024
|
||||
order: 2
|
||||
description: Demonstrates how to build visual programming interfaces with node-based workflows on infinite canvas. Features an interactive workflow system where users can create, connect, and execute nodes. Includes node and connection shapes, port-based bindings, a graph execution engine, and data flow between nodes.
|
||||
embed: workflow
|
||||
githubLink: https://github.com/tldraw/tldraw/tree/main/templates/workflow
|
||||
keywords:
|
||||
- workflow
|
||||
- process
|
||||
- diagram
|
||||
- flowchart
|
||||
---
|
||||
|
||||
To build with a workflow starter kit, run this command in your terminal:
|
||||
|
||||
```bash
|
||||
npm create tldraw@latest -- --template workflow
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Use cases
|
||||
|
||||
Use the workflow starter kit to build:
|
||||
|
||||
<StarterKitBento type="workflow" />
|
||||
|
||||
---
|
||||
|
||||
## How it works
|
||||
|
||||

|
||||
|
||||
### 1. Shape system: Nodes and connections
|
||||
|
||||
Nodes are custom tldraw shapes that represent workflow steps. Each node has input and output ports defined in its shape utility, and new node types can be created or customized. Nodes accept inputs and produce outputs, which can be joined together with connections. Connections are also shapes, but they use tldraw's binding system to stay attached to specific ports on nodes.
|
||||
|
||||
### 2. Binding system: Smart connections
|
||||
|
||||
When you create a connection between nodes, tldraw's binding system tracks the relationship. To create a connection, drag from a port. If you move a node, its connections update automatically. You can also drag an existing connection to reconnect it elsewhere or disconnect it entirely. The binding utilities handle connection lifecycle as nodes change.
|
||||
|
||||
### 3. Interaction layer: Port tools
|
||||
|
||||
The starter kit extends tldraw's select tool with custom port interactions. When you click or drag near a port, the `PointingPort` tool activates. You can create connections by dragging from any port, or click an output port to open a node picker and insert a new connected node. These behaviors are implemented using tldraw's state machine system, where tools are organized as states that can have child states.
|
||||
|
||||
### 4. Execution engine
|
||||
|
||||
The execution system is designed to be replaced by your own business logic. It reads your node graph, resolves dependencies (which nodes need to run before others), and executes them in the correct order. Nodes expose their computation through a simple interface, and the engine handles the orchestration. You can run a workflow graph (for example, using a "Play" action) to see how information flows between nodes.
|
||||
|
||||
### 5. Data flow and processing
|
||||
|
||||
You control the data that flows through your workflow and what happens to it. The starter kit provides the infrastructure: nodes update instantly to show their results as data flows through the connections. The framework moves data between nodes and triggers updates when values change. Your data can come from user inputs, external APIs, databases, or file uploads.
|
||||
|
||||
---
|
||||
|
||||
## Customization
|
||||
|
||||
### Adding custom nodes
|
||||
|
||||
To add custom node types, create a new file in `src/nodes/types`. The easiest way is to duplicate an existing node, like `MultiplyNode.tsx`.
|
||||
|
||||
Start by defining the type of your node:
|
||||
|
||||
```tsx
|
||||
import { T } from 'tldraw'
|
||||
|
||||
// First, we create a validator for our node type
|
||||
export const CustomNode = T.object({
|
||||
type: T.literal('custom'), // each node needs a unique "type"
|
||||
someData: T.number,
|
||||
// ...
|
||||
})
|
||||
|
||||
// Then, we can derive a typescript type from the validator
|
||||
export type CustomNode = T.TypeOf<typeof CustomNode>
|
||||
```
|
||||
|
||||
Once you have your node’s type definition, create a node definition for it:
|
||||
|
||||
```tsx
|
||||
export class CustomNodeDefinition extends NodeDefinition<CustomNode> {
|
||||
static type = 'custom' // This must match "type" from above
|
||||
static validator = CustomNode
|
||||
|
||||
// How to label your node in the UI
|
||||
title = 'My custom node'
|
||||
heading = 'Custom' // Shown in the node header
|
||||
icon = <span>🐝</span>
|
||||
|
||||
// Return a default version of your node
|
||||
getDefault() { ... }
|
||||
|
||||
// Return the height of your node, in pixels
|
||||
getBodyHeightPx(shape, node) { ... }
|
||||
|
||||
// Return all ports for your node. Each port has a terminal:
|
||||
// 'end' = input port (receives data), 'start' = output port (sends data)
|
||||
getPorts(shape, node) { ... }
|
||||
|
||||
// Run this node! Work through the input port values and
|
||||
// produce values for the output ports.
|
||||
async execute(shape, node, inputs) { ... }
|
||||
|
||||
// Get values to use as outputs when we're NOT running
|
||||
// this node. Often, you might return a previously computed
|
||||
// value from `execute`.
|
||||
getOutputInfo(shape, node, inputs) { ... }
|
||||
|
||||
// A react component for rendering your node on the canvas.
|
||||
Component = CustomNodeComponent
|
||||
}
|
||||
|
||||
function CustomNodeComponent({ shape, node }) {
|
||||
return <div>...</div>
|
||||
}
|
||||
```
|
||||
|
||||
Next, add your node definition to the system. In `src/nodes/nodeTypes.tsx`, include it in `NodeDefinitions`. Finally, add your node to the UI: open `src/components/WorkflowToolbar.tsx` and insert a `<ToolbarItem tool="node-custom" />` wherever you’d like your node to appear in the toolbar. If your node has any input ports (terminal = end), you can also add an `OnCanvasComponentPickerItem` for it in `src/components/OnCanvasComponentPicker`.
|
||||
|
||||
### Data fetching and integrations
|
||||
|
||||
Adding data fetching or integrations to a node is straightforward. You can make `fetch` requests from your custom node’s `execute` method. See `src/nodes/types/EarthquakeNode.tsx` for an example of a node that, when run, fetches recent earthquakes from the [USGS API](https://earthquake.usgs.gov/fdsnws/event/1/). Once fetched, it picks a random earthquake, displays some data, and outputs the magnitude to be used in downstream nodes.
|
||||
|
||||
You may also want to store data from the last execution to display in the UI. In the earthquake example, the data is stashed in an `earthquakeData` prop in the node definition. In `execute`, the shape is updated to store this data. Then, both the UI and `getOutputInfo` can reference it.
|
||||
|
||||
### Extending execution
|
||||
|
||||
Because it’s just a demo, this starter kit has a very minimal execution model: values are either numbers or a special `STOP_EXECUTION` flag used to implement conditionals.
|
||||
|
||||
If you want to change the type of data flowing through the system, edit `WorkflowValue` in `src/nodes/types/shared.tsx`. You’ll need to resolve type errors elsewhere, since several places assume the system only works with numbers.
|
||||
|
||||
The execution system is defined in `src/execution/ExecutionGraph.tsx`. It’s designed to be easily replaced by your own custom engine. For example, you might want executions to run entirely server-side, or you might want to “test” nodes individually from the canvas and then “deploy” a running workflow as an automation on a server.
|
||||
|
||||
### UI customization
|
||||
|
||||
Customizing tldraw’s UI mostly works by replacing specific components. Take a look at `src/App.tsx` to see how we diverge from tldraw’s default UI:
|
||||
|
||||
- We add the on-canvas component picker and workflow outline/play buttons in `InFrontOfTheCanvas`.
|
||||
- We add a custom vertical toolbar to the left of the screen and some extra actions in the bottom of the screen in `Toolbar`.
|
||||
- We remove the `MenuPanel` entirely.
|
||||
- We selectively hide the `StylePanel` depending on what’s selected.
|
||||
|
||||
To further customize the UI of this starter kit, read up on [customizing tldraw’s UI as a whole](/docs/user-interface).
|
||||
|
||||
---
|
||||
|
||||
## Further reading
|
||||
|
||||
- **[Shape utilities](/docs/shapes)**: Learn how to create custom shapes and extend tldraw's shape system with advanced geometry, rendering, and interaction patterns.
|
||||
- **[Binding system](/sdk-features/bindings)**: Learn more about tldraw's binding system for creating relationships between shapes, automatic updates, and connection management.
|
||||
- **[Editor state management](/docs/editor)**: Learn how to work with tldraw's reactive state system, editor lifecycle, and event handling for complex canvas applications.
|
||||
- **[Customize the user interface](/docs/user-interface)**: Learn how to customize the user interface of your tldraw application.
|
||||
|
||||
---
|
||||
|
||||
## Building with this starter kit?
|
||||
|
||||
If you build something great, please share it with us in our [#show-and-tell](https://discord.tldraw.com/?utm_source=docs&utm_medium=organic&utm_campaign=sociallink) channel on Discord. We want to see what you've built!
|
||||
Reference in New Issue
Block a user