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
7 changes: 4 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -149,13 +149,14 @@ jobs:
# holding still as an update gets ready, Home's message box holding still while the
# microphone's notice shows, a reply staying where it was written once its turn is over,
# every skin's top-left corner clear of the Mac window's buttons, OpenDots' Bots badge on its
# icon's corner inside the rail, and the version in the account row read whole, which only a
# browser's layout and cascade resolve. The
# icon's corner inside the rail, the version in the account row read whole, and a Bot's orb
# one color in the sidebar, on its page and at the top of its conversation, which only a
# browser's layout, cascade and paint resolve. The
# onboarding frame reads the generated app config, which `bun test` alone doesn't write (the
# test script's pretest does).
- run: |
bun run generate:app-config
bun test app/tests/tool-line-reduced-motion.test.tsx app/tests/onboarding-window-buttons.test.tsx app/tests/onboarding-orb-room.test.tsx app/tests/transcript-rail-clearance.test.tsx app/tests/a2ui-frame.test.tsx app/tests/update-banner-layout.test.tsx app/tests/composer-dictation-notice-layout.test.tsx app/tests/conversation-intro-halo.test.tsx app/tests/transcript-room.test.tsx app/tests/skins-window-buttons.test.tsx app/tests/opendots-rail-badge.test.tsx app/tests/account-version-layout.test.tsx
bun test app/tests/tool-line-reduced-motion.test.tsx app/tests/onboarding-window-buttons.test.tsx app/tests/onboarding-orb-room.test.tsx app/tests/transcript-rail-clearance.test.tsx app/tests/a2ui-frame.test.tsx app/tests/update-banner-layout.test.tsx app/tests/composer-dictation-notice-layout.test.tsx app/tests/conversation-intro-halo.test.tsx app/tests/transcript-room.test.tsx app/tests/skins-window-buttons.test.tsx app/tests/opendots-rail-badge.test.tsx app/tests/account-version-layout.test.tsx app/tests/bot-orb-one-color.test.tsx
env:
OPENBOT_APP_BROWSER: '1'
# The desktop setup window's, for the same reason: the AI step's form holding still while the
Expand Down
25 changes: 8 additions & 17 deletions app/src/components/agents/bot-orb.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,16 @@ import { orbPalette } from "@/lib/orb-palette";
/** At this size and smaller, an idle orb is drawn still. */
const STATIC_MAX_SIZE = 32;

/*
* The still orb: the live orb's base sphere (a lit radial in the palette's own stops) with one
* conic sweep over it for the mesh, and a soft highlight on top. Drawn from the custom properties
* set on the element, so the fill is written once and the palette is the only thing that varies.
*/
const STATIC_FILL = [
"radial-gradient(circle at 30% 24%, hsl(0 0% 100% / 0.28), transparent 34%)",
"conic-gradient(from 200deg at 70% 30%, var(--c1), transparent 30% 65%, var(--c1))",
"radial-gradient(circle at 38% 34%, var(--c4) 0%, var(--c2) 42%, var(--c3) 100%)",
].join(", ");

/**
* A Bot drawn as its orb, in the palette its seed picks (`orbPalette`), so renaming a Bot keeps
* its color.
*
* At avatar sizes an idle Bot gets a still orb drawn with CSS alone: twenty animated orbs in a
* sidebar would each run a blur and a backdrop filter on every frame. A larger orb, or one whose
* Bot is working, is the live `AgentOrb`. Neither moves under reduced motion: `AgentOrb` honours
* it, and the still orb has nothing to move.
* One sphere at every size (`.bot-orb-still` and `.bot-orb-live` in styles.css), so a Bot is the
* same color in the sidebar, at the top of its page and at the top of a conversation. At avatar
* sizes an idle Bot's sphere is drawn still, with CSS alone: twenty animated orbs in a sidebar would
* each run a blur and a backdrop filter on every frame. A larger orb, or one whose Bot is working,
* is the live `AgentOrb` drawing the same sphere, in the palette as it is in every state. Neither
* moves under reduced motion: `AgentOrb` honours it, and the still orb has nothing to move.
*/
export function BotOrb({
seed,
Expand All @@ -44,14 +35,13 @@ export function BotOrb({
role="img"
aria-label={name}
data-variant="static"
className="inline-block shrink-0 rounded-full ring-1 ring-foreground/10"
className="bot-orb-still inline-block shrink-0 rounded-full ring-1 ring-foreground/10"
style={
{
"--c1": colors.c1,
"--c2": colors.c2,
"--c3": colors.c3,
"--c4": colors.c4,
background: STATIC_FILL,
height: size,
width: size,
} as CSSProperties
Expand All @@ -77,6 +67,7 @@ export function BotOrb({
colors={colors}
size={`${size}px`}
state={state === "working" ? "thinking" : "idle"}
trueColors
/>
</span>
</span>
Expand Down
11 changes: 10 additions & 1 deletion app/src/components/agents/orb/agent-orb.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,12 @@ export interface AgentOrbProps {
* scrolled off screen or in a hidden tab, so a loop nobody watches does not keep running.
*/
still?: boolean;
/**
* Drawn in `colors` as given, in every state: for a Bot's orb, whose colors are how a person
* tells that Bot from another. The state still sets how it moves, but never mutes it at rest or
* turns its hue while it works.
*/
trueColors?: boolean;
}

const AgentOrb: React.FC<AgentOrbProps> = ({
Expand All @@ -110,6 +116,7 @@ const AgentOrb: React.FC<AgentOrbProps> = ({
amplitude,
state = "idle",
still = false,
trueColors = false,
}) => {
const shouldReduceMotion = useReducedMotion() || still;
const amplitudeValue = useAmplitudeValue(amplitude);
Expand Down Expand Up @@ -293,7 +300,9 @@ const AgentOrb: React.FC<AgentOrbProps> = ({
// Saturation and hue only affect the gradient disc. Applying them
// to the whole component would desaturate the semantic accent too,
// and `error` would lose the very red that identifies it.
filter: `saturate(${stateMotion.saturation}) hue-rotate(${stateMotion.hueRotate}deg)`,
filter: trueColors
? undefined
: `saturate(${stateMotion.saturation}) hue-rotate(${stateMotion.hueRotate}deg)`,
height: "100%",
scale: reactiveScale,
width: "100%",
Expand Down
42 changes: 28 additions & 14 deletions app/src/components/bot-profile/profile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,33 @@ const NOTIFY_LABEL: Record<BotNotify, string> = {
none: "Nothing (badges only)",
};

/** The top of a Bot's page: its orb, what it is for and what it does. */
export function BotProfileHeader({
agent,
}: {
agent: Pick<
AgentProfile,
"name" | "title" | "roleDescription" | "avatarSeed"
>;
}) {
return (
<div className="mt-6 flex items-center gap-3">
<AbstractAvatar name={agent.name} seed={agent.avatarSeed} size={40} />
<div className="min-w-0">
{/* Up to three lines, so a phone shows the whole of a subtitle like the starter's. */}
{agent.title && (
<p className="line-clamp-3 text-pretty font-medium">
{shownSubtitle(agent.title)}
</p>
)}
<p className="line-clamp-2 text-muted-foreground text-sm">
{agent.roleDescription}
</p>
</div>
</div>
);
}

/**
* A Bot's own page: what it needs from you, its state for you, what it is doing, the tasks you marked
* done this week, its settings, and what can be done to it.
Expand All @@ -78,20 +105,7 @@ export function BotProfile({

return (
<>
<div className="mt-6 flex items-center gap-3">
<AbstractAvatar name={agent.name} seed={agent.avatarSeed} size={40} />
<div className="min-w-0">
{/* Up to three lines, so a phone shows the whole of a subtitle like the starter's. */}
{agent.title && (
<p className="line-clamp-3 text-pretty font-medium">
{shownSubtitle(agent.title)}
</p>
)}
<p className="line-clamp-2 text-muted-foreground text-sm">
{agent.roleDescription}
</p>
</div>
</div>
<BotProfileHeader agent={agent} />
<div className="mt-4">
<BotPausedBanner agentId={agent.id} />
</div>
Expand Down
4 changes: 2 additions & 2 deletions app/src/components/edition/business-orb.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@ export function BusinessOrb({ orb = true }: { orb?: boolean }) {
/>
{orb ? (
<div className="absolute inset-0 flex items-center justify-center">
{/* `.bot-orb-live` gives it the same finish as a Bot's orb in a conversation. */}
{/* `.bot-orb-live` and `trueColors` give it the same finish as a Bot's orb in a conversation. */}
<span className="bot-orb-live inline-flex rounded-full ring-1 ring-foreground/10">
<AgentOrb colors={ORB_COLORS} size="176px" />
<AgentOrb colors={ORB_COLORS} size="176px" trueColors />
</span>
</div>
) : null}
Expand Down
4 changes: 3 additions & 1 deletion app/src/components/onboarding/demo-orb.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import { orbPalette } from "@/lib/orb-palette";
* it, or OpenBot's own mark with no seed.
*
* Always the live `AgentOrb`, because a picture shows a Bot listening, working and done, where
* `BotOrb` only draws idle, working and waiting. Decorative: the picture around it is labelled, so
* `BotOrb` only draws idle, working and waiting. A Bot's keeps its palette in every state, as
* `BotOrb`'s does (`trueColors`). Decorative: the picture around it is labelled, so
* the orb has no name of its own. `still` holds it while its picture is off screen.
*/
export function DemoOrb({
Expand All @@ -34,6 +35,7 @@ export function DemoOrb({
size={`${size}px`}
state={state}
still={still}
trueColors={Boolean(seed)}
/>
</span>
);
Expand Down
65 changes: 49 additions & 16 deletions app/src/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -295,28 +295,61 @@
}

/*
* A Bot's live orb, toned for the app's surfaces. AgentOrb's own contrast boost and full-strength
* halftone wash light colors out to white; these keep each Bot's palette true. Only `BotOrb`
* carries `.bot-orb-live`, so the orb anywhere else is unchanged.
* A Bot's sphere, the one drawing of a Bot at every size, so it reads as the same Bot wherever it
* is: its palette's lightest stop (c4) lit through c2 to c3 at the rim, with one sweep of c1 across
* it. `BotOrb` draws it still at avatar sizes, with CSS alone, and live when larger or busy, where
* the sweep turns. Before, the live orb drew AgentOrb's own mesh instead, in which c3, a violet in
* most palettes, lay across the top: the Personal Assistant was green in the sidebar and blue, teal
* and pink at the top of its page and its conversation (bot-orb-one-color.test.tsx).
*/
.bot-orb-live .agent-orb::before {
filter: blur(var(--blur-amount)) contrast(1.15) saturate(1.3);
}
.bot-orb-live .agent-orb::after {
opacity: 0.25;
backdrop-filter: blur(calc(var(--blur-amount) * 1.5)) contrast(1.25);
.bot-orb-still {
background:
radial-gradient(circle at 30% 24%, hsl(0 0% 100% / 0.28), transparent 34%),
conic-gradient(
from 200deg at 70% 30%,
var(--c1),
transparent 30% 65%,
var(--c1)
),
radial-gradient(
circle at 38% 34%,
var(--c4) 0%,
var(--c2) 42%,
var(--c3) 100%
);
}
/*
* The disc's ground. The mesh is transparent in places, and the orb's ground (`--bg`, the page's
* `--background`) fills the halftone and the inner edge. On the dark page that made a dark hole in
* the bottom of the sphere, with the halo's pattern showing through it. The disc is drawn on the
* light page's `--background` in both themes, so it reads the same on either page and its own
* colors carry it, as the still orb's do. Written here rather than as a token, since theme.css
* holds only shadcn's tokens.
* The live orb's disc: the same sphere as its ground, on the light page's `--background` in both
* themes, so it reads the same on either page (the mesh is clear in places, and on the dark page its
* own `--background` made a dark hole in the bottom of the sphere, with the halo's pattern showing
* through). Written here rather than as a token, since theme.css holds only shadcn's tokens. The
* sheen and the rim are AgentOrb's own, and its halftone is kept faint, since at full strength it
* washes light colors out to white.
*/
.bot-orb-live .agent-orb {
--background: oklch(0.983 0.003 286.4);
background: var(--background);
background-color: var(--background);
background-image: radial-gradient(
circle at 38% 34%,
var(--c4) 0%,
var(--c2) 42%,
var(--c3) 100%
);
}
/* The sweep, turning as AgentOrb turns its mesh, faster while the Bot works. */
.bot-orb-live .agent-orb::before {
background: conic-gradient(
from calc(200deg + var(--angle)) at 70% 30%,
var(--c1),
transparent 30% 65%,
var(--c1)
);
box-shadow: none;
filter: blur(var(--blur-amount));
}
.bot-orb-live .agent-orb::after {
opacity: 0.25;
backdrop-filter: blur(calc(var(--blur-amount) * 1.5)) contrast(1.25);
}

/*
Expand Down
Loading
Loading