Prompt Playground
A developer-facing prompt playground with system and user editors, model and sampling controls, a live token meter that estimates cost, and a simulated streamed completion.
MCP
Code
:root {
--bg: #0c0d10;
--surface: #15161b;
--surface-2: #1d1f27;
--ink: #e9eaf0;
--muted: #9a9cab;
--accent: #8b5cf6;
--accent-2: #22d3ee;
--ok: #34d399;
--warn: #fbbf24;
--danger: #f87171;
--line: rgba(255, 255, 255, 0.10);
--line-2: rgba(255, 255, 255, 0.18);
--r-sm: 8px;
--r-md: 12px;
--r-lg: 16px;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
html {
-webkit-text-size-adjust: 100%;
}
body {
margin: 0;
background:
radial-gradient(1100px 600px at 82% -8%, rgba(139, 92, 246, 0.16), transparent 60%),
radial-gradient(900px 500px at 6% 108%, rgba(34, 211, 238, 0.10), transparent 55%),
var(--bg);
color: var(--ink);
font-family: "Inter", system-ui, -apple-system, sans-serif;
line-height: 1.5;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
min-height: 100vh;
}
.pp {
max-width: 1240px;
margin: 0 auto;
padding: clamp(16px, 3vw, 32px);
}
/* ---------- Header ---------- */
.pp__top {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
margin-bottom: 22px;
}
.pp__brand {
display: flex;
align-items: center;
gap: 14px;
}
.pp__dot {
width: 38px;
height: 38px;
flex: none;
border-radius: 11px;
background: linear-gradient(140deg, var(--accent), var(--accent-2));
box-shadow: 0 0 0 1px var(--line-2), 0 8px 24px rgba(139, 92, 246, 0.4);
position: relative;
}
.pp__dot::after {
content: "";
position: absolute;
inset: 10px;
border-radius: 5px;
background: rgba(12, 13, 16, 0.75);
box-shadow: inset 0 0 0 1.5px rgba(255, 255, 255, 0.5);
}
.pp__title {
margin: 0;
font-size: 19px;
font-weight: 700;
letter-spacing: -0.01em;
}
.pp__sub {
margin: 2px 0 0;
font-size: 13px;
color: var(--muted);
}
.pp__pill {
display: inline-flex;
align-items: center;
gap: 7px;
padding: 6px 12px;
border-radius: 999px;
font-size: 12.5px;
font-weight: 600;
border: 1px solid var(--line);
background: var(--surface);
color: var(--muted);
}
.pp__pill::before {
content: "";
width: 7px;
height: 7px;
border-radius: 50%;
background: var(--muted);
}
.pp__pill[data-state="idle"]::before { background: var(--muted); }
.pp__pill[data-state="running"] {
color: var(--accent-2);
border-color: rgba(34, 211, 238, 0.35);
}
.pp__pill[data-state="running"]::before {
background: var(--accent-2);
animation: pulse 1s ease-in-out infinite;
}
.pp__pill[data-state="done"] {
color: var(--ok);
border-color: rgba(52, 211, 153, 0.35);
}
.pp__pill[data-state="done"]::before { background: var(--ok); }
@keyframes pulse {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.4; transform: scale(0.7); }
}
/* ---------- Grid ---------- */
.pp__grid {
display: grid;
grid-template-columns: minmax(0, 1.15fr) minmax(0, 0.85fr);
grid-template-areas:
"editors controls"
"output controls";
gap: 16px;
align-items: start;
}
.pp__col--editors { grid-area: editors; }
.pp__col--controls { grid-area: controls; }
.pp__col--output { grid-area: output; }
.pp__col {
display: flex;
flex-direction: column;
gap: 16px;
min-width: 0;
}
/* ---------- Cards ---------- */
.card {
background: linear-gradient(180deg, rgba(255, 255, 255, 0.02), transparent), var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-lg);
padding: 16px;
}
.card__head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
margin-bottom: 11px;
}
.card__label {
font-size: 12px;
font-weight: 600;
letter-spacing: 0.05em;
text-transform: uppercase;
color: var(--muted);
}
.card__count {
font-family: "JetBrains Mono", monospace;
font-size: 11.5px;
color: var(--muted);
padding: 2px 8px;
border-radius: 999px;
border: 1px solid var(--line);
background: var(--surface-2);
}
.card__hint {
margin: 8px 0 0;
font-size: 12px;
color: var(--muted);
}
.card__hint code {
font-family: "JetBrains Mono", monospace;
font-size: 11px;
color: var(--accent-2);
background: rgba(34, 211, 238, 0.1);
padding: 1px 5px;
border-radius: 5px;
}
/* ---------- Editors ---------- */
.editor {
width: 100%;
resize: vertical;
border: 1px solid var(--line);
border-radius: var(--r-md);
background: var(--bg);
color: var(--ink);
font-family: "JetBrains Mono", monospace;
font-size: 13px;
line-height: 1.6;
padding: 12px 13px;
min-height: 90px;
transition: border-color 0.15s, box-shadow 0.15s;
}
.editor--tall { min-height: 150px; }
.editor::placeholder { color: var(--muted); }
.editor:focus-visible,
.editor:focus {
outline: none;
border-color: rgba(139, 92, 246, 0.6);
box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.18);
}
/* ---------- Compiled preview ---------- */
.preview {
margin: 0;
white-space: pre-wrap;
word-break: break-word;
font-family: "JetBrains Mono", monospace;
font-size: 12.5px;
line-height: 1.65;
color: var(--ink);
background: var(--bg);
border: 1px solid var(--line);
border-radius: var(--r-md);
padding: 12px 13px;
max-height: 190px;
overflow: auto;
}
.preview:focus-visible {
outline: 2px solid rgba(139, 92, 246, 0.6);
outline-offset: 2px;
}
.preview .tok {
color: var(--accent-2);
background: rgba(34, 211, 238, 0.1);
border-radius: 4px;
padding: 0 3px;
}
.preview .tok--unresolved {
color: var(--warn);
background: rgba(251, 191, 36, 0.14);
}
#unresolvedBadge {
color: var(--warn);
border-color: rgba(251, 191, 36, 0.35);
background: rgba(251, 191, 36, 0.1);
}
/* ---------- Fields ---------- */
.field {
padding: 13px 0;
border-top: 1px solid var(--line);
}
.field:first-of-type {
border-top: none;
padding-top: 4px;
}
.field__row {
display: flex;
align-items: baseline;
justify-content: space-between;
margin-bottom: 9px;
}
.field__label {
font-size: 13px;
font-weight: 500;
color: var(--ink);
}
.field__val {
font-family: "JetBrains Mono", monospace;
font-size: 13px;
font-weight: 600;
color: var(--accent-2);
}
.field__meta {
margin: 8px 0 0;
font-size: 11.5px;
color: var(--muted);
}
/* Select */
.select {
position: relative;
margin-top: 8px;
}
.select select {
appearance: none;
-webkit-appearance: none;
width: 100%;
padding: 10px 34px 10px 12px;
border-radius: var(--r-md);
border: 1px solid var(--line);
background: var(--surface-2);
color: var(--ink);
font-family: inherit;
font-size: 13.5px;
font-weight: 500;
cursor: pointer;
transition: border-color 0.15s, box-shadow 0.15s;
}
.select select:focus-visible {
outline: none;
border-color: rgba(139, 92, 246, 0.6);
box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.18);
}
.select__chev {
position: absolute;
right: 12px;
top: 50%;
transform: translateY(-50%);
width: 12px;
height: 12px;
color: var(--muted);
pointer-events: none;
}
/* Sliders */
.slider {
-webkit-appearance: none;
appearance: none;
width: 100%;
height: 6px;
border-radius: 999px;
background: var(--surface-2);
border: 1px solid var(--line);
outline: none;
cursor: pointer;
}
.slider::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 18px;
height: 18px;
border-radius: 50%;
background: var(--ink);
border: 3px solid var(--accent);
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.5);
transition: transform 0.12s, box-shadow 0.12s;
}
.slider::-moz-range-thumb {
width: 16px;
height: 16px;
border-radius: 50%;
background: var(--ink);
border: 3px solid var(--accent);
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.5);
}
.slider:hover::-webkit-slider-thumb { transform: scale(1.12); }
.slider:active::-webkit-slider-thumb { transform: scale(1.2); }
.slider:focus-visible::-webkit-slider-thumb {
box-shadow: 0 0 0 4px rgba(139, 92, 246, 0.25);
}
.slider:focus-visible {
box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.12);
}
/* ---------- Variables ---------- */
.linkbtn {
background: none;
border: none;
color: var(--accent-2);
font-family: inherit;
font-size: 12.5px;
font-weight: 600;
cursor: pointer;
padding: 2px 4px;
border-radius: 6px;
transition: color 0.15s, background 0.15s;
}
.linkbtn:hover { background: rgba(34, 211, 238, 0.1); }
.linkbtn:focus-visible {
outline: 2px solid rgba(34, 211, 238, 0.5);
outline-offset: 1px;
}
.vars {
display: flex;
flex-direction: column;
gap: 8px;
}
.var {
display: grid;
grid-template-columns: minmax(0, 0.42fr) minmax(0, 1fr) auto;
gap: 8px;
align-items: center;
}
.var__key {
font-family: "JetBrains Mono", monospace;
font-size: 12.5px;
font-weight: 600;
color: var(--accent-2);
border: 1px solid var(--line);
background: var(--bg);
border-radius: var(--r-sm);
padding: 8px 9px;
min-width: 0;
}
.var__val {
font-size: 12.5px;
color: var(--ink);
border: 1px solid var(--line);
background: var(--surface-2);
border-radius: var(--r-sm);
padding: 8px 9px;
font-family: inherit;
min-width: 0;
}
.var__key:focus-visible,
.var__val:focus-visible {
outline: none;
border-color: rgba(139, 92, 246, 0.6);
box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.16);
}
.var__del {
flex: none;
width: 30px;
height: 30px;
border-radius: var(--r-sm);
border: 1px solid var(--line);
background: var(--surface-2);
color: var(--muted);
cursor: pointer;
font-size: 15px;
line-height: 1;
display: grid;
place-items: center;
transition: color 0.15s, border-color 0.15s, background 0.15s;
}
.var__del:hover {
color: var(--danger);
border-color: rgba(248, 113, 113, 0.4);
background: rgba(248, 113, 113, 0.08);
}
.var__del:focus-visible {
outline: 2px solid rgba(248, 113, 113, 0.5);
outline-offset: 1px;
}
.vars__empty {
font-size: 12px;
color: var(--muted);
padding: 4px 0;
}
/* ---------- Meter ---------- */
.card--meter {
background:
linear-gradient(180deg, rgba(139, 92, 246, 0.06), transparent 60%),
var(--surface);
}
.meter {
display: flex;
flex-direction: column;
gap: 10px;
}
.meter__row {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 10px;
}
.meter__k {
font-size: 13px;
color: var(--muted);
}
.meter__v {
font-family: "JetBrains Mono", monospace;
font-size: 14px;
font-weight: 600;
color: var(--ink);
}
.meter__bar {
position: relative;
height: 8px;
border-radius: 999px;
background: var(--surface-2);
border: 1px solid var(--line);
overflow: hidden;
margin: 2px 0;
}
.meter__fill {
position: absolute;
inset: 0;
width: 0%;
border-radius: 999px;
background: linear-gradient(90deg, var(--accent-2), var(--accent));
transition: width 0.35s cubic-bezier(0.22, 1, 0.36, 1), background 0.3s;
}
.meter__fill[data-warn="true"] {
background: linear-gradient(90deg, var(--warn), var(--danger));
}
.meter__ctx {
margin: 0;
font-size: 11.5px;
color: var(--muted);
font-family: "JetBrains Mono", monospace;
}
.meter__row--cost {
margin-top: 4px;
padding-top: 12px;
border-top: 1px solid var(--line);
}
.meter__v--cost {
color: var(--ok);
font-size: 16px;
}
/* ---------- Output ---------- */
.card--output {
display: flex;
flex-direction: column;
min-height: 320px;
}
.card__head--run {
align-items: center;
}
.runrow {
display: flex;
align-items: center;
gap: 8px;
}
.latency {
font-family: "JetBrains Mono", monospace;
font-size: 11.5px;
color: var(--muted);
margin-right: 2px;
}
.btn {
font-family: inherit;
font-size: 13px;
font-weight: 600;
border-radius: var(--r-md);
cursor: pointer;
border: 1px solid var(--line);
padding: 8px 14px;
display: inline-flex;
align-items: center;
gap: 7px;
transition: transform 0.1s, background 0.15s, border-color 0.15s, box-shadow 0.15s, color 0.15s;
}
.btn:active { transform: translateY(1px); }
.btn--ghost {
background: var(--surface-2);
color: var(--muted);
}
.btn--ghost:hover {
color: var(--ink);
border-color: var(--line-2);
}
.btn--run {
background: linear-gradient(140deg, var(--accent), #7c3aed);
border-color: transparent;
color: #fff;
box-shadow: 0 6px 18px rgba(139, 92, 246, 0.35);
}
.btn--run:hover {
box-shadow: 0 8px 24px rgba(139, 92, 246, 0.5);
}
.btn--run:focus-visible,
.btn--ghost:focus-visible {
outline: none;
box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.3);
}
.btn__ico { font-size: 10px; }
.btn--run[data-running="true"] {
background: var(--surface-2);
color: var(--danger);
box-shadow: none;
border-color: rgba(248, 113, 113, 0.4);
}
.output {
flex: 1;
border: 1px solid var(--line);
border-radius: var(--r-md);
background: var(--bg);
padding: 15px 16px;
overflow: auto;
font-size: 14px;
line-height: 1.68;
min-height: 200px;
}
.output:focus-visible {
outline: 2px solid rgba(139, 92, 246, 0.5);
outline-offset: 2px;
}
.output__empty {
height: 100%;
min-height: 170px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 10px;
color: var(--muted);
text-align: center;
}
.output__empty p { margin: 0; font-size: 13.5px; }
.output__glyph {
font-size: 30px;
color: var(--accent);
opacity: 0.55;
}
.output__body {
white-space: pre-wrap;
word-break: break-word;
}
.output__body h3 {
font-size: 15px;
margin: 0 0 8px;
color: var(--ink);
}
.caret {
display: inline-block;
width: 8px;
height: 1.05em;
transform: translateY(2px);
margin-left: 1px;
background: var(--accent-2);
border-radius: 1px;
animation: blink 1s steps(2, start) infinite;
}
@keyframes blink {
50% { opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
.caret { animation: none; }
.pp__pill[data-state="running"]::before { animation: none; }
}
/* ---------- Responsive ---------- */
@media (max-width: 900px) {
.pp__grid {
grid-template-columns: 1fr;
grid-template-areas:
"controls"
"editors"
"output";
}
}
@media (max-width: 520px) {
.pp {
padding: 14px;
}
.pp__top {
flex-wrap: wrap;
}
.card {
padding: 13px;
border-radius: var(--r-md);
}
.var {
grid-template-columns: minmax(0, 0.5fr) minmax(0, 1fr) auto;
}
.runrow .btn--ghost {
padding: 8px 10px;
}
.latency { display: none !important; }
}(function () {
"use strict";
/* ------------------------------------------------------------------ *
* Model catalog — pricing is USD per 1M input tokens (illustrative). *
* ------------------------------------------------------------------ */
var MODELS = [
{ id: "aurora-mini", name: "Aurora Mini", ctx: 32000, inPrice: 0.15, speed: 22 },
{ id: "aurora-pro", name: "Aurora Pro", ctx: 128000, inPrice: 3.0, speed: 14 },
{ id: "aurora-max", name: "Aurora Max (1M)", ctx: 1000000, inPrice: 12.0, speed: 9 },
{ id: "nimbus-flash", name: "Nimbus Flash", ctx: 64000, inPrice: 0.35, speed: 30 }
];
var usd = new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
minimumFractionDigits: 2,
maximumFractionDigits: 5
});
var intFmt = new Intl.NumberFormat("en-US");
/* --------------------------- DOM refs --------------------------- */
var $ = function (id) { return document.getElementById(id); };
var systemPrompt = $("systemPrompt");
var userPrompt = $("userPrompt");
var compiledPreview = $("compiledPreview");
var unresolvedBadge = $("unresolvedBadge");
var modelSelect = $("modelSelect");
var modelPrice = $("modelPrice");
var temperature = $("temperature");
var temperatureVal = $("temperatureVal");
var topP = $("topP");
var topPVal = $("topPVal");
var maxTokens = $("maxTokens");
var maxTokensVal = $("maxTokensVal");
var varsList = $("varsList");
var addVarBtn = $("addVarBtn");
var charCount = $("charCount");
var tokCount = $("tokCount");
var ctxFill = $("ctxFill");
var ctxLabel = $("ctxLabel");
var costOut = $("costOut");
var runBtn = $("runBtn");
var clearBtn = $("clearBtn");
var output = $("output");
var outputEmpty = $("outputEmpty");
var statusPill = $("statusPill");
var latencyEl = $("latency");
var meterBar = ctxFill.parentElement;
/* ---------------------- Variables state ----------------------- */
var variables = [
{ key: "persona", val: "Nova" },
{ key: "product", val: "Halo Notes" },
{ key: "audience", val: "power users" },
{ key: "length", val: "short" }
];
/* ---------------------- Populate models ----------------------- */
MODELS.forEach(function (m) {
var opt = document.createElement("option");
opt.value = m.id;
opt.textContent = m.name;
modelSelect.appendChild(opt);
});
modelSelect.value = "aurora-pro";
function currentModel() {
return MODELS.filter(function (m) { return m.id === modelSelect.value; })[0] || MODELS[0];
}
/* --------------------- Token estimation ----------------------- */
// Rough heuristic: ~1 token per 4 chars, nudged by whitespace-delimited words.
function estimateTokens(text) {
if (!text) return 0;
var chars = text.length;
var words = (text.trim().match(/\S+/g) || []).length;
return Math.max(Math.ceil(chars / 4), Math.ceil(words * 1.3));
}
/* ------------------- Template interpolation ------------------- */
var VAR_RE = /\{\{\s*([\w.-]+)\s*\}\}/g;
function varMap() {
var map = {};
variables.forEach(function (v) {
if (v.key.trim()) map[v.key.trim()] = v.val;
});
return map;
}
function resolve(text) {
var map = varMap();
var unresolved = [];
var out = text.replace(VAR_RE, function (_, key) {
if (Object.prototype.hasOwnProperty.call(map, key) && map[key] !== "") {
return map[key];
}
unresolved.push(key);
return "{{" + key + "}}";
});
return { text: out, unresolved: unresolved };
}
function escapeHtml(s) {
return s
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">");
}
// Highlight resolved (accent) and unresolved (warn) template tokens.
function highlightCompiled(system, user) {
var map = varMap();
function mark(text) {
return escapeHtml(text).replace(VAR_RE, function (m, key) {
var known = Object.prototype.hasOwnProperty.call(map, key) && map[key] !== "";
var cls = known ? "tok" : "tok tok--unresolved";
return '<span class="' + cls + '">' + escapeHtml(known ? map[key] : m) + "</span>";
});
}
return (
'<span style="color:var(--muted)">system:</span> ' + mark(system) +
'\n\n<span style="color:var(--muted)">user:</span> ' + mark(user)
);
}
/* ----------------------- Recompute -------------------------- */
function refresh() {
var sysTok = estimateTokens(systemPrompt.value);
var userTok = estimateTokens(userPrompt.value);
// per-editor counts
document.querySelector('[data-count-for="systemPrompt"]').textContent = sysTok + " tok";
document.querySelector('[data-count-for="userPrompt"]').textContent = userTok + " tok";
var totalChars = systemPrompt.value.length + userPrompt.value.length;
var totalTok = sysTok + userTok;
charCount.textContent = intFmt.format(totalChars);
tokCount.textContent = intFmt.format(totalTok);
var model = currentModel();
// context bar
var pct = Math.min(100, (totalTok / model.ctx) * 100);
ctxFill.style.width = pct.toFixed(1) + "%";
ctxFill.setAttribute("data-warn", pct > 85 ? "true" : "false");
meterBar.setAttribute("aria-valuenow", Math.round(pct));
ctxLabel.textContent =
intFmt.format(totalTok) + " / " + intFmt.format(model.ctx) + " tokens of context";
// cost = input tokens * price-per-million
var cost = (totalTok / 1e6) * model.inPrice;
costOut.textContent = usd.format(cost);
modelPrice.textContent =
"$" + model.inPrice.toFixed(2) + " / 1M input tokens · " +
intFmt.format(model.ctx) + " ctx";
// compiled preview
compiledPreview.innerHTML = highlightCompiled(systemPrompt.value, userPrompt.value);
var res = resolve(systemPrompt.value + " " + userPrompt.value);
var uniqueUnres = res.unresolved.filter(function (v, i, a) { return a.indexOf(v) === i; });
if (uniqueUnres.length) {
unresolvedBadge.hidden = false;
unresolvedBadge.textContent = uniqueUnres.length + " unresolved";
} else {
unresolvedBadge.hidden = true;
}
}
/* ----------------------- Variables UI ----------------------- */
function renderVars() {
varsList.innerHTML = "";
if (!variables.length) {
var empty = document.createElement("p");
empty.className = "vars__empty";
empty.textContent = "No variables. Add one to use in {{template}} tokens.";
varsList.appendChild(empty);
return;
}
variables.forEach(function (v, idx) {
var row = document.createElement("div");
row.className = "var";
var key = document.createElement("input");
key.className = "var__key";
key.value = v.key;
key.setAttribute("aria-label", "Variable name " + (idx + 1));
key.placeholder = "name";
key.addEventListener("input", function () {
variables[idx].key = key.value.replace(/\s+/g, "");
key.value = variables[idx].key;
refresh();
});
var val = document.createElement("input");
val.className = "var__val";
val.value = v.val;
val.setAttribute("aria-label", "Value for " + (v.key || "variable " + (idx + 1)));
val.placeholder = "value";
val.addEventListener("input", function () {
variables[idx].val = val.value;
refresh();
});
var del = document.createElement("button");
del.className = "var__del";
del.type = "button";
del.innerHTML = "×";
del.setAttribute("aria-label", "Remove variable " + (v.key || idx + 1));
del.addEventListener("click", function () {
variables.splice(idx, 1);
renderVars();
refresh();
});
row.appendChild(key);
row.appendChild(val);
row.appendChild(del);
varsList.appendChild(row);
});
}
addVarBtn.addEventListener("click", function () {
variables.push({ key: "", val: "" });
renderVars();
var inputs = varsList.querySelectorAll(".var__key");
if (inputs.length) inputs[inputs.length - 1].focus();
refresh();
});
/* ----------------------- Streaming run ---------------------- */
var rafId = null;
var running = false;
function buildCompletion() {
var model = currentModel();
var map = varMap();
var product = map.product || "the product";
var audience = map.audience || "your users";
var persona = map.persona || "the assistant";
var temp = parseFloat(temperature.value);
var flavor = temp > 1.1
? "an exploratory, high-variance take"
: temp < 0.4
? "a tight, deterministic take"
: "a balanced take";
return (
"## " + product + " — now with Dark Mode\n\n" +
"Hey " + audience + ", " + persona + " here. We just shipped a system-aware " +
"Dark Mode across " + product + ". It follows your OS theme by default and " +
"remembers per-workspace overrides.\n\n" +
"Highlights:\n" +
"• Automatic light/dark switching with a manual toggle\n" +
"• Recalibrated contrast for AA-compliant readability\n" +
"• Zero layout shift — themes swap via CSS variables\n\n" +
"[simulated by " + model.name + " · temp " + temp.toFixed(2) +
" · top-p " + parseFloat(topP.value).toFixed(2) + " · " + flavor + "]"
);
}
function tokenize(text) {
// split into word-ish chunks so streaming feels like tokens
return text.match(/\s+|\S+/g) || [];
}
function setStatus(state, label) {
statusPill.setAttribute("data-state", state);
statusPill.textContent = label;
}
function stopStream() {
running = false;
if (rafId) cancelAnimationFrame(rafId);
rafId = null;
runBtn.setAttribute("data-running", "false");
runBtn.querySelector(".btn__txt").textContent = "Run";
runBtn.querySelector(".btn__ico").innerHTML = "▶";
}
function runStream() {
if (running) { // acts as a Stop button
stopStream();
setStatus("idle", "Stopped");
var caretEl = output.querySelector(".caret");
if (caretEl) caretEl.remove();
return;
}
// Respect the max-tokens ceiling by truncating the simulated stream.
var cap = parseInt(maxTokens.value, 10);
var chunks = tokenize(buildCompletion()).slice(0, cap);
var model = currentModel();
running = true;
outputEmpty.hidden = true;
output.innerHTML = '<div class="output__body"></div>';
var body = output.querySelector(".output__body");
var caret = document.createElement("span");
caret.className = "caret";
body.appendChild(caret);
runBtn.setAttribute("data-running", "true");
runBtn.querySelector(".btn__txt").textContent = "Stop";
runBtn.querySelector(".btn__ico").innerHTML = "■";
latencyEl.hidden = false;
setStatus("running", "Streaming");
var i = 0;
var start = performance.now();
var lastEmit = start;
// Frame-aligned streaming: emit a chunk once enough time has elapsed,
// pacing derived from the model's simulated tokens/sec speed.
var interval = 1000 / model.speed;
function frame(now) {
if (!running) return;
if (now - lastEmit >= interval) {
var text = document.createTextNode(chunks[i]);
body.insertBefore(text, caret);
i++;
lastEmit = now;
output.scrollTop = output.scrollHeight;
latencyEl.textContent =
((now - start) / 1000).toFixed(1) + "s · " + i + " tok";
}
if (i < chunks.length) {
rafId = requestAnimationFrame(frame);
} else {
finishStream(start, i);
}
}
rafId = requestAnimationFrame(frame);
}
function finishStream(start, emitted) {
running = false;
rafId = null;
var caretEl = output.querySelector(".caret");
if (caretEl) caretEl.remove();
runBtn.setAttribute("data-running", "false");
runBtn.querySelector(".btn__txt").textContent = "Run";
runBtn.querySelector(".btn__ico").innerHTML = "▶";
var secs = (performance.now() - start) / 1000;
latencyEl.textContent = secs.toFixed(1) + "s · " + emitted + " tok";
setStatus("done", "Done");
}
runBtn.addEventListener("click", runStream);
clearBtn.addEventListener("click", function () {
stopStream();
output.innerHTML = "";
output.appendChild(outputEmpty);
outputEmpty.hidden = false;
latencyEl.hidden = true;
setStatus("idle", "Idle");
});
/* ----------------------- Wire inputs ------------------------ */
systemPrompt.addEventListener("input", refresh);
userPrompt.addEventListener("input", refresh);
modelSelect.addEventListener("change", refresh);
temperature.addEventListener("input", function () {
temperatureVal.textContent = parseFloat(temperature.value).toFixed(2);
});
topP.addEventListener("input", function () {
topPVal.textContent = parseFloat(topP.value).toFixed(2);
});
maxTokens.addEventListener("input", function () {
maxTokensVal.textContent = maxTokens.value;
});
// Keyboard shortcut: Cmd/Ctrl+Enter to run.
document.addEventListener("keydown", function (e) {
if ((e.metaKey || e.ctrlKey) && e.key === "Enter") {
e.preventDefault();
runStream();
}
});
/* --------------------------- Init --------------------------- */
temperatureVal.textContent = parseFloat(temperature.value).toFixed(2);
topPVal.textContent = parseFloat(topP.value).toFixed(2);
maxTokensVal.textContent = maxTokens.value;
renderVars();
refresh();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Prompt Playground</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap"
rel="stylesheet"
/>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<main class="pp" aria-label="Prompt playground">
<header class="pp__top">
<div class="pp__brand">
<span class="pp__dot" aria-hidden="true"></span>
<div>
<h1 class="pp__title">Prompt Playground</h1>
<p class="pp__sub">Compose, meter and run prompts against a simulated model.</p>
</div>
</div>
<div class="pp__topmeta">
<span class="pp__pill" id="statusPill" data-state="idle">Idle</span>
</div>
</header>
<div class="pp__grid">
<!-- Editors -->
<section class="pp__col pp__col--editors" aria-label="Prompt editors">
<article class="card">
<div class="card__head">
<label class="card__label" for="systemPrompt">System prompt</label>
<span class="card__count" data-count-for="systemPrompt">0 tok</span>
</div>
<textarea
id="systemPrompt"
class="editor"
spellcheck="false"
rows="5"
aria-describedby="systemHint"
>You are {{persona}}, a concise assistant for {{product}}. Answer in a friendly, technical tone and never invent facts.</textarea>
<p class="card__hint" id="systemHint">Sets the model's role and guardrails.</p>
</article>
<article class="card card--grow">
<div class="card__head">
<label class="card__label" for="userPrompt">User prompt</label>
<span class="card__count" data-count-for="userPrompt">0 tok</span>
</div>
<textarea
id="userPrompt"
class="editor editor--tall"
spellcheck="false"
rows="8"
aria-describedby="userHint"
>Write a {{length}} release note for {{product}} announcing dark mode. Audience: {{audience}}.</textarea>
<p class="card__hint" id="userHint">Use <code>{{variable}}</code> tokens — resolve them in the Variables panel.</p>
</article>
<article class="card" aria-label="Compiled prompt preview">
<div class="card__head">
<span class="card__label">Compiled preview</span>
<span class="card__count" id="unresolvedBadge" hidden>0 unresolved</span>
</div>
<pre class="preview" id="compiledPreview" tabindex="0"></pre>
</article>
</section>
<!-- Controls -->
<aside class="pp__col pp__col--controls" aria-label="Parameters">
<article class="card">
<div class="card__head">
<span class="card__label">Parameters</span>
</div>
<div class="field">
<label class="field__label" for="modelSelect">Model</label>
<div class="select">
<select id="modelSelect" aria-describedby="modelPrice"></select>
<svg class="select__chev" viewBox="0 0 12 12" aria-hidden="true"><path d="M2 4l4 4 4-4" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
</div>
<p class="field__meta" id="modelPrice"></p>
</div>
<div class="field">
<div class="field__row">
<label class="field__label" for="temperature">Temperature</label>
<output class="field__val" id="temperatureVal">0.70</output>
</div>
<input id="temperature" class="slider" type="range" min="0" max="2" step="0.01" value="0.7" />
<p class="field__meta">Higher = more random, lower = more deterministic.</p>
</div>
<div class="field">
<div class="field__row">
<label class="field__label" for="topP">Top-p</label>
<output class="field__val" id="topPVal">1.00</output>
</div>
<input id="topP" class="slider" type="range" min="0" max="1" step="0.01" value="1" />
<p class="field__meta">Nucleus sampling cutoff.</p>
</div>
<div class="field">
<div class="field__row">
<label class="field__label" for="maxTokens">Max output tokens</label>
<output class="field__val" id="maxTokensVal">512</output>
</div>
<input id="maxTokens" class="slider" type="range" min="64" max="4096" step="64" value="512" />
<p class="field__meta">Upper bound on the completion length.</p>
</div>
</article>
<!-- Variables -->
<article class="card">
<div class="card__head">
<span class="card__label">Variables</span>
<button class="linkbtn" id="addVarBtn" type="button">+ Add</button>
</div>
<div class="vars" id="varsList" aria-live="polite"></div>
</article>
<!-- Meter -->
<article class="card card--meter" aria-label="Token and cost meter">
<div class="card__head">
<span class="card__label">Usage estimate</span>
</div>
<div class="meter">
<div class="meter__row">
<span class="meter__k">Characters</span>
<span class="meter__v" id="charCount">0</span>
</div>
<div class="meter__row">
<span class="meter__k">Input tokens (est.)</span>
<span class="meter__v" id="tokCount">0</span>
</div>
<div class="meter__bar" role="progressbar" aria-label="Context window used" aria-valuemin="0" aria-valuemax="100" aria-valuenow="0">
<span class="meter__fill" id="ctxFill"></span>
</div>
<p class="meter__ctx" id="ctxLabel">0 / 0 tokens of context</p>
<div class="meter__row meter__row--cost">
<span class="meter__k">Est. input cost</span>
<span class="meter__v meter__v--cost" id="costOut">$0.00</span>
</div>
</div>
</article>
</aside>
<!-- Output -->
<section class="pp__col pp__col--output" aria-label="Model response">
<article class="card card--output">
<div class="card__head card__head--run">
<span class="card__label">Response</span>
<div class="runrow">
<span class="latency" id="latency" hidden></span>
<button class="btn btn--ghost" id="clearBtn" type="button">Clear</button>
<button class="btn btn--run" id="runBtn" type="button">
<span class="btn__ico" aria-hidden="true">▶</span>
<span class="btn__txt">Run</span>
</button>
</div>
</div>
<div class="output" id="output" aria-live="polite" tabindex="0">
<div class="output__empty" id="outputEmpty">
<span class="output__glyph" aria-hidden="true">◉</span>
<p>Press <strong>Run</strong> to stream a simulated completion.</p>
</div>
</div>
</article>
</section>
</div>
</main>
<script src="script.js"></script>
</body>
</html>The Prompt Playground is a self-contained workbench for iterating on LLM prompts. Two editors hold the system and user messages, while a parameter rail exposes a model selector, temperature and top-p sliders, and a max-tokens control. A live meter tallies characters and estimated tokens across both prompts and multiplies them by the selected model’s per-million input price to project a request cost in real time. A {{variable}} panel lets you define template values that are interpolated into the compiled prompt before sending.
Pressing Run compiles the messages, then produces a simulated completion that streams in token by token so you can feel the latency of a real request. The streaming loop is driven by requestAnimationFrame for smooth, frame-aligned appends, and can be stopped mid-flight. Everything updates optimistically: dragging a slider re-labels its value, editing a prompt re-counts tokens, and defining a variable immediately re-resolves the preview.
The component demonstrates the requestAnimationFrame scheduling API for cancellable, jank-free streaming, alongside Intl.NumberFormat for locale-aware cost formatting and a lightweight {{template}} interpolation pass over textarea input.