Skip to content

Commit 7c0410c

Browse files
committed
3d + improve iterations + allow refreshing
1 parent b5109a9 commit 7c0410c

File tree

7 files changed

+182
-87
lines changed

7 files changed

+182
-87
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,4 @@ yarn-error.log*
3434
# typescript
3535
*.tsbuildinfo
3636
next-env.d.ts
37+
.idea

app/PreviewShape/PreviewShape.tsx

+26-3
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,14 @@ import {
1010
stopEventPropagation,
1111
DefaultSpinner,
1212
} from '@tldraw/tldraw'
13+
import { CompletionMessageItem } from '../lib/getHtmlFromOpenAI'
14+
import React from 'react'
1315

1416
export type PreviewShape = TLBaseShape<
1517
'preview',
1618
{
1719
html: string
18-
source: string
20+
history: CompletionMessageItem[],
1921
w: number
2022
h: number
2123
}
@@ -26,7 +28,7 @@ export class PreviewShapeUtil extends BaseBoxShapeUtil<PreviewShape> {
2628
getDefaultProps(): PreviewShape['props'] {
2729
return {
2830
html: '',
29-
source: '',
31+
history: [],
3032
w: (960 * 2) / 3,
3133
h: (540 * 2) / 3,
3234
}
@@ -39,9 +41,10 @@ export class PreviewShapeUtil extends BaseBoxShapeUtil<PreviewShape> {
3941

4042
override component(shape: PreviewShape) {
4143
const isEditing = useIsEditing(shape.id)
44+
const [reloadKey, setReloadKey] = React.useState(0)
4245
const toast = useToasts()
4346
return (
44-
<HTMLContainer className="tl-embed-container" id={shape.id}>
47+
<HTMLContainer className="tl-embed-container" id={shape.id} key={reloadKey}>
4548
{shape.props.html ? (
4649
<iframe
4750
className="tl-embed"
@@ -95,6 +98,26 @@ export class PreviewShapeUtil extends BaseBoxShapeUtil<PreviewShape> {
9598
>
9699
<Icon icon="duplicate" />
97100
</div>
101+
<div
102+
style={{
103+
position: 'absolute',
104+
top: 0,
105+
right: -80,
106+
height: 40,
107+
width: 40,
108+
display: 'flex',
109+
alignItems: 'center',
110+
justifyContent: 'center',
111+
cursor: 'pointer',
112+
pointerEvents: 'all',
113+
}}
114+
onClick={() => {
115+
setReloadKey(reloadKey + 1)
116+
}}
117+
onPointerDown={stopEventPropagation}
118+
>
119+
<Icon icon="undo" />
120+
</div>
98121
</HTMLContainer>
99122
)
100123
}

app/components/ExportButton.tsx

+8-8
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,20 @@
1-
import { useEditor, getSvgAsImage, useToasts, createShapeId } from '@tldraw/tldraw'
2-
import { useState } from 'react'
3-
import { PreviewShape } from '../PreviewShape/PreviewShape'
4-
import { getHtmlFromOpenAI } from '../lib/getHtmlFromOpenAI'
51
import { useMakeReal } from '../hooks/useMakeReal'
62

7-
export function ExportButton() {
8-
const makeReal = useMakeReal()
3+
export function ExportButton({
4+
mode,
5+
}: {
6+
mode: 'tailwind' | 'threejs',
7+
}) {
8+
const makeReal = useMakeReal(mode)
99

1010
// A tailwind styled button that is pinned to the bottom right of the screen
1111
return (
1212
<button
1313
onClick={makeReal}
14-
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded m-2"
14+
className='bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded m-2'
1515
style={{ cursor: 'pointer', zIndex: 100000, pointerEvents: 'all' }}
1616
>
17-
Make Real
17+
{`Make Real${mode === 'threejs' ? ' (3D!)' : ''}`}
1818
</button>
1919
)
2020
}

app/hooks/useMakeReal.ts

+6-3
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,19 @@
11
import { useEditor, useToasts } from '@tldraw/tldraw'
22
import { useCallback } from 'react'
33
import { makeReal } from '../lib/makeReal'
4+
import { CompletionMessageItem } from '../lib/getHtmlFromOpenAI'
45

5-
export function useMakeReal() {
6+
export function useMakeReal(
7+
mode: 'tailwind' | 'threejs',
8+
) {
69
const editor = useEditor()
710
const toast = useToasts()
811

912
return useCallback(async () => {
1013
const input = document.getElementById('openai_key_risky_but_cool') as HTMLInputElement
1114
const apiKey = input?.value ?? null
1215
try {
13-
await makeReal(editor, apiKey)
16+
await makeReal(editor, apiKey, mode)
1417
} catch (e: any) {
1518
console.error(e)
1619
toast.addToast({
@@ -19,5 +22,5 @@ export function useMakeReal() {
1922
description: `${e.message.slice(0, 100)}`,
2023
})
2124
}
22-
}, [editor, toast])
25+
}, [editor, mode, toast])
2326
}

app/lib/getHtmlFromOpenAI.ts

+108-60
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
const systemPrompt = `You are an expert web developer who specializes in tailwind css.
1+
const tailwindSystemPrompt = `You are an expert web developer who specializes in tailwind css.
22
A user will provide you with a low-fidelity wireframe of an application.
33
You will return a single html file that uses HTML, tailwind css, and JavaScript to create a high fidelity website.
44
Include any extra CSS and JavaScript in the html file.
@@ -9,49 +9,87 @@ They may also provide you with the html of a previous design that they want you
99
Carry out any changes they request from you.
1010
In the wireframe, the previous design's html will appear as a white rectangle.
1111
Use creative license to make the application more fleshed out.
12-
Use JavaScript modules and unkpkg to import any necessary dependencies.
12+
Use JavaScript modules and unpkg to import any necessary dependencies.
1313
1414
Respond ONLY with the contents of the html file.`
1515

16+
const threeJsSystemPrompt = `You are an expert web developer who specializes in three.js.
17+
A user will provide you with a low-fidelity wireframe of an application.
18+
You will return a single html file that uses HTML, three.js, and JavaScript to create a high fidelity website.
19+
Add a hidden div into the start of the body that includes the detailed instructions from the user, the contents adjusted to take the requested changes into account. If for example the user requests a change to the color of a button, change the color of the button in the instructions also, instead of saying "change button color".
20+
Include any extra CSS and JavaScript in the html file.
21+
The user will provide you with notes in text, arrows, or drawings.
22+
The user may also include images of other websites as style references. Transfer the styles as best as you can, matching colors.
23+
Prefer to use standard materials instead of basic and avoid custom shaders.
24+
Unless otherwise specified, set up orbit controls and a directional light attached to the camera.
25+
Use an import map e.g. <script type="importmap">{"imports": {"three": "https://unpkg.com/[email protected]/build/three.module.js","OrbitControls": "https://unpkg.com/[email protected]/examples/jsm/controls/OrbitControls.js"}}</script>
26+
They may also provide you with the html of a previous design that they want you to iterate from.
27+
Carry out any changes they request from you, keep everything the same except for the requested changes.
28+
In the wireframe, the previous design's html will appear as a white rectangle.
29+
Use creative license to make the application more fleshed out.
30+
Use JavaScript modules and unpkg to import any necessary dependencies.
31+
Respond ONLY with the contents of the html file.`
32+
33+
const threeJsText = 'Turn this into a single html file using three.js.'
34+
const tailwindText = 'Turn this into a single html file using tailwind.'
35+
const changesText = 'Please make the changes as specified in the image.'
36+
1637
export async function getHtmlFromOpenAI({
17-
image,
18-
html,
19-
apiKey,
20-
}: {
21-
image: string
22-
html: string
23-
apiKey: string
38+
image,
39+
apiKey,
40+
mode,
41+
history,
42+
}: {
43+
image: string;
44+
apiKey: string;
45+
mode: 'tailwind' | 'threejs',
46+
history: CompletionMessageItem[],
2447
}) {
48+
const userMessage: CompletionMessageItem = {
49+
role: 'user',
50+
content: [
51+
{
52+
type: 'image_url',
53+
image_url: {
54+
url: image,
55+
detail: 'high',
56+
},
57+
},
58+
],
59+
}
60+
61+
if (history.length === 0) {
62+
(userMessage.content as any[]).push({
63+
type: 'text',
64+
text: mode === 'tailwind' ? tailwindText : threeJsText,
65+
})
66+
} else {
67+
(userMessage.content as any[]).push({
68+
type: 'text',
69+
text: changesText,
70+
})
71+
}
72+
73+
const systemMessage: CompletionMessageItem = {
74+
role: 'system',
75+
content: mode === 'tailwind' ? tailwindSystemPrompt : threeJsSystemPrompt,
76+
}
77+
78+
const last4NonSystemMessages = history
79+
.filter((item) => item.role !== 'system')
80+
.slice(-4)
81+
82+
const messages: CompletionMessageItem[] = [
83+
systemMessage,
84+
...last4NonSystemMessages,
85+
userMessage,
86+
]
87+
2588
const body: GPT4VCompletionRequest = {
2689
model: 'gpt-4-vision-preview',
2790
max_tokens: 4096,
2891
temperature: 0,
29-
messages: [
30-
{
31-
role: 'system',
32-
content: systemPrompt,
33-
},
34-
{
35-
role: 'user',
36-
content: [
37-
{
38-
type: 'image_url',
39-
image_url: {
40-
url: image,
41-
detail: 'high',
42-
},
43-
},
44-
{
45-
type: 'text',
46-
text: 'Turn this into a single html file using tailwind.',
47-
},
48-
{
49-
type: 'text',
50-
text: html,
51-
},
52-
],
53-
},
54-
],
92+
messages,
5593
}
5694

5795
let json = null
@@ -67,41 +105,51 @@ export async function getHtmlFromOpenAI({
67105
},
68106
body: JSON.stringify(body),
69107
})
70-
console.log(resp)
71108
json = await resp.json()
72109
} catch (e) {
73-
console.log(e)
110+
console.error(e)
111+
}
112+
113+
const newHistory = [...messages]
114+
115+
if (json) {
116+
newHistory.push(json.choices[0].message)
74117
}
75118

76-
return json
119+
return {
120+
response: json,
121+
history: newHistory,
122+
}
77123
}
78124

79125
type MessageContent =
80126
| string
81127
| (
82-
| string
83-
| {
84-
type: 'image_url'
85-
image_url:
86-
| string
87-
| {
88-
url: string
89-
detail: 'low' | 'high' | 'auto'
90-
}
91-
}
92-
| {
93-
type: 'text'
94-
text: string
95-
}
96-
)[]
128+
| string
129+
| {
130+
type: 'image_url'
131+
image_url:
132+
| string
133+
| {
134+
url: string
135+
detail: 'low' | 'high' | 'auto'
136+
}
137+
}
138+
| {
139+
type: 'text'
140+
text: string
141+
}
142+
)[]
143+
144+
export type CompletionMessageItem = {
145+
role: 'system' | 'user' | 'assistant' | 'function'
146+
content: MessageContent
147+
name?: string | undefined
148+
};
97149

98150
export type GPT4VCompletionRequest = {
99151
model: 'gpt-4-vision-preview'
100-
messages: {
101-
role: 'system' | 'user' | 'assistant' | 'function'
102-
content: MessageContent
103-
name?: string | undefined
104-
}[]
152+
messages: CompletionMessageItem[]
105153
functions?: any[] | undefined
106154
function_call?: any | undefined
107155
stream?: boolean | undefined
@@ -114,8 +162,8 @@ export type GPT4VCompletionRequest = {
114162
presence_penalty?: number | undefined
115163
logit_bias?:
116164
| {
117-
[x: string]: number
118-
}
165+
[x: string]: number
166+
}
119167
| undefined
120168
stop?: (string[] | string) | undefined
121169
}

0 commit comments

Comments
 (0)