Skip to content

Commit f3ae8e6

Browse files
author
mcarbonell
committed
feat: update summarize-text to use HybridAI (Chrome AI + Gemini fallback)
1 parent 2bae512 commit f3ae8e6

File tree

4 files changed

+419
-50
lines changed

4 files changed

+419
-50
lines changed

CHROME_AI_APIS_SUMMARY.md

Whitespace-only changes.

build/shared/hybrid-ai.js

Lines changed: 370 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,370 @@
1+
// hybrid-ai.js - Intelligent AI system with Chrome APIs + Gemini Cloud fallback
2+
// Priority: Chrome Built-in APIs (free, private) → Gemini Cloud (API key required)
3+
4+
class HybridAI {
5+
constructor() {
6+
this.chromeAI = null;
7+
this.geminiAPI = null;
8+
this.availability = null;
9+
this.systemPrompt = '';
10+
}
11+
12+
async init(options = {}) {
13+
this.systemPrompt = options.systemPrompt || '';
14+
15+
// Check Chrome APIs availability
16+
this.availability = await this.checkChromeAPIs();
17+
18+
// Load Gemini API if available
19+
if (typeof GeminiAPI !== 'undefined') {
20+
const storage = typeof chrome !== 'undefined' && chrome.storage
21+
? await chrome.storage.local.get('gemini_api_key')
22+
: { gemini_api_key: localStorage.getItem('gemini_api_key') };
23+
24+
if (storage.gemini_api_key) {
25+
this.geminiAPI = new GeminiAPI(storage.gemini_api_key);
26+
}
27+
}
28+
29+
return this.availability;
30+
}
31+
32+
async checkChromeAPIs() {
33+
return {
34+
prompt: 'LanguageModel' in self,
35+
summarizer: 'Summarizer' in self,
36+
translator: 'Translator' in self,
37+
detector: 'LanguageDetector' in self,
38+
writer: 'Writer' in self,
39+
rewriter: 'Rewriter' in self,
40+
proofreader: 'Proofreader' in self
41+
};
42+
}
43+
44+
// ====================
45+
// SUMMARIZE
46+
// ====================
47+
48+
async summarize(text, options = {}, onChunk = null) {
49+
// Try Chrome Summarizer API first
50+
if (this.availability?.summarizer) {
51+
try {
52+
if (!this.chromeAI) this.chromeAI = new ChromeAI();
53+
if (onChunk) {
54+
for await (const chunk of this.chromeAI.summarizeStreaming(text, options)) {
55+
onChunk(chunk);
56+
}
57+
return;
58+
} else {
59+
return await this.chromeAI.summarize(text, options);
60+
}
61+
} catch (error) {
62+
console.warn('Chrome Summarizer failed, falling back to Gemini:', error);
63+
}
64+
}
65+
66+
// Fallback to Gemini Cloud
67+
if (this.geminiAPI) {
68+
const prompt = `Summarize the following text concisely:\n\n${text}`;
69+
if (onChunk) {
70+
for await (const chunk of this.geminiAPI.generateTextStream(prompt)) {
71+
onChunk(chunk);
72+
}
73+
return;
74+
} else {
75+
return await this.geminiAPI.generateText(prompt);
76+
}
77+
}
78+
79+
throw new Error('No AI service available. Enable Chrome AI or add Gemini API key.');
80+
}
81+
82+
async *summarizeStreaming(text, options = {}) {
83+
// Try Chrome Summarizer API first
84+
if (this.availability?.summarizer) {
85+
try {
86+
if (!this.chromeAI) this.chromeAI = new ChromeAI();
87+
for await (const chunk of this.chromeAI.summarizeStreaming(text, options)) {
88+
yield chunk;
89+
}
90+
return;
91+
} catch (error) {
92+
console.warn('Chrome Summarizer failed, falling back to Gemini:', error);
93+
}
94+
}
95+
96+
// Fallback to Gemini Cloud
97+
if (this.geminiAPI) {
98+
const prompt = `Summarize the following text concisely:\n\n${text}`;
99+
for await (const chunk of this.geminiAPI.generateTextStream(prompt)) {
100+
yield chunk;
101+
}
102+
return;
103+
}
104+
105+
throw new Error('No AI service available');
106+
}
107+
108+
// ====================
109+
// IMPROVE TEXT
110+
// ====================
111+
112+
async improveText(text, options = {}, onChunk = null) {
113+
// Try Chrome Rewriter API first
114+
if (this.availability?.rewriter) {
115+
try {
116+
if (!this.chromeAI) this.chromeAI = new ChromeAI();
117+
if (onChunk) {
118+
for await (const chunk of this.chromeAI.rewriteStreaming(text, { tone: 'formal', ...options })) {
119+
onChunk(chunk);
120+
}
121+
return;
122+
} else {
123+
return await this.chromeAI.rewrite(text, { tone: 'formal', ...options });
124+
}
125+
} catch (error) {
126+
console.warn('Chrome Rewriter failed, falling back to Gemini:', error);
127+
}
128+
}
129+
130+
// Fallback to Gemini Cloud
131+
if (this.geminiAPI) {
132+
const prompt = `Improve the following text (grammar, clarity, style):\n\n${text}`;
133+
if (onChunk) {
134+
for await (const chunk of this.geminiAPI.generateTextStream(prompt)) {
135+
onChunk(chunk);
136+
}
137+
return;
138+
} else {
139+
return await this.geminiAPI.generateText(prompt);
140+
}
141+
}
142+
143+
throw new Error('No AI service available');
144+
}
145+
146+
async *improveTextStreaming(text, options = {}) {
147+
// Try Chrome Rewriter API first
148+
if (this.availability?.rewriter) {
149+
try {
150+
if (!this.chromeAI) this.chromeAI = new ChromeAI();
151+
for await (const chunk of this.chromeAI.rewriteStreaming(text, { tone: 'formal', ...options })) {
152+
yield chunk;
153+
}
154+
return;
155+
} catch (error) {
156+
console.warn('Chrome Rewriter failed, falling back to Gemini:', error);
157+
}
158+
}
159+
160+
// Fallback to Gemini Cloud
161+
if (this.geminiAPI) {
162+
const prompt = `Improve the following text (grammar, clarity, style):\n\n${text}`;
163+
for await (const chunk of this.geminiAPI.generateTextStream(prompt)) {
164+
yield chunk;
165+
}
166+
return;
167+
}
168+
169+
throw new Error('No AI service available');
170+
}
171+
172+
// ====================
173+
// TRANSLATE
174+
// ====================
175+
176+
async translate(text, sourceLang, targetLang) {
177+
// Try Chrome Translator API first
178+
if (this.availability?.translator) {
179+
try {
180+
if (!this.chromeAI) this.chromeAI = new ChromeAI();
181+
return await this.chromeAI.translate(text, sourceLang, targetLang);
182+
} catch (error) {
183+
console.warn('Chrome Translator failed, falling back to Gemini:', error);
184+
}
185+
}
186+
187+
// Fallback to Gemini Cloud
188+
if (this.geminiAPI) {
189+
const prompt = `Translate from ${sourceLang} to ${targetLang}:\n\n${text}`;
190+
return await this.geminiAPI.generateText(prompt);
191+
}
192+
193+
throw new Error('No AI service available');
194+
}
195+
196+
// ====================
197+
// DETECT LANGUAGE
198+
// ====================
199+
200+
async detectLanguage(text) {
201+
// Try Chrome Language Detector API first
202+
if (this.availability?.detector) {
203+
try {
204+
if (!this.chromeAI) this.chromeAI = new ChromeAI();
205+
return await this.chromeAI.detectLanguage(text);
206+
} catch (error) {
207+
console.warn('Chrome Detector failed, falling back to Gemini:', error);
208+
}
209+
}
210+
211+
// Fallback to Gemini Cloud
212+
if (this.geminiAPI) {
213+
const prompt = `Detect the language of this text and respond with only the ISO 639-1 code (e.g., 'en', 'es', 'fr'):\n\n${text}`;
214+
const result = await this.geminiAPI.generateText(prompt);
215+
return { language: result.trim().toLowerCase(), confidence: 0.8 };
216+
}
217+
218+
throw new Error('No AI service available');
219+
}
220+
221+
// ====================
222+
// CHAT
223+
// ====================
224+
225+
async chat(message, options = {}, onChunk = null) {
226+
// Prepend system prompt if available
227+
const fullMessage = this.systemPrompt ? `${this.systemPrompt}\n\nUser: ${message}` : message;
228+
229+
// Try Chrome Prompt API first
230+
if (this.availability?.prompt) {
231+
try {
232+
if (!this.chromeAI) this.chromeAI = new ChromeAI();
233+
if (onChunk) {
234+
for await (const chunk of this.chromeAI.promptStreaming(fullMessage)) {
235+
onChunk(chunk);
236+
}
237+
return;
238+
} else {
239+
return await this.chromeAI.prompt(fullMessage);
240+
}
241+
} catch (error) {
242+
console.warn('Chrome Prompt failed, falling back to Gemini:', error);
243+
}
244+
}
245+
246+
// Fallback to Gemini Cloud
247+
if (this.geminiAPI) {
248+
if (onChunk) {
249+
for await (const chunk of this.geminiAPI.generateTextStream(fullMessage)) {
250+
onChunk(chunk);
251+
}
252+
return;
253+
} else {
254+
return await this.geminiAPI.generateText(fullMessage);
255+
}
256+
}
257+
258+
throw new Error('No AI service available');
259+
}
260+
261+
async *chatStreaming(message, options = {}) {
262+
// Prepend system prompt if available
263+
const fullMessage = this.systemPrompt ? `${this.systemPrompt}\n\nUser: ${message}` : message;
264+
265+
// Try Chrome Prompt API first
266+
if (this.availability?.prompt) {
267+
try {
268+
if (!this.chromeAI) this.chromeAI = new ChromeAI();
269+
for await (const chunk of this.chromeAI.promptStreaming(fullMessage)) {
270+
yield chunk;
271+
}
272+
return;
273+
} catch (error) {
274+
console.warn('Chrome Prompt failed, falling back to Gemini:', error);
275+
}
276+
}
277+
278+
// Fallback to Gemini Cloud
279+
if (this.geminiAPI) {
280+
for await (const chunk of this.geminiAPI.generateTextStream(fullMessage)) {
281+
yield chunk;
282+
}
283+
return;
284+
}
285+
286+
throw new Error('No AI service available');
287+
}
288+
289+
// ====================
290+
// PROOFREAD
291+
// ====================
292+
293+
async proofread(text) {
294+
// Try Chrome Proofreader API first
295+
if (this.availability?.proofreader) {
296+
try {
297+
if (!this.chromeAI) this.chromeAI = new ChromeAI();
298+
return await this.chromeAI.proofread(text);
299+
} catch (error) {
300+
console.warn('Chrome Proofreader failed, falling back to Gemini:', error);
301+
}
302+
}
303+
304+
// Fallback to Gemini Cloud
305+
if (this.geminiAPI) {
306+
const prompt = `Check this text for grammar and spelling errors. Return corrections in JSON format:\n\n${text}`;
307+
const result = await this.geminiAPI.generateText(prompt);
308+
return { correctedInput: result, corrections: [] };
309+
}
310+
311+
throw new Error('No AI service available');
312+
}
313+
314+
// ====================
315+
// UTILITY
316+
// ====================
317+
318+
get hasChromeAI() {
319+
return this.availability && Object.values(this.availability).some(v => v);
320+
}
321+
322+
get hasGeminiAPI() {
323+
return !!this.geminiAPI;
324+
}
325+
326+
getAvailableServices() {
327+
const services = [];
328+
329+
if (this.availability?.prompt) services.push('Chrome Prompt API (Local)');
330+
if (this.availability?.summarizer) services.push('Chrome Summarizer API (Local)');
331+
if (this.availability?.translator) services.push('Chrome Translator API (Local)');
332+
if (this.availability?.detector) services.push('Chrome Language Detector API (Local)');
333+
if (this.availability?.writer) services.push('Chrome Writer API (Local)');
334+
if (this.availability?.rewriter) services.push('Chrome Rewriter API (Local)');
335+
if (this.availability?.proofreader) services.push('Chrome Proofreader API (Local)');
336+
337+
if (this.geminiAPI) services.push('Gemini Cloud API (API Key)');
338+
339+
return services;
340+
}
341+
342+
getPreferredService(feature) {
343+
const map = {
344+
summarize: this.availability?.summarizer ? 'Chrome Summarizer' : 'Gemini Cloud',
345+
improve: this.availability?.rewriter ? 'Chrome Rewriter' : 'Gemini Cloud',
346+
translate: this.availability?.translator ? 'Chrome Translator' : 'Gemini Cloud',
347+
detect: this.availability?.detector ? 'Chrome Detector' : 'Gemini Cloud',
348+
chat: this.availability?.prompt ? 'Chrome Prompt' : 'Gemini Cloud',
349+
proofread: this.availability?.proofreader ? 'Chrome Proofreader' : 'Gemini Cloud'
350+
};
351+
352+
return map[feature] || 'Unknown';
353+
}
354+
355+
destroy() {
356+
if (this.chromeAI) {
357+
this.chromeAI.destroy();
358+
this.chromeAI = null;
359+
}
360+
}
361+
}
362+
363+
// Export
364+
if (typeof module !== 'undefined' && module.exports) {
365+
module.exports = HybridAI;
366+
}
367+
368+
if (typeof window !== 'undefined') {
369+
window.HybridAI = HybridAI;
370+
}

extension/tools/ai/summarize-text.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,9 +76,9 @@ <h5 style="margin-bottom: 8px;">⚙️ Setup</h5>
7676
</div>
7777
</div>
7878

79+
<script src="../../shared/chrome-ai-apis.js"></script>
7980
<script src="../../shared/gemini-api.js"></script>
80-
<script src="../../shared/summarize-ui.js"></script>
81-
<script src="chat-ai-storage.js"></script>
81+
<script src="../../shared/hybrid-ai.js"></script>
8282
<script src="summarize-text.js"></script>
8383
</body>
8484
</html>

0 commit comments

Comments
 (0)