openfree commited on
Commit
a0f3eef
Β·
verified Β·
1 Parent(s): faf269c

Upload index (9).html

Browse files
Files changed (1) hide show
  1. index (9).html +901 -0
index (9).html ADDED
@@ -0,0 +1,901 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="ko">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>μ‹œλ‹ˆμ–΄ 말벗 AI</title>
7
+ <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
8
+ <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
9
+ <style>
10
+ :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;--file-color:#28a745;--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}
11
+ *{box-sizing:border-box;margin:0;padding:0}
12
+ 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}
13
+ .container{max-width:1600px;margin:0 auto;padding:15px;flex-grow:1;display:flex;flex-direction:column;width:100%}
14
+ .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)}
15
+ .main-content{display:flex;gap:15px;flex-grow:1;min-height:0}
16
+ .sidebar{width:320px;flex-shrink:0;display:flex;flex-direction:column;gap:12px;overflow-y:auto;max-height:calc(100vh - 120px);padding-right:5px}
17
+ .sidebar::-webkit-scrollbar{width:6px}
18
+ .sidebar::-webkit-scrollbar-thumb{background:linear-gradient(135deg,var(--primary-color),var(--secondary-color));border-radius:6px}
19
+ .chat-section{flex-grow:1;display:flex;flex-direction:column;min-width:0}
20
+ .logo{display:flex;align-items:center;justify-content:center;gap:15px}
21
+ .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}
22
+ .current-time{font-size:12px;color:#888;margin-top:5px}
23
+ .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}
24
+ .email-section label{font-size:12px;color:#888;display:block;margin-bottom:6px}
25
+ .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}
26
+ .email-section input:focus{outline:none;border-color:var(--primary-color);box-shadow:0 0 10px rgba(111,66,193,0.2)}
27
+ .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}
28
+ .sidebar-section:hover{border-color:var(--primary-color);box-shadow:0 0 15px rgba(111,66,193,0.1)}
29
+ .sidebar-section h3{margin:0 0 12px 0;font-size:13px;text-transform:uppercase;letter-spacing:0.5px;color:#999;font-weight:600}
30
+ .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)}
31
+ .streaming-indicator.active{display:flex}
32
+ .streaming-dot{width:8px;height:8px;background:var(--streaming-color);border-radius:50%;animation:pulse 1.5s infinite}
33
+ @keyframes pulse{0%,100%{opacity:0.3;transform:scale(0.8)}50%{opacity:1;transform:scale(1.2)}}
34
+ .settings-grid{display:flex;flex-direction:column;gap:12px}
35
+ .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}
36
+ .setting-item:hover{background:rgba(111,66,193,0.1)}
37
+ .setting-label{font-size:12px;color:#aaa;display:flex;align-items:center;gap:6px}
38
+ .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)}
39
+ .toggle-switch.active{background:linear-gradient(135deg,var(--primary-color),var(--hover-color));box-shadow:0 0 10px rgba(111,66,193,0.3)}
40
+ .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)}
41
+ .toggle-switch.active .toggle-slider{transform:translateX(18px)}
42
+ /* 건강 진단 μ„Ήμ…˜ */
43
+ .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)}
44
+ .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}
45
+ .gauge-container{position:relative;width:150px;height:75px;overflow:hidden}
46
+ .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}
47
+ .gauge-center{position:absolute;width:100px;height:100px;background:var(--card-bg);border-radius:50%;top:25px;left:25px}
48
+ .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)}
49
+ .gauge-value{position:absolute;bottom:5px;left:50%;transform:translateX(-50%);font-size:18px;font-weight:bold;color:white}
50
+ .gauge-label{margin-top:10px;font-size:14px;font-weight:600;padding:5px 15px;border-radius:20px;text-transform:uppercase}
51
+ .gauge-label.excellent{background:linear-gradient(135deg,#00ff00,#28a745);color:#000}
52
+ .gauge-label.good{background:linear-gradient(135deg,#28a745,#5cb85c);color:#fff}
53
+ .gauge-label.normal{background:linear-gradient(135deg,#ffc107,#ff9800);color:#000}
54
+ .gauge-label.warning{background:linear-gradient(135deg,#dc3545,#c0392b);color:#fff}
55
+ .gauge-label.critical{background:linear-gradient(135deg,#ff0000,#8b0000);color:#fff;animation:blink-critical 1s infinite}
56
+ @keyframes blink-critical{0%,100%{opacity:1}50%{opacity:0.5}}
57
+ .health-stats{display:grid;grid-template-columns:1fr 1fr;gap:8px}
58
+ .health-stat{background:rgba(0,0,0,0.3);padding:10px;border-radius:8px;text-align:center}
59
+ .health-stat-value{font-size:14px;font-weight:bold;margin-bottom:4px;padding:4px 8px;border-radius:12px;display:inline-block}
60
+ .health-stat-value.excellent{background:linear-gradient(135deg,#00ff00,#28a745);color:#000}
61
+ .health-stat-value.good{background:linear-gradient(135deg,#28a745,#5cb85c);color:#fff}
62
+ .health-stat-value.normal{background:linear-gradient(135deg,#ffc107,#ff9800);color:#000}
63
+ .health-stat-value.warning{background:linear-gradient(135deg,#dc3545,#c0392b);color:#fff}
64
+ .health-stat-value.critical{background:linear-gradient(135deg,#ff0000,#8b0000);color:#fff}
65
+ .health-stat-label{font-size:10px;color:#888;text-transform:uppercase}
66
+ .health-chart-container{height:150px;margin-top:10px;background:rgba(0,0,0,0.2);border-radius:8px;padding:10px}
67
+ .memory-stats{display:flex;justify-content:space-around;padding:10px;background:rgba(0,0,0,0.3);border-radius:6px;margin-bottom:10px}
68
+ .stat-item{text-align:center}
69
+ .stat-value{font-size:18px;font-weight:bold;color:var(--primary-color)}
70
+ .stat-label{font-size:10px;color:#888;text-transform:uppercase}
71
+ .memory-list{max-height:150px;overflow-y:auto}
72
+ .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}
73
+ .memory-item:hover{transform:translateX(5px);box-shadow:0 2px 10px rgba(74,158,255,0.2)}
74
+ .memory-item:hover .memory-delete{opacity:1}
75
+ .memory-score{position:absolute;top:5px;right:30px;font-size:10px;color:var(--memory-color);background:rgba(74,158,255,0.2);padding:2px 6px;border-radius:4px}
76
+ .memory-delete{position:absolute;top:5px;right:5px;font-size:14px;color:#ff6b6b;cursor:pointer;opacity:0;transition:opacity 0.3s;background:rgba(255,107,107,0.2);width:20px;height:20px;border-radius:50%;display:flex;align-items:center;justify-content:center;line-height:1}
77
+ .memory-delete:hover{background:rgba(255,107,107,0.4);transform:scale(1.1)}
78
+ .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%}
79
+ .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)}
80
+ .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}
81
+ @keyframes slideIn{from{opacity:0;transform:translateY(10px)}to{opacity:1;transform:translateY(0)}}
82
+ .message.user{background:linear-gradient(135deg,#2c3e50,#34495e);margin-left:auto;border-bottom-right-radius:4px}
83
+ .message.assistant{background:linear-gradient(135deg,#c44569,#ff6b9d);margin-right:auto;border-bottom-left-radius:4px}
84
+ .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)}
85
+ .message.assistant.streaming::after{content:'β–Œ';animation:blink 1s infinite;color:white}
86
+ @keyframes blink{0%,50%{opacity:1}51%,100%{opacity:0}}
87
+ .message h1,.message h2,.message h3,.message h4{margin:0.8em 0 0.5em 0;color:#fff;font-weight:600}
88
+ .message h1{font-size:1.4em;border-bottom:2px solid rgba(255,255,255,0.3);padding-bottom:8px}
89
+ .message h2{font-size:1.2em;border-bottom:1px solid rgba(255,255,255,0.2);padding-bottom:6px}
90
+ .message h3{font-size:1.1em}
91
+ .message p{margin:0.5em 0;line-height:1.7}
92
+ .message ul,.message ol{margin:0.5em 0;padding-left:1.5em}
93
+ .message li{margin:0.4em 0;line-height:1.6}
94
+ .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}
95
+ .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)}
96
+ .message pre code{background:none;padding:0;color:#abb2bf}
97
+ .message table{width:100%;border-collapse:separate;border-spacing:0;margin:1em 0;font-size:0.9em;border-radius:8px;overflow:hidden;border:2px solid var(--table-border)}
98
+ .message thead{background:var(--table-header)}
99
+ .message th{padding:12px 15px;text-align:left;font-weight:600;color:#fff;border-bottom:2px solid var(--table-border);white-space:nowrap}
100
+ .message td{padding:10px 15px;border-bottom:1px solid rgba(111,66,193,0.3);vertical-align:top}
101
+ .message tbody tr:nth-child(even){background:var(--table-row-even)}
102
+ .message tbody tr:nth-child(odd){background:rgba(0,0,0,0.2)}
103
+ .message tbody tr:hover{background:var(--table-row-hover)}
104
+ .message tbody tr:last-child td{border-bottom:none}
105
+ .message blockquote{border-left:4px solid #ff6b9d;padding:10px 15px;margin:1em 0;background:rgba(255,107,157,0.1);border-radius:0 8px 8px 0;font-style:italic}
106
+ .message hr{border:none;height:1px;background:linear-gradient(90deg,transparent,rgba(255,255,255,0.3),transparent);margin:1.5em 0}
107
+ .message a{color:#4a9eff;text-decoration:none;border-bottom:1px dotted #4a9eff}
108
+ .message a:hover{color:#7bb8ff;border-bottom-style:solid}
109
+ .message strong{color:#fff;font-weight:600}
110
+ .message em{color:#ffb6c1;font-style:italic}
111
+ .text-input-section{margin-top:12px}
112
+ .quick-actions{display:flex;gap:8px;padding:8px 12px;justify-content:center;flex-wrap:wrap}
113
+ .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}
114
+ .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)}
115
+ .quick-btn:active{transform:scale(0.95)}
116
+ .input-container{display:flex;gap:10px;align-items:center}
117
+ #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}
118
+ #text-input:focus{outline:none;border-color:#ff6b9d;box-shadow:0 0 10px rgba(255,107,157,0.2)}
119
+ 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}
120
+ button:hover:not(:disabled){transform:translateY(-2px);box-shadow:0 4px 12px rgba(255,107,157,0.5)}
121
+ button:disabled{opacity:0.5;cursor:not-allowed}
122
+ #send-button{background:linear-gradient(135deg,#2ecc71,#27ae60)}
123
+ .file-upload-button{background:linear-gradient(135deg,var(--file-color),#229954);width:100%;display:flex;align-items:center;justify-content:center;gap:8px;padding:10px}
124
+ .uploaded-files{margin-top:10px;max-height:80px;overflow-y:auto}
125
+ .uploaded-file{display:flex;justify-content:space-between;align-items:center;padding:8px 10px;margin-bottom:6px;background:rgba(40,167,69,0.1);border-radius:6px;border:1px solid rgba(40,167,69,0.3);font-size:12px;transition:all 0.3s}
126
+ .uploaded-file:hover{background:rgba(40,167,69,0.2)}
127
+ .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}
128
+ @keyframes slideDown{from{transform:translateX(-50%) translateY(-20px);opacity:0}to{transform:translateX(-50%) translateY(0);opacity:1}}
129
+ .toast.success{background:linear-gradient(135deg,#4caf50,#45a049);color:white}
130
+ .toast.error{background:linear-gradient(135deg,#f44336,#e53935);color:white}
131
+ .toast.warning{background:linear-gradient(135deg,var(--warning-color),#ff9800);color:#333}
132
+ /* API μ„Ήμ…˜ */
133
+ .api-section{background:rgba(30,30,30,0.95);border-top:1px solid var(--border-color);padding:15px;margin-top:15px}
134
+ .api-section h3{text-align:center;margin-bottom:10px;color:#888;font-size:12px;text-transform:uppercase}
135
+ .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}
136
+ .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}
137
+ .api-endpoint h4{color:#ff6b9d;margin-bottom:10px;font-size:14px}
138
+ .api-method{display:inline-block;padding:3px 8px;border-radius:4px;font-size:11px;font-weight:bold;margin-right:10px}
139
+ .api-method.get{background:#28a745;color:white}
140
+ .api-method.post{background:#007bff;color:white}
141
+ .api-method.delete{background:#dc3545;color:white}
142
+ .api-url{font-family:monospace;color:#4a9eff;font-size:13px}
143
+ .api-desc{color:#aaa;font-size:12px;margin-top:8px;line-height:1.5}
144
+ .api-params{margin-top:10px;font-size:12px}
145
+ .api-params code{background:rgba(0,0,0,0.4);padding:2px 6px;border-radius:3px;color:#e06c75}
146
+ .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}
147
+ .collapse-btn:hover{background:rgba(111,66,193,0.2);color:white}
148
+ @media (max-width:768px){.main-content{flex-direction:column}.sidebar{width:100%;max-height:none}.health-stats{grid-template-columns:1fr 1fr}}
149
+ </style>
150
+ </head>
151
+ <body>
152
+ <div id="error-toast" class="toast"></div>
153
+ <div class="container">
154
+ <div class="header">
155
+ <div class="logo"><h1>μ‹œλ‹ˆμ–΄ 말벗 AI</h1></div>
156
+ <div class="current-time" id="current-time"></div>
157
+ </div>
158
+ <div class="main-content">
159
+ <div class="sidebar">
160
+ <div class="email-section">
161
+ <label>이메일 (ν•„μˆ˜)</label>
162
+ <input type="email" id="user-email" placeholder="your@email.com">
163
+ </div>
164
+ <!-- 건강 진단 μ„Ήμ…˜ -->
165
+ <div class="sidebar-section health-section">
166
+ <h3>건강 μƒνƒœ 진단</h3>
167
+ <div class="health-gauge">
168
+ <div class="gauge-container">
169
+ <div class="gauge-bg"></div>
170
+ <div class="gauge-center"></div>
171
+ <div class="gauge-needle" id="gauge-needle"></div>
172
+ <div class="gauge-value" id="gauge-value">μ–‘ν˜Έ</div>
173
+ </div>
174
+ <div class="gauge-label good" id="gauge-label">μ–‘ν˜Έ</div>
175
+ </div>
176
+ <div class="health-stats">
177
+ <div class="health-stat">
178
+ <div class="health-stat-value good" id="cognitive-value">μ–‘ν˜Έ</div>
179
+ <div class="health-stat-label">인지λŠ₯λ ₯</div>
180
+ </div>
181
+ <div class="health-stat">
182
+ <div class="health-stat-value good" id="conversation-value">μ–‘ν˜Έ</div>
183
+ <div class="health-stat-label">λŒ€ν™”λŠ₯λ ₯</div>
184
+ </div>
185
+ <div class="health-stat">
186
+ <div class="health-stat-value good" id="memory-value">μ–‘ν˜Έ</div>
187
+ <div class="health-stat-label">κΈ°μ–΅λŠ₯λ ₯</div>
188
+ </div>
189
+ <div class="health-stat">
190
+ <div class="health-stat-value good" id="emotional-value">μ–‘ν˜Έ</div>
191
+ <div class="health-stat-label">κ°μ •μƒνƒœ</div>
192
+ </div>
193
+ </div>
194
+ <div class="health-chart-container">
195
+ <canvas id="health-chart"></canvas>
196
+ </div>
197
+ </div>
198
+ <div class="sidebar-section">
199
+ <h3>μ„€μ •</h3>
200
+ <div class="settings-grid">
201
+ <div class="setting-item">
202
+ <span class="setting-label">μ›Ή 검색</span>
203
+ <div id="search-toggle" class="toggle-switch active"><div class="toggle-slider"></div></div>
204
+ </div>
205
+ <div class="setting-item">
206
+ <span class="setting-label">μžκ°€ ν•™μŠ΅</span>
207
+ <div id="learning-toggle" class="toggle-switch active"><div class="toggle-slider"></div></div>
208
+ </div>
209
+ <div class="setting-item">
210
+ <span class="setting-label">슀트리밍</span>
211
+ <div id="streaming-toggle" class="toggle-switch active"><div class="toggle-slider"></div></div>
212
+ </div>
213
+ </div>
214
+ <div style="margin-top:12px;">
215
+ <label style="font-size:12px;color:#888;">이름 (호칭)</label>
216
+ <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;">
217
+ </div>
218
+ </div>
219
+ <div class="sidebar-section">
220
+ <h3>파일 μ—…λ‘œλ“œ</h3>
221
+ <button class="file-upload-button" id="file-upload-button">파일 선택</button>
222
+ <input type="file" id="file-input" multiple accept=".pdf,.csv,.txt" style="display:none;">
223
+ <div class="uploaded-files" id="uploaded-files"></div>
224
+ </div>
225
+ <div class="sidebar-section">
226
+ <h3>λŒ€ν™” κΈ°μ–΅</h3>
227
+ <div class="memory-stats">
228
+ <div class="stat-item"><div class="stat-value" id="memory-count">0</div><div class="stat-label">총 κΈ°μ–΅</div></div>
229
+ <div class="stat-item"><div class="stat-value" id="vector-count">0</div><div class="stat-label">μ€‘μš” κΈ°μ–΅</div></div>
230
+ <div class="stat-item"><div class="stat-value" id="relation-count">0</div><div class="stat-label">μ—°κ΄€ 관계</div></div>
231
+ </div>
232
+ <div class="memory-list" id="memory-list"></div>
233
+ </div>
234
+ <div style="text-align:center;margin-top:10px;">
235
+ <button id="end-session-button">λŒ€ν™” μ €μž₯ν•˜κΈ°</button>
236
+ </div>
237
+ </div>
238
+ <div class="chat-section">
239
+ <div class="chat-container">
240
+ <div class="streaming-indicator" id="streaming-indicator"><div class="streaming-dot"></div><span style="font-size:12px;color:var(--streaming-color);">응닡 생성 쀑...</span></div>
241
+ <div class="chat-messages" id="chat-messages"></div>
242
+ <div class="quick-actions">
243
+ <button class="quick-btn" data-prompt="였늘 기뢄이 μ–΄λ– μ„Έμš”?" title="μ•ˆλΆ€ 인사">μ•ˆλΆ€ 인사</button>
244
+ <button class="quick-btn" data-prompt="였늘 날씨가 μ–΄λ•Œμš”?" title="날씨 이야기">날씨 이야기</button>
245
+ <button class="quick-btn" data-prompt="μš”μ¦˜ 건강은 μ–΄λ– μ„Έμš”?" title="건강 체크">건강 체크</button>
246
+ <button class="quick-btn" data-prompt="μž¬λ―ΈμžˆλŠ” 이야기 ν•΄μ£Όμ„Έμš”" title="이야기">이야기 ν•΄μ€˜</button>
247
+ <button class="quick-btn" data-prompt="였늘 λ‰΄μŠ€ μ’€ μ•Œλ €μ£Όμ„Έμš”" title="λ‰΄μŠ€">였늘 λ‰΄μŠ€</button>
248
+ </div>
249
+ <div class="text-input-section">
250
+ <div class="input-container">
251
+ <input type="text" id="text-input" placeholder="νŽΈν•˜κ²Œ 말씀해 μ£Όμ„Έμš”...">
252
+ <button id="send-button">전솑</button>
253
+ </div>
254
+ </div>
255
+ </div>
256
+ </div>
257
+ </div>
258
+ </div>
259
+ <!-- API λ¬Έμ„œ μ„Ήμ…˜ -->
260
+ <div class="api-section" id="api-section">
261
+ <button class="collapse-btn" id="api-collapse-btn">API λ¬Έμ„œ μ—΄κΈ°/λ‹«κΈ°</button>
262
+ <h3>API μ—”λ“œν¬μΈνŠΈ κ°€μ΄λ“œ</h3>
263
+ <div class="api-docs-container" id="api-docs-container">
264
+ <div class="api-endpoint">
265
+ <h4>μ±„νŒ… 슀트리밍</h4>
266
+ <span class="api-method post">POST</span>
267
+ <span class="api-url">/chat/text/stream</span>
268
+ <div class="api-desc">μ‹€μ‹œκ°„ 슀트리밍 λ°©μ‹μœΌλ‘œ AI와 λŒ€ν™”ν•©λ‹ˆλ‹€.</div>
269
+ <div class="api-params">
270
+ <strong>Parameters:</strong><br>
271
+ <code>message</code> - μ‚¬μš©μž λ©”μ‹œμ§€<br>
272
+ <code>user_email</code> - μ‚¬μš©μž 이메일 (ν•„μˆ˜)<br>
273
+ <code>session_id</code> - μ„Έμ…˜ ID<br>
274
+ <code>web_search_enabled</code> - μ›Ή 검색 ν™œμ„±ν™” μ—¬λΆ€<br>
275
+ <code>self_learning_enabled</code> - μžκ°€ ν•™μŠ΅ ν™œμ„±ν™” μ—¬λΆ€
276
+ </div>
277
+ </div>
278
+ <div class="api-endpoint">
279
+ <h4>μƒˆ μ„Έμ…˜ 생성</h4>
280
+ <span class="api-method post">POST</span>
281
+ <span class="api-url">/session/new</span>
282
+ <div class="api-desc">μƒˆλ‘œμš΄ λŒ€ν™” μ„Έμ…˜μ„ μ‹œμž‘ν•©λ‹ˆλ‹€.</div>
283
+ <div class="api-params">
284
+ <strong>Parameters:</strong><br>
285
+ <code>user_email</code> - μ‚¬μš©μž 이메일 (ν•„μˆ˜)
286
+ </div>
287
+ </div>
288
+ <div class="api-endpoint">
289
+ <h4>μ„Έμ…˜ μ’…λ£Œ 및 μ €μž₯</h4>
290
+ <span class="api-method post">POST</span>
291
+ <span class="api-url">/session/end</span>
292
+ <div class="api-desc">μ„Έμ…˜μ„ μ’…λ£Œν•˜κ³  λŒ€ν™” λ‚΄μš©μ„ κΈ°μ–΅μœΌλ‘œ μ €μž₯ν•©λ‹ˆλ‹€.</div>
293
+ <div class="api-params">
294
+ <strong>Parameters:</strong><br>
295
+ <code>session_id</code> - μ„Έμ…˜ ID<br>
296
+ <code>user_email</code> - μ‚¬μš©μž 이메일
297
+ </div>
298
+ </div>
299
+ <div class="api-endpoint">
300
+ <h4>건강 μƒνƒœ 쑰회</h4>
301
+ <span class="api-method get">GET</span>
302
+ <span class="api-url">/health/current?user_email={email}</span>
303
+ <div class="api-desc">ν˜„μž¬ 건강 μƒνƒœ 진단 κ²°κ³Όλ₯Ό μ‘°νšŒν•©λ‹ˆλ‹€.</div>
304
+ </div>
305
+ <div class="api-endpoint">
306
+ <h4>건강 νžˆμŠ€ν† λ¦¬</h4>
307
+ <span class="api-method get">GET</span>
308
+ <span class="api-url">/health/history?user_email={email}&days={days}</span>
309
+ <div class="api-desc">졜근 건강 μƒνƒœ λ³€ν™” 좔이λ₯Ό μ‘°νšŒν•©λ‹ˆλ‹€.</div>
310
+ </div>
311
+ <div class="api-endpoint">
312
+ <h4>κΈ°μ–΅ 검색</h4>
313
+ <span class="api-method post">POST</span>
314
+ <span class="api-url">/memory/search</span>
315
+ <div class="api-desc">μ €μž₯된 기얡을 μ‹œλ§¨ν‹± κ²€μƒ‰ν•©λ‹ˆλ‹€.</div>
316
+ <div class="api-params">
317
+ <strong>Parameters:</strong><br>
318
+ <code>query</code> - 검색어<br>
319
+ <code>user_email</code> - μ‚¬μš©μž 이메일<br>
320
+ <code>k</code> - 결과 개수 (기본 10)<br>
321
+ <code>threshold</code> - μœ μ‚¬λ„ μž„κ³„κ°’ (κΈ°λ³Έ 0.7)
322
+ </div>
323
+ </div>
324
+ <div class="api-endpoint">
325
+ <h4>λͺ¨λ“  κΈ°μ–΅ 쑰회</h4>
326
+ <span class="api-method get">GET</span>
327
+ <span class="api-url">/memory/all?user_email={email}</span>
328
+ <div class="api-desc">μ‚¬μš©μžμ˜ λͺ¨λ“  기얡을 μ‘°νšŒν•©λ‹ˆλ‹€.</div>
329
+ </div>
330
+ <div class="api-endpoint">
331
+ <h4>κΈ°μ–΅ μ‚­μ œ</h4>
332
+ <span class="api-method delete">DELETE</span>
333
+ <span class="api-url">/memory/{memory_id}?user_email={email}</span>
334
+ <div class="api-desc">νŠΉμ • 기얡을 μ‚­μ œν•©λ‹ˆλ‹€.</div>
335
+ </div>
336
+ <div class="api-endpoint">
337
+ <h4>파일 μ—…λ‘œλ“œ</h4>
338
+ <span class="api-method post">POST</span>
339
+ <span class="api-url">/upload/file</span>
340
+ <div class="api-desc">PDF, CSV, TXT νŒŒμΌμ„ μ—…λ‘œλ“œν•©λ‹ˆλ‹€.</div>
341
+ <div class="api-params">
342
+ <strong>Form Data:</strong><br>
343
+ <code>file</code> - μ—…λ‘œλ“œν•  파일<br>
344
+ <code>session_id</code> - μ„Έμ…˜ ID<br>
345
+ <code>user_email</code> - μ‚¬μš©μž 이메일
346
+ </div>
347
+ </div>
348
+ <div class="api-endpoint">
349
+ <h4>μ›Ή 검색</h4>
350
+ <span class="api-method post">POST</span>
351
+ <span class="api-url">/search/web</span>
352
+ <div class="api-desc">μ›Ή 검색을 μˆ˜ν–‰ν•©λ‹ˆλ‹€.</div>
353
+ <div class="api-params">
354
+ <strong>Parameters:</strong><br>
355
+ <code>query</code> - 검색어<br>
356
+ <code>count</code> - 결과 개수 (기본 10)
357
+ </div>
358
+ </div>
359
+ <div class="api-endpoint">
360
+ <h4>URL 크둀링</h4>
361
+ <span class="api-method post">POST</span>
362
+ <span class="api-url">/crawl/url</span>
363
+ <div class="api-desc">νŠΉμ • URL의 λ‚΄μš©μ„ ν¬λ‘€λ§ν•©λ‹ˆλ‹€.</div>
364
+ <div class="api-params">
365
+ <strong>Parameters:</strong><br>
366
+ <code>url</code> - 크둀링할 URL
367
+ </div>
368
+ </div>
369
+ <div class="api-endpoint">
370
+ <h4>μŠ€ν† λ¦¬μ§€ 정보</h4>
371
+ <span class="api-method get">GET</span>
372
+ <span class="api-url">/storage/info</span>
373
+ <div class="api-desc">ν˜„μž¬ μŠ€ν† λ¦¬μ§€ μƒνƒœλ₯Ό μ‘°νšŒν•©λ‹ˆλ‹€.</div>
374
+ </div>
375
+ </div>
376
+ </div>
377
+ <script>
378
+ let webSearchEnabled=true;
379
+ let selfLearningEnabled=true;
380
+ let streamingEnabled=true;
381
+ let currentSessionId=null;
382
+ let userName=localStorage.getItem('userName')||'';
383
+ let userEmail=localStorage.getItem('userEmail')||'';
384
+ let userMemories={};
385
+ let uploadedFiles=[];
386
+ let isStreaming=false;
387
+ let healthChart=null;
388
+ marked.setOptions({breaks:true,gfm:true,headerIds:false,mangle:false});
389
+
390
+ // 숫자λ₯Ό 4단계 ν…μŠ€νŠΈλ‘œ λ³€ν™˜
391
+ function scoreToLabel(score) {
392
+ if (score >= 80) return { text: 'μ–‘ν˜Έ', class: 'good' };
393
+ if (score >= 60) return { text: '보톡', class: 'normal' };
394
+ if (score >= 40) return { text: 'μœ„ν—˜', class: 'warning' };
395
+ return { text: 'κΈ΄κΈ‰', class: 'critical' };
396
+ }
397
+
398
+ const elements={
399
+ endSessionButton:document.getElementById('end-session-button'),
400
+ sendButton:document.getElementById('send-button'),
401
+ chatMessages:document.getElementById('chat-messages'),
402
+ searchToggle:document.getElementById('search-toggle'),
403
+ learningToggle:document.getElementById('learning-toggle'),
404
+ streamingToggle:document.getElementById('streaming-toggle'),
405
+ textInput:document.getElementById('text-input'),
406
+ memoryList:document.getElementById('memory-list'),
407
+ userNameInput:document.getElementById('user-name'),
408
+ userEmailInput:document.getElementById('user-email'),
409
+ fileUploadButton:document.getElementById('file-upload-button'),
410
+ fileInput:document.getElementById('file-input'),
411
+ uploadedFilesDiv:document.getElementById('uploaded-files'),
412
+ currentTimeDiv:document.getElementById('current-time'),
413
+ streamingIndicator:document.getElementById('streaming-indicator'),
414
+ memoryCount:document.getElementById('memory-count'),
415
+ vectorCount:document.getElementById('vector-count'),
416
+ relationCount:document.getElementById('relation-count'),
417
+ gaugeNeedle:document.getElementById('gauge-needle'),
418
+ gaugeValue:document.getElementById('gauge-value'),
419
+ gaugeLabel:document.getElementById('gauge-label'),
420
+ cognitiveValue:document.getElementById('cognitive-value'),
421
+ conversationValue:document.getElementById('conversation-value'),
422
+ memoryValue:document.getElementById('memory-value'),
423
+ emotionalValue:document.getElementById('emotional-value'),
424
+ apiCollapseBtn:document.getElementById('api-collapse-btn'),
425
+ apiDocsContainer:document.getElementById('api-docs-container')
426
+ };
427
+
428
+ // API μ„Ήμ…˜ ν† κΈ€
429
+ elements.apiCollapseBtn.addEventListener('click',()=>{
430
+ const container=elements.apiDocsContainer;
431
+ if(container.style.display==='none' || container.style.display===''){
432
+ container.style.display='block';
433
+ elements.apiCollapseBtn.textContent='API λ¬Έμ„œ λ‹«κΈ°';
434
+ }else{
435
+ container.style.display='none';
436
+ elements.apiCollapseBtn.textContent='API λ¬Έμ„œ μ—΄κΈ°/λ‹«κΈ°';
437
+ }
438
+ });
439
+
440
+ // 건강 차트 μ΄ˆκΈ°ν™”
441
+ function initHealthChart(){
442
+ const ctx=document.getElementById('health-chart').getContext('2d');
443
+ healthChart=new Chart(ctx,{
444
+ type:'line',
445
+ data:{
446
+ labels:[],
447
+ datasets:[
448
+ {label:'인지',data:[],borderColor:'#4a9eff',backgroundColor:'rgba(74,158,255,0.1)',tension:0.4,fill:false,pointRadius:2},
449
+ {label:'λŒ€ν™”',data:[],borderColor:'#9b59b6',backgroundColor:'rgba(155,89,182,0.1)',tension:0.4,fill:false,pointRadius:2},
450
+ {label:'κΈ°μ–΅',data:[],borderColor:'#2ecc71',backgroundColor:'rgba(46,204,113,0.1)',tension:0.4,fill:false,pointRadius:2},
451
+ {label:'감정',data:[],borderColor:'#e74c3c',backgroundColor:'rgba(231,76,60,0.1)',tension:0.4,fill:false,pointRadius:2}
452
+ ]
453
+ },
454
+ options:{
455
+ responsive:true,
456
+ maintainAspectRatio:false,
457
+ plugins:{legend:{display:true,position:'bottom',labels:{boxWidth:8,padding:8,font:{size:9},color:'#888'}}},
458
+ scales:{
459
+ x:{display:true,grid:{color:'rgba(255,255,255,0.05)'},ticks:{font:{size:8},color:'#666',maxRotation:0}},
460
+ y:{display:true,min:0,max:100,grid:{color:'rgba(255,255,255,0.05)'},ticks:{font:{size:8},color:'#666'}}
461
+ }
462
+ }
463
+ });
464
+ }
465
+
466
+ // κ²Œμ΄μ§€ μ—…λ°μ΄νŠΈ (4단계 ν…μŠ€νŠΈ)
467
+ function updateGauge(riskLevel,riskLabel){
468
+ const angle=-90+(riskLevel/100)*180;
469
+ elements.gaugeNeedle.style.transform=`rotate(${angle}deg)`;
470
+
471
+ const label = scoreToLabel(riskLevel);
472
+ elements.gaugeValue.textContent=label.text;
473
+ elements.gaugeLabel.textContent=riskLabel || label.text;
474
+ elements.gaugeLabel.className='gauge-label ' + label.class;
475
+ }
476
+
477
+ // 건강 μ§€ν‘œ μ—…λ°μ΄νŠΈ (4단계 ν…μŠ€νŠΈ)
478
+ function updateHealthStat(element, score) {
479
+ const label = scoreToLabel(score);
480
+ element.textContent = label.text;
481
+ element.className = 'health-stat-value ' + label.class;
482
+ }
483
+
484
+ // 건강 μƒνƒœ λ‘œλ“œ
485
+ async function loadHealthStatus(){
486
+ if(!userEmail)return;
487
+ try{
488
+ const response=await fetch(`/health/current?user_email=${encodeURIComponent(userEmail)}`);
489
+ const data=await response.json();
490
+ if(data&&!data.error){
491
+ updateHealthStat(elements.cognitiveValue, data.cognitive||70);
492
+ updateHealthStat(elements.conversationValue, data.conversation||70);
493
+ updateHealthStat(elements.memoryValue, data.memory||70);
494
+ updateHealthStat(elements.emotionalValue, data.emotional||70);
495
+ updateGauge(data.risk_level||70,data.risk_label||'μ–‘ν˜Έ');
496
+ }
497
+ }catch(error){
498
+ console.error('Failed to load health status:',error);
499
+ }
500
+ }
501
+
502
+ // 건강 νžˆμŠ€ν† λ¦¬ λ‘œλ“œ
503
+ async function loadHealthHistory(){
504
+ if(!userEmail)return;
505
+ try{
506
+ const response=await fetch(`/health/history?user_email=${encodeURIComponent(userEmail)}&days=7`);
507
+ const data=await response.json();
508
+ if(data.history&&healthChart){
509
+ const labels=data.history.map(h=>{
510
+ const date=new Date(h.date);
511
+ return `${date.getMonth()+1}/${date.getDate()}`;
512
+ }).reverse();
513
+ const cognitive=data.history.map(h=>h.cognitive).reverse();
514
+ const conversation=data.history.map(h=>h.conversation).reverse();
515
+ const memory=data.history.map(h=>h.memory).reverse();
516
+ const emotional=data.history.map(h=>h.emotional).reverse();
517
+ healthChart.data.labels=labels.slice(-7);
518
+ healthChart.data.datasets[0].data=cognitive.slice(-7);
519
+ healthChart.data.datasets[1].data=conversation.slice(-7);
520
+ healthChart.data.datasets[2].data=memory.slice(-7);
521
+ healthChart.data.datasets[3].data=emotional.slice(-7);
522
+ healthChart.update();
523
+ }
524
+ }catch(error){
525
+ console.error('Failed to load health history:',error);
526
+ }
527
+ }
528
+
529
+ async function loadGraphStats(){
530
+ if(!userEmail)return;
531
+ try{
532
+ const response=await fetch(`/memory/graph/stats?user_email=${encodeURIComponent(userEmail)}`);
533
+ const stats=await response.json();
534
+ if(stats.total_edges!==undefined){
535
+ elements.relationCount.textContent=stats.total_edges;
536
+ }
537
+ }catch(error){
538
+ console.error('Failed to load graph stats:',error);
539
+ }
540
+ }
541
+
542
+ function updateCurrentTime(){
543
+ const now=new Date();
544
+ const kstTime=new Date(now.toLocaleString("en-US",{timeZone:"Asia/Seoul"}));
545
+ const timeString=kstTime.toLocaleString('ko-KR',{year:'numeric',month:'long',day:'numeric',hour:'2-digit',minute:'2-digit',second:'2-digit',weekday:'long'});
546
+ elements.currentTimeDiv.textContent=timeString;
547
+ }
548
+ updateCurrentTime();
549
+ setInterval(updateCurrentTime,1000);
550
+ elements.userNameInput.value=userName;
551
+ elements.userEmailInput.value=userEmail;
552
+ elements.userNameInput.addEventListener('input',()=>{
553
+ userName=elements.userNameInput.value;
554
+ localStorage.setItem('userName',userName);
555
+ });
556
+ elements.userEmailInput.addEventListener('input',()=>{
557
+ userEmail=elements.userEmailInput.value.trim();
558
+ localStorage.setItem('userEmail',userEmail);
559
+ });
560
+ elements.searchToggle.addEventListener('click',()=>{
561
+ webSearchEnabled=!webSearchEnabled;
562
+ elements.searchToggle.classList.toggle('active',webSearchEnabled);
563
+ showToast(webSearchEnabled?'μ›Ή 검색 ν™œμ„±ν™”':'μ›Ή 검색 λΉ„ν™œμ„±ν™”','success');
564
+ });
565
+ elements.learningToggle.addEventListener('click',()=>{
566
+ selfLearningEnabled=!selfLearningEnabled;
567
+ elements.learningToggle.classList.toggle('active',selfLearningEnabled);
568
+ showToast(selfLearningEnabled?'μžκ°€ ν•™μŠ΅ ν™œμ„±ν™”':'μžκ°€ ν•™μŠ΅ λΉ„ν™œμ„±ν™”','success');
569
+ });
570
+ elements.streamingToggle.addEventListener('click',()=>{
571
+ streamingEnabled=!streamingEnabled;
572
+ elements.streamingToggle.classList.toggle('active',streamingEnabled);
573
+ showToast(streamingEnabled?'슀트리밍 ν™œμ„±ν™”':'슀트리밍 λΉ„ν™œμ„±ν™”','success');
574
+ });
575
+ elements.fileUploadButton.addEventListener('click',()=>{
576
+ userEmail=elements.userEmailInput.value.trim();
577
+ if(!userEmail||!userEmail.includes('@')){
578
+ showToast('이메일을 λ¨Όμ € μž…λ ₯ν•΄μ£Όμ„Έμš”.','error');
579
+ elements.userEmailInput.focus();
580
+ return;
581
+ }
582
+ elements.fileInput.click();
583
+ });
584
+ elements.fileInput.addEventListener('change',async(e)=>{
585
+ const files=Array.from(e.target.files);
586
+ await handleFileUpload(files);
587
+ });
588
+
589
+ async function handleFileUpload(files){
590
+ if(!currentSessionId){
591
+ await startNewSession();
592
+ }
593
+ if(!currentSessionId)return;
594
+ for(const file of files){
595
+ const fileExt=file.name.split('.').pop().toLowerCase();
596
+ if(!['pdf','csv','txt'].includes(fileExt)){
597
+ showToast(`μ§€μ›ν•˜μ§€ μ•ŠλŠ” 파일 ν˜•μ‹: ${file.name}`,'error');
598
+ continue;
599
+ }
600
+ const formData=new FormData();
601
+ formData.append('file',file);
602
+ formData.append('session_id',currentSessionId);
603
+ formData.append('user_email',userEmail);
604
+ try{
605
+ const response=await fetch('/upload/file',{method:'POST',body:formData});
606
+ if(response.ok){
607
+ const result=await response.json();
608
+ uploadedFiles.push(result);
609
+ displayUploadedFile(result);
610
+ showToast(`${file.name} μ—…λ‘œλ“œ μ™„λ£Œ!`,'success');
611
+ elements.textInput.placeholder=`${uploadedFiles.length}개 파일 첨뢀됨`;
612
+ }
613
+ }catch(error){
614
+ console.error('File upload error:',error);
615
+ showToast(`파일 μ—…λ‘œλ“œ μ‹€νŒ¨: ${file.name}`,'error');
616
+ }
617
+ }
618
+ }
619
+
620
+ function displayUploadedFile(fileInfo){
621
+ const fileDiv=document.createElement('div');
622
+ fileDiv.className='uploaded-file';
623
+ fileDiv.innerHTML=`<span>${fileInfo.filename}</span><span style="cursor:pointer;color:#ff6b6b;" onclick="removeFile('${fileInfo.file_id}',this.parentElement)">X</span>`;
624
+ elements.uploadedFilesDiv.appendChild(fileDiv);
625
+ }
626
+
627
+ async function removeFile(fileId,element){
628
+ try{
629
+ const response=await fetch(`/upload/file/${fileId}?user_email=${encodeURIComponent(userEmail)}`,{method:'DELETE'});
630
+ if(response.ok){
631
+ element.remove();
632
+ uploadedFiles=uploadedFiles.filter(f=>f.file_id!==fileId);
633
+ showToast('파일이 μ œκ±°λ˜μ—ˆμŠ΅λ‹ˆλ‹€.','success');
634
+ if(uploadedFiles.length===0){
635
+ elements.textInput.placeholder='νŽΈν•˜κ²Œ 말씀해 μ£Όμ„Έμš”...';
636
+ }else{
637
+ elements.textInput.placeholder=`${uploadedFiles.length}개 파일 첨뢀됨`;
638
+ }
639
+ }
640
+ }catch(error){
641
+ console.error('File removal error:',error);
642
+ }
643
+ }
644
+
645
+ async function deleteMemory(memoryId,element){
646
+ if(!confirm('이 기얡을 μ‚­μ œν•˜μ‹œκ² μŠ΅λ‹ˆκΉŒ?'))return;
647
+ try{
648
+ const response=await fetch(`/memory/${memoryId}?user_email=${encodeURIComponent(userEmail)}`,{method:'DELETE'});
649
+ if(response.ok){
650
+ element.remove();
651
+ showToast('기얡이 μ‚­μ œλ˜μ—ˆμŠ΅λ‹ˆλ‹€.','success');
652
+ await loadMemories();
653
+ }else{
654
+ showToast('μ‚­μ œ μ‹€νŒ¨','error');
655
+ }
656
+ }catch(error){
657
+ console.error('Delete memory error:',error);
658
+ showToast('μ‚­μ œ 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€.','error');
659
+ }
660
+ }
661
+
662
+ async function sendMessage(){
663
+ const message=elements.textInput.value.trim();
664
+ userEmail=elements.userEmailInput.value.trim();
665
+ if(!message){
666
+ showToast('λ©”μ‹œμ§€λ₯Ό μž…λ ₯ν•΄μ£Όμ„Έμš”.','warning');
667
+ return;
668
+ }
669
+ if(isStreaming){
670
+ showToast('응닡 생성 μ€‘μž…λ‹ˆλ‹€.','warning');
671
+ return;
672
+ }
673
+ if(!userEmail||!userEmail.includes('@')){
674
+ showToast('μ˜¬λ°”λ₯Έ 이메일을 μž…λ ₯ν•΄μ£Όμ„Έμš”.','error');
675
+ elements.userEmailInput.focus();
676
+ return;
677
+ }
678
+ localStorage.setItem('userEmail',userEmail);
679
+ if(!currentSessionId){
680
+ const success=await startNewSession();
681
+ if(!success){
682
+ showToast('μ„Έμ…˜ 생성 μ‹€νŒ¨. λ‹€μ‹œ μ‹œλ„ν•΄μ£Όμ„Έμš”.','error');
683
+ return;
684
+ }
685
+ }
686
+ addMessage('user',message);
687
+ elements.textInput.value='';
688
+ if(streamingEnabled){
689
+ await sendStreamingMessage(message);
690
+ }else{
691
+ showToast('μŠ€νŠΈλ¦¬λ°μ„ ν™œμ„±ν™”ν•΄μ£Όμ„Έμš”.','warning');
692
+ }
693
+ }
694
+
695
+ async function sendStreamingMessage(message){
696
+ isStreaming=true;
697
+ elements.sendButton.disabled=true;
698
+ elements.streamingIndicator.classList.add('active');
699
+ const assistantMessage=document.createElement('div');
700
+ assistantMessage.classList.add('message','assistant','streaming');
701
+ elements.chatMessages.appendChild(assistantMessage);
702
+ try{
703
+ const response=await fetch('/chat/text/stream',{
704
+ method:'POST',
705
+ headers:{'Content-Type':'application/json'},
706
+ body:JSON.stringify({
707
+ message:message,
708
+ web_search_enabled:webSearchEnabled,
709
+ session_id:currentSessionId,
710
+ user_email:userEmail,
711
+ user_name:userName,
712
+ memories:userMemories,
713
+ self_learning_enabled:selfLearningEnabled,
714
+ uploaded_files:uploadedFiles.map(f=>f.file_id)
715
+ })
716
+ });
717
+ if(!response.ok){
718
+ const errorData=await response.json();
719
+ assistantMessage.classList.remove('streaming');
720
+ assistantMessage.textContent='였λ₯˜: '+(errorData.error||'μ„œλ²„ 였λ₯˜');
721
+ showToast(errorData.error||'μ„œλ²„ 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€.','error');
722
+ return;
723
+ }
724
+ const reader=response.body.getReader();
725
+ const decoder=new TextDecoder();
726
+ let content='';
727
+ let buffer='';
728
+ while(true){
729
+ const{done,value}=await reader.read();
730
+ if(done)break;
731
+ buffer+=decoder.decode(value,{stream:true});
732
+ const lines=buffer.split('\n');
733
+ buffer=lines.pop()||'';
734
+ for(const line of lines){
735
+ if(line.startsWith('data: ')){
736
+ const data=line.slice(6);
737
+ if(data==='[DONE]'){
738
+ assistantMessage.classList.remove('streaming');
739
+ break;
740
+ }
741
+ try{
742
+ const parsed=JSON.parse(data);
743
+ if(parsed.content){
744
+ content+=parsed.content;
745
+ assistantMessage.innerHTML=marked.parse(content);
746
+ elements.chatMessages.scrollTop=elements.chatMessages.scrollHeight;
747
+ }
748
+ }catch(e){
749
+ console.log('Parse error:',e,data);
750
+ }
751
+ }
752
+ }
753
+ }
754
+ }catch(error){
755
+ console.error('Streaming error:',error);
756
+ assistantMessage.classList.remove('streaming');
757
+ assistantMessage.textContent='였λ₯˜: '+error.message;
758
+ showToast('슀트리밍 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€.','error');
759
+ }finally{
760
+ isStreaming=false;
761
+ elements.sendButton.disabled=false;
762
+ elements.streamingIndicator.classList.remove('active');
763
+ await loadMemories();
764
+ await loadHealthStatus();
765
+ await loadHealthHistory();
766
+ }
767
+ }
768
+
769
+ function addMessage(role,content){
770
+ const messageDiv=document.createElement('div');
771
+ messageDiv.classList.add('message',role);
772
+ if(role==='assistant'){
773
+ messageDiv.innerHTML=marked.parse(content);
774
+ }else{
775
+ messageDiv.textContent=content;
776
+ }
777
+ elements.chatMessages.appendChild(messageDiv);
778
+ elements.chatMessages.scrollTop=elements.chatMessages.scrollHeight;
779
+ }
780
+
781
+ async function loadMemories(){
782
+ if(!userEmail)return;
783
+ try{
784
+ const response=await fetch(`/memory/all?user_email=${encodeURIComponent(userEmail)}`);
785
+ const memories=await response.json();
786
+ userMemories={};
787
+ elements.memoryList.innerHTML='';
788
+ memories.forEach(memory=>{
789
+ if(!userMemories[memory.category]){
790
+ userMemories[memory.category]=[];
791
+ }
792
+ userMemories[memory.category].push(memory.content);
793
+ const item=document.createElement('div');
794
+ item.className='memory-item';
795
+ const importance=(memory.importance*100).toFixed(0);
796
+ item.innerHTML=`<div class="memory-score">${importance}%</div><div class="memory-delete" onclick="deleteMemory(${memory.id}, this.parentElement)">X</div><div style="font-size:10px;color:#888;margin-bottom:4px;">${memory.category}</div><div>${memory.content}</div>`;
797
+ elements.memoryList.appendChild(item);
798
+ });
799
+ elements.memoryCount.textContent=memories.length;
800
+ elements.vectorCount.textContent=memories.filter(m=>m.importance>0.7).length;
801
+ await loadGraphStats();
802
+ }catch(error){
803
+ console.error('Failed to load memories:',error);
804
+ }
805
+ }
806
+
807
+ async function startNewSession(){
808
+ userEmail=elements.userEmailInput.value.trim();
809
+ if(!userEmail||!userEmail.includes('@')){
810
+ showToast('μ˜¬λ°”λ₯Έ 이메일을 μž…λ ₯ν•΄μ£Όμ„Έμš”.','warning');
811
+ return false;
812
+ }
813
+ try{
814
+ const response=await fetch('/session/new',{
815
+ method:'POST',
816
+ headers:{'Content-Type':'application/json'},
817
+ body:JSON.stringify({user_email:userEmail})
818
+ });
819
+ const data=await response.json();
820
+ if(data.error){
821
+ showToast(data.error,'error');
822
+ return false;
823
+ }
824
+ currentSessionId=data.session_id;
825
+ console.log('New session started:',currentSessionId);
826
+ uploadedFiles=[];
827
+ elements.uploadedFilesDiv.innerHTML='';
828
+ await loadMemories();
829
+ await loadHealthStatus();
830
+ await loadHealthHistory();
831
+ // ν™˜μ˜ λ©”μ‹œμ§€ ν‘œμ‹œ
832
+ const displayName=userName||userEmail.split('@')[0];
833
+ addMessage('assistant',`μ—¬λ³΄μ„Έμš”, ${displayName}λ‹˜! 잘 μ§€λ‚΄μ…¨μ–΄μš”? 였늘 ν•˜λ£¨ μ–΄λ– μ…¨λŠ”μ§€ μ–˜κΈ°ν•΄ μ£Όμ„Έμš”.`);
834
+ return true;
835
+ }catch(error){
836
+ console.error('Session creation error:',error);
837
+ showToast('μ„Έμ…˜ 생성 μ‹€νŒ¨','error');
838
+ return false;
839
+ }
840
+ }
841
+
842
+ async function endSession(){
843
+ if(!currentSessionId||!userEmail){
844
+ showToast('μ €μž₯ν•  μ„Έμ…˜μ΄ μ—†μŠ΅λ‹ˆλ‹€.','warning');
845
+ return;
846
+ }
847
+ try{
848
+ showToast('λŒ€ν™” λ‚΄μš©μ„ μ €μž₯ν•˜λŠ” 쀑...','success');
849
+ const response=await fetch('/session/end',{
850
+ method:'POST',
851
+ headers:{'Content-Type':'application/json'},
852
+ body:JSON.stringify({session_id:currentSessionId,user_email:userEmail})
853
+ });
854
+ if(response.ok){
855
+ showToast('λŒ€ν™”κ°€ μ„±κ³΅μ μœΌλ‘œ μ €μž₯λ˜μ—ˆμŠ΅λ‹ˆλ‹€!','success');
856
+ await loadMemories();
857
+ await loadHealthStatus();
858
+ await loadHealthHistory();
859
+ }
860
+ }catch(error){
861
+ console.error('Failed to end session:',error);
862
+ showToast('μ €μž₯ 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€.','error');
863
+ }
864
+ }
865
+
866
+ function showToast(message,type='info'){
867
+ const toast=document.getElementById('error-toast');
868
+ toast.textContent=message;
869
+ toast.className=`toast ${type}`;
870
+ toast.style.display='block';
871
+ setTimeout(()=>{
872
+ toast.style.display='none';
873
+ },3000);
874
+ }
875
+
876
+ elements.textInput.addEventListener('keypress',(e)=>{
877
+ if(e.key==='Enter'&&!e.shiftKey){
878
+ e.preventDefault();
879
+ sendMessage();
880
+ }
881
+ });
882
+ elements.sendButton.addEventListener('click',sendMessage);
883
+ document.querySelectorAll('.quick-btn').forEach(btn=>{
884
+ btn.addEventListener('click',()=>{
885
+ const prompt=btn.dataset.prompt;
886
+ if(prompt&&!isStreaming){
887
+ elements.textInput.value=prompt;
888
+ sendMessage();
889
+ }
890
+ });
891
+ });
892
+ elements.endSessionButton.addEventListener('click',endSession);
893
+ window.addEventListener('DOMContentLoaded',()=>{
894
+ initHealthChart();
895
+ if(userEmail&&userEmail.includes('@')){
896
+ startNewSession();
897
+ }
898
+ });
899
+ </script>
900
+ </body>
901
+ </html>