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:
parent
afba3cef5a
commit
9aeb2c01f8
@ -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)
|
||||
|
||||
|
||||
@ -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`;
|
||||
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@ -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
@ -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
@ -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";
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user