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

View File

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

View File

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

View File

@ -1,14 +1,8 @@
import type { IntegrationSetupState, Me } from "@rakazo/contracts"; import type { IntegrationSetupState, Me } from "@rakazo/contracts";
import { collectLocalProfileSignals, type UserProfileSignals } from "./user-profile-harvest"; 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 = export type DiscoverySourceId =
| "profile-basics" | "profile-basics"
| "model-ready"
| "connected-tools" | "connected-tools"
| "specialist-crew"; | "specialist-crew";
@ -17,11 +11,10 @@ export type DiscoveryStatus = "pending" | "scanning" | "found" | "skipped";
export interface DiscoveryItem { export interface DiscoveryItem {
id: DiscoverySourceId; id: DiscoverySourceId;
status: DiscoveryStatus; status: DiscoveryStatus;
/** Honest one-line result — only set when we truly know something. */
detail?: string; detail?: string;
} }
export type OnboardingDiscoveryPhase = "loading" | "model" | "integrations" | "assembly"; export type OnboardingDiscoveryPhase = "loading" | "emoji" | "integrations" | "assembly";
export interface DiscoveryContext { export interface DiscoveryContext {
phase: OnboardingDiscoveryPhase; phase: OnboardingDiscoveryPhase;
@ -55,7 +48,6 @@ export function computeDiscoveryItems(context: DiscoveryContext): DiscoveryItem[
context.profileSignals ?? (context.me ? collectLocalProfileSignals(context.me) : null); context.profileSignals ?? (context.me ? collectLocalProfileSignals(context.me) : null);
const configuredCount = const configuredCount =
context.connectedIntegrationCount ?? countConfiguredIntegrations(context.integrationSetup); context.connectedIntegrationCount ?? countConfiguredIntegrations(context.integrationSetup);
const needsModel = context.me?.needsModel;
const profile: DiscoveryItem = (() => { const profile: DiscoveryItem = (() => {
if (context.profilePersisted && signals) { if (context.profilePersisted && signals) {
@ -73,23 +65,6 @@ export function computeDiscoveryItems(context: DiscoveryContext): DiscoveryItem[
return { id: "profile-basics", status: "pending" }; 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 = (() => { const tools: DiscoveryItem = (() => {
if (configuredCount > 0) { if (configuredCount > 0) {
return { return {
@ -117,10 +92,9 @@ export function computeDiscoveryItems(context: DiscoveryContext): DiscoveryItem[
return { id: "specialist-crew", status: "pending" }; 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[] { export function visibleDiscoveryItems(items: DiscoveryItem[]): DiscoveryItem[] {
return items; 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 "./openai-voice.js";
export * from "./page-browser-session.js"; export * from "./page-browser-session.js";
export * from "./pi-credentials.js"; export * from "./pi-credentials.js";
export * from "./pi-model-config.js";
export * from "./pi-models.js"; export * from "./pi-models.js";
export * from "./pi-oauth.js"; export * from "./pi-oauth.js";
export * from "./pi-openai-compatible-provider.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 AvatarStyle, AvatarStyleProvider, useAvatarStyle } from "./avatar-style.js";
export { type GrokColorDef } from "./bot-avatar.js";
export { export {
BotAvatar, BotAvatar,
DEFAULT_GROK_BOT_COLOR, DEFAULT_GROK_BOT_COLOR,
GROK_BOT_COLORS, GROK_BOT_COLORS,
GROK_COLOR_LIST,
GROK_MASCOT_SHAPES, GROK_MASCOT_SHAPES,
GROK_SHAPES,
GrokShapePreview, GrokShapePreview,
parseBotAvatar, parseBotAvatar,
resolvePersonaColorDef, resolvePersonaColorDef,
resolvePersonaShape, resolvePersonaShape,
SHIPPED_SHAPE_KEYS,
} from "./bot-avatar.js"; } from "./bot-avatar.js";
export * from "./components/ui/alert-dialog.js"; export * from "./components/ui/alert-dialog.js";
export * from "./components/ui/badge.js"; export * from "./components/ui/badge.js";