| | <!DOCTYPE html> |
| | <html lang="ko"> |
| | <head> |
| | <meta charset="UTF-8"> |
| | <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| | <title>μλμ΄ λ§λ² AI</title> |
| | <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script> |
| | <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> |
| | <style> |
| | :root{--primary-color:#6f42c1;--secondary-color:#563d7c;--dark-bg:#121212;--card-bg:#1e1e1e;--text-color:#f8f9fa;--border-color:#333;--hover-color:#8a5cf6;--memory-color:#4a9eff;--streaming-color:#ff6b6b;--warning-color:#ffc107;--table-header:#2d1f3d;--table-border:#6f42c1;--table-row-even:#1a1a2e;--table-row-hover:#2a2040;--safe-color:#28a745;--caution-color:#ffc107;--danger-color:#dc3545;--critical-color:#ff0000} |
| | *{box-sizing:border-box;margin:0;padding:0} |
| | body{font-family:"SF Pro Display",-apple-system,BlinkMacSystemFont,"Noto Sans KR",sans-serif;background:linear-gradient(135deg,var(--dark-bg) 0%,#1a1a2e 100%);color:var(--text-color);min-height:100vh;display:flex;flex-direction:column} |
| | .container{max-width:1600px;margin:0 auto;padding:15px;flex-grow:1;display:flex;flex-direction:column;width:100%} |
| | .header{text-align:center;padding:10px 0;border-bottom:1px solid var(--border-color);margin-bottom:15px;flex-shrink:0;background:rgba(30,30,30,0.95);backdrop-filter:blur(10px)} |
| | .main-content{display:flex;gap:15px;flex-grow:1;min-height:0} |
| | .sidebar{width:320px;flex-shrink:0;display:flex;flex-direction:column;gap:12px;overflow-y:auto;max-height:calc(100vh - 120px);padding-right:5px} |
| | .sidebar::-webkit-scrollbar{width:6px} |
| | .sidebar::-webkit-scrollbar-thumb{background:linear-gradient(135deg,var(--primary-color),var(--secondary-color));border-radius:6px} |
| | .chat-section{flex-grow:1;display:flex;flex-direction:column;min-width:0} |
| | .logo{display:flex;align-items:center;justify-content:center;gap:15px} |
| | .logo h1{background:linear-gradient(135deg,#ff6b9d,#c44569);-webkit-background-clip:text;background-clip:text;color:transparent;font-size:26px;letter-spacing:1px;font-weight:700} |
| | .current-time{font-size:12px;color:#888;margin-top:5px} |
| | .storage-info{font-size:10px;color:#666;margin-top:3px} |
| | .storage-info.persistent{color:#28a745} |
| | .storage-info.local{color:#ffc107} |
| | .email-section{background:rgba(30,30,30,0.8);backdrop-filter:blur(10px);border-radius:10px;padding:14px;border:1px solid var(--border-color);margin-bottom:12px} |
| | .email-section label{font-size:12px;color:#888;display:block;margin-bottom:6px} |
| | .email-section input{width:100%;padding:10px;background:rgba(0,0,0,0.3);border:1px solid #333;border-radius:6px;color:white;font-size:13px} |
| | .email-section input:focus{outline:none;border-color:var(--primary-color);box-shadow:0 0 10px rgba(111,66,193,0.2)} |
| | .sidebar-section{background:rgba(30,30,30,0.8);backdrop-filter:blur(10px);border-radius:10px;padding:14px;border:1px solid var(--border-color);transition:all 0.3s ease} |
| | .sidebar-section:hover{border-color:var(--primary-color);box-shadow:0 0 15px rgba(111,66,193,0.1)} |
| | .sidebar-section h3{margin:0 0 12px 0;font-size:13px;text-transform:uppercase;letter-spacing:0.5px;color:#999;font-weight:600;display:flex;align-items:center;justify-content:space-between} |
| | .section-toggle{cursor:pointer;font-size:16px;transition:transform 0.3s} |
| | .section-toggle.collapsed{transform:rotate(-90deg)} |
| | .section-content{transition:max-height 0.3s ease-out;overflow:hidden} |
| | .section-content.collapsed{max-height:0!important} |
| | .streaming-indicator{display:none;align-items:center;gap:8px;padding:8px 12px;background:linear-gradient(135deg,rgba(255,107,107,0.1),rgba(111,66,193,0.1));border-radius:6px;margin-bottom:10px;border:1px solid var(--streaming-color)} |
| | .streaming-indicator.active{display:flex} |
| | .streaming-dot{width:8px;height:8px;background:var(--streaming-color);border-radius:50%;animation:pulse 1.5s infinite} |
| | @keyframes pulse{0%,100%{opacity:0.3;transform:scale(0.8)}50%{opacity:1;transform:scale(1.2)}} |
| | .settings-grid{display:flex;flex-direction:column;gap:12px} |
| | .setting-item{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:8px;background:rgba(0,0,0,0.3);border-radius:6px;transition:background 0.3s} |
| | .setting-item:hover{background:rgba(111,66,193,0.1)} |
| | .setting-label{font-size:12px;color:#aaa;display:flex;align-items:center;gap:6px} |
| | .toggle-switch{position:relative;width:40px;height:22px;background:linear-gradient(135deg,#555,#666);border-radius:11px;cursor:pointer;transition:all 0.3s;box-shadow:inset 0 1px 3px rgba(0,0,0,0.3)} |
| | .toggle-switch.active{background:linear-gradient(135deg,var(--primary-color),var(--hover-color));box-shadow:0 0 10px rgba(111,66,193,0.3)} |
| | .toggle-slider{position:absolute;top:2px;left:2px;width:18px;height:18px;background:white;border-radius:50%;transition:transform 0.3s;box-shadow:0 2px 4px rgba(0,0,0,0.2)} |
| | .toggle-switch.active .toggle-slider{transform:translateX(18px)} |
| | .health-section{background:linear-gradient(135deg,rgba(255,107,157,0.1),rgba(111,66,193,0.1));border:1px solid rgba(255,107,157,0.3)} |
| | .health-gauge{display:flex;flex-direction:column;align-items:center;padding:15px;background:rgba(0,0,0,0.3);border-radius:10px;margin-bottom:15px} |
| | .gauge-container{position:relative;width:150px;height:75px;overflow:hidden} |
| | .gauge-bg{width:150px;height:150px;border-radius:50%;background:conic-gradient(from 180deg,var(--critical-color) 0deg,var(--danger-color) 36deg,var(--caution-color) 72deg,var(--safe-color) 108deg,#00ff00 144deg,#00ff00 180deg,transparent 180deg);position:absolute;top:0;left:0} |
| | .gauge-center{position:absolute;width:100px;height:100px;background:var(--card-bg);border-radius:50%;top:25px;left:25px} |
| | .gauge-needle{position:absolute;width:4px;height:60px;background:white;left:73px;top:15px;transform-origin:bottom center;transform:rotate(-90deg);transition:transform 0.5s ease;border-radius:2px;box-shadow:0 0 10px rgba(255,255,255,0.5)} |
| | .gauge-value{position:absolute;bottom:5px;left:50%;transform:translateX(-50%);font-size:18px;font-weight:bold;color:white} |
| | .gauge-label{margin-top:10px;font-size:14px;font-weight:600;padding:5px 15px;border-radius:20px;text-transform:uppercase} |
| | .gauge-label.good{background:linear-gradient(135deg,#28a745,#5cb85c);color:#fff} |
| | .gauge-label.normal{background:linear-gradient(135deg,#ffc107,#ff9800);color:#000} |
| | .gauge-label.warning{background:linear-gradient(135deg,#dc3545,#c0392b);color:#fff} |
| | .gauge-label.critical{background:linear-gradient(135deg,#ff0000,#8b0000);color:#fff;animation:blink-critical 1s infinite} |
| | @keyframes blink-critical{0%,100%{opacity:1}50%{opacity:0.5}} |
| | .health-stats{display:grid;grid-template-columns:1fr 1fr;gap:8px} |
| | .health-stat{background:rgba(0,0,0,0.3);padding:10px;border-radius:8px;text-align:center} |
| | .health-stat-value{font-size:14px;font-weight:bold;margin-bottom:4px;padding:4px 8px;border-radius:12px;display:inline-block} |
| | .health-stat-value.good{background:linear-gradient(135deg,#28a745,#5cb85c);color:#fff} |
| | .health-stat-value.normal{background:linear-gradient(135deg,#ffc107,#ff9800);color:#000} |
| | .health-stat-value.warning{background:linear-gradient(135deg,#dc3545,#c0392b);color:#fff} |
| | .health-stat-value.critical{background:linear-gradient(135deg,#ff0000,#8b0000);color:#fff} |
| | .health-stat-label{font-size:10px;color:#888;text-transform:uppercase} |
| | .health-chart-container{height:150px;margin-top:10px;background:rgba(0,0,0,0.2);border-radius:8px;padding:10px} |
| | .memory-stats{display:flex;justify-content:space-around;padding:10px;background:rgba(0,0,0,0.3);border-radius:6px;margin-bottom:10px} |
| | .stat-item{text-align:center} |
| | .stat-value{font-size:18px;font-weight:bold;color:var(--primary-color)} |
| | .stat-label{font-size:10px;color:#888;text-transform:uppercase} |
| | .memory-search-box{display:flex;gap:8px;margin-bottom:10px} |
| | .memory-search-box input{flex:1;padding:8px;background:rgba(0,0,0,0.3);border:1px solid #333;border-radius:6px;color:white;font-size:12px} |
| | .memory-search-box button{padding:8px 12px;font-size:11px} |
| | .memory-list{max-height:150px;overflow-y:auto} |
| | .memory-item{padding:10px;margin-bottom:8px;background:linear-gradient(135deg,rgba(74,158,255,0.1),rgba(111,66,193,0.1));border-radius:6px;border-left:3px solid var(--memory-color);position:relative;font-size:12px;transition:all 0.3s;cursor:pointer} |
| | .memory-item:hover{transform:translateX(5px);box-shadow:0 2px 10px rgba(74,158,255,0.2)} |
| | .memory-item:hover .memory-actions{opacity:1} |
| | .memory-score{position:absolute;top:5px;right:50px;font-size:10px;color:var(--memory-color);background:rgba(74,158,255,0.2);padding:2px 6px;border-radius:4px} |
| | .memory-actions{position:absolute;top:5px;right:5px;display:flex;gap:4px;opacity:0;transition:opacity 0.3s} |
| | .memory-action-btn{font-size:12px;cursor:pointer;background:rgba(0,0,0,0.3);width:20px;height:20px;border-radius:50%;display:flex;align-items:center;justify-content:center;line-height:1;transition:all 0.2s} |
| | .memory-action-btn.delete{color:#ff6b6b} |
| | .memory-action-btn.delete:hover{background:rgba(255,107,107,0.4)} |
| | .memory-action-btn.like{color:#28a745} |
| | .memory-action-btn.like:hover{background:rgba(40,167,69,0.4)} |
| | .memory-action-btn.dislike{color:#ffc107} |
| | .memory-action-btn.dislike:hover{background:rgba(255,193,7,0.4)} |
| | .memory-action-btn.link{color:#4a9eff} |
| | .memory-action-btn.link:hover{background:rgba(74,158,255,0.4)} |
| | .memory-relations{margin-top:8px;padding-top:8px;border-top:1px dashed rgba(255,255,255,0.1);font-size:10px;color:#888;display:none} |
| | .memory-item.expanded .memory-relations{display:block} |
| | .history-section{background:linear-gradient(135deg,rgba(46,204,113,0.1),rgba(39,174,96,0.1));border:1px solid rgba(46,204,113,0.3)} |
| | .history-list{max-height:120px;overflow-y:auto} |
| | .history-item{padding:8px 10px;margin-bottom:6px;background:rgba(0,0,0,0.3);border-radius:6px;cursor:pointer;transition:all 0.2s;font-size:11px;display:flex;justify-content:space-between;align-items:center} |
| | .history-item:hover{background:rgba(46,204,113,0.2);transform:translateX(3px)} |
| | .history-item .date{color:#888;font-size:10px} |
| | .history-item .summary{flex:1;margin-left:10px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis} |
| | .tools-section{background:linear-gradient(135deg,rgba(155,89,182,0.1),rgba(142,68,173,0.1));border:1px solid rgba(155,89,182,0.3)} |
| | .tool-grid{display:grid;grid-template-columns:1fr 1fr;gap:8px} |
| | .tool-btn{padding:10px;background:rgba(0,0,0,0.3);border:1px solid #333;border-radius:6px;cursor:pointer;transition:all 0.2s;text-align:center;font-size:11px;color:#aaa} |
| | .tool-btn:hover{background:rgba(155,89,182,0.2);border-color:#9b59b6;color:white} |
| | .tool-btn i{display:block;font-size:16px;margin-bottom:4px} |
| | .chat-container{border-radius:10px;background:rgba(30,30,30,0.8);backdrop-filter:blur(10px);box-shadow:0 4px 20px rgba(0,0,0,0.3);padding:15px;flex-grow:1;display:flex;flex-direction:column;border:1px solid var(--border-color);overflow:hidden;min-height:0;height:100%} |
| | .chat-messages{flex-grow:1;overflow-y:auto;padding:10px;scrollbar-width:thin;scrollbar-color:var(--primary-color) transparent;min-height:0;max-height:calc(100vh - 280px)} |
| | .message{margin-bottom:12px;padding:12px 16px;border-radius:10px;font-size:15px;line-height:1.8;position:relative;max-width:85%;animation:slideIn 0.3s ease-out;word-wrap:break-word} |
| | @keyframes slideIn{from{opacity:0;transform:translateY(10px)}to{opacity:1;transform:translateY(0)}} |
| | .message.user{background:linear-gradient(135deg,#2c3e50,#34495e);margin-left:auto;border-bottom-right-radius:4px} |
| | .message.assistant{background:linear-gradient(135deg,#c44569,#ff6b9d);margin-right:auto;border-bottom-left-radius:4px} |
| | .message.assistant.streaming{background:linear-gradient(135deg,rgba(196,69,105,0.8),rgba(255,107,157,0.8));border:1px solid var(--streaming-color)} |
| | .message.assistant.streaming::after{content:'β';animation:blink 1s infinite;color:white} |
| | @keyframes blink{0%,50%{opacity:1}51%,100%{opacity:0}} |
| | .message p{margin:0.5em 0;line-height:1.7} |
| | .message code{background:rgba(0,0,0,0.4);padding:2px 8px;border-radius:4px;font-family:'JetBrains Mono','Fira Code',monospace;font-size:0.9em;color:#e06c75} |
| | .message pre{background:rgba(0,0,0,0.5);padding:15px;border-radius:8px;overflow-x:auto;margin:1em 0;border:1px solid rgba(255,255,255,0.1)} |
| | .message pre code{background:none;padding:0;color:#abb2bf} |
| | .text-input-section{margin-top:12px} |
| | .quick-actions{display:flex;gap:8px;padding:8px 12px;justify-content:center;flex-wrap:wrap} |
| | .quick-btn{background:var(--card-bg);border:1px solid var(--border-color);border-radius:20px;padding:8px 16px;font-size:13px;cursor:pointer;transition:all 0.2s ease;color:#aaa} |
| | .quick-btn:hover{background:linear-gradient(135deg,#c44569,#ff6b9d);color:white;border-color:#ff6b9d;box-shadow:0 4px 12px rgba(255,107,157,0.3)} |
| | .quick-btn:active{transform:scale(0.95)} |
| | .input-container{display:flex;gap:10px;align-items:center} |
| | #text-input{flex-grow:1;background:rgba(0,0,0,0.4);color:var(--text-color);border:1px solid var(--border-color);padding:12px 16px;border-radius:8px;font-size:15px;transition:all 0.3s} |
| | #text-input:focus{outline:none;border-color:#ff6b9d;box-shadow:0 0 10px rgba(255,107,157,0.2)} |
| | button{background:linear-gradient(135deg,#c44569,#ff6b9d);color:white;border:none;padding:12px 20px;font-size:14px;cursor:pointer;transition:all 0.3s;text-transform:uppercase;letter-spacing:0.5px;border-radius:8px;box-shadow:0 2px 8px rgba(255,107,157,0.3);font-weight:600} |
| | button:hover:not(:disabled){transform:translateY(-2px);box-shadow:0 4px 12px rgba(255,107,157,0.5)} |
| | button:disabled{opacity:0.5;cursor:not-allowed} |
| | #send-button{background:linear-gradient(135deg,#2ecc71,#27ae60)} |
| | .auto-save-indicator{font-size:10px;color:#888;text-align:center;margin-top:8px} |
| | .auto-save-indicator.saving{color:#ffc107} |
| | .auto-save-indicator.saved{color:#28a745} |
| | .toast{position:fixed;top:20px;left:50%;transform:translateX(-50%);padding:14px 24px;border-radius:8px;font-size:14px;z-index:1000;display:none;box-shadow:0 4px 12px rgba(0,0,0,0.3);animation:slideDown 0.3s ease-out} |
| | @keyframes slideDown{from{transform:translateX(-50%) translateY(-20px);opacity:0}to{transform:translateX(-50%) translateY(0);opacity:1}} |
| | .toast.success{background:linear-gradient(135deg,#4caf50,#45a049);color:white} |
| | .toast.error{background:linear-gradient(135deg,#f44336,#e53935);color:white} |
| | .toast.warning{background:linear-gradient(135deg,var(--warning-color),#ff9800);color:#333} |
| | .modal-overlay{position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.7);display:none;justify-content:center;align-items:center;z-index:1000} |
| | .modal-overlay.active{display:flex} |
| | .modal{background:var(--card-bg);border-radius:12px;padding:20px;max-width:500px;width:90%;max-height:80vh;overflow-y:auto;border:1px solid var(--border-color)} |
| | .modal h3{margin-bottom:15px;color:#ff6b9d} |
| | .modal-close{float:right;cursor:pointer;font-size:20px;color:#888} |
| | .modal-close:hover{color:white} |
| | .modal input,.modal textarea{width:100%;padding:10px;margin-bottom:10px;background:rgba(0,0,0,0.3);border:1px solid #333;border-radius:6px;color:white;font-size:13px} |
| | .modal textarea{min-height:100px;resize:vertical} |
| | .modal-actions{display:flex;gap:10px;margin-top:15px} |
| | .modal-actions button{flex:1} |
| | .relation-graph{background:rgba(0,0,0,0.3);border-radius:8px;padding:10px;margin-top:10px} |
| | .relation-item{display:flex;align-items:center;gap:8px;padding:6px;margin-bottom:4px;background:rgba(74,158,255,0.1);border-radius:4px;font-size:11px} |
| | .relation-strength{width:50px;height:6px;background:#333;border-radius:3px;overflow:hidden} |
| | .relation-strength-fill{height:100%;background:linear-gradient(90deg,#4a9eff,#ff6b9d);border-radius:3px} |
| | .api-section{background:rgba(30,30,30,0.95);border-top:1px solid var(--border-color);padding:15px;margin-top:15px} |
| | .api-section h3{text-align:center;margin-bottom:10px;color:#888;font-size:12px;text-transform:uppercase} |
| | .api-docs-container{background:rgba(0,0,0,0.3);border-radius:10px;padding:20px;border:1px solid var(--border-color);max-height:500px;overflow-y:auto;display:none} |
| | .api-endpoint{background:rgba(111,66,193,0.1);border:1px solid rgba(111,66,193,0.3);border-radius:8px;padding:15px;margin-bottom:15px} |
| | .api-endpoint h4{color:#ff6b9d;margin-bottom:10px;font-size:14px} |
| | .api-method{display:inline-block;padding:3px 8px;border-radius:4px;font-size:11px;font-weight:bold;margin-right:10px} |
| | .api-method.get{background:#28a745;color:white} |
| | .api-method.post{background:#007bff;color:white} |
| | .api-method.delete{background:#dc3545;color:white} |
| | .api-url{font-family:monospace;color:#4a9eff;font-size:13px} |
| | .api-desc{color:#aaa;font-size:12px;margin-top:8px;line-height:1.5} |
| | .collapse-btn{background:rgba(0,0,0,0.3);border:1px solid var(--border-color);color:#888;padding:8px 20px;font-size:11px;margin:0 auto;display:block;margin-bottom:10px} |
| | .collapse-btn:hover{background:rgba(111,66,193,0.2);color:white} |
| | @media (max-width:768px){.main-content{flex-direction:column}.sidebar{width:100%;max-height:none}.health-stats{grid-template-columns:1fr 1fr}.tool-grid{grid-template-columns:1fr}} |
| | </style> |
| | </head> |
| | <body> |
| | <div id="error-toast" class="toast"></div> |
| |
|
| | |
| | <div class="modal-overlay" id="relation-modal"> |
| | <div class="modal"> |
| | <span class="modal-close" onclick="closeModal('relation-modal')">×</span> |
| | <h3>π κΈ°μ΅ μ°κ΄ κ΄κ³</h3> |
| | <div id="relation-content"></div> |
| | </div> |
| | </div> |
| |
|
| | |
| | <div class="modal-overlay" id="crawl-modal"> |
| | <div class="modal"> |
| | <span class="modal-close" onclick="closeModal('crawl-modal')">×</span> |
| | <h3>π URL ν¬λ‘€λ§</h3> |
| | <input type="url" id="crawl-url-input" placeholder="https://example.com"> |
| | <div class="modal-actions"> |
| | <button onclick="crawlUrl()">ν¬λ‘€λ§ μμ</button> |
| | </div> |
| | <div id="crawl-result" style="margin-top:15px;font-size:12px;max-height:300px;overflow-y:auto;"></div> |
| | </div> |
| | </div> |
| |
|
| | |
| | <div class="modal-overlay" id="search-modal"> |
| | <div class="modal"> |
| | <span class="modal-close" onclick="closeModal('search-modal')">×</span> |
| | <h3>π μΉ κ²μ ν
μ€νΈ</h3> |
| | <input type="text" id="search-query-input" placeholder="κ²μμ΄ μ
λ ₯..."> |
| | <div class="modal-actions"> |
| | <button onclick="testWebSearch()">κ²μ</button> |
| | </div> |
| | <div id="search-result" style="margin-top:15px;font-size:12px;max-height:300px;overflow-y:auto;"></div> |
| | </div> |
| | </div> |
| |
|
| | <div class="container"> |
| | <div class="header"> |
| | <div class="logo"><h1>μλμ΄ λ§λ² AI</h1></div> |
| | <div class="current-time" id="current-time"></div> |
| | <div class="storage-info" id="storage-info">μ€ν λ¦¬μ§ νμΈ μ€...</div> |
| | </div> |
| | <div class="main-content"> |
| | <div class="sidebar"> |
| | <div class="email-section"> |
| | <label>μ΄λ©μΌ (νμ)</label> |
| | <input type="email" id="user-email" placeholder="your@email.com"> |
| | </div> |
| |
|
| | |
| | <div class="sidebar-section health-section"> |
| | <h3>κ±΄κ° μν μ§λ¨ <span class="section-toggle" data-target="health-content">βΌ</span></h3> |
| | <div class="section-content" id="health-content"> |
| | <div class="health-gauge"> |
| | <div class="gauge-container"> |
| | <div class="gauge-bg"></div> |
| | <div class="gauge-center"></div> |
| | <div class="gauge-needle" id="gauge-needle"></div> |
| | <div class="gauge-value" id="gauge-value">μνΈ</div> |
| | </div> |
| | <div class="gauge-label good" id="gauge-label">μνΈ</div> |
| | </div> |
| | <div class="health-stats"> |
| | <div class="health-stat"> |
| | <div class="health-stat-value good" id="cognitive-value">μνΈ</div> |
| | <div class="health-stat-label">μΈμ§λ₯λ ₯</div> |
| | </div> |
| | <div class="health-stat"> |
| | <div class="health-stat-value good" id="conversation-value">μνΈ</div> |
| | <div class="health-stat-label">λνλ₯λ ₯</div> |
| | </div> |
| | <div class="health-stat"> |
| | <div class="health-stat-value good" id="memory-value">μνΈ</div> |
| | <div class="health-stat-label">κΈ°μ΅λ₯λ ₯</div> |
| | </div> |
| | <div class="health-stat"> |
| | <div class="health-stat-value good" id="emotional-value">μνΈ</div> |
| | <div class="health-stat-label">κ°μ μν</div> |
| | </div> |
| | </div> |
| | <div class="health-chart-container"> |
| | <canvas id="health-chart"></canvas> |
| | </div> |
| | </div> |
| | </div> |
| |
|
| | |
| | <div class="sidebar-section"> |
| | <h3>μ€μ <span class="section-toggle" data-target="settings-content">βΌ</span></h3> |
| | <div class="section-content" id="settings-content"> |
| | <div class="settings-grid"> |
| | <div class="setting-item"> |
| | <span class="setting-label">μΉ κ²μ</span> |
| | <div id="search-toggle" class="toggle-switch active"><div class="toggle-slider"></div></div> |
| | </div> |
| | <div class="setting-item"> |
| | <span class="setting-label">μκ° νμ΅</span> |
| | <div id="learning-toggle" class="toggle-switch active"><div class="toggle-slider"></div></div> |
| | </div> |
| | <div class="setting-item"> |
| | <span class="setting-label">μ€νΈλ¦¬λ°</span> |
| | <div id="streaming-toggle" class="toggle-switch active"><div class="toggle-slider"></div></div> |
| | </div> |
| | <div class="setting-item"> |
| | <span class="setting-label">μλ μ μ₯ (3λΆ)</span> |
| | <div id="autosave-toggle" class="toggle-switch active"><div class="toggle-slider"></div></div> |
| | </div> |
| | </div> |
| | <div style="margin-top:12px;"> |
| | <label style="font-size:12px;color:#888;">μ΄λ¦ (νΈμΉ)</label> |
| | <input type="text" id="user-name" placeholder="μ΄λ₯΄μ μ±ν¨..." style="width:100%;margin-top:4px;padding:8px;background:rgba(0,0,0,0.3);border:1px solid #333;border-radius:6px;color:white;font-size:12px;"> |
| | </div> |
| | </div> |
| | </div> |
| |
|
| | |
| | <div class="sidebar-section history-section"> |
| | <h3>π μ΄μ λν <span class="section-toggle" data-target="history-content">βΌ</span></h3> |
| | <div class="section-content" id="history-content"> |
| | <div class="history-list" id="history-list"> |
| | <div style="text-align:center;color:#888;font-size:11px;padding:20px;">μ΄μ λνκ° μμ΅λλ€</div> |
| | </div> |
| | </div> |
| | </div> |
| |
|
| | |
| | <div class="sidebar-section"> |
| | <h3>λν κΈ°μ΅ <span class="section-toggle" data-target="memory-content">βΌ</span></h3> |
| | <div class="section-content" id="memory-content"> |
| | <div class="memory-stats"> |
| | <div class="stat-item"><div class="stat-value" id="memory-count">0</div><div class="stat-label">μ΄ κΈ°μ΅</div></div> |
| | <div class="stat-item"><div class="stat-value" id="vector-count">0</div><div class="stat-label">μ€μ κΈ°μ΅</div></div> |
| | <div class="stat-item"><div class="stat-value" id="relation-count">0</div><div class="stat-label">μ°κ΄ κ΄κ³</div></div> |
| | </div> |
| | |
| | <div class="memory-search-box"> |
| | <input type="text" id="memory-search-input" placeholder="κΈ°μ΅ κ²μ..."> |
| | <button onclick="searchMemories()">κ²μ</button> |
| | </div> |
| | <div class="memory-list" id="memory-list"></div> |
| | </div> |
| | </div> |
| |
|
| | |
| | <div class="sidebar-section tools-section"> |
| | <h3>π λꡬ <span class="section-toggle" data-target="tools-content">βΌ</span></h3> |
| | <div class="section-content" id="tools-content"> |
| | <div class="tool-grid"> |
| | <div class="tool-btn" onclick="openModal('crawl-modal')"> |
| | <i>π</i>URL ν¬λ‘€λ§ |
| | </div> |
| | <div class="tool-btn" onclick="openModal('search-modal')"> |
| | <i>π</i>μΉ κ²μ |
| | </div> |
| | <div class="tool-btn" onclick="loadStorageInfo()"> |
| | <i>πΎ</i>μ€ν λ¦¬μ§ μ 보 |
| | </div> |
| | <div class="tool-btn" onclick="exportMemories()"> |
| | <i>π€</i>κΈ°μ΅ λ΄λ³΄λ΄κΈ° |
| | </div> |
| | </div> |
| | </div> |
| | </div> |
| |
|
| | <div style="text-align:center;margin-top:10px;"> |
| | <button id="end-session-button">λν μ μ₯νκΈ°</button> |
| | <div class="auto-save-indicator" id="auto-save-indicator">μλ μ μ₯ λκΈ° μ€</div> |
| | </div> |
| | </div> |
| |
|
| | <div class="chat-section"> |
| | <div class="chat-container"> |
| | <div class="streaming-indicator" id="streaming-indicator"><div class="streaming-dot"></div><span style="font-size:12px;color:var(--streaming-color);">μλ΅ μμ± μ€...</span></div> |
| | <div class="chat-messages" id="chat-messages"></div> |
| | <div class="quick-actions"> |
| | <button class="quick-btn" data-prompt="μ€λ κΈ°λΆμ΄ μ΄λ μΈμ?" title="μλΆ μΈμ¬">μλΆ μΈμ¬</button> |
| | <button class="quick-btn" data-prompt="μ€λ λ μ¨κ° μ΄λμ?" title="λ μ¨ μ΄μΌκΈ°">λ μ¨ μ΄μΌκΈ°</button> |
| | <button class="quick-btn" data-prompt="μμ¦ κ±΄κ°μ μ΄λ μΈμ?" title="κ±΄κ° μ²΄ν¬">κ±΄κ° μ²΄ν¬</button> |
| | <button class="quick-btn" data-prompt="μ¬λ―Έμλ μ΄μΌκΈ° ν΄μ£ΌμΈμ" title="μ΄μΌκΈ°">μ΄μΌκΈ° ν΄μ€</button> |
| | <button class="quick-btn" data-prompt="μ€λ λ΄μ€ μ’ μλ €μ£ΌμΈμ" title="λ΄μ€">μ€λ λ΄μ€</button> |
| | </div> |
| | <div class="text-input-section"> |
| | <div class="input-container"> |
| | <input type="text" id="text-input" placeholder="νΈνκ² λ§μν΄ μ£ΌμΈμ..."> |
| | <button id="send-button">μ μ‘</button> |
| | </div> |
| | </div> |
| | </div> |
| | </div> |
| | </div> |
| | </div> |
| |
|
| | <div class="api-section" id="api-section"> |
| | <button class="collapse-btn" id="api-collapse-btn">API λ¬Έμ μ΄κΈ°/λ«κΈ°</button> |
| | <h3>API μλν¬μΈνΈ κ°μ΄λ</h3> |
| | <div class="api-docs-container" id="api-docs-container"> |
| | <div class="api-endpoint"> |
| | <h4>μ±ν
μ€νΈλ¦¬λ°</h4> |
| | <span class="api-method post">POST</span> |
| | <span class="api-url">/chat/text/stream</span> |
| | <div class="api-desc">μ€μκ° μ€νΈλ¦¬λ° λ°©μμΌλ‘ AIμ λνν©λλ€.</div> |
| | </div> |
| | <div class="api-endpoint"> |
| | <h4>μ μΈμ
μμ±</h4> |
| | <span class="api-method post">POST</span> |
| | <span class="api-url">/session/new</span> |
| | </div> |
| | <div class="api-endpoint"> |
| | <h4>μΈμ
μ’
λ£ λ° μ μ₯</h4> |
| | <span class="api-method post">POST</span> |
| | <span class="api-url">/session/end</span> |
| | </div> |
| | <div class="api-endpoint"> |
| | <h4>μλ μ μ₯</h4> |
| | <span class="api-method post">POST</span> |
| | <span class="api-url">/session/auto-save</span> |
| | </div> |
| | <div class="api-endpoint"> |
| | <h4>κ±΄κ° μν μ‘°ν</h4> |
| | <span class="api-method get">GET</span> |
| | <span class="api-url">/health/current?user_email={email}</span> |
| | </div> |
| | <div class="api-endpoint"> |
| | <h4>κ±΄κ° νμ€ν 리</h4> |
| | <span class="api-method get">GET</span> |
| | <span class="api-url">/health/history?user_email={email}&days=30</span> |
| | </div> |
| | <div class="api-endpoint"> |
| | <h4>κΈ°μ΅ κ²μ</h4> |
| | <span class="api-method post">POST</span> |
| | <span class="api-url">/memory/search</span> |
| | <div class="api-desc">μλ§¨ν± κ²μμΌλ‘ κ΄λ ¨ κΈ°μ΅μ μ°Ύμ΅λλ€.</div> |
| | </div> |
| | <div class="api-endpoint"> |
| | <h4>κΈ°μ΅ νΌλλ°±</h4> |
| | <span class="api-method post">POST</span> |
| | <span class="api-url">/memory/feedback</span> |
| | <div class="api-desc">κΈ°μ΅μ λν νΌλλ°±μΌλ‘ κ°ννμ΅μ μνν©λλ€.</div> |
| | </div> |
| | <div class="api-endpoint"> |
| | <h4>κΈ°μ΅ κ΄κ³ μ‘°ν</h4> |
| | <span class="api-method get">GET</span> |
| | <span class="api-url">/memory/relationships/{memory_id}?user_email={email}</span> |
| | </div> |
| | <div class="api-endpoint"> |
| | <h4>κΈ°μ΅ κ·Έλν ν΅κ³</h4> |
| | <span class="api-method get">GET</span> |
| | <span class="api-url">/memory/graph/stats?user_email={email}</span> |
| | </div> |
| | <div class="api-endpoint"> |
| | <h4>λͺ¨λ κΈ°μ΅ μ‘°ν</h4> |
| | <span class="api-method get">GET</span> |
| | <span class="api-url">/memory/all?user_email={email}</span> |
| | </div> |
| | <div class="api-endpoint"> |
| | <h4>κΈ°μ΅ μμ </h4> |
| | <span class="api-method delete">DELETE</span> |
| | <span class="api-url">/memory/{memory_id}?user_email={email}</span> |
| | </div> |
| | <div class="api-endpoint"> |
| | <h4>μ΅κ·Ό λν λͺ©λ‘</h4> |
| | <span class="api-method get">GET</span> |
| | <span class="api-url">/history/recent?user_email={email}</span> |
| | </div> |
| | <div class="api-endpoint"> |
| | <h4>λν λ΄μ© μ‘°ν</h4> |
| | <span class="api-method get">GET</span> |
| | <span class="api-url">/history/{session_id}?user_email={email}</span> |
| | </div> |
| | <div class="api-endpoint"> |
| | <h4>μΉ κ²μ</h4> |
| | <span class="api-method post">POST</span> |
| | <span class="api-url">/search/web</span> |
| | </div> |
| | <div class="api-endpoint"> |
| | <h4>URL ν¬λ‘€λ§</h4> |
| | <span class="api-method post">POST</span> |
| | <span class="api-url">/crawl/url</span> |
| | </div> |
| | <div class="api-endpoint"> |
| | <h4>μ€ν λ¦¬μ§ μ 보</h4> |
| | <span class="api-method get">GET</span> |
| | <span class="api-url">/storage/info</span> |
| | </div> |
| | </div> |
| | </div> |
| |
|
| | <script> |
| | let webSearchEnabled=true,selfLearningEnabled=true,streamingEnabled=true,autoSaveEnabled=true; |
| | let currentSessionId=null,autoSaveInterval=null; |
| | let userName=localStorage.getItem('userName')||''; |
| | let userEmail=localStorage.getItem('userEmail')||''; |
| | let userMemories={},isStreaming=false,healthChart=null; |
| | |
| | marked.setOptions({breaks:true,gfm:true,headerIds:false,mangle:false}); |
| | |
| | function scoreToLabel(score){ |
| | if(score>=80)return{text:'μνΈ',class:'good'}; |
| | if(score>=60)return{text:'보ν΅',class:'normal'}; |
| | if(score>=40)return{text:'μν',class:'warning'}; |
| | return{text:'κΈ΄κΈ',class:'critical'}; |
| | } |
| | |
| | const elements={ |
| | endSessionButton:document.getElementById('end-session-button'), |
| | sendButton:document.getElementById('send-button'), |
| | chatMessages:document.getElementById('chat-messages'), |
| | searchToggle:document.getElementById('search-toggle'), |
| | learningToggle:document.getElementById('learning-toggle'), |
| | streamingToggle:document.getElementById('streaming-toggle'), |
| | autosaveToggle:document.getElementById('autosave-toggle'), |
| | textInput:document.getElementById('text-input'), |
| | memoryList:document.getElementById('memory-list'), |
| | historyList:document.getElementById('history-list'), |
| | userNameInput:document.getElementById('user-name'), |
| | userEmailInput:document.getElementById('user-email'), |
| | currentTimeDiv:document.getElementById('current-time'), |
| | storageInfo:document.getElementById('storage-info'), |
| | streamingIndicator:document.getElementById('streaming-indicator'), |
| | autoSaveIndicator:document.getElementById('auto-save-indicator'), |
| | memoryCount:document.getElementById('memory-count'), |
| | vectorCount:document.getElementById('vector-count'), |
| | relationCount:document.getElementById('relation-count'), |
| | gaugeNeedle:document.getElementById('gauge-needle'), |
| | gaugeValue:document.getElementById('gauge-value'), |
| | gaugeLabel:document.getElementById('gauge-label'), |
| | cognitiveValue:document.getElementById('cognitive-value'), |
| | conversationValue:document.getElementById('conversation-value'), |
| | memoryValue:document.getElementById('memory-value'), |
| | emotionalValue:document.getElementById('emotional-value'), |
| | apiCollapseBtn:document.getElementById('api-collapse-btn'), |
| | apiDocsContainer:document.getElementById('api-docs-container'), |
| | memorySearchInput:document.getElementById('memory-search-input') |
| | }; |
| | |
| | |
| | document.querySelectorAll('.section-toggle').forEach(toggle=>{ |
| | toggle.addEventListener('click',()=>{ |
| | const targetId=toggle.dataset.target; |
| | const content=document.getElementById(targetId); |
| | content.classList.toggle('collapsed'); |
| | toggle.classList.toggle('collapsed'); |
| | }); |
| | }); |
| | |
| | |
| | function openModal(modalId){ |
| | document.getElementById(modalId).classList.add('active'); |
| | } |
| | function closeModal(modalId){ |
| | document.getElementById(modalId).classList.remove('active'); |
| | } |
| | |
| | |
| | elements.apiCollapseBtn.addEventListener('click',()=>{ |
| | const container=elements.apiDocsContainer; |
| | if(container.style.display==='none'||container.style.display===''){ |
| | container.style.display='block'; |
| | elements.apiCollapseBtn.textContent='API λ¬Έμ λ«κΈ°'; |
| | }else{ |
| | container.style.display='none'; |
| | elements.apiCollapseBtn.textContent='API λ¬Έμ μ΄κΈ°/λ«κΈ°'; |
| | } |
| | }); |
| | |
| | |
| | function initHealthChart(){ |
| | const ctx=document.getElementById('health-chart').getContext('2d'); |
| | healthChart=new Chart(ctx,{ |
| | type:'line', |
| | data:{labels:[],datasets:[ |
| | {label:'μΈμ§',data:[],borderColor:'#4a9eff',tension:0.4,fill:false,pointRadius:2}, |
| | {label:'λν',data:[],borderColor:'#9b59b6',tension:0.4,fill:false,pointRadius:2}, |
| | {label:'κΈ°μ΅',data:[],borderColor:'#2ecc71',tension:0.4,fill:false,pointRadius:2}, |
| | {label:'κ°μ ',data:[],borderColor:'#e74c3c',tension:0.4,fill:false,pointRadius:2} |
| | ]}, |
| | options:{responsive:true,maintainAspectRatio:false,plugins:{legend:{display:true,position:'bottom',labels:{boxWidth:8,padding:8,font:{size:9},color:'#888'}}},scales:{x:{display:true,grid:{color:'rgba(255,255,255,0.05)'},ticks:{font:{size:8},color:'#666',maxRotation:0}},y:{display:true,min:0,max:100,grid:{color:'rgba(255,255,255,0.05)'},ticks:{font:{size:8},color:'#666'}}}} |
| | }); |
| | } |
| | |
| | function updateGauge(riskLevel,riskLabel){ |
| | const angle=-90+(riskLevel/100)*180; |
| | elements.gaugeNeedle.style.transform='rotate('+angle+'deg)'; |
| | const label=scoreToLabel(riskLevel); |
| | elements.gaugeValue.textContent=label.text; |
| | elements.gaugeLabel.textContent=riskLabel||label.text; |
| | elements.gaugeLabel.className='gauge-label '+label.class; |
| | } |
| | |
| | function updateHealthStat(element,score){ |
| | const label=scoreToLabel(score); |
| | element.textContent=label.text; |
| | element.className='health-stat-value '+label.class; |
| | } |
| | |
| | |
| | async function loadStorageInfo(){ |
| | try{ |
| | const response=await fetch('/storage/info'); |
| | const data=await response.json(); |
| | if(data.is_persistent){ |
| | elements.storageInfo.textContent='πΎ μꡬ μ€ν λ¦¬μ§ νμ±ν ('+formatBytes(data.db_size)+')'; |
| | elements.storageInfo.className='storage-info persistent'; |
| | }else{ |
| | elements.storageInfo.textContent='β οΈ λ‘컬 μ€ν λ¦¬μ§ (μμ)'; |
| | elements.storageInfo.className='storage-info local'; |
| | } |
| | showToast('μ€ν 리μ§: '+(data.is_persistent?'μꡬ':'μμ')+' / DB: '+formatBytes(data.db_size),'success'); |
| | }catch(error){ |
| | elements.storageInfo.textContent='μ€ν λ¦¬μ§ μ 보 μμ'; |
| | console.error('Storage info error:',error); |
| | } |
| | } |
| | |
| | function formatBytes(bytes){ |
| | if(bytes===0)return'0 Bytes'; |
| | const k=1024; |
| | const sizes=['Bytes','KB','MB','GB']; |
| | const i=Math.floor(Math.log(bytes)/Math.log(k)); |
| | return parseFloat((bytes/Math.pow(k,i)).toFixed(2))+' '+sizes[i]; |
| | } |
| | |
| | |
| | async function loadHealthStatus(){ |
| | if(!userEmail)return; |
| | try{ |
| | const response=await fetch('/health/current?user_email='+encodeURIComponent(userEmail)); |
| | const data=await response.json(); |
| | if(data&&!data.error){ |
| | updateHealthStat(elements.cognitiveValue,data.cognitive||70); |
| | updateHealthStat(elements.conversationValue,data.conversation||70); |
| | updateHealthStat(elements.memoryValue,data.memory||70); |
| | updateHealthStat(elements.emotionalValue,data.emotional||70); |
| | updateGauge(data.risk_level||70,data.risk_label||'μνΈ'); |
| | } |
| | }catch(error){console.error('Failed to load health status:',error);} |
| | } |
| | |
| | async function loadHealthHistory(){ |
| | if(!userEmail)return; |
| | try{ |
| | const response=await fetch('/health/history?user_email='+encodeURIComponent(userEmail)+'&days=7'); |
| | const data=await response.json(); |
| | if(data.history&&healthChart){ |
| | const labels=data.history.map(h=>{const date=new Date(h.date);return (date.getMonth()+1)+'/'+date.getDate();}).reverse(); |
| | const cognitive=data.history.map(h=>h.cognitive).reverse(); |
| | const conversation=data.history.map(h=>h.conversation).reverse(); |
| | const memory=data.history.map(h=>h.memory).reverse(); |
| | const emotional=data.history.map(h=>h.emotional).reverse(); |
| | healthChart.data.labels=labels.slice(-7); |
| | healthChart.data.datasets[0].data=cognitive.slice(-7); |
| | healthChart.data.datasets[1].data=conversation.slice(-7); |
| | healthChart.data.datasets[2].data=memory.slice(-7); |
| | healthChart.data.datasets[3].data=emotional.slice(-7); |
| | healthChart.update(); |
| | } |
| | }catch(error){console.error('Failed to load health history:',error);} |
| | } |
| | |
| | |
| | async function loadRecentHistory(){ |
| | if(!userEmail)return; |
| | try{ |
| | const response=await fetch('/history/recent?user_email='+encodeURIComponent(userEmail)); |
| | const history=await response.json(); |
| | if(Array.isArray(history)&&history.length>0){ |
| | elements.historyList.innerHTML=''; |
| | history.forEach(h=>{ |
| | const item=document.createElement('div'); |
| | item.className='history-item'; |
| | const date=new Date(h.created_at); |
| | const dateStr=(date.getMonth()+1)+'/'+date.getDate()+' '+date.getHours()+':'+String(date.getMinutes()).padStart(2,'0'); |
| | item.innerHTML='<span class="date">'+dateStr+'</span><span class="summary">'+(h.summary||'λν λ΄μ©')+'</span>'; |
| | item.onclick=()=>loadConversation(h.id); |
| | elements.historyList.appendChild(item); |
| | }); |
| | }else{ |
| | elements.historyList.innerHTML='<div style="text-align:center;color:#888;font-size:11px;padding:20px;">μ΄μ λνκ° μμ΅λλ€</div>'; |
| | } |
| | }catch(error){console.error('Failed to load history:',error);} |
| | } |
| | |
| | |
| | async function loadConversation(sessionId){ |
| | if(!userEmail)return; |
| | try{ |
| | const response=await fetch('/history/'+sessionId+'?user_email='+encodeURIComponent(userEmail)); |
| | const messages=await response.json(); |
| | if(Array.isArray(messages)&&messages.length>0){ |
| | elements.chatMessages.innerHTML=''; |
| | messages.forEach(msg=>{ |
| | addMessage(msg.role,msg.content); |
| | }); |
| | currentSessionId=sessionId; |
| | showToast('μ΄μ λνλ₯Ό λΆλ¬μμ΅λλ€.','success'); |
| | } |
| | }catch(error){ |
| | console.error('Failed to load conversation:',error); |
| | showToast('λν λΆλ¬μ€κΈ° μ€ν¨','error'); |
| | } |
| | } |
| | |
| | |
| | async function loadGraphStats(){ |
| | if(!userEmail)return; |
| | try{ |
| | const response=await fetch('/memory/graph/stats?user_email='+encodeURIComponent(userEmail)); |
| | const stats=await response.json(); |
| | if(stats.total_edges!==undefined){elements.relationCount.textContent=stats.total_edges;} |
| | }catch(error){console.error('Failed to load graph stats:',error);} |
| | } |
| | |
| | |
| | async function searchMemories(){ |
| | const query=elements.memorySearchInput.value.trim(); |
| | if(!query||!userEmail){ |
| | showToast('κ²μμ΄λ₯Ό μ
λ ₯ν΄μ£ΌμΈμ.','warning'); |
| | return; |
| | } |
| | try{ |
| | const response=await fetch('/memory/search',{ |
| | method:'POST', |
| | headers:{'Content-Type':'application/json'}, |
| | body:JSON.stringify({query:query,user_email:userEmail,k:20,threshold:0.5}) |
| | }); |
| | const data=await response.json(); |
| | if(data.results&&data.results.length>0){ |
| | displayMemories(data.results); |
| | showToast(data.results.length+'κ°μ κ΄λ ¨ κΈ°μ΅μ μ°Ύμμ΅λλ€.','success'); |
| | }else{ |
| | showToast('κ΄λ ¨ κΈ°μ΅μ΄ μμ΅λλ€.','warning'); |
| | } |
| | }catch(error){ |
| | console.error('Memory search error:',error); |
| | showToast('κ²μ μ€ μ€λ₯κ° λ°μνμ΅λλ€.','error'); |
| | } |
| | } |
| | |
| | |
| | async function showMemoryRelations(memoryId){ |
| | if(!userEmail)return; |
| | try{ |
| | const response=await fetch('/memory/relationships/'+memoryId+'?user_email='+encodeURIComponent(userEmail)); |
| | const data=await response.json(); |
| | const container=document.getElementById('relation-content'); |
| | if(data.direct_relationships&&data.direct_relationships.length>0){ |
| | let html='<div class="relation-graph">'; |
| | data.direct_relationships.forEach(rel=>{ |
| | const strengthPercent=(rel.strength*100).toFixed(0); |
| | html+='<div class="relation-item">'; |
| | html+='<div class="relation-strength"><div class="relation-strength-fill" style="width:'+strengthPercent+'%"></div></div>'; |
| | html+='<span style="flex:1">'+rel.content_preview+'</span>'; |
| | html+='</div>'; |
| | }); |
| | html+='</div>'; |
| | html+='<p style="margin-top:10px;font-size:11px;color:#888;">μ΄ '+data.total_connections+'κ°μ μ°κ²°</p>'; |
| | container.innerHTML=html; |
| | }else{ |
| | container.innerHTML='<p style="color:#888;">μ°κ΄λ κΈ°μ΅μ΄ μμ΅λλ€.</p>'; |
| | } |
| | openModal('relation-modal'); |
| | }catch(error){ |
| | console.error('Relations error:',error); |
| | showToast('κ΄κ³ μ‘°ν μ€ν¨','error'); |
| | } |
| | } |
| | |
| | |
| | async function sendMemoryFeedback(memoryId,feedback){ |
| | if(!userEmail)return; |
| | try{ |
| | await fetch('/memory/feedback',{ |
| | method:'POST', |
| | headers:{'Content-Type':'application/json'}, |
| | body:JSON.stringify({ |
| | memory_ids:[memoryId], |
| | feedback:feedback, |
| | context:{}, |
| | user_email:userEmail |
| | }) |
| | }); |
| | showToast('νΌλλ°±μ΄ λ°μλμμ΅λλ€.','success'); |
| | }catch(error){ |
| | console.error('Feedback error:',error); |
| | } |
| | } |
| | |
| | |
| | async function crawlUrl(){ |
| | const url=document.getElementById('crawl-url-input').value.trim(); |
| | if(!url){ |
| | showToast('URLμ μ
λ ₯ν΄μ£ΌμΈμ.','warning'); |
| | return; |
| | } |
| | const resultDiv=document.getElementById('crawl-result'); |
| | resultDiv.innerHTML='<p style="color:#888;">ν¬λ‘€λ§ μ€...</p>'; |
| | try{ |
| | const response=await fetch('/crawl/url',{ |
| | method:'POST', |
| | headers:{'Content-Type':'application/json'}, |
| | body:JSON.stringify({url:url}) |
| | }); |
| | const data=await response.json(); |
| | if(data.success){ |
| | resultDiv.innerHTML='<h4 style="color:#28a745;">β μ±κ³΅</h4>'+ |
| | '<p><strong>μ λͺ©:</strong> '+data.title+'</p>'+ |
| | '<p><strong>κΈΈμ΄:</strong> '+data.content_length+'μ</p>'+ |
| | '<div style="max-height:200px;overflow-y:auto;background:rgba(0,0,0,0.3);padding:10px;border-radius:6px;margin-top:10px;">'+ |
| | data.content.substring(0,2000)+'...</div>'; |
| | }else{ |
| | resultDiv.innerHTML='<p style="color:#dc3545;">μ€λ₯: '+data.error+'</p>'; |
| | } |
| | }catch(error){ |
| | resultDiv.innerHTML='<p style="color:#dc3545;">ν¬λ‘€λ§ μ€ν¨: '+error.message+'</p>'; |
| | } |
| | } |
| | |
| | |
| | async function testWebSearch(){ |
| | const query=document.getElementById('search-query-input').value.trim(); |
| | if(!query){ |
| | showToast('κ²μμ΄λ₯Ό μ
λ ₯ν΄μ£ΌμΈμ.','warning'); |
| | return; |
| | } |
| | const resultDiv=document.getElementById('search-result'); |
| | resultDiv.innerHTML='<p style="color:#888;">κ²μ μ€...</p>'; |
| | try{ |
| | const response=await fetch('/search/web',{ |
| | method:'POST', |
| | headers:{'Content-Type':'application/json'}, |
| | body:JSON.stringify({query:query,count:5}) |
| | }); |
| | const data=await response.json(); |
| | if(data.results&&data.results.length>0){ |
| | let html='<p style="color:#28a745;">'+data.count+'κ° κ²°κ³Ό</p>'; |
| | data.results.forEach((r,i)=>{ |
| | html+='<div style="margin:10px 0;padding:10px;background:rgba(0,0,0,0.3);border-radius:6px;">'; |
| | html+='<a href="'+r.url+'" target="_blank" style="color:#4a9eff;text-decoration:none;">'+(i+1)+'. '+r.title+'</a>'; |
| | html+='<p style="color:#aaa;font-size:11px;margin-top:5px;">'+r.description+'</p>'; |
| | html+='</div>'; |
| | }); |
| | resultDiv.innerHTML=html; |
| | }else{ |
| | resultDiv.innerHTML='<p style="color:#ffc107;">κ²μ κ²°κ³Όκ° μμ΅λλ€.</p>'; |
| | } |
| | }catch(error){ |
| | resultDiv.innerHTML='<p style="color:#dc3545;">κ²μ μ€ν¨: '+error.message+'</p>'; |
| | } |
| | } |
| | |
| | |
| | async function exportMemories(){ |
| | if(!userEmail)return; |
| | try{ |
| | const response=await fetch('/memory/all?user_email='+encodeURIComponent(userEmail)); |
| | const memories=await response.json(); |
| | const dataStr=JSON.stringify(memories,null,2); |
| | const blob=new Blob([dataStr],{type:'application/json'}); |
| | const url=URL.createObjectURL(blob); |
| | const a=document.createElement('a'); |
| | a.href=url; |
| | a.download='memories_'+userEmail.split('@')[0]+'_'+new Date().toISOString().split('T')[0]+'.json'; |
| | a.click(); |
| | URL.revokeObjectURL(url); |
| | showToast('κΈ°μ΅μ΄ λ΄λ³΄λ΄μ‘μ΅λλ€.','success'); |
| | }catch(error){ |
| | showToast('λ΄λ³΄λ΄κΈ° μ€ν¨','error'); |
| | } |
| | } |
| | |
| | |
| | function startAutoSave(){ |
| | if(autoSaveInterval)clearInterval(autoSaveInterval); |
| | if(!autoSaveEnabled)return; |
| | autoSaveInterval=setInterval(async()=>{ |
| | if(currentSessionId&&userEmail){ |
| | elements.autoSaveIndicator.textContent='μλ μ μ₯ μ€...'; |
| | elements.autoSaveIndicator.className='auto-save-indicator saving'; |
| | try{ |
| | await fetch('/session/auto-save',{ |
| | method:'POST', |
| | headers:{'Content-Type':'application/json'}, |
| | body:JSON.stringify({session_id:currentSessionId,user_email:userEmail}) |
| | }); |
| | elements.autoSaveIndicator.textContent='μλ μ μ₯ μλ£ ('+new Date().toLocaleTimeString()+')'; |
| | elements.autoSaveIndicator.className='auto-save-indicator saved'; |
| | }catch(error){ |
| | elements.autoSaveIndicator.textContent='μλ μ μ₯ μ€ν¨'; |
| | console.error('Auto-save error:',error); |
| | } |
| | } |
| | },180000); |
| | } |
| | |
| | function updateCurrentTime(){ |
| | const now=new Date(); |
| | const kstTime=new Date(now.toLocaleString("en-US",{timeZone:"Asia/Seoul"})); |
| | const timeString=kstTime.toLocaleString('ko-KR',{year:'numeric',month:'long',day:'numeric',hour:'2-digit',minute:'2-digit',second:'2-digit',weekday:'long'}); |
| | elements.currentTimeDiv.textContent=timeString; |
| | } |
| | updateCurrentTime(); |
| | setInterval(updateCurrentTime,1000); |
| | |
| | elements.userNameInput.value=userName; |
| | elements.userEmailInput.value=userEmail; |
| | elements.userNameInput.addEventListener('input',()=>{userName=elements.userNameInput.value;localStorage.setItem('userName',userName);}); |
| | elements.userEmailInput.addEventListener('input',()=>{userEmail=elements.userEmailInput.value.trim();localStorage.setItem('userEmail',userEmail);}); |
| | |
| | elements.searchToggle.addEventListener('click',()=>{webSearchEnabled=!webSearchEnabled;elements.searchToggle.classList.toggle('active',webSearchEnabled);showToast(webSearchEnabled?'μΉ κ²μ νμ±ν':'μΉ κ²μ λΉνμ±ν','success');}); |
| | elements.learningToggle.addEventListener('click',()=>{selfLearningEnabled=!selfLearningEnabled;elements.learningToggle.classList.toggle('active',selfLearningEnabled);showToast(selfLearningEnabled?'μκ° νμ΅ νμ±ν':'μκ° νμ΅ λΉνμ±ν','success');}); |
| | elements.streamingToggle.addEventListener('click',()=>{streamingEnabled=!streamingEnabled;elements.streamingToggle.classList.toggle('active',streamingEnabled);showToast(streamingEnabled?'μ€νΈλ¦¬λ° νμ±ν':'μ€νΈλ¦¬λ° λΉνμ±ν','success');}); |
| | elements.autosaveToggle.addEventListener('click',()=>{ |
| | autoSaveEnabled=!autoSaveEnabled; |
| | elements.autosaveToggle.classList.toggle('active',autoSaveEnabled); |
| | if(autoSaveEnabled){startAutoSave();showToast('μλ μ μ₯ νμ±ν','success');} |
| | else{if(autoSaveInterval)clearInterval(autoSaveInterval);showToast('μλ μ μ₯ λΉνμ±ν','success');} |
| | }); |
| | |
| | async function deleteMemory(memoryId,element){ |
| | if(!confirm('μ΄ κΈ°μ΅μ μμ νμκ² μ΅λκΉ?'))return; |
| | try{ |
| | const response=await fetch('/memory/'+memoryId+'?user_email='+encodeURIComponent(userEmail),{method:'DELETE'}); |
| | if(response.ok){element.remove();showToast('κΈ°μ΅μ΄ μμ λμμ΅λλ€.','success');await loadMemories();} |
| | else{showToast('μμ μ€ν¨','error');} |
| | }catch(error){console.error('Delete memory error:',error);showToast('μμ μ€ μ€λ₯κ° λ°μνμ΅λλ€.','error');} |
| | } |
| | |
| | async function sendMessage(){ |
| | const message=elements.textInput.value.trim(); |
| | userEmail=elements.userEmailInput.value.trim(); |
| | if(!message){showToast('λ©μμ§λ₯Ό μ
λ ₯ν΄μ£ΌμΈμ.','warning');return;} |
| | if(isStreaming){showToast('μλ΅ μμ± μ€μ
λλ€.','warning');return;} |
| | if(!userEmail||!userEmail.includes('@')){showToast('μ¬λ°λ₯Έ μ΄λ©μΌμ μ
λ ₯ν΄μ£ΌμΈμ.','error');elements.userEmailInput.focus();return;} |
| | localStorage.setItem('userEmail',userEmail); |
| | if(!currentSessionId){const success=await startNewSession();if(!success){showToast('μΈμ
μμ± μ€ν¨. λ€μ μλν΄μ£ΌμΈμ.','error');return;}} |
| | addMessage('user',message); |
| | elements.textInput.value=''; |
| | if(streamingEnabled){await sendStreamingMessage(message);} |
| | else{showToast('μ€νΈλ¦¬λ°μ νμ±νν΄μ£ΌμΈμ.','warning');} |
| | } |
| | |
| | async function sendStreamingMessage(message){ |
| | isStreaming=true; |
| | elements.sendButton.disabled=true; |
| | elements.streamingIndicator.classList.add('active'); |
| | const assistantMessage=document.createElement('div'); |
| | assistantMessage.classList.add('message','assistant','streaming'); |
| | elements.chatMessages.appendChild(assistantMessage); |
| | try{ |
| | const response=await fetch('/chat/text/stream',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({message:message,web_search_enabled:webSearchEnabled,session_id:currentSessionId,user_email:userEmail,user_name:userName,memories:userMemories,self_learning_enabled:selfLearningEnabled})}); |
| | if(!response.ok){const errorData=await response.json();assistantMessage.classList.remove('streaming');assistantMessage.textContent='μ€λ₯: '+(errorData.error||'μλ² μ€λ₯');showToast(errorData.error||'μλ² μ€λ₯κ° λ°μνμ΅λλ€.','error');return;} |
| | const reader=response.body.getReader(); |
| | const decoder=new TextDecoder(); |
| | let content='',buffer=''; |
| | while(true){ |
| | const{done,value}=await reader.read(); |
| | if(done)break; |
| | buffer+=decoder.decode(value,{stream:true}); |
| | const lines=buffer.split('\n'); |
| | buffer=lines.pop()||''; |
| | for(const line of lines){ |
| | if(line.startsWith('data: ')){ |
| | const data=line.slice(6); |
| | if(data==='[DONE]'){assistantMessage.classList.remove('streaming');break;} |
| | try{const parsed=JSON.parse(data);if(parsed.content){content+=parsed.content;assistantMessage.innerHTML=marked.parse(content);elements.chatMessages.scrollTop=elements.chatMessages.scrollHeight;}} |
| | catch(e){console.log('Parse error:',e,data);} |
| | } |
| | } |
| | } |
| | }catch(error){console.error('Streaming error:',error);assistantMessage.classList.remove('streaming');assistantMessage.textContent='μ€λ₯: '+error.message;showToast('μ€νΈλ¦¬λ° μ€ μ€λ₯κ° λ°μνμ΅λλ€.','error');} |
| | finally{isStreaming=false;elements.sendButton.disabled=false;elements.streamingIndicator.classList.remove('active');await loadMemories();await loadHealthStatus();await loadHealthHistory();} |
| | } |
| | |
| | function addMessage(role,content){ |
| | const messageDiv=document.createElement('div'); |
| | messageDiv.classList.add('message',role); |
| | if(role==='assistant'){messageDiv.innerHTML=marked.parse(content);} |
| | else{messageDiv.textContent=content;} |
| | elements.chatMessages.appendChild(messageDiv); |
| | elements.chatMessages.scrollTop=elements.chatMessages.scrollHeight; |
| | } |
| | |
| | function displayMemories(memories){ |
| | elements.memoryList.innerHTML=''; |
| | memories.forEach(memory=>{ |
| | const item=document.createElement('div'); |
| | item.className='memory-item'; |
| | item.dataset.id=memory.id; |
| | const importance=(memory.importance*100).toFixed(0); |
| | item.innerHTML=` |
| | <div class="memory-score">${importance}%</div> |
| | <div class="memory-actions"> |
| | <span class="memory-action-btn like" onclick="event.stopPropagation();sendMemoryFeedback(${memory.id},'μ’μμ')" title="μ’μμ">π</span> |
| | <span class="memory-action-btn dislike" onclick="event.stopPropagation();sendMemoryFeedback(${memory.id},'λ³λ‘')" title="λ³λ‘">π</span> |
| | <span class="memory-action-btn link" onclick="event.stopPropagation();showMemoryRelations(${memory.id})" title="μ°κ΄ κ΄κ³">π</span> |
| | <span class="memory-action-btn delete" onclick="event.stopPropagation();deleteMemory(${memory.id},this.closest('.memory-item'))" title="μμ ">β</span> |
| | </div> |
| | <div style="font-size:10px;color:#888;margin-bottom:4px;">${memory.category}</div> |
| | <div>${memory.content}</div> |
| | `; |
| | item.onclick=()=>item.classList.toggle('expanded'); |
| | elements.memoryList.appendChild(item); |
| | }); |
| | } |
| | |
| | async function loadMemories(){ |
| | if(!userEmail)return; |
| | try{ |
| | const response=await fetch('/memory/all?user_email='+encodeURIComponent(userEmail)); |
| | const memories=await response.json(); |
| | userMemories={}; |
| | memories.forEach(memory=>{ |
| | if(!userMemories[memory.category]){userMemories[memory.category]=[];} |
| | userMemories[memory.category].push(memory.content); |
| | }); |
| | displayMemories(memories); |
| | elements.memoryCount.textContent=memories.length; |
| | elements.vectorCount.textContent=memories.filter(m=>m.importance>0.7).length; |
| | await loadGraphStats(); |
| | }catch(error){console.error('Failed to load memories:',error);} |
| | } |
| | |
| | async function startNewSession(){ |
| | userEmail=elements.userEmailInput.value.trim(); |
| | if(!userEmail||!userEmail.includes('@')){showToast('μ¬λ°λ₯Έ μ΄λ©μΌμ μ
λ ₯ν΄μ£ΌμΈμ.','warning');return false;} |
| | try{ |
| | const response=await fetch('/session/new',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({user_email:userEmail})}); |
| | const data=await response.json(); |
| | if(data.error){showToast(data.error,'error');return false;} |
| | currentSessionId=data.session_id; |
| | console.log('New session started:',currentSessionId); |
| | await loadMemories(); |
| | await loadHealthStatus(); |
| | await loadHealthHistory(); |
| | await loadRecentHistory(); |
| | await loadStorageInfo(); |
| | startAutoSave(); |
| | const displayName=userName||userEmail.split('@')[0]; |
| | addMessage('assistant','μ¬λ³΄μΈμ, '+displayName+'λ! μ μ§λ΄μ
¨μ΄μ? μ€λ ν루 μ΄λ μ
¨λμ§ μκΈ°ν΄ μ£ΌμΈμ.'); |
| | return true; |
| | }catch(error){console.error('Session creation error:',error);showToast('μΈμ
μμ± μ€ν¨','error');return false;} |
| | } |
| | |
| | async function endSession(){ |
| | if(!currentSessionId||!userEmail){showToast('μ μ₯ν μΈμ
μ΄ μμ΅λλ€.','warning');return;} |
| | try{ |
| | showToast('λν λ΄μ©μ μ μ₯νλ μ€...','success'); |
| | const response=await fetch('/session/end',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({session_id:currentSessionId,user_email:userEmail})}); |
| | if(response.ok){showToast('λνκ° μ±κ³΅μ μΌλ‘ μ μ₯λμμ΅λλ€!','success');await loadMemories();await loadHealthStatus();await loadHealthHistory();await loadRecentHistory();} |
| | }catch(error){console.error('Failed to end session:',error);showToast('μ μ₯ μ€ μ€λ₯κ° λ°μνμ΅λλ€.','error');} |
| | } |
| | |
| | function showToast(message,type){ |
| | const toast=document.getElementById('error-toast'); |
| | toast.textContent=message; |
| | toast.className='toast '+type; |
| | toast.style.display='block'; |
| | setTimeout(()=>{toast.style.display='none';},3000); |
| | } |
| | |
| | elements.textInput.addEventListener('keypress',(e)=>{if(e.key==='Enter'&&!e.shiftKey){e.preventDefault();sendMessage();}}); |
| | elements.sendButton.addEventListener('click',sendMessage); |
| | document.querySelectorAll('.quick-btn').forEach(btn=>{btn.addEventListener('click',()=>{const prompt=btn.dataset.prompt;if(prompt&&!isStreaming){elements.textInput.value=prompt;sendMessage();}});}); |
| | elements.endSessionButton.addEventListener('click',endSession); |
| | elements.memorySearchInput.addEventListener('keypress',(e)=>{if(e.key==='Enter'){searchMemories();}}); |
| | |
| | window.addEventListener('DOMContentLoaded',()=>{ |
| | initHealthChart(); |
| | loadStorageInfo(); |
| | if(userEmail&&userEmail.includes('@')){startNewSession();} |
| | }); |
| | </script> |
| | </body> |
| | </html> |