Files
aitbc/apps/marketplace/src/main.ts
AITBC System b033923756 chore: normalize file permissions across repository
- Remove executable permissions from configuration files (.editorconfig, .env.example, .gitignore)
- Remove executable permissions from documentation files (README.md, LICENSE, SECURITY.md)
- Remove executable permissions from web assets (HTML, CSS, JS files)
- Remove executable permissions from data files (JSON, SQL, YAML, requirements.txt)
- Remove executable permissions from source code files across all apps
- Add executable permissions to Python
2026-03-08 11:26:18 +01:00

274 lines
8.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Type declarations for global objects
declare global {
interface Window {
analytics?: {
track: (event: string, data: any) => void;
};
}
}
import './style.css';
import {
fetchMarketplaceOffers,
fetchMarketplaceStats,
submitMarketplaceBid,
} from './lib/api';
import type { MarketplaceOffer, MarketplaceStats } from './lib/api';
const app = document.querySelector<HTMLDivElement>('#app');
if (!app) {
throw new Error('Unable to mount marketplace app');
}
app.innerHTML = `
<main>
<section class="dashboard-grid" id="stats-panel">
<article class="stat-card">
<h2>Total Offers</h2>
<strong id="stat-total-offers">--</strong>
<span>Listings currently visible</span>
</article>
<article class="stat-card">
<h2>Open Capacity</h2>
<strong id="stat-open-capacity">--</strong>
<span>GPU / compute units available</span>
</article>
<article class="stat-card">
<h2>Average Price</h2>
<strong id="stat-average-price">--</strong>
<span>Credits per unit per hour</span>
</article>
<article class="stat-card">
<h2>Active Bids</h2>
<strong id="stat-active-bids">--</strong>
<span>Open bids awaiting match</span>
</article>
</section>
<section class="panels">
<article class="panel" id="offers-panel">
<h2>Available Offers</h2>
<div id="offers-table-wrapper" class="table-wrapper">
<p class="empty-state">Fetching marketplace offers…</p>
</div>
</article>
<article class="panel">
<h2>Submit a Bid</h2>
<form class="bid-form" id="bid-form">
<div>
<label for="bid-provider">Preferred provider</label>
<input id="bid-provider" name="provider" placeholder="Alpha Pool" required />
</div>
<div>
<label for="bid-capacity">Capacity required (units)</label>
<input id="bid-capacity" name="capacity" type="number" min="1" step="1" required />
</div>
<div>
<label for="bid-price">Bid price (credits/unit/hr)</label>
<input id="bid-price" name="price" type="number" min="0" step="0.01" required />
</div>
<div>
<label for="bid-notes">Notes (optional)</label>
<textarea id="bid-notes" name="notes" rows="3" placeholder="Add constraints, time windows, etc."></textarea>
</div>
<button type="submit">Submit Bid</button>
</form>
</article>
</section>
</main>
<aside id="toast" class="toast"></aside>
`;
const selectors = {
totalOffers: document.querySelector<HTMLSpanElement>('#stat-total-offers')!,
openCapacity: document.querySelector<HTMLSpanElement>('#stat-open-capacity')!,
averagePrice: document.querySelector<HTMLSpanElement>('#stat-average-price')!,
activeBids: document.querySelector<HTMLSpanElement>('#stat-active-bids')!,
statsWrapper: document.querySelector<HTMLDivElement>('#stats-grid')!,
offersWrapper: document.querySelector<HTMLDivElement>('#offers-table-wrapper')!,
bidForm: document.querySelector<HTMLFormElement>('#bid-form')!,
toast: document.querySelector<HTMLDivElement>('#toast')!,
};
function formatNumber(value: number, options: Intl.NumberFormatOptions = {}): string {
return new Intl.NumberFormat(undefined, options).format(value);
}
function renderStats(stats: MarketplaceStats): void {
selectors.totalOffers.textContent = formatNumber(stats.totalOffers);
selectors.openCapacity.textContent = `${formatNumber(stats.openCapacity)} units`;
selectors.averagePrice.textContent = `${formatNumber(stats.averagePrice, {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
})} credits`;
selectors.activeBids.textContent = formatNumber(stats.activeBids);
}
function statusClass(status: string): string {
switch (status.toLowerCase()) {
case 'open':
return 'status-pill status-open';
case 'reserved':
return 'status-pill status-reserved';
default:
return 'status-pill';
}
}
function renderOffers(offers: MarketplaceOffer[]): void {
const wrapper = selectors.offersWrapper;
if (!wrapper) return;
if (offers.length === 0) {
wrapper.innerHTML = '<p class="empty-state">No offers available right now. Check back soon or submit a bid.</p>';
return;
}
const cards = offers
.map(
(offer) => `
<article class="offer-card">
<div class="offer-card-header">
<div class="offer-gpu-name">${offer.gpu_model || 'Unknown GPU'}</div>
<span class="${statusClass(offer.status)}">${offer.status}</span>
</div>
<div class="offer-provider">${offer.provider}${offer.region ? ` · ${offer.region}` : ''}</div>
<div class="offer-specs">
<div class="spec-item">
<span class="spec-label">VRAM</span>
<span class="spec-value">${offer.gpu_memory_gb ? `${offer.gpu_memory_gb} GB` : '—'}</span>
</div>
<div class="spec-item">
<span class="spec-label">GPUs</span>
<span class="spec-value">${offer.gpu_count ?? 1}×</span>
</div>
<div class="spec-item">
<span class="spec-label">CUDA</span>
<span class="spec-value">${offer.cuda_version || '—'}</span>
</div>
<div class="spec-item">
<span class="spec-label">Capacity</span>
<span class="spec-value">${formatNumber(offer.capacity)} units</span>
</div>
</div>
${offer.attributes?.models?.length ? `
<div class="offer-plugins">
<span class="plugin-badge">Ollama</span>
</div>
<div class="offer-models">
<span class="models-label">Available Models</span>
<div class="model-tags">${offer.attributes.models.map((m: string) => `<span class="model-tag">${m}</span>`).join('')}</div>
</div>` : ''}
<div class="offer-pricing">
<div class="offer-price">${formatNumber(offer.price_per_hour ?? offer.price, { minimumFractionDigits: 2, maximumFractionDigits: 2 })} <small>credits/hr</small></div>
<div class="offer-sla">${offer.sla}</div>
</div>
</article>
`,
)
.join('');
wrapper.innerHTML = `<div class="offers-grid">${cards}</div>`;
}
function showToast(message: string, duration = 2500): void {
if (!selectors.toast) return;
selectors.toast.textContent = message;
selectors.toast.classList.add('visible');
window.setTimeout(() => {
selectors.toast?.classList.remove('visible');
}, duration);
}
async function loadDashboard(): Promise<void> {
// Show skeleton loading states
showSkeletons();
try {
const [stats, offers] = await Promise.all([
fetchMarketplaceStats(),
fetchMarketplaceOffers(),
]);
renderStats(stats);
renderOffers(offers);
} catch (error) {
console.error(error);
const wrapper = selectors.offersWrapper;
if (wrapper) {
wrapper.innerHTML = '<p class="empty-state">Failed to load offers. Please retry shortly.</p>';
}
showToast('Failed to load marketplace data.');
}
}
function showSkeletons() {
const statsWrapper = selectors.statsWrapper;
const offersWrapper = selectors.offersWrapper;
if (statsWrapper) {
statsWrapper.innerHTML = `
<div class="skeleton-grid">
${Array(4).fill('').map(() => `
<div class="skeleton skeleton-card"></div>
`).join('')}
</div>
`;
}
if (offersWrapper) {
offersWrapper.innerHTML = `
<div class="skeleton-list">
${Array(6).fill('').map(() => `
<div class="skeleton skeleton-card"></div>
`).join('')}
</div>
`;
}
}
selectors.bidForm?.addEventListener('submit', async (event) => {
event.preventDefault();
const form = selectors.bidForm;
if (!form) return;
const formData = new FormData(form);
const provider = formData.get('provider')?.toString().trim();
const capacity = Number(formData.get('capacity'));
const price = Number(formData.get('price'));
const notes = formData.get('notes')?.toString().trim();
if (!provider || Number.isNaN(capacity) || Number.isNaN(price)) {
showToast('Please complete the required fields.');
return;
}
try {
const submitButton = form.querySelector('button');
if (submitButton) {
submitButton.setAttribute('disabled', 'disabled');
}
await submitMarketplaceBid({ provider, capacity, price, notes });
form.reset();
showToast('Bid submitted successfully!');
} catch (error) {
console.error(error);
showToast('Unable to submit bid. Please try again.');
} finally {
const submitButton = form.querySelector('button');
if (submitButton) {
submitButton.removeAttribute('disabled');
}
}
});
loadDashboard();