mirror of
https://github.com/mengxi-ream/read-frog.git
synced 2026-09-01 15:36:33 +08:00
fix(hosted-ai): expose language detection in Built-in AI feature assignments (#2095)
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@read-frog/extension": patch
|
||||
---
|
||||
|
||||
fix(hosted-ai): expose language detection in Built-in AI feature assignments
|
||||
+50
-8
@@ -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()
|
||||
}}
|
||||
|
||||
Reference in New Issue
Block a user