ltcai 0.1.1 → 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 {
@@ -407,7 +409,7 @@
407
409
  background: rgba(255,255,255,0.04);
408
410
  }
409
411
 
410
- .pw-modal-overlay {
412
+ .acct-modal-overlay {
411
413
  display: none;
412
414
  position: fixed;
413
415
  inset: 0;
@@ -417,20 +419,31 @@
417
419
  align-items: center;
418
420
  justify-content: center;
419
421
  }
420
- .pw-modal-overlay.open { display: flex; }
421
- .pw-modal {
422
+ .acct-modal-overlay.open { display: flex; }
423
+ .acct-modal {
422
424
  background: var(--surface, #1e293b);
423
425
  border: 1px solid rgba(255,255,255,0.08);
424
426
  border-radius: 16px;
425
- padding: 28px;
426
427
  width: 100%;
427
- max-width: 360px;
428
+ max-width: 380px;
428
429
  display: flex;
429
430
  flex-direction: column;
430
- gap: 16px;
431
431
  box-shadow: 0 20px 60px rgba(0,0,0,0.5);
432
+ overflow: hidden;
432
433
  }
433
- .pw-modal h3 { font-size: 15px; font-weight: 600; margin: 0; }
434
+ .acct-tabs {
435
+ display: flex;
436
+ border-bottom: 1px solid rgba(255,255,255,0.07);
437
+ }
438
+ .acct-tab {
439
+ flex: 1; padding: 14px; font-size: 13px; font-weight: 500;
440
+ background: none; border: none; color: var(--muted); cursor: pointer;
441
+ transition: all .15s; border-bottom: 2px solid transparent;
442
+ }
443
+ .acct-tab.active { color: var(--text, #f8fafc); border-bottom-color: var(--accent, #6366f1); }
444
+ .acct-body { padding: 24px; display: flex; flex-direction: column; gap: 14px; }
445
+ .acct-tab-panel { display: none; flex-direction: column; gap: 14px; }
446
+ .acct-tab-panel.active { display: flex; }
434
447
  .pw-field { display: flex; flex-direction: column; gap: 5px; }
435
448
  .pw-field label { font-size: 11px; color: var(--muted); }
436
449
  .pw-field input {
@@ -457,6 +470,41 @@
457
470
  .pw-msg.error { color: #f87171; }
458
471
  .pw-msg.success { color: #4ade80; }
459
472
 
473
+ .lang-picker { position: relative; }
474
+ .lang-picker-menu {
475
+ display: none;
476
+ position: absolute;
477
+ top: calc(100% + 6px);
478
+ right: 0;
479
+ background: #1e293b;
480
+ border: 1px solid rgba(255,255,255,0.1);
481
+ border-radius: 10px;
482
+ overflow: hidden;
483
+ box-shadow: 0 8px 24px rgba(0,0,0,0.4);
484
+ z-index: 200;
485
+ min-width: 90px;
486
+ }
487
+ .lang-picker-menu.open { display: block; }
488
+ .lang-option {
489
+ display: flex; align-items: center; gap: 8px;
490
+ padding: 9px 14px; font-size: 13px; cursor: pointer;
491
+ color: var(--muted); transition: background .12s;
492
+ }
493
+ .lang-option:hover { background: rgba(255,255,255,0.06); color: var(--text, #f8fafc); }
494
+ .lang-option.active { color: var(--accent, #6366f1); font-weight: 600; }
495
+
496
+ .auth-lang-picker {
497
+ position: absolute; top: 20px; right: 20px;
498
+ }
499
+ .auth-lang-btn {
500
+ background: rgba(255,255,255,0.07);
501
+ border: 1px solid rgba(255,255,255,0.12);
502
+ color: #94a3b8; font-size: 12px; padding: 5px 10px;
503
+ border-radius: 8px; cursor: pointer; transition: all .15s;
504
+ }
505
+ .auth-lang-btn:hover { background: rgba(255,255,255,0.12); color: #f8fafc; }
506
+ .auth-lang-picker .lang-picker-menu { top: calc(100% + 6px); right: 0; }
507
+
460
508
  .messages-viewport {
461
509
  flex: 1;
462
510
  overflow-y: auto;
@@ -2761,34 +2809,6 @@
2761
2809
  </div>
2762
2810
  <div class="bg-grid"></div>
2763
2811
 
2764
- <div id="auth-overlay" class="auth-overlay">
2765
- <div class="auth-orb auth-orb-1"></div>
2766
- <div class="auth-orb auth-orb-2"></div>
2767
- <div class="auth-card">
2768
- <div id="login-form">
2769
- <div class="auth-logo"><i class="ti ti-brain"></i></div>
2770
- <h2 class="auth-title">Lattice AI</h2>
2771
- <p class="auth-subtitle">Local AI Workspace — Apple Silicon</p>
2772
- <input class="auth-input" type="email" id="login-email" placeholder="이메일 주소">
2773
- <input class="auth-input" type="password" id="login-pw" placeholder="비밀번호">
2774
- <button class="auth-submit" onclick="handleLogin()">로그인</button>
2775
- <p class="auth-switch">계정이 없으신가요?
2776
- <a href="#" onclick="toggleAuth(true)">회원가입</a></p>
2777
- </div>
2778
- <div id="register-form" style="display: none;">
2779
- <div class="auth-logo"><i class="ti ti-user-plus"></i></div>
2780
- <h2 class="auth-title">계정 만들기</h2>
2781
- <p class="auth-subtitle">Lattice AI 워크스페이스에 참여하세요</p>
2782
- <input class="auth-input" type="email" id="reg-email" placeholder="이메일 주소">
2783
- <input class="auth-input" type="password" id="reg-pw" placeholder="비밀번호">
2784
- <input class="auth-input" type="text" id="reg-name" placeholder="이름">
2785
- <input class="auth-input" type="text" id="reg-nickname" placeholder="별명">
2786
- <button class="auth-submit" onclick="handleRegister()">가입하기</button>
2787
- <p class="auth-switch">이미 계정이 있나요?
2788
- <a href="#" onclick="toggleAuth(false)">로그인</a></p>
2789
- </div>
2790
- </div>
2791
- </div>
2792
2812
 
2793
2813
  <div class="app-layout">
2794
2814
  <!-- Sidebar -->
@@ -2811,9 +2831,9 @@
2811
2831
  <!-- History items -->
2812
2832
  </div>
2813
2833
  <div class="sidebar-footer">
2814
- <button id="admin-btn" class="admin-btn" onclick="openAdminPanel()"><i class="ti ti-shield-lock"></i> 관리자 대시보드</button>
2815
- <button class="status-btn" onclick="openStatusPanel()"><i class="ti ti-info-circle"></i> 상태 보기</button>
2816
- <button id="setup-wizard-btn" class="setup-wizard-sidebar-btn" onclick="openSetupWizard()"><i class="ti ti-sparkles"></i> 자동 설정</button>
2834
+ <button id="admin-btn" class="admin-btn" onclick="openAdminPanel()"><i class="ti ti-shield-lock"></i> <span data-i18n="admin_dashboard">관리자 대시보드</span></button>
2835
+ <button class="status-btn" onclick="openStatusPanel()"><i class="ti ti-info-circle"></i> <span data-i18n="my_status">내 상태 보기</span></button>
2836
+ <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>
2817
2837
  <button id="new-chat-btn" class="new-chat-btn"><i class="ti ti-plus"></i> New Chat</button>
2818
2838
  </div>
2819
2839
  </aside>
@@ -2832,30 +2852,59 @@
2832
2852
  </div>
2833
2853
  <div class="header-pills">
2834
2854
  <div class="status-pill"><i class="ti ti-device-desktop"></i> Local</div>
2835
- <button onclick="openPwModal()" class="logout-btn" title="비밀번호 변경"><i class="ti ti-user"></i></button>
2836
- <button onclick="logout()" class="logout-btn">로그아웃</button>
2855
+ <div class="lang-picker" id="header-lang-picker">
2856
+ <button class="logout-btn" id="lang-btn" onclick="toggleLangMenu('header-lang-picker')" title="Language">🌐</button>
2857
+ <div class="lang-picker-menu" id="header-lang-picker-menu">
2858
+ <div class="lang-option" id="header-lang-ko" onclick="setLang('ko')">🇰🇷 한국어</div>
2859
+ <div class="lang-option" id="header-lang-en" onclick="setLang('en')">🇺🇸 English</div>
2860
+ </div>
2861
+ </div>
2862
+ <button onclick="openAcctModal()" class="logout-btn" title="계정 설정"><i class="ti ti-user"></i></button>
2863
+ <button onclick="logout()" class="logout-btn" data-i18n="logout">로그아웃</button>
2837
2864
  </div>
2838
2865
  </header>
2839
2866
 
2840
- <div class="pw-modal-overlay" id="pw-modal-overlay">
2841
- <div class="pw-modal">
2842
- <h3>🔐 비밀번호 변경</h3>
2843
- <div class="pw-field">
2844
- <label>현재 비밀번호</label>
2845
- <input type="password" id="pw-cur" placeholder="현재 비밀번호">
2867
+ <div class="acct-modal-overlay" id="acct-modal-overlay">
2868
+ <div class="acct-modal">
2869
+ <div class="acct-tabs">
2870
+ <button class="acct-tab active" id="tab-profile" onclick="switchAcctTab('profile')" data-i18n="tab_profile">프로필</button>
2871
+ <button class="acct-tab" id="tab-password" onclick="switchAcctTab('password')" data-i18n="tab_password">비밀번호</button>
2846
2872
  </div>
2847
- <div class="pw-field">
2848
- <label>새 비밀번호</label>
2849
- <input type="password" id="pw-new" placeholder="새 비밀번호 (4자 이상)">
2850
- </div>
2851
- <div class="pw-field">
2852
- <label>새 비밀번호 확인</label>
2853
- <input type="password" id="pw-new2" placeholder="새 비밀번호 재입력">
2854
- </div>
2855
- <div class="pw-msg" id="pw-msg"></div>
2856
- <div class="pw-actions">
2857
- <button class="pw-cancel" onclick="closePwModal()">취소</button>
2858
- <button class="pw-submit" id="pw-submit-btn" onclick="submitPwChange()">변경</button>
2873
+ <div class="acct-body">
2874
+ <div class="acct-tab-panel active" id="panel-profile">
2875
+ <div class="pw-field">
2876
+ <label data-i18n="label_name">이름</label>
2877
+ <input type="text" id="profile-name" placeholder="이름" data-i18n-ph="ph_name">
2878
+ </div>
2879
+ <div class="pw-field">
2880
+ <label data-i18n="label_nickname">닉네임</label>
2881
+ <input type="text" id="profile-nickname" placeholder="닉네임" data-i18n-ph="ph_nickname">
2882
+ </div>
2883
+ <div class="pw-msg" id="profile-msg"></div>
2884
+ <div class="pw-actions">
2885
+ <button class="pw-cancel" onclick="closeAcctModal()" data-i18n="btn_cancel">취소</button>
2886
+ <button class="pw-submit" id="profile-submit-btn" onclick="submitProfileChange()" data-i18n="btn_save">저장</button>
2887
+ </div>
2888
+ </div>
2889
+ <div class="acct-tab-panel" id="panel-password">
2890
+ <div class="pw-field">
2891
+ <label data-i18n="label_cur_pw">현재 비밀번호</label>
2892
+ <input type="password" id="pw-cur" placeholder="현재 비밀번호" data-i18n-ph="ph_cur_pw">
2893
+ </div>
2894
+ <div class="pw-field">
2895
+ <label data-i18n="label_new_pw">새 비밀번호</label>
2896
+ <input type="password" id="pw-new" placeholder="새 비밀번호 (4자 이상)" data-i18n-ph="ph_new_pw">
2897
+ </div>
2898
+ <div class="pw-field">
2899
+ <label data-i18n="label_new_pw2">새 비밀번호 확인</label>
2900
+ <input type="password" id="pw-new2" placeholder="새 비밀번호 재입력" data-i18n-ph="ph_new_pw2">
2901
+ </div>
2902
+ <div class="pw-msg" id="pw-msg"></div>
2903
+ <div class="pw-actions">
2904
+ <button class="pw-cancel" onclick="closeAcctModal()" data-i18n="btn_cancel">취소</button>
2905
+ <button class="pw-submit" id="pw-submit-btn" onclick="submitPwChange()" data-i18n="btn_change">변경</button>
2906
+ </div>
2907
+ </div>
2859
2908
  </div>
2860
2909
  </div>
2861
2910
  </div>
@@ -2872,16 +2921,16 @@
2872
2921
  <div class="ops-card interactive" onclick="openVpcPanel()">
2873
2922
  <div>
2874
2923
  <div class="ops-label">PRIVATE VPC</div>
2875
- <div id="ops-vpc" class="ops-value">설정 안 됨</div>
2876
- <div id="ops-vpc-meta" class="ops-meta">클릭하여 VPC 연결 설정</div>
2924
+ <div id="ops-vpc" class="ops-value" data-i18n="vpc_not_set">설정 안 됨</div>
2925
+ <div id="ops-vpc-meta" class="ops-meta" data-i18n="vpc_click_to_set">클릭하여 VPC 연결 설정</div>
2877
2926
  </div>
2878
2927
  <div class="ops-icon"><i class="ti ti-cloud-lock"></i></div>
2879
2928
  </div>
2880
2929
  <div class="ops-card interactive" onclick="openAdminPanel()">
2881
2930
  <div>
2882
2931
  <div class="ops-label">SECURITY</div>
2883
- <div class="ops-value">민감정보 감시</div>
2884
- <div id="security-admin-meta" class="ops-meta">관리자 대시보드 접근</div>
2932
+ <div class="ops-value" data-i18n="security_monitor">민감정보 감시</div>
2933
+ <div id="security-admin-meta" class="ops-meta" data-i18n="admin_dashboard_access">관리자 대시보드 접근</div>
2885
2934
  </div>
2886
2935
  <div class="ops-icon"><i class="ti ti-shield-check"></i></div>
2887
2936
  </div>
@@ -2890,12 +2939,12 @@
2890
2939
  <div class="messages-viewport" id="chat-viewport">
2891
2940
  <div class="empty-state" id="empty-state">
2892
2941
  <div style="width:64px;height:64px;background:linear-gradient(135deg,rgba(34,211,160,0.18),rgba(129,140,248,0.12));border:1px solid rgba(34,211,160,0.18);border-radius:18px;display:flex;align-items:center;justify-content:center;font-size:28px;color:var(--accent);margin:0 auto 18px;box-shadow:0 0 32px rgba(34,211,160,0.14)"><i class="ti ti-sparkles"></i></div>
2893
- <h1>무엇을 만들까요?</h1>
2894
- <p>로컬 모델, 이미지 분석, 코드 생성, 프라이빗 VPC — 모든 걸 한 화면에서 이어가세요.</p>
2942
+ <h1 data-i18n="empty_title">무엇을 만들까요?</h1>
2943
+ <p data-i18n="empty_sub">로컬 모델, 이미지 분석, 코드 생성, 프라이빗 VPC — 모든 걸 한 화면에서 이어가세요.</p>
2895
2944
  <div class="empty-grid">
2896
- <div class="empty-chip" onclick="document.getElementById('user-input').value='보고서 초안을 만들어줘';document.getElementById('user-input').focus()"><span class="empty-chip-icon"><i class="ti ti-file-text"></i></span>파일 생성 · 코드 초안</div>
2897
- <div class="empty-chip" onclick="document.getElementById('user-input').value='VPC 보안 구성을 점검해줘';document.getElementById('user-input').focus()"><span class="empty-chip-icon"><i class="ti ti-shield-check"></i></span>VPC 보안 구성 점검</div>
2898
- <div class="empty-chip" onclick="document.getElementById('user-input').value='이 내용을 지식베이스에 정리해줘';document.getElementById('user-input').focus()"><span class="empty-chip-icon"><i class="ti ti-brain"></i></span>로컬 지식 정리</div>
2945
+ <div class="empty-chip" id="chip-file" onclick="document.getElementById('user-input').value=t('chip_file_prompt');document.getElementById('user-input').focus()"><span class="empty-chip-icon"><i class="ti ti-file-text"></i></span><span data-i18n="chip_file">파일 생성 · 코드 초안</span></div>
2946
+ <div class="empty-chip" id="chip-vpc" onclick="document.getElementById('user-input').value=t('chip_vpc_prompt');document.getElementById('user-input').focus()"><span class="empty-chip-icon"><i class="ti ti-shield-check"></i></span><span data-i18n="chip_vpc">VPC 보안 구성 점검</span></div>
2947
+ <div class="empty-chip" id="chip-kb" onclick="document.getElementById('user-input').value=t('chip_kb_prompt');document.getElementById('user-input').focus()"><span class="empty-chip-icon"><i class="ti ti-brain"></i></span><span data-i18n="chip_kb">로컬 지식 정리</span></div>
2899
2948
  </div>
2900
2949
  </div>
2901
2950
  </div>
@@ -2918,16 +2967,16 @@
2918
2967
  <i class="ti ti-paperclip" style="font-size: 20px;"></i>
2919
2968
  <input type="file" id="doc-input" accept=".pdf,.docx,.xlsx,.pptx,.txt,.md,.csv" hidden onchange="attachDocument(this)">
2920
2969
  </label>
2921
- <textarea id="user-input" placeholder="Lattice AI에게 작업을 지시하세요..." rows="1"></textarea>
2970
+ <textarea id="user-input" placeholder="Lattice AI에게 작업을 지시하세요..." rows="1" data-i18n-ph="ph_input"></textarea>
2922
2971
  <button class="send-btn" id="send-btn"><i class="ti ti-send"></i></button>
2923
2972
  </div>
2924
2973
  <div class="file-toolbar">
2925
- <span class="file-toolbar-label">파일 만들기</span>
2974
+ <span class="file-toolbar-label" data-i18n="create_file">파일 만들기</span>
2926
2975
  <button class="file-type-btn" onclick="openFileCreate('docx')"><i class="ti ti-file-word"></i> DOCX</button>
2927
2976
  <button class="file-type-btn" onclick="openFileCreate('xlsx')"><i class="ti ti-file-spreadsheet"></i> XLSX</button>
2928
2977
  <button class="file-type-btn" onclick="openFileCreate('pptx')"><i class="ti ti-presentation"></i> PPTX</button>
2929
2978
  <button class="file-type-btn" onclick="openFileCreate('pdf')"><i class="ti ti-file-type-pdf"></i> PDF</button>
2930
- <button class="file-type-btn" onclick="openLocalBrowser()"><i class="ti ti-folder-open"></i> 로컬 파일</button>
2979
+ <button class="file-type-btn" onclick="openLocalBrowser()"><i class="ti ti-folder-open"></i> <span data-i18n="local_files">로컬 파일</span></button>
2931
2980
  </div>
2932
2981
  </div>
2933
2982
  </div>
@@ -2938,8 +2987,8 @@
2938
2987
  <section class="model-panel">
2939
2988
  <div class="model-panel-header">
2940
2989
  <div>
2941
- <h2>모델 스위처</h2>
2942
- <p style="color: var(--muted); font-size: 12px; margin-top: 4px;">실행 엔진을 설치하고, 엔진에 맞는 local/cloud LLM을 선택합니다.</p>
2990
+ <h2 data-i18n="model_switcher">모델 스위처</h2>
2991
+ <p style="color: var(--muted); font-size: 12px; margin-top: 4px;" data-i18n="model_switcher_sub">실행 엔진을 설치하고, 엔진에 맞는 local/cloud LLM을 선택합니다.</p>
2943
2992
  </div>
2944
2993
  <button class="admin-close" onclick="closeModelPanel()"><i class="ti ti-x"></i></button>
2945
2994
  </div>
@@ -2951,12 +3000,12 @@
2951
3000
  <div id="perm-overlay" class="perm-overlay" style="display:none">
2952
3001
  <div class="perm-dialog">
2953
3002
  <div class="perm-icon"><i class="ti ti-shield-lock"></i></div>
2954
- <div class="perm-title" id="perm-title">파일 접근 요청</div>
3003
+ <div class="perm-title" id="perm-title" data-i18n="perm_title">파일 접근 요청</div>
2955
3004
  <div class="perm-path" id="perm-path"></div>
2956
3005
  <div class="perm-desc" id="perm-desc"></div>
2957
3006
  <div class="perm-actions">
2958
- <button class="perm-deny-btn" onclick="resolvePermission(false)">거부</button>
2959
- <button class="perm-allow-btn" onclick="resolvePermission(true)">허용</button>
3007
+ <button class="perm-deny-btn" onclick="resolvePermission(false)" data-i18n="btn_deny">거부</button>
3008
+ <button class="perm-allow-btn" onclick="resolvePermission(true)" data-i18n="btn_allow">허용</button>
2960
3009
  </div>
2961
3010
  </div>
2962
3011
  </div>
@@ -3267,7 +3316,6 @@
3267
3316
  if (typeof acquireVsCodeApi === 'function') {
3268
3317
  vscode = acquireVsCodeApi();
3269
3318
  console.log("VS Code API Acquired");
3270
- document.getElementById('auth-overlay').style.display = 'none'; // VS Code에서는 로그인 건너뜀
3271
3319
  }
3272
3320
  } catch (e) { /* Not in VS Code */ }
3273
3321
 
@@ -3279,79 +3327,205 @@
3279
3327
  }
3280
3328
  });
3281
3329
 
3282
- function toggleAuth(showRegister) {
3283
- document.getElementById('login-form').style.display = showRegister ? 'none' : 'block';
3284
- document.getElementById('register-form').style.display = showRegister ? 'block' : 'none';
3285
- }
3330
+ async function logout() {
3331
+ try { await apiFetch('/logout', { method: 'POST' }); } catch (_) {}
3332
+ localStorage.removeItem('ltcai_user_email');
3333
+ localStorage.removeItem('ltcai_user_nickname');
3334
+ localStorage.removeItem('ltcai_is_admin');
3335
+ window.location.href = '/account';
3336
+ }
3337
+
3338
+ const I18N = {
3339
+ ko: {
3340
+ // 인증
3341
+ login_title: 'Lattice AI', login_sub: 'Local AI Workspace — Apple Silicon',
3342
+ ph_email: '이메일 주소', ph_password: '비밀번호', btn_login: '로그인',
3343
+ no_account: '계정이 없으신가요?', go_register: '회원가입',
3344
+ register_title: '계정 만들기', register_sub: 'Lattice AI 워크스페이스에 참여하세요',
3345
+ ph_new_pw: '비밀번호 (4자 이상)', ph_pw_confirm: '비밀번호 확인',
3346
+ ph_fullname: '이름', ph_nick: '닉네임',
3347
+ btn_register: '가입하기', have_account: '이미 계정이 있나요?', go_login: '로그인',
3348
+ // 헤더 / 사이드바
3349
+ logout: '로그아웃', admin_dashboard: '관리자 대시보드',
3350
+ my_status: '내 상태 보기', auto_setup: '자동 설정',
3351
+ // 계정 모달
3352
+ tab_profile: '프로필', tab_password: '비밀번호',
3353
+ label_name: '이름', label_nickname: '닉네임',
3354
+ label_cur_pw: '현재 비밀번호', label_new_pw: '새 비밀번호', label_new_pw2: '새 비밀번호 확인',
3355
+ ph_name: '이름', ph_nickname: '닉네임', ph_cur_pw: '현재 비밀번호',
3356
+ ph_new_pw2: '새 비밀번호 재입력',
3357
+ btn_save: '저장', btn_change: '변경', btn_cancel: '취소',
3358
+ // ops 스트립
3359
+ vpc_not_set: '설정 안 됨', vpc_click_to_set: '클릭하여 VPC 연결 설정',
3360
+ security_monitor: '민감정보 감시', admin_dashboard_access: '관리자 대시보드 접근', admin_has_rights: '관리자 권한 있음',
3361
+ // 빈 화면
3362
+ empty_title: '무엇을 만들까요?',
3363
+ empty_sub: '로컬 모델, 이미지 분석, 코드 생성, 프라이빗 VPC — 모든 걸 한 화면에서 이어가세요.',
3364
+ chip_file: '파일 생성 · 코드 초안', chip_vpc: 'VPC 보안 구성 점검', chip_kb: '로컬 지식 정리',
3365
+ chip_file_prompt: '보고서 초안을 만들어줘',
3366
+ chip_vpc_prompt: 'VPC 보안 구성을 점검해줘',
3367
+ chip_kb_prompt: '이 내용을 지식베이스에 정리해줘',
3368
+ // 입력창
3369
+ ph_input: 'Lattice AI에게 작업을 지시하세요...',
3370
+ // 파일 툴바
3371
+ create_file: '파일 만들기', local_files: '로컬 파일',
3372
+ // 패널 제목
3373
+ model_switcher: '모델 스위처',
3374
+ model_switcher_sub: '실행 엔진을 설치하고, 엔진에 맞는 local/cloud LLM을 선택합니다.',
3375
+ // 권한 다이얼로그
3376
+ perm_title: '파일 접근 요청', btn_deny: '거부', btn_allow: '허용',
3377
+ },
3378
+ en: {
3379
+ // Auth
3380
+ login_title: 'Lattice AI', login_sub: 'Local AI Workspace — Apple Silicon',
3381
+ ph_email: 'Email address', ph_password: 'Password', btn_login: 'Log in',
3382
+ no_account: "Don't have an account?", go_register: 'Sign up',
3383
+ register_title: 'Create Account', register_sub: 'Join the Lattice AI workspace',
3384
+ ph_new_pw: 'Password (min. 4 chars)', ph_pw_confirm: 'Confirm password',
3385
+ ph_fullname: 'Full name', ph_nick: 'Nickname',
3386
+ btn_register: 'Sign up', have_account: 'Already have an account?', go_login: 'Log in',
3387
+ // Header / Sidebar
3388
+ logout: 'Logout', admin_dashboard: 'Admin Dashboard',
3389
+ my_status: 'My Status', auto_setup: 'Auto Setup',
3390
+ // Account modal
3391
+ tab_profile: 'Profile', tab_password: 'Password',
3392
+ label_name: 'Name', label_nickname: 'Nickname',
3393
+ label_cur_pw: 'Current Password', label_new_pw: 'New Password', label_new_pw2: 'Confirm New Password',
3394
+ ph_name: 'Name', ph_nickname: 'Nickname', ph_cur_pw: 'Current password',
3395
+ ph_new_pw2: 'Confirm new password',
3396
+ btn_save: 'Save', btn_change: 'Change', btn_cancel: 'Cancel',
3397
+ // Ops strip
3398
+ vpc_not_set: 'Not configured', vpc_click_to_set: 'Click to set up VPC',
3399
+ security_monitor: 'Sensitive data monitor', admin_dashboard_access: 'Admin dashboard access', admin_has_rights: 'Has admin rights',
3400
+ // Empty state
3401
+ empty_title: 'What would you like to build?',
3402
+ empty_sub: 'Local models, image analysis, code generation, private VPC — all in one workspace.',
3403
+ chip_file: 'Create file · Code draft', chip_vpc: 'Review VPC security', chip_kb: 'Organize knowledge',
3404
+ chip_file_prompt: 'Draft a report for me',
3405
+ chip_vpc_prompt: 'Review my VPC security configuration',
3406
+ chip_kb_prompt: 'Organize this into my knowledge base',
3407
+ // Input
3408
+ ph_input: 'Ask Lattice AI anything...',
3409
+ // File toolbar
3410
+ create_file: 'Create file', local_files: 'Local files',
3411
+ // Panel titles
3412
+ model_switcher: 'Model Switcher',
3413
+ model_switcher_sub: 'Install a runtime engine and select a local/cloud LLM.',
3414
+ // Permission dialog
3415
+ perm_title: 'File Access Request', btn_deny: 'Deny', btn_allow: 'Allow',
3416
+ }
3417
+ };
3418
+ let currentLang = localStorage.getItem('ltcai_lang') || 'ko';
3286
3419
 
3287
- async function handleRegister() {
3288
- const email = document.getElementById('reg-email').value;
3289
- const password = document.getElementById('reg-pw').value;
3290
- const name = document.getElementById('reg-name').value;
3291
- const nickname = document.getElementById('reg-nickname').value;
3292
- const res = await apiFetch('/register', {
3293
- method: 'POST',
3294
- headers: { 'Content-Type': 'application/json' },
3295
- body: JSON.stringify({ email, password, name, nickname })
3296
- });
3297
- if (res.ok) { alert("가입 완료! 로그인 해주세요."); toggleAuth(false); }
3298
- else { alert("가입 실패: " + (await res.json()).detail); }
3299
- }
3420
+ function t(key) { return (I18N[currentLang] || I18N.ko)[key] || key; }
3300
3421
 
3301
- async function handleLogin() {
3302
- const email = document.getElementById('login-email').value;
3303
- const password = document.getElementById('login-pw').value;
3304
- const res = await apiFetch('/login', {
3305
- method: 'POST',
3306
- headers: { 'Content-Type': 'application/json' },
3307
- body: JSON.stringify({ email, password })
3422
+ function applyI18n() {
3423
+ document.querySelectorAll('[data-i18n]').forEach(el => {
3424
+ el.textContent = t(el.dataset.i18n);
3308
3425
  });
3309
- if (res.ok) {
3310
- const data = await res.json();
3311
- currentUserNickname = data.nickname;
3312
- currentUserEmail = data.email;
3313
- localStorage.setItem('ltcai_user_email', currentUserEmail);
3314
- localStorage.setItem('ltcai_user_nickname', currentUserNickname);
3315
- localStorage.setItem('ltcai_is_admin', data.is_admin ? 'true' : 'false');
3316
- isAdmin = Boolean(data.is_admin);
3317
- document.getElementById('user-nickname-display').innerText = currentUserNickname;
3318
- const av = document.getElementById('user-avatar-initial');
3319
- if (av) av.textContent = (currentUserNickname || 'G')[0].toUpperCase();
3320
- document.getElementById('admin-btn').style.display = 'flex';
3321
- document.getElementById('security-admin-meta').textContent = isAdmin ? '관리자 권한 있음' : '관리자 대시보드 접근';
3322
- document.getElementById('auth-overlay').style.display = 'none';
3323
- loadVpcStatus();
3324
- restoreCurrentConversation().then(() => {
3325
- if (!chatViewport.querySelector('.message')) {
3326
- addMessage('ai', `반갑습니다, <b>${currentUserNickname}</b>님!`);
3327
- }
3426
+ document.querySelectorAll('[data-i18n-ph]').forEach(el => {
3427
+ el.placeholder = t(el.dataset.i18nPh);
3428
+ });
3429
+ // 언어 선택기 active 표시 업데이트
3430
+ ['auth', 'header'].forEach(prefix => {
3431
+ ['ko', 'en'].forEach(lang => {
3432
+ const el = document.getElementById(`${prefix}-lang-${lang}`);
3433
+ if (el) el.classList.toggle('active', lang === currentLang);
3328
3434
  });
3329
- } else { alert("로그인 실패!"); }
3435
+ });
3436
+ const authBtn = document.getElementById('auth-lang-btn');
3437
+ if (authBtn) authBtn.textContent = `🌐 ${currentLang === 'ko' ? '한국어' : 'English'}`;
3330
3438
  }
3331
3439
 
3332
- function logout() {
3333
- localStorage.removeItem('ltcai_user_email');
3334
- localStorage.removeItem('ltcai_user_nickname');
3335
- localStorage.removeItem('ltcai_is_admin');
3336
- location.reload();
3440
+ function toggleLangMenu(pickerId) {
3441
+ const menu = document.getElementById(`${pickerId}-menu`);
3442
+ if (!menu) return;
3443
+ const isOpen = menu.classList.contains('open');
3444
+ document.querySelectorAll('.lang-picker-menu').forEach(m => m.classList.remove('open'));
3445
+ if (!isOpen) menu.classList.add('open');
3337
3446
  }
3338
3447
 
3339
- function openPwModal() {
3340
- document.getElementById('pw-cur').value = '';
3341
- document.getElementById('pw-new').value = '';
3342
- document.getElementById('pw-new2').value = '';
3343
- const msg = document.getElementById('pw-msg');
3344
- msg.textContent = '';
3345
- msg.className = 'pw-msg';
3346
- document.getElementById('pw-modal-overlay').classList.add('open');
3448
+ function setLang(lang) {
3449
+ currentLang = lang;
3450
+ localStorage.setItem('ltcai_lang', lang);
3451
+ document.querySelectorAll('.lang-picker-menu').forEach(m => m.classList.remove('open'));
3452
+ applyI18n();
3347
3453
  }
3348
- function closePwModal() {
3349
- document.getElementById('pw-modal-overlay').classList.remove('open');
3454
+
3455
+ document.addEventListener('click', (e) => {
3456
+ if (!e.target.closest('.lang-picker')) {
3457
+ document.querySelectorAll('.lang-picker-menu').forEach(m => m.classList.remove('open'));
3458
+ }
3459
+ });
3460
+
3461
+ function switchAcctTab(tab) {
3462
+ ['profile', 'password'].forEach(t => {
3463
+ document.getElementById(`tab-${t}`).classList.toggle('active', t === tab);
3464
+ document.getElementById(`panel-${t}`).classList.toggle('active', t === tab);
3465
+ });
3466
+ }
3467
+ async function openAcctModal() {
3468
+ ['profile-msg', 'pw-msg'].forEach(id => {
3469
+ const el = document.getElementById(id);
3470
+ el.textContent = ''; el.className = 'pw-msg';
3471
+ });
3472
+ ['pw-cur', 'pw-new', 'pw-new2'].forEach(id => document.getElementById(id).value = '');
3473
+ switchAcctTab('profile');
3474
+ try {
3475
+ const res = await fetch('/account/profile');
3476
+ if (res.ok) {
3477
+ const data = await res.json();
3478
+ document.getElementById('profile-name').value = data.name || '';
3479
+ document.getElementById('profile-nickname').value = data.nickname || '';
3480
+ }
3481
+ } catch {}
3482
+ document.getElementById('acct-modal-overlay').classList.add('open');
3483
+ }
3484
+ function closeAcctModal() {
3485
+ document.getElementById('acct-modal-overlay').classList.remove('open');
3350
3486
  }
3351
3487
  document.addEventListener('click', (e) => {
3352
- const overlay = document.getElementById('pw-modal-overlay');
3353
- if (e.target === overlay) closePwModal();
3488
+ const overlay = document.getElementById('acct-modal-overlay');
3489
+ if (e.target === overlay) closeAcctModal();
3354
3490
  });
3491
+ async function submitProfileChange() {
3492
+ const name = document.getElementById('profile-name').value.trim();
3493
+ const nickname = document.getElementById('profile-nickname').value.trim();
3494
+ const msg = document.getElementById('profile-msg');
3495
+ const btn = document.getElementById('profile-submit-btn');
3496
+ if (!name || !nickname) {
3497
+ msg.textContent = '이름과 닉네임을 입력해주세요.';
3498
+ msg.className = 'pw-msg error';
3499
+ return;
3500
+ }
3501
+ btn.disabled = true; btn.textContent = '저장 중...';
3502
+ try {
3503
+ const res = await fetch('/account/profile', {
3504
+ method: 'PATCH',
3505
+ headers: { 'Content-Type': 'application/json' },
3506
+ body: JSON.stringify({ name, nickname })
3507
+ });
3508
+ const data = await res.json();
3509
+ if (res.ok) {
3510
+ currentUserNickname = data.nickname;
3511
+ localStorage.setItem('ltcai_user_nickname', data.nickname);
3512
+ document.getElementById('user-nickname-display').innerText = data.nickname;
3513
+ const av = document.getElementById('user-avatar-initial');
3514
+ if (av) av.textContent = (data.nickname || 'G')[0].toUpperCase();
3515
+ msg.textContent = '✅ 프로필이 변경되었습니다.';
3516
+ msg.className = 'pw-msg success';
3517
+ setTimeout(closeAcctModal, 1500);
3518
+ } else {
3519
+ msg.textContent = data.detail || '저장 실패';
3520
+ msg.className = 'pw-msg error';
3521
+ }
3522
+ } catch {
3523
+ msg.textContent = '서버 연결 실패';
3524
+ msg.className = 'pw-msg error';
3525
+ } finally {
3526
+ btn.disabled = false; btn.textContent = '저장';
3527
+ }
3528
+ }
3355
3529
  async function submitPwChange() {
3356
3530
  const cur = document.getElementById('pw-cur').value;
3357
3531
  const nw = document.getElementById('pw-new').value;
@@ -3373,8 +3547,7 @@
3373
3547
  msg.className = 'pw-msg error';
3374
3548
  return;
3375
3549
  }
3376
- btn.disabled = true;
3377
- btn.textContent = '변경 중...';
3550
+ btn.disabled = true; btn.textContent = '변경 중...';
3378
3551
  try {
3379
3552
  const res = await fetch('/account/change-password', {
3380
3553
  method: 'POST',
@@ -3385,7 +3558,7 @@
3385
3558
  if (res.ok) {
3386
3559
  msg.textContent = '✅ 비밀번호가 변경되었습니다.';
3387
3560
  msg.className = 'pw-msg success';
3388
- setTimeout(closePwModal, 1500);
3561
+ setTimeout(closeAcctModal, 1500);
3389
3562
  } else {
3390
3563
  msg.textContent = data.detail || '변경 실패';
3391
3564
  msg.className = 'pw-msg error';
@@ -3394,8 +3567,7 @@
3394
3567
  msg.textContent = '서버 연결 실패';
3395
3568
  msg.className = 'pw-msg error';
3396
3569
  } finally {
3397
- btn.disabled = false;
3398
- btn.textContent = '변경';
3570
+ btn.disabled = false; btn.textContent = '변경';
3399
3571
  }
3400
3572
  }
3401
3573
 
@@ -3775,9 +3947,27 @@
3775
3947
  }
3776
3948
 
3777
3949
  async function openAdminPanel() {
3950
+ if (!isAdmin) {
3951
+ showToast(currentLang === 'ko' ? '관리자 권한이 없습니다.' : 'Admin access required.');
3952
+ return;
3953
+ }
3778
3954
  window.location.href = '/admin';
3779
3955
  }
3780
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
+
3781
3971
  function closeAdminPanel() {
3782
3972
  document.getElementById('admin-overlay').style.display = 'none';
3783
3973
  }
@@ -5269,6 +5459,33 @@
5269
5459
  window.addEventListener('focus', tryClipboardReadFallback);
5270
5460
 
5271
5461
  document.getElementById('new-chat-btn').onclick = startNewChat;
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
+
5272
5489
  loadModelStatus();
5273
5490
  loadVpcStatus();
5274
5491
  restoreCurrentConversation();