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" .}}
-
-
-