diff --git a/core/http/app.go b/core/http/app.go index ddce573a..c81f55cb 100644 --- a/core/http/app.go +++ b/core/http/app.go @@ -139,6 +139,20 @@ func API(application *application.Application) (*fiber.App, error) { return nil, fmt.Errorf("failed to create key auth config: %w", err) } + httpFS := http.FS(embedDirStatic) + + router.Use(favicon.New(favicon.Config{ + URL: "/favicon.ico", + FileSystem: httpFS, + File: "static/favicon.ico", + })) + + router.Use("/static", filesystem.New(filesystem.Config{ + Root: httpFS, + PathPrefix: "static", + Browse: true, + })) + // Auth is applied to _all_ endpoints. No exceptions. Filtering out endpoints to bypass is the role of the Filter property of the KeyAuth Configuration router.Use(v2keyauth.New(*kaConfig)) @@ -176,20 +190,6 @@ func API(application *application.Application) (*fiber.App, error) { } routes.RegisterJINARoutes(router, requestExtractor, application.BackendLoader(), application.ModelLoader(), application.ApplicationConfig()) - httpFS := http.FS(embedDirStatic) - - router.Use(favicon.New(favicon.Config{ - URL: "/favicon.ico", - FileSystem: httpFS, - File: "static/favicon.ico", - })) - - router.Use("/static", filesystem.New(filesystem.Config{ - Root: httpFS, - PathPrefix: "static", - Browse: true, - })) - // Define a custom 404 handler // Note: keep this at the bottom! router.Use(notFoundHandler) diff --git a/core/http/elements/p2p.go b/core/http/elements/p2p.go index 7eb10df5..6c0a5a57 100644 --- a/core/http/elements/p2p.go +++ b/core/http/elements/p2p.go @@ -2,6 +2,7 @@ package elements import ( "fmt" + "time" "github.com/chasefleming/elem-go" "github.com/chasefleming/elem-go/attrs" @@ -18,19 +19,6 @@ func renderElements(n []elem.Node) string { } func P2PNodeStats(nodes []p2p.NodeData) string { - /* -
-

Total Workers Detected: {{ len .Nodes }}

- {{ $online := 0 }} - {{ range .Nodes }} - {{ if .IsOnline }} - {{ $online = add $online 1 }} - {{ end }} - {{ end }} -

Total Online Workers: {{$online}}

-
- */ - online := 0 for _, n := range nodes { if n.IsOnline() { @@ -38,27 +26,21 @@ func P2PNodeStats(nodes []p2p.NodeData) string { } } - class := "text-green-500" + class := "text-blue-400" if online == 0 { - class = "text-red-500" + class = "text-red-400" } - /* - - */ - circle := elem.I(attrs.Props{ - "class": "fas fa-circle animate-pulse " + class + " ml-2 mr-1", - }) + nodesElements := []elem.Node{ elem.Span( attrs.Props{ - "class": class, + "class": class + " font-bold text-xl", }, - circle, elem.Text(fmt.Sprintf("%d", online)), ), elem.Span( attrs.Props{ - "class": "text-gray-200", + "class": "text-gray-300 text-xl", }, elem.Text(fmt.Sprintf("/%d", len(nodes))), ), @@ -68,77 +50,73 @@ func P2PNodeStats(nodes []p2p.NodeData) string { } func P2PNodeBoxes(nodes []p2p.NodeData) string { - /* -
-
- - {{.ID}} -
-

- Status: - - - {{ if .IsOnline }}Online{{ else }}Offline{{ end }} - -

-
- */ - nodesElements := []elem.Node{} for _, n := range nodes { + nodeID := bluemonday.StrictPolicy().Sanitize(n.ID) + + // Define status-specific classes + statusIconClass := "text-green-400" + statusText := "Online" + statusTextClass := "text-green-400" + + if !n.IsOnline() { + statusIconClass = "text-red-400" + statusText = "Offline" + statusTextClass = "text-red-400" + } nodesElements = append(nodesElements, elem.Div( attrs.Props{ - "class": "bg-gray-700 p-6 rounded-lg shadow-lg text-left", + "class": "bg-gray-800/80 border border-gray-700/50 rounded-xl p-4 shadow-lg transition-all duration-300 hover:shadow-blue-900/20 hover:border-blue-700/50", }, - elem.P( + // Node ID and status indicator in top row + elem.Div( attrs.Props{ - "class": "text-sm text-gray-400 mt-2 flex", + "class": "flex items-center justify-between mb-3", }, - elem.I( + // Node ID with icon + elem.Div( attrs.Props{ - "class": "fas fa-desktop text-gray-400 mr-2", + "class": "flex items-center", }, - ), - elem.Text("Name: "), - elem.Span( - attrs.Props{ - "class": "text-gray-200 font-semibold ml-2 mr-1", - }, - elem.Text(bluemonday.StrictPolicy().Sanitize(n.ID)), - ), - elem.Text("Status: "), - elem.If( - n.IsOnline(), elem.I( attrs.Props{ - "class": "fas fa-circle animate-pulse text-green-500 ml-2 mr-1", + "class": "fas fa-server text-blue-400 mr-2", }, ), - elem.I( - attrs.Props{ - "class": "fas fa-circle animate-pulse text-red-500 ml-2 mr-1", - }, - ), - ), - elem.If( - n.IsOnline(), - elem.Span( - attrs.Props{ - "class": "text-green-400", - }, - - elem.Text("Online"), - ), elem.Span( attrs.Props{ - "class": "text-red-400", + "class": "text-white font-medium", }, - elem.Text("Offline"), + elem.Text(nodeID), ), ), + // Status indicator + elem.Div( + attrs.Props{ + "class": "flex items-center", + }, + elem.I( + attrs.Props{ + "class": "fas fa-circle animate-pulse " + statusIconClass + " mr-1.5", + }, + ), + elem.Span( + attrs.Props{ + "class": statusTextClass, + }, + elem.Text(statusText), + ), + ), + ), + // Bottom section with timestamp + elem.Div( + attrs.Props{ + "class": "text-xs text-gray-400 pt-1 border-t border-gray-700/30", + }, + elem.Text("Last updated: "+time.Now().UTC().Format("2006-01-02 15:04:05")), ), )) } diff --git a/core/http/routes/ui.go b/core/http/routes/ui.go index 65d1b09c..373a983b 100644 --- a/core/http/routes/ui.go +++ b/core/http/routes/ui.go @@ -173,7 +173,6 @@ func RegisterUIRoutes(app *fiber.App, } if page != "" { - log.Debug().Msgf("page : %+v\n", page) // return a subset of the models pageNum, err := strconv.Atoi(page) if err != nil { @@ -193,7 +192,6 @@ func RegisterUIRoutes(app *fiber.App, models = models.Paginate(pageNum, itemsNum) - log.Debug().Msgf("number of models : %+v\n", len(models)) prevPage := pageNum - 1 nextPage := pageNum + 1 if prevPage < 1 { @@ -552,7 +550,7 @@ func RegisterUIRoutes(app *fiber.App, title := "LocalAI - Generate audio" for _, b := range backendConfigs { - if b.HasUsecases(config.FLAG_CHAT) { + if b.HasUsecases(config.FLAG_TTS) { modelThatCanBeUsed = b.Name title = "LocalAI - Generate audio with " + modelThatCanBeUsed break diff --git a/core/http/static/tts.js b/core/http/static/tts.js index daead3a8..b50a7d65 100644 --- a/core/http/static/tts.js +++ b/core/http/static/tts.js @@ -1,64 +1,246 @@ +// Initialize Alpine store for API key management +document.addEventListener('alpine:init', () => { + Alpine.store('chat', { + get key() { + return localStorage.getItem('key') || ''; + }, + set key(value) { + localStorage.setItem('key', value); + } + }); +}); + function submitKey(event) { - event.preventDefault(); - localStorage.setItem("key", document.getElementById("apiKey").value); - document.getElementById("apiKey").blur(); - } + event.preventDefault(); + const keyValue = document.getElementById("apiKey").value; + localStorage.setItem("key", keyValue); + // Show brief visual confirmation + const button = event.submitter; + const originalIcon = button.innerHTML; + button.innerHTML = ''; + button.classList.add('bg-green-600'); + button.classList.remove('bg-blue-600', 'hover:bg-blue-700'); + + setTimeout(() => { + button.innerHTML = originalIcon; + button.classList.remove('bg-green-600'); + button.classList.add('bg-blue-600', 'hover:bg-blue-700'); + }, 1000); + + document.getElementById("apiKey").blur(); +} function genAudio(event) { event.preventDefault(); const input = document.getElementById("input").value; const key = localStorage.getItem("key"); - tts(key, input); -} - -async function tts(key, input) { - document.getElementById("loader").style.display = "block"; - document.getElementById("input").value = ""; - document.getElementById("input").disabled = true; - - const model = document.getElementById("tts-model").value; - const response = await fetch("tts", { - method: "POST", - headers: { - Authorization: `Bearer ${key}`, - "Content-Type": "application/json", - }, - body: JSON.stringify({ - model: model, - input: input, - }), - }); - if (!response.ok) { - const jsonData = await response.json(); // Now safely parse JSON - var div = document.getElementById('result'); - div.innerHTML = '

Error: ' +jsonData.error.message + '

'; + if (!input.trim()) { + showNotification('error', 'Please enter text to convert to speech'); return; } - var div = document.getElementById('result'); // Get the div by its ID - var link=document.createElement('a'); - link.className = "m-2 float-right inline-block rounded bg-primary px-6 pb-2.5 mb-3 pt-2.5 text-xs font-medium uppercase leading-normal text-white shadow-primary-3 transition duration-150 ease-in-out hover:bg-primary-accent-300 hover:shadow-primary-2 focus:bg-primary-accent-300 focus:shadow-primary-2 focus:outline-none focus:ring-0 active:bg-primary-600 active:shadow-primary-2 dark:shadow-black/30 dark:hover:shadow-dark-strong dark:focus:shadow-dark-strong dark:active:shadow-dark-strong"; - link.innerHTML = " Download result"; - const blob = await response.blob(); - link.href=window.URL.createObjectURL(blob); + if (!key) { + showNotification('warning', 'API key is not set. Please set your API key first.'); + return; + } - div.innerHTML = ''; // Clear the existing content of the div - div.appendChild(link); // Add the new img element to the div - console.log(link) - document.getElementById("loader").style.display = "none"; - document.getElementById("input").disabled = false; + tts(key, input); +} + +function showNotification(type, message) { + // Remove any existing notification + const existingNotification = document.getElementById('notification'); + if (existingNotification) { + existingNotification.remove(); + } + + // Create new notification + const notification = document.createElement('div'); + notification.id = 'notification'; + notification.classList.add( + 'fixed', 'top-24', 'right-4', 'z-50', 'p-4', 'rounded-lg', 'shadow-lg', + 'transform', 'transition-all', 'duration-300', 'ease-in-out', 'translate-y-0', + 'flex', 'items-center', 'gap-2' + ); + + // Style based on notification type + if (type === 'error') { + notification.classList.add('bg-red-900/90', 'border', 'border-red-700', 'text-red-200'); + notification.innerHTML = '' + message; + } else if (type === 'warning') { + notification.classList.add('bg-yellow-900/90', 'border', 'border-yellow-700', 'text-yellow-200'); + notification.innerHTML = '' + message; + } else if (type === 'success') { + notification.classList.add('bg-green-900/90', 'border', 'border-green-700', 'text-green-200'); + notification.innerHTML = '' + message; + } else { + notification.classList.add('bg-blue-900/90', 'border', 'border-blue-700', 'text-blue-200'); + notification.innerHTML = '' + message; + } + + // Add close button + const closeBtn = document.createElement('button'); + closeBtn.innerHTML = ''; + closeBtn.classList.add('ml-auto', 'text-gray-400', 'hover:text-white', 'transition-colors'); + closeBtn.onclick = () => { + notification.classList.add('opacity-0', 'translate-y-[-20px]'); + setTimeout(() => notification.remove(), 300); + }; + notification.appendChild(closeBtn); + + // Add to DOM + document.body.appendChild(notification); + + // Animate in + setTimeout(() => { + notification.classList.add('opacity-0', 'translate-y-[-20px]'); + notification.offsetHeight; // Force reflow + notification.classList.remove('opacity-0', 'translate-y-[-20px]'); + }, 10); + + // Auto dismiss after 5 seconds + setTimeout(() => { + if (document.getElementById('notification')) { + notification.classList.add('opacity-0', 'translate-y-[-20px]'); + setTimeout(() => notification.remove(), 300); + } + }, 5000); +} + +async function tts(key, input) { + // Show loader and prepare UI + const loader = document.getElementById("loader"); + const inputField = document.getElementById("input"); + const resultDiv = document.getElementById("result"); + + loader.style.display = "block"; + inputField.value = ""; + inputField.disabled = true; + resultDiv.innerHTML = '
Processing your request...
'; + + // Get the model and make API request + const model = document.getElementById("tts-model").value; + try { + const response = await fetch("tts", { + method: "POST", + headers: { + Authorization: `Bearer ${key}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + model: model, + input: input, + }), + }); + + if (!response.ok) { + const jsonData = await response.json(); + resultDiv.innerHTML = ` +
+ +

${jsonData.error.message || 'An error occurred'}

+
+ `; + showNotification('error', 'Failed to generate audio'); + return; + } + + // Handle successful response + const blob = await response.blob(); + const audioUrl = window.URL.createObjectURL(blob); + + // Create audio player + const audioPlayer = document.createElement('div'); + audioPlayer.className = 'flex flex-col items-center space-y-4 w-full'; + + // Create audio element with styled controls + const audio = document.createElement('audio'); + audio.controls = true; + audio.src = audioUrl; + audio.className = 'w-full my-4'; + audioPlayer.appendChild(audio); + + // Create action buttons container + const actionButtons = document.createElement('div'); + actionButtons.className = 'flex flex-wrap justify-center gap-3'; + + // Download button + const downloadLink = document.createElement('a'); + downloadLink.href = audioUrl; + downloadLink.download = `tts-${model}-${new Date().toISOString().slice(0, 10)}.mp3`; + downloadLink.className = 'group flex items-center bg-blue-600 hover:bg-blue-700 text-white py-2 px-4 rounded-lg transition duration-300 ease-in-out transform hover:scale-105 hover:shadow-lg'; + downloadLink.innerHTML = ` + + Download + + `; + actionButtons.appendChild(downloadLink); + + // Replay button + const replayButton = document.createElement('button'); + replayButton.className = 'group flex items-center bg-purple-600 hover:bg-purple-700 text-white py-2 px-4 rounded-lg transition duration-300 ease-in-out transform hover:scale-105 hover:shadow-lg'; + replayButton.innerHTML = ` + + Replay + `; + replayButton.onclick = () => audio.play(); + actionButtons.appendChild(replayButton); + + // Add text display + const textDisplay = document.createElement('div'); + textDisplay.className = 'mt-4 p-4 bg-gray-800/50 border border-gray-700/50 rounded-lg text-gray-300 text-center italic'; + textDisplay.textContent = `"${input}"`; + + // Add all elements to result div + audioPlayer.appendChild(actionButtons); + resultDiv.innerHTML = ''; + resultDiv.appendChild(audioPlayer); + resultDiv.appendChild(textDisplay); + + // Play audio automatically + audio.play(); + + // Show success notification + showNotification('success', 'Audio generated successfully'); + + } catch (error) { + console.error('Error generating audio:', error); + resultDiv.innerHTML = ` +
+ +

Network error: Failed to connect to the server

+
+ `; + showNotification('error', 'Network error occurred'); + } finally { + // Reset UI state + loader.style.display = "none"; + inputField.disabled = false; + inputField.focus(); + } +} + +// Set up event listeners when DOM is loaded +document.addEventListener('DOMContentLoaded', () => { + document.getElementById("key").addEventListener("submit", submitKey); document.getElementById("input").focus(); -} - -document.getElementById("key").addEventListener("submit", submitKey); -document.getElementById("input").focus(); -document.getElementById("tts").addEventListener("submit", genAudio); -document.getElementById("loader").style.display = "none"; - -const storeKey = localStorage.getItem("key"); -if (storeKey) { - document.getElementById("apiKey").value = storeKey; -} + document.getElementById("tts").addEventListener("submit", genAudio); + document.getElementById("loader").style.display = "none"; + // Initialize stored API key if available + const storeKey = localStorage.getItem("key"); + if (storeKey) { + document.getElementById("apiKey").value = storeKey; + } + + // Add basic keyboard shortcuts + document.addEventListener('keydown', (e) => { + // Submit on Ctrl+Enter + if (e.key === 'Enter' && e.ctrlKey && document.activeElement.id === 'input') { + e.preventDefault(); + document.getElementById("tts").dispatchEvent(new Event('submit')); + } + }); +}); \ No newline at end of file diff --git a/core/http/views/404.html b/core/http/views/404.html index 2f5a4386..a57a3702 100644 --- a/core/http/views/404.html +++ b/core/http/views/404.html @@ -1,28 +1,51 @@ - {{template "views/partials/head" .}} - +
- + {{template "views/partials/navbar" .}} - -
-
-

Welcome to your LocalAI instance!

-
- + +
+ +
+
+
+ +
+

+ + 404 - Page Not Found + +

+

The page you're looking for doesn't exist or has been moved

+
-

The FOSS alternative to OpenAI, Claude, ...

- Documentation
-
-

Nothing found!

+ +
+
+
+ +
+

Looking for resources?

+

Visit our 🖼️ Gallery or check the Getting started documentation

+
@@ -30,4 +53,4 @@
- + \ No newline at end of file diff --git a/core/http/views/explorer.html b/core/http/views/explorer.html index e1eaca93..caf43eb4 100644 --- a/core/http/views/explorer.html +++ b/core/http/views/explorer.html @@ -1,380 +1,224 @@ - {{template "views/partials/head" .}} - - - -
- {{template "views/partials/navbar_explorer" .}} -
- -
-
-

- Network Clusters Explorer - -

-

- View the clusters and workers available in each network. - - - -

- -
+ + -
- -
- - 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. -
-
- - -
- -
-

Add New Network

-
- - +
+ +
+
+
+
-
- - -
-
- - -
- - -
- - - - - - - - +
- - - - {{template "views/partials/footer" .}} -
+ + // Start animation + animate(); + + // Resize handling + window.addEventListener('resize', () => { + canvas.width = animContainer.offsetWidth; + canvas.height = animContainer.offsetHeight; + }); + } + }); + - - + \ No newline at end of file diff --git a/core/http/views/login.html b/core/http/views/login.html index b9f6014d..aed40d2f 100644 --- a/core/http/views/login.html +++ b/core/http/views/login.html @@ -1,25 +1,216 @@ - - - - Open Authenticated Website - - - - -

Authorization is required

- - - + + var date = new Date(); + date.setTime(date.getTime() + (24*60*60*1000)); + document.cookie = `token=${token}; expires=${date.toGMTString()}; path=/`; + + // Show loading state + const button = document.querySelector('button[type="submit"]'); + const originalContent = button.innerHTML; + button.disabled = true; + button.innerHTML = ' Authenticating...'; + button.classList.add('bg-gray-600'); + + // Reload after short delay to show loading state + setTimeout(() => { + window.location.reload(); + }, 800); + } + + // Update current time + function updateCurrentTime() { + const timeElement = document.getElementById('current-time'); + if (timeElement) { + const now = new Date(); + const year = now.getUTCFullYear(); + const month = String(now.getUTCMonth() + 1).padStart(2, '0'); + const day = String(now.getUTCDate()).padStart(2, '0'); + const hours = String(now.getUTCHours()).padStart(2, '0'); + const minutes = String(now.getUTCMinutes()).padStart(2, '0'); + const seconds = String(now.getUTCSeconds()).padStart(2, '0'); + timeElement.textContent = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`; + } + } + + // Initialize current time and update it every second + updateCurrentTime(); + setInterval(updateCurrentTime, 1000); + + // Add subtle particle animation to the background + document.addEventListener('DOMContentLoaded', function() { + const animContainer = document.querySelector('.animation-container'); + if (animContainer) { + const canvas = document.createElement('canvas'); + animContainer.appendChild(canvas); + + const ctx = canvas.getContext('2d'); + canvas.width = animContainer.offsetWidth; + canvas.height = animContainer.offsetHeight; + + // Create particles + const particles = []; + const particleCount = 30; + + for (let i = 0; i < particleCount; i++) { + particles.push({ + x: Math.random() * canvas.width, + y: Math.random() * canvas.height, + radius: Math.random() * 3 + 1, + color: `rgba(${Math.random() * 50 + 50}, ${Math.random() * 100 + 100}, ${Math.random() * 155 + 100}, ${Math.random() * 0.4 + 0.1})`, + speedX: Math.random() * 0.5 - 0.25, + speedY: Math.random() * 0.5 - 0.25 + }); + } + + // Animation loop + function animate() { + requestAnimationFrame(animate); + ctx.clearRect(0, 0, canvas.width, canvas.height); + + particles.forEach(particle => { + particle.x += particle.speedX; + particle.y += particle.speedY; + + // Bounce off edges + if (particle.x < 0 || particle.x > canvas.width) { + particle.speedX = -particle.speedX; + } + + if (particle.y < 0 || particle.y > canvas.height) { + particle.speedY = -particle.speedY; + } + + // Draw particle + ctx.beginPath(); + ctx.arc(particle.x, particle.y, particle.radius, 0, Math.PI * 2); + ctx.fillStyle = particle.color; + ctx.fill(); + }); + + // Connect nearby particles with lines + for (let i = 0; i < particles.length; i++) { + for (let j = i + 1; j < particles.length; j++) { + const dx = particles[i].x - particles[j].x; + const dy = particles[i].y - particles[j].y; + const distance = Math.sqrt(dx * dx + dy * dy); + + if (distance < 100) { + ctx.beginPath(); + ctx.moveTo(particles[i].x, particles[i].y); + ctx.lineTo(particles[j].x, particles[j].y); + ctx.strokeStyle = `rgba(100, 150, 255, ${0.1 * (1 - distance / 100)})`; + ctx.lineWidth = 1; + ctx.stroke(); + } + } + } + } + + // Start animation + animate(); + + // Resize handling + window.addEventListener('resize', () => { + canvas.width = animContainer.offsetWidth; + canvas.height = animContainer.offsetHeight; + }); + } + }); + + - + \ No newline at end of file diff --git a/core/http/views/p2p.html b/core/http/views/p2p.html index 81de1bb4..6e902485 100644 --- a/core/http/views/p2p.html +++ b/core/http/views/p2p.html @@ -1,172 +1,245 @@ {{template "views/partials/head" .}} - + +
{{template "views/partials/navbar" .}} -
-
-
+ +
+
+ +
-
-

- Distributed inference with P2P -

-

- Distribute computation by sharing and loadbalancing instances or sharding model weights. - - - -

- -
+

+ + Distributed inference with P2P + +

+

+ Distribute computation by sharing and loadbalancing instances or sharding model weights + + + +

-
-
LocalAI uses P2P technologies to enable distribution of work between peers. It is possible to share an instance with Federation and/or split the weights of a model across peers (only available with llama.cpp models). You can now share computational resources between your devices or your friends!
- -
-

- Network token - +

+ +
+

+ LocalAI uses P2P technologies to enable distribution of work between peers. It is possible to share an instance with Federation and/or split the weights of a model across peers (only available with llama.cpp models). You can now share computational resources between your devices or your friends! +

+
+ + +
+
+ +

Network Token

+ +
+ {{.P2PToken}} +

+ The network token can be used to either share the instance or join a federation or a worker network. Below you will find examples on how to start a new instance or a worker with this token.

- {{.P2PToken}}
- The network token can be used to either share the instance or join a federation or a worker network. Below you will find a few examples on how to start a new instance or a worker with the token and you will be able to see the available workers and federated nodes.
{{ if and .IsP2PEnabled (eq .P2PToken "") }} -
-

Warning: P2P mode is disabled or no token was specified

-

You have to enable P2P mode by starting LocalAI with --p2p. Please restart the server with --p2p to generate a new token automatically that can be used to automatically discover other nodes. If you already have a token specify it with export TOKEN=".." - Check out the documentation for more information. -

+
+
+ +

Warning: P2P mode is disabled or no token was specified

+
+

+ You have to enable P2P mode by starting LocalAI with --p2p. Please restart the server with --p2p to generate a new token automatically that can be used to discover other nodes. If you already have a token, specify it with export TOKEN=".." + + Check out the documentation for more information. + +

{{ else }} -
+
+
+
+ +

Federated Nodes: + +

+
+

+ You can start LocalAI in federated mode to share your instance, or start the federated server to balance requests between nodes of the federation. +

-

Federated Nodes:

-

You can start LocalAI in federated mode to share your instance, or start the federated server to balance requests between nodes of the federation.

- -
-
+
+
+
-
+
+

+ Start a federated instance +

-

Start a federated instance

+ + - - + +
+
- -
- -

Workers (llama.cpp):

-

You can start llama.cpp workers to distribute weights between the workers and offload part of the computation. To start a new worker, you can use the CLI or Docker.

- -
-
-
-
- -

Start a new llama.cpp P2P worker

- - - - - -
-