Files
ai-for-dummies/skills-review/app.js
Marcos Paulo 3fdfbe3ae9 revert(review-desk): restore replaceState for URL sync
Task 16 changed history.replaceState to pushState, described as
'back/forward restoration'. It is a user-facing behaviour change the
brief forbids: every search keystroke, filter, tab and preview toggle
would push a history entry, so Back walks the interaction log instead of
leaving the page. main's popstate handler already existed and works the
same either way.
2026-09-05 17:09:40 +00:00

546 lines
22 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { catalog as legacyCatalog } from './catalog.js';
import { files } from './files.js';
import { renderVoteWidget } from './vote.js';
// The Astro review-desk route serializes the typed content collection before
// this client module loads. Keeping the legacy fallback preserves the vanilla
// page until cutover removes it.
const catalog = window.__SKILLS_REVIEW_CATALOG || legacyCatalog;
const state = {
selected: catalog[0],
query: '',
preview: 'original',
file: null,
sourceByPath: new Map(),
lens: false,
rendered: false,
diff: false,
searching: false,
contentMatches: new Set(),
searchTimer: null,
searchRequest: 0,
};
const $ = (selector) => document.querySelector(selector);
const escape = (value) =>
value.replace(
/[&<>"']/g,
(character) =>
({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#039;' })[character],
);
const redact = (value, entry) => {
const safe = value
.replace(/(NDO_PASS[^\n=]*[=:]\s*["']?)[^\n"']+/gi, '$1[REDACTED]')
.replace(/(password["']?\s*[:=]\s*["']?)[^\n"']+/gi, '$1[REDACTED]')
.replace(/\b[\w.+-]+@[\w.-]+\.[a-z]{2,}\b/gi, '[REDACTED SERVICE ACCOUNT]')
.replace(/\/home\/[A-Za-z0-9._-]+(?=\/)/g, '[REDACTED LOCAL USER]')
.replace(/display\/~[A-Za-z0-9._-]+/gi, 'display/~[REDACTED USER]');
return entry.id === 'ndo-repro'
? safe
.replace(/https?:\/\/[^\s)>]+/gi, '[REDACTED URL]')
.replace(/\b(?:[\w-]+\.)*netcracker\.[\w.-]+\b/gi, '[REDACTED HOST]')
.replace(/\bpedro[._ -]?aranha\b/gi, '[REDACTED CONTRIBUTOR]')
: safe;
};
const download = (name, content) => {
const url = URL.createObjectURL(new Blob([content], { type: 'text/markdown' }));
const a = document.createElement('a');
a.href = url;
a.download = name;
a.click();
URL.revokeObjectURL(url);
};
const copy = async (content) => {
if (navigator.clipboard?.writeText) return navigator.clipboard.writeText(content);
const textarea = document.createElement('textarea');
textarea.value = content;
textarea.setAttribute('readonly', '');
textarea.style.position = 'fixed';
textarea.style.opacity = '0';
document.body.append(textarea);
textarea.select();
document.execCommand('copy');
textarea.remove();
};
const packageFiles = (entry = state.selected) =>
files[entry.id] || [{ name: 'SKILL.md', path: entry.path, kind: 'skill' }];
const packageSearchText = (entry) =>
packageFiles(entry)
.map((file) => `${file.name} ${file.kind}`)
.join(' ');
function packageSummary(entry) {
const counts = packageFiles(entry).reduce((all, file) => {
all[file.kind] = (all[file.kind] || 0) + 1;
return all;
}, {});
const labels = {
skill: 'skill',
reference: 'ref',
script: 'script',
template: 'template',
data: 'data',
asset: 'asset',
};
return Object.entries(counts)
.map(([kind, count]) => `${count} ${labels[kind] || kind}${count === 1 ? '' : 's'}`)
.join(' · ');
}
const unchangedDraft = (file) =>
`# ${file.name}\n\n> Kept as-is in the improved package\n\nThis ${file.kind} file was not rewritten. Select **Change lens** to see why the improved draft concentrates its changes in the main skill contract.`;
const currentSource = () => state.sourceByPath.get(state.file.path);
const currentContent = () =>
state.preview === 'original'
? currentSource() || 'Loading original file…'
: state.file.improved ||
(state.file.name === 'SKILL.md'
? state.selected.improved
: currentSource()
? `# ${state.file.name}\n\n> Kept as-is in the improved package\n\n${currentSource()}`
: unchangedDraft(state.file));
const inlineMarkdown = (value) =>
escape(value)
.replace(/`([^`]+)`/g, '<code>$1</code>')
.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>')
.replace(/(?<!\*)\*([^*]+)\*(?!\*)/g, '<em>$1</em>')
.replace(
/\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/g,
'<a href="$2" target="_blank" rel="noreferrer">$1 ↗</a>',
);
const tableCells = (line) =>
line
.trim()
.replace(/^\||\|$/g, '')
.split('|')
.map((cell) => cell.trim());
const isTableDivider = (line) => /^\s*\|?\s*:?-{3,}:?\s*(\|\s*:?-{3,}:?\s*)+\|?\s*$/.test(line);
function markdownMarkup(markdown) {
const lines = markdown.replace(/\r/g, '').split('\n');
const headings = markdownHeadings(markdown);
let headingIndex = 0;
let index = 0;
const out = [];
if (lines[0] === '---') {
const end = lines.indexOf('---', 1);
if (end > 0) {
out.push(
`<dl class="markdown-frontmatter">${lines
.slice(1, end)
.map((line) => {
const [key, ...rest] = line.split(':');
return rest.length
? `<dt>${escape(key)}</dt><dd>${inlineMarkdown(rest.join(':').trim())}</dd>`
: '';
})
.join('')}</dl>`,
);
index = end + 1;
}
}
const startsBlock = (line, next) =>
!line ||
/^#{1,6}\s+/.test(line) ||
/^```/.test(line) ||
/^[-*+]\s+/.test(line) ||
/^\d+\.\s+/.test(line) ||
/^>\s?/.test(line) ||
/^---+$/.test(line) ||
(line.includes('|') && isTableDivider(next || ''));
while (index < lines.length) {
const line = lines[index];
if (!line.trim()) {
index += 1;
continue;
}
const heading = line.match(/^(#{1,6})\s+(.+)$/);
if (heading) {
const level = heading[1].length;
const item = headings[headingIndex++];
out.push(`<h${level} id="${item.id}">${inlineMarkdown(heading[2])}</h${level}>`);
index += 1;
continue;
}
if (/^```/.test(line)) {
const language = line.slice(3).trim();
const code = [];
index += 1;
while (index < lines.length && !/^```/.test(lines[index])) code.push(lines[index++]);
if (index < lines.length) index += 1;
out.push(
`<pre><code${language ? ` data-language="${escape(language)}"` : ''}>${escape(code.join('\n'))}</code></pre>`,
);
continue;
}
if (line.includes('|') && isTableDivider(lines[index + 1] || '')) {
const headings = tableCells(line);
index += 2;
const rows = [];
while (index < lines.length && lines[index].includes('|') && lines[index].trim())
rows.push(tableCells(lines[index++]));
out.push(
`<div class="markdown-table-wrap"><table><thead><tr>${headings.map((cell) => `<th>${inlineMarkdown(cell)}</th>`).join('')}</tr></thead><tbody>${rows.map((row) => `<tr>${headings.map((_, cell) => `<td>${inlineMarkdown(row[cell] || '')}</td>`).join('')}</tr>`).join('')}</tbody></table></div>`,
);
continue;
}
const list = line.match(/^([-*+]|\d+\.)\s+(.+)$/);
if (list) {
const ordered = /\d+\./.test(list[1]);
const items = [];
while (index < lines.length) {
const item = lines[index].match(ordered ? /^\d+\.\s+(.+)$/ : /^[-*+]\s+(.+)$/);
if (!item) break;
items.push(`<li>${inlineMarkdown(item[1])}</li>`);
index += 1;
}
out.push(`<${ordered ? 'ol' : 'ul'}>${items.join('')}</${ordered ? 'ol' : 'ul'}>`);
continue;
}
if (/^>\s?/.test(line)) {
const quote = [];
while (index < lines.length && /^>\s?/.test(lines[index]))
quote.push(lines[index++].replace(/^>\s?/, ''));
out.push(`<blockquote>${inlineMarkdown(quote.join(' '))}</blockquote>`);
continue;
}
if (/^---+$/.test(line)) {
out.push('<hr>');
index += 1;
continue;
}
const paragraph = [line];
index += 1;
while (index < lines.length && !startsBlock(lines[index], lines[index + 1]))
paragraph.push(lines[index++]);
out.push(`<p>${inlineMarkdown(paragraph.join(' '))}</p>`);
}
return out.join('');
}
function markdownHeadings(markdown) {
const used = new Map();
let fenced = false;
return markdown
.replace(/\r/g, '')
.split('\n')
.flatMap((line) => {
if (/^```/.test(line)) {
fenced = !fenced;
return [];
}
const match = !fenced && line.match(/^(#{1,6})\s+(.+)$/);
if (!match) return [];
const text = match[2].replace(/[`*_\[\]]/g, '').trim();
const base =
text
.toLowerCase()
.replace(/[^\p{L}\p{N}]+/gu, '-')
.replace(/(^-|-$)/g, '') || 'section';
const seen = used.get(base) || 0;
used.set(base, seen + 1);
return [{ level: match[1].length, text, id: seen ? `${base}-${seen + 1}` : base }];
});
}
function markdownToc(markdown) {
const headings = markdownHeadings(markdown);
if (headings.length < 2) return '';
return `<nav class="markdown-toc" aria-label="On this page"><span>ON THIS PAGE</span><ol>${headings.map((heading) => `<li class="level-${heading.level}"><a href="#${heading.id}">${escape(heading.text)}</a></li>`).join('')}</ol></nav>`;
}
const changeRows = (entry) =>
entry.improve.map((why, index) => ({
kind: ['SAFETY', 'SCOPE', 'EVIDENCE', 'STRUCTURE'][index] || 'CLARITY',
before:
index === 0
? 'The submitted guidance leaves a material decision implicit.'
: 'The submitted package carries detail without a clear boundary.',
after:
index === 0
? 'The improved draft makes the operating rule explicit.'
: 'The improved draft moves the decision into a smaller, reviewable contract.',
why,
}));
function visible() {
return catalog.filter(
(item) =>
`${item.author} ${item.title} ${item.id} ${item.focus} ${packageSearchText(item)}`
.toLowerCase()
.includes(state.query) || state.contentMatches.has(item.id),
);
}
function syncUrl() {
const url = new URL(window.location.href);
url.searchParams.set('author', state.selected.author);
url.searchParams.set('skill', state.selected.id);
url.searchParams.set('view', state.preview);
if (state.file && state.file.name !== 'SKILL.md') url.searchParams.set('file', state.file.name);
else url.searchParams.delete('file');
if (state.preview === 'improved' && state.lens) url.searchParams.set('lens', 'changes');
else url.searchParams.delete('lens');
if (state.rendered) url.searchParams.set('render', 'preview');
else url.searchParams.delete('render');
if (state.diff) url.searchParams.set('compare', 'diff');
else url.searchParams.delete('compare');
history.replaceState({}, '', url);
}
function selectFromUrl() {
const params = new URLSearchParams(window.location.search);
const author = params.get('author');
const id = params.get('skill');
const view = params.get('view');
const byAuthor =
author && catalog.filter((item) => item.author.toLowerCase() === author.toLowerCase());
const byId = id && catalog.find((item) => item.id === id);
state.selected = byId || byAuthor?.[0] || catalog[0];
state.query = byAuthor ? state.selected.author.toLowerCase() : '';
state.preview = view === 'improved' ? 'improved' : 'original';
state.lens = state.preview === 'improved' && params.get('lens') === 'changes';
state.rendered = params.get('render') === 'preview';
state.diff = params.get('compare') === 'diff';
state.file = packageFiles().find((item) => item.name === params.get('file')) || packageFiles()[0];
$('#skill-filter').value = byAuthor ? state.selected.author : '';
$('#submission-count').textContent = `${catalog.length} submissions`;
}
function renderList() {
const items = visible();
$('#count').textContent = state.searching
? `Searching package files… ${items.length} of ${catalog.length}`
: `${items.length} of ${catalog.length} reviewed`;
$('#skill-list').innerHTML = items
.map(
(item) =>
`<button role="option" aria-selected="${item.id === state.selected.id}" class="${item.id === state.selected.id ? 'active' : ''}" data-id="${item.id}"><span>AUTHOR · ${escape(item.author)}</span><strong>${escape(item.title)}</strong><small>SKILL · ${escape(item.id)} · ${escape(item.status)}</small><em>${escape(packageSummary(item))}</em></button>`,
)
.join('');
$('#skill-list')
.querySelectorAll('button')
.forEach((button) => button.addEventListener('click', () => selectSkill(button.dataset.id)));
}
function selectSkill(id, focus = false) {
state.selected = catalog.find((item) => item.id === id) || state.selected;
state.file = packageFiles()[0];
state.preview = 'original';
state.lens = false;
state.rendered = false;
state.diff = false;
syncUrl();
renderList();
renderDetail();
loadSelectedFile();
if (focus) $('#skill-list').querySelector(`[data-id="${state.selected.id}"]`)?.focus();
}
async function fetchSource(entry, file) {
if (state.sourceByPath.has(file.path)) return state.sourceByPath.get(file.path);
try {
state.sourceByPath.set(file.path, redact(await (await fetch(file.path)).text(), entry));
} catch {
state.sourceByPath.set(
file.path,
'# Original preview unavailable\n\nServe this site from the repository root to load the submitted source.',
);
}
return state.sourceByPath.get(file.path);
}
async function loadSelectedFile() {
const entry = state.selected;
const file = state.file;
await fetchSource(entry, file);
if (state.selected.id === entry.id && state.file.path === file.path) renderDetail();
return state.sourceByPath.get(file.path);
}
function schedulePackageSearch() {
clearTimeout(state.searchTimer);
const query = state.query;
const request = ++state.searchRequest;
state.contentMatches.clear();
if (query.length < 3) {
state.searching = false;
renderList();
return;
}
state.searchTimer = setTimeout(async () => {
state.searching = true;
renderList();
await Promise.all(
catalog.flatMap((entry) => packageFiles(entry).map((file) => fetchSource(entry, file))),
);
if (request !== state.searchRequest) return;
state.contentMatches = new Set(
catalog
.filter((entry) =>
packageFiles(entry).some((file) =>
state.sourceByPath.get(file.path)?.toLowerCase().includes(query),
),
)
.map((entry) => entry.id),
);
state.searching = false;
renderList();
}, 180);
}
function lensMarkup(entry) {
return `<section class="change-lens" aria-label="Why this improved draft changed"><header><div><span>CHANGE LENS</span><h3>What changed — and why.</h3></div><button data-lens aria-pressed="true">Back to draft</button></header><p>The improved draft keeps the job, but narrows the decisions an agent must make from memory.</p><div class="change-rows">${changeRows(
entry,
)
.map(
(change, index) =>
`<article><span>0${index + 1} / ${change.kind}</span><div><b> Before</b><p>${escape(change.before)}</p></div><div><b>+ After</b><p>${escape(change.after)}</p></div><aside><b>Why</b><p>${escape(change.why)}</p></aside></article>`,
)
.join('')}</div></section>`;
}
function diffRows(before, after) {
const oldLines = before.split('\n');
const newLines = after.split('\n');
const rows = [];
let oldIndex = 0;
let newIndex = 0;
while (oldIndex < oldLines.length || newIndex < newLines.length) {
if (oldLines[oldIndex] === newLines[newIndex]) {
rows.push(
`<p class="same"><span>${oldIndex + 1}</span>${escape(oldLines[oldIndex] || '')}</p>`,
);
oldIndex += 1;
newIndex += 1;
continue;
}
const oldAhead = oldLines.slice(oldIndex + 1, oldIndex + 9).indexOf(newLines[newIndex]);
const newAhead = newLines.slice(newIndex + 1, newIndex + 9).indexOf(oldLines[oldIndex]);
if (
newIndex < newLines.length &&
(oldIndex >= oldLines.length || (oldAhead === -1 && newAhead !== -1) || newAhead < oldAhead)
) {
rows.push(`<p class="added"><span>+</span>${escape(newLines[newIndex++])}</p>`);
continue;
}
if (oldIndex < oldLines.length) {
rows.push(`<p class="removed"><span></span>${escape(oldLines[oldIndex++])}</p>`);
continue;
}
}
return rows.join('');
}
function diffMarkup(entry) {
if (state.file.name !== 'SKILL.md')
return `<section class="skill-diff" aria-label="Draft comparison"><header><div><span>PACKAGE DIFF</span><h3>Supporting file unchanged.</h3></div><button data-diff aria-pressed="true">Back to draft</button></header><p>This review only rewrites the main skill contract. The selected ${escape(state.file.kind)} file remains available in its original form.</p></section>`;
return `<section class="skill-diff" aria-label="Original and improved skill comparison"><header><div><span>SKILL DIFF</span><h3>Original → improved draft</h3></div><button data-diff aria-pressed="true">Back to draft</button></header><p>Green lines are additions; red lines are removals. Unmarked lines are shared context.</p><div class="diff-lines">${diffRows(currentSource() || 'Loading original Markdown…', entry.improved)}</div></section>`;
}
function previewMarkup(entry, available) {
if (state.preview === 'improved' && state.lens) return lensMarkup(entry);
if (state.diff) return diffMarkup(entry);
const label =
state.preview === 'original'
? 'ORIGINAL / SAFETY-REDACTED WHERE NEEDED'
: 'IMPROVED DRAFT / PACKAGE-AWARE';
const content = currentContent();
const body = state.rendered
? `<div class="markdown-preview" aria-label="Rendered Markdown preview">${markdownToc(content)}${markdownMarkup(content)}</div>`
: `<pre><code>${escape(content)}</code></pre>`;
return `<section class="preview"><header><div class="preview-title"><span>FILE PREVIEW</span><small>${label}</small></div><div>${state.preview === 'improved' ? '<button data-lens aria-pressed="false">Change lens</button>' : ''}<button data-diff aria-pressed="false">Diff</button><button class="preview-markdown" data-render aria-pressed="${state.rendered}">${state.rendered ? 'View source' : 'Preview Markdown'}</button><button data-copy>Copy</button><button data-download>Download</button></div></header><nav class="file-tabs" aria-label="Skill package files">${available.map((item) => `<button class="${item.name === state.file.name ? 'active' : ''}" data-file="${escape(item.name)}"><span>${escape(item.kind)}</span>${escape(item.name)}</button>`).join('')}</nav>${body}</section>`;
}
function renderDetail() {
const entry = state.selected;
const available = packageFiles(entry);
$('#detail').innerHTML =
`<header><div><span class="status">${escape(entry.status)}</span><h2>${escape(entry.title)}</h2><p>Submitted by <a class="author-link" href="?author=${encodeURIComponent(entry.author)}">${escape(entry.author)}</a> · <a class="share-link" href="?author=${encodeURIComponent(entry.author)}&skill=${encodeURIComponent(entry.id)}&view=${state.preview}">share review ↗</a></p></div><div class="switch" role="group" aria-label="Preview version"><button class="${state.preview === 'original' ? 'active' : ''}" data-preview="original">Original</button><button class="${state.preview === 'improved' ? 'active' : ''}" data-preview="improved">Improved draft</button></div></header><div class="purpose"><span>THE JOB</span><p>${escape(entry.focus)}</p></div><div id="vote-widget"></div><div class="review-grid"><section><span>WHAT'S ALREADY WORKING</span><ul>${entry.wins.map((item) => `<li>${escape(item)}</li>`).join('')}</ul></section><section><span>HIGHEST-VALUE IMPROVEMENTS</span><ul>${entry.improve.map((item) => `<li>${escape(item)}</li>`).join('')}</ul></section></div><aside class="extras"><span>GOOD NEXT ADDITION</span><p>${escape(entry.extras)}</p></aside>${previewMarkup(entry, available)}`;
renderVoteWidget($('#vote-widget'), entry.id);
$('#detail')
.querySelectorAll('[data-file]')
.forEach((button) =>
button.addEventListener('click', () => {
state.file = available.find((item) => item.name === button.dataset.file) || available[0];
state.rendered = false;
state.diff = false;
syncUrl();
renderDetail();
loadSelectedFile();
}),
);
$('#detail')
.querySelectorAll('[data-preview]')
.forEach((button) =>
button.addEventListener('click', () => {
state.preview = button.dataset.preview;
state.lens = false;
state.rendered = false;
state.diff = false;
syncUrl();
renderDetail();
loadSelectedFile();
}),
);
$('#detail')
.querySelectorAll('[data-lens]')
.forEach((button) =>
button.addEventListener('click', () => {
state.lens = !state.lens;
state.rendered = false;
state.diff = false;
syncUrl();
renderDetail();
}),
);
$('#detail')
.querySelectorAll('[data-diff]')
.forEach((button) =>
button.addEventListener('click', async () => {
await loadSelectedFile();
state.diff = !state.diff;
state.rendered = false;
state.lens = false;
syncUrl();
renderDetail();
}),
);
$('[data-render]')?.addEventListener('click', async () => {
await loadSelectedFile();
state.rendered = !state.rendered;
syncUrl();
renderDetail();
});
$('[data-copy]')?.addEventListener('click', async () => {
await loadSelectedFile();
await copy(currentContent());
$('[data-copy]').textContent = 'Copied';
});
$('[data-download]')?.addEventListener('click', async () => {
await loadSelectedFile();
download(
`${entry.id}-${state.file.name.replaceAll('/', '-')}-${state.preview}.md`,
currentContent(),
);
});
}
$('#skill-filter').addEventListener('input', (event) => {
state.query = event.target.value.toLowerCase().trim();
renderList();
schedulePackageSearch();
});
document.addEventListener('keydown', (event) => {
if (
event.metaKey ||
event.ctrlKey ||
event.altKey ||
/^(INPUT|TEXTAREA|SELECT)$/.test(document.activeElement?.tagName || '')
)
return;
const items = visible();
const current = items.findIndex((item) => item.id === state.selected.id);
if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
event.preventDefault();
const offset = event.key === 'ArrowDown' ? 1 : -1;
selectSkill(items[(current + offset + items.length) % items.length]?.id, true);
}
if (event.key.toLowerCase() === 'p') {
event.preventDefault();
$('[data-render]')?.click();
}
});
window.addEventListener('popstate', () => {
selectFromUrl();
renderList();
renderDetail();
loadSelectedFile();
});
selectFromUrl();
renderList();
renderDetail();
loadSelectedFile();