evOo-local/apps/web/src/lib/onboarding-discovery.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

140 lines
4.8 KiB
TypeScript

import type { IntegrationSetupState, Me } from "@rakazo/contracts";
import { collectLocalProfileSignals, type UserProfileSignals } from "./user-profile-harvest";
/**
* Visible onboarding steps only — future discovery lives in docs/TODO, not the UI.
* Profile harvest seeds user MEMORY.md (help/vibe + Oo relationship contract — compounding via use,
* respectful pushback, anti-creep tone; `user-relationship-contract.ts`, `docs/OO-RELATIONSHIP.md`).
*/
export type DiscoverySourceId =
| "profile-basics"
| "model-ready"
| "connected-tools"
| "specialist-crew";
export type DiscoveryStatus = "pending" | "scanning" | "found" | "skipped";
export interface DiscoveryItem {
id: DiscoverySourceId;
status: DiscoveryStatus;
/** Honest one-line result — only set when we truly know something. */
detail?: string;
}
export type OnboardingDiscoveryPhase = "loading" | "model" | "integrations" | "assembly";
export interface DiscoveryContext {
phase: OnboardingDiscoveryPhase;
me?: Pick<
Me,
"name" | "email" | "needsModel" | "defaultProvider" | "defaultModel" | "computerHost"
> | null;
integrationSetup?: IntegrationSetupState | null;
connectedIntegrationCount?: number;
profileSignals?: UserProfileSignals;
profilePersisted?: boolean;
crewReady?: boolean;
}
export function summarizeProfileSignals(signals: UserProfileSignals): string {
const parts: string[] = [];
if (signals.name) parts.push(signals.name);
parts.push(signals.timezone);
if (signals.uiLocale && signals.uiLocale !== "en") parts.push(signals.uiLocale);
return parts.join(" · ");
}
export function countConfiguredIntegrations(
setup: IntegrationSetupState | null | undefined,
): number {
return setup?.providers.filter((provider) => provider.configured).length ?? 0;
}
export function computeDiscoveryItems(context: DiscoveryContext): DiscoveryItem[] {
const signals =
context.profileSignals ?? (context.me ? collectLocalProfileSignals(context.me) : null);
const configuredCount =
context.connectedIntegrationCount ?? countConfiguredIntegrations(context.integrationSetup);
const needsModel = context.me?.needsModel;
const profile: DiscoveryItem = (() => {
if (context.profilePersisted && signals) {
return { id: "profile-basics", status: "found", detail: summarizeProfileSignals(signals) };
}
if (context.phase === "assembly" && signals) {
return { id: "profile-basics", status: "scanning", detail: summarizeProfileSignals(signals) };
}
if (signals && context.me) {
return { id: "profile-basics", status: "found", detail: summarizeProfileSignals(signals) };
}
if (context.phase === "loading") {
return { id: "profile-basics", status: "scanning" };
}
return { id: "profile-basics", status: "pending" };
})();
const model: DiscoveryItem = (() => {
if (needsModel === false) {
const modelLabel =
context.me?.defaultProvider && context.me.defaultModel
? `${context.me.defaultProvider} / ${context.me.defaultModel}`
: undefined;
return { id: "model-ready", status: "found", detail: modelLabel };
}
if (context.phase === "model") {
return { id: "model-ready", status: "scanning" };
}
if (needsModel === true) {
return { id: "model-ready", status: "pending" };
}
return { id: "model-ready", status: "scanning" };
})();
const tools: DiscoveryItem = (() => {
if (configuredCount > 0) {
return {
id: "connected-tools",
status: "found",
detail: String(configuredCount),
};
}
if (context.phase === "integrations") {
return { id: "connected-tools", status: "scanning" };
}
if (context.integrationSetup && !context.integrationSetup.needsSetup) {
return { id: "connected-tools", status: "skipped" };
}
return { id: "connected-tools", status: "pending" };
})();
const crew: DiscoveryItem = (() => {
if (context.crewReady) {
return { id: "specialist-crew", status: "found", detail: "Chief of Staff" };
}
if (context.phase === "assembly") {
return { id: "specialist-crew", status: "scanning" };
}
return { id: "specialist-crew", status: "pending" };
})();
return [profile, model, tools, crew];
}
/** Onboarding UI: only steps in flight — no stub laundry list (weniger ist mehr). */
export function visibleDiscoveryItems(items: DiscoveryItem[]): DiscoveryItem[] {
return items;
}
export function discoveryProgress(items: DiscoveryItem[]): {
done: number;
total: number;
active: DiscoveryItem | null;
} {
const done = items.filter((item) => item.status === "found" || item.status === "skipped").length;
const active =
items.find((item) => item.status === "scanning") ??
items.find((item) => item.status === "pending") ??
null;
return { done, total: items.length, active };
}