| |
| |
| |
|
|
| 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 => `<li>${s}</li>`).join(''); |
| const picks = Object.entries(ai.recommended_entrypoints || {}).map( |
| ([k, v]) => `<tr><td><code>${k}</code></td><td><code>${v}</code></td></tr>` |
| ).join(''); |
| const intentRows = (help.intent_map || []).slice(0, 12).map(intent => ` |
| <tr> |
| <td><code>${intent.id}</code></td> |
| <td>${intent.user_wants}</td> |
| <td><code>${intent.method} ${intent.path}</code></td> |
| <td>${(intent.keywords || []).slice(0, 4).join(', ')}</td> |
| </tr> |
| `).join(''); |
| panel.innerHTML = ` |
| <p><strong>${help.summary || ''}</strong></p> |
| <p>${ai.read_this_first || ''}</p> |
| <h3>Query algorithm</h3> |
| <ol>${steps}</ol> |
| <h3>Recommended entrypoints</h3> |
| <table class="api-table"> |
| <thead><tr><th>Key</th><th>Endpoint</th></tr></thead> |
| <tbody>${picks}</tbody> |
| </table> |
| <h3>Intent map (first 12 — full list in <a href="/api/max/help">/api/max/help</a>)</h3> |
| <table class="api-table"> |
| <thead><tr><th>ID</th><th>User wants</th><th>Endpoint</th><th>Keywords</th></tr></thead> |
| <tbody>${intentRows}</tbody> |
| </table> |
| <p>Search intents: <code>GET /api/max/help/intents?q=futures</code> |
| | Download: <a href="/api/max/help/guide.json">guide.json</a></p> |
| `; |
| } catch (err) { |
| panel.innerHTML = `<p class="help-note">Could not load AI guide: ${err.message}. Use <code>/api/max/help</code> directly.</p>`; |
| } |
| } |
|
|
| 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 = ` |
| <div class="stat-pill"><strong>${(summary.comprehensive_resources || 0) + (summary.unified_registry_entries || 0)}</strong><span>Resource entries</span></div> |
| <div class="stat-pill"><strong>${providers.healthy_count ?? 0}/${providers.probed_count ?? 0}</strong><span>Provider probes</span></div> |
| <div class="stat-pill"><strong>${pages.functional_count ?? 0}/${pages.total ?? 0}</strong><span>Functional pages</span></div> |
| <div class="stat-pill"><strong>${(pressure.providers || []).length}</strong><span>Pressure-tracked providers</span></div> |
| <div class="stat-pill"><strong>${health.cache?.entries ?? 0}</strong><span>Cache entries</span></div> |
| `; |
| } catch (err) { |
| el.innerHTML = `<p class="help-note">Live /api/max stats unavailable (${err.message}). Check <code>/api/max/health</code> after the Space starts.</p>`; |
| } |
| } |
|
|
| 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 = '<a href="#max-provider-api">Max Provider API</a>'; |
| toc.insertBefore(li, toc.children[2] || null); |
| } |
|
|
| const endpoints = help.catalog?.endpoints || []; |
| const rows = endpoints.map(ep => ` |
| <tr> |
| <td class="method-${String(ep.method || 'GET').toLowerCase()}">${ep.method || 'GET'}</td> |
| <td><code>${ep.path}</code></td> |
| <td>${ep.group || 'API'}</td> |
| <td>${ep.purpose || ''}</td> |
| </tr> |
| `).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 => `<li>${n}</li>`).join(''); |
|
|
| const section = document.createElement('section'); |
| section.className = 'help-section'; |
| section.id = 'max-provider-api'; |
| section.innerHTML = ` |
| <h2>Max Provider API — Endpoint Catalog</h2> |
| <p>Live from <code>/api/max/help</code> (schema v${help.schema_version || '1'}). ${help.intent_count || 0} intents mapped for AI agents.</p> |
| <table class="api-table"> |
| <thead><tr><th>Method</th><th>Path</th><th>Group</th><th>Purpose</th></tr></thead> |
| <tbody>${rows || '<tr><td colspan="4">Endpoint catalog unavailable.</td></tr>'}</tbody> |
| </table> |
| <div class="code-wrap"> |
| <button class="copy-btn" type="button">Copy</button> |
| <pre class="code-block"><code>${examples || `curl ${API_BASE}/api/max/health`}</code></pre> |
| </div> |
| <h3>Runtime guarantees</h3> |
| <ul>${notes}</ul> |
| `; |
| main.insertBefore(section, main.firstElementChild?.nextSibling || main.firstElementChild || null); |
| this.setupCopyButtons(section); |
| this.setupToc(); |
| } |
| } |
|
|
| export default HelpPage; |
|
|