From d28cb7246a92d7400c94b11bf8a8fc78b40bfb39 Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Tue, 4 Aug 2026 17:11:27 +0200 Subject: [PATCH] trailblazers: Fix AI API key prompt when Writer/Rewriter polyfill falls back to native Prompt API Same fix as GoogleChrome/starter-extended-blog#18: isNative was computed via apiClass.toString().includes('[native code]'), which is always false for the built-in-ai-task-apis-polyfills fallback since it installs a plain JS class. This wrongly triggered the "enter your AI API key" prompt even when the polyfill runs entirely on-device through the browser's native Prompt API (LanguageModel). Replace the toString() sniff with isNativeAIFeature(), which checks the polyfill's __isPolyfill marker and, when polyfilled, whether the underlying LanguageModel is itself native rather than also polyfilled. dist/js/ai/*.js is mirrored by hand from public/js/ai/*.js rather than via `npm run build`: eleventy passthrough-copies these files verbatim (no transform), and the pre-fix files were confirmed byte-identical between public/ and dist/, so the manual mirror matches what a full build would produce. A full `npm install` was skipped since it pulls in heavy transitive ML runtime deps (transformers.js, onnxruntime-web, webllm) unrelated to this fix. --- trailblazers/dist/js/ai/ai-classifier.js | 3 ++- trailblazers/dist/js/ai/ai-features.js | 4 ++-- trailblazers/dist/js/ai/ai-multimodal.js | 3 ++- trailblazers/dist/js/ai/ai-rewriter.js | 3 ++- trailblazers/dist/js/ai/ai-tag-suggestions.js | 3 ++- trailblazers/dist/js/ai/ai-translator-core.js | 3 ++- trailblazers/dist/js/ai/ai-ui-utils.js | 19 +++++++++++++++++++ trailblazers/dist/js/ai/ai-writer.js | 3 ++- trailblazers/public/js/ai/ai-classifier.js | 3 ++- trailblazers/public/js/ai/ai-features.js | 4 ++-- trailblazers/public/js/ai/ai-multimodal.js | 3 ++- trailblazers/public/js/ai/ai-rewriter.js | 3 ++- .../public/js/ai/ai-tag-suggestions.js | 3 ++- .../public/js/ai/ai-translator-core.js | 3 ++- trailblazers/public/js/ai/ai-ui-utils.js | 19 +++++++++++++++++++ trailblazers/public/js/ai/ai-writer.js | 3 ++- 16 files changed, 66 insertions(+), 16 deletions(-) diff --git a/trailblazers/dist/js/ai/ai-classifier.js b/trailblazers/dist/js/ai/ai-classifier.js index 265b521a3d..8de3e28e3f 100644 --- a/trailblazers/dist/js/ai/ai-classifier.js +++ b/trailblazers/dist/js/ai/ai-classifier.js @@ -1,6 +1,7 @@ import { updateDraftData } from '../drafts/draft-manager.js'; import { customAlert } from '../utils/dialog-utils.js'; import { getMonitor, runAIAction } from './ai-features.js'; +import { isNativeAIFeature } from './ai-ui-utils.js'; import { refreshAIVisibility } from './ai-toggle.js'; import { renderClassifierResults, @@ -70,7 +71,7 @@ export async function initAIClassifier(ui, updateCallback) { } const input = `Title: ${title}\n\nContent: ${content}`; - const isNative = ClassifierClass.toString().includes('[native code]'); + const isNative = isNativeAIFeature(ClassifierClass); await runAIAction( ui, diff --git a/trailblazers/dist/js/ai/ai-features.js b/trailblazers/dist/js/ai/ai-features.js index c6f52b51c7..9711ca7bf3 100644 --- a/trailblazers/dist/js/ai/ai-features.js +++ b/trailblazers/dist/js/ai/ai-features.js @@ -1,7 +1,7 @@ import { detectLanguage } from './ai-language-detection.js'; import { customAlert } from '../utils/dialog-utils.js'; import { refreshAIVisibility } from './ai-toggle.js'; -import { getMonitor, runAIAction } from './ai-ui-utils.js'; +import { getMonitor, runAIAction, isNativeAIFeature } from './ai-ui-utils.js'; export { getMonitor, runAIAction }; @@ -36,7 +36,7 @@ async function runSummarizer(ui, type, input, targetInput, updateCallback) { const btn = type === 'headline' ? ui.aiSuggestTitleBtn : ui.aiSuggestDescriptionBtn; - const isNative = Summarizer.toString().includes('[native code]'); + const isNative = isNativeAIFeature(Summarizer); await runAIAction( ui, diff --git a/trailblazers/dist/js/ai/ai-multimodal.js b/trailblazers/dist/js/ai/ai-multimodal.js index ecd524b30d..d1009091c3 100644 --- a/trailblazers/dist/js/ai/ai-multimodal.js +++ b/trailblazers/dist/js/ai/ai-multimodal.js @@ -1,5 +1,6 @@ import { detectLanguage } from './ai-language-detection.js'; import { checkAIKeys } from './ai-config.js'; +import { isNativeAIFeature } from './ai-ui-utils.js'; /** @type {Object} */ export const imageMetadataSchema = { @@ -38,7 +39,7 @@ export async function generateImageMetadata(imageSource, ui) { return null; } - const isNative = LanguageModel.toString().includes('[native code]'); + const isNative = isNativeAIFeature(LanguageModel); if (!isNative && !checkAIKeys(ui)) { return null; } diff --git a/trailblazers/dist/js/ai/ai-rewriter.js b/trailblazers/dist/js/ai/ai-rewriter.js index 3c6d08bf09..5e317f1a51 100644 --- a/trailblazers/dist/js/ai/ai-rewriter.js +++ b/trailblazers/dist/js/ai/ai-rewriter.js @@ -1,6 +1,7 @@ import { detectLanguage } from './ai-language-detection.js'; import { customAlert, customConfirm } from '../utils/dialog-utils.js'; import { getMonitor, runAIAction } from './ai-features.js'; +import { isNativeAIFeature } from './ai-ui-utils.js'; import { refreshAIVisibility } from './ai-toggle.js'; /** @@ -89,7 +90,7 @@ export async function initAIRewriter(ui, updateCallback) { return customAlert(ui, 'Please write some content first.'); } - const isNative = Rewriter.toString().includes('[native code]'); + const isNative = isNativeAIFeature(Rewriter); await runAIAction( ui, diff --git a/trailblazers/dist/js/ai/ai-tag-suggestions.js b/trailblazers/dist/js/ai/ai-tag-suggestions.js index 4f81d91aed..136048cea2 100644 --- a/trailblazers/dist/js/ai/ai-tag-suggestions.js +++ b/trailblazers/dist/js/ai/ai-tag-suggestions.js @@ -1,6 +1,7 @@ import { detectLanguage } from './ai-language-detection.js'; import { customAlert } from '../utils/dialog-utils.js'; import { runAIAction } from './ai-features.js'; +import { isNativeAIFeature } from './ai-ui-utils.js'; import { refreshAIVisibility } from './ai-toggle.js'; import { runTagGeneration } from './ai-tag-generator.js'; @@ -60,7 +61,7 @@ export async function initTagSuggestions(ui, updateCallback) { return customAlert(ui, 'Please write some content first.'); } - const isNative = LanguageModel.toString().includes('[native code]'); + const isNative = isNativeAIFeature(LanguageModel); await runAIAction( ui, diff --git a/trailblazers/dist/js/ai/ai-translator-core.js b/trailblazers/dist/js/ai/ai-translator-core.js index 85b929b3a2..8480392613 100644 --- a/trailblazers/dist/js/ai/ai-translator-core.js +++ b/trailblazers/dist/js/ai/ai-translator-core.js @@ -1,6 +1,7 @@ import { detectLanguage } from './ai-language-detection.js'; import { customAlert, customConfirm } from '../utils/dialog-utils.js'; import { runAIAction } from './ai-features.js'; +import { isNativeAIFeature } from './ai-ui-utils.js'; import { updatePreview } from './ai-translator-preview.js'; import { drafts, currentDraftId } from '../drafts/draft-manager.js'; import { generateMarkdown } from '../utils/markdown-utils.js'; @@ -50,7 +51,7 @@ export async function runTranslation( const preview = details.querySelector('.translation-preview'); const btn = details.querySelector('.translate-btn'); - const isNative = Translator.toString().includes('[native code]'); + const isNative = isNativeAIFeature(Translator); await runAIAction( ui, diff --git a/trailblazers/dist/js/ai/ai-ui-utils.js b/trailblazers/dist/js/ai/ai-ui-utils.js index a4b3bbd35d..48898109e7 100644 --- a/trailblazers/dist/js/ai/ai-ui-utils.js +++ b/trailblazers/dist/js/ai/ai-ui-utils.js @@ -24,6 +24,25 @@ export const getMonitor = (ui, lang, modelName) => ({ }, }); +/** + * Determines whether an on-device AI API (Writer, Rewriter, Summarizer, etc.) + * can run without a cloud AI API key. A `Function.toString()` "[native code]" + * check misses the case where `built-in-ai-task-apis-polyfills` installs a + * plain-class fallback that itself runs on the browser's native, on-device + * Prompt API (`LanguageModel`) — that path needs no key either. + * @param {Function} apiClass - The task API class (e.g. `Writer`, `Rewriter`). + * @return {boolean} True if the feature can run without a cloud AI API key. + */ +export function isNativeAIFeature(apiClass) { + if (typeof apiClass !== 'function') { + return false; + } + if (!apiClass.__isPolyfill) { + return true; + } + return typeof LanguageModel !== 'undefined' && !LanguageModel.__isPolyfill; +} + /** * Executes an AI action with UI feedback (loading state). * @param {Object} ui - The UI elements. diff --git a/trailblazers/dist/js/ai/ai-writer.js b/trailblazers/dist/js/ai/ai-writer.js index d2d42b9638..40d488261c 100644 --- a/trailblazers/dist/js/ai/ai-writer.js +++ b/trailblazers/dist/js/ai/ai-writer.js @@ -1,6 +1,7 @@ import { detectLanguage } from './ai-language-detection.js'; import { customAlert, customConfirm } from '../utils/dialog-utils.js'; import { getMonitor, runAIAction } from './ai-features.js'; +import { isNativeAIFeature } from './ai-ui-utils.js'; import { refreshAIVisibility } from './ai-toggle.js'; /** @@ -79,7 +80,7 @@ export async function initAIWriter(ui, updateCallback) { } } - const isNative = Writer.toString().includes('[native code]'); + const isNative = isNativeAIFeature(Writer); await runAIAction( ui, diff --git a/trailblazers/public/js/ai/ai-classifier.js b/trailblazers/public/js/ai/ai-classifier.js index 265b521a3d..8de3e28e3f 100644 --- a/trailblazers/public/js/ai/ai-classifier.js +++ b/trailblazers/public/js/ai/ai-classifier.js @@ -1,6 +1,7 @@ import { updateDraftData } from '../drafts/draft-manager.js'; import { customAlert } from '../utils/dialog-utils.js'; import { getMonitor, runAIAction } from './ai-features.js'; +import { isNativeAIFeature } from './ai-ui-utils.js'; import { refreshAIVisibility } from './ai-toggle.js'; import { renderClassifierResults, @@ -70,7 +71,7 @@ export async function initAIClassifier(ui, updateCallback) { } const input = `Title: ${title}\n\nContent: ${content}`; - const isNative = ClassifierClass.toString().includes('[native code]'); + const isNative = isNativeAIFeature(ClassifierClass); await runAIAction( ui, diff --git a/trailblazers/public/js/ai/ai-features.js b/trailblazers/public/js/ai/ai-features.js index c6f52b51c7..9711ca7bf3 100644 --- a/trailblazers/public/js/ai/ai-features.js +++ b/trailblazers/public/js/ai/ai-features.js @@ -1,7 +1,7 @@ import { detectLanguage } from './ai-language-detection.js'; import { customAlert } from '../utils/dialog-utils.js'; import { refreshAIVisibility } from './ai-toggle.js'; -import { getMonitor, runAIAction } from './ai-ui-utils.js'; +import { getMonitor, runAIAction, isNativeAIFeature } from './ai-ui-utils.js'; export { getMonitor, runAIAction }; @@ -36,7 +36,7 @@ async function runSummarizer(ui, type, input, targetInput, updateCallback) { const btn = type === 'headline' ? ui.aiSuggestTitleBtn : ui.aiSuggestDescriptionBtn; - const isNative = Summarizer.toString().includes('[native code]'); + const isNative = isNativeAIFeature(Summarizer); await runAIAction( ui, diff --git a/trailblazers/public/js/ai/ai-multimodal.js b/trailblazers/public/js/ai/ai-multimodal.js index ecd524b30d..d1009091c3 100644 --- a/trailblazers/public/js/ai/ai-multimodal.js +++ b/trailblazers/public/js/ai/ai-multimodal.js @@ -1,5 +1,6 @@ import { detectLanguage } from './ai-language-detection.js'; import { checkAIKeys } from './ai-config.js'; +import { isNativeAIFeature } from './ai-ui-utils.js'; /** @type {Object} */ export const imageMetadataSchema = { @@ -38,7 +39,7 @@ export async function generateImageMetadata(imageSource, ui) { return null; } - const isNative = LanguageModel.toString().includes('[native code]'); + const isNative = isNativeAIFeature(LanguageModel); if (!isNative && !checkAIKeys(ui)) { return null; } diff --git a/trailblazers/public/js/ai/ai-rewriter.js b/trailblazers/public/js/ai/ai-rewriter.js index 3c6d08bf09..5e317f1a51 100644 --- a/trailblazers/public/js/ai/ai-rewriter.js +++ b/trailblazers/public/js/ai/ai-rewriter.js @@ -1,6 +1,7 @@ import { detectLanguage } from './ai-language-detection.js'; import { customAlert, customConfirm } from '../utils/dialog-utils.js'; import { getMonitor, runAIAction } from './ai-features.js'; +import { isNativeAIFeature } from './ai-ui-utils.js'; import { refreshAIVisibility } from './ai-toggle.js'; /** @@ -89,7 +90,7 @@ export async function initAIRewriter(ui, updateCallback) { return customAlert(ui, 'Please write some content first.'); } - const isNative = Rewriter.toString().includes('[native code]'); + const isNative = isNativeAIFeature(Rewriter); await runAIAction( ui, diff --git a/trailblazers/public/js/ai/ai-tag-suggestions.js b/trailblazers/public/js/ai/ai-tag-suggestions.js index 4f81d91aed..136048cea2 100644 --- a/trailblazers/public/js/ai/ai-tag-suggestions.js +++ b/trailblazers/public/js/ai/ai-tag-suggestions.js @@ -1,6 +1,7 @@ import { detectLanguage } from './ai-language-detection.js'; import { customAlert } from '../utils/dialog-utils.js'; import { runAIAction } from './ai-features.js'; +import { isNativeAIFeature } from './ai-ui-utils.js'; import { refreshAIVisibility } from './ai-toggle.js'; import { runTagGeneration } from './ai-tag-generator.js'; @@ -60,7 +61,7 @@ export async function initTagSuggestions(ui, updateCallback) { return customAlert(ui, 'Please write some content first.'); } - const isNative = LanguageModel.toString().includes('[native code]'); + const isNative = isNativeAIFeature(LanguageModel); await runAIAction( ui, diff --git a/trailblazers/public/js/ai/ai-translator-core.js b/trailblazers/public/js/ai/ai-translator-core.js index 85b929b3a2..8480392613 100644 --- a/trailblazers/public/js/ai/ai-translator-core.js +++ b/trailblazers/public/js/ai/ai-translator-core.js @@ -1,6 +1,7 @@ import { detectLanguage } from './ai-language-detection.js'; import { customAlert, customConfirm } from '../utils/dialog-utils.js'; import { runAIAction } from './ai-features.js'; +import { isNativeAIFeature } from './ai-ui-utils.js'; import { updatePreview } from './ai-translator-preview.js'; import { drafts, currentDraftId } from '../drafts/draft-manager.js'; import { generateMarkdown } from '../utils/markdown-utils.js'; @@ -50,7 +51,7 @@ export async function runTranslation( const preview = details.querySelector('.translation-preview'); const btn = details.querySelector('.translate-btn'); - const isNative = Translator.toString().includes('[native code]'); + const isNative = isNativeAIFeature(Translator); await runAIAction( ui, diff --git a/trailblazers/public/js/ai/ai-ui-utils.js b/trailblazers/public/js/ai/ai-ui-utils.js index a4b3bbd35d..48898109e7 100644 --- a/trailblazers/public/js/ai/ai-ui-utils.js +++ b/trailblazers/public/js/ai/ai-ui-utils.js @@ -24,6 +24,25 @@ export const getMonitor = (ui, lang, modelName) => ({ }, }); +/** + * Determines whether an on-device AI API (Writer, Rewriter, Summarizer, etc.) + * can run without a cloud AI API key. A `Function.toString()` "[native code]" + * check misses the case where `built-in-ai-task-apis-polyfills` installs a + * plain-class fallback that itself runs on the browser's native, on-device + * Prompt API (`LanguageModel`) — that path needs no key either. + * @param {Function} apiClass - The task API class (e.g. `Writer`, `Rewriter`). + * @return {boolean} True if the feature can run without a cloud AI API key. + */ +export function isNativeAIFeature(apiClass) { + if (typeof apiClass !== 'function') { + return false; + } + if (!apiClass.__isPolyfill) { + return true; + } + return typeof LanguageModel !== 'undefined' && !LanguageModel.__isPolyfill; +} + /** * Executes an AI action with UI feedback (loading state). * @param {Object} ui - The UI elements. diff --git a/trailblazers/public/js/ai/ai-writer.js b/trailblazers/public/js/ai/ai-writer.js index d2d42b9638..40d488261c 100644 --- a/trailblazers/public/js/ai/ai-writer.js +++ b/trailblazers/public/js/ai/ai-writer.js @@ -1,6 +1,7 @@ import { detectLanguage } from './ai-language-detection.js'; import { customAlert, customConfirm } from '../utils/dialog-utils.js'; import { getMonitor, runAIAction } from './ai-features.js'; +import { isNativeAIFeature } from './ai-ui-utils.js'; import { refreshAIVisibility } from './ai-toggle.js'; /** @@ -79,7 +80,7 @@ export async function initAIWriter(ui, updateCallback) { } } - const isNative = Writer.toString().includes('[native code]'); + const isNative = isNativeAIFeature(Writer); await runAIAction( ui,