ltcai 0.1.2 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -0
- package/package.json +3 -3
- package/requirements.txt +1 -0
- package/server.py +167 -18
- package/static/account.html +509 -0
- package/static/admin.html +214 -67
- package/static/{indexd.html → chat.html} +299 -124
- package/static/index.html +0 -270
|
@@ -180,6 +180,48 @@
|
|
|
180
180
|
flex-shrink: 0;
|
|
181
181
|
}
|
|
182
182
|
|
|
183
|
+
.sidebar-search {
|
|
184
|
+
padding: 8px 10px;
|
|
185
|
+
border-bottom: 1px solid rgba(255,255,255,0.05);
|
|
186
|
+
}
|
|
187
|
+
.sidebar-search input {
|
|
188
|
+
width: 100%;
|
|
189
|
+
padding: 7px 10px 7px 30px;
|
|
190
|
+
background: rgba(255,255,255,0.04);
|
|
191
|
+
border: 1px solid rgba(255,255,255,0.07);
|
|
192
|
+
border-radius: 8px;
|
|
193
|
+
color: var(--text);
|
|
194
|
+
font-size: 12px;
|
|
195
|
+
font-family: inherit;
|
|
196
|
+
outline: none;
|
|
197
|
+
transition: border-color .15s;
|
|
198
|
+
}
|
|
199
|
+
.sidebar-search input:focus { border-color: rgba(34,211,160,0.4); }
|
|
200
|
+
.sidebar-search input::placeholder { color: var(--faint); }
|
|
201
|
+
.sidebar-search-wrap {
|
|
202
|
+
position: relative;
|
|
203
|
+
}
|
|
204
|
+
.sidebar-search-wrap i {
|
|
205
|
+
position: absolute;
|
|
206
|
+
left: 8px; top: 50%;
|
|
207
|
+
transform: translateY(-50%);
|
|
208
|
+
color: var(--faint);
|
|
209
|
+
font-size: 13px;
|
|
210
|
+
pointer-events: none;
|
|
211
|
+
}
|
|
212
|
+
.history-item-del {
|
|
213
|
+
margin-left: auto;
|
|
214
|
+
opacity: 0;
|
|
215
|
+
color: var(--faint);
|
|
216
|
+
font-size: 13px;
|
|
217
|
+
padding: 2px 4px;
|
|
218
|
+
border-radius: 4px;
|
|
219
|
+
transition: all .15s;
|
|
220
|
+
flex-shrink: 0;
|
|
221
|
+
}
|
|
222
|
+
.history-item:hover .history-item-del { opacity: 1; }
|
|
223
|
+
.history-item-del:hover { color: #ff6b6b; background: rgba(255,107,107,0.12); }
|
|
224
|
+
|
|
183
225
|
.history-container {
|
|
184
226
|
flex: 1;
|
|
185
227
|
overflow-y: auto;
|
|
@@ -335,6 +377,8 @@
|
|
|
335
377
|
background: rgba(29,42,60,0.82);
|
|
336
378
|
backdrop-filter: blur(20px);
|
|
337
379
|
-webkit-backdrop-filter: blur(20px);
|
|
380
|
+
position: relative;
|
|
381
|
+
z-index: 50;
|
|
338
382
|
}
|
|
339
383
|
|
|
340
384
|
.header-left, .header-pills {
|
|
@@ -418,6 +462,62 @@
|
|
|
418
462
|
justify-content: center;
|
|
419
463
|
}
|
|
420
464
|
.acct-modal-overlay.open { display: flex; }
|
|
465
|
+
|
|
466
|
+
/* MCP 관리 모달 */
|
|
467
|
+
.mcp-modal-overlay {
|
|
468
|
+
display: none;
|
|
469
|
+
position: fixed;
|
|
470
|
+
inset: 0;
|
|
471
|
+
background: rgba(0,0,0,0.6);
|
|
472
|
+
backdrop-filter: blur(4px);
|
|
473
|
+
z-index: 1000;
|
|
474
|
+
align-items: center;
|
|
475
|
+
justify-content: center;
|
|
476
|
+
}
|
|
477
|
+
.mcp-modal-overlay.open { display: flex; }
|
|
478
|
+
.mcp-modal {
|
|
479
|
+
background: var(--surface, #1e293b);
|
|
480
|
+
border: 1px solid rgba(255,255,255,0.08);
|
|
481
|
+
border-radius: 16px;
|
|
482
|
+
width: 100%;
|
|
483
|
+
max-width: 560px;
|
|
484
|
+
max-height: 80vh;
|
|
485
|
+
display: flex;
|
|
486
|
+
flex-direction: column;
|
|
487
|
+
box-shadow: 0 20px 60px rgba(0,0,0,0.5);
|
|
488
|
+
overflow: hidden;
|
|
489
|
+
}
|
|
490
|
+
.mcp-modal-header {
|
|
491
|
+
padding: 18px 20px;
|
|
492
|
+
border-bottom: 1px solid rgba(255,255,255,0.07);
|
|
493
|
+
display: flex; align-items: center; justify-content: space-between;
|
|
494
|
+
}
|
|
495
|
+
.mcp-modal-header h3 { font-size: 15px; font-weight: 700; color: var(--text); }
|
|
496
|
+
.mcp-modal-close { background: none; border: none; color: var(--faint); cursor: pointer; font-size: 18px; padding: 2px 6px; border-radius: 6px; }
|
|
497
|
+
.mcp-modal-close:hover { color: var(--text); background: rgba(255,255,255,0.07); }
|
|
498
|
+
.mcp-modal-body { flex: 1; overflow-y: auto; padding: 16px 20px; }
|
|
499
|
+
.mcp-section-label { font-size: 10px; font-weight: 700; color: var(--faint); text-transform: uppercase; letter-spacing: .08em; margin: 12px 0 8px; }
|
|
500
|
+
.mcp-item {
|
|
501
|
+
display: flex; align-items: center; gap: 12px;
|
|
502
|
+
padding: 11px 14px;
|
|
503
|
+
background: rgba(255,255,255,0.03);
|
|
504
|
+
border: 1px solid rgba(255,255,255,0.06);
|
|
505
|
+
border-radius: 10px;
|
|
506
|
+
margin-bottom: 6px;
|
|
507
|
+
}
|
|
508
|
+
.mcp-item-icon { font-size: 20px; flex-shrink: 0; }
|
|
509
|
+
.mcp-item-info { flex: 1; min-width: 0; }
|
|
510
|
+
.mcp-item-name { font-size: 13px; font-weight: 600; color: var(--text); }
|
|
511
|
+
.mcp-item-desc { font-size: 11px; color: var(--faint); margin-top: 2px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
512
|
+
.mcp-item-status { font-size: 10.5px; color: var(--accent); font-weight: 600; }
|
|
513
|
+
.mcp-item-status.inactive { color: var(--faint); }
|
|
514
|
+
.mcp-install-btn {
|
|
515
|
+
padding: 6px 12px; border-radius: 7px; font-size: 12px; font-weight: 600;
|
|
516
|
+
border: 1px solid rgba(34,211,160,0.3); background: rgba(34,211,160,0.08);
|
|
517
|
+
color: var(--accent); cursor: pointer; transition: all .15s; flex-shrink: 0;
|
|
518
|
+
}
|
|
519
|
+
.mcp-install-btn:hover { background: rgba(34,211,160,0.15); }
|
|
520
|
+
.mcp-install-btn.installed { border-color: rgba(255,255,255,0.1); background: rgba(255,255,255,0.04); color: var(--faint); }
|
|
421
521
|
.acct-modal {
|
|
422
522
|
background: var(--surface, #1e293b);
|
|
423
523
|
border: 1px solid rgba(255,255,255,0.08);
|
|
@@ -2807,43 +2907,6 @@
|
|
|
2807
2907
|
</div>
|
|
2808
2908
|
<div class="bg-grid"></div>
|
|
2809
2909
|
|
|
2810
|
-
<div id="auth-overlay" class="auth-overlay">
|
|
2811
|
-
<div class="auth-orb auth-orb-1"></div>
|
|
2812
|
-
<div class="auth-orb auth-orb-2"></div>
|
|
2813
|
-
<div class="auth-lang-picker lang-picker" id="auth-lang-picker">
|
|
2814
|
-
<button class="auth-lang-btn" id="auth-lang-btn" onclick="toggleLangMenu('auth-lang-picker')">🌐 Languages</button>
|
|
2815
|
-
<div class="lang-picker-menu" id="auth-lang-picker-menu">
|
|
2816
|
-
<div class="lang-option" id="auth-lang-ko" onclick="setLang('ko')">🇰🇷 한국어</div>
|
|
2817
|
-
<div class="lang-option" id="auth-lang-en" onclick="setLang('en')">🇺🇸 English</div>
|
|
2818
|
-
</div>
|
|
2819
|
-
</div>
|
|
2820
|
-
<div class="auth-card">
|
|
2821
|
-
<div id="login-form">
|
|
2822
|
-
<div class="auth-logo"><i class="ti ti-brain"></i></div>
|
|
2823
|
-
<h2 class="auth-title" data-i18n="login_title">Lattice AI</h2>
|
|
2824
|
-
<p class="auth-subtitle" data-i18n="login_sub">Local AI Workspace — Apple Silicon</p>
|
|
2825
|
-
<input class="auth-input" type="email" id="login-email" placeholder="이메일 주소" data-i18n-ph="ph_email">
|
|
2826
|
-
<input class="auth-input" type="password" id="login-pw" placeholder="비밀번호" data-i18n-ph="ph_password">
|
|
2827
|
-
<button class="auth-submit" onclick="handleLogin()" data-i18n="btn_login">로그인</button>
|
|
2828
|
-
<p class="auth-switch"><span data-i18n="no_account">계정이 없으신가요?</span>
|
|
2829
|
-
<a href="#" onclick="toggleAuth(true)" data-i18n="go_register">회원가입</a></p>
|
|
2830
|
-
</div>
|
|
2831
|
-
<div id="register-form" style="display: none;">
|
|
2832
|
-
<div class="auth-logo"><i class="ti ti-user-plus"></i></div>
|
|
2833
|
-
<h2 class="auth-title" data-i18n="register_title">계정 만들기</h2>
|
|
2834
|
-
<p class="auth-subtitle" data-i18n="register_sub">Lattice AI 워크스페이스에 참여하세요</p>
|
|
2835
|
-
<input class="auth-input" type="email" id="reg-email" placeholder="이메일 주소" data-i18n-ph="ph_email">
|
|
2836
|
-
<input class="auth-input" type="password" id="reg-pw" placeholder="비밀번호 (4자 이상)" data-i18n-ph="ph_new_pw">
|
|
2837
|
-
<input class="auth-input" type="password" id="reg-pw2" placeholder="비밀번호 확인" data-i18n-ph="ph_pw_confirm">
|
|
2838
|
-
<input class="auth-input" type="text" id="reg-name" placeholder="이름" data-i18n-ph="ph_fullname">
|
|
2839
|
-
<input class="auth-input" type="text" id="reg-nickname" placeholder="닉네임" data-i18n-ph="ph_nick">
|
|
2840
|
-
<div id="reg-msg" style="font-size:12px;min-height:16px;margin-bottom:4px;"></div>
|
|
2841
|
-
<button class="auth-submit" id="reg-submit-btn" onclick="handleRegister()" data-i18n="btn_register">가입하기</button>
|
|
2842
|
-
<p class="auth-switch"><span data-i18n="have_account">이미 계정이 있나요?</span>
|
|
2843
|
-
<a href="#" onclick="toggleAuth(false)" data-i18n="go_login">로그인</a></p>
|
|
2844
|
-
</div>
|
|
2845
|
-
</div>
|
|
2846
|
-
</div>
|
|
2847
2910
|
|
|
2848
2911
|
<div class="app-layout">
|
|
2849
2912
|
<!-- Sidebar -->
|
|
@@ -2862,6 +2925,12 @@
|
|
|
2862
2925
|
<div style="font-size:10.5px;color:var(--faint)">Local Workspace</div>
|
|
2863
2926
|
</div>
|
|
2864
2927
|
</div>
|
|
2928
|
+
<div class="sidebar-search">
|
|
2929
|
+
<div class="sidebar-search-wrap">
|
|
2930
|
+
<i class="ti ti-search"></i>
|
|
2931
|
+
<input type="text" id="history-search-input" placeholder="대화 검색..." oninput="onHistorySearch(this.value)">
|
|
2932
|
+
</div>
|
|
2933
|
+
</div>
|
|
2865
2934
|
<div class="history-container" id="history-container">
|
|
2866
2935
|
<!-- History items -->
|
|
2867
2936
|
</div>
|
|
@@ -2869,6 +2938,7 @@
|
|
|
2869
2938
|
<button id="admin-btn" class="admin-btn" onclick="openAdminPanel()"><i class="ti ti-shield-lock"></i> <span data-i18n="admin_dashboard">관리자 대시보드</span></button>
|
|
2870
2939
|
<button class="status-btn" onclick="openStatusPanel()"><i class="ti ti-info-circle"></i> <span data-i18n="my_status">내 상태 보기</span></button>
|
|
2871
2940
|
<button id="setup-wizard-btn" class="setup-wizard-sidebar-btn" onclick="openSetupWizard()"><i class="ti ti-sparkles"></i> <span data-i18n="auto_setup">자동 설정</span></button>
|
|
2941
|
+
<button class="setup-wizard-sidebar-btn" onclick="openMcpModal()"><i class="ti ti-plug-connected"></i> MCP 관리</button>
|
|
2872
2942
|
<button id="new-chat-btn" class="new-chat-btn"><i class="ti ti-plus"></i> New Chat</button>
|
|
2873
2943
|
</div>
|
|
2874
2944
|
</aside>
|
|
@@ -2944,6 +3014,19 @@
|
|
|
2944
3014
|
</div>
|
|
2945
3015
|
</div>
|
|
2946
3016
|
|
|
3017
|
+
<!-- MCP 관리 모달 -->
|
|
3018
|
+
<div class="mcp-modal-overlay" id="mcp-modal-overlay" onclick="if(event.target===this)closeMcpModal()">
|
|
3019
|
+
<div class="mcp-modal">
|
|
3020
|
+
<div class="mcp-modal-header">
|
|
3021
|
+
<h3><i class="ti ti-plug-connected"></i> MCP 서버 관리</h3>
|
|
3022
|
+
<button class="mcp-modal-close" onclick="closeMcpModal()"><i class="ti ti-x"></i></button>
|
|
3023
|
+
</div>
|
|
3024
|
+
<div class="mcp-modal-body" id="mcp-modal-body">
|
|
3025
|
+
<div style="color:var(--faint);font-size:13px;text-align:center;padding:24px">로딩 중...</div>
|
|
3026
|
+
</div>
|
|
3027
|
+
</div>
|
|
3028
|
+
</div>
|
|
3029
|
+
|
|
2947
3030
|
<section class="ops-strip" aria-label="workspace status">
|
|
2948
3031
|
<div class="ops-card primary interactive" onclick="openModelPanel()">
|
|
2949
3032
|
<div>
|
|
@@ -3351,7 +3434,6 @@
|
|
|
3351
3434
|
if (typeof acquireVsCodeApi === 'function') {
|
|
3352
3435
|
vscode = acquireVsCodeApi();
|
|
3353
3436
|
console.log("VS Code API Acquired");
|
|
3354
|
-
document.getElementById('auth-overlay').style.display = 'none'; // VS Code에서는 로그인 건너뜀
|
|
3355
3437
|
}
|
|
3356
3438
|
} catch (e) { /* Not in VS Code */ }
|
|
3357
3439
|
|
|
@@ -3363,80 +3445,12 @@
|
|
|
3363
3445
|
}
|
|
3364
3446
|
});
|
|
3365
3447
|
|
|
3366
|
-
function
|
|
3367
|
-
|
|
3368
|
-
document.getElementById('register-form').style.display = showRegister ? 'block' : 'none';
|
|
3369
|
-
}
|
|
3370
|
-
|
|
3371
|
-
async function handleRegister() {
|
|
3372
|
-
const email = document.getElementById('reg-email').value.trim();
|
|
3373
|
-
const password = document.getElementById('reg-pw').value;
|
|
3374
|
-
const password2 = document.getElementById('reg-pw2').value;
|
|
3375
|
-
const name = document.getElementById('reg-name').value.trim();
|
|
3376
|
-
const nickname = document.getElementById('reg-nickname').value.trim();
|
|
3377
|
-
const msg = document.getElementById('reg-msg');
|
|
3378
|
-
const btn = document.getElementById('reg-submit-btn');
|
|
3379
|
-
const setMsg = (text, isError) => {
|
|
3380
|
-
msg.textContent = text;
|
|
3381
|
-
msg.style.color = isError ? '#f87171' : '#4ade80';
|
|
3382
|
-
};
|
|
3383
|
-
if (!email || !password || !password2 || !name || !nickname) return setMsg('모든 항목을 입력해주세요.', true);
|
|
3384
|
-
if (password.length < 4) return setMsg('비밀번호는 4자 이상이어야 합니다.', true);
|
|
3385
|
-
if (password !== password2) return setMsg('비밀번호가 일치하지 않습니다.', true);
|
|
3386
|
-
btn.disabled = true; btn.textContent = '가입 중...';
|
|
3387
|
-
try {
|
|
3388
|
-
const res = await apiFetch('/register', {
|
|
3389
|
-
method: 'POST',
|
|
3390
|
-
headers: { 'Content-Type': 'application/json' },
|
|
3391
|
-
body: JSON.stringify({ email, password, name, nickname })
|
|
3392
|
-
});
|
|
3393
|
-
if (res.ok) {
|
|
3394
|
-
setMsg('✅ 가입 완료! 로그인 해주세요.', false);
|
|
3395
|
-
setTimeout(() => toggleAuth(false), 1200);
|
|
3396
|
-
} else {
|
|
3397
|
-
const data = await res.json();
|
|
3398
|
-
setMsg(data.detail || '가입 실패', true);
|
|
3399
|
-
}
|
|
3400
|
-
} catch { setMsg('서버 연결 실패', true); }
|
|
3401
|
-
finally { btn.disabled = false; btn.textContent = '가입하기'; }
|
|
3402
|
-
}
|
|
3403
|
-
|
|
3404
|
-
async function handleLogin() {
|
|
3405
|
-
const email = document.getElementById('login-email').value;
|
|
3406
|
-
const password = document.getElementById('login-pw').value;
|
|
3407
|
-
const res = await apiFetch('/login', {
|
|
3408
|
-
method: 'POST',
|
|
3409
|
-
headers: { 'Content-Type': 'application/json' },
|
|
3410
|
-
body: JSON.stringify({ email, password })
|
|
3411
|
-
});
|
|
3412
|
-
if (res.ok) {
|
|
3413
|
-
const data = await res.json();
|
|
3414
|
-
currentUserNickname = data.nickname;
|
|
3415
|
-
currentUserEmail = data.email;
|
|
3416
|
-
localStorage.setItem('ltcai_user_email', currentUserEmail);
|
|
3417
|
-
localStorage.setItem('ltcai_user_nickname', currentUserNickname);
|
|
3418
|
-
localStorage.setItem('ltcai_is_admin', data.is_admin ? 'true' : 'false');
|
|
3419
|
-
isAdmin = Boolean(data.is_admin);
|
|
3420
|
-
document.getElementById('user-nickname-display').innerText = currentUserNickname;
|
|
3421
|
-
const av = document.getElementById('user-avatar-initial');
|
|
3422
|
-
if (av) av.textContent = (currentUserNickname || 'G')[0].toUpperCase();
|
|
3423
|
-
document.getElementById('admin-btn').style.display = 'flex';
|
|
3424
|
-
document.getElementById('security-admin-meta').textContent = isAdmin ? '관리자 권한 있음' : '관리자 대시보드 접근';
|
|
3425
|
-
document.getElementById('auth-overlay').style.display = 'none';
|
|
3426
|
-
loadVpcStatus();
|
|
3427
|
-
restoreCurrentConversation().then(() => {
|
|
3428
|
-
if (!chatViewport.querySelector('.message')) {
|
|
3429
|
-
addMessage('ai', `반갑습니다, <b>${currentUserNickname}</b>님!`);
|
|
3430
|
-
}
|
|
3431
|
-
});
|
|
3432
|
-
} else { alert("로그인 실패!"); }
|
|
3433
|
-
}
|
|
3434
|
-
|
|
3435
|
-
function logout() {
|
|
3448
|
+
async function logout() {
|
|
3449
|
+
try { await apiFetch('/logout', { method: 'POST' }); } catch (_) {}
|
|
3436
3450
|
localStorage.removeItem('ltcai_user_email');
|
|
3437
3451
|
localStorage.removeItem('ltcai_user_nickname');
|
|
3438
3452
|
localStorage.removeItem('ltcai_is_admin');
|
|
3439
|
-
location.
|
|
3453
|
+
window.location.href = '/account';
|
|
3440
3454
|
}
|
|
3441
3455
|
|
|
3442
3456
|
const I18N = {
|
|
@@ -3461,7 +3475,7 @@
|
|
|
3461
3475
|
btn_save: '저장', btn_change: '변경', btn_cancel: '취소',
|
|
3462
3476
|
// ops 스트립
|
|
3463
3477
|
vpc_not_set: '설정 안 됨', vpc_click_to_set: '클릭하여 VPC 연결 설정',
|
|
3464
|
-
security_monitor: '민감정보 감시', admin_dashboard_access: '관리자 대시보드 접근',
|
|
3478
|
+
security_monitor: '민감정보 감시', admin_dashboard_access: '관리자 대시보드 접근', admin_has_rights: '관리자 권한 있음',
|
|
3465
3479
|
// 빈 화면
|
|
3466
3480
|
empty_title: '무엇을 만들까요?',
|
|
3467
3481
|
empty_sub: '로컬 모델, 이미지 분석, 코드 생성, 프라이빗 VPC — 모든 걸 한 화면에서 이어가세요.',
|
|
@@ -3500,7 +3514,7 @@
|
|
|
3500
3514
|
btn_save: 'Save', btn_change: 'Change', btn_cancel: 'Cancel',
|
|
3501
3515
|
// Ops strip
|
|
3502
3516
|
vpc_not_set: 'Not configured', vpc_click_to_set: 'Click to set up VPC',
|
|
3503
|
-
security_monitor: 'Sensitive data monitor', admin_dashboard_access: 'Admin dashboard access',
|
|
3517
|
+
security_monitor: 'Sensitive data monitor', admin_dashboard_access: 'Admin dashboard access', admin_has_rights: 'Has admin rights',
|
|
3504
3518
|
// Empty state
|
|
3505
3519
|
empty_title: 'What would you like to build?',
|
|
3506
3520
|
empty_sub: 'Local models, image analysis, code generation, private VPC — all in one workspace.',
|
|
@@ -4051,9 +4065,27 @@
|
|
|
4051
4065
|
}
|
|
4052
4066
|
|
|
4053
4067
|
async function openAdminPanel() {
|
|
4068
|
+
if (!isAdmin) {
|
|
4069
|
+
showToast(currentLang === 'ko' ? '관리자 권한이 없습니다.' : 'Admin access required.');
|
|
4070
|
+
return;
|
|
4071
|
+
}
|
|
4054
4072
|
window.location.href = '/admin';
|
|
4055
4073
|
}
|
|
4056
4074
|
|
|
4075
|
+
function showToast(msg) {
|
|
4076
|
+
let t = document.getElementById('ltcai-toast');
|
|
4077
|
+
if (!t) {
|
|
4078
|
+
t = document.createElement('div');
|
|
4079
|
+
t.id = 'ltcai-toast';
|
|
4080
|
+
t.style.cssText = 'position:fixed;bottom:28px;left:50%;transform:translateX(-50%);background:#1e2330;color:#f8fafc;border:1px solid rgba(255,255,255,0.12);border-radius:10px;padding:10px 18px;font-size:13px;font-weight:600;z-index:9999;box-shadow:0 8px 24px rgba(0,0,0,0.4);pointer-events:none;transition:opacity .2s;';
|
|
4081
|
+
document.body.appendChild(t);
|
|
4082
|
+
}
|
|
4083
|
+
t.textContent = msg;
|
|
4084
|
+
t.style.opacity = '1';
|
|
4085
|
+
clearTimeout(t._timer);
|
|
4086
|
+
t._timer = setTimeout(() => { t.style.opacity = '0'; }, 2200);
|
|
4087
|
+
}
|
|
4088
|
+
|
|
4057
4089
|
function closeAdminPanel() {
|
|
4058
4090
|
document.getElementById('admin-overlay').style.display = 'none';
|
|
4059
4091
|
}
|
|
@@ -5012,27 +5044,59 @@
|
|
|
5012
5044
|
}
|
|
5013
5045
|
}
|
|
5014
5046
|
|
|
5047
|
+
function renderHistoryItems(conversations) {
|
|
5048
|
+
if (!conversations.length) {
|
|
5049
|
+
historyContainer.innerHTML = '<div class="history-section-label">CHATS</div><div class="history-empty">아직 저장된 대화가 없습니다.</div>';
|
|
5050
|
+
return;
|
|
5051
|
+
}
|
|
5052
|
+
historyContainer.innerHTML = '<div class="history-section-label">CHATS</div>' + conversations.map(item => `
|
|
5053
|
+
<div class="history-item ${item.id === currentConversationId ? 'active' : ''}" data-conversation-id="${escapeHtml(item.id)}" title="${escapeHtml(item.title || '')}">
|
|
5054
|
+
<i class="ti ti-message-2"></i>
|
|
5055
|
+
<span style="flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis">${escapeHtml(item.title || '새 대화')}</span>
|
|
5056
|
+
<span class="history-item-del" onclick="event.stopPropagation();deleteConversation('${escapeHtml(item.id)}')"><i class="ti ti-trash"></i></span>
|
|
5057
|
+
</div>
|
|
5058
|
+
`).join('');
|
|
5059
|
+
historyContainer.querySelectorAll('.history-item').forEach(item => {
|
|
5060
|
+
item.onclick = () => openConversation(item.dataset.conversationId);
|
|
5061
|
+
});
|
|
5062
|
+
}
|
|
5063
|
+
|
|
5015
5064
|
async function loadHistory() {
|
|
5016
5065
|
try {
|
|
5017
5066
|
const conversations = await fetchConversations();
|
|
5018
|
-
|
|
5019
|
-
historyContainer.innerHTML = '<div class="history-section-label">CHATS</div><div class="history-empty">아직 저장된 대화가 없습니다.</div>';
|
|
5020
|
-
return [];
|
|
5021
|
-
}
|
|
5022
|
-
historyContainer.innerHTML = '<div class="history-section-label">CHATS</div>' + conversations.map(item => `
|
|
5023
|
-
<div class="history-item ${item.id === currentConversationId ? 'active' : ''}" data-conversation-id="${escapeHtml(item.id)}" title="${escapeHtml(item.title || '')}">
|
|
5024
|
-
<i class="ti ti-message-2"></i>
|
|
5025
|
-
<span>${escapeHtml(item.title || '새 대화')}</span>
|
|
5026
|
-
</div>
|
|
5027
|
-
`).join('');
|
|
5028
|
-
historyContainer.querySelectorAll('.history-item').forEach(item => {
|
|
5029
|
-
item.onclick = () => openConversation(item.dataset.conversationId);
|
|
5030
|
-
});
|
|
5067
|
+
renderHistoryItems(conversations);
|
|
5031
5068
|
return conversations;
|
|
5032
5069
|
} catch (e) { }
|
|
5033
5070
|
return [];
|
|
5034
5071
|
}
|
|
5035
5072
|
|
|
5073
|
+
let _searchDebounce = null;
|
|
5074
|
+
async function onHistorySearch(q) {
|
|
5075
|
+
clearTimeout(_searchDebounce);
|
|
5076
|
+
if (!q.trim()) { loadHistory(); return; }
|
|
5077
|
+
_searchDebounce = setTimeout(async () => {
|
|
5078
|
+
try {
|
|
5079
|
+
const res = await apiFetch(`/history/search?q=${encodeURIComponent(q)}`);
|
|
5080
|
+
if (!res.ok) return;
|
|
5081
|
+
const data = await res.json();
|
|
5082
|
+
const results = (data.results || []).map(r => ({
|
|
5083
|
+
id: r.conversation_id,
|
|
5084
|
+
title: r.title || '새 대화',
|
|
5085
|
+
}));
|
|
5086
|
+
renderHistoryItems(results);
|
|
5087
|
+
} catch {}
|
|
5088
|
+
}, 300);
|
|
5089
|
+
}
|
|
5090
|
+
|
|
5091
|
+
async function deleteConversation(conversationId) {
|
|
5092
|
+
if (!confirm('이 대화를 삭제할까요?')) return;
|
|
5093
|
+
try {
|
|
5094
|
+
await apiFetch(`/history/conversations/${encodeURIComponent(conversationId)}`, { method: 'DELETE' });
|
|
5095
|
+
if (currentConversationId === conversationId) startNewConversation();
|
|
5096
|
+
loadHistory();
|
|
5097
|
+
} catch {}
|
|
5098
|
+
}
|
|
5099
|
+
|
|
5036
5100
|
async function restoreCurrentConversation() {
|
|
5037
5101
|
const conversations = await loadHistory();
|
|
5038
5102
|
if (conversations.some(item => item.id === currentConversationId)) {
|
|
@@ -5546,6 +5610,32 @@
|
|
|
5546
5610
|
|
|
5547
5611
|
document.getElementById('new-chat-btn').onclick = startNewChat;
|
|
5548
5612
|
applyI18n();
|
|
5613
|
+
|
|
5614
|
+
// Session check — redirect to /account if not logged in
|
|
5615
|
+
(async function restoreSession() {
|
|
5616
|
+
try {
|
|
5617
|
+
const res = await apiFetch('/account/profile');
|
|
5618
|
+
if (res.ok) {
|
|
5619
|
+
const data = await res.json();
|
|
5620
|
+
currentUserEmail = data.email;
|
|
5621
|
+
currentUserNickname = data.nickname || data.name || data.email;
|
|
5622
|
+
isAdmin = Boolean(data.is_admin);
|
|
5623
|
+
localStorage.setItem('ltcai_user_email', currentUserEmail);
|
|
5624
|
+
localStorage.setItem('ltcai_user_nickname', currentUserNickname);
|
|
5625
|
+
localStorage.setItem('ltcai_is_admin', isAdmin ? 'true' : 'false');
|
|
5626
|
+
document.getElementById('user-nickname-display').innerText = currentUserNickname;
|
|
5627
|
+
const av = document.getElementById('user-avatar-initial');
|
|
5628
|
+
if (av) av.textContent = (currentUserNickname || 'G')[0].toUpperCase();
|
|
5629
|
+
document.getElementById('admin-btn').style.display = 'flex';
|
|
5630
|
+
document.getElementById('security-admin-meta').textContent = isAdmin ? t('admin_has_rights') : t('admin_dashboard_access');
|
|
5631
|
+
} else {
|
|
5632
|
+
window.location.href = '/account';
|
|
5633
|
+
}
|
|
5634
|
+
} catch (_) {
|
|
5635
|
+
window.location.href = '/account';
|
|
5636
|
+
}
|
|
5637
|
+
})();
|
|
5638
|
+
|
|
5549
5639
|
loadModelStatus();
|
|
5550
5640
|
loadVpcStatus();
|
|
5551
5641
|
restoreCurrentConversation();
|
|
@@ -6073,6 +6163,91 @@
|
|
|
6073
6163
|
_footBtns(`<button class="wbtn wbtn-primary" onclick="closeSetupWizard();loadModelStatus()">완료 ✓</button>`);
|
|
6074
6164
|
}
|
|
6075
6165
|
</script>
|
|
6166
|
+
|
|
6167
|
+
<script>
|
|
6168
|
+
// ── MCP 관리 모달 ────────────────────────────────────────────────────────
|
|
6169
|
+
async function openMcpModal() {
|
|
6170
|
+
document.getElementById('mcp-modal-overlay').classList.add('open');
|
|
6171
|
+
await renderMcpModal();
|
|
6172
|
+
}
|
|
6173
|
+
|
|
6174
|
+
function closeMcpModal() {
|
|
6175
|
+
document.getElementById('mcp-modal-overlay').classList.remove('open');
|
|
6176
|
+
}
|
|
6177
|
+
|
|
6178
|
+
async function renderMcpModal() {
|
|
6179
|
+
const body = document.getElementById('mcp-modal-body');
|
|
6180
|
+
body.innerHTML = '<div style="color:var(--faint);font-size:13px;text-align:center;padding:24px">로딩 중...</div>';
|
|
6181
|
+
try {
|
|
6182
|
+
const [installedRes, toolsRes] = await Promise.all([
|
|
6183
|
+
apiFetch('/mcp/installed'),
|
|
6184
|
+
apiFetch('/mcp/tools'),
|
|
6185
|
+
]);
|
|
6186
|
+
const installed = installedRes.ok ? await installedRes.json() : {};
|
|
6187
|
+
const allTools = toolsRes.ok ? await toolsRes.json() : [];
|
|
6188
|
+
|
|
6189
|
+
const installedIds = new Set(Object.keys(installed));
|
|
6190
|
+
const installedItems = allTools.filter(t => installedIds.has(t.id));
|
|
6191
|
+
const availableItems = allTools.filter(t => !installedIds.has(t.id));
|
|
6192
|
+
|
|
6193
|
+
let html = '';
|
|
6194
|
+
|
|
6195
|
+
if (installedItems.length) {
|
|
6196
|
+
html += '<div class="mcp-section-label">설치됨</div>';
|
|
6197
|
+
html += installedItems.map(mcp => `
|
|
6198
|
+
<div class="mcp-item">
|
|
6199
|
+
<div class="mcp-item-icon">${mcp.icon || '🔌'}</div>
|
|
6200
|
+
<div class="mcp-item-info">
|
|
6201
|
+
<div class="mcp-item-name">${escapeHtml(mcp.name || mcp.id)}</div>
|
|
6202
|
+
<div class="mcp-item-desc">${escapeHtml(mcp.description || '')}</div>
|
|
6203
|
+
</div>
|
|
6204
|
+
<span class="mcp-item-status">활성</span>
|
|
6205
|
+
</div>
|
|
6206
|
+
`).join('');
|
|
6207
|
+
}
|
|
6208
|
+
|
|
6209
|
+
if (availableItems.length) {
|
|
6210
|
+
html += '<div class="mcp-section-label">설치 가능</div>';
|
|
6211
|
+
html += availableItems.map(mcp => `
|
|
6212
|
+
<div class="mcp-item" id="mcp-item-${escapeHtml(mcp.id)}">
|
|
6213
|
+
<div class="mcp-item-icon">${mcp.icon || '🔌'}</div>
|
|
6214
|
+
<div class="mcp-item-info">
|
|
6215
|
+
<div class="mcp-item-name">${escapeHtml(mcp.name || mcp.id)}</div>
|
|
6216
|
+
<div class="mcp-item-desc">${escapeHtml(mcp.description || '')}</div>
|
|
6217
|
+
</div>
|
|
6218
|
+
<button class="mcp-install-btn" onclick="installMcp('${escapeHtml(mcp.id)}')">설치</button>
|
|
6219
|
+
</div>
|
|
6220
|
+
`).join('');
|
|
6221
|
+
}
|
|
6222
|
+
|
|
6223
|
+
if (!html) html = '<div style="color:var(--faint);font-size:13px;text-align:center;padding:24px">사용 가능한 MCP 서버가 없습니다.</div>';
|
|
6224
|
+
body.innerHTML = html;
|
|
6225
|
+
} catch (e) {
|
|
6226
|
+
body.innerHTML = `<div style="color:#ff6b6b;font-size:13px;text-align:center;padding:24px">로드 실패: ${escapeHtml(e.message)}</div>`;
|
|
6227
|
+
}
|
|
6228
|
+
}
|
|
6229
|
+
|
|
6230
|
+
async function installMcp(id) {
|
|
6231
|
+
const btn = document.querySelector(`#mcp-item-${CSS.escape(id)} .mcp-install-btn`);
|
|
6232
|
+
if (btn) { btn.disabled = true; btn.textContent = '설치 중...'; }
|
|
6233
|
+
try {
|
|
6234
|
+
const res = await apiFetch('/mcp/install', {
|
|
6235
|
+
method: 'POST',
|
|
6236
|
+
headers: { 'Content-Type': 'application/json' },
|
|
6237
|
+
body: JSON.stringify({ id }),
|
|
6238
|
+
});
|
|
6239
|
+
if (res.ok) {
|
|
6240
|
+
await renderMcpModal();
|
|
6241
|
+
} else {
|
|
6242
|
+
const d = await res.json().catch(() => ({}));
|
|
6243
|
+
if (btn) { btn.disabled = false; btn.textContent = '설치'; }
|
|
6244
|
+
alert(d.detail || '설치 실패');
|
|
6245
|
+
}
|
|
6246
|
+
} catch {
|
|
6247
|
+
if (btn) { btn.disabled = false; btn.textContent = '설치'; }
|
|
6248
|
+
}
|
|
6249
|
+
}
|
|
6250
|
+
</script>
|
|
6076
6251
|
</body>
|
|
6077
6252
|
|
|
6078
6253
|
</html>
|