* feat(web): Ingo UX — proactivity signals, Oo face richness, soft audio Priority S: Heute Brief buckets (Needs you ≤3 / Doing / Done), roster doing-verbs and Braucht-dich chips, sidebar badge on Heute, need-you ping on waiting states. Priority A/B: Web Audio cues (app-start shimmer, interact tick, success-resolve, need-you ouh), reaction vocals with cooldown, optional eOo cold-open voice mark. Oo eyes gain iris/specular detail and asymmetric blink; hands reduced to rest except punctuation poses; speaking mouth animates. Co-authored-by: cynarAI <cynarAI@users.noreply.github.com> * fix(web): biome format and unused import in HeuteBrief Co-authored-by: cynarAI <cynarAI@users.noreply.github.com> * fix(web): a11y and import order for Heute badge Co-authored-by: cynarAI <cynarAI@users.noreply.github.com> * docs+motion: Motion God craft — pose stagger, audio bus, ducking Add MOTION-EVOO-CHARACTER.md (Brand Soul brief): eyes lead, mouth ~80%, hands punctuation, 40–80 ms eye→mouth lag, 220–320 ms springs, SFX LUFS and duck-under-TTS/PTT, nod mh/Mhh, reduced-motion rules. Implement motion-tokens, staggered mouthPose in EvooMascot, shared oo-audio-bus with voice-lane ducking, mint→gold success, hm nod vocal. Co-authored-by: cynarAI <cynarAI@users.noreply.github.com> * fix(web): align conversation roster with bot-only sidebar groups after rebase Co-authored-by: cynarAI <cynarAI@users.noreply.github.com> --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>
138 lines
3.6 KiB
TypeScript
138 lines
3.6 KiB
TypeScript
import type { Bot, Group, RunActivityRow } from "@rakazo/contracts";
|
|
import { describe, expect, it } from "vitest";
|
|
import { buildHeuteBrief, HEUTE_NEEDS_YOU_CAP, isToday } from "./heute-brief";
|
|
|
|
const now = new Date("2026-09-22T12:00:00.000Z");
|
|
|
|
function bot(overrides: Partial<Bot> = {}): Bot {
|
|
return {
|
|
id: "bot-1",
|
|
spaceId: "space-1",
|
|
name: "Chief",
|
|
title: "",
|
|
description: "",
|
|
instructions: "",
|
|
color: "#000",
|
|
notifyOnFinish: false,
|
|
pinned: false,
|
|
sectionId: null,
|
|
archivedAt: null,
|
|
unread: false,
|
|
parentBotId: null,
|
|
memoryScope: null,
|
|
threadId: "thread-1",
|
|
preview: "",
|
|
status: "idle",
|
|
computerMode: "team",
|
|
updatedAt: "2026-09-22T10:00:00.000Z",
|
|
createdAt: "2026-09-01T00:00:00.000Z",
|
|
voiceId: null,
|
|
autoSpeak: false,
|
|
modelProvider: null,
|
|
modelId: null,
|
|
thinkingLevel: null,
|
|
teamChatAmbientEnabled: false,
|
|
teamChatRules: "",
|
|
webhookConfigured: false,
|
|
spawnKey: null,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function group(overrides: Partial<Group> = {}): Group {
|
|
return {
|
|
id: "group-1",
|
|
spaceId: "space-1",
|
|
name: "Launch",
|
|
pinned: false,
|
|
sectionId: null,
|
|
archivedAt: null,
|
|
members: [],
|
|
threadId: "thread-g-1",
|
|
preview: "",
|
|
unread: false,
|
|
updatedAt: "2026-09-22T09:00:00.000Z",
|
|
createdAt: "2026-09-01T00:00:00.000Z",
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function run(overrides: Partial<RunActivityRow> = {}): RunActivityRow {
|
|
return {
|
|
runId: "run-1",
|
|
botId: "bot-1",
|
|
botName: "Chief",
|
|
groupId: null,
|
|
groupName: null,
|
|
threadId: "thread-1",
|
|
status: "waiting_input",
|
|
trigger: "user",
|
|
notificationsEnabled: true,
|
|
promptSnippet: "Need approval",
|
|
updatedAt: "2026-09-22T11:00:00.000Z",
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
describe("isToday", () => {
|
|
it("matches same calendar day", () => {
|
|
expect(isToday("2026-09-22T08:30:00.000Z", now)).toBe(true);
|
|
expect(isToday("2026-09-21T23:59:00.000Z", now)).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("buildHeuteBrief", () => {
|
|
it("collects needs-you, doing, and done buckets", () => {
|
|
const brief = buildHeuteBrief({
|
|
bots: [
|
|
bot({ id: "bot-1", unread: true }),
|
|
bot({ id: "bot-2", unread: true, updatedAt: "2026-09-22T08:00:00.000Z", name: "Steve" }),
|
|
],
|
|
groups: [group({ updatedAt: "2026-09-22T09:30:00.000Z" })],
|
|
activeRuns: [
|
|
run({ runId: "run-1", botId: "bot-1" }),
|
|
run({ runId: "run-3", botId: "bot-4", botName: "Worker", status: "running" }),
|
|
],
|
|
recentRuns: [
|
|
run({
|
|
runId: "run-2",
|
|
botId: "bot-3",
|
|
botName: "Ops",
|
|
status: "failed",
|
|
trigger: "routine",
|
|
}),
|
|
run({
|
|
runId: "run-4",
|
|
botId: "bot-5",
|
|
botName: "DoneBot",
|
|
status: "completed",
|
|
updatedAt: "2026-09-22T09:00:00.000Z",
|
|
}),
|
|
],
|
|
now,
|
|
});
|
|
|
|
expect(brief.actionCount).toBe(3);
|
|
expect(brief.needsYou.map((item) => item.kind)).toEqual(["run", "run", "unread"]);
|
|
expect(brief.doing).toHaveLength(1);
|
|
expect(brief.doing[0]?.run.runId).toBe("run-3");
|
|
expect(brief.done).toHaveLength(1);
|
|
expect(brief.done[0]?.run.runId).toBe("run-4");
|
|
});
|
|
|
|
it("caps needs-you at three items", () => {
|
|
const bots = Array.from({ length: 5 }, (_, index) =>
|
|
bot({ id: `bot-${index}`, unread: true, name: `Bot ${index}` }),
|
|
);
|
|
const brief = buildHeuteBrief({
|
|
bots,
|
|
groups: [],
|
|
activeRuns: [],
|
|
recentRuns: [],
|
|
now,
|
|
});
|
|
expect(brief.needsYou).toHaveLength(HEUTE_NEEDS_YOU_CAP);
|
|
expect(brief.actionCount).toBe(5);
|
|
});
|
|
});
|