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.
160 lines
5.6 KiB
TypeScript
160 lines
5.6 KiB
TypeScript
import type { UserProfileSignals } from "./user-profile-harvest";
|
|
|
|
/** Product bar: entry never requires power-user literacy (Ingo — Thermomix mom to hard math). */
|
|
export const SKILL_FLOOR_LABEL = "grandma-simple entry";
|
|
/** Product bar: depth is expert-grade per specialty, not one mediocre generalist. */
|
|
export const SKILL_CEILING_LABEL = "expert-grade per specialty";
|
|
|
|
/** How the user wants to relate to Oos — never a lifestyle label. */
|
|
export type InteractionStyle = "unknown" | "geek-peer" | "quiet-doer";
|
|
|
|
/** Onboarding focus ids from `apps/api/src/onboarding.ts` — maps to crew fit, not persona for everyone. */
|
|
export type OnboardingFocusId = "day" | "inbox" | "research" | "crew";
|
|
|
|
export interface UserHelpVibeHints {
|
|
interactionStyle: InteractionStyle;
|
|
/** Specialist lanes to consider when spawning — suggestions only, not assignments. */
|
|
suggestedLanes: string[];
|
|
/** Honest observed signals — no invented hobbies or scene/club/office guesses. */
|
|
observedSignals: string[];
|
|
focusChoiceId?: OnboardingFocusId;
|
|
inferredAt: string;
|
|
}
|
|
|
|
/** Focus choice → help shape. Lifestyle-agnostic: same lanes can serve any user. */
|
|
export const FOCUS_HELP_VIBE_MAP: Record<
|
|
OnboardingFocusId,
|
|
{ interactionStyle: InteractionStyle; suggestedLanes: string[] }
|
|
> = {
|
|
day: {
|
|
interactionStyle: "quiet-doer",
|
|
suggestedLanes: ["inbox-work", "chief-operator", "curator"],
|
|
},
|
|
inbox: {
|
|
interactionStyle: "quiet-doer",
|
|
suggestedLanes: ["inbox-work", "inbox-life", "chief-operator"],
|
|
},
|
|
research: {
|
|
interactionStyle: "geek-peer",
|
|
suggestedLanes: ["curator", "builder", "engineer"],
|
|
},
|
|
crew: {
|
|
interactionStyle: "unknown",
|
|
suggestedLanes: ["chief-operator", "specialist"],
|
|
},
|
|
};
|
|
|
|
export function mergeHelpVibeHints(
|
|
base: UserHelpVibeHints,
|
|
focusId: OnboardingFocusId,
|
|
): UserHelpVibeHints {
|
|
const mapped = FOCUS_HELP_VIBE_MAP[focusId];
|
|
const lanes = [...new Set([...base.suggestedLanes, ...mapped.suggestedLanes])];
|
|
return {
|
|
...base,
|
|
focusChoiceId: focusId,
|
|
interactionStyle:
|
|
base.interactionStyle === "unknown" ? mapped.interactionStyle : base.interactionStyle,
|
|
suggestedLanes: lanes,
|
|
observedSignals: [...base.observedSignals, `focus:${focusId}`],
|
|
inferredAt: new Date().toISOString(),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Infer help & vibe from signals we actually have — never fake lifestyle or scene/office type.
|
|
* Interaction style stays `unknown` until focus choice or explicit prefs land.
|
|
*/
|
|
export function inferHelpVibeHints(
|
|
signals: UserProfileSignals,
|
|
extras?: { focusChoiceId?: OnboardingFocusId },
|
|
): UserHelpVibeHints {
|
|
const observedSignals: string[] = [];
|
|
const suggestedLanes = new Set<string>();
|
|
|
|
if (signals.computerHost) {
|
|
observedSignals.push("computer-attached");
|
|
suggestedLanes.add("builder");
|
|
suggestedLanes.add("engineer");
|
|
}
|
|
|
|
if (signals.defaultProvider === "openai-compatible") {
|
|
observedSignals.push("self-hosted-model");
|
|
suggestedLanes.add("engineer");
|
|
}
|
|
|
|
if (signals.uiLocale) {
|
|
observedSignals.push(`ui-locale:${signals.uiLocale}`);
|
|
}
|
|
|
|
let hints: UserHelpVibeHints = {
|
|
interactionStyle: "unknown",
|
|
suggestedLanes: [...suggestedLanes],
|
|
observedSignals,
|
|
inferredAt: new Date().toISOString(),
|
|
};
|
|
|
|
if (extras?.focusChoiceId) {
|
|
hints = mergeHelpVibeHints(hints, extras.focusChoiceId);
|
|
}
|
|
|
|
return hints;
|
|
}
|
|
|
|
export function summarizeHelpVibe(hints: UserHelpVibeHints): string {
|
|
const parts: string[] = [];
|
|
if (hints.interactionStyle !== "unknown") {
|
|
parts.push(hints.interactionStyle === "geek-peer" ? "geek-peer OK" : "quiet-doer");
|
|
}
|
|
if (hints.suggestedLanes.length) {
|
|
parts.push(hints.suggestedLanes.slice(0, 3).join(", "));
|
|
}
|
|
if (hints.focusChoiceId) {
|
|
parts.push(`focus:${hints.focusChoiceId}`);
|
|
}
|
|
return parts.length ? parts.join(" · ") : "learning from you";
|
|
}
|
|
|
|
const HELP_VIBE_SECTION = "## Help & vibe (inferred)";
|
|
|
|
export function mergeHelpVibeIntoMemory(existing: string, hints: UserHelpVibeHints): string {
|
|
const block = formatHelpVibeBlock(hints);
|
|
const marker = HELP_VIBE_SECTION;
|
|
const start = existing.indexOf(marker);
|
|
if (start === -1) {
|
|
const trimmed = existing.trimEnd();
|
|
const prefix = trimmed.length ? `${trimmed}\n\n` : "";
|
|
return `${prefix}${marker}\n\n${block}\n`;
|
|
}
|
|
const rest = existing.slice(start + marker.length);
|
|
const nextSection = rest.search(/\n## /);
|
|
const tail = nextSection === -1 ? "" : rest.slice(nextSection);
|
|
return `${existing.slice(0, start).trimEnd()}\n\n${marker}\n\n${block}${tail}`;
|
|
}
|
|
|
|
function formatHelpVibeBlock(hints: UserHelpVibeHints): string {
|
|
const styleLine =
|
|
hints.interactionStyle === "unknown"
|
|
? "unknown — learn from conversations (geek-peer vs quiet-doer)"
|
|
: hints.interactionStyle === "geek-peer"
|
|
? "geek-peer — fachsimpeln welcome when they want depth"
|
|
: "quiet-doer — execute without acting like we know better";
|
|
|
|
const lines = [
|
|
`- **Updated:** ${hints.inferredAt}`,
|
|
`- **Interaction style:** ${styleLine}`,
|
|
hints.focusChoiceId ? `- **Focus choice:** ${hints.focusChoiceId}` : null,
|
|
hints.observedSignals.length
|
|
? `- **Observed signals:** ${hints.observedSignals.join(", ")}`
|
|
: null,
|
|
hints.suggestedLanes.length
|
|
? `- **Suggested specialist lanes:** ${hints.suggestedLanes.join(", ")} (_suggestions — spawn/select accordingly_)`
|
|
: null,
|
|
`- **Skill floor:** ${SKILL_FLOOR_LABEL}`,
|
|
`- **Skill ceiling:** ${SKILL_CEILING_LABEL}`,
|
|
"",
|
|
"_Simple entry, deep ceiling — domain excellence without power-user literacy. Not one mediocre Wunderwuzzi._",
|
|
];
|
|
return lines.filter((line) => line !== null).join("\n");
|
|
}
|