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
|
- ✅ #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)
|
||||||
|
|
||||||
|
|||||||
@ -18,9 +18,7 @@ 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":
|
case "connected-tools":
|
||||||
return t`Model connection`;
|
|
||||||
case "connected-tools":
|
|
||||||
return t`Connected tools`;
|
return t`Connected tools`;
|
||||||
case "specialist-crew":
|
case "specialist-crew":
|
||||||
return t`Specialist crew`;
|
return t`Specialist crew`;
|
||||||
|
|||||||
@ -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",
|
canConfigure: true,
|
||||||
me: {
|
needsSetup: false,
|
||||||
name: "Ingo",
|
webUrl: "https://example.com",
|
||||||
email: "ingo@example.com",
|
providers: [
|
||||||
needsModel: false,
|
{ id: "composio", configured: true },
|
||||||
defaultProvider: "openrouter",
|
{ id: "pipedream", configured: false },
|
||||||
defaultModel: "gpt-4",
|
],
|
||||||
computerHost: null,
|
};
|
||||||
},
|
expect(countConfiguredIntegrations(setup)).toBe(1);
|
||||||
});
|
|
||||||
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", () => {
|
it("returns 0 for null", () => {
|
||||||
const items = computeDiscoveryItems({ phase: "assembly", crewReady: true });
|
expect(countConfiguredIntegrations(null)).toBe(0);
|
||||||
expect(items.map((item) => item.id)).toEqual([
|
});
|
||||||
"profile-basics",
|
});
|
||||||
"model-ready",
|
|
||||||
"connected-tools",
|
describe("summarizeProfileSignals", () => {
|
||||||
"specialist-crew",
|
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("reports configured integration count honestly", () => {
|
});
|
||||||
expect(
|
|
||||||
countConfiguredIntegrations({
|
it("includes uiLocale when non-en", () => {
|
||||||
canConfigure: true,
|
expect(
|
||||||
needsSetup: false,
|
summarizeProfileSignals({ name: "Bob", timezone: "", locale: "de", uiLocale: "de", languages: [], harvestedAt: new Date().toISOString() }),
|
||||||
webUrl: "https://example.com",
|
).toBe("Bob · · de");
|
||||||
providers: [
|
});
|
||||||
{ id: "slack", configured: true },
|
});
|
||||||
{ id: "gmail", configured: false },
|
|
||||||
],
|
describe("computeDiscoveryItems", () => {
|
||||||
}),
|
it("returns only three visible onboarding steps", () => {
|
||||||
).toBe(1);
|
const context: DiscoveryContext = {
|
||||||
});
|
phase: "assembly",
|
||||||
|
me: { needsModel: false, computerHost: null, email: "a@b.com", name: "Test", defaultProvider: null, defaultModel: null },
|
||||||
it("tracks assembly progress for crew and persisted profile", () => {
|
crewReady: true,
|
||||||
const signals = collectLocalProfileSignals({
|
profilePersisted: true,
|
||||||
name: "Robin",
|
};
|
||||||
email: "r@example.com",
|
const items = computeDiscoveryItems(context);
|
||||||
defaultProvider: null,
|
expect(items.map((i) => i.id)).toEqual(["profile-basics", "connected-tools", "specialist-crew"]);
|
||||||
defaultModel: null,
|
});
|
||||||
computerHost: null,
|
|
||||||
});
|
it("has length 3", () => {
|
||||||
const items = computeDiscoveryItems({
|
const items = computeDiscoveryItems({ phase: "loading" });
|
||||||
phase: "assembly",
|
expect(items).toHaveLength(3);
|
||||||
profileSignals: signals,
|
});
|
||||||
profilePersisted: true,
|
|
||||||
crewReady: true,
|
it("progress counts found + skipped", () => {
|
||||||
me: {
|
const items = [
|
||||||
name: "Robin",
|
{ id: "profile-basics" as const, status: "found" as const },
|
||||||
email: "r@example.com",
|
{ id: "connected-tools" as const, status: "pending" as const },
|
||||||
needsModel: false,
|
{ id: "specialist-crew" as const, status: "skipped" as const },
|
||||||
defaultProvider: null,
|
];
|
||||||
defaultModel: null,
|
const progress = discoveryProgress(items);
|
||||||
computerHost: null,
|
expect(progress.done).toBe(2);
|
||||||
},
|
expect(progress.total).toBe(3);
|
||||||
});
|
|
||||||
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);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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
@ -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
@ -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";
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user