Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 8 additions & 12 deletions app/src/features/browser/BrowserPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,11 @@ import { WindowBar } from "./WindowBar";
const PAD = 12;
/**
* The bot's color around the page, in px: on the sides and top, and below,
* where the pill that says who is in control hangs over its edge.
* where the pill that says who is in control hangs over its edge. The pill
* reaches into it less than this, so it never covers the page.
*/
const STAGE = 10;
const STAGE_BOTTOM = 26;
/** Kept under the page, for the pill and what the bot just did. */
const UNDER = 56;

export function BrowserPanel({
bot,
Expand All @@ -63,13 +62,9 @@ export function BrowserPanel({
const [focused, setFocused] = useState(false);
const ask = state?.ask ?? null;
const windowed = state?.window === true;
// The page takes the shape of the room the panel has for it (spec 21.3).
// Notices that come and go are not counted: they never resize the page.
const [inside, body] = useRoom();
const room = inside && {
width: inside.width - 2 * PAD - 2 * STAGE,
height: inside.height - 2 * PAD - STAGE - STAGE_BOTTOM - UNDER,
};
// The page takes the shape of the room it has on the desk (spec 21.3), so
// it fills it at its own size, with no bands of the desk beside it.
const [room, stage] = useRoom();
useFitPage(bot, room, watched);
// A new tab is blank: the owner says where it goes.
const address = useRef<HTMLInputElement>(null);
Expand Down Expand Up @@ -126,7 +121,6 @@ export function BrowserPanel({
</>
)}
<div
ref={body}
className="@container flex min-h-0 flex-1 flex-col overflow-y-auto"
style={{ padding: PAD }}
>
Expand Down Expand Up @@ -159,6 +153,7 @@ export function BrowserPanel({
}
>
<div
ref={stage}
className="grid min-h-0 flex-1 place-items-center"
style={{ containerType: "size" }}
>
Expand Down Expand Up @@ -207,14 +202,15 @@ export function BrowserPanel({
)}
</p>
)}
{hands.held && <LessonArea bot={bot} lesson={lesson} />}
{hands.held && <LessonArea lesson={lesson} />}
{hands.held && lesson.steps === null && (
<HeldNotes
bot={bot}
task={ask}
tabs={state?.tabs.length ?? 0}
focused={focused}
hands={hands}
onTeach={() => void lesson.start()}
/>
)}
{live && !hands.held && !ask && <TakeNotes bot={bot} hands={hands} />}
Expand Down
69 changes: 52 additions & 17 deletions app/src/features/browser/HandsBars.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
// refuse to sign in here (spec 21.11).

import { Check, ChevronDown, GraduationCap, Hand, Keyboard, MousePointerClick } from "lucide-react";
import { useState } from "react";
import { type ReactNode, useState } from "react";
import { useT } from "../../i18n";
import type { Bot } from "../../lib/protocol.gen";
import { Button } from "../../ui/Button";
Expand Down Expand Up @@ -91,14 +91,25 @@ export function ControlPill({

/**
* The window button, and why to take the browser or use a window, folded:
* an owner who knows it does not read it every time.
* an owner who knows it does not read it every time. One row, so the page
* above keeps the room.
*/
function WindowAndWhy({ hands, why }: { hands: Hands; why: string }) {
function WindowAndWhy({
hands,
why,
children,
}: {
hands: Hands;
why: ReactNode;
/** More buttons, before the window's. */
children?: ReactNode;
}) {
const t = useT().browser.hands;
const [open, setOpen] = useState(false);
return (
<>
<div className="flex flex-wrap items-center justify-center gap-x-2 gap-y-1">
{children}
<WindowButton hands={hands} />
<button
type="button"
Expand All @@ -114,7 +125,9 @@ function WindowAndWhy({ hands, why }: { hands: Hands; why: string }) {
/>
</button>
</div>
{open && <p className="max-w-md animate-rise text-muted text-xs">{why}</p>}
{open && (
<div className="flex max-w-md animate-rise flex-col gap-1 text-muted text-xs">{why}</div>
)}
</>
);
}
Expand All @@ -124,41 +137,63 @@ export function TakeNotes({ bot, hands }: { bot: Bot; hands: Hands }) {
const t = useT().browser;
return (
<div className="mt-2 flex flex-col items-center gap-2 text-center">
<WindowAndWhy hands={hands} why={`${t.hands.takeWhy(bot.name)} ${t.window.why(bot.name)}`} />
<WindowAndWhy
hands={hands}
why={<p>{`${t.hands.takeWhy(bot.name)} ${t.window.why(bot.name)}`}</p>}
/>
</div>
);
}

/** Under the pill, while the owner has the browser. */
/**
* Under the pill, while the owner has the browser: what the bot asked, and
* one row of buttons, with the rest folded, so the page keeps the room.
*/
export function HeldNotes({
bot,
task,
tabs,
focused,
hands,
onTeach,
}: {
bot: Bot;
task: string | null;
/** How many tabs are open: with more than one, where the bot goes on. */
tabs: number;
focused: boolean;
hands: Hands;
/** Starts a lesson (spec 21.13). */
onTeach(): void;
}) {
const t = useT().browser;
const Hint = focused ? Keyboard : MousePointerClick;
return (
<div className="mt-2 flex flex-col items-center gap-1.5 text-center text-xs">
<p className="flex items-center gap-1.5 font-medium text-ink-soft">
<Hint aria-hidden size={12} className="shrink-0" />
{focused ? t.hands.typing : t.hands.clickToType}
</p>
<p className="line-clamp-2 max-w-md text-ink-soft" data-selectable>
{task ? t.help.asks(bot.name, task) : t.hands.waits(bot.name)}
</p>
{tabs > 1 && <p className="text-muted">{t.hands.leaves(bot.name)}</p>}
<div className="mt-1 flex flex-col items-center gap-2">
<WindowAndWhy hands={hands} why={t.window.hint} />
</div>
{task && (
<p className="line-clamp-1 max-w-md text-ink-soft" data-selectable>
{t.help.asks(bot.name, task)}
</p>
)}
<WindowAndWhy
hands={hands}
why={
<>
<p className="flex items-center justify-center gap-1.5">
<Hint aria-hidden size={12} className="shrink-0" />
{focused ? t.hands.typing : t.hands.clickToType}
</p>
{!task && <p>{t.hands.waits(bot.name)}</p>}
{tabs > 1 && <p>{t.hands.leaves(bot.name)}</p>}
<p>{t.lesson.teachWhy(bot.name)}</p>
<p>{t.window.hint}</p>
</>
}
>
<Button size="sm" icon={GraduationCap} onClick={onTeach}>
{t.lesson.teach}
</Button>
</WindowAndWhy>
</div>
);
}
20 changes: 7 additions & 13 deletions app/src/features/browser/Lesson.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
// end they name the task and make it a routine, or send it for the bot to
// remember.

import { AlarmClock, GraduationCap, Send, X } from "lucide-react";
import { AlarmClock, Send, X } from "lucide-react";
import { type FormEvent, useState } from "react";
import { useT } from "../../i18n";
import type { Bot, BrowserState, LessonStep } from "../../lib/protocol.gen";
Expand Down Expand Up @@ -57,18 +57,11 @@ export function useLesson(bot: Pick<Bot, "id">, state: BrowserState | null): Les
};
}

/** Under the owner's pill: start a lesson, or the one being given, live. */
export function LessonArea({ bot, lesson }: { bot: Bot; lesson: Lesson }) {
/** Under the owner's pill: the lesson being given, live. */
export function LessonArea({ lesson }: { lesson: Lesson }) {
const t = useT().browser.lesson;
if (lesson.steps === null) {
return (
<div className="mt-2 flex flex-col items-center gap-1 text-center">
<Button size="sm" icon={GraduationCap} onClick={() => void lesson.start()}>
{t.teach}
</Button>
<p className="max-w-md text-muted text-xs">{t.teachWhy(bot.name)}</p>
</div>
);
return null;
}
return (
<section
Expand All @@ -79,10 +72,11 @@ export function LessonArea({ bot, lesson }: { bot: Bot; lesson: Lesson }) {
<span aria-hidden className="live-dot" />
{t.recording}
</p>
{/* A fixed height: steps coming in never resize the page above. */}
{lesson.steps.length === 0 ? (
<p className="text-muted text-xs">{t.empty}</p>
<p className="h-16 text-muted text-xs">{t.empty}</p>
) : (
<ol className="flex max-h-40 flex-col gap-1 overflow-y-auto text-ink-soft text-xs">
<ol className="flex h-16 flex-col gap-1 overflow-y-auto text-ink-soft text-xs">
{lesson.steps.map((step, index) => (
// biome-ignore lint/suspicious/noArrayIndexKey: steps only grow at the end
<li key={index} className="animate-rise">
Expand Down
12 changes: 6 additions & 6 deletions app/src/features/browser/browser.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,28 +71,28 @@ describe("browser panel", () => {
expect(within(panel()).queryByText("Opened elsewhere.com")).toBeNull();
});

test("gives the page the shape of the room the panel has, and shows it as large as fits", async () => {
test("gives the page the shape of the room on the desk, and shows it as large as fits", async () => {
const { fake, scout } = crew();
fake.browser.open(scout.id, "https://example.com/", "Example");
// The panel under its address row, as the app measures it: 516 by 688
// for the page, once what goes around and under it is set aside.
// The room on the desk, inside its edge, as the app measures it: all
// of it is the page's, so no band of the desk shows beside it.
vi.spyOn(Element.prototype, "clientWidth", "get").mockReturnValue(560);
vi.spyOn(Element.prototype, "clientHeight", "get").mockReturnValue(804);
await openScout(fake);
fireEvent.click(toggle());
const resized = () =>
fake.calls.filter((call) => call.method === "browser.resize").map((call) => call.params);
await waitFor(() => expect(resized()).toEqual([{ botId: scout.id, width: 516, height: 688 }]));
await waitFor(() => expect(resized()).toEqual([{ botId: scout.id, width: 560, height: 804 }]));
// A narrow room keeps the desktop layout's width, in the room's shape.
expect(fake.browser.size(scout.id)).toEqual({ width: 800, height: 1066 });
expect(fake.browser.size(scout.id)).toEqual({ width: 800, height: 1148 });

act(() => {
fake.browser.frame(scout.id, "AAAA");
});
// The bot's cursor lands where it acted on the taller page.
const screenshot = within(panel()).getByRole("figure", { name: "What Scout sees" });
act(() => {
fake.browser.act(scout.id, "click", { x: 400, y: 1066, label: "More" });
fake.browser.act(scout.id, "click", { x: 400, y: 1148, label: "More" });
});
const cursor = screenshot.querySelector(".bot-cursor") as HTMLElement;
expect(cursor.style.top).toBe("100%");
Expand Down
2 changes: 2 additions & 0 deletions app/src/features/browser/tabs.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@ describe("the tabs of the bot's browser", () => {
await openPanel(fake, scout.id);
fireEvent.click(within(panel()).getByRole("button", { name: "Take control" }));
expect(await within(panel()).findByText("You are in control")).toBeDefined();
// Folded with the rest of what the owner should know.
fireEvent.click(within(panel()).getByRole("button", { name: "How it works" }));
expect(within(panel()).getByText("Scout goes on in the tab you leave open.")).toBeDefined();

fireEvent.click(tab("Rye · Example, example.com"));
Expand Down
Loading
Loading