mirror of
https://github.com/mudler/LocalAI.git
synced 2025-05-20 10:35:01 +00:00
feat(p2p): add network explorer and community pools (#3125)
* WIP Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * Fixups Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * Wire up a simple explorer DB Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * wip Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * WIP Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * refactor: group services id so can be identified easily in the ledger table Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * feat(discovery): discovery service now gather worker informations correctly Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * feat(explorer): display network token Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * feat(explorer): display form to add new networks Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * feat(explorer): stop from overwriting networks Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * feat(explorer): display only networks with active workers Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * feat(explorer): list only clusters in a network if it has online workers Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * remove invalid and inactive networks if networks have no workers delete them from the database, similarly, if invalid. Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * ci: add workflow to deploy new explorer versions automatically Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * build-api: build with p2p tag Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * Allow to specify a connection timeout Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * logging Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * Better p2p defaults Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * Set loglevel Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * Fix dht enable Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * Default to info for loglevel Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * Add navbar Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * Slightly improve rendering Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * Allow to copy the token easily Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * ci fixups Signed-off-by: Ettore Di Giacinto <mudler@localai.io> --------- Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
This commit is contained in:
parent
5fcafc3d1e
commit
9e3e892ac7
19 changed files with 1082 additions and 17 deletions
105
core/http/endpoints/explorer/dashboard.go
Normal file
105
core/http/endpoints/explorer/dashboard.go
Normal file
|
@ -0,0 +1,105 @@
|
|||
package explorer
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"sort"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/mudler/LocalAI/core/explorer"
|
||||
"github.com/mudler/LocalAI/internal"
|
||||
)
|
||||
|
||||
func Dashboard() func(*fiber.Ctx) error {
|
||||
return func(c *fiber.Ctx) error {
|
||||
|
||||
summary := fiber.Map{
|
||||
"Title": "LocalAI API - " + internal.PrintableVersion(),
|
||||
"Version": internal.PrintableVersion(),
|
||||
}
|
||||
|
||||
if string(c.Context().Request.Header.ContentType()) == "application/json" || len(c.Accepts("html")) == 0 {
|
||||
// The client expects a JSON response
|
||||
return c.Status(fiber.StatusOK).JSON(summary)
|
||||
} else {
|
||||
// Render index
|
||||
return c.Render("views/explorer", summary)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type AddNetworkRequest struct {
|
||||
Token string `json:"token"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
type Network struct {
|
||||
explorer.Network
|
||||
explorer.TokenData
|
||||
Token string `json:"token"`
|
||||
}
|
||||
|
||||
func ShowNetworks(db *explorer.Database, ds *explorer.DiscoveryServer) func(*fiber.Ctx) error {
|
||||
return func(c *fiber.Ctx) error {
|
||||
networkState := ds.NetworkState()
|
||||
results := []Network{}
|
||||
for token, network := range networkState.Networks {
|
||||
networkData, exists := db.Get(token) // get the token data
|
||||
hasWorkers := false
|
||||
for _, cluster := range network.Clusters {
|
||||
if len(cluster.Workers) > 0 {
|
||||
hasWorkers = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if exists && hasWorkers {
|
||||
results = append(results, Network{Network: network, TokenData: networkData, Token: token})
|
||||
}
|
||||
}
|
||||
|
||||
// order by number of clusters
|
||||
sort.Slice(results, func(i, j int) bool {
|
||||
return len(results[i].Clusters) > len(results[j].Clusters)
|
||||
})
|
||||
|
||||
return c.JSON(results)
|
||||
}
|
||||
}
|
||||
|
||||
func AddNetwork(db *explorer.Database) func(*fiber.Ctx) error {
|
||||
return func(c *fiber.Ctx) error {
|
||||
request := new(AddNetworkRequest)
|
||||
if err := c.BodyParser(request); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Cannot parse JSON"})
|
||||
}
|
||||
|
||||
if request.Token == "" {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Token is required"})
|
||||
}
|
||||
|
||||
if request.Name == "" {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Name is required"})
|
||||
}
|
||||
|
||||
if request.Description == "" {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Description is required"})
|
||||
}
|
||||
|
||||
// TODO: check if token is valid, otherwise reject
|
||||
// try to decode the token from base64
|
||||
_, err := base64.StdEncoding.DecodeString(request.Token)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid token"})
|
||||
}
|
||||
|
||||
if _, exists := db.Get(request.Token); exists {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Token already exists"})
|
||||
}
|
||||
err = db.Set(request.Token, explorer.TokenData{Name: request.Name, Description: request.Description})
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Cannot add token"})
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusOK).JSON(fiber.Map{"message": "Token added"})
|
||||
}
|
||||
}
|
|
@ -15,7 +15,7 @@ func ShowP2PNodes(appConfig *config.ApplicationConfig) func(*fiber.Ctx) error {
|
|||
// Render index
|
||||
return func(c *fiber.Ctx) error {
|
||||
return c.JSON(schema.P2PNodesResponse{
|
||||
Nodes: p2p.GetAvailableNodes(p2p.NetworkID(appConfig.P2PNetworkID, "")),
|
||||
Nodes: p2p.GetAvailableNodes(p2p.NetworkID(appConfig.P2PNetworkID, p2p.WorkerID)),
|
||||
FederatedNodes: p2p.GetAvailableNodes(p2p.NetworkID(appConfig.P2PNetworkID, p2p.FederatedID)),
|
||||
})
|
||||
}
|
||||
|
|
46
core/http/explorer.go
Normal file
46
core/http/explorer.go
Normal file
|
@ -0,0 +1,46 @@
|
|||
package http
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/gofiber/fiber/v2/middleware/favicon"
|
||||
"github.com/gofiber/fiber/v2/middleware/filesystem"
|
||||
"github.com/mudler/LocalAI/core/explorer"
|
||||
"github.com/mudler/LocalAI/core/http/routes"
|
||||
)
|
||||
|
||||
func Explorer(db *explorer.Database, discoveryServer *explorer.DiscoveryServer) *fiber.App {
|
||||
|
||||
fiberCfg := fiber.Config{
|
||||
Views: renderEngine(),
|
||||
// We disable the Fiber startup message as it does not conform to structured logging.
|
||||
// We register a startup log line with connection information in the OnListen hook to keep things user friendly though
|
||||
DisableStartupMessage: false,
|
||||
// Override default error handler
|
||||
}
|
||||
|
||||
app := fiber.New(fiberCfg)
|
||||
|
||||
routes.RegisterExplorerRoutes(app, db, discoveryServer)
|
||||
|
||||
httpFS := http.FS(embedDirStatic)
|
||||
|
||||
app.Use(favicon.New(favicon.Config{
|
||||
URL: "/favicon.ico",
|
||||
FileSystem: httpFS,
|
||||
File: "static/favicon.ico",
|
||||
}))
|
||||
|
||||
app.Use("/static", filesystem.New(filesystem.Config{
|
||||
Root: httpFS,
|
||||
PathPrefix: "static",
|
||||
Browse: true,
|
||||
}))
|
||||
|
||||
// Define a custom 404 handler
|
||||
// Note: keep this at the bottom!
|
||||
app.Use(notFoundHandler)
|
||||
|
||||
return app
|
||||
}
|
13
core/http/routes/explorer.go
Normal file
13
core/http/routes/explorer.go
Normal file
|
@ -0,0 +1,13 @@
|
|||
package routes
|
||||
|
||||
import (
|
||||
"github.com/gofiber/fiber/v2"
|
||||
coreExplorer "github.com/mudler/LocalAI/core/explorer"
|
||||
"github.com/mudler/LocalAI/core/http/endpoints/explorer"
|
||||
)
|
||||
|
||||
func RegisterExplorerRoutes(app *fiber.App, db *coreExplorer.Database, ds *coreExplorer.DiscoveryServer) {
|
||||
app.Get("/", explorer.Dashboard())
|
||||
app.Post("/network/add", explorer.AddNetwork(db))
|
||||
app.Get("/networks", explorer.ShowNetworks(db, ds))
|
||||
}
|
|
@ -105,14 +105,14 @@ func RegisterUIRoutes(app *fiber.App,
|
|||
|
||||
/* show nodes live! */
|
||||
app.Get("/p2p/ui/workers", auth, func(c *fiber.Ctx) error {
|
||||
return c.SendString(elements.P2PNodeBoxes(p2p.GetAvailableNodes(p2p.NetworkID(appConfig.P2PNetworkID, ""))))
|
||||
return c.SendString(elements.P2PNodeBoxes(p2p.GetAvailableNodes(p2p.NetworkID(appConfig.P2PNetworkID, p2p.WorkerID))))
|
||||
})
|
||||
app.Get("/p2p/ui/workers-federation", auth, func(c *fiber.Ctx) error {
|
||||
return c.SendString(elements.P2PNodeBoxes(p2p.GetAvailableNodes(p2p.NetworkID(appConfig.P2PNetworkID, p2p.FederatedID))))
|
||||
})
|
||||
|
||||
app.Get("/p2p/ui/workers-stats", auth, func(c *fiber.Ctx) error {
|
||||
return c.SendString(elements.P2PNodeStats(p2p.GetAvailableNodes(p2p.NetworkID(appConfig.P2PNetworkID, ""))))
|
||||
return c.SendString(elements.P2PNodeStats(p2p.GetAvailableNodes(p2p.NetworkID(appConfig.P2PNetworkID, p2p.WorkerID))))
|
||||
})
|
||||
app.Get("/p2p/ui/workers-federation-stats", auth, func(c *fiber.Ctx) error {
|
||||
return c.SendString(elements.P2PNodeStats(p2p.GetAvailableNodes(p2p.NetworkID(appConfig.P2PNetworkID, p2p.FederatedID))))
|
||||
|
|
342
core/http/views/explorer.html
Normal file
342
core/http/views/explorer.html
Normal file
|
@ -0,0 +1,342 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
{{template "views/partials/head" .}}
|
||||
|
||||
<style>
|
||||
body {
|
||||
background-color: #1a202c;
|
||||
color: #e2e8f0;
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
}
|
||||
.container {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
position: relative;
|
||||
}
|
||||
.network-card {
|
||||
background-color: #2d3748;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||
}
|
||||
.network-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 6px 10px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
.network-title {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 10px;
|
||||
color: #63b3ed;
|
||||
}
|
||||
.network-token {
|
||||
font-size: 14px;
|
||||
font-style: italic;
|
||||
color: #cbd5e0;
|
||||
margin-bottom: 10px;
|
||||
word-break: break-word; /* Breaks words to prevent overflow */
|
||||
overflow-wrap: break-word; /* Ensures long strings break */
|
||||
white-space: pre-wrap; /* Preserves whitespace for breaking */
|
||||
}
|
||||
.cluster {
|
||||
margin-top: 10px;
|
||||
background-color: #4a5568;
|
||||
padding: 10px;
|
||||
border-radius: 6px;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
.cluster:hover {
|
||||
background-color: #5a6b78;
|
||||
}
|
||||
.cluster-title {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: #e2e8f0;
|
||||
}
|
||||
.form-container {
|
||||
background-color: #2d3748;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.form-control {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
font-weight: bold;
|
||||
}
|
||||
input[type="text"],
|
||||
textarea {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #4a5568;
|
||||
background-color: #3a4250;
|
||||
color: #e2e8f0;
|
||||
transition: border-color 0.3s ease, background-color 0.3s ease;
|
||||
}
|
||||
input[type="text"]:focus,
|
||||
textarea:focus {
|
||||
border-color: #63b3ed;
|
||||
background-color: #4a5568;
|
||||
}
|
||||
button {
|
||||
background-color: #3182ce;
|
||||
color: #e2e8f0;
|
||||
padding: 10px 20px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
.error {
|
||||
color: #e53e3e;
|
||||
margin-top: 5px;
|
||||
}
|
||||
.success {
|
||||
color: #38a169;
|
||||
margin-top: 5px;
|
||||
}
|
||||
/* Spinner Styles */
|
||||
.spinner {
|
||||
display: inline-block;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border: 5px solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: 50%;
|
||||
border-top-color: #3182ce;
|
||||
animation: spin 1s linear infinite;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* Center the loading text and spinner */
|
||||
.loading-container {
|
||||
text-align: center;
|
||||
padding: 50px;
|
||||
}
|
||||
.warning-box {
|
||||
border-radius: 5px;
|
||||
}
|
||||
.warning-box i {
|
||||
margin-right: 10px;
|
||||
}
|
||||
.token-box {
|
||||
background-color: #4a5568;
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
margin-top: 10px;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
}
|
||||
.token-box:hover {
|
||||
background-color: #5a6b7e;
|
||||
}
|
||||
.token-text {
|
||||
overflow-wrap: break-word;
|
||||
font-family: monospace;
|
||||
}
|
||||
.copy-icon {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
color: #e2e8f0;
|
||||
}
|
||||
</style>
|
||||
|
||||
<body class="bg-gray-900 text-gray-200">
|
||||
<div class="flex flex-col min-h-screen" x-data="networkClusters()" x-init="init()">
|
||||
{{template "views/partials/navbar_explorer" .}}
|
||||
|
||||
<header class="text-center py-12">
|
||||
<h1 class="text-5xl font-bold text-gray-100">Network Clusters Explorer</h1>
|
||||
<p class="mt-4 text-lg">View the clusters and workers available in each network.</p>
|
||||
</header>
|
||||
|
||||
<div class="container mx-auto px-4 flex-grow">
|
||||
<!-- Warning Box -->
|
||||
<div class="warning-box bg-yellow-100 text-gray-800 mb-20 pt-5 pb-5 pr-5 pl-5 text-lg">
|
||||
<i class="fa-solid fa-triangle-exclamation"></i>
|
||||
The explorer is a global, community-driven tool to share network tokens and view available clusters in the globe.
|
||||
Anyone can use the tokens to offload computation and use the clusters available or share resources.
|
||||
This is provided without any warranty. Use it at your own risk. We are not responsible for any potential harm or misuse. Sharing tokens globally allows anyone from the internet to use your instances.
|
||||
Although the community will address bugs, this is experimental software and may be insecure to deploy on your hardware unless you take all necessary precautions.
|
||||
</div>
|
||||
<div class="flow-root">
|
||||
<!-- Toggle button for showing/hiding the form -->
|
||||
<button class="bg-red-600 hover:bg-blue-600 float-right mb-2 flex items-center px-4 py-2 rounded" @click="toggleForm()">
|
||||
<!-- Conditional icon display -->
|
||||
<i :class="showForm ? 'fa-solid fa-times' : 'fa-solid fa-plus'" class="mr-2"></i>
|
||||
<span x-text="showForm ? 'Close' : 'Add New Network'"></span>
|
||||
</button>
|
||||
</div>
|
||||
<!-- Form for adding a new network -->
|
||||
<div class="form-container" x-show="showForm" @click.outside="showForm = false">
|
||||
<h2 class="text-3xl font-bold mb-4"><i class="fa-solid fa-plus"></i> Add New Network</h2>
|
||||
<div class="form-control">
|
||||
<label for="name">Network Name</label>
|
||||
<input type="text" id="name" x-model="newNetwork.name" placeholder="Enter network name" />
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label for="description">Description</label>
|
||||
<textarea id="description" x-model="newNetwork.description" placeholder="Enter description"></textarea>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label for="token">Token</label>
|
||||
<textarea id="token" x-model="newNetwork.token" placeholder="Enter token"></textarea>
|
||||
</div>
|
||||
<button @click="addNetwork"><i class="fa-solid fa-plus"></i> Add Network</button>
|
||||
<template x-if="errorMessage">
|
||||
<p class="error" x-text="errorMessage"></p>
|
||||
</template>
|
||||
<template x-if="successMessage">
|
||||
<p class="success" x-text="successMessage"></p>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- Loading Spinner -->
|
||||
<template x-if="networks.length === 0 && !loadingComplete">
|
||||
<div class="loading-container">
|
||||
<div class="spinner"></div>
|
||||
<p class="text-center mt-4">Loading networks...</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template x-if="networks.length === 0 && loadingComplete">
|
||||
<div class="loading-container">
|
||||
<p class="text-center mt-4">No networks available with online workers</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Display Networks -->
|
||||
<template x-for="network in networks" :key="network.name">
|
||||
<div class="network-card">
|
||||
<div class="network-title" x-text="network.name"></div>
|
||||
<div class="token-box" @click="copyToken(network.token)">
|
||||
<i class="fa-solid fa-copy copy-icon"></i>
|
||||
Token (click to copy): <br>
|
||||
<span class="token-text" x-text="network.token"></span>
|
||||
</div>
|
||||
|
||||
<div class="cluster">
|
||||
<p class="text-lg">Description</p>
|
||||
<p x-text="network.description"></p>
|
||||
</div>
|
||||
|
||||
<template x-for="cluster in network.Clusters" :key="cluster.NetworkID + cluster.Type">
|
||||
<div class="cluster">
|
||||
<div class="cluster-title" x-text="'Cluster Type: ' + cluster.Type"></div>
|
||||
<p x-show="cluster.NetworkID" x-text="'Network ID: ' + (cluster.NetworkID || 'N/A')"></p>
|
||||
<p x-text="'Number of Workers: ' + cluster.Workers.length"></p>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<script>
|
||||
function networkClusters() {
|
||||
return {
|
||||
networks: [],
|
||||
newNetwork: {
|
||||
name: '',
|
||||
description: '',
|
||||
token: ''
|
||||
},
|
||||
errorMessage: '',
|
||||
successMessage: '',
|
||||
showForm: false, // Form visibility state
|
||||
loadingComplete: false, // To track if loading is complete
|
||||
toggleForm() {
|
||||
this.showForm = !this.showForm;
|
||||
console.log('Toggling form:', this.showForm);
|
||||
},
|
||||
fetchNetworks() {
|
||||
console.log('Fetching networks...');
|
||||
fetch('/networks')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
console.log('Data fetched successfully:', data);
|
||||
this.networks = data;
|
||||
this.loadingComplete = true; // Set loading complete
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error fetching networks:', error);
|
||||
this.loadingComplete = true; // Ensure spinner is hidden if error occurs
|
||||
});
|
||||
},
|
||||
|
||||
addNetwork() {
|
||||
this.errorMessage = '';
|
||||
this.successMessage = '';
|
||||
console.log('Adding new network:', this.newNetwork);
|
||||
|
||||
// Validate input
|
||||
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 => {
|
||||
console.log('Network added successfully:', data);
|
||||
this.successMessage = 'Network added successfully!';
|
||||
this.fetchNetworks(); // Refresh the networks list
|
||||
this.newNetwork = { name: '', description: '', token: '' }; // Clear form
|
||||
})
|
||||
.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) {
|
||||
navigator.clipboard.writeText(token)
|
||||
.then(() => {
|
||||
console.log('Token copied to clipboard:', token);
|
||||
alert('Token copied to clipboard!');
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('Failed to copy token:', err);
|
||||
});
|
||||
},
|
||||
init() {
|
||||
console.log('Initializing Alpine component...');
|
||||
this.fetchNetworks();
|
||||
setInterval(() => {
|
||||
this.fetchNetworks();
|
||||
}, 5000); // Refresh every 5 seconds
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{{template "views/partials/footer" .}}
|
||||
</div>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
39
core/http/views/partials/navbar_explorer.html
Normal file
39
core/http/views/partials/navbar_explorer.html
Normal file
|
@ -0,0 +1,39 @@
|
|||
<nav class="bg-gray-800 shadow-lg">
|
||||
<div class="container mx-auto px-4 py-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
<!-- Logo Image: Replace 'logo_url_here' with your actual logo URL -->
|
||||
<a href="/" class="text-white text-xl font-bold"><img src="https://github.com/go-skynet/LocalAI/assets/2420543/0966aa2a-166e-4f99-a3e5-6c915fc997dd" alt="LocalAI Logo" class="h-10 mr-3 border-2 border-gray-300 shadow rounded"></a>
|
||||
<a href="/" class="text-white text-xl font-bold">LocalAI</a>
|
||||
</div>
|
||||
<!-- Menu button for small screens -->
|
||||
<div class="lg:hidden">
|
||||
<button id="menu-toggle" class="text-gray-400 hover:text-white focus:outline-none">
|
||||
<i class="fas fa-bars fa-lg"></i>
|
||||
</button>
|
||||
</div>
|
||||
<!-- Navigation links -->
|
||||
<div class="hidden lg:flex lg:items-center lg:justify-end lg:flex-1 lg:w-0">
|
||||
<a href="/" class="text-gray-400 hover:text-white px-3 py-2 rounded"><i class="fas fa-home pr-2"></i>Home</a>
|
||||
<a href="https://localai.io" class="text-gray-400 hover:text-white px-3 py-2 rounded" target="_blank" ><i class="fas fa-book-reader pr-2"></i> Documentation</a>
|
||||
<a href="https://models.localai.io/" class="text-gray-400 hover:text-white px-3 py-2 rounded"><i class="fas fa-brain pr-2"></i> Models</a>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Collapsible menu for small screens -->
|
||||
<div class="hidden lg:hidden" id="mobile-menu">
|
||||
<div class="pt-4 pb-3 border-t border-gray-700">
|
||||
<a href="/" class="block text-gray-400 hover:text-white px-3 py-2 rounded mt-1"><i class="fas fa-home pr-2"></i>Home</a>
|
||||
<a href="https://localai.io" class="block text-gray-400 hover:text-white px-3 py-2 rounded mt-1" target="_blank" ><i class="fas fa-book-reader pr-2"></i> Documentation</a>
|
||||
<a href="https://models.localai.io/" class="text-gray-400 hover:text-white px-3 py-2 rounded"><i class="fas fa-brain pr-2"></i> Models</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<script>
|
||||
// JavaScript to toggle the mobile menu
|
||||
document.getElementById('menu-toggle').addEventListener('click', function () {
|
||||
var mobileMenu = document.getElementById('mobile-menu');
|
||||
mobileMenu.classList.toggle('hidden');
|
||||
});
|
||||
</script>
|
Loading…
Add table
Add a link
Reference in a new issue