feat: add card examples and speed up rtc connect
This commit is contained in:
parent
04afead5af
commit
23fd806e6d
41 changed files with 3327 additions and 3 deletions
282
examples/cards/templates/litellm-ups-usage-live/template.html
Normal file
282
examples/cards/templates/litellm-ups-usage-live/template.html
Normal file
|
|
@ -0,0 +1,282 @@
|
|||
<div data-litellm-usage-card style="font-family: var(--card-font, 'Iosevka', 'SF Mono', ui-monospace, Menlo, Consolas, monospace); background:#ffffff; color:#111827; padding:14px 16px;">
|
||||
<div style="display:flex; align-items:flex-start; justify-content:space-between; gap:12px; margin-bottom:10px;">
|
||||
<div data-usage-subtitle style="font-size:0.86rem; line-height:1.35; color:#4b5563; font-weight:600;">Loading…</div>
|
||||
<span data-usage-status style="font-size:0.8rem; line-height:1.2; font-weight:700; color:#6b7280; white-space:nowrap;">Loading…</span>
|
||||
</div>
|
||||
|
||||
<div data-usage-grid style="display:grid; grid-template-columns:repeat(2, minmax(0, 1fr)); gap:12px;">
|
||||
<section style="min-width:0;">
|
||||
<div style="font-size:0.74rem; line-height:1.2; text-transform:uppercase; letter-spacing:0.04em; color:#6b7280;">Last 24h</div>
|
||||
<div style="display:flex; align-items:flex-end; gap:6px; margin-top:4px;">
|
||||
<span data-usage-tokens-24h style="font-size:2rem; line-height:0.95; font-weight:800; letter-spacing:-0.04em;">--</span>
|
||||
<span style="font-size:0.9rem; line-height:1.2; font-weight:700; color:#4b5563; padding-bottom:0.22rem;">tokens</span>
|
||||
</div>
|
||||
<div data-usage-power-24h style="margin-top:4px; font-size:0.9rem; line-height:1.3; color:#1f2937; font-weight:700;">--</div>
|
||||
<div data-usage-window-24h style="margin-top:2px; font-size:0.76rem; line-height:1.3; color:#6b7280;">--</div>
|
||||
</section>
|
||||
|
||||
<section data-usage-month-section style="min-width:0;">
|
||||
<div style="font-size:0.74rem; line-height:1.2; text-transform:uppercase; letter-spacing:0.04em; color:#6b7280;">This Month</div>
|
||||
<div style="display:flex; align-items:flex-end; gap:6px; margin-top:4px;">
|
||||
<span data-usage-tokens-month style="font-size:2rem; line-height:0.95; font-weight:800; letter-spacing:-0.04em;">--</span>
|
||||
<span style="font-size:0.9rem; line-height:1.2; font-weight:700; color:#4b5563; padding-bottom:0.22rem;">tokens</span>
|
||||
</div>
|
||||
<div data-usage-power-month style="margin-top:4px; font-size:0.9rem; line-height:1.3; color:#1f2937; font-weight:700;">--</div>
|
||||
<div data-usage-window-month style="margin-top:2px; font-size:0.76rem; line-height:1.3; color:#6b7280;">--</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div style="margin-top:10px; font-size:0.82rem; line-height:1.35; color:#6b7280;">Updated <span data-usage-updated>--</span></div>
|
||||
</div>
|
||||
<script>
|
||||
(() => {
|
||||
const script = document.currentScript;
|
||||
const root = script?.closest('[data-nanobot-card-root]');
|
||||
const state = window.__nanobotGetCardState?.(script) || {};
|
||||
if (!(root instanceof HTMLElement)) return;
|
||||
|
||||
const subtitleEl = root.querySelector('[data-usage-subtitle]');
|
||||
const statusEl = root.querySelector('[data-usage-status]');
|
||||
const updatedEl = root.querySelector('[data-usage-updated]');
|
||||
const gridEl = root.querySelector('[data-usage-grid]');
|
||||
const monthSectionEl = root.querySelector('[data-usage-month-section]');
|
||||
const tokens24hEl = root.querySelector('[data-usage-tokens-24h]');
|
||||
const power24hEl = root.querySelector('[data-usage-power-24h]');
|
||||
const window24hEl = root.querySelector('[data-usage-window-24h]');
|
||||
const tokensMonthEl = root.querySelector('[data-usage-tokens-month]');
|
||||
const powerMonthEl = root.querySelector('[data-usage-power-month]');
|
||||
const windowMonthEl = root.querySelector('[data-usage-window-month]');
|
||||
if (!(subtitleEl instanceof HTMLElement) || !(statusEl instanceof HTMLElement) || !(updatedEl instanceof HTMLElement) || !(gridEl instanceof HTMLElement) || !(monthSectionEl instanceof HTMLElement) || !(tokens24hEl instanceof HTMLElement) || !(power24hEl instanceof HTMLElement) || !(window24hEl instanceof HTMLElement) || !(tokensMonthEl instanceof HTMLElement) || !(powerMonthEl instanceof HTMLElement) || !(windowMonthEl instanceof HTMLElement)) return;
|
||||
|
||||
const subtitle = typeof state.subtitle === 'string' ? state.subtitle : '';
|
||||
const configuredToolName24h = typeof state.tool_name_24h === 'string'
|
||||
? state.tool_name_24h.trim()
|
||||
: typeof state.tool_name === 'string'
|
||||
? state.tool_name.trim()
|
||||
: '';
|
||||
const configuredToolNameMonth = typeof state.tool_name_month === 'string'
|
||||
? state.tool_name_month.trim()
|
||||
: '';
|
||||
const rawToolArguments24h = state && typeof state.tool_arguments_24h === 'object' && state.tool_arguments_24h && !Array.isArray(state.tool_arguments_24h)
|
||||
? state.tool_arguments_24h
|
||||
: state && typeof state.tool_arguments === 'object' && state.tool_arguments && !Array.isArray(state.tool_arguments)
|
||||
? state.tool_arguments
|
||||
: {};
|
||||
const rawToolArgumentsMonth = state && typeof state.tool_arguments_month === 'object' && state.tool_arguments_month && !Array.isArray(state.tool_arguments_month)
|
||||
? state.tool_arguments_month
|
||||
: {};
|
||||
const source24h = typeof state.source_url_24h === 'string' ? state.source_url_24h.trim() : '';
|
||||
const sourceMonth = typeof state.source_url_month === 'string' ? state.source_url_month.trim() : '';
|
||||
const refreshMsRaw = Number(state.refresh_ms);
|
||||
const refreshMs = Number.isFinite(refreshMsRaw) && refreshMsRaw >= 60000 ? refreshMsRaw : 15 * 60 * 1000;
|
||||
|
||||
const tokenFormatter = new Intl.NumberFormat([], { notation: 'compact', maximumFractionDigits: 1 });
|
||||
const kwhFormatter = new Intl.NumberFormat([], { minimumFractionDigits: 1, maximumFractionDigits: 1 });
|
||||
const moneyFormatter = new Intl.NumberFormat([], { style: 'currency', currency: 'USD', minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
||||
|
||||
subtitleEl.textContent = subtitle || 'LiteLLM activity vs local UPS energy';
|
||||
const hasMonthSource = Boolean(
|
||||
sourceMonth ||
|
||||
configuredToolNameMonth ||
|
||||
Object.keys(rawToolArgumentsMonth).length,
|
||||
);
|
||||
if (!hasMonthSource) {
|
||||
monthSectionEl.style.display = 'none';
|
||||
gridEl.style.gridTemplateColumns = 'minmax(0, 1fr)';
|
||||
}
|
||||
const updateLiveContent = (snapshot) => {
|
||||
window.__nanobotSetCardLiveContent?.(script, snapshot);
|
||||
};
|
||||
|
||||
const setStatus = (label, color) => {
|
||||
statusEl.textContent = label;
|
||||
statusEl.style.color = color;
|
||||
};
|
||||
|
||||
const parseLocalTimestamp = (raw) => {
|
||||
if (typeof raw !== 'string' || !raw.trim()) return null;
|
||||
const match = raw.trim().match(/^(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2}:\d{2}) ([+-]\d{2})(\d{2})$/);
|
||||
if (!match) return null;
|
||||
const value = `${match[1]}T${match[2]}${match[3]}:${match[4]}`;
|
||||
const parsed = new Date(value);
|
||||
return Number.isNaN(parsed.getTime()) ? null : parsed;
|
||||
};
|
||||
|
||||
const formatRangeLabel = (payload, fallbackLabel) => {
|
||||
const startRaw = typeof payload?.range_start_local === 'string' ? payload.range_start_local : '';
|
||||
const endRaw = typeof payload?.range_end_local === 'string' ? payload.range_end_local : '';
|
||||
const start = parseLocalTimestamp(startRaw);
|
||||
const end = parseLocalTimestamp(endRaw);
|
||||
if (!(start instanceof Date) || Number.isNaN(start.getTime()) || !(end instanceof Date) || Number.isNaN(end.getTime())) {
|
||||
return fallbackLabel;
|
||||
}
|
||||
return `${start.toLocaleDateString([], { month: 'short', day: 'numeric' })} to ${end.toLocaleDateString([], { month: 'short', day: 'numeric' })}`;
|
||||
};
|
||||
|
||||
const renderSection = (elements, payload, fallbackLabel) => {
|
||||
const tokens = Number(payload?.total_tokens_processed);
|
||||
const kwh = Number(payload?.total_ups_kwh_in_range);
|
||||
const localCost = Number(payload?.local_cost_usd_in_range);
|
||||
|
||||
elements.tokens.textContent = Number.isFinite(tokens) ? tokenFormatter.format(tokens) : '--';
|
||||
elements.power.textContent =
|
||||
Number.isFinite(kwh) && Number.isFinite(localCost)
|
||||
? `${kwhFormatter.format(kwh)} kWh · ${moneyFormatter.format(localCost)}`
|
||||
: Number.isFinite(kwh)
|
||||
? `${kwhFormatter.format(kwh)} kWh`
|
||||
: '--';
|
||||
elements.window.textContent = formatRangeLabel(payload, fallbackLabel);
|
||||
};
|
||||
|
||||
const blankSection = (elements, fallbackLabel) => {
|
||||
elements.tokens.textContent = '--';
|
||||
elements.power.textContent = '--';
|
||||
elements.window.textContent = fallbackLabel;
|
||||
};
|
||||
|
||||
const shellEscape = (value) => `'${String(value ?? '').replace(/'/g, `'\"'\"'`)}'`;
|
||||
|
||||
const buildLegacyExecCommand = (rawUrl) => {
|
||||
if (typeof rawUrl !== 'string' || !rawUrl.startsWith('/script/proxy/')) return '';
|
||||
const [pathPart, queryPart = ''] = rawUrl.split('?', 2);
|
||||
const relativeScript = pathPart.slice('/script/proxy/'.length).replace(/^\/+/, '');
|
||||
if (!relativeScript) return '';
|
||||
const params = new URLSearchParams(queryPart);
|
||||
const args = params.getAll('arg').map((value) => value.trim()).filter(Boolean);
|
||||
const scriptPath = `$HOME/.nanobot/workspace/${relativeScript}`;
|
||||
return `python3 ${scriptPath}${args.length ? ` ${args.map(shellEscape).join(' ')}` : ''}`;
|
||||
};
|
||||
|
||||
const resolveToolCall = (toolName, toolArguments, legacySourceUrl) => {
|
||||
if (toolName) {
|
||||
return {
|
||||
toolName,
|
||||
toolArguments,
|
||||
};
|
||||
}
|
||||
const legacyCommand = buildLegacyExecCommand(legacySourceUrl);
|
||||
if (!legacyCommand) return null;
|
||||
return {
|
||||
toolName: 'exec',
|
||||
toolArguments: { command: legacyCommand },
|
||||
};
|
||||
};
|
||||
|
||||
const loadPayload = async (toolCall) => {
|
||||
if (!toolCall) throw new Error('Missing tool_name/tool_arguments');
|
||||
if (!window.__nanobotCallToolAsync) throw new Error('Async tool helper unavailable');
|
||||
const toolResult = await window.__nanobotCallToolAsync(
|
||||
toolCall.toolName,
|
||||
toolCall.toolArguments,
|
||||
{ timeoutMs: 180000 },
|
||||
);
|
||||
if (toolResult && toolResult.parsed && typeof toolResult.parsed === 'object' && !Array.isArray(toolResult.parsed)) {
|
||||
return toolResult.parsed;
|
||||
}
|
||||
if (typeof toolResult?.content === 'string' && toolResult.content.trim()) {
|
||||
const parsed = JSON.parse(toolResult.content);
|
||||
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
||||
return parsed;
|
||||
}
|
||||
}
|
||||
throw new Error('Tool returned invalid JSON');
|
||||
};
|
||||
|
||||
const refresh = async () => {
|
||||
setStatus('Refreshing', '#6b7280');
|
||||
try {
|
||||
const toolCall24h = resolveToolCall(configuredToolName24h, rawToolArguments24h, source24h);
|
||||
const toolCallMonth = hasMonthSource
|
||||
? resolveToolCall(configuredToolNameMonth, rawToolArgumentsMonth, sourceMonth)
|
||||
: null;
|
||||
const jobs = [loadPayload(toolCall24h), hasMonthSource ? loadPayload(toolCallMonth) : Promise.resolve(null)];
|
||||
const results = await Promise.allSettled(jobs);
|
||||
const twentyFourHour = results[0].status === 'fulfilled' ? results[0].value : null;
|
||||
const month = hasMonthSource && results[1].status === 'fulfilled' ? results[1].value : null;
|
||||
|
||||
if (twentyFourHour) {
|
||||
renderSection(
|
||||
{ tokens: tokens24hEl, power: power24hEl, window: window24hEl },
|
||||
twentyFourHour,
|
||||
'Last 24 hours',
|
||||
);
|
||||
} else {
|
||||
blankSection(
|
||||
{ tokens: tokens24hEl, power: power24hEl, window: window24hEl },
|
||||
'Last 24 hours',
|
||||
);
|
||||
}
|
||||
|
||||
if (hasMonthSource && month) {
|
||||
renderSection(
|
||||
{ tokens: tokensMonthEl, power: powerMonthEl, window: windowMonthEl },
|
||||
month,
|
||||
'This month',
|
||||
);
|
||||
} else if (hasMonthSource) {
|
||||
blankSection(
|
||||
{ tokens: tokensMonthEl, power: powerMonthEl, window: windowMonthEl },
|
||||
'This month',
|
||||
);
|
||||
}
|
||||
|
||||
const successCount = [twentyFourHour, month].filter(Boolean).length;
|
||||
const expectedCount = hasMonthSource ? 2 : 1;
|
||||
if (successCount === expectedCount) {
|
||||
setStatus('Live', '#047857');
|
||||
} else if (successCount === 1) {
|
||||
setStatus('Partial', '#b45309');
|
||||
} else {
|
||||
setStatus('Unavailable', '#b91c1c');
|
||||
}
|
||||
|
||||
const updatedText = new Date().toLocaleString([], { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' });
|
||||
updatedEl.textContent = updatedText;
|
||||
updateLiveContent({
|
||||
kind: 'litellm_ups_usage',
|
||||
subtitle: subtitleEl.textContent || null,
|
||||
status: statusEl.textContent || null,
|
||||
updated_at: updatedText,
|
||||
last_24h: twentyFourHour
|
||||
? {
|
||||
total_tokens_processed: Number(twentyFourHour.total_tokens_processed) || 0,
|
||||
total_ups_kwh_in_range: Number(twentyFourHour.total_ups_kwh_in_range) || 0,
|
||||
local_cost_usd_in_range: Number(twentyFourHour.local_cost_usd_in_range) || 0,
|
||||
range_start_local: twentyFourHour.range_start_local || null,
|
||||
range_end_local: twentyFourHour.range_end_local || null,
|
||||
}
|
||||
: null,
|
||||
this_month: month
|
||||
? {
|
||||
total_tokens_processed: Number(month.total_tokens_processed) || 0,
|
||||
total_ups_kwh_in_range: Number(month.total_ups_kwh_in_range) || 0,
|
||||
local_cost_usd_in_range: Number(month.local_cost_usd_in_range) || 0,
|
||||
range_start_local: month.range_start_local || null,
|
||||
range_end_local: month.range_end_local || null,
|
||||
}
|
||||
: null,
|
||||
});
|
||||
} catch (error) {
|
||||
const errorText = String(error);
|
||||
blankSection({ tokens: tokens24hEl, power: power24hEl, window: window24hEl }, 'Last 24 hours');
|
||||
if (hasMonthSource) {
|
||||
blankSection({ tokens: tokensMonthEl, power: powerMonthEl, window: windowMonthEl }, 'This month');
|
||||
}
|
||||
updatedEl.textContent = errorText;
|
||||
setStatus('Unavailable', '#b91c1c');
|
||||
updateLiveContent({
|
||||
kind: 'litellm_ups_usage',
|
||||
subtitle: subtitleEl.textContent || null,
|
||||
status: 'Unavailable',
|
||||
updated_at: errorText,
|
||||
last_24h: null,
|
||||
this_month: null,
|
||||
error: errorText,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
void refresh();
|
||||
window.setInterval(() => { void refresh(); }, refreshMs);
|
||||
})();
|
||||
</script>
|
||||
Loading…
Add table
Add a link
Reference in a new issue