Skip to content

Commit

Permalink
Merge branch 'main' into generate-improvements
Browse files Browse the repository at this point in the history
  • Loading branch information
steveruizok committed Feb 19, 2025
2 parents f71807e + 3b2d3bb commit ca3f1db
Show file tree
Hide file tree
Showing 7 changed files with 102 additions and 160 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,10 @@ This repository is a pnpm monorepo. It has three parts:

1. Clone this repository.

2. Install the latest version of corepack.
2. Enable `corepack`

```bash
npm install -g corepack@latest
corepack enable
```

3. Install dependencies using [pnpm](https://pnpm.io/).
Expand Down
87 changes: 71 additions & 16 deletions example/client/App.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,77 @@
import { TLComponents, Tldraw } from 'tldraw'
import { ContextBoundsHelper } from './components/ContextBoundsHelper'
import { PromptInput } from './components/PromptInput'

const components: TLComponents = {
InFrontOfTheCanvas: () => {
return (
<>
<ContextBoundsHelper />
<PromptInput />
</>
)
},
}
import { FormEventHandler, useCallback, useEffect, useRef, useState } from 'react'
import { DefaultSpinner, Editor, Tldraw } from 'tldraw'
import { useTldrawAiExample } from './useTldrawAiExample'

function App() {
const [editor, setEditor] = useState<Editor | null>(null) // [1]
return (
<div className="tldraw-ai-container">
<Tldraw persistenceKey="tldraw-ai-demo" onMount={setEditor} />
{editor && <InputBar editor={editor} />}
</div>
)
}

function InputBar({ editor }: { editor: Editor }) {
const ai = useTldrawAiExample(editor)

// The state of the prompt input, either idle or loading with a cancel callback
const [isGenerating, setIsGenerating] = useState(false)

// A stashed cancel function that we can call if the user clicks the button while loading
const rCancelFn = useRef<(() => void) | null>(null)

useEffect(() => {
// Put the editor and ai helpers onto the window for debugging. You can run commands like `ai.prompt('draw a unicorn')` in the console.
if (!editor) return
;(window as any).editor = editor
;(window as any).ai = ai
}, [ai, editor])

const handleSubmit = useCallback<FormEventHandler<HTMLFormElement>>(
async (e) => {
e.preventDefault()

// If we have a stashed cancel function, call it and stop here
if (rCancelFn.current) {
rCancelFn.current()
rCancelFn.current = null
setIsGenerating(false)
return
}

try {
const formData = new FormData(e.currentTarget)
const value = formData.get('input') as string

// We call the ai module with the value from the input field and get back a promise and a cancel function
const { promise, cancel } = ai.prompt({ message: value, stream: true })

// Stash the cancel function so we can call it if the user clicks the button again
rCancelFn.current = cancel

// Set the state to loading
setIsGenerating(true)

// ...wait for the promise to resolve
await promise

// ...then set the state back to idle
setIsGenerating(false)
} catch (e: any) {
console.error(e)
setIsGenerating(false)
}
},
[prompt]
)

return (
<div style={{ position: 'fixed', inset: 0 }}>
<Tldraw persistenceKey="tldraw-ai-demo" components={components} />
<div className="prompt-input">
<form onSubmit={handleSubmit}>
<input name="input" type="text" autoComplete="off" placeholder="Enter your prompt…" />
<button>{isGenerating ? <DefaultSpinner /> : 'Send'}</button>
</form>
</div>
)
}
Expand Down
32 changes: 0 additions & 32 deletions example/client/components/ContextBoundsHelper.tsx

This file was deleted.

76 changes: 0 additions & 76 deletions example/client/components/PromptInput.tsx

This file was deleted.

24 changes: 16 additions & 8 deletions example/client/index.css
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@500;700@400;700&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@500;700&display=swap');
@import url('tldraw/tldraw.css');

body {
font-family: 'Inter', sans-serif;
overscroll-behavior: none;
}

.tldraw-ai-container {
position: fixed;
inset: 0;
display: grid;
grid-template-rows: 1fr 48px;
}

/* Prompt input */

.prompt-input {
position: absolute;
bottom: 100px;
left: 0;
width: 100%;
pointer-events: none;
display: flex;
Expand All @@ -26,19 +30,23 @@ body {
padding: 8px;
gap: 8px;
pointer-events: all;
background-color: var(--color-panel);
border-radius: 8px;
box-shadow: var(--shadow-1);
}

.prompt-input input[type='text'] {
width: 240px;
width: 320px;
max-width: 100%;
height: 36px;
height: 28px;
padding: 2px 8px;
}

.prompt-input button {
width: 64px;
height: 36px;
background: hsl(214, 84%, 56%);
color: hsl(0, 0%, 100%);
appearance: none;
border: 0;
border-radius: 2px;
cursor: pointer;
}
37 changes: 12 additions & 25 deletions example/client/useTldrawAiExample.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,22 @@
import { TLAiChange, TLAiResult, TldrawAiOptions, useTldrawAi } from '@tldraw/ai'
import { Editor } from 'tldraw'
import { ShapeDescriptions } from './transforms/ShapeDescriptions'
import { SimpleCoordinates } from './transforms/SimpleCoordinates'
import { SimpleIds } from './transforms/SimpleIds'

export function useTldrawAiExample() {
return useTldrawAi(options)
/**
* A hook that calls `useTldrawAi` with static options.
*
* @param editor - (optional) The editor instance to use. If not provided, the hook will try to use the editor from React context.
*/
export function useTldrawAiExample(editor?: Editor) {
return useTldrawAi({ editor, ...STATIC_TLDRAWAI_OPTIONS })
}

// Tip: It's best to define these options outside of any React function. If you do define them inside
// of a React hook / function, be sure to memoize them correctly with useMemo or useCallback hooks.
// See documentation in README.md or notes at the bottom of this file.

const options: TldrawAiOptions = {
// [1]
const STATIC_TLDRAWAI_OPTIONS: TldrawAiOptions = {
// Transforms that will be applied to the prompt before it's sent and to changes as they're received.
transforms: [SimpleIds, ShapeDescriptions, SimpleCoordinates],
// [2]
// A function that calls the backend and return generated changes. See worker/do/OpenAiService.ts#generate for the backend part.
generate: async ({ editor, prompt, signal }) => {
const res = await fetch('/generate', {
method: 'POST',
Expand All @@ -29,7 +31,7 @@ const options: TldrawAiOptions = {

return result.changes
},
// [3]
// A function similar to `generate` but that will stream changes from the AI as they are ready. See worker/do/OpenAiService.ts#stream for the backend part.
stream: async function* ({ editor, prompt, signal }) {
const res = await fetch('/stream', {
method: 'POST',
Expand Down Expand Up @@ -77,18 +79,3 @@ const options: TldrawAiOptions = {
}
},
}

/*
[1]
All of the transforms that will be applied to the prompt and changes, useful for
things like normalizing coordinates, adding descriptions, or simplifying IDs
[2]
A function that return changes. These can be generated locally or on the backend,
but usually are the result of passing your prompt to a LLM or other model.
[3]
A function similar to `generate` but that will stream changes from the AI as they are ready.
You don't need to implement both, you could implement only one or the other depending on
whether you want to use streaming.
*/
2 changes: 1 addition & 1 deletion package/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@tldraw/ai",
"version": "0.0.5",
"version": "0.0.6",
"homepage": "https://tldraw.dev",
"license": "MIT",
"author": {
Expand Down

0 comments on commit ca3f1db

Please sign in to comment.