Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ A personal trading dashboard for [Hyperliquid](https://hyperliquid.xyz) — live
| **HL Pulse** | Hyperliquid ecosystem intel — HL-filtered news, HYPE stats, HyperEVM TVL, top protocols, AI content studio (X threads / posts / digests) |
| **Fundamentals** | Top 100 coins — price, 24h/7d/30d%, market cap, volume, ATH drawdown |
| **DeFi** | DeFiLlama macro dashboard — total TVL, chain dominance, protocol rankings, stablecoin supply |
| **Settings** | Wallet switcher, API proxy, CoinGecko key, RSS proxy, bot worker URL, AI endpoints (Edge Function + Vercel backend), APIU proxy, Telegram notifications |

---

Expand Down
280 changes: 219 additions & 61 deletions app.js

Large diffs are not rendered by default.

70 changes: 69 additions & 1 deletion dailybrief.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,77 @@
'use strict';

const _DBR_INDEX_URL = 'https://raw.githubusercontent.com/RavellerH/Hype/gh-pages/briefs/index.json';
const _DBR_RAW_BASE = 'https://raw.githubusercontent.com/RavellerH/Hype/gh-pages/briefs/';

let _dbrBriefs = [];
let _dbrLoaded = false;
let _dbrExpanded = new Set();
let _dbrFullOpen = new Set(); // files whose full markdown is expanded
const _dbrFullCache = {}; // file → markdown text

// ── Minimal markdown renderer (shared with research.js) ─────────────────────
// Escapes first, then supports: #–#### headings, **bold**, `code`, fenced
// code blocks, -/*/1. lists, --- rules, [links](https://…), paragraphs.

function _hypeMdEsc(s) {
return String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}

function _hypeMd(md) {
const lines = _hypeMdEsc(md || '').split('\n');
const out = [];
let inList = false, inCode = false;
const inline = t => t
.replace(/`([^`]+)`/g, '<code>$1</code>')
.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>')
.replace(/\[([^\]]+)\]\((https?:[^)\s]+)\)/g, '<a href="$2" target="_blank" rel="noopener">$1</a>');
for (const raw of lines) {
const l = raw.trimEnd();
if (/^```/.test(l.trim())) { inCode = !inCode; out.push(inCode ? '<pre class="hmd-pre">' : '</pre>'); continue; }
if (inCode) { out.push(raw + '\n'); continue; }
const li = l.match(/^\s*[-*]\s+(.*)/) || l.match(/^\s*\d+\.\s+(.*)/);
if (li) { if (!inList) { out.push('<ul class="hmd-ul">'); inList = true; } out.push(`<li>${inline(li[1])}</li>`); continue; }
if (inList) { out.push('</ul>'); inList = false; }
const h = l.match(/^(#{1,4})\s+(.*)/);
if (h) { const lv = Math.min(h[1].length + 2, 6); out.push(`<h${lv} class="hmd-h">${inline(h[2])}</h${lv}>`); continue; }
if (/^\s*(-{3,}|\*{3,})\s*$/.test(l)) { out.push('<hr class="hmd-hr">'); continue; }
if (!l.trim()) continue;
out.push(`<p class="hmd-p">${inline(l)}</p>`);
}
if (inList) out.push('</ul>');
if (inCode) out.push('</pre>');
return out.join('');
}

// Fetch + toggle the full markdown of a report; shared by brief & research
async function _hypeMdToggle(openSet, cache, baseUrl, file, rerender) {
if (openSet.has(file)) { openSet.delete(file); rerender(); return; }
openSet.add(file);
rerender(); // shows "loading…" if not cached yet
if (cache[file] == null) {
try {
const r = await fetch(baseUrl + encodeURIComponent(file));
cache[file] = r.ok ? await r.text() : `*Failed to load (${r.status})*`;
} catch (e) {
cache[file] = `*Failed to load: ${e.message}*`;
}
rerender();
}
}

function _hypeMdBlock(openSet, cache, file, toggleFn) {
const isOpen = openSet.has(file);
const btn = `<button class="btn btn-ghost btn-sm" onclick="${toggleFn}('${_dbrEsc(file)}')">${isOpen ? '▲ Hide full report' : '📄 Read full report'}</button>`;
if (!isOpen) return `<div style="margin-top:8px;display:flex;gap:10px;align-items:center">${btn}</div>`;
const body = cache[file] == null
? '<div class="loading" style="padding:14px"><div class="spinner"></div> Loading report…</div>'
: `<div class="hmd-body">${_hypeMd(cache[file])}</div>`;
return `<div style="margin-top:8px;display:flex;gap:10px;align-items:center">${btn}</div>${body}`;
}

function dbrToggleFull(file) {
_hypeMdToggle(_dbrFullOpen, _dbrFullCache, _DBR_RAW_BASE, file, _dbrRender);
}

function _dbrEsc(s) {
if (s == null) return '';
Expand Down Expand Up @@ -112,7 +179,8 @@ function _dbrCardHTML(b, isLatest) {
</div>
${b.news_summary ? `<div style="font-size:13px;color:var(--text-muted);margin:8px 0;line-height:1.5;"><b style="color:var(--text);">News:</b> ${_dbrEsc(b.news_summary)}</div>` : ''}
${b.takeaway ? `<div style="font-style:italic;font-size:13px;color:var(--accent);margin-top:8px;border-top:1px solid var(--border);padding-top:8px;">${_dbrEsc(b.takeaway)}</div>` : ''}
<div style="margin-top:8px;"><a href="https://github.com/RavellerH/Hype/blob/gh-pages/briefs/${_dbrEsc(b.file)}" target="_blank" rel="noopener" style="font-size:11px;color:var(--text-faint);">view source markdown ↗</a></div>
${b.file ? _hypeMdBlock(_dbrFullOpen, _dbrFullCache, b.file, 'dbrToggleFull') : ''}
<div style="margin-top:8px;"><a href="https://github.com/RavellerH/Hype/blob/gh-pages/briefs/${_dbrEsc(b.file)}" target="_blank" rel="noopener" style="font-size:11px;color:var(--text-faint);">view source on GitHub ↗</a></div>
`;

return `
Expand Down
67 changes: 56 additions & 11 deletions fundamentals.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ let _fundSort = { col: 'market_cap_rank', dir: 1 };
let _fundSearch = '';
let _fundLoaded = false;
let _fundTimer = null;
let _fundPins = new Set(JSON.parse(localStorage.getItem('hype_fund_pins') || '[]'));

// ── Fetch ─────────────────────────────────────────────────────────────────────

Expand Down Expand Up @@ -61,7 +62,41 @@ function _fPrice(p) {
function _renderFund() {
const el = document.getElementById('fundamentals-content');
if (!el) return;
el.innerHTML = _renderFundGlobalBar() + _renderFundToolbar() + _renderFundTable();
el.innerHTML = _renderFundGlobalBar() + _renderFundToolbar()
+ `<div id="fund-table-zone" style="overflow:auto;flex:1;display:flex;flex-direction:column">${_renderFundTable()}</div>`;
}

// Re-render only the table — keeps the search input mounted so it doesn't
// lose focus on every keystroke
function _renderFundTableOnly() {
const zone = document.getElementById('fund-table-zone');
if (zone) zone.innerHTML = _renderFundTable();
else _renderFund();
}

// 7-day inline SVG sparkline from CoinGecko sparkline_in_7d data
function _fundSpark(c) {
const prices = c.sparkline_in_7d?.price;
if (!Array.isArray(prices) || prices.length < 10) return '<span style="color:var(--text-faint)">—</span>';
const step = Math.max(1, Math.floor(prices.length / 40));
const pts = prices.filter((_, i) => i % step === 0);
const min = Math.min(...pts), max = Math.max(...pts);
const range = max - min || 1;
const w = 84, h = 24, pad = 1;
const path = pts.map((p, i) => {
const x = pad + (i / (pts.length - 1)) * (w - pad * 2);
const y = h - pad - ((p - min) / range) * (h - pad * 2);
return `${i ? 'L' : 'M'}${x.toFixed(1)},${y.toFixed(1)}`;
}).join('');
const up = pts[pts.length - 1] >= pts[0];
const color = up ? 'var(--green)' : 'var(--red)';
return `<svg width="${w}" height="${h}" viewBox="0 0 ${w} ${h}" style="display:block;margin-left:auto"><path d="${path}" fill="none" stroke="${color}" stroke-width="1.3" stroke-linejoin="round" stroke-linecap="round"/></svg>`;
}

function fundTogglePin(id) {
if (_fundPins.has(id)) _fundPins.delete(id); else _fundPins.add(id);
localStorage.setItem('hype_fund_pins', JSON.stringify([..._fundPins]));
_renderFundTableOnly();
}

function _renderFundGlobalBar() {
Expand Down Expand Up @@ -108,9 +143,15 @@ function _renderFundTable() {
coins = coins.filter(c => c.symbol.toLowerCase().includes(q) || c.name.toLowerCase().includes(q));
}
const { col, dir } = _fundSort;
const val = c => col === 'vol_mcap'
? (c.total_volume && c.market_cap ? c.total_volume / c.market_cap : null)
: c[col];
coins.sort((a, b) => {
const av = a[col] ?? (dir > 0 ? Infinity : -Infinity);
const bv = b[col] ?? (dir > 0 ? Infinity : -Infinity);
// Pinned coins always float to the top, keeping the chosen sort within groups
const ap = _fundPins.has(a.id) ? 0 : 1, bp = _fundPins.has(b.id) ? 0 : 1;
if (ap !== bp) return ap - bp;
const av = val(a) ?? (dir > 0 ? Infinity : -Infinity);
const bv = val(b) ?? (dir > 0 ? Infinity : -Infinity);
return (av < bv ? -1 : av > bv ? 1 : 0) * dir;
});

Expand All @@ -121,7 +162,10 @@ function _renderFundTable() {
const rows = coins.map(c => {
const vm = c.total_volume && c.market_cap ? (c.total_volume / c.market_cap * 100) : null;
const athPct = c.ath_change_percentage;
return `<tr>
const pinned = _fundPins.has(c.id);
return `<tr${pinned ? ' style="background:var(--accent-subtle)"' : ''}>
<td style="padding-right:2px"><button onclick="fundTogglePin('${c.id}')" title="${pinned ? 'Unpin' : 'Pin to top'}"
style="background:none;border:none;cursor:pointer;font-size:13px;padding:2px;color:${pinned ? 'var(--accent)' : 'var(--text-faint)'}">${pinned ? '★' : '☆'}</button></td>
<td class="num" style="color:var(--text-faint)">${c.market_cap_rank || '—'}</td>
<td>
<div style="display:flex;align-items:center;gap:7px">
Expand All @@ -133,6 +177,7 @@ function _renderFundTable() {
<td class="num">${_fPrice(c.current_price)}</td>
<td class="num">${_fPct(c.price_change_percentage_24h_in_currency ?? c.price_change_percentage_24h)}</td>
<td class="num">${_fPct(c.price_change_percentage_7d_in_currency)}</td>
<td style="padding:3px 10px">${_fundSpark(c)}</td>
<td class="num">${_fPct(c.price_change_percentage_30d_in_currency)}</td>
<td class="num">$${_fShort(c.market_cap || 0)}</td>
<td class="num">$${_fShort(c.total_volume || 0)}</td>
Expand All @@ -142,33 +187,33 @@ function _renderFundTable() {
}).join('');

return `
<div style="overflow:auto;flex:1">
<table class="fund-table">
<thead><tr>
<th style="width:26px"></th>
${th('market_cap_rank', '#')}
${th('name', 'Coin')}
${th('current_price', 'Price', 'right')}
${th('price_change_percentage_24h', '24h', 'right')}
${th('price_change_percentage_7d_in_currency', '7d', 'right')}
<th style="text-align:right">7d Chart</th>
${th('price_change_percentage_30d_in_currency', '30d', 'right')}
${th('market_cap', 'Mkt Cap', 'right')}
${th('total_volume', '24h Vol', 'right')}
${th('total_volume', 'Vol/MCap', 'right')}
${th('vol_mcap', 'Vol/MCap', 'right')}
${th('ath_change_percentage', 'ATH %', 'right')}
</tr></thead>
<tbody>${rows || '<tr><td colspan="10" style="text-align:center;padding:24px;color:var(--text-faint)">No results</td></tr>'}</tbody>
</table>
</div>`;
<tbody>${rows || '<tr><td colspan="12" style="text-align:center;padding:24px;color:var(--text-faint)">No results</td></tr>'}</tbody>
</table>`;
}

// ── Actions ───────────────────────────────────────────────────────────────────

function fundSort(col) {
if (_fundSort.col === col) _fundSort.dir *= -1;
else { _fundSort.col = col; _fundSort.dir = 1; }
_renderFund();
_renderFundTableOnly();
}
function fundSearch(q) { _fundSearch = q; _renderFund(); }
function fundSearch(q) { _fundSearch = q; _renderFundTableOnly(); }
async function fundRefresh() {
_fundLoaded = false;
const el = document.getElementById('fundamentals-content');
Expand Down
4 changes: 4 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
<symbol id="i-journal" viewBox="0 0 24 24"><path d="M12 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.375 2.625a2.121 2.121 0 1 1 3 3L12 15l-4 1 1-4Z"/></symbol>
<symbol id="i-analytics" viewBox="0 0 24 24"><path d="M3 3v16a2 2 0 0 0 2 2h16"/><path d="M7 16c1.5-3 3-8 5-8s2.5 4 4 4 2.5-3 4-5"/></symbol>
<symbol id="i-docs" viewBox="0 0 24 24"><path d="M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H20v20H6.5a2.5 2.5 0 0 1 0-5H20"/></symbol>
<symbol id="i-settings" viewBox="0 0 24 24"><path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"/><circle cx="12" cy="12" r="3"/></symbol>
<symbol id="i-refresh" viewBox="0 0 24 24"><path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8"/><path d="M21 3v5h-5"/></symbol>
<symbol id="i-download" viewBox="0 0 24 24"><path d="M12 15V3"/><path d="m7 10 5 5 5-5"/><path d="M5 21h14"/></symbol>
</svg>
Expand Down Expand Up @@ -126,6 +127,7 @@
<a class="nav-item" data-page="watchlist" onclick="navigate('watchlist')"><svg class="nav-ico"><use href="#i-watchlist"/></svg>Watchlist</a>
<a class="nav-item" data-page="journal" onclick="navigate('journal')"><svg class="nav-ico"><use href="#i-journal"/></svg>Journal</a>
<a class="nav-item" data-page="analytics" onclick="navigate('analytics')"><svg class="nav-ico"><use href="#i-analytics"/></svg>Analytics</a>
<a class="nav-item" data-page="settings" onclick="navigate('settings')"><svg class="nav-ico"><use href="#i-settings"/></svg>Settings</a>
</nav>

<!-- Nav drawer overlay -->
Expand Down Expand Up @@ -172,6 +174,7 @@
<button class="drawer-item" data-page="watchlist" onclick="navigate('watchlist')"><svg class="nav-ico"><use href="#i-watchlist"/></svg>Watchlist</button>
<button class="drawer-item" data-page="journal" onclick="navigate('journal')"><svg class="nav-ico"><use href="#i-journal"/></svg>Journal</button>
<button class="drawer-item" data-page="analytics" onclick="navigate('analytics')"><svg class="nav-ico"><use href="#i-analytics"/></svg>Analytics</button>
<button class="drawer-item" data-page="settings" onclick="navigate('settings')"><svg class="nav-ico"><use href="#i-settings"/></svg>Settings</button>
</div>
</nav>

Expand Down Expand Up @@ -204,6 +207,7 @@
<div class="page" id="page-trend"><div id="trend-content"><div class="loading"><div class="spinner"></div> Loading…</div></div></div>
<div class="page" id="page-onchain"><div id="onchain-content"><div class="loading"><div class="spinner"></div> Loading…</div></div></div>
<div class="page" id="page-heatmap"><div id="heatmap-content"><div class="loading"><div class="spinner"></div> Loading…</div></div></div>
<div class="page" id="page-settings"><div id="settings-content"><div class="loading"><div class="spinner"></div> Loading…</div></div></div>
</main>

<!-- Market Detail Modal -->
Expand Down
15 changes: 10 additions & 5 deletions kb.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,13 @@
'use strict';

// ── Supabase init ─────────────────────────────────────────────
const _KB_URL = 'https://eiqlvbylkcmgvksrxqld.supabase.co';
const _KB_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImVpcWx2Ynlsa2NtZ3Zrc3J4cWxkIiwicm9sZSI6ImFub24iLCJpYXQiOjE3Nzg5NTI4NjgsImV4cCI6MjA5NDUyODg2OH0.PcGDHYlajqwnZ7c3ZPtssG534kd3sKwE8aT1ROlFpo8';
// Overridable via Settings → Knowledge Base Storage (hype_kb_supabase_url /
// hype_kb_supabase_key); the built-in project is the fallback. The target
// project must contain the kb_wiki and kb_trades tables.
const _KB_DEFAULT_URL = 'https://eiqlvbylkcmgvksrxqld.supabase.co';
const _KB_DEFAULT_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImVpcWx2Ynlsa2NtZ3Zrc3J4cWxkIiwicm9sZSI6ImFub24iLCJpYXQiOjE3Nzg5NTI4NjgsImV4cCI6MjA5NDUyODg2OH0.PcGDHYlajqwnZ7c3ZPtssG534kd3sKwE8aT1ROlFpo8';
const _KB_URL = localStorage.getItem('hype_kb_supabase_url') || _KB_DEFAULT_URL;
const _KB_KEY = localStorage.getItem('hype_kb_supabase_key') || _KB_DEFAULT_KEY;
const _kbDb = (window.supabase && window.supabase.createClient)
? window.supabase.createClient(_KB_URL, _KB_KEY)
: null;
Expand Down Expand Up @@ -357,7 +362,7 @@ async function kbWikiAiGenerate() {
if (btn) { btn.disabled = true; btn.textContent = '⏳ Generating…'; }

try {
const resp = await fetch('/api/chat', {
const resp = await fetch(_apiBase() + '/api/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ mode: 'wiki-generate', title, category: cat, coins: coinsRaw, tags: tagsRaw })
Expand Down Expand Up @@ -387,7 +392,7 @@ async function kbWikiAiEnhance(wikiId) {
if (btn) { btn.disabled = true; btn.textContent = '⏳ Enhancing…'; }

try {
const resp = await fetch('/api/chat', {
const resp = await fetch(_apiBase() + '/api/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ mode: 'wiki-enhance', title: entry.title, category: entry.category, content: entry.content })
Expand Down Expand Up @@ -708,7 +713,7 @@ async function kbConfirmClose(tradeId) {

async function _analyzeTradeWithAI(trade) {
try {
const resp = await fetch('/api/chat', {
const resp = await fetch(_apiBase() + '/api/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ mode: 'trade-analyze', trade })
Expand Down
10 changes: 10 additions & 0 deletions llm.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,16 @@
* const text = await _callLLM('chat', userMsg, { history: _chatMsgs, maxTokens: 1024 });
*/

/**
* Base URL for the app's own /api/* endpoints (Vercel serverless).
* Empty string on the Vercel deployment itself (relative fetch works);
* on GitHub Pages the user sets their Vercel URL once in
* Settings → App Backend URL (key shared with dailybrief.js/research.js).
*/
function _apiBase() {
return (localStorage.getItem('hype_trigger_backend_url') || '').trim().replace(/\/$/, '');
}

function _llmRouterUrl() {
const base = (localStorage.getItem('hype_edge_fn_url') || '').trim().replace(/\/$/, '');
if (!base) return null;
Expand Down
12 changes: 6 additions & 6 deletions logger.js
Original file line number Diff line number Diff line change
Expand Up @@ -252,15 +252,15 @@ function loggerInit() {
function loggerSetStatus(state, msg, ts) {
const dot = document.getElementById('logger-dot');
const tip = document.getElementById('logger-tip');
const lastTs = ts || localStorage.getItem(LOG_LAST_KEY);
const lastNum = parseInt(lastTs || '0');
const ago = lastNum ? Math.round((Date.now() - lastNum) / 60000) + 'm ago' : 'never';
const text = `Logger: ${msg || state} · last ${ago}`;
if (dot) {
dot.className = `logger-dot ${state}`;
dot.title = text; // hover tooltip — the #logger-tip span is display:none
}
if (tip) {
const lastTs = ts || localStorage.getItem(LOG_LAST_KEY);
const lastNum = parseInt(lastTs || '0');
const ago = lastNum ? Math.round((Date.now() - lastNum) / 60000) + 'm ago' : 'never';
tip.textContent = `Logger: ${msg || state} · last ${ago}`;
}
if (tip) tip.textContent = text;
}

function loggerRefreshStatus() {
Expand Down
2 changes: 1 addition & 1 deletion mvrv-ai.js
Original file line number Diff line number Diff line change
Expand Up @@ -1048,7 +1048,7 @@ async function sendChat() {
].filter(Boolean).join('\n');

try {
const res = await fetch('/api/chat', {
const res = await fetch(_apiBase() + '/api/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ system, messages: _chatHistory.map(m => ({ role: m.role, content: m.content })) }),
Expand Down
Loading