console.warn("[TTS Ошибка]: Воспроизведение заблокировано политикой браузера. Нужен клик.");
resetSpeechState();
});
};
}
============================================================================
БЛОК 3: УПРАВЛЕНИЕ ЖИЗНЕННЫМ ЦИКЛОМ ПРИЛОЖЕНИЯ (МЕНЕДЖЕР ЗАПУСКА STARTAPP)
============================================================================
/
* Главная асинхронная функция инициализации Евы.
* Выполняет каскадную загрузку локальных баз знаний из папки data/и подготавливает UI.
*/
async function startApp() {
try {
console.log(«[Инициализация]: Запуск загрузки баз данных из папки data/…»);
Асинхронно запрашиваем чтение всех файлов через внешний модуль Database из папки data/
const allData = await Database.loadAll();
Проверяем валидность структуры полученного объекта и наличие основной базы
if (allData && allData.base) {
КРИТИЧЕСКИЙ ШАГ: Дублируем данные в глобальное окно для доступности из любой точки системы
window.allData = allData;
Передаем загруженные массивы в аналитический модуль нейросети (ИИ-мозг)
AI.setData(allData);
ПРИНУДИТЕЛЬНОЕ ОБНОВЛЕНИЕ ЛИЧНОСТИ ИЗ SETTINGS.JSON
if (allData.settings) {
if (allData.settings.voice && typeof EvaConfig !== 'undefined') {
Заменяем сухие захардкоженные параметры голоса в config.js на серверные ползунки
EvaConfig.voice.pitch = allData.settings.voice.pitch;
EvaConfig.voice.rate = allData.settings.voice.rate;
EvaConfig.voice.volume = allData.settings.voice.volume;
}
СИНХРОНИЗАЦИЯ ВСЕХ 8 КИБЕР-ЭМОЦИЙ И ШЕПОТА ИЗ АДМИНКИ НА ЛЕТУ
if (allData.settings.emotions && typeof EvaConfig !== 'undefined') {
for (let emoKey in allData.settings.emotions) {
if (EvaConfig.emotions[emoKey]) {
EvaConfig.emotions[emoKey].pitch = allData.settings.emotions[emoKey].pitch;
EvaConfig.emotions[emoKey].rate = allData.settings.emotions[emoKey].rate;
EvaConfig.emotions[emoKey].volume = allData.settings.emotions[emoKey].volume;
EvaConfig.emotions[emoKey].prefix = allData.settings.emotions[emoKey].prefix;
} else {
Если эмоции не было в базовом коде (например, страх или шёпот), создаем её на лету
EvaConfig.emotions[emoKey] = allData.settings.emotions[emoKey];
}
}
}
}
Обновляем счетчик записей на графическом интерфейсе пользователя
Visual.updateLog(`READY (${allData.base.length})`);
Переключаем визуальное состояние аватара в режим дыхания/ожидания
Visual.setState('wait');
Если кнопка инициализации найдена в DOM, активируем её неоновую подсветку
if (window.mainBtn) window.mainBtn.className = 'btn-start-neon';
console.log(«[Инициализация]: Успешно завершена. Ева готова к работе.»);
}
} catch (e) {
Логируем критическую ошибку (например, повреждение JSON файла или отсутствие файлов)
console.error(«[Критическая ошибка инициализации]:», e);
Выводим тревожный статус на главный экран приложения
Visual.updateLog(«ERROR»);
}
}
============================================================================
БЛОК 4: ИНТЕГРАЦИЯ И ПАРСИНГ РЕЗУЛЬТАТОВ ИЗ СЕТИ ИНТЕРНЕТ (WIKIPEDIA)
============================================================================
/
* Обработчик успешного получения данных из Wikipedia.
* Выполняет вывод текста на экран, чанк-анимацию бегущей строки и запускает озвучку.
* @param {string} responseText - Текст статьи, возвращенный парсером.
* @param {string|null} incomingQuestion - Вопрос, на который был осуществлен поиск.
*/
function onWikiResultReceived(responseText, incomingQuestion = null) {
Активируем системные блокировки, защищающие от прерывания речи новыми звуками микрофонаwindow.isSpeakingWiki = true; window.isWaitingDecision = true;
// Вычисляем итоговый вопрос: берем переданный или вытягиваем из системного буфера ожидания
const finalQuestion = incomingQuestion || window.pendingQuestion;
console.log("[Wiki Модуль]: Запуск обработки статьи. Вопрос:", finalQuestion);
// 💥 ИНТЕГРАЦИЯ СЕТЕВЫХ НАСТРОЕК: Извлекаем оранжевые подсказки из файла data/settings.json
const currentDecisionHints = window.allData?.settings?.hints?.decision_hints || "ПОИЩИ / ВИКИ / ОТВЕТ... / ОТМЕНА";
// Принудительно выводим актуальные подсказки на верхний статус-бар экрана Евы
if (typeof Visual !== 'undefined' && typeof Visual.updateHints === 'function') {
Visual.updateHints(true, currentDecisionHints);
}
// Добавляем полноценный блок ответа Евы в графическое окно диалога чата
if (typeof Visual !== 'undefined' && typeof Visual.addMsg === 'function') {
Visual.addMsg(responseText, 'eva-msg');
}
// РЕАЛИЗАЦИЯ БЕГУЩЕЙ СТРОКИ: Дробим текст статьи на массив отдельных слов по пробелам
const words = responseText.split(" ");
let currentWordIndex = 0;
// Создаем цикличный интервал для симуляции синхронного чтения текста Евой на экране
const textTickerInterval = setInterval(() => {
// Условие остановки: массив слов исчерпан или внешняя логика принудительно сбросила флаг блокировки
// 💥 АППАРАТНАЯ ЗАЩИТА: Останавливаем интервал только если слова закончились И синтезатор полностью замолчал
if (currentWordIndex >= words.length || (!window.isSpeakingWiki && !window.speechSynthesis.speaking)) {
clearInterval(textTickerInterval);
// Сбрасываем текст живой плашки в исходное состояние тишины
if (typeof Visual !== 'undefined' && typeof Visual.setLiveText === 'function') {
Visual.setLiveText("");
}
return;
}
// Вырезаем порцию из 4-х слов для плавного отображения на экране
const chunk = words.slice(currentWordIndex, currentWordIndex + 4);
const tickerText = chunk.join(" ");
// Отправляем сформированную порцию слов в текстовое поле живого вывода (синяя плашка статус-бара)
if (typeof Visual !== 'undefined' && typeof Visual.setLiveText === 'function') {
Visual.setLiveText(tickerText);
}
// Сдвигаем курсор индекса вперед на размер обработанной порции
currentWordIndex += 4;
}, 1500); // Оптимальный интервал чтения порции в полторы секунды
// Передаем полную статью на озвучивание в TTS-движок голосовой системы speak(responseText);
// Валидация данных для запуска фонового автоматического сохранения в энциклопедическую базу wiki_base.json
if (finalQuestion && finalQuestion.length > 0 && responseText.length > 10) {
console.log("[Wiki Модуль]: Условия выполнены. Отправка статьи на автосохранение...");
// Вызываем функцию фонового экспорта данных в базу знаний
if (typeof autoSaveToWiki === 'function') {
autoSaveToWiki(finalQuestion, responseText);
} else {
console.warn("[Wiki Модуль Предупреждение]: Функция autoSaveToWiki не обнаружена в каскаде скриптов.");
}
} else {
// Если статья пустая или некорректная, превентивно снимаем блокировки и усыпляем микрофон
window.isSpeakingWiki = false;
window.isWaitingDecision = false;
if (typeof Visual !== 'undefined' && typeof Visual.updateHints === 'function') {
Visual.updateHints(false);
}
}
}
============================================================================
БЛОК 5: ИНТЕРФЕЙСНЫЙ ОБРАБОТЧИК ГЛАВНОЙ УПРАВЛЯЮЩЕЙ КНОПКИ
============================================================================
if (window.mainBtn) {
/
* Обработка нажатия на центральную неоновую кнопку управления Евой.
* Реализует два режима: Вход (Активация микрофона) и Выход (Сортировка и сохранение баз).
*/
window.mainBtn.onclick = async () ⇒ {
РЕЖИМ А: Приложение спало (window.isLive === false). Запускаем сессию.
if (!window.isLive) {
window.isLive = true; Переводим систему в статус «В эфире»
window.freeTalk = false; По умолчанию включаем режим обращения по имени
Меняем текстовое содержание и стиль кнопки на стоп-режим (красный неон)
window.mainBtn.innerText = «ВЫЙТИ И СОХРАНИТЬ»;
window.mainBtn.className = 'btn-stop-neon';
Инициализируем аудио-контекст захвата голоса и запускаем анализ частот микрофона
if (typeof initVoiceSystem === 'function') initVoiceSystem();
if (typeof startMicLevel === 'function') await startMicLevel();
Извлекаем случайное приветствие Евы из конфигурационного файла проекта
const hello = EvaConfig.getRandomGreeting();
speak(hello);
Visual.addMsg(hello, 'eva-msg');
Запускаем таймер слежения за активностью пользователя
resetIdleTimer();
} else {
РЕЖИМ Б: Приложение активно. Пользователь завершает сессию. Сортируем память.
window.isLive = false;
window.mainBtn.innerText = «СОРТИРОВКА…»;
💥 МГНОВЕННЫЙ ЗАПУСК АВТОМАТИЧЕСКОГО СЕРВЕРНОГО БЭКАПА 5 БАЗ ДАННЫХ
try {
console.log(«[Бэкап Модуль]: Инициирую создание точки восстановления перед выходом…»);
const backupResponse = await fetch('admin.php?api=create_backup');
if (backupResponse.ok) {
const backupData = await backupResponse.json();
if (backupData.status === 'ok') {
console.log(«%c[Бэкап Модуль]: Базы успешно заархивированы. Метка: » + backupData.timestamp, «color: green; font-weight: bold;»);
}
}
} catch (backupErr) {
console.warn(«[Бэкап Модуль Предупреждение]: Не удалось создать резервную копию:», backupErr);
}
Сбрасываем фоновые таймеры и деактивируем медиа-потоки записи звука
if (window.idleTimer) clearTimeout(window.idleTimer);
if (window.rec) try { window.rec.stop(); } catch(e) {}
ИНТЕЛЛЕКТУАЛЬНАЯ СОРТИРОВКА: Разделяем временный буфер памяти AI.memory
const wikiData = AI.memory.filter(item ⇒ item.isWiki === true);
const baseData = AI.memory.filter(item ⇒ item.isWiki !== true);
Очищаем временные маркеры 'isWiki', чтобы они не засоряли итоговые файлы базы данных
wikiData.forEach(item ⇒ delete item.isWiki);
try {
console.log(«[Сохранение]: Запуск параллельной отправки данных на сервер в папку data/…»);
Асинхронно отправляем массив базовых знаний на обработку скрипту save.php (в папку data/)
const saveBase = fetch('save.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(baseData)
});
Одновременно отправляем энциклопедические данные скрипту save_wiki.php (в папку data/)
const saveWiki = fetch('save_wiki.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(wikiData)
});
Синхронизируем завершение обоих сетевых запросов
const [res1, res2] = await Promise.all([saveBase, saveWiki]);
Проверяем успешность ответов сервера (HTTP статус 200)
if (res1.ok && res2.ok) {
window.mainBtn.innerText = «СОХРАНЕНО!»;
Выполняем мягкую перезагрузку страницы через 1 секунду для обновления кэша данных
setTimeout(() ⇒ location.reload(), 1000); } else {
throw new Error("Сервер ответил отказом при записи файлов.");
}
} catch (err) {
console.error("[Ошибка сохранения баз данных в папку data/]:", err);
alert("Не удалось сохранить данные. Проверьте права доступа (CHMOD 777) к папке data.");
// Возвращаем интерфейс в рабочее состояние для предотвращения потери сессии
window.mainBtn.innerText = "ОШИБКА";
window.isLive = true;
}
}
};
}
============================================================================
БЛОК 6: ГЛАВНЫЙ АСИНХРОННЫЙ КОНВЕЙЕР ОБРАБОТКИ РЕЧИ И КОМАНД
============================================================================
/
* Объединенный монолитный обработчик распознанной речи.
* Проводит входную строку через каскад фильтров: режимы, перехваты решений, инструменты, лингвистический анализ.
* @param {string} text - Распознанная строка текста от голосового движка.
*/
async function handleSpeech(text) {
Предохранитель А: Если Ева говорит в данный момент, полностью игнорируем входящий сигнал
if (window.isEvaSpeaking) return;
Предохранитель Б: Отсекаем пустые срабатывания микрофона или шумы
if (!text || text.trim().length < 1) return;
Сбрасываем внутренний таймер инициативы, фиксируя активность пользователя
if (typeof resetIdleTimer === 'function') resetIdleTimer();
ПРИНУДИТЕЛЬНЫЙ ВЫВОД: Отображаем фразу пользователя в чате для исключения эффекта зависания
if (typeof Visual !== 'undefined' && typeof Visual.addMsg === 'function') {
Visual.addMsg(text, 'user-msg');
}
Очищаем текстовую плашку предварительного (живого) распознавания речевого потока
if (typeof Visual !== 'undefined' && typeof Visual.setLiveText === 'function') {
Visual.setLiveText(«»);
}
Приводим строку к нижнему регистру и очищаем от концевых пробелов для точного сравнения
const cleanRaw = text.toLowerCase().trim();
console.log(«[Речевой конвейер]: Начинаю анализ фразы:», cleanRaw);
————————————————————————
ШАГ 6.0: ПЕРЕКЛЮЧЕНИЕ РЕЖИМОВ ОБЩЕНИЯ (Мгновенная реакция)
————————————————————————
Переход в режим свободной беседы (без постоянного повторения имени «Ева»)
if (cleanRaw.includes(«давай без имени») || cleanRaw.includes(«давай поболтаем») || cleanRaw.includes(«давай поговорим»)) {
window.freeTalk = true;
clearTimeout(window.idleTimer);
if (Visual.container) Visual.container.classList.add('free-talk-mode');
if (window.mainBtn) window.mainBtn.className = 'btn-free-neon';
const resp = Tools.humanize(window.allData?.settings?.phrases?.freetalk_on || «хорошо, давай поболтаем без имен»);
speak(resp);
Visual.addMsg(resp, 'eva-msg');
resetIdleTimer();
return; Прерываем конвейер
}
Возврат в строгий режим (Ева реагирует только тогда, когда зовут по имени)
if (cleanRaw.includes(«хватит болтать») || cleanRaw.includes(«верни имя»)) {
window.freeTalk = false;
if (Visual.container) Visual.container.classList.remove('free-talk-mode');
if (window.mainBtn) window.mainBtn.className = 'btn-stop-neon';
const resp = Tools.humanize(window.allData?.settings?.phrases?.freetalk_off || «поняла, теперь зови меня по имени»);
speak(resp);
Visual.addMsg(resp, 'eva-msg');
resetIdleTimer();
return; Прерываем конвейер
}
————————————————————————
ШАГ 6.1: ПЕРЕХВАТ СОСТОЯНИЯ ОЖИДАНИЯ ВЫБОРА (window.isWaitingDecision)
————————————————————————
if (window.isWaitingDecision) {
Перехват 1.1: Полный сброс операции и закрытие модальных окон обучения
if (cleanRaw.includes(«отмена») || cleanRaw.includes(«проехали») || cleanRaw.includes(«не надо») || cleanRaw === 'выйти' || cleanRaw === 'закрой') {
window.isWaitingDecision = false;
window.pendingQuestion = «»;
window.isSpeakingWiki = false;
window.isEvaSpeaking = false;
Прячем оранжевые текстовые подсказки управления
if (typeof Visual !== 'undefined' && typeof Visual.updateHints === 'function') Visual.updateHints(false);
Закрываем любые диалоговые окна обучения через поиск CSS селекторов в DOM
const windows = document.querySelectorAll('.learn-window, .learn-form, #learnModal, [id*=«learn»], [class*=«learn»]');
windows.forEach(win ⇒ {
win.style.display = 'none';
win.classList.remove('active', 'open', 'show');
});
Возвращаем аватар в режим ожидания и озвучиваем отмену
if (typeof Visual !== 'undefined' && typeof Visual.setState === 'function') Visual.setState('wait');
Динамическое считывание фразы отмены из настроек settings.json
const resp = Tools.humanize(window.allData?.settings?.phrases?.cancel || «хорошо, отменяю» || «ладно, забыли» || «Oкей, проехали»);
speak(resp);
Visual.addMsg(resp, 'eva-msg');
return;
}
Перехват 1.2: Команда принудительного асинхронного поиска в Wikipedia
if (cleanRaw.includes(«поищи») || cleanRaw.includes(«вики») || cleanRaw.includes(«что такое») || cleanRaw.includes(«кто такой») || cleanRaw.includes(«интернет»)) {
if (window.pendingQuestion && window.pendingQuestion.length > 0) {
window.isWaitingDecision = false;
Сбрасываем интерфейсные маркеры и переводим аватар в режим размышления
if (typeof Visual !== 'undefined' && typeof Visual.updateHints === 'function') Visual.updateHints(false);
if (typeof Visual !== 'undefined' && typeof Visual.setState === 'function') Visual.setState('think');
if (typeof Visual !== 'undefined' && typeof Visual.setLiveText === 'function') {
Visual.setLiveText(window.allData?.settings?.phrases?.wiki_searching || «Связываюсь с Википедией…»);
}
try {
let wikiAnswer = null;
Каскадный поиск доступного метода интеграции с API Википедии
💥 УМНАЯ МОДУЛЬНАЯ МАРШРУТИЗАЦИЯ: Передаем управление модулю инструментов
Tools.searchWiki сам заглянет в settings.json и выберет правильную папку навыка!
if (typeof Tools !== 'undefined' && typeof Tools.searchWiki === 'function') {
wikiAnswer = await Tools.searchWiki(window.pendingQuestion);
} else {
console.error(«[Речевой конвейер]: Критическая ошибка: Модуль Tools.searchWiki не обнаружен!»);
}
Если статья была успешно найдена и запущена внутри onWikiResultReceived (Блок 4)
if (window.isSpeakingWiki === true || (wikiAnswer && wikiAnswer.trim().length > 5)) {
console.log(«[Речевой конвейер]: Вики-запрос успешно обработан в Блоке 4. Подавление дублей сработало.»);
Дополнительная подстраховка: если Блок 4 почему-то не вывел текст сам
if (window.isSpeakingWiki === false && wikiAnswer && wikiAnswer.trim().length > 5) {
speak(wikiAnswer);
Visual.addMsg(wikiAnswer, 'eva-msg');
}
} else {
Сюда система придет только если Википедия действительно сбойнула (ошибка 404 или пусто)
window.isLearning = true;
if (typeof Visual !== 'undefined' && typeof Visual.updateHints === 'function') {
Visual.updateHints(true, «НЕ ЗАПОМИНАЙ / НЕ СОХРАНЯЙ»);
}
const resp = window.allData?.settings?.phrases?.wiki_not_found || «В интернете не нашла. Научишь меня сам?»;
speak(resp);
Visual.addMsg(resp, 'eva-msg');
}
} catch (e) {
ВОССТАНОВЛЕННЫЙ БЛОК ОБРАБОТКИ ОШИБОК: Фиксируем сбой и разблокируем UI
console.error(«[Ошибка вызова Wiki API]:», e);
window.isSpeakingWiki = false;
window.isWaitingDecision = false;
}
Возвращаем аватар в режим прослушивания
if (typeof Visual !== 'undefined' && typeof Visual.setState === 'function') Visual.setState('listen');
} else {
speak(«Что именно мне поискать в интернете?»);
}
return;
}
Перехват 1.3: Команда ручного обучения текущему вопросу фразами «ответ [текст]»
if (cleanRaw.includes(«правильный ответ») || cleanRaw.includes(«ответ») || cleanRaw.includes(«запомни»)) {
Вырезаем системные триггеры, оставляя чистый текст ответа пользователя
let userAnswer = cleanRaw.replace(/правильный ответ|ответ|запомни/g, «»).trim();
if (userAnswer.length > 1) {
Записываем пару Вопрос→Ответ в структуру оперативной памяти ИИ
AI.learn(window.pendingQuestion, userAnswer);
window.isWaitingDecision = false;
if (typeof Visual !== 'undefined' && typeof Visual.updateHints === 'function') {
Visual.updateHints(false);
}
Динамическое считывание фразы успешного обучения из настроек settings.json
const resp = Tools.humanize(window.allData?.settings?.phrases?.learned || «запомнила, теперь я буду это знать»);
speak(resp);
if (typeof Visual !== 'undefined' && typeof Visual.addMsg === 'function') {
Visual.addMsg(resp, 'eva-msg');
}
} else {
Если пользователь не назвал сам текст ответа, Ева запрашивает его
speak(«Я слушаю, какой правильный ответ?»);
}
return; Прерываем речевой конвейер на этом шаге
}
}
————————————————————————
ШАГ 6.2: РЕЖИМ ПРЯМОГО ПОШАГОВОГО ИНТЕРАКТИВНОГО ОБУЧЕНИЯ (window.isLearning)
————————————————————————
if (window.isLearning) {
Проверяем команду выхода из жесткого пошагового режима обучения фраз
if (cleanRaw.includes(«не запоминай») || cleanRaw.includes(«отмена»)) {
window.isLearning = false;
if (typeof Visual !== 'undefined' && typeof Visual.updateHints === 'function') Visual.updateHints(false);
Динамическое считывание фразы сброса проехали (forgot) из админки личности Евы
speak(Tools.humanize(window.allData?.settings?.phrases?.forgot || «хорошо, проехали»));
return;
}
Обучаем систему: связываем прошлый зафиксированный вопрос window.lastQ с текущей репликой ответа
AI.learn(window.lastQ, text);
window.isLearning = false;
if (typeof Visual !== 'undefined' && typeof Visual.updateHints === 'function') Visual.updateHints(false);
Динамическое считывание короткой фразы успеха (learned) для пошагового обучения
speak(Tools.humanize(window.allData?.settings?.phrases?.learned || «запомнила»));
return;
}
————————————————————————
ШАГ 6.3: ИНСТРУМЕНТЫ АВТОМАТИЗАЦИИ (МАТЕМАТИКА, ВРЕМЯ, СЛОВАРЬ ХАРАКТЕРА)
————————————————————————
Проверка 3.1: Поиск совпадений в текстовой матрице характера личности Евы из tools.js
const personalResp = Tools.processPersonality(cleanRaw);
if (personalResp && !window.isWaitingDecision) {
speak(personalResp);
Visual.addMsg(personalResp, 'eva-msg');
return;
}
Проверка 3.2: Математический калькулятор (вычисление регулярными выражениями фраз типа «пять плюс два»)
const mathResult = Tools.calculate(cleanRaw);
if (mathResult) {
speak(mathResult);
Visual.addMsg(mathResult, 'eva-msg');
return;
}
Проверка 3.3: Запрос текущего системного времени или даты с веб-сервера
if (cleanRaw.includes(«время») || cleanRaw.includes(«час») || cleanRaw.includes(«число»)) {
const timeStr = await Tools.getServerDateTime();
speak(timeStr);
Visual.addMsg(timeStr, 'eva-msg');
return;
}
————————————————————————
ШАГ 6.4: ПРОВЕРКА НАЛИЧИЯ ИМЕНИ (АКТИВАЦИОННЫЙ ФИЛЬТР ВХОДНОГО ПОТОКА)
————————————————————————
Система реагирует, если найдено имя «Ева», «Еву» ИЛИ активирован режим freeTalk (без имени)
const hasName = cleanRaw.includes(«ева») || cleanRaw.includes(«еву») || window.freeTalk;
if (!hasName) {
Если имя не произнесено, просто пишем текст в плашку, не запуская глубокий анализ логики баз данных
Visual.setLiveText(text);
return;
}
Вычленяем чистый запрос для передачи в ассоциативный ИИ-мозг (ai.js)
const cleanForAI = text.replace(/ева|еву/gi, ).trim();
if (cleanForAI.length < 1) return; Выходим, если пользователь назвал только имя Евы без вопроса
Рендерим статус размышлений аватара на верхней панели
Visual.setState('think');
————————————————————————
ШАГ 6.5: СЕМАНТИЧЕСКИЙ АССОЦИАТИВНЫЙ ПОИСК В БАЗАХ ЗНАНИЙ (AI.THINK)
————————————————————————
let aiResult = (typeof AI !== 'undefined' && typeof AI.think === 'function') ? AI.think(cleanForAI) : null;
Вторичный каскад: Если в основной базе пусто — проверяем энциклопедическую Wiki-базу (wiki_base.json)
if ((!aiResult || !aiResult.found) && window.allData?.wiki) { // Пробегаем по массиву вопросов wiki-базы, приводя строки к нижнему регистру
const wikiMatch = window.allData.wiki.find(item =>
item.questions && item.questions.some(q => q.toLowerCase().trim() === cleanForAI.toLowerCase())
);
if (wikiMatch) {
// ЗАЩИТНЫЙ МЕХАНИЗМ: Формируем искусственный массив answers, если структура Wiki-объекта отличается
const fallbackAnswers = wikiMatch.answers || [wikiMatch.text || wikiMatch.description || "Вот информация по вашему запросу."];
aiResult = {
found: { answers: fallbackAnswers },
clean: cleanForAI
};
}
}
// ВЫВОД РЕЗУЛЬТАТОВ АНАЛИЗА БАЗ ДАННЫХ
if (aiResult && aiResult.found && aiResult.found.answers && aiResult.found.answers.length > 0) {
// Реакция А: Ответ НАЙДЕН. Выбираем случайный вариант из массива ответов для живости диалога
let response = aiResult.found.answers[Math.floor(Math.random() * aiResult.found.answers.length)];
// Прогоняем сухой ответ через фильтр лингвистического очеловечивания и обращения по имени
if (typeof Tools !== 'undefined' && typeof Tools.humanize === 'function') {
response = Tools.humanize(response);
}
// Переводим аватар в статус генерации реплики
Visual.setState('speak');
speak(response);
Visual.addMsg(response, 'eva-msg');
// Фиксируем контекст текущей ноды для обеспечения поддержки древовидных sub-веток
if (typeof AI !== 'undefined') AI.currentContext = aiResult.found;
} else {
// ============================================================================
// 💥 РЕАКЦИЯ Б: ОТВЕТ АБСОЛЮТНО НЕ НАЙДЕН В ПАМЯТИ ИИ ЕВЫ
// Активируем шлюзы интерактивного диалога обучения и ожидания решения
// ============================================================================
window.isWaitingDecision = true;
window.pendingQuestion = cleanForAI;
// Запоминаем текущий чистый вопрос для режима прямого пошагового обучения
window.lastQ = cleanForAI;
// Считываем строку оранжевых подсказок выбора действий из папки data/settings.json
const currentDecisionHints = window.allData?.settings?.hints?.decision_hints || "ПОИЩИ / ВИКИ / ОТВЕТ... / ОТМЕНА";
// Обновляем оранжевые подсказки действий на верхнем статус-баре экрана Евы
if (typeof Visual !== 'undefined' && typeof Visual.updateHints === 'function') {
Visual.updateHints(true, currentDecisionHints);
}
// Считываем главный предустановленный вопрос Евы из папки data/settings.json
const prompt = window.allData?.settings?.phrases?.no_answer || "Я не знаю правильного ответа. Научишь меня или мне поискать в интернете?";
// Запускаем генерацию речи ИИ через TTS речевую систему voice_system.js
speak(prompt);
// Выводим текстовое облако сообщения ассистента в центральный чат приложения
if (typeof Visual !== 'undefined' && typeof Visual.addMsg === 'function') {
Visual.addMsg(prompt, 'eva-msg');
}
}
}
============================================================================
БЛОК 7: АВТОНОМНЫЙ ТАЙМЕР ИНИЦИАТИВЫ И ПРОАКТИВНОСТИ ЕВЫ (СКУКА)
============================================================================
/
* Сброс и перезапуск таймера неактивности пользователя.
* Если пользователь молчит, Ева с вероятностью 30% может проявить инициативу и задать вопрос сама.
*/
function resetIdleTimer() {
Стираем предыдущий запущенный таймер активности
clearTimeout(window.idleTimer);
Защитные условия: Ева молчит, если микрофон выключен, идет обучение или ожидается решение команды
if (!window.isLive || window.isLearning || window.isWaitingDecision) return;
Запускаем новый таймер ожидания на базе глобальной конфигурации window.IDLE_TIME
window.idleTimer = setTimeout(() ⇒ {
Проверяем, не говорит ли Ева прямо сейчас, и вычисляем случайный шанс (30%)
if (!window.speechSynthesis.speaking && Math.random() ⇐ 0.3) {
Запрашиваем из ИИ случайную заготовку вопроса для пользователя
const entry = AI.getEvaQuestionEntry();
if (entry) {
Выбираем случайный вариант вопроса, очеловечиваем его и воспроизводим
const text = Tools.humanize(entry.answers[Math.floor(Math.random() * entry.answers.length)]);
AI.currentContext = entry; Запоминаем контекст для анализа ответа пользователя
speak(text);
Visual.addMsg(text, 'eva-msg');
}
Рекурсивно перезапускаем таймер для следующего шага проверки
resetIdleTimer();
} else {
Если условия рандома не прошли, просто уходим на новый круг ожидания
resetIdleTimer();
}
}, window.IDLE_TIME);
}
============================================================================
БЛОК 8: СЕРВЕРНОЕ АВТОМАТИЧЕСКОЕ СОХРАНЕНИЕ ДАННЫХ И СБРОС БЛОКИРОВОК
============================================================================
/
* Асинхронное автоматическое сохранение полученных из интернета знаний на server.
* Выполняет обновление оперативной памяти, запись в файл и полную разблокировку UI.
* @param {string} question - Вопрос пользователя.
* @param {string} answer - Текст найденной статьи из Wikipedia.
*/
async function autoSaveToWiki(question, answer) {
console.log(«[AutoSave Модуль]: Подготовка структуры объекта данных для записи…»);
Создаем каноничную структуру сущности знаний для базы данных const newEntry = {
type: "qa",
questions: [question.toLowerCase().trim()],
answers: [answer],
sub: []
};
// Проверяем существование массива и пушим новый элемент в оперативную память браузера
if (!window.allData.wiki) window.allData.wiki = [];
window.allData.wiki.push(newEntry);
console.log("[AutoSave Модуль]: Отправка обновленного массива в Database.saveWiki...");
// Асинхронно передаем массив модулю сохранения и дожидаемся ответа от сервера
const success = await Database.saveWiki(window.allData.wiki);
// Анализируем маркер успешности проведения дисковой операции записи сервером
if (success) {
console.log("%c[AutoSave Модуль]: УСПЕШНО ЗАПИСАНО В wiki_base.json", "color: green; font-weight: bold;");
// --- КРИТИЧЕСКИЙ СБРОС ВСЕХ СИСТЕМНЫХ БЛОКИРОВОК ДЛЯ ПОДДЕРЖАНИЯ ДИАЛОГА ---
window.isWaitingDecision = false; // Разблокируем шлюзы обработки новых фраз (Слон, Бублик)
window.pendingQuestion = ""; // Очищаем буфер временного вопроса
window.isSpeakingWiki = false; // Отключаем защиту чтения статьи Википедии
window.isEvaSpeaking = false; // Сбрасываем статус речи
// --- ИНТЕРФЕЙСНАЯ ОЧИСТКА ЭКРАНА ---
// Полностью очищаем текстовую строку от застрявших серых/мусорных букв
if (typeof Visual !== 'undefined' && typeof Visual.setLiveText === 'function') {
Visual.setLiveText("");
}
// Деактивируем оранжевую строку подсказок команд
if (typeof Visual !== 'undefined' && typeof Visual.updateHints === 'function') {
Visual.updateHints(false);
}
// Автоматически закрываем всплывающие формы обучения диалогов изменением инлайновых CSS свойств
const windows = document.querySelectorAll('.learn-window, .learn-form, #learnModal, [id*="learn"], [class*="learn"]');
windows.forEach(win => {
win.style.display = 'none';
win.classList.remove('active', 'open', 'show');
});
if (typeof Visual !== 'undefined' && typeof Visual.setState === 'function') {
Visual.setState('wait');
}
// --- ПРОАКТИВНАЯ АКТИВАЦИЯ МИКРОФОНА ---
// Если сессия продолжается, принудительно перезапускаем аудиопоток захвата голоса
if (window.isLive && window.rec) {
try {
window.rec.start();
console.log("%c[AutoSave Модуль]: МИКРОФОН СНОВА АКТИВЕН ДЛЯ НОВОГО ДИАЛОГА!", "color: green; font-weight: bold;");
} catch(e) {
console.log("[AutoSave Модуль]: Поток микрофона уже запущен или управляется извне.");
}
}
} else {
console.error("[AutoSave Модуль]: КРИТИЧЕСКАЯ ОШИБКА: Сервер вернул статус false при записи файла.");
}
}
============================================================================
БЛОК 9: ТОЧКА ВХОДА И РЕГИСТРАЦИЯ КОМПОНЕНТОВ СИСТЕМЫ
============================================================================
Автоматический выпуск первичного каскада инициализации при подключении скрипта
startApp();
ГЛОБАЛЬНАЯ РЕГИСТРАЦИЯ: Назначаем финальную монолитную функцию handleSpeech в глобальное окно window
window.handleSpeech = handleSpeech;
/
* ============================================================================
* ЕВА - ИНТЕЛЛЕКТУАЛЬНЫЙ ГОЛОСОВОЙ ПОМОЩНИК
* Файл: ai.js
* Назначение: Лингвистический ИИ-мозг и оперативная память ассистента.
* Анализ речевых паттернов, управление контекстом и обучение фраз
* ============================================================================
*/
const AI = {
============================================================================
БЛОК 1: ОПЕРАТИВНЫЕ МАТРИЦЫ И БУФЕРЫ ХРАНЕНИЯ ДАННЫХ
============================================================================
memory: [], Сюда кэшируются разговорные блоки из base.json
randomQuestions: [], Сюда импортируется пул инициативных вопросов из random_questions.json
jokes: [], Сюда загружается юмористическая база данных из jokes.json
currentContext: null, Хранит ссылку на текущий активный узел диалога для вложенных веток sub
============================================================================
БЛОК 2: ИНИЦИАЛИЗАЦИЯ И СИНХРОНИЗАЦИЯ С СЕТЕВЫМ МОДУЛЕМ DATABASE
============================================================================
/
- Заполнить оперативную память ИИ структурированными массивами из JSON-файлов.
- @param {Object} data - Объект, содержащий массивы base, randomQs и jokes.
- /
setData(data) { this.memory = data.base || [];
this.randomQuestions = data.randomQs || [];
this.jokes = data.jokes || [];
console.log("[AI Мозг]: Все каскады баз данных успешно загружены в оперативную память.");
},
// ============================================================================
// БЛОК 3: ЦЕНТРАЛЬНЫЙ СЕМАНТИЧЕСКИЙ АНАЛИЗАТОР И АЛГОРИТМ МЫШЛЕНИЯ
// ============================================================================
/**
* Ассоциативный и контекстный поиск подходящего ответа на реплику пользователя.
* @param {string} userInput - Сырая строка распознанного текста из микрофона.
* @returns {Object|null} Объект с найденным QA-блоком и очищенной строкой ответа или null.
*/
think(userInput) {
// ТИПОВАЯ ЗАЩИТА: Проверяем, что входные данные существуют и являются строкой
if (!userInput || typeof userInput !== 'string') return null;
// Безопасно приводим реплику к нижнему регистру, вырезая триггерные обращения к Еве
const clean = userInput.toLowerCase().replace(/ева|еву/g, '').trim();
if (!clean) return null;
let found = null;
// ШАГ 3.1: КОНТЕКСТНЫЙ ПОИСК (Приоритет №1)
// Если Ева сейчас находится внутри вложенной ветки диалога, сначала сканируем её sub-массив
if (this.currentContext && this.currentContext.sub) {
found = this.currentContext.sub.find(r =>
r.questions && r.questions.some(q => q.toLowerCase().trim() === clean)
);
}
// ШАГ 3.2: КОРНЕВОЙ ГЛОБАЛЬНЫЙ ПОИСК (Приоритет №2)
// Если во вложенном контексте совпадений нет, объединяем и сканируем корни всех баз данных
if (!found) {
const allBases = [...this.memory, ...this.randomQuestions, ...this.jokes];
found = allBases.find(r =>
r.questions && r.questions.some(q => q.toLowerCase().trim() === clean)
);
}
// Возвращаем структурированный ответ и очищенную фразу для логики шагов в logic.js
return { found, clean };
},
// ============================================================================
// БЛОК 4: ИНТЕРАКТИВНЫЙ МЕХАНИЗМ РУЧНОГО И КОНТЕКСТНОГО ОБУЧЕНИЯ
// ============================================================================
/**
* Записать новую логическую пару Вопрос->Ответ в структуру оперативной памяти.
* Поддерживает каскадную вложенность и сохраняет древовидную целостность.
* @param {string} question - Текст вопроса.
* @param {string} answer - Текст правильного ответа.
*/
learn(question, answer) {
// Конструируем каноничный диалоговый узел с обязательной инициализацией массива sub
const entry = {
type: "qa",
questions: [question],
answers: [answer],
sub: [] // Обязательное пустое поле для обеспечения поддержки вложенности в будущем
};
// Если обучение запущено внутри диалоговой ветки, привязываем блок к дочернему sub-массиву
if (this.currentContext) {
if (!this.currentContext.sub) this.currentContext.sub = [];
this.currentContext.sub.push(entry);
} else {
// Если контекст чистый, пушим новое знание в корень основной памяти
this.memory.push(entry);
}
console.log("[AI Мозг]: В оперативную память успешно внесено новое знание.");
},
// ============================================================================
// БЛОК 5: МОДУЛЬ ПРОАКТИВНОСТИ И ИНИЦИАТИВНОГО ВЗАИМОДЕЙСТВИЯ
// ============================================================================
/**
* Извлечь случайную структуру вопроса для симуляции проявления инициативы Евы.
* Используется фоновым таймером неактивности пользователя timer.js.
* @returns {Object|null} QA-блок вопроса или null при пустой базе.
*/
getEvaQuestionEntry() {
if (this.randomQuestions.length === 0) return null;
// Вычисляем случайный индекс в диапазоне длины пула инициативных вопросов
const randomIndex = Math.floor(Math.random() * this.randomQuestions.length);
return this.randomQuestions[randomIndex];
}
};
Регистрируем объект ИИ-мозга в глобальном пространстве window
window.AI = AI;
