visual-ai-editor
v1.6.0
Published
AI-powered visual HTML editor with selection tools, design-system enforcement, and surgical source patching. Works with any OpenAI-compatible LLM provider, including local ones like Ollama.
Maintainers
Readme
visual-ai-editor
AI-powered visual HTML editor. Select any element on the page, describe what you want to change, and the AI modifies it following your design system.
Works everywhere: static HTML, React, Angular, Vue, or any framework — with surgical, formatting-preserving saves on static/server-rendered pages, and an AI-agent handoff for framework pages where the rendered DOM can't be written straight back to source.
Bring your own LLM: any OpenAI-compatible chat-completions API works (Groq, OpenAI, OpenRouter, Ollama, LM Studio, ...) — see Choosing an AI provider.
This is a development/staging tool by default. It serves your project directory over HTTP and its API is unauthenticated unless you configure
apiToken. Read Security & production before deploying it anywhere reachable by the public internet.
Install
npm install visual-ai-editorThat's the only install — markdown rendering for the DESIGN.md viewer
(marked) is bundled directly into
dist/, and TypeScript types for both the client (visual-ai-editor) and
server (visual-ai-editor/server) entry points ship with the package —
nothing extra to install for either.
Zero-config start (recommended)
npx visual-ai-editor startFirst run creates a .env in your project root — open it, paste your API key
into AI_API_KEY=, and run the same command again. That's the entire setup:
- the server boots and serves your project's static files plus the
/api/*endpoints; - the editor toolbar is auto-injected into every
.htmlpage served — no start script to write, no<script>tags to add; .gitignoreis created/updated so the.env(and your key) never reaches git;- your browser opens on the served project.
Flags: --port <n> (default 3000), --no-inject (serve without auto-injecting
the client), --no-open (don't open the browser). Running the bare
npx visual-ai-editor does the same as start.
Auto-injection is skipped on any page that already loads the editor manually or
has a data-ai-editor="off" attribute, so it never double-boots. Using a local
model (Ollama / LM Studio)? Uncomment the matching AI_ENDPOINT line in the
generated .env — no API key needed.
Manual wiring (start script + explicit script tag)
Prefer explicit control, or need the server embedded in your own process? Everything the CLI does is available programmatically:
1. Create start-ai-editor.js in your project root:
const { startServer } = require('visual-ai-editor/server');
startServer({
port: 3000,
envPath: require('path').join(__dirname, '.env'), // AI_API_KEY=...
inject: true // optional — auto-inject the client like the CLI does (default: false)
});2. Add one line to your HTML (before </body> — skip if using inject: true):
<script type="module">
import { init } from '/__ai-editor/ai-editor.esm.js';
init({ apiBase: '/api' });
</script>(The /__ai-editor/ virtual path is served by the backend from this package's
own dist/ — the old ./node_modules/visual-ai-editor/dist/... path also
still works.)
3. Create .env with your key:
# Any OpenAI-compatible provider works — see "Choosing an AI provider" below.
# Groq's free tier is the fastest way to try it: console.groq.com
AI_API_KEY=your_key_here4. Run:
node start-ai-editor.jsEither way, the CSS is embedded in the bundle and injected automatically (no relative paths to break), and the backend runs in-process, serving your project's static files plus the /api/* endpoints.
Choosing an AI provider
By default the server talks to Groq's OpenAI-compatible endpoint, but any OpenAI-compatible chat-completions API works — pass ai options to startServer() or set environment variables:
startServer({
ai: {
endpoint: 'https://api.openai.com/v1/chat/completions', // default: Groq
model: 'gpt-4o-mini', // default: llama-3.3-70b-versatile
apiKey: process.env.OPENAI_API_KEY // default: AI_API_KEY / GROQ_API_KEY env
}
});Or via .env (no code changes needed):
AI_ENDPOINT=https://api.openai.com/v1/chat/completions
AI_MODEL=gpt-4o-mini
AI_API_KEY=sk-...Local models (Ollama, LM Studio)
Local providers get a shorthand — no need to type out the endpoint URL, and no API key is required (there's nothing to authenticate against on localhost; the server auto-detects this from the endpoint's host):
startServer({
ai: { provider: 'ollama', model: 'llama3.2' } // pull it first: `ollama pull llama3.2`
});startServer({
ai: { provider: 'lmstudio', model: 'your-loaded-model' }
});provider: 'ollama' resolves to http://localhost:11434/v1/chat/completions;
provider: 'lmstudio' to http://localhost:1234/v1/chat/completions. An
explicit endpoint always overrides the preset, so nothing about the
generic contract above changes — this is purely a shortcut. The apiKey
requirement auto-detects off for any localhost/127.0.0.1 endpoint, preset
or not; override either direction with requiresApiKey: true / false.
A note on model capability. This was tested live against a real local
Ollama instance during development. The plumbing (no-auth requests, error
propagation, structured-JSON parsing) works correctly regardless of model
size. Response quality doesn't: a capable model (Groq's 70B, GPT-4o-mini,
or a similarly-sized local model your hardware can run) reliably follows the
full instruction set (DESIGN.md compliance, force mode, the {html}/{warn}
JSON contract). A small local model (3B parameters, in testing) sometimes
produced truncated or malformed JSON once the system prompt included a
project's DESIGN.md — this is a model-capability limit, not a bug to route
around; if edits come back malformed or empty with a local model, try a
larger one before assuming something's broken.
The legacy GROQ_API_KEY/GROQ_MODEL env vars still work as a fallback for existing setups.
AI agent onboarding
On npm install, this package also delivers an AGENTS.md file to your
project root — a guide explaining to any AI coding agent (Claude Code, Cursor,
etc.) how the editor works: the API contract, how DESIGN.md enforcement
works, how force mode works, and common troubleshooting. This means an agent
working on your project can read AGENTS.md and understand this tool without
you having to explain it.
- If your project has no
AGENTS.mdyet, one is created. - If you already have one, our section is appended to it (or updated in place on future upgrades) — your existing content is never touched.
- If your npm setup blocks install scripts, run
npx visual-ai-editor agents:initmanually to get the same result.
DESIGN.md — keep the AI on-brand
DESIGN.md is what stops the AI from inventing colors, spacing, or components
outside your design system. If your project doesn't have one yet, this package
helps you create it, following the methodology from
camaraux.com.br/como-criar-aplicar-design-md-ia.
On npm install, a postinstall check looks for DESIGN.md in your project
and prints a one-line nudge if it's missing — it never fails the install.
# Locate DESIGN.md and report which of the 11 recommended sections were found
npx visual-ai-editor design:check
# No DESIGN.md yet? Generate a guided prompt (DESIGN.prompt.md)
npx visual-ai-editor design:init
# Scan your project's CSS/HTML/JS for colors that aren't in DESIGN.md's palette
npx visual-ai-editor design:lintdesign:lint is a deterministic, non-AI check — it extracts every color mentioned in DESIGN.md (frontmatter tokens, prose, code fences) and flags any hex/rgb/rgba color elsewhere in your project that isn't in that set. It's a heuristic (regex-based, so hash-prefixed IDs or dynamically-generated colors can produce false positives), but it's a fast way to catch drift between your documented palette and what's actually in your CSS. The same palette check runs automatically, server-side, on every /api/edit response — see below.
design:init writes DESIGN.prompt.md to your project root. Paste it into
Claude Code, Cursor, or any AI agent (cat DESIGN.prompt.md | claude works
too) — it walks the agent through an interview (visual personality, references,
product type, priorities) and then the 11-section structure (Overview,
Principles, Colors, Typography, Spacing, Elevation, Layout, Components,
Accessibility, AI Rules, Example Prompts) plus a validation checklist. Once
DESIGN.md exists at your project root, the editor picks it up automatically
— no configuration needed.
If your npm setup requires approving install scripts (e.g. npm audit
signatures / allow-scripts policies), the postinstall check is opt-in-safe:
it never blocks or fails the install either way — worst case, you just don't
get the one-line nudge and can run design:check manually.
Deterministic palette enforcement
DESIGN.md enforcement isn't just "hope the LLM notices" — every /api/edit
response is checked server-side against every color mentioned in DESIGN.md
(via regex extraction, not another AI call). If the model's edit introduces a
color that's neither in your palette nor already present in the selected
element, the request comes back as a warning (same { warn } shape as when
the model itself declines), with the computed HTML attached so "apply anyway"
doesn't cost a second AI call. This catches the case where the model thinks
it's compliant but isn't — it doesn't replace the model's own judgment about
spacing, typography or component conflicts, which still relies on the model
reading DESIGN.md in its prompt.
Saving edits
Static / server-rendered HTML (no DESIGN.md frontend framework detected):
AIEditor.saveToFile() sends surgical patches — { before, after } pairs
of the exact HTML that changed — to /api/save, which locates and replaces
just that text in your source file. This means:
- Your file's formatting, comments and indentation elsewhere are untouched.
- Git diffs are small and readable — one line changed, not the whole file.
- A timestamped backup is written to
.ai-editor/history/before every save (kept up to 100 most recent; add.ai-editor/to.gitignore). - If a patch's
beforetext can't be located (you hand-edited the file since the last save, for example), that edit alone falls back to writing the full page snapshot — you're told which mode was used in the status bar. - Multi-page projects: pass
page(the browser'slocation.pathname, sent automatically) and the server resolves it to a file insidestaticDir, rejecting any path that escapes it.
Framework pages (React/Vue/... — detected via React's _debugSource fiber
data or Vue 3's __file component metadata, or an explicit
data-ai-source="path/to/File.jsx:42" attribute you add yourself): writing
the rendered DOM back over JSX/templates would be wrong, so saveToFile()
instead posts a handoff manifest to /api/handoff, appended to
.ai-editor/pending-changes.md — one entry per edit, with the detected
source location, the instruction, and the before/after HTML. Open that file
with an AI coding agent (Claude Code, Cursor, ...) and ask it to apply the
listed changes to your real source files; delete each entry once applied.
Quick Start (manual wiring)
HTML (script tag)
<link rel="stylesheet" href="node_modules/visual-ai-editor/dist/ai-editor.css">
<script src="node_modules/visual-ai-editor/dist/ai-editor.js"></script>
<script>
AIEditor.init({ apiBase: '/api' });
</script>React
import { useEffect } from 'react';
import AIEditor from 'visual-ai-editor';
import 'visual-ai-editor/dist/ai-editor.css';
function App() {
useEffect(() => {
AIEditor.init({ apiBase: '/api' });
return () => AIEditor.destroy();
}, []);
return <div>Your content here</div>;
}Angular
import { Component, OnInit, OnDestroy } from '@angular/core';
import AIEditor from 'visual-ai-editor';
import 'visual-ai-editor/dist/ai-editor.css';
@Component({ selector: 'app-root', template: '<div>...</div>' })
export class AppComponent implements OnInit, OnDestroy {
ngOnInit() { AIEditor.init({ apiBase: '/api' }); }
ngOnDestroy() { AIEditor.destroy(); }
}Vue
<script setup>
import { onMounted, onUnmounted } from 'vue';
import AIEditor from 'visual-ai-editor';
import 'visual-ai-editor/dist/ai-editor.css';
onMounted(() => AIEditor.init({ apiBase: '/api' }));
onUnmounted(() => AIEditor.destroy());
</script>API
AIEditor.init(options?)
Creates the editor UI and binds all event listeners.
| Option | Type | Default | Description |
|---|---|---|---|
| apiBase | string | '/api' | Base URL for backend endpoints |
| apiToken | string | none | Sent as Authorization: Bearer <token> on write endpoints. Must match the server's apiToken (see Security & production) |
| cssInject | boolean | true | Auto-inject CSS into <head> |
| cssUrl | string | 'dist/ai-editor.css' | Custom CSS URL when injecting |
| locale | 'en' \| 'pt-BR' | <html lang> or 'en' | UI language |
| maxHtmlSize | number | 60000 | Reject a selection (client-side, no request sent) whose serialized HTML exceeds this many characters |
| onAfterApply | (elements) => void | none | Called after an AI edit (or redo) swaps in new elements. Use this to rebind event listeners your own code attached to the old elements — a DOM replacement doesn't carry them over (see Event listeners on edited elements) |
| onAfterUndo | (elements) => void | none | Called after an undo restores the previous elements |
AIEditor.destroy()
Removes all editor UI, event listeners, and cleans up state. Call this on component unmount.
AIEditor.setTool(tool)
Switches the active selection tool.
'cursor'— click to select single elements'area'— drag to select elements by rectangle'pencil'— draw freehand lasso to select elements
AIEditor.selectElements(elements)
Programmatically select an array of DOM elements.
Event listeners on edited elements
Replacing an element (el.replaceWith(newEl), which is how AI edits and redo
are applied) drops any event listeners attached directly to it with
addEventListener. If your page attaches listeners outside of a framework's
own reactivity (e.g. plain <script> blocks, as in a static HTML page), pass
onAfterApply to init() and re-run whatever wiring code attaches those
listeners, scoped to the returned elements:
init({
apiBase: '/api',
onAfterApply: function (elements) {
elements.forEach(function (el) {
if (el.matches('.chip')) el.addEventListener('click', onChipClick);
});
}
});React/Vue/Angular apps don't need this — their own reactivity re-binds listeners on re-render (and framework pages typically go through the handoff flow rather than live DOM replacement anyway).
Server Required
The editor needs a backend API with these endpoints:
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/edit | Send HTML + instruction to AI, returns { html }, { warn }, or { warn, violations, html } for a palette conflict |
| GET | /api/design | Returns { md, exists, palette } — palette is the array of colors extracted from DESIGN.md |
| POST | /api/save | Applies patches surgically to a source file (falls back to writing html in full), with an automatic backup |
| POST | /api/handoff | Appends a change manifest to .ai-editor/pending-changes.md for framework pages an AI coding agent should apply |
Setup (programmatic — recommended)
express and dotenv are already dependencies of this package — nothing extra to install.
// start-ai-editor.js
const { startServer } = require('visual-ai-editor/server');
startServer({ port: 3000, envPath: require('path').join(__dirname, '.env') });node start-ai-editor.jsstartServer(options):
| Option | Type | Default | Description |
|---|---|---|---|
| port | number | process.env.PORT \|\| 3000 | Port to listen on |
| envPath | string | none | Path to a .env file to load (AI_API_KEY/AI_ENDPOINT/AI_MODEL, or legacy GROQ_API_KEY/GROQ_MODEL) |
| designMdPath | string | <cwd>/DESIGN.md | Where to read the design system reference from. Re-read automatically when the file's mtime changes — no server restart needed |
| indexHtmlPath | string | <cwd>/index.html | Fallback save target when /api/save isn't given a page |
| staticDir | string | process.cwd() | Directory served as static files. Narrow this to a public/-style folder in production instead of your whole project root |
| apiToken | string | none | If set, /api/edit, /api/save and /api/handoff require Authorization: Bearer <token> (or an X-AI-Editor-Token header). Pass the same value to the client via init({ apiToken }) |
| allowUnsafeProduction | boolean | false | Bypasses the production safety check (see below) when you have auth at another layer (reverse proxy, VPN, etc.) |
| silent | boolean | false | If true, builds the Express app but doesn't call .listen() — returns { app, port } so you can mount it yourself |
| locale | 'en' \| 'pt-BR' | 'en' | Language for server-generated messages (errors, warnings, logs) |
| backup | boolean | true | Write a timestamped backup to .ai-editor/history/ before every save |
| maxHtmlBytes | number | 200000 | Reject /api/edit selections larger than this (413) before calling the AI provider |
| ai | object | see Choosing an AI provider | { endpoint, model, apiKey, jsonMode, temperature } |
Setup (legacy — copy server.js)
Still supported for backward compatibility:
cp node_modules/visual-ai-editor/server/server.js ./server.js
cp node_modules/visual-ai-editor/server/.env.example ./.env
# Edit .env and set GROQ_API_KEY=your_key_here
node server.jsSecurity & production
This tool is built for local development and internal/staging use — a designer or developer editing their own project on their own machine. It is not hardened to be exposed to the public internet as-is. Concretely:
/api/editand/api/saveare unauthenticated by default. Anyone who can reach the server can burn through your Groq API quota via/api/edit, or overwriteindexHtmlPathvia/api/save. SetapiToken(server) + pass the same value toinit({ apiToken })(client) to require a bearer token on both endpoints. This is a shared-secret check, not real user authentication — it stops opportunistic/automated abuse, not a determined attacker who can read your page's source (the token is visible in the client bundle you serve). For real production protection, put this behind your own auth layer (session check, reverse-proxy auth, VPN) and treatapiTokenas a second layer, not the only one.- The static file server defaults to serving your whole
process.cwd()..git/,node_modules/,.env*, lockfiles,.ssh/,.aws/, and this tool's own.ai-editor/(save backups + handoff manifests, which may contain snippets of your source) are blocked outright. Everything else understaticDiris still served, because the editor needs to read/write your actual project files. In production, setstaticDirto a narrowpublic/folder rather than your whole repo. - Save backups accumulate in
.ai-editor/history/(capped at the 100 most recent per file). Add.ai-editor/to.gitignore— it's local working state, not something to commit. startServer()/buildApp()refuse to start whenNODE_ENV=productionand noapiTokenis set — you'll get a thrown error explaining why, instead of a silently-exposed editor. PassallowUnsafeProduction: trueonly if you've already solved authentication at another layer and understand the tradeoff.- The client-side toolbar has no visibility gate. If you ship the
init()call in your production bundle, every visitor sees and can use the editor UI (even if the backend correctly rejects their requests). Gateinit()behind an environment check or an authenticated route in your own app — e.g. only call it whenprocess.env.NODE_ENV !== 'production', or behind a feature flag / admin-only route.
Recommended pattern: run this on a separate, non-public port/host during
development or in a staging environment gated by your own auth (e.g. behind a
VPN or an authenticated reverse proxy), with apiToken set as a second layer.
Don't wire it into your public production bundle.
CSS Variables
The editor reads these CSS variables from your page:
:root {
--font: 'Inter', sans-serif; /* Font family */
--warning: #f4b400; /* Warning color (yellow) */
--lg: 16px; /* Toolbar left position */
}Features
- Click selection — click any element to select it
- Area selection — drag a rectangle to select multiple elements
- Lasso selection — draw freehand around elements
- AI editing — describe changes in natural language, any OpenAI-compatible provider
- Design system enforcement — the AI is instructed to follow
DESIGN.md, backed by a deterministic server-side color-palette check that catches what the model misses - Force mode — override design-system warnings; reuses the already-computed HTML, so it costs zero extra AI calls
- Surgical saves — patches your source file in place, preserving formatting, with an automatic pre-save backup
- AI-agent handoff — framework pages (React/Vue) export a change manifest instead of corrupting rendered output
- Undo & redo — Ctrl+Z / Ctrl+Y (client-side, zero tokens)
- Multi-element editing — select multiple elements and edit them together
- DESIGN.md viewer — view your design system reference in a modal
design:lint— deterministic CLI check for off-palette colors across your whole project- English / Portuguese (pt-BR) UI — auto-detected from
<html lang>, or set explicitly
File Structure
visual-ai-editor/
├── dist/
│ ├── ai-editor.js (UMD — <script> tag)
│ ├── ai-editor.esm.js (ES modules — import)
│ ├── ai-editor.min.js (minified)
│ └── ai-editor.css (styles)
├── src/
│ ├── index.js (entry point)
│ ├── core.js (state, utils, CSS-context collection, source detection)
│ ├── i18n.js (client-side EN / pt-BR strings)
│ ├── tools.js (selection tools)
│ ├── selection.js (selection logic)
│ ├── actions.js (AI, save, undo/redo)
│ └── ui.js (UI creation, init/destroy)
├── server/
│ ├── index.js (Express app — startServer/buildApp)
│ └── server.js (legacy shim)
├── lib/
│ ├── design-tokens.js (color extraction + palette validation)
│ ├── patch.js (surgical source patching — exact + fuzzy match)
│ ├── design-check.js (DESIGN.md discovery + section coverage)
│ ├── server-messages.js (server-side EN / pt-BR strings)
│ └── agents-md.js (AGENTS.md installer)
├── types/
│ ├── index.d.ts (client types)
│ └── server.d.ts (server types)
├── test/ (node:test — `npm test`)
└── examples/
├── static.html
├── react.jsx
├── angular.ts
└── vue.vueLicense
MIT
