agent-memory / static /index.html
eriquesouza
Revise agent and documentation for ISP customer service PoC
eba72b3
Raw
History Blame Contribute Delete
40 kB
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Agent Memory β€” Fase 1</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600&family=Syne:wght@400;600;700;800&display=swap" rel="stylesheet">
<style>
:root {
--bg: #0B0E14;
--bg2: #111520;
--bg3: #181D2A;
--bg4: #1E2436;
--border: #252D3E;
--border2: #2E3A50;
--text: #D4DCF0;
--text2: #7E8FA8;
--text3: #4A5568;
--accent: #F0B429;
--accent2: #E09010;
--danger: #EF4444;
--episodic: #3B9EFF;
--semantic: #A78BFA;
--state: #FB923C;
--procedural:#34D399;
--seed-glow: rgba(240,180,41,0.15);
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: 'IBM Plex Mono', monospace;
background: var(--bg);
color: var(--text);
height: 100vh;
display: flex;
flex-direction: column;
overflow: hidden;
}
/* ── Header ─────────────────────────────────────────── */
header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 20px;
background: var(--bg2);
border-bottom: 1px solid var(--border);
flex-shrink: 0;
}
.logo {
display: flex;
align-items: center;
gap: 10px;
}
.logo-icon {
width: 28px; height: 28px;
background: var(--accent);
border-radius: 4px;
display: flex; align-items: center; justify-content: center;
font-size: 14px;
}
.logo-text {
font-family: 'Syne', sans-serif;
font-weight: 800;
font-size: 15px;
letter-spacing: 0.04em;
color: var(--text);
}
.logo-badge {
font-size: 9px;
background: var(--bg4);
border: 1px solid var(--border2);
color: var(--accent);
padding: 2px 6px;
border-radius: 2px;
letter-spacing: 0.1em;
font-weight: 600;
}
.header-right {
display: flex;
align-items: center;
gap: 12px;
}
.header-stat {
font-size: 10px;
color: var(--text3);
letter-spacing: 0.06em;
}
.header-stat span {
color: var(--accent);
font-weight: 600;
}
.btn-reset {
font-family: 'IBM Plex Mono', monospace;
font-size: 10px;
padding: 4px 10px;
background: transparent;
border: 1px solid var(--border2);
color: var(--text2);
border-radius: 3px;
cursor: pointer;
letter-spacing: 0.06em;
transition: all 0.15s;
}
.btn-reset:hover { border-color: var(--danger); color: var(--danger); }
/* ── Main layout ────────────────────────────────────── */
.layout {
display: flex;
flex: 1;
overflow: hidden;
}
/* ── Chat panel ─────────────────────────────────────── */
.chat-panel {
width: 45%;
display: flex;
flex-direction: column;
border-right: 1px solid var(--border);
}
.panel-label {
font-size: 9px;
letter-spacing: 0.12em;
color: var(--text3);
font-weight: 600;
padding: 8px 16px;
border-bottom: 1px solid var(--border);
background: var(--bg2);
text-transform: uppercase;
}
.messages {
flex: 1;
overflow-y: auto;
padding: 16px;
display: flex;
flex-direction: column;
gap: 14px;
scrollbar-width: thin;
scrollbar-color: var(--border2) transparent;
}
.msg {
display: flex;
flex-direction: column;
gap: 4px;
max-width: 88%;
}
.msg.user { align-self: flex-end; align-items: flex-end; }
.msg.agent { align-self: flex-start; align-items: flex-start; }
.msg-label {
font-size: 9px;
letter-spacing: 0.1em;
color: var(--text3);
text-transform: uppercase;
}
.msg-bubble {
font-size: 12px;
line-height: 1.6;
padding: 10px 14px;
border-radius: 4px;
white-space: pre-wrap;
}
.msg.user .msg-bubble {
background: var(--bg4);
border: 1px solid var(--border2);
color: var(--text);
}
.msg.agent .msg-bubble {
background: var(--bg3);
border: 1px solid var(--border);
color: var(--text);
}
.msg-meta {
font-size: 9px;
color: var(--text3);
display: flex;
gap: 6px;
align-items: center;
}
.mem-used-pill {
font-size: 8px;
background: rgba(240,180,41,0.12);
border: 1px solid rgba(240,180,41,0.3);
color: var(--accent);
padding: 1px 5px;
border-radius: 2px;
cursor: pointer;
transition: background 0.15s, border-color 0.15s;
}
.mem-used-pill:hover {
background: rgba(240,180,41,0.22);
border-color: rgba(240,180,41,0.5);
}
.new-mem-pill.clickable {
cursor: pointer;
transition: background 0.15s, border-color 0.15s;
}
.new-mem-pill.clickable:hover {
background: rgba(52,211,153,0.18);
border-color: rgba(52,211,153,0.45);
}
.new-mem-pill {
font-size: 8px;
background: rgba(52,211,153,0.1);
border: 1px solid rgba(52,211,153,0.3);
color: var(--procedural);
padding: 1px 5px;
border-radius: 2px;
}
.thinking {
display: flex;
gap: 5px;
align-items: center;
padding: 10px 14px;
background: var(--bg3);
border: 1px solid var(--border);
border-radius: 4px;
width: fit-content;
}
.dot {
width: 5px; height: 5px;
background: var(--text3);
border-radius: 50%;
animation: pulse 1.2s ease-in-out infinite;
}
.dot:nth-child(2) { animation-delay: 0.2s; }
.dot:nth-child(3) { animation-delay: 0.4s; }
@keyframes pulse {
0%, 80%, 100% { opacity: 0.3; transform: scale(0.8); }
40% { opacity: 1; transform: scale(1); }
}
/* ── Chat input ─────────────────────────────────────── */
.input-area {
padding: 12px 16px;
border-top: 1px solid var(--border);
background: var(--bg2);
}
.input-row {
display: flex;
gap: 8px;
align-items: flex-end;
}
.chat-input {
flex: 1;
font-family: 'IBM Plex Mono', monospace;
font-size: 12px;
background: var(--bg3);
border: 1px solid var(--border2);
color: var(--text);
padding: 10px 12px;
border-radius: 4px;
resize: none;
min-height: 40px;
max-height: 100px;
outline: none;
line-height: 1.5;
transition: border-color 0.15s;
}
.chat-input:focus { border-color: var(--accent); }
.chat-input::placeholder { color: var(--text3); }
.send-btn {
font-family: 'IBM Plex Mono', monospace;
font-size: 10px;
font-weight: 600;
letter-spacing: 0.08em;
padding: 10px 14px;
background: var(--accent);
color: #0B0E14;
border: none;
border-radius: 4px;
cursor: pointer;
transition: background 0.15s;
white-space: nowrap;
align-self: flex-end;
height: 40px;
}
.send-btn:hover { background: var(--accent2); }
.send-btn:disabled { opacity: 0.4; cursor: not-allowed; }
/* ── Warehouse panel ────────────────────────────────── */
.warehouse-panel {
flex: 1;
display: flex;
flex-direction: column;
background: var(--bg);
overflow: hidden;
}
.warehouse-header {
padding: 8px 16px;
border-bottom: 1px solid var(--border);
background: var(--bg2);
display: flex;
flex-direction: column;
gap: 8px;
flex-shrink: 0;
}
.warehouse-top {
display: flex;
align-items: center;
justify-content: space-between;
}
.filters {
display: flex;
gap: 4px;
flex-wrap: wrap;
}
.filter-btn {
font-family: 'IBM Plex Mono', monospace;
font-size: 9px;
font-weight: 600;
letter-spacing: 0.08em;
padding: 3px 8px;
border-radius: 2px;
border: 1px solid var(--border2);
background: transparent;
color: var(--text3);
cursor: pointer;
transition: all 0.15s;
text-transform: uppercase;
}
.filter-btn:hover { color: var(--text2); }
.filter-btn.active-all { border-color: var(--text2); color: var(--text); }
.filter-btn.active-episodic { border-color: var(--episodic); color: var(--episodic); background: rgba(59,158,255,0.08); }
.filter-btn.active-semantic { border-color: var(--semantic); color: var(--semantic); background: rgba(167,139,250,0.08); }
.filter-btn.active-state { border-color: var(--state); color: var(--state); background: rgba(251,146,60,0.08); }
.filter-btn.active-procedural{ border-color: var(--procedural); color: var(--procedural); background: rgba(52,211,153,0.08); }
.sort-select {
font-family: 'IBM Plex Mono', monospace;
font-size: 9px;
background: var(--bg3);
border: 1px solid var(--border2);
color: var(--text2);
padding: 3px 6px;
border-radius: 2px;
outline: none;
cursor: pointer;
}
.warehouse-count {
font-size: 9px;
color: var(--text3);
letter-spacing: 0.08em;
}
/* ── Memory grid ─────────────────────────────────────── */
.memory-grid {
flex: 1;
overflow-y: auto;
padding: 12px;
display: flex;
flex-direction: column;
gap: 8px;
scrollbar-width: thin;
scrollbar-color: var(--border2) transparent;
}
/* ── Memory card ─────────────────────────────────────── */
.mem-card {
background: var(--bg3);
border: 1px solid var(--border);
border-radius: 4px;
padding: 10px 12px;
position: relative;
overflow: visible;
transition: border-color 0.2s, box-shadow 0.2s;
cursor: pointer;
}
.mem-card:hover { border-color: var(--border2); }
.mem-card::before {
content: '';
position: absolute;
left: 0; top: 0; bottom: 0;
width: 3px;
}
.mem-card.episodic::before { background: var(--episodic); }
.mem-card.semantic::before { background: var(--semantic); }
.mem-card.state::before { background: var(--state); }
.mem-card.procedural::before { background: var(--procedural); }
.mem-card.seed-card {
background: linear-gradient(135deg, var(--bg3) 80%, rgba(240,180,41,0.04));
border-color: rgba(240,180,41,0.2);
}
@keyframes glow-used {
0% { box-shadow: 0 0 0 0 rgba(240,180,41,0); border-color: var(--border); }
20% { box-shadow: 0 0 12px 3px rgba(240,180,41,0.35); border-color: rgba(240,180,41,0.6); }
100%{ box-shadow: 0 0 0 0 rgba(240,180,41,0); border-color: var(--border2); }
}
.mem-card.just-used {
animation: glow-used 1.8s ease-out forwards;
}
@keyframes slide-in {
from { opacity: 0; transform: translateY(-10px); }
to { opacity: 1; transform: translateY(0); }
}
.mem-card.new-card {
animation: slide-in 0.35s ease-out forwards;
border-color: rgba(52,211,153,0.35);
}
.card-top {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 6px;
}
.card-left {
display: flex;
align-items: center;
gap: 6px;
}
.type-badge {
font-size: 8px;
font-weight: 600;
letter-spacing: 0.1em;
padding: 2px 6px;
border-radius: 2px;
text-transform: uppercase;
}
.type-badge.episodic { background: rgba(59,158,255,0.15); color: var(--episodic); }
.type-badge.semantic { background: rgba(167,139,250,0.15); color: var(--semantic); }
.type-badge.state { background: rgba(251,146,60,0.15); color: var(--state); }
.type-badge.procedural { background: rgba(52,211,153,0.15); color: var(--procedural); }
.source-badge {
font-size: 8px;
color: var(--text3);
letter-spacing: 0.06em;
}
.source-badge.seed { color: var(--accent); }
.card-actions {
display: flex;
gap: 6px;
}
.archive-btn {
font-size: 9px;
background: transparent;
border: none;
color: var(--text3);
cursor: pointer;
padding: 2px;
line-height: 1;
transition: color 0.15s;
font-family: 'IBM Plex Mono', monospace;
}
.archive-btn:hover { color: var(--danger); }
.card-summary {
font-family: 'Syne', sans-serif;
font-size: 12px;
font-weight: 600;
color: var(--text);
margin-bottom: 6px;
line-height: 1.4;
word-break: break-word;
}
.card-content {
font-size: 11px;
color: var(--text);
line-height: 1.6;
margin-bottom: 8px;
word-break: break-word;
white-space: pre-wrap;
max-height: 5.6em;
overflow: hidden;
position: relative;
}
.card-content::after {
content: '';
position: absolute;
left: 0; right: 0; bottom: 0;
height: 1.4em;
background: linear-gradient(transparent, var(--bg3));
pointer-events: none;
}
.mem-card.seed-card .card-content::after {
background: linear-gradient(transparent, var(--bg3));
}
.card-content-hint {
font-size: 9px;
color: var(--text3);
letter-spacing: 0.06em;
margin-bottom: 6px;
}
.card-tags {
display: flex;
flex-wrap: wrap;
gap: 4px;
margin-bottom: 8px;
}
.tag {
font-size: 8px;
padding: 1px 5px;
background: var(--bg4);
border: 1px solid var(--border2);
color: var(--text3);
border-radius: 2px;
letter-spacing: 0.04em;
}
.card-footer {
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 4px;
}
.card-stats {
display: flex;
gap: 10px;
font-size: 9px;
color: var(--text3);
letter-spacing: 0.04em;
}
.card-stats .val { color: var(--text2); }
.score-bar {
display: flex;
align-items: center;
gap: 5px;
}
.score-track {
width: 50px;
height: 3px;
background: var(--bg4);
border-radius: 2px;
overflow: hidden;
}
.score-fill {
height: 100%;
border-radius: 2px;
transition: width 0.4s ease;
}
.score-fill.episodic { background: var(--episodic); }
.score-fill.semantic { background: var(--semantic); }
.score-fill.state { background: var(--state); }
.score-fill.procedural { background: var(--procedural); }
.score-val {
font-size: 8px;
color: var(--text3);
width: 28px;
}
.card-dates-row {
display: flex;
align-items: center;
gap: 5px;
margin-bottom: 7px;
}
.date-label {
font-size: 9px;
color: var(--text3);
letter-spacing: 0.06em;
text-transform: uppercase;
}
.date-val {
font-size: 9px;
color: var(--text2);
letter-spacing: 0.03em;
}
.date-sep {
font-size: 9px;
color: var(--text3);
}
/* ── Empty state ─────────────────────────────────────── */
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 200px;
gap: 8px;
color: var(--text3);
font-size: 11px;
letter-spacing: 0.06em;
}
/* ── Scrollbar ───────────────────────────────────────── */
::-webkit-scrollbar { width: 4px; height: 4px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--border2); border-radius: 2px; }
/* ── Legend ──────────────────────────────────────────── */
.legend {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.legend-item {
display: flex;
align-items: center;
gap: 4px;
font-size: 8px;
color: var(--text3);
letter-spacing: 0.06em;
}
.legend-dot {
width: 6px; height: 6px;
border-radius: 50%;
flex-shrink: 0;
}
/* ── Memory detail modal ─────────────────────────────── */
.modal-overlay {
position: fixed;
inset: 0;
background: rgba(11, 14, 20, 0.82);
backdrop-filter: blur(4px);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
padding: 20px;
opacity: 0;
visibility: hidden;
transition: opacity 0.2s, visibility 0.2s;
}
.modal-overlay.open {
opacity: 1;
visibility: visible;
}
.modal-panel {
background: var(--bg2);
border: 1px solid var(--border2);
border-radius: 6px;
width: min(560px, 100%);
max-height: min(85vh, 720px);
display: flex;
flex-direction: column;
box-shadow: 0 16px 48px rgba(0, 0, 0, 0.45);
position: relative;
overflow: hidden;
}
.modal-panel::before {
content: '';
position: absolute;
left: 0; top: 0; bottom: 0;
width: 4px;
}
.modal-panel.episodic::before { background: var(--episodic); }
.modal-panel.semantic::before { background: var(--semantic); }
.modal-panel.state::before { background: var(--state); }
.modal-panel.procedural::before{ background: var(--procedural); }
.modal-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 12px;
padding: 16px 18px 12px 22px;
border-bottom: 1px solid var(--border);
flex-shrink: 0;
}
.modal-header-left {
display: flex;
flex-direction: column;
gap: 8px;
min-width: 0;
}
.modal-header-badges {
display: flex;
align-items: center;
gap: 6px;
flex-wrap: wrap;
}
.modal-id {
font-size: 10px;
color: var(--text3);
letter-spacing: 0.06em;
}
.modal-close {
font-family: 'IBM Plex Mono', monospace;
font-size: 14px;
background: transparent;
border: 1px solid var(--border2);
color: var(--text2);
width: 28px;
height: 28px;
border-radius: 3px;
cursor: pointer;
flex-shrink: 0;
line-height: 1;
transition: border-color 0.15s, color 0.15s;
}
.modal-close:hover { border-color: var(--danger); color: var(--danger); }
.modal-body {
padding: 14px 18px 18px 22px;
overflow-y: auto;
flex: 1;
display: flex;
flex-direction: column;
gap: 14px;
scrollbar-width: thin;
scrollbar-color: var(--border2) transparent;
}
.modal-section-label {
font-size: 9px;
color: var(--text3);
letter-spacing: 0.1em;
text-transform: uppercase;
margin-bottom: 6px;
}
.modal-summary {
font-family: 'Syne', sans-serif;
font-size: 15px;
font-weight: 700;
color: var(--text);
line-height: 1.4;
word-break: break-word;
}
.modal-content {
font-size: 12px;
color: var(--text);
line-height: 1.65;
white-space: pre-wrap;
word-break: break-word;
background: var(--bg3);
border: 1px solid var(--border);
border-radius: 4px;
padding: 12px 14px;
}
.modal-meta-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 8px 16px;
}
.modal-meta-item {
display: flex;
flex-direction: column;
gap: 2px;
}
.modal-meta-item .label {
font-size: 9px;
color: var(--text3);
letter-spacing: 0.08em;
text-transform: uppercase;
}
.modal-meta-item .value {
font-size: 11px;
color: var(--text2);
}
.modal-tags {
display: flex;
flex-wrap: wrap;
gap: 4px;
}
.modal-footer-actions {
display: flex;
gap: 8px;
padding-top: 4px;
}
.modal-btn-secondary {
font-family: 'IBM Plex Mono', monospace;
font-size: 10px;
padding: 6px 10px;
background: var(--bg3);
border: 1px solid var(--border2);
color: var(--text2);
border-radius: 3px;
cursor: pointer;
letter-spacing: 0.06em;
transition: border-color 0.15s, color 0.15s;
}
.modal-btn-secondary:hover { border-color: var(--accent); color: var(--accent); }
</style>
</head>
<body>
<header>
<div class="logo">
<div class="logo-icon">🧠</div>
<span class="logo-text">AGENT MEMORY</span>
<span class="logo-badge">FASE 1 β€” BASELINE</span>
</div>
<div class="header-right">
<span class="header-stat">memΓ³rias ativas: <span id="mem-count">β€”</span></span>
<div class="legend">
<div class="legend-item"><div class="legend-dot" style="background:var(--episodic)"></div>episΓ³dica</div>
<div class="legend-item"><div class="legend-dot" style="background:var(--semantic)"></div>semΓ’ntica</div>
<div class="legend-item"><div class="legend-dot" style="background:var(--state)"></div>estado</div>
<div class="legend-item"><div class="legend-dot" style="background:var(--procedural)"></div>procedural</div>
</div>
<button class="btn-reset" onclick="resetMemories()">β†Ί RESET</button>
</div>
</header>
<div class="layout">
<!-- ── Chat ───────────────────────────────────────── -->
<div class="chat-panel">
<div class="panel-label">// terminal β€” atendimento ISP</div>
<div class="messages" id="messages">
<div class="msg agent">
<div class="msg-label">agente</div>
<div class="msg-bubble">OlΓ‘. Sou o assistente de atendimento desta operadora de internet, TV e telefone.
Uso memΓ³rias de falas de clientes anteriores β€” exatamente como disseram β€” para tentar ajudar em situaΓ§Γ΅es parecidas: lentidΓ£o, quedas, fatura, cancelamento, instalaΓ§Γ£o, portabilidade e mais.
O painel Γ  direita mostra o que foi lembrado. Conte o seu problema que eu tento orientar com base nisso.</div>
</div>
</div>
<div class="input-area">
<div class="input-row">
<textarea
id="chat-input"
class="chat-input"
placeholder="descreva seu problema ou dΓΊvida..."
rows="1"
onkeydown="handleKey(event)"
oninput="autoResize(this)"
></textarea>
<button class="send-btn" id="send-btn" onclick="sendMessage()">ENVIAR</button>
</div>
</div>
</div>
<!-- ── Warehouse ──────────────────────────────────── -->
<div class="warehouse-panel">
<div class="warehouse-header">
<div class="warehouse-top">
<div class="panel-label" style="padding:0; border:none; background:none; font-size:9px;">// armazΓ©m de memΓ³rias</div>
<div style="display:flex; align-items:center; gap:8px;">
<span class="warehouse-count" id="filter-count"></span>
<select class="sort-select" id="sort-select" onchange="renderMemories()">
<option value="newest">↓ mais recente</option>
<option value="oldest">↑ mais antiga</option>
<option value="score">↓ relevΓ’ncia</option>
<option value="access">↓ mais acessada</option>
</select>
</div>
</div>
<div class="filters">
<button class="filter-btn active-all" data-filter="all" onclick="setFilter('all', this)">TODAS</button>
<button class="filter-btn" data-filter="episodic" onclick="setFilter('episodic', this)">EPISΓ“DICA</button>
<button class="filter-btn" data-filter="semantic" onclick="setFilter('semantic', this)">SEMΓ‚NTICA</button>
<button class="filter-btn" data-filter="state" onclick="setFilter('state', this)">ESTADO</button>
<button class="filter-btn" data-filter="procedural" onclick="setFilter('procedural', this)">PROCEDURAL</button>
</div>
</div>
<div class="memory-grid" id="memory-grid">
<div class="empty-state">
<span>β–  β–  β– </span>
<span>carregando armazΓ©m...</span>
</div>
</div>
</div>
</div>
<!-- Modal de detalhe da memΓ³ria -->
<div class="modal-overlay" id="memory-modal" onclick="closeModalOnBackdrop(event)">
<div class="modal-panel" id="memory-modal-panel" role="dialog" aria-labelledby="modal-summary">
<div class="modal-header">
<div class="modal-header-left">
<div class="modal-header-badges" id="modal-badges"></div>
<span class="modal-id" id="modal-id"></span>
</div>
<button class="modal-close" onclick="closeMemoryModal()" title="fechar" aria-label="fechar">βœ•</button>
</div>
<div class="modal-body" id="modal-body"></div>
</div>
</div>
<script>
// ── State ──────────────────────────────────────────────────────────────────
let allMemories = [];
let activeFilter = 'all';
let conversationHistory = [];
let highlightIds = new Set();
let newIds = new Set();
// ── Boot ───────────────────────────────────────────────────────────────────
async function boot() {
const res = await fetch('/api/memories');
const data = await res.json();
allMemories = data.memories;
renderMemories();
updateCount();
}
boot();
// ── Filter & Sort ──────────────────────────────────────────────────────────
function setFilter(filter, btn) {
activeFilter = filter;
document.querySelectorAll('.filter-btn').forEach(b => {
b.className = 'filter-btn';
});
btn.classList.add('active-' + filter);
renderMemories();
}
function getSorted(mems) {
const sort = document.getElementById('sort-select').value;
const arr = [...mems];
if (sort === 'newest') arr.sort((a, b) => new Date(b.created_at) - new Date(a.created_at));
if (sort === 'oldest') arr.sort((a, b) => new Date(a.created_at) - new Date(b.created_at));
if (sort === 'score') arr.sort((a, b) => b.relevance_score - a.relevance_score);
if (sort === 'access') arr.sort((a, b) => b.access_count - a.access_count);
return arr;
}
// ── Render memories ────────────────────────────────────────────────────────
function renderMemories() {
const grid = document.getElementById('memory-grid');
let filtered = activeFilter === 'all'
? allMemories
: allMemories.filter(m => m.type === activeFilter);
const sorted = getSorted(filtered);
document.getElementById('filter-count').textContent =
`${sorted.length} memΓ³ria${sorted.length !== 1 ? 's' : ''}`;
if (sorted.length === 0) {
grid.innerHTML = `<div class="empty-state"><span>β–  β–  β– </span><span>sem memΓ³rias nesta categoria</span></div>`;
return;
}
grid.innerHTML = '';
sorted.forEach(m => {
grid.appendChild(buildCard(m));
});
// Apply highlight animations
highlightIds.forEach(id => {
const el = document.getElementById('card-' + id);
if (el) {
el.classList.remove('just-used');
void el.offsetWidth; // force reflow
el.classList.add('just-used');
}
});
newIds.forEach(id => {
const el = document.getElementById('card-' + id);
if (el) el.classList.add('new-card');
});
}
function buildCard(m) {
const card = document.createElement('div');
card.className = `mem-card ${m.type}${m.source === 'seed' ? ' seed-card' : ''}`;
card.id = 'card-' + m.id;
card.onclick = (e) => {
if (e.target.closest('.archive-btn')) return;
openMemoryModal(m.id);
};
const date = formatDate(m.created_at);
const score = m.relevance_score;
const scorePercent = Math.round(score * 100);
const tagsHtml = m.context_tags.length > 0
? m.context_tags.map(t => `<span class="tag">${t}</span>`).join('')
: '';
const lastAcc = m.last_accessed ? formatDate(m.last_accessed) : 'β€”';
card.innerHTML = `
<div class="card-top">
<div class="card-left">
<span class="type-badge ${m.type}">${m.type}</span>
<span class="source-badge ${m.source === 'seed' ? 'seed' : ''}">
${m.source === 'seed' ? '⚑ seed' : m.source === 'agent' ? 'agente' : 'usuÑrio'}
</span>
</div>
<div class="card-actions">
<button class="archive-btn" onclick="event.stopPropagation(); archiveCard('${m.id}')" title="arquivar">βœ•</button>
</div>
</div>
<div class="card-summary">${escHtml(m.summary)}</div>
<div class="card-dates-row">
<span class="date-label">criada</span>
<span class="date-val">${date}</span>
<span class="date-sep">Β·</span>
<span class="date-label">acessada</span>
<span class="date-val">${lastAcc}</span>
</div>
<div class="card-content-hint">trecho Β· clique para ver completo</div>
<div class="card-content">${escHtml(m.content)}</div>
${tagsHtml ? `<div class="card-tags">${tagsHtml}</div>` : ''}
<div class="card-footer">
<div class="card-stats">
<span>ID: <span class="val">${m.id}</span></span>
<span>acessos: <span class="val">${m.access_count}</span></span>
</div>
<div class="score-bar">
<div class="score-track">
<div class="score-fill ${m.type}" style="width:${scorePercent}%"></div>
</div>
<span class="score-val">${score.toFixed(2)}</span>
</div>
</div>
`;
return card;
}
// ── Memory modal ───────────────────────────────────────────────────────────
function findMemory(id) {
return allMemories.find(m => m.id === id);
}
function sourceLabel(source) {
if (source === 'seed') return '⚑ seed';
if (source === 'agent') return 'agente';
return 'usuΓ‘rio';
}
function openMemoryModal(id) {
const m = findMemory(id);
if (!m) return;
const overlay = document.getElementById('memory-modal');
const panel = document.getElementById('memory-modal-panel');
panel.className = `modal-panel ${m.type}`;
document.getElementById('modal-badges').innerHTML = `
<span class="type-badge ${m.type}">${m.type}</span>
<span class="source-badge ${m.source === 'seed' ? 'seed' : ''}">${sourceLabel(m.source)}</span>
${m.active === false ? '<span class="source-badge">arquivada</span>' : ''}
`;
document.getElementById('modal-id').textContent = `ID: ${m.id}`;
const scorePercent = Math.round(m.relevance_score * 100);
const tagsHtml = m.context_tags.length > 0
? m.context_tags.map(t => `<span class="tag">${escHtml(t)}</span>`).join('')
: '<span class="value">β€”</span>';
document.getElementById('modal-body').innerHTML = `
<div>
<div class="modal-section-label">resumo</div>
<div class="modal-summary" id="modal-summary">${escHtml(m.summary)}</div>
</div>
<div>
<div class="modal-section-label">conteΓΊdo completo</div>
<div class="modal-content">${escHtml(m.content)}</div>
</div>
<div>
<div class="modal-section-label">tags de contexto</div>
<div class="modal-tags">${tagsHtml}</div>
</div>
<div>
<div class="modal-section-label">metadados</div>
<div class="modal-meta-grid">
<div class="modal-meta-item">
<span class="label">criada em</span>
<span class="value">${formatDate(m.created_at)}</span>
</div>
<div class="modal-meta-item">
<span class="label">ΓΊltimo acesso</span>
<span class="value">${m.last_accessed ? formatDate(m.last_accessed) : 'β€”'}</span>
</div>
<div class="modal-meta-item">
<span class="label">acessos</span>
<span class="value">${m.access_count}</span>
</div>
<div class="modal-meta-item">
<span class="label">relevΓ’ncia</span>
<span class="value">${m.relevance_score.toFixed(2)} (${scorePercent}%)</span>
</div>
<div class="modal-meta-item">
<span class="label">decay rate</span>
<span class="value">${m.decay_rate}</span>
</div>
<div class="modal-meta-item">
<span class="label">status</span>
<span class="value">${m.active !== false ? 'ativa' : 'arquivada'}</span>
</div>
</div>
</div>
<div class="modal-footer-actions">
<button type="button" class="modal-btn-secondary" onclick="scrollToCardFromModal('${m.id}')">ver no armazΓ©m</button>
</div>
`;
overlay.classList.add('open');
document.body.style.overflow = 'hidden';
}
function closeMemoryModal() {
document.getElementById('memory-modal').classList.remove('open');
document.body.style.overflow = '';
}
function closeModalOnBackdrop(e) {
if (e.target.id === 'memory-modal') closeMemoryModal();
}
function scrollToCardFromModal(id) {
closeMemoryModal();
scrollToCard(id);
}
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') closeMemoryModal();
});
// ── Archive ────────────────────────────────────────────────────────────────
async function archiveCard(id) {
await fetch(`/api/memories/${id}`, { method: 'DELETE' });
allMemories = allMemories.filter(m => m.id !== id);
renderMemories();
updateCount();
}
// ── Reset ──────────────────────────────────────────────────────────────────
async function resetMemories() {
if (!confirm('Resetar todas as memΓ³rias para o estado inicial (seeds)?')) return;
const res = await fetch('/api/memories/reset', { method: 'POST' });
const data = await res.json();
const fresh = await fetch('/api/memories');
const freshData = await fresh.json();
allMemories = freshData.memories;
highlightIds.clear();
newIds.clear();
renderMemories();
updateCount();
}
// ── Chat ───────────────────────────────────────────────────────────────────
function handleKey(e) {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
sendMessage();
}
}
function autoResize(el) {
el.style.height = 'auto';
el.style.height = Math.min(el.scrollHeight, 100) + 'px';
}
async function sendMessage() {
const input = document.getElementById('chat-input');
const msg = input.value.trim();
if (!msg) return;
const sendBtn = document.getElementById('send-btn');
sendBtn.disabled = true;
input.value = '';
input.style.height = 'auto';
// Add user message
appendMsg('user', msg);
conversationHistory.push({ role: 'user', content: msg });
// Show thinking indicator
const thinkingId = showThinking();
try {
const res = await fetch('/api/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
message: msg,
conversation_history: conversationHistory.slice(-6),
}),
});
const data = await res.json();
removeThinking(thinkingId);
// Update warehouse before UI so modal/pills resolve IDs
highlightIds = new Set(data.memories_used);
newIds = new Set(data.new_memories.map(m => m.id));
allMemories = data.all_memories;
appendAgentMsg(data);
conversationHistory.push({ role: 'assistant', content: data.response });
renderMemories();
updateCount();
// Clear highlights after animation
setTimeout(() => {
highlightIds.clear();
newIds.clear();
}, 3000);
} catch (err) {
removeThinking(thinkingId);
appendMsg('agent', `[erro] ${err.message}`);
}
sendBtn.disabled = false;
input.focus();
}
function appendMsg(role, text) {
const msgs = document.getElementById('messages');
const div = document.createElement('div');
div.className = `msg ${role}`;
div.innerHTML = `
<div class="msg-label">${role === 'user' ? 'vocΓͺ' : 'agente'}</div>
<div class="msg-bubble">${escHtml(text)}</div>
`;
msgs.appendChild(div);
msgs.scrollTop = msgs.scrollHeight;
}
function appendAgentMsg(data) {
const msgs = document.getElementById('messages');
const div = document.createElement('div');
div.className = 'msg agent';
const usedPills = data.memories_used.length > 0
? data.memories_used.map(id => {
const mem = (data.all_memories || []).find(m => m.id === id)
|| data.new_memories.find(m => m.id === id)
|| findMemory(id);
const label = mem
? (mem.summary.length > 42 ? mem.summary.slice(0, 42) + '…' : mem.summary)
: `mem:${id}`;
return `<span class="mem-used-pill" onclick="openMemoryModal('${id}')" title="${escAttr(mem ? mem.summary : id)}">${escHtml(label)}</span>`;
}).join(' ')
: '';
const newPills = data.new_memories.length > 0
? data.new_memories.map(m =>
`<span class="new-mem-pill clickable" onclick="openMemoryModal('${m.id}')" title="${escAttr(m.summary)}">+ ${escHtml(m.summary.slice(0, 36))}${m.summary.length > 36 ? '…' : ''}</span>`
).join(' ')
: '';
const meta = (usedPills || newPills)
? `<div class="msg-meta">
${usedPills ? `<span style="color:var(--text3);font-size:8px;">usou:</span> ${usedPills}` : ''}
${newPills ? `<span style="color:var(--text3);font-size:8px;margin-left:6px;">criou:</span> ${newPills}` : ''}
</div>`
: '';
div.innerHTML = `
<div class="msg-label">agente</div>
<div class="msg-bubble">${escHtml(data.response)}</div>
${meta}
`;
msgs.appendChild(div);
msgs.scrollTop = msgs.scrollHeight;
}
function scrollToCard(id) {
const el = document.getElementById('card-' + id);
if (el) {
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
el.classList.remove('just-used');
void el.offsetWidth;
el.classList.add('just-used');
}
}
let thinkingCounter = 0;
function showThinking() {
const id = 'thinking-' + (++thinkingCounter);
const msgs = document.getElementById('messages');
const div = document.createElement('div');
div.id = id;
div.className = 'msg agent';
div.innerHTML = `
<div class="msg-label">agente</div>
<div class="thinking">
<div class="dot"></div>
<div class="dot"></div>
<div class="dot"></div>
</div>
`;
msgs.appendChild(div);
msgs.scrollTop = msgs.scrollHeight;
return id;
}
function removeThinking(id) {
const el = document.getElementById(id);
if (el) el.remove();
}
function updateCount() {
const active = allMemories.filter(m => m.active !== false).length;
document.getElementById('mem-count').textContent = active;
}
// ── Utils ──────────────────────────────────────────────────────────────────
function escHtml(s) {
return String(s)
.replace(/&/g,'&amp;')
.replace(/</g,'&lt;')
.replace(/>/g,'&gt;')
.replace(/"/g,'&quot;')
.replace(/\n/g,'<br>');
}
function escAttr(s) {
return String(s)
.replace(/&/g,'&amp;')
.replace(/"/g,'&quot;')
.replace(/'/g,'&#39;');
}
function formatDate(iso) {
if (!iso || iso === '') return 'β€”';
try {
// Normalise: Python isoformat uses +00:00 which JS handles, but guard anyway
const normalised = iso.replace(' ', 'T');
const d = new Date(normalised);
if (isNaN(d.getTime())) return iso;
return d.toLocaleDateString('pt-BR', { day: '2-digit', month: '2-digit', year: '2-digit' })
+ ' ' + d.toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' });
} catch { return iso; }
}
</script>
</body>
</html>