mirror of
https://github.com/myronblair/jarvis
synced 2026-06-30 17:50:23 -05:00
feat: mobile UI, chat history search, news source filtering
- Mobile (#11): responsive 3-button bottom nav (STATS/CHAT/INFO), panel switching, compact topbar, touch-friendly inputs; panels show one-at-a-time on screens <900px - Search (#12): 🔍 button next to TRANSMIT opens search modal; history.php endpoint queries conversations table; results show role, timestamp, and snippet - News filter (#13): ⚙ gear on NEWS tab reveals category checkboxes; hidden categories stored in localStorage; empty-state message when all hidden Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
// Chat history search endpoint
|
||||
require_once __DIR__ . '/../config.php';
|
||||
require_once __DIR__ . '/../../includes/auth.php';
|
||||
|
||||
header('Content-Type: application/json');
|
||||
AuthMiddleware::requireAuth();
|
||||
|
||||
$q = trim($_GET['q'] ?? '');
|
||||
if (strlen($q) < 2) {
|
||||
echo json_encode(['results' => [], 'error' => 'Query too short']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$rows = JarvisDB::query(
|
||||
"SELECT role, content, created_at FROM conversations
|
||||
WHERE content LIKE ? ORDER BY created_at DESC LIMIT 25",
|
||||
['%' . $q . '%']
|
||||
) ?? [];
|
||||
|
||||
echo json_encode(['results' => $rows, 'total' => count($rows)]);
|
||||
@@ -72,6 +72,7 @@ $endpoints = [
|
||||
'agent' => 'agent.php',
|
||||
'planner' => 'planner.php',
|
||||
'jellyfin' => 'jellyfin.php',
|
||||
'history' => 'history.php',
|
||||
'arc' => 'arc.php',
|
||||
'directives' => 'directives.php',
|
||||
'memory' => 'memory.php',
|
||||
|
||||
+171
-4
@@ -301,11 +301,39 @@ body::after{
|
||||
#mainLayout.swapped.focus-mode #rightPanel{transform:translateX(-20px)}
|
||||
#btn-swap-panels{background:none;border:1px solid var(--panel-border);color:var(--text-dim);padding:3px 8px;cursor:pointer;font-family:var(--font-mono);font-size:0.6rem;letter-spacing:1px;transition:all 0.2s}
|
||||
#btn-swap-panels:hover,#btn-swap-panels.active{color:var(--cyan);border-color:var(--cyan)}
|
||||
/* Mobile fallback */
|
||||
/* ── MOBILE RESPONSIVE ──────────────────────────────────────────────── */
|
||||
@media(max-width:900px){
|
||||
#mainLayout{grid-template-columns:1fr;grid-template-rows:auto}
|
||||
#mainLayout{grid-template-columns:1fr!important;grid-template-rows:1fr!important;padding:6px;gap:6px}
|
||||
#leftPanel,#rightPanel{display:none}
|
||||
#leftPanel.mob-active,#rightPanel.mob-active{display:flex!important;flex-direction:column}
|
||||
#centerPanel{display:none}
|
||||
#centerPanel.mob-active{display:flex!important;flex-direction:column}
|
||||
#topBar{padding:0 10px;height:42px}
|
||||
.tb-center{display:none}
|
||||
.tb-logo{font-size:0.85rem;letter-spacing:2px}
|
||||
#clock{font-size:0.85rem}
|
||||
#date-display{display:none}
|
||||
#btn-swap-panels,.btn-panels{display:none}
|
||||
#inputArea{padding:8px 6px}
|
||||
#textInput{font-size:0.85rem;padding:8px 10px;min-height:40px}
|
||||
#sendBtn{min-height:40px;padding:0 14px;font-size:0.65rem}
|
||||
#micBtn{min-height:40px;min-width:40px;font-size:1.1rem}
|
||||
#app{padding-bottom:48px}
|
||||
#mobileNav{display:flex!important}
|
||||
}
|
||||
@media(min-width:901px){#mobileNav{display:none!important}}
|
||||
#mobileNav{
|
||||
display:none;position:fixed;bottom:0;left:0;right:0;z-index:100;
|
||||
background:rgba(0,8,22,0.96);border-top:1px solid var(--panel-border);height:48px;
|
||||
}
|
||||
.mob-nav-btn{
|
||||
flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;
|
||||
font-family:var(--font-display);font-size:0.5rem;letter-spacing:1.5px;
|
||||
color:var(--text-dim);cursor:pointer;gap:2px;border:none;background:none;transition:color 0.2s;
|
||||
}
|
||||
.mob-nav-btn .mob-icon{font-size:1rem;line-height:1}
|
||||
.mob-nav-btn.active{color:var(--cyan)}
|
||||
.mob-nav-btn.active .mob-icon{filter:drop-shadow(0 0 4px var(--cyan))}
|
||||
/* Panel toggle button */
|
||||
.btn-panels{
|
||||
background:rgba(0,212,255,0.06);
|
||||
@@ -1210,6 +1238,7 @@ body::after{
|
||||
<input type="text" id="textInput" placeholder="Enter command or speak to JARVIS..."
|
||||
autocomplete="off" onkeydown="if(event.key==='Enter')sendMessage()"/>
|
||||
<button id="sendBtn" onclick="sendMessage()">TRANSMIT</button>
|
||||
<button id="searchBtn" onclick="openSearchModal()" title="Search chat history" style="background:transparent;border:1px solid var(--panel-border);color:var(--text-dim);font-size:1rem;padding:0 10px;cursor:pointer;transition:all 0.2s;min-height:36px" onmouseover="this.style.color='var(--cyan)';this.style.borderColor='var(--cyan)'" onmouseout="this.style.color='var(--text-dim)';this.style.borderColor='var(--panel-border)'">🔍</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1283,6 +1312,13 @@ body::after{
|
||||
<div id="alerts-list"><div class="loading-shimmer"></div></div>
|
||||
</div>
|
||||
<div id="tab-news" class="tab-pane" style="overflow-y:auto;flex:1">
|
||||
<div style="display:flex;align-items:center;justify-content:flex-end;margin-bottom:4px">
|
||||
<button onclick="toggleNewsFilter()" title="Filter news sources" style="background:none;border:none;color:var(--text-dim);cursor:pointer;font-size:0.85rem;padding:2px 4px;transition:color 0.2s" onmouseover="this.style.color='var(--cyan)'" onmouseout="this.style.color='var(--text-dim)'">⚙</button>
|
||||
</div>
|
||||
<div id="news-filter-panel" style="display:none;margin-bottom:8px;padding:8px;background:rgba(0,212,255,0.04);border:1px solid var(--panel-border);border-radius:4px">
|
||||
<div style="font-family:var(--font-display);font-size:0.52rem;letter-spacing:2px;color:var(--cyan);margin-bottom:6px">SHOW CATEGORIES</div>
|
||||
<div id="news-filter-checkboxes" style="display:flex;flex-direction:column;gap:4px"></div>
|
||||
</div>
|
||||
<div id="news-list"><div class="loading-shimmer"></div></div>
|
||||
</div>
|
||||
<div id="tab-agents" class="tab-pane" style="overflow-y:auto;flex:1">
|
||||
@@ -2547,6 +2583,7 @@ function showApp(name, greeting, silent = false) {
|
||||
loadAlerts();
|
||||
loadWeather();
|
||||
loadNews();
|
||||
initMobile();
|
||||
// Guardian Mode — badge refresh + proactive chat
|
||||
setTimeout(() => {
|
||||
_refreshGuardianBadge();
|
||||
@@ -3231,6 +3268,17 @@ async function loadWeather() {
|
||||
}
|
||||
|
||||
// ── NEWS ──────────────────────────────────────────────────────────────
|
||||
function getNewsHidden() {
|
||||
try { return JSON.parse(localStorage.getItem('news_hidden_cats') || '[]'); } catch(e) { return []; }
|
||||
}
|
||||
function setNewsHidden(arr) { localStorage.setItem('news_hidden_cats', JSON.stringify(arr)); }
|
||||
|
||||
function toggleNewsFilter() {
|
||||
const panel = document.getElementById('news-filter-panel');
|
||||
panel.style.display = panel.style.display === 'none' ? 'block' : 'none';
|
||||
}
|
||||
|
||||
let _newsCats = [];
|
||||
async function loadNews() {
|
||||
const d = await api('news');
|
||||
const el = document.getElementById('news-list');
|
||||
@@ -3238,10 +3286,24 @@ async function loadNews() {
|
||||
el.innerHTML = '<div style="color:var(--text-dim);font-size:0.65rem;text-align:center;padding:20px">News loading...</div>';
|
||||
return;
|
||||
}
|
||||
const catLabels = { headlines: '📰 TOP HEADLINES', technology: '💻 TECHNOLOGY' };
|
||||
const catLabels = { headlines: '📰 TOP HEADLINES', technology: '💻 TECHNOLOGY', pinned: '📌 JARVIS PINNED' };
|
||||
const hidden = getNewsHidden();
|
||||
_newsCats = Object.keys(d.categories);
|
||||
|
||||
// Build filter checkboxes
|
||||
const cbContainer = document.getElementById('news-filter-checkboxes');
|
||||
if (cbContainer) {
|
||||
cbContainer.innerHTML = _newsCats.map(cat => `
|
||||
<label style="display:flex;align-items:center;gap:8px;cursor:pointer;font-size:0.6rem;color:var(--text-primary)">
|
||||
<input type="checkbox" ${hidden.includes(cat) ? '' : 'checked'} onchange="toggleNewsCat('${cat}',this.checked)"
|
||||
style="accent-color:var(--cyan)"/>
|
||||
${catLabels[cat] || cat.toUpperCase()}
|
||||
</label>`).join('');
|
||||
}
|
||||
|
||||
let html = '';
|
||||
for (const [cat, articles] of Object.entries(d.categories)) {
|
||||
if (!articles.length) continue;
|
||||
if (!articles.length || hidden.includes(cat)) continue;
|
||||
html += `<div class="news-cat-header">${catLabels[cat] || cat.toUpperCase()}</div>`;
|
||||
for (const a of articles.slice(0, 5)) {
|
||||
const ctxKey = 'news_' + (cat + '_' + a.title).replace(/[^a-z0-9]/gi,'').slice(0,30);
|
||||
@@ -3254,11 +3316,24 @@ async function loadNews() {
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
if (!html) html = '<div style="color:var(--text-dim);font-size:0.65rem;text-align:center;padding:20px">All categories hidden — use ⚙ to show sources</div>';
|
||||
const ageMin = d.cache_age_s > 0 ? Math.round(d.cache_age_s/60) : 0;
|
||||
html += `<div style="font-size:0.5rem;color:var(--text-dim);text-align:right;margin-top:8px">Updated ${ageMin}m ago</div>`;
|
||||
el.innerHTML = html;
|
||||
}
|
||||
|
||||
function toggleNewsCat(cat, show) {
|
||||
const hidden = getNewsHidden();
|
||||
if (show) {
|
||||
const idx = hidden.indexOf(cat);
|
||||
if (idx > -1) hidden.splice(idx, 1);
|
||||
} else {
|
||||
if (!hidden.includes(cat)) hidden.push(cat);
|
||||
}
|
||||
setNewsHidden(hidden);
|
||||
loadNews();
|
||||
}
|
||||
|
||||
// ── TABS ──────────────────────────────────────────────────────────────
|
||||
function switchTab(name) {
|
||||
if (name === 'sites') { openSitesModal(); return; }
|
||||
@@ -4996,6 +5071,68 @@ document.addEventListener('keydown', e => {
|
||||
if (e.key === 'Escape') closeVisionLightbox();
|
||||
});
|
||||
|
||||
// ── CHAT HISTORY SEARCH ───────────────────────────────────────────────────────
|
||||
function openSearchModal() {
|
||||
document.getElementById('searchModal').style.display = 'flex';
|
||||
document.getElementById('searchInput').focus();
|
||||
}
|
||||
function closeSearchModal() {
|
||||
document.getElementById('searchModal').style.display = 'none';
|
||||
document.getElementById('searchResults').innerHTML = '<div style="color:var(--text-dim);font-size:0.65rem;text-align:center;padding:20px">Type to search your JARVIS conversations</div>';
|
||||
document.getElementById('searchInput').value = '';
|
||||
}
|
||||
async function runSearch() {
|
||||
const q = document.getElementById('searchInput').value.trim();
|
||||
if (!q) return;
|
||||
const el = document.getElementById('searchResults');
|
||||
el.innerHTML = '<div style="color:var(--text-dim);font-size:0.65rem;text-align:center;padding:20px">Searching...</div>';
|
||||
try {
|
||||
const d = await api('history?q=' + encodeURIComponent(q));
|
||||
if (!d.results || !d.results.length) {
|
||||
el.innerHTML = '<div style="color:var(--text-dim);font-size:0.65rem;text-align:center;padding:20px">No results for "' + q + '"</div>';
|
||||
return;
|
||||
}
|
||||
el.innerHTML = d.results.map(r => {
|
||||
const role = r.role === 'user' ? '👤' : '🤖';
|
||||
const ts = new Date(r.created_at).toLocaleString('en-US', {month:'short',day:'numeric',hour:'2-digit',minute:'2-digit'});
|
||||
const snippet = r.content.length > 200 ? r.content.slice(0,197) + '…' : r.content;
|
||||
return `<div style="background:rgba(0,212,255,0.04);border:1px solid var(--panel-border);border-radius:4px;padding:10px 12px">
|
||||
<div style="display:flex;justify-content:space-between;margin-bottom:4px">
|
||||
<span style="font-family:var(--font-display);font-size:0.55rem;letter-spacing:1px;color:var(--cyan)">${role} ${r.role.toUpperCase()}</span>
|
||||
<span style="font-size:0.52rem;color:var(--text-dim)">${ts}</span>
|
||||
</div>
|
||||
<div style="font-size:0.68rem;color:var(--text-primary);line-height:1.4">${snippet.replace(/</g,'<')}</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
} catch(e) {
|
||||
el.innerHTML = '<div style="color:var(--red);font-size:0.65rem;text-align:center;padding:20px">Search failed</div>';
|
||||
}
|
||||
}
|
||||
document.getElementById('searchModal')?.addEventListener('click', e => {
|
||||
if (e.target === document.getElementById('searchModal')) closeSearchModal();
|
||||
});
|
||||
|
||||
// ── MOBILE PANEL SWITCHER ─────────────────────────────────────────────────────
|
||||
function mobSwitch(which) {
|
||||
if (window.innerWidth > 900) return;
|
||||
const panels = {left:'leftPanel', center:'centerPanel', right:'rightPanel'};
|
||||
Object.entries(panels).forEach(([k, id]) => {
|
||||
document.getElementById(id)?.classList.toggle('mob-active', k === which);
|
||||
});
|
||||
document.querySelectorAll('.mob-nav-btn').forEach(b => b.classList.remove('active'));
|
||||
document.getElementById('mob-btn-' + which)?.classList.add('active');
|
||||
if (which === 'right') loadNews();
|
||||
}
|
||||
function initMobile() {
|
||||
if (window.innerWidth > 900) return;
|
||||
['leftPanel','centerPanel','rightPanel'].forEach(id =>
|
||||
document.getElementById(id)?.classList.remove('mob-active'));
|
||||
document.getElementById('leftPanel')?.classList.add('mob-active');
|
||||
document.querySelectorAll('.mob-nav-btn').forEach(b => b.classList.remove('active'));
|
||||
document.getElementById('mob-btn-left')?.classList.add('active');
|
||||
}
|
||||
window.addEventListener('resize', initMobile);
|
||||
|
||||
</script>
|
||||
|
||||
<!-- VISION LIGHTBOX -->
|
||||
@@ -5009,5 +5146,35 @@ document.addEventListener('keydown', e => {
|
||||
<pre id="vision-lb-analysis"></pre>
|
||||
</div>
|
||||
|
||||
<!-- CHAT HISTORY SEARCH MODAL -->
|
||||
<div id="searchModal" style="display:none;position:fixed;inset:0;z-index:200;background:rgba(0,0,0,0.7);backdrop-filter:blur(4px);align-items:center;justify-content:center">
|
||||
<div style="background:#000d1f;border:1px solid var(--panel-border);border-radius:6px;padding:20px;width:min(620px,94vw);max-height:80vh;display:flex;flex-direction:column;gap:12px">
|
||||
<div style="display:flex;align-items:center;justify-content:space-between">
|
||||
<div style="font-family:var(--font-display);font-size:0.75rem;letter-spacing:3px;color:var(--cyan)">◈ CHAT HISTORY SEARCH</div>
|
||||
<button onclick="closeSearchModal()" style="background:none;border:none;color:var(--text-dim);cursor:pointer;font-size:1rem">✕</button>
|
||||
</div>
|
||||
<div style="display:flex;gap:8px">
|
||||
<input id="searchInput" type="text" placeholder="Search conversations…"
|
||||
style="flex:1;background:rgba(0,212,255,0.05);border:1px solid var(--panel-border);color:var(--text-primary);font-family:var(--font-mono);font-size:0.75rem;padding:8px 12px;outline:none"
|
||||
onkeydown="if(event.key==='Enter')runSearch()" autocomplete="off"/>
|
||||
<button onclick="runSearch()" style="background:rgba(0,212,255,0.1);border:1px solid rgba(0,212,255,0.3);color:var(--cyan);font-family:var(--font-display);font-size:0.6rem;letter-spacing:2px;padding:8px 16px;cursor:pointer">SEARCH</button>
|
||||
</div>
|
||||
<div id="searchResults" style="overflow-y:auto;flex:1;display:flex;flex-direction:column;gap:6px;min-height:60px">
|
||||
<div style="color:var(--text-dim);font-size:0.65rem;text-align:center;padding:20px">Type to search your JARVIS conversations</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav id="mobileNav">
|
||||
<button class="mob-nav-btn active" id="mob-btn-left" onclick="mobSwitch('left')">
|
||||
<span class="mob-icon">📊</span>STATS
|
||||
</button>
|
||||
<button class="mob-nav-btn" id="mob-btn-center" onclick="mobSwitch('center')">
|
||||
<span class="mob-icon">💬</span>CHAT
|
||||
</button>
|
||||
<button class="mob-nav-btn" id="mob-btn-right" onclick="mobSwitch('right')">
|
||||
<span class="mob-icon">🛰</span>INFO
|
||||
</button>
|
||||
</nav>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user