Skip to content

Commit 22490f3

Browse files
Copilotbenfoxall
andcommitted
Add Prompt API integration for Previous Hacks page
Co-authored-by: benfoxall <51385+benfoxall@users.noreply.github.com>
1 parent 10a1eb9 commit 22490f3

3 files changed

Lines changed: 274 additions & 1 deletion

File tree

_sass/_style.scss

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,20 @@ ol {
108108
}
109109
}
110110
}
111+
.hack-tagline {
112+
display: block;
113+
min-height: 1.5em;
114+
font-size: 1rem;
115+
color: #666;
116+
font-style: italic;
117+
margin-top: 0.25em;
118+
opacity: 0;
119+
transition: opacity 0.3s ease-in-out;
120+
121+
&--loaded {
122+
opacity: 1;
123+
}
124+
}
111125
.⏹ {
112126
display: flex;
113127
align-items: center;

assets/prompt-api-summaries.js

Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
1+
/**
2+
* Prompt API Summaries for Remote Hack Previous Hacks page
3+
*
4+
* Uses Chrome's Prompt API (origin trial) to generate fun, short summaries
5+
* for each hack day entry on the Previous Hacks page.
6+
*
7+
* Origin trial: https://developer.chrome.com/origintrials/#/view_trial/2533837740349325313
8+
* Prompt API docs: https://github.com/webmachinelearning/prompt-api/blob/main/README.md
9+
*/
10+
11+
(function() {
12+
'use strict';
13+
14+
// Store parsed feed items for reuse
15+
let feedItems = null;
16+
// Store the language model session
17+
let session = null;
18+
19+
/**
20+
* Check if the Prompt API is available
21+
* @returns {Promise<boolean>}
22+
*/
23+
async function isPromptAPIAvailable() {
24+
if (!('ai' in self) || !('languageModel' in self.ai)) {
25+
console.info('[Prompt API] Not available: ai.languageModel not found in window');
26+
return false;
27+
}
28+
29+
try {
30+
const capabilities = await self.ai.languageModel.capabilities();
31+
if (capabilities.available === 'no') {
32+
console.info('[Prompt API] Not available: capabilities.available is "no"');
33+
return false;
34+
}
35+
console.info('[Prompt API] Available with capabilities:', capabilities);
36+
return true;
37+
} catch (error) {
38+
console.info('[Prompt API] Not available due to error:', error.message);
39+
return false;
40+
}
41+
}
42+
43+
/**
44+
* Create or reuse a language model session
45+
* @returns {Promise<Object|null>}
46+
*/
47+
async function getSession() {
48+
if (session) {
49+
return session;
50+
}
51+
52+
try {
53+
session = await self.ai.languageModel.create({
54+
systemPrompt: `You are a writer for Remote Hack, a chill monthly hackday community.
55+
Your style is casual, fun, and slightly irreverent. You use light humour and keep things brief.
56+
When summarising hack days, focus on the vibe and interesting tidbits rather than listing everything.
57+
Keep summaries to a single short sentence - punchy and memorable, like a witty tagline.
58+
Don't use emojis. Don't start with "A" or "The".`
59+
});
60+
return session;
61+
} catch (error) {
62+
console.error('[Prompt API] Failed to create session:', error.message);
63+
return null;
64+
}
65+
}
66+
67+
/**
68+
* Parse the feed.xml RSS content
69+
* @param {string} xmlText - Raw XML content
70+
* @returns {Array<{title: string, url: string, description: string}>}
71+
*/
72+
function parseFeed(xmlText) {
73+
const parser = new DOMParser();
74+
const doc = parser.parseFromString(xmlText, 'application/xml');
75+
76+
const parseError = doc.querySelector('parsererror');
77+
if (parseError) {
78+
throw new Error('Failed to parse feed.xml: ' + parseError.textContent);
79+
}
80+
81+
const items = doc.querySelectorAll('item');
82+
const result = [];
83+
84+
items.forEach(item => {
85+
const title = item.querySelector('title')?.textContent || '';
86+
const link = item.querySelector('link')?.textContent || '';
87+
const description = item.querySelector('description')?.textContent || '';
88+
89+
result.push({
90+
title: title.trim(),
91+
url: link.trim(),
92+
description: description.trim()
93+
});
94+
});
95+
96+
return result;
97+
}
98+
99+
/**
100+
* Load and parse the feed.xml file
101+
* @returns {Promise<Array>}
102+
*/
103+
async function loadFeed() {
104+
if (feedItems) {
105+
return feedItems;
106+
}
107+
108+
try {
109+
const response = await fetch('/feed.xml');
110+
if (!response.ok) {
111+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
112+
}
113+
const xmlText = await response.text();
114+
feedItems = parseFeed(xmlText);
115+
console.info('[Prompt API] Loaded feed with', feedItems.length, 'items');
116+
return feedItems;
117+
} catch (error) {
118+
console.error('[Prompt API] Failed to load feed:', error.message);
119+
return [];
120+
}
121+
}
122+
123+
/**
124+
* Find the tagline element for a given hack URL
125+
* @param {string} url - The URL of the hack entry
126+
* @returns {HTMLElement|null}
127+
*/
128+
function findTaglineElement(url) {
129+
// Extract the path from the URL (e.g., "/hacks/50/" from "https://remotehack.space/hacks/50/")
130+
const urlPath = new URL(url).pathname;
131+
132+
// Find the link that matches this path
133+
const links = document.querySelectorAll('.past-events a');
134+
for (const link of links) {
135+
const linkPath = new URL(link.href).pathname;
136+
if (linkPath === urlPath) {
137+
// Find the tagline element within the same list item
138+
const li = link.closest('li');
139+
return li?.querySelector('.hack-tagline');
140+
}
141+
}
142+
return null;
143+
}
144+
145+
/**
146+
* Generate a summary for a hack day using the Prompt API
147+
* @param {Object} item - Feed item with title, url, description
148+
* @param {string} prompt - Custom prompt to use
149+
* @returns {Promise<string>}
150+
*/
151+
async function generateSummary(item, prompt) {
152+
const modelSession = await getSession();
153+
if (!modelSession) {
154+
return '';
155+
}
156+
157+
// Skip if there's no real content
158+
if (!item.description || item.description.length < 20) {
159+
return '';
160+
}
161+
162+
try {
163+
const fullPrompt = `${prompt}
164+
165+
Hack day: ${item.title}
166+
Content: ${item.description}`;
167+
168+
const result = await modelSession.prompt(fullPrompt);
169+
return result.trim();
170+
} catch (error) {
171+
console.warn('[Prompt API] Failed to generate summary for', item.title, ':', error.message);
172+
return '';
173+
}
174+
}
175+
176+
/**
177+
* Update all taglines on the page with generated summaries
178+
* @param {string} prompt - The prompt to use for generation
179+
*/
180+
async function updateTaglines(prompt = 'Write a short, witty one-sentence summary of this hack day (max 10 words):') {
181+
const available = await isPromptAPIAvailable();
182+
if (!available) {
183+
console.info('[Prompt API] Skipping tagline updates - API not available');
184+
return;
185+
}
186+
187+
const items = await loadFeed();
188+
if (items.length === 0) {
189+
console.info('[Prompt API] No feed items to process');
190+
return;
191+
}
192+
193+
console.info('[Prompt API] Generating summaries with prompt:', prompt);
194+
195+
// Process items sequentially to avoid overwhelming the API
196+
for (const item of items) {
197+
const taglineEl = findTaglineElement(item.url);
198+
if (!taglineEl) {
199+
continue;
200+
}
201+
202+
const summary = await generateSummary(item, prompt);
203+
if (summary) {
204+
taglineEl.textContent = summary;
205+
taglineEl.classList.add('hack-tagline--loaded');
206+
}
207+
}
208+
209+
console.info('[Prompt API] Finished updating taglines');
210+
}
211+
212+
/**
213+
* Global function to update taglines with a custom prompt
214+
* Can be called from the browser console for experimentation
215+
*
216+
* @param {string} prompt - Custom prompt (e.g., "how many people attended", "was it fun")
217+
* @example
218+
* // From browser console:
219+
* updateHackTaglines("In one word, was this hack day productive?")
220+
* updateHackTaglines("How many people were mentioned?")
221+
* updateHackTaglines("What was the most interesting project?")
222+
*/
223+
window.updateHackTaglines = async function(prompt) {
224+
if (!prompt || typeof prompt !== 'string') {
225+
console.error('[Prompt API] Please provide a prompt string');
226+
console.info('[Prompt API] Example: updateHackTaglines("was this hack day fun?")');
227+
return;
228+
}
229+
230+
// Reset all taglines first
231+
const taglines = document.querySelectorAll('.hack-tagline');
232+
taglines.forEach(el => {
233+
el.textContent = '';
234+
el.classList.remove('hack-tagline--loaded');
235+
});
236+
237+
// Destroy existing session to get fresh results
238+
if (session) {
239+
try {
240+
session.destroy();
241+
} catch (e) {
242+
// Ignore errors on destroy
243+
}
244+
session = null;
245+
}
246+
247+
await updateTaglines(prompt);
248+
};
249+
250+
// Run on page load
251+
if (document.readyState === 'loading') {
252+
document.addEventListener('DOMContentLoaded', () => updateTaglines());
253+
} else {
254+
updateTaglines();
255+
}
256+
})();

pages/previous-hacks.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ Here are links to write-ups of our previous hack days!
1717
- {{hack.event_name}}
1818
{% endif %}
1919
</a>
20+
<span class="hack-tagline"></span>
2021
</li>
2122
{% endfor %}
22-
</ol>
23+
</ol>
24+
25+
<script src="/assets/prompt-api-summaries.js"></script>

0 commit comments

Comments
 (0)