Full Code of SawyerHood/draw-a-ui for AI

main 1aca63b4d54a cached
19 files
24.4 KB
8.5k tokens
28 symbols
1 requests
Download .txt
Repository: SawyerHood/draw-a-ui
Branch: main
Commit: 1aca63b4d54a
Files: 19
Total size: 24.4 KB

Directory structure:
gitextract_w27hu8_s/

├── .eslintrc.json
├── .github/
│   └── workflows/
│       └── ci.yml
├── .gitignore
├── LICENSE
├── README.md
├── app/
│   ├── api/
│   │   └── toHtml/
│   │       └── route.ts
│   ├── globals.css
│   ├── layout.tsx
│   └── page.tsx
├── components/
│   └── PreviewModal.tsx
├── lib/
│   ├── blobToBase64.ts
│   ├── getBrowserCanvasMaxSize.ts
│   ├── getSvgAsImage.ts
│   └── png.ts
├── next.config.js
├── package.json
├── postcss.config.js
├── tailwind.config.ts
└── tsconfig.json

================================================
FILE CONTENTS
================================================

================================================
FILE: .eslintrc.json
================================================
{
  "extends": "next/core-web-vitals"
}


================================================
FILE: .github/workflows/ci.yml
================================================
name: Build and Lint

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  build-and-lint:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v2

      - name: Set up Node.js
        uses: actions/setup-node@v2
        with:
          node-version: "20" # Specify the Node.js version

      - name: Install dependencies
        run: npm install

      - name: Run build
        run: npm run build

      - name: Run lint
        run: npm run lint


================================================
FILE: .gitignore
================================================
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.js
.yarn/install-state.gz

# testing
/coverage

# next.js
/.next/
/out/

# production
/build

# misc
.DS_Store
*.pem

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# local env files
.env*.local

# vercel
.vercel

# typescript
*.tsbuildinfo
next-env.d.ts


================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2023 Sawyer Hood

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: README.md
================================================
# draw-a-ui

This is an app that uses tldraw and the gpt-4-vision api to generate html based on a wireframe you draw.

> The spiritual successor to this project is [Terragon Labs](https://terragonlabs.com).

![A demo of the app](./demo.gif)

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.

> 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.

## Getting Started

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.

> 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).

```bash
echo "OPENAI_API_KEY=sk-your-key" > .env.local
npm install
npm run dev
```

Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.


================================================
FILE: app/api/toHtml/route.ts
================================================
import { OpenAI } from "openai";

const systemPrompt = `You are an expert tailwind developer. A user will provide you with a
 low-fidelity wireframe of an application and you will return 
 a single html file that uses tailwind to create the website. Use creative license to make the application more fleshed out.
if you need to insert an image, use placehold.co to create a placeholder image. Respond only with the html file.`;

export async function POST(request: Request) {
  const openai = new OpenAI();
  const { image } = await request.json();

  const resp = await openai.chat.completions.create({
    model: "gpt-4o",
    max_tokens: 4096,
    messages: [
      {
        role: "system",
        content: systemPrompt,
      },
      {
        role: "user",
        content: [
          {
            type: "image_url",
            image_url: { url: image, detail: "high" },
          },
          {
            type: "text",
            text: "Turn this into a single html file using tailwind.",
          },
        ],
      },
    ],
  });

  return new Response(JSON.stringify(resp), {
    headers: {
      "content-type": "application/json; charset=UTF-8",
    },
  });
}


================================================
FILE: app/globals.css
================================================
@tailwind base;
@tailwind components;
@tailwind utilities;

.tlui-help-menu {
  display: none !important;
}

.tlui-debug-panel {
  display: none !important;
}


================================================
FILE: app/layout.tsx
================================================
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";

const inter = Inter({ subsets: ["latin"] });

export const metadata: Metadata = {
  title: "Create Next App",
  description: "Generated by create next app",
};

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <head>
        <meta
          name="viewport"
          content="width=device-width, initial-scale=1, viewport-fit=cover"
        />
      </head>
      <body className={inter.className}>{children}</body>
    </html>
  );
}


================================================
FILE: app/page.tsx
================================================
"use client";

import dynamic from "next/dynamic";
import "@tldraw/tldraw/tldraw.css";
import { useEditor } from "@tldraw/tldraw";
import { getSvgAsImage } from "@/lib/getSvgAsImage";
import { blobToBase64 } from "@/lib/blobToBase64";
import React, { useEffect, useState } from "react";
import ReactDOM from "react-dom";
import { PreviewModal } from "@/components/PreviewModal";

const Tldraw = dynamic(async () => (await import("@tldraw/tldraw")).Tldraw, {
  ssr: false,
});

export default function Home() {
  const [html, setHtml] = useState<null | string>(null);

  useEffect(() => {
    const listener = (e: KeyboardEvent) => {
      if (e.key === "Escape") {
        setHtml(null);
      }
    };
    window.addEventListener("keydown", listener);

    return () => {
      window.removeEventListener("keydown", listener);
    };
  });

  return (
    <>
      <div className={`w-screen h-screen`}>
        <Tldraw persistenceKey="tldraw">
          <ExportButton setHtml={setHtml} />
        </Tldraw>
      </div>
      {html &&
        ReactDOM.createPortal(
          <div
            className="fixed top-0 left-0 right-0 bottom-0 flex justify-center items-center"
            style={{ zIndex: 2000, backgroundColor: "rgba(0,0,0,0.5)" }}
            onClick={() => setHtml(null)}
          >
            <PreviewModal html={html} setHtml={setHtml} />
          </div>,
          document.body
        )}
    </>
  );
}

function ExportButton({ setHtml }: { setHtml: (html: string) => void }) {
  const editor = useEditor();
  const [loading, setLoading] = useState(false);
  // A tailwind styled button that is pinned to the bottom right of the screen
  return (
    <button
      onClick={async (e) => {
        setLoading(true);
        try {
          e.preventDefault();
          const svg = await editor.getSvg(
            Array.from(editor.currentPageShapeIds)
          );
          if (!svg) {
            return;
          }
          const png = await getSvgAsImage(svg, {
            type: "png",
            quality: 1,
            scale: 1,
          });
          const dataUrl = await blobToBase64(png!);
          const resp = await fetch("/api/toHtml", {
            method: "POST",
            headers: {
              "Content-Type": "application/json",
            },
            body: JSON.stringify({ image: dataUrl }),
          });

          const json = await resp.json();

          if (json.error) {
            alert("Error from open ai: " + JSON.stringify(json.error));
            return;
          }

          const message = json.choices[0].message.content;
          const start = message.indexOf("<!DOCTYPE html>");
          const end = message.indexOf("</html>");
          const html = message.slice(start, end + "</html>".length);
          setHtml(html);
        } finally {
          setLoading(false);
        }
      }}
      className="fixed bottom-4 right-4 bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded ="
      style={{ zIndex: 1000 }}
      disabled={loading}
    >
      {loading ? (
        <div className="flex justify-center items-center ">
          <div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div>
        </div>
      ) : (
        "Make Real"
      )}
    </button>
  );
}


================================================
FILE: components/PreviewModal.tsx
================================================
"use client";

import { use, useEffect, useState } from "react";
import Prism from "prismjs";
import "prismjs/components/prism-cshtml";

import "prismjs/themes/prism-tomorrow.css";

export function PreviewModal({
  html,
  setHtml,
}: {
  html: string | null;
  setHtml: (html: string | null) => void;
}) {
  const [activeTab, setActiveTab] = useState<"preview" | "code">("preview");

  useEffect(() => {
    const highlight = async () => {
      await Prism.highlightAll(); // <--- prepare Prism
    };
    highlight(); // <--- call the async function
  }, [html, activeTab]); // <--- run when post updates

  if (!html) {
    return null;
  }

  return (
    <div
      onClick={(e) => {
        e.stopPropagation();
      }}
      className="bg-white rounded-lg shadow-xl flex flex-col"
      style={{
        width: "calc(100% - 64px)",
        height: "calc(100% - 64px)",
      }}
    >
      <div className="flex justify-between items-center p-4 border-b">
        <div className="flex space-x-1">
          <TabButton
            active={activeTab === "preview"}
            onClick={() => {
              setActiveTab("preview");
            }}
          >
            Preview
          </TabButton>
          <TabButton
            active={activeTab === "code"}
            onClick={() => {
              setActiveTab("code");
            }}
          >
            Code
          </TabButton>
        </div>

        <button
          className="p-2 rounded-md hover:bg-gray-200 focus:outline-none focus:ring"
          onClick={() => {
            setHtml(null);
          }}
        >
          <svg
            className="w-6 h-6 text-gray-600"
            xmlns="http://www.w3.org/2000/svg"
            fill="none"
            viewBox="0 0 24 24"
            stroke="currentColor"
            aria-hidden="true"
          >
            <path
              strokeLinecap="round"
              strokeLinejoin="round"
              strokeWidth="2"
              d="M6 18L18 6M6 6l12 12"
            ></path>
          </svg>
        </button>
      </div>

      {activeTab === "preview" ? (
        <iframe className="w-full h-full" srcDoc={html} />
      ) : (
        <pre className="overflow-auto p-4">
          <code className="language-markup">{html}</code>
        </pre>
      )}
    </div>
  );
}

interface TabButtonProps extends React.HTMLAttributes<HTMLButtonElement> {
  active: boolean;
}

function TabButton({ active, ...buttonProps }: TabButtonProps) {
  const className = active
    ? "px-4 py-2 text-sm font-medium text-white bg-blue-500 rounded-t-md focus:outline-none focus:ring"
    : "px-4 py-2 text-sm font-medium text-blue-500 bg-transparent hover:bg-blue-100 focus:bg-blue-100 rounded-t-md focus:outline-none focus:ring";
  return <button className={className} {...buttonProps}></button>;
}


================================================
FILE: lib/blobToBase64.ts
================================================
export function blobToBase64(blob: Blob) {
  return new Promise((resolve, _) => {
    const reader = new FileReader();
    reader.onloadend = () => resolve(reader.result);
    reader.readAsDataURL(blob);
  });
}


================================================
FILE: lib/getBrowserCanvasMaxSize.ts
================================================
import canvasSize from "canvas-size";

export type CanvasMaxSize = {
  maxWidth: number;
  maxHeight: number;
  maxArea: number;
};

let maxSizePromise: Promise<CanvasMaxSize> | null = null;

export function getBrowserCanvasMaxSize() {
  if (!maxSizePromise) {
    maxSizePromise = calculateBrowserCanvasMaxSize();
  }

  return maxSizePromise;
}

async function calculateBrowserCanvasMaxSize(): Promise<CanvasMaxSize> {
  const maxWidth = await canvasSize.maxWidth({ usePromise: true });
  const maxHeight = await canvasSize.maxHeight({ usePromise: true });
  const maxArea = await canvasSize.maxArea({ usePromise: true });
  return {
    maxWidth: maxWidth.width,
    maxHeight: maxHeight.height,
    maxArea: maxArea.width * maxArea.height,
  };
}


================================================
FILE: lib/getSvgAsImage.ts
================================================
import { getBrowserCanvasMaxSize } from "./getBrowserCanvasMaxSize";
import { PngHelpers } from "./png";

type TLCopyType = "jpeg" | "json" | "png" | "svg";
type TLExportType = "jpeg" | "json" | "png" | "svg" | "webp";

/**
 * This is all copied from node_modules/@tldraw/tldraw/src/lib/utils/export.ts
 */
export async function getSvgAsImage(
  svg: SVGElement,
  options: {
    type: TLCopyType | TLExportType;
    quality: number;
    scale: number;
  }
) {
  const { type, quality, scale } = options;

  const width = +svg.getAttribute("width")!;
  const height = +svg.getAttribute("height")!;
  let scaledWidth = width * scale;
  let scaledHeight = height * scale;

  const dataUrl = await getSvgAsDataUrl(svg);

  const canvasSizes = await getBrowserCanvasMaxSize();
  if (width > canvasSizes.maxWidth) {
    scaledWidth = canvasSizes.maxWidth;
    scaledHeight = (scaledWidth / width) * height;
  }
  if (height > canvasSizes.maxHeight) {
    scaledHeight = canvasSizes.maxHeight;
    scaledWidth = (scaledHeight / height) * width;
  }
  if (scaledWidth * scaledHeight > canvasSizes.maxArea) {
    const ratio = Math.sqrt(canvasSizes.maxArea / (scaledWidth * scaledHeight));
    scaledWidth *= ratio;
    scaledHeight *= ratio;
  }

  scaledWidth = Math.floor(scaledWidth);
  scaledHeight = Math.floor(scaledHeight);
  const effectiveScale = scaledWidth / width;

  const canvas = await new Promise<HTMLCanvasElement | null>((resolve) => {
    const image = new Image();
    image.crossOrigin = "anonymous";

    image.onload = async () => {
      // safari will fire `onLoad` before the fonts in the SVG are
      // actually loaded. just waiting around a while is brittle, but
      // there doesn't seem to be any better solution for now :( see
      // https://bugs.webkit.org/show_bug.cgi?id=219770
      await new Promise((resolve) => setTimeout(resolve, 250));

      const canvas = document.createElement("canvas") as HTMLCanvasElement;
      const ctx = canvas.getContext("2d")!;

      canvas.width = scaledWidth;
      canvas.height = scaledHeight;

      ctx.imageSmoothingEnabled = true;
      ctx.imageSmoothingQuality = "high";
      ctx.drawImage(image, 0, 0, scaledWidth, scaledHeight);

      URL.revokeObjectURL(dataUrl);

      resolve(canvas);
    };

    image.onerror = () => {
      resolve(null);
    };

    image.src = dataUrl;
  });

  if (!canvas) return null;

  const blob = await new Promise<Blob | null>((resolve) =>
    canvas.toBlob(
      (blob) => {
        if (!blob) {
          resolve(null);
        }
        resolve(blob);
      },
      "image/" + type,
      quality
    )
  );

  if (!blob) return null;

  const view = new DataView(await blob.arrayBuffer());
  return PngHelpers.setPhysChunk(view, effectiveScale, {
    type: "image/" + type,
  });
}

export async function getSvgAsDataUrl(svg: SVGElement) {
  const clone = svg.cloneNode(true) as SVGGraphicsElement;
  clone.setAttribute("encoding", 'UTF-8"');

  const fileReader = new FileReader();
  const imgs = Array.from(clone.querySelectorAll("image")) as SVGImageElement[];

  for (const img of imgs) {
    const src = img.getAttribute("xlink:href");
    if (src) {
      if (!src.startsWith("data:")) {
        const blob = await (await fetch(src)).blob();
        const base64 = await new Promise<string>((resolve, reject) => {
          fileReader.onload = () => resolve(fileReader.result as string);
          fileReader.onerror = () => reject(fileReader.error);
          fileReader.readAsDataURL(blob);
        });
        img.setAttribute("xlink:href", base64);
      }
    }
  }

  return getSvgAsDataUrlSync(clone);
}

export function getSvgAsDataUrlSync(node: SVGElement) {
  const svgStr = new XMLSerializer().serializeToString(node);
  // NOTE: `unescape` works everywhere although deprecated
  const base64SVG = window.btoa(unescape(encodeURIComponent(svgStr)));
  return `data:image/svg+xml;base64,${base64SVG}`;
}


================================================
FILE: lib/png.ts
================================================
type BufferInput = string | ArrayBuffer | Buffer;

interface CRCCalculator<T = BufferInput | Uint8Array> {
  (value: T, previous?: number): number;
}

let TABLE: Array<number> | Int32Array = [
  0x00000000, 0x77073096, 0xee0e612c, 0x990951ba, 0x076dc419, 0x706af48f,
  0xe963a535, 0x9e6495a3, 0x0edb8832, 0x79dcb8a4, 0xe0d5e91e, 0x97d2d988,
  0x09b64c2b, 0x7eb17cbd, 0xe7b82d07, 0x90bf1d91, 0x1db71064, 0x6ab020f2,
  0xf3b97148, 0x84be41de, 0x1adad47d, 0x6ddde4eb, 0xf4d4b551, 0x83d385c7,
  0x136c9856, 0x646ba8c0, 0xfd62f97a, 0x8a65c9ec, 0x14015c4f, 0x63066cd9,
  0xfa0f3d63, 0x8d080df5, 0x3b6e20c8, 0x4c69105e, 0xd56041e4, 0xa2677172,
  0x3c03e4d1, 0x4b04d447, 0xd20d85fd, 0xa50ab56b, 0x35b5a8fa, 0x42b2986c,
  0xdbbbc9d6, 0xacbcf940, 0x32d86ce3, 0x45df5c75, 0xdcd60dcf, 0xabd13d59,
  0x26d930ac, 0x51de003a, 0xc8d75180, 0xbfd06116, 0x21b4f4b5, 0x56b3c423,
  0xcfba9599, 0xb8bda50f, 0x2802b89e, 0x5f058808, 0xc60cd9b2, 0xb10be924,
  0x2f6f7c87, 0x58684c11, 0xc1611dab, 0xb6662d3d, 0x76dc4190, 0x01db7106,
  0x98d220bc, 0xefd5102a, 0x71b18589, 0x06b6b51f, 0x9fbfe4a5, 0xe8b8d433,
  0x7807c9a2, 0x0f00f934, 0x9609a88e, 0xe10e9818, 0x7f6a0dbb, 0x086d3d2d,
  0x91646c97, 0xe6635c01, 0x6b6b51f4, 0x1c6c6162, 0x856530d8, 0xf262004e,
  0x6c0695ed, 0x1b01a57b, 0x8208f4c1, 0xf50fc457, 0x65b0d9c6, 0x12b7e950,
  0x8bbeb8ea, 0xfcb9887c, 0x62dd1ddf, 0x15da2d49, 0x8cd37cf3, 0xfbd44c65,
  0x4db26158, 0x3ab551ce, 0xa3bc0074, 0xd4bb30e2, 0x4adfa541, 0x3dd895d7,
  0xa4d1c46d, 0xd3d6f4fb, 0x4369e96a, 0x346ed9fc, 0xad678846, 0xda60b8d0,
  0x44042d73, 0x33031de5, 0xaa0a4c5f, 0xdd0d7cc9, 0x5005713c, 0x270241aa,
  0xbe0b1010, 0xc90c2086, 0x5768b525, 0x206f85b3, 0xb966d409, 0xce61e49f,
  0x5edef90e, 0x29d9c998, 0xb0d09822, 0xc7d7a8b4, 0x59b33d17, 0x2eb40d81,
  0xb7bd5c3b, 0xc0ba6cad, 0xedb88320, 0x9abfb3b6, 0x03b6e20c, 0x74b1d29a,
  0xead54739, 0x9dd277af, 0x04db2615, 0x73dc1683, 0xe3630b12, 0x94643b84,
  0x0d6d6a3e, 0x7a6a5aa8, 0xe40ecf0b, 0x9309ff9d, 0x0a00ae27, 0x7d079eb1,
  0xf00f9344, 0x8708a3d2, 0x1e01f268, 0x6906c2fe, 0xf762575d, 0x806567cb,
  0x196c3671, 0x6e6b06e7, 0xfed41b76, 0x89d32be0, 0x10da7a5a, 0x67dd4acc,
  0xf9b9df6f, 0x8ebeeff9, 0x17b7be43, 0x60b08ed5, 0xd6d6a3e8, 0xa1d1937e,
  0x38d8c2c4, 0x4fdff252, 0xd1bb67f1, 0xa6bc5767, 0x3fb506dd, 0x48b2364b,
  0xd80d2bda, 0xaf0a1b4c, 0x36034af6, 0x41047a60, 0xdf60efc3, 0xa867df55,
  0x316e8eef, 0x4669be79, 0xcb61b38c, 0xbc66831a, 0x256fd2a0, 0x5268e236,
  0xcc0c7795, 0xbb0b4703, 0x220216b9, 0x5505262f, 0xc5ba3bbe, 0xb2bd0b28,
  0x2bb45a92, 0x5cb36a04, 0xc2d7ffa7, 0xb5d0cf31, 0x2cd99e8b, 0x5bdeae1d,
  0x9b64c2b0, 0xec63f226, 0x756aa39c, 0x026d930a, 0x9c0906a9, 0xeb0e363f,
  0x72076785, 0x05005713, 0x95bf4a82, 0xe2b87a14, 0x7bb12bae, 0x0cb61b38,
  0x92d28e9b, 0xe5d5be0d, 0x7cdcefb7, 0x0bdbdf21, 0x86d3d2d4, 0xf1d4e242,
  0x68ddb3f8, 0x1fda836e, 0x81be16cd, 0xf6b9265b, 0x6fb077e1, 0x18b74777,
  0x88085ae6, 0xff0f6a70, 0x66063bca, 0x11010b5c, 0x8f659eff, 0xf862ae69,
  0x616bffd3, 0x166ccf45, 0xa00ae278, 0xd70dd2ee, 0x4e048354, 0x3903b3c2,
  0xa7672661, 0xd06016f7, 0x4969474d, 0x3e6e77db, 0xaed16a4a, 0xd9d65adc,
  0x40df0b66, 0x37d83bf0, 0xa9bcae53, 0xdebb9ec5, 0x47b2cf7f, 0x30b5ffe9,
  0xbdbdf21c, 0xcabac28a, 0x53b39330, 0x24b4a3a6, 0xbad03605, 0xcdd70693,
  0x54de5729, 0x23d967bf, 0xb3667a2e, 0xc4614ab8, 0x5d681b02, 0x2a6f2b94,
  0xb40bbe37, 0xc30c8ea1, 0x5a05df1b, 0x2d02ef8d,
];

if (typeof Int32Array !== "undefined") {
  TABLE = new Int32Array(TABLE);
}

// crc32, https://github.com/alexgorbatchev/crc/blob/master/src/calculators/crc32.ts
const crc: CRCCalculator<Uint8Array> = (current, previous) => {
  let crc = previous === 0 ? 0 : ~~previous! ^ -1;

  for (let index = 0; index < current.length; index++) {
    crc = TABLE[(crc ^ current[index]) & 0xff] ^ (crc >>> 8);
  }

  return crc ^ -1;
};

const LEN_SIZE = 4;
const CRC_SIZE = 4;

/** @public */
export class PngHelpers {
  static isPng(view: DataView, offset: number) {
    if (
      view.getUint8(offset + 0) === 0x89 &&
      view.getUint8(offset + 1) === 0x50 &&
      view.getUint8(offset + 2) === 0x4e &&
      view.getUint8(offset + 3) === 0x47 &&
      view.getUint8(offset + 4) === 0x0d &&
      view.getUint8(offset + 5) === 0x0a &&
      view.getUint8(offset + 6) === 0x1a &&
      view.getUint8(offset + 7) === 0x0a
    ) {
      return true;
    }
    return false;
  }

  static getChunkType(view: DataView, offset: number) {
    return [
      String.fromCharCode(view.getUint8(offset)),
      String.fromCharCode(view.getUint8(offset + 1)),
      String.fromCharCode(view.getUint8(offset + 2)),
      String.fromCharCode(view.getUint8(offset + 3)),
    ].join("");
  }

  static readChunks(view: DataView, offset = 0) {
    const chunks: Record<
      string,
      { dataOffset: number; size: number; start: number }
    > = {};
    if (!PngHelpers.isPng(view, offset)) {
      throw new Error("Not a PNG");
    }
    offset += 8;

    while (offset <= view.buffer.byteLength) {
      const start = offset;
      const len = view.getInt32(offset);
      offset += 4;
      const chunkType = PngHelpers.getChunkType(view, offset);

      if (chunkType === "IDAT" && chunks[chunkType]) {
        offset += len + LEN_SIZE + CRC_SIZE;
        continue;
      }

      if (chunkType === "IEND") {
        break;
      }

      chunks[chunkType] = {
        start,
        dataOffset: offset + 4,
        size: len,
      };
      offset += len + LEN_SIZE + CRC_SIZE;
    }

    return chunks;
  }

  static parsePhys(view: DataView, offset: number) {
    return {
      ppux: view.getUint32(offset),
      ppuy: view.getUint32(offset + 4),
      unit: view.getUint8(offset + 4),
    };
  }

  static findChunk(view: DataView, type: string) {
    const chunks = PngHelpers.readChunks(view);
    return chunks[type];
  }

  static setPhysChunk(view: DataView, dpr = 1, options?: BlobPropertyBag) {
    let offset = 46;
    let size = 0;
    const res1 = PngHelpers.findChunk(view, "pHYs");
    if (res1) {
      offset = res1.start;
      size = res1.size;
    }

    const res2 = PngHelpers.findChunk(view, "IDAT");
    if (res2) {
      offset = res2.start;
      size = 0;
    }

    const pHYsData = new ArrayBuffer(21);
    const pHYsDataView = new DataView(pHYsData);

    pHYsDataView.setUint32(0, 9);

    pHYsDataView.setUint8(4, "p".charCodeAt(0));
    pHYsDataView.setUint8(5, "H".charCodeAt(0));
    pHYsDataView.setUint8(6, "Y".charCodeAt(0));
    pHYsDataView.setUint8(7, "s".charCodeAt(0));

    const DPI_96 = 2835.5;

    pHYsDataView.setInt32(8, DPI_96 * dpr);
    pHYsDataView.setInt32(12, DPI_96 * dpr);
    pHYsDataView.setInt8(16, 1);

    const crcBit = new Uint8Array(pHYsData.slice(4, 17));
    pHYsDataView.setInt32(17, crc(crcBit));

    const startBuf = view.buffer.slice(0, offset);
    const endBuf = view.buffer.slice(offset + size);

    return new Blob([startBuf, pHYsData, endBuf], options);
  }
}


================================================
FILE: next.config.js
================================================
/** @type {import('next').NextConfig} */
const nextConfig = {}

module.exports = nextConfig


================================================
FILE: package.json
================================================
{
  "name": "draw-a-ui",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint"
  },
  "dependencies": {
    "@tldraw/tldraw": "2.0.0-alpha.17",
    "canvas-size": "^1.2.6",
    "next": "^14.2.3",
    "openai": "^4.47.1",
    "prismjs": "^1.29.0",
    "react": "^18.3.1",
    "react-dom": "^18.3.1"
  },
  "devDependencies": {
    "@types/canvas-size": "^1.2.1",
    "@types/node": "^20",
    "@types/prismjs": "^1.26.3",
    "@types/react": "^18",
    "@types/react-dom": "^18",
    "autoprefixer": "^10.0.1",
    "eslint": "^8",
    "eslint-config-next": "^14.2.3",
    "postcss": "^8",
    "tailwindcss": "^3.3.0",
    "typescript": "^5"
  },
  "engines": {
    "node": ">=18.0.0"
  }
}


================================================
FILE: postcss.config.js
================================================
module.exports = {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
  },
}


================================================
FILE: tailwind.config.ts
================================================
import type { Config } from 'tailwindcss'

const config: Config = {
  content: [
    './pages/**/*.{js,ts,jsx,tsx,mdx}',
    './components/**/*.{js,ts,jsx,tsx,mdx}',
    './app/**/*.{js,ts,jsx,tsx,mdx}',
  ],
  theme: {
    extend: {
      backgroundImage: {
        'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))',
        'gradient-conic':
          'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))',
      },
    },
  },
  plugins: [],
}
export default config


================================================
FILE: tsconfig.json
================================================
{
  "compilerOptions": {
    "target": "es5",
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "skipLibCheck": true,
    "strict": true,
    "noEmit": true,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "bundler",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "preserve",
    "incremental": true,
    "plugins": [
      {
        "name": "next"
      }
    ],
    "paths": {
      "@/*": ["./*"]
    }
  },
  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
  "exclude": ["node_modules"]
}
Download .txt
gitextract_w27hu8_s/

├── .eslintrc.json
├── .github/
│   └── workflows/
│       └── ci.yml
├── .gitignore
├── LICENSE
├── README.md
├── app/
│   ├── api/
│   │   └── toHtml/
│   │       └── route.ts
│   ├── globals.css
│   ├── layout.tsx
│   └── page.tsx
├── components/
│   └── PreviewModal.tsx
├── lib/
│   ├── blobToBase64.ts
│   ├── getBrowserCanvasMaxSize.ts
│   ├── getSvgAsImage.ts
│   └── png.ts
├── next.config.js
├── package.json
├── postcss.config.js
├── tailwind.config.ts
└── tsconfig.json
Download .txt
SYMBOL INDEX (28 symbols across 8 files)

FILE: app/api/toHtml/route.ts
  function POST (line 8) | async function POST(request: Request) {

FILE: app/layout.tsx
  function RootLayout (line 12) | function RootLayout({

FILE: app/page.tsx
  function Home (line 16) | function Home() {
  function ExportButton (line 54) | function ExportButton({ setHtml }: { setHtml: (html: string) => void }) {

FILE: components/PreviewModal.tsx
  function PreviewModal (line 9) | function PreviewModal({
  type TabButtonProps (line 95) | interface TabButtonProps extends React.HTMLAttributes<HTMLButtonElement> {
  function TabButton (line 99) | function TabButton({ active, ...buttonProps }: TabButtonProps) {

FILE: lib/blobToBase64.ts
  function blobToBase64 (line 1) | function blobToBase64(blob: Blob) {

FILE: lib/getBrowserCanvasMaxSize.ts
  type CanvasMaxSize (line 3) | type CanvasMaxSize = {
  function getBrowserCanvasMaxSize (line 11) | function getBrowserCanvasMaxSize() {
  function calculateBrowserCanvasMaxSize (line 19) | async function calculateBrowserCanvasMaxSize(): Promise<CanvasMaxSize> {

FILE: lib/getSvgAsImage.ts
  type TLCopyType (line 4) | type TLCopyType = "jpeg" | "json" | "png" | "svg";
  type TLExportType (line 5) | type TLExportType = "jpeg" | "json" | "png" | "svg" | "webp";
  function getSvgAsImage (line 10) | async function getSvgAsImage(
  function getSvgAsDataUrl (line 102) | async function getSvgAsDataUrl(svg: SVGElement) {
  function getSvgAsDataUrlSync (line 127) | function getSvgAsDataUrlSync(node: SVGElement) {

FILE: lib/png.ts
  type BufferInput (line 1) | type BufferInput = string | ArrayBuffer | Buffer;
  type CRCCalculator (line 3) | interface CRCCalculator<T = BufferInput | Uint8Array> {
  constant TABLE (line 7) | let TABLE: Array<number> | Int32Array = [
  constant LEN_SIZE (line 68) | const LEN_SIZE = 4;
  constant CRC_SIZE (line 69) | const CRC_SIZE = 4;
  class PngHelpers (line 72) | class PngHelpers {
    method isPng (line 73) | static isPng(view: DataView, offset: number) {
    method getChunkType (line 89) | static getChunkType(view: DataView, offset: number) {
    method readChunks (line 98) | static readChunks(view: DataView, offset = 0) {
    method parsePhys (line 134) | static parsePhys(view: DataView, offset: number) {
    method findChunk (line 142) | static findChunk(view: DataView, type: string) {
    method setPhysChunk (line 147) | static setPhysChunk(view: DataView, dpr = 1, options?: BlobPropertyBag) {
Condensed preview — 19 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (27K chars).
[
  {
    "path": ".eslintrc.json",
    "chars": 40,
    "preview": "{\n  \"extends\": \"next/core-web-vitals\"\n}\n"
  },
  {
    "path": ".github/workflows/ci.yml",
    "chars": 492,
    "preview": "name: Build and Lint\n\non:\n  push:\n    branches: [main]\n  pull_request:\n    branches: [main]\n\njobs:\n  build-and-lint:\n   "
  },
  {
    "path": ".gitignore",
    "chars": 391,
    "preview": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/.pn"
  },
  {
    "path": "LICENSE",
    "chars": 1068,
    "preview": "MIT License\n\nCopyright (c) 2023 Sawyer Hood\n\nPermission is hereby granted, free of charge, to any person obtaining a cop"
  },
  {
    "path": "README.md",
    "chars": 1094,
    "preview": "# draw-a-ui\n\nThis is an app that uses tldraw and the gpt-4-vision api to generate html based on a wireframe you draw.\n\n>"
  },
  {
    "path": "app/api/toHtml/route.ts",
    "chars": 1184,
    "preview": "import { OpenAI } from \"openai\";\n\nconst systemPrompt = `You are an expert tailwind developer. A user will provide you wi"
  },
  {
    "path": "app/globals.css",
    "chars": 159,
    "preview": "@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\n.tlui-help-menu {\n  display: none !important;\n}\n\n.tlui-debug"
  },
  {
    "path": "app/layout.tsx",
    "chars": 617,
    "preview": "import type { Metadata } from \"next\";\nimport { Inter } from \"next/font/google\";\nimport \"./globals.css\";\n\nconst inter = I"
  },
  {
    "path": "app/page.tsx",
    "chars": 3298,
    "preview": "\"use client\";\n\nimport dynamic from \"next/dynamic\";\nimport \"@tldraw/tldraw/tldraw.css\";\nimport { useEditor } from \"@tldra"
  },
  {
    "path": "components/PreviewModal.tsx",
    "chars": 2829,
    "preview": "\"use client\";\n\nimport { use, useEffect, useState } from \"react\";\nimport Prism from \"prismjs\";\nimport \"prismjs/components"
  },
  {
    "path": "lib/blobToBase64.ts",
    "chars": 212,
    "preview": "export function blobToBase64(blob: Blob) {\n  return new Promise((resolve, _) => {\n    const reader = new FileReader();\n "
  },
  {
    "path": "lib/getBrowserCanvasMaxSize.ts",
    "chars": 751,
    "preview": "import canvasSize from \"canvas-size\";\n\nexport type CanvasMaxSize = {\n  maxWidth: number;\n  maxHeight: number;\n  maxArea:"
  },
  {
    "path": "lib/getSvgAsImage.ts",
    "chars": 3942,
    "preview": "import { getBrowserCanvasMaxSize } from \"./getBrowserCanvasMaxSize\";\nimport { PngHelpers } from \"./png\";\n\ntype TLCopyTyp"
  },
  {
    "path": "lib/png.ts",
    "chars": 6883,
    "preview": "type BufferInput = string | ArrayBuffer | Buffer;\n\ninterface CRCCalculator<T = BufferInput | Uint8Array> {\n  (value: T, "
  },
  {
    "path": "next.config.js",
    "chars": 92,
    "preview": "/** @type {import('next').NextConfig} */\nconst nextConfig = {}\n\nmodule.exports = nextConfig\n"
  },
  {
    "path": "package.json",
    "chars": 792,
    "preview": "{\n  \"name\": \"draw-a-ui\",\n  \"version\": \"0.1.0\",\n  \"private\": true,\n  \"scripts\": {\n    \"dev\": \"next dev\",\n    \"build\": \"ne"
  },
  {
    "path": "postcss.config.js",
    "chars": 82,
    "preview": "module.exports = {\n  plugins: {\n    tailwindcss: {},\n    autoprefixer: {},\n  },\n}\n"
  },
  {
    "path": "tailwind.config.ts",
    "chars": 495,
    "preview": "import type { Config } from 'tailwindcss'\n\nconst config: Config = {\n  content: [\n    './pages/**/*.{js,ts,jsx,tsx,mdx}',"
  },
  {
    "path": "tsconfig.json",
    "chars": 595,
    "preview": "{\n  \"compilerOptions\": {\n    \"target\": \"es5\",\n    \"lib\": [\"dom\", \"dom.iterable\", \"esnext\"],\n    \"allowJs\": true,\n    \"sk"
  }
]

About this extraction

This page contains the full source code of the SawyerHood/draw-a-ui GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 19 files (24.4 KB), approximately 8.5k tokens, and a symbol index with 28 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!