mirror of
https://github.com/mudler/LocalAI.git
synced 2026-03-31 13:15:51 -04:00
feat: redesign explorer and models pages with react-ui theme - Updated logo and branding to match LocalAI's current design - Applied react-ui color scheme and CSS variables throughout - Added grid/list view toggle for models page - Implemented enhanced filter chips with active state highlighting - Added sort options and improved pagination - Redesigned explorer page cards and token display - Modernized navbar styling with sticky positioning - Improved modal design with inline actions - Ensured mobile-responsive design maintained Co-authored-by: localai-bot <localai-bot@noreply.github.com>
268 lines
17 KiB
HTML
268 lines
17 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
|
|
{{template "views/partials/head" .}}
|
|
|
|
<body class="bg-[var(--color-bg-primary)] text-[var(--color-text-primary)]">
|
|
<div class="app-layout no-sidebar">
|
|
{{template "views/partials/navbar_explorer" .}}
|
|
|
|
<main class="main-content">
|
|
<div class="main-content-inner" x-data="networkClusters()" x-init="init()">
|
|
<div class="animation-container">
|
|
<canvas id="networkCanvas"></canvas>
|
|
<div class="text-overlay">
|
|
<header class="text-center py-12">
|
|
<h1 class="hero-title" style="background: var(--gradient-text); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text;">
|
|
<i class="fa-solid fa-circle-nodes mr-2" style="-webkit-text-fill-color: var(--color-primary);"></i> Network Explorer
|
|
</h1>
|
|
<p class="hero-subtitle mt-2">
|
|
Explore clusters and workers across the federated network
|
|
<a href="https://localai.io/features/distribute/" target="_blank" class="inline-flex items-center ml-1 text-[var(--color-primary)] hover:text-[var(--color-primary-hover)] transition-colors">
|
|
<i class="fas fa-circle-info"></i>
|
|
</a>
|
|
</p>
|
|
</header>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="container mx-auto px-4 py-6 flex-grow" style="max-width: 900px;">
|
|
<!-- Warning Box -->
|
|
<div class="card p-4 mb-6 border-l-4 border-l-[var(--color-warning)]">
|
|
<div class="flex items-start gap-3">
|
|
<div class="flex items-center gap-1 text-[var(--color-warning)] flex-shrink-0 mt-0.5">
|
|
<i class="fa-solid fa-triangle-exclamation"></i>
|
|
<i class="fa-solid fa-flask text-xs"></i>
|
|
</div>
|
|
<p class="text-xs text-[var(--color-text-secondary)] leading-relaxed">
|
|
The explorer is a community-driven tool to share network tokens and view available clusters.
|
|
Anyone can use tokens to offload computation or share resources.
|
|
<strong class="text-[var(--color-text-primary)]">Use at your own risk.</strong>
|
|
Sharing tokens globally allows anyone to use your instances. This is experimental software.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Add Network Button -->
|
|
<div class="flex justify-end mb-4">
|
|
<button type="button"
|
|
class="inline-flex items-center gap-1.5 text-xs text-[var(--color-text-secondary)] hover:text-[var(--color-primary)] bg-[var(--color-bg-secondary)] hover:bg-[var(--color-primary-light)] border border-[var(--color-border-subtle)] hover:border-[var(--color-primary-border)] rounded-lg py-2 px-3 transition-colors"
|
|
@click="toggleForm()">
|
|
<i :class="showForm ? 'fa-solid fa-times' : 'fa-solid fa-plus'"></i>
|
|
<span x-text="showForm ? 'Close' : 'Add Network'"></span>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Form for adding a new network -->
|
|
<div x-show="showForm" x-transition @click.outside="showForm = false"
|
|
class="card p-5 mb-6">
|
|
<h2 class="text-sm font-semibold text-[var(--color-text-primary)] mb-4 flex items-center gap-2">
|
|
<i class="fa-solid fa-plus text-[var(--color-primary)]"></i> Add New Network
|
|
</h2>
|
|
<div class="space-y-4">
|
|
<div>
|
|
<label for="name" class="text-xs font-medium text-[var(--color-text-secondary)] mb-1 block">Network Name</label>
|
|
<input type="text" id="name" x-model="newNetwork.name" placeholder="Enter network name"
|
|
class="input w-full text-sm" />
|
|
</div>
|
|
<div>
|
|
<label for="description" class="text-xs font-medium text-[var(--color-text-secondary)] mb-1 block">Description</label>
|
|
<textarea id="description" x-model="newNetwork.description" placeholder="Enter description"
|
|
class="input w-full text-sm" rows="2"></textarea>
|
|
</div>
|
|
<div>
|
|
<label for="token" class="text-xs font-medium text-[var(--color-text-secondary)] mb-1 block">Token</label>
|
|
<textarea id="token" x-model="newNetwork.token" placeholder="Enter token"
|
|
class="input w-full text-sm font-mono" rows="2"></textarea>
|
|
</div>
|
|
<div class="flex items-center gap-3">
|
|
<button type="button" @click="addNetwork"
|
|
class="inline-flex items-center gap-1.5 text-xs text-white bg-[var(--color-primary)] hover:bg-[var(--color-primary-hover)] rounded-lg py-2 px-4 transition-colors">
|
|
<i class="fa-solid fa-plus"></i> Add Network
|
|
</button>
|
|
<p x-show="errorMessage" class="text-xs text-[var(--color-error)]" x-text="errorMessage"></p>
|
|
<p x-show="successMessage" class="text-xs text-[var(--color-success)]" x-text="successMessage"></p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Loading Spinner -->
|
|
<template x-if="networks.length === 0 && !loadingComplete">
|
|
<div class="text-center py-16">
|
|
<svg class="animate-spin h-8 w-8 text-[var(--color-primary)] mx-auto mb-3" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
|
</svg>
|
|
<p class="text-sm text-[var(--color-text-secondary)]">Loading networks...</p>
|
|
</div>
|
|
</template>
|
|
|
|
<template x-if="networks.length === 0 && loadingComplete">
|
|
<div class="text-center py-16">
|
|
<i class="fa-solid fa-circle-nodes text-3xl text-[var(--color-text-muted)] mb-3"></i>
|
|
<p class="text-sm text-[var(--color-text-secondary)]">No networks available with online workers</p>
|
|
</div>
|
|
</template>
|
|
|
|
<!-- Display Networks -->
|
|
<div class="space-y-4">
|
|
<template x-for="network in networks" :key="network.name">
|
|
<div class="card overflow-hidden">
|
|
<!-- Network Header -->
|
|
<div class="p-4 border-b border-[var(--color-border-subtle)]">
|
|
<div class="flex items-center gap-2 mb-3">
|
|
<i class="fa-solid fa-circle-nodes text-[var(--color-primary)]"></i>
|
|
<span class="text-base font-semibold text-[var(--color-text-primary)]" x-text="network.name"></span>
|
|
</div>
|
|
|
|
<!-- Token -->
|
|
<div class="bg-[var(--color-bg-primary)] rounded-lg p-3 cursor-pointer border border-[var(--color-border-subtle)] hover:border-[var(--color-primary-border)] transition-colors group"
|
|
@click="copyToken(network.token)">
|
|
<div class="flex items-center justify-between mb-1.5">
|
|
<span class="text-[10px] font-medium text-[var(--color-text-muted)] uppercase tracking-wider">
|
|
<i class="fa-solid fa-key mr-1"></i>Token (click to copy)
|
|
</span>
|
|
<i class="fa-solid fa-copy text-xs text-[var(--color-text-muted)] group-hover:text-[var(--color-primary)] transition-colors"></i>
|
|
</div>
|
|
<code class="text-xs text-[var(--color-text-secondary)] break-all font-mono leading-relaxed" x-text="network.token"></code>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Description -->
|
|
<div class="px-4 py-3 border-b border-[var(--color-border-subtle)]">
|
|
<p class="text-xs text-[var(--color-text-secondary)]" x-text="network.description"></p>
|
|
</div>
|
|
|
|
<!-- Clusters -->
|
|
<div class="p-4">
|
|
<h3 class="text-xs font-semibold text-[var(--color-text-muted)] uppercase tracking-wider mb-3">
|
|
Available Clusters
|
|
</h3>
|
|
<div class="space-y-3">
|
|
<template x-for="cluster in network.Clusters" :key="cluster.NetworkID + cluster.Type">
|
|
<div class="bg-[var(--color-bg-primary)] rounded-lg p-3 border border-[var(--color-border-subtle)]">
|
|
<!-- Cluster badges -->
|
|
<div class="flex flex-wrap gap-1.5 mb-3">
|
|
<span class="inline-flex items-center text-[10px] px-2 py-0.5 rounded-full bg-[var(--color-warning-light)] text-[var(--color-warning)] border border-[var(--color-warning)]/20"
|
|
x-text="'Type: ' + cluster.Type"></span>
|
|
<span x-show="cluster.NetworkID"
|
|
class="inline-flex items-center text-[10px] px-2 py-0.5 rounded-full bg-[var(--color-accent-light)] text-[var(--color-accent)] border border-[var(--color-accent)]/20"
|
|
x-text="'ID: ' + (cluster.NetworkID || 'N/A')"></span>
|
|
<span class="inline-flex items-center text-[10px] px-2 py-0.5 rounded-full bg-[var(--color-primary-light)] text-[var(--color-primary)] border border-[var(--color-primary)]/20"
|
|
x-text="cluster.Workers.length + ' workers'"></span>
|
|
</div>
|
|
|
|
<!-- Federated connect commands -->
|
|
<div x-show="cluster.Type == 'federated'" class="space-y-2">
|
|
<p class="text-[10px] font-medium text-[var(--color-text-muted)] uppercase tracking-wider">Connect via Docker:</p>
|
|
<div class="relative group/cmd">
|
|
<code class="block bg-[var(--color-bg-secondary)] text-[var(--color-warning)] p-3 rounded-lg text-xs break-all border border-[var(--color-border-subtle)] font-mono leading-relaxed cursor-pointer"
|
|
@click="copyToken($el.textContent)">docker run -d --restart=always -e ADDRESS=":80" -e LOCALAI_P2P_NETWORK_ID=<span x-text="cluster.NetworkID"></span> -e LOCALAI_P2P_LOGLEVEL=debug --name local-ai -e TOKEN="<span x-text="network.token"></span>" --net host -ti localai/localai:master federated --debug</code>
|
|
<i class="fa-solid fa-copy absolute top-2 right-2 text-[10px] text-[var(--color-text-muted)] group-hover/cmd:text-[var(--color-primary)] transition-colors"></i>
|
|
</div>
|
|
<p class="text-[10px] font-medium text-[var(--color-text-muted)] uppercase tracking-wider mt-2">Connect via CLI:</p>
|
|
<div class="relative group/cmd">
|
|
<code class="block bg-[var(--color-bg-secondary)] text-[var(--color-warning)] p-3 rounded-lg text-xs break-all border border-[var(--color-border-subtle)] font-mono leading-relaxed cursor-pointer"
|
|
@click="copyToken($el.textContent)">ADDRESS=":80" LOCALAI_P2P_NETWORK_ID=<span x-text="cluster.NetworkID"></span> LOCALAI_P2P_LOGLEVEL=debug TOKEN="<span x-text="network.token"></span>" local-ai federated --debug</code>
|
|
<i class="fa-solid fa-copy absolute top-2 right-2 text-[10px] text-[var(--color-text-muted)] group-hover/cmd:text-[var(--color-primary)] transition-colors"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
function networkClusters() {
|
|
return {
|
|
networks: [],
|
|
newNetwork: {
|
|
name: '',
|
|
description: '',
|
|
token: ''
|
|
},
|
|
errorMessage: '',
|
|
successMessage: '',
|
|
showForm: false,
|
|
loadingComplete: false,
|
|
toggleForm() {
|
|
this.showForm = !this.showForm;
|
|
},
|
|
fetchNetworks() {
|
|
fetch('/networks')
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
this.networks = data;
|
|
this.loadingComplete = true;
|
|
})
|
|
.catch(error => {
|
|
console.error('Error fetching networks:', error);
|
|
this.loadingComplete = true;
|
|
});
|
|
},
|
|
addNetwork() {
|
|
this.errorMessage = '';
|
|
this.successMessage = '';
|
|
|
|
if (!this.newNetwork.name || !this.newNetwork.description || !this.newNetwork.token) {
|
|
this.errorMessage = 'All fields are required.';
|
|
return;
|
|
}
|
|
|
|
fetch('/network/add', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(this.newNetwork)
|
|
})
|
|
.then(response => {
|
|
if (!response.ok) {
|
|
return response.json().then(err => { throw err; });
|
|
}
|
|
return response.json();
|
|
})
|
|
.then(data => {
|
|
this.successMessage = 'Network added successfully!';
|
|
this.fetchNetworks();
|
|
this.newNetwork = { name: '', description: '', token: '' };
|
|
})
|
|
.catch(error => {
|
|
console.error('Error adding network:', error);
|
|
this.errorMessage = 'Failed to add network. Please try again.'
|
|
if (error.error) {
|
|
this.errorMessage += " Error: " + error.error;
|
|
}
|
|
});
|
|
},
|
|
copyToken(token) {
|
|
if (navigator.clipboard && window.isSecureContext) {
|
|
navigator.clipboard.writeText(token)
|
|
.then(() => alert('Copied to clipboard!'))
|
|
.catch(() => fallbackCopy(token));
|
|
} else {
|
|
fallbackCopy(token);
|
|
}
|
|
},
|
|
init() {
|
|
this.fetchNetworks();
|
|
setInterval(() => {
|
|
this.fetchNetworks();
|
|
}, 5000);
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
<script src="static/p2panimation.js"></script>
|
|
|
|
{{template "views/partials/footer" .}}
|
|
</div>
|
|
</main>
|
|
</div>
|
|
|
|
</body>
|
|
|
|
</html>
|