skill
AV Creator
UnreviewedUse when the user asks to "create an AV", "show me a visualization", "build a chart/diagram/map in AV", "make an AV widget", "make a widget", "create a widget", "build a widget", "display something...
{
"schema_version": 1,
"type": "skill",
"slug": "av-creator",
"title": "AV Creator",
"brief": "Use when the user asks to \"create an AV\", \"show me a visualization\", \"build a chart/diagram/map in AV\", \"make an AV widget\", \"make a widget\", \"create a widget\", \"build a widget\", \"display something...",
"version": "1.0.0",
"tags": [],
"license": "MIT",
"source_path": "SKILL.md",
"readme": "# AV Creator\n\nCreate custom HTML visualizations and push them to Adom Viewer.\n\n**Terminology:** \"Widget\" and \"View\" are synonyms. Both refer to an app/tab inside Adom Viewer. There is no separate category — every piece of content pushed to AV is a view/widget and gets the same treatment.\n\n## Rule #1: Always Show the User — EVERY TIME\n\n**After ANY action that produces a visual result in AV, you MUST screenshot it and show the user.** This includes:\n\n- Pushing new content to AV\n- Updating or re-pushing existing content\n- Publishing a wiki page (push the wiki page to AV as an iframe, then screenshot)\n- Completing any workflow that changes what's visible in AV\n- Editing a skill, view, or widget file (reload AV, then screenshot)\n\nNever say \"check your viewer\", \"it's published\", \"done\", or \"updated\" without showing the result. **The user should see the visual output inline in the conversation every single time.** Capture it and present it. This is non-negotiable.\n\n## Rule #2: Real Views Only — No Fakes (keyword: `real-view`)\n\n**You MUST always use the REAL view/app — NEVER create a fake mockup, placeholder, or approximation.** This is non-negotiable.\n\nWhen showing content in AV — whether for a dropdown landing page, a wiki submission, a demo, or any other purpose — you MUST use the actual running view. This means:\n\n- **Landing pages**: When a user selects a view from the AV dropdown, load the REAL app (the actual HTML/iframe), not a placeholder screen or SVG sketch. Use sample data if no user data is available (e.g., a sample GLB for 3dView, a sample .kicad_mod for Fp3dView).\n- **Wiki submissions**: When capturing screenshots for wiki pages, capture the REAL view running with real data — not a generated mockup that looks similar.\n- **Demos and previews**: Always instantiate the actual view. If you need data to populate it, use sample data or generate real data from a real source.\n\n**What \"fake\" means and why it's banned:**\n- A static SVG/HTML that *looks like* the 3D viewer but isn't the real Babylon.js viewer = FAKE\n- A hand-crafted HTML page that *approximates* the fp3d pad view but doesn't use fp-to-3d.js = FAKE\n- A screenshot description or explainer text where the real app should be = FAKE\n- Generic placeholder screens with play-button icons and \"Ask Claude to...\" text = FAKE\n\n**The keyword `real-view`**: If the user says \"real-view\", it is a reminder that you MUST use the actual view, not a mockup. But you should ALWAYS default to using real views even without the keyword — the keyword exists only as an explicit enforcement mechanism.\n\n**Why this matters**: Views are the core feature of AV — they are real mini-apps. When you substitute a fake for a real view, you defeat the entire purpose of the platform.\n\n## Rule #3: Always Pass Attribution (viewId + skill + author)\n\n**Every tab in AV shows a tooltip on hover with attribution info.** When pushing content — whether via curl, `av_display`, or `av_display_file` — you MUST always include `viewId`, `skill`, and `author` fields. This is non-negotiable.\n\n- **`viewId`**: The View ID (e.g., `ClaudeApi`, `SymView`, `FpView`). Shown as \"View: ClaudeApi\" in tooltip. Also used for tab icon matching.\n- **`skill`**: The skill name (e.g., `claude-api`, `av-creator`, `symbol-creator`)\n- **`author`**: Full name with handle (e.g., `John Lauer (@john)`)\n\n**MCP tool calls:**\n```\nav_display_file(file_path, title, viewId=\"ClaudeApi\", skill=\"claude-api\", author=\"John Lauer (@john)\")\nav_display(content, title, viewId=\"SymView\", skill=\"symbol-creator\", author=\"John Lauer (@john)\")\n```\n\n**Tooltip shows (in order):**\n1. **View:** ClaudeApi — the widget name, so users learn what's generating content\n2. **Author:** John Lauer (@john) — who made this\n3. **Skill:** claude-api — which skill created it\n\n**Why:** Without these, the tooltip shows \"Source: av_display_file\" which is useless. The user wants to know *what widget this is*, *who made it*, and *what skill created it* — not the internal transport mechanism. The `source` field is auto-set by the server and only shown as a fallback when no `viewId`/`skill`/`author` are present.\n\n## Rule #4: Always Reload AV Yourself\n\n**After restarting the AV server or making any change that requires a browser refresh, you MUST reload AV yourself using `av_reload`.** Never ask the user to refresh their browser — that's your job. This is non-negotiable.\n\nReload sequence after server restart:\n1. Call `av_reload` (uses mgmt relay on port 8772, works even during server restart)\n2. If `av_reload` reports 0 viewers connected, try once more after 3 seconds\n3. Only if all attempts fail (viewer truly disconnected), tell the user: *\"I tried to reload AV for you automatically, but your viewer tab isn't connected to my servers right now. As a last resort, could you refresh the AV panel?\"*\n\nNever just say \"can you refresh AV?\" without trying first. The user should never have to do busywork that you can handle.\n\n## Quick Reference\n\n```bash\n# 1. Push content to AV\ncurl -s http://127.0.0.1:8771/api/display -X POST \\\n -H \"Content-Type: application/json\" \\\n -d \"{\\\"action\\\":\\\"display\\\",\\\"content\\\":{\\\"contentType\\\":\\\"html_interactive\\\",\\\"content\\\":\\\"$HTML\\\",\\\"title\\\":\\\"My Widget\\\",\\\"id\\\":\\\"$(uuidgen)\\\"}}\"\n\n# 2. Screenshot it (mgmt server)\ncurl -s http://127.0.0.1:8772/ -X POST \\\n -H \"Content-Type: application/json\" \\\n -d '{\"action\":\"screenshot\"}'\n# Returns: { ok: true, filePath: \"/home/adom/project/project-content/screenshots/mgmt-screenshot-*.png\", data: \"data:image/png;base64,...\" }\n\n# 3. Read the screenshot to show the user\n# Use the Read tool on the filePath from step 2\n```\n\n## Push API Shape\n\nPOST to `http://127.0.0.1:8771/api/display`:\n\n```json\n{\n \"action\": \"display\",\n \"content\": {\n \"contentType\": \"html_interactive\",\n \"content\": \"<html>...</html>\",\n \"title\": \"Widget Title\",\n \"id\": \"any-unique-string\",\n \"source\": \"av_display\",\n \"skill\": \"av-creator\",\n \"author\": \"John Lauer\"\n }\n}\n```\n\nThe `source`, `skill`, and `author` fields are **required**. They appear in a tooltip when the user hovers over the tab for 1 second, showing attribution and origin.\n\n- `author`: Full name of the person whose skill created this content\n- `skill`: The skill name that triggered the creation (e.g., `av-creator`, `symbol-creator`)\n- `source`: The AV tool used (e.g., `av_display`, `av_display_file`, `av_3d_display`)\n\nFor the `id` field, use `uuidgen` in bash or any unique string.\n\n## Content Types\n\n| Type | When to use | Example |\n|------|-------------|---------|\n| `html_interactive` | Custom widgets with JS, charts, interactive content | Skills map, dashboards, data explorers |\n| `html` | Static HTML without scripts | Tables, formatted reports |\n| `svg` | Vector graphics | Diagrams, schematics, charts |\n| `markdown` | Text content | Documentation, notes, summaries |\n| `image` | Base64 data URI | Screenshots, photos |\n\n**Default to `html_interactive`** for most visualizations — it supports `<script>` tags for interactivity.\n\n**`html_interactive` rendering:** Content is rendered **inline** (injected into the page via `innerHTML` with scripts re-executed), NOT in a sandboxed iframe. This means:\n- Scripts run in the same window context as AV — they have full access to `navigator.clipboard`, `document`, etc.\n- `window.parent.postMessage()` still works (sends to self when not in iframe, caught by AV's message listener).\n- Clipboard API works directly because the paste event's user activation is preserved (no iframe sandbox to block it).\n- If `scrollable: true` is set on the content, it renders in a `padded` div; otherwise in a `fullscreen` div.\n\n**Important: Always use the `display` action.** All AV content MUST go through the `display` action so it appears as a tab in the tab bar. Never use fullscreen-only actions like `show_jlcpcb_results` or `show_instapcb` that bypass the tab system — those are legacy patterns being phased out. Tabs are essential for users to navigate between views, compare results, and maintain context. If you're building a new viewer or skill that pushes to AV, always use `action: \"display\"` with a `contentType`.\n\n## Tab Icons\n\nEvery AV tab gets an inline SVG icon. There are two layers:\n\n### Per-View Icons (for dropdown views)\n\nEach view in `VIEW_RENDERERS` has its own unique icon in `tabIconSvg()`, keyed by `_viewName`. Views are like apps — every one MUST have a distinct icon. Examples:\n\n| View | Icon |\n|------|------|\n| SymView | IC chip with pins |\n| FpView | Top-down pad layout |\n| SchView | Zigzag wire with nodes |\n| 3dView | Wireframe cube with interior edges |\n| Fp3dView | Extruded 3D pad in perspective |\n| Basic3dView | Simple cube outline |\n| DeskConduit | Laptop with upload arrow |\n| ContConduit | Two boxes with bidirectional arrows |\n| GChat | Chat bubble with text lines |\n| InstrView | Oscilloscope with waveform |\n| JlcSearch / MouserSearch / DkSearch | Shopping cart |\n| InstaPCB | Circuit board with traces |\n| MovieMaker | Film clapperboard |\n\n### Per-Skill Icons (for pushed content with `skill` field)\n\nPushed content can override the default contentType icon by matching the `skill` field. These are checked BEFORE contentType icons:\n\n| Skill | Icon |\n|-------|------|\n| `screenshot-paste` | Clipboard with lines |\n| `solder-jet-sizer` | Nozzle with dots on pad |\n\nTo add a new skill icon, add a check in `tabIconSvg()` for `item.skill === 'your-skill-name'`.\n\n### Per-ContentType Icons (fallback for pushed content)\n\nPushed content (via `av_display`) gets icons based on `contentType` if no skill icon matched:\n\n| Content Type | Icon |\n|-------------|------|\n| `html_interactive` / `html` | `</>` code brackets |\n| `3d` | Wireframe cube with interior edges |\n| `basic_3d` | Simple cube outline |\n| `symbol_3d` | Rectangle + mini cube |\n| `library_review` | Three vertical panes |\n| `svg` | Frame with landscape |\n| `image` | Frame with photo |\n| `markdown` | M with chevron |\n| `capture` | Camera |\n\nWhen creating a new view, you MUST add a unique icon in the `tabIconSvg()` function. Use 14x14px viewBox, stroke color `${c}`, `stroke-width=\"1\"`, `fill=\"none\"`. The icon must be self-contained and visually distinct at small size.\n\n## Theme Tokens\n\nUse these exact colors for all AV widgets. Do NOT use other color schemes.\n\n### Backgrounds\n| Token | Value | Use |\n|-------|-------|-----|\n| bg | `#0d1117` | Page background |\n| bgSurface | `#161b22` | Cards, panels |\n| bgElevated | `#1c2128` | Hover states, elevated cards |\n| bgOverlay | `#21262d` | Tooltips, dropdowns |\n\n### Text\n| Token | Value | Use |\n|-------|-------|-----|\n| text | `#e6edf3` | Primary text |\n| textSecondary | `#8b949e` | Labels, descriptions |\n| textMuted | `#484f58` | Disabled, placeholders |\n\n### Accent (Adom Teal)\n| Token | Value | Use |\n|-------|-------|-----|\n| accent | `#00b8b0` | Primary accent, headings |\n| accentBright | `#00e6dc` | Hover, focus, links |\n| accentMuted | `rgba(0, 184, 176, 0.12)` | Tinted backgrounds |\n\n### Borders\n| Token | Value | Use |\n|-------|-------|-----|\n| border | `#30363d` | Standard borders |\n| borderMuted | `#21262d` | Subtle borders |\n| accentBorder | `rgba(0, 184, 176, 0.3)` | Accent borders |\n\n### Semantic\n| Token | Value |\n|-------|-------|\n| success | `#3fb950` |\n| warning | `#d29922` |\n| danger | `#f85149` |\n\n### Typography\n```css\nfont-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\nfont-family: 'SF Mono', 'Cascadia Code', Consolas, monospace; /* code */\n```\n\n### Radii\n`4px` (small), `6px` (medium), `8px` (large)\n\n## HTML Template\n\nStart every widget from this boilerplate:\n\n```html\n<!DOCTYPE html>\n<html>\n<head>\n<meta charset=\"utf-8\">\n<style>\n* { margin: 0; padding: 0; box-sizing: border-box; }\nbody {\n background: #0d1117;\n color: #e6edf3;\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n padding: 20px;\n overflow-y: auto;\n}\nh1 { font-size: 18px; font-weight: 600; color: #00b8b0; margin-bottom: 4px; }\n.subtitle { font-size: 12px; color: #8b949e; margin-bottom: 16px; }\n.card {\n padding: 10px 12px;\n background: #161b22;\n border: 1px solid #21262d;\n border-radius: 6px;\n margin-bottom: 8px;\n}\n.card:hover { border-color: rgba(0,184,176,0.3); background: #1c2128; }\n.accent { color: #00b8b0; }\n.muted { color: #8b949e; font-size: 12px; }\n.mono { font-family: 'SF Mono', 'Cascadia Code', Consolas, monospace; }\n</style>\n</head>\n<body>\n\n<h1>Title Here</h1>\n<div class=\"subtitle\">Description here</div>\n\n<!-- Your content -->\n\n<script>\n// Interactive logic here (optional)\n</script>\n</body>\n</html>\n```\n\n## Workflow\n\n1. **Write HTML** to a temp file (e.g., `/tmp/my-widget.html`) using the template above\n2. **Push to AV** using the curl command — escape the HTML into JSON with `python3 -c \"import sys,json; print(json.dumps(sys.stdin.read()))\"`\n3. **Show the viewer** — On the **first push of the session**, always call the `av_display` MCP tool (e.g., `mcp__adom-viewer__av_display` or `mcp__adom-viewer-2__av_display`) to ensure the AV viewer panel is visible to the user. Subsequent pushes in the same session don't need this step since the viewer is already open.\n4. **Screenshot** via mgmt server and read the image to show the user\n5. **Iterate** if the user wants changes — edit the HTML, re-push, re-screenshot\n\n### Push Pattern (Bash)\n\n```bash\n# Write HTML to temp file, then push\nHTML_JSON=$(cat /tmp/my-widget.html | python3 -c \"import sys,json; print(json.dumps(sys.stdin.read()))\")\nID=$(python3 -c \"import uuid; print(uuid.uuid4())\")\ncurl -s http://127.0.0.1:8771/api/display -X POST \\\n -H \"Content-Type: application/json\" \\\n -d \"{\\\"action\\\":\\\"display\\\",\\\"content\\\":{\\\"contentType\\\":\\\"html_interactive\\\",\\\"content\\\":${HTML_JSON},\\\"title\\\":\\\"My Widget\\\",\\\"id\\\":\\\"${ID}\\\",\\\"source\\\":\\\"av_display\\\",\\\"skill\\\":\\\"av-creator\\\",\\\"author\\\":\\\"John Lauer\\\"}}\"\n```\n\n### Screenshot Pattern (Bash)\n\n```bash\nRESULT=$(curl -s http://127.0.0.1:8772/ -X POST \\\n -H \"Content-Type: application/json\" \\\n -d '{\"action\":\"screenshot\"}')\nFILE=$(echo \"$RESULT\" | python3 -c \"import sys,json; print(json.loads(sys.stdin.read())['filePath'])\")\n# Then use the Read tool on $FILE to show the user\n```\n\nIf the screenshot returns `{\"error\":\"No viewer connected\"}`, tell the user to open their Adom Viewer tab, then retry.\n\n## Tips\n\n- **Self-contained** — no external CSS/JS dependencies. Everything inline.\n- **JSON escaping** — always use the python3 JSON escape pattern. Raw HTML in curl breaks on quotes and newlines.\n- **Check status first** — `curl -s http://127.0.0.1:8771/api/display -X POST -H \"Content-Type: application/json\" -d '{\"action\":\"get_status\"}'` returns `{ viewerCount, hasContent, historyLength }`.\n- **Grid layouts** — use CSS Grid (`grid-template-columns: repeat(auto-fill, minmax(200px, 1fr))`) for responsive card layouts.\n- **Interactivity** — `html_interactive` runs in an iframe with full JS support. Add click handlers, transitions, filters.\n- **Large content** — for very large HTML (>100KB), write to a file in `/home/adom/project/project-content/` and use the `display_file` action instead.\n\n## AI-Controllable Widgets\n\nAV widgets can receive commands from Claude after they load. This enables Claude to toggle UI elements, update data, change views, highlight items, etc. without re-pushing the entire widget.\n\n### Pattern\n\nThe widget listens for postMessage commands from the parent:\n\n```html\n<script>\n// Signal readiness to parent\nparent.postMessage({ type: 'widget_ready' }, '*');\n\n// Listen for commands from Claude (via parent)\nwindow.addEventListener('message', (e) => {\n const msg = e.data;\n if (!msg?.type) return;\n\n switch (msg.type) {\n case 'toggle_panel':\n document.getElementById(msg.panelId).classList.toggle('hidden');\n break;\n case 'highlight_item':\n document.querySelector(`[data-id=\"${msg.id}\"]`).classList.add('highlighted');\n break;\n case 'update_data':\n renderData(msg.data);\n break;\n case 'set_view':\n switchView(msg.view);\n break;\n }\n});\n</script>\n```\n\n### Sending Commands\n\nAfter pushing the widget, send commands via the internal API:\n\n```bash\n# Send a command to the active widget's iframe\ncurl -s http://127.0.0.1:8771/api/display -X POST \\\n -H \"Content-Type: application/json\" \\\n -d '{\"action\":\"post_message\",\"message\":{\"type\":\"highlight_item\",\"id\":\"resistor-1\"}}'\n```\n\nOr via WebSocket: the parent relays `post_message` actions to the active iframe's `contentWindow.postMessage()`.\n\n### Guidelines\n\n- Always send `widget_ready` so the parent knows when commands can be sent\n- Use descriptive `type` names (e.g., `toggle_toolbar`, `set_camera`, `show_caption`)\n- Include a comment block at the top of `<script>` listing supported commands\n- Keep command handling idempotent — sending the same command twice should be safe\n\n## Screenshot Support\n\nScreenshots are critical in AV — they're how Claude verifies and shows work to the user. AV uses **html2canvas** in the parent document to capture any content, including `html_interactive` iframes. **Your widget supports screenshots out of the box — no extra code needed.**\n\n### How It Works\n\nThe AV viewer (`index.html`) loads `html2canvas.min.js` in the parent document. When a screenshot is requested via the mgmt server (`POST http://127.0.0.1:8772/` with `{\"action\":\"screenshot\"}`), the `captureOVScreenshot()` function runs a 4-strategy fallback:\n\n| Strategy | What it captures | How |\n|----------|-----------------|-----|\n| **1: 3D iframe** | 3D/Babylon.js content | postMessage to 3D iframe, which renders its canvas |\n| **1b: html2canvas on iframe** | `html_interactive` widgets | Clones iframe DOM + styles into an offscreen parent container, runs html2canvas on the clone |\n| **2: SVG serialization** | SVG content | Serializes SVG to image via `XMLSerializer` |\n| **3: Image element** | Static images | Draws `<img>` to canvas (with try/catch for cross-origin tainting) |\n| **4: html2canvas on parent DOM** | Any parent-rendered content | Runs html2canvas directly on `contentEl` |\n\nStrategy 1b is the key one for `html_interactive` widgets. It works by:\n1. Accessing `iframe.contentDocument` (same-origin because `srcdoc` iframes with `allow-same-origin` share the parent's origin)\n2. Cloning all `<style>` tags and body content into a temporary offscreen `<div>` in the parent\n3. Copying computed styles (background, font, padding) from the iframe body\n4. Running `html2canvas(container, { scale: 2, useCORS: true })` on the clone\n5. Cleaning up the temporary container\n\n### Widget Author Guidelines\n\n**For most widgets — do nothing.** The html2canvas pipeline handles standard DOM content automatically.\n\n**For best screenshot results:**\n\n- **Use DOM elements, not `<canvas>`** — html2canvas captures DOM beautifully but can't read canvas pixel data\n- **Keep images self-contained** — use inline SVG or base64 data URIs instead of external image URLs. Cross-origin images may be tainted and render as blank\n- **Avoid `backdrop-filter`** — html2canvas doesn't support it; use solid backgrounds instead\n- **Test with the screenshot command** — after pushing your widget, always screenshot and verify\n\n### If Your Widget Uses `<canvas>`\n\nhtml2canvas can't capture `<canvas>` pixel content. If your widget renders to canvas (charts, WebGL, etc.), add a custom capture handler that responds **before** the parent's html2canvas fallback:\n\n```html\n<script>\nwindow.addEventListener('message', (e) => {\n if (e.data?.type === 'mgmt_capture_request') {\n const canvas = document.getElementById('myCanvas');\n parent.postMessage({\n type: 'mgmt_canvas_capture',\n _reqId: e.data._reqId,\n data: canvas.toDataURL('image/png')\n }, '*');\n }\n});\n</script>\n```\n\nThe parent listens for `mgmt_canvas_capture` responses. If your widget responds, Strategy 1 handles it and the later strategies are skipped.\n\n### Rule #5: Every New Widget Gets the Full Treatment\n\n**This is non-negotiable.** When you create ANY new widget or skill that pushes content to AV — whether it's a standalone view, a skill showcase, a dashboard, a tool, or anything else — you MUST do ALL of the following. There is NO distinction between \"pushed content\" and \"views\" for this rule. If it shows up in AV, it gets the full treatment.\n\n1. **Add a unique tab icon** — Add a skill-level icon check in `tabIconSvg()` in `viewer/viewer/index.html` for `item.skill === 'your-skill-name'`. Every widget MUST have its own distinct 14x14px inline SVG icon (stroke color `${c}`, `stroke-width=\"1\"`, `fill=\"none\"`). The generic `</>` code brackets icon is NOT acceptable for any named skill/widget.\n2. **Add it to the dropdown** — Add an `<option>` to the `<select id=\"view-select\">` in `viewer/viewer/index.html` with its View ID and display name (e.g., `<option value=\"myview\">MyView — My New View</option>`).\n3. **Add it to VIEW_RENDERERS** — Register `viewId`, `title`, `author`, and `render` function in the `VIEW_RENDERERS` object in `index.html`. The `author` field MUST be the full name of the person who created the view (from git history or skill frontmatter) — NOT \"Adom Viewer\" or any generic name.\n4. **Add it to the About page** — Add a skill card in the `showAbout()` function so users can discover it.\n5. **Add it to the Skills Map** — Add an entry in `viewer/viewer/skills-map.html` so it appears in the skills map visualization.\n6. **Ensure screenshot support works** — Two options:\n 1. **DOM-based content** — html2canvas handles it automatically via Strategy 1b/4. Nothing extra needed.\n 2. **Canvas-based content** — implement the `mgmt_capture_request` → `mgmt_canvas_capture` postMessage handler shown above. This is what `3d.html` does for Babylon.js captures.\n\n**Why this matters:** Without the full treatment, the widget looks like a second-class citizen — generic icon, not discoverable in the dropdown, missing from About/Skills Map. Every widget is an app. Treat it like one.\n\n**Never ship a widget that can't be screenshotted.** If the mgmt screenshot returns \"No capturable content visible\", your viewer is broken — fix it before deploying.\n\n### Tab Switching for Multi-Tab Screenshots\n\nTo programmatically switch tabs before screenshotting (e.g., to capture each tab in sequence):\n\n```bash\n# Switch to tab at index 0\ncurl -s http://127.0.0.1:8771/api/display -X POST \\\n -H \"Content-Type: application/json\" \\\n -d '{\"action\":\"switch_tab\",\"index\":0}'\n\n# Wait for iframe to render\nsleep 3\n\n# Screenshot\ncurl -s http://127.0.0.1:8772/ -X POST \\\n -H \"Content-Type: application/json\" \\\n -d '{\"action\":\"screenshot\"}'\n```\n\nTab indices are 0-based in the order content was pushed.\n\n## Troubleshooting\n\n| Symptom | Cause | Fix |\n|---------|-------|-----|\n| `Invalid JSON` | HTML not properly escaped | Use the python3 JSON escape pattern |\n| `viewerCount: 0` | No browser tab open | Tell user to open Adom Viewer tab |\n| `No viewer connected` on screenshot | Viewer tab not connected | User needs to open/refresh the AV tab |\n| White text invisible | Using light theme colors | Stick to the dark theme tokens above |\n| Content too wide | No overflow handling | Add `overflow-x: auto` or `word-break: break-word` |\n| Edits not showing after re-push | Browser iframe cache | See **Iframe Caching** below |\n| Cross-origin fetch fails in srcdoc | Null origin in `av_display_file` | Use iframe approach instead (see below) |\n\n### Iframe Caching — STOP Before You Loop\n\n**This is the #1 time-waster when iterating on AV widgets.** You edit a file, re-push to AV, but the old code still runs. You then spend 30+ minutes fighting phantom bugs, pushing the same iframe over and over.\n\n**Before debugging anything else, check these in order:**\n\n1. **Are you editing the right file?** If a file server serves from `dir/`, make sure you're editing in `dir/` — not a duplicate copy elsewhere. Run: `curl http://127.0.0.1:PORT/file.html | grep \"your_new_function\"` — if it returns 0, you're editing the wrong copy.\n\n2. **Is the browser serving a cached version?** Push with a unique filename instead: `cp widget.html widget-$(date +%s).html` and point the iframe at the new name. The browser literally cannot cache a file it's never seen.\n\n3. **Stop after 2 failed attempts.** If re-pushing the same iframe URL twice doesn't show your changes, the problem is NOT \"push it again.\" It's either (a) wrong file, (b) browser cache, or (c) a JS error crashing the widget before your changes execute. Check the browser console.\n\n### srcdoc vs Iframe Origin\n\n`av_display_file` and `av_display` inject content as `srcdoc` in an iframe. This gives a **null origin**, which means:\n- **Cross-origin fetches fail** (CORS blocks requests from null origin)\n- `location.origin` returns `\"null\"` (string), not a real origin\n- `parent.location` throws if the parent is on a different origin\n\n**If your widget needs network access** (fetching APIs, loading 3D tiles, etc.), you MUST serve it from a real file server and push an iframe wrapper:\n```html\n<iframe src=\"https://<widget-url>.adom.cloud/widget.html\" style=\"width:100%;height:100%;border:none\"></iframe>\n```\nThis gives the widget a real origin and full network access.\n\n### CesiumJS / Heavy 3D Widgets in AV\n\nCesiumJS (and similar WebGL-heavy libraries) have special challenges in AV:\n\n**Two approaches — tradeoffs:**\n\n| Approach | Pros | Cons |\n|----------|------|------|\n| `av_display_file` (inline srcdoc) | av_capture sees HTML/DOM overlays; single push | Null origin — `location.origin` is `\"null\"` (string). Must hardcode absolute URLs. `CESIUM_BASE_URL` must be set before loading CesiumJS so workers resolve correctly. av_capture CANNOT see WebGL canvas content. |\n| iframe wrapper (file server) | Real origin — URL detection works naturally; full network access | av_capture sees DOM overlays but NOT WebGL canvas (cross-origin restriction). Must have file server running. |\n\n**Key fixes for both approaches:**\n\n1. **Set `CESIUM_BASE_URL`** before the CesiumJS `<script>` tag — CesiumJS uses this to find its web workers. Without it, workers fail silently in srcdoc/iframe contexts:\n ```html\n <script>window.CESIUM_BASE_URL = 'https://cesium.com/downloads/cesiumjs/releases/1.119/Build/Cesium/';</script>\n <script src=\"https://cesium.com/downloads/cesiumjs/releases/1.119/Build/Cesium/Cesium.js\"></script>\n ```\n\n2. **URL detection for proxied services** — When running inside AV (either srcdoc or nested iframe), `location.origin` may be `\"null\"` or different from expected. Use `location.href` regex matching with a hardcoded fallback:\n ```javascript\n const CODER_BASE = (() => {\n const href = location.href;\n const m = href.match(/^(https?:\\/\\/[^/]+)/);\n if (m && m[1] !== 'null' && !href.startsWith('about:')) return m[1];\n try { const ph = parent.location.href; const m2 = ph.match(/^(https?:\\/\\/[^/]+)/); if (m2) return m2[1]; } catch {}\n return 'https://coder.<your-slug>.containers.adom.inc'; // hardcoded fallback\n })();\n const TILE_PROXY = CODER_BASE + '/proxy/8793';\n ```\n\n3. **CORS proxy for external tiles** — OSM tiles (`tile.openstreetmap.org`) don't send CORS headers. Google 3D Tiles need API key injection. Proxy both through a local Node server that adds `Access-Control-Allow-Origin: *`. For Google 3D Tiles, rewrite JSON `\"uri\"` fields to route through the proxy.\n\n4. **av_capture limitation** — av_capture CANNOT screenshot WebGL canvas content inside cross-origin iframes. The toolbar/DOM overlays will be visible but the 3D globe/scene will appear black. This is a browser security restriction, not a bug. Use `av_tab_capture` (requires capture companion tab setup) to capture the full tab including WebGL content.\n\n5. **Prefer inline (`av_display_file`) for screenshotability** — If you need av_capture to see the toolbar and UI (even though WebGL is black), inline is better because at least DOM elements render. The iframe approach may show nothing at all if the tab doesn't switch properly.",
"author": {
"id": "695820315b5f1e4db2fcf602",
"name": "Kyle Bergstedt",
"email": "[email protected]"
},
"visibility": {
"public": true
},
"hero": null,
"sample_prompts": [],
"discovery_triggers": [],
"discovery_pitch": null,
"metadata": {},
"created_at": "2026-05-28T05:29:55.413Z",
"updated_at": "2026-05-28T05:29:55.413Z",
"sub_skills": [],
"parent_app": null
}