/**
* Help Page — live API catalog, search, copy buttons, smooth navigation
*/
const API_BASE = window.location.origin;
class HelpPage {
async init() {
this.setupSearch();
this.setupCopyButtons();
this.setupToc();
await this.loadLiveStats();
await this.renderAiAgentGuide();
await this.injectMaxProviderGuide();
console.log('[Help] Ready with /api/max catalog');
}
async renderAiAgentGuide() {
const panel = document.getElementById('ai-agent-panel');
if (!panel) return;
try {
const help = await this.fetchJson('/api/max/help');
const ai = help.for_ai_agents || {};
const steps = (ai.query_algorithm || []).map(s => `
${s}`).join('');
const picks = Object.entries(ai.recommended_entrypoints || {}).map(
([k, v]) => `${k} | ${v} |
`
).join('');
const intentRows = (help.intent_map || []).slice(0, 12).map(intent => `
${intent.id} |
${intent.user_wants} |
${intent.method} ${intent.path} |
${(intent.keywords || []).slice(0, 4).join(', ')} |
`).join('');
panel.innerHTML = `
${help.summary || ''}
${ai.read_this_first || ''}
Query algorithm
${steps}
Recommended entrypoints
Intent map (first 12 — full list in /api/max/help)
| ID | User wants | Endpoint | Keywords |
${intentRows}
Search intents: GET /api/max/help/intents?q=futures
| Download: guide.json
`;
} catch (err) {
panel.innerHTML = `Could not load AI guide: ${err.message}. Use /api/max/help directly.
`;
}
}
setupSearch() {
const input = document.getElementById('help-search');
if (!input) return;
input.addEventListener('input', (e) => {
const q = e.target.value.toLowerCase().trim();
document.querySelectorAll('.help-section').forEach((section) => {
const text = section.textContent.toLowerCase();
section.style.display = !q || text.includes(q) ? '' : 'none';
});
});
}
setupToc() {
document.querySelectorAll('.help-toc a').forEach((link) => {
link.addEventListener('click', (e) => {
const id = link.getAttribute('href')?.slice(1);
const el = id && document.getElementById(id);
if (el) {
e.preventDefault();
el.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
});
});
}
setupCopyButtons(root = document) {
root.querySelectorAll('[data-copy], .copy-btn').forEach((btn) => {
if (btn.dataset.boundCopy === 'true') return;
btn.dataset.boundCopy = 'true';
btn.addEventListener('click', async () => {
const text = btn.getAttribute('data-copy') || btn.closest('.code-wrap')?.querySelector('code')?.textContent;
if (!text) return;
try {
await navigator.clipboard.writeText(text.trim());
const label = btn.textContent;
btn.textContent = 'Copied!';
setTimeout(() => { btn.textContent = label; }, 1500);
} catch {
btn.textContent = 'Copy failed';
}
});
});
}
async fetchJson(path, timeoutMs = 15000) {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeoutMs);
try {
const res = await fetch(`${API_BASE}${path}`, { signal: controller.signal, headers: { Accept: 'application/json' } });
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return await res.json();
} finally {
clearTimeout(timer);
}
}
async loadLiveStats() {
const el = document.getElementById('live-stats');
if (!el) return;
try {
const [health, providers, pages, pressure] = await Promise.all([
this.fetchJson('/api/max/health'),
this.fetchJson('/api/max/providers/status?symbol=BTC'),
this.fetchJson('/api/max/pages/functionality'),
this.fetchJson('/api/max/pressure/status'),
]);
const summary = health.summary || health.resource_registry || {};
el.innerHTML = `
${(summary.comprehensive_resources || 0) + (summary.unified_registry_entries || 0)}Resource entries
${providers.healthy_count ?? 0}/${providers.probed_count ?? 0}Provider probes
${pages.functional_count ?? 0}/${pages.total ?? 0}Functional pages
${(pressure.providers || []).length}Pressure-tracked providers
${health.cache?.entries ?? 0}Cache entries
`;
} catch (err) {
el.innerHTML = `Live /api/max stats unavailable (${err.message}). Check /api/max/health after the Space starts.
`;
}
}
async injectMaxProviderGuide() {
const main = document.querySelector('.help-main');
if (!main || document.getElementById('max-provider-api')) return;
let help;
try {
help = await this.fetchJson('/api/max/help');
} catch (error) {
help = {
catalog: { endpoints: [] },
examples: {
health: 'curl $BASE/api/max/health',
providers: "curl '$BASE/api/max/providers/status?symbol=BTC&include_slow=true'",
visual: "curl '$BASE/api/max/system/visual?symbol=BTC'",
},
important_notes: [`Could not load live catalog: ${error.message}`],
};
}
const toc = document.querySelector('.help-toc ul');
if (toc && !toc.querySelector('a[href="#max-provider-api"]')) {
const li = document.createElement('li');
li.innerHTML = 'Max Provider API';
toc.insertBefore(li, toc.children[2] || null);
}
const endpoints = help.catalog?.endpoints || [];
const rows = endpoints.map(ep => `
| ${ep.method || 'GET'} |
${ep.path} |
${ep.group || 'API'} |
${ep.purpose || ''} |
`).join('');
const examples = Object.entries(help.quick_examples || help.examples || {}).map(
([name, cmd]) => `${name}: ${String(cmd).replaceAll('{BASE}', API_BASE).replaceAll('$BASE', API_BASE)}`
).join('\n');
const notes = (help.important_rules || help.important_notes || []).map(n => `${n}`).join('');
const section = document.createElement('section');
section.className = 'help-section';
section.id = 'max-provider-api';
section.innerHTML = `
Max Provider API — Endpoint Catalog
Live from /api/max/help (schema v${help.schema_version || '1'}). ${help.intent_count || 0} intents mapped for AI agents.
| Method | Path | Group | Purpose |
${rows || '| Endpoint catalog unavailable. |
'}
${examples || `curl ${API_BASE}/api/max/health`}
Runtime guarantees
`;
main.insertBefore(section, main.firstElementChild?.nextSibling || main.firstElementChild || null);
this.setupCopyButtons(section);
this.setupToc();
}
}
export default HelpPage;