ltcai 0.1.2 → 0.1.3

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.
@@ -335,6 +335,8 @@
335
335
  background: rgba(29,42,60,0.82);
336
336
  backdrop-filter: blur(20px);
337
337
  -webkit-backdrop-filter: blur(20px);
338
+ position: relative;
339
+ z-index: 50;
338
340
  }
339
341
 
340
342
  .header-left, .header-pills {
@@ -2807,43 +2809,6 @@
2807
2809
  </div>
2808
2810
  <div class="bg-grid"></div>
2809
2811
 
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
2812
 
2848
2813
  <div class="app-layout">
2849
2814
  <!-- Sidebar -->
@@ -3351,7 +3316,6 @@
3351
3316
  if (typeof acquireVsCodeApi === 'function') {
3352
3317
  vscode = acquireVsCodeApi();
3353
3318
  console.log("VS Code API Acquired");
3354
- document.getElementById('auth-overlay').style.display = 'none'; // VS Code에서는 로그인 건너뜀
3355
3319
  }
3356
3320
  } catch (e) { /* Not in VS Code */ }
3357
3321
 
@@ -3363,80 +3327,12 @@
3363
3327
  }
3364
3328
  });
3365
3329
 
3366
- function toggleAuth(showRegister) {
3367
- document.getElementById('login-form').style.display = showRegister ? 'none' : 'block';
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() {
3330
+ async function logout() {
3331
+ try { await apiFetch('/logout', { method: 'POST' }); } catch (_) {}
3436
3332
  localStorage.removeItem('ltcai_user_email');
3437
3333
  localStorage.removeItem('ltcai_user_nickname');
3438
3334
  localStorage.removeItem('ltcai_is_admin');
3439
- location.reload();
3335
+ window.location.href = '/account';
3440
3336
  }
3441
3337
 
3442
3338
  const I18N = {
@@ -3461,7 +3357,7 @@
3461
3357
  btn_save: '저장', btn_change: '변경', btn_cancel: '취소',
3462
3358
  // ops 스트립
3463
3359
  vpc_not_set: '설정 안 됨', vpc_click_to_set: '클릭하여 VPC 연결 설정',
3464
- security_monitor: '민감정보 감시', admin_dashboard_access: '관리자 대시보드 접근',
3360
+ security_monitor: '민감정보 감시', admin_dashboard_access: '관리자 대시보드 접근', admin_has_rights: '관리자 권한 있음',
3465
3361
  // 빈 화면
3466
3362
  empty_title: '무엇을 만들까요?',
3467
3363
  empty_sub: '로컬 모델, 이미지 분석, 코드 생성, 프라이빗 VPC — 모든 걸 한 화면에서 이어가세요.',
@@ -3500,7 +3396,7 @@
3500
3396
  btn_save: 'Save', btn_change: 'Change', btn_cancel: 'Cancel',
3501
3397
  // Ops strip
3502
3398
  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',
3399
+ security_monitor: 'Sensitive data monitor', admin_dashboard_access: 'Admin dashboard access', admin_has_rights: 'Has admin rights',
3504
3400
  // Empty state
3505
3401
  empty_title: 'What would you like to build?',
3506
3402
  empty_sub: 'Local models, image analysis, code generation, private VPC — all in one workspace.',
@@ -4051,9 +3947,27 @@
4051
3947
  }
4052
3948
 
4053
3949
  async function openAdminPanel() {
3950
+ if (!isAdmin) {
3951
+ showToast(currentLang === 'ko' ? '관리자 권한이 없습니다.' : 'Admin access required.');
3952
+ return;
3953
+ }
4054
3954
  window.location.href = '/admin';
4055
3955
  }
4056
3956
 
3957
+ function showToast(msg) {
3958
+ let t = document.getElementById('ltcai-toast');
3959
+ if (!t) {
3960
+ t = document.createElement('div');
3961
+ t.id = 'ltcai-toast';
3962
+ 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;';
3963
+ document.body.appendChild(t);
3964
+ }
3965
+ t.textContent = msg;
3966
+ t.style.opacity = '1';
3967
+ clearTimeout(t._timer);
3968
+ t._timer = setTimeout(() => { t.style.opacity = '0'; }, 2200);
3969
+ }
3970
+
4057
3971
  function closeAdminPanel() {
4058
3972
  document.getElementById('admin-overlay').style.display = 'none';
4059
3973
  }
@@ -5546,6 +5460,32 @@
5546
5460
 
5547
5461
  document.getElementById('new-chat-btn').onclick = startNewChat;
5548
5462
  applyI18n();
5463
+
5464
+ // Session check — redirect to /account if not logged in
5465
+ (async function restoreSession() {
5466
+ try {
5467
+ const res = await apiFetch('/account/profile');
5468
+ if (res.ok) {
5469
+ const data = await res.json();
5470
+ currentUserEmail = data.email;
5471
+ currentUserNickname = data.nickname || data.name || data.email;
5472
+ isAdmin = Boolean(data.is_admin);
5473
+ localStorage.setItem('ltcai_user_email', currentUserEmail);
5474
+ localStorage.setItem('ltcai_user_nickname', currentUserNickname);
5475
+ localStorage.setItem('ltcai_is_admin', isAdmin ? 'true' : 'false');
5476
+ document.getElementById('user-nickname-display').innerText = currentUserNickname;
5477
+ const av = document.getElementById('user-avatar-initial');
5478
+ if (av) av.textContent = (currentUserNickname || 'G')[0].toUpperCase();
5479
+ document.getElementById('admin-btn').style.display = 'flex';
5480
+ document.getElementById('security-admin-meta').textContent = isAdmin ? t('admin_has_rights') : t('admin_dashboard_access');
5481
+ } else {
5482
+ window.location.href = '/account';
5483
+ }
5484
+ } catch (_) {
5485
+ window.location.href = '/account';
5486
+ }
5487
+ })();
5488
+
5549
5489
  loadModelStatus();
5550
5490
  loadVpcStatus();
5551
5491
  restoreCurrentConversation();
package/static/index.html DELETED
@@ -1,270 +0,0 @@
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>Lattice AI | Local Hub</title>
7
- <link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;600&family=Inter:wght@400;500&display=swap" rel="stylesheet">
8
- <style>
9
- :root {
10
- --bg-color: #0f172a;
11
- --glass-bg: rgba(30, 41, 59, 0.7);
12
- --primary-accent: #6366f1;
13
- --secondary-accent: #a855f7;
14
- --text-main: #f8fafc;
15
- --text-dim: #94a3b8;
16
- --bot-msg-bg: rgba(51, 65, 85, 0.5);
17
- --user-msg-bg: linear-gradient(135deg, #6366f1 0%, #a855f7 100%);
18
- }
19
-
20
- * {
21
- margin: 0;
22
- padding: 0;
23
- box-sizing: border-box;
24
- font-family: 'Inter', sans-serif;
25
- }
26
-
27
- body {
28
- background-color: var(--bg-color);
29
- background-image:
30
- radial-gradient(at 0% 0%, rgba(99, 102, 241, 0.15) 0px, transparent 50%),
31
- radial-gradient(at 100% 100%, rgba(168, 85, 247, 0.15) 0px, transparent 50%);
32
- height: 100vh;
33
- display: flex;
34
- flex-direction: column;
35
- color: var(--text-main);
36
- overflow: hidden;
37
- }
38
-
39
- header {
40
- padding: 1.5rem 2rem;
41
- background: var(--glass-bg);
42
- backdrop-filter: blur(12px);
43
- border-bottom: 1px solid rgba(255, 255, 255, 0.1);
44
- display: flex;
45
- justify-content: space-between;
46
- align-items: center;
47
- z-index: 10;
48
- }
49
-
50
- .logo {
51
- font-family: 'Outfit', sans-serif;
52
- font-weight: 600;
53
- font-size: 1.5rem;
54
- background: linear-gradient(to right, #818cf8, #c084fc);
55
- -webkit-background-clip: text;
56
- -webkit-text-fill-color: transparent;
57
- display: flex;
58
- align-items: center;
59
- gap: 0.5rem;
60
- }
61
-
62
- .status-badge {
63
- display: flex;
64
- align-items: center;
65
- gap: 0.5rem;
66
- font-size: 0.85rem;
67
- padding: 0.4rem 0.8rem;
68
- background: rgba(34, 197, 94, 0.1);
69
- color: #4ade80;
70
- border-radius: 20px;
71
- border: 1px solid rgba(34, 197, 94, 0.2);
72
- }
73
-
74
- .status-dot {
75
- width: 8px;
76
- height: 8px;
77
- background-color: #4ade80;
78
- border-radius: 50%;
79
- box-shadow: 0 0 8px #4ade80;
80
- animation: pulse 2s infinite;
81
- }
82
-
83
- @keyframes pulse {
84
- 0% { opacity: 1; }
85
- 50% { opacity: 0.4; }
86
- 100% { opacity: 1; }
87
- }
88
-
89
- #chat-container {
90
- flex: 1;
91
- overflow-y: auto;
92
- padding: 2rem;
93
- display: flex;
94
- flex-direction: column;
95
- gap: 1.5rem;
96
- scroll-behavior: smooth;
97
- }
98
-
99
- .message {
100
- max-width: 80%;
101
- padding: 1rem 1.25rem;
102
- border-radius: 1.25rem;
103
- line-height: 1.6;
104
- font-size: 0.95rem;
105
- position: relative;
106
- animation: slideUp 0.3s ease-out;
107
- }
108
-
109
- @keyframes slideUp {
110
- from { opacity: 0; transform: translateY(10px); }
111
- to { opacity: 1; transform: translateY(0); }
112
- }
113
-
114
- .user-message {
115
- align-self: flex-end;
116
- background: var(--user-msg-bg);
117
- color: white;
118
- border-bottom-right-radius: 0.25rem;
119
- box-shadow: 0 4px 15px rgba(99, 102, 241, 0.3);
120
- }
121
-
122
- .bot-message {
123
- align-self: flex-start;
124
- background: var(--bot-msg-bg);
125
- backdrop-filter: blur(5px);
126
- border: 1px solid rgba(255, 255, 255, 0.05);
127
- border-bottom-left-radius: 0.25rem;
128
- }
129
-
130
- .input-area {
131
- padding: 2rem;
132
- background: linear-gradient(to top, var(--bg-color), transparent);
133
- position: relative;
134
- }
135
-
136
- .input-wrapper {
137
- max-width: 900px;
138
- margin: 0 auto;
139
- background: var(--glass-bg);
140
- backdrop-filter: blur(20px);
141
- border: 1px solid rgba(255, 255, 255, 0.1);
142
- border-radius: 1.5rem;
143
- padding: 0.75rem;
144
- display: flex;
145
- gap: 0.75rem;
146
- box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
147
- transition: transform 0.2s, border-color 0.2s;
148
- }
149
-
150
- textarea {
151
- flex: 1;
152
- background: transparent;
153
- border: none;
154
- color: white;
155
- padding: 0.75rem;
156
- resize: none;
157
- outline: none;
158
- font-size: 1rem;
159
- max-height: 150px;
160
- }
161
-
162
- #send-btn {
163
- background: var(--primary-accent);
164
- color: white;
165
- border: none;
166
- width: 45px;
167
- height: 45px;
168
- border-radius: 12px;
169
- cursor: pointer;
170
- display: flex;
171
- align-items: center;
172
- justify-content: center;
173
- transition: all 0.2s;
174
- }
175
-
176
- #send-btn:hover { background: var(--secondary-accent); transform: scale(1.05); }
177
-
178
- .typing { display: flex; gap: 4px; padding: 0.5rem; }
179
- .dot { width: 6px; height: 6px; background: var(--text-dim); border-radius: 50%; animation: bounce 1.4s infinite ease-in-out; }
180
- .dot:nth-child(2) { animation-delay: 0.2s; }
181
- .dot:nth-child(3) { animation-delay: 0.4s; }
182
-
183
- @keyframes bounce { 0%, 80%, 100% { transform: scale(0); } 40% { transform: scale(1); } }
184
- </style>
185
- </head>
186
- <body>
187
- <header>
188
- <div class="logo">
189
- <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2a10 10 0 1 0 10 10H12V2z"></path><path d="M12 12L2.1 12.1"></path><path d="M12 12v10"></path><path d="M12 12l8.1-5.4"></path></svg>
190
- Lattice AI | Premium
191
- </div>
192
- <div class="status-badge"><div class="status-dot"></div>Local Hub Active</div>
193
- </header>
194
-
195
- <div id="chat-container">
196
- <div class="message bot-message">
197
- ✨ 안녕하세요! 제가 정성껏 만든 프리미엄 로컬 허브입니다. <br>
198
- 브라우저에서 직접 Gemma 4와 대화해 보세요!
199
- </div>
200
- </div>
201
-
202
- <div class="input-area">
203
- <div class="input-wrapper">
204
- <textarea id="user-input" placeholder="메시지를 입력하세요..." rows="1"></textarea>
205
- <button id="send-btn">
206
- <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="22" y1="2" x2="11" y2="13"></line><polygon points="22 2 15 22 11 13 2 9 22 2"></polygon></svg>
207
- </button>
208
- </div>
209
- </div>
210
-
211
- <script>
212
- const chatContainer = document.getElementById('chat-container');
213
- const userInput = document.getElementById('user-input');
214
- const sendBtn = document.getElementById('send-btn');
215
-
216
- userInput.addEventListener('input', () => {
217
- userInput.style.height = 'auto';
218
- userInput.style.height = userInput.scrollHeight + 'px';
219
- });
220
-
221
- async function sendMessage() {
222
- const message = userInput.value.trim();
223
- if (!message) return;
224
- addMessage(message, 'user');
225
- userInput.value = '';
226
- userInput.style.height = 'auto';
227
- const loadingMsg = addLoadingMessage();
228
- chatContainer.scrollTop = chatContainer.scrollHeight;
229
- try {
230
- const response = await fetch('/chat', {
231
- method: 'POST',
232
- headers: { 'Content-Type': 'application/json' },
233
- body: JSON.stringify({ message: message, stream: false })
234
- });
235
- const data = await response.json();
236
- loadingMsg.remove();
237
- addMessage(data.response, 'bot');
238
- } catch (error) {
239
- loadingMsg.remove();
240
- addMessage('❌ 서버 연결 실패', 'bot');
241
- }
242
- chatContainer.scrollTop = chatContainer.scrollHeight;
243
- }
244
-
245
- function addMessage(text, side) {
246
- const div = document.createElement('div');
247
- div.className = `message ${side}-message`;
248
- div.innerHTML = text.replace(/\n/g, '<br>');
249
- chatContainer.appendChild(div);
250
- return div;
251
- }
252
-
253
- function addLoadingMessage() {
254
- const div = document.createElement('div');
255
- div.className = 'message bot-message typing';
256
- div.innerHTML = '<div class="dot"></div><div class="dot"></div><div class="dot"></div>';
257
- chatContainer.appendChild(div);
258
- return div;
259
- }
260
-
261
- sendBtn.addEventListener('click', sendMessage);
262
- userInput.addEventListener('keydown', (e) => {
263
- if (e.key === 'Enter' && !e.shiftKey) {
264
- e.preventDefault();
265
- sendMessage();
266
- }
267
- });
268
- </script>
269
- </body>
270
- </html>