evOo-local/apps/web/src/lib/user-help-vibe.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

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");
}