ltcai 2.2.2 → 3.0.1

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 (78) hide show
  1. package/README.md +66 -27
  2. package/codex_telegram_bot.py +6 -2
  3. package/docs/CHANGELOG.md +154 -0
  4. package/docs/V3_BACKEND_ARCHITECTURE.md +138 -0
  5. package/docs/V3_FRONTEND.md +136 -0
  6. package/knowledge_graph.py +649 -21
  7. package/latticeai/__init__.py +1 -1
  8. package/latticeai/api/admin.py +47 -0
  9. package/latticeai/api/agents.py +54 -31
  10. package/latticeai/api/auth.py +1 -1
  11. package/latticeai/api/chat.py +10 -2
  12. package/latticeai/api/search.py +236 -0
  13. package/latticeai/api/static_routes.py +21 -2
  14. package/latticeai/core/config.py +16 -0
  15. package/latticeai/core/embedding_providers.py +502 -0
  16. package/latticeai/core/local_embeddings.py +86 -0
  17. package/latticeai/core/logging_safety.py +62 -0
  18. package/latticeai/core/workspace_os.py +1 -1
  19. package/latticeai/server_app.py +49 -1
  20. package/latticeai/services/agent_runtime.py +245 -0
  21. package/latticeai/services/search_service.py +346 -0
  22. package/package.json +8 -4
  23. package/static/account.html +9 -4
  24. package/static/activity.html +4 -4
  25. package/static/admin.html +8 -3
  26. package/static/agents.html +4 -4
  27. package/static/chat.html +16 -11
  28. package/static/css/reference/account.css +439 -0
  29. package/static/css/reference/admin.css +610 -0
  30. package/static/css/reference/base.css +1658 -0
  31. package/static/{lattice-reference.css → css/reference/chat.css} +271 -3633
  32. package/static/css/reference/graph.css +1016 -0
  33. package/static/css/responsive.css +248 -1
  34. package/static/css/tokens.css +132 -126
  35. package/static/favicon.ico +0 -0
  36. package/static/graph.html +9 -4
  37. package/static/manifest.json +3 -3
  38. package/static/platform.css +1 -1
  39. package/static/plugins.html +4 -4
  40. package/static/scripts/account.js +4 -4
  41. package/static/scripts/chat.js +227 -77
  42. package/static/scripts/workspace.js +78 -0
  43. package/static/sw.js +5 -3
  44. package/static/v3/css/lattice.base.css +128 -0
  45. package/static/v3/css/lattice.components.css +447 -0
  46. package/static/v3/css/lattice.shell.css +407 -0
  47. package/static/v3/css/lattice.tokens.css +132 -0
  48. package/static/v3/css/lattice.views.css +277 -0
  49. package/static/v3/index.html +40 -0
  50. package/static/v3/js/app.js +26 -0
  51. package/static/v3/js/core/api.js +327 -0
  52. package/static/v3/js/core/components.js +215 -0
  53. package/static/v3/js/core/dom.js +148 -0
  54. package/static/v3/js/core/fixtures.js +171 -0
  55. package/static/v3/js/core/router.js +37 -0
  56. package/static/v3/js/core/routes.js +73 -0
  57. package/static/v3/js/core/shell.js +363 -0
  58. package/static/v3/js/core/store.js +113 -0
  59. package/static/v3/js/views/admin-audit.js +185 -0
  60. package/static/v3/js/views/admin-permissions.js +178 -0
  61. package/static/v3/js/views/admin-policies.js +103 -0
  62. package/static/v3/js/views/admin-private-vpc.js +138 -0
  63. package/static/v3/js/views/admin-security.js +181 -0
  64. package/static/v3/js/views/admin-users.js +168 -0
  65. package/static/v3/js/views/agents.js +194 -0
  66. package/static/v3/js/views/chat.js +450 -0
  67. package/static/v3/js/views/files.js +180 -0
  68. package/static/v3/js/views/home.js +119 -0
  69. package/static/v3/js/views/hybrid-search.js +195 -0
  70. package/static/v3/js/views/knowledge-graph.js +238 -0
  71. package/static/v3/js/views/models.js +247 -0
  72. package/static/v3/js/views/my-computer.js +237 -0
  73. package/static/v3/js/views/pipeline.js +161 -0
  74. package/static/v3/js/views/settings.js +258 -0
  75. package/static/workflows.html +4 -4
  76. package/static/workspace.css +408 -14
  77. package/static/workspace.html +43 -24
  78. package/telegram_bot.py +18 -14
@@ -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>Plugin SDK — Lattice AI</title>
7
- <script src="/static/scripts/ux.js?v=2.2.2"></script>
8
- <link rel="stylesheet" href="/static/css/tokens.css?v=2.2.2" />
9
- <link rel="stylesheet" href="/static/platform.css?v=2.2.2" />
10
- <link rel="stylesheet" href="/static/css/responsive.css?v=2.2.2" />
7
+ <script src="/static/scripts/ux.js?v=3.0.0"></script>
8
+ <link rel="stylesheet" href="/static/css/tokens.css?v=3.0.0" />
9
+ <link rel="stylesheet" href="/static/platform.css?v=3.0.0" />
10
+ <link rel="stylesheet" href="/static/css/responsive.css?v=3.0.0" />
11
11
  </head>
12
12
  <body>
13
13
  <main>
@@ -162,7 +162,7 @@ const API_BASE = window.location.protocol === 'file:' ? 'http://localhost:4825'
162
162
  localStorage.setItem('ltcai_user_nickname', data.nickname || data.name || data.email);
163
163
  localStorage.setItem('ltcai_is_admin', data.is_admin ? 'true' : 'false');
164
164
  requestSetupAfterLogin();
165
- window.location.href = '/chat?setup=1';
165
+ window.location.href = '/app';
166
166
  } else {
167
167
  const data = await res.json().catch(() => ({}));
168
168
  setMsg('login-msg', data.detail || t('err_login_fail'));
@@ -205,7 +205,7 @@ const API_BASE = window.location.protocol === 'file:' ? 'http://localhost:4825'
205
205
  localStorage.setItem('ltcai_user_nickname', data.nickname || data.name || data.email);
206
206
  localStorage.setItem('ltcai_is_admin', data.is_admin ? 'true' : 'false');
207
207
  requestSetupAfterLogin();
208
- window.location.href = '/chat?setup=1';
208
+ window.location.href = '/app';
209
209
  }
210
210
  });
211
211
  } else {
@@ -221,9 +221,9 @@ const API_BASE = window.location.protocol === 'file:' ? 'http://localhost:4825'
221
221
  }
222
222
  }
223
223
 
224
- // If already logged in, skip to chat
224
+ // If already logged in, skip to the v3 workspace shell.
225
225
  apiFetch('/account/profile').then(r => {
226
- if (r.ok) window.location.href = '/chat';
226
+ if (r.ok) window.location.href = '/app';
227
227
  }).catch(() => {});
228
228
 
229
229
  initSSO();
@@ -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;
@@ -226,9 +342,13 @@ const chatViewport = document.getElementById('chat-viewport');
226
342
  logout: '로그아웃', admin_dashboard: '관리자 대시보드',
227
343
  my_status: '내 상태 보기', auto_setup: '자동 설정',
228
344
  nav_home: '홈', nav_chat: '채팅', nav_workspace: 'Workspace OS', nav_knowledge: '지식 그래프',
229
- nav_pipeline: '파이프라인', nav_files: '내 컴퓨터',
345
+ nav_pipeline: '파이프라인', nav_files: '파일', nav_computer: '내 컴퓨터',
346
+ nav_search: '검색', nav_new_chat: '새 대화',
230
347
  nav_model_status: '모델 상태', nav_runtime: '실행 방식 설정',
231
348
  nav_advanced_settings: '고급 설정',
349
+ nav_user_management: '사용자 관리', nav_permission_management: '권한 관리',
350
+ nav_audit_logs: '감사 로그', nav_security: '보안', nav_sensitive_data: '민감정보 감시',
351
+ nav_org_policies: '조직 정책', nav_private_vpc: 'Private VPC',
232
352
  history_search_ph: '대화 검색...', new_chat: 'New Chat',
233
353
  history_section: '대화', history_empty: '아직 저장된 대화가 없습니다.',
234
354
  new_conversation: '새 대화', previous_history: '이전 대화 기록',
@@ -305,9 +425,13 @@ const chatViewport = document.getElementById('chat-viewport');
305
425
  logout: 'Logout', admin_dashboard: 'Admin Dashboard',
306
426
  my_status: 'My Status', auto_setup: 'Auto Setup',
307
427
  nav_home: 'Home', nav_chat: 'Chat', nav_workspace: 'Workspace OS', nav_knowledge: 'Knowledge Graph',
308
- nav_pipeline: 'Pipeline', nav_files: 'My Computer',
428
+ nav_pipeline: 'Pipeline', nav_files: 'Files', nav_computer: 'My Computer',
429
+ nav_search: 'Search', nav_new_chat: 'New Chat',
309
430
  nav_model_status: 'Model Status', nav_runtime: 'Execution Settings',
310
431
  nav_advanced_settings: 'Advanced Settings',
432
+ nav_user_management: 'User Management', nav_permission_management: 'Permissions',
433
+ nav_audit_logs: 'Audit Logs', nav_security: 'Security', nav_sensitive_data: 'Sensitive Data',
434
+ nav_org_policies: 'Org Policies', nav_private_vpc: 'Private VPC',
311
435
  history_search_ph: 'Search chats...', new_chat: 'New Chat',
312
436
  history_section: 'Chats', history_empty: 'No saved chats yet.',
313
437
  new_conversation: 'New chat', previous_history: 'Previous chat history',
@@ -559,26 +683,37 @@ const chatViewport = document.getElementById('chat-viewport');
559
683
  const BASE_NAV_ITEMS = [
560
684
  { id: 'home', icon: 'ti-home', labelKey: 'nav_home' },
561
685
  { id: 'chat', icon: 'ti-message-circle', labelKey: 'nav_chat' },
562
- { id: 'workspace-os', icon: 'ti-layout-dashboard', labelKey: 'nav_workspace' },
563
686
  { id: 'knowledge', icon: 'ti-chart-dots-3', labelKey: 'nav_knowledge' },
564
687
  { id: 'pipeline', icon: 'ti-git-branch', labelKey: 'nav_pipeline' },
565
- { id: 'files', icon: 'ti-device-desktop', labelKey: 'nav_files' },
566
- { id: 'status', icon: 'ti-info-circle', labelKey: 'my_status' },
688
+ { id: 'files', icon: 'ti-files', labelKey: 'nav_files' },
689
+ { id: 'my-computer', icon: 'ti-device-desktop', labelKey: 'nav_computer' },
690
+ { id: 'search', icon: 'ti-search', labelKey: 'nav_search' },
691
+ { id: 'new-chat', icon: 'ti-plus', labelKey: 'nav_new_chat' },
567
692
  ];
568
693
 
569
694
  const ADVANCED_NAV_ITEMS = [
695
+ { id: 'workspace-os', icon: 'ti-layout-dashboard', labelKey: 'nav_workspace' },
570
696
  { id: 'model-status', icon: 'ti-cpu-2', labelKey: 'nav_model_status' },
571
697
  { id: 'runtime', icon: 'ti-adjustments-cog', labelKey: 'nav_runtime' },
572
698
  { id: 'advanced-settings', icon: 'ti-settings', labelKey: 'nav_advanced_settings' },
573
699
  ];
574
700
 
701
+ const ADMIN_NAV_ITEMS = [
702
+ { id: 'admin-users', icon: 'ti-users', labelKey: 'nav_user_management' },
703
+ { id: 'admin-permissions', icon: 'ti-key', labelKey: 'nav_permission_management' },
704
+ { id: 'admin-audit', icon: 'ti-report-search', labelKey: 'nav_audit_logs' },
705
+ { id: 'admin-security', icon: 'ti-shield-check', labelKey: 'nav_security' },
706
+ { id: 'admin-sensitive-data', icon: 'ti-radar', labelKey: 'nav_sensitive_data' },
707
+ { id: 'admin-policies', icon: 'ti-building-skyscraper', labelKey: 'nav_org_policies' },
708
+ { id: 'private-vpc', icon: 'ti-cloud-lock', labelKey: 'nav_private_vpc' },
709
+ ];
710
+
575
711
  function navItemsForMode(mode) {
576
712
  if (mode === 'advanced') return [...BASE_NAV_ITEMS, ...ADVANCED_NAV_ITEMS];
577
- // 관리자 모드: 내 상태 보기 뒤에 고급 3개 + 관리자 대시보드
578
713
  if (mode === 'admin') return [
579
714
  ...BASE_NAV_ITEMS,
580
715
  ...ADVANCED_NAV_ITEMS,
581
- { id: 'admin-dashboard', icon: 'ti-shield-lock', labelKey: 'admin_dashboard' },
716
+ ...ADMIN_NAV_ITEMS,
582
717
  ];
583
718
  return BASE_NAV_ITEMS;
584
719
  }
@@ -627,12 +762,25 @@ const chatViewport = document.getElementById('chat-viewport');
627
762
  else if (id === 'knowledge') openDataGraph();
628
763
  else if (id === 'pipeline') openPipelineModal();
629
764
  else if (id === 'files') openLocalBrowser();
630
- else if (id === 'computer') openCuPanel();
765
+ else if (id === 'my-computer') openCuPanel();
766
+ else if (id === 'search') {
767
+ showChat();
768
+ markActiveNav('search');
769
+ const search = document.getElementById('history-search-input');
770
+ if (search) search.focus();
771
+ }
772
+ else if (id === 'new-chat') startNewChat();
631
773
  else if (id === 'status') openStatusPanel();
632
774
  else if (id === 'model-status') openStatusPanel();
633
775
  else if (id === 'runtime') openModelPanel();
634
776
  else if (id === 'advanced-settings') openAdvancedSettingsPanel();
635
777
  else if (id === 'admin-dashboard') openAdminPanel();
778
+ else if (id === 'admin-users') window.location.href = `${API_BASE}/admin#users`;
779
+ else if (id === 'admin-permissions') window.location.href = `${API_BASE}/admin#permissions`;
780
+ else if (id === 'admin-audit') window.location.href = `${API_BASE}/admin#audit`;
781
+ else if (id === 'admin-security' || id === 'admin-sensitive-data') window.location.href = `${API_BASE}/admin#security`;
782
+ else if (id === 'admin-policies') window.location.href = `${API_BASE}/admin#enterprise`;
783
+ else if (id === 'private-vpc') openVpcPanel();
636
784
  }
637
785
 
638
786
  function focusChatInput() {
@@ -731,12 +879,12 @@ const chatViewport = document.getElementById('chat-viewport');
731
879
  focusChatInput();
732
880
  }
733
881
 
734
- function openAdvancedSettingsPanel() {
735
- document.getElementById('advanced-settings-overlay')?.classList.add('open');
736
- }
882
+ function openAdvancedSettingsPanel() {
883
+ showModalLayer('advanced-settings-overlay');
884
+ }
737
885
 
738
- function closeAdvancedSettingsPanel() {
739
- document.getElementById('advanced-settings-overlay')?.classList.remove('open');
886
+ function closeAdvancedSettingsPanel() {
887
+ closeModalLayer('advanced-settings-overlay');
740
888
  }
741
889
 
742
890
  function updateWorkspaceModeUi() {
@@ -778,19 +926,19 @@ const chatViewport = document.getElementById('chat-viewport');
778
926
  }
779
927
 
780
928
  function selectWorkspace(kind) {
781
- setWorkspacePreference(kind);
782
- document.getElementById('workspace-modal-overlay')?.classList.remove('open');
783
- updateWorkspaceModeUi();
784
- openModeSelector();
785
- }
929
+ setWorkspacePreference(kind);
930
+ closeModalLayer('workspace-modal-overlay');
931
+ updateWorkspaceModeUi();
932
+ openModeSelector();
933
+ }
786
934
 
787
- function openModeSelector() {
788
- updateWorkspaceModeUi();
789
- document.getElementById('mode-modal-overlay')?.classList.add('open');
790
- }
935
+ function openModeSelector() {
936
+ updateWorkspaceModeUi();
937
+ showModalLayer('mode-modal-overlay');
938
+ }
791
939
 
792
- function closeModeSelector() {
793
- document.getElementById('mode-modal-overlay')?.classList.remove('open');
940
+ function closeModeSelector() {
941
+ closeModalLayer('mode-modal-overlay');
794
942
  }
795
943
 
796
944
  function selectMode(mode) {
@@ -829,12 +977,12 @@ const chatViewport = document.getElementById('chat-viewport');
829
977
  async function startOnboardingIfNeeded(force = false) {
830
978
  updateWorkspaceModeUi();
831
979
  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');
980
+ const hasLoadedModel = await modelLoadedForChat();
981
+ if (!force && !forceAfterLogin && onboardingComplete() && hasLoadedModel) {
982
+ closeModalLayer('onboarding-overlay');
983
+ return;
984
+ }
985
+ showModalLayer('onboarding-overlay');
838
986
  if (forceAfterLogin || !hasLoadedModel) {
839
987
  renderPcAnalysis();
840
988
  } else {
@@ -1051,7 +1199,7 @@ const chatViewport = document.getElementById('chat-viewport');
1051
1199
  // Top pick callout
1052
1200
  const top = rec.top_pick;
1053
1201
  const topHtml = top ? `
1054
- <div style="border:1px solid #16a34a;background:#f0fdf4;border-radius:10px;padding:10px 12px;margin:8px 0">
1202
+ <div style="border:1px solid var(--success);background:var(--accent-soft);color:var(--text);border-radius:10px;padding:10px 12px;margin:8px 0">
1055
1203
  <div style="font-weight:700">⭐ Best for this PC — ${escapeHtml(top.name || top.id)} ${badge('recommended')}</div>
1056
1204
  <div style="font-size:12px;opacity:0.8;margin-top:3px">${escapeHtml(top.reason || '')}</div>
1057
1205
  <div style="font-size:12px;margin-top:4px">${escapeHtml(top.size || '')} · ${escapeHtml(ram(top))} · ${escapeHtml(nextStep(rec.engine))}</div>
@@ -1362,12 +1510,12 @@ const chatViewport = document.getElementById('chat-viewport');
1362
1510
  `);
1363
1511
  }
1364
1512
 
1365
- function finishOnboarding(mode) {
1366
- selectMode(mode);
1367
- setOnboardingComplete();
1368
- document.getElementById('onboarding-overlay')?.classList.remove('open');
1369
- showChat();
1370
- }
1513
+ function finishOnboarding(mode) {
1514
+ selectMode(mode);
1515
+ setOnboardingComplete();
1516
+ closeModalLayer('onboarding-overlay');
1517
+ showChat();
1518
+ }
1371
1519
 
1372
1520
  function switchAcctTab(tab) {
1373
1521
  ['profile', 'password'].forEach(t => {
@@ -1390,10 +1538,10 @@ const chatViewport = document.getElementById('chat-viewport');
1390
1538
  document.getElementById('profile-nickname').value = data.nickname || '';
1391
1539
  }
1392
1540
  } catch {}
1393
- document.getElementById('acct-modal-overlay').classList.add('open');
1541
+ showModalLayer('acct-modal-overlay');
1394
1542
  }
1395
1543
  function closeAcctModal() {
1396
- document.getElementById('acct-modal-overlay').classList.remove('open');
1544
+ closeModalLayer('acct-modal-overlay');
1397
1545
  }
1398
1546
  document.addEventListener('click', (e) => {
1399
1547
  const overlay = document.getElementById('acct-modal-overlay');
@@ -1707,7 +1855,7 @@ const chatViewport = document.getElementById('chat-viewport');
1707
1855
  }
1708
1856
 
1709
1857
  async function openModelPanel() {
1710
- document.getElementById('model-overlay').style.display = 'flex';
1858
+ showModalLayer('model-overlay');
1711
1859
  document.getElementById('model-list').innerHTML = '<div class="sensitivity-preview">실행 엔진과 모델 목록을 불러오는 중입니다...</div>';
1712
1860
  try {
1713
1861
  const res = await apiFetch('/engines');
@@ -1742,7 +1890,7 @@ const chatViewport = document.getElementById('chat-viewport');
1742
1890
  }
1743
1891
 
1744
1892
  function closeModelPanel() {
1745
- document.getElementById('model-overlay').style.display = 'none';
1893
+ closeModalLayer('model-overlay');
1746
1894
  }
1747
1895
 
1748
1896
  async function installEngine(engineId) {
@@ -2194,7 +2342,9 @@ const chatViewport = document.getElementById('chat-viewport');
2194
2342
  if (!t) {
2195
2343
  t = document.createElement('div');
2196
2344
  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;';
2345
+ // Styling lives in static/css/responsive.css (#ltcai-toast) so the
2346
+ // toast is token-driven and adapts to light/dark. Only the opacity
2347
+ // animation state is toggled inline below.
2198
2348
  document.body.appendChild(t);
2199
2349
  }
2200
2350
  t.textContent = msg;
@@ -2204,7 +2354,7 @@ const chatViewport = document.getElementById('chat-viewport');
2204
2354
  }
2205
2355
 
2206
2356
  function closeAdminPanel() {
2207
- document.getElementById('admin-overlay').style.display = 'none';
2357
+ closeModalLayer('admin-overlay');
2208
2358
  }
2209
2359
 
2210
2360
  // ── VPC Panel ────────────────────────────────────────
@@ -2235,7 +2385,7 @@ const chatViewport = document.getElementById('chat-viewport');
2235
2385
  }
2236
2386
 
2237
2387
  async function openVpcPanel() {
2238
- document.getElementById('vpc-overlay').style.display = 'flex';
2388
+ showModalLayer('vpc-overlay');
2239
2389
  try {
2240
2390
  const res = await apiFetch('/vpc/status');
2241
2391
  if (res.ok) fillVpcPanelForm(await res.json());
@@ -2243,7 +2393,7 @@ const chatViewport = document.getElementById('chat-viewport');
2243
2393
  }
2244
2394
 
2245
2395
  function closeVpcPanel() {
2246
- document.getElementById('vpc-overlay').style.display = 'none';
2396
+ closeModalLayer('vpc-overlay');
2247
2397
  }
2248
2398
 
2249
2399
  function selectVpcProvider(name, btn) {
@@ -2293,7 +2443,7 @@ const chatViewport = document.getElementById('chat-viewport');
2293
2443
 
2294
2444
  // ── Status Summary Panel ─────────────────────────────
2295
2445
  async function openStatusPanel() {
2296
- document.getElementById('status-overlay').style.display = 'flex';
2446
+ showModalLayer('status-overlay');
2297
2447
  document.getElementById('status-panel-body').innerHTML = '<div class="sensitivity-preview">상태를 불러오는 중...</div>';
2298
2448
  try {
2299
2449
  const mode = getCurrentMode();
@@ -2362,7 +2512,7 @@ const chatViewport = document.getElementById('chat-viewport');
2362
2512
  }
2363
2513
 
2364
2514
  function closeStatusPanel() {
2365
- document.getElementById('status-overlay').style.display = 'none';
2515
+ closeModalLayer('status-overlay');
2366
2516
  }
2367
2517
 
2368
2518
  // ── 파일 생성 패널 ────────────────────────────────────
@@ -2415,11 +2565,11 @@ const chatViewport = document.getElementById('chat-viewport');
2415
2565
  </div>`;
2416
2566
  }
2417
2567
  document.getElementById('file-create-form').innerHTML = formHtml;
2418
- document.getElementById('file-create-overlay').style.display = 'flex';
2568
+ showModalLayer('file-create-overlay');
2419
2569
  }
2420
2570
 
2421
2571
  function closeFileCreate() {
2422
- document.getElementById('file-create-overlay').style.display = 'none';
2572
+ closeModalLayer('file-create-overlay');
2423
2573
  }
2424
2574
 
2425
2575
  function _formatBytes(b) {
@@ -2507,24 +2657,24 @@ const chatViewport = document.getElementById('chat-viewport');
2507
2657
  document.getElementById('perm-title').textContent = '파일 접근 요청';
2508
2658
  document.getElementById('perm-path').textContent = path;
2509
2659
  document.getElementById('perm-desc').textContent = `AI가 아래 경로에 대한 "${actionLabel}" 작업을 요청합니다. 허용하시겠습니까?`;
2510
- document.getElementById('perm-overlay').style.display = 'flex';
2660
+ showModalLayer('perm-overlay', { restorePrevious: true });
2511
2661
  });
2512
2662
  }
2513
2663
 
2514
2664
  function resolvePermission(allowed) {
2515
- document.getElementById('perm-overlay').style.display = 'none';
2665
+ closeModalLayer('perm-overlay');
2516
2666
  if (_permResolve) { _permResolve(allowed); _permResolve = null; }
2517
2667
  }
2518
2668
 
2519
2669
  let _localCurrentPath = '~';
2520
2670
 
2521
2671
  async function openLocalBrowser() {
2522
- document.getElementById('local-browser-overlay').style.display = 'flex';
2672
+ showModalLayer('local-browser-overlay');
2523
2673
  await localNav(_localCurrentPath || '~');
2524
2674
  }
2525
2675
 
2526
2676
  function closeLocalBrowser() {
2527
- document.getElementById('local-browser-overlay').style.display = 'none';
2677
+ closeModalLayer('local-browser-overlay');
2528
2678
  }
2529
2679
 
2530
2680
  async function localNav(path) {
@@ -2710,8 +2860,8 @@ const chatViewport = document.getElementById('chat-viewport');
2710
2860
  document.getElementById('editor-filepath').textContent = path;
2711
2861
  document.getElementById('file-editor-content').value = text;
2712
2862
  document.getElementById('editor-status').textContent = '';
2713
- document.getElementById('local-browser-overlay').style.display = 'none';
2714
- document.getElementById('file-editor-overlay').style.display = 'flex';
2863
+ closeModalLayer('local-browser-overlay', { skipRestore: true });
2864
+ showModalLayer('file-editor-overlay');
2715
2865
  } catch(e) {
2716
2866
  resultEl.innerHTML = `
2717
2867
  <div class="sensitivity-preview">⚠️ 문서 읽기 실패: ${escapeHtml(e.message)}<br>
@@ -2761,15 +2911,15 @@ const chatViewport = document.getElementById('chat-viewport');
2761
2911
  document.getElementById('editor-filepath').textContent = path;
2762
2912
  document.getElementById('file-editor-content').value = content;
2763
2913
  document.getElementById('editor-status').textContent = '';
2764
- document.getElementById('local-browser-overlay').style.display = 'none';
2765
- document.getElementById('file-editor-overlay').style.display = 'flex';
2914
+ closeModalLayer('local-browser-overlay', { skipRestore: true });
2915
+ showModalLayer('file-editor-overlay');
2766
2916
  } catch(e) {
2767
2917
  resultEl.innerHTML = `<div class="sensitivity-preview">${escapeHtml(e.message)}</div>`;
2768
2918
  }
2769
2919
  }
2770
2920
 
2771
2921
  function closeFileEditor() {
2772
- document.getElementById('file-editor-overlay').style.display = 'none';
2922
+ closeModalLayer('file-editor-overlay');
2773
2923
  }
2774
2924
 
2775
2925
  async function saveLocalFile() {
@@ -2860,8 +3010,8 @@ const chatViewport = document.getElementById('chat-viewport');
2860
3010
  document.getElementById('file-editor-content').value = text;
2861
3011
  document.getElementById('editor-status').textContent = '⚠️ 이미지/표 등 비텍스트 요소는 표시되지 않을 수 있습니다';
2862
3012
  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';
3013
+ closeModalLayer('local-browser-overlay', { skipRestore: true });
3014
+ showModalLayer('file-editor-overlay');
2865
3015
  } catch(e) {
2866
3016
  resultEl.innerHTML = `<div class="sensitivity-preview">텍스트 추출 실패: ${escapeHtml(e.message)}</div>`;
2867
3017
  }
@@ -4186,12 +4336,12 @@ const chatViewport = document.getElementById('chat-viewport');
4186
4336
  let cuAgentAbort = null;
4187
4337
 
4188
4338
  async function openCuPanel() {
4189
- document.getElementById('cu-overlay').style.display = 'flex';
4339
+ showModalLayer('cu-overlay');
4190
4340
  await cuRefreshStatus();
4191
4341
  }
4192
4342
 
4193
4343
  function closeCuPanel() {
4194
- document.getElementById('cu-overlay').style.display = 'none';
4344
+ closeModalLayer('cu-overlay');
4195
4345
  }
4196
4346
 
4197
4347
  async function cuRefreshStatus() {
@@ -4358,12 +4508,12 @@ const chatViewport = document.getElementById('chat-viewport');
4358
4508
  let _wizItems = []; // items selected by user in step 2
4359
4509
 
4360
4510
  function openSetupWizard() {
4361
- document.getElementById('setup-overlay').classList.add('open');
4511
+ showModalLayer('setup-overlay');
4362
4512
  _runStep1();
4363
4513
  }
4364
4514
 
4365
4515
  function closeSetupWizard() {
4366
- document.getElementById('setup-overlay').classList.remove('open');
4516
+ closeModalLayer('setup-overlay');
4367
4517
  }
4368
4518
 
4369
4519
  // Prevent click-through on overlay background
@@ -4678,12 +4828,12 @@ const chatViewport = document.getElementById('chat-viewport');
4678
4828
  let _mcpCurrentTab = 'registry';
4679
4829
 
4680
4830
  async function openMcpModal() {
4681
- document.getElementById('mcp-modal-overlay').classList.add('open');
4831
+ showModalLayer('mcp-modal-overlay');
4682
4832
  await renderMcpModal(_mcpCurrentTab);
4683
4833
  }
4684
4834
 
4685
4835
  function closeMcpModal() {
4686
- document.getElementById('mcp-modal-overlay').classList.remove('open');
4836
+ closeModalLayer('mcp-modal-overlay');
4687
4837
  }
4688
4838
 
4689
4839
  function switchMcpTab(tab, btn) {