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>
This commit is contained in:
Cursor Agent 2026-10-06 15:21:34 +00:00
parent 628b008e2a
commit 6b6e34c1a6
No known key found for this signature in database
5 changed files with 56 additions and 5 deletions

View File

@ -52,7 +52,7 @@ test("German voice step lists German voices after choosing Deutsch", async ({ pa
const germanVoice = page.getByRole("button", { name: /^(Eva|Bernd)$/i });
await expect(germanVoice.first()).toBeVisible({ timeout: 30_000 });
await expect(page.getByRole("button", { name: "Alloy", exact: true })).toHaveCount(0);
await expect(germanVoice.filter({ has: page.locator('[aria-pressed="true"]') }).first()).toBeVisible();
await expect(page.getByRole("button", { name: /^(Eva|Bernd)$/i, pressed: true })).toBeVisible();
});
test("main conversation does not show a group subtitle for one Oo", async ({ page }) => {

View File

@ -1,9 +1,12 @@
import { Trans } from "@lingui/react/macro";
import type { VoiceInfo } from "@rakazo/contracts";
import { voicesForLocale } from "@rakazo/core";
import { Button } from "@rakazo/ui-web";
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 type { UiLocale } from "../../lib/ui-locale";
@ -39,7 +42,7 @@ export function OnboardingVoiceStep({
await rpc.voice.connect({ provider: "kokoro", apiKey: KOKORO_LOCAL_TOKEN });
const listed = await rpc.voice.voices({ provider: "kokoro" });
if (cancelled) return;
const ordered = voicesForLocale(listed, uiLocale).slice(0, 6);
const ordered = onboardingVoicesFromList(listed, uiLocale);
setVoiceOptions(ordered);
const nextId = defaultOnboardingVoiceId(uiLocale, ordered);
if (nextId) onSelectedVoiceIdChange(nextId);

View File

@ -1,5 +1,9 @@
import { describe, expect, it } from "vitest";
import { defaultOnboardingVoiceId, offlineOnboardingVoices } from "./onboarding-voices.js";
import {
defaultOnboardingVoiceId,
offlineOnboardingVoices,
onboardingVoicesFromList,
} from "./onboarding-voices.js";
describe("offlineOnboardingVoices", () => {
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", () => {
it("prefers df_eva for German", () => {
expect(defaultOnboardingVoiceId("de", offlineOnboardingVoices("de"))).toBe("df_eva");

View File

@ -13,11 +13,41 @@ const ENGLISH_ONBOARDING_VOICES: VoiceInfo[] = [
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 ?? "";
}

View File

@ -1004,9 +1004,11 @@ export function ShellPage() {
return;
}
if (inProject && !botId) {
if (routeOnHeute.current) return;
navigate(firstThreadRoute(bootstrap.bots, groupList, true), { replace: true });
return;
}
if (routeOnHeute.current) return;
const selectedBotId = bootstrap.thread?.botId ?? bootstrap.bots[0]?.id;
if (selectedBotId && selectedBotId !== botId) {
navigate(`/app/${selectedBotId}`, { replace: true });