feat: show Ollama plugin badge and available LLM models on marketplace cards

Backend:
- Add attributes field to MarketplaceOfferView schema
- Include attributes in _to_offer_view() response

Frontend:
- Add attributes type with Ollama fields to OfferRecord
- Show purple 'Ollama' plugin badge on cards with models
- Display available LLM model tags (gemma3, deepseek-r1, etc.)
- Add plugin-badge and model-tag CSS styles
This commit is contained in:
oib
2026-02-14 16:57:15 +01:00
parent 417259171a
commit 72e21fd07f
5 changed files with 65 additions and 0 deletions

View File

@@ -196,6 +196,7 @@ class MarketplaceOfferView(BaseModel):
cuda_version: Optional[str] = None
price_per_hour: Optional[float] = None
region: Optional[str] = None
attributes: Optional[dict] = None
class MarketplaceStatsView(BaseModel):

View File

@@ -87,4 +87,5 @@ class MarketplaceService:
cuda_version=offer.cuda_version,
price_per_hour=offer.price_per_hour,
region=offer.region,
attributes=offer.attributes,
)

View File

@@ -16,6 +16,13 @@ interface OfferRecord {
cuda_version?: string;
price_per_hour?: number;
region?: string;
attributes?: {
ollama_host?: string;
models?: string[];
vram_mb?: number;
driver?: string;
[key: string]: unknown;
};
}
interface OffersResponse {

View File

@@ -151,6 +151,14 @@ function renderOffers(offers: MarketplaceOffer[]): void {
<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 => `<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>

View File

@@ -203,6 +203,54 @@ body {
color: #64748b;
}
.offer-plugins {
margin-bottom: 8px;
}
.plugin-badge {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 3px 10px;
border-radius: 999px;
font-size: 0.75rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.04em;
background: linear-gradient(135deg, #6366f1, #8b5cf6);
color: #ffffff;
}
.offer-models {
margin-bottom: 16px;
}
.models-label {
display: block;
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 0.06em;
color: #94a3b8;
margin-bottom: 8px;
}
.model-tags {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.model-tag {
display: inline-block;
padding: 3px 10px;
border-radius: 8px;
font-size: 0.78rem;
font-weight: 500;
background: #f1f5f9;
color: #334155;
border: 1px solid #e2e8f0;
}
.status-pill {
display: inline-flex;
align-items: center;