c68b2303a0
Serves a localhost page in the pseudo-skill register with the reference markers as clickable buttons: clicks land in <workdir>/events.jsonl, a persistent Monitor tail wakes the session, the session rewrites page.html, and the browser live-reloads over SSE. Actions per marker: expand / show real code / explain / ask, plus a page-level ask box; anchors become forge deep-links when the project has a reachable remote; incidental findings are surfaced on the page as anchored notes. The port is configured in one place (the PORT constant atop scripts/server.py; --port overrides per run). Mechanics proven live against the aura repo before extraction into this skill.
196 lines
7.1 KiB
HTML
196 lines
7.1 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>explore — {{TITLE}}</title>
|
|
<style>
|
|
:root {
|
|
--bg: #10141a; --panel: #171c24; --border: #232b36;
|
|
--fg: #d6dde6; --dim: #68737f; --comment: #7d8894;
|
|
--kw: #c792ea; --str: #a5d6a7;
|
|
--marker-bg: #1f3a52; --marker-border: #35618a; --marker-fg: #8fd6ff;
|
|
--ok: #63d68f; --busy: #e8c268; --err: #e87979;
|
|
}
|
|
* { box-sizing: border-box; }
|
|
html, body { margin: 0; }
|
|
body {
|
|
background: var(--bg); color: var(--fg);
|
|
font: 15px/1.5 "JetBrains Mono", "Fira Code", ui-monospace, monospace;
|
|
max-width: 980px; margin-inline: auto; padding: 2rem 1.5rem 5rem;
|
|
}
|
|
header { display: flex; align-items: baseline; gap: .9rem; margin-bottom: .4rem; }
|
|
h1 { font-size: 1.05rem; margin: 0; font-weight: 600; }
|
|
#conn { font-size: .78rem; color: var(--dim); }
|
|
#conn.live::before { content: "\25CF "; color: var(--ok); }
|
|
#conn.dead::before { content: "\25CF "; color: var(--err); }
|
|
p.hint { color: var(--dim); font-size: .82rem; margin: 0 0 1.4rem; }
|
|
pre.pseudo {
|
|
background: var(--panel); border: 1px solid var(--border); border-radius: 8px;
|
|
padding: 1.15rem 1.35rem; overflow-x: auto; line-height: 1.72; margin: 0;
|
|
}
|
|
.c { color: var(--comment); }
|
|
.note {
|
|
display: block; margin: .4rem 0 .55rem; padding: .5rem .85rem;
|
|
border-left: 3px solid var(--marker-border); border-radius: 0 6px 6px 0;
|
|
background: #1a2330; color: #aebccb; font-size: .88em; line-height: 1.55;
|
|
white-space: pre-wrap;
|
|
}
|
|
.note.code { white-space: pre; overflow-x: auto; font-size: .85em; line-height: 1.5; color: var(--fg); }
|
|
.note.finding { border-left-color: var(--busy); background: #241f14; color: #d9c9a0; }
|
|
.warn { text-decoration: underline wavy var(--busy); text-decoration-skip-ink: none; cursor: help; }
|
|
.k { color: var(--kw); }
|
|
.s { color: var(--str); }
|
|
a.src { color: inherit; text-decoration: underline dotted; text-underline-offset: 3px; }
|
|
a.src:hover { color: var(--marker-fg); }
|
|
button.marker {
|
|
font: inherit; font-size: .92em; line-height: 1.25;
|
|
border: 1px solid var(--marker-border); border-radius: 5px;
|
|
background: var(--marker-bg); color: var(--marker-fg);
|
|
padding: 0 .4em; cursor: pointer; vertical-align: baseline;
|
|
}
|
|
button.marker:hover { background: #2a4f70; }
|
|
|
|
/* click menu */
|
|
#menu {
|
|
position: fixed; z-index: 10; display: none; flex-direction: column;
|
|
background: var(--panel); border: 1px solid var(--marker-border);
|
|
border-radius: 8px; padding: .3rem; box-shadow: 0 6px 24px #0009;
|
|
}
|
|
#menu.open { display: flex; }
|
|
#menu button {
|
|
font: inherit; font-size: .85rem; text-align: left;
|
|
background: none; border: 0; color: var(--fg);
|
|
padding: .35rem .7rem; border-radius: 5px; cursor: pointer;
|
|
}
|
|
#menu button:hover { background: var(--marker-bg); color: var(--marker-fg); }
|
|
|
|
/* page-level Q&A */
|
|
#qa { margin-top: 1.3rem; }
|
|
#qa .q { color: var(--dim); font-size: .84rem; margin: 0 0 .35rem; }
|
|
|
|
/* status + ask box */
|
|
#status { min-height: 1.4rem; margin: 1rem 0 0; font-size: .84rem; color: var(--dim); }
|
|
#status.busy { color: var(--busy); }
|
|
#status.err { color: var(--err); }
|
|
form#ask { display: flex; gap: .6rem; margin-top: 1.6rem; }
|
|
form#ask input {
|
|
flex: 1; font: inherit; font-size: .88rem;
|
|
background: var(--panel); border: 1px solid var(--border); border-radius: 7px;
|
|
color: var(--fg); padding: .5rem .8rem; outline: none;
|
|
}
|
|
form#ask input:focus { border-color: var(--marker-border); }
|
|
form#ask button {
|
|
font: inherit; font-size: .88rem; cursor: pointer;
|
|
background: var(--marker-bg); color: var(--marker-fg);
|
|
border: 1px solid var(--marker-border); border-radius: 7px; padding: .5rem 1rem;
|
|
}
|
|
form#ask button:hover { background: #2a4f70; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<header>
|
|
<h1>explore — {{TITLE}}</h1>
|
|
<span id="conn" class="dead">connecting…</span>
|
|
</header>
|
|
<p class="hint">{{HINT}} Click a <button class="marker" type="button" tabindex="-1" style="pointer-events:none">[n]</button> marker to zoom.</p>
|
|
|
|
<pre class="pseudo">{{CONTENT}}</pre>
|
|
|
|
<div id="menu" role="menu">
|
|
<button data-action="expand">expand — one level deeper</button>
|
|
<button data-action="code">show real code</button>
|
|
<button data-action="explain">explain</button>
|
|
<button data-action="ask">ask about this…</button>
|
|
</div>
|
|
|
|
<p id="status"></p>
|
|
|
|
<form id="ask">
|
|
<input name="q" placeholder="Ask anything about this page…" autocomplete="off">
|
|
<button type="submit">send</button>
|
|
</form>
|
|
|
|
<script>
|
|
"use strict";
|
|
const menu = document.getElementById("menu");
|
|
const statusEl = document.getElementById("status");
|
|
let currentMarker = null;
|
|
|
|
function setStatus(cls, text) {
|
|
statusEl.className = cls;
|
|
statusEl.textContent = text;
|
|
}
|
|
|
|
async function send(ev) {
|
|
const label = ev.marker ? `${ev.action} [${ev.marker}]` : ev.action;
|
|
setStatus("busy", `sent: ${label} — waiting for Claude to rewrite the page…`);
|
|
try {
|
|
const r = await fetch("/event", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(ev),
|
|
});
|
|
if (!r.ok) throw new Error(`HTTP ${r.status}`);
|
|
} catch {
|
|
setStatus("err", "send failed — is the explore server running?");
|
|
}
|
|
}
|
|
|
|
// marker click -> action menu next to the marker
|
|
document.querySelectorAll("button.marker[data-marker]").forEach((btn) => {
|
|
btn.addEventListener("click", (e) => {
|
|
e.stopPropagation();
|
|
currentMarker = btn.dataset.marker;
|
|
const rect = btn.getBoundingClientRect();
|
|
menu.classList.add("open");
|
|
// measure after showing, then clamp into the viewport
|
|
const mw = menu.offsetWidth, mh = menu.offsetHeight;
|
|
let x = rect.left, y = rect.bottom + 6;
|
|
if (x + mw > innerWidth - 8) x = innerWidth - mw - 8;
|
|
if (y + mh > innerHeight - 8) y = rect.top - mh - 6;
|
|
menu.style.left = `${x}px`;
|
|
menu.style.top = `${y}px`;
|
|
});
|
|
});
|
|
|
|
menu.addEventListener("click", (e) => {
|
|
const action = e.target.dataset && e.target.dataset.action;
|
|
if (!action) return;
|
|
menu.classList.remove("open");
|
|
if (action === "ask") {
|
|
const text = prompt(`Question about [${currentMarker}]:`);
|
|
if (text === null || text.trim() === "") return;
|
|
send({ marker: currentMarker, action, text: text.trim() });
|
|
} else {
|
|
send({ marker: currentMarker, action });
|
|
}
|
|
});
|
|
|
|
document.addEventListener("click", () => menu.classList.remove("open"));
|
|
document.addEventListener("keydown", (e) => {
|
|
if (e.key === "Escape") menu.classList.remove("open");
|
|
});
|
|
|
|
// free-text question about the whole page
|
|
document.getElementById("ask").addEventListener("submit", (e) => {
|
|
e.preventDefault();
|
|
const input = e.target.elements.q;
|
|
const text = input.value.trim();
|
|
if (!text) return;
|
|
send({ marker: null, action: "ask", text });
|
|
input.value = "";
|
|
});
|
|
|
|
// live-reload via SSE
|
|
const conn = document.getElementById("conn");
|
|
const es = new EventSource("/stream");
|
|
es.addEventListener("reload", () => location.reload());
|
|
es.onopen = () => { conn.className = "live"; conn.textContent = "live"; };
|
|
es.onerror = () => { conn.className = "dead"; conn.textContent = "reconnecting…"; };
|
|
</script>
|
|
|
|
</body>
|
|
</html>
|