[
  {
    "path": ".eslintrc.json",
    "content": "{\n  \"extends\": \"next/core-web-vitals\"\n}\n"
  },
  {
    "path": ".github/workflows/ci.yml",
    "content": "name: Build and Lint\n\non:\n  push:\n    branches: [main]\n  pull_request:\n    branches: [main]\n\njobs:\n  build-and-lint:\n    runs-on: ubuntu-latest\n\n    steps:\n      - uses: actions/checkout@v2\n\n      - name: Set up Node.js\n        uses: actions/setup-node@v2\n        with:\n          node-version: \"20\" # Specify the Node.js version\n\n      - name: Install dependencies\n        run: npm install\n\n      - name: Run build\n        run: npm run build\n\n      - name: Run lint\n        run: npm run lint\n"
  },
  {
    "path": ".gitignore",
    "content": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/.pnp\n.pnp.js\n.yarn/install-state.gz\n\n# testing\n/coverage\n\n# next.js\n/.next/\n/out/\n\n# production\n/build\n\n# misc\n.DS_Store\n*.pem\n\n# debug\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n\n# local env files\n.env*.local\n\n# vercel\n.vercel\n\n# typescript\n*.tsbuildinfo\nnext-env.d.ts\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2023 Sawyer Hood\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# 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> The spiritual successor to this project is [Terragon Labs](https://terragonlabs.com).\n\n![A demo of the app](./demo.gif)\n\nThis 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.\n\n> 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.\n\n## Getting Started\n\nThis 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.\n\n> 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).\n\n```bash\necho \"OPENAI_API_KEY=sk-your-key\" > .env.local\nnpm install\nnpm run dev\n```\n\nOpen [http://localhost:3000](http://localhost:3000) with your browser to see the result.\n"
  },
  {
    "path": "app/api/toHtml/route.ts",
    "content": "import { OpenAI } from \"openai\";\n\nconst systemPrompt = `You are an expert tailwind developer. A user will provide you with a\n low-fidelity wireframe of an application and you will return \n a single html file that uses tailwind to create the website. Use creative license to make the application more fleshed out.\nif you need to insert an image, use placehold.co to create a placeholder image. Respond only with the html file.`;\n\nexport async function POST(request: Request) {\n  const openai = new OpenAI();\n  const { image } = await request.json();\n\n  const resp = await openai.chat.completions.create({\n    model: \"gpt-4o\",\n    max_tokens: 4096,\n    messages: [\n      {\n        role: \"system\",\n        content: systemPrompt,\n      },\n      {\n        role: \"user\",\n        content: [\n          {\n            type: \"image_url\",\n            image_url: { url: image, detail: \"high\" },\n          },\n          {\n            type: \"text\",\n            text: \"Turn this into a single html file using tailwind.\",\n          },\n        ],\n      },\n    ],\n  });\n\n  return new Response(JSON.stringify(resp), {\n    headers: {\n      \"content-type\": \"application/json; charset=UTF-8\",\n    },\n  });\n}\n"
  },
  {
    "path": "app/globals.css",
    "content": "@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\n.tlui-help-menu {\n  display: none !important;\n}\n\n.tlui-debug-panel {\n  display: none !important;\n}\n"
  },
  {
    "path": "app/layout.tsx",
    "content": "import type { Metadata } from \"next\";\nimport { Inter } from \"next/font/google\";\nimport \"./globals.css\";\n\nconst inter = Inter({ subsets: [\"latin\"] });\n\nexport const metadata: Metadata = {\n  title: \"Create Next App\",\n  description: \"Generated by create next app\",\n};\n\nexport default function RootLayout({\n  children,\n}: {\n  children: React.ReactNode;\n}) {\n  return (\n    <html lang=\"en\">\n      <head>\n        <meta\n          name=\"viewport\"\n          content=\"width=device-width, initial-scale=1, viewport-fit=cover\"\n        />\n      </head>\n      <body className={inter.className}>{children}</body>\n    </html>\n  );\n}\n"
  },
  {
    "path": "app/page.tsx",
    "content": "\"use client\";\n\nimport dynamic from \"next/dynamic\";\nimport \"@tldraw/tldraw/tldraw.css\";\nimport { useEditor } from \"@tldraw/tldraw\";\nimport { getSvgAsImage } from \"@/lib/getSvgAsImage\";\nimport { blobToBase64 } from \"@/lib/blobToBase64\";\nimport React, { useEffect, useState } from \"react\";\nimport ReactDOM from \"react-dom\";\nimport { PreviewModal } from \"@/components/PreviewModal\";\n\nconst Tldraw = dynamic(async () => (await import(\"@tldraw/tldraw\")).Tldraw, {\n  ssr: false,\n});\n\nexport default function Home() {\n  const [html, setHtml] = useState<null | string>(null);\n\n  useEffect(() => {\n    const listener = (e: KeyboardEvent) => {\n      if (e.key === \"Escape\") {\n        setHtml(null);\n      }\n    };\n    window.addEventListener(\"keydown\", listener);\n\n    return () => {\n      window.removeEventListener(\"keydown\", listener);\n    };\n  });\n\n  return (\n    <>\n      <div className={`w-screen h-screen`}>\n        <Tldraw persistenceKey=\"tldraw\">\n          <ExportButton setHtml={setHtml} />\n        </Tldraw>\n      </div>\n      {html &&\n        ReactDOM.createPortal(\n          <div\n            className=\"fixed top-0 left-0 right-0 bottom-0 flex justify-center items-center\"\n            style={{ zIndex: 2000, backgroundColor: \"rgba(0,0,0,0.5)\" }}\n            onClick={() => setHtml(null)}\n          >\n            <PreviewModal html={html} setHtml={setHtml} />\n          </div>,\n          document.body\n        )}\n    </>\n  );\n}\n\nfunction ExportButton({ setHtml }: { setHtml: (html: string) => void }) {\n  const editor = useEditor();\n  const [loading, setLoading] = useState(false);\n  // A tailwind styled button that is pinned to the bottom right of the screen\n  return (\n    <button\n      onClick={async (e) => {\n        setLoading(true);\n        try {\n          e.preventDefault();\n          const svg = await editor.getSvg(\n            Array.from(editor.currentPageShapeIds)\n          );\n          if (!svg) {\n            return;\n          }\n          const png = await getSvgAsImage(svg, {\n            type: \"png\",\n            quality: 1,\n            scale: 1,\n          });\n          const dataUrl = await blobToBase64(png!);\n          const resp = await fetch(\"/api/toHtml\", {\n            method: \"POST\",\n            headers: {\n              \"Content-Type\": \"application/json\",\n            },\n            body: JSON.stringify({ image: dataUrl }),\n          });\n\n          const json = await resp.json();\n\n          if (json.error) {\n            alert(\"Error from open ai: \" + JSON.stringify(json.error));\n            return;\n          }\n\n          const message = json.choices[0].message.content;\n          const start = message.indexOf(\"<!DOCTYPE html>\");\n          const end = message.indexOf(\"</html>\");\n          const html = message.slice(start, end + \"</html>\".length);\n          setHtml(html);\n        } finally {\n          setLoading(false);\n        }\n      }}\n      className=\"fixed bottom-4 right-4 bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded =\"\n      style={{ zIndex: 1000 }}\n      disabled={loading}\n    >\n      {loading ? (\n        <div className=\"flex justify-center items-center \">\n          <div className=\"animate-spin rounded-full h-4 w-4 border-b-2 border-white\"></div>\n        </div>\n      ) : (\n        \"Make Real\"\n      )}\n    </button>\n  );\n}\n"
  },
  {
    "path": "components/PreviewModal.tsx",
    "content": "\"use client\";\n\nimport { use, useEffect, useState } from \"react\";\nimport Prism from \"prismjs\";\nimport \"prismjs/components/prism-cshtml\";\n\nimport \"prismjs/themes/prism-tomorrow.css\";\n\nexport function PreviewModal({\n  html,\n  setHtml,\n}: {\n  html: string | null;\n  setHtml: (html: string | null) => void;\n}) {\n  const [activeTab, setActiveTab] = useState<\"preview\" | \"code\">(\"preview\");\n\n  useEffect(() => {\n    const highlight = async () => {\n      await Prism.highlightAll(); // <--- prepare Prism\n    };\n    highlight(); // <--- call the async function\n  }, [html, activeTab]); // <--- run when post updates\n\n  if (!html) {\n    return null;\n  }\n\n  return (\n    <div\n      onClick={(e) => {\n        e.stopPropagation();\n      }}\n      className=\"bg-white rounded-lg shadow-xl flex flex-col\"\n      style={{\n        width: \"calc(100% - 64px)\",\n        height: \"calc(100% - 64px)\",\n      }}\n    >\n      <div className=\"flex justify-between items-center p-4 border-b\">\n        <div className=\"flex space-x-1\">\n          <TabButton\n            active={activeTab === \"preview\"}\n            onClick={() => {\n              setActiveTab(\"preview\");\n            }}\n          >\n            Preview\n          </TabButton>\n          <TabButton\n            active={activeTab === \"code\"}\n            onClick={() => {\n              setActiveTab(\"code\");\n            }}\n          >\n            Code\n          </TabButton>\n        </div>\n\n        <button\n          className=\"p-2 rounded-md hover:bg-gray-200 focus:outline-none focus:ring\"\n          onClick={() => {\n            setHtml(null);\n          }}\n        >\n          <svg\n            className=\"w-6 h-6 text-gray-600\"\n            xmlns=\"http://www.w3.org/2000/svg\"\n            fill=\"none\"\n            viewBox=\"0 0 24 24\"\n            stroke=\"currentColor\"\n            aria-hidden=\"true\"\n          >\n            <path\n              strokeLinecap=\"round\"\n              strokeLinejoin=\"round\"\n              strokeWidth=\"2\"\n              d=\"M6 18L18 6M6 6l12 12\"\n            ></path>\n          </svg>\n        </button>\n      </div>\n\n      {activeTab === \"preview\" ? (\n        <iframe className=\"w-full h-full\" srcDoc={html} />\n      ) : (\n        <pre className=\"overflow-auto p-4\">\n          <code className=\"language-markup\">{html}</code>\n        </pre>\n      )}\n    </div>\n  );\n}\n\ninterface TabButtonProps extends React.HTMLAttributes<HTMLButtonElement> {\n  active: boolean;\n}\n\nfunction TabButton({ active, ...buttonProps }: TabButtonProps) {\n  const className = active\n    ? \"px-4 py-2 text-sm font-medium text-white bg-blue-500 rounded-t-md focus:outline-none focus:ring\"\n    : \"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\";\n  return <button className={className} {...buttonProps}></button>;\n}\n"
  },
  {
    "path": "lib/blobToBase64.ts",
    "content": "export function blobToBase64(blob: Blob) {\n  return new Promise((resolve, _) => {\n    const reader = new FileReader();\n    reader.onloadend = () => resolve(reader.result);\n    reader.readAsDataURL(blob);\n  });\n}\n"
  },
  {
    "path": "lib/getBrowserCanvasMaxSize.ts",
    "content": "import canvasSize from \"canvas-size\";\n\nexport type CanvasMaxSize = {\n  maxWidth: number;\n  maxHeight: number;\n  maxArea: number;\n};\n\nlet maxSizePromise: Promise<CanvasMaxSize> | null = null;\n\nexport function getBrowserCanvasMaxSize() {\n  if (!maxSizePromise) {\n    maxSizePromise = calculateBrowserCanvasMaxSize();\n  }\n\n  return maxSizePromise;\n}\n\nasync function calculateBrowserCanvasMaxSize(): Promise<CanvasMaxSize> {\n  const maxWidth = await canvasSize.maxWidth({ usePromise: true });\n  const maxHeight = await canvasSize.maxHeight({ usePromise: true });\n  const maxArea = await canvasSize.maxArea({ usePromise: true });\n  return {\n    maxWidth: maxWidth.width,\n    maxHeight: maxHeight.height,\n    maxArea: maxArea.width * maxArea.height,\n  };\n}\n"
  },
  {
    "path": "lib/getSvgAsImage.ts",
    "content": "import { getBrowserCanvasMaxSize } from \"./getBrowserCanvasMaxSize\";\nimport { PngHelpers } from \"./png\";\n\ntype TLCopyType = \"jpeg\" | \"json\" | \"png\" | \"svg\";\ntype TLExportType = \"jpeg\" | \"json\" | \"png\" | \"svg\" | \"webp\";\n\n/**\n * This is all copied from node_modules/@tldraw/tldraw/src/lib/utils/export.ts\n */\nexport async function getSvgAsImage(\n  svg: SVGElement,\n  options: {\n    type: TLCopyType | TLExportType;\n    quality: number;\n    scale: number;\n  }\n) {\n  const { type, quality, scale } = options;\n\n  const width = +svg.getAttribute(\"width\")!;\n  const height = +svg.getAttribute(\"height\")!;\n  let scaledWidth = width * scale;\n  let scaledHeight = height * scale;\n\n  const dataUrl = await getSvgAsDataUrl(svg);\n\n  const canvasSizes = await getBrowserCanvasMaxSize();\n  if (width > canvasSizes.maxWidth) {\n    scaledWidth = canvasSizes.maxWidth;\n    scaledHeight = (scaledWidth / width) * height;\n  }\n  if (height > canvasSizes.maxHeight) {\n    scaledHeight = canvasSizes.maxHeight;\n    scaledWidth = (scaledHeight / height) * width;\n  }\n  if (scaledWidth * scaledHeight > canvasSizes.maxArea) {\n    const ratio = Math.sqrt(canvasSizes.maxArea / (scaledWidth * scaledHeight));\n    scaledWidth *= ratio;\n    scaledHeight *= ratio;\n  }\n\n  scaledWidth = Math.floor(scaledWidth);\n  scaledHeight = Math.floor(scaledHeight);\n  const effectiveScale = scaledWidth / width;\n\n  const canvas = await new Promise<HTMLCanvasElement | null>((resolve) => {\n    const image = new Image();\n    image.crossOrigin = \"anonymous\";\n\n    image.onload = async () => {\n      // safari will fire `onLoad` before the fonts in the SVG are\n      // actually loaded. just waiting around a while is brittle, but\n      // there doesn't seem to be any better solution for now :( see\n      // https://bugs.webkit.org/show_bug.cgi?id=219770\n      await new Promise((resolve) => setTimeout(resolve, 250));\n\n      const canvas = document.createElement(\"canvas\") as HTMLCanvasElement;\n      const ctx = canvas.getContext(\"2d\")!;\n\n      canvas.width = scaledWidth;\n      canvas.height = scaledHeight;\n\n      ctx.imageSmoothingEnabled = true;\n      ctx.imageSmoothingQuality = \"high\";\n      ctx.drawImage(image, 0, 0, scaledWidth, scaledHeight);\n\n      URL.revokeObjectURL(dataUrl);\n\n      resolve(canvas);\n    };\n\n    image.onerror = () => {\n      resolve(null);\n    };\n\n    image.src = dataUrl;\n  });\n\n  if (!canvas) return null;\n\n  const blob = await new Promise<Blob | null>((resolve) =>\n    canvas.toBlob(\n      (blob) => {\n        if (!blob) {\n          resolve(null);\n        }\n        resolve(blob);\n      },\n      \"image/\" + type,\n      quality\n    )\n  );\n\n  if (!blob) return null;\n\n  const view = new DataView(await blob.arrayBuffer());\n  return PngHelpers.setPhysChunk(view, effectiveScale, {\n    type: \"image/\" + type,\n  });\n}\n\nexport async function getSvgAsDataUrl(svg: SVGElement) {\n  const clone = svg.cloneNode(true) as SVGGraphicsElement;\n  clone.setAttribute(\"encoding\", 'UTF-8\"');\n\n  const fileReader = new FileReader();\n  const imgs = Array.from(clone.querySelectorAll(\"image\")) as SVGImageElement[];\n\n  for (const img of imgs) {\n    const src = img.getAttribute(\"xlink:href\");\n    if (src) {\n      if (!src.startsWith(\"data:\")) {\n        const blob = await (await fetch(src)).blob();\n        const base64 = await new Promise<string>((resolve, reject) => {\n          fileReader.onload = () => resolve(fileReader.result as string);\n          fileReader.onerror = () => reject(fileReader.error);\n          fileReader.readAsDataURL(blob);\n        });\n        img.setAttribute(\"xlink:href\", base64);\n      }\n    }\n  }\n\n  return getSvgAsDataUrlSync(clone);\n}\n\nexport function getSvgAsDataUrlSync(node: SVGElement) {\n  const svgStr = new XMLSerializer().serializeToString(node);\n  // NOTE: `unescape` works everywhere although deprecated\n  const base64SVG = window.btoa(unescape(encodeURIComponent(svgStr)));\n  return `data:image/svg+xml;base64,${base64SVG}`;\n}\n"
  },
  {
    "path": "lib/png.ts",
    "content": "type BufferInput = string | ArrayBuffer | Buffer;\n\ninterface CRCCalculator<T = BufferInput | Uint8Array> {\n  (value: T, previous?: number): number;\n}\n\nlet TABLE: Array<number> | Int32Array = [\n  0x00000000, 0x77073096, 0xee0e612c, 0x990951ba, 0x076dc419, 0x706af48f,\n  0xe963a535, 0x9e6495a3, 0x0edb8832, 0x79dcb8a4, 0xe0d5e91e, 0x97d2d988,\n  0x09b64c2b, 0x7eb17cbd, 0xe7b82d07, 0x90bf1d91, 0x1db71064, 0x6ab020f2,\n  0xf3b97148, 0x84be41de, 0x1adad47d, 0x6ddde4eb, 0xf4d4b551, 0x83d385c7,\n  0x136c9856, 0x646ba8c0, 0xfd62f97a, 0x8a65c9ec, 0x14015c4f, 0x63066cd9,\n  0xfa0f3d63, 0x8d080df5, 0x3b6e20c8, 0x4c69105e, 0xd56041e4, 0xa2677172,\n  0x3c03e4d1, 0x4b04d447, 0xd20d85fd, 0xa50ab56b, 0x35b5a8fa, 0x42b2986c,\n  0xdbbbc9d6, 0xacbcf940, 0x32d86ce3, 0x45df5c75, 0xdcd60dcf, 0xabd13d59,\n  0x26d930ac, 0x51de003a, 0xc8d75180, 0xbfd06116, 0x21b4f4b5, 0x56b3c423,\n  0xcfba9599, 0xb8bda50f, 0x2802b89e, 0x5f058808, 0xc60cd9b2, 0xb10be924,\n  0x2f6f7c87, 0x58684c11, 0xc1611dab, 0xb6662d3d, 0x76dc4190, 0x01db7106,\n  0x98d220bc, 0xefd5102a, 0x71b18589, 0x06b6b51f, 0x9fbfe4a5, 0xe8b8d433,\n  0x7807c9a2, 0x0f00f934, 0x9609a88e, 0xe10e9818, 0x7f6a0dbb, 0x086d3d2d,\n  0x91646c97, 0xe6635c01, 0x6b6b51f4, 0x1c6c6162, 0x856530d8, 0xf262004e,\n  0x6c0695ed, 0x1b01a57b, 0x8208f4c1, 0xf50fc457, 0x65b0d9c6, 0x12b7e950,\n  0x8bbeb8ea, 0xfcb9887c, 0x62dd1ddf, 0x15da2d49, 0x8cd37cf3, 0xfbd44c65,\n  0x4db26158, 0x3ab551ce, 0xa3bc0074, 0xd4bb30e2, 0x4adfa541, 0x3dd895d7,\n  0xa4d1c46d, 0xd3d6f4fb, 0x4369e96a, 0x346ed9fc, 0xad678846, 0xda60b8d0,\n  0x44042d73, 0x33031de5, 0xaa0a4c5f, 0xdd0d7cc9, 0x5005713c, 0x270241aa,\n  0xbe0b1010, 0xc90c2086, 0x5768b525, 0x206f85b3, 0xb966d409, 0xce61e49f,\n  0x5edef90e, 0x29d9c998, 0xb0d09822, 0xc7d7a8b4, 0x59b33d17, 0x2eb40d81,\n  0xb7bd5c3b, 0xc0ba6cad, 0xedb88320, 0x9abfb3b6, 0x03b6e20c, 0x74b1d29a,\n  0xead54739, 0x9dd277af, 0x04db2615, 0x73dc1683, 0xe3630b12, 0x94643b84,\n  0x0d6d6a3e, 0x7a6a5aa8, 0xe40ecf0b, 0x9309ff9d, 0x0a00ae27, 0x7d079eb1,\n  0xf00f9344, 0x8708a3d2, 0x1e01f268, 0x6906c2fe, 0xf762575d, 0x806567cb,\n  0x196c3671, 0x6e6b06e7, 0xfed41b76, 0x89d32be0, 0x10da7a5a, 0x67dd4acc,\n  0xf9b9df6f, 0x8ebeeff9, 0x17b7be43, 0x60b08ed5, 0xd6d6a3e8, 0xa1d1937e,\n  0x38d8c2c4, 0x4fdff252, 0xd1bb67f1, 0xa6bc5767, 0x3fb506dd, 0x48b2364b,\n  0xd80d2bda, 0xaf0a1b4c, 0x36034af6, 0x41047a60, 0xdf60efc3, 0xa867df55,\n  0x316e8eef, 0x4669be79, 0xcb61b38c, 0xbc66831a, 0x256fd2a0, 0x5268e236,\n  0xcc0c7795, 0xbb0b4703, 0x220216b9, 0x5505262f, 0xc5ba3bbe, 0xb2bd0b28,\n  0x2bb45a92, 0x5cb36a04, 0xc2d7ffa7, 0xb5d0cf31, 0x2cd99e8b, 0x5bdeae1d,\n  0x9b64c2b0, 0xec63f226, 0x756aa39c, 0x026d930a, 0x9c0906a9, 0xeb0e363f,\n  0x72076785, 0x05005713, 0x95bf4a82, 0xe2b87a14, 0x7bb12bae, 0x0cb61b38,\n  0x92d28e9b, 0xe5d5be0d, 0x7cdcefb7, 0x0bdbdf21, 0x86d3d2d4, 0xf1d4e242,\n  0x68ddb3f8, 0x1fda836e, 0x81be16cd, 0xf6b9265b, 0x6fb077e1, 0x18b74777,\n  0x88085ae6, 0xff0f6a70, 0x66063bca, 0x11010b5c, 0x8f659eff, 0xf862ae69,\n  0x616bffd3, 0x166ccf45, 0xa00ae278, 0xd70dd2ee, 0x4e048354, 0x3903b3c2,\n  0xa7672661, 0xd06016f7, 0x4969474d, 0x3e6e77db, 0xaed16a4a, 0xd9d65adc,\n  0x40df0b66, 0x37d83bf0, 0xa9bcae53, 0xdebb9ec5, 0x47b2cf7f, 0x30b5ffe9,\n  0xbdbdf21c, 0xcabac28a, 0x53b39330, 0x24b4a3a6, 0xbad03605, 0xcdd70693,\n  0x54de5729, 0x23d967bf, 0xb3667a2e, 0xc4614ab8, 0x5d681b02, 0x2a6f2b94,\n  0xb40bbe37, 0xc30c8ea1, 0x5a05df1b, 0x2d02ef8d,\n];\n\nif (typeof Int32Array !== \"undefined\") {\n  TABLE = new Int32Array(TABLE);\n}\n\n// crc32, https://github.com/alexgorbatchev/crc/blob/master/src/calculators/crc32.ts\nconst crc: CRCCalculator<Uint8Array> = (current, previous) => {\n  let crc = previous === 0 ? 0 : ~~previous! ^ -1;\n\n  for (let index = 0; index < current.length; index++) {\n    crc = TABLE[(crc ^ current[index]) & 0xff] ^ (crc >>> 8);\n  }\n\n  return crc ^ -1;\n};\n\nconst LEN_SIZE = 4;\nconst CRC_SIZE = 4;\n\n/** @public */\nexport class PngHelpers {\n  static isPng(view: DataView, offset: number) {\n    if (\n      view.getUint8(offset + 0) === 0x89 &&\n      view.getUint8(offset + 1) === 0x50 &&\n      view.getUint8(offset + 2) === 0x4e &&\n      view.getUint8(offset + 3) === 0x47 &&\n      view.getUint8(offset + 4) === 0x0d &&\n      view.getUint8(offset + 5) === 0x0a &&\n      view.getUint8(offset + 6) === 0x1a &&\n      view.getUint8(offset + 7) === 0x0a\n    ) {\n      return true;\n    }\n    return false;\n  }\n\n  static getChunkType(view: DataView, offset: number) {\n    return [\n      String.fromCharCode(view.getUint8(offset)),\n      String.fromCharCode(view.getUint8(offset + 1)),\n      String.fromCharCode(view.getUint8(offset + 2)),\n      String.fromCharCode(view.getUint8(offset + 3)),\n    ].join(\"\");\n  }\n\n  static readChunks(view: DataView, offset = 0) {\n    const chunks: Record<\n      string,\n      { dataOffset: number; size: number; start: number }\n    > = {};\n    if (!PngHelpers.isPng(view, offset)) {\n      throw new Error(\"Not a PNG\");\n    }\n    offset += 8;\n\n    while (offset <= view.buffer.byteLength) {\n      const start = offset;\n      const len = view.getInt32(offset);\n      offset += 4;\n      const chunkType = PngHelpers.getChunkType(view, offset);\n\n      if (chunkType === \"IDAT\" && chunks[chunkType]) {\n        offset += len + LEN_SIZE + CRC_SIZE;\n        continue;\n      }\n\n      if (chunkType === \"IEND\") {\n        break;\n      }\n\n      chunks[chunkType] = {\n        start,\n        dataOffset: offset + 4,\n        size: len,\n      };\n      offset += len + LEN_SIZE + CRC_SIZE;\n    }\n\n    return chunks;\n  }\n\n  static parsePhys(view: DataView, offset: number) {\n    return {\n      ppux: view.getUint32(offset),\n      ppuy: view.getUint32(offset + 4),\n      unit: view.getUint8(offset + 4),\n    };\n  }\n\n  static findChunk(view: DataView, type: string) {\n    const chunks = PngHelpers.readChunks(view);\n    return chunks[type];\n  }\n\n  static setPhysChunk(view: DataView, dpr = 1, options?: BlobPropertyBag) {\n    let offset = 46;\n    let size = 0;\n    const res1 = PngHelpers.findChunk(view, \"pHYs\");\n    if (res1) {\n      offset = res1.start;\n      size = res1.size;\n    }\n\n    const res2 = PngHelpers.findChunk(view, \"IDAT\");\n    if (res2) {\n      offset = res2.start;\n      size = 0;\n    }\n\n    const pHYsData = new ArrayBuffer(21);\n    const pHYsDataView = new DataView(pHYsData);\n\n    pHYsDataView.setUint32(0, 9);\n\n    pHYsDataView.setUint8(4, \"p\".charCodeAt(0));\n    pHYsDataView.setUint8(5, \"H\".charCodeAt(0));\n    pHYsDataView.setUint8(6, \"Y\".charCodeAt(0));\n    pHYsDataView.setUint8(7, \"s\".charCodeAt(0));\n\n    const DPI_96 = 2835.5;\n\n    pHYsDataView.setInt32(8, DPI_96 * dpr);\n    pHYsDataView.setInt32(12, DPI_96 * dpr);\n    pHYsDataView.setInt8(16, 1);\n\n    const crcBit = new Uint8Array(pHYsData.slice(4, 17));\n    pHYsDataView.setInt32(17, crc(crcBit));\n\n    const startBuf = view.buffer.slice(0, offset);\n    const endBuf = view.buffer.slice(offset + size);\n\n    return new Blob([startBuf, pHYsData, endBuf], options);\n  }\n}\n"
  },
  {
    "path": "next.config.js",
    "content": "/** @type {import('next').NextConfig} */\nconst nextConfig = {}\n\nmodule.exports = nextConfig\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"draw-a-ui\",\n  \"version\": \"0.1.0\",\n  \"private\": true,\n  \"scripts\": {\n    \"dev\": \"next dev\",\n    \"build\": \"next build\",\n    \"start\": \"next start\",\n    \"lint\": \"next lint\"\n  },\n  \"dependencies\": {\n    \"@tldraw/tldraw\": \"2.0.0-alpha.17\",\n    \"canvas-size\": \"^1.2.6\",\n    \"next\": \"^14.2.3\",\n    \"openai\": \"^4.47.1\",\n    \"prismjs\": \"^1.29.0\",\n    \"react\": \"^18.3.1\",\n    \"react-dom\": \"^18.3.1\"\n  },\n  \"devDependencies\": {\n    \"@types/canvas-size\": \"^1.2.1\",\n    \"@types/node\": \"^20\",\n    \"@types/prismjs\": \"^1.26.3\",\n    \"@types/react\": \"^18\",\n    \"@types/react-dom\": \"^18\",\n    \"autoprefixer\": \"^10.0.1\",\n    \"eslint\": \"^8\",\n    \"eslint-config-next\": \"^14.2.3\",\n    \"postcss\": \"^8\",\n    \"tailwindcss\": \"^3.3.0\",\n    \"typescript\": \"^5\"\n  },\n  \"engines\": {\n    \"node\": \">=18.0.0\"\n  }\n}\n"
  },
  {
    "path": "postcss.config.js",
    "content": "module.exports = {\n  plugins: {\n    tailwindcss: {},\n    autoprefixer: {},\n  },\n}\n"
  },
  {
    "path": "tailwind.config.ts",
    "content": "import type { Config } from 'tailwindcss'\n\nconst config: Config = {\n  content: [\n    './pages/**/*.{js,ts,jsx,tsx,mdx}',\n    './components/**/*.{js,ts,jsx,tsx,mdx}',\n    './app/**/*.{js,ts,jsx,tsx,mdx}',\n  ],\n  theme: {\n    extend: {\n      backgroundImage: {\n        'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))',\n        'gradient-conic':\n          'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))',\n      },\n    },\n  },\n  plugins: [],\n}\nexport default config\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"es5\",\n    \"lib\": [\"dom\", \"dom.iterable\", \"esnext\"],\n    \"allowJs\": true,\n    \"skipLibCheck\": true,\n    \"strict\": true,\n    \"noEmit\": true,\n    \"esModuleInterop\": true,\n    \"module\": \"esnext\",\n    \"moduleResolution\": \"bundler\",\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"jsx\": \"preserve\",\n    \"incremental\": true,\n    \"plugins\": [\n      {\n        \"name\": \"next\"\n      }\n    ],\n    \"paths\": {\n      \"@/*\": [\"./*\"]\n    }\n  },\n  \"include\": [\"next-env.d.ts\", \"**/*.ts\", \"**/*.tsx\", \".next/types/**/*.ts\"],\n  \"exclude\": [\"node_modules\"]\n}\n"
  }
]