├── .gitignore ├── Dockerfile ├── README.md ├── bun.lockb ├── docker-compose.yml ├── htmx-ai.js ├── htmx.min.js ├── index.html ├── package.json ├── red-button.html ├── server.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore 2 | 3 | # Logs 4 | 5 | logs 6 | _.log 7 | npm-debug.log_ 8 | yarn-debug.log* 9 | yarn-error.log* 10 | lerna-debug.log* 11 | .pnpm-debug.log* 12 | 13 | # Caches 14 | 15 | .cache 16 | 17 | # Diagnostic reports (https://nodejs.org/api/report.html) 18 | 19 | report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json 20 | 21 | # Runtime data 22 | 23 | pids 24 | _.pid 25 | _.seed 26 | *.pid.lock 27 | 28 | # Directory for instrumented libs generated by jscoverage/JSCover 29 | 30 | lib-cov 31 | 32 | # Coverage directory used by tools like istanbul 33 | 34 | coverage 35 | *.lcov 36 | 37 | # nyc test coverage 38 | 39 | .nyc_output 40 | 41 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 42 | 43 | .grunt 44 | 45 | # Bower dependency directory (https://bower.io/) 46 | 47 | bower_components 48 | 49 | # node-waf configuration 50 | 51 | .lock-wscript 52 | 53 | # Compiled binary addons (https://nodejs.org/api/addons.html) 54 | 55 | build/Release 56 | 57 | # Dependency directories 58 | 59 | node_modules/ 60 | jspm_packages/ 61 | 62 | # Snowpack dependency directory (https://snowpack.dev/) 63 | 64 | web_modules/ 65 | 66 | # TypeScript cache 67 | 68 | *.tsbuildinfo 69 | 70 | # Optional npm cache directory 71 | 72 | .npm 73 | 74 | # Optional eslint cache 75 | 76 | .eslintcache 77 | 78 | # Optional stylelint cache 79 | 80 | .stylelintcache 81 | 82 | # Microbundle cache 83 | 84 | .rpt2_cache/ 85 | .rts2_cache_cjs/ 86 | .rts2_cache_es/ 87 | .rts2_cache_umd/ 88 | 89 | # Optional REPL history 90 | 91 | .node_repl_history 92 | 93 | # Output of 'npm pack' 94 | 95 | *.tgz 96 | 97 | # Yarn Integrity file 98 | 99 | .yarn-integrity 100 | 101 | # dotenv environment variable files 102 | 103 | .env 104 | .env.development.local 105 | .env.test.local 106 | .env.production.local 107 | .env.local 108 | 109 | # parcel-bundler cache (https://parceljs.org/) 110 | 111 | .parcel-cache 112 | 113 | # Next.js build output 114 | 115 | .next 116 | out 117 | 118 | # Nuxt.js build / generate output 119 | 120 | .nuxt 121 | dist 122 | 123 | # Gatsby files 124 | 125 | # Comment in the public line in if your project uses Gatsby and not Next.js 126 | 127 | # https://nextjs.org/blog/next-9-1#public-directory-support 128 | 129 | # public 130 | 131 | # vuepress build output 132 | 133 | .vuepress/dist 134 | 135 | # vuepress v2.x temp and cache directory 136 | 137 | .temp 138 | 139 | # Docusaurus cache and generated files 140 | 141 | .docusaurus 142 | 143 | # Serverless directories 144 | 145 | .serverless/ 146 | 147 | # FuseBox cache 148 | 149 | .fusebox/ 150 | 151 | # DynamoDB Local files 152 | 153 | .dynamodb/ 154 | 155 | # TernJS port file 156 | 157 | .tern-port 158 | 159 | # Stores VSCode versions used for testing VSCode extensions 160 | 161 | .vscode-test 162 | 163 | # yarn v2 164 | 165 | .yarn/cache 166 | .yarn/unplugged 167 | .yarn/build-state.yml 168 | .yarn/install-state.gz 169 | .pnp.* 170 | 171 | # IntelliJ based IDEs 172 | .idea 173 | 174 | # Finder (MacOS) folder config 175 | .DS_Store 176 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # use the official Bun image 2 | # see all versions at https://hub.docker.com/r/oven/bun/tags 3 | FROM oven/bun:1 as base 4 | WORKDIR /usr/src/app 5 | 6 | # install dependencies into temp directory 7 | # this will cache them and speed up future builds 8 | FROM base AS install 9 | RUN mkdir -p /temp/dev 10 | COPY package.json bun.lockb /temp/dev/ 11 | RUN cd /temp/dev && bun install --frozen-lockfile 12 | 13 | # install with --production (exclude devDependencies) 14 | RUN mkdir -p /temp/prod 15 | COPY package.json bun.lockb /temp/prod/ 16 | RUN cd /temp/prod && bun install --frozen-lockfile --production 17 | 18 | # copy node_modules from temp directory 19 | # then copy all (non-ignored) project files into the image 20 | FROM base AS prerelease 21 | COPY --from=install /temp/dev/node_modules node_modules 22 | COPY . . 23 | 24 | # [optional] tests & build 25 | ENV NODE_ENV=production 26 | RUN bun test 27 | 28 | # copy production dependencies and source code into final image 29 | FROM base AS release 30 | COPY --from=install /temp/prod/node_modules node_modules 31 | COPY --from=prerelease /usr/src/app/server.ts . 32 | COPY --from=prerelease /usr/src/app/package.json . 33 | 34 | # run the app 35 | USER bun 36 | EXPOSE 80/tcp 37 | ENTRYPOINT [ "bun", "--watch", "run", "server.ts"] 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # htmx-ai 2 | 3 | HTMX-AI allows you to create AI generated webdesign by just providing a prompt via the `hx-ai` attribute. 4 | 5 | **You can see a [live demo here](https://htmx-ai.bufferhead.com/)** 6 | 7 | [](http://www.youtube.com/watch?v=NP6hpM5YLRo 'Build your Website with HTMX powered by AI') 8 | 9 | **You can learn more about how it works and how i made it [here](http://www.youtube.com/watch?v=NP6hpM5YLRo)** 10 | 11 | ```html 12 | 13 | ``` 14 | 15 | You can use hx-target just like you would in any other HTMX application to specify where the response should be inserted. 16 | 17 | ```html 18 | 19 |
20 | ``` 21 | 22 | ## Configuration 23 | 24 | HTMX-AI uses the OpenAI APi in the background. You need to provide an API key in the `.env` file. 25 | 26 | ```env 27 | OPENAI_API_KEY=your-api-key 28 | ``` 29 | 30 | To enable the htmx-ai extension on a page you need to initialize it on one parent element like this: 31 | 32 | ```html 33 | 34 | ``` 35 | 36 | If you want to use anything other than the default api endpoint at htmx-ai.test, you can set a custom endpoint 37 | 38 | ```html 39 | 40 | ``` 41 | 42 | ## Run with reverse proxy 43 | 44 | First you need to configure traefik as a reverse proxy. (like described [here](https://github.com/korridor/reverse-proxy-docker-traefik)). 45 | 46 | Afterwards you can start the service with the following command: 47 | 48 | ```bash 49 | docker-compose up -d 50 | ``` 51 | 52 | ## Run with bun 53 | 54 | To install dependencies: 55 | 56 | ```bash 57 | bun install 58 | ``` 59 | 60 | To run: 61 | 62 | ```bash 63 | bun run server.ts 64 | ``` 65 | 66 | ## Disclaimer 67 | 68 | DO NOT use untested and unreviewed AI generated content in production. This is a proof of concept and should not be used in production. -------------------------------------------------------------------------------- /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bufferhead-code/htmx-ai/3cb054283a0883729e52cc10bf3bf57736807169/bun.lockb -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | js: 3 | build: . 4 | labels: 5 | - "traefik.enable=true" 6 | - "traefik.http.routers.js.rule=Host(`htmx-ai.test`)" 7 | - "traefik.http.routers.js.entrypoints=web" 8 | - "traefik.http.services.js.loadbalancer.server.port=80" 9 | environment: 10 | OPENAI_API_KEY: '${OPENAI_API_KEY}' 11 | volumes: 12 | - "./server.ts:/usr/src/app/server.ts" 13 | networks: 14 | - reverse-proxy 15 | networks: 16 | reverse-proxy: 17 | name: "${NETWORK_NAME}" 18 | external: true 19 | -------------------------------------------------------------------------------- /htmx-ai.js: -------------------------------------------------------------------------------- 1 | htmx.defineExtension('ai', { 2 | onEvent: function (name, evt) { 3 | if (name === 'htmx:beforeProcessNode') { 4 | evt.target.querySelectorAll('[hx-ai]').forEach(el => { 5 | const nextEndpoint = el.closest(['hx-ai-endpoint'])?.getAttribute('hx-ai-endpoint'); 6 | el.setAttribute('hx-post', nextEndpoint ?? 'http://htmx-ai.test/'); 7 | if(el.getAttribute('hx-ai').startsWith('js:')){ 8 | el.setAttribute('hx-vals', 'js:{prompt: ' + el.getAttribute('hx-ai').replace('js:', '') + '}'); 9 | } 10 | else { 11 | el.setAttribute('hx-vals', '{"prompt": "' + el.getAttribute('hx-ai') + '"}'); 12 | } 13 | }); 14 | } 15 | } 16 | }) -------------------------------------------------------------------------------- /htmx.min.js: -------------------------------------------------------------------------------- 1 | (function(e,t){if(typeof define==="function"&&define.amd){define([],t)}else if(typeof module==="object"&&module.exports){module.exports=t()}else{e.htmx=e.htmx||t()}})(typeof self!=="undefined"?self:this,function(){return function(){"use strict";var Q={onLoad:F,process:zt,on:de,off:ge,trigger:ce,ajax:Nr,find:C,findAll:f,closest:v,values:function(e,t){var r=dr(e,t||"post");return r.values},remove:_,addClass:z,removeClass:n,toggleClass:$,takeClass:W,defineExtension:Ur,removeExtension:Br,logAll:V,logNone:j,logger:null,config:{historyEnabled:true,historyCacheSize:10,refreshOnHistoryMiss:false,defaultSwapStyle:"innerHTML",defaultSwapDelay:0,defaultSettleDelay:20,includeIndicatorStyles:true,indicatorClass:"htmx-indicator",requestClass:"htmx-request",addedClass:"htmx-added",settlingClass:"htmx-settling",swappingClass:"htmx-swapping",allowEval:true,allowScriptTags:true,inlineScriptNonce:"",attributesToSettle:["class","style","width","height"],withCredentials:false,timeout:0,wsReconnectDelay:"full-jitter",wsBinaryType:"blob",disableSelector:"[hx-disable], [data-hx-disable]",useTemplateFragments:false,scrollBehavior:"smooth",defaultFocusScroll:false,getCacheBusterParam:false,globalViewTransitions:false,methodsThatUseUrlParams:["get"],selfRequestsOnly:false,ignoreTitle:false,scrollIntoViewOnBoost:true,triggerSpecsCache:null},parseInterval:d,_:t,createEventSource:function(e){return new EventSource(e,{withCredentials:true})},createWebSocket:function(e){var t=new WebSocket(e,[]);t.binaryType=Q.config.wsBinaryType;return t},version:"1.9.10"};var r={addTriggerHandler:Lt,bodyContains:se,canAccessLocalStorage:U,findThisElement:xe,filterValues:yr,hasAttribute:o,getAttributeValue:te,getClosestAttributeValue:ne,getClosestMatch:c,getExpressionVars:Hr,getHeaders:xr,getInputValues:dr,getInternalData:ae,getSwapSpecification:wr,getTriggerSpecs:it,getTarget:ye,makeFragment:l,mergeObjects:le,makeSettleInfo:T,oobSwap:Ee,querySelectorExt:ue,selectAndSwap:je,settleImmediately:nr,shouldCancel:ut,triggerEvent:ce,triggerErrorEvent:fe,withExtensions:R};var w=["get","post","put","delete","patch"];var i=w.map(function(e){return"[hx-"+e+"], [data-hx-"+e+"]"}).join(", ");var S=e("head"),q=e("title"),H=e("svg",true);function e(e,t=false){return new RegExp(`<${e}(\\s[^>]*>|>)([\\s\\S]*?)<\\/${e}>`,t?"gim":"im")}function d(e){if(e==undefined){return undefined}let t=NaN;if(e.slice(-2)=="ms"){t=parseFloat(e.slice(0,-2))}else if(e.slice(-1)=="s"){t=parseFloat(e.slice(0,-1))*1e3}else if(e.slice(-1)=="m"){t=parseFloat(e.slice(0,-1))*1e3*60}else{t=parseFloat(e)}return isNaN(t)?undefined:t}function ee(e,t){return e.getAttribute&&e.getAttribute(t)}function o(e,t){return e.hasAttribute&&(e.hasAttribute(t)||e.hasAttribute("data-"+t))}function te(e,t){return ee(e,t)||ee(e,"data-"+t)}function u(e){return e.parentElement}function re(){return document}function c(e,t){while(e&&!t(e)){e=u(e)}return e?e:null}function L(e,t,r){var n=te(t,r);var i=te(t,"hx-disinherit");if(e!==t&&i&&(i==="*"||i.split(" ").indexOf(r)>=0)){return"unset"}else{return n}}function ne(t,r){var n=null;c(t,function(e){return n=L(t,e,r)});if(n!=="unset"){return n}}function h(e,t){var r=e.matches||e.matchesSelector||e.msMatchesSelector||e.mozMatchesSelector||e.webkitMatchesSelector||e.oMatchesSelector;return r&&r.call(e,t)}function A(e){var t=/<([a-z][^\/\0>\x20\t\r\n\f]*)/i;var r=t.exec(e);if(r){return r[1].toLowerCase()}else{return""}}function a(e,t){var r=new DOMParser;var n=r.parseFromString(e,"text/html");var i=n.body;while(t>0){t--;i=i.firstChild}if(i==null){i=re().createDocumentFragment()}return i}function N(e){return/"+n+"",0);return i.querySelector("template").content}switch(r){case"thead":case"tbody":case"tfoot":case"colgroup":case"caption":return a("46 | Generating new hero section, this might take a while. AI might be cool but it's not fast. 47 |
48 |The revolutionary plugin for AI-driven interactions.
58 |<button hx-ai="Make a fancy website">Activate AI</button>
70 |
71 | 107 | Generating new feature section, this might take a while. AI might be cool but it's not fast. 108 |
109 |Revolutionizing how you interact 119 | with 120 | HTMX
121 |Easily integrate HTMX AI Plugin into your projects without 136 | hassle.
137 |Experience blazing fast performance with HTMX AI 151 | Plugin.
152 |Leverage the power of AI for advanced functionality.
168 |