mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-27 21:33:41 +02:00
Flow drops from five steps to three: role + use_case merge into a single About-you screen (one Skip covers both; Continue stamps skip markers on whichever group was left unanswered), and the source question leaves onboarding entirely. Source is now collected only by the workspace source-backfill prompt, which additionally waits until agents/squads have completed at least SOURCE_BACKFILL_MIN_AGENT_DONE_ISSUES (3) issues in the workspace — attribution is asked after Multica has visibly delivered value, not before. The count rides a limit:1 issues query keyed under issueKeys.all so realtime invalidations keep it fresh, enabled only for users who still owe an answer. Server: questionnaire complete() narrows to role + use_case so the funnel step doesn't stall on the now-deferred source; a new metrics-only onboarding_source_submitted event (+ Prometheus counter) tracks the backfill prompt's answer/decline transition once per user. Co-authored-by: Lambda <lambda@multica.ai> Co-authored-by: multica-agent <github@multica.ai>
75 lines
3.0 KiB
TypeScript
75 lines
3.0 KiB
TypeScript
import type { User } from "../types";
|
|
import type { QuestionnaireAnswers } from "./types";
|
|
|
|
/**
|
|
* Maximum number of times the user can close the backfill prompt with
|
|
* the X / ESC / outside-click before we treat it as a permanent
|
|
* dismissal. After that the prompt stops appearing.
|
|
*
|
|
* Submit and explicit Skip are always terminal (they write to the
|
|
* server). The count exists only for the "I'll think about it later"
|
|
* close path — without a cap, a user who never decides would see the
|
|
* prompt every login forever.
|
|
*/
|
|
export const SOURCE_BACKFILL_MAX_DISMISSALS = 3;
|
|
|
|
/**
|
|
* Minimum number of issues completed by an AI assignee (agent or
|
|
* squad) in the current workspace before the source prompt may open.
|
|
*
|
|
* Source is not asked during onboarding at all — attribution is a
|
|
* zero-payoff question for the user, so we wait until Multica has
|
|
* demonstrably delivered value (agents finished real work) before
|
|
* spending goodwill on it. Answer rates for "how did you hear about
|
|
* us" prompts are also materially better after an activation moment
|
|
* than at signup. 3 ≈ one Helper starter-task batch, so an engaged
|
|
* new user typically crosses it within the first session.
|
|
*
|
|
* The count itself comes from `agentCompletedIssueCountOptions` in
|
|
* `./queries.ts`; the modal combines it with `needsSourceBackfill`.
|
|
*/
|
|
export const SOURCE_BACKFILL_MIN_AGENT_DONE_ISSUES = 3;
|
|
|
|
/**
|
|
* Should we ask this already-onboarded user where they heard about
|
|
* Multica?
|
|
*
|
|
* Returns true for users who:
|
|
* - have completed onboarding (`onboarded_at` set), and
|
|
* - have not recorded any source (empty array or absent), and
|
|
* - did not previously decline the source question (skip marker), and
|
|
* - have not closed this backfill prompt enough times to dismiss it.
|
|
*
|
|
* This is the user-level half of the gate. The workspace-level half —
|
|
* "have agents completed at least SOURCE_BACKFILL_MIN_AGENT_DONE_ISSUES
|
|
* issues here?" — needs a server query, so it lives in the modal
|
|
* (`source-backfill-modal.tsx`), which also uses this predicate to
|
|
* decide whether that query is worth running at all.
|
|
*
|
|
* Pure function — `dismissCount` is passed in so this stays callable
|
|
* from core (no localStorage / StorageAdapter dependency).
|
|
*/
|
|
export function needsSourceBackfill(
|
|
user: User | null | undefined,
|
|
dismissCount: number,
|
|
): boolean {
|
|
if (!user) return false;
|
|
if (!user.onboarded_at) return false;
|
|
if (dismissCount >= SOURCE_BACKFILL_MAX_DISMISSALS) return false;
|
|
|
|
const q = user.onboarding_questionnaire as
|
|
| Partial<QuestionnaireAnswers>
|
|
| null
|
|
| undefined;
|
|
if (!q) return true;
|
|
if (q.source_skipped === true) return false;
|
|
// Pre-multi-select rows wrote `source` as a bare string. Treat a
|
|
// non-empty string the same as a one-element array — the user did
|
|
// answer. Mirrors `OnboardingFlow.mergeQuestionnaire` (views) and
|
|
// `stringOrSlice.UnmarshalJSON` (server).
|
|
const raw: unknown = q.source;
|
|
if (Array.isArray(raw)) return raw.length === 0;
|
|
if (typeof raw === "string") return raw.length === 0;
|
|
return true;
|
|
}
|