feat: emoji-first onboarding flow with accessory/pose customization, language step

- Rewrite Onboarding.tsx: 5-step flow (emoji → preferences → language → integrations → bot)
- Remove model selection from onboarding entirely
- Add shape selector (8 GrokShapes), color picker (12 colors), accessory (9 options), pose (8 poses)
- Add user preference form (name, timezone, hobbies, work style, communication style)
- Add language selection step with 9 locales persisting to localStorage
- Persist avatarValue ({color}::shape_{index}) on bot creation
- Persist user preferences to bot instructions
- Update onboarding-discovery: 3 items (profile, tools, crew), no model
- Update OnboardingDiscoveryShell: remove model-ready source label
- Export GROK_COLOR_LIST, GROK_SHAPES, GrokColorDef, SHIPPED_SHAPE_KEYS from ui-web
- Update onboarding-discovery.test.ts with proper mock types
This commit is contained in:
Ingo Wagner 2026-09-21 23:18:34 +00:00
parent afba3cef5a
commit 9aeb2c01f8
8 changed files with 1777 additions and 800 deletions

View File

@ -37,16 +37,16 @@ Stash message: `wip-aside-for-local-gate`. After restore you should see dirty On
- ✅ #19 user preferences persisted to bot instructions during onboarding
- ✅ #21 Language step added (9 locales, persists to localStorage `rakazo.uiLocale`)
- ✅ executor.ts split (Task #8): 769 Zeilen extrahiert in 8 Files
- executor-completion.ts (5 completion functions)
- executor-computer.ts (10 computer/notification functions)
- executor-effects.ts (recordEffect, completeEffect, uncertainEffectError)
- executor-model.ts (resolveModelKey, deploymentKeyFor, withModelCredentialLock)
- executor-utils.ts (selectRunConnections, loadCurrentTurnImages)
- executor-tools.ts (selectBuiltinToolsForRun, filterPageBrowserTools, PAGE_BROWSER_TOOL_NAMES)
- executor-attachments.ts (missingTurnImagesInstruction, settleSteeringAttachmentLoads + constants)
- executor-types.ts (ExecutorDeps interface)
- 4159 Zeilen (von 4928), 0 neue TS errors, build green, 54 tests passing
- ✅ Vite build verified green
- executor-completion.ts, executor-computer.ts, executor-effects.ts, executor-model.ts
- executor-utils.ts, executor-tools.ts, executor-attachments.ts, executor-types.ts
- 4159 Zeilen (von 4928), build green, 54 tests passing
- ✅ pi-runtime.ts split: 1726 → 561 Zeilen (1165 gespart)
- pi-model-config.ts (~1500 Zeilen) — model config + 40+ utility functions
- pi-runtime.ts bleibt 561 Zeilen (PiAgentRuntime class + module setup)
- ✅ Playwright E2E test: onboarding-emoji-first.spec.ts
- ✅ completeOnboarding helper updated for new flow
- ✅ executor.test.ts imports fixed
- ✅ Build verified green, commit + push
## executor.ts split status
| File | Lines |
@ -61,9 +61,13 @@ Stash message: `wip-aside-for-local-gate`. After restore you should see dirty On
| executor-attachments.ts | 43 |
| executor-types.ts | 45 |
## pi-runtime.ts split status
| File | Lines |
|------|-------|
| pi-runtime.ts | 561 |
| pi-model-config.ts | ~1500 |
## Pending
- #9 Split pi-runtime.ts (1726)
- Playwright E2E tests for new onboarding flow
- Accessory/pose customization in onboarding (currently only shape+color)
- Kill fat banner on first chat (requires backend language support)

View File

@ -18,9 +18,7 @@ function sourceLabel(id: DiscoverySourceId, t: ReturnType<typeof useLingui>["t"]
switch (id) {
case "profile-basics":
return t`Your profile`;
case "model-ready":
return t`Model connection`;
case "connected-tools":
case "connected-tools":
return t`Connected tools`;
case "specialist-crew":
return t`Specialist crew`;

View File

@ -4,95 +4,68 @@ import {
countConfiguredIntegrations,
discoveryProgress,
summarizeProfileSignals,
visibleDiscoveryItems,
} from "./onboarding-discovery";
import { collectLocalProfileSignals } from "./user-profile-harvest";
type DiscoveryContext,
} from "./onboarding-discovery.js";
import type { IntegrationSetupState } from "@rakazo/contracts";
describe("onboarding-discovery", () => {
it("marks profile basics found when me is loaded", () => {
const items = computeDiscoveryItems({
phase: "loading",
me: {
name: "Ingo",
email: "ingo@example.com",
needsModel: false,
defaultProvider: "openrouter",
defaultModel: "gpt-4",
computerHost: null,
},
});
const profile = items.find((item) => item.id === "profile-basics");
expect(profile?.status).toBe("found");
expect(profile?.detail).toContain("Ingo");
describe("countConfiguredIntegrations", () => {
it("counts configured providers", () => {
const setup: IntegrationSetupState = {
canConfigure: true,
needsSetup: false,
webUrl: "https://example.com",
providers: [
{ id: "composio", configured: true },
{ id: "pipedream", configured: false },
],
};
expect(countConfiguredIntegrations(setup)).toBe(1);
});
it("returns only four visible onboarding steps", () => {
const items = computeDiscoveryItems({ phase: "assembly", crewReady: true });
expect(items.map((item) => item.id)).toEqual([
"profile-basics",
"model-ready",
"connected-tools",
"specialist-crew",
]);
});
it("reports configured integration count honestly", () => {
expect(
countConfiguredIntegrations({
canConfigure: true,
needsSetup: false,
webUrl: "https://example.com",
providers: [
{ id: "slack", configured: true },
{ id: "gmail", configured: false },
],
}),
).toBe(1);
});
it("tracks assembly progress for crew and persisted profile", () => {
const signals = collectLocalProfileSignals({
name: "Robin",
email: "r@example.com",
defaultProvider: null,
defaultModel: null,
computerHost: null,
});
const items = computeDiscoveryItems({
phase: "assembly",
profileSignals: signals,
profilePersisted: true,
crewReady: true,
me: {
name: "Robin",
email: "r@example.com",
needsModel: false,
defaultProvider: null,
defaultModel: null,
computerHost: null,
},
});
expect(items.find((item) => item.id === "specialist-crew")?.status).toBe("found");
const progress = discoveryProgress(items);
expect(progress.done).toBeGreaterThanOrEqual(3);
});
it("summarizes profile signals without inventing data", () => {
const summary = summarizeProfileSignals(
collectLocalProfileSignals({
name: "Ingo",
email: "a@b.c",
defaultProvider: null,
defaultModel: null,
computerHost: null,
}),
);
expect(summary).toContain("Ingo");
expect(summary.length).toBeGreaterThan(0);
});
it("visibleDiscoveryItems passes through the four onboarding steps", () => {
const items = computeDiscoveryItems({ phase: "loading" });
expect(visibleDiscoveryItems(items)).toHaveLength(4);
it("returns 0 for null", () => {
expect(countConfiguredIntegrations(null)).toBe(0);
});
});
describe("summarizeProfileSignals", () => {
it("includes name and timezone", () => {
expect(
summarizeProfileSignals({ name: "Alice", timezone: "Europe/Vienna", locale: "de-DE", languages: [], harvestedAt: new Date().toISOString() }),
).toBe("Alice · Europe/Vienna");
});
it("includes uiLocale when non-en", () => {
expect(
summarizeProfileSignals({ name: "Bob", timezone: "", locale: "de", uiLocale: "de", languages: [], harvestedAt: new Date().toISOString() }),
).toBe("Bob · · de");
});
});
describe("computeDiscoveryItems", () => {
it("returns only three visible onboarding steps", () => {
const context: DiscoveryContext = {
phase: "assembly",
me: { needsModel: false, computerHost: null, email: "a@b.com", name: "Test", defaultProvider: null, defaultModel: null },
crewReady: true,
profilePersisted: true,
};
const items = computeDiscoveryItems(context);
expect(items.map((i) => i.id)).toEqual(["profile-basics", "connected-tools", "specialist-crew"]);
});
it("has length 3", () => {
const items = computeDiscoveryItems({ phase: "loading" });
expect(items).toHaveLength(3);
});
it("progress counts found + skipped", () => {
const items = [
{ id: "profile-basics" as const, status: "found" as const },
{ id: "connected-tools" as const, status: "pending" as const },
{ id: "specialist-crew" as const, status: "skipped" as const },
];
const progress = discoveryProgress(items);
expect(progress.done).toBe(2);
expect(progress.total).toBe(3);
});
});

View File

@ -1,14 +1,8 @@
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";
@ -17,11 +11,10 @@ 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 type OnboardingDiscoveryPhase = "loading" | "emoji" | "integrations" | "assembly";
export interface DiscoveryContext {
phase: OnboardingDiscoveryPhase;
@ -55,7 +48,6 @@ export function computeDiscoveryItems(context: DiscoveryContext): DiscoveryItem[
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) {
@ -73,23 +65,6 @@ export function computeDiscoveryItems(context: DiscoveryContext): DiscoveryItem[
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 {
@ -117,10 +92,9 @@ export function computeDiscoveryItems(context: DiscoveryContext): DiscoveryItem[
return { id: "specialist-crew", status: "pending" };
})();
return [profile, model, tools, crew];
return [profile, tools, crew];
}
/** Onboarding UI: only steps in flight — no stub laundry list (weniger ist mehr). */
export function visibleDiscoveryItems(items: DiscoveryItem[]): DiscoveryItem[] {
return items;
}

File diff suppressed because it is too large Load Diff

View File

@ -85,7 +85,6 @@ export * from "./openai-compatible-url.js";
export * from "./openai-voice.js";
export * from "./page-browser-session.js";
export * from "./pi-credentials.js";
export * from "./pi-model-config.js";
export * from "./pi-models.js";
export * from "./pi-oauth.js";
export * from "./pi-openai-compatible-provider.js";

File diff suppressed because it is too large Load Diff

View File

@ -1,13 +1,17 @@
export { type AvatarStyle, AvatarStyleProvider, useAvatarStyle } from "./avatar-style.js";
export { type GrokColorDef } from "./bot-avatar.js";
export {
BotAvatar,
DEFAULT_GROK_BOT_COLOR,
GROK_BOT_COLORS,
GROK_COLOR_LIST,
GROK_MASCOT_SHAPES,
GROK_SHAPES,
GrokShapePreview,
parseBotAvatar,
resolvePersonaColorDef,
resolvePersonaShape,
SHIPPED_SHAPE_KEYS,
} from "./bot-avatar.js";
export * from "./components/ui/alert-dialog.js";
export * from "./components/ui/badge.js";