First-run onboarding UX: naming, locale, voices, Heute route, mobile drawer (#171)
* Fix first-run onboarding UX: naming, locale, voices, Heute route, mobile drawer Co-authored-by: cynarAI <cynarAI@users.noreply.github.com> * Fix biome import order and formatting Co-authored-by: cynarAI <cynarAI@users.noreply.github.com> * Fix biome formatting on OoStage and E2E specs Co-authored-by: cynarAI <cynarAI@users.noreply.github.com> * Fix E2E specs for onboarding name step and Heute reload route Co-authored-by: cynarAI <cynarAI@users.noreply.github.com> * Fix German onboarding heading matcher in E2E Co-authored-by: cynarAI <cynarAI@users.noreply.github.com> * Fix German signup E2E, Escape menu stacking, and Heute rhythm click Co-authored-by: cynarAI <cynarAI@users.noreply.github.com> * Make onboarding E2E helpers locale-aware for German UI Co-authored-by: cynarAI <cynarAI@users.noreply.github.com> * Fix completeOnboarding language skip for Skip · locale label Co-authored-by: cynarAI <cynarAI@users.noreply.github.com> * Use language-step Skip · locale button in completeOnboarding Co-authored-by: cynarAI <cynarAI@users.noreply.github.com> * Fix German E2E: Nachricht composer, voice loading wait, Heute rhythm Co-authored-by: cynarAI <cynarAI@users.noreply.github.com> * Stabilize Heute reload URL and German voice E2E assertions Co-authored-by: cynarAI <cynarAI@users.noreply.github.com> * Fix Heute bootstrap redirect and German onboarding voices When Kokoro lists no locale-matched voices, onboarding now uses offline presets instead of showing English-only options. Shell bootstrap skips bot redirects while the user is on /app/heute. Co-authored-by: cynarAI <cynarAI@users.noreply.github.com> * Relax German voice E2E name matching for voice labels Co-authored-by: cynarAI <cynarAI@users.noreply.github.com> * Harden Heute route and German voice onboarding E2E Rebased onto main with Expo SDK 57 typecheck fixes. Voice options expose stable test ids; Shell keeps /app/heute across bootstrap and redirects legacy /app/heute bot URLs. Heute coach E2E asserts URL after reload. Co-authored-by: cynarAI <cynarAI@users.noreply.github.com> * Format Shell after Heute route helper Co-authored-by: cynarAI <cynarAI@users.noreply.github.com> --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>
This commit is contained in:
parent
ba89574c83
commit
0e1036b2ff
@ -307,6 +307,10 @@ test("group chats share every context-menu action", async ({ page }, testInfo) =
|
|||||||
await group.click({ button: "right" });
|
await group.click({ button: "right" });
|
||||||
await captureScreenshot(page, testInfo, "group-context-menu-mobile");
|
await captureScreenshot(page, testInfo, "group-context-menu-mobile");
|
||||||
await page.keyboard.press("Escape");
|
await page.keyboard.press("Escape");
|
||||||
|
if ((await page.getByRole("menu").count()) > 0) {
|
||||||
|
await page.keyboard.press("Escape");
|
||||||
|
}
|
||||||
|
await expect(page.getByRole("menu")).toHaveCount(0);
|
||||||
await page.setViewportSize({ width: 1280, height: 800 });
|
await page.setViewportSize({ width: 1280, height: 800 });
|
||||||
|
|
||||||
await group.click({ button: "right" });
|
await group.click({ button: "right" });
|
||||||
|
|||||||
87
apps/web/e2e/first-run-onboarding-ux.spec.ts
Normal file
87
apps/web/e2e/first-run-onboarding-ux.spec.ts
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
import { expect, test } from "@playwright/test";
|
||||||
|
import { completeOnboarding, rpc, signup } from "./helpers";
|
||||||
|
|
||||||
|
test("onboarding names the first Oo and shows it in chat", async ({ page }) => {
|
||||||
|
const stamp = Date.now();
|
||||||
|
await signup(page, `name-oo-${stamp}@rakazo.test`, "password12", "Namer");
|
||||||
|
await page.waitForURL(/\/onboarding/, { timeout: 20_000 });
|
||||||
|
await page.getByRole("button", { name: /^(Continue|Weiter)$/i }).click();
|
||||||
|
await expect(page.getByTestId("onboarding-name-step")).toBeVisible();
|
||||||
|
await page.getByTestId("onboarding-oo-name-input").fill("Luna");
|
||||||
|
await page
|
||||||
|
.getByTestId("onboarding-name-step")
|
||||||
|
.getByRole("button", { name: /^(Continue|Weiter)$/i })
|
||||||
|
.click();
|
||||||
|
await page.getByRole("button", { name: /^(Skip|Überspringen ·)/i }).click();
|
||||||
|
await page.getByRole("button", { name: /^(Skip|Überspringen)$/i }).click();
|
||||||
|
await expect(page).toHaveURL(/\/app\/g\/[^/]+/, { timeout: 30_000 });
|
||||||
|
await expect(
|
||||||
|
page.getByTestId("bot-settings-trigger").getByText("Luna", { exact: true }),
|
||||||
|
).toBeVisible();
|
||||||
|
expect((await rpc<{ name: string }>(page, "bots/ensureMain", {})).name).toBe("Luna");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("German UI keeps Deutsch on language skip label", async ({ page }) => {
|
||||||
|
await page.addInitScript(() => {
|
||||||
|
Object.defineProperty(navigator, "language", { value: "de-DE", configurable: true });
|
||||||
|
localStorage.setItem("rakazo.uiLocale", "de");
|
||||||
|
});
|
||||||
|
const stamp = Date.now();
|
||||||
|
await signup(page, `de-skip-${stamp}@rakazo.test`, "password12", "Deutsch");
|
||||||
|
await page.getByRole("button", { name: /^(Continue|Weiter)$/i }).click();
|
||||||
|
await page.getByRole("button", { name: /^(Skip|Überspringen)$/i }).click();
|
||||||
|
await expect(
|
||||||
|
page.getByRole("heading", { name: /Wähle deine Sprache|Choose your language/i }),
|
||||||
|
).toBeVisible();
|
||||||
|
await expect(page.getByRole("button", { name: /Überspringen · Deutsch/i })).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("German voice step lists German voices after choosing Deutsch", async ({ page }) => {
|
||||||
|
await page.addInitScript(() => {
|
||||||
|
Object.defineProperty(navigator, "language", { value: "de-DE", configurable: true });
|
||||||
|
localStorage.setItem("rakazo.uiLocale", "de");
|
||||||
|
});
|
||||||
|
const stamp = Date.now();
|
||||||
|
await signup(page, `de-voice-${stamp}@rakazo.test`, "password12", "Stimme");
|
||||||
|
await page.getByRole("button", { name: /^(Continue|Weiter)$/i }).click();
|
||||||
|
await page.getByRole("button", { name: /^(Skip|Überspringen)$/i }).click();
|
||||||
|
await page.getByRole("button", { name: "Deutsch", exact: true }).click();
|
||||||
|
await expect(page.getByText(/Loading voices|Stimmen werden geladen/i)).toHaveCount(0, {
|
||||||
|
timeout: 30_000,
|
||||||
|
});
|
||||||
|
await expect(page.getByTestId("onboarding-voice-df_eva")).toBeVisible({ timeout: 30_000 });
|
||||||
|
await expect(page.getByTestId("onboarding-voice-dm_bernd")).toBeVisible();
|
||||||
|
await expect(page.getByTestId("onboarding-voice-af_alloy")).toHaveCount(0);
|
||||||
|
await expect(page.getByTestId("onboarding-voice-df_eva")).toHaveAttribute("aria-pressed", "true");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("main conversation does not show a group subtitle for one Oo", async ({ page }) => {
|
||||||
|
await page.addInitScript(() => {
|
||||||
|
Object.defineProperty(navigator, "language", { value: "de-DE", configurable: true });
|
||||||
|
localStorage.setItem("rakazo.uiLocale", "de");
|
||||||
|
});
|
||||||
|
const stamp = Date.now();
|
||||||
|
await signup(page, `main-1-1-${stamp}@rakazo.test`, "password12", "Solo");
|
||||||
|
await completeOnboarding(page, undefined, { mainConversation: true });
|
||||||
|
await expect(page.getByTestId("bot-settings-trigger")).not.toContainText("Gruppenunterhaltung");
|
||||||
|
await expect(page.getByTestId("oo-group-stage")).toContainText(/1:1/i);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe("mobile shell", () => {
|
||||||
|
test.use({
|
||||||
|
hasTouch: true,
|
||||||
|
isMobile: true,
|
||||||
|
viewport: { width: 390, height: 844 },
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Escape closes the mobile navigation drawer", async ({ page }) => {
|
||||||
|
const stamp = Date.now();
|
||||||
|
await signup(page, `mobile-esc-${stamp}@rakazo.test`, "password12", "Escape");
|
||||||
|
await completeOnboarding(page);
|
||||||
|
await page.getByRole("button", { name: "Open navigation" }).click();
|
||||||
|
await expect(page.getByRole("button", { name: "Close navigation" })).toBeVisible();
|
||||||
|
await page.keyboard.press("Escape");
|
||||||
|
await expect(page.getByRole("button", { name: "Close navigation" })).toHaveCount(0);
|
||||||
|
await expect(page.getByTestId("open-navigation")).toBeFocused();
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -87,6 +87,16 @@ async function visible(locator: Locator, timeout: number) {
|
|||||||
return locator.isVisible({ timeout }).catch(() => false);
|
return locator.isVisible({ timeout }).catch(() => false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const onboardingContinueButton = /^(Continue|Weiter)$/i;
|
||||||
|
const onboardingSkipButton = /^(Skip|Überspringen)$/i;
|
||||||
|
const onboardingLanguageSkipButton = /^(Skip|Überspringen) ·/i;
|
||||||
|
|
||||||
|
/** After the look picker, skip the optional Oo naming step (keeps the default name). */
|
||||||
|
export async function skipOnboardingNameStep(page: Page) {
|
||||||
|
await expect(page.getByTestId("onboarding-name-step")).toBeVisible();
|
||||||
|
await page.getByRole("button", { name: onboardingSkipButton }).click();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Walks the first-run wizard, which lands in the main conversation with the main bot's first
|
* Walks the first-run wizard, which lands in the main conversation with the main bot's first
|
||||||
* onboarding question pending (the composer works regardless). Unless `mainConversation` is
|
* onboarding question pending (the composer works regardless). Unless `mainConversation` is
|
||||||
@ -95,23 +105,29 @@ async function visible(locator: Locator, timeout: number) {
|
|||||||
export async function completeOnboarding(
|
export async function completeOnboarding(
|
||||||
page: Page,
|
page: Page,
|
||||||
testInfo?: TestInfo,
|
testInfo?: TestInfo,
|
||||||
options: { mainConversation?: boolean } = {},
|
options: { mainConversation?: boolean; ooName?: string } = {},
|
||||||
) {
|
) {
|
||||||
await page.waitForURL(/\/(onboarding|app)/, { timeout: 20_000 });
|
await page.waitForURL(/\/(onboarding|app)/, { timeout: 20_000 });
|
||||||
|
|
||||||
if (page.url().includes("/onboarding")) {
|
if (page.url().includes("/onboarding")) {
|
||||||
await expect(page.locator('[data-rakazo-surface="onboarding"]')).toHaveAttribute(
|
await expect(page.locator('[data-rakazo-surface="onboarding"]')).toHaveAttribute(
|
||||||
"data-onboarding-step",
|
"data-onboarding-step",
|
||||||
/^(emoji|language|voice|integrations|scan|bot)$/,
|
/^(emoji|name|language|voice|integrations|scan|bot)$/,
|
||||||
{ timeout: 30_000 },
|
{ timeout: 30_000 },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const design = page.getByRole("heading", { name: /Design your Oo/i });
|
const design = page.getByRole("heading", { name: /Design your Oo|Gestalte dein Oo/i });
|
||||||
const language = page.getByRole("heading", { name: /Choose your language/i });
|
const naming = page.getByTestId("onboarding-name-step");
|
||||||
const voice = page.getByRole("heading", { name: /Pick a voice/i });
|
const language = page.getByRole("heading", {
|
||||||
const integrations = page.getByRole("heading", { name: "Server integrations", exact: true });
|
name: /Choose your language|Wähle deine Sprache/i,
|
||||||
const chief = page.getByText("Chief").first();
|
});
|
||||||
|
const voice = page.getByRole("heading", { name: /Pick a voice|Wähle eine Stimme/i });
|
||||||
|
const integrations = page.getByRole("heading", {
|
||||||
|
name: /Server integrations|Server-Integrationen/i,
|
||||||
|
});
|
||||||
|
const expectedOoName = options.ooName ?? "Chief";
|
||||||
|
const chief = page.getByText(expectedOoName).first();
|
||||||
|
|
||||||
// Scripted CI has needsModel false, so a fresh owner starts on integrations.
|
// Scripted CI has needsModel false, so a fresh owner starts on integrations.
|
||||||
// A model-gated run still opens the look picker first.
|
// A model-gated run still opens the look picker first.
|
||||||
@ -119,23 +135,30 @@ export async function completeOnboarding(
|
|||||||
await expect(entry.first()).toBeVisible({ timeout: 20_000 });
|
await expect(entry.first()).toBeVisible({ timeout: 20_000 });
|
||||||
|
|
||||||
if (await design.isVisible()) {
|
if (await design.isVisible()) {
|
||||||
await page.getByRole("button", { name: "Continue", exact: true }).click();
|
await page.getByRole("button", { name: onboardingContinueButton }).click();
|
||||||
|
await expect(naming).toBeVisible();
|
||||||
|
if (options.ooName) {
|
||||||
|
await page.getByTestId("onboarding-oo-name-input").fill(options.ooName);
|
||||||
|
await naming.getByRole("button", { name: onboardingContinueButton }).click();
|
||||||
|
} else {
|
||||||
|
await page.getByRole("button", { name: onboardingSkipButton }).click();
|
||||||
|
}
|
||||||
await expect(language).toBeVisible();
|
await expect(language).toBeVisible();
|
||||||
await page.getByRole("button", { name: /Skip/i }).click();
|
await page.getByRole("button", { name: onboardingLanguageSkipButton }).click();
|
||||||
await expect(voice).toBeVisible();
|
await expect(voice).toBeVisible();
|
||||||
await page.getByRole("button", { name: "Skip", exact: true }).click();
|
await page.getByRole("button", { name: onboardingSkipButton }).click();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (await visible(integrations, 8_000)) {
|
if (await visible(integrations, 8_000)) {
|
||||||
if (testInfo) await captureScreenshot(page, testInfo, "02-connect-apps");
|
if (testInfo) await captureScreenshot(page, testInfo, "02-connect-apps");
|
||||||
await page.getByRole("button", { name: "Skip", exact: true }).click();
|
await page.getByRole("button", { name: onboardingSkipButton }).click();
|
||||||
}
|
}
|
||||||
|
|
||||||
// The scan step, when shown, either asks first (skip it) or advances by itself. Onboarding
|
// The scan step, when shown, either asks first (skip it) or advances by itself. Onboarding
|
||||||
// then lands in the main conversation.
|
// then lands in the main conversation.
|
||||||
const skipScan = page
|
const skipScan = page
|
||||||
.locator('[data-onboarding-step="scan"]')
|
.locator('[data-onboarding-step="scan"]')
|
||||||
.getByRole("button", { name: "Skip", exact: true });
|
.getByRole("button", { name: onboardingSkipButton });
|
||||||
await expect(async () => {
|
await expect(async () => {
|
||||||
if (await skipScan.isVisible()) await skipScan.click();
|
if (await skipScan.isVisible()) await skipScan.click();
|
||||||
expect(page.url()).toMatch(/\/app\/g\/[^/]+$/);
|
expect(page.url()).toMatch(/\/app\/g\/[^/]+$/);
|
||||||
@ -171,15 +194,17 @@ export async function signup(
|
|||||||
) {
|
) {
|
||||||
await expect(async () => {
|
await expect(async () => {
|
||||||
await page.goto("/sign-up", { waitUntil: "domcontentloaded" });
|
await page.goto("/sign-up", { waitUntil: "domcontentloaded" });
|
||||||
await expect(page.getByRole("heading", { name: "Create your evOo" })).toBeVisible({
|
await expect(
|
||||||
|
page.getByRole("heading", { name: /Create your evOo|Dein evOo erstellen/i }),
|
||||||
|
).toBeVisible({
|
||||||
timeout: 5_000,
|
timeout: 5_000,
|
||||||
});
|
});
|
||||||
}).toPass({ timeout: 45_000 });
|
}).toPass({ timeout: 45_000 });
|
||||||
if (testInfo) await captureScreenshot(page, testInfo, "01-sign-up");
|
if (testInfo) await captureScreenshot(page, testInfo, "01-sign-up");
|
||||||
await page.getByPlaceholder("Your name").fill(name);
|
await page.getByPlaceholder(/Your name|Dein Name/i).fill(name);
|
||||||
await page.getByPlaceholder("Your email address").fill(email);
|
await page.getByPlaceholder(/Your email address|Deine E-Mail-Adresse/i).fill(email);
|
||||||
await page.getByPlaceholder("Password").fill(password);
|
await page.getByPlaceholder(/^Password$|^Passwort$/i).fill(password);
|
||||||
await page.getByRole("button", { name: "Create account" }).click();
|
await page.getByRole("button", { name: /Create account|Konto erstellen/i }).click();
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function captureScreenshot(page: Page, testInfo: TestInfo, name: string) {
|
export async function captureScreenshot(page: Page, testInfo: TestInfo, name: string) {
|
||||||
@ -274,7 +299,7 @@ export async function waitForChatComposer(page: Page) {
|
|||||||
await expect(page.getByTestId("shell-root")).toHaveAttribute("data-ready", "true", {
|
await expect(page.getByTestId("shell-root")).toHaveAttribute("data-ready", "true", {
|
||||||
timeout: 30_000,
|
timeout: 30_000,
|
||||||
});
|
});
|
||||||
await expect(page.getByRole("combobox", { name: "Message" })).toBeVisible();
|
await expect(page.getByRole("combobox", { name: /^Message$|^Nachricht$/i })).toBeVisible();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Create a named bot via RPC for test setup (skips the + picker). */
|
/** Create a named bot via RPC for test setup (skips the + picker). */
|
||||||
|
|||||||
@ -18,7 +18,10 @@ test("Heute shows native brief instead of Mission Control iframe", async ({ page
|
|||||||
await completeOnboarding(page);
|
await completeOnboarding(page);
|
||||||
|
|
||||||
await openHeute(page);
|
await openHeute(page);
|
||||||
await expect(page).toHaveURL(/\/app\/[^/]+$/);
|
await expect(page).toHaveURL(/\/app\/heute$/);
|
||||||
|
await page.reload();
|
||||||
|
await expect(page.getByTestId("heute-brief")).toBeVisible();
|
||||||
|
await expect(page).toHaveURL(/\/app\/heute$/);
|
||||||
|
|
||||||
const brief = page.getByTestId("heute-brief");
|
const brief = page.getByTestId("heute-brief");
|
||||||
await expect(brief).toBeVisible();
|
await expect(brief).toBeVisible();
|
||||||
@ -81,12 +84,21 @@ test("Heute shows the coach's brief, habits and goals, checks habits off and set
|
|||||||
await expect(page.getByTestId(`heute-habit-streak-${habit.id}`)).toHaveText("3 days");
|
await expect(page.getByTestId(`heute-habit-streak-${habit.id}`)).toHaveText("3 days");
|
||||||
await captureScreenshot(page, testInfo, "heute-coach");
|
await captureScreenshot(page, testInfo, "heute-coach");
|
||||||
|
|
||||||
await page.reload();
|
await page.reload({ waitUntil: "domcontentloaded" });
|
||||||
await openHeute(page);
|
await expect(page).toHaveURL(/\/app\/heute$/);
|
||||||
|
await expect(page.getByTestId("heute-brief")).toBeVisible();
|
||||||
await expect(page.getByTestId(`heute-habit-${habit.id}`).getByRole("checkbox")).toBeChecked();
|
await expect(page.getByTestId(`heute-habit-${habit.id}`).getByRole("checkbox")).toBeChecked();
|
||||||
|
|
||||||
// Check-in times live behind one compact control.
|
// Check-in times live behind one compact control.
|
||||||
await page.getByTestId("heute-rhythm").click();
|
const rhythm = page.getByTestId("heute-rhythm");
|
||||||
|
await expect(page.getByTestId("heute-coach-brief")).toBeVisible();
|
||||||
|
await rhythm.scrollIntoViewIfNeeded();
|
||||||
|
await expect(rhythm).toBeVisible({ timeout: 30_000 });
|
||||||
|
const rhythmLoaded = page.waitForResponse(
|
||||||
|
(response) => response.url().includes("/rpc/coach/rhythm/get") && response.ok(),
|
||||||
|
);
|
||||||
|
await rhythm.click();
|
||||||
|
await rhythmLoaded;
|
||||||
const panel = page.getByTestId("heute-rhythm-panel");
|
const panel = page.getByTestId("heute-rhythm-panel");
|
||||||
await expect(panel).toBeVisible();
|
await expect(panel).toBeVisible();
|
||||||
const morning = panel.getByTestId("heute-rhythm-morning");
|
const morning = panel.getByTestId("heute-rhythm-morning");
|
||||||
|
|||||||
@ -11,7 +11,7 @@ test("focus choice suggests apps and preserves a completed connection", async ({
|
|||||||
const stamp = Date.now();
|
const stamp = Date.now();
|
||||||
await signup(page, `onboarding-${stamp}@rakazo.test`, "password12", "Robin");
|
await signup(page, `onboarding-${stamp}@rakazo.test`, "password12", "Robin");
|
||||||
// The main bot greets the user in the main conversation, where onboarding lands.
|
// The main bot greets the user in the main conversation, where onboarding lands.
|
||||||
await completeOnboarding(page, undefined, { mainConversation: true });
|
await completeOnboarding(page, undefined, { mainConversation: true, ooName: "Robin" });
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
page.getByText("Hey Robin. Fresh start on my side, so I’ll keep this short."),
|
page.getByText("Hey Robin. Fresh start on my side, so I’ll keep this short."),
|
||||||
@ -31,12 +31,11 @@ test("focus choice suggests apps and preserves a completed connection", async ({
|
|||||||
await captureScreenshot(page, testInfo, "choice-card-onboarding");
|
await captureScreenshot(page, testInfo, "choice-card-onboarding");
|
||||||
|
|
||||||
await page.getByTestId("focus-choice-day").click();
|
await page.getByTestId("focus-choice-day").click();
|
||||||
// The focus step suggests apps but must not rename the bot: the name the
|
// The focus step must not rename the bot: onboarding keeps the chosen name.
|
||||||
// user chose during creation ("Chief") is preserved.
|
|
||||||
await expect(
|
await expect(
|
||||||
page.getByTestId("bot-settings-trigger").getByText("Chief", { exact: true }),
|
page.getByTestId("bot-settings-trigger").getByText("Robin", { exact: true }),
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
expect((await rpc<{ name: string }>(page, "bots/ensureMain", {})).name).toBe("Chief");
|
expect((await rpc<{ name: string }>(page, "bots/ensureMain", {})).name).toBe("Robin");
|
||||||
await expect(page.getByRole("combobox", { name: "Message" })).toBeVisible();
|
await expect(page.getByRole("combobox", { name: "Message" })).toBeVisible();
|
||||||
// The app cards speak for themselves; onboarding adds no text around them.
|
// The app cards speak for themselves; onboarding adds no text around them.
|
||||||
await expect(page.getByText(/^Got it/)).toHaveCount(0);
|
await expect(page.getByText(/^Got it/)).toHaveCount(0);
|
||||||
@ -103,7 +102,7 @@ test("focus choice suggests apps and preserves a completed connection", async ({
|
|||||||
|
|
||||||
test("choice refresh failures leave options available for retry", async ({ page }) => {
|
test("choice refresh failures leave options available for retry", async ({ page }) => {
|
||||||
await signup(page, `choice-refresh-${Date.now()}@rakazo.test`, "password12", "Choice Retry");
|
await signup(page, `choice-refresh-${Date.now()}@rakazo.test`, "password12", "Choice Retry");
|
||||||
await completeOnboarding(page, undefined, { mainConversation: true });
|
await completeOnboarding(page, undefined, { mainConversation: true, ooName: "Robin" });
|
||||||
const choice = page.getByTestId("focus-choice-day");
|
const choice = page.getByTestId("focus-choice-day");
|
||||||
await expect(choice).toBeEnabled();
|
await expect(choice).toBeEnabled();
|
||||||
// Keep the existing choice rendered while its save succeeds and navigation refresh fails.
|
// Keep the existing choice rendered while its save succeeds and navigation refresh fails.
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { expect, test } from "@playwright/test";
|
import { expect, test } from "@playwright/test";
|
||||||
import { captureScreenshot, signup } from "./helpers";
|
import { captureScreenshot, signup, skipOnboardingNameStep } from "./helpers";
|
||||||
|
|
||||||
test.beforeEach(async ({ page }) => {
|
test.beforeEach(async ({ page }) => {
|
||||||
await page.route("**/rpc/integrationSetup/get", (route) =>
|
await page.route("**/rpc/integrationSetup/get", (route) =>
|
||||||
@ -49,11 +49,12 @@ test("emoji-first onboarding: shape + color selection with live preview", async
|
|||||||
|
|
||||||
if (testInfo) await captureScreenshot(page, testInfo, "02-shape-color-preview");
|
if (testInfo) await captureScreenshot(page, testInfo, "02-shape-color-preview");
|
||||||
|
|
||||||
// Click Continue → goes to the language step
|
// Click Continue → naming, then language.
|
||||||
await page
|
await page
|
||||||
.getByRole("button", { name: /Continue/i })
|
.getByRole("button", { name: /Continue/i })
|
||||||
.first()
|
.first()
|
||||||
.click();
|
.click();
|
||||||
|
await skipOnboardingNameStep(page);
|
||||||
|
|
||||||
// Step 2: Language selection
|
// Step 2: Language selection
|
||||||
await expect(page.getByRole("heading", { name: /Choose your language/i })).toBeVisible();
|
await expect(page.getByRole("heading", { name: /Choose your language/i })).toBeVisible();
|
||||||
@ -96,6 +97,7 @@ test("onboarding: skip language defaults to English", async ({ page }) => {
|
|||||||
.getByRole("button", { name: /Continue/i })
|
.getByRole("button", { name: /Continue/i })
|
||||||
.first()
|
.first()
|
||||||
.click();
|
.click();
|
||||||
|
await skipOnboardingNameStep(page);
|
||||||
|
|
||||||
// Language step — click Skip, then skip voice.
|
// Language step — click Skip, then skip voice.
|
||||||
await expect(page.getByRole("heading", { name: /Choose your language/i })).toBeVisible();
|
await expect(page.getByRole("heading", { name: /Choose your language/i })).toBeVisible();
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import type { Page } from "@playwright/test";
|
import type { Page } from "@playwright/test";
|
||||||
import { expect, test } from "@playwright/test";
|
import { expect, test } from "@playwright/test";
|
||||||
import { captureScreenshot, signup } from "./helpers";
|
import { captureScreenshot, signup, skipOnboardingNameStep } from "./helpers";
|
||||||
|
|
||||||
test.beforeEach(async ({ page }) => {
|
test.beforeEach(async ({ page }) => {
|
||||||
await page.route("**/rpc/integrationSetup/get", (route) =>
|
await page.route("**/rpc/integrationSetup/get", (route) =>
|
||||||
@ -77,5 +77,6 @@ test("mobile onboarding: category pick and swipe look previews", async ({ page }
|
|||||||
.getByRole("button", { name: /Continue/i })
|
.getByRole("button", { name: /Continue/i })
|
||||||
.first()
|
.first()
|
||||||
.click();
|
.click();
|
||||||
|
await skipOnboardingNameStep(page);
|
||||||
await expect(page.getByRole("heading", { name: /Choose your language/i })).toBeVisible();
|
await expect(page.getByRole("heading", { name: /Choose your language/i })).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|||||||
@ -4,7 +4,14 @@
|
|||||||
* with its origin, confirms a pending one, and forgets one or all of them.
|
* with its origin, confirms a pending one, and forgets one or all of them.
|
||||||
*/
|
*/
|
||||||
import { expect, test } from "@playwright/test";
|
import { expect, test } from "@playwright/test";
|
||||||
import { captureScreenshot, openUserSettings, rpc, signup, waitForChatComposer } from "./helpers";
|
import {
|
||||||
|
captureScreenshot,
|
||||||
|
openUserSettings,
|
||||||
|
rpc,
|
||||||
|
signup,
|
||||||
|
skipOnboardingNameStep,
|
||||||
|
waitForChatComposer,
|
||||||
|
} from "./helpers";
|
||||||
|
|
||||||
test.use({ timezoneId: "Europe/Vienna" });
|
test.use({ timezoneId: "Europe/Vienna" });
|
||||||
|
|
||||||
@ -34,6 +41,7 @@ test("onboarding feeds the profile; facts can be confirmed and forgotten", async
|
|||||||
timeout: 30_000,
|
timeout: 30_000,
|
||||||
});
|
});
|
||||||
await page.getByRole("button", { name: "Continue", exact: true }).click();
|
await page.getByRole("button", { name: "Continue", exact: true }).click();
|
||||||
|
await skipOnboardingNameStep(page);
|
||||||
|
|
||||||
await expect(page.getByRole("heading", { name: /Choose your language/i })).toBeVisible();
|
await expect(page.getByRole("heading", { name: /Choose your language/i })).toBeVisible();
|
||||||
await page.getByRole("button", { name: "English", exact: true }).click();
|
await page.getByRole("button", { name: "English", exact: true }).click();
|
||||||
|
|||||||
@ -120,6 +120,10 @@ function SessionApp() {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Route path="/app" element={user ? <ShellPage /> : <Navigate to="/sign-in" replace />} />
|
<Route path="/app" element={user ? <ShellPage /> : <Navigate to="/sign-in" replace />} />
|
||||||
|
<Route
|
||||||
|
path="/app/heute"
|
||||||
|
element={user ? <ShellPage /> : <Navigate to="/sign-in" replace />}
|
||||||
|
/>
|
||||||
<Route
|
<Route
|
||||||
path="/app/g/:groupId"
|
path="/app/g/:groupId"
|
||||||
element={user ? <ShellPage /> : <Navigate to="/sign-in" replace />}
|
element={user ? <ShellPage /> : <Navigate to="/sign-in" replace />}
|
||||||
|
|||||||
98
apps/web/src/components/onboarding/OnboardingNameStep.tsx
Normal file
98
apps/web/src/components/onboarding/OnboardingNameStep.tsx
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
import { Trans, useLingui } from "@lingui/react/macro";
|
||||||
|
import type { EvooPose } from "@rakazo/ui-web";
|
||||||
|
import { BotAvatar, Button } from "@rakazo/ui-web";
|
||||||
|
import { useId, useState } from "react";
|
||||||
|
import type { OnboardingEmojiConfig } from "./OnboardingOoDesigner";
|
||||||
|
|
||||||
|
export const DEFAULT_FIRST_OO_NAME = "Chief";
|
||||||
|
|
||||||
|
type OnboardingNameStepProps = {
|
||||||
|
config: OnboardingEmojiConfig;
|
||||||
|
name: string;
|
||||||
|
onNameChange: (name: string) => void;
|
||||||
|
onBack: () => void;
|
||||||
|
onContinue: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function OnboardingNameStep({
|
||||||
|
config,
|
||||||
|
name,
|
||||||
|
onNameChange,
|
||||||
|
onBack,
|
||||||
|
onContinue,
|
||||||
|
}: OnboardingNameStepProps) {
|
||||||
|
const { t } = useLingui();
|
||||||
|
const inputId = useId();
|
||||||
|
const [draft, setDraft] = useState(name);
|
||||||
|
|
||||||
|
function commitAndContinue() {
|
||||||
|
const trimmed = draft.trim() || DEFAULT_FIRST_OO_NAME;
|
||||||
|
onNameChange(trimmed);
|
||||||
|
onContinue();
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mx-auto max-w-xl" data-testid="onboarding-name-step">
|
||||||
|
<h2 className="text-xl font-semibold tracking-tight text-foreground">
|
||||||
|
<Trans>Name your Oo</Trans>
|
||||||
|
</h2>
|
||||||
|
<p className="mt-1 text-sm text-muted-foreground">
|
||||||
|
<Trans>Something short and yours — or keep the default and rename later.</Trans>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="mt-8 flex flex-col items-center gap-4 sm:flex-row sm:items-start">
|
||||||
|
<BotAvatar
|
||||||
|
color={config.avatarValue}
|
||||||
|
size={120}
|
||||||
|
identity="onboarding-name-preview"
|
||||||
|
pose={config.pose as EvooPose}
|
||||||
|
interactive
|
||||||
|
className="shrink-0"
|
||||||
|
/>
|
||||||
|
<div className="w-full min-w-0 flex-1">
|
||||||
|
<label htmlFor={inputId} className="sr-only">
|
||||||
|
<Trans>Oo name</Trans>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id={inputId}
|
||||||
|
data-testid="onboarding-oo-name-input"
|
||||||
|
type="text"
|
||||||
|
maxLength={48}
|
||||||
|
autoComplete="off"
|
||||||
|
autoCapitalize="words"
|
||||||
|
placeholder={DEFAULT_FIRST_OO_NAME}
|
||||||
|
value={draft}
|
||||||
|
onChange={(event) => setDraft(event.target.value)}
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
if (event.key === "Enter") commitAndContinue();
|
||||||
|
}}
|
||||||
|
className="w-full rounded-2xl border-2 border-border bg-card px-4 py-3 text-lg font-semibold tracking-tight text-foreground shadow-sm outline-none transition-colors placeholder:text-muted-foreground/60 focus:border-primary"
|
||||||
|
/>
|
||||||
|
<p className="mt-2 text-xs text-muted-foreground">
|
||||||
|
{t`Press Enter or Continue — skip keeps ${DEFAULT_FIRST_OO_NAME}.`}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-8 flex items-center justify-between">
|
||||||
|
<Button variant="ghost" size="sm" onClick={onBack}>
|
||||||
|
<Trans>Back</Trans>
|
||||||
|
</Button>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => {
|
||||||
|
onNameChange(DEFAULT_FIRST_OO_NAME);
|
||||||
|
onContinue();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Trans>Skip</Trans>
|
||||||
|
</Button>
|
||||||
|
<Button onClick={commitAndContinue}>
|
||||||
|
<Trans>Continue</Trans>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,15 +1,20 @@
|
|||||||
import { Trans } from "@lingui/react/macro";
|
import { Trans } from "@lingui/react/macro";
|
||||||
import type { VoiceInfo } from "@rakazo/contracts";
|
import type { VoiceInfo } from "@rakazo/contracts";
|
||||||
import { defaultVoiceForLocale, voicesForLocale } from "@rakazo/core";
|
|
||||||
import { Button } from "@rakazo/ui-web";
|
import { Button } from "@rakazo/ui-web";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
import {
|
||||||
|
defaultOnboardingVoiceId,
|
||||||
|
offlineOnboardingVoices,
|
||||||
|
onboardingVoicesFromList,
|
||||||
|
} from "../../lib/onboarding-voices";
|
||||||
import { rpc } from "../../lib/rpc";
|
import { rpc } from "../../lib/rpc";
|
||||||
import { readUiLocale } from "../../lib/ui-locale";
|
import type { UiLocale } from "../../lib/ui-locale";
|
||||||
|
|
||||||
/** Must match packages/adapters KokoroVoiceProvider placeholder. */
|
/** Must match packages/adapters KokoroVoiceProvider placeholder. */
|
||||||
export const KOKORO_LOCAL_TOKEN = "local-kokoro";
|
export const KOKORO_LOCAL_TOKEN = "local-kokoro";
|
||||||
|
|
||||||
type OnboardingVoiceStepProps = {
|
type OnboardingVoiceStepProps = {
|
||||||
|
uiLocale: UiLocale;
|
||||||
selectedVoiceId: string;
|
selectedVoiceId: string;
|
||||||
onSelectedVoiceIdChange: (voiceId: string) => void;
|
onSelectedVoiceIdChange: (voiceId: string) => void;
|
||||||
onBack: () => void;
|
onBack: () => void;
|
||||||
@ -18,6 +23,7 @@ type OnboardingVoiceStepProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function OnboardingVoiceStep({
|
export function OnboardingVoiceStep({
|
||||||
|
uiLocale,
|
||||||
selectedVoiceId,
|
selectedVoiceId,
|
||||||
onSelectedVoiceIdChange,
|
onSelectedVoiceIdChange,
|
||||||
onBack,
|
onBack,
|
||||||
@ -36,13 +42,17 @@ export function OnboardingVoiceStep({
|
|||||||
await rpc.voice.connect({ provider: "kokoro", apiKey: KOKORO_LOCAL_TOKEN });
|
await rpc.voice.connect({ provider: "kokoro", apiKey: KOKORO_LOCAL_TOKEN });
|
||||||
const listed = await rpc.voice.voices({ provider: "kokoro" });
|
const listed = await rpc.voice.voices({ provider: "kokoro" });
|
||||||
if (cancelled) return;
|
if (cancelled) return;
|
||||||
const locale = readUiLocale();
|
const ordered = onboardingVoicesFromList(listed, uiLocale);
|
||||||
const ordered = voicesForLocale(listed, locale).slice(0, 6);
|
|
||||||
setVoiceOptions(ordered);
|
setVoiceOptions(ordered);
|
||||||
const defaultVoice = defaultVoiceForLocale(listed, locale);
|
const nextId = defaultOnboardingVoiceId(uiLocale, ordered);
|
||||||
if (defaultVoice) onSelectedVoiceIdChange(defaultVoice.id);
|
if (nextId) onSelectedVoiceIdChange(nextId);
|
||||||
} catch {
|
} catch {
|
||||||
if (!cancelled) setVoiceOptions([]);
|
if (!cancelled) {
|
||||||
|
const fallback = offlineOnboardingVoices(uiLocale);
|
||||||
|
setVoiceOptions(fallback);
|
||||||
|
const nextId = defaultOnboardingVoiceId(uiLocale, fallback);
|
||||||
|
if (nextId) onSelectedVoiceIdChange(nextId);
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
if (!cancelled) setVoiceLoading(false);
|
if (!cancelled) setVoiceLoading(false);
|
||||||
}
|
}
|
||||||
@ -54,7 +64,7 @@ export function OnboardingVoiceStep({
|
|||||||
return () => {
|
return () => {
|
||||||
cancelled = true;
|
cancelled = true;
|
||||||
};
|
};
|
||||||
}, [onSelectedVoiceIdChange]);
|
}, [onSelectedVoiceIdChange, uiLocale]);
|
||||||
|
|
||||||
async function skipVoice() {
|
async function skipVoice() {
|
||||||
await previewTask.current;
|
await previewTask.current;
|
||||||
@ -82,6 +92,7 @@ export function OnboardingVoiceStep({
|
|||||||
<button
|
<button
|
||||||
key={voice.id}
|
key={voice.id}
|
||||||
type="button"
|
type="button"
|
||||||
|
data-testid={`onboarding-voice-${voice.id}`}
|
||||||
onClick={() => onSelectedVoiceIdChange(voice.id)}
|
onClick={() => onSelectedVoiceIdChange(voice.id)}
|
||||||
aria-pressed={selectedVoiceId === voice.id}
|
aria-pressed={selectedVoiceId === voice.id}
|
||||||
className={`rounded-xl border-2 px-3 py-3 text-left text-sm font-medium transition-all ${
|
className={`rounded-xl border-2 px-3 py-3 text-left text-sm font-medium transition-all ${
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import {
|
|||||||
resolveBotPersona,
|
resolveBotPersona,
|
||||||
resolveTextStagePhase,
|
resolveTextStagePhase,
|
||||||
} from "../../lib/bot-persona";
|
} from "../../lib/bot-persona";
|
||||||
|
import { isMultiOoGroupChat } from "../../lib/group-chat";
|
||||||
import {
|
import {
|
||||||
extractGroupSpark,
|
extractGroupSpark,
|
||||||
type GroupStageMember,
|
type GroupStageMember,
|
||||||
@ -317,6 +318,7 @@ export function OoGroupStage({
|
|||||||
[snapshot, speaker, enrichMember],
|
[snapshot, speaker, enrichMember],
|
||||||
);
|
);
|
||||||
const roster = snapshot?.members ?? [];
|
const roster = snapshot?.members ?? [];
|
||||||
|
const multiOo = isMultiOoGroupChat(roster.length);
|
||||||
const groupSpark = useMemo(
|
const groupSpark = useMemo(
|
||||||
() => extractGroupSpark(snapshot, workingBots.length),
|
() => extractGroupSpark(snapshot, workingBots.length),
|
||||||
[snapshot, workingBots.length],
|
[snapshot, workingBots.length],
|
||||||
@ -405,7 +407,7 @@ export function OoGroupStage({
|
|||||||
if (sparkLabel && groupSpark?.kind === "peer-reply") return sparkLabel;
|
if (sparkLabel && groupSpark?.kind === "peer-reply") return sparkLabel;
|
||||||
if (awarenessLabel) return awarenessLabel;
|
if (awarenessLabel) return awarenessLabel;
|
||||||
if (sparkLabel) return sparkLabel;
|
if (sparkLabel) return sparkLabel;
|
||||||
if (!speaker) return t`Group conversation`;
|
if (!speaker) return multiOo ? t`Group conversation` : t`1:1 conversation`;
|
||||||
if (running && workingBots.length > 1) {
|
if (running && workingBots.length > 1) {
|
||||||
return t`${workingBots.length} Oos are working — ${speaker.name} has the floor`;
|
return t`${workingBots.length} Oos are working — ${speaker.name} has the floor`;
|
||||||
}
|
}
|
||||||
@ -429,8 +431,15 @@ export function OoGroupStage({
|
|||||||
thinkingIndex,
|
thinkingIndex,
|
||||||
thinkingLines,
|
thinkingLines,
|
||||||
workingBots.length,
|
workingBots.length,
|
||||||
|
multiOo,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const headerSubtitle = multiOo
|
||||||
|
? t`Group conversation`
|
||||||
|
: speaker
|
||||||
|
? t`1:1 with ${speaker.name}`
|
||||||
|
: t`1:1 conversation`;
|
||||||
|
|
||||||
const energy = groupStageEnergy(phase, groupSpark, workingBots.length);
|
const energy = groupStageEnergy(phase, groupSpark, workingBots.length);
|
||||||
|
|
||||||
const phaseLabel = showEnsembleOnly
|
const phaseLabel = showEnsembleOnly
|
||||||
@ -454,7 +463,7 @@ export function OoGroupStage({
|
|||||||
return (
|
return (
|
||||||
<section
|
<section
|
||||||
data-testid="oo-group-stage"
|
data-testid="oo-group-stage"
|
||||||
aria-label={t`Group conversation`}
|
aria-label={multiOo ? t`Group conversation` : t`1:1 conversation`}
|
||||||
className={`relative shrink-0 overflow-hidden border-b border-border/60 ${hasMessages ? "max-h-[28vh]" : ""}`}
|
className={`relative shrink-0 overflow-hidden border-b border-border/60 ${hasMessages ? "max-h-[28vh]" : ""}`}
|
||||||
>
|
>
|
||||||
<TextStageCanvas
|
<TextStageCanvas
|
||||||
@ -479,7 +488,7 @@ export function OoGroupStage({
|
|||||||
<h2 className="mt-1.5 truncate text-ui-title font-semibold tracking-tight text-foreground">
|
<h2 className="mt-1.5 truncate text-ui-title font-semibold tracking-tight text-foreground">
|
||||||
{groupName ?? t`Group`}
|
{groupName ?? t`Group`}
|
||||||
</h2>
|
</h2>
|
||||||
<p className="truncate text-ui-body-sm text-muted-foreground">{t`Group conversation`}</p>
|
<p className="truncate text-ui-body-sm text-muted-foreground">{headerSubtitle}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{awarenessLabel && awarenessEvent && speaker ? (
|
{awarenessLabel && awarenessEvent && speaker ? (
|
||||||
|
|||||||
5
apps/web/src/lib/app-routes.ts
Normal file
5
apps/web/src/lib/app-routes.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export const HEUTE_APP_PATH = "/app/heute";
|
||||||
|
|
||||||
|
export function isHeuteAppPath(pathname: string): boolean {
|
||||||
|
return pathname === HEUTE_APP_PATH || pathname.endsWith(`${HEUTE_APP_PATH}`);
|
||||||
|
}
|
||||||
14
apps/web/src/lib/group-chat.test.ts
Normal file
14
apps/web/src/lib/group-chat.test.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { isMultiOoGroupChat } from "./group-chat.js";
|
||||||
|
|
||||||
|
describe("isMultiOoGroupChat", () => {
|
||||||
|
it("treats a single Oo as 1:1", () => {
|
||||||
|
expect(isMultiOoGroupChat(0)).toBe(false);
|
||||||
|
expect(isMultiOoGroupChat(1)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("treats two or more Oos as a group", () => {
|
||||||
|
expect(isMultiOoGroupChat(2)).toBe(true);
|
||||||
|
expect(isMultiOoGroupChat(5)).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
4
apps/web/src/lib/group-chat.ts
Normal file
4
apps/web/src/lib/group-chat.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
/** True when the encounter has more than one Oo (group chat), not a 1:1 thread. */
|
||||||
|
export function isMultiOoGroupChat(memberCount: number): boolean {
|
||||||
|
return memberCount > 1;
|
||||||
|
}
|
||||||
41
apps/web/src/lib/onboarding-voices.test.ts
Normal file
41
apps/web/src/lib/onboarding-voices.test.ts
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import {
|
||||||
|
defaultOnboardingVoiceId,
|
||||||
|
offlineOnboardingVoices,
|
||||||
|
onboardingVoicesFromList,
|
||||||
|
} from "./onboarding-voices.js";
|
||||||
|
|
||||||
|
describe("offlineOnboardingVoices", () => {
|
||||||
|
it("offers German voices for de", () => {
|
||||||
|
expect(offlineOnboardingVoices("de").map((voice) => voice.id)).toEqual([
|
||||||
|
"df_eva",
|
||||||
|
"dm_bernd",
|
||||||
|
"df_victoria",
|
||||||
|
"dm_martin",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("offers English voices for en", () => {
|
||||||
|
const ids = offlineOnboardingVoices("en").map((voice) => voice.id);
|
||||||
|
expect(ids).toContain("af_heart");
|
||||||
|
expect(ids).not.toContain("df_eva");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("onboardingVoicesFromList", () => {
|
||||||
|
it("uses offline German presets when the server only lists English voices", () => {
|
||||||
|
const englishOnly = [{ id: "af_alloy", label: "Alloy", description: "neutral" }];
|
||||||
|
expect(onboardingVoicesFromList(englishOnly, "de").map((voice) => voice.id)).toEqual([
|
||||||
|
"df_eva",
|
||||||
|
"dm_bernd",
|
||||||
|
"df_victoria",
|
||||||
|
"dm_martin",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("defaultOnboardingVoiceId", () => {
|
||||||
|
it("prefers df_eva for German", () => {
|
||||||
|
expect(defaultOnboardingVoiceId("de", offlineOnboardingVoices("de"))).toBe("df_eva");
|
||||||
|
});
|
||||||
|
});
|
||||||
53
apps/web/src/lib/onboarding-voices.ts
Normal file
53
apps/web/src/lib/onboarding-voices.ts
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import type { VoiceInfo } from "@rakazo/contracts";
|
||||||
|
import { defaultVoiceForLocale, GERMAN_VOICE_PRESETS, voicesForLocale } from "@rakazo/core";
|
||||||
|
|
||||||
|
/** English presets when the local voice server is unreachable during onboarding. */
|
||||||
|
const ENGLISH_ONBOARDING_VOICES: VoiceInfo[] = [
|
||||||
|
{ id: "af_heart", label: "Heart", description: "warm" },
|
||||||
|
{ id: "af_bella", label: "Bella", description: "expressive" },
|
||||||
|
{ id: "af_sky", label: "Sky", description: "bright" },
|
||||||
|
{ id: "af_nova", label: "Nova", description: "energetic" },
|
||||||
|
{ id: "am_adam", label: "Adam", description: "clear" },
|
||||||
|
{ id: "am_michael", label: "Michael", description: "steady" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const OFFLINE_CATALOG = [...GERMAN_VOICE_PRESETS, ...ENGLISH_ONBOARDING_VOICES];
|
||||||
|
|
||||||
|
const KOKORO_LANGUAGE_PREFIXES: Record<string, string> = {
|
||||||
|
de: "d",
|
||||||
|
en: "ab",
|
||||||
|
es: "e",
|
||||||
|
fr: "f",
|
||||||
|
hi: "h",
|
||||||
|
it: "i",
|
||||||
|
ja: "j",
|
||||||
|
pt: "p",
|
||||||
|
zh: "z",
|
||||||
|
};
|
||||||
|
|
||||||
|
function listedVoicesSpeakLocale(listed: VoiceInfo[], locale: string): boolean {
|
||||||
|
const language = locale.toLowerCase().split("-")[0] ?? "";
|
||||||
|
const prefixes = KOKORO_LANGUAGE_PREFIXES[language];
|
||||||
|
if (!prefixes) return listed.length > 0;
|
||||||
|
return listed.some((voice) => {
|
||||||
|
const prefix = /^([a-z])[fm]_/.exec(voice.id)?.[1];
|
||||||
|
return Boolean(prefix && prefixes.includes(prefix));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Locale-aware voice choices for onboarding when listing from the server fails. */
|
||||||
|
export function offlineOnboardingVoices(locale: string): VoiceInfo[] {
|
||||||
|
return voicesForLocale(OFFLINE_CATALOG, locale).slice(0, 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Prefer server voices that speak the UI language; otherwise use offline presets. */
|
||||||
|
export function onboardingVoicesFromList(listed: VoiceInfo[], locale: string): VoiceInfo[] {
|
||||||
|
if (listedVoicesSpeakLocale(listed, locale)) {
|
||||||
|
return voicesForLocale(listed, locale).slice(0, 6);
|
||||||
|
}
|
||||||
|
return offlineOnboardingVoices(locale);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function defaultOnboardingVoiceId(locale: string, voices: VoiceInfo[]): string {
|
||||||
|
return defaultVoiceForLocale(voices, locale)?.id ?? "";
|
||||||
|
}
|
||||||
@ -303,6 +303,13 @@ msgstr "1:1-Unterhaltung"
|
|||||||
msgid "1:1 conversation with {title}"
|
msgid "1:1 conversation with {title}"
|
||||||
msgstr "1:1-Unterhaltung mit {title}"
|
msgstr "1:1-Unterhaltung mit {title}"
|
||||||
|
|
||||||
|
#. placeholder {0}: mainConversationEntry.bot.name
|
||||||
|
#. placeholder {0}: speaker.name
|
||||||
|
#: src/components/stage/OoStage.tsx
|
||||||
|
#: src/pages/Shell.tsx
|
||||||
|
msgid "1:1 with {0}"
|
||||||
|
msgstr "1:1 mit {0}"
|
||||||
|
|
||||||
#: src/pages/shell/bot-picker.tsx
|
#: src/pages/shell/bot-picker.tsx
|
||||||
msgid "About groups"
|
msgid "About groups"
|
||||||
msgstr "Über Gruppen"
|
msgstr "Über Gruppen"
|
||||||
@ -783,6 +790,7 @@ msgstr "Verfügbar, sobald die Routine gespeichert ist"
|
|||||||
|
|
||||||
#: src/components/browser/InAppBrowser.tsx
|
#: src/components/browser/InAppBrowser.tsx
|
||||||
#: src/components/CookingMode.tsx
|
#: src/components/CookingMode.tsx
|
||||||
|
#: src/components/onboarding/OnboardingNameStep.tsx
|
||||||
#: src/components/onboarding/OnboardingVoiceStep.tsx
|
#: src/components/onboarding/OnboardingVoiceStep.tsx
|
||||||
#: src/pages/Onboarding.tsx
|
#: src/pages/Onboarding.tsx
|
||||||
#: src/pages/PluginsOverlay.tsx
|
#: src/pages/PluginsOverlay.tsx
|
||||||
@ -1423,6 +1431,7 @@ msgid "Context limit"
|
|||||||
msgstr "Kontextlimit"
|
msgstr "Kontextlimit"
|
||||||
|
|
||||||
#: src/components/integrations/IntegrationSetup.tsx
|
#: src/components/integrations/IntegrationSetup.tsx
|
||||||
|
#: src/components/onboarding/OnboardingNameStep.tsx
|
||||||
#: src/components/onboarding/OnboardingOoDesigner.tsx
|
#: src/components/onboarding/OnboardingOoDesigner.tsx
|
||||||
#: src/components/onboarding/OnboardingScanStep.tsx
|
#: src/components/onboarding/OnboardingScanStep.tsx
|
||||||
#: src/components/onboarding/OnboardingVoiceStep.tsx
|
#: src/components/onboarding/OnboardingVoiceStep.tsx
|
||||||
@ -3317,6 +3326,10 @@ msgstr "Gruppe benennen"
|
|||||||
msgid "Name this routine"
|
msgid "Name this routine"
|
||||||
msgstr "Routine benennen"
|
msgstr "Routine benennen"
|
||||||
|
|
||||||
|
#: src/components/onboarding/OnboardingNameStep.tsx
|
||||||
|
msgid "Name your Oo"
|
||||||
|
msgstr "Gib deinem Oo einen Namen"
|
||||||
|
|
||||||
#: src/components/OoAtelier.tsx
|
#: src/components/OoAtelier.tsx
|
||||||
msgid "Neck"
|
msgid "Neck"
|
||||||
msgstr "Hals"
|
msgstr "Hals"
|
||||||
@ -3627,6 +3640,10 @@ msgstr "Nur leere Spaces können gelöscht werden. Lösche zuerst die Bots und G
|
|||||||
msgid "Only file and folder names are read."
|
msgid "Only file and folder names are read."
|
||||||
msgstr "Gelesen werden nur Datei- und Ordnernamen."
|
msgstr "Gelesen werden nur Datei- und Ordnernamen."
|
||||||
|
|
||||||
|
#: src/components/onboarding/OnboardingNameStep.tsx
|
||||||
|
msgid "Oo name"
|
||||||
|
msgstr "Name des Oo"
|
||||||
|
|
||||||
#: src/pages/ScratchpadSection.tsx
|
#: src/pages/ScratchpadSection.tsx
|
||||||
#: src/pages/shell/computer-panel-preview.tsx
|
#: src/pages/shell/computer-panel-preview.tsx
|
||||||
msgid "Open"
|
msgid "Open"
|
||||||
@ -3833,6 +3850,10 @@ msgstr "Vorlieben"
|
|||||||
msgid "Present"
|
msgid "Present"
|
||||||
msgstr "Anwesend"
|
msgstr "Anwesend"
|
||||||
|
|
||||||
|
#: src/components/onboarding/OnboardingNameStep.tsx
|
||||||
|
msgid "Press Enter or Continue — skip keeps {DEFAULT_FIRST_OO_NAME}."
|
||||||
|
msgstr "Enter oder Weiter — Überspringen behält Chief."
|
||||||
|
|
||||||
#. placeholder {0}: props.name
|
#. placeholder {0}: props.name
|
||||||
#: src/components/ArtifactFileCard.tsx
|
#: src/components/ArtifactFileCard.tsx
|
||||||
msgid "Preview {0}"
|
msgid "Preview {0}"
|
||||||
@ -4574,6 +4595,7 @@ msgid "Skills"
|
|||||||
msgstr "Skills"
|
msgstr "Skills"
|
||||||
|
|
||||||
#: src/components/integrations/IntegrationSetup.tsx
|
#: src/components/integrations/IntegrationSetup.tsx
|
||||||
|
#: src/components/onboarding/OnboardingNameStep.tsx
|
||||||
#: src/components/onboarding/OnboardingScanStep.tsx
|
#: src/components/onboarding/OnboardingScanStep.tsx
|
||||||
#: src/components/onboarding/OnboardingVoiceStep.tsx
|
#: src/components/onboarding/OnboardingVoiceStep.tsx
|
||||||
#: src/lib/onboarding-copy.ts
|
#: src/lib/onboarding-copy.ts
|
||||||
@ -4581,9 +4603,10 @@ msgstr "Skills"
|
|||||||
msgid "Skip"
|
msgid "Skip"
|
||||||
msgstr "Überspringen"
|
msgstr "Überspringen"
|
||||||
|
|
||||||
|
#. placeholder {0}: UI_LOCALE_LABELS[activeUiLocale]
|
||||||
#: src/pages/Onboarding.tsx
|
#: src/pages/Onboarding.tsx
|
||||||
msgid "Skip · English"
|
msgid "Skip · {0}"
|
||||||
msgstr "Überspringen · Englisch"
|
msgstr "Überspringen · {0}"
|
||||||
|
|
||||||
#: src/lib/localized-provider-hint.ts
|
#: src/lib/localized-provider-hint.ts
|
||||||
msgid "Skip or deploy key"
|
msgid "Skip or deploy key"
|
||||||
@ -4614,6 +4637,10 @@ msgstr "Software-Update"
|
|||||||
msgid "Something needs your attention."
|
msgid "Something needs your attention."
|
||||||
msgstr "Etwas braucht deine Aufmerksamkeit."
|
msgstr "Etwas braucht deine Aufmerksamkeit."
|
||||||
|
|
||||||
|
#: src/components/onboarding/OnboardingNameStep.tsx
|
||||||
|
msgid "Something short and yours — or keep the default and rename later."
|
||||||
|
msgstr "Kurz und persönlich — oder Standard behalten und später umbenennen."
|
||||||
|
|
||||||
#: src/lib/voice-persona-copy.ts
|
#: src/lib/voice-persona-copy.ts
|
||||||
msgid "Sorting priorities…"
|
msgid "Sorting priorities…"
|
||||||
msgstr "Sortiere die Prioritäten…"
|
msgstr "Sortiere die Prioritäten…"
|
||||||
|
|||||||
@ -303,6 +303,13 @@ msgstr "1:1 conversation"
|
|||||||
msgid "1:1 conversation with {title}"
|
msgid "1:1 conversation with {title}"
|
||||||
msgstr "1:1 conversation with {title}"
|
msgstr "1:1 conversation with {title}"
|
||||||
|
|
||||||
|
#. placeholder {0}: mainConversationEntry.bot.name
|
||||||
|
#. placeholder {0}: speaker.name
|
||||||
|
#: src/components/stage/OoStage.tsx
|
||||||
|
#: src/pages/Shell.tsx
|
||||||
|
msgid "1:1 with {0}"
|
||||||
|
msgstr "1:1 with {0}"
|
||||||
|
|
||||||
#: src/pages/shell/bot-picker.tsx
|
#: src/pages/shell/bot-picker.tsx
|
||||||
msgid "About groups"
|
msgid "About groups"
|
||||||
msgstr "About groups"
|
msgstr "About groups"
|
||||||
@ -783,6 +790,7 @@ msgstr "Available after the routine is saved"
|
|||||||
|
|
||||||
#: src/components/browser/InAppBrowser.tsx
|
#: src/components/browser/InAppBrowser.tsx
|
||||||
#: src/components/CookingMode.tsx
|
#: src/components/CookingMode.tsx
|
||||||
|
#: src/components/onboarding/OnboardingNameStep.tsx
|
||||||
#: src/components/onboarding/OnboardingVoiceStep.tsx
|
#: src/components/onboarding/OnboardingVoiceStep.tsx
|
||||||
#: src/pages/Onboarding.tsx
|
#: src/pages/Onboarding.tsx
|
||||||
#: src/pages/PluginsOverlay.tsx
|
#: src/pages/PluginsOverlay.tsx
|
||||||
@ -1423,6 +1431,7 @@ msgid "Context limit"
|
|||||||
msgstr "Context limit"
|
msgstr "Context limit"
|
||||||
|
|
||||||
#: src/components/integrations/IntegrationSetup.tsx
|
#: src/components/integrations/IntegrationSetup.tsx
|
||||||
|
#: src/components/onboarding/OnboardingNameStep.tsx
|
||||||
#: src/components/onboarding/OnboardingOoDesigner.tsx
|
#: src/components/onboarding/OnboardingOoDesigner.tsx
|
||||||
#: src/components/onboarding/OnboardingScanStep.tsx
|
#: src/components/onboarding/OnboardingScanStep.tsx
|
||||||
#: src/components/onboarding/OnboardingVoiceStep.tsx
|
#: src/components/onboarding/OnboardingVoiceStep.tsx
|
||||||
@ -3317,6 +3326,10 @@ msgstr "Name this group"
|
|||||||
msgid "Name this routine"
|
msgid "Name this routine"
|
||||||
msgstr "Name this routine"
|
msgstr "Name this routine"
|
||||||
|
|
||||||
|
#: src/components/onboarding/OnboardingNameStep.tsx
|
||||||
|
msgid "Name your Oo"
|
||||||
|
msgstr "Name your Oo"
|
||||||
|
|
||||||
#: src/components/OoAtelier.tsx
|
#: src/components/OoAtelier.tsx
|
||||||
msgid "Neck"
|
msgid "Neck"
|
||||||
msgstr "Neck"
|
msgstr "Neck"
|
||||||
@ -3627,6 +3640,10 @@ msgstr "Only empty spaces can be deleted. Delete its bots and groups first."
|
|||||||
msgid "Only file and folder names are read."
|
msgid "Only file and folder names are read."
|
||||||
msgstr "Only file and folder names are read."
|
msgstr "Only file and folder names are read."
|
||||||
|
|
||||||
|
#: src/components/onboarding/OnboardingNameStep.tsx
|
||||||
|
msgid "Oo name"
|
||||||
|
msgstr "Oo name"
|
||||||
|
|
||||||
#: src/pages/ScratchpadSection.tsx
|
#: src/pages/ScratchpadSection.tsx
|
||||||
#: src/pages/shell/computer-panel-preview.tsx
|
#: src/pages/shell/computer-panel-preview.tsx
|
||||||
msgid "Open"
|
msgid "Open"
|
||||||
@ -3833,6 +3850,10 @@ msgstr "Preferences"
|
|||||||
msgid "Present"
|
msgid "Present"
|
||||||
msgstr "Present"
|
msgstr "Present"
|
||||||
|
|
||||||
|
#: src/components/onboarding/OnboardingNameStep.tsx
|
||||||
|
msgid "Press Enter or Continue — skip keeps {DEFAULT_FIRST_OO_NAME}."
|
||||||
|
msgstr "Press Enter or Continue — skip keeps {DEFAULT_FIRST_OO_NAME}."
|
||||||
|
|
||||||
#. placeholder {0}: props.name
|
#. placeholder {0}: props.name
|
||||||
#: src/components/ArtifactFileCard.tsx
|
#: src/components/ArtifactFileCard.tsx
|
||||||
msgid "Preview {0}"
|
msgid "Preview {0}"
|
||||||
@ -4574,6 +4595,7 @@ msgid "Skills"
|
|||||||
msgstr "Skills"
|
msgstr "Skills"
|
||||||
|
|
||||||
#: src/components/integrations/IntegrationSetup.tsx
|
#: src/components/integrations/IntegrationSetup.tsx
|
||||||
|
#: src/components/onboarding/OnboardingNameStep.tsx
|
||||||
#: src/components/onboarding/OnboardingScanStep.tsx
|
#: src/components/onboarding/OnboardingScanStep.tsx
|
||||||
#: src/components/onboarding/OnboardingVoiceStep.tsx
|
#: src/components/onboarding/OnboardingVoiceStep.tsx
|
||||||
#: src/lib/onboarding-copy.ts
|
#: src/lib/onboarding-copy.ts
|
||||||
@ -4581,9 +4603,10 @@ msgstr "Skills"
|
|||||||
msgid "Skip"
|
msgid "Skip"
|
||||||
msgstr "Skip"
|
msgstr "Skip"
|
||||||
|
|
||||||
|
#. placeholder {0}: UI_LOCALE_LABELS[activeUiLocale]
|
||||||
#: src/pages/Onboarding.tsx
|
#: src/pages/Onboarding.tsx
|
||||||
msgid "Skip · English"
|
msgid "Skip · {0}"
|
||||||
msgstr "Skip · English"
|
msgstr "Skip · {0}"
|
||||||
|
|
||||||
#: src/lib/localized-provider-hint.ts
|
#: src/lib/localized-provider-hint.ts
|
||||||
msgid "Skip or deploy key"
|
msgid "Skip or deploy key"
|
||||||
@ -4614,6 +4637,10 @@ msgstr "Software update"
|
|||||||
msgid "Something needs your attention."
|
msgid "Something needs your attention."
|
||||||
msgstr "Something needs your attention."
|
msgstr "Something needs your attention."
|
||||||
|
|
||||||
|
#: src/components/onboarding/OnboardingNameStep.tsx
|
||||||
|
msgid "Something short and yours — or keep the default and rename later."
|
||||||
|
msgstr "Something short and yours — or keep the default and rename later."
|
||||||
|
|
||||||
#: src/lib/voice-persona-copy.ts
|
#: src/lib/voice-persona-copy.ts
|
||||||
msgid "Sorting priorities…"
|
msgid "Sorting priorities…"
|
||||||
msgstr "Sorting priorities…"
|
msgstr "Sorting priorities…"
|
||||||
|
|||||||
@ -303,6 +303,13 @@ msgstr ""
|
|||||||
msgid "1:1 conversation with {title}"
|
msgid "1:1 conversation with {title}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#. placeholder {0}: mainConversationEntry.bot.name
|
||||||
|
#. placeholder {0}: speaker.name
|
||||||
|
#: src/components/stage/OoStage.tsx
|
||||||
|
#: src/pages/Shell.tsx
|
||||||
|
msgid "1:1 with {0}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/shell/bot-picker.tsx
|
#: src/pages/shell/bot-picker.tsx
|
||||||
msgid "About groups"
|
msgid "About groups"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -783,6 +790,7 @@ msgstr "Disponible después de guardar la rutina"
|
|||||||
|
|
||||||
#: src/components/browser/InAppBrowser.tsx
|
#: src/components/browser/InAppBrowser.tsx
|
||||||
#: src/components/CookingMode.tsx
|
#: src/components/CookingMode.tsx
|
||||||
|
#: src/components/onboarding/OnboardingNameStep.tsx
|
||||||
#: src/components/onboarding/OnboardingVoiceStep.tsx
|
#: src/components/onboarding/OnboardingVoiceStep.tsx
|
||||||
#: src/pages/Onboarding.tsx
|
#: src/pages/Onboarding.tsx
|
||||||
#: src/pages/PluginsOverlay.tsx
|
#: src/pages/PluginsOverlay.tsx
|
||||||
@ -1423,6 +1431,7 @@ msgid "Context limit"
|
|||||||
msgstr "Límite de contexto"
|
msgstr "Límite de contexto"
|
||||||
|
|
||||||
#: src/components/integrations/IntegrationSetup.tsx
|
#: src/components/integrations/IntegrationSetup.tsx
|
||||||
|
#: src/components/onboarding/OnboardingNameStep.tsx
|
||||||
#: src/components/onboarding/OnboardingOoDesigner.tsx
|
#: src/components/onboarding/OnboardingOoDesigner.tsx
|
||||||
#: src/components/onboarding/OnboardingScanStep.tsx
|
#: src/components/onboarding/OnboardingScanStep.tsx
|
||||||
#: src/components/onboarding/OnboardingVoiceStep.tsx
|
#: src/components/onboarding/OnboardingVoiceStep.tsx
|
||||||
@ -3317,6 +3326,10 @@ msgstr "Nombra este grupo"
|
|||||||
msgid "Name this routine"
|
msgid "Name this routine"
|
||||||
msgstr "Nombra esta rutina"
|
msgstr "Nombra esta rutina"
|
||||||
|
|
||||||
|
#: src/components/onboarding/OnboardingNameStep.tsx
|
||||||
|
msgid "Name your Oo"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/OoAtelier.tsx
|
#: src/components/OoAtelier.tsx
|
||||||
msgid "Neck"
|
msgid "Neck"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -3627,6 +3640,10 @@ msgstr ""
|
|||||||
msgid "Only file and folder names are read."
|
msgid "Only file and folder names are read."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/onboarding/OnboardingNameStep.tsx
|
||||||
|
msgid "Oo name"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/ScratchpadSection.tsx
|
#: src/pages/ScratchpadSection.tsx
|
||||||
#: src/pages/shell/computer-panel-preview.tsx
|
#: src/pages/shell/computer-panel-preview.tsx
|
||||||
msgid "Open"
|
msgid "Open"
|
||||||
@ -3833,6 +3850,10 @@ msgstr ""
|
|||||||
msgid "Present"
|
msgid "Present"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/onboarding/OnboardingNameStep.tsx
|
||||||
|
msgid "Press Enter or Continue — skip keeps {DEFAULT_FIRST_OO_NAME}."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#. placeholder {0}: props.name
|
#. placeholder {0}: props.name
|
||||||
#: src/components/ArtifactFileCard.tsx
|
#: src/components/ArtifactFileCard.tsx
|
||||||
msgid "Preview {0}"
|
msgid "Preview {0}"
|
||||||
@ -4574,6 +4595,7 @@ msgid "Skills"
|
|||||||
msgstr "Habilidades"
|
msgstr "Habilidades"
|
||||||
|
|
||||||
#: src/components/integrations/IntegrationSetup.tsx
|
#: src/components/integrations/IntegrationSetup.tsx
|
||||||
|
#: src/components/onboarding/OnboardingNameStep.tsx
|
||||||
#: src/components/onboarding/OnboardingScanStep.tsx
|
#: src/components/onboarding/OnboardingScanStep.tsx
|
||||||
#: src/components/onboarding/OnboardingVoiceStep.tsx
|
#: src/components/onboarding/OnboardingVoiceStep.tsx
|
||||||
#: src/lib/onboarding-copy.ts
|
#: src/lib/onboarding-copy.ts
|
||||||
@ -4581,8 +4603,9 @@ msgstr "Habilidades"
|
|||||||
msgid "Skip"
|
msgid "Skip"
|
||||||
msgstr "Omitir"
|
msgstr "Omitir"
|
||||||
|
|
||||||
|
#. placeholder {0}: UI_LOCALE_LABELS[activeUiLocale]
|
||||||
#: src/pages/Onboarding.tsx
|
#: src/pages/Onboarding.tsx
|
||||||
msgid "Skip · English"
|
msgid "Skip · {0}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/lib/localized-provider-hint.ts
|
#: src/lib/localized-provider-hint.ts
|
||||||
@ -4614,6 +4637,10 @@ msgstr "Actualización de software"
|
|||||||
msgid "Something needs your attention."
|
msgid "Something needs your attention."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/onboarding/OnboardingNameStep.tsx
|
||||||
|
msgid "Something short and yours — or keep the default and rename later."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/lib/voice-persona-copy.ts
|
#: src/lib/voice-persona-copy.ts
|
||||||
msgid "Sorting priorities…"
|
msgid "Sorting priorities…"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|||||||
@ -303,6 +303,13 @@ msgstr ""
|
|||||||
msgid "1:1 conversation with {title}"
|
msgid "1:1 conversation with {title}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#. placeholder {0}: mainConversationEntry.bot.name
|
||||||
|
#. placeholder {0}: speaker.name
|
||||||
|
#: src/components/stage/OoStage.tsx
|
||||||
|
#: src/pages/Shell.tsx
|
||||||
|
msgid "1:1 with {0}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/shell/bot-picker.tsx
|
#: src/pages/shell/bot-picker.tsx
|
||||||
msgid "About groups"
|
msgid "About groups"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -783,6 +790,7 @@ msgstr ""
|
|||||||
|
|
||||||
#: src/components/browser/InAppBrowser.tsx
|
#: src/components/browser/InAppBrowser.tsx
|
||||||
#: src/components/CookingMode.tsx
|
#: src/components/CookingMode.tsx
|
||||||
|
#: src/components/onboarding/OnboardingNameStep.tsx
|
||||||
#: src/components/onboarding/OnboardingVoiceStep.tsx
|
#: src/components/onboarding/OnboardingVoiceStep.tsx
|
||||||
#: src/pages/Onboarding.tsx
|
#: src/pages/Onboarding.tsx
|
||||||
#: src/pages/PluginsOverlay.tsx
|
#: src/pages/PluginsOverlay.tsx
|
||||||
@ -1423,6 +1431,7 @@ msgid "Context limit"
|
|||||||
msgstr "कॉन्टेक्स्ट सीमा"
|
msgstr "कॉन्टेक्स्ट सीमा"
|
||||||
|
|
||||||
#: src/components/integrations/IntegrationSetup.tsx
|
#: src/components/integrations/IntegrationSetup.tsx
|
||||||
|
#: src/components/onboarding/OnboardingNameStep.tsx
|
||||||
#: src/components/onboarding/OnboardingOoDesigner.tsx
|
#: src/components/onboarding/OnboardingOoDesigner.tsx
|
||||||
#: src/components/onboarding/OnboardingScanStep.tsx
|
#: src/components/onboarding/OnboardingScanStep.tsx
|
||||||
#: src/components/onboarding/OnboardingVoiceStep.tsx
|
#: src/components/onboarding/OnboardingVoiceStep.tsx
|
||||||
@ -3317,6 +3326,10 @@ msgstr "इस समूह को नाम दें"
|
|||||||
msgid "Name this routine"
|
msgid "Name this routine"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/onboarding/OnboardingNameStep.tsx
|
||||||
|
msgid "Name your Oo"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/OoAtelier.tsx
|
#: src/components/OoAtelier.tsx
|
||||||
msgid "Neck"
|
msgid "Neck"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -3627,6 +3640,10 @@ msgstr ""
|
|||||||
msgid "Only file and folder names are read."
|
msgid "Only file and folder names are read."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/onboarding/OnboardingNameStep.tsx
|
||||||
|
msgid "Oo name"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/ScratchpadSection.tsx
|
#: src/pages/ScratchpadSection.tsx
|
||||||
#: src/pages/shell/computer-panel-preview.tsx
|
#: src/pages/shell/computer-panel-preview.tsx
|
||||||
msgid "Open"
|
msgid "Open"
|
||||||
@ -3833,6 +3850,10 @@ msgstr ""
|
|||||||
msgid "Present"
|
msgid "Present"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/onboarding/OnboardingNameStep.tsx
|
||||||
|
msgid "Press Enter or Continue — skip keeps {DEFAULT_FIRST_OO_NAME}."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#. placeholder {0}: props.name
|
#. placeholder {0}: props.name
|
||||||
#: src/components/ArtifactFileCard.tsx
|
#: src/components/ArtifactFileCard.tsx
|
||||||
msgid "Preview {0}"
|
msgid "Preview {0}"
|
||||||
@ -4574,6 +4595,7 @@ msgid "Skills"
|
|||||||
msgstr "स्किल्स"
|
msgstr "स्किल्स"
|
||||||
|
|
||||||
#: src/components/integrations/IntegrationSetup.tsx
|
#: src/components/integrations/IntegrationSetup.tsx
|
||||||
|
#: src/components/onboarding/OnboardingNameStep.tsx
|
||||||
#: src/components/onboarding/OnboardingScanStep.tsx
|
#: src/components/onboarding/OnboardingScanStep.tsx
|
||||||
#: src/components/onboarding/OnboardingVoiceStep.tsx
|
#: src/components/onboarding/OnboardingVoiceStep.tsx
|
||||||
#: src/lib/onboarding-copy.ts
|
#: src/lib/onboarding-copy.ts
|
||||||
@ -4581,8 +4603,9 @@ msgstr "स्किल्स"
|
|||||||
msgid "Skip"
|
msgid "Skip"
|
||||||
msgstr "छोड़ें"
|
msgstr "छोड़ें"
|
||||||
|
|
||||||
|
#. placeholder {0}: UI_LOCALE_LABELS[activeUiLocale]
|
||||||
#: src/pages/Onboarding.tsx
|
#: src/pages/Onboarding.tsx
|
||||||
msgid "Skip · English"
|
msgid "Skip · {0}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/lib/localized-provider-hint.ts
|
#: src/lib/localized-provider-hint.ts
|
||||||
@ -4614,6 +4637,10 @@ msgstr ""
|
|||||||
msgid "Something needs your attention."
|
msgid "Something needs your attention."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/onboarding/OnboardingNameStep.tsx
|
||||||
|
msgid "Something short and yours — or keep the default and rename later."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/lib/voice-persona-copy.ts
|
#: src/lib/voice-persona-copy.ts
|
||||||
msgid "Sorting priorities…"
|
msgid "Sorting priorities…"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|||||||
@ -303,6 +303,13 @@ msgstr ""
|
|||||||
msgid "1:1 conversation with {title}"
|
msgid "1:1 conversation with {title}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#. placeholder {0}: mainConversationEntry.bot.name
|
||||||
|
#. placeholder {0}: speaker.name
|
||||||
|
#: src/components/stage/OoStage.tsx
|
||||||
|
#: src/pages/Shell.tsx
|
||||||
|
msgid "1:1 with {0}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/shell/bot-picker.tsx
|
#: src/pages/shell/bot-picker.tsx
|
||||||
msgid "About groups"
|
msgid "About groups"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -783,6 +790,7 @@ msgstr "루틴을 저장한 후 사용할 수 있습니다"
|
|||||||
|
|
||||||
#: src/components/browser/InAppBrowser.tsx
|
#: src/components/browser/InAppBrowser.tsx
|
||||||
#: src/components/CookingMode.tsx
|
#: src/components/CookingMode.tsx
|
||||||
|
#: src/components/onboarding/OnboardingNameStep.tsx
|
||||||
#: src/components/onboarding/OnboardingVoiceStep.tsx
|
#: src/components/onboarding/OnboardingVoiceStep.tsx
|
||||||
#: src/pages/Onboarding.tsx
|
#: src/pages/Onboarding.tsx
|
||||||
#: src/pages/PluginsOverlay.tsx
|
#: src/pages/PluginsOverlay.tsx
|
||||||
@ -1423,6 +1431,7 @@ msgid "Context limit"
|
|||||||
msgstr "컨텍스트 한도"
|
msgstr "컨텍스트 한도"
|
||||||
|
|
||||||
#: src/components/integrations/IntegrationSetup.tsx
|
#: src/components/integrations/IntegrationSetup.tsx
|
||||||
|
#: src/components/onboarding/OnboardingNameStep.tsx
|
||||||
#: src/components/onboarding/OnboardingOoDesigner.tsx
|
#: src/components/onboarding/OnboardingOoDesigner.tsx
|
||||||
#: src/components/onboarding/OnboardingScanStep.tsx
|
#: src/components/onboarding/OnboardingScanStep.tsx
|
||||||
#: src/components/onboarding/OnboardingVoiceStep.tsx
|
#: src/components/onboarding/OnboardingVoiceStep.tsx
|
||||||
@ -3317,6 +3326,10 @@ msgstr "그룹 이름"
|
|||||||
msgid "Name this routine"
|
msgid "Name this routine"
|
||||||
msgstr "이 루틴의 이름을 정하세요"
|
msgstr "이 루틴의 이름을 정하세요"
|
||||||
|
|
||||||
|
#: src/components/onboarding/OnboardingNameStep.tsx
|
||||||
|
msgid "Name your Oo"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/OoAtelier.tsx
|
#: src/components/OoAtelier.tsx
|
||||||
msgid "Neck"
|
msgid "Neck"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -3627,6 +3640,10 @@ msgstr ""
|
|||||||
msgid "Only file and folder names are read."
|
msgid "Only file and folder names are read."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/onboarding/OnboardingNameStep.tsx
|
||||||
|
msgid "Oo name"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/ScratchpadSection.tsx
|
#: src/pages/ScratchpadSection.tsx
|
||||||
#: src/pages/shell/computer-panel-preview.tsx
|
#: src/pages/shell/computer-panel-preview.tsx
|
||||||
msgid "Open"
|
msgid "Open"
|
||||||
@ -3833,6 +3850,10 @@ msgstr ""
|
|||||||
msgid "Present"
|
msgid "Present"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/onboarding/OnboardingNameStep.tsx
|
||||||
|
msgid "Press Enter or Continue — skip keeps {DEFAULT_FIRST_OO_NAME}."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#. placeholder {0}: props.name
|
#. placeholder {0}: props.name
|
||||||
#: src/components/ArtifactFileCard.tsx
|
#: src/components/ArtifactFileCard.tsx
|
||||||
msgid "Preview {0}"
|
msgid "Preview {0}"
|
||||||
@ -4574,6 +4595,7 @@ msgid "Skills"
|
|||||||
msgstr "스킬"
|
msgstr "스킬"
|
||||||
|
|
||||||
#: src/components/integrations/IntegrationSetup.tsx
|
#: src/components/integrations/IntegrationSetup.tsx
|
||||||
|
#: src/components/onboarding/OnboardingNameStep.tsx
|
||||||
#: src/components/onboarding/OnboardingScanStep.tsx
|
#: src/components/onboarding/OnboardingScanStep.tsx
|
||||||
#: src/components/onboarding/OnboardingVoiceStep.tsx
|
#: src/components/onboarding/OnboardingVoiceStep.tsx
|
||||||
#: src/lib/onboarding-copy.ts
|
#: src/lib/onboarding-copy.ts
|
||||||
@ -4581,8 +4603,9 @@ msgstr "스킬"
|
|||||||
msgid "Skip"
|
msgid "Skip"
|
||||||
msgstr "건너뛰기"
|
msgstr "건너뛰기"
|
||||||
|
|
||||||
|
#. placeholder {0}: UI_LOCALE_LABELS[activeUiLocale]
|
||||||
#: src/pages/Onboarding.tsx
|
#: src/pages/Onboarding.tsx
|
||||||
msgid "Skip · English"
|
msgid "Skip · {0}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/lib/localized-provider-hint.ts
|
#: src/lib/localized-provider-hint.ts
|
||||||
@ -4614,6 +4637,10 @@ msgstr "소프트웨어 업데이트"
|
|||||||
msgid "Something needs your attention."
|
msgid "Something needs your attention."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/onboarding/OnboardingNameStep.tsx
|
||||||
|
msgid "Something short and yours — or keep the default and rename later."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/lib/voice-persona-copy.ts
|
#: src/lib/voice-persona-copy.ts
|
||||||
msgid "Sorting priorities…"
|
msgid "Sorting priorities…"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|||||||
@ -303,6 +303,13 @@ msgstr ""
|
|||||||
msgid "1:1 conversation with {title}"
|
msgid "1:1 conversation with {title}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#. placeholder {0}: mainConversationEntry.bot.name
|
||||||
|
#. placeholder {0}: speaker.name
|
||||||
|
#: src/components/stage/OoStage.tsx
|
||||||
|
#: src/pages/Shell.tsx
|
||||||
|
msgid "1:1 with {0}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/shell/bot-picker.tsx
|
#: src/pages/shell/bot-picker.tsx
|
||||||
msgid "About groups"
|
msgid "About groups"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -783,6 +790,7 @@ msgstr ""
|
|||||||
|
|
||||||
#: src/components/browser/InAppBrowser.tsx
|
#: src/components/browser/InAppBrowser.tsx
|
||||||
#: src/components/CookingMode.tsx
|
#: src/components/CookingMode.tsx
|
||||||
|
#: src/components/onboarding/OnboardingNameStep.tsx
|
||||||
#: src/components/onboarding/OnboardingVoiceStep.tsx
|
#: src/components/onboarding/OnboardingVoiceStep.tsx
|
||||||
#: src/pages/Onboarding.tsx
|
#: src/pages/Onboarding.tsx
|
||||||
#: src/pages/PluginsOverlay.tsx
|
#: src/pages/PluginsOverlay.tsx
|
||||||
@ -1423,6 +1431,7 @@ msgid "Context limit"
|
|||||||
msgstr "Limite de contexto"
|
msgstr "Limite de contexto"
|
||||||
|
|
||||||
#: src/components/integrations/IntegrationSetup.tsx
|
#: src/components/integrations/IntegrationSetup.tsx
|
||||||
|
#: src/components/onboarding/OnboardingNameStep.tsx
|
||||||
#: src/components/onboarding/OnboardingOoDesigner.tsx
|
#: src/components/onboarding/OnboardingOoDesigner.tsx
|
||||||
#: src/components/onboarding/OnboardingScanStep.tsx
|
#: src/components/onboarding/OnboardingScanStep.tsx
|
||||||
#: src/components/onboarding/OnboardingVoiceStep.tsx
|
#: src/components/onboarding/OnboardingVoiceStep.tsx
|
||||||
@ -3317,6 +3326,10 @@ msgstr "Dar nome ao grupo"
|
|||||||
msgid "Name this routine"
|
msgid "Name this routine"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/onboarding/OnboardingNameStep.tsx
|
||||||
|
msgid "Name your Oo"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/OoAtelier.tsx
|
#: src/components/OoAtelier.tsx
|
||||||
msgid "Neck"
|
msgid "Neck"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -3627,6 +3640,10 @@ msgstr ""
|
|||||||
msgid "Only file and folder names are read."
|
msgid "Only file and folder names are read."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/onboarding/OnboardingNameStep.tsx
|
||||||
|
msgid "Oo name"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/ScratchpadSection.tsx
|
#: src/pages/ScratchpadSection.tsx
|
||||||
#: src/pages/shell/computer-panel-preview.tsx
|
#: src/pages/shell/computer-panel-preview.tsx
|
||||||
msgid "Open"
|
msgid "Open"
|
||||||
@ -3833,6 +3850,10 @@ msgstr ""
|
|||||||
msgid "Present"
|
msgid "Present"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/onboarding/OnboardingNameStep.tsx
|
||||||
|
msgid "Press Enter or Continue — skip keeps {DEFAULT_FIRST_OO_NAME}."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#. placeholder {0}: props.name
|
#. placeholder {0}: props.name
|
||||||
#: src/components/ArtifactFileCard.tsx
|
#: src/components/ArtifactFileCard.tsx
|
||||||
msgid "Preview {0}"
|
msgid "Preview {0}"
|
||||||
@ -4574,6 +4595,7 @@ msgid "Skills"
|
|||||||
msgstr "Habilidades"
|
msgstr "Habilidades"
|
||||||
|
|
||||||
#: src/components/integrations/IntegrationSetup.tsx
|
#: src/components/integrations/IntegrationSetup.tsx
|
||||||
|
#: src/components/onboarding/OnboardingNameStep.tsx
|
||||||
#: src/components/onboarding/OnboardingScanStep.tsx
|
#: src/components/onboarding/OnboardingScanStep.tsx
|
||||||
#: src/components/onboarding/OnboardingVoiceStep.tsx
|
#: src/components/onboarding/OnboardingVoiceStep.tsx
|
||||||
#: src/lib/onboarding-copy.ts
|
#: src/lib/onboarding-copy.ts
|
||||||
@ -4581,8 +4603,9 @@ msgstr "Habilidades"
|
|||||||
msgid "Skip"
|
msgid "Skip"
|
||||||
msgstr "Pular"
|
msgstr "Pular"
|
||||||
|
|
||||||
|
#. placeholder {0}: UI_LOCALE_LABELS[activeUiLocale]
|
||||||
#: src/pages/Onboarding.tsx
|
#: src/pages/Onboarding.tsx
|
||||||
msgid "Skip · English"
|
msgid "Skip · {0}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/lib/localized-provider-hint.ts
|
#: src/lib/localized-provider-hint.ts
|
||||||
@ -4614,6 +4637,10 @@ msgstr ""
|
|||||||
msgid "Something needs your attention."
|
msgid "Something needs your attention."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/onboarding/OnboardingNameStep.tsx
|
||||||
|
msgid "Something short and yours — or keep the default and rename later."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/lib/voice-persona-copy.ts
|
#: src/lib/voice-persona-copy.ts
|
||||||
msgid "Sorting priorities…"
|
msgid "Sorting priorities…"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|||||||
@ -303,6 +303,13 @@ msgstr ""
|
|||||||
msgid "1:1 conversation with {title}"
|
msgid "1:1 conversation with {title}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#. placeholder {0}: mainConversationEntry.bot.name
|
||||||
|
#. placeholder {0}: speaker.name
|
||||||
|
#: src/components/stage/OoStage.tsx
|
||||||
|
#: src/pages/Shell.tsx
|
||||||
|
msgid "1:1 with {0}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/shell/bot-picker.tsx
|
#: src/pages/shell/bot-picker.tsx
|
||||||
msgid "About groups"
|
msgid "About groups"
|
||||||
msgstr "О группах"
|
msgstr "О группах"
|
||||||
@ -783,6 +790,7 @@ msgstr "Доступно после сохранения задачи"
|
|||||||
|
|
||||||
#: src/components/browser/InAppBrowser.tsx
|
#: src/components/browser/InAppBrowser.tsx
|
||||||
#: src/components/CookingMode.tsx
|
#: src/components/CookingMode.tsx
|
||||||
|
#: src/components/onboarding/OnboardingNameStep.tsx
|
||||||
#: src/components/onboarding/OnboardingVoiceStep.tsx
|
#: src/components/onboarding/OnboardingVoiceStep.tsx
|
||||||
#: src/pages/Onboarding.tsx
|
#: src/pages/Onboarding.tsx
|
||||||
#: src/pages/PluginsOverlay.tsx
|
#: src/pages/PluginsOverlay.tsx
|
||||||
@ -1423,6 +1431,7 @@ msgid "Context limit"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/integrations/IntegrationSetup.tsx
|
#: src/components/integrations/IntegrationSetup.tsx
|
||||||
|
#: src/components/onboarding/OnboardingNameStep.tsx
|
||||||
#: src/components/onboarding/OnboardingOoDesigner.tsx
|
#: src/components/onboarding/OnboardingOoDesigner.tsx
|
||||||
#: src/components/onboarding/OnboardingScanStep.tsx
|
#: src/components/onboarding/OnboardingScanStep.tsx
|
||||||
#: src/components/onboarding/OnboardingVoiceStep.tsx
|
#: src/components/onboarding/OnboardingVoiceStep.tsx
|
||||||
@ -3317,6 +3326,10 @@ msgstr "Назовите эту группу"
|
|||||||
msgid "Name this routine"
|
msgid "Name this routine"
|
||||||
msgstr "Назовите эту задачу"
|
msgstr "Назовите эту задачу"
|
||||||
|
|
||||||
|
#: src/components/onboarding/OnboardingNameStep.tsx
|
||||||
|
msgid "Name your Oo"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/OoAtelier.tsx
|
#: src/components/OoAtelier.tsx
|
||||||
msgid "Neck"
|
msgid "Neck"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -3627,6 +3640,10 @@ msgstr "Удалить можно только пустые пространст
|
|||||||
msgid "Only file and folder names are read."
|
msgid "Only file and folder names are read."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/onboarding/OnboardingNameStep.tsx
|
||||||
|
msgid "Oo name"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/ScratchpadSection.tsx
|
#: src/pages/ScratchpadSection.tsx
|
||||||
#: src/pages/shell/computer-panel-preview.tsx
|
#: src/pages/shell/computer-panel-preview.tsx
|
||||||
msgid "Open"
|
msgid "Open"
|
||||||
@ -3833,6 +3850,10 @@ msgstr ""
|
|||||||
msgid "Present"
|
msgid "Present"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/onboarding/OnboardingNameStep.tsx
|
||||||
|
msgid "Press Enter or Continue — skip keeps {DEFAULT_FIRST_OO_NAME}."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#. placeholder {0}: props.name
|
#. placeholder {0}: props.name
|
||||||
#: src/components/ArtifactFileCard.tsx
|
#: src/components/ArtifactFileCard.tsx
|
||||||
msgid "Preview {0}"
|
msgid "Preview {0}"
|
||||||
@ -4574,6 +4595,7 @@ msgid "Skills"
|
|||||||
msgstr "Навыки"
|
msgstr "Навыки"
|
||||||
|
|
||||||
#: src/components/integrations/IntegrationSetup.tsx
|
#: src/components/integrations/IntegrationSetup.tsx
|
||||||
|
#: src/components/onboarding/OnboardingNameStep.tsx
|
||||||
#: src/components/onboarding/OnboardingScanStep.tsx
|
#: src/components/onboarding/OnboardingScanStep.tsx
|
||||||
#: src/components/onboarding/OnboardingVoiceStep.tsx
|
#: src/components/onboarding/OnboardingVoiceStep.tsx
|
||||||
#: src/lib/onboarding-copy.ts
|
#: src/lib/onboarding-copy.ts
|
||||||
@ -4581,8 +4603,9 @@ msgstr "Навыки"
|
|||||||
msgid "Skip"
|
msgid "Skip"
|
||||||
msgstr "Пропустить"
|
msgstr "Пропустить"
|
||||||
|
|
||||||
|
#. placeholder {0}: UI_LOCALE_LABELS[activeUiLocale]
|
||||||
#: src/pages/Onboarding.tsx
|
#: src/pages/Onboarding.tsx
|
||||||
msgid "Skip · English"
|
msgid "Skip · {0}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/lib/localized-provider-hint.ts
|
#: src/lib/localized-provider-hint.ts
|
||||||
@ -4614,6 +4637,10 @@ msgstr "Обновление программного обеспечения"
|
|||||||
msgid "Something needs your attention."
|
msgid "Something needs your attention."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/onboarding/OnboardingNameStep.tsx
|
||||||
|
msgid "Something short and yours — or keep the default and rename later."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/lib/voice-persona-copy.ts
|
#: src/lib/voice-persona-copy.ts
|
||||||
msgid "Sorting priorities…"
|
msgid "Sorting priorities…"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|||||||
@ -303,6 +303,13 @@ msgstr ""
|
|||||||
msgid "1:1 conversation with {title}"
|
msgid "1:1 conversation with {title}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#. placeholder {0}: mainConversationEntry.bot.name
|
||||||
|
#. placeholder {0}: speaker.name
|
||||||
|
#: src/components/stage/OoStage.tsx
|
||||||
|
#: src/pages/Shell.tsx
|
||||||
|
msgid "1:1 with {0}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/shell/bot-picker.tsx
|
#: src/pages/shell/bot-picker.tsx
|
||||||
msgid "About groups"
|
msgid "About groups"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -783,6 +790,7 @@ msgstr ""
|
|||||||
|
|
||||||
#: src/components/browser/InAppBrowser.tsx
|
#: src/components/browser/InAppBrowser.tsx
|
||||||
#: src/components/CookingMode.tsx
|
#: src/components/CookingMode.tsx
|
||||||
|
#: src/components/onboarding/OnboardingNameStep.tsx
|
||||||
#: src/components/onboarding/OnboardingVoiceStep.tsx
|
#: src/components/onboarding/OnboardingVoiceStep.tsx
|
||||||
#: src/pages/Onboarding.tsx
|
#: src/pages/Onboarding.tsx
|
||||||
#: src/pages/PluginsOverlay.tsx
|
#: src/pages/PluginsOverlay.tsx
|
||||||
@ -1423,6 +1431,7 @@ msgid "Context limit"
|
|||||||
msgstr "Bağlam sınırı"
|
msgstr "Bağlam sınırı"
|
||||||
|
|
||||||
#: src/components/integrations/IntegrationSetup.tsx
|
#: src/components/integrations/IntegrationSetup.tsx
|
||||||
|
#: src/components/onboarding/OnboardingNameStep.tsx
|
||||||
#: src/components/onboarding/OnboardingOoDesigner.tsx
|
#: src/components/onboarding/OnboardingOoDesigner.tsx
|
||||||
#: src/components/onboarding/OnboardingScanStep.tsx
|
#: src/components/onboarding/OnboardingScanStep.tsx
|
||||||
#: src/components/onboarding/OnboardingVoiceStep.tsx
|
#: src/components/onboarding/OnboardingVoiceStep.tsx
|
||||||
@ -3317,6 +3326,10 @@ msgstr "Gruba bir ad ver"
|
|||||||
msgid "Name this routine"
|
msgid "Name this routine"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/onboarding/OnboardingNameStep.tsx
|
||||||
|
msgid "Name your Oo"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/OoAtelier.tsx
|
#: src/components/OoAtelier.tsx
|
||||||
msgid "Neck"
|
msgid "Neck"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -3627,6 +3640,10 @@ msgstr ""
|
|||||||
msgid "Only file and folder names are read."
|
msgid "Only file and folder names are read."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/onboarding/OnboardingNameStep.tsx
|
||||||
|
msgid "Oo name"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/ScratchpadSection.tsx
|
#: src/pages/ScratchpadSection.tsx
|
||||||
#: src/pages/shell/computer-panel-preview.tsx
|
#: src/pages/shell/computer-panel-preview.tsx
|
||||||
msgid "Open"
|
msgid "Open"
|
||||||
@ -3833,6 +3850,10 @@ msgstr ""
|
|||||||
msgid "Present"
|
msgid "Present"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/onboarding/OnboardingNameStep.tsx
|
||||||
|
msgid "Press Enter or Continue — skip keeps {DEFAULT_FIRST_OO_NAME}."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#. placeholder {0}: props.name
|
#. placeholder {0}: props.name
|
||||||
#: src/components/ArtifactFileCard.tsx
|
#: src/components/ArtifactFileCard.tsx
|
||||||
msgid "Preview {0}"
|
msgid "Preview {0}"
|
||||||
@ -4574,6 +4595,7 @@ msgid "Skills"
|
|||||||
msgstr "Beceriler"
|
msgstr "Beceriler"
|
||||||
|
|
||||||
#: src/components/integrations/IntegrationSetup.tsx
|
#: src/components/integrations/IntegrationSetup.tsx
|
||||||
|
#: src/components/onboarding/OnboardingNameStep.tsx
|
||||||
#: src/components/onboarding/OnboardingScanStep.tsx
|
#: src/components/onboarding/OnboardingScanStep.tsx
|
||||||
#: src/components/onboarding/OnboardingVoiceStep.tsx
|
#: src/components/onboarding/OnboardingVoiceStep.tsx
|
||||||
#: src/lib/onboarding-copy.ts
|
#: src/lib/onboarding-copy.ts
|
||||||
@ -4581,8 +4603,9 @@ msgstr "Beceriler"
|
|||||||
msgid "Skip"
|
msgid "Skip"
|
||||||
msgstr "Atla"
|
msgstr "Atla"
|
||||||
|
|
||||||
|
#. placeholder {0}: UI_LOCALE_LABELS[activeUiLocale]
|
||||||
#: src/pages/Onboarding.tsx
|
#: src/pages/Onboarding.tsx
|
||||||
msgid "Skip · English"
|
msgid "Skip · {0}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/lib/localized-provider-hint.ts
|
#: src/lib/localized-provider-hint.ts
|
||||||
@ -4614,6 +4637,10 @@ msgstr ""
|
|||||||
msgid "Something needs your attention."
|
msgid "Something needs your attention."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/onboarding/OnboardingNameStep.tsx
|
||||||
|
msgid "Something short and yours — or keep the default and rename later."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/lib/voice-persona-copy.ts
|
#: src/lib/voice-persona-copy.ts
|
||||||
msgid "Sorting priorities…"
|
msgid "Sorting priorities…"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|||||||
@ -303,6 +303,13 @@ msgstr ""
|
|||||||
msgid "1:1 conversation with {title}"
|
msgid "1:1 conversation with {title}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#. placeholder {0}: mainConversationEntry.bot.name
|
||||||
|
#. placeholder {0}: speaker.name
|
||||||
|
#: src/components/stage/OoStage.tsx
|
||||||
|
#: src/pages/Shell.tsx
|
||||||
|
msgid "1:1 with {0}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/shell/bot-picker.tsx
|
#: src/pages/shell/bot-picker.tsx
|
||||||
msgid "About groups"
|
msgid "About groups"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -783,6 +790,7 @@ msgstr "保存例行任务后可用"
|
|||||||
|
|
||||||
#: src/components/browser/InAppBrowser.tsx
|
#: src/components/browser/InAppBrowser.tsx
|
||||||
#: src/components/CookingMode.tsx
|
#: src/components/CookingMode.tsx
|
||||||
|
#: src/components/onboarding/OnboardingNameStep.tsx
|
||||||
#: src/components/onboarding/OnboardingVoiceStep.tsx
|
#: src/components/onboarding/OnboardingVoiceStep.tsx
|
||||||
#: src/pages/Onboarding.tsx
|
#: src/pages/Onboarding.tsx
|
||||||
#: src/pages/PluginsOverlay.tsx
|
#: src/pages/PluginsOverlay.tsx
|
||||||
@ -1423,6 +1431,7 @@ msgid "Context limit"
|
|||||||
msgstr "上下文限制"
|
msgstr "上下文限制"
|
||||||
|
|
||||||
#: src/components/integrations/IntegrationSetup.tsx
|
#: src/components/integrations/IntegrationSetup.tsx
|
||||||
|
#: src/components/onboarding/OnboardingNameStep.tsx
|
||||||
#: src/components/onboarding/OnboardingOoDesigner.tsx
|
#: src/components/onboarding/OnboardingOoDesigner.tsx
|
||||||
#: src/components/onboarding/OnboardingScanStep.tsx
|
#: src/components/onboarding/OnboardingScanStep.tsx
|
||||||
#: src/components/onboarding/OnboardingVoiceStep.tsx
|
#: src/components/onboarding/OnboardingVoiceStep.tsx
|
||||||
@ -3317,6 +3326,10 @@ msgstr "给这个群组命名"
|
|||||||
msgid "Name this routine"
|
msgid "Name this routine"
|
||||||
msgstr "给这个例行任务命名"
|
msgstr "给这个例行任务命名"
|
||||||
|
|
||||||
|
#: src/components/onboarding/OnboardingNameStep.tsx
|
||||||
|
msgid "Name your Oo"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/OoAtelier.tsx
|
#: src/components/OoAtelier.tsx
|
||||||
msgid "Neck"
|
msgid "Neck"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -3627,6 +3640,10 @@ msgstr ""
|
|||||||
msgid "Only file and folder names are read."
|
msgid "Only file and folder names are read."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/onboarding/OnboardingNameStep.tsx
|
||||||
|
msgid "Oo name"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/ScratchpadSection.tsx
|
#: src/pages/ScratchpadSection.tsx
|
||||||
#: src/pages/shell/computer-panel-preview.tsx
|
#: src/pages/shell/computer-panel-preview.tsx
|
||||||
msgid "Open"
|
msgid "Open"
|
||||||
@ -3833,6 +3850,10 @@ msgstr ""
|
|||||||
msgid "Present"
|
msgid "Present"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/onboarding/OnboardingNameStep.tsx
|
||||||
|
msgid "Press Enter or Continue — skip keeps {DEFAULT_FIRST_OO_NAME}."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#. placeholder {0}: props.name
|
#. placeholder {0}: props.name
|
||||||
#: src/components/ArtifactFileCard.tsx
|
#: src/components/ArtifactFileCard.tsx
|
||||||
msgid "Preview {0}"
|
msgid "Preview {0}"
|
||||||
@ -4574,6 +4595,7 @@ msgid "Skills"
|
|||||||
msgstr "技能"
|
msgstr "技能"
|
||||||
|
|
||||||
#: src/components/integrations/IntegrationSetup.tsx
|
#: src/components/integrations/IntegrationSetup.tsx
|
||||||
|
#: src/components/onboarding/OnboardingNameStep.tsx
|
||||||
#: src/components/onboarding/OnboardingScanStep.tsx
|
#: src/components/onboarding/OnboardingScanStep.tsx
|
||||||
#: src/components/onboarding/OnboardingVoiceStep.tsx
|
#: src/components/onboarding/OnboardingVoiceStep.tsx
|
||||||
#: src/lib/onboarding-copy.ts
|
#: src/lib/onboarding-copy.ts
|
||||||
@ -4581,8 +4603,9 @@ msgstr "技能"
|
|||||||
msgid "Skip"
|
msgid "Skip"
|
||||||
msgstr "跳过"
|
msgstr "跳过"
|
||||||
|
|
||||||
|
#. placeholder {0}: UI_LOCALE_LABELS[activeUiLocale]
|
||||||
#: src/pages/Onboarding.tsx
|
#: src/pages/Onboarding.tsx
|
||||||
msgid "Skip · English"
|
msgid "Skip · {0}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/lib/localized-provider-hint.ts
|
#: src/lib/localized-provider-hint.ts
|
||||||
@ -4614,6 +4637,10 @@ msgstr "软件更新"
|
|||||||
msgid "Something needs your attention."
|
msgid "Something needs your attention."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/onboarding/OnboardingNameStep.tsx
|
||||||
|
msgid "Something short and yours — or keep the default and rename later."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/lib/voice-persona-copy.ts
|
#: src/lib/voice-persona-copy.ts
|
||||||
msgid "Sorting priorities…"
|
msgid "Sorting priorities…"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|||||||
@ -5,6 +5,10 @@ import { useEffect, useMemo, useRef, useState } from "react";
|
|||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { IntegrationSetup } from "../components/integrations/IntegrationSetup";
|
import { IntegrationSetup } from "../components/integrations/IntegrationSetup";
|
||||||
import { OnboardingDiscoveryShell } from "../components/onboarding/OnboardingDiscoveryShell";
|
import { OnboardingDiscoveryShell } from "../components/onboarding/OnboardingDiscoveryShell";
|
||||||
|
import {
|
||||||
|
DEFAULT_FIRST_OO_NAME,
|
||||||
|
OnboardingNameStep,
|
||||||
|
} from "../components/onboarding/OnboardingNameStep";
|
||||||
import {
|
import {
|
||||||
type OnboardingEmojiConfig,
|
type OnboardingEmojiConfig,
|
||||||
OnboardingOoDesigner,
|
OnboardingOoDesigner,
|
||||||
@ -23,7 +27,12 @@ import {
|
|||||||
import { findLookCategory, lookPresetToAvatarValue } from "../lib/onboarding-look-categories";
|
import { findLookCategory, lookPresetToAvatarValue } from "../lib/onboarding-look-categories";
|
||||||
import { onboardingProfileUpdate } from "../lib/onboarding-profile";
|
import { onboardingProfileUpdate } from "../lib/onboarding-profile";
|
||||||
import { rpc, selectSpace } from "../lib/rpc";
|
import { rpc, selectSpace } from "../lib/rpc";
|
||||||
import { persistUiLocale, UI_LOCALE_LABELS, type UiLocale } from "../lib/ui-locale";
|
import {
|
||||||
|
persistUiLocale,
|
||||||
|
resolveUiLocale,
|
||||||
|
UI_LOCALE_LABELS,
|
||||||
|
type UiLocale,
|
||||||
|
} from "../lib/ui-locale";
|
||||||
import { collectLocalProfileSignals, persistUserProfileHarvest } from "../lib/user-profile-harvest";
|
import { collectLocalProfileSignals, persistUserProfileHarvest } from "../lib/user-profile-harvest";
|
||||||
|
|
||||||
/** Selects the main conversation's space and returns its route. */
|
/** Selects the main conversation's space and returns its route. */
|
||||||
@ -44,8 +53,9 @@ export function OnboardingPage() {
|
|||||||
const { t } = useLingui();
|
const { t } = useLingui();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [step, setStep] = useState<
|
const [step, setStep] = useState<
|
||||||
"loading" | "emoji" | "language" | "voice" | "integrations" | "scan" | "bot"
|
"loading" | "emoji" | "name" | "language" | "voice" | "integrations" | "scan" | "bot"
|
||||||
>("emoji");
|
>("emoji");
|
||||||
|
const [ooName, setOoName] = useState(DEFAULT_FIRST_OO_NAME);
|
||||||
const [selectedVoiceId, setSelectedVoiceId] = useState("");
|
const [selectedVoiceId, setSelectedVoiceId] = useState("");
|
||||||
const [voiceSkipped, setVoiceSkipped] = useState(false);
|
const [voiceSkipped, setVoiceSkipped] = useState(false);
|
||||||
const [integrationSetup, setIntegrationSetup] = useState<IntegrationSetupState | null>(null);
|
const [integrationSetup, setIntegrationSetup] = useState<IntegrationSetupState | null>(null);
|
||||||
@ -57,14 +67,9 @@ export function OnboardingPage() {
|
|||||||
const [crewReady, setCrewReady] = useState(false);
|
const [crewReady, setCrewReady] = useState(false);
|
||||||
|
|
||||||
const [emojiConfig, setEmojiConfig] = useState<OnboardingEmojiConfig>(defaultEmojiConfig);
|
const [emojiConfig, setEmojiConfig] = useState<OnboardingEmojiConfig>(defaultEmojiConfig);
|
||||||
const [currentLocale, setCurrentLocale] = useState<string>(() => {
|
const [currentLocale, setCurrentLocale] = useState<UiLocale>(() => resolveUiLocale());
|
||||||
try {
|
|
||||||
return localStorage.getItem("rakazo.uiLocale") ?? "en";
|
|
||||||
} catch {
|
|
||||||
return "en";
|
|
||||||
}
|
|
||||||
});
|
|
||||||
const [languageChoice, setLanguageChoice] = useState<UiLocale | null>(null);
|
const [languageChoice, setLanguageChoice] = useState<UiLocale | null>(null);
|
||||||
|
const activeUiLocale = languageChoice ?? currentLocale;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
@ -110,7 +115,7 @@ export function OnboardingPage() {
|
|||||||
const discoveryPhase: OnboardingDiscoveryPhase =
|
const discoveryPhase: OnboardingDiscoveryPhase =
|
||||||
step === "loading"
|
step === "loading"
|
||||||
? "loading"
|
? "loading"
|
||||||
: step === "emoji" || step === "language" || step === "voice"
|
: step === "emoji" || step === "name" || step === "language" || step === "voice"
|
||||||
? "emoji"
|
? "emoji"
|
||||||
: step === "integrations"
|
: step === "integrations"
|
||||||
? "integrations"
|
? "integrations"
|
||||||
@ -145,7 +150,10 @@ export function OnboardingPage() {
|
|||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
const bot = await rpc.bots.ensureMain();
|
const bot = await rpc.bots.ensureMain();
|
||||||
await rpc.bots.update({ botId: bot.id, color: emojiConfig.avatarValue }).catch(() => null);
|
const trimmedName = ooName.trim() || DEFAULT_FIRST_OO_NAME;
|
||||||
|
await rpc.bots
|
||||||
|
.update({ botId: bot.id, color: emojiConfig.avatarValue, name: trimmedName })
|
||||||
|
.catch(() => null);
|
||||||
setCrewReady(true);
|
setCrewReady(true);
|
||||||
|
|
||||||
await rpc.profile
|
await rpc.profile
|
||||||
@ -213,7 +221,11 @@ export function OnboardingPage() {
|
|||||||
<OnboardingDiscoveryShell
|
<OnboardingDiscoveryShell
|
||||||
items={discoveryItems}
|
items={discoveryItems}
|
||||||
compact={
|
compact={
|
||||||
step === "emoji" || step === "language" || step === "voice" || step === "integrations"
|
step === "emoji" ||
|
||||||
|
step === "name" ||
|
||||||
|
step === "language" ||
|
||||||
|
step === "voice" ||
|
||||||
|
step === "integrations"
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{error && step !== "bot" ? <p className="mb-4 text-sm text-destructive">{error}</p> : null}
|
{error && step !== "bot" ? <p className="mb-4 text-sm text-destructive">{error}</p> : null}
|
||||||
@ -222,6 +234,16 @@ export function OnboardingPage() {
|
|||||||
<OnboardingOoDesigner
|
<OnboardingOoDesigner
|
||||||
config={emojiConfig}
|
config={emojiConfig}
|
||||||
onChange={setEmojiConfig}
|
onChange={setEmojiConfig}
|
||||||
|
onContinue={() => setStep("name")}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{step === "name" ? (
|
||||||
|
<OnboardingNameStep
|
||||||
|
config={emojiConfig}
|
||||||
|
name={ooName}
|
||||||
|
onNameChange={setOoName}
|
||||||
|
onBack={() => setStep("emoji")}
|
||||||
onContinue={() => setStep("language")}
|
onContinue={() => setStep("language")}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
@ -241,7 +263,7 @@ export function OnboardingPage() {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
persistUiLocale(code as UiLocale);
|
persistUiLocale(code as UiLocale);
|
||||||
setCurrentLocale(code);
|
setCurrentLocale(code as UiLocale);
|
||||||
setLanguageChoice(code as UiLocale);
|
setLanguageChoice(code as UiLocale);
|
||||||
setStep("voice");
|
setStep("voice");
|
||||||
}}
|
}}
|
||||||
@ -256,11 +278,18 @@ export function OnboardingPage() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-8 flex items-center justify-between">
|
<div className="mt-8 flex items-center justify-between">
|
||||||
<Button variant="ghost" size="sm" onClick={() => setStep("emoji")}>
|
<Button variant="ghost" size="sm" onClick={() => setStep("name")}>
|
||||||
<Trans>Back</Trans>
|
<Trans>Back</Trans>
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={() => setStep("voice")}>
|
<Button
|
||||||
<Trans>Skip · English</Trans>
|
onClick={() => {
|
||||||
|
const locale = resolveUiLocale();
|
||||||
|
setLanguageChoice(locale);
|
||||||
|
setCurrentLocale(locale);
|
||||||
|
setStep("voice");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t`Skip · ${UI_LOCALE_LABELS[activeUiLocale]}`}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -268,6 +297,7 @@ export function OnboardingPage() {
|
|||||||
|
|
||||||
{step === "voice" ? (
|
{step === "voice" ? (
|
||||||
<OnboardingVoiceStep
|
<OnboardingVoiceStep
|
||||||
|
uiLocale={activeUiLocale}
|
||||||
selectedVoiceId={selectedVoiceId}
|
selectedVoiceId={selectedVoiceId}
|
||||||
onSelectedVoiceIdChange={setSelectedVoiceId}
|
onSelectedVoiceIdChange={setSelectedVoiceId}
|
||||||
onBack={() => setStep("language")}
|
onBack={() => setStep("language")}
|
||||||
|
|||||||
@ -84,7 +84,7 @@ import {
|
|||||||
useState,
|
useState,
|
||||||
} from "react";
|
} from "react";
|
||||||
import { createPortal } from "react-dom";
|
import { createPortal } from "react-dom";
|
||||||
import { useNavigate, useParams, useSearchParams } from "react-router-dom";
|
import { useLocation, useNavigate, useParams, useSearchParams } from "react-router-dom";
|
||||||
import { ArtifactPane } from "../components/ArtifactPane";
|
import { ArtifactPane } from "../components/ArtifactPane";
|
||||||
import { InAppBrowser } from "../components/browser/InAppBrowser";
|
import { InAppBrowser } from "../components/browser/InAppBrowser";
|
||||||
import { ComputerUpdateProgress } from "../components/ComputerUpdateProgress";
|
import { ComputerUpdateProgress } from "../components/ComputerUpdateProgress";
|
||||||
@ -95,6 +95,7 @@ import { useWideLayout } from "../hooks/useWideLayout";
|
|||||||
import { readActivityMode, writeActivityMode } from "../lib/activity-mode";
|
import { readActivityMode, writeActivityMode } from "../lib/activity-mode";
|
||||||
import { rosterActivitySubtitle } from "../lib/agent-activity";
|
import { rosterActivitySubtitle } from "../lib/agent-activity";
|
||||||
import { agentPointerFeed } from "../lib/agent-pointer";
|
import { agentPointerFeed } from "../lib/agent-pointer";
|
||||||
|
import { HEUTE_APP_PATH, isHeuteAppPath } from "../lib/app-routes";
|
||||||
import type { ArtifactTarget } from "../lib/artifact-open";
|
import type { ArtifactTarget } from "../lib/artifact-open";
|
||||||
import { attachmentMaxBytes, uploadAttachment } from "../lib/artifact-upload";
|
import { attachmentMaxBytes, uploadAttachment } from "../lib/artifact-upload";
|
||||||
import { authClient } from "../lib/auth";
|
import { authClient } from "../lib/auth";
|
||||||
@ -103,6 +104,7 @@ import { requestBrowserNotificationPermission } from "../lib/browser-notificatio
|
|||||||
import { loadComputerScreen } from "../lib/computer-screen";
|
import { loadComputerScreen } from "../lib/computer-screen";
|
||||||
import { scheduleFocusPrompt } from "../lib/focus-prompt";
|
import { scheduleFocusPrompt } from "../lib/focus-prompt";
|
||||||
import { formatBytes } from "../lib/format-bytes";
|
import { formatBytes } from "../lib/format-bytes";
|
||||||
|
import { isMultiOoGroupChat } from "../lib/group-chat";
|
||||||
import {
|
import {
|
||||||
isWorkingRunStatus,
|
isWorkingRunStatus,
|
||||||
resolveActiveGroupSpeaker,
|
resolveActiveGroupSpeaker,
|
||||||
@ -267,14 +269,26 @@ import { SortableGroup } from "../components/SortableGroup";
|
|||||||
export function ShellPage() {
|
export function ShellPage() {
|
||||||
const { t } = useLingui();
|
const { t } = useLingui();
|
||||||
const { botId, groupId } = useParams();
|
const { botId, groupId } = useParams();
|
||||||
|
const location = useLocation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [searchParams, setSearchParams] = useSearchParams();
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
|
const onHeuteRoute = isHeuteAppPath(location.pathname);
|
||||||
|
useEffect(() => {
|
||||||
|
if (botId === "heute") {
|
||||||
|
navigate(HEUTE_APP_PATH, { replace: true });
|
||||||
|
}
|
||||||
|
}, [botId, navigate]);
|
||||||
const [activeNavPillar, setActiveNavPillar] = useState<NavPillar>(() => {
|
const [activeNavPillar, setActiveNavPillar] = useState<NavPillar>(() => {
|
||||||
|
if (isHeuteAppPath(location.pathname)) return "heute";
|
||||||
if (groupId) return "projekte";
|
if (groupId) return "projekte";
|
||||||
if (botId) return "team";
|
if (botId) return "team";
|
||||||
return "heute";
|
return "heute";
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isHeuteAppPath(location.pathname)) setActiveNavPillar("heute");
|
||||||
|
}, [location.pathname]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (botId) setActiveNavPillar("team");
|
if (botId) setActiveNavPillar("team");
|
||||||
}, [botId]);
|
}, [botId]);
|
||||||
@ -611,6 +625,11 @@ export function ShellPage() {
|
|||||||
const recordingSkill = activeTaughtSkills.find((skill) => skill.status === "recording") ?? null;
|
const recordingSkill = activeTaughtSkills.find((skill) => skill.status === "recording") ?? null;
|
||||||
const routeBotId = useRef<string | undefined>(botId);
|
const routeBotId = useRef<string | undefined>(botId);
|
||||||
routeBotId.current = botId;
|
routeBotId.current = botId;
|
||||||
|
const routePathname = useRef(location.pathname);
|
||||||
|
routePathname.current = location.pathname;
|
||||||
|
const routeOnHeute = useRef(onHeuteRoute);
|
||||||
|
routeOnHeute.current = onHeuteRoute;
|
||||||
|
const holdingHeuteRoute = () => routeOnHeute.current || isHeuteAppPath(routePathname.current);
|
||||||
const routeGroupId = useRef<string | undefined>(groupId);
|
const routeGroupId = useRef<string | undefined>(groupId);
|
||||||
routeGroupId.current = groupId;
|
routeGroupId.current = groupId;
|
||||||
const activeBotId = useRef<string | undefined>(inGroup ? undefined : active?.id);
|
const activeBotId = useRef<string | undefined>(inGroup ? undefined : active?.id);
|
||||||
@ -745,6 +764,7 @@ export function ShellPage() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!currentBotId || !list.some((bot) => bot.id === currentBotId)) {
|
if (!currentBotId || !list.some((bot) => bot.id === currentBotId)) {
|
||||||
|
if (holdingHeuteRoute()) return;
|
||||||
navigate(firstThreadRoute(list, groupList, inProject), { replace: true });
|
navigate(firstThreadRoute(list, groupList, inProject), { replace: true });
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
@ -992,9 +1012,11 @@ export function ShellPage() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (inProject && !botId) {
|
if (inProject && !botId) {
|
||||||
|
if (holdingHeuteRoute()) return;
|
||||||
navigate(firstThreadRoute(bootstrap.bots, groupList, true), { replace: true });
|
navigate(firstThreadRoute(bootstrap.bots, groupList, true), { replace: true });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (holdingHeuteRoute()) return;
|
||||||
const selectedBotId = bootstrap.thread?.botId ?? bootstrap.bots[0]?.id;
|
const selectedBotId = bootstrap.thread?.botId ?? bootstrap.bots[0]?.id;
|
||||||
if (selectedBotId && selectedBotId !== botId) {
|
if (selectedBotId && selectedBotId !== botId) {
|
||||||
navigate(`/app/${selectedBotId}`, { replace: true });
|
navigate(`/app/${selectedBotId}`, { replace: true });
|
||||||
@ -1580,6 +1602,9 @@ export function ShellPage() {
|
|||||||
: snapshot?.botId === active?.id
|
: snapshot?.botId === active?.id
|
||||||
? snapshot
|
? snapshot
|
||||||
: null;
|
: null;
|
||||||
|
const activeGroupMemberCount =
|
||||||
|
activeGroup?.members?.length ?? activeSnapshot?.members?.length ?? 0;
|
||||||
|
const multiOoInGroup = inGroup && isMultiOoGroupChat(activeGroupMemberCount);
|
||||||
const activeReplyTarget =
|
const activeReplyTarget =
|
||||||
replyTarget && activeSnapshot?.messages.some((message) => message.id === replyTarget.id)
|
replyTarget && activeSnapshot?.messages.some((message) => message.id === replyTarget.id)
|
||||||
? replyTarget
|
? replyTarget
|
||||||
@ -2796,6 +2821,7 @@ export function ShellPage() {
|
|||||||
ooSound.play("interact");
|
ooSound.play("interact");
|
||||||
setActiveNavPillar("heute");
|
setActiveNavPillar("heute");
|
||||||
setMobileSidebarOpen(false);
|
setMobileSidebarOpen(false);
|
||||||
|
navigate(HEUTE_APP_PATH);
|
||||||
}}
|
}}
|
||||||
className={rosterRowClass(activeNavPillar === "heute")}
|
className={rosterRowClass(activeNavPillar === "heute")}
|
||||||
>
|
>
|
||||||
@ -3213,10 +3239,17 @@ export function ShellPage() {
|
|||||||
<span className="block truncate text-ui-body-sm font-medium text-muted-foreground/80 leading-snug">
|
<span className="block truncate text-ui-body-sm font-medium text-muted-foreground/80 leading-snug">
|
||||||
{composerRunning || active.status === "running" ? t`Busy` : active.status}
|
{composerRunning || active.status === "running" ? t`Busy` : active.status}
|
||||||
</span>
|
</span>
|
||||||
) : inGroup && projectSessionKey ? (
|
) : multiOoInGroup && projectSessionKey ? (
|
||||||
<span className="block truncate text-ui-body-sm font-medium text-muted-foreground/80 leading-snug">
|
<span className="block truncate text-ui-body-sm font-medium text-muted-foreground/80 leading-snug">
|
||||||
{t`Group conversation`}
|
{t`Group conversation`}
|
||||||
</span>
|
</span>
|
||||||
|
) : inGroup &&
|
||||||
|
activeGroup?.kind === "main" &&
|
||||||
|
activeGroupMemberCount <= 1 &&
|
||||||
|
mainConversationEntry ? (
|
||||||
|
<span className="block truncate text-ui-body-sm font-medium text-muted-foreground/80 leading-snug">
|
||||||
|
{t`1:1 with ${mainConversationEntry.bot.name}`}
|
||||||
|
</span>
|
||||||
) : inGroup && activeGroup?.kind !== "main" ? (
|
) : inGroup && activeGroup?.kind !== "main" ? (
|
||||||
<span className="block truncate text-ui-body-sm font-medium text-muted-foreground/80 leading-snug">
|
<span className="block truncate text-ui-body-sm font-medium text-muted-foreground/80 leading-snug">
|
||||||
{t`Project · sessions & folders`}
|
{t`Project · sessions & folders`}
|
||||||
|
|||||||
@ -20,6 +20,7 @@ export function SidebarToggleButtons({
|
|||||||
{collapsed && desktopBridge() ? <WindowChrome /> : null}
|
{collapsed && desktopBridge() ? <WindowChrome /> : null}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
data-testid="open-navigation"
|
||||||
aria-label={t`Open navigation`}
|
aria-label={t`Open navigation`}
|
||||||
onClick={onOpenNavigation}
|
onClick={onOpenNavigation}
|
||||||
className="app-no-drag grid size-control-lg shrink-0 place-items-center rounded-lg text-foreground/75 hover:bg-accent md:hidden"
|
className="app-no-drag grid size-control-lg shrink-0 place-items-center rounded-lg text-foreground/75 hover:bg-accent md:hidden"
|
||||||
|
|||||||
@ -17,6 +17,20 @@ export function useMobileSidebar() {
|
|||||||
return () => desktop.removeEventListener("change", closeMobileSidebar);
|
return () => desktop.removeEventListener("change", closeMobileSidebar);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!mobileSidebarOpen) return;
|
||||||
|
function onKeyDown(event: KeyboardEvent) {
|
||||||
|
if (event.key !== "Escape" || window.matchMedia("(min-width: 768px)").matches) return;
|
||||||
|
event.preventDefault();
|
||||||
|
setMobileSidebarOpen(false);
|
||||||
|
queueMicrotask(() => {
|
||||||
|
document.querySelector<HTMLElement>('[data-testid="open-navigation"]')?.focus();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
window.addEventListener("keydown", onKeyDown);
|
||||||
|
return () => window.removeEventListener("keydown", onKeyDown);
|
||||||
|
}, [mobileSidebarOpen]);
|
||||||
|
|
||||||
function onMobileSidebarTouchStart(event: React.TouchEvent<HTMLDivElement>) {
|
function onMobileSidebarTouchStart(event: React.TouchEvent<HTMLDivElement>) {
|
||||||
if (
|
if (
|
||||||
mobileSidebarOpen ||
|
mobileSidebarOpen ||
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user