Skip to content

Commit 630dd42

Browse files
ndbroadbentclaude
andcommitted
Add sortable activity list modal and score field
- Add `score` field to ClassifiedActivity (interestingScore biome.json bun.lock chat-to-map CLAUDE.md coverage dist images knip.json lefthook.yml LICENSE node_modules package.json PRD.txt README.md scripts src Taskfile.yml tests tmp TODO.md tsconfig.build.json tsconfig.json tsconfig.types.json vitest.config.ts 2 + funScore) - Activity list modal now has sort dropdown: Most Interesting, Oldest, Newest - Default sort is "Most Interesting" (by score descending) - Render activity list dynamically in JavaScript for sorting 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 2cef167 commit 630dd42

4 files changed

Lines changed: 67 additions & 50 deletions

File tree

src/classifier/index.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -60,11 +60,16 @@ function toClassifiedActivity(
6060
const capitalizedTitle = title.charAt(0).toUpperCase() + title.slice(1)
6161

6262
// Build activity without ID first
63+
const funScore = response.fun
64+
const interestingScore = response.int
65+
const score = interestingScore * 2 + funScore
66+
6367
const activity = {
6468
messageId: candidate.messageId,
6569
activity: capitalizedTitle,
66-
funScore: response.fun,
67-
interestingScore: response.int,
70+
funScore,
71+
interestingScore,
72+
score,
6873
category: normalizeCategory(response.cat),
6974
confidence: response.conf,
7075
originalMessage: candidate.content,
@@ -233,17 +238,12 @@ export function filterActivities(suggestions: readonly ClassifiedActivity[]): Cl
233238
}
234239

235240
/**
236-
* Sort activities by score (interesting prioritized over fun).
237-
* Score = interestingScore * 2 + funScore
241+
* Sort activities by score (highest first).
238242
*/
239243
export function sortActivitiesByScore(
240244
activities: readonly ClassifiedActivity[]
241245
): ClassifiedActivity[] {
242-
return [...activities].sort((a, b) => {
243-
const scoreA = a.interestingScore * 2 + a.funScore
244-
const scoreB = b.interestingScore * 2 + b.funScore
245-
return scoreB - scoreA
246-
})
246+
return [...activities].sort((a, b) => b.score - a.score)
247247
}
248248

249249
/**

src/export/map-html.ts

Lines changed: 49 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ interface MapPoint {
2626
activityId: string
2727
location: string
2828
date: string
29+
score: number
2930
url: string | null
3031
color: string
3132
imagePath: string | null
@@ -105,6 +106,7 @@ function toMapPoints(
105106
activityId: s.activityId,
106107
location: formatLocation(s) ?? '',
107108
date: s.timestamp.toISOString().split('T')[0] ?? '',
109+
score: s.score,
108110
url: extractUrl(s.originalMessage),
109111
color,
110112
imagePath: config.imagePaths?.get(s.activityId) ?? null,
@@ -155,40 +157,6 @@ function generateMarkersJS(points: readonly MapPoint[]): string {
155157
.join('\n')
156158
}
157159

158-
/**
159-
* Generate activity list HTML for modal.
160-
*/
161-
function generateActivityListHTML(points: readonly MapPoint[]): string {
162-
return points
163-
.map((p) => {
164-
const senderName = p.sender.split(' ')[0] ?? p.sender
165-
const mapsUrl = p.placeId
166-
? `https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(p.activity)}&query_place_id=${p.placeId}`
167-
: null
168-
const thumbnail = p.imagePath
169-
? `<img src="${escapeJS(p.imagePath)}" class="activity-thumb" alt="" />`
170-
: '<div class="activity-thumb-placeholder"></div>'
171-
172-
return `
173-
<div class="activity-row">
174-
${thumbnail}
175-
<div class="activity-content">
176-
<div class="activity-title">${escapeJS(p.activity)}</div>
177-
<div class="activity-meta">
178-
${p.location ? `<span class="activity-location">${escapeJS(p.location)}</span> · ` : ''}
179-
${escapeJS(senderName)} · ${p.date}
180-
</div>
181-
<div class="activity-links">
182-
${mapsUrl ? `<a href="${escapeJS(mapsUrl)}" target="_blank">Google Maps</a>` : ''}
183-
${p.url ? `<a href="${escapeJS(p.url)}" target="_blank">Source</a>` : ''}
184-
</div>
185-
</div>
186-
</div>
187-
`
188-
})
189-
.join('')
190-
}
191-
192160
/**
193161
* Generate legend HTML.
194162
*/
@@ -369,8 +337,10 @@ export function exportToMapHTML(
369337
.modal-overlay { display:none; position:fixed; inset:0; background:rgba(0,0,0,0.5); z-index:2000; justify-content:center; align-items:center; }
370338
.modal-overlay.open { display:flex; }
371339
.modal { background:#fff; border-radius:12px; width:90%; max-width:700px; max-height:85vh; display:flex; flex-direction:column; box-shadow:0 20px 50px rgba(0,0,0,0.3); }
372-
.modal-header { padding:20px 24px; border-bottom:1px solid #e5e7eb; display:flex; justify-content:space-between; align-items:center; }
373-
.modal-header h2 { margin:0; font-size:20px; font-weight:600; }
340+
.modal-header { padding:20px 24px; border-bottom:1px solid #e5e7eb; display:flex; justify-content:space-between; align-items:center; gap:16px; }
341+
.modal-header h2 { margin:0; font-size:20px; font-weight:600; flex:1; }
342+
.sort-control { display:flex; align-items:center; gap:8px; font-size:14px; color:#6b7280; }
343+
.sort-control select { padding:6px 10px; border:1px solid #d1d5db; border-radius:6px; font-size:14px; background:#fff; cursor:pointer; }
374344
.modal-close { background:none; border:none; font-size:24px; cursor:pointer; color:#6b7280; padding:4px 8px; }
375345
.modal-close:hover { color:#111; }
376346
.modal-body { padding:16px 24px; overflow-y:auto; flex:1; }
@@ -409,11 +379,17 @@ export function exportToMapHTML(
409379
<div class="modal" onclick="event.stopPropagation()">
410380
<div class="modal-header">
411381
<h2>Activity List</h2>
382+
<div class="sort-control">
383+
<label for="sortSelect">Sort by:</label>
384+
<select id="sortSelect" onchange="sortActivities(this.value)">
385+
<option value="score">Most Interesting</option>
386+
<option value="oldest">Oldest</option>
387+
<option value="newest">Newest</option>
388+
</select>
389+
</div>
412390
<button class="modal-close" onclick="closeModal()">&times;</button>
413391
</div>
414-
<div class="modal-body">
415-
${generateActivityListHTML(points)}
416-
</div>
392+
<div class="modal-body" id="activityListBody"></div>
417393
</div>
418394
</div>
419395
@@ -445,10 +421,44 @@ export function exportToMapHTML(
445421
map.fitBounds(markersLayer.getBounds(), { padding: [50, 50] });
446422
}
447423
424+
// Activity data for sorting
425+
var activities = ${JSON.stringify(
426+
points.map((p) => ({
427+
id: p.activityId,
428+
activity: p.activity,
429+
sender: p.sender.split(' ')[0] ?? p.sender,
430+
location: p.location,
431+
date: p.date,
432+
score: p.score,
433+
imagePath: p.imagePath,
434+
placeId: p.placeId,
435+
url: p.url
436+
}))
437+
)};
438+
439+
function renderActivityList(sorted) {
440+
var html = sorted.map(function(a) {
441+
var mapsUrl = a.placeId ? 'https://www.google.com/maps/search/?api=1&query=' + encodeURIComponent(a.activity) + '&query_place_id=' + a.placeId : null;
442+
var thumb = a.imagePath ? '<img src="' + a.imagePath + '" class="activity-thumb" alt="" />' : '<div class="activity-thumb-placeholder"></div>';
443+
var links = (mapsUrl ? '<a href="' + mapsUrl + '" target="_blank">Google Maps</a>' : '') + (a.url ? '<a href="' + a.url + '" target="_blank">Source</a>' : '');
444+
return '<div class="activity-row">' + thumb + '<div class="activity-content"><div class="activity-title">' + a.activity + '</div><div class="activity-meta">' + (a.location ? '<span class="activity-location">' + a.location + '</span> · ' : '') + a.sender + ' · ' + a.date + '</div><div class="activity-links">' + links + '</div></div></div>';
445+
}).join('');
446+
document.getElementById('activityListBody').innerHTML = html;
447+
}
448+
449+
function sortActivities(sortBy) {
450+
var sorted = activities.slice();
451+
if (sortBy === 'score') sorted.sort(function(a, b) { return b.score - a.score; });
452+
else if (sortBy === 'oldest') sorted.sort(function(a, b) { return a.date.localeCompare(b.date); });
453+
else if (sortBy === 'newest') sorted.sort(function(a, b) { return b.date.localeCompare(a.date); });
454+
renderActivityList(sorted);
455+
}
456+
448457
// Modal functions
449458
function openModal() {
450459
document.getElementById('activityModal').classList.add('open');
451460
document.body.style.overflow = 'hidden';
461+
sortActivities(document.getElementById('sortSelect').value);
452462
}
453463
function closeModal(e) {
454464
if (!e || e.target === e.currentTarget) {

src/test-support/index.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,11 +43,16 @@ export function createActivity(
4343
): ClassifiedActivity {
4444
const { messageId, activity, activityId: providedId, ...rest } = overrides
4545

46+
const funScore = (rest as Partial<ClassifiedActivity>).funScore ?? 0.7
47+
const interestingScore = (rest as Partial<ClassifiedActivity>).interestingScore ?? 0.5
48+
const score = (rest as Partial<ClassifiedActivity>).score ?? interestingScore * 2 + funScore
49+
4650
const base = {
4751
messageId,
4852
activity,
49-
funScore: 0.7,
50-
interestingScore: 0.5,
53+
funScore,
54+
interestingScore,
55+
score,
5156
category: 'other' as ActivityCategory,
5257
confidence: 0.9,
5358
originalMessage: activity,

src/types/classifier.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ export interface ClassifiedActivity {
2121
readonly funScore: number
2222
/** How interesting/unique is this activity? 0=common/mundane, 1=rare/novel */
2323
readonly interestingScore: number
24+
/** Combined score derived from interestingScore and funScore */
25+
readonly score: number
2426
readonly category: ActivityCategory
2527
readonly confidence: number
2628
readonly originalMessage: string

0 commit comments

Comments
 (0)