evOo-local/apps/web/src/lib/voice-handoff.ts
cynarAI 6c7eed7580 Initial clean evOo-local from f00577b
Fresh history cut from cynarAI/evoo antigravity-handoff/project-timeline
(Voice First + project timeline). Brand docs/titles as evOo; keep @rakazo/*
packages as tech debt. Excludes node_modules, caches, secrets, voice blobs.
2026-09-20 17:15:16 +02:00

74 lines
2.4 KiB
TypeScript

import type { ThreadSnapshot } from "@rakazo/contracts";
import type { BotPersonaProfile } from "./bot-persona";
/** UX/UI/product-design specialists — client workshop Voice flow, not executive handoff. */
export function isClientWorkshopSpecialty(input: {
name: string;
title: string;
description: string;
}): boolean {
const haystack = `${input.name} ${input.title} ${input.description}`.toLowerCase();
return (
haystack.includes("ux designer") ||
haystack.includes("ux/ui") ||
haystack.includes("ui designer") ||
haystack.includes("product design") ||
haystack.includes("voice first") ||
(haystack.includes("design") &&
(haystack.includes("lead") || haystack.includes("product") || haystack.includes("ux")))
);
}
export interface OutgoingHandoff {
messageId: string;
toBotId: string;
toBotName?: string;
text: string;
}
/** Most recent handoff this bot initiated (fromBotId === botId). */
export function extractOutgoingHandoff(
snapshot: ThreadSnapshot | null,
botId: string,
): OutgoingHandoff | null {
if (!snapshot) return null;
const members = snapshot.members ?? [];
for (const message of [...snapshot.messages].reverse()) {
for (let index = message.blocks.length - 1; index >= 0; index -= 1) {
const block = message.blocks[index];
if (block?.kind !== "handoff" || block.fromBotId !== botId) continue;
const member = members.find((entry) => entry.botId === block.toBotId);
return {
messageId: message.id,
toBotId: block.toBotId,
toBotName: member?.name,
text: block.text.trim(),
};
}
}
return null;
}
/** Short spoken line when Voice opens on a bot that just handed work off — no task brief. */
export function voiceHandoffTransferLine(
profile: BotPersonaProfile,
targetName: string | undefined,
): string {
const target = targetName?.trim() || "a specialist";
switch (profile.archetype) {
case "cto-executive":
return `This goes to ${target} now. Hang up to continue in text.`;
case "chief-operator":
return `Transferred to ${target}. Hang up to continue in text.`;
default:
return `Handed to ${target}. Hang up to continue in text.`;
}
}
export function voiceHandoffIdleCaption(targetName: string | undefined): string {
const target = targetName?.trim();
return target
? `Handed to ${target} — hang up to continue in text.`
: `Handed to a specialist — hang up to continue in text.`;
}