ltcai 0.1.28 → 0.1.29
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 +8 -3
- package/auto_setup.py +605 -0
- package/docs/CHANGELOG.md +30 -0
- package/kg_schema.py +723 -0
- package/package.json +4 -1
- package/server.py +727 -42
- package/static/account.html +5 -616
- package/static/admin.html +236 -1371
- package/static/chat.html +204 -7146
- package/static/graph.html +15 -1436
- package/static/lattice-reference.css +6557 -71
- package/static/scripts/account.js +230 -0
- package/static/scripts/admin.js +1198 -0
- package/static/scripts/chat.js +4634 -0
- package/static/scripts/graph.js +1059 -0
- package/static/sw.js +11 -1
package/static/account.html
CHANGED
|
@@ -5,400 +5,14 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
|
|
6
6
|
<title>Lattice AI</title>
|
|
7
7
|
<link rel="manifest" href="/manifest.json">
|
|
8
|
-
<meta name="theme-color" content="#
|
|
8
|
+
<meta name="theme-color" content="#f3ecff">
|
|
9
9
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
|
10
10
|
<link rel="apple-touch-icon" href="/icons/apple-touch-icon.png">
|
|
11
11
|
<link rel="icon" type="image/png" sizes="32x32" href="/icons/favicon-32.png">
|
|
12
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
13
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
14
|
+
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap">
|
|
12
15
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@tabler/icons-webfont@latest/tabler-icons.min.css">
|
|
13
|
-
<style>
|
|
14
|
-
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap');
|
|
15
|
-
|
|
16
|
-
:root {
|
|
17
|
-
--bg: #282a36;
|
|
18
|
-
--text: #f7f7f2;
|
|
19
|
-
--faint: #8d93ab;
|
|
20
|
-
--muted: #c4c8d8;
|
|
21
|
-
--accent: #a77cff;
|
|
22
|
-
--accent-2: #20b8aa;
|
|
23
|
-
--shadow: 0 24px 70px rgba(5,7,12,0.46);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
27
|
-
html, body { height: 100%; }
|
|
28
|
-
body {
|
|
29
|
-
font-family: Inter, -apple-system, BlinkMacSystemFont, sans-serif;
|
|
30
|
-
color: var(--text);
|
|
31
|
-
background:
|
|
32
|
-
radial-gradient(circle at 50% 35%, rgba(167,124,255,0.16), transparent 34%),
|
|
33
|
-
radial-gradient(circle at 80% 75%, rgba(32,184,170,0.10), transparent 28%),
|
|
34
|
-
linear-gradient(135deg, #292b38 0%, #242632 52%, #2f3040 100%);
|
|
35
|
-
display: flex;
|
|
36
|
-
align-items: center;
|
|
37
|
-
justify-content: center;
|
|
38
|
-
min-height: 100vh;
|
|
39
|
-
padding: 24px;
|
|
40
|
-
overflow: hidden;
|
|
41
|
-
position: relative;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
body::before {
|
|
45
|
-
content: '';
|
|
46
|
-
position: fixed;
|
|
47
|
-
inset: 0;
|
|
48
|
-
background:
|
|
49
|
-
radial-gradient(circle, rgba(247,247,242,0.66) 1px, transparent 1.8px),
|
|
50
|
-
linear-gradient(rgba(157,177,255,0.12) 1px, transparent 1px),
|
|
51
|
-
linear-gradient(90deg, rgba(157,177,255,0.08) 1px, transparent 1px);
|
|
52
|
-
background-size: 82px 82px, 46px 46px, 46px 46px;
|
|
53
|
-
background-position: 16px 18px, 0 0, 0 0;
|
|
54
|
-
mask-image: linear-gradient(180deg, rgba(0,0,0,0.35), rgba(0,0,0,0.06));
|
|
55
|
-
pointer-events: none;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
body::after {
|
|
59
|
-
content: '';
|
|
60
|
-
position: fixed;
|
|
61
|
-
inset: 0;
|
|
62
|
-
background:
|
|
63
|
-
linear-gradient(116deg, transparent 0 44%, rgba(125,183,255,0.08) 44.1%, transparent 44.3% 100%),
|
|
64
|
-
linear-gradient(28deg, transparent 0 62%, rgba(167,124,255,0.08) 62.1%, transparent 62.3% 100%);
|
|
65
|
-
pointer-events: none;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
.orb {
|
|
69
|
-
display: none;
|
|
70
|
-
}
|
|
71
|
-
.orb-1 { width: 440px; height: 440px; top: -170px; left: -120px; background: rgba(34,211,160,0.13); }
|
|
72
|
-
.orb-2 { width: 380px; height: 380px; bottom: -130px; right: -90px; background: rgba(129,140,248,0.11); }
|
|
73
|
-
|
|
74
|
-
.card {
|
|
75
|
-
width: min(400px, 100%);
|
|
76
|
-
background: rgba(34,36,49,0.86);
|
|
77
|
-
border: 1px solid rgba(218,225,255,0.14);
|
|
78
|
-
border-radius: 10px;
|
|
79
|
-
padding: 38px 34px;
|
|
80
|
-
box-shadow: var(--shadow), 0 0 0 1px rgba(167,124,255,0.06);
|
|
81
|
-
position: relative;
|
|
82
|
-
z-index: 1;
|
|
83
|
-
backdrop-filter: blur(28px);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
.card::before {
|
|
87
|
-
content: '';
|
|
88
|
-
position: absolute;
|
|
89
|
-
top: 0; left: 50%;
|
|
90
|
-
transform: translateX(-50%);
|
|
91
|
-
width: 55%; height: 1px;
|
|
92
|
-
background: linear-gradient(90deg, transparent, rgba(167,124,255,0.55), rgba(32,184,170,0.5), transparent);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
.logo {
|
|
96
|
-
width: 54px; height: 54px;
|
|
97
|
-
background: radial-gradient(circle at 34% 30%, #ffffff 0 16%, #a77cff 17% 62%, #5b6cff 100%);
|
|
98
|
-
border-radius: 10px;
|
|
99
|
-
display: flex; align-items: center; justify-content: center;
|
|
100
|
-
font-size: 26px; color: #040706;
|
|
101
|
-
margin: 0 auto 18px;
|
|
102
|
-
box-shadow: 0 0 32px rgba(167,124,255,0.28), 0 8px 24px rgba(0,0,0,0.38);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
.title {
|
|
106
|
-
text-align: center;
|
|
107
|
-
font-size: 23px;
|
|
108
|
-
font-weight: 800;
|
|
109
|
-
margin-bottom: 6px;
|
|
110
|
-
background: linear-gradient(135deg, #fff 40%, #cfc6ff 76%, #8be9df);
|
|
111
|
-
-webkit-background-clip: text;
|
|
112
|
-
-webkit-text-fill-color: transparent;
|
|
113
|
-
background-clip: text;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
.subtitle {
|
|
117
|
-
text-align: center;
|
|
118
|
-
color: var(--muted);
|
|
119
|
-
font-size: 12.5px;
|
|
120
|
-
margin-bottom: 26px;
|
|
121
|
-
line-height: 1.5;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
.input {
|
|
125
|
-
width: 100%;
|
|
126
|
-
padding: 12px 14px;
|
|
127
|
-
margin-bottom: 10px;
|
|
128
|
-
background: rgba(20,22,31,0.74);
|
|
129
|
-
border: 1px solid rgba(218,225,255,0.14);
|
|
130
|
-
color: var(--text);
|
|
131
|
-
border-radius: 8px;
|
|
132
|
-
outline: none;
|
|
133
|
-
font-size: 14px;
|
|
134
|
-
font-family: inherit;
|
|
135
|
-
transition: border-color .15s, box-shadow .15s;
|
|
136
|
-
}
|
|
137
|
-
.input:focus {
|
|
138
|
-
border-color: rgba(167,124,255,0.5);
|
|
139
|
-
box-shadow: 0 0 0 3px rgba(167,124,255,0.09);
|
|
140
|
-
}
|
|
141
|
-
.input::placeholder { color: var(--faint); }
|
|
142
|
-
|
|
143
|
-
.submit {
|
|
144
|
-
width: 100%;
|
|
145
|
-
padding: 13px;
|
|
146
|
-
background: linear-gradient(135deg, #f7f7f2, #cfc6ff);
|
|
147
|
-
color: #242632;
|
|
148
|
-
border: none;
|
|
149
|
-
border-radius: 8px;
|
|
150
|
-
cursor: pointer;
|
|
151
|
-
font-weight: 800;
|
|
152
|
-
font-size: 14px;
|
|
153
|
-
font-family: inherit;
|
|
154
|
-
box-shadow: 0 0 22px rgba(167,124,255,0.18), 0 4px 12px rgba(0,0,0,0.3);
|
|
155
|
-
transition: all .18s;
|
|
156
|
-
margin-top: 4px;
|
|
157
|
-
}
|
|
158
|
-
.submit:hover {
|
|
159
|
-
background: linear-gradient(135deg, #ffffff, #d9d1ff);
|
|
160
|
-
box-shadow: 0 0 30px rgba(167,124,255,0.26), 0 4px 14px rgba(0,0,0,0.3);
|
|
161
|
-
transform: translateY(-1px);
|
|
162
|
-
}
|
|
163
|
-
.submit:disabled { opacity: 0.6; cursor: not-allowed; transform: none; }
|
|
164
|
-
|
|
165
|
-
.switch {
|
|
166
|
-
margin-top: 18px;
|
|
167
|
-
text-align: center;
|
|
168
|
-
font-size: 12.5px;
|
|
169
|
-
color: var(--faint);
|
|
170
|
-
}
|
|
171
|
-
.switch a { color: #cfc6ff; text-decoration: none; font-weight: 700; }
|
|
172
|
-
.switch a:hover { text-decoration: underline; }
|
|
173
|
-
|
|
174
|
-
.sso-divider {
|
|
175
|
-
display: flex; align-items: center; gap: 10px;
|
|
176
|
-
margin: 14px 0 10px;
|
|
177
|
-
color: var(--faint); font-size: 11.5px;
|
|
178
|
-
}
|
|
179
|
-
.sso-divider::before, .sso-divider::after {
|
|
180
|
-
content: ''; flex: 1;
|
|
181
|
-
height: 1px; background: rgba(255,255,255,0.07);
|
|
182
|
-
}
|
|
183
|
-
.sso-btn {
|
|
184
|
-
width: 100%;
|
|
185
|
-
padding: 12px;
|
|
186
|
-
background: rgba(247,247,242,0.045);
|
|
187
|
-
border: 1px solid rgba(218,225,255,0.13);
|
|
188
|
-
color: var(--text);
|
|
189
|
-
border-radius: 8px;
|
|
190
|
-
cursor: pointer;
|
|
191
|
-
font-weight: 600;
|
|
192
|
-
font-size: 13.5px;
|
|
193
|
-
font-family: inherit;
|
|
194
|
-
display: flex; align-items: center; justify-content: center; gap: 8px;
|
|
195
|
-
transition: all .18s;
|
|
196
|
-
}
|
|
197
|
-
.sso-btn:hover {
|
|
198
|
-
background: rgba(167,124,255,0.10);
|
|
199
|
-
border-color: rgba(167,124,255,0.3);
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
.msg {
|
|
203
|
-
font-size: 12px;
|
|
204
|
-
min-height: 18px;
|
|
205
|
-
margin-bottom: 6px;
|
|
206
|
-
text-align: center;
|
|
207
|
-
color: #ffbdb8;
|
|
208
|
-
}
|
|
209
|
-
.msg.ok { color: #9be7cd; }
|
|
210
|
-
|
|
211
|
-
/* Lang picker */
|
|
212
|
-
.lang-wrap {
|
|
213
|
-
position: absolute;
|
|
214
|
-
top: 12px;
|
|
215
|
-
right: 12px;
|
|
216
|
-
z-index: 10;
|
|
217
|
-
}
|
|
218
|
-
.lang-btn {
|
|
219
|
-
background: rgba(34,36,49,0.72);
|
|
220
|
-
border: 1px solid rgba(218,225,255,0.14);
|
|
221
|
-
color: var(--text);
|
|
222
|
-
border-radius: 8px;
|
|
223
|
-
padding: 7px 12px;
|
|
224
|
-
font-size: 13px;
|
|
225
|
-
font-family: inherit;
|
|
226
|
-
cursor: pointer;
|
|
227
|
-
transition: background .15s;
|
|
228
|
-
}
|
|
229
|
-
.lang-btn:hover { background: rgba(167,124,255,0.12); }
|
|
230
|
-
.lang-menu {
|
|
231
|
-
display: none;
|
|
232
|
-
position: absolute;
|
|
233
|
-
top: calc(100% + 6px);
|
|
234
|
-
right: 0;
|
|
235
|
-
background: #222431;
|
|
236
|
-
border: 1px solid rgba(218,225,255,0.14);
|
|
237
|
-
border-radius: 8px;
|
|
238
|
-
padding: 4px;
|
|
239
|
-
min-width: 130px;
|
|
240
|
-
box-shadow: 0 8px 24px rgba(0,0,0,0.4);
|
|
241
|
-
}
|
|
242
|
-
.lang-menu.open { display: block; }
|
|
243
|
-
.lang-opt {
|
|
244
|
-
padding: 7px 10px;
|
|
245
|
-
border-radius: 7px;
|
|
246
|
-
cursor: pointer;
|
|
247
|
-
font-size: 13px;
|
|
248
|
-
color: var(--faint);
|
|
249
|
-
}
|
|
250
|
-
.lang-opt:hover { background: rgba(167,124,255,0.10); color: var(--text); }
|
|
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
|
-
}
|
|
401
|
-
</style>
|
|
402
16
|
<link rel="stylesheet" href="/static/lattice-reference.css">
|
|
403
17
|
</head>
|
|
404
18
|
<body class="lattice-ref-auth">
|
|
@@ -489,231 +103,6 @@
|
|
|
489
103
|
<a href="#" onclick="return false;" id="privacy-link">개인정보 처리방침</a>
|
|
490
104
|
</footer>
|
|
491
105
|
|
|
492
|
-
<script>
|
|
493
|
-
const API_BASE = window.location.protocol === 'file:' ? 'http://localhost:4825' : '';
|
|
494
|
-
function apiFetch(path, opts = {}) {
|
|
495
|
-
const headers = { ...(opts.headers || {}) };
|
|
496
|
-
return fetch(API_BASE + path, { credentials: 'include', ...opts, headers });
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
// ── i18n ──────────────────────────────────────────────
|
|
500
|
-
const I18N = {
|
|
501
|
-
ko: {
|
|
502
|
-
login_title: 'Lattice AI', login_sub: '내 PC에서 시작하는<br>개인 AI 워크스페이스',
|
|
503
|
-
ph_email: '이메일 주소', ph_pw: '비밀번호', ph_new_pw: '비밀번호 (4자 이상)',
|
|
504
|
-
ph_pw_confirm: '비밀번호 확인', ph_name: '이름', ph_nick: '닉네임',
|
|
505
|
-
btn_login: '로그인', btn_register: '가입하기',
|
|
506
|
-
no_account: '계정이 없으신가요?', go_register: '회원가입',
|
|
507
|
-
have_account: '이미 계정이 있나요?', go_login: '로그인',
|
|
508
|
-
reg_title: '계정 만들기', reg_sub: 'Lattice AI 워크스페이스에 참여하세요',
|
|
509
|
-
err_pw_mismatch: '비밀번호가 일치하지 않습니다.',
|
|
510
|
-
err_fill: '모든 항목을 입력해주세요.',
|
|
511
|
-
err_login_fail: '이메일 또는 비밀번호가 틀렸습니다.',
|
|
512
|
-
err_server: '서버 연결 실패',
|
|
513
|
-
sso_divider: '조직 계정으로 로그인', sso_btn: '로 로그인',
|
|
514
|
-
ms_sso: 'Microsoft Entra ID로 계속하기', okta_sso: 'Okta SSO로 계속하기',
|
|
515
|
-
local_start: '로컬 계정으로 시작', help: '도움말', privacy: '개인정보 처리방침',
|
|
516
|
-
sso_unavailable: 'SSO가 아직 설정되지 않았습니다. 로컬 계정으로 시작하거나 관리자에게 문의하세요.',
|
|
517
|
-
},
|
|
518
|
-
en: {
|
|
519
|
-
login_title: 'Lattice AI', login_sub: 'Your personal AI workspace<br>starts on this PC',
|
|
520
|
-
ph_email: 'Email address', ph_pw: 'Password', ph_new_pw: 'Password (min. 4 chars)',
|
|
521
|
-
ph_pw_confirm: 'Confirm password', ph_name: 'Full name', ph_nick: 'Nickname',
|
|
522
|
-
btn_login: 'Log in', btn_register: 'Sign up',
|
|
523
|
-
no_account: "Don't have an account?", go_register: 'Sign up',
|
|
524
|
-
have_account: 'Already have an account?', go_login: 'Log in',
|
|
525
|
-
reg_title: 'Create Account', reg_sub: 'Join the Lattice AI workspace',
|
|
526
|
-
err_pw_mismatch: 'Passwords do not match.',
|
|
527
|
-
err_fill: 'Please fill in all fields.',
|
|
528
|
-
err_login_fail: 'Invalid email or password.',
|
|
529
|
-
err_server: 'Server connection failed',
|
|
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.',
|
|
534
|
-
}
|
|
535
|
-
};
|
|
536
|
-
|
|
537
|
-
let lang = localStorage.getItem('ltcai_lang') || 'ko';
|
|
538
|
-
function t(k) { return (I18N[lang] || I18N.ko)[k] || k; }
|
|
539
|
-
|
|
540
|
-
function applyI18n() {
|
|
541
|
-
document.getElementById('login-title').textContent = t('login_title');
|
|
542
|
-
document.getElementById('login-sub').innerHTML = t('login_sub');
|
|
543
|
-
document.getElementById('reg-title').textContent = t('reg_title');
|
|
544
|
-
document.getElementById('reg-sub').textContent = t('reg_sub');
|
|
545
|
-
document.getElementById('login-btn').textContent = t('btn_login');
|
|
546
|
-
document.getElementById('reg-btn').textContent = t('btn_register');
|
|
547
|
-
document.getElementById('go-register-link').textContent = t('go_register');
|
|
548
|
-
document.getElementById('have-account-text').textContent = t('have_account');
|
|
549
|
-
document.getElementById('go-login-link').textContent = t('go_login');
|
|
550
|
-
document.getElementById('login-email').placeholder = t('ph_email');
|
|
551
|
-
document.getElementById('login-pw').placeholder = t('ph_pw');
|
|
552
|
-
document.getElementById('reg-email').placeholder = t('ph_email');
|
|
553
|
-
document.getElementById('reg-pw').placeholder = t('ph_new_pw');
|
|
554
|
-
document.getElementById('reg-pw2').placeholder = t('ph_pw_confirm');
|
|
555
|
-
document.getElementById('reg-name').placeholder = t('ph_name');
|
|
556
|
-
document.getElementById('reg-nick').placeholder = t('ph_nick');
|
|
557
|
-
document.getElementById('sso-divider-text').textContent = t('sso_divider');
|
|
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');
|
|
563
|
-
['ko', 'en'].forEach(l => {
|
|
564
|
-
const el = document.getElementById(`opt-${l}`);
|
|
565
|
-
if (el) el.classList.toggle('active', l === lang);
|
|
566
|
-
});
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
async function initSSO() {
|
|
570
|
-
try {
|
|
571
|
-
const res = await apiFetch('/auth/sso/config');
|
|
572
|
-
if (!res.ok) return;
|
|
573
|
-
const cfg = await res.json();
|
|
574
|
-
if (cfg.enabled) {
|
|
575
|
-
window._ssoEnabled = true;
|
|
576
|
-
window._ssoProviderName = cfg.provider_name;
|
|
577
|
-
applyI18n();
|
|
578
|
-
}
|
|
579
|
-
} catch {}
|
|
580
|
-
}
|
|
581
|
-
|
|
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);
|
|
588
|
-
window.location.href = '/auth/sso/login';
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
function togglePasswordVisibility() {
|
|
592
|
-
const input = document.getElementById('login-pw');
|
|
593
|
-
input.type = input.type === 'password' ? 'text' : 'password';
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
function toggleLang() {
|
|
597
|
-
const m = document.getElementById('lang-menu');
|
|
598
|
-
m.classList.toggle('open');
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
function setLang(l) {
|
|
602
|
-
lang = l;
|
|
603
|
-
localStorage.setItem('ltcai_lang', l);
|
|
604
|
-
document.getElementById('lang-menu').classList.remove('open');
|
|
605
|
-
applyI18n();
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
document.addEventListener('click', e => {
|
|
609
|
-
if (!e.target.closest('.lang-wrap'))
|
|
610
|
-
document.getElementById('lang-menu').classList.remove('open');
|
|
611
|
-
});
|
|
612
|
-
|
|
613
|
-
function showSection(name) {
|
|
614
|
-
document.getElementById('login-section').style.display = name === 'login' ? '' : 'none';
|
|
615
|
-
document.getElementById('register-section').style.display = name === 'register' ? '' : 'none';
|
|
616
|
-
document.getElementById('login-msg').textContent = '';
|
|
617
|
-
document.getElementById('reg-msg').textContent = '';
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
function setMsg(id, text, ok = false) {
|
|
621
|
-
const el = document.getElementById(id);
|
|
622
|
-
el.textContent = text;
|
|
623
|
-
el.className = 'msg' + (ok ? ' ok' : '');
|
|
624
|
-
}
|
|
625
|
-
|
|
626
|
-
async function doLogin() {
|
|
627
|
-
const email = document.getElementById('login-email').value.trim();
|
|
628
|
-
const password = document.getElementById('login-pw').value;
|
|
629
|
-
if (!email || !password) { setMsg('login-msg', t('err_fill')); return; }
|
|
630
|
-
const btn = document.getElementById('login-btn');
|
|
631
|
-
btn.disabled = true;
|
|
632
|
-
btn.textContent = '...';
|
|
633
|
-
try {
|
|
634
|
-
const res = await apiFetch('/login', {
|
|
635
|
-
method: 'POST',
|
|
636
|
-
headers: { 'Content-Type': 'application/json' },
|
|
637
|
-
body: JSON.stringify({ email, password })
|
|
638
|
-
});
|
|
639
|
-
if (res.ok) {
|
|
640
|
-
const data = await res.json();
|
|
641
|
-
localStorage.setItem('ltcai_user_email', data.email);
|
|
642
|
-
localStorage.setItem('ltcai_user_nickname', data.nickname || data.name || data.email);
|
|
643
|
-
localStorage.setItem('ltcai_is_admin', data.is_admin ? 'true' : 'false');
|
|
644
|
-
window.location.href = '/chat';
|
|
645
|
-
} else {
|
|
646
|
-
const data = await res.json().catch(() => ({}));
|
|
647
|
-
setMsg('login-msg', data.detail || t('err_login_fail'));
|
|
648
|
-
btn.disabled = false;
|
|
649
|
-
btn.textContent = t('btn_login');
|
|
650
|
-
}
|
|
651
|
-
} catch {
|
|
652
|
-
setMsg('login-msg', t('err_server'));
|
|
653
|
-
btn.disabled = false;
|
|
654
|
-
btn.textContent = t('btn_login');
|
|
655
|
-
}
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
async function doRegister() {
|
|
659
|
-
const email = document.getElementById('reg-email').value.trim();
|
|
660
|
-
const pw = document.getElementById('reg-pw').value;
|
|
661
|
-
const pw2 = document.getElementById('reg-pw2').value;
|
|
662
|
-
const name = document.getElementById('reg-name').value.trim();
|
|
663
|
-
const nickname = document.getElementById('reg-nick').value.trim();
|
|
664
|
-
if (!email || !pw || !name || !nickname) { setMsg('reg-msg', t('err_fill')); return; }
|
|
665
|
-
if (pw !== pw2) { setMsg('reg-msg', t('err_pw_mismatch')); return; }
|
|
666
|
-
const btn = document.getElementById('reg-btn');
|
|
667
|
-
btn.disabled = true;
|
|
668
|
-
btn.textContent = '...';
|
|
669
|
-
try {
|
|
670
|
-
const res = await apiFetch('/register', {
|
|
671
|
-
method: 'POST',
|
|
672
|
-
headers: { 'Content-Type': 'application/json' },
|
|
673
|
-
body: JSON.stringify({ email, password: pw, name, nickname })
|
|
674
|
-
});
|
|
675
|
-
if (res.ok) {
|
|
676
|
-
setMsg('reg-msg', lang === 'ko' ? '가입 완료! 로그인 중...' : 'Registered! Logging in...', true);
|
|
677
|
-
await apiFetch('/login', {
|
|
678
|
-
method: 'POST',
|
|
679
|
-
headers: { 'Content-Type': 'application/json' },
|
|
680
|
-
body: JSON.stringify({ email, password: pw })
|
|
681
|
-
}).then(r => r.ok ? r.json() : null).then(data => {
|
|
682
|
-
if (data) {
|
|
683
|
-
localStorage.setItem('ltcai_user_email', data.email);
|
|
684
|
-
localStorage.setItem('ltcai_user_nickname', data.nickname || data.name || data.email);
|
|
685
|
-
localStorage.setItem('ltcai_is_admin', data.is_admin ? 'true' : 'false');
|
|
686
|
-
window.location.href = '/chat';
|
|
687
|
-
}
|
|
688
|
-
});
|
|
689
|
-
} else {
|
|
690
|
-
const data = await res.json().catch(() => ({}));
|
|
691
|
-
setMsg('reg-msg', data.detail || '가입 실패');
|
|
692
|
-
btn.disabled = false;
|
|
693
|
-
btn.textContent = t('btn_register');
|
|
694
|
-
}
|
|
695
|
-
} catch {
|
|
696
|
-
setMsg('reg-msg', t('err_server'));
|
|
697
|
-
btn.disabled = false;
|
|
698
|
-
btn.textContent = t('btn_register');
|
|
699
|
-
}
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
// If already logged in, skip to chat
|
|
703
|
-
apiFetch('/account/profile').then(r => {
|
|
704
|
-
if (r.ok) window.location.href = '/chat';
|
|
705
|
-
}).catch(() => {});
|
|
706
|
-
|
|
707
|
-
initSSO();
|
|
708
|
-
|
|
709
|
-
// Handle invite code in URL
|
|
710
|
-
const urlCode = new URLSearchParams(window.location.search).get('code');
|
|
711
|
-
if (urlCode) {
|
|
712
|
-
document.getElementById('reg-email').focus();
|
|
713
|
-
showSection('register');
|
|
714
|
-
}
|
|
715
|
-
|
|
716
|
-
applyI18n();
|
|
717
|
-
</script>
|
|
106
|
+
<script src="/static/scripts/account.js"></script>
|
|
718
107
|
</body>
|
|
719
108
|
</html>
|