|
5 | 5 | * for each hack day entry on the Previous Hacks page. |
6 | 6 | * |
7 | 7 | * Summarizer API docs: https://developer.chrome.com/docs/ai/summarizer-api |
8 | | - * MDN: https://developer.mozilla.org/en-US/docs/Web/API/Summarizer |
| 8 | + * MDN: https://developer.mozilla.org/en-US/docs/Web/API/Summarizer_API/Using |
9 | 9 | * |
10 | 10 | * Summaries are cached in IndexedDB using a hash of content + context |
11 | 11 | * to avoid regenerating unchanged content. |
|
25 | 25 | // Store parsed feed items for reuse |
26 | 26 | let feedItems = null; |
27 | 27 | // Store the summarizer instance |
28 | | - let summarizer = null; |
| 28 | + let summarizerInstance = null; |
29 | 29 | // Store the IndexedDB instance |
30 | 30 | let db = null; |
31 | 31 |
|
|
133 | 133 | * @returns {Promise<boolean>} |
134 | 134 | */ |
135 | 135 | async function isSummarizerAvailable() { |
136 | | - if (!('ai' in self) || !('summarizer' in self.ai)) { |
137 | | - console.info('[Summarizer] Not available: ai.summarizer not found in window'); |
| 136 | + // Check for global Summarizer constructor (new API) |
| 137 | + if (typeof Summarizer === 'undefined') { |
| 138 | + console.info('[Summarizer] Not available: Summarizer constructor not found'); |
138 | 139 | return false; |
139 | 140 | } |
140 | 141 |
|
141 | 142 | try { |
142 | | - const capabilities = await self.ai.summarizer.capabilities(); |
143 | | - if (capabilities.available === 'no') { |
144 | | - console.info('[Summarizer] Not available: capabilities.available is "no"'); |
| 143 | + const availability = await Summarizer.availability(); |
| 144 | + if (availability === 'unavailable') { |
| 145 | + console.info('[Summarizer] Not available: availability is "unavailable"'); |
145 | 146 | return false; |
146 | 147 | } |
147 | | - console.info('[Summarizer] Available with capabilities:', capabilities); |
| 148 | + console.info('[Summarizer] Available with availability:', availability); |
148 | 149 | return true; |
149 | 150 | } catch (error) { |
150 | 151 | console.info('[Summarizer] Not available due to error:', error.message); |
|
158 | 159 | * @returns {Promise<Object|null>} |
159 | 160 | */ |
160 | 161 | async function getSummarizer(sharedContext) { |
161 | | - if (summarizer) { |
162 | | - return summarizer; |
| 162 | + if (summarizerInstance) { |
| 163 | + return summarizerInstance; |
163 | 164 | } |
164 | 165 |
|
165 | 166 | try { |
166 | | - summarizer = await self.ai.summarizer.create({ |
| 167 | + summarizerInstance = await Summarizer.create({ |
167 | 168 | sharedContext: sharedContext, |
168 | 169 | type: 'tl;dr', |
169 | 170 | length: 'short', |
170 | 171 | format: 'plain-text' |
171 | 172 | }); |
172 | | - return summarizer; |
| 173 | + return summarizerInstance; |
173 | 174 | } catch (error) { |
174 | 175 | console.error('[Summarizer] Failed to create summarizer:', error.message); |
175 | 176 | return null; |
|
264 | 265 | } |
265 | 266 |
|
266 | 267 | /** |
267 | | - * Generate a summary for a hack day using the Summarizer API |
268 | | - * @param {Object} item - Feed item with title, url, description |
| 268 | + * Generate a summary for content using the Summarizer API |
| 269 | + * @param {string} content - Content to summarize |
269 | 270 | * @param {string} context - Context for summarization |
| 271 | + * @param {boolean} useCache - Whether to use caching (default: true) |
270 | 272 | * @returns {Promise<string>} |
271 | 273 | */ |
272 | | - async function generateSummary(item, context) { |
| 274 | + async function generateSummaryForContent(content, context, useCache = true) { |
273 | 275 | // Skip if there's no real content |
274 | | - if (!item.description || item.description.length < 20) { |
| 276 | + if (!content || content.length < 20) { |
275 | 277 | return ''; |
276 | 278 | } |
277 | 279 |
|
278 | 280 | // Create cache key from content + context |
279 | | - const cacheKey = await hashString(item.description + context); |
| 281 | + const cacheKey = await hashString(content + context); |
280 | 282 |
|
281 | | - // Check cache first |
282 | | - const cached = await getCachedSummary(cacheKey); |
283 | | - if (cached) { |
284 | | - console.info('[Summarizer] Using cached summary for', item.title); |
285 | | - return cached; |
| 283 | + // Check cache first (if enabled) |
| 284 | + if (useCache) { |
| 285 | + const cached = await getCachedSummary(cacheKey); |
| 286 | + if (cached) { |
| 287 | + console.info('[Summarizer] Using cached summary'); |
| 288 | + return cached; |
| 289 | + } |
286 | 290 | } |
287 | 291 |
|
288 | | - const summarizerInstance = await getSummarizer(context); |
289 | | - if (!summarizerInstance) { |
| 292 | + const summarizer = await getSummarizer(context); |
| 293 | + if (!summarizer) { |
290 | 294 | return ''; |
291 | 295 | } |
292 | 296 |
|
293 | 297 | try { |
294 | | - const content = `Hack day: ${item.title}\n\n${item.description}`; |
295 | | - const result = await summarizerInstance.summarize(content); |
| 298 | + const result = await summarizer.summarize(content); |
296 | 299 | const summary = result.trim(); |
297 | 300 |
|
298 | 301 | // Cache the result |
299 | | - await cacheSummary(cacheKey, summary); |
| 302 | + if (useCache) { |
| 303 | + await cacheSummary(cacheKey, summary); |
| 304 | + } |
300 | 305 |
|
301 | 306 | return summary; |
302 | 307 | } catch (error) { |
303 | | - console.warn('[Summarizer] Failed to generate summary for', item.title, ':', error.message); |
| 308 | + console.warn('[Summarizer] Failed to generate summary:', error.message); |
304 | 309 | return ''; |
305 | 310 | } |
306 | 311 | } |
307 | 312 |
|
| 313 | + /** |
| 314 | + * Generate a summary for a hack day using the Summarizer API |
| 315 | + * @param {Object} item - Feed item with title, url, description |
| 316 | + * @param {string} context - Context for summarization |
| 317 | + * @returns {Promise<string>} |
| 318 | + */ |
| 319 | + async function generateSummary(item, context) { |
| 320 | + const content = `Hack day: ${item.title}\n\n${item.description}`; |
| 321 | + const summary = await generateSummaryForContent(content, context); |
| 322 | + if (summary) { |
| 323 | + console.info('[Summarizer] Generated summary for', item.title); |
| 324 | + } |
| 325 | + return summary; |
| 326 | + } |
| 327 | + |
308 | 328 | /** |
309 | 329 | * Update all taglines on the page with generated summaries |
310 | 330 | * @param {string} context - The context to use for summarization |
|
393 | 413 | }); |
394 | 414 |
|
395 | 415 | // Destroy existing summarizer to get fresh results with new context |
396 | | - if (summarizer) { |
| 416 | + if (summarizerInstance) { |
397 | 417 | try { |
398 | | - summarizer.destroy(); |
| 418 | + summarizerInstance.destroy(); |
399 | 419 | } catch (e) { |
400 | 420 | // Ignore errors on destroy |
401 | 421 | } |
402 | | - summarizer = null; |
| 422 | + summarizerInstance = null; |
403 | 423 | } |
404 | 424 |
|
405 | 425 | await updateTaglines(context); |
|
415 | 435 | */ |
416 | 436 | window.clearHackSummaryCache = clearCache; |
417 | 437 |
|
| 438 | + /** |
| 439 | + * Test function to generate summaries without DOM changes |
| 440 | + * Useful for testing the Summarizer API in Chrome DevTools |
| 441 | + * |
| 442 | + * @param {number} count - Number of summaries to generate (default: 3) |
| 443 | + * @param {string} context - Custom context for summarization |
| 444 | + * @example |
| 445 | + * // From browser console: |
| 446 | + * testSummarizer() |
| 447 | + * testSummarizer(5) |
| 448 | + * testSummarizer(2, "What projects did people work on?") |
| 449 | + */ |
| 450 | + window.testSummarizer = async function(count = 3, context = DEFAULT_CONTEXT) { |
| 451 | + console.info('[Summarizer] Testing Summarizer API...'); |
| 452 | + |
| 453 | + // Check availability |
| 454 | + const available = await isSummarizerAvailable(); |
| 455 | + if (!available) { |
| 456 | + console.error('[Summarizer] API not available - cannot run test'); |
| 457 | + return; |
| 458 | + } |
| 459 | + |
| 460 | + // Load feed |
| 461 | + const items = await loadFeed(); |
| 462 | + if (items.length === 0) { |
| 463 | + console.error('[Summarizer] No feed items available for testing'); |
| 464 | + return; |
| 465 | + } |
| 466 | + |
| 467 | + // Filter items with content |
| 468 | + const itemsWithContent = items.filter(item => item.description && item.description.length >= 20); |
| 469 | + const testItems = itemsWithContent.slice(0, count); |
| 470 | + |
| 471 | + console.info(`[Summarizer] Generating ${testItems.length} test summaries with context: "${context}"`); |
| 472 | + console.info('---'); |
| 473 | + |
| 474 | + for (const item of testItems) { |
| 475 | + console.info(`Processing: ${item.title}`); |
| 476 | + const startTime = performance.now(); |
| 477 | + |
| 478 | + // Generate summary without caching for testing |
| 479 | + const summary = await generateSummaryForContent( |
| 480 | + `Hack day: ${item.title}\n\n${item.description}`, |
| 481 | + context, |
| 482 | + false // Skip cache for testing |
| 483 | + ); |
| 484 | + |
| 485 | + const elapsed = Math.round(performance.now() - startTime); |
| 486 | + |
| 487 | + if (summary) { |
| 488 | + console.info(`✓ ${item.title} (${elapsed}ms):`); |
| 489 | + console.info(` "${summary}"`); |
| 490 | + } else { |
| 491 | + console.warn(`✗ ${item.title}: Failed to generate summary`); |
| 492 | + } |
| 493 | + console.info('---'); |
| 494 | + } |
| 495 | + |
| 496 | + console.info('[Summarizer] Test complete'); |
| 497 | + }; |
| 498 | + |
418 | 499 | // Run on page load |
419 | 500 | if (document.readyState === 'loading') { |
420 | 501 | document.addEventListener('DOMContentLoaded', () => updateTaglines()); |
|
0 commit comments