fix(hosted-ai): expose language detection in Built-in AI feature assignments (#2095)

This commit is contained in:
ananaBMaster
2026-08-17 14:22:07 -07:00
committed by GitHub
parent aea6f604ed
commit 208397b797
4 changed files with 80 additions and 15 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"@read-frog/extension": patch
---
fix(hosted-ai): expose language detection in Built-in AI feature assignments
@@ -19,6 +19,7 @@ const {
selectedProviderIdAtom,
setProviderConfigMock,
testState,
writeConfigMock,
writeConfigAtom,
} = vi.hoisted(() => ({
anchoredToastAddMock: vi.fn<(options: unknown) => void>(),
@@ -29,6 +30,7 @@ const {
selectedProviderIdAtom: {},
setProviderConfigMock: vi.fn<(value: unknown) => void>(),
testState: { selectedProviderId: "provider-1" },
writeConfigMock: vi.fn<(value: unknown) => void>(),
writeConfigAtom: {},
}))
@@ -40,7 +42,8 @@ const providerConfig = {
}
const config = {
languageDetection: { mode: "local" },
languageDetection: { mode: "basic", providerId: undefined as string | undefined },
providersConfig: [providerConfig],
selectionToolbar: { customActions: [] },
}
@@ -64,6 +67,7 @@ vi.mock("jotai", () => ({
},
useSetAtom: (atom: object) => {
if (atom === providerWriteAtom) return setProviderConfigMock
if (atom === writeConfigAtom) return writeConfigMock
if (atom === selectedProviderIdAtom)
return (value: string) => {
testState.selectedProviderId = value
@@ -186,13 +190,14 @@ function makeUltraAccessStatus(accessAllowed: boolean) {
}
}
/** Must mirror BUILT_IN_FEATURE_KEYS, or new rows go unasserted. */
const ULTRA_FEATURE_LABELS = [
/** Must mirror the built-in hosted assignment rows, except dynamic custom actions. */
const BUILT_IN_ASSIGNMENT_LABELS = [
"feature.pageTranslation",
"feature.videoSubtitles",
"feature.selectionTranslation",
"feature.inputTranslation",
"feature.noteSuggestion",
"options.apiProviders.languageDetection.title",
] as const
vi.mock("@/utils/i18n", () => ({
@@ -246,7 +251,9 @@ describe("ProvidersConfig", () => {
beforeEach(() => {
anchoredToastAddMock.mockReset()
setProviderConfigMock.mockReset()
writeConfigMock.mockReset()
testState.selectedProviderId = providerConfig.id
config.languageDetection = { mode: "basic", providerId: undefined }
hostedAiState.value = { status: undefined, isPending: false, isError: true }
})
@@ -278,7 +285,7 @@ describe("ProvidersConfig", () => {
expect(screen.queryByText("options.apiProviders.form.delete")).not.toBeInTheDocument()
// Both tiers list every hosted-capable feature row; the normal tier marks
// the Ultra-gated ones with the badge instead of hiding them.
for (const label of ULTRA_FEATURE_LABELS) {
for (const label of BUILT_IN_ASSIGNMENT_LABELS) {
expect(screen.getByText(label)).toBeInTheDocument()
}
})
@@ -300,7 +307,7 @@ describe("ProvidersConfig", () => {
expect(
screen.getByText("options.apiProviders.providers.attribution.builtInAiAdvance"),
).toBeInTheDocument()
for (const label of ULTRA_FEATURE_LABELS) {
for (const label of BUILT_IN_ASSIGNMENT_LABELS) {
expect(screen.getByText(label)).toBeInTheDocument()
}
expect(screen.queryByText("options.apiProviders.sponsorCta")).not.toBeInTheDocument()
@@ -312,7 +319,7 @@ describe("ProvidersConfig", () => {
renderProvidersConfig()
for (const label of ULTRA_FEATURE_LABELS) {
for (const label of BUILT_IN_ASSIGNMENT_LABELS) {
expect(screen.getByRole("switch", { name: label })).not.toHaveAttribute(
"aria-disabled",
"true",
@@ -331,7 +338,7 @@ describe("ProvidersConfig", () => {
renderProvidersConfig()
// base-ui renders a span[role=switch]; disabled surfaces as aria-disabled.
for (const label of ULTRA_FEATURE_LABELS) {
for (const label of BUILT_IN_ASSIGNMENT_LABELS) {
expect(screen.getByRole("switch", { name: label })).toHaveAttribute("aria-disabled", "true")
}
})
@@ -346,11 +353,29 @@ describe("ProvidersConfig", () => {
renderProvidersConfig()
for (const label of ULTRA_FEATURE_LABELS) {
for (const label of BUILT_IN_ASSIGNMENT_LABELS) {
expect(screen.getByRole("switch", { name: label })).not.toBeDisabled()
}
})
it("assigns language detection from a Built-in AI editor", () => {
testState.selectedProviderId = BUILT_IN_AI_PROVIDER_ID
renderProvidersConfig()
fireEvent.click(
screen.getByRole("switch", {
name: "options.apiProviders.languageDetection.title",
}),
)
expect(writeConfigMock).toHaveBeenCalledWith({
languageDetection: {
mode: "llm",
providerId: BUILT_IN_AI_PROVIDER_ID,
},
})
})
it("counts default assignments on the free Built-in AI card badge", () => {
// Note suggestion defaults to the OpenAI provider, so only the built-in
// Dictionary action counts on the free card; the Ultra card has nothing
@@ -373,6 +398,23 @@ describe("ProvidersConfig", () => {
).not.toBeInTheDocument()
})
it("counts language detection on the assigned Built-in AI card badge", () => {
config.languageDetection = {
mode: "llm",
providerId: BUILT_IN_AI_PROVIDER_ID,
}
const { container } = renderProvidersConfig()
const freeCard = container.querySelector(`[data-provider-id="${BUILT_IN_AI_PROVIDER_ID}"]`)
if (!(freeCard instanceof HTMLElement)) {
throw new Error("Built-in provider card not rendered")
}
expect(
within(freeCard).getByText("options.apiProviders.badges.featureCount:2"),
).toBeInTheDocument()
})
it("opens the provider a ?provider= deep link names", () => {
testState.selectedProviderId = BUILT_IN_AI_PROVIDER_ID
@@ -350,6 +350,10 @@ function BuiltInProviderCard({ providerId }: { providerId: BuiltInAiProviderId }
const assignedCustomActions = getSelectionToolbarActions(config.selectionToolbar).filter(
(action) => action.providerId === providerId,
)
const isLanguageDetectionProvider =
config.languageDetection.mode === "llm" && config.languageDetection.providerId === providerId
const totalAssigned =
assignedFeatures.length + assignedCustomActions.length + (isLanguageDetectionProvider ? 1 : 0)
return (
<EntityListItem.Root
@@ -358,10 +362,13 @@ function BuiltInProviderCard({ providerId }: { providerId: BuiltInAiProviderId }
onClick={() => setSelectedProviderId(providerId)}
>
<EntityListItem.Badges>
<FeatureCountBadge count={assignedFeatures.length + assignedCustomActions.length}>
<FeatureCountBadge count={totalAssigned}>
{assignedFeatures.map((key) => (
<li key={key}>{i18n.t(getFeatureLabelI18nKey(key))}</li>
))}
{isLanguageDetectionProvider && (
<li>{i18n.t("options.apiProviders.languageDetection.title")}</li>
)}
{assignedCustomActions.map((action) => (
<li key={action.id}>{action.name}</li>
))}
@@ -382,10 +389,8 @@ function BuiltInProviderCard({ providerId }: { providerId: BuiltInAiProviderId }
/**
* Every hosted-capable FEATURE_KEYS entry, in FEATURE_KEYS order. Language
* detection is deliberately absent: it is a ProviderCapability but not a
* FeatureKey (its providerId is optional and only meaningful in "llm" mode),
* so it cannot use ProviderEditor.FeatureAssignment and stays owned by the
* Language detection section.
* detection is a separate ProviderCapability rather than a FeatureKey, so the
* built-in editor renders it with LanguageDetectionAssignment below.
*/
const BUILT_IN_FEATURE_KEYS = [
"pageTranslation",
@@ -436,6 +441,9 @@ function BuiltInProviderPanel({ providerId }: { providerId: BuiltInAiProviderId
{...getAssignmentStatus(featureKey)}
/>
))}
<ProviderEditor.LanguageDetectionAssignment
{...getAssignmentStatus("languageDetection")}
/>
<ProviderEditor.CustomActionAssignments {...getAssignmentStatus("customAction")} />
</ProviderEditor.Assignments>
</EntityEditor.Body>
@@ -436,7 +436,13 @@ function CompatibleFeatureAssignments() {
})
}
function LanguageDetectionAssignment() {
function LanguageDetectionAssignment({
disabled = false,
requiresUltra = false,
}: {
disabled?: boolean
requiresUltra?: boolean
} = {}) {
const {
state: {
assignmentTarget: { providerId, providerType },
@@ -445,7 +451,9 @@ function LanguageDetectionAssignment() {
} = useProviderEditor()
const config = useAtomValue(configAtom)
if (!providerType || !isLLMProvider(providerType)) {
// Built-in providers have no local providerType, but declare this capability
// in the provider registry. Local non-LLM providers still cannot take it.
if (providerType && !isLLMProvider(providerType)) {
return null
}
@@ -455,6 +463,8 @@ function LanguageDetectionAssignment() {
return (
<AssignmentRow
checked={isAssigned}
disabled={disabled}
requiresUltra={requiresUltra}
onCheckedChange={(checked) => {
if (checked) void actions.assignLanguageDetection()
}}