20 |
24 |
25 | {children}
26 |
27 | );
28 | }
29 |
--------------------------------------------------------------------------------
/lib/getBrowserCanvasMaxSize.ts:
--------------------------------------------------------------------------------
1 | import canvasSize from "canvas-size";
2 |
3 | export type CanvasMaxSize = {
4 | maxWidth: number;
5 | maxHeight: number;
6 | maxArea: number;
7 | };
8 |
9 | let maxSizePromise: Promise | null = null;
10 |
11 | export function getBrowserCanvasMaxSize() {
12 | if (!maxSizePromise) {
13 | maxSizePromise = calculateBrowserCanvasMaxSize();
14 | }
15 |
16 | return maxSizePromise;
17 | }
18 |
19 | async function calculateBrowserCanvasMaxSize(): Promise {
20 | const maxWidth = await canvasSize.maxWidth({ usePromise: true });
21 | const maxHeight = await canvasSize.maxHeight({ usePromise: true });
22 | const maxArea = await canvasSize.maxArea({ usePromise: true });
23 | return {
24 | maxWidth: maxWidth.width,
25 | maxHeight: maxHeight.height,
26 | maxArea: maxArea.width * maxArea.height,
27 | };
28 | }
29 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "draw-a-ui",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint"
10 | },
11 | "dependencies": {
12 | "@tldraw/tldraw": "2.0.0-alpha.17",
13 | "canvas-size": "^1.2.6",
14 | "next": "^14.2.3",
15 | "openai": "^4.47.1",
16 | "prismjs": "^1.29.0",
17 | "react": "^18.3.1",
18 | "react-dom": "^18.3.1"
19 | },
20 | "devDependencies": {
21 | "@types/canvas-size": "^1.2.1",
22 | "@types/node": "^20",
23 | "@types/prismjs": "^1.26.3",
24 | "@types/react": "^18",
25 | "@types/react-dom": "^18",
26 | "autoprefixer": "^10.0.1",
27 | "eslint": "^8",
28 | "eslint-config-next": "^14.2.3",
29 | "postcss": "^8",
30 | "tailwindcss": "^3.3.0",
31 | "typescript": "^5"
32 | },
33 | "engines": {
34 | "node": ">=18.0.0"
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Sawyer Hood
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # draw-a-ui
2 |
3 | This is an app that uses tldraw and the gpt-4-vision api to generate html based on a wireframe you draw.
4 |
5 | > The spiritual successor to this project is [Terragon Labs](https://terragonlabs.com).
6 |
7 | 
8 |
9 | This works by just taking the current canvas SVG, converting it to a PNG, and sending that png to gpt-4-vision with instructions to return a single html file with tailwind.
10 |
11 | > Disclaimer: This is a demo and is not intended for production use. It doesn't have any auth so you will go broke if you deploy it.
12 |
13 | ## Getting Started
14 |
15 | This is a Next.js app. To get started run the following commands in the root directory of the project. You will need an OpenAI API key with access to the GPT-4 Vision API.
16 |
17 | > Note this uses Next.js 14 and requires a version of `node` greater than 18.17. [Read more here](https://nextjs.org/docs/pages/building-your-application/upgrading/version-14).
18 |
19 | ```bash
20 | echo "OPENAI_API_KEY=sk-your-key" > .env.local
21 | npm install
22 | npm run dev
23 | ```
24 |
25 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
26 |
--------------------------------------------------------------------------------
/public/next.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/api/toHtml/route.ts:
--------------------------------------------------------------------------------
1 | import { OpenAI } from "openai";
2 |
3 | const systemPrompt = `You are an expert tailwind developer. A user will provide you with a
4 | low-fidelity wireframe of an application and you will return
5 | a single html file that uses tailwind to create the website. Use creative license to make the application more fleshed out.
6 | if you need to insert an image, use placehold.co to create a placeholder image. Respond only with the html file.`;
7 |
8 | export async function POST(request: Request) {
9 | const openai = new OpenAI();
10 | const { image } = await request.json();
11 |
12 | const resp = await openai.chat.completions.create({
13 | model: "gpt-4o",
14 | max_tokens: 4096,
15 | messages: [
16 | {
17 | role: "system",
18 | content: systemPrompt,
19 | },
20 | {
21 | role: "user",
22 | content: [
23 | {
24 | type: "image_url",
25 | image_url: { url: image, detail: "high" },
26 | },
27 | {
28 | type: "text",
29 | text: "Turn this into a single html file using tailwind.",
30 | },
31 | ],
32 | },
33 | ],
34 | });
35 |
36 | return new Response(JSON.stringify(resp), {
37 | headers: {
38 | "content-type": "application/json; charset=UTF-8",
39 | },
40 | });
41 | }
42 |
--------------------------------------------------------------------------------
/components/PreviewModal.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { use, useEffect, useState } from "react";
4 | import Prism from "prismjs";
5 | import "prismjs/components/prism-cshtml";
6 |
7 | import "prismjs/themes/prism-tomorrow.css";
8 |
9 | export function PreviewModal({
10 | html,
11 | setHtml,
12 | }: {
13 | html: string | null;
14 | setHtml: (html: string | null) => void;
15 | }) {
16 | const [activeTab, setActiveTab] = useState<"preview" | "code">("preview");
17 |
18 | useEffect(() => {
19 | const highlight = async () => {
20 | await Prism.highlightAll(); // <--- prepare Prism
21 | };
22 | highlight(); // <--- call the async function
23 | }, [html, activeTab]); // <--- run when post updates
24 |
25 | if (!html) {
26 | return null;
27 | }
28 |
29 | return (
30 |