feat: migrate skills review desk to astro
This commit is contained in:
+458
-94
@@ -1,94 +1,292 @@
|
||||
import { catalog } from './catalog.js';
|
||||
import { catalog as legacyCatalog } from './catalog.js';
|
||||
import { files } from './files.js';
|
||||
import { renderVoteWidget } from './vote.js';
|
||||
|
||||
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) => ({ '&':'&', '<':'<', '>':'>', '"':'"', "'":''' })[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;
|
||||
// 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) =>
|
||||
({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[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 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 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(' ');
|
||||
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 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 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 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 = [];
|
||||
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; }
|
||||
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 || ''));
|
||||
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; }
|
||||
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; }
|
||||
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>`);
|
||||
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 }];
|
||||
});
|
||||
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 '';
|
||||
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
|
||||
}));
|
||||
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 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);
|
||||
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.pushState({}, '', 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 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() : '';
|
||||
@@ -102,80 +300,246 @@ function selectFromUrl() {
|
||||
}
|
||||
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)));
|
||||
$('#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();
|
||||
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.'); }
|
||||
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;
|
||||
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; }
|
||||
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))));
|
||||
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();
|
||||
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>`;
|
||||
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;
|
||||
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; }
|
||||
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>`;
|
||||
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>`;
|
||||
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)}`;
|
||||
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()); });
|
||||
$('#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(); }
|
||||
$('#skill-filter').addEventListener('input', (event) => {
|
||||
state.query = event.target.value.toLowerCase().trim();
|
||||
renderList();
|
||||
schedulePackageSearch();
|
||||
});
|
||||
window.addEventListener('popstate', () => { selectFromUrl(); renderList(); renderDetail(); loadSelectedFile(); });
|
||||
selectFromUrl(); renderList(); renderDetail(); loadSelectedFile();
|
||||
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();
|
||||
|
||||
Reference in New Issue
Block a user