Skip to content
Draft
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
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,19 @@ Newest first. `Unreleased` is what is on `main` and not yet tagged.

## Unreleased

- Every Bot is now a kite, OpenBot's mascot, instead of an orb. A kite rests with no face and shows
pixel eyes while it is in touch with you. A small square beside it says what it is doing: blue
while it listens, works or answers, amber while it waits on you, green when it is done, red when
a task failed. The Bots list and a Bot's page draw each Bot that way: waiting while it has a
question, an approval or a stalled hand-off for you, red with a failed task, dimmed with no face
while it is paused, and green with a reply to read. A conversation's kite works while the Bot has
the turn, and a busy conversation in the sidebar shows it in the kite rather than three dots.
Each Bot keeps its orb's place in the palette, now the brand's eight colours, so most stay in
their colour family (the teal slot becomes graphite, the sky slot teal), and gets a sail pattern
from the same seed. OpenBot's own mark (sign-in, onboarding, the New Bot sheet, the skins'
frames, OpenBot for Business) is the violet kite. A kite holds still under reduced motion and at 32 px and under, where only its
square moves, shows no face under 22 px, and uses each colour's lighter twin in dark mode. A skin
that set `botLook: "orb"` sets `botLook: "kite"`.
- OpenBot Desktop's menu bar and tray no longer have an item that opens the back end's own
dashboard, which is branded as another product. The Status window and Settings, This computer
list each part by what it does: "Front end and Bots", "Background jobs", "Database" and "Bot
Expand Down
Binary file removed app/src/assets/openbot-logo.png
Binary file not shown.
14 changes: 10 additions & 4 deletions app/src/components/agents/abstract-avatar.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,25 @@
import Avatar from "boring-avatars";
import { useBotLook } from "@/components/agents/bot-look";
import { BotOrb } from "@/components/agents/bot-orb";
import { BotKite } from "@/components/agents/kite/kite";
import type { KiteState } from "@/components/agents/kite/kite-draw";

/** A Bot's avatar: its orb, or its face in a skin that has not opted in to orbs. */
/**
* A Bot's avatar: its kite, or its face in a skin that has not opted in to kites. `state` is what
* the kite shows; a face has no states.
*/
export function AbstractAvatar({
name,
seed,
size = 40,
state = "idle",
}: {
name: string;
seed: string;
size?: number;
state?: KiteState;
}) {
if (useBotLook() === "orb") {
return <BotOrb name={name} seed={seed} size={size} />;
if (useBotLook() === "kite") {
return <BotKite name={name} seed={seed} size={size} state={state} />;
}

return (
Expand Down
6 changes: 3 additions & 3 deletions app/src/components/agents/bot-look.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { createContext, useContext } from "react";

/** How a Bot is drawn: as its orb, or as the generated face. */
export type BotLook = "orb" | "face";
/** How a Bot is drawn: as its kite, OpenBot's mascot, or as the generated face. */
export type BotLook = "kite" | "face";

/*
* Set once, from the active skin's `botLook` (see `_authed.tsx`), so a component asks how to draw a
* Bot rather than which skin is on. A skin opts in to orbs by setting `botLook: "orb"`; without a
* Bot rather than which skin is on. A skin opts in to kites by setting `botLook: "kite"`; without a
* provider, as in a component test, Bots are faces.
*/
const BotLookContext = createContext<BotLook>("face");
Expand Down
82 changes: 0 additions & 82 deletions app/src/components/agents/bot-orb.tsx

This file was deleted.

62 changes: 62 additions & 0 deletions app/src/components/agents/kite/frame-clock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/**
* One animation loop for every Bot on the page, so a list of fifty costs one frame request rather
* than fifty. It starts with its first subscriber, stops with its last, and asks for no frame while
* the tab is hidden.
*/

type FrameCallback = (now: number) => void;

const subscribers = new Set<FrameCallback>();
let frame: number | null = null;
/** The document whose visibility the clock follows, while it has subscribers. */
let watched: Document | null = null;

function run(now: number) {
frame = null;
// A Set visits what is added during the loop and skips what is removed, so a Bot that unmounts
// mid-frame is not drawn after it has gone. One that throws is reported and skipped: it must not
// stop every other Bot on the page, or the frames after this one.
for (const callback of subscribers) {
try {
callback(now);
} catch (error) {
console.error(error);
}
}
schedule();
}

function schedule() {
if (frame !== null || subscribers.size === 0 || document.hidden) return;
frame = window.requestAnimationFrame(run);
}

function halt() {
if (frame !== null) window.cancelAnimationFrame(frame);
frame = null;
}

/** A hidden tab draws nothing, and picks up where its Bots are now once it is shown. */
function onVisibility() {
if (document.hidden) halt();
else schedule();
}

/**
* Calls `callback` once every animation frame, with the frame's timestamp, until the returned
* function is called.
*/
export function subscribeFrame(callback: FrameCallback): () => void {
subscribers.add(callback);
if (!watched) {
watched = document;
watched.addEventListener("visibilitychange", onVisibility);
}
schedule();
return () => {
if (!subscribers.delete(callback) || subscribers.size > 0) return;
halt();
watched?.removeEventListener("visibilitychange", onVisibility);
watched = null;
};
}
Loading
Loading