Gate aquarium UI behind agents
This commit is contained in:
parent
e504a0f6ad
commit
4c2943ddff
3 changed files with 190 additions and 56 deletions
|
|
@ -63,8 +63,8 @@ async function launchSmokeBrowser() {
|
|||
async function smokeViewport(browser, viewport, screenshotPath, exerciseFluidPanel) {
|
||||
const page = await browser.newPage({ viewport });
|
||||
await page.goto(url, { waitUntil: "networkidle" });
|
||||
await page.locator("h1").filter({ hasText: "Operator Console" }).waitFor({ state: "attached" });
|
||||
await page.locator(".immersiveShell").waitFor();
|
||||
await page.locator('[data-agent-node="coordinator"]').waitFor({ state: "attached" });
|
||||
await page.locator(".agentSmokeCanvas").waitFor();
|
||||
await page.locator(".agentCrispCanvas").waitFor();
|
||||
await page.waitForTimeout(1000);
|
||||
|
|
@ -75,25 +75,9 @@ async function smokeViewport(browser, viewport, screenshotPath, exerciseFluidPan
|
|||
throw new Error(`agent smoke canvas did not render: ${smokeProbe.reason}`);
|
||||
}
|
||||
|
||||
const operatorSurface = await page.evaluate(() => {
|
||||
return [".immersiveTopbar", ".deckRail", ".diegeticPanel"].every((selector) => {
|
||||
const element = document.querySelector(`.agentStage ${selector}`);
|
||||
if (!element) return false;
|
||||
const style = window.getComputedStyle(element);
|
||||
const rect = element.getBoundingClientRect();
|
||||
return (
|
||||
style.opacity !== "0" &&
|
||||
style.visibility !== "hidden" &&
|
||||
rect.width > 20 &&
|
||||
rect.height > 20
|
||||
);
|
||||
});
|
||||
});
|
||||
if (!operatorSurface) {
|
||||
throw new Error("static operator overlays are not visible");
|
||||
}
|
||||
if (!crispProbe.nonBlank && !operatorSurface) {
|
||||
throw new Error(`no crisp operator surface rendered: ${crispProbe.reason}`);
|
||||
const quietSurface = await page.evaluate(() => !document.querySelector(".agentFocusSurface"));
|
||||
if (!quietSurface) {
|
||||
throw new Error("operator surface rendered before an aquarium object was selected");
|
||||
}
|
||||
|
||||
const projectionProbe = await page.evaluate(() => {
|
||||
|
|
@ -176,6 +160,24 @@ async function smokeViewport(browser, viewport, screenshotPath, exerciseFluidPan
|
|||
throw new Error("research DOM node has no clickable bounds");
|
||||
}
|
||||
await page.mouse.click(researchBox.x + researchBox.width / 2, researchBox.y + researchBox.height / 2);
|
||||
const operatorSurface = await page.evaluate(() => {
|
||||
const focus = document.querySelector('.agentStage [data-agent-focus="research"]');
|
||||
if (!(focus instanceof HTMLElement)) return { ok: false, reason: "research focus surface missing" };
|
||||
const style = window.getComputedStyle(focus);
|
||||
const surfaces = [".immersiveTopbar", ".deckRail", ".diegeticPanel"].every((selector) => {
|
||||
const element = focus.querySelector(selector);
|
||||
if (!(element instanceof HTMLElement)) return false;
|
||||
const rect = element.getBoundingClientRect();
|
||||
return rect.width > 20 && rect.height > 20;
|
||||
});
|
||||
return {
|
||||
ok: style.opacity !== "0" && style.visibility !== "hidden" && surfaces,
|
||||
reason: `opacity=${style.opacity} visibility=${style.visibility} surfaces=${surfaces}`,
|
||||
};
|
||||
});
|
||||
if (!operatorSurface.ok) {
|
||||
throw new Error(`agent-owned operator surface did not open: ${operatorSurface.reason}`);
|
||||
}
|
||||
try {
|
||||
await page.waitForFunction(() => {
|
||||
const audio = window.__epiphanyAquariumAudio;
|
||||
|
|
@ -195,7 +197,7 @@ async function smokeViewport(browser, viewport, screenshotPath, exerciseFluidPan
|
|||
throw new Error(`aquarium audio did not wake correctly: ${JSON.stringify(audio)}`, { cause: error });
|
||||
}
|
||||
audioProbe = await page.evaluate(() => window.__epiphanyAquariumAudio ?? null);
|
||||
await page.locator(".deckRail button").nth(1).click();
|
||||
await page.locator('[data-agent-focus="research"] .deckRail button').nth(1).dispatchEvent("pointerdown", { bubbles: true });
|
||||
await page.waitForFunction(() => {
|
||||
const audio = window.__epiphanyAquariumAudio;
|
||||
return audio?.state === "running" && audio.interfaceHitCount >= 1;
|
||||
|
|
|
|||
|
|
@ -933,7 +933,6 @@ export function App() {
|
|||
|
||||
const operatorSurface = (
|
||||
<>
|
||||
<div className="hudGrid" aria-hidden="true" />
|
||||
<header className="immersiveTopbar">
|
||||
<div className="operatorIdentity">
|
||||
<p className="eyebrow">Epiphany MVP</p>
|
||||
|
|
@ -1471,7 +1470,8 @@ function AgentConstellation({
|
|||
const agentNodeRefs = useRef(new globalThis.Map<string, HTMLButtonElement>());
|
||||
const thoughtNodeRefs = useRef(new globalThis.Map<string, HTMLDivElement>());
|
||||
const optionHaloNodeRefs = useRef(new globalThis.Map<string, HTMLDivElement>());
|
||||
const [selectedAgentId, setSelectedAgentId] = useState("coordinator");
|
||||
const focusSurfaceRef = useRef<HTMLDivElement | null>(null);
|
||||
const [selectedAgentId, setSelectedAgentId] = useState<string | null>(null);
|
||||
const [hoveredAgentId, setHoveredAgentId] = useState<string | null>(null);
|
||||
const agents = useMemo<ProjectedAgent[]>(() => {
|
||||
return constellationSpecs.map((spec) => {
|
||||
|
|
@ -1614,6 +1614,7 @@ function AgentConstellation({
|
|||
const agentNode = agentNodeRefs.current.get(projection.id);
|
||||
const thoughtNode = thoughtNodeRefs.current.get(projection.id);
|
||||
const optionHaloNode = optionHaloNodeRefs.current.get(projection.id);
|
||||
const focusSurfaceNode = (selectedAgentId ?? hoveredAgentId) === projection.id ? focusSurfaceRef.current : null;
|
||||
const properties: Array<[string, string]> = [
|
||||
["--agent-x", `${projection.xPercent}%`],
|
||||
["--agent-y", `${projection.yPercent}%`],
|
||||
|
|
@ -1631,17 +1632,19 @@ function AgentConstellation({
|
|||
agentNode?.style.setProperty(name, value);
|
||||
thoughtNode?.style.setProperty(name, value);
|
||||
optionHaloNode?.style.setProperty(name, value);
|
||||
focusSurfaceNode?.style.setProperty(name, value);
|
||||
}
|
||||
thoughtNode?.toggleAttribute("data-agent-hot", projection.hover > 0.35);
|
||||
optionHaloNode?.toggleAttribute("data-agent-hot", projection.hover > 0.2);
|
||||
}
|
||||
}, []);
|
||||
}, [hoveredAgentId, selectedAgentId]);
|
||||
|
||||
useEffect(() => {
|
||||
rendererRef.current?.setFrame({
|
||||
activeLabel: activeDeck ? `${deckLabels[activeDeck]} / ${activeSubdeck ?? ""}` : undefined,
|
||||
agents: aquariumAgents,
|
||||
onProjectionFrame: applyProjectionFrame,
|
||||
selectedAgentId,
|
||||
selectedAgentId: selectedAgentId ?? "",
|
||||
ui,
|
||||
variant,
|
||||
});
|
||||
|
|
@ -1737,8 +1740,12 @@ function AgentConstellation({
|
|||
const agentId = rendererRef.current?.pickAgent();
|
||||
if (agentId) {
|
||||
setSelectedAgentId(agentId);
|
||||
} else {
|
||||
setSelectedAgentId(null);
|
||||
}
|
||||
}
|
||||
const focusedAgentId = selectedAgentId;
|
||||
const focusedAgent = focusedAgentId ? aquariumAgents.find((agent) => agent.id === focusedAgentId) : null;
|
||||
|
||||
return (
|
||||
<section
|
||||
|
|
@ -1760,9 +1767,6 @@ function AgentConstellation({
|
|||
</div>
|
||||
)}
|
||||
<div className="agentStage">
|
||||
<div className="aquariumOperatorLayer" onPointerDownCapture={handleInterfacePointerDown}>
|
||||
{operatorSurface}
|
||||
</div>
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
className="agentSmokeCanvas"
|
||||
|
|
@ -1779,6 +1783,26 @@ function AgentConstellation({
|
|||
aria-hidden="true"
|
||||
/>
|
||||
<div className="agentStageVignette" aria-hidden="true" />
|
||||
{operatorSurface && focusedAgent && (
|
||||
<div
|
||||
className={`agentFocusSurface ${selectedAgentId === focusedAgent.id ? "locked" : "preview"} ${
|
||||
focusedAgent.baseX > 62 ? "anchorLeft" : focusedAgent.baseX < 38 ? "anchorRight" : "anchorCenter"
|
||||
} ${focusedAgent.baseY > 58 ? "anchorUp" : "anchorDown"}`}
|
||||
ref={focusSurfaceRef}
|
||||
data-agent-focus={focusedAgent.id}
|
||||
onPointerDownCapture={handleInterfacePointerDown}
|
||||
style={
|
||||
{
|
||||
"--agent-x": `${focusedAgent.baseX}%`,
|
||||
"--agent-y": `${focusedAgent.baseY}%`,
|
||||
"--agent-color": focusedAgent.color,
|
||||
"--agent-glow": focusedAgent.glow,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
>
|
||||
{operatorSurface}
|
||||
</div>
|
||||
)}
|
||||
{agents.map((agent) => (
|
||||
<button
|
||||
className={`agentCharacter ${agent.shape} ${agent.tone} ${selectedAgentId === agent.id ? "selected" : ""}`}
|
||||
|
|
@ -1883,19 +1907,21 @@ function AgentConstellation({
|
|||
</div>
|
||||
);
|
||||
})}
|
||||
<div className="constellationInspector">
|
||||
<div>
|
||||
<span>{selectedAgent.title}</span>
|
||||
<strong>{selectedAgent.name}</strong>
|
||||
<p>{selectedAgent.thought}</p>
|
||||
{variant !== "fullscreen" && (
|
||||
<div className="constellationInspector">
|
||||
<div>
|
||||
<span>{selectedAgent.title}</span>
|
||||
<strong>{selectedAgent.name}</strong>
|
||||
<p>{selectedAgent.thought}</p>
|
||||
</div>
|
||||
<dl className="facts compact">
|
||||
<div><dt>Status</dt><dd><Pill tone={selectedAgent.tone}>{selectedAgent.status}</Pill></dd></div>
|
||||
<div><dt>Detail</dt><dd>{selectedAgent.detail}</dd></div>
|
||||
<div><dt>Jobs</dt><dd>{selectedAgent.jobs}</dd></div>
|
||||
<div><dt>Review</dt><dd>{selectedAgent.review}</dd></div>
|
||||
</dl>
|
||||
</div>
|
||||
<dl className="facts compact">
|
||||
<div><dt>Status</dt><dd><Pill tone={selectedAgent.tone}>{selectedAgent.status}</Pill></dd></div>
|
||||
<div><dt>Detail</dt><dd>{selectedAgent.detail}</dd></div>
|
||||
<div><dt>Jobs</dt><dd>{selectedAgent.jobs}</dd></div>
|
||||
<div><dt>Review</dt><dd>{selectedAgent.review}</dd></div>
|
||||
</dl>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -472,21 +472,6 @@ body:has(.immersiveShell) {
|
|||
mask-image: radial-gradient(circle at 50% 50%, rgba(0, 0, 0, 0.8), transparent 72%);
|
||||
}
|
||||
|
||||
.aquariumOperatorLayer {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 7;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.aquariumOperatorLayer > * {
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.aquariumOperatorLayer .hudGrid {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.immersiveTopbar,
|
||||
.deckRail,
|
||||
.diegeticPanel,
|
||||
|
|
@ -495,6 +480,115 @@ body:has(.immersiveShell) {
|
|||
z-index: 8;
|
||||
}
|
||||
|
||||
.agentFocusSurface {
|
||||
position: absolute;
|
||||
left: var(--agent-x);
|
||||
top: var(--agent-y);
|
||||
z-index: 7;
|
||||
display: grid;
|
||||
grid-template-areas:
|
||||
"top top"
|
||||
"rail panel"
|
||||
"toast panel";
|
||||
grid-template-columns: 86px minmax(360px, min(680px, 44vw));
|
||||
grid-template-rows: auto minmax(0, 1fr) auto;
|
||||
gap: 10px;
|
||||
max-width: calc(100vw - 32px);
|
||||
max-height: min(78vh, 820px);
|
||||
opacity: 0;
|
||||
pointer-events: auto;
|
||||
transform: translate(var(--focus-x, -12%), var(--focus-y, -8%)) scale(0.94);
|
||||
transform-origin: 18% 12%;
|
||||
transition:
|
||||
opacity 170ms ease,
|
||||
filter 170ms ease,
|
||||
transform 170ms ease;
|
||||
filter: drop-shadow(0 0 22px color-mix(in srgb, var(--agent-glow) 32%, transparent));
|
||||
}
|
||||
|
||||
.agentFocusSurface.preview,
|
||||
.agentFocusSurface.locked {
|
||||
opacity: 1;
|
||||
transform: translate(var(--focus-x, -12%), var(--focus-y, -8%)) scale(1);
|
||||
}
|
||||
|
||||
.agentFocusSurface.anchorLeft {
|
||||
--focus-x: -92%;
|
||||
}
|
||||
|
||||
.agentFocusSurface.anchorRight {
|
||||
--focus-x: 10%;
|
||||
}
|
||||
|
||||
.agentFocusSurface.anchorCenter {
|
||||
--focus-x: -48%;
|
||||
}
|
||||
|
||||
.agentFocusSurface.anchorUp {
|
||||
--focus-y: -86%;
|
||||
}
|
||||
|
||||
.agentFocusSurface.anchorDown {
|
||||
--focus-y: -10%;
|
||||
}
|
||||
|
||||
.agentFocusSurface .immersiveTopbar,
|
||||
.agentFocusSurface .deckRail,
|
||||
.agentFocusSurface .diegeticPanel,
|
||||
.agentFocusSurface .hudToastStack {
|
||||
position: relative;
|
||||
top: auto;
|
||||
right: auto;
|
||||
bottom: auto;
|
||||
left: auto;
|
||||
z-index: auto;
|
||||
}
|
||||
|
||||
.agentFocusSurface .immersiveTopbar {
|
||||
grid-area: top;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
padding: 9px 10px;
|
||||
border: 1px solid rgba(219, 238, 216, 0.28);
|
||||
border-radius: 10px;
|
||||
background: rgba(8, 17, 14, 0.74);
|
||||
backdrop-filter: blur(16px);
|
||||
}
|
||||
|
||||
.agentFocusSurface .deckRail {
|
||||
grid-area: rail;
|
||||
width: auto;
|
||||
align-content: start;
|
||||
}
|
||||
|
||||
.agentFocusSurface .diegeticPanel {
|
||||
grid-area: panel;
|
||||
width: min(680px, 44vw);
|
||||
min-width: 360px;
|
||||
max-height: min(66vh, 700px);
|
||||
}
|
||||
|
||||
.agentFocusSurface .hudToastStack {
|
||||
grid-area: toast;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.agentFocusSurface .operatorIdentity {
|
||||
max-width: min(420px, 34vw);
|
||||
}
|
||||
|
||||
.agentFocusSurface .operatorTopControls {
|
||||
max-width: min(360px, 32vw);
|
||||
}
|
||||
|
||||
.agentFocusSurface .deckRail button {
|
||||
min-height: 60px;
|
||||
}
|
||||
|
||||
.agentFocusSurface .deckBody {
|
||||
max-height: calc(min(66vh, 700px) - 68px);
|
||||
}
|
||||
|
||||
.immersiveTopbar {
|
||||
top: 18px;
|
||||
right: 24px;
|
||||
|
|
@ -1087,17 +1181,29 @@ body:has(.immersiveShell) {
|
|||
color: #172018;
|
||||
background: rgba(249, 252, 246, 0.88);
|
||||
box-shadow: 0 14px 30px rgba(6, 11, 9, 0.26);
|
||||
opacity: var(--agent-bubble-opacity, 0.62);
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transform:
|
||||
translate(-10%, -142%)
|
||||
rotate(var(--agent-bubble-tilt, 0deg))
|
||||
scale(var(--agent-scale, 1));
|
||||
scale(calc(var(--agent-scale, 1) * 0.96));
|
||||
animation: thoughtDrift 7600ms ease-in-out infinite;
|
||||
transition:
|
||||
opacity 160ms ease,
|
||||
transform 160ms ease;
|
||||
}
|
||||
|
||||
.thoughtBubble.selected,
|
||||
.thoughtBubble[data-agent-hot] {
|
||||
z-index: 4;
|
||||
opacity: var(--agent-bubble-opacity, 0.62);
|
||||
transform:
|
||||
translate(-10%, -142%)
|
||||
rotate(var(--agent-bubble-tilt, 0deg))
|
||||
scale(var(--agent-scale, 1));
|
||||
}
|
||||
|
||||
.thoughtBubble.selected {
|
||||
z-index: 4;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
|
|
|
|||
Reference in a new issue