ltcai 2.2.1 → 2.2.7

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.
Files changed (44) hide show
  1. package/README.md +183 -140
  2. package/codex_telegram_bot.py +6 -2
  3. package/docs/CHANGELOG.md +100 -23
  4. package/docs/EDITION_STRATEGY.md +8 -8
  5. package/docs/ENTERPRISE.md +5 -5
  6. package/docs/PLUGIN_SDK.md +4 -4
  7. package/docs/V2_ARCHITECTURE.md +9 -9
  8. package/docs/architecture.md +18 -17
  9. package/docs/images/admin-dashboard.png +0 -0
  10. package/docs/images/knowledge-graph.png +0 -0
  11. package/docs/images/lattice-ai-demo.gif +0 -0
  12. package/docs/images/lattice-ai-hero.png +0 -0
  13. package/docs/images/mobile-responsive.png +0 -0
  14. package/docs/images/pipeline.png +0 -0
  15. package/docs/images/workspace-dark.png +0 -0
  16. package/docs/images/workspace-light.png +0 -0
  17. package/latticeai/__init__.py +1 -1
  18. package/latticeai/api/static_routes.py +10 -0
  19. package/latticeai/core/logging_safety.py +62 -0
  20. package/latticeai/core/workspace_os.py +1 -1
  21. package/package.json +10 -5
  22. package/static/account.html +9 -4
  23. package/static/activity.html +4 -4
  24. package/static/admin.html +8 -3
  25. package/static/agents.html +4 -4
  26. package/static/chat.html +15 -10
  27. package/static/css/reference/account.css +303 -0
  28. package/static/css/reference/admin.css +610 -0
  29. package/static/css/reference/base.css +1658 -0
  30. package/static/{lattice-reference.css → css/reference/chat.css} +243 -3599
  31. package/static/css/reference/graph.css +1016 -0
  32. package/static/css/responsive.css +226 -4
  33. package/static/css/tokens.css +16 -5
  34. package/static/favicon.ico +0 -0
  35. package/static/graph.html +9 -4
  36. package/static/platform.css +1 -1
  37. package/static/plugins.html +4 -4
  38. package/static/scripts/chat.js +187 -69
  39. package/static/scripts/ux.js +1 -1
  40. package/static/sw.js +5 -3
  41. package/static/workflows.html +4 -4
  42. package/static/workspace.css +75 -14
  43. package/static/workspace.html +5 -5
  44. package/telegram_bot.py +18 -14
@@ -14,17 +14,133 @@ const chatViewport = document.getElementById('chat-viewport');
14
14
  let telegramHistorySyncEnabled = false;
15
15
  let telegramHistorySyncInFlight = false;
16
16
 
17
- // ── 멀티 LLM 파이프라인 상태 ──
18
- let pipelineConfig = { planning: null, executing: null, reviewing: null };
19
- let pipelineActive = false; // true이면 전송 시 pipeline 모드
20
-
21
- function openPipelineModal() {
22
- document.getElementById('pipeline-overlay').style.display = 'flex';
23
- loadPipelineModelOptions();
24
- }
25
- function closePipelineModal() {
26
- document.getElementById('pipeline-overlay').style.display = 'none';
27
- }
17
+ // ── 멀티 LLM 파이프라인 상태 ──
18
+ let pipelineConfig = { planning: null, executing: null, reviewing: null };
19
+ let pipelineActive = false; // true이면 전송 시 pipeline 모드
20
+ const MODAL_LAYER_IDS = [
21
+ 'acct-modal-overlay',
22
+ 'mcp-modal-overlay',
23
+ 'workspace-modal-overlay',
24
+ 'mode-modal-overlay',
25
+ 'model-overlay',
26
+ 'pipeline-overlay',
27
+ 'admin-overlay',
28
+ 'vpc-overlay',
29
+ 'status-overlay',
30
+ 'file-create-overlay',
31
+ 'file-editor-overlay',
32
+ 'local-browser-overlay',
33
+ 'advanced-settings-overlay',
34
+ 'cu-overlay',
35
+ 'setup-overlay',
36
+ 'onboarding-overlay',
37
+ 'perm-overlay',
38
+ ];
39
+ const MODAL_LAYER_SET = new Set(MODAL_LAYER_IDS);
40
+ let activeModalLayerId = null;
41
+ let restoreModalLayerId = null;
42
+ let bodyOverflowBeforeModal = '';
43
+
44
+ function modalLayerEl(id) {
45
+ return document.getElementById(id);
46
+ }
47
+
48
+ function isModalLayerVisible(el) {
49
+ if (!el) return false;
50
+ return el.classList.contains('open') || getComputedStyle(el).display !== 'none';
51
+ }
52
+
53
+ function refreshBodyModalLock() {
54
+ const hasOpen = MODAL_LAYER_IDS.some(id => isModalLayerVisible(modalLayerEl(id)));
55
+ if (hasOpen) {
56
+ if (!document.body.classList.contains('modal-open')) {
57
+ bodyOverflowBeforeModal = document.body.style.overflow || '';
58
+ }
59
+ document.body.classList.add('modal-open');
60
+ document.body.style.overflow = 'hidden';
61
+ } else {
62
+ document.body.classList.remove('modal-open');
63
+ document.body.style.overflow = bodyOverflowBeforeModal;
64
+ bodyOverflowBeforeModal = '';
65
+ }
66
+ }
67
+
68
+ function hideModalElement(id) {
69
+ const el = modalLayerEl(id);
70
+ if (!el) return;
71
+ el.classList.remove('open');
72
+ el.style.display = 'none';
73
+ el.setAttribute('aria-hidden', 'true');
74
+ if ('inert' in el) el.inert = true;
75
+ }
76
+
77
+ function showModalLayer(id, options = {}) {
78
+ const el = modalLayerEl(id);
79
+ if (!el) return;
80
+ const current = activeModalLayerId && activeModalLayerId !== id ? activeModalLayerId : null;
81
+ MODAL_LAYER_IDS.forEach(otherId => {
82
+ if (otherId !== id) hideModalElement(otherId);
83
+ });
84
+ if (options.restorePrevious && current) restoreModalLayerId = current;
85
+ else if (!options.keepRestore) restoreModalLayerId = null;
86
+ el.removeAttribute('aria-hidden');
87
+ if ('inert' in el) el.inert = false;
88
+ el.classList.add('open');
89
+ el.style.display = 'flex';
90
+ activeModalLayerId = id;
91
+ refreshBodyModalLock();
92
+ }
93
+
94
+ function closeModalLayer(id, options = {}) {
95
+ hideModalElement(id);
96
+ if (activeModalLayerId === id) activeModalLayerId = null;
97
+ const restoreId = restoreModalLayerId;
98
+ if (!options.skipRestore) restoreModalLayerId = null;
99
+ refreshBodyModalLock();
100
+ if (!options.skipRestore && restoreId && restoreId !== id) {
101
+ showModalLayer(restoreId, { keepRestore: true });
102
+ }
103
+ }
104
+
105
+ function closeAllModalLayers() {
106
+ MODAL_LAYER_IDS.forEach(hideModalElement);
107
+ activeModalLayerId = null;
108
+ restoreModalLayerId = null;
109
+ refreshBodyModalLock();
110
+ }
111
+
112
+ function dismissModalLayer(id) {
113
+ if (id === 'perm-overlay') {
114
+ resolvePermission(false);
115
+ return;
116
+ }
117
+ closeModalLayer(id);
118
+ }
119
+
120
+ document.addEventListener('keydown', (event) => {
121
+ if (event.key !== 'Escape') return;
122
+ const visible = [...MODAL_LAYER_IDS].reverse().find(id => isModalLayerVisible(modalLayerEl(id)));
123
+ if (!visible) return;
124
+ event.preventDefault();
125
+ dismissModalLayer(visible);
126
+ });
127
+
128
+ document.addEventListener('click', (event) => {
129
+ const target = event.target;
130
+ if (target && target.id && MODAL_LAYER_SET.has(target.id)) dismissModalLayer(target.id);
131
+ });
132
+
133
+ window.addEventListener('pagehide', closeAllModalLayers);
134
+ window.addEventListener('popstate', closeAllModalLayers);
135
+ window.addEventListener('hashchange', closeAllModalLayers);
136
+
137
+ function openPipelineModal() {
138
+ showModalLayer('pipeline-overlay');
139
+ loadPipelineModelOptions();
140
+ }
141
+ function closePipelineModal() {
142
+ closeModalLayer('pipeline-overlay');
143
+ }
28
144
  function resetPipeline() {
29
145
  pipelineConfig = { planning: null, executing: null, reviewing: null };
30
146
  pipelineActive = false;
@@ -731,12 +847,12 @@ const chatViewport = document.getElementById('chat-viewport');
731
847
  focusChatInput();
732
848
  }
733
849
 
734
- function openAdvancedSettingsPanel() {
735
- document.getElementById('advanced-settings-overlay')?.classList.add('open');
736
- }
850
+ function openAdvancedSettingsPanel() {
851
+ showModalLayer('advanced-settings-overlay');
852
+ }
737
853
 
738
- function closeAdvancedSettingsPanel() {
739
- document.getElementById('advanced-settings-overlay')?.classList.remove('open');
854
+ function closeAdvancedSettingsPanel() {
855
+ closeModalLayer('advanced-settings-overlay');
740
856
  }
741
857
 
742
858
  function updateWorkspaceModeUi() {
@@ -778,19 +894,19 @@ const chatViewport = document.getElementById('chat-viewport');
778
894
  }
779
895
 
780
896
  function selectWorkspace(kind) {
781
- setWorkspacePreference(kind);
782
- document.getElementById('workspace-modal-overlay')?.classList.remove('open');
783
- updateWorkspaceModeUi();
784
- openModeSelector();
785
- }
897
+ setWorkspacePreference(kind);
898
+ closeModalLayer('workspace-modal-overlay');
899
+ updateWorkspaceModeUi();
900
+ openModeSelector();
901
+ }
786
902
 
787
- function openModeSelector() {
788
- updateWorkspaceModeUi();
789
- document.getElementById('mode-modal-overlay')?.classList.add('open');
790
- }
903
+ function openModeSelector() {
904
+ updateWorkspaceModeUi();
905
+ showModalLayer('mode-modal-overlay');
906
+ }
791
907
 
792
- function closeModeSelector() {
793
- document.getElementById('mode-modal-overlay')?.classList.remove('open');
908
+ function closeModeSelector() {
909
+ closeModalLayer('mode-modal-overlay');
794
910
  }
795
911
 
796
912
  function selectMode(mode) {
@@ -829,12 +945,12 @@ const chatViewport = document.getElementById('chat-viewport');
829
945
  async function startOnboardingIfNeeded(force = false) {
830
946
  updateWorkspaceModeUi();
831
947
  const forceAfterLogin = consumeSetupAfterLoginFlag();
832
- const hasLoadedModel = await modelLoadedForChat();
833
- if (!force && !forceAfterLogin && onboardingComplete() && hasLoadedModel) {
834
- document.getElementById('onboarding-overlay')?.classList.remove('open');
835
- return;
836
- }
837
- document.getElementById('onboarding-overlay')?.classList.add('open');
948
+ const hasLoadedModel = await modelLoadedForChat();
949
+ if (!force && !forceAfterLogin && onboardingComplete() && hasLoadedModel) {
950
+ closeModalLayer('onboarding-overlay');
951
+ return;
952
+ }
953
+ showModalLayer('onboarding-overlay');
838
954
  if (forceAfterLogin || !hasLoadedModel) {
839
955
  renderPcAnalysis();
840
956
  } else {
@@ -1051,7 +1167,7 @@ const chatViewport = document.getElementById('chat-viewport');
1051
1167
  // Top pick callout
1052
1168
  const top = rec.top_pick;
1053
1169
  const topHtml = top ? `
1054
- <div style="border:1px solid #16a34a;background:#f0fdf4;border-radius:10px;padding:10px 12px;margin:8px 0">
1170
+ <div style="border:1px solid var(--success);background:var(--accent-soft);color:var(--text);border-radius:10px;padding:10px 12px;margin:8px 0">
1055
1171
  <div style="font-weight:700">⭐ Best for this PC — ${escapeHtml(top.name || top.id)} ${badge('recommended')}</div>
1056
1172
  <div style="font-size:12px;opacity:0.8;margin-top:3px">${escapeHtml(top.reason || '')}</div>
1057
1173
  <div style="font-size:12px;margin-top:4px">${escapeHtml(top.size || '')} · ${escapeHtml(ram(top))} · ${escapeHtml(nextStep(rec.engine))}</div>
@@ -1362,12 +1478,12 @@ const chatViewport = document.getElementById('chat-viewport');
1362
1478
  `);
1363
1479
  }
1364
1480
 
1365
- function finishOnboarding(mode) {
1366
- selectMode(mode);
1367
- setOnboardingComplete();
1368
- document.getElementById('onboarding-overlay')?.classList.remove('open');
1369
- showChat();
1370
- }
1481
+ function finishOnboarding(mode) {
1482
+ selectMode(mode);
1483
+ setOnboardingComplete();
1484
+ closeModalLayer('onboarding-overlay');
1485
+ showChat();
1486
+ }
1371
1487
 
1372
1488
  function switchAcctTab(tab) {
1373
1489
  ['profile', 'password'].forEach(t => {
@@ -1390,10 +1506,10 @@ const chatViewport = document.getElementById('chat-viewport');
1390
1506
  document.getElementById('profile-nickname').value = data.nickname || '';
1391
1507
  }
1392
1508
  } catch {}
1393
- document.getElementById('acct-modal-overlay').classList.add('open');
1509
+ showModalLayer('acct-modal-overlay');
1394
1510
  }
1395
1511
  function closeAcctModal() {
1396
- document.getElementById('acct-modal-overlay').classList.remove('open');
1512
+ closeModalLayer('acct-modal-overlay');
1397
1513
  }
1398
1514
  document.addEventListener('click', (e) => {
1399
1515
  const overlay = document.getElementById('acct-modal-overlay');
@@ -1707,7 +1823,7 @@ const chatViewport = document.getElementById('chat-viewport');
1707
1823
  }
1708
1824
 
1709
1825
  async function openModelPanel() {
1710
- document.getElementById('model-overlay').style.display = 'flex';
1826
+ showModalLayer('model-overlay');
1711
1827
  document.getElementById('model-list').innerHTML = '<div class="sensitivity-preview">실행 엔진과 모델 목록을 불러오는 중입니다...</div>';
1712
1828
  try {
1713
1829
  const res = await apiFetch('/engines');
@@ -1742,7 +1858,7 @@ const chatViewport = document.getElementById('chat-viewport');
1742
1858
  }
1743
1859
 
1744
1860
  function closeModelPanel() {
1745
- document.getElementById('model-overlay').style.display = 'none';
1861
+ closeModalLayer('model-overlay');
1746
1862
  }
1747
1863
 
1748
1864
  async function installEngine(engineId) {
@@ -2194,7 +2310,9 @@ const chatViewport = document.getElementById('chat-viewport');
2194
2310
  if (!t) {
2195
2311
  t = document.createElement('div');
2196
2312
  t.id = 'ltcai-toast';
2197
- t.style.cssText = 'position:fixed;bottom:28px;left:50%;transform:translateX(-50%);background:rgba(255,255,255,0.96);color:#14162c;border:1px solid rgba(111,66,232,0.16);border-radius:10px;padding:10px 18px;font-size:13px;font-weight:600;z-index:9999;box-shadow:0 8px 24px rgba(88,72,150,0.16);pointer-events:none;transition:opacity .2s;';
2313
+ // Styling lives in static/css/responsive.css (#ltcai-toast) so the
2314
+ // toast is token-driven and adapts to light/dark. Only the opacity
2315
+ // animation state is toggled inline below.
2198
2316
  document.body.appendChild(t);
2199
2317
  }
2200
2318
  t.textContent = msg;
@@ -2204,7 +2322,7 @@ const chatViewport = document.getElementById('chat-viewport');
2204
2322
  }
2205
2323
 
2206
2324
  function closeAdminPanel() {
2207
- document.getElementById('admin-overlay').style.display = 'none';
2325
+ closeModalLayer('admin-overlay');
2208
2326
  }
2209
2327
 
2210
2328
  // ── VPC Panel ────────────────────────────────────────
@@ -2235,7 +2353,7 @@ const chatViewport = document.getElementById('chat-viewport');
2235
2353
  }
2236
2354
 
2237
2355
  async function openVpcPanel() {
2238
- document.getElementById('vpc-overlay').style.display = 'flex';
2356
+ showModalLayer('vpc-overlay');
2239
2357
  try {
2240
2358
  const res = await apiFetch('/vpc/status');
2241
2359
  if (res.ok) fillVpcPanelForm(await res.json());
@@ -2243,7 +2361,7 @@ const chatViewport = document.getElementById('chat-viewport');
2243
2361
  }
2244
2362
 
2245
2363
  function closeVpcPanel() {
2246
- document.getElementById('vpc-overlay').style.display = 'none';
2364
+ closeModalLayer('vpc-overlay');
2247
2365
  }
2248
2366
 
2249
2367
  function selectVpcProvider(name, btn) {
@@ -2293,7 +2411,7 @@ const chatViewport = document.getElementById('chat-viewport');
2293
2411
 
2294
2412
  // ── Status Summary Panel ─────────────────────────────
2295
2413
  async function openStatusPanel() {
2296
- document.getElementById('status-overlay').style.display = 'flex';
2414
+ showModalLayer('status-overlay');
2297
2415
  document.getElementById('status-panel-body').innerHTML = '<div class="sensitivity-preview">상태를 불러오는 중...</div>';
2298
2416
  try {
2299
2417
  const mode = getCurrentMode();
@@ -2362,7 +2480,7 @@ const chatViewport = document.getElementById('chat-viewport');
2362
2480
  }
2363
2481
 
2364
2482
  function closeStatusPanel() {
2365
- document.getElementById('status-overlay').style.display = 'none';
2483
+ closeModalLayer('status-overlay');
2366
2484
  }
2367
2485
 
2368
2486
  // ── 파일 생성 패널 ────────────────────────────────────
@@ -2415,11 +2533,11 @@ const chatViewport = document.getElementById('chat-viewport');
2415
2533
  </div>`;
2416
2534
  }
2417
2535
  document.getElementById('file-create-form').innerHTML = formHtml;
2418
- document.getElementById('file-create-overlay').style.display = 'flex';
2536
+ showModalLayer('file-create-overlay');
2419
2537
  }
2420
2538
 
2421
2539
  function closeFileCreate() {
2422
- document.getElementById('file-create-overlay').style.display = 'none';
2540
+ closeModalLayer('file-create-overlay');
2423
2541
  }
2424
2542
 
2425
2543
  function _formatBytes(b) {
@@ -2507,24 +2625,24 @@ const chatViewport = document.getElementById('chat-viewport');
2507
2625
  document.getElementById('perm-title').textContent = '파일 접근 요청';
2508
2626
  document.getElementById('perm-path').textContent = path;
2509
2627
  document.getElementById('perm-desc').textContent = `AI가 아래 경로에 대한 "${actionLabel}" 작업을 요청합니다. 허용하시겠습니까?`;
2510
- document.getElementById('perm-overlay').style.display = 'flex';
2628
+ showModalLayer('perm-overlay', { restorePrevious: true });
2511
2629
  });
2512
2630
  }
2513
2631
 
2514
2632
  function resolvePermission(allowed) {
2515
- document.getElementById('perm-overlay').style.display = 'none';
2633
+ closeModalLayer('perm-overlay');
2516
2634
  if (_permResolve) { _permResolve(allowed); _permResolve = null; }
2517
2635
  }
2518
2636
 
2519
2637
  let _localCurrentPath = '~';
2520
2638
 
2521
2639
  async function openLocalBrowser() {
2522
- document.getElementById('local-browser-overlay').style.display = 'flex';
2640
+ showModalLayer('local-browser-overlay');
2523
2641
  await localNav(_localCurrentPath || '~');
2524
2642
  }
2525
2643
 
2526
2644
  function closeLocalBrowser() {
2527
- document.getElementById('local-browser-overlay').style.display = 'none';
2645
+ closeModalLayer('local-browser-overlay');
2528
2646
  }
2529
2647
 
2530
2648
  async function localNav(path) {
@@ -2710,8 +2828,8 @@ const chatViewport = document.getElementById('chat-viewport');
2710
2828
  document.getElementById('editor-filepath').textContent = path;
2711
2829
  document.getElementById('file-editor-content').value = text;
2712
2830
  document.getElementById('editor-status').textContent = '';
2713
- document.getElementById('local-browser-overlay').style.display = 'none';
2714
- document.getElementById('file-editor-overlay').style.display = 'flex';
2831
+ closeModalLayer('local-browser-overlay', { skipRestore: true });
2832
+ showModalLayer('file-editor-overlay');
2715
2833
  } catch(e) {
2716
2834
  resultEl.innerHTML = `
2717
2835
  <div class="sensitivity-preview">⚠️ 문서 읽기 실패: ${escapeHtml(e.message)}<br>
@@ -2761,15 +2879,15 @@ const chatViewport = document.getElementById('chat-viewport');
2761
2879
  document.getElementById('editor-filepath').textContent = path;
2762
2880
  document.getElementById('file-editor-content').value = content;
2763
2881
  document.getElementById('editor-status').textContent = '';
2764
- document.getElementById('local-browser-overlay').style.display = 'none';
2765
- document.getElementById('file-editor-overlay').style.display = 'flex';
2882
+ closeModalLayer('local-browser-overlay', { skipRestore: true });
2883
+ showModalLayer('file-editor-overlay');
2766
2884
  } catch(e) {
2767
2885
  resultEl.innerHTML = `<div class="sensitivity-preview">${escapeHtml(e.message)}</div>`;
2768
2886
  }
2769
2887
  }
2770
2888
 
2771
2889
  function closeFileEditor() {
2772
- document.getElementById('file-editor-overlay').style.display = 'none';
2890
+ closeModalLayer('file-editor-overlay');
2773
2891
  }
2774
2892
 
2775
2893
  async function saveLocalFile() {
@@ -2860,8 +2978,8 @@ const chatViewport = document.getElementById('chat-viewport');
2860
2978
  document.getElementById('file-editor-content').value = text;
2861
2979
  document.getElementById('editor-status').textContent = '⚠️ 이미지/표 등 비텍스트 요소는 표시되지 않을 수 있습니다';
2862
2980
  document.getElementById('editor-status').style.color = 'var(--accent-2)';
2863
- document.getElementById('local-browser-overlay').style.display = 'none';
2864
- document.getElementById('file-editor-overlay').style.display = 'flex';
2981
+ closeModalLayer('local-browser-overlay', { skipRestore: true });
2982
+ showModalLayer('file-editor-overlay');
2865
2983
  } catch(e) {
2866
2984
  resultEl.innerHTML = `<div class="sensitivity-preview">텍스트 추출 실패: ${escapeHtml(e.message)}</div>`;
2867
2985
  }
@@ -4186,12 +4304,12 @@ const chatViewport = document.getElementById('chat-viewport');
4186
4304
  let cuAgentAbort = null;
4187
4305
 
4188
4306
  async function openCuPanel() {
4189
- document.getElementById('cu-overlay').style.display = 'flex';
4307
+ showModalLayer('cu-overlay');
4190
4308
  await cuRefreshStatus();
4191
4309
  }
4192
4310
 
4193
4311
  function closeCuPanel() {
4194
- document.getElementById('cu-overlay').style.display = 'none';
4312
+ closeModalLayer('cu-overlay');
4195
4313
  }
4196
4314
 
4197
4315
  async function cuRefreshStatus() {
@@ -4358,12 +4476,12 @@ const chatViewport = document.getElementById('chat-viewport');
4358
4476
  let _wizItems = []; // items selected by user in step 2
4359
4477
 
4360
4478
  function openSetupWizard() {
4361
- document.getElementById('setup-overlay').classList.add('open');
4479
+ showModalLayer('setup-overlay');
4362
4480
  _runStep1();
4363
4481
  }
4364
4482
 
4365
4483
  function closeSetupWizard() {
4366
- document.getElementById('setup-overlay').classList.remove('open');
4484
+ closeModalLayer('setup-overlay');
4367
4485
  }
4368
4486
 
4369
4487
  // Prevent click-through on overlay background
@@ -4678,12 +4796,12 @@ const chatViewport = document.getElementById('chat-viewport');
4678
4796
  let _mcpCurrentTab = 'registry';
4679
4797
 
4680
4798
  async function openMcpModal() {
4681
- document.getElementById('mcp-modal-overlay').classList.add('open');
4799
+ showModalLayer('mcp-modal-overlay');
4682
4800
  await renderMcpModal(_mcpCurrentTab);
4683
4801
  }
4684
4802
 
4685
4803
  function closeMcpModal() {
4686
- document.getElementById('mcp-modal-overlay').classList.remove('open');
4804
+ closeModalLayer('mcp-modal-overlay');
4687
4805
  }
4688
4806
 
4689
4807
  function switchMcpTab(tab, btn) {
@@ -1,5 +1,5 @@
1
1
  /* ============================================================================
2
- * Lattice AI — Shared UX runtime (v2.2.1)
2
+ * Lattice AI — Shared UX runtime (v2.2.2)
3
3
  *
4
4
  * 모든 페이지에서 로드된다. 페이지마다 일부 요소가 없을 수 있으므로 전부
5
5
  * 방어적으로(존재 확인 후) 동작한다. 기존 chat.js / graph.js / admin.js 의
package/static/sw.js CHANGED
@@ -1,12 +1,14 @@
1
1
  // Lattice AI Service Worker — enables PWA install on Android/iOS
2
2
  // Strategy: network-first for API, cache-first for static assets.
3
- const CACHE = "ltcai-v110";
3
+ const CACHE = "ltcai-v226";
4
4
  const STATIC = [
5
5
  "/",
6
6
  "/workspace",
7
- "/static/lattice-reference.css",
8
- "/static/workspace.css",
9
7
  "/static/css/tokens.css",
8
+ "/static/css/reference/base.css",
9
+ "/static/css/reference/chat.css",
10
+ "/static/css/responsive.css",
11
+ "/static/workspace.css",
10
12
  "/static/scripts/chat.js",
11
13
  "/static/scripts/admin.js",
12
14
  "/static/scripts/graph.js",
@@ -4,10 +4,10 @@
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover, interactive-widget=resizes-content" />
6
6
  <title>Workflow Designer — Lattice AI</title>
7
- <script src="/static/scripts/ux.js?v=2.2.1"></script>
8
- <link rel="stylesheet" href="/static/css/tokens.css?v=2.2.1" />
9
- <link rel="stylesheet" href="/static/platform.css?v=2.2.1" />
10
- <link rel="stylesheet" href="/static/css/responsive.css?v=2.2.1" />
7
+ <script src="/static/scripts/ux.js?v=2.2.7"></script>
8
+ <link rel="stylesheet" href="/static/css/tokens.css?v=2.2.7" />
9
+ <link rel="stylesheet" href="/static/platform.css?v=2.2.7" />
10
+ <link rel="stylesheet" href="/static/css/responsive.css?v=2.2.7" />
11
11
  </head>
12
12
  <body>
13
13
  <main>
@@ -5,6 +5,7 @@
5
5
  --bg: var(--lt-bg, #f6f7f9);
6
6
  --surface: var(--lt-surface, #ffffff);
7
7
  --surface-2: var(--lt-surface-2, #f0f4f8);
8
+ --input: var(--lt-input, var(--surface));
8
9
  --ink: var(--lt-ink, #101828);
9
10
  --muted: var(--lt-muted, #667085);
10
11
  --line: var(--lt-line, #d9e0e8);
@@ -199,8 +200,8 @@ h2 {
199
200
  }
200
201
 
201
202
  .secondary-action {
202
- background: #e6efff;
203
- color: #174ea6;
203
+ background: var(--accent-soft, #e6efff);
204
+ color: var(--blue);
204
205
  padding: 0 12px;
205
206
  font-weight: 700;
206
207
  }
@@ -317,7 +318,7 @@ h2 {
317
318
 
318
319
  .list-item {
319
320
  border: 1px solid var(--line);
320
- background: #fbfcfe;
321
+ background: var(--surface-2);
321
322
  border-radius: 8px;
322
323
  padding: 12px;
323
324
  display: grid;
@@ -354,8 +355,9 @@ h2 {
354
355
  .tag {
355
356
  font-size: 11px;
356
357
  font-weight: 800;
357
- color: #344054;
358
- background: #edf2f7;
358
+ color: var(--ink);
359
+ background: var(--surface-2);
360
+ border: 1px solid var(--line);
359
361
  border-radius: 999px;
360
362
  padding: 3px 8px;
361
363
  }
@@ -373,7 +375,7 @@ input, select, textarea {
373
375
  border: 1px solid var(--line);
374
376
  border-radius: 8px;
375
377
  color: var(--ink);
376
- background: #fff;
378
+ background: var(--input);
377
379
  min-height: 38px;
378
380
  padding: 9px 10px;
379
381
  }
@@ -445,6 +447,11 @@ textarea {
445
447
  position: absolute;
446
448
  opacity: 0;
447
449
  pointer-events: none;
450
+ /* 절대배치 + width:auto 인 체크박스는 컨테이닝 블록(뷰포트) 폭만큼 늘어나
451
+ * 가로 오버플로우를 만든다. 1px 박스로 가둬 레이아웃에 영향 없게 한다. */
452
+ width: 1px;
453
+ height: 1px;
454
+ margin: 0;
448
455
  }
449
456
 
450
457
  .toggle-row span {
@@ -463,7 +470,7 @@ textarea {
463
470
  top: 3px;
464
471
  left: 3px;
465
472
  border-radius: 50%;
466
- background: white;
473
+ background: var(--surface);
467
474
  transition: transform 160ms ease;
468
475
  }
469
476
 
@@ -592,10 +599,10 @@ textarea {
592
599
  .quickswitch-row { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 14px; }
593
600
  .switch-chip {
594
601
  display: inline-flex; align-items: center; gap: 6px;
595
- border: 1px solid var(--line); background: #fbfcfe; color: var(--ink);
602
+ border: 1px solid var(--line); background: var(--surface-2); color: var(--ink);
596
603
  border-radius: 999px; padding: 6px 12px; font-weight: 700; font-size: 13px; cursor: pointer;
597
604
  }
598
- .switch-chip.active { border-color: var(--blue); background: #eef2ff; color: var(--blue); }
605
+ .switch-chip.active { border-color: var(--blue); background: var(--accent-soft, #eef2ff); color: var(--blue); }
599
606
 
600
607
  /* Entity explorer */
601
608
  .entity-card { text-align: left; cursor: pointer; width: 100%; font: inherit; }
@@ -610,10 +617,10 @@ textarea {
610
617
  /* Skill marketplace tabs */
611
618
  .tab-bar { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 12px; }
612
619
  .tab {
613
- border: 1px solid var(--line); background: #fbfcfe; color: var(--muted);
620
+ border: 1px solid var(--line); background: var(--surface-2); color: var(--muted);
614
621
  border-radius: 999px; padding: 6px 14px; font-weight: 700; font-size: 13px; cursor: pointer;
615
622
  }
616
- .tab.active { border-color: var(--blue); background: #eef2ff; color: var(--blue); }
623
+ .tab.active { border-color: var(--blue); background: var(--accent-soft, #eef2ff); color: var(--blue); }
617
624
  .tab-count {
618
625
  display: inline-block; min-width: 16px; padding: 0 5px; margin-left: 4px;
619
626
  border-radius: 999px; background: var(--red); color: #fff; font-size: 11px;
@@ -628,7 +635,7 @@ textarea {
628
635
  }
629
636
  .capability-card {
630
637
  display: flex; align-items: center; gap: 10px;
631
- border: 1px solid var(--line); border-radius: 8px; padding: 10px 12px; background: #fbfcfe;
638
+ border: 1px solid var(--line); border-radius: 8px; padding: 10px 12px; background: var(--surface-2);
632
639
  }
633
640
  .capability-card i { font-size: 18px; }
634
641
  .capability-card.off i { color: var(--muted); }
@@ -645,7 +652,7 @@ textarea {
645
652
  min-width: 0;
646
653
  border: 1px solid var(--line);
647
654
  border-radius: 8px;
648
- background: #fbfcfe;
655
+ background: var(--surface-2);
649
656
  padding: 14px;
650
657
  display: grid;
651
658
  gap: 7px;
@@ -686,7 +693,7 @@ textarea {
686
693
  .skill-progress-track {
687
694
  height: 7px;
688
695
  border-radius: 999px;
689
- background: #e5eaf2;
696
+ background: var(--surface-3, var(--surface-2));
690
697
  overflow: hidden;
691
698
  }
692
699
  .skill-progress-track span {
@@ -720,3 +727,57 @@ textarea {
720
727
  .small-action { min-height: 44px; }
721
728
  .workspace-rail a { min-height: 44px; display: flex; align-items: center; }
722
729
  }
730
+
731
+ :root[data-lt-theme="dark"] main {
732
+ background:
733
+ radial-gradient(circle at 80% 14%, rgba(167, 139, 250, 0.12), transparent 30%),
734
+ linear-gradient(180deg, var(--bg) 0%, #10122c 100%);
735
+ }
736
+
737
+ :root[data-lt-theme="dark"] .workspace-band,
738
+ :root[data-lt-theme="dark"] .workspace-panel,
739
+ :root[data-lt-theme="dark"] .metric-card {
740
+ background: rgba(22, 22, 58, 0.92);
741
+ border-color: rgba(160, 170, 230, 0.22);
742
+ box-shadow: 0 18px 44px rgba(0, 0, 0, 0.38);
743
+ }
744
+
745
+ :root[data-lt-theme="dark"] input,
746
+ :root[data-lt-theme="dark"] select,
747
+ :root[data-lt-theme="dark"] textarea,
748
+ :root[data-lt-theme="dark"] .list-item,
749
+ :root[data-lt-theme="dark"] .health-card,
750
+ :root[data-lt-theme="dark"] .capability-card,
751
+ :root[data-lt-theme="dark"] .switch-chip,
752
+ :root[data-lt-theme="dark"] .tab,
753
+ :root[data-lt-theme="dark"] .tag {
754
+ background: rgba(255, 255, 255, 0.06);
755
+ border-color: rgba(160, 170, 230, 0.22);
756
+ color: var(--ink);
757
+ }
758
+
759
+ :root[data-lt-theme="dark"] input::placeholder,
760
+ :root[data-lt-theme="dark"] textarea::placeholder {
761
+ color: rgba(226, 232, 255, 0.48);
762
+ }
763
+
764
+ :root[data-lt-theme="dark"] .status-pill,
765
+ :root[data-lt-theme="dark"] .workspace-role-pill {
766
+ background: rgba(255, 255, 255, 0.10);
767
+ color: var(--blue);
768
+ }
769
+
770
+ :root[data-lt-theme="dark"] .status-complete {
771
+ background: rgba(52, 211, 153, 0.16);
772
+ color: #86efac;
773
+ }
774
+
775
+ :root[data-lt-theme="dark"] .status-running {
776
+ background: rgba(251, 191, 36, 0.16);
777
+ color: #fbbf24;
778
+ }
779
+
780
+ :root[data-lt-theme="dark"] .status-failed {
781
+ background: rgba(244, 113, 113, 0.16);
782
+ color: #fca5a5;
783
+ }