visualize-83767a — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited visualize-83767a (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Render rich interactive visuals directly inline in chat using render_visualization.
render_visualization(html_code, title) with an HTML or SVG content fragment<!DOCTYPE>, <html>, <head>, or <body> — the tool wraps your content automatically<style> first (prefer inline styles) → visible content → <script> lastsendPrompt() bridge, and openLink() bridgesendPrompt() — see the sendPrompt bridge section for patterns and examplesThese rules keep visuals clean, accessible, and consistent with the host UI:
The tool injects theme-aware CSS variables that automatically adapt to light/dark mode. Hardcoding hex colors will break in one mode or the other.
| Token | Purpose |
|---|---|
--color-text-primary | Main text |
--color-text-secondary | Labels, muted text |
--color-text-tertiary | Hints, placeholders |
--color-text-info/success/warning/danger | Semantic text |
--color-bg-primary | Main background |
--color-bg-secondary | Cards, surfaces |
--color-bg-tertiary | Page background |
--color-border-tertiary | Default borders (0.15 alpha) |
--color-border-secondary | Hover borders (0.3 alpha) |
--font-sans | Default font |
--font-mono | Code font |
--radius-md / --radius-lg / --radius-xl | 8px / 12px / 16px |
Each ramp provides fill, stroke, and text variants that adapt to the theme automatically via CSS classes.
| Ramp | 50 (light fill) | 200 | 400 | 600 (light stroke) | 800 (light title) |
|---|---|---|---|---|---|
| purple | #EEEDFE | #AFA9EC | #7F77DD | #534AB7 | #3C3489 |
| teal | #E1F5EE | #5DCAA5 | #1D9E75 | #0F6E56 | #085041 |
| coral | #FAECE7 | #F0997B | #D85A30 | #993C1D | #712B13 |
| pink | #FBEAF0 | #ED93B1 | #D4537E | #993556 | #72243E |
| gray | #F1EFE8 | #B4B2A9 | #888780 | #5F5E5A | #444441 |
| blue | #E6F1FB | #85B7EB | #378ADD | #185FA5 | #0C447C |
| green | #EAF3DE | #97C459 | #639922 | #3B6D11 | #27500A |
| amber | #FAEEDA | #EF9F27 | #BA7517 | #854F0B | #633806 |
| red | #FCEBEB | #F09595 | #E24B4A | #A32D2D | #791F1F |
Color assignment rules:
| Series | Color | Hex |
|---|---|---|
| 1 | teal-400 | #1D9E75 |
| 2 | purple-400 | #7F77DD |
| 3 | coral-400 | #D85A30 |
| 4 | blue-400 | #378ADD |
| 5 | amber-400 | #BA7517 |
For area/line fills, use same color at 20% opacity.
Always use this SVG boilerplate:
<svg width="100%" viewBox="0 0 680 H">
<defs>
<marker id="arrow" viewBox="0 0 10 10" refX="8" refY="5"
markerWidth="6" markerHeight="6" orient="auto-start-reverse">
<path d="M2 1L8 5L2 9" fill="none" stroke="context-stroke"
stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</marker>
</defs>
</svg>| Class | Purpose |
|---|---|
.t | 14px primary text |
.ts | 12px secondary text |
.th | 14px bold (500) primary text |
.box | Neutral rect (secondary bg, tertiary border) |
.node | Clickable element (cursor pointer, hover opacity) |
.arr | Arrow line (1.5px, border-secondary) |
.leader | Dashed guide line (0.5px, tertiary) |
.c-{ramp} | Color ramp on <g> — auto-sets fill/stroke on child rect/circle/ellipse and text colors |
Every text inside a box needs dominant-baseline="central" with y at the vertical center of its slot. Without this, text sits ~4px too high and looks misaligned.
When: "walk me through the steps", "what's the process"
marker-end="url(#arrow)" on arrow pathsSingle-line node:
<g class="node c-teal" onclick="sendPrompt('Tell me about X')">
<rect x="100" y="20" width="180" height="44" rx="8"/>
<text class="th" x="190" y="42" text-anchor="middle" dominant-baseline="central">Label</text>
</g>Two-line node:
<g class="node c-teal">
<rect x="100" y="20" width="200" height="56" rx="8"/>
<text class="th" x="200" y="38" text-anchor="middle" dominant-baseline="central">Title</text>
<text class="ts" x="200" y="56" text-anchor="middle" dominant-baseline="central">Subtitle</text>
</g>When: "what's the architecture", "where does X live"
When: "how does X actually work", "explain X" (spatial concept)
Load Chart.js in your HTML fragment:
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.1/chart.umd.min.js"></script>Setup pattern:
<div style="position: relative; height: 300px;">
<canvas id="chart"></canvas>
</div>
<script>
const ctx = document.getElementById('chart').getContext('2d');
const s = getComputedStyle(document.documentElement);
const textColor = s.getPropertyValue('--color-text-secondary').trim();
const gridColor = s.getPropertyValue('--color-border-tertiary').trim();
new Chart(ctx, {
type: 'bar',
data: {
labels: ['Q1','Q2','Q3','Q4'],
datasets: [{ label: 'Revenue', data: [12,19,8,15],
backgroundColor: '#1D9E75', borderRadius: 4, borderSkipped: false }]
},
options: {
responsive: true, maintainAspectRatio: false,
plugins: { legend: { labels: { color: textColor, font: { size: 12 } } } },
scales: {
x: { grid: { display: false }, ticks: { color: textColor, font: { size: 12 } }, border: { color: gridColor } },
y: { grid: { color: gridColor }, ticks: { color: textColor, font: { size: 12 } }, border: { display: false } }
}
}
});
</script>Chart rules:
position: relative and explicit heightresponsive: true, maintainAspectRatio: falseborderRadius: 4 on barstension: 0.3 for smooth curvescutout: '60%' — never use pieChart type selection:
| Data shape | Type | Notes |
|---|---|---|
| Categories + values | Bar | Horizontal if labels are long |
| Time series | Line | tension: 0.3 |
| Parts of whole | Doughnut | cutout: '60%' |
| Two variables | Scatter | Add trend line if relevant |
Horizontal bar:
<svg width="100%" viewBox="0 0 680 80">
<text class="ts" x="40" y="30">Category A</text>
<rect x="160" y="16" width="360" height="20" rx="4" fill="#1D9E75" opacity="0.8"/>
<text class="ts" x="530" y="30">72%</text>
</svg><div style="display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:12px;">
<div style="background:var(--color-bg-secondary); border:0.5px solid var(--color-border-tertiary);
border-radius:var(--radius-lg); padding:16px;">
<div style="font-size:12px; color:var(--color-text-secondary);">Label</div>
<div style="font-size:28px; font-weight:500; color:var(--color-text-primary); margin-top:4px;">$3,870</div>
</div>
<!-- repeat -->
</div>Side-by-side cards with a shared header row, metrics in each column.
Sliders or buttons that update visible state via JavaScript. Example: a slider controlling a variable with a live-updating SVG or value display.
sendPrompt(text) is the function that makes visualizations conversational. When called, it injects the given text into the chat input field and submits it — exactly as if the user had typed and sent it themselves. The model then receives that message and responds normally, creating a feedback loop between the visual and the conversation.
This is what separates a static diagram from an exploration interface. A user sees a system architecture diagram, clicks on the "Load Balancer" node, and the model receives "Tell me more about the load balancer — how does it distribute traffic across the backend services?" as a user message. The model then responds with details, and could even generate a new sub-diagram showing the load balancer internals. The user never had to type anything — they just clicked.
Without sendPrompt, interactive elements inside the iframe are isolated — they can toggle visibility, animate, or filter data, but they can never talk back to the model. The user sees a cool diagram but has to manually type follow-up questions. With sendPrompt, every clickable element becomes a conversation starter. The diagram itself becomes a navigation interface for the topic.
The function is auto-injected by the tool. It uses Open WebUI's native postMessage protocol to submit the text as a user message. If the AI is still generating a response, the message is automatically queued and sent once generation completes. This requires iframe Sandbox Allow Same Origin to be enabled in Open WebUI settings — without it, the function silently fails (the visualization still works, but clicks do nothing).
The text you pass to sendPrompt becomes the user's message to the model. Write it as a natural follow-up question — conversational, specific, and referencing the context of the diagram:
Good prompt text (specific, contextual, references the diagram):
"Explain the attention mechanism — how does it decide which tokens to focus on?""Break down the CI/CD pipeline stage. What tools are typically used here?""Show me a more detailed diagram of the data processing layer""What happens when the load balancer detects a failed backend node?""Compare the pros and cons of the monolith vs microservices approach shown here"Bad prompt text (vague, generic, loses context):
"Tell me more" — more about what?"Explain" — explain what?"Details" — not a sentence, model won't know what to doSimple patterns — single-click sendPrompt on a node or button:
onclick="sendPrompt('Explain the API gateway — what does it handle?')" on a diagram nodeonclick="sendPrompt('I chose B: O(n log n). Am I right? Explain why.')" on answer buttonsonclick="sendPrompt('Show me a more advanced example with edge cases.')" on a "Go deeper →" buttononclick="sendPrompt('Compare REST vs GraphQL — when should I use each?')" on one of two nodesForm / preference collector — gather multiple user selections, then send them all at once. Use local JS to track choices (button highlights, state object) and a submit button that composes a sendPrompt from the collected answers:
<script>
var choices = {};
function pick(category, value, btn) {
choices[category] = value;
// Highlight selected button, dim siblings
btn.parentElement.querySelectorAll('button').forEach(function(b) {
b.classList.toggle('active', b === btn);
});
}
function submitChoices() {
var parts = [];
for (var k in choices) parts.push(k + ': ' + choices[k]);
sendPrompt('Here are my preferences:\n' + parts.join('\n') + '\nGive me a personalized recommendation based on these choices.');
}
</script>
<h3>What's your style?</h3>
<p style="margin:8px 0 4px;">Pace</p>
<button onclick="pick('pace','relaxed',this)">Relaxed</button>
<button onclick="pick('pace','moderate',this)">Moderate</button>
<button onclick="pick('pace','intensive',this)">Intensive</button>
<p style="margin:8px 0 4px;">Focus</p>
<button onclick="pick('focus','culture',this)">Culture</button>
<button onclick="pick('focus','nature',this)">Nature</button>
<button onclick="pick('focus','food',this)">Food</button>
<button onclick="submitChoices()" style="margin-top:12px; font-weight:500;">Get my recommendation →</button>This pattern is powerful because the model receives a structured summary of all user preferences in one message. Use local JS for the selection UI (instant feedback), then sendPrompt only on final submit.
| User action | Use | Why |
|---|---|---|
| Learn more about a component | sendPrompt | Model gives a contextual explanation |
| Explore a stage / drill down | sendPrompt | Model can generate a sub-diagram |
| Submit answers or preferences | sendPrompt | Model evaluates or personalizes |
| Toggle views, adjust sliders | Local JS | Instant feedback, no reasoning needed |
| Filter/sort data | Local JS | Instant response, no model needed |
Visualizations should feel alive and polished — not static images dumped into chat. Build interfaces that invite interaction:
<details> elements or JS-toggled sections so users can explore at their own pace without overwhelming them upfront.node class adds this for SVG elements; for HTML, use :hover styles)transition: all 0.2s ease to interactive elements for a polished feel.active class or distinct stylingThe goal is to build something that feels like a real app component embedded in chat — not a screenshot. If the visualization has multiple facets, give the user controls to explore them. If it has hierarchical information, let them expand and collapse. If it has data, let them sort or filter.
openLink(url) opens a URL in a new browser tab from within the visualization iframe. Normal <a href="..."> links inside an iframe can behave unpredictably (opening inside the iframe, being blocked by sandbox restrictions, etc.). This function handles that by opening the link in the parent window instead.
<button onclick="openLink('https://docs.example.com/api-reference')">
Open API docs ↗
</button>Or in SVG:
<g class="node c-blue" onclick="openLink('https://github.com/org/repo')">
<rect x="100" y="20" width="200" height="44" rx="8"/>
<text class="th" x="200" y="42" text-anchor="middle" dominant-baseline="central">View source ↗</text>
</g>Use openLink for external references, documentation links, or source code links. Unlike sendPrompt, this navigates away from the chat — use it when the user needs to access an external resource, not when they need the model to explain something.
| Rule | Value |
|---|---|
| SVG viewBox width | Always 680px |
| Font sizes | 14px labels, 12px subtitles |
| Stroke width | 0.5px for borders |
| Max colors per diagram | 2–3 ramps |
| Subtitle max length | 5 words |
| Corner radius (SVG) | rx=4 default, rx=8 emphasis |
| Corner radius (HTML) | var(--radius-md) or -lg |
| Min font size | 11px |
| Heading sizes | h1=22px, h2=18px, h3=16px |
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.