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
345 changes: 243 additions & 102 deletions CHANGELOG.md

Large diffs are not rendered by default.

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