ltcai 0.3.0 → 0.3.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.
@@ -11,6 +11,8 @@ const chatViewport = document.getElementById('chat-viewport');
11
11
  let currentUserNickname = "Guest";
12
12
  let currentUserEmail = "";
13
13
  let isAdmin = false;
14
+ let telegramHistorySyncEnabled = false;
15
+ let telegramHistorySyncInFlight = false;
14
16
 
15
17
  // ── 멀티 LLM 파이프라인 상태 ──
16
18
  let pipelineConfig = { planning: null, executing: null, reviewing: null };
@@ -517,6 +519,32 @@ const chatViewport = document.getElementById('chat-viewport');
517
519
  localStorage.setItem(scopedStorageKey('onboarding_complete'), 'true');
518
520
  }
519
521
 
522
+ function consumeSetupAfterLoginFlag() {
523
+ try {
524
+ const params = new URLSearchParams(window.location.search);
525
+ const value = params.get('setup') === '1' || sessionStorage.getItem('ltcai_force_setup_after_login') === 'true';
526
+ sessionStorage.removeItem('ltcai_force_setup_after_login');
527
+ if (params.get('setup') === '1') {
528
+ const cleanUrl = `${window.location.pathname}${window.location.hash || ''}`;
529
+ window.history.replaceState({}, '', cleanUrl);
530
+ }
531
+ return value;
532
+ } catch (_) {
533
+ return false;
534
+ }
535
+ }
536
+
537
+ async function modelLoadedForChat() {
538
+ try {
539
+ const res = await apiFetch('/health');
540
+ if (!res.ok) return false;
541
+ const data = await res.json();
542
+ return Boolean(data.current_model || (data.loaded_models || []).length);
543
+ } catch (_) {
544
+ return false;
545
+ }
546
+ }
547
+
520
548
  function workspaceLabel(kind) {
521
549
  if (kind === 'company') return currentLang === 'ko' ? '회사 워크스페이스' : 'Company Workspace';
522
550
  return currentLang === 'ko' ? '개인 워크스페이스' : 'Personal Workspace';
@@ -799,14 +827,20 @@ const chatViewport = document.getElementById('chat-viewport');
799
827
  let onboardingRecs = null;
800
828
  let onboardingSelectedModel = null;
801
829
 
802
- function startOnboardingIfNeeded(force = false) {
830
+ async function startOnboardingIfNeeded(force = false) {
803
831
  updateWorkspaceModeUi();
804
- if (!force && onboardingComplete()) {
832
+ const forceAfterLogin = consumeSetupAfterLoginFlag();
833
+ const hasLoadedModel = await modelLoadedForChat();
834
+ if (!force && !forceAfterLogin && onboardingComplete() && hasLoadedModel) {
805
835
  document.getElementById('onboarding-overlay')?.classList.remove('open');
806
836
  return;
807
837
  }
808
838
  document.getElementById('onboarding-overlay')?.classList.add('open');
809
- renderOnboardingWorkspace();
839
+ if (forceAfterLogin || !hasLoadedModel) {
840
+ renderPcAnalysis();
841
+ } else {
842
+ renderOnboardingWorkspace();
843
+ }
810
844
  }
811
845
 
812
846
  function renderOnboardingShell(stepIndex, bodyHtml, actionsHtml = '') {
@@ -1260,7 +1294,7 @@ const chatViewport = document.getElementById('chat-viewport');
1260
1294
  selectMode(mode);
1261
1295
  setOnboardingComplete();
1262
1296
  document.getElementById('onboarding-overlay')?.classList.remove('open');
1263
- showHome();
1297
+ showChat();
1264
1298
  }
1265
1299
 
1266
1300
  function switchAcctTab(tab) {
@@ -1836,7 +1870,17 @@ const chatViewport = document.getElementById('chat-viewport');
1836
1870
  if (!finalData) throw new Error('모델 준비 응답이 비어 있습니다.');
1837
1871
  closeModelPanel();
1838
1872
  await loadModelStatus();
1839
- addMessage('ai', `<b>${escapeHtml(compactModelName(finalData.current || modelId))}</b> 로드 되었습니다.`);
1873
+ // 피드백 #1/#2: 사용자가 클릭한 modelId 아니라 백엔드가 돌려준 current를 신뢰한다.
1874
+ const actualCurrent = finalData.current || (finalData.resolution && finalData.resolution.expected_current) || modelId;
1875
+ window.__latticeActiveModel = actualCurrent;
1876
+ let statusLine = `<b>${escapeHtml(compactModelName(actualCurrent))}</b> 로드 되었습니다.`;
1877
+ if (finalData.ready_to_chat === false) {
1878
+ const reason = (finalData.smoke_test && finalData.smoke_test.reason) || '채팅 호환성 검사 실패';
1879
+ statusLine += `<br><span class="sensitivity-preview">⚠️ 현재 채팅 호환성이 낮습니다 (${escapeHtml(reason)}). 다른 실행 엔진을 추천합니다.</span>`;
1880
+ } else if (finalData.compatibility_status === 'unknown') {
1881
+ statusLine += `<br><span class="sensitivity-preview">호환성 테스트를 완료하지 못했습니다. 채팅이 가능하지만 답변 품질이 일정하지 않을 수 있어요.</span>`;
1882
+ }
1883
+ addMessage('ai', statusLine);
1840
1884
  } catch (e) {
1841
1885
  document.getElementById('model-list').innerHTML = `
1842
1886
  <div class="sensitivity-preview">${escapeHtml(e.message)}</div>
@@ -1849,6 +1893,19 @@ const chatViewport = document.getElementById('chat-viewport');
1849
1893
  return prepareAndLoadModel(encodedId, engine);
1850
1894
  }
1851
1895
 
1896
+ // 피드백 #1/#2: 사용자가 직접 모델을 선택했을 때도 같은 표준 흐름을 타도록 노출.
1897
+ async function selectModelByCard(card) {
1898
+ if (!card || !card.id) {
1899
+ throw new Error('모델 카드가 비어 있습니다.');
1900
+ }
1901
+ const encoded = encodeURIComponent(card.id);
1902
+ const engine = card.engine || (Array.isArray(card.engine_options) && card.engine_options[0]?.engine) || '';
1903
+ return prepareAndLoadModel(encoded, engine);
1904
+ }
1905
+ if (typeof window !== 'undefined') {
1906
+ window.selectModelByCard = selectModelByCard;
1907
+ }
1908
+
1852
1909
  function fillVpcForm(config) {
1853
1910
  if (!config) return;
1854
1911
  document.getElementById('vpc-provider').value = config.provider || '';
@@ -1938,7 +1995,7 @@ const chatViewport = document.getElementById('chat-viewport');
1938
1995
  if (!t) {
1939
1996
  t = document.createElement('div');
1940
1997
  t.id = 'ltcai-toast';
1941
- t.style.cssText = 'position:fixed;bottom:28px;left:50%;transform:translateX(-50%);background:#1e2330;color:#f8fafc;border:1px solid rgba(255,255,255,0.12);border-radius:10px;padding:10px 18px;font-size:13px;font-weight:600;z-index:9999;box-shadow:0 8px 24px rgba(0,0,0,0.4);pointer-events:none;transition:opacity .2s;';
1998
+ 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;';
1942
1999
  document.body.appendChild(t);
1943
2000
  }
1944
2001
  t.textContent = msg;
@@ -3264,10 +3321,13 @@ const chatViewport = document.getElementById('chat-viewport');
3264
3321
  }
3265
3322
 
3266
3323
  async function syncTelegramHistory() {
3324
+ if (!telegramHistorySyncEnabled || telegramHistorySyncInFlight) return;
3325
+ telegramHistorySyncInFlight = true;
3267
3326
  try {
3268
3327
  const res = await apiFetch('/history');
3269
3328
  if (!res.ok) return;
3270
3329
  const history = await res.json();
3330
+ let added = false;
3271
3331
  for (const item of history) {
3272
3332
  if (item.source !== 'telegram') continue;
3273
3333
  const key = `${item.timestamp || ''}:${item.role}:${item.content}`;
@@ -3278,9 +3338,13 @@ const chatViewport = document.getElementById('chat-viewport');
3278
3338
  ? 'Lattice AI'
3279
3339
  : (item.user_nickname || 'Telegram');
3280
3340
  addMessage(role, item.content || '', null, sender);
3341
+ added = true;
3281
3342
  }
3282
- loadHistory();
3343
+ if (added) loadHistory();
3283
3344
  } catch (e) { }
3345
+ finally {
3346
+ telegramHistorySyncInFlight = false;
3347
+ }
3284
3348
  }
3285
3349
 
3286
3350
  async function sendToAgent(text, extraCtx = '') {
@@ -3866,10 +3930,19 @@ const chatViewport = document.getElementById('chat-viewport');
3866
3930
  const res = await apiFetch('/runtime_features');
3867
3931
  if (res.ok) {
3868
3932
  const f = await res.json();
3933
+ telegramHistorySyncEnabled = Boolean(f.telegram_enabled);
3869
3934
  if (!f.graph_enabled) {
3870
3935
  const btn = document.getElementById('data-graph-btn');
3871
3936
  if (btn) btn.style.display = 'none';
3872
3937
  }
3938
+ return;
3939
+ }
3940
+ } catch (_) {}
3941
+ try {
3942
+ const res = await apiFetch('/health');
3943
+ if (res.ok) {
3944
+ const data = await res.json();
3945
+ telegramHistorySyncEnabled = Boolean(data.features?.telegram_enabled);
3873
3946
  }
3874
3947
  } catch (_) {}
3875
3948
  })();
@@ -3878,7 +3951,7 @@ const chatViewport = document.getElementById('chat-viewport');
3878
3951
  loadVpcStatus();
3879
3952
  restoreCurrentConversation();
3880
3953
  syncTelegramHistory();
3881
- setInterval(syncTelegramHistory, 2500);
3954
+ setInterval(syncTelegramHistory, 15000);
3882
3955
 
3883
3956
  // ── 내 컴퓨터 ──────────────────────────────────────────────────
3884
3957
  let cuAgentRunning = false;
@@ -4370,7 +4443,7 @@ const chatViewport = document.getElementById('chat-viewport');
4370
4443
  function _showComplete() {
4371
4444
  _subtitle('설정 완료!');
4372
4445
  _footInfo('');
4373
- _footBtns(`<button class="wbtn wbtn-primary" onclick="closeSetupWizard();loadModelStatus()">완료 ✓</button>`);
4446
+ _footBtns(`<button class="wbtn wbtn-primary" onclick="closeSetupWizard();loadModelStatus();showChat()">완료 ✓</button>`);
4374
4447
  }
4375
4448
 
4376
4449
  // ── MCP 관리 모달 ────────────────────────────────────────────────────────
@@ -324,8 +324,12 @@ const API_BASE = window.location.protocol === 'file:' ? 'http://localhost:4825'
324
324
  </div>
325
325
  ${rootRows ? `<div class="local-root-list">${rootRows}</div>` : ''}
326
326
  <div class="local-option-row">
327
- <label><input type="checkbox" ${localState.includeOcr ? 'checked' : ''} onchange="setLocalOption('includeOcr', this.checked)"> ${t('local_ocr')}</label>
328
- <label><input type="checkbox" ${localState.watchEnabled ? 'checked' : ''} onchange="setLocalOption('watchEnabled', this.checked)"> ${t('local_watch')}</label>
327
+ <button type="button" class="local-option-btn ${localState.includeOcr ? 'active' : ''}" onclick="setLocalOption('includeOcr', ${localState.includeOcr ? 'false' : 'true'})" aria-pressed="${localState.includeOcr ? 'true' : 'false'}">
328
+ <i class="ti ti-photo-scan"></i><span>${t('local_ocr')}</span>
329
+ </button>
330
+ <button type="button" class="local-option-btn ${localState.watchEnabled ? 'active' : ''}" onclick="setLocalOption('watchEnabled', ${localState.watchEnabled ? 'false' : 'true'})" aria-pressed="${localState.watchEnabled ? 'true' : 'false'}">
331
+ <i class="ti ti-refresh-dot"></i><span>${t('local_watch')}</span>
332
+ </button>
329
333
  </div>
330
334
  <div class="local-source-actions">
331
335
  <button class="local-source-btn" ${localState.busy ? 'disabled' : ''} onclick="runLocalTree()" title="${t('local_tree')}"><i class="ti ti-folders"></i>${t('local_tree')}</button>
package/static/sw.js CHANGED
@@ -1,6 +1,6 @@
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-v6";
3
+ const CACHE = "ltcai-v7";
4
4
  const STATIC = [
5
5
  "/",
6
6
  "/static/lattice-reference.css",