forked from Rockachopa/Timmy-time-dashboard
Co-authored-by: Perplexity Computer <perplexity@tower.local> Co-committed-by: Perplexity Computer <perplexity@tower.local>
387 lines
15 KiB
HTML
387 lines
15 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}Nexus{% endblock %}
|
|
|
|
{% block extra_styles %}{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="container-fluid nexus-layout py-3">
|
|
|
|
<div class="nexus-header mb-3">
|
|
<div class="d-flex justify-content-between align-items-center">
|
|
<div>
|
|
<div class="nexus-title">// NEXUS</div>
|
|
<div class="nexus-subtitle">
|
|
Persistent conversational awareness — always present, always learning.
|
|
</div>
|
|
</div>
|
|
<!-- Sovereignty Pulse badge -->
|
|
<div class="nexus-pulse-badge" id="nexus-pulse-badge">
|
|
<span class="nexus-pulse-dot nexus-pulse-{{ pulse.health }}"></span>
|
|
<span class="nexus-pulse-label">SOVEREIGNTY</span>
|
|
<span class="nexus-pulse-value" id="pulse-overall">{{ pulse.overall_pct }}%</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="nexus-grid-v2">
|
|
|
|
<!-- ── LEFT: Conversation ────────────────────────────────── -->
|
|
<div class="nexus-chat-col">
|
|
<div class="card mc-panel nexus-chat-panel">
|
|
<div class="card-header mc-panel-header d-flex justify-content-between align-items-center">
|
|
<span>// CONVERSATION</span>
|
|
<div class="d-flex align-items-center gap-2">
|
|
<span class="nexus-msg-count" id="nexus-msg-count"
|
|
title="Messages in this session">{{ messages|length }} msgs</span>
|
|
<button class="mc-btn mc-btn-sm"
|
|
hx-delete="/nexus/history"
|
|
hx-target="#nexus-chat-log"
|
|
hx-swap="beforeend"
|
|
hx-confirm="Clear nexus conversation?">
|
|
CLEAR
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card-body p-2" id="nexus-chat-log">
|
|
{% for msg in messages %}
|
|
<div class="chat-message {{ 'user' if msg.role == 'user' else 'agent' }}">
|
|
<div class="msg-meta">
|
|
{{ 'YOU' if msg.role == 'user' else 'TIMMY' }} // {{ msg.timestamp }}
|
|
</div>
|
|
<div class="msg-body {% if msg.role == 'assistant' %}timmy-md{% endif %}">
|
|
{{ msg.content | e }}
|
|
</div>
|
|
</div>
|
|
{% else %}
|
|
<div class="nexus-empty-state">
|
|
Nexus is ready. Start a conversation — memories will surface in real time.
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
|
|
<div class="card-footer p-2">
|
|
<form hx-post="/nexus/chat"
|
|
hx-target="#nexus-chat-log"
|
|
hx-swap="beforeend"
|
|
hx-on::after-request="this.reset(); document.getElementById('nexus-chat-log').scrollTop = 999999;">
|
|
<div class="d-flex gap-2">
|
|
<input type="text"
|
|
name="message"
|
|
id="nexus-input"
|
|
class="mc-search-input flex-grow-1"
|
|
placeholder="Talk to Timmy..."
|
|
autocomplete="off"
|
|
required>
|
|
<button type="submit" class="mc-btn mc-btn-primary">SEND</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ── RIGHT: Awareness sidebar ──────────────────────────── -->
|
|
<div class="nexus-sidebar-col">
|
|
|
|
<!-- Cognitive State Panel -->
|
|
<div class="card mc-panel nexus-cognitive-panel mb-3">
|
|
<div class="card-header mc-panel-header">
|
|
<span>// COGNITIVE STATE</span>
|
|
<span class="nexus-engagement-badge" id="cog-engagement">
|
|
{{ introspection.cognitive.engagement | upper }}
|
|
</span>
|
|
</div>
|
|
<div class="card-body p-2">
|
|
<div class="nexus-cog-grid">
|
|
<div class="nexus-cog-item">
|
|
<div class="nexus-cog-label">MOOD</div>
|
|
<div class="nexus-cog-value" id="cog-mood">{{ introspection.cognitive.mood }}</div>
|
|
</div>
|
|
<div class="nexus-cog-item">
|
|
<div class="nexus-cog-label">FOCUS</div>
|
|
<div class="nexus-cog-value nexus-cog-focus" id="cog-focus">
|
|
{{ introspection.cognitive.focus_topic or '—' }}
|
|
</div>
|
|
</div>
|
|
<div class="nexus-cog-item">
|
|
<div class="nexus-cog-label">DEPTH</div>
|
|
<div class="nexus-cog-value" id="cog-depth">{{ introspection.cognitive.conversation_depth }}</div>
|
|
</div>
|
|
<div class="nexus-cog-item">
|
|
<div class="nexus-cog-label">INITIATIVE</div>
|
|
<div class="nexus-cog-value nexus-cog-focus" id="cog-initiative">
|
|
{{ introspection.cognitive.last_initiative or '—' }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% if introspection.cognitive.active_commitments %}
|
|
<div class="nexus-commitments mt-2">
|
|
<div class="nexus-cog-label">ACTIVE COMMITMENTS</div>
|
|
{% for c in introspection.cognitive.active_commitments %}
|
|
<div class="nexus-commitment-item">{{ c | e }}</div>
|
|
{% endfor %}
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Recent Thoughts Panel -->
|
|
<div class="card mc-panel nexus-thoughts-panel mb-3">
|
|
<div class="card-header mc-panel-header">
|
|
<span>// THOUGHT STREAM</span>
|
|
</div>
|
|
<div class="card-body p-2" id="nexus-thoughts-body">
|
|
{% if introspection.recent_thoughts %}
|
|
{% for t in introspection.recent_thoughts %}
|
|
<div class="nexus-thought-item">
|
|
<div class="nexus-thought-meta">
|
|
<span class="nexus-thought-seed">{{ t.seed_type }}</span>
|
|
<span class="nexus-thought-time">{{ t.created_at[:16] }}</span>
|
|
</div>
|
|
<div class="nexus-thought-content">{{ t.content | e }}</div>
|
|
</div>
|
|
{% endfor %}
|
|
{% else %}
|
|
<div class="nexus-empty-state">No thoughts yet. The thinking engine will populate this.</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Sovereignty Pulse Detail -->
|
|
<div class="card mc-panel nexus-sovereignty-panel mb-3">
|
|
<div class="card-header mc-panel-header">
|
|
<span>// SOVEREIGNTY PULSE</span>
|
|
<span class="nexus-health-badge nexus-health-{{ pulse.health }}" id="pulse-health">
|
|
{{ pulse.health | upper }}
|
|
</span>
|
|
</div>
|
|
<div class="card-body p-2">
|
|
<div class="nexus-pulse-meters" id="nexus-pulse-meters">
|
|
{% for layer in pulse.layers %}
|
|
<div class="nexus-pulse-layer">
|
|
<div class="nexus-pulse-layer-label">{{ layer.name | upper }}</div>
|
|
<div class="nexus-pulse-bar-track">
|
|
<div class="nexus-pulse-bar-fill" style="width: {{ layer.sovereign_pct }}%"></div>
|
|
</div>
|
|
<div class="nexus-pulse-layer-pct">{{ layer.sovereign_pct }}%</div>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
<div class="nexus-pulse-stats mt-2">
|
|
<div class="nexus-pulse-stat">
|
|
<span class="nexus-pulse-stat-label">Crystallizations</span>
|
|
<span class="nexus-pulse-stat-value" id="pulse-cryst">{{ pulse.crystallizations_last_hour }}</span>
|
|
</div>
|
|
<div class="nexus-pulse-stat">
|
|
<span class="nexus-pulse-stat-label">API Independence</span>
|
|
<span class="nexus-pulse-stat-value" id="pulse-api-indep">{{ pulse.api_independence_pct }}%</span>
|
|
</div>
|
|
<div class="nexus-pulse-stat">
|
|
<span class="nexus-pulse-stat-label">Total Events</span>
|
|
<span class="nexus-pulse-stat-value" id="pulse-events">{{ pulse.total_events }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Live Memory Context -->
|
|
<div class="card mc-panel nexus-memory-panel mb-3">
|
|
<div class="card-header mc-panel-header">
|
|
<span>// LIVE MEMORY</span>
|
|
<span class="badge ms-2" style="background:var(--purple-dim, rgba(168,85,247,0.15)); color:var(--purple);">
|
|
{{ stats.total_entries }} stored
|
|
</span>
|
|
</div>
|
|
<div class="card-body p-2">
|
|
<div id="nexus-memory-panel" class="nexus-memory-hits">
|
|
<div class="nexus-memory-label">Relevant memories appear here as you chat.</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Session Analytics -->
|
|
<div class="card mc-panel nexus-analytics-panel mb-3">
|
|
<div class="card-header mc-panel-header">// SESSION ANALYTICS</div>
|
|
<div class="card-body p-2">
|
|
<div class="nexus-analytics-grid" id="nexus-analytics">
|
|
<div class="nexus-analytics-item">
|
|
<span class="nexus-analytics-label">Messages</span>
|
|
<span class="nexus-analytics-value" id="analytics-msgs">{{ introspection.analytics.total_messages }}</span>
|
|
</div>
|
|
<div class="nexus-analytics-item">
|
|
<span class="nexus-analytics-label">Avg Response</span>
|
|
<span class="nexus-analytics-value" id="analytics-avg">{{ introspection.analytics.avg_response_length }} chars</span>
|
|
</div>
|
|
<div class="nexus-analytics-item">
|
|
<span class="nexus-analytics-label">Memory Hits</span>
|
|
<span class="nexus-analytics-value" id="analytics-mem">{{ introspection.analytics.memory_hits_total }}</span>
|
|
</div>
|
|
<div class="nexus-analytics-item">
|
|
<span class="nexus-analytics-label">Duration</span>
|
|
<span class="nexus-analytics-value" id="analytics-dur">{{ introspection.analytics.session_duration_minutes }} min</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Teaching Panel -->
|
|
<div class="card mc-panel nexus-teach-panel">
|
|
<div class="card-header mc-panel-header">// TEACH TIMMY</div>
|
|
<div class="card-body p-2">
|
|
<form hx-post="/nexus/teach"
|
|
hx-target="#nexus-teach-response"
|
|
hx-swap="innerHTML"
|
|
hx-on::after-request="this.reset()">
|
|
<div class="d-flex gap-2 mb-2">
|
|
<input type="text"
|
|
name="fact"
|
|
class="mc-search-input flex-grow-1"
|
|
placeholder="e.g. I prefer dark themes"
|
|
required>
|
|
<button type="submit" class="mc-btn mc-btn-primary">TEACH</button>
|
|
</div>
|
|
</form>
|
|
<div id="nexus-teach-response"></div>
|
|
|
|
<div class="nexus-facts-header mt-3">// KNOWN FACTS</div>
|
|
<ul class="nexus-facts-list" id="nexus-facts-list">
|
|
{% for fact in facts %}
|
|
<li class="nexus-fact-item">{{ fact.content | e }}</li>
|
|
{% else %}
|
|
<li class="nexus-fact-empty">No personal facts stored yet.</li>
|
|
{% endfor %}
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
|
|
</div><!-- /sidebar -->
|
|
</div><!-- /nexus-grid -->
|
|
|
|
</div>
|
|
|
|
<!-- WebSocket for live Nexus updates -->
|
|
<script>
|
|
(function() {
|
|
var wsProto = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
var wsUrl = wsProto + '//' + location.host + '/nexus/ws';
|
|
var ws = null;
|
|
var reconnectDelay = 2000;
|
|
|
|
function connect() {
|
|
ws = new WebSocket(wsUrl);
|
|
ws.onmessage = function(e) {
|
|
try {
|
|
var data = JSON.parse(e.data);
|
|
if (data.type === 'nexus_state') {
|
|
updateCognitive(data.introspection.cognitive);
|
|
updateThoughts(data.introspection.recent_thoughts);
|
|
updateAnalytics(data.introspection.analytics);
|
|
updatePulse(data.sovereignty_pulse);
|
|
}
|
|
} catch(err) { /* ignore parse errors */ }
|
|
};
|
|
ws.onclose = function() {
|
|
setTimeout(connect, reconnectDelay);
|
|
};
|
|
ws.onerror = function() { ws.close(); };
|
|
}
|
|
|
|
function updateCognitive(c) {
|
|
var el;
|
|
el = document.getElementById('cog-mood');
|
|
if (el) el.textContent = c.mood;
|
|
el = document.getElementById('cog-engagement');
|
|
if (el) el.textContent = c.engagement.toUpperCase();
|
|
el = document.getElementById('cog-focus');
|
|
if (el) el.textContent = c.focus_topic || '\u2014';
|
|
el = document.getElementById('cog-depth');
|
|
if (el) el.textContent = c.conversation_depth;
|
|
el = document.getElementById('cog-initiative');
|
|
if (el) el.textContent = c.last_initiative || '\u2014';
|
|
}
|
|
|
|
function updateThoughts(thoughts) {
|
|
var container = document.getElementById('nexus-thoughts-body');
|
|
if (!container || !thoughts || thoughts.length === 0) return;
|
|
var html = '';
|
|
for (var i = 0; i < thoughts.length; i++) {
|
|
var t = thoughts[i];
|
|
html += '<div class="nexus-thought-item">'
|
|
+ '<div class="nexus-thought-meta">'
|
|
+ '<span class="nexus-thought-seed">' + escHtml(t.seed_type) + '</span>'
|
|
+ '<span class="nexus-thought-time">' + escHtml((t.created_at || '').substring(0,16)) + '</span>'
|
|
+ '</div>'
|
|
+ '<div class="nexus-thought-content">' + escHtml(t.content) + '</div>'
|
|
+ '</div>';
|
|
}
|
|
container.innerHTML = html;
|
|
}
|
|
|
|
function updateAnalytics(a) {
|
|
var el;
|
|
el = document.getElementById('analytics-msgs');
|
|
if (el) el.textContent = a.total_messages;
|
|
el = document.getElementById('analytics-avg');
|
|
if (el) el.textContent = a.avg_response_length + ' chars';
|
|
el = document.getElementById('analytics-mem');
|
|
if (el) el.textContent = a.memory_hits_total;
|
|
el = document.getElementById('analytics-dur');
|
|
if (el) el.textContent = a.session_duration_minutes + ' min';
|
|
}
|
|
|
|
function updatePulse(p) {
|
|
var el;
|
|
el = document.getElementById('pulse-overall');
|
|
if (el) el.textContent = p.overall_pct + '%';
|
|
el = document.getElementById('pulse-health');
|
|
if (el) {
|
|
el.textContent = p.health.toUpperCase();
|
|
el.className = 'nexus-health-badge nexus-health-' + p.health;
|
|
}
|
|
el = document.getElementById('pulse-cryst');
|
|
if (el) el.textContent = p.crystallizations_last_hour;
|
|
el = document.getElementById('pulse-api-indep');
|
|
if (el) el.textContent = p.api_independence_pct + '%';
|
|
el = document.getElementById('pulse-events');
|
|
if (el) el.textContent = p.total_events;
|
|
|
|
// Update pulse badge dot
|
|
var badge = document.getElementById('nexus-pulse-badge');
|
|
if (badge) {
|
|
var dot = badge.querySelector('.nexus-pulse-dot');
|
|
if (dot) {
|
|
dot.className = 'nexus-pulse-dot nexus-pulse-' + p.health;
|
|
}
|
|
}
|
|
|
|
// Update layer bars
|
|
var meters = document.getElementById('nexus-pulse-meters');
|
|
if (meters && p.layers) {
|
|
var html = '';
|
|
for (var i = 0; i < p.layers.length; i++) {
|
|
var l = p.layers[i];
|
|
html += '<div class="nexus-pulse-layer">'
|
|
+ '<div class="nexus-pulse-layer-label">' + escHtml(l.name.toUpperCase()) + '</div>'
|
|
+ '<div class="nexus-pulse-bar-track">'
|
|
+ '<div class="nexus-pulse-bar-fill" style="width:' + l.sovereign_pct + '%"></div>'
|
|
+ '</div>'
|
|
+ '<div class="nexus-pulse-layer-pct">' + l.sovereign_pct + '%</div>'
|
|
+ '</div>';
|
|
}
|
|
meters.innerHTML = html;
|
|
}
|
|
}
|
|
|
|
function escHtml(s) {
|
|
if (!s) return '';
|
|
var d = document.createElement('div');
|
|
d.textContent = s;
|
|
return d.innerHTML;
|
|
}
|
|
|
|
connect();
|
|
})();
|
|
</script>
|
|
{% endblock %}
|