Skip to content

Commit a746922

Browse files
snapsynapseclaude
andcommitted
Add search, compare, and export features with canonical data export
Build pipeline restructured into three phases: (1) canonical data export as the intermediate JSON artifact, (2) HTML view generation, (3) file output. data.json is the first build output — designed as the Phase 5 JSON contract prototype that feeds client-side search, product comparison, and CSV/JSON export. New features: - Site-wide search with lazy-loaded index, debounced matching, keyboard nav - Compare page (up to 3 products, URL state, capability×product table) - CSV/JSON export on implementations and compare pages - Compare nav link added to all pages Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent df5d9ff commit a746922

11 files changed

Lines changed: 1191 additions & 110 deletions

File tree

docs/about.html

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -67,10 +67,15 @@ <h1><a href="index.html" onclick="passTheme(this)" style="color: inherit; text-d
6767
<nav class="site-nav" id="siteNav" aria-label="Main navigation">
6868
<a href="index.html" class="site-nav-link" onclick="passTheme(this)">Capabilities</a>
6969
<a href="implementations.html" class="site-nav-link" onclick="passTheme(this)">Detailed Availability</a>
70+
<a href="compare.html" class="site-nav-link" onclick="passTheme(this)">Compare</a>
7071
<a href="constraints.html" class="site-nav-link" onclick="passTheme(this)">Access & Limits</a>
7172
<a href="about.html" class="site-nav-link active" onclick="passTheme(this)">About</a>
7273
</nav>
7374
<div class="header-actions">
75+
<div class="site-search" role="combobox" aria-expanded="false" aria-haspopup="listbox" aria-owns="searchResults">
76+
<input type="search" id="siteSearchInput" class="search-input" placeholder="Search features..." aria-label="Search features" aria-autocomplete="list" aria-controls="searchResults" autocomplete="off">
77+
<ul id="searchResults" class="search-results" role="listbox" hidden></ul>
78+
</div>
7479
<a href="https://github.com/snapsynapse/ai-capability-reference" class="github-link" title="View on GitHub">GitHub</a>
7580
<button class="theme-toggle" onclick="toggleTheme()" title="Toggle light/dark mode">🌓</button>
7681
</div>
@@ -98,11 +103,11 @@ <h2>What is this?</h2>
98103

99104
<h2>How it's built</h2>
100105

101-
<p>There is no database behind this project. Every piece of data — features, pricing tiers, platform support, talking points — lives in plain markdown files under <code>data/</code>. A single build script (<code>node scripts/build.js</code>) reads those files and renders the entire static site into <code>docs/</code>.</p>
106+
<p>There is no database behind this project. Every piece of data — capabilities, implementations, pricing tiers, platform support, talking points — lives in plain markdown and YAML files under <code>data/</code>. A single build script (<code>node scripts/build.js</code>) reads those files and renders the entire static site into <code>docs/</code>.</p>
102107

103108
<p>That's the whole stack: markdown files, javascript, and Git.</p>
104109

105-
<p>This means contributing doesn't require a dev environment, an ORM, or a running database. You edit a <code>.md</code> file, open a PR, and the CI rebuilds the site. Git provides versioning, review, and an audit trail for every change. If you can read a markdown table, you can read (and fix) the data.</p>
110+
<p>This means contributing doesn't require a dev environment, an ORM, or a running database. You edit a <code>.md</code> or <code>.yml</code> file, open a PR, and the CI rebuilds the site. Git provides versioning, review, and an audit trail for every change. If you can read a markdown table, you can read (and fix) the data.</p>
106111

107112
<h2>Scope</h2>
108113

@@ -118,13 +123,13 @@ <h2>Scope</h2>
118123

119124
<p>All prices are listed in <strong>USD</strong>. Feature availability and pricing reflect the <strong>United States</strong> region by default; availability may differ in other regions.</p>
120125

121-
<h2>Platforms Covered</h2>
126+
<h2>Products Covered</h2>
122127

123-
<table><thead><tr><th>Platform</th><th>Vendor</th><th>Features Tracked</th></tr></thead><tbody><tr><td><strong>ChatGPT</strong></td><td>OpenAI</td><td>Agent Mode, Canvas, Voice, Sora, DALL-E, Search, Deep Research, Codex, Custom GPTs</td></tr><tr><td><strong>Claude</strong></td><td>Anthropic</td><td>Code, Cowork, MCP, Connectors, Projects, Artifacts, Extended Thinking, Vision</td></tr><tr><td><strong>Copilot</strong></td><td>Microsoft</td><td>Office Integration, Designer, Vision, Voice, Agent Builder</td></tr><tr><td><strong>Gemini</strong></td><td>Google</td><td>Advanced, NotebookLM, AI Studio, Deep Research, Gems, Workspace, Imagen, Veo, Live, Project Astra</td></tr><tr><td><strong>Perplexity</strong></td><td>Perplexity AI</td><td>Comet Browser, Agent Mode, Pro Search, Focus, Collections, Voice</td></tr><tr><td><strong>Grok</strong></td><td>xAI</td><td>Chat, Aurora (images), DeepSearch, Think Mode, Voice</td></tr><tr><td><strong>Meta</strong></td><td>Meta</td><td>Llama 3.3, Llama 4</td></tr><tr><td><strong>Mistral</strong></td><td>Mistral</td><td>Codestral, Mistral Large/Nemo, Mistral Small 3</td></tr><tr><td><strong>DeepSeek</strong></td><td>DeepSeek</td><td>DeepSeek V3 / R1</td></tr><tr><td><strong>Alibaba</strong></td><td>Alibaba</td><td>Qwen 2.5, Qwen 3, Qwen 3.5, Qwen-Coder</td></tr><tr><td><strong>Ollama</strong></td><td>Ollama</td><td>Self-hosted runtime product</td></tr><tr><td><strong>LM Studio</strong></td><td>LM Studio</td><td>Self-hosted runtime product</td></tr></tbody></table>
128+
<table><thead><tr><th>Product</th><th>Vendor</th><th>What's Tracked</th></tr></thead><tbody><tr><td><strong>ChatGPT</strong></td><td>OpenAI</td><td>Agent Mode, Canvas, Voice, Sora, DALL-E, Search, Deep Research, Codex, Custom GPTs</td></tr><tr><td><strong>Claude</strong></td><td>Anthropic</td><td>Code, Cowork, MCP, Connectors, Projects, Artifacts, Extended Thinking, Vision</td></tr><tr><td><strong>Copilot</strong></td><td>Microsoft</td><td>Office Integration, Designer, Vision, Voice, Agent Builder</td></tr><tr><td><strong>Gemini</strong></td><td>Google</td><td>Advanced, NotebookLM, AI Studio, Deep Research, Gems, Workspace, Imagen, Veo, Live, Project Astra</td></tr><tr><td><strong>Perplexity</strong></td><td>Perplexity AI</td><td>Comet Browser, Agent Mode, Pro Search, Focus, Collections, Voice</td></tr><tr><td><strong>Grok</strong></td><td>xAI</td><td>Chat, Aurora (images), DeepSearch, Think Mode, Voice</td></tr><tr><td><strong>Meta</strong></td><td>Meta</td><td>Llama 3.3, Llama 4</td></tr><tr><td><strong>Mistral</strong></td><td>Mistral</td><td>Codestral, Mistral Large/Nemo, Mistral Small 3</td></tr><tr><td><strong>DeepSeek</strong></td><td>DeepSeek</td><td>DeepSeek V3 / R1</td></tr><tr><td><strong>Alibaba</strong></td><td>Alibaba</td><td>Qwen 2.5, Qwen 3, Qwen 3.5, Qwen-Coder</td></tr><tr><td><strong>Ollama</strong></td><td>Ollama</td><td>Self-hosted runtime product</td></tr><tr><td><strong>LM Studio</strong></td><td>LM Studio</td><td>Self-hosted runtime product</td></tr></tbody></table>
124129
<h2>Features</h2>
125130

126-
<ul><li><strong>Plan-by-plan availability</strong> — See exactly which tier unlocks each feature</li>
127-
<li><strong>Capability-first view</strong> — Browse plain-English capabilities in addition to the feature view</li>
131+
<ul><li><strong>Plan-by-plan availability</strong> — See exactly which tier unlocks each implementation</li>
132+
<li><strong>Capability-first view</strong> — Browse plain-English capabilities as the primary entry point</li>
128133
<li><strong>Platform support</strong> — Windows, macOS, Linux, iOS, Android, web, terminal, API</li>
129134
<li><strong>Talking points</strong> — Ready-to-use sentences for presentations (click to copy)</li>
130135
<li><strong>Category filtering</strong> — Voice, Coding, Research, Agents, and more</li>
@@ -206,6 +211,7 @@ <h2>Credits</h2>
206211
</p>
207212
</footer>
208213
</div>
214+
<script src="assets/search.js"></script>
209215
<script>
210216
function toggleTheme() {
211217
document.documentElement.classList.toggle('light-mode');

docs/assets/compare.js

Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
/**
2+
* Product comparison — loads search-index.json and builds a capability × product
3+
* comparison table for up to 3 selected products.
4+
*/
5+
(function () {
6+
'use strict';
7+
8+
var container = document.getElementById('comparisonResult');
9+
var exportPanel = document.getElementById('compareExport');
10+
if (!container) return; // not on compare page
11+
12+
var index = null;
13+
var selectedProducts = [];
14+
15+
// Read URL state
16+
function readURL() {
17+
var params = new URLSearchParams(window.location.search);
18+
var ids = (params.get('products') || '').split(',').filter(Boolean);
19+
ids.forEach(function (id) {
20+
var cb = document.querySelector('.product-checkbox input[value="' + id + '"]');
21+
if (cb) cb.checked = true;
22+
});
23+
return ids;
24+
}
25+
26+
function updateURL() {
27+
var params = new URLSearchParams(window.location.search);
28+
if (selectedProducts.length) {
29+
params.set('products', selectedProducts.join(','));
30+
} else {
31+
params.delete('products');
32+
}
33+
var qs = params.toString();
34+
var url = window.location.pathname + (qs ? '?' + qs : '');
35+
history.replaceState(null, '', url);
36+
}
37+
38+
function loadIndex(cb) {
39+
if (index) return cb();
40+
var xhr = new XMLHttpRequest();
41+
xhr.open('GET', 'assets/data.json');
42+
xhr.onload = function () {
43+
if (xhr.status === 200) {
44+
try { index = JSON.parse(xhr.responseText); } catch (e) { index = { capabilities: [], implementations: [], products: [] }; }
45+
}
46+
cb();
47+
};
48+
xhr.onerror = function () { cb(); };
49+
xhr.send();
50+
}
51+
52+
function getSelectedProducts() {
53+
var checkboxes = document.querySelectorAll('.product-checkbox input:checked');
54+
var ids = [];
55+
for (var i = 0; i < checkboxes.length; i++) ids.push(checkboxes[i].value);
56+
return ids;
57+
}
58+
59+
// Enforce max 3 selection
60+
function enforceMax() {
61+
var all = document.querySelectorAll('.product-checkbox input');
62+
var checked = document.querySelectorAll('.product-checkbox input:checked');
63+
for (var i = 0; i < all.length; i++) {
64+
all[i].disabled = (checked.length >= 3 && !all[i].checked);
65+
}
66+
}
67+
68+
window.updateComparison = function () {
69+
enforceMax();
70+
selectedProducts = getSelectedProducts();
71+
updateURL();
72+
73+
if (selectedProducts.length < 2) {
74+
container.innerHTML = '<p class="compare-hint">Select at least 2 products to compare.</p>';
75+
if (exportPanel) exportPanel.hidden = true;
76+
return;
77+
}
78+
79+
loadIndex(function () {
80+
renderComparison();
81+
});
82+
};
83+
84+
function renderComparison() {
85+
if (!index) return;
86+
87+
// Build lookup: product → capability → implementation
88+
var implMap = {}; // { productId: { capId: impl } }
89+
selectedProducts.forEach(function (pid) { implMap[pid] = {}; });
90+
91+
index.implementations.forEach(function (impl) {
92+
if (selectedProducts.indexOf(impl.product) === -1) return;
93+
(impl.capabilities || []).forEach(function (capId) {
94+
if (!implMap[impl.product][capId]) {
95+
implMap[impl.product][capId] = impl;
96+
}
97+
});
98+
});
99+
100+
// Filter capabilities to those relevant for at least one selected product
101+
var relevantCaps = index.capabilities.filter(function (cap) {
102+
for (var i = 0; i < selectedProducts.length; i++) {
103+
if (implMap[selectedProducts[i]][cap.id]) return true;
104+
}
105+
return false;
106+
});
107+
108+
// Group capabilities by group
109+
var groups = {};
110+
var groupOrder = [];
111+
relevantCaps.forEach(function (cap) {
112+
var g = cap.group || 'other';
113+
if (!groups[g]) { groups[g] = []; groupOrder.push(g); }
114+
groups[g].push(cap);
115+
});
116+
117+
// Product name lookup
118+
var prodNames = {};
119+
index.products.forEach(function (p) { prodNames[p.id] = p.name; });
120+
121+
// Build table
122+
var html = '<table class="compare-table"><thead><tr><th>Capability</th>';
123+
selectedProducts.forEach(function (pid) {
124+
html += '<th>' + escapeHtml(prodNames[pid] || pid) + '</th>';
125+
});
126+
html += '</tr></thead><tbody>';
127+
128+
groupOrder.forEach(function (group) {
129+
html += '<tr class="compare-group-row"><td colspan="' + (selectedProducts.length + 1) + '">' + escapeHtml(formatGroup(group)) + '</td></tr>';
130+
groups[group].forEach(function (cap) {
131+
html += '<tr><td class="compare-cap-name">' + escapeHtml(cap.name) + '</td>';
132+
selectedProducts.forEach(function (pid) {
133+
var impl = implMap[pid][cap.id];
134+
if (impl) {
135+
var badge = gatingBadge(impl.gating);
136+
html += '<td class="compare-cell compare-yes">' + escapeHtml(impl.name) + ' ' + badge + '</td>';
137+
} else {
138+
html += '<td class="compare-cell compare-no">&mdash;</td>';
139+
}
140+
});
141+
html += '</tr>';
142+
});
143+
});
144+
145+
html += '</tbody></table>';
146+
147+
// Summary
148+
var total = index.capabilities.length;
149+
var summaryParts = selectedProducts.map(function (pid) {
150+
var count = 0;
151+
index.capabilities.forEach(function (cap) { if (implMap[pid][cap.id]) count++; });
152+
return escapeHtml(prodNames[pid] || pid) + ': ' + count + '/' + total;
153+
});
154+
html = '<p class="compare-summary">' + summaryParts.join(' &bull; ') + '</p>' + html;
155+
156+
container.innerHTML = html;
157+
if (exportPanel) exportPanel.hidden = false;
158+
}
159+
160+
function gatingBadge(gating) {
161+
if (!gating) return '';
162+
var cls = 'badge-gating';
163+
if (gating === 'free') cls += ' badge-free';
164+
else if (gating === 'paid') cls += ' badge-paid';
165+
return '<span class="' + cls + '">' + escapeHtml(gating) + '</span>';
166+
}
167+
168+
function formatGroup(g) {
169+
return g.replace(/-/g, ' ').replace(/\b\w/g, function (c) { return c.toUpperCase(); });
170+
}
171+
172+
function escapeHtml(s) {
173+
var d = document.createElement('div');
174+
d.appendChild(document.createTextNode(s || ''));
175+
return d.innerHTML;
176+
}
177+
178+
// Expose data for export.js
179+
window._compareData = function () {
180+
if (!index || selectedProducts.length < 2) return null;
181+
var prodNames = {};
182+
index.products.forEach(function (p) { prodNames[p.id] = p.name; });
183+
var implMap = {};
184+
selectedProducts.forEach(function (pid) { implMap[pid] = {}; });
185+
index.implementations.forEach(function (impl) {
186+
if (selectedProducts.indexOf(impl.product) === -1) return;
187+
(impl.capabilities || []).forEach(function (capId) {
188+
if (!implMap[impl.product][capId]) implMap[impl.product][capId] = impl;
189+
});
190+
});
191+
return { capabilities: index.capabilities, selectedProducts: selectedProducts, prodNames: prodNames, implMap: implMap };
192+
};
193+
194+
// Init from URL
195+
var initial = readURL();
196+
if (initial.length >= 2) {
197+
selectedProducts = initial;
198+
loadIndex(function () {
199+
enforceMax();
200+
renderComparison();
201+
});
202+
}
203+
})();

docs/assets/data.json

Lines changed: 1 addition & 0 deletions
Large diffs are not rendered by default.

docs/assets/export.js

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
/**
2+
* Export — CSV and JSON download for implementations page (filtered) and compare page.
3+
*/
4+
(function () {
5+
'use strict';
6+
7+
var today = new Date().toISOString().split('T')[0];
8+
9+
function download(content, filename, mime) {
10+
var blob = new Blob([content], { type: mime });
11+
var url = URL.createObjectURL(blob);
12+
var a = document.createElement('a');
13+
a.href = url;
14+
a.download = filename;
15+
document.body.appendChild(a);
16+
a.click();
17+
document.body.removeChild(a);
18+
URL.revokeObjectURL(url);
19+
}
20+
21+
function isComparePage() {
22+
return !!document.getElementById('comparisonResult');
23+
}
24+
25+
// --- Implementations page export ---
26+
27+
function getVisibleFeatures() {
28+
var cards = document.querySelectorAll('.feature-card');
29+
var features = [];
30+
for (var i = 0; i < cards.length; i++) {
31+
var card = cards[i];
32+
if (card.style.display === 'none' || card.closest('.platform-section[style*="display: none"]')) continue;
33+
var name = (card.querySelector('.feature-name') || {}).textContent || '';
34+
var platform = card.getAttribute('data-platform') || '';
35+
var category = card.getAttribute('data-category') || '';
36+
var gating = card.getAttribute('data-gating') || '';
37+
var status = card.getAttribute('data-status') || '';
38+
var launched = card.getAttribute('data-launched') || '';
39+
var verified = card.getAttribute('data-verified') || '';
40+
features.push({
41+
name: name.trim(),
42+
product: platform,
43+
category: category,
44+
gating: gating,
45+
status: status,
46+
launched: launched,
47+
verified: verified
48+
});
49+
}
50+
return features;
51+
}
52+
53+
function featuresToCSV(features) {
54+
var headers = ['Name', 'Product', 'Category', 'Access', 'Status', 'Launched', 'Verified'];
55+
var rows = [headers.join(',')];
56+
features.forEach(function (f) {
57+
rows.push([f.name, f.product, f.category, f.gating, f.status, f.launched, f.verified]
58+
.map(function (v) { return '"' + String(v).replace(/"/g, '""') + '"'; })
59+
.join(','));
60+
});
61+
return rows.join('\n');
62+
}
63+
64+
// --- Compare page export ---
65+
66+
function getCompareData() {
67+
if (typeof window._compareData === 'function') return window._compareData();
68+
return null;
69+
}
70+
71+
function compareToRows(data) {
72+
var rows = [];
73+
data.capabilities.forEach(function (cap) {
74+
var row = { capability: cap.name, group: cap.group || '' };
75+
data.selectedProducts.forEach(function (pid) {
76+
var impl = data.implMap[pid][cap.id];
77+
row[data.prodNames[pid] || pid] = impl ? impl.name + ' (' + (impl.gating || '') + ')' : '';
78+
});
79+
rows.push(row);
80+
});
81+
return rows;
82+
}
83+
84+
function compareToCSV(data) {
85+
var rows = compareToRows(data);
86+
if (!rows.length) return '';
87+
var headers = Object.keys(rows[0]);
88+
var lines = [headers.map(function (h) { return '"' + h.replace(/"/g, '""') + '"'; }).join(',')];
89+
rows.forEach(function (row) {
90+
lines.push(headers.map(function (h) { return '"' + String(row[h] || '').replace(/"/g, '""') + '"'; }).join(','));
91+
});
92+
return lines.join('\n');
93+
}
94+
95+
// --- Public API ---
96+
97+
window.exportCSV = function () {
98+
if (isComparePage()) {
99+
var data = getCompareData();
100+
if (!data) return;
101+
download(compareToCSV(data), 'airef-compare-' + today + '.csv', 'text/csv');
102+
} else {
103+
var features = getVisibleFeatures();
104+
if (!features.length) return;
105+
download(featuresToCSV(features), 'airef-features-' + today + '.csv', 'text/csv');
106+
}
107+
};
108+
109+
window.exportJSON = function () {
110+
if (isComparePage()) {
111+
var data = getCompareData();
112+
if (!data) return;
113+
var output = { exported: new Date().toISOString(), products: data.selectedProducts.map(function (pid) { return data.prodNames[pid] || pid; }), rows: compareToRows(data) };
114+
download(JSON.stringify(output, null, 2), 'airef-compare-' + today + '.json', 'application/json');
115+
} else {
116+
var features = getVisibleFeatures();
117+
if (!features.length) return;
118+
var output = { exported: new Date().toISOString(), features: features };
119+
download(JSON.stringify(output, null, 2), 'airef-features-' + today + '.json', 'application/json');
120+
}
121+
};
122+
123+
// Add export buttons to implementations page if not already present
124+
if (!isComparePage()) {
125+
var filterBar = document.querySelector('.filters');
126+
if (filterBar && !document.getElementById('implExportBtns')) {
127+
var div = document.createElement('div');
128+
div.id = 'implExportBtns';
129+
div.className = 'export-actions';
130+
div.innerHTML = '<button onclick="exportCSV()" class="export-btn">Export CSV</button> <button onclick="exportJSON()" class="export-btn">Export JSON</button>';
131+
filterBar.parentNode.insertBefore(div, filterBar.nextSibling);
132+
}
133+
}
134+
})();

0 commit comments

Comments
 (0)