* 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>
140 lines
3.8 KiB
TypeScript
140 lines
3.8 KiB
TypeScript
import type { Bot, Group, RunActivityRow } from "@rakazo/contracts";
|
|
import { isActionableRun, isDoingStatus, isFailedRoutine } from "./agent-activity";
|
|
|
|
export type JetztRunItem = {
|
|
kind: "run";
|
|
run: RunActivityRow;
|
|
};
|
|
|
|
export type JetztUnreadItem = {
|
|
kind: "unread";
|
|
chatKind: "bot" | "group";
|
|
chat: Bot | Group;
|
|
};
|
|
|
|
export type JetztItem = JetztRunItem | JetztUnreadItem;
|
|
|
|
export type DoingItem = {
|
|
kind: "run";
|
|
run: RunActivityRow;
|
|
};
|
|
|
|
export type DoneItem = {
|
|
kind: "run";
|
|
run: RunActivityRow;
|
|
};
|
|
|
|
/** @deprecated Use needsYou — kept for gradual migration in tests. */
|
|
export type WeiterItem = {
|
|
chatKind: "bot" | "group";
|
|
chat: Bot | Group;
|
|
};
|
|
|
|
export type HeuteBriefData = {
|
|
/** Needs you — capped at three visible rows. */
|
|
needsYou: JetztItem[];
|
|
/** Active runs where the agent is working, not blocked. */
|
|
doing: DoingItem[];
|
|
/** Recently completed runs from today. */
|
|
done: DoneItem[];
|
|
/** Total actionable count (may exceed visible cap). */
|
|
actionCount: number;
|
|
/** @deprecated Alias for needsYou — legacy bucket name. */
|
|
jetzt: JetztItem[];
|
|
/** @deprecated Empty — replaced by doing/done buckets. */
|
|
weiter: WeiterItem[];
|
|
};
|
|
|
|
export const HEUTE_NEEDS_YOU_CAP = 3;
|
|
|
|
export function isToday(iso: string, now = new Date()): boolean {
|
|
const date = new Date(iso);
|
|
if (Number.isNaN(date.getTime())) return false;
|
|
return (
|
|
date.getDate() === now.getDate() &&
|
|
date.getMonth() === now.getMonth() &&
|
|
date.getFullYear() === now.getFullYear()
|
|
);
|
|
}
|
|
|
|
export function cleanBriefPreview(raw?: string | null): string {
|
|
if (!raw) return "";
|
|
let text = raw;
|
|
text = text.replace(/^#{1,6}\s+/gm, "");
|
|
text = text.replace(/\[(?:to|RECOVERY PROTOCOL|PO)[^\]]*\]/gi, "");
|
|
text = text.replace(/[*_`#]/g, "");
|
|
text = text.replace(/\\/g, "");
|
|
return text.replace(/\s+/g, " ").trim();
|
|
}
|
|
|
|
function runTargetKey(run: RunActivityRow): string {
|
|
return run.groupId ? `group:${run.groupId}` : `bot:${run.botId}`;
|
|
}
|
|
|
|
function chatTargetKey(chatKind: "bot" | "group", chatId: string): string {
|
|
return chatKind === "group" ? `group:${chatId}` : `bot:${chatId}`;
|
|
}
|
|
|
|
export function buildHeuteBrief(params: {
|
|
bots: Bot[];
|
|
groups: Group[];
|
|
activeRuns: RunActivityRow[];
|
|
recentRuns: RunActivityRow[];
|
|
now?: Date;
|
|
}): HeuteBriefData {
|
|
const { bots, groups, activeRuns, recentRuns, now = new Date() } = params;
|
|
const needsYouAll: JetztItem[] = [];
|
|
const coveredTargets = new Set<string>();
|
|
|
|
const actionableRuns = [
|
|
...activeRuns.filter(isActionableRun),
|
|
...recentRuns.filter(isFailedRoutine),
|
|
];
|
|
const seenRunIds = new Set<string>();
|
|
for (const run of actionableRuns) {
|
|
if (seenRunIds.has(run.runId)) continue;
|
|
seenRunIds.add(run.runId);
|
|
needsYouAll.push({ kind: "run", run });
|
|
coveredTargets.add(runTargetKey(run));
|
|
}
|
|
|
|
const unreadChats: JetztUnreadItem[] = [
|
|
...bots
|
|
.filter((bot) => bot.unread)
|
|
.map((bot) => ({ kind: "unread" as const, chatKind: "bot" as const, chat: bot })),
|
|
...groups
|
|
.filter((group) => group.unread)
|
|
.map((group) => ({
|
|
kind: "unread" as const,
|
|
chatKind: "group" as const,
|
|
chat: group,
|
|
})),
|
|
];
|
|
for (const item of unreadChats) {
|
|
const key = chatTargetKey(item.chatKind, item.chat.id);
|
|
if (coveredTargets.has(key)) continue;
|
|
needsYouAll.push(item);
|
|
coveredTargets.add(key);
|
|
}
|
|
|
|
const doing: DoingItem[] = activeRuns
|
|
.filter((run) => isDoingStatus(run.status))
|
|
.map((run) => ({ kind: "run" as const, run }));
|
|
|
|
const done: DoneItem[] = recentRuns
|
|
.filter((run) => run.status === "completed" && isToday(run.updatedAt, now))
|
|
.slice(0, 5)
|
|
.map((run) => ({ kind: "run" as const, run }));
|
|
|
|
const needsYou = needsYouAll.slice(0, HEUTE_NEEDS_YOU_CAP);
|
|
|
|
return {
|
|
needsYou,
|
|
doing,
|
|
done,
|
|
actionCount: needsYouAll.length,
|
|
jetzt: needsYouAll,
|
|
weiter: [],
|
|
};
|
|
}
|