This repository has been archived on 2026-03-24. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
Timmy-time-dashboard/src/dashboard/templates/nexus.html
2026-03-24 02:50:40 +00:00

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 &mdash; 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 %}