├── .gitattributes ├── llm-coder ├── src ├── noop.js ├── Header.js ├── utils │ ├── crypto.js │ ├── csp.js │ ├── logging.js │ └── math.js ├── constants │ ├── global.js │ └── chat.js ├── Home.js ├── root.js ├── App.js ├── LICENSE ├── sw.js ├── index.html ├── hooks │ ├── useLLMVision.js │ ├── useLLMHtmlGeneration.js │ ├── useSpeech.js │ ├── useLLMVision.jsold.js │ └── useLLMGeneration.js ├── playground │ └── model_load_test.ts └── Chat.js ├── public ├── site.gif ├── site.webm ├── site.webp └── site-50.webp ├── Makefile ├── site.apng ├── README.md ├── .github └── workflows │ └── static.yml ├── .gitignore └── LICENSE /.gitattributes: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /llm-coder: -------------------------------------------------------------------------------- 1 | ./src -------------------------------------------------------------------------------- /src/noop.js: -------------------------------------------------------------------------------- 1 | export default {}; 2 | -------------------------------------------------------------------------------- /public/site.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pdufour/llm-coder/HEAD/public/site.gif -------------------------------------------------------------------------------- /public/site.webm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pdufour/llm-coder/HEAD/public/site.webm -------------------------------------------------------------------------------- /public/site.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pdufour/llm-coder/HEAD/public/site.webp -------------------------------------------------------------------------------- /public/site-50.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pdufour/llm-coder/HEAD/public/site-50.webp -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | run: 2 | static-web-server -p 3000 --cors-allow-origins "*" --cors-allow-headers "*" --cors-expose-headers "*" -d . -e false -z 3 | -------------------------------------------------------------------------------- /site.apng: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:fd72f5fddc54105afd8b3bdde7d5821ff38e3e89ef92714f267544b583352cd2 3 | size 39922469 4 | -------------------------------------------------------------------------------- /src/Header.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const h = React.createElement; 4 | 5 | export const Header = () => h('div', { className: 'w-full overflow-hidden' }); 6 | -------------------------------------------------------------------------------- /src/utils/crypto.js: -------------------------------------------------------------------------------- 1 | export const encodeSHA256 = async (script) => 2 | `${btoa( 3 | String.fromCharCode( 4 | ...new Uint8Array( 5 | await crypto.subtle.digest("SHA-256", new TextEncoder().encode(script)) 6 | ) 7 | ) 8 | )}`; 9 | -------------------------------------------------------------------------------- /src/constants/global.js: -------------------------------------------------------------------------------- 1 | export const SESSION_OPTIONS = { 2 | executionProviders: ['webgpu'], 3 | graphOptimizationLevel: 'all', 4 | executionMode: 'sequential', 5 | intraOpNumThreads: 4, 6 | interOpNumThreads: 4, 7 | logSeverityLevel: 1, 8 | enableMemoryPattern: true, 9 | } 10 | -------------------------------------------------------------------------------- /src/Home.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Chat } from "./Chat.js"; 3 | import { Header } from "./Header.js"; 4 | 5 | const h = React.createElement; 6 | 7 | export const Home = () => h(React.Fragment, null, 8 | h(Header), 9 | h('div', { className: 'w-full' }, 10 | h(Chat) 11 | ) 12 | ); 13 | -------------------------------------------------------------------------------- /src/root.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | 4 | import { App } from "./App.js"; 5 | 6 | // await navigator.serviceWorker.ready; 7 | const rootElement = document.getElementById("root"); 8 | const root = ReactDOM.createRoot(rootElement); 9 | root.render(React.createElement(App)); 10 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import { env } from "@huggingface/transformers"; 2 | import React from "react"; 3 | import { Home } from "./Home.js"; 4 | 5 | const h = React.createElement; 6 | 7 | async function init() { 8 | const wasmPaths = new URL( 9 | ".", 10 | await import.meta.resolve("#onnxruntime-webgpu") 11 | ).toString(); 12 | env.backends.onnx.wasm.wasmPaths = wasmPaths; 13 | } 14 | 15 | export const App = () => { 16 | return h( 17 | "div", 18 | { className: "min-h-screen min-w-full" }, 19 | h( 20 | "div", 21 | { className: "mx-auto self-center flex flex-col items-center" }, 22 | h( 23 | "div", 24 | { 25 | className: 26 | "flex flex-col w-full self-center items-center justify-center", 27 | }, 28 | h(Home) 29 | ) 30 | ) 31 | ); 32 | } 33 | 34 | init(); 35 | -------------------------------------------------------------------------------- /src/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Paul Dufour 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 | # llm-coder 2 | llm-coder is an open source privacy-first AI website-builder powered by your browser. No API keys required. No server required. See the live demo at [https://pdufour.github.io/llm-coder/](https://pdufour.github.io/llm-coder/). 3 | 4 | ![site](https://github.com/pdufour/llm-coder/raw/main/public/site-50.webp) 5 | 6 | **Privacy:** All AI models run locally in your browser. Your data never leaves your device - no servers involved. 7 | 8 | It supports the following features: 9 | 1. Describe a site you want - the tool will generate the HTML and CSS for you 10 | 2. Drag and drop an image - the tool will generate the HTML and CSS for you 11 | 12 | ## Tech Stack 13 | - [onnxruntime-web](https://onnxruntime.ai/docs/tutorials/web/) 14 | 15 | ## Support 16 | - Text generation (via Qwen2) 17 | - Vision parsing (via Qwen2-VL) 18 | - Audio transcription (Via Moonshine-Base) 19 | 20 | ## Design Principles 21 | - No backends 22 | - No JS build steps unless the compilation happens directly in the browser. 23 | 24 | ## Setup 25 | 26 | 1. Clone the repository 27 | 2. Install [static-web-server](https://static-web-server.net/download-and-install/#macos) 28 | 2. Run `make run` 29 | 3. Visit http://localhost:3000/llm-coder in the browser 30 | -------------------------------------------------------------------------------- /src/utils/csp.js: -------------------------------------------------------------------------------- 1 | import { encodeSHA256 } from "./crypto.js"; 2 | 3 | export class CSPBuilder { 4 | constructor() { 5 | this.policy = {}; 6 | } 7 | 8 | addDirective(directive, source) { 9 | if (!this.policy[directive]) { 10 | this.policy[directive] = new Set(); 11 | } 12 | this.policy[directive].add(source); 13 | return this; // Enable chaining 14 | } 15 | 16 | async addScriptHash(script) { 17 | const hash = await encodeSHA256(script); 18 | this.addDirective("script-src", `'sha256-${hash}'`); 19 | return this; // Enable chaining 20 | } 21 | 22 | addScriptNonce() { 23 | if (!this.nonce) { 24 | this.nonce = this.generateNonce(); 25 | } 26 | this.addDirective("script-src", `'nonce-${this.nonce}'`); 27 | return this; // Enable chaining 28 | } 29 | 30 | generateNonce() { 31 | const array = new Uint8Array(16); // 128-bit nonce 32 | crypto.getRandomValues(array); 33 | return btoa(String.fromCharCode(...array)); 34 | } 35 | 36 | async build() { 37 | const cspString = Object.entries(this.policy) 38 | .map( 39 | ([directive, sources]) => 40 | `${directive} ${Array.from(sources).join(" ")}` 41 | ) 42 | .join("; "); 43 | return { cspString, nonce: this.nonce }; // Return both CSP and nonce for use in templates 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /.github/workflows/static.yml: -------------------------------------------------------------------------------- 1 | # Simple workflow for deploying static content to GitHub Pages 2 | name: Deploy static content to Pages 3 | 4 | on: 5 | # Runs on pushes targeting the default branch 6 | push: 7 | branches: ["main"] 8 | 9 | # Allows you to run this workflow manually from the Actions tab 10 | workflow_dispatch: 11 | 12 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 13 | permissions: 14 | contents: read 15 | pages: write 16 | id-token: write 17 | 18 | # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. 19 | # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. 20 | concurrency: 21 | group: "pages" 22 | cancel-in-progress: false 23 | 24 | jobs: 25 | # Single deploy job since we're just deploying 26 | deploy: 27 | environment: 28 | name: github-pages 29 | url: ${{ steps.deployment.outputs.page_url }} 30 | runs-on: ubuntu-latest 31 | steps: 32 | - name: Checkout 33 | uses: actions/checkout@v4 34 | - name: Setup Pages 35 | uses: actions/configure-pages@v5 36 | - name: Upload artifact 37 | uses: actions/upload-pages-artifact@v3 38 | with: 39 | # Upload entire repository 40 | path: 'src' 41 | - name: Deploy to GitHub Pages 42 | id: deployment 43 | uses: actions/deploy-pages@v4 44 | -------------------------------------------------------------------------------- /src/sw.js: -------------------------------------------------------------------------------- 1 | import { registerRoute } from "https://cdn.jsdelivr.net/npm/workbox-routing/+esm"; 2 | import { NetworkFirst } from "https://cdn.jsdelivr.net/npm/workbox-strategies/+esm"; 3 | 4 | import initSwc, { 5 | transformSync, 6 | } from "https://cdn.jsdelivr.net/npm/@swc/wasm-web/+esm"; 7 | import "https://cdn.jsdelivr.net/npm/workbox-sw/+esm"; 8 | 9 | workbox.setConfig({ 10 | debug: true, 11 | }); 12 | 13 | self.addEventListener("install", function (event) { 14 | event.waitUntil(self.skipWaiting()); 15 | }); 16 | 17 | self.addEventListener("activate", function (event) { 18 | event.waitUntil(self.clients.claim()); 19 | }); 20 | 21 | let swcInitialized = false; 22 | 23 | registerRoute( 24 | ({ url }) => url.pathname.match(/\.(tsx?|jsx)$/), 25 | async ({ request }) => { 26 | const response = await fetch(request); 27 | 28 | if (!swcInitialized) { 29 | await initSwc(); 30 | swcInitialized = true; 31 | } 32 | 33 | const source = await response.text(); 34 | 35 | const { code } = transformSync(source, { 36 | jsc: { 37 | "externalHelpers": true, 38 | loose: true, 39 | parser: { syntax: "typescript", tsx: true }, 40 | target: "es2022", 41 | transform: { 42 | react: { 43 | development: true, 44 | }, 45 | }, 46 | }, 47 | module: { 48 | type: "es6", 49 | interop: "node" // Or "swc" or "none" 50 | }, 51 | }); 52 | 53 | return new Response(code, { 54 | headers: { 55 | "Content-Type": "application/javascript", 56 | "Cache-Control": "no-cache", 57 | }, 58 | }); 59 | } 60 | ); 61 | -------------------------------------------------------------------------------- /src/utils/logging.js: -------------------------------------------------------------------------------- 1 | import { Float16Array, getFloat16, setFloat16 } from "@petamoriken/float16"; 2 | 3 | class Logger { 4 | static indentLevel = 0 5 | 6 | static group(...args) { 7 | logger.indentLevel += 1 8 | console.group(...args) 9 | } 10 | 11 | static groupCollapsed(...args) { 12 | logger.indentLevel += 1 13 | console.groupCollapsed(...args) 14 | } 15 | 16 | static groupEnd() { 17 | if (logger.indentLevel === 0) { 18 | console.error("logGroupEnd() called too many times") 19 | return 20 | } 21 | logger.indentLevel -= 1 22 | console.groupEnd() 23 | } 24 | 25 | static tensor(name, tensor) { 26 | logger.groupCollapsed(`[NUMPY] ${name}:`) 27 | logger.debug(`Shape: (${tensor.dims.join(", ")})`) 28 | logger.debug(`Dtype: ${tensor.type}`) 29 | 30 | let data 31 | if (tensor.data) { 32 | if (tensor.type === "float16") { 33 | const view = new DataView( 34 | tensor.data.buffer, 35 | tensor.data.byteOffset, 36 | tensor.data.byteLength 37 | ) 38 | const littleEndian = true // Adjust if necessary 39 | 40 | const numElements = tensor.data.length 41 | data = [] 42 | for (let i = 0; i < numElements; i++) { 43 | const offset = i * 2 44 | const value = getFloat16(view, offset, littleEndian) 45 | data.push(value) 46 | } 47 | } else { 48 | data = Array.from(tensor.data).map(val => { 49 | return val.toString() 50 | }) 51 | } 52 | 53 | if (tensor.dims[0] === 1) { 54 | logger.debug(`Values: [${data[0]}]`) 55 | } 56 | 57 | logger.debug(`First few values: [${data.slice(0, 5).join(", ")}]`) 58 | logger.debug(`Last few values: [${data.slice(-5).join(", ")}]`) 59 | } 60 | logger.groupEnd() 61 | } 62 | } 63 | 64 | export const logger = new Proxy(Logger, { 65 | get(target, prop) { 66 | // If the method exists on Logger, use it 67 | if (prop in target) { 68 | return target[prop] 69 | } 70 | 71 | // If the method doesn’t exist on Logger, proxy it to console 72 | if (typeof console[prop] === "function") { 73 | return console[prop].bind(console) 74 | } 75 | 76 | // If neither Logger nor console has the method, return undefined 77 | return undefined 78 | } 79 | }) 80 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* 131 | 132 | 133 | .DS_STORE 134 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Voice Code Interface 8 | 9 | 10 | 11 |
12 | 38 | 69 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /src/constants/chat.js: -------------------------------------------------------------------------------- 1 | import { CSPBuilder } from "../utils/csp.js"; 2 | 3 | export const LLM_VISION_MODEL_CONFIG = { 4 | modelConfig: { 5 | qwen2vl: { 6 | modelId: "pdufour/Qwen2-VL-2B-Instruct-ONNX-Q4-F16", 7 | generation: { 8 | baseModelId: "Qwen/Qwen2-VL-2B-Instruct", 9 | max_single_chat_length: 256, 10 | max_seq_length: 1024, 11 | }, 12 | } 13 | }, 14 | }; 15 | 16 | export const LLM_HTML_MODEL_CONFIG = { 17 | modelConfig: { 18 | webllm: { 19 | modelId: "Qwen2.5-Coder-1.5B-Instruct-q4f16_1-MLC", 20 | generation: { 21 | max_tokens: 500, 22 | temperature: 0.3, 23 | top_p: 0.9, 24 | }, 25 | warmup: { 26 | max_tokens: 1, 27 | } 28 | }, 29 | huggingface: { 30 | modelId: "Qwen/Qwen2.5-Coder-1.5B-Instruct", 31 | options: { 32 | device: "webgpu", 33 | dtype: "q4f16", 34 | }, 35 | generation: { 36 | max_new_tokens: 500, 37 | temperature: 0.3, 38 | top_p: 0.9, 39 | do_sample: true, 40 | }, 41 | warmup: { 42 | max_new_tokens: 1, 43 | } 44 | }, 45 | }, 46 | backend: "webllm", 47 | }; 48 | 49 | const { cspString: IFRAME_CSP, nonce: IFRAME_CSP_NONCE } = 50 | await new CSPBuilder() 51 | .addDirective("default-src", "'none'") 52 | .addDirective("img-src", "'none'") 53 | .addDirective("style-src", "'unsafe-inline'") 54 | .addDirective("script-src", "https://cdn.tailwindcss.com") 55 | .addScriptNonce() 56 | .build(); 57 | 58 | export const IFRAME_POSTMESSAGE_SCRIPT = /*js*/ ` 59 | window.addEventListener('message', function(event) { 60 | if (event.origin !== "${window.location.origin}") { 61 | return; 62 | } 63 | 64 | if (event.data.type === 'updateContent') { 65 | // Update the body content 66 | document.body.innerHTML = event.data.content; 67 | 68 | // Find and execute all script tags in the new content 69 | const scripts = document.body.querySelectorAll('script'); 70 | scripts.forEach(script => { 71 | const newScript = document.createElement('script'); 72 | if (script.src) { 73 | // If the script has a src attribute, copy it 74 | newScript.src = script.src; 75 | } else { 76 | // Otherwise, copy the inline script content 77 | newScript.textContent = script.textContent; 78 | } 79 | document.body.appendChild(newScript); 80 | // Remove the original script tag 81 | script.remove(); 82 | }); 83 | } 84 | }); 85 | `; 86 | 87 | export const IFRAME_TAILWIND_URL = `https://cdn.tailwindcss.com`; 88 | 89 | export const IFRAME_TAILWIND_CONFIG_SCRIPT = /*js*/ `; 90 | tailwind.config = { 91 | theme: { 92 | extend: { 93 | colors: { 94 | primary: "#3B82F6", 95 | secondary: "#10B981", 96 | }, 97 | }, 98 | }, 99 | }; 100 | `; 101 | 102 | export const IFRAME_TEMPLATE = ` 103 | 104 | 105 | 106 | 107 | 108 | 109 | 116 | 119 | 120 | 121 | 122 | 123 | `; 124 | -------------------------------------------------------------------------------- /src/hooks/useLLMVision.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useLLMGeneration } from "./useLLMGeneration.js"; 3 | export const SYSTEM_PROMPT = `You are a specialized HTML generator that creates complete, production-ready HTML using ONLY Tailwind CSS classes. 4 | 5 | IMPORTANT: 6 | - Use EXCLUSIVELY Tailwind CSS classes for ALL styling - NO style attributes or style tags 7 | - Every visual styling must be done through Tailwind classes 8 | - Never use inline styles, CSS, or style tags 9 | - If you need a style that seems custom, use Tailwind's arbitrary value syntax like [w-123px] 10 | - Always write out the full, actual HTML elements with real content 11 | - Never use Lorem Ipsum - write realistic English content 12 | - Never include code comments or explanations 13 | - DO NOT wrap the output in markdown code blocks 14 | 15 | STRICT STYLING RULES: 16 | - ❌ NO