ltcai 0.1.28 → 0.1.30

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.
@@ -0,0 +1,230 @@
1
+ /* Lattice AI — account.html scripts */
2
+
3
+ const API_BASE = window.location.protocol === 'file:' ? 'http://localhost:4825' : '';
4
+ function apiFetch(path, opts = {}) {
5
+ const headers = { ...(opts.headers || {}) };
6
+ return fetch(API_BASE + path, { credentials: 'include', ...opts, headers });
7
+ }
8
+
9
+ // ── i18n ──────────────────────────────────────────────
10
+ const I18N = {
11
+ ko: {
12
+ login_title: 'Lattice AI', login_sub: '내 PC에서 시작하는<br>개인 AI 워크스페이스',
13
+ ph_email: '이메일 주소', ph_pw: '비밀번호', ph_new_pw: '비밀번호 (4자 이상)',
14
+ ph_pw_confirm: '비밀번호 확인', ph_name: '이름', ph_nick: '닉네임',
15
+ btn_login: '로그인', btn_register: '가입하기',
16
+ no_account: '계정이 없으신가요?', go_register: '회원가입',
17
+ have_account: '이미 계정이 있나요?', go_login: '로그인',
18
+ reg_title: '계정 만들기', reg_sub: 'Lattice AI 워크스페이스에 참여하세요',
19
+ err_pw_mismatch: '비밀번호가 일치하지 않습니다.',
20
+ err_fill: '모든 항목을 입력해주세요.',
21
+ err_login_fail: '이메일 또는 비밀번호가 틀렸습니다.',
22
+ err_server: '서버 연결 실패',
23
+ sso_divider: '조직 계정으로 로그인', sso_btn: '로 로그인',
24
+ ms_sso: 'Microsoft Entra ID로 계속하기', okta_sso: 'Okta SSO로 계속하기',
25
+ local_start: '로컬 계정으로 시작', help: '도움말', privacy: '개인정보 처리방침',
26
+ language_btn: '🌐 한국어',
27
+ sso_unavailable: 'SSO가 아직 설정되지 않았습니다. 로컬 계정으로 시작하거나 관리자에게 문의하세요.',
28
+ },
29
+ en: {
30
+ login_title: 'Lattice AI', login_sub: 'Your personal AI workspace<br>starts on this PC',
31
+ ph_email: 'Email address', ph_pw: 'Password', ph_new_pw: 'Password (min. 4 chars)',
32
+ ph_pw_confirm: 'Confirm password', ph_name: 'Full name', ph_nick: 'Nickname',
33
+ btn_login: 'Log in', btn_register: 'Sign up',
34
+ no_account: "Don't have an account?", go_register: 'Sign up',
35
+ have_account: 'Already have an account?', go_login: 'Log in',
36
+ reg_title: 'Create Account', reg_sub: 'Join the Lattice AI workspace',
37
+ err_pw_mismatch: 'Passwords do not match.',
38
+ err_fill: 'Please fill in all fields.',
39
+ err_login_fail: 'Invalid email or password.',
40
+ err_server: 'Server connection failed',
41
+ sso_divider: 'Sign in with organization account', sso_btn: 'Sign in with',
42
+ ms_sso: 'Continue with Microsoft Entra ID', okta_sso: 'Continue with Okta SSO',
43
+ local_start: 'Start with a local account', help: 'Help', privacy: 'Privacy Policy',
44
+ language_btn: '🌐 English',
45
+ sso_unavailable: 'SSO is not configured yet. Start with a local account or contact your administrator.',
46
+ }
47
+ };
48
+
49
+ let lang = localStorage.getItem('ltcai_lang') || 'ko';
50
+ function t(k) { return (I18N[lang] || I18N.ko)[k] || k; }
51
+
52
+ function applyI18n() {
53
+ document.documentElement.lang = lang;
54
+ document.getElementById('lang-btn').textContent = t('language_btn');
55
+ document.getElementById('login-title').textContent = t('login_title');
56
+ document.getElementById('login-sub').innerHTML = t('login_sub');
57
+ document.getElementById('reg-title').textContent = t('reg_title');
58
+ document.getElementById('reg-sub').textContent = t('reg_sub');
59
+ document.getElementById('login-btn').textContent = t('btn_login');
60
+ document.getElementById('reg-btn').textContent = t('btn_register');
61
+ document.getElementById('go-register-link').textContent = t('go_register');
62
+ document.getElementById('have-account-text').textContent = t('have_account');
63
+ document.getElementById('go-login-link').textContent = t('go_login');
64
+ document.getElementById('login-email').placeholder = t('ph_email');
65
+ document.getElementById('login-pw').placeholder = t('ph_pw');
66
+ document.getElementById('reg-email').placeholder = t('ph_email');
67
+ document.getElementById('reg-pw').placeholder = t('ph_new_pw');
68
+ document.getElementById('reg-pw2').placeholder = t('ph_pw_confirm');
69
+ document.getElementById('reg-name').placeholder = t('ph_name');
70
+ document.getElementById('reg-nick').placeholder = t('ph_nick');
71
+ document.getElementById('sso-divider-text').textContent = t('sso_divider');
72
+ document.getElementById('sso-ms-label').textContent = t('ms_sso');
73
+ document.getElementById('sso-okta-label').textContent = t('okta_sso');
74
+ document.getElementById('local-start-label').textContent = t('local_start');
75
+ document.getElementById('help-link').textContent = t('help');
76
+ document.getElementById('privacy-link').textContent = t('privacy');
77
+ ['ko', 'en'].forEach(l => {
78
+ const el = document.getElementById(`opt-${l}`);
79
+ if (el) el.classList.toggle('active', l === lang);
80
+ });
81
+ }
82
+
83
+ async function initSSO() {
84
+ try {
85
+ const res = await apiFetch('/auth/sso/config');
86
+ if (!res.ok) return;
87
+ const cfg = await res.json();
88
+ if (cfg.enabled) {
89
+ window._ssoEnabled = true;
90
+ window._ssoProviderName = cfg.provider_name;
91
+ applyI18n();
92
+ }
93
+ } catch {}
94
+ }
95
+
96
+ function doSSOLogin(provider) {
97
+ if (!window._ssoEnabled) {
98
+ setMsg('login-msg', t('sso_unavailable'));
99
+ return;
100
+ }
101
+ if (provider) sessionStorage.setItem('ltcai_sso_provider_hint', provider);
102
+ window.location.href = '/auth/sso/login';
103
+ }
104
+
105
+ function togglePasswordVisibility() {
106
+ const input = document.getElementById('login-pw');
107
+ input.type = input.type === 'password' ? 'text' : 'password';
108
+ }
109
+
110
+ function toggleLang() {
111
+ const m = document.getElementById('lang-menu');
112
+ m.classList.toggle('open');
113
+ }
114
+
115
+ function setLang(l) {
116
+ lang = l;
117
+ localStorage.setItem('ltcai_lang', l);
118
+ document.getElementById('lang-menu').classList.remove('open');
119
+ applyI18n();
120
+ }
121
+
122
+ document.addEventListener('click', e => {
123
+ if (!e.target.closest('.lang-wrap'))
124
+ document.getElementById('lang-menu').classList.remove('open');
125
+ });
126
+
127
+ function showSection(name) {
128
+ document.getElementById('login-section').style.display = name === 'login' ? '' : 'none';
129
+ document.getElementById('register-section').style.display = name === 'register' ? '' : 'none';
130
+ document.getElementById('login-msg').textContent = '';
131
+ document.getElementById('reg-msg').textContent = '';
132
+ }
133
+
134
+ function setMsg(id, text, ok = false) {
135
+ const el = document.getElementById(id);
136
+ el.textContent = text;
137
+ el.className = 'msg' + (ok ? ' ok' : '');
138
+ }
139
+
140
+ async function doLogin() {
141
+ const email = document.getElementById('login-email').value.trim();
142
+ const password = document.getElementById('login-pw').value;
143
+ if (!email || !password) { setMsg('login-msg', t('err_fill')); return; }
144
+ const btn = document.getElementById('login-btn');
145
+ btn.disabled = true;
146
+ btn.textContent = '...';
147
+ try {
148
+ const res = await apiFetch('/login', {
149
+ method: 'POST',
150
+ headers: { 'Content-Type': 'application/json' },
151
+ body: JSON.stringify({ email, password })
152
+ });
153
+ if (res.ok) {
154
+ const data = await res.json();
155
+ localStorage.setItem('ltcai_user_email', data.email);
156
+ localStorage.setItem('ltcai_user_nickname', data.nickname || data.name || data.email);
157
+ localStorage.setItem('ltcai_is_admin', data.is_admin ? 'true' : 'false');
158
+ window.location.href = '/chat';
159
+ } else {
160
+ const data = await res.json().catch(() => ({}));
161
+ setMsg('login-msg', data.detail || t('err_login_fail'));
162
+ btn.disabled = false;
163
+ btn.textContent = t('btn_login');
164
+ }
165
+ } catch {
166
+ setMsg('login-msg', t('err_server'));
167
+ btn.disabled = false;
168
+ btn.textContent = t('btn_login');
169
+ }
170
+ }
171
+
172
+ async function doRegister() {
173
+ const email = document.getElementById('reg-email').value.trim();
174
+ const pw = document.getElementById('reg-pw').value;
175
+ const pw2 = document.getElementById('reg-pw2').value;
176
+ const name = document.getElementById('reg-name').value.trim();
177
+ const nickname = document.getElementById('reg-nick').value.trim();
178
+ if (!email || !pw || !name || !nickname) { setMsg('reg-msg', t('err_fill')); return; }
179
+ if (pw !== pw2) { setMsg('reg-msg', t('err_pw_mismatch')); return; }
180
+ const btn = document.getElementById('reg-btn');
181
+ btn.disabled = true;
182
+ btn.textContent = '...';
183
+ try {
184
+ const res = await apiFetch('/register', {
185
+ method: 'POST',
186
+ headers: { 'Content-Type': 'application/json' },
187
+ body: JSON.stringify({ email, password: pw, name, nickname })
188
+ });
189
+ if (res.ok) {
190
+ setMsg('reg-msg', lang === 'ko' ? '가입 완료! 로그인 중...' : 'Registered! Logging in...', true);
191
+ await apiFetch('/login', {
192
+ method: 'POST',
193
+ headers: { 'Content-Type': 'application/json' },
194
+ body: JSON.stringify({ email, password: pw })
195
+ }).then(r => r.ok ? r.json() : null).then(data => {
196
+ if (data) {
197
+ localStorage.setItem('ltcai_user_email', data.email);
198
+ localStorage.setItem('ltcai_user_nickname', data.nickname || data.name || data.email);
199
+ localStorage.setItem('ltcai_is_admin', data.is_admin ? 'true' : 'false');
200
+ window.location.href = '/chat';
201
+ }
202
+ });
203
+ } else {
204
+ const data = await res.json().catch(() => ({}));
205
+ setMsg('reg-msg', data.detail || '가입 실패');
206
+ btn.disabled = false;
207
+ btn.textContent = t('btn_register');
208
+ }
209
+ } catch {
210
+ setMsg('reg-msg', t('err_server'));
211
+ btn.disabled = false;
212
+ btn.textContent = t('btn_register');
213
+ }
214
+ }
215
+
216
+ // If already logged in, skip to chat
217
+ apiFetch('/account/profile').then(r => {
218
+ if (r.ok) window.location.href = '/chat';
219
+ }).catch(() => {});
220
+
221
+ initSSO();
222
+
223
+ // Handle invite code in URL
224
+ const urlCode = new URLSearchParams(window.location.search).get('code');
225
+ if (urlCode) {
226
+ document.getElementById('reg-email').focus();
227
+ showSection('register');
228
+ }
229
+
230
+ applyI18n();