- Remove the preset document suite (letters, emails, invoices, quotes, PDF generation, line items) with its routes, API handlers and tables - Rebrand to Cobble: site data, Lato, handoff design tokens (light and dark) in main.css, brand assets in public/brand, ember stone logo and regenerated favicons - Add bots, bot_messages, bot_tasks and bot_task_steps tables, regenerate DB types (BUN_SQLITE_COBBLE_*) and add a sample crew seed (seed:bots) - Add the chat shell at /admin/bots: rail with pinned coordinator and user popover, bottom-anchored conversation with composer, working-on panel, and a send-message API scoped to the session user - Add BotStone and Wordmark components, crew status and relative time utils with tests, and update AGENTS.md Co-Authored-By: Claude Opus 5.5 <[email protected]>
79 lines
2.0 KiB
TypeScript
79 lines
2.0 KiB
TypeScript
import type { BotFace, BotKind } from "@/src/dict/bot-dict";
|
|
|
|
type CrewBot = {
|
|
kind?: BotKind | string | null;
|
|
face?: BotFace | string | null;
|
|
};
|
|
|
|
type Params = {
|
|
bots: CrewBot[];
|
|
};
|
|
|
|
const NUMBER_WORDS = [
|
|
"No",
|
|
"One",
|
|
"Two",
|
|
"Three",
|
|
"Four",
|
|
"Five",
|
|
"Six",
|
|
"Seven",
|
|
"Eight",
|
|
"Nine",
|
|
"Ten",
|
|
] as const;
|
|
|
|
function countWord({ n }: { n: number }): string {
|
|
return NUMBER_WORDS[n] ?? String(n);
|
|
}
|
|
|
|
/**
|
|
* Working and needs-you counts across the workers (the coordinator excluded).
|
|
*/
|
|
export function crewCounts({ bots }: Params) {
|
|
const workers = bots.filter((bot) => bot.kind !== "coordinator");
|
|
|
|
return {
|
|
working: workers.filter((bot) => bot.face === "working").length,
|
|
needs_you: workers.filter((bot) => bot.face === "needs-you").length,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* The coordinator's rail subtitle: `3 working · 1 needs you`.
|
|
*/
|
|
export function crewStatusLine({ bots }: Params): string {
|
|
const { working, needs_you } = crewCounts({ bots });
|
|
const parts: string[] = [];
|
|
|
|
if (working) parts.push(`${working} working`);
|
|
if (needs_you) parts.push(`${needs_you} need${needs_you == 1 ? "s" : ""} you`);
|
|
|
|
return parts.length ? parts.join(" · ") : "All quiet";
|
|
}
|
|
|
|
/**
|
|
* The signature status sentence: `Three bots are working. One needs you.`
|
|
*/
|
|
export function crewStatusSentence({ bots }: Params): string {
|
|
const { working, needs_you } = crewCounts({ bots });
|
|
const sentences: string[] = [];
|
|
|
|
if (working) {
|
|
sentences.push(
|
|
working == 1
|
|
? `One bot is working.`
|
|
: `${countWord({ n: working })} bots are working.`,
|
|
);
|
|
}
|
|
|
|
if (needs_you) {
|
|
const subject = working
|
|
? countWord({ n: needs_you })
|
|
: `${countWord({ n: needs_you })} bot${needs_you == 1 ? "" : "s"}`;
|
|
sentences.push(`${subject} need${needs_you == 1 ? "s" : ""} you.`);
|
|
}
|
|
|
|
return sentences.length ? sentences.join(" ") : "Nothing needs you.";
|
|
}
|