kt / index.html
openfree's picture
Update index.html
14670b6 verified
<!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')">&times;</span>
<h3>πŸ”— κΈ°μ–΅ μ—°κ΄€ 관계</h3>
<div id="relation-content"></div>
</div>
</div>
<!-- URL 크둀링 λͺ¨λ‹¬ -->
<div class="modal-overlay" id="crawl-modal">
<div class="modal">
<span class="modal-close" onclick="closeModal('crawl-modal')">&times;</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')">&times;</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');
}
// API λ¬Έμ„œ ν† κΈ€
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);
}
}
// URL 크둀링 (μƒˆλ‘œ μΆ”κ°€)
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); // 3λΆ„
}
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>