Spaces:
Runtime error
Runtime error
| <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,'&') | |
| .replace(/</g,'<') | |
| .replace(/>/g,'>') | |
| .replace(/"/g,'"') | |
| .replace(/\n/g,'<br>'); | |
| } | |
| function escAttr(s) { | |
| return String(s) | |
| .replace(/&/g,'&') | |
| .replace(/"/g,'"') | |
| .replace(/'/g,'''); | |
| } | |
| 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> | |