Compare commits
10 Commits
a9a251b80a
...
490e2fefe8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
490e2fefe8 | ||
|
|
83ec966a67 | ||
|
|
057e039e83 | ||
|
|
6b6e34c1a6 | ||
|
|
628b008e2a | ||
|
|
7f35ac1aa9 | ||
|
|
2d5bb7ea35 | ||
|
|
b0ec12ec6c | ||
|
|
1511880f1b | ||
|
|
c5b2ebc2b3 |
@ -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" });
|
||||||
|
|||||||
@ -5,12 +5,15 @@ test("onboarding names the first Oo and shows it in chat", async ({ page }) => {
|
|||||||
const stamp = Date.now();
|
const stamp = Date.now();
|
||||||
await signup(page, `name-oo-${stamp}@rakazo.test`, "password12", "Namer");
|
await signup(page, `name-oo-${stamp}@rakazo.test`, "password12", "Namer");
|
||||||
await page.waitForURL(/\/onboarding/, { timeout: 20_000 });
|
await page.waitForURL(/\/onboarding/, { timeout: 20_000 });
|
||||||
await page.getByRole("button", { name: "Continue", exact: true }).click();
|
await page.getByRole("button", { name: /^(Continue|Weiter)$/i }).click();
|
||||||
await expect(page.getByTestId("onboarding-name-step")).toBeVisible();
|
await expect(page.getByTestId("onboarding-name-step")).toBeVisible();
|
||||||
await page.getByTestId("onboarding-oo-name-input").fill("Luna");
|
await page.getByTestId("onboarding-oo-name-input").fill("Luna");
|
||||||
await page.getByTestId("onboarding-name-step").getByRole("button", { name: "Continue" }).click();
|
await page
|
||||||
await page.getByRole("button", { name: /Skip/i }).click();
|
.getByTestId("onboarding-name-step")
|
||||||
await page.getByRole("button", { name: "Skip", exact: true }).click();
|
.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).toHaveURL(/\/app\/g\/[^/]+/, { timeout: 30_000 });
|
||||||
await expect(
|
await expect(
|
||||||
page.getByTestId("bot-settings-trigger").getByText("Luna", { exact: true }),
|
page.getByTestId("bot-settings-trigger").getByText("Luna", { exact: true }),
|
||||||
@ -25,8 +28,8 @@ test("German UI keeps Deutsch on language skip label", async ({ page }) => {
|
|||||||
});
|
});
|
||||||
const stamp = Date.now();
|
const stamp = Date.now();
|
||||||
await signup(page, `de-skip-${stamp}@rakazo.test`, "password12", "Deutsch");
|
await signup(page, `de-skip-${stamp}@rakazo.test`, "password12", "Deutsch");
|
||||||
await page.getByRole("button", { name: "Continue", exact: true }).click();
|
await page.getByRole("button", { name: /^(Continue|Weiter)$/i }).click();
|
||||||
await page.getByRole("button", { name: "Skip", exact: true }).click();
|
await page.getByRole("button", { name: /^(Skip|Überspringen)$/i }).click();
|
||||||
await expect(
|
await expect(
|
||||||
page.getByRole("heading", { name: /Wähle deine Sprache|Choose your language/i }),
|
page.getByRole("heading", { name: /Wähle deine Sprache|Choose your language/i }),
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
@ -40,15 +43,16 @@ test("German voice step lists German voices after choosing Deutsch", async ({ pa
|
|||||||
});
|
});
|
||||||
const stamp = Date.now();
|
const stamp = Date.now();
|
||||||
await signup(page, `de-voice-${stamp}@rakazo.test`, "password12", "Stimme");
|
await signup(page, `de-voice-${stamp}@rakazo.test`, "password12", "Stimme");
|
||||||
await page.getByRole("button", { name: "Continue", exact: true }).click();
|
await page.getByRole("button", { name: /^(Continue|Weiter)$/i }).click();
|
||||||
await page.getByRole("button", { name: "Skip", exact: true }).click();
|
await page.getByRole("button", { name: /^(Skip|Überspringen)$/i }).click();
|
||||||
await page.getByRole("button", { name: "Deutsch", exact: true }).click();
|
await page.getByRole("button", { name: "Deutsch", exact: true }).click();
|
||||||
await expect(page.getByRole("button", { name: "Eva", exact: true })).toBeVisible();
|
await expect(page.getByText(/Loading voices|Stimmen werden geladen/i)).toHaveCount(0, {
|
||||||
await expect(page.getByRole("button", { name: "Alloy", exact: true })).toHaveCount(0);
|
timeout: 30_000,
|
||||||
await expect(page.getByRole("button", { name: "Eva", exact: true })).toHaveAttribute(
|
});
|
||||||
"aria-pressed",
|
await expect(page.getByTestId("onboarding-voice-df_eva")).toBeVisible({ timeout: 30_000 });
|
||||||
"true",
|
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 }) => {
|
test("main conversation does not show a group subtitle for one Oo", async ({ page }) => {
|
||||||
|
|||||||
@ -87,10 +87,14 @@ 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). */
|
/** After the look picker, skip the optional Oo naming step (keeps the default name). */
|
||||||
export async function skipOnboardingNameStep(page: Page) {
|
export async function skipOnboardingNameStep(page: Page) {
|
||||||
await expect(page.getByTestId("onboarding-name-step")).toBeVisible();
|
await expect(page.getByTestId("onboarding-name-step")).toBeVisible();
|
||||||
await page.getByRole("button", { name: "Skip", exact: true }).click();
|
await page.getByRole("button", { name: onboardingSkipButton }).click();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -113,11 +117,15 @@ export async function completeOnboarding(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const design = page.getByRole("heading", { name: /Design your Oo/i });
|
const design = page.getByRole("heading", { name: /Design your Oo|Gestalte dein Oo/i });
|
||||||
const naming = page.getByTestId("onboarding-name-step");
|
const naming = page.getByTestId("onboarding-name-step");
|
||||||
const language = page.getByRole("heading", { name: /Choose your language/i });
|
const language = page.getByRole("heading", {
|
||||||
const voice = page.getByRole("heading", { name: /Pick a voice/i });
|
name: /Choose your language|Wähle deine Sprache/i,
|
||||||
const integrations = page.getByRole("heading", { name: "Server integrations", exact: true });
|
});
|
||||||
|
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 expectedOoName = options.ooName ?? "Chief";
|
||||||
const chief = page.getByText(expectedOoName).first();
|
const chief = page.getByText(expectedOoName).first();
|
||||||
|
|
||||||
@ -127,30 +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();
|
await expect(naming).toBeVisible();
|
||||||
if (options.ooName) {
|
if (options.ooName) {
|
||||||
await page.getByTestId("onboarding-oo-name-input").fill(options.ooName);
|
await page.getByTestId("onboarding-oo-name-input").fill(options.ooName);
|
||||||
await naming.getByRole("button", { name: "Continue", exact: true }).click();
|
await naming.getByRole("button", { name: onboardingContinueButton }).click();
|
||||||
} else {
|
} else {
|
||||||
await page.getByRole("button", { name: "Skip", exact: true }).click();
|
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\/[^/]+$/);
|
||||||
@ -186,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) {
|
||||||
@ -289,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). */
|
||||||
|
|||||||
@ -84,13 +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 expect(page).toHaveURL(/\/app\/heute$/);
|
await expect(page).toHaveURL(/\/app\/heute$/);
|
||||||
await expect(page.getByTestId("heute-brief")).toBeVisible();
|
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");
|
||||||
|
|||||||
@ -1,9 +1,12 @@
|
|||||||
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 { 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 } from "../../lib/onboarding-voices";
|
import {
|
||||||
|
defaultOnboardingVoiceId,
|
||||||
|
offlineOnboardingVoices,
|
||||||
|
onboardingVoicesFromList,
|
||||||
|
} from "../../lib/onboarding-voices";
|
||||||
import { rpc } from "../../lib/rpc";
|
import { rpc } from "../../lib/rpc";
|
||||||
import type { UiLocale } from "../../lib/ui-locale";
|
import type { UiLocale } from "../../lib/ui-locale";
|
||||||
|
|
||||||
@ -39,7 +42,7 @@ 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 ordered = voicesForLocale(listed, uiLocale).slice(0, 6);
|
const ordered = onboardingVoicesFromList(listed, uiLocale);
|
||||||
setVoiceOptions(ordered);
|
setVoiceOptions(ordered);
|
||||||
const nextId = defaultOnboardingVoiceId(uiLocale, ordered);
|
const nextId = defaultOnboardingVoiceId(uiLocale, ordered);
|
||||||
if (nextId) onSelectedVoiceIdChange(nextId);
|
if (nextId) onSelectedVoiceIdChange(nextId);
|
||||||
@ -89,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 ${
|
||||||
|
|||||||
@ -1,5 +1,9 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import { defaultOnboardingVoiceId, offlineOnboardingVoices } from "./onboarding-voices.js";
|
import {
|
||||||
|
defaultOnboardingVoiceId,
|
||||||
|
offlineOnboardingVoices,
|
||||||
|
onboardingVoicesFromList,
|
||||||
|
} from "./onboarding-voices.js";
|
||||||
|
|
||||||
describe("offlineOnboardingVoices", () => {
|
describe("offlineOnboardingVoices", () => {
|
||||||
it("offers German voices for de", () => {
|
it("offers German voices for de", () => {
|
||||||
@ -18,6 +22,18 @@ describe("offlineOnboardingVoices", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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", () => {
|
describe("defaultOnboardingVoiceId", () => {
|
||||||
it("prefers df_eva for German", () => {
|
it("prefers df_eva for German", () => {
|
||||||
expect(defaultOnboardingVoiceId("de", offlineOnboardingVoices("de"))).toBe("df_eva");
|
expect(defaultOnboardingVoiceId("de", offlineOnboardingVoices("de"))).toBe("df_eva");
|
||||||
|
|||||||
@ -13,11 +13,41 @@ const ENGLISH_ONBOARDING_VOICES: VoiceInfo[] = [
|
|||||||
|
|
||||||
const OFFLINE_CATALOG = [...GERMAN_VOICE_PRESETS, ...ENGLISH_ONBOARDING_VOICES];
|
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. */
|
/** Locale-aware voice choices for onboarding when listing from the server fails. */
|
||||||
export function offlineOnboardingVoices(locale: string): VoiceInfo[] {
|
export function offlineOnboardingVoices(locale: string): VoiceInfo[] {
|
||||||
return voicesForLocale(OFFLINE_CATALOG, locale).slice(0, 6);
|
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 {
|
export function defaultOnboardingVoiceId(locale: string, voices: VoiceInfo[]): string {
|
||||||
return defaultVoiceForLocale(voices, locale)?.id ?? "";
|
return defaultVoiceForLocale(voices, locale)?.id ?? "";
|
||||||
}
|
}
|
||||||
|
|||||||
@ -273,6 +273,11 @@ export function ShellPage() {
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [searchParams, setSearchParams] = useSearchParams();
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
const onHeuteRoute = isHeuteAppPath(location.pathname);
|
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 (isHeuteAppPath(location.pathname)) return "heute";
|
||||||
if (groupId) return "projekte";
|
if (groupId) return "projekte";
|
||||||
@ -620,8 +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);
|
const routeOnHeute = useRef(onHeuteRoute);
|
||||||
routeOnHeute.current = 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);
|
||||||
@ -756,7 +764,7 @@ export function ShellPage() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!currentBotId || !list.some((bot) => bot.id === currentBotId)) {
|
if (!currentBotId || !list.some((bot) => bot.id === currentBotId)) {
|
||||||
if (routeOnHeute.current) return;
|
if (holdingHeuteRoute()) return;
|
||||||
navigate(firstThreadRoute(list, groupList, inProject), { replace: true });
|
navigate(firstThreadRoute(list, groupList, inProject), { replace: true });
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
@ -1004,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 });
|
||||||
|
|||||||
@ -22,14 +22,13 @@ export function useMobileSidebar() {
|
|||||||
function onKeyDown(event: KeyboardEvent) {
|
function onKeyDown(event: KeyboardEvent) {
|
||||||
if (event.key !== "Escape" || window.matchMedia("(min-width: 768px)").matches) return;
|
if (event.key !== "Escape" || window.matchMedia("(min-width: 768px)").matches) return;
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
|
||||||
setMobileSidebarOpen(false);
|
setMobileSidebarOpen(false);
|
||||||
queueMicrotask(() => {
|
queueMicrotask(() => {
|
||||||
document.querySelector<HTMLElement>('[data-testid="open-navigation"]')?.focus();
|
document.querySelector<HTMLElement>('[data-testid="open-navigation"]')?.focus();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
window.addEventListener("keydown", onKeyDown, true);
|
window.addEventListener("keydown", onKeyDown);
|
||||||
return () => window.removeEventListener("keydown", onKeyDown, true);
|
return () => window.removeEventListener("keydown", onKeyDown);
|
||||||
}, [mobileSidebarOpen]);
|
}, [mobileSidebarOpen]);
|
||||||
|
|
||||||
function onMobileSidebarTouchStart(event: React.TouchEvent<HTMLDivElement>) {
|
function onMobileSidebarTouchStart(event: React.TouchEvent<HTMLDivElement>) {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user