mirror of
https://github.com/multica-ai/multica.git
synced 2026-08-12 10:59:06 +02:00
* refactor(onboarding): remove starter-content kit, unify install-runtime issue across mark-onboarded paths (MUL-2438) Drops the post-onboarding ImportStarterContent / DismissStarterContent flow (handler + routes + StarterContentPrompt + templates + locale strings + analytics event). The bug — web onboarding seeding 6+ starter issues without a runtime — only existed through that path; with it gone the source disappears. The "install a runtime" issue from BootstrapOnboardingNoRuntime is now the canonical no-runtime onboarding seed. The title/description and a LockAndFindActiveDuplicate-deduped seeder move to handler/no_runtime_issue.go, and CompleteOnboarding / CreateWorkspace / AcceptInvitation seed it whenever the workspace has no runtime yet, so every mark-onboarded entry point lands the user on a concrete next step. starter_content_state column is kept and continues to be claimed as 'imported' in all five entry points so older desktop builds (which still render the legacy dialog on NULL) don't surface it to accounts created after this change. Co-authored-by: multica-agent <github@multica.ai> * fix(onboarding): backfill starter_content_state for in-window NULL users (MUL-2438) 054 only covered pre-feature users. Anyone onboarded between then and the starter-content kit removal could still sit at NULL, and old desktop clients gate the legacy StarterContentPrompt on `starter_content_state IS NULL`. The import/dismiss routes are gone, so leaving these rows NULL would surface a dialog whose buttons 404. Mark them 'imported' to match the new helper's claim semantics. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Lambda <lambda@multica.ai> Co-authored-by: multica-agent <github@multica.ai>
127 lines
4.2 KiB
TypeScript
127 lines
4.2 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useRef, useState } from "react";
|
|
import { Loader2, AlertCircle } from "lucide-react";
|
|
import { toast } from "sonner";
|
|
import { Button } from "@multica/ui/components/ui/button";
|
|
import {
|
|
completeOnboarding,
|
|
type OnboardingCompletionPath,
|
|
} from "@multica/core/onboarding";
|
|
import { useT } from "../../i18n";
|
|
|
|
/**
|
|
* Step 5 — the final onboarding beat.
|
|
*
|
|
* Runtime-skipped finalizer. The runtime-connected path now bootstraps one
|
|
* default assistant plus one onboarding issue server-side and routes there
|
|
* directly. This step remains for users who skip runtime connection: it only
|
|
* flips `onboarded_at` and lands them in the workspace.
|
|
* Two consequences of that move:
|
|
*
|
|
* 1. This step can't fail in user-visible ways any more. `completeOnboarding`
|
|
* is one PATCH to `/api/me`; the only failure mode is a network error,
|
|
* which we surface as a toast + Retry, not a full error screen.
|
|
* 2. The sub-issue "Unknown" assignee race is gone for free — by the time
|
|
* the import runs, the user has already landed in the workspace, so
|
|
* `listMembers` has resolved and the current user's member_id is in
|
|
* the query cache.
|
|
*/
|
|
export function StepFirstIssue({
|
|
onFinished,
|
|
completionPath,
|
|
workspaceId,
|
|
}: {
|
|
/** Called after `onboarded_at` is set server-side. Parent handles
|
|
* navigation to the workspace landing page. */
|
|
onFinished: () => void;
|
|
/** Which exit label the server should record on `onboarding_completed`.
|
|
* Computed in the parent shell where runtime + waitlist state are
|
|
* both in scope. */
|
|
completionPath: OnboardingCompletionPath;
|
|
workspaceId?: string;
|
|
}) {
|
|
const { t } = useT("onboarding");
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [retrying, setRetrying] = useState(false);
|
|
const started = useRef(false);
|
|
const onFinishedRef = useRef(onFinished);
|
|
onFinishedRef.current = onFinished;
|
|
const completionPathRef = useRef(completionPath);
|
|
completionPathRef.current = completionPath;
|
|
const workspaceIdRef = useRef(workspaceId);
|
|
workspaceIdRef.current = workspaceId;
|
|
|
|
useEffect(() => {
|
|
if (started.current) return;
|
|
started.current = true;
|
|
(async () => {
|
|
try {
|
|
await completeOnboarding(
|
|
completionPathRef.current,
|
|
workspaceIdRef.current,
|
|
);
|
|
onFinishedRef.current();
|
|
} catch (err) {
|
|
setError(
|
|
err instanceof Error ? err.message : t(($) => $.errors.skip_failed),
|
|
);
|
|
}
|
|
})();
|
|
}, [t]);
|
|
|
|
const retry = async () => {
|
|
if (retrying) return;
|
|
setRetrying(true);
|
|
setError(null);
|
|
try {
|
|
await completeOnboarding(
|
|
completionPathRef.current,
|
|
workspaceIdRef.current,
|
|
);
|
|
onFinishedRef.current();
|
|
} catch (err) {
|
|
const msg =
|
|
err instanceof Error ? err.message : t(($) => $.first_issue.retry_failed);
|
|
setError(msg);
|
|
toast.error(msg);
|
|
} finally {
|
|
setRetrying(false);
|
|
}
|
|
};
|
|
|
|
if (error) {
|
|
return (
|
|
<div className="animate-onboarding-enter flex w-full flex-col items-center gap-6 text-center">
|
|
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-destructive/10 text-destructive">
|
|
<AlertCircle className="h-6 w-6" />
|
|
</div>
|
|
<div className="flex flex-col gap-2">
|
|
<h1 className="text-2xl font-semibold tracking-tight">
|
|
{t(($) => $.first_issue.error_title)}
|
|
</h1>
|
|
<p className="text-sm text-muted-foreground">{error}</p>
|
|
</div>
|
|
<Button onClick={retry} disabled={retrying}>
|
|
{retrying && <Loader2 className="h-4 w-4 animate-spin" />}
|
|
{t(($) => $.first_issue.retry)}
|
|
</Button>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="animate-onboarding-enter flex w-full flex-col items-center gap-6 text-center">
|
|
<Loader2 className="h-10 w-10 animate-spin text-primary" />
|
|
<div className="flex flex-col gap-2">
|
|
<h1 className="text-2xl font-semibold tracking-tight">
|
|
{t(($) => $.first_issue.finishing)}
|
|
</h1>
|
|
<p className="text-sm text-muted-foreground">
|
|
{t(($) => $.first_issue.opening)}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|