diff --git a/api/endpoints/chat.php b/api/endpoints/chat.php
index a13724d..b95fd65 100644
--- a/api/endpoints/chat.php
+++ b/api/endpoints/chat.php
@@ -8,6 +8,7 @@ if ($method !== 'POST') {
$message = trim($data['message'] ?? '');
$sessionId = $data['session_id'] ?? session_id();
$panelCtx = $data['context'] ?? null; // Panel item selected by user (VM, device, alert, etc.)
+$stream = !empty($data['stream']);
if (!$message) {
echo json_encode(['error' => 'Message required']); exit;
@@ -1632,6 +1633,18 @@ if (!$reply) {
}
}
+// ── Tier 0.25: Quick-note capture ────────────────────────────────────────────
+if (!$reply && preg_match('/^note:\s*(.+)/iu', $message, $nm)) {
+ $noteText = trim($nm[1]);
+ $ts = date('Y-m-d H:i');
+ JarvisDB::query(
+ "INSERT INTO kb_facts (category, fact, source, confidence) VALUES (?,?,?,?)",
+ ['notes', "[{$ts}] {$noteText}", 'user-note', 1.0]
+ );
+ $reply = "Note saved to Memory Core, {$userAddr}: \"{$noteText}\"";
+ $source = 'intent:quick_note';
+}
+
// ── Tier 0.5: Multi-step command detection ──────────────────────────────────
// Detect "do X and Y" or "X then Y" compound commands (only when no reply yet)
if (!$reply) {
@@ -1670,7 +1683,7 @@ if (!$reply) {
if ($best && $bestS >= 1) {
@file_get_contents(HA_URL.'/api/services/scene/turn_on', false,
stream_context_create(['http'=>['method'=>'POST','timeout'=>4,
- 'header'=>"Authorization: Bearer ".HA_TOKEN."
+ 'header'=>"Authorization: Bearer ".HA_TOKEN."
Content-Type: application/json",
'content'=>json_encode(['entity_id'=>$best['entity_id']])]]));
$mReply = ($best['attributes']['friendly_name'] ?? $best['entity_id']) . ' activated';
@@ -1817,6 +1830,44 @@ Content-Type: application/json",
break;
}
+ case 'restart_agent':
+ // Extract target hostname from message
+ $msgLow = strtolower($message);
+ $agentMap = [
+ 'homebridge' => 'homebridge_b57cbaea',
+ 'jellyfin' => 'jellyfin_7e386833',
+ 'networkbackup' => 'networkbackup_NetworkB',
+ 'network backup'=> 'networkbackup_NetworkB',
+ 'novacpx' => 'novacpx_e3b07264',
+ 'nova' => 'novacpx_e3b07264',
+ 'mediastack' => 'MediaStack_2c00b1b8',
+ 'media stack' => 'MediaStack_2c00b1b8',
+ 'homeassistant' => 'homeassistant_ha',
+ 'home assistant'=> 'homeassistant_ha',
+ ];
+ $targetAgentId = null;
+ $targetName = null;
+ foreach ($agentMap as $keyword => $agentId) {
+ if (str_contains($msgLow, $keyword)) {
+ $targetAgentId = $agentId;
+ $targetName = ucfirst($keyword);
+ break;
+ }
+ }
+ if ($targetAgentId) {
+ JarvisDB::insert(
+ "INSERT INTO agent_commands (agent_id, command_type, command_data, status, created_at)
+ VALUES (?, 'restart_service', ?, 'pending', NOW())",
+ [$targetAgentId, json_encode(['service' => 'jarvis-agent'])]
+ );
+ $reply = "Restart command sent to the {$targetName} agent, {$userAddr}. It should come back online within 15 seconds.";
+ } else {
+ // Fall back to listing restartable agents
+ $reply = "Which agent should I restart, {$userAddr}? I can restart: HomeAssistant, Homebridge, Jellyfin, MediaStack, NetworkBackup, or NovaCPX.";
+ }
+ $source = 'intent:restart_agent';
+ break;
+
case 'restart_agent':
// Extract target hostname from message
$msgLow = strtolower($message);
@@ -2132,6 +2183,75 @@ if (!$reply && defined('GROQ_API_KEY') && GROQ_API_KEY) {
$userMsg = $ctxSnippet ? $ctxSnippet . "\n" . $message : $message;
$groqMessages[] = ['role' => 'user', 'content' => $userMsg];
+ if ($stream) {
+ // ── Streaming SSE path ──────────────────────────────────────────
+ while (ob_get_level()) ob_end_clean();
+ header('Content-Type: text/event-stream');
+ header('Cache-Control: no-cache');
+ header('X-Accel-Buffering: no');
+ header('Connection: keep-alive');
+ ob_implicit_flush(true);
+
+ $streamedReply = '';
+ $ch = curl_init('https://api.groq.com/openai/v1/chat/completions');
+ curl_setopt_array($ch, [
+ CURLOPT_POST => true,
+ CURLOPT_POSTFIELDS => json_encode([
+ 'model' => $groqModel,
+ 'messages' => $groqMessages,
+ 'max_tokens' => 400,
+ 'temperature' => 0.7,
+ 'stream' => true,
+ ]),
+ CURLOPT_HTTPHEADER => [
+ 'Authorization: Bearer ' . GROQ_API_KEY,
+ 'Content-Type: application/json',
+ ],
+ CURLOPT_TIMEOUT => GROQ_TIMEOUT,
+ CURLOPT_CONNECTTIMEOUT => 5,
+ CURLOPT_SSL_VERIFYPEER => true,
+ CURLOPT_WRITEFUNCTION => function($ch, $rawData) use (&$streamedReply) {
+ $lines = explode("\n", $rawData);
+ foreach ($lines as $line) {
+ $line = trim($line);
+ if ($line === '' || $line === 'data: [DONE]') continue;
+ if (!str_starts_with($line, 'data: ')) continue;
+ $ev = json_decode(substr($line, 6), true);
+ $tok = $ev['choices'][0]['delta']['content'] ?? null;
+ if ($tok !== null && $tok !== '') {
+ echo 'data: ' . json_encode(['type' => 'token', 'token' => $tok]) . "\n\n";
+ flush();
+ $streamedReply .= $tok;
+ }
+ }
+ return strlen($rawData);
+ },
+ ]);
+ curl_exec($ch);
+ curl_close($ch);
+
+ $reply = $streamedReply ? trim($streamedReply) : "Groq AI is temporarily unavailable, {$userAddr}.";
+ $source = $streamedReply ? 'groq:' . $groqModel : 'fallback';
+
+ JarvisDB::insert(
+ 'INSERT INTO conversations (session_id, role, content) VALUES (?,?,?)',
+ [$sessionId, 'assistant', $reply]
+ );
+ KBEngine::learnFromConversation($message, $reply);
+
+ echo 'data: ' . json_encode([
+ 'type' => 'complete',
+ 'reply' => $reply,
+ 'source' => $source,
+ 'session_id' => $sessionId,
+ 'ui_action' => $uiAction ?? null,
+ 'arc_job' => null,
+ 'open_network_map' => false,
+ ]) . "\n\n";
+ flush();
+ exit;
+ }
+
$ch = curl_init('https://api.groq.com/openai/v1/chat/completions');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
diff --git a/public_html/assets/css/jarvis.css b/public_html/assets/css/jarvis.css
index a20e9a5..9fcea8f 100644
--- a/public_html/assets/css/jarvis.css
+++ b/public_html/assets/css/jarvis.css
@@ -648,6 +648,46 @@ body::after{
box-shadow:0 0 4px var(--red);
}
@keyframes waveBounce{from{height:4px}to{height:24px}}
+.wave-bar.live{animation:none!important;transition:height 0.06s}
+
+/* ── AMBIENT DIM MODE ─────────────────────────────────────────────────── */
+.ambient-dim-active .panel,.ambient-dim-active #bottomBar{
+ opacity:0.12;transition:opacity 2s ease;pointer-events:none}
+.ambient-dim-active .panel:hover,.ambient-dim-active #bottomBar:hover{
+ opacity:1;pointer-events:auto;transition:opacity 0.3s ease}
+
+/* ── THEME BUTTONS ────────────────────────────────────────────────────── */
+.theme-btn{
+ background:none;border:1px solid rgba(0,212,255,0.25);border-radius:50%;
+ width:14px;height:14px;cursor:pointer;padding:0;font-size:0.6rem;line-height:1;
+ display:flex;align-items:center;justify-content:center;color:var(--cyan);
+ transition:all 0.2s;flex-shrink:0}
+.theme-btn.active{border-color:currentColor;box-shadow:0 0 6px currentColor}
+.theme-btn:hover{opacity:0.8;transform:scale(1.2)}
+
+/* ── CANCEL BUTTON (in thinking bubble) ──────────────────────────────── */
+.thinking-cancel{
+ background:none;border:1px solid rgba(255,34,68,0.4);color:rgba(255,34,68,0.8);
+ font-family:var(--font-mono);font-size:0.55rem;letter-spacing:1px;
+ padding:2px 8px;border-radius:2px;cursor:pointer;margin-top:6px;display:block}
+.thinking-cancel:hover{background:rgba(255,34,68,0.1)}
+
+/* ── QUICK NOTE BAR ──────────────────────────────────────────────────── */
+#quickNoteBar{
+ position:fixed;bottom:90px;left:50%;transform:translateX(-50%);
+ width:500px;max-width:90vw;background:rgba(0,8,16,0.95);
+ border:1px solid var(--cyan);border-radius:3px;padding:8px 14px;
+ display:none;z-index:1100;align-items:center;gap:8px}
+#quickNoteBar.open{display:flex}
+#quickNoteInput{
+ flex:1;background:none;border:none;color:var(--cyan);
+ font-family:var(--font-mono);font-size:0.75rem;outline:none;letter-spacing:0.5px}
+#quickNoteInput::placeholder{color:rgba(0,212,255,0.4)}
+
+/* ── STREAMING MESSAGE ───────────────────────────────────────────────── */
+.msg.jarvis.streaming::after{
+ content:'▋';animation:blink 0.7s step-end infinite;color:var(--cyan);margin-left:2px}
+@keyframes blink{0%,100%{opacity:1}50%{opacity:0}}
/* ── RIGHT PANEL ─────────────────────────────────────────────────── */
#rightPanel{display:flex;flex-direction:column;gap:10px;overflow-y:auto}
diff --git a/public_html/assets/js/jarvis-app.js b/public_html/assets/js/jarvis-app.js
index f6f3092..78aeba1 100644
--- a/public_html/assets/js/jarvis-app.js
+++ b/public_html/assets/js/jarvis-app.js
@@ -196,6 +196,17 @@ function showApp(name, greeting, silent = false) {
setTimeout(checkSuggestions, 15000);
setInterval(checkSuggestions, 1800000); // every 30 min // baseline on load
setInterval(pollAlertsProactive, 60000); // poll every 60s
+ setInterval(() => {
+ const layout = document.getElementById('mainLayout');
+ if (!layout) return;
+ if (Date.now() - lastActivity > 90000) layout.classList.add('ambient-dim-active');
+ else layout.classList.remove('ambient-dim-active');
+ }, 5000);
+ setTimeout(() => {
+ if ('Notification' in window && Notification.permission === 'default') {
+ Notification.requestPermission();
+ }
+ }, 9000);
// Guardian Mode — badge refresh + proactive chat
setTimeout(() => {
_refreshGuardianBadge();
@@ -1101,7 +1112,7 @@ function showThinking() {
const log = document.getElementById('chatLog');
const div = document.createElement('div');
div.className = 'msg jarvis';
- div.innerHTML = '
';
+ div.innerHTML = '';
div.id = 'thinking-bubble';
log.appendChild(div);
log.scrollTop = log.scrollHeight;
@@ -1144,26 +1155,18 @@ async function sendMessage() {
const text = input.value.trim();
if (!text) return;
- // Local commands — no API round-trip
var t2 = text.toLowerCase();
- // Sleep command
- if (SLEEP_CMDS.test(t2)) {
- input.value = '';
- addMessage('user', text);
- enterSleepMode();
- return;
- }
+ if (SLEEP_CMDS.test(t2)) { input.value=''; addMessage('user',text); enterSleepMode(); return; }
if (NM_OPEN_RE.test(t2)) {
input.value=''; addMessage('user',text);
addMessage('jarvis','Launching network topology display.');
- speak('Launching network topology display.');
- openNetMap(); return;
+ speak('Launching network topology display.'); openNetMap(); return;
}
if (NM_CLOSE_RE.test(t2)) {
input.value=''; addMessage('user',text);
- var isOpen=document.getElementById('netMapOverlay')&&document.getElementById('netMapOverlay').classList.contains('nm-open');
+ var isOpen=document.getElementById('netMapOverlay')?.classList.contains('nm-open');
if(isOpen){closeNetMap();addMessage('jarvis','Network map closed.');speak('Network map closed.');}
else addMessage('jarvis','Network map is not currently active.');
return;
@@ -1171,32 +1174,108 @@ async function sendMessage() {
input.value = '';
addMessage('user', text);
showThinking();
+ _abortController = new AbortController();
try {
- const payload = {message:text, session_id:sessionId};
- if (selectedContext) {
- payload.context = selectedContext;
- clearContext();
- }
- const data = await api('chat', 'POST', payload);
- const bubble = document.getElementById('thinking-bubble');
- if (bubble) bubble.remove();
+ const payload = {message:text, session_id:sessionId, stream:true};
+ if (selectedContext) { payload.context = selectedContext; clearContext(); }
- if (data.reply) {
- addMessage('jarvis', data.reply, data.source || null);
- speak(data.reply);
+ const resp = await fetch('/api.php?action=chat', {
+ method: 'POST',
+ headers: {'Content-Type':'application/json','X-Session-Token':sessionToken},
+ body: JSON.stringify(payload),
+ signal: _abortController.signal,
+ credentials: 'include',
+ });
+ _abortController = null;
+
+ if (resp.status === 401) { logout(); return; }
+
+ const ct = resp.headers.get('Content-Type') || '';
+
+ if (ct.includes('text/event-stream')) {
+ // ── Streaming path (Groq LLM with token-by-token delivery) ──────
+ const bubble = document.getElementById('thinking-bubble');
+ if (bubble) bubble.remove();
+ let msgEl = null, accum = '';
+ const reader = resp.body.getReader();
+ const dec = new TextDecoder();
+ let lineBuf = '';
+ while (true) {
+ const {done, value} = await reader.read();
+ if (done) break;
+ lineBuf += dec.decode(value, {stream:true});
+ const lines = lineBuf.split('\n');
+ lineBuf = lines.pop();
+ for (const line of lines) {
+ if (!line.startsWith('data: ')) continue;
+ let ev; try { ev = JSON.parse(line.slice(6)); } catch { continue; }
+ if (ev.type === 'token') {
+ accum += ev.token;
+ if (!msgEl) msgEl = _addStreamingMsg(accum);
+ else _updateStreamingMsg(msgEl, accum);
+ } else if (ev.type === 'complete') {
+ const finalText = ev.reply || accum;
+ if (msgEl) _finalizeStreamingMsg(msgEl, finalText, ev.source);
+ else addMessage('jarvis', finalText, ev.source);
+ speak(finalText);
+ if (ev.open_network_map) openNetMap();
+ if (ev.ui_action === 'focus_mode' && panelsVisible) togglePanels(true);
+ if (ev.ui_action === 'show_panels' && !panelsVisible) togglePanels(true);
+ if (ev.arc_job) onArcJobStarted(ev.arc_job, ev.source||'');
+ }
+ }
+ }
+ } else {
+ // ── Regular JSON path (intent/KB — near-instant) ────────────────
+ const data = await resp.json();
+ const bubble = document.getElementById('thinking-bubble');
+ if (bubble) bubble.remove();
+ if (data.reply) { addMessage('jarvis', data.reply, data.source||null); speak(data.reply); }
+ if (data.open_network_map) openNetMap();
+ if (data.ui_action === 'focus_mode' && panelsVisible) togglePanels(true);
+ if (data.ui_action === 'show_panels' && !panelsVisible) togglePanels(true);
+ if (data.arc_job) onArcJobStarted(data.arc_job, data.source||'');
}
- if (data.open_network_map) { openNetMap(); }
- if (data.ui_action === 'focus_mode') { if (panelsVisible) togglePanels(true); }
- if (data.ui_action === 'show_panels') { if (!panelsVisible) togglePanels(true); }
- if (data.arc_job) { onArcJobStarted(data.arc_job, data.source || ''); }
} catch(e) {
+ _abortController = null;
const bubble = document.getElementById('thinking-bubble');
if (bubble) bubble.remove();
- addMessage('jarvis', 'I encountered a communication error, Sir. Please check my API connection.');
+ if (e.name === 'AbortError') addMessage('jarvis', 'Request cancelled, Sir.');
+ else addMessage('jarvis', 'I encountered a communication error, Sir. Please check my API connection.');
}
}
+function _addStreamingMsg(text) {
+ const log = document.getElementById('chatLog');
+ const div = document.createElement('div');
+ div.className = 'msg jarvis streaming';
+ div.id = 'streaming-bubble';
+ div.textContent = text;
+ log.appendChild(div);
+ log.scrollTop = log.scrollHeight;
+ return div;
+}
+function _updateStreamingMsg(el, text) {
+ if (!el) return;
+ el.textContent = text;
+ const log = document.getElementById('chatLog');
+ if (log) log.scrollTop = log.scrollHeight;
+}
+function _finalizeStreamingMsg(el, text, source) {
+ if (!el) return;
+ el.id = ''; el.classList.remove('streaming');
+ el.textContent = text;
+ if (source) {
+ const s = document.createElement('div');
+ s.className = 'msg-source'; s.textContent = source;
+ el.appendChild(s);
+ }
+}
+function cancelRequest() {
+ if (_abortController) { _abortController.abort(); _abortController = null; }
+}
+
// ── VOICE RECOGNITION ─────────────────────────────────────────────────
function initVoice() {
const SR = window.SpeechRecognition || window.webkitSpeechRecognition;
@@ -1371,6 +1450,7 @@ function startListening() {
return;
}
isListening = true;
+ _startWaveform();
_scheduleRecStart(50);
}
@@ -1380,9 +1460,42 @@ function stopListening() {
voiceMuted = false;
updateMicBtn();
clearTimeout(_recTimer);
+ _stopWaveform();
try { recognition.abort(); } catch(_) {}
}
+// ── VOICE WAVEFORM (Web Audio API) ──────────────────────────────────────────
+async function _startWaveform() {
+ if (_waveAudioCtx) return;
+ try {
+ _waveStream = await navigator.mediaDevices.getUserMedia({audio:true, video:false});
+ _waveAudioCtx = new (window.AudioContext || window.webkitAudioContext)();
+ _waveAnalyser = _waveAudioCtx.createAnalyser();
+ _waveAnalyser.fftSize = 32;
+ _waveAudioCtx.createMediaStreamSource(_waveStream).connect(_waveAnalyser);
+ const bars = document.querySelectorAll('#waveform .wave-bar');
+ bars.forEach(b => b.classList.add('live'));
+ const buf = new Uint8Array(_waveAnalyser.frequencyBinCount);
+ (function drawWave() {
+ _waveRafId = requestAnimationFrame(drawWave);
+ _waveAnalyser.getByteFrequencyData(buf);
+ bars.forEach((bar, i) => {
+ const v = (buf[i % buf.length] || 0) / 255;
+ bar.style.height = (4 + Math.round(v * 20)) + 'px';
+ });
+ })();
+ } catch(_) { /* mic permission denied — CSS animation continues */ }
+}
+function _stopWaveform() {
+ if (_waveRafId) { cancelAnimationFrame(_waveRafId); _waveRafId = null; }
+ if (_waveStream) { _waveStream.getTracks().forEach(t => t.stop()); _waveStream = null; }
+ if (_waveAudioCtx) { _waveAudioCtx.close().catch(()=>{}); _waveAudioCtx = null; }
+ _waveAnalyser = null;
+ document.querySelectorAll('#waveform .wave-bar').forEach(b => {
+ b.classList.remove('live'); b.style.height = '';
+ });
+}
+
// ── SPEECH SYNTHESIS ──────────────────────────────────────────────────
function loadVoices() {
const set = () => {
@@ -1405,6 +1518,11 @@ function loadVoices() {
}
let _ttsAudio = null;
+let _abortController = null;
+let _waveAudioCtx = null;
+let _waveAnalyser = null;
+let _waveStream = null;
+let _waveRafId = null;
async function speak(text) {
if (!text) return;
@@ -1547,6 +1665,53 @@ async function triggerMorningBriefing() {
} catch(e) {}
}
+// ── ACCENT COLOR THEMES ───────────────────────────────────────────────────────
+const _THEMES = {
+ 'stark-blue': {'--cyan':'#00d4ff','--cyan2':'#00a8cc','--cyan3':'rgba(0,212,255,0.15)'},
+ 'widow-red': {'--cyan':'#ff3366','--cyan2':'#cc1a44','--cyan3':'rgba(255,51,102,0.15)'},
+ 'hulk-green': {'--cyan':'#39ff14','--cyan2':'#27b30d','--cyan3':'rgba(57,255,20,0.15)'},
+};
+function applyTheme(name) {
+ const t = _THEMES[name]; if (!t) return;
+ const root = document.documentElement;
+ Object.entries(t).forEach(([k,v]) => root.style.setProperty(k, v));
+ localStorage.setItem('jarvis_theme', name);
+ document.querySelectorAll('.theme-btn').forEach(b => b.classList.toggle('active', b.dataset.theme === name));
+}
+// Apply saved theme on load
+(function() {
+ const saved = localStorage.getItem('jarvis_theme');
+ if (saved && saved !== 'stark-blue') setTimeout(() => applyTheme(saved), 50);
+})();
+
+// ── QUICK-NOTE CAPTURE ────────────────────────────────────────────────────────
+function openQuickNote() {
+ const bar = document.getElementById('quickNoteBar');
+ if (!bar) return;
+ bar.classList.add('open');
+ setTimeout(() => document.getElementById('quickNoteInput')?.focus(), 50);
+}
+function closeQuickNote() {
+ const bar = document.getElementById('quickNoteBar');
+ if (bar) bar.classList.remove('open');
+ const inp = document.getElementById('quickNoteInput');
+ if (inp) inp.value = '';
+}
+async function saveQuickNote() {
+ const inp = document.getElementById('quickNoteInput');
+ if (!inp || !inp.value.trim()) { closeQuickNote(); return; }
+ const note = inp.value.trim();
+ closeQuickNote();
+ try {
+ await api('chat', 'POST', {message: 'note: ' + note, session_id: sessionId});
+ addMessage('jarvis', 'Note saved to Memory Core, Sir: "' + note + '"');
+ } catch(_) {}
+}
+function handleNoteKey(e) {
+ if (e.key === 'Enter') { e.preventDefault(); saveQuickNote(); }
+ else if (e.key === 'Escape') { e.stopPropagation(); closeQuickNote(); }
+}
+
// ── KEYBOARD SHORTCUTS ───────────────────────────────────────────────────────────────
document.addEventListener('keydown', function(e) {
const tag = (document.activeElement?.tagName || '').toLowerCase();
@@ -1558,11 +1723,13 @@ document.addEventListener('keydown', function(e) {
if (el && (el.style.display === 'flex' || el.style.display === 'block')) el.style.display = 'none';
});
if (document.getElementById('netMapOverlay')?.classList.contains('nm-open')) closeNetMap();
+ if (document.getElementById('quickNoteBar')?.classList.contains('open')) closeQuickNote();
return;
}
if (inInput) return;
if (e.key === 'F5') { e.preventDefault(); refreshAll(); return; }
if (e.key === 'm' || e.key === 'M') { toggleVoice(); return; }
+ if (e.key === 'n' || e.key === 'N') { openQuickNote(); return; }
if (e.key === ' ') { e.preventDefault(); document.getElementById('textInput')?.focus(); return; }
const tabMap = {'1':'ha','2':'alerts','3':'news','4':'agents'};
if (tabMap[e.key]) {
diff --git a/public_html/assets/js/jarvis-protocols.js b/public_html/assets/js/jarvis-protocols.js
index 72099d1..f381aa7 100644
--- a/public_html/assets/js/jarvis-protocols.js
+++ b/public_html/assets/js/jarvis-protocols.js
@@ -476,6 +476,12 @@ async function loadGuardian() {
_guardianUnread = unread;
_updateGuardianBadge(unread, critU);
+ if (critU > 0 && document.hidden && 'Notification' in window && Notification.permission === 'granted') {
+ new Notification('JARVIS ALERT', {
+ body: critU + ' critical alert' + (critU > 1 ? 's' : '') + ' require your attention.',
+ icon: '/favicon.ico',
+ });
+ }
const lastScan = status.last_scan
? new Date(status.last_scan + 'Z').toLocaleTimeString()
diff --git a/public_html/index.html b/public_html/index.html
index 5428d3f..c9e5f2b 100644
--- a/public_html/index.html
+++ b/public_html/index.html
@@ -67,6 +67,11 @@
+
+
+
+
+
@@ -276,6 +281,7 @@
+✎ NOTE