Initial commit: полный проект my_speaker
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
const DEFAULT_BACKEND_URL = "http://192.268.200.111:8000";
|
||||
let backendUrl = localStorage.getItem("backendUrl") || DEFAULT_BACKEND_URL;
|
||||
|
||||
export function setBackendUrl(url) {
|
||||
backendUrl = url;
|
||||
localStorage.setItem("backendUrl", url);
|
||||
}
|
||||
|
||||
export async function apiFetch(endpoint, options = {}) {
|
||||
const token = localStorage.getItem("token");
|
||||
const headers = {
|
||||
"Content-Type": "application/json",
|
||||
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
||||
...options.headers,
|
||||
};
|
||||
|
||||
const response = await fetch(`${backendUrl}/api${endpoint}`, {
|
||||
...options,
|
||||
headers,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 401) {
|
||||
localStorage.removeItem("token");
|
||||
window.location.reload(); // Редирект на login
|
||||
}
|
||||
throw new Error(`API error: ${response.statusText}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
export async function login(username, password) {
|
||||
return apiFetch("/auth/login", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ username, password }),
|
||||
});
|
||||
}
|
||||
|
||||
export async function logout() {
|
||||
await apiFetch("/auth/logout", { method: "POST" });
|
||||
localStorage.removeItem("token");
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
// Другие методы по API_DOCUMENTATION.md
|
||||
export async function getSpeechData() {
|
||||
return apiFetch("/dashboard/speech-data");
|
||||
}
|
||||
|
||||
export async function getSysMonitor() {
|
||||
return apiFetch("/dashboard/sys-monitor");
|
||||
}
|
||||
|
||||
export async function getRecentPhrases(limit = 10) {
|
||||
return apiFetch(`/dashboard/recent-phrases?limit=${limit}`);
|
||||
}
|
||||
|
||||
export async function getSettings() {
|
||||
return apiFetch("/settings");
|
||||
}
|
||||
|
||||
export async function updateSettings(section, data) {
|
||||
return apiFetch(`/settings/${section}`, {
|
||||
method: "PUT",
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
}
|
||||
|
||||
export async function restart() {
|
||||
return apiFetch("/settings/restart", { method: "POST" });
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { initRouter } from "./modules/router.js";
|
||||
import { loadLogin } from "./modules/login.js";
|
||||
import { setBgColor, setMainColor, setBorderColor } from "./modules/utils.js";
|
||||
import { setBackendUrl } from "./api.js";
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
const color = localStorage.getItem("mainColor") || "#0f0";
|
||||
setMainColor(color);
|
||||
const borderColor = localStorage.getItem("borderColor") || "#0f0";
|
||||
setBorderColor(borderColor);
|
||||
const bgColor = localStorage.getItem("bgColor") || "#000";
|
||||
setBgColor(bgColor);
|
||||
|
||||
const backend =
|
||||
localStorage.getItem("backendUrl") || "http://192.168.200.111:8000";
|
||||
setBackendUrl(backend);
|
||||
|
||||
if (!localStorage.getItem("token")) {
|
||||
loadLogin();
|
||||
} else {
|
||||
initRouter();
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,47 @@
|
||||
<header>
|
||||
<h1>My Speaker</h1>
|
||||
<button id="logout-btn">[Logout]</button>
|
||||
<div class="info-row">
|
||||
<div id="user-name">User: admin</div>
|
||||
<div id="polling-interval">Polling Interval: 5000 ms</div>
|
||||
<div id="current-time">Current Time: 15:35:28</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<style>
|
||||
header {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 8px 16px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0;
|
||||
flex: 0 0 auto; /* Не растягивается */
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
#logout-btn {
|
||||
margin-left: auto; /* Прижимаем вправо */
|
||||
flex: 0 0 auto;
|
||||
padding: 8px 16px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.info-row {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
#user-name,
|
||||
#polling-interval,
|
||||
#current-time {
|
||||
white-space: nowrap;
|
||||
color: aliceblue;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,5 @@
|
||||
<nav>
|
||||
<button data-section="dashboard">[ Dashboard ]</button>
|
||||
<button data-section="settings">[ Settings ]</button>
|
||||
<button data-section="commands">[ Commands ]</button>
|
||||
</nav>
|
||||
@@ -0,0 +1,20 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>My Speaker</title>
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<link rel="stylesheet" href="style.css" />
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<!-- Контент загружается динамически -->
|
||||
</div>
|
||||
<script type="module" src="app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,29 @@
|
||||
import { getSettings, updateSettings } from "../api.js";
|
||||
|
||||
export async function loadCommands() {
|
||||
const content = document.getElementById("content");
|
||||
const commandsHtml = await fetch("pages/commands.html").then((res) =>
|
||||
res.text()
|
||||
);
|
||||
content.innerHTML = commandsHtml;
|
||||
|
||||
const settings = await getSettings();
|
||||
document.getElementById("commands-file").value =
|
||||
settings.commands.commands_file;
|
||||
|
||||
// Предполагаем, что API возвращает список команд, но по docs — только файл. Для списка — заглушка.
|
||||
const commandsList = document.getElementById("commands-list");
|
||||
commandsList.innerHTML = '<p>"Turn on light"</p><p>"Play music"</p>'; // Заглушка, расширить если нужно
|
||||
|
||||
document.getElementById("edit-btn").addEventListener("click", () => {
|
||||
alert("Edit mode"); // Логика редактирования
|
||||
});
|
||||
|
||||
document.getElementById("save-btn").addEventListener("click", async () => {
|
||||
const data = {
|
||||
commands_file: document.getElementById("commands-file").value,
|
||||
};
|
||||
await updateSettings("commands", data);
|
||||
alert("Saved");
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
import {
|
||||
getSpeechData,
|
||||
getSysMonitor,
|
||||
getRecentPhrases,
|
||||
getSettings,
|
||||
} from "../api.js";
|
||||
|
||||
let speechChart;
|
||||
let energyChart;
|
||||
let historyMaxLen = 20; // Значение по умолчанию в области видимости модуля
|
||||
|
||||
export async function loadDashboard() {
|
||||
const content = document.getElementById("content");
|
||||
const dashboardHtml = await fetch("pages/dashboard.html").then((res) =>
|
||||
res.text()
|
||||
);
|
||||
content.innerHTML = dashboardHtml;
|
||||
|
||||
// Инициализация графика речи
|
||||
const speechCtx = document.getElementById("speech-chart").getContext("2d");
|
||||
speechChart = new Chart(speechCtx, {
|
||||
type: "line",
|
||||
data: {
|
||||
labels: [],
|
||||
datasets: [
|
||||
{
|
||||
label: "Speech Prob",
|
||||
data: [],
|
||||
borderColor: "#0f0",
|
||||
backgroundColor: "rgba(0, 255, 0, 0.1)",
|
||||
borderWidth: 2,
|
||||
pointRadius: 0,
|
||||
fill: false,
|
||||
},
|
||||
{
|
||||
label: "Speech Threshold",
|
||||
data: [],
|
||||
borderColor: "#ffcc00",
|
||||
borderWidth: 1,
|
||||
pointRadius: 0,
|
||||
borderDash: [5, 5],
|
||||
fill: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
max: 1,
|
||||
title: {
|
||||
display: true,
|
||||
text: "Speech Probability",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Инициализация графика энергии
|
||||
const energyCtx = document.getElementById("energy-chart").getContext("2d");
|
||||
energyChart = new Chart(energyCtx, {
|
||||
type: "line",
|
||||
data: {
|
||||
labels: [],
|
||||
datasets: [
|
||||
{
|
||||
label: "Energy",
|
||||
data: [],
|
||||
borderColor: "#ff00ff",
|
||||
backgroundColor: "rgba(255, 0, 255, 0.1)",
|
||||
borderWidth: 2,
|
||||
pointRadius: 0,
|
||||
fill: false,
|
||||
},
|
||||
{
|
||||
label: "Energy Threshold",
|
||||
data: [],
|
||||
borderColor: "#ff6600",
|
||||
borderWidth: 1,
|
||||
pointRadius: 0,
|
||||
borderDash: [5, 5],
|
||||
fill: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
title: {
|
||||
display: true,
|
||||
text: "Energy Level",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Получение и обработка настроек
|
||||
let settings;
|
||||
try {
|
||||
settings = await getSettings();
|
||||
} catch (error) {
|
||||
console.error("Failed to load settings:", error);
|
||||
settings = { system: { polling_interval_ms: 1000, history_maxlen: 20 } }; // Значения по умолчанию
|
||||
}
|
||||
const pollingIntervalMs = settings.system.polling_interval_ms || 1000;
|
||||
historyMaxLen = settings.system.history_maxlen || 20; // Обновляем глобальную переменную
|
||||
|
||||
// Установка интервала опроса и передача значения в хедер
|
||||
setInterval(updateDashboard, pollingIntervalMs);
|
||||
updateDashboard();
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("updateHeader", { detail: pollingIntervalMs })
|
||||
);
|
||||
}
|
||||
|
||||
async function updateDashboard() {
|
||||
try {
|
||||
const [speech, sys, phrases] = await Promise.all([
|
||||
getSpeechData(),
|
||||
getSysMonitor(),
|
||||
getRecentPhrases(5),
|
||||
]);
|
||||
|
||||
// Update main-params
|
||||
document.getElementById("status").textContent = `Status: ${speech.state}`;
|
||||
|
||||
// Обновление карточки речи
|
||||
document.getElementById("speech-value").textContent =
|
||||
speech.speech_prob.toFixed(2);
|
||||
document.getElementById("speech-max").textContent =
|
||||
speech.speech_threshold.toFixed(2);
|
||||
document.getElementById("sp-detect").textContent = speech.speech_detected
|
||||
? "True"
|
||||
: "False";
|
||||
|
||||
// Обновление карточки энергии
|
||||
document.getElementById("energy-value").textContent = Math.round(
|
||||
speech.energy
|
||||
);
|
||||
document.getElementById("energy-max").textContent = Math.round(
|
||||
speech.energy_threshold
|
||||
);
|
||||
document.getElementById("noise-floor").textContent = Math.round(
|
||||
speech.noise_floor
|
||||
);
|
||||
// Обновление системного монитора
|
||||
document.getElementById("cpu").textContent = `${sys.cpu_percent}%`;
|
||||
document.getElementById("mem").textContent = `${sys.memory_percent}%`;
|
||||
document.getElementById("temp").textContent = `${sys.temperature}°C`;
|
||||
|
||||
// Update speech chart
|
||||
const now = new Date();
|
||||
const timestamp =
|
||||
now.toLocaleTimeString() +
|
||||
"." +
|
||||
now.getMilliseconds().toString().padStart(3, "0");
|
||||
speechChart.data.labels.push(timestamp);
|
||||
speechChart.data.datasets[0].data.push(speech.speech_prob);
|
||||
speechChart.data.datasets[1].data.push(speech.speech_threshold);
|
||||
if (speechChart.data.labels.length > historyMaxLen) {
|
||||
speechChart.data.labels.shift();
|
||||
speechChart.data.datasets.forEach((ds) => ds.data.shift());
|
||||
}
|
||||
speechChart.update("none"); // Используем 'none' для оптимизации при частых обновлениях
|
||||
|
||||
// Update energy chart
|
||||
energyChart.data.labels.push(timestamp);
|
||||
energyChart.data.datasets[0].data.push(speech.energy);
|
||||
energyChart.data.datasets[1].data.push(speech.energy_threshold);
|
||||
if (energyChart.data.labels.length > historyMaxLen) {
|
||||
energyChart.data.labels.shift();
|
||||
energyChart.data.datasets.forEach((ds) => ds.data.shift());
|
||||
}
|
||||
energyChart.update("none"); // Используем 'none' для оптимизации при частых обновлениях
|
||||
|
||||
// Update footer
|
||||
const footer = document.getElementById("footer");
|
||||
footer.innerHTML = phrases
|
||||
.map(
|
||||
(p) =>
|
||||
`<p class="history-item">${p.timestamp} - ${
|
||||
p.action || "неизвестно"
|
||||
} - ${p.text}</p>`
|
||||
)
|
||||
.join("");
|
||||
} catch (error) {
|
||||
console.error("Error updating dashboard:", error);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { logout as apiLogout } from "../api.js";
|
||||
|
||||
export function renderHeader(pollingIntervalMs) {
|
||||
// Установка имени пользователя (заглушка)
|
||||
const userNameElement = document.getElementById("user-name");
|
||||
if (userNameElement) {
|
||||
userNameElement.textContent = "User: admin"; // Заглушка, заменить на данные из /auth/me
|
||||
}
|
||||
|
||||
// Отображение частоты опроса
|
||||
const pollingIntervalElement = document.getElementById("polling-interval");
|
||||
if (pollingIntervalElement) {
|
||||
pollingIntervalElement.textContent = `Polling Interval: ${
|
||||
pollingIntervalMs || 1000
|
||||
} ms`; // Значение по умолчанию 1000 мс
|
||||
}
|
||||
|
||||
// Отображение текущего времени
|
||||
const currentTimeElement = document.getElementById("current-time");
|
||||
if (currentTimeElement) {
|
||||
function updateTime() {
|
||||
const now = new Date().toLocaleTimeString("ru-RU", {
|
||||
timeZone: "Europe/Moscow",
|
||||
hour12: false,
|
||||
});
|
||||
currentTimeElement.textContent = `Current Time: ${now}`;
|
||||
}
|
||||
updateTime(); // Инициализация
|
||||
setInterval(updateTime, 1000); // Обновление каждую секунду
|
||||
}
|
||||
|
||||
// Обработчик кнопки выхода
|
||||
const logoutBtn = document.getElementById("logout-btn");
|
||||
if (logoutBtn) {
|
||||
logoutBtn.addEventListener("click", apiLogout);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import { login as apiLogin, setBackendUrl } from "../api.js";
|
||||
import { initRouter } from "./router.js";
|
||||
import { setBgColor, setMainColor, setBorderColor } from "./utils.js";
|
||||
|
||||
export async function loadLogin() {
|
||||
const app = document.getElementById("app");
|
||||
app.innerHTML = "";
|
||||
const loginHtml = await fetch("pages/login.html").then((res) => res.text());
|
||||
app.insertAdjacentHTML("beforeend", loginHtml);
|
||||
|
||||
const form = document.getElementById("login-form");
|
||||
form.addEventListener("submit", async (e) => {
|
||||
e.preventDefault();
|
||||
const username = document.getElementById("username").value;
|
||||
const password = document.getElementById("password").value;
|
||||
try {
|
||||
const data = await apiLogin(username, password);
|
||||
localStorage.setItem("token", data.access_token);
|
||||
initRouter();
|
||||
} catch (err) {
|
||||
document.getElementById("error").textContent = "ERROR: Invalid login";
|
||||
}
|
||||
});
|
||||
|
||||
// Settings
|
||||
const settingsBtn = document.getElementById("settings-btn");
|
||||
settingsBtn.addEventListener("click", () => {
|
||||
const settings = document.getElementById("settings-form");
|
||||
settings.style.display =
|
||||
settings.style.display === "none" ? "block" : "none";
|
||||
});
|
||||
|
||||
const colorInput = document.getElementById("main-color");
|
||||
colorInput.value = localStorage.getItem("mainColor") || "#0f0";
|
||||
colorInput.addEventListener("change", (e) => {
|
||||
setMainColor(e.target.value);
|
||||
});
|
||||
const colorBorderInput = document.getElementById("border-color");
|
||||
colorBorderInput.value = localStorage.getItem("borderColor") || "#0f0";
|
||||
colorBorderInput.addEventListener("change", (e) => {
|
||||
setBorderColor(e.target.value);
|
||||
});
|
||||
const colorBgInput = document.getElementById("bg-color");
|
||||
colorBgInput.value = localStorage.getItem("bgColor") || "#000";
|
||||
colorBgInput.addEventListener("change", (e) => {
|
||||
setBgColor(e.target.value);
|
||||
});
|
||||
|
||||
const backendInput = document.getElementById("backend-url");
|
||||
backendInput.value =
|
||||
localStorage.getItem("backendUrl") || "http://localhost:8000";
|
||||
backendInput.addEventListener("change", (e) => {
|
||||
setBackendUrl(e.target.value);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export function renderNav(currentSection) {
|
||||
document.querySelectorAll("nav button").forEach((btn) => {
|
||||
btn.classList.toggle("active", btn.dataset.section === currentSection); // Используем переданный currentSection
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import { loadDashboard } from "./dashboard.js";
|
||||
import { loadSettings } from "./settings.js";
|
||||
import { loadCommands } from "./commands.js";
|
||||
import { renderHeader } from "./header.js";
|
||||
import { renderNav } from "./nav.js";
|
||||
import { apiFetch } from "../api.js";
|
||||
|
||||
let currentSection = "dashboard"; // Локальная переменная в модуле
|
||||
|
||||
export async function initRouter() {
|
||||
const app = document.getElementById("app");
|
||||
app.innerHTML = "";
|
||||
|
||||
// Загрузка header и nav
|
||||
const headerHtml = await fetch("components/header.html").then((res) =>
|
||||
res.text()
|
||||
);
|
||||
app.insertAdjacentHTML("beforeend", headerHtml);
|
||||
renderHeader(1000); // Начальное значение до получения настроек
|
||||
|
||||
const navHtml = await fetch("components/nav.html").then((res) => res.text());
|
||||
app.insertAdjacentHTML("beforeend", navHtml);
|
||||
renderNav(currentSection);
|
||||
|
||||
// Основной контейнер
|
||||
app.insertAdjacentHTML(
|
||||
"beforeend",
|
||||
'<div class="main-container"><div id="main-params" class="main-params"></div><div id="content" class="content"></div></div><footer id="footer"></footer>'
|
||||
);
|
||||
|
||||
// Загрузка main-params
|
||||
const mainParamsHtml = await fetch("pages/main-params.html").then((res) =>
|
||||
res.text()
|
||||
);
|
||||
document.getElementById("main-params").innerHTML = mainParamsHtml;
|
||||
|
||||
// Футер
|
||||
updateFooter();
|
||||
|
||||
// Навигация
|
||||
document.querySelectorAll("nav button").forEach((btn) => {
|
||||
btn.addEventListener("click", () => {
|
||||
currentSection = btn.dataset.section;
|
||||
renderNav(currentSection); // Обновляем навигацию
|
||||
loadSection();
|
||||
});
|
||||
});
|
||||
|
||||
// Обработка события обновления хедера
|
||||
window.addEventListener("updateHeader", (event) => {
|
||||
renderHeader(event.detail); // Обновляем хедер с полученным pollingIntervalMs
|
||||
});
|
||||
|
||||
loadSection();
|
||||
}
|
||||
|
||||
async function loadSection() {
|
||||
switch (currentSection) {
|
||||
case "dashboard":
|
||||
loadDashboard();
|
||||
break;
|
||||
case "settings":
|
||||
loadSettings();
|
||||
break;
|
||||
case "commands":
|
||||
loadCommands();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
async function updateFooter() {
|
||||
const phrases = await apiFetch("/dashboard/recent-phrases?limit=5");
|
||||
const footer = document.getElementById("footer");
|
||||
footer.innerHTML =
|
||||
'== RECENT PHRASES ==<div class="phrases">' +
|
||||
phrases.map((p) => `<p>${p.text}</p>`).join("") +
|
||||
"</div>";
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
import { getSettings, updateSettings, restart } from "../api.js";
|
||||
|
||||
export async function loadSettings() {
|
||||
const content = document.getElementById("content");
|
||||
if (!content) {
|
||||
console.error("Content element not found");
|
||||
return;
|
||||
}
|
||||
|
||||
// Показать индикатор загрузки
|
||||
content.innerHTML = '<div class="loading">Loading settings...</div>';
|
||||
|
||||
try {
|
||||
const settingsHtml = await fetch("pages/settings.html").then((res) =>
|
||||
res.text()
|
||||
);
|
||||
content.innerHTML = settingsHtml;
|
||||
|
||||
const allSettings = await getSettings();
|
||||
|
||||
// Заполнение форм
|
||||
fillForm("vad-form", allSettings.vad);
|
||||
fillForm("system-form", allSettings.system);
|
||||
fillForm("stt-form", allSettings.stt);
|
||||
fillForm("commands-form", allSettings.commands);
|
||||
fillForm("auth-form", allSettings.auth);
|
||||
fillForm("tts-form", allSettings.tts);
|
||||
fillForm("audio-settings-form", allSettings.audio_settings);
|
||||
|
||||
// Инициализация аккордеона
|
||||
initAccordion();
|
||||
|
||||
// Обработчики кнопок сохранения
|
||||
document.querySelectorAll(".save-btn").forEach((btn) => {
|
||||
btn.addEventListener("click", async () => {
|
||||
const section = btn.dataset.section;
|
||||
const formId = section.replace('_', '-') + '-form';
|
||||
const form = document.getElementById(formId);
|
||||
if (!form) {
|
||||
console.error(`Form ${formId} not found`);
|
||||
alert(`Form for ${section} not found`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Собираем данные формы вручную
|
||||
const formData = {};
|
||||
|
||||
// Обрабатываем все поля ввода
|
||||
form.querySelectorAll("input, select").forEach((input) => {
|
||||
if (input.type === "checkbox") {
|
||||
formData[input.name] = input.checked;
|
||||
} else if (input.value !== "") {
|
||||
formData[input.name] = input.value;
|
||||
}
|
||||
});
|
||||
|
||||
// Выводим данные в консоль для отладки
|
||||
console.log("Отправляемые данные:", formData);
|
||||
|
||||
try {
|
||||
await updateSettings(section, formData);
|
||||
alert(
|
||||
`${
|
||||
section.charAt(0).toUpperCase() + section.slice(1)
|
||||
} settings saved successfully`
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(`Failed to save ${section} settings:`, error);
|
||||
alert(`Failed to save ${section} settings`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Обработчик переинициализации VAD
|
||||
const restartBtn = document.getElementById("restart-btn");
|
||||
if (restartBtn) {
|
||||
restartBtn.addEventListener("click", async () => {
|
||||
if (confirm("Are you sure you want to restart VAD?")) {
|
||||
try {
|
||||
await restart();
|
||||
alert("VAD restarted successfully");
|
||||
} catch (error) {
|
||||
console.error("Failed to restart VAD:", error);
|
||||
alert("Failed to restart VAD");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading settings:", error);
|
||||
content.innerHTML =
|
||||
'<div class="error">Failed to load settings. Check console for details.</div>';
|
||||
}
|
||||
}
|
||||
|
||||
// Функция инициализации аккордеона
|
||||
function initAccordion() {
|
||||
// Получаем все заголовки секций
|
||||
const sectionHeaders = document.querySelectorAll(".section-header");
|
||||
|
||||
// Добавляем обработчик клика для каждого заголовка
|
||||
sectionHeaders.forEach((header) => {
|
||||
header.addEventListener("click", function () {
|
||||
// Получаем содержимое секции
|
||||
const content = this.nextElementSibling;
|
||||
|
||||
// Если секция уже активна, просто сворачиваем её
|
||||
if (!this.classList.contains("collapsed")) {
|
||||
this.classList.add("collapsed");
|
||||
content.classList.remove("active");
|
||||
return;
|
||||
}
|
||||
|
||||
// Сворачиваем все секции
|
||||
document.querySelectorAll(".section-header").forEach((h) => {
|
||||
h.classList.add("collapsed");
|
||||
h.nextElementSibling.classList.remove("active");
|
||||
});
|
||||
|
||||
// Разворачиваем текущую секцию
|
||||
this.classList.remove("collapsed");
|
||||
content.classList.add("active");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function fillForm(formId, data) {
|
||||
const form = document.getElementById(formId);
|
||||
if (!form) {
|
||||
console.warn(`Form ${formId} not found`);
|
||||
return;
|
||||
}
|
||||
|
||||
for (const [key, value] of Object.entries(data)) {
|
||||
const input = form.querySelector(`[name="${key}"]`);
|
||||
if (input) {
|
||||
if (input.type === "checkbox") {
|
||||
input.checked = !!value; // Приводим к булевому типу
|
||||
} else {
|
||||
input.value = value !== undefined ? value : "";
|
||||
}
|
||||
} else {
|
||||
console.warn(`Input for ${key} not found in ${formId}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function processFormData(section, formData) {
|
||||
const data = {};
|
||||
for (let [key, value] of formData) {
|
||||
if (value === "") continue; // Пропускаем пустые значения
|
||||
switch (key) {
|
||||
case "mode":
|
||||
case "sample_rate":
|
||||
case "frame_duration_ms":
|
||||
case "ring_buffer_size":
|
||||
case "energy_threshold":
|
||||
case "min_speech_duration_ms":
|
||||
case "max_record_duration_sec":
|
||||
case "target_rms":
|
||||
case "max_gain":
|
||||
case "history_maxlen":
|
||||
case "polling_interval_ms":
|
||||
case "port":
|
||||
case "timeout":
|
||||
case "playback_device":
|
||||
case "capture_device":
|
||||
case "playback_volume":
|
||||
case "capture_volume":
|
||||
data[key] = parseInt(value);
|
||||
break;
|
||||
case "speech_threshold":
|
||||
case "record_stop_delay_sec":
|
||||
case "prebuffer_duration_sec":
|
||||
case "noise_reduction_factor":
|
||||
case "noise_update_alpha":
|
||||
case "min_rms_threshold_factor":
|
||||
case "gain_smoothing_alpha":
|
||||
case "adaptive_energy_std_factor":
|
||||
case "pause_duration":
|
||||
case "post_stop_wait":
|
||||
data[key] = parseFloat(value);
|
||||
break;
|
||||
case "adaptive_energy":
|
||||
case "noise_reduction_enabled":
|
||||
case "auto_gain_enabled":
|
||||
case "record_on_speech":
|
||||
case "log_save_file":
|
||||
case "save_TTS_files":
|
||||
data[key] = value === "on"; // Checkbox возвращает 'on' при checked или 'off' при unchecked
|
||||
break;
|
||||
default:
|
||||
data[key] = value;
|
||||
}
|
||||
}
|
||||
return data;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
export function setMainColor(color) {
|
||||
document.documentElement.style.setProperty("--main-color", color);
|
||||
localStorage.setItem("mainColor", color);
|
||||
}
|
||||
export function setBorderColor(color) {
|
||||
document.documentElement.style.setProperty("--border-color", color);
|
||||
localStorage.setItem("borderColor", color);
|
||||
}
|
||||
export function setBgColor(color) {
|
||||
document.documentElement.style.setProperty("--bg-color", color);
|
||||
localStorage.setItem("bgColor", color);
|
||||
}
|
||||
export function showError(msg) {
|
||||
alert(msg); // Или в UI
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
<label>Commands File: <input id="commands-file" type="text" /></label>
|
||||
<button id="edit-btn">[ EDIT ]</button>
|
||||
<button id="save-btn">[ SAVE ]</button>
|
||||
<div id="commands-list"></div>
|
||||
@@ -0,0 +1,27 @@
|
||||
<div class="charts-container">
|
||||
<div class="chart-box">
|
||||
<h3>Speech Probability</h3>
|
||||
<canvas id="speech-chart"></canvas>
|
||||
</div>
|
||||
<div class="chart-box">
|
||||
<h3>Energy Level</h3>
|
||||
<canvas id="energy-chart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.charts-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
margin-bottom: 40px;
|
||||
overflow: visible; /* Убеждаемся, что содержимое не обрезается */
|
||||
}
|
||||
|
||||
.chart-box {
|
||||
background-color: rgba(0, 0, 0, 0.2);
|
||||
border-radius: 5px;
|
||||
padding: 10px;
|
||||
height: 250px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,33 @@
|
||||
<h1>== ♩ ♪ ♫ [My Speaker] ♫ ♪ ♩ ==</h1>
|
||||
<form id="login-form">
|
||||
<div class="form-group">
|
||||
<label for="username">Your Name:</label>
|
||||
<input id="username" type="text" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="password">Secret Code:</label>
|
||||
<input id="password" type="password" />
|
||||
</div>
|
||||
<button type="submit">[==Let's Go==]</button>
|
||||
</form>
|
||||
<p id="error" class="error"></p>
|
||||
<p>Default: admin / admin</p>
|
||||
<button id="settings-btn">[Settings]</button>
|
||||
<div id="settings-form">
|
||||
<div class="form-group">
|
||||
<label for="main-color">Main Color:</label>
|
||||
<input id="main-color" type="color" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="border-color">Border Color:</label>
|
||||
<input id="border-color" type="color" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="bg-color">Background Color:</label>
|
||||
<input id="bg-color" type="color" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="backend-url">Backend URL:</label>
|
||||
<input id="backend-url" type="text" />
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,29 @@
|
||||
<p id="status">Status: Listening</p>
|
||||
|
||||
<div class="card-container">
|
||||
<!-- Карточка для речи -->
|
||||
<div class="card">
|
||||
<h3>Речь:</h3>
|
||||
<p>
|
||||
<span id="speech-value">0.00</span> из <span id="speech-max">1.00</span>
|
||||
</p>
|
||||
<p><span id="sp-detect">False</span></p>
|
||||
</div>
|
||||
|
||||
<!-- Карточка для энергии -->
|
||||
<div class="card">
|
||||
<h3>Энергия:</h3>
|
||||
<p><span id="energy-value">0</span> из <span id="energy-max">100</span></p>
|
||||
<p>Шум: <span id="noise-floor">0</span></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-container">
|
||||
<!-- Карточка для системного монитора -->
|
||||
<div class="system-card">
|
||||
<h3>Системный монитор:</h3>
|
||||
<p>Процессор: <span id="cpu">0%</span></p>
|
||||
<p>Память: <span id="mem">0%</span></p>
|
||||
<p>Температура: <span id="temp">0°C</span></p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,654 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Settings</title>
|
||||
<link rel="stylesheet" href="/style.css" />
|
||||
<style>
|
||||
.settings-section {
|
||||
margin-bottom: 10px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
background-color: var(--header-color);
|
||||
padding: 10px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.section-header:hover {
|
||||
background-color: var(--header-hover-color);
|
||||
}
|
||||
|
||||
.section-content {
|
||||
display: none;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.section-content.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.toggle-icon::after {
|
||||
content: "▼";
|
||||
}
|
||||
|
||||
.section-header.collapsed .toggle-icon::after {
|
||||
content: "►";
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h2>Settings</h2>
|
||||
<div class="settings-container">
|
||||
<!-- Вкладка VAD -->
|
||||
<div class="settings-section">
|
||||
<div class="section-header collapsed">
|
||||
<h3>VAD Settings</h3>
|
||||
<span class="toggle-icon"></span>
|
||||
</div>
|
||||
<div class="section-content">
|
||||
<form id="vad-form" class="settings-form">
|
||||
<table>
|
||||
<tr>
|
||||
<td>Mode:</td>
|
||||
<td>
|
||||
<input
|
||||
type="number"
|
||||
id="vad-mode"
|
||||
name="mode"
|
||||
min="0"
|
||||
max="3"
|
||||
step="1"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Sample Rate:</td>
|
||||
<td>
|
||||
<input
|
||||
type="number"
|
||||
id="vad-sample-rate"
|
||||
name="sample_rate"
|
||||
min="8000"
|
||||
max="48000"
|
||||
step="100"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Frame Duration (ms):</td>
|
||||
<td>
|
||||
<input
|
||||
type="number"
|
||||
id="vad-frame-duration-ms"
|
||||
name="frame_duration_ms"
|
||||
min="5"
|
||||
max="50"
|
||||
step="1"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Ring Buffer Size:</td>
|
||||
<td>
|
||||
<input
|
||||
type="number"
|
||||
id="vad-ring-buffer-size"
|
||||
name="ring_buffer_size"
|
||||
min="10"
|
||||
max="100"
|
||||
step="1"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Speech Threshold:</td>
|
||||
<td>
|
||||
<input
|
||||
type="number"
|
||||
id="vad-speech-threshold"
|
||||
name="speech_threshold"
|
||||
min="0"
|
||||
max="1"
|
||||
step="0.1"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Energy Threshold:</td>
|
||||
<td>
|
||||
<input
|
||||
type="number"
|
||||
id="vad-energy-threshold"
|
||||
name="energy_threshold"
|
||||
min="100"
|
||||
max="1000"
|
||||
step="10"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Min Speech Duration (ms):</td>
|
||||
<td>
|
||||
<input
|
||||
type="number"
|
||||
id="vad-min-speech-duration-ms"
|
||||
name="min_speech_duration_ms"
|
||||
min="50"
|
||||
max="500"
|
||||
step="10"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Record Stop Delay (s):</td>
|
||||
<td>
|
||||
<input
|
||||
type="number"
|
||||
id="vad-record-stop-delay-sec"
|
||||
name="record_stop_delay_sec"
|
||||
min="0"
|
||||
max="5"
|
||||
step="0.5"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Prebuffer Duration (s):</td>
|
||||
<td>
|
||||
<input
|
||||
type="number"
|
||||
id="vad-prebuffer-duration-sec"
|
||||
name="prebuffer_duration_sec"
|
||||
min="0"
|
||||
max="5"
|
||||
step="0.5"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Max Record Duration (s):</td>
|
||||
<td>
|
||||
<input
|
||||
type="number"
|
||||
id="vad-max-record-duration-sec"
|
||||
name="max_record_duration_sec"
|
||||
min="1"
|
||||
max="10"
|
||||
step="1"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Target RMS:</td>
|
||||
<td>
|
||||
<input
|
||||
type="number"
|
||||
id="vad-target-rms"
|
||||
name="target_rms"
|
||||
min="1000"
|
||||
max="5000"
|
||||
step="100"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Max Gain:</td>
|
||||
<td>
|
||||
<input
|
||||
type="number"
|
||||
id="vad-max-gain"
|
||||
name="max_gain"
|
||||
min="1"
|
||||
max="10"
|
||||
step="0.5"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Noise Reduction Factor:</td>
|
||||
<td>
|
||||
<input
|
||||
type="number"
|
||||
id="vad-noise-reduction-factor"
|
||||
name="noise_reduction_factor"
|
||||
min="1"
|
||||
max="3"
|
||||
step="0.1"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Noise Update Alpha:</td>
|
||||
<td>
|
||||
<input
|
||||
type="number"
|
||||
id="vad-noise-update-alpha"
|
||||
name="noise_update_alpha"
|
||||
min="0"
|
||||
max="1"
|
||||
step="0.01"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Min RMS Threshold Factor:</td>
|
||||
<td>
|
||||
<input
|
||||
type="number"
|
||||
id="vad-min-rms-threshold-factor"
|
||||
name="min_rms_threshold_factor"
|
||||
min="1"
|
||||
max="5"
|
||||
step="0.1"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Gain Smoothing Alpha:</td>
|
||||
<td>
|
||||
<input
|
||||
type="number"
|
||||
id="vad-gain-smoothing-alpha"
|
||||
name="gain_smoothing_alpha"
|
||||
min="0"
|
||||
max="1"
|
||||
step="0.01"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Adaptive Energy Std Factor:</td>
|
||||
<td>
|
||||
<input
|
||||
type="number"
|
||||
id="vad-adaptive-energy-std-factor"
|
||||
name="adaptive_energy_std_factor"
|
||||
min="1"
|
||||
max="3"
|
||||
step="0.1"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Adaptive Energy:</td>
|
||||
<td>
|
||||
<input
|
||||
type="checkbox"
|
||||
id="vad-adaptive-energy"
|
||||
name="adaptive_energy"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Noise Reduction Enabled:</td>
|
||||
<td>
|
||||
<input
|
||||
type="checkbox"
|
||||
id="vad-noise-reduction-enabled"
|
||||
name="noise_reduction_enabled"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Auto Gain Enabled:</td>
|
||||
<td>
|
||||
<input
|
||||
type="checkbox"
|
||||
id="vad-auto-gain-enabled"
|
||||
name="auto_gain_enabled"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Record on Speech:</td>
|
||||
<td>
|
||||
<input
|
||||
type="checkbox"
|
||||
id="vad-record-on-speech"
|
||||
name="record_on_speech"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<button
|
||||
type="button"
|
||||
id="save-vad"
|
||||
class="save-btn"
|
||||
data-section="vad"
|
||||
>
|
||||
Save VAD
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Вкладка System -->
|
||||
<div class="settings-section">
|
||||
<div class="section-header collapsed">
|
||||
<h3>System Settings</h3>
|
||||
<span class="toggle-icon"></span>
|
||||
</div>
|
||||
<div class="section-content">
|
||||
<form id="system-form" class="settings-form">
|
||||
<table>
|
||||
<tr>
|
||||
<td>History Max Length:</td>
|
||||
<td>
|
||||
<input
|
||||
type="number"
|
||||
id="system-history-maxlen"
|
||||
name="history_maxlen"
|
||||
min="10"
|
||||
max="100"
|
||||
step="1"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Polling Interval (ms):</td>
|
||||
<td>
|
||||
<input
|
||||
type="number"
|
||||
id="system-polling-interval-ms"
|
||||
name="polling_interval_ms"
|
||||
min="500"
|
||||
max="5000"
|
||||
step="100"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Logging Level:</td>
|
||||
<td>
|
||||
<select id="system-logging-level" name="logging_level">
|
||||
<option>DEBUG</option>
|
||||
<option>INFO</option>
|
||||
<option>WARNING</option>
|
||||
<option>ERROR</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Log File:</td>
|
||||
<td>
|
||||
<input type="text" id="system-log-file" name="log_file" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Log Save File:</td>
|
||||
<td>
|
||||
<input
|
||||
type="checkbox"
|
||||
id="system-log-save-file"
|
||||
name="log_save_file"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<button
|
||||
type="button"
|
||||
id="save-system"
|
||||
class="save-btn"
|
||||
data-section="system"
|
||||
>
|
||||
Save System
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Вкладка STT -->
|
||||
<div class="settings-section">
|
||||
<div class="section-header collapsed">
|
||||
<h3>STT Settings</h3>
|
||||
<span class="toggle-icon"></span>
|
||||
</div>
|
||||
<div class="section-content">
|
||||
<form id="stt-form" class="settings-form">
|
||||
<table>
|
||||
<tr>
|
||||
<td>Model Path:</td>
|
||||
<td>
|
||||
<input type="text" id="stt-model-path" name="model_path" />
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<button
|
||||
type="button"
|
||||
id="save-stt"
|
||||
class="save-btn"
|
||||
data-section="stt"
|
||||
>
|
||||
Save STT
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Вкладка Commands -->
|
||||
<div class="settings-section">
|
||||
<div class="section-header collapsed">
|
||||
<h3>Commands Settings</h3>
|
||||
<span class="toggle-icon"></span>
|
||||
</div>
|
||||
<div class="section-content">
|
||||
<form id="commands-form" class="settings-form">
|
||||
<table>
|
||||
<tr>
|
||||
<td>Commands File:</td>
|
||||
<td>
|
||||
<input
|
||||
type="text"
|
||||
id="commands-commands-file"
|
||||
name="commands_file"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<button
|
||||
type="button"
|
||||
id="save-commands"
|
||||
class="save-btn"
|
||||
data-section="commands"
|
||||
>
|
||||
Save Commands
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Вкладка Auth -->
|
||||
<div class="settings-section">
|
||||
<div class="section-header collapsed">
|
||||
<h3>Auth Settings</h3>
|
||||
<span class="toggle-icon"></span>
|
||||
</div>
|
||||
<div class="section-content">
|
||||
<form id="auth-form" class="settings-form">
|
||||
<table>
|
||||
<tr>
|
||||
<td>Username:</td>
|
||||
<td>
|
||||
<input type="text" id="auth-username" name="username" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Password:</td>
|
||||
<td>
|
||||
<input type="password" id="auth-password" name="password" />
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<button
|
||||
type="button"
|
||||
id="save-auth"
|
||||
class="save-btn"
|
||||
data-section="auth"
|
||||
>
|
||||
Save Auth
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Настройки TTS -->
|
||||
<div class="settings-section">
|
||||
<div class="section-header collapsed">
|
||||
<h3>TTS Settings</h3>
|
||||
<span class="toggle-icon"></span>
|
||||
</div>
|
||||
<div class="section-content">
|
||||
<form id="tts-form">
|
||||
<div class="form-group">
|
||||
<label for="host">Host:</label>
|
||||
<input type="text" id="host" name="host" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="port">Port:</label>
|
||||
<input type="number" id="port" name="port" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="timeout">Timeout (seconds):</label>
|
||||
<input type="number" id="timeout" name="timeout" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="pause_duration">Pause Duration (seconds):</label>
|
||||
<input
|
||||
type="number"
|
||||
id="pause_duration"
|
||||
name="pause_duration"
|
||||
step="0.1"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="post_stop_wait">Post Stop Wait (seconds):</label>
|
||||
<input
|
||||
type="number"
|
||||
id="post_stop_wait"
|
||||
name="post_stop_wait"
|
||||
step="0.1"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="voice">Voice:</label>
|
||||
<input type="text" id="voice" name="voice" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="save_TTS_files">Save TTS Files:</label>
|
||||
<input
|
||||
type="checkbox"
|
||||
id="save_TTS_files"
|
||||
name="save_TTS_files"
|
||||
/>
|
||||
</div>
|
||||
<button type="button" class="save-btn" data-section="tts">
|
||||
Save TTS Settings
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Настройки аудио -->
|
||||
<div class="settings-section">
|
||||
<div class="section-header collapsed">
|
||||
<h3>Audio Settings</h3>
|
||||
<span class="toggle-icon"></span>
|
||||
</div>
|
||||
<div class="section-content">
|
||||
<form id="audio-settings-form">
|
||||
<div class="form-group">
|
||||
<label for="playback_device">Playback Device:</label>
|
||||
<input
|
||||
type="number"
|
||||
id="playback_device"
|
||||
name="playback_device"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="capture_device">Capture Device:</label>
|
||||
<input type="number" id="capture_device" name="capture_device" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="playback_volume">Playback Volume:</label>
|
||||
<input
|
||||
type="range"
|
||||
id="playback_volume"
|
||||
name="playback_volume"
|
||||
min="0"
|
||||
max="100"
|
||||
step="10"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="capture_volume">Capture Volume:</label>
|
||||
<input
|
||||
type="range"
|
||||
id="capture_volume"
|
||||
name="capture_volume"
|
||||
min="0"
|
||||
max="100"
|
||||
step="10"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="save-btn"
|
||||
data-section="audio_settings"
|
||||
>
|
||||
Save Audio Settings
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Кнопка переинициализации -->
|
||||
<div class="settings-section">
|
||||
<div class="section-header collapsed">
|
||||
<h3>System Control</h3>
|
||||
<span class="toggle-icon"></span>
|
||||
</div>
|
||||
<div class="section-content">
|
||||
<button type="button" id="restart-btn">Restart VAD</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
// Получаем все заголовки секций
|
||||
const sectionHeaders = document.querySelectorAll(".section-header");
|
||||
|
||||
// Добавляем обработчик клика для каждого заголовка
|
||||
sectionHeaders.forEach((header) => {
|
||||
header.addEventListener("click", function () {
|
||||
// Получаем содержимое секции
|
||||
const content = this.nextElementSibling;
|
||||
|
||||
// Если секция уже активна, просто сворачиваем её
|
||||
if (!this.classList.contains("collapsed")) {
|
||||
this.classList.add("collapsed");
|
||||
content.classList.remove("active");
|
||||
return;
|
||||
}
|
||||
|
||||
// Сворачиваем все секции
|
||||
document.querySelectorAll(".section-header").forEach((h) => {
|
||||
h.classList.add("collapsed");
|
||||
h.nextElementSibling.classList.remove("active");
|
||||
});
|
||||
|
||||
// Разворачиваем текущую секцию
|
||||
this.classList.remove("collapsed");
|
||||
content.classList.add("active");
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,244 @@
|
||||
:root {
|
||||
--main-color: #0f0; /* Дефолтный зелёный, меняется из localStorage */
|
||||
--bg-color: #000;
|
||||
--error-color: #f00;
|
||||
--border-color: #0f0;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: "Press Start 2P", cursive;
|
||||
background: var(--bg-color);
|
||||
color: var(--main-color);
|
||||
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
font-size: 12px; /* Пиксельный размер */
|
||||
}
|
||||
|
||||
/* Стили для формы логина */
|
||||
.form-group {
|
||||
margin-bottom: 15px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
margin-bottom: 8px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.form-group input {
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
#login-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
#settings-form {
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
margin: 15px auto 0;
|
||||
}
|
||||
|
||||
h1 {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
button {
|
||||
margin-top: 15px;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
/* Центрирование текстовых элементов */
|
||||
#error,
|
||||
p {
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
margin: 10px auto;
|
||||
}
|
||||
|
||||
#settings-btn {
|
||||
display: block;
|
||||
margin: 15px auto;
|
||||
}
|
||||
|
||||
/* Стили для элементов истории в dashboard */
|
||||
.history-item {
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
margin: 5px 0;
|
||||
padding: 5px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
font-family: monospace;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
#app {
|
||||
width: 100%;
|
||||
max-width: 1200px;
|
||||
border: 2px solid var(--border-color);
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 0 50px var(--main-color);
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
header,
|
||||
nav {
|
||||
border: 1px solid var(--border-color);
|
||||
box-shadow: 0 0 10px var(--main-color);
|
||||
border-radius: 10px;
|
||||
padding: 5px;
|
||||
margin-bottom: 10px;
|
||||
justify-content: center; /* ← центрирует по горизонтали */
|
||||
gap: 10px; /* отступ между кнопками (опционально) */
|
||||
}
|
||||
footer {
|
||||
border: 1px solid var(--border-color);
|
||||
box-shadow: 0 0 10px var(--main-color);
|
||||
border-radius: 10px;
|
||||
padding: 5px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
footer p {
|
||||
font-size: 28px;
|
||||
}
|
||||
.main-container {
|
||||
display: flex;
|
||||
flex-direction: row; /* Десктоп: горизонтально */
|
||||
}
|
||||
|
||||
.main-params {
|
||||
border-radius: 10px;
|
||||
width: 40%;
|
||||
border: 1px solid var(--border-color);
|
||||
box-shadow: 0 0 10px var(--main-color);
|
||||
border-radius: 10px;
|
||||
margin-right: 10px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.content {
|
||||
width: 60%;
|
||||
border: 1px solid var(--border-color);
|
||||
box-shadow: 0 0 10px var(--main-color);
|
||||
border-radius: 10px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
background: var(--bg-color);
|
||||
border: 1px solid var(--border-color);
|
||||
height: 10px;
|
||||
margin: 5px 0;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
background: var(--main-color);
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
button {
|
||||
background: var(--bg-color);
|
||||
color: var(--main-color);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
padding: 8px 16px;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background: var(--main-color);
|
||||
color: var(--bg-color);
|
||||
box-shadow: 0 0 0px var(--main-color);
|
||||
transition: box-shadow 0.3s ease;
|
||||
}
|
||||
|
||||
input,
|
||||
select {
|
||||
background: var(--bg-color);
|
||||
color: var(--main-color);
|
||||
border: 1px solid var(--border-color);
|
||||
font-family: inherit;
|
||||
}
|
||||
input[type="color"] {
|
||||
width: 250px; /* Увеличьте ширину */
|
||||
height: 25px; /* Увеличьте высоту */
|
||||
padding: 2px; /* Добавьте внутренние отступы */
|
||||
}
|
||||
.error {
|
||||
color: var(--error-color);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.main-container {
|
||||
flex-direction: column; /* Мобильные: вертикально */
|
||||
}
|
||||
.main-params,
|
||||
.content {
|
||||
width: 100%;
|
||||
}
|
||||
button,
|
||||
input,
|
||||
select {
|
||||
font-size: 14px; /* Увеличено для мобильных */
|
||||
}
|
||||
}
|
||||
|
||||
/* Пиксельные рамки */
|
||||
.pixel-border {
|
||||
border: 2px solid var(--border-color);
|
||||
box-shadow: inset 0 0 0 1px var(--bg-color), 0 0 0 1px var(--border-color);
|
||||
}
|
||||
|
||||
/* Стили для карточек */
|
||||
.card-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 15px;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.card,
|
||||
.system-card {
|
||||
border: 1px solid var(--border-color);
|
||||
box-shadow: 0 0 10px var(--main-color);
|
||||
border-radius: 10px;
|
||||
padding: 10px;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.card h3,
|
||||
.system-card h3 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 10px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* Адаптивность для мобильных устройств */
|
||||
@media (max-width: 768px) {
|
||||
.card-container {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user