ltcai 0.1.26 → 0.1.28

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.
@@ -210,9 +210,9 @@
210
210
 
211
211
  /* Lang picker */
212
212
  .lang-wrap {
213
- position: fixed;
214
- top: 20px;
215
- right: 24px;
213
+ position: absolute;
214
+ top: 12px;
215
+ right: 12px;
216
216
  z-index: 10;
217
217
  }
218
218
  .lang-btn {
@@ -249,41 +249,220 @@
249
249
  }
250
250
  .lang-opt:hover { background: rgba(167,124,255,0.10); color: var(--text); }
251
251
  .lang-opt.active { color: var(--accent); font-weight: 700; }
252
+
253
+ /* PPT reference theme: bright indigo sign-in with SSO and language switcher. */
254
+ :root {
255
+ --bg: #f7f3ff;
256
+ --text: #1f2140;
257
+ --faint: #8a86a8;
258
+ --muted: #66627f;
259
+ --accent: #6f4bf6;
260
+ --accent-2: #7b6dff;
261
+ --shadow: 0 26px 80px rgba(86, 70, 160, 0.22);
262
+ }
263
+ body {
264
+ background:
265
+ radial-gradient(circle at 74% 22%, rgba(111,75,246,0.16), transparent 28%),
266
+ radial-gradient(circle at 16% 18%, rgba(196,181,253,0.32), transparent 26%),
267
+ linear-gradient(135deg, #fbf9ff 0%, #f4efff 48%, #ffffff 100%);
268
+ }
269
+ body::before {
270
+ background:
271
+ radial-gradient(circle, rgba(123,109,255,0.28) 1px, transparent 1.8px),
272
+ linear-gradient(rgba(123,109,255,0.08) 1px, transparent 1px),
273
+ linear-gradient(90deg, rgba(123,109,255,0.06) 1px, transparent 1px);
274
+ mask-image: linear-gradient(180deg, rgba(0,0,0,0.18), rgba(0,0,0,0.02));
275
+ }
276
+ body::after {
277
+ background:
278
+ radial-gradient(ellipse at 8% 78%, rgba(142,111,255,0.24), transparent 34%),
279
+ linear-gradient(115deg, transparent 0 35%, rgba(111,75,246,0.09) 35.2%, transparent 35.6% 100%);
280
+ }
281
+ .login-shell {
282
+ width: min(920px, 100%);
283
+ display: grid;
284
+ grid-template-columns: minmax(280px, 400px) minmax(220px, 1fr);
285
+ align-items: center;
286
+ gap: 46px;
287
+ position: relative;
288
+ z-index: 1;
289
+ }
290
+ .brand-preview {
291
+ min-height: 360px;
292
+ border-radius: 34px;
293
+ background:
294
+ radial-gradient(circle at 54% 45%, rgba(111,75,246,0.24), transparent 18%),
295
+ linear-gradient(145deg, rgba(255,255,255,0.68), rgba(244,239,255,0.32));
296
+ border: 1px solid rgba(111,75,246,0.12);
297
+ box-shadow: inset 0 1px 0 rgba(255,255,255,0.85);
298
+ position: relative;
299
+ overflow: hidden;
300
+ }
301
+ .brand-preview::before {
302
+ content: '';
303
+ position: absolute;
304
+ inset: auto -12% 0 -8%;
305
+ height: 46%;
306
+ background: linear-gradient(135deg, rgba(111,75,246,0.24), rgba(255,255,255,0.12));
307
+ clip-path: polygon(0 62%, 18% 42%, 36% 54%, 55% 26%, 76% 44%, 100% 20%, 100% 100%, 0 100%);
308
+ filter: blur(1px);
309
+ }
310
+ .preview-node {
311
+ position: absolute;
312
+ width: 52px;
313
+ height: 52px;
314
+ border-radius: 17px;
315
+ background: #fff;
316
+ border: 1px solid rgba(111,75,246,0.18);
317
+ box-shadow: 0 18px 40px rgba(111,75,246,0.16);
318
+ }
319
+ .preview-node::after {
320
+ content: '';
321
+ position: absolute;
322
+ inset: 14px;
323
+ border-radius: 10px;
324
+ background: linear-gradient(135deg, #6f4bf6, #9f8cff);
325
+ }
326
+ .preview-node.n1 { top: 52px; left: 58px; }
327
+ .preview-node.n2 { top: 112px; right: 64px; }
328
+ .preview-node.n3 { left: 120px; bottom: 92px; }
329
+ .preview-node.n4 { right: 120px; bottom: 58px; }
330
+ .preview-line {
331
+ position: absolute;
332
+ height: 2px;
333
+ width: 150px;
334
+ background: linear-gradient(90deg, transparent, rgba(111,75,246,0.45), transparent);
335
+ transform-origin: left center;
336
+ }
337
+ .preview-line.l1 { top: 108px; left: 112px; transform: rotate(18deg); }
338
+ .preview-line.l2 { top: 186px; left: 176px; transform: rotate(112deg); }
339
+ .preview-line.l3 { bottom: 112px; left: 172px; transform: rotate(-13deg); }
340
+ .card {
341
+ background: rgba(255,255,255,0.86);
342
+ border: 1px solid rgba(111,75,246,0.13);
343
+ border-radius: 14px;
344
+ box-shadow: var(--shadow), inset 0 1px 0 rgba(255,255,255,0.9);
345
+ }
346
+ .card::before {
347
+ background: linear-gradient(90deg, transparent, rgba(111,75,246,0.65), rgba(123,109,255,0.45), transparent);
348
+ }
349
+ .logo {
350
+ background: linear-gradient(135deg, #6f4bf6 0%, #8d7aff 100%);
351
+ color: #fff;
352
+ box-shadow: 0 16px 34px rgba(111,75,246,0.28);
353
+ }
354
+ .title {
355
+ background: linear-gradient(135deg, #1f2140 35%, #6f4bf6 82%);
356
+ -webkit-background-clip: text;
357
+ background-clip: text;
358
+ }
359
+ .input {
360
+ background: #fbfaff;
361
+ border-color: rgba(111,75,246,0.16);
362
+ color: var(--text);
363
+ }
364
+ .input:focus {
365
+ border-color: rgba(111,75,246,0.52);
366
+ box-shadow: 0 0 0 4px rgba(111,75,246,0.10);
367
+ }
368
+ .submit {
369
+ background: linear-gradient(135deg, #6f4bf6, #7b6dff);
370
+ color: #fff;
371
+ box-shadow: 0 14px 30px rgba(111,75,246,0.24);
372
+ }
373
+ .submit:hover {
374
+ background: linear-gradient(135deg, #5f3ee6, #705fff);
375
+ box-shadow: 0 18px 38px rgba(111,75,246,0.30);
376
+ }
377
+ .sso-btn {
378
+ background: #fff;
379
+ border-color: rgba(111,75,246,0.15);
380
+ color: var(--text);
381
+ }
382
+ .sso-btn:hover {
383
+ background: #f6f2ff;
384
+ border-color: rgba(111,75,246,0.34);
385
+ }
386
+ .lang-btn {
387
+ background: rgba(255,255,255,0.86);
388
+ border-color: rgba(111,75,246,0.18);
389
+ color: var(--text);
390
+ box-shadow: 0 10px 28px rgba(86,70,160,0.12);
391
+ }
392
+ .lang-menu {
393
+ background: #fff;
394
+ border-color: rgba(111,75,246,0.15);
395
+ box-shadow: 0 18px 44px rgba(86,70,160,0.16);
396
+ }
397
+ @media (max-width: 760px) {
398
+ .login-shell { display: block; }
399
+ .brand-preview { display: none; }
400
+ }
252
401
  </style>
402
+ <link rel="stylesheet" href="/static/lattice-reference.css">
253
403
  </head>
254
- <body>
404
+ <body class="lattice-ref-auth">
255
405
  <div class="orb orb-1"></div>
256
406
  <div class="orb orb-2"></div>
257
-
258
- <div class="lang-wrap">
259
- <button class="lang-btn" id="lang-btn" onclick="toggleLang()">🌐 <span id="lang-label">한국어</span></button>
260
- <div class="lang-menu" id="lang-menu">
261
- <div class="lang-opt" id="opt-ko" onclick="setLang('ko')">🇰🇷 한국어</div>
262
- <div class="lang-opt" id="opt-en" onclick="setLang('en')">🇺🇸 English</div>
263
- </div>
407
+ <div class="auth-titlebar" aria-hidden="true">
408
+ <div class="auth-window-brand"><i class="ti ti-cube-3d-sphere"></i><span>Lattice AI</span></div>
409
+ <div class="auth-window-controls"><span></span><span></span><span></span></div>
410
+ </div>
411
+ <div class="auth-wave auth-wave-left" aria-hidden="true"></div>
412
+ <div class="auth-wave auth-wave-right" aria-hidden="true"></div>
413
+ <div class="auth-network" aria-hidden="true">
414
+ <span></span><span></span><span></span><span></span><span></span><span></span>
264
415
  </div>
265
416
 
417
+ <div class="login-shell">
418
+ <div class="brand-preview" aria-hidden="true">
419
+ <div class="preview-node n1"></div>
420
+ <div class="preview-node n2"></div>
421
+ <div class="preview-node n3"></div>
422
+ <div class="preview-node n4"></div>
423
+ <div class="preview-line l1"></div>
424
+ <div class="preview-line l2"></div>
425
+ <div class="preview-line l3"></div>
426
+ </div>
266
427
  <div class="card">
428
+ <div class="lang-wrap">
429
+ <button class="lang-btn" id="lang-btn" onclick="toggleLang()">🌐 Language</button>
430
+ <div class="lang-menu" id="lang-menu">
431
+ <div class="lang-opt" id="opt-ko" onclick="setLang('ko')">🇰🇷 한국어</div>
432
+ <div class="lang-opt" id="opt-en" onclick="setLang('en')">🇺🇸 English</div>
433
+ </div>
434
+ </div>
267
435
  <!-- Login form -->
268
436
  <div id="login-section">
269
- <div class="logo"><i class="ti ti-brain"></i></div>
270
- <h2 class="title" id="login-title">Lattice AI</h2>
271
- <p class="subtitle" id="login-sub">Local AI Workspace — Apple Silicon</p>
272
- <input class="input" type="email" id="login-email" placeholder="이메일 주소">
273
- <input class="input" type="password" id="login-pw" placeholder="비밀번호" onkeydown="if(event.key==='Enter')doLogin()">
437
+ <div class="hero-logo">
438
+ <div class="hero-logo-mark"><i class="ti ti-cube-3d-sphere"></i></div>
439
+ <h2 class="title" id="login-title">Lattice AI</h2>
440
+ </div>
441
+ <p class="subtitle" id="login-sub">내 PC에서 시작하는<br>개인 AI 워크스페이스</p>
442
+ <label class="auth-field">
443
+ <i class="ti ti-mail"></i>
444
+ <input class="input" type="email" id="login-email" placeholder="이메일 주소">
445
+ </label>
446
+ <label class="auth-field">
447
+ <i class="ti ti-lock"></i>
448
+ <input class="input" type="password" id="login-pw" placeholder="비밀번호" onkeydown="if(event.key==='Enter')doLogin()">
449
+ <button type="button" class="field-eye" onclick="togglePasswordVisibility()" title="비밀번호 보기"><i class="ti ti-eye"></i></button>
450
+ </label>
274
451
  <div class="msg" id="login-msg"></div>
275
452
  <button class="submit" id="login-btn" onclick="doLogin()" data-ko="로그인" data-en="Log in">로그인</button>
276
- <div id="sso-section" style="display:none;">
277
- <div class="sso-divider" id="sso-divider-text">또는</div>
278
- <button class="sso-btn" id="sso-btn" onclick="doSSOLogin()">
279
- <i class="ti ti-building"></i>
280
- <span id="sso-btn-label">SSO로 로그인</span>
453
+ <button class="register-cta" id="go-register-link" onclick="showSection('register');return false;">회원가입</button>
454
+ <div id="sso-section">
455
+ <div class="sso-divider" id="sso-divider-text">조직 계정으로 로그인</div>
456
+ <button class="sso-btn sso-ms" onclick="doSSOLogin('microsoft')">
457
+ <span class="ms-logo" aria-hidden="true"><b></b><b></b><b></b><b></b></span>
458
+ <span id="sso-ms-label">Microsoft Entra ID로 계속하기</span>
459
+ </button>
460
+ <button class="sso-btn sso-okta" onclick="doSSOLogin('okta')">
461
+ <span class="okta-logo" aria-hidden="true"></span>
462
+ <span id="sso-okta-label">Okta SSO로 계속하기</span>
281
463
  </button>
282
464
  </div>
283
- <p class="switch" id="login-switch">
284
- <span id="no-account-text">계정이 없으신가요?</span>
285
- <a href="#" onclick="showSection('register');return false;" id="go-register-link">회원가입</a>
286
- </p>
465
+ <button class="local-start" onclick="showSection('register');return false;"><i class="ti ti-device-desktop"></i> <span id="local-start-label">로컬 계정으로 시작</span></button>
287
466
  </div>
288
467
 
289
468
  <!-- Register form -->
@@ -304,6 +483,11 @@
304
483
  </p>
305
484
  </div>
306
485
  </div>
486
+ </div>
487
+ <footer class="auth-footer">
488
+ <a href="#" onclick="return false;" id="help-link">도움말</a>
489
+ <a href="#" onclick="return false;" id="privacy-link">개인정보 처리방침</a>
490
+ </footer>
307
491
 
308
492
  <script>
309
493
  const API_BASE = window.location.protocol === 'file:' ? 'http://localhost:4825' : '';
@@ -315,7 +499,7 @@
315
499
  // ── i18n ──────────────────────────────────────────────
316
500
  const I18N = {
317
501
  ko: {
318
- login_title: 'Lattice AI', login_sub: 'Local AI Workspace Apple Silicon',
502
+ login_title: 'Lattice AI', login_sub: ' PC에서 시작하는<br>개인 AI 워크스페이스',
319
503
  ph_email: '이메일 주소', ph_pw: '비밀번호', ph_new_pw: '비밀번호 (4자 이상)',
320
504
  ph_pw_confirm: '비밀번호 확인', ph_name: '이름', ph_nick: '닉네임',
321
505
  btn_login: '로그인', btn_register: '가입하기',
@@ -326,10 +510,13 @@
326
510
  err_fill: '모든 항목을 입력해주세요.',
327
511
  err_login_fail: '이메일 또는 비밀번호가 틀렸습니다.',
328
512
  err_server: '서버 연결 실패',
329
- sso_divider: '또는', sso_btn: '로 로그인',
513
+ sso_divider: '조직 계정으로 로그인', sso_btn: '로 로그인',
514
+ ms_sso: 'Microsoft Entra ID로 계속하기', okta_sso: 'Okta SSO로 계속하기',
515
+ local_start: '로컬 계정으로 시작', help: '도움말', privacy: '개인정보 처리방침',
516
+ sso_unavailable: 'SSO가 아직 설정되지 않았습니다. 로컬 계정으로 시작하거나 관리자에게 문의하세요.',
330
517
  },
331
518
  en: {
332
- login_title: 'Lattice AI', login_sub: 'Local AI Workspace Apple Silicon',
519
+ login_title: 'Lattice AI', login_sub: 'Your personal AI workspace<br>starts on this PC',
333
520
  ph_email: 'Email address', ph_pw: 'Password', ph_new_pw: 'Password (min. 4 chars)',
334
521
  ph_pw_confirm: 'Confirm password', ph_name: 'Full name', ph_nick: 'Nickname',
335
522
  btn_login: 'Log in', btn_register: 'Sign up',
@@ -340,7 +527,10 @@
340
527
  err_fill: 'Please fill in all fields.',
341
528
  err_login_fail: 'Invalid email or password.',
342
529
  err_server: 'Server connection failed',
343
- sso_divider: 'or', sso_btn: 'Sign in with',
530
+ sso_divider: 'Sign in with organization account', sso_btn: 'Sign in with',
531
+ ms_sso: 'Continue with Microsoft Entra ID', okta_sso: 'Continue with Okta SSO',
532
+ local_start: 'Start with a local account', help: 'Help', privacy: 'Privacy Policy',
533
+ sso_unavailable: 'SSO is not configured yet. Start with a local account or contact your administrator.',
344
534
  }
345
535
  };
346
536
 
@@ -349,12 +539,11 @@
349
539
 
350
540
  function applyI18n() {
351
541
  document.getElementById('login-title').textContent = t('login_title');
352
- document.getElementById('login-sub').textContent = t('login_sub');
542
+ document.getElementById('login-sub').innerHTML = t('login_sub');
353
543
  document.getElementById('reg-title').textContent = t('reg_title');
354
544
  document.getElementById('reg-sub').textContent = t('reg_sub');
355
545
  document.getElementById('login-btn').textContent = t('btn_login');
356
546
  document.getElementById('reg-btn').textContent = t('btn_register');
357
- document.getElementById('no-account-text').textContent = t('no_account');
358
547
  document.getElementById('go-register-link').textContent = t('go_register');
359
548
  document.getElementById('have-account-text').textContent = t('have_account');
360
549
  document.getElementById('go-login-link').textContent = t('go_login');
@@ -365,11 +554,12 @@
365
554
  document.getElementById('reg-pw2').placeholder = t('ph_pw_confirm');
366
555
  document.getElementById('reg-name').placeholder = t('ph_name');
367
556
  document.getElementById('reg-nick').placeholder = t('ph_nick');
368
- document.getElementById('lang-label').textContent = lang === 'ko' ? '한국어' : 'English';
369
557
  document.getElementById('sso-divider-text').textContent = t('sso_divider');
370
- const ssoName = window._ssoProviderName || 'SSO';
371
- document.getElementById('sso-btn-label').textContent =
372
- lang === 'ko' ? `${ssoName}${t('sso_btn')}` : `${t('sso_btn')} ${ssoName}`;
558
+ document.getElementById('sso-ms-label').textContent = t('ms_sso');
559
+ document.getElementById('sso-okta-label').textContent = t('okta_sso');
560
+ document.getElementById('local-start-label').textContent = t('local_start');
561
+ document.getElementById('help-link').textContent = t('help');
562
+ document.getElementById('privacy-link').textContent = t('privacy');
373
563
  ['ko', 'en'].forEach(l => {
374
564
  const el = document.getElementById(`opt-${l}`);
375
565
  if (el) el.classList.toggle('active', l === lang);
@@ -382,17 +572,27 @@
382
572
  if (!res.ok) return;
383
573
  const cfg = await res.json();
384
574
  if (cfg.enabled) {
575
+ window._ssoEnabled = true;
385
576
  window._ssoProviderName = cfg.provider_name;
386
- document.getElementById('sso-section').style.display = '';
387
577
  applyI18n();
388
578
  }
389
579
  } catch {}
390
580
  }
391
581
 
392
- function doSSOLogin() {
582
+ function doSSOLogin(provider) {
583
+ if (!window._ssoEnabled) {
584
+ setMsg('login-msg', t('sso_unavailable'));
585
+ return;
586
+ }
587
+ if (provider) sessionStorage.setItem('ltcai_sso_provider_hint', provider);
393
588
  window.location.href = '/auth/sso/login';
394
589
  }
395
590
 
591
+ function togglePasswordVisibility() {
592
+ const input = document.getElementById('login-pw');
593
+ input.type = input.type === 'password' ? 'text' : 'password';
594
+ }
595
+
396
596
  function toggleLang() {
397
597
  const m = document.getElementById('lang-menu');
398
598
  m.classList.toggle('open');
package/static/admin.html CHANGED
@@ -595,10 +595,27 @@
595
595
  .lang-option:hover { background: rgba(167,124,255,0.10); color: var(--text); }
596
596
  .lang-option.active { color: var(--accent); font-weight: 600; }
597
597
  </style>
598
+ <link rel="stylesheet" href="/static/lattice-reference.css">
598
599
  <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
599
600
  </head>
600
601
 
601
- <body>
602
+ <body class="lattice-ref-admin">
603
+ <aside class="reference-rail admin-rail">
604
+ <div class="rail-brand"><i class="ti ti-shield-lock"></i><strong>LATTICE AI</strong><span>Administrator</span></div>
605
+ <nav>
606
+ <a class="active" href="/admin"><i class="ti ti-layout-dashboard"></i> 대시보드</a>
607
+ <a href="/admin"><i class="ti ti-users"></i> 사용자 관리</a>
608
+ <a href="/admin"><i class="ti ti-key"></i> 권한 관리</a>
609
+ <a href="/admin"><i class="ti ti-lock-access"></i> SSO 관리</a>
610
+ <a href="/admin"><i class="ti ti-shield-check"></i> 보안 모니터링</a>
611
+ <a href="/admin"><i class="ti ti-report-search"></i> 감사 로그</a>
612
+ <a href="/chat"><i class="ti ti-message-circle"></i> 채팅으로</a>
613
+ </nav>
614
+ <div class="rail-project">
615
+ <strong>admin@lattice.ai</strong>
616
+ <span>시스템 관리자</span>
617
+ </div>
618
+ </aside>
602
619
  <div class="page">
603
620
  <header class="topbar">
604
621
  <div class="brand">