feat(studio): add flat footer (ask agent + record) for the inspector

This commit is contained in:
Vance Ingalls
2026-07-09 11:52:12 -07:00
parent 1acb1e63c7
commit 3bb943e96a
2 changed files with 113 additions and 0 deletions
@@ -0,0 +1,55 @@
// @vitest-environment happy-dom
import React, { act } from "react";
import { createRoot } from "react-dom/client";
import { afterEach, describe, expect, it, vi } from "vitest";
import { PropertyPanelFlatFooter } from "./PropertyPanelFlatFooter";
(globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
afterEach(() => {
document.body.innerHTML = "";
});
function renderFooter(overrides: Partial<Parameters<typeof PropertyPanelFlatFooter>[0]> = {}) {
const host = document.createElement("div");
document.body.append(host);
const root = createRoot(host);
act(() => {
root.render(<PropertyPanelFlatFooter {...overrides} />);
});
return { host, root };
}
describe("PropertyPanelFlatFooter", () => {
it("renders the ask-agent affordance and fires onAskAgent on click", () => {
const onAskAgent = vi.fn();
const { host, root } = renderFooter({ onAskAgent });
expect(host.textContent).toContain("Ask agent about this element");
const askButton = host.querySelector<HTMLButtonElement>('[data-flat-footer-ask="true"]');
act(() => askButton?.dispatchEvent(new MouseEvent("click", { bubbles: true })));
expect(onAskAgent).toHaveBeenCalledTimes(1);
act(() => root.unmount());
});
it("shows the idle record affordance and toggles recording on click", () => {
const onToggleRecording = vi.fn();
const { host, root } = renderFooter({ recordingState: "idle", onToggleRecording });
const recordButton = host.querySelector<HTMLButtonElement>('[data-flat-footer-record="true"]');
expect(recordButton?.title).toBe("Record gesture (R)");
act(() => recordButton?.dispatchEvent(new MouseEvent("click", { bubbles: true })));
expect(onToggleRecording).toHaveBeenCalledTimes(1);
act(() => root.unmount());
});
it("shows the recording duration while recording", () => {
const { host, root } = renderFooter({
recordingState: "recording",
recordingDuration: 2.4,
onToggleRecording: vi.fn(),
});
const recordButton = host.querySelector<HTMLButtonElement>('[data-flat-footer-record="true"]');
expect(recordButton?.title).toBe("Stop recording 2.4s");
act(() => root.unmount());
});
});
@@ -0,0 +1,58 @@
export function PropertyPanelFlatFooter({
onAskAgent,
recordingState,
recordingDuration,
onToggleRecording,
}: {
onAskAgent?: () => void;
recordingState?: "idle" | "recording" | "preview";
recordingDuration?: number;
onToggleRecording?: () => void;
}) {
const recording = recordingState === "recording";
const recordTitle = recording
? `Stop recording ${(recordingDuration ?? 0).toFixed(1)}s`
: "Record gesture (R)";
return (
<div className="flex items-center justify-between border-t border-panel-hairline px-4 py-[11px]">
<button
type="button"
data-flat-footer-ask="true"
onClick={onAskAgent}
disabled={!onAskAgent}
className="flex items-center gap-[7px] text-[11px] font-medium text-panel-text-2 disabled:cursor-not-allowed"
>
<svg
width="13"
height="13"
viewBox="0 0 16 16"
fill="currentColor"
className="text-panel-accent"
>
<path d="M8 1l1.4 4.6L14 7l-4.6 1.4L8 13l-1.4-4.6L2 7l4.6-1.4z" />
</svg>
Ask agent about this element
</button>
{onToggleRecording && (
<button
type="button"
data-flat-footer-record="true"
aria-label={recordTitle}
title={recordTitle}
onMouseDown={(e) => e.preventDefault()}
onClick={onToggleRecording}
className={recording ? "text-panel-danger animate-pulse" : "text-panel-danger"}
>
<svg width="14" height="14" viewBox="0 0 14 14" fill="currentColor">
{recording ? (
<rect x="2" y="2" width="10" height="10" rx="1.5" />
) : (
<circle cx="7" cy="7" r="6" />
)}
</svg>
</button>
)}
</div>
);
}