ltcai 0.3.0 → 0.3.2

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) {
@@ -1435,7 +1469,7 @@ const chatViewport = document.getElementById('chat-viewport');
1435
1469
  const icon = isUnavailable ? 'ti-lock' : (engineMissing || needsPull) ? 'ti-cloud-download' : verifyUnknown ? 'ti-activity' : 'ti-switch-3';
1436
1470
  const cls = (engineMissing || needsPull) && isLocalEngine ? ' needs-pull' : '';
1437
1471
  const action = isLocalEngine
1438
- ? `prepareAndLoadModel('${encodeURIComponent(model.id)}', '${engine?.id || ''}')`
1472
+ ? `selectModelByCard('${encodeURIComponent(model.id)}', '${engine?.id || ''}')`
1439
1473
  : `loadSelectedModel('${encodeURIComponent(model.id)}', '${engine?.id || ''}')`;
1440
1474
  return `
1441
1475
  <button class="model-option${cls}" ${isUnavailable ? 'disabled' : ''} onclick="${action}">
@@ -1635,7 +1669,17 @@ const chatViewport = document.getElementById('chat-viewport');
1635
1669
  if (!res.ok) throw new Error(data.detail || '모델 로드에 실패했습니다.');
1636
1670
  closeModelPanel();
1637
1671
  await loadModelStatus();
1638
- addMessage('ai', `모델을 <b>${escapeHtml(compactModelName(data.current || modelId))}</b>로 전환했습니다.`);
1672
+ // 피드백 #1/#2: 클라우드 경로도 백엔드 current 단일 진실원으로 사용한다.
1673
+ const actualCurrent = resolveActualCurrent(data, modelId);
1674
+ setCurrentModel(actualCurrent);
1675
+ updateCurrentModelUI(actualCurrent);
1676
+ let statusLine = `모델을 <b>${escapeHtml(compactModelName(actualCurrent))}</b>로 전환했습니다.`;
1677
+ const compat = describeCompatibility(data);
1678
+ if (compat) {
1679
+ statusLine += `<br><span class="sensitivity-preview">${escapeHtml(compat.message)}</span>`;
1680
+ showModelCompatibilityWarning(data);
1681
+ }
1682
+ addMessage('ai', statusLine);
1639
1683
  } catch (e) {
1640
1684
  document.getElementById('model-list').innerHTML = `
1641
1685
  <div class="sensitivity-preview">${escapeHtml(e.message)}</div>
@@ -1795,6 +1839,66 @@ const chatViewport = document.getElementById('chat-viewport');
1795
1839
  if (buffer.trim()) dispatchBlock(buffer.trim());
1796
1840
  }
1797
1841
 
1842
+ // 피드백 #1/#2: "사용자가 보는 현재 모델" === "실제로 채팅에 사용되는 모델".
1843
+ // 백엔드가 돌려준 current/resolution을 단일 진실원으로 사용한다.
1844
+ function resolveActualCurrent(finalData, fallbackId) {
1845
+ if (!finalData) return fallbackId || '';
1846
+ return (
1847
+ finalData.current
1848
+ || (finalData.resolution && (finalData.resolution.expected_current || finalData.resolution.resolved_model))
1849
+ || fallbackId
1850
+ || ''
1851
+ );
1852
+ }
1853
+
1854
+ function setCurrentModel(modelId) {
1855
+ if (!modelId) return;
1856
+ window.__latticeActiveModel = modelId;
1857
+ }
1858
+
1859
+ function updateCurrentModelUI(modelId) {
1860
+ if (!modelId) return;
1861
+ const modelEl = document.getElementById('ops-model');
1862
+ if (modelEl) {
1863
+ modelEl.textContent = compactModelName(modelId);
1864
+ modelEl.title = modelId;
1865
+ }
1866
+ const metaEl = document.getElementById('ops-model-meta');
1867
+ if (metaEl && !metaEl.dataset.loaded) {
1868
+ metaEl.dataset.loaded = 'true';
1869
+ }
1870
+ }
1871
+
1872
+ function describeCompatibility(finalData) {
1873
+ if (!finalData) return null;
1874
+ if (finalData.ready_to_chat === false) {
1875
+ const reason = (finalData.smoke_test && finalData.smoke_test.reason) || '채팅 호환성 검사 실패';
1876
+ return {
1877
+ severity: 'degraded',
1878
+ message: `⚠️ 채팅 호환성이 낮습니다 (${reason}). 다른 실행 엔진을 추천합니다.`,
1879
+ };
1880
+ }
1881
+ if (finalData.compatibility_status === 'degraded') {
1882
+ return {
1883
+ severity: 'degraded',
1884
+ message: '⚠️ 모델은 로드됐지만 호환성 테스트가 degraded로 나왔습니다. 답변 품질이 일정하지 않을 수 있어요.',
1885
+ };
1886
+ }
1887
+ if (finalData.compatibility_status === 'unknown') {
1888
+ return {
1889
+ severity: 'unknown',
1890
+ message: '호환성 테스트를 완료하지 못했습니다. 채팅이 가능하지만 답변 품질이 일정하지 않을 수 있어요.',
1891
+ };
1892
+ }
1893
+ return null;
1894
+ }
1895
+
1896
+ function showModelCompatibilityWarning(finalData) {
1897
+ const info = describeCompatibility(finalData);
1898
+ if (!info) return;
1899
+ try { showToast(info.message); } catch (_) {}
1900
+ }
1901
+
1798
1902
  async function prepareAndLoadModel(encodedId, engine = '') {
1799
1903
  const modelId = decodeURIComponent(encodedId);
1800
1904
  const displayName = compactModelName(modelId);
@@ -1836,12 +1940,22 @@ const chatViewport = document.getElementById('chat-viewport');
1836
1940
  if (!finalData) throw new Error('모델 준비 응답이 비어 있습니다.');
1837
1941
  closeModelPanel();
1838
1942
  await loadModelStatus();
1839
- addMessage('ai', `<b>${escapeHtml(compactModelName(finalData.current || modelId))}</b> 로드 되었습니다.`);
1943
+ const actualCurrent = resolveActualCurrent(finalData, modelId);
1944
+ setCurrentModel(actualCurrent);
1945
+ updateCurrentModelUI(actualCurrent);
1946
+ let statusLine = `<b>${escapeHtml(compactModelName(actualCurrent))}</b> 로드 되었습니다.`;
1947
+ const compat = describeCompatibility(finalData);
1948
+ if (compat) {
1949
+ statusLine += `<br><span class="sensitivity-preview">${escapeHtml(compat.message)}</span>`;
1950
+ }
1951
+ addMessage('ai', statusLine);
1952
+ return finalData;
1840
1953
  } catch (e) {
1841
1954
  document.getElementById('model-list').innerHTML = `
1842
1955
  <div class="sensitivity-preview">${escapeHtml(e.message)}</div>
1843
1956
  <button class="admin-action" onclick="openModelPanel()" style="margin-top: 12px;">목록으로 돌아가기</button>
1844
1957
  `;
1958
+ throw e;
1845
1959
  }
1846
1960
  }
1847
1961
 
@@ -1849,6 +1963,38 @@ const chatViewport = document.getElementById('chat-viewport');
1849
1963
  return prepareAndLoadModel(encodedId, engine);
1850
1964
  }
1851
1965
 
1966
+ // 피드백 #1/#2: 모델 카드 클릭 → prepare/load → smoke test → current 반영 → 채팅 가능 여부 표시
1967
+ // 가 하나의 흐름으로 이어지도록 한다. encodedId/engine 또는 card 객체 양쪽 모두 받는다.
1968
+ async function selectModelByCard(modelIdOrCard, engineArg) {
1969
+ let encoded;
1970
+ let engine = engineArg || '';
1971
+ if (typeof modelIdOrCard === 'string') {
1972
+ encoded = modelIdOrCard.includes('%') ? modelIdOrCard : encodeURIComponent(modelIdOrCard);
1973
+ } else if (modelIdOrCard && modelIdOrCard.id) {
1974
+ encoded = encodeURIComponent(modelIdOrCard.id);
1975
+ if (!engine) {
1976
+ engine = modelIdOrCard.engine
1977
+ || (Array.isArray(modelIdOrCard.engine_options) && modelIdOrCard.engine_options[0]?.engine)
1978
+ || '';
1979
+ }
1980
+ } else {
1981
+ throw new Error('모델 카드가 비어 있습니다.');
1982
+ }
1983
+ const result = await prepareAndLoadModel(encoded, engine);
1984
+ if (result && result.current) {
1985
+ setCurrentModel(result.current);
1986
+ updateCurrentModelUI(result.current);
1987
+ }
1988
+ if (result && (result.ready_to_chat === false || result.compatibility_status === 'degraded')) {
1989
+ showModelCompatibilityWarning(result);
1990
+ }
1991
+ return result;
1992
+ }
1993
+ if (typeof window !== 'undefined') {
1994
+ window.selectModelByCard = selectModelByCard;
1995
+ window.prepareAndLoadModel = prepareAndLoadModel;
1996
+ }
1997
+
1852
1998
  function fillVpcForm(config) {
1853
1999
  if (!config) return;
1854
2000
  document.getElementById('vpc-provider').value = config.provider || '';
@@ -1938,7 +2084,7 @@ const chatViewport = document.getElementById('chat-viewport');
1938
2084
  if (!t) {
1939
2085
  t = document.createElement('div');
1940
2086
  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;';
2087
+ 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
2088
  document.body.appendChild(t);
1943
2089
  }
1944
2090
  t.textContent = msg;
@@ -3264,10 +3410,13 @@ const chatViewport = document.getElementById('chat-viewport');
3264
3410
  }
3265
3411
 
3266
3412
  async function syncTelegramHistory() {
3413
+ if (!telegramHistorySyncEnabled || telegramHistorySyncInFlight) return;
3414
+ telegramHistorySyncInFlight = true;
3267
3415
  try {
3268
3416
  const res = await apiFetch('/history');
3269
3417
  if (!res.ok) return;
3270
3418
  const history = await res.json();
3419
+ let added = false;
3271
3420
  for (const item of history) {
3272
3421
  if (item.source !== 'telegram') continue;
3273
3422
  const key = `${item.timestamp || ''}:${item.role}:${item.content}`;
@@ -3278,9 +3427,13 @@ const chatViewport = document.getElementById('chat-viewport');
3278
3427
  ? 'Lattice AI'
3279
3428
  : (item.user_nickname || 'Telegram');
3280
3429
  addMessage(role, item.content || '', null, sender);
3430
+ added = true;
3281
3431
  }
3282
- loadHistory();
3432
+ if (added) loadHistory();
3283
3433
  } catch (e) { }
3434
+ finally {
3435
+ telegramHistorySyncInFlight = false;
3436
+ }
3284
3437
  }
3285
3438
 
3286
3439
  async function sendToAgent(text, extraCtx = '') {
@@ -3866,10 +4019,19 @@ const chatViewport = document.getElementById('chat-viewport');
3866
4019
  const res = await apiFetch('/runtime_features');
3867
4020
  if (res.ok) {
3868
4021
  const f = await res.json();
4022
+ telegramHistorySyncEnabled = Boolean(f.telegram_enabled);
3869
4023
  if (!f.graph_enabled) {
3870
4024
  const btn = document.getElementById('data-graph-btn');
3871
4025
  if (btn) btn.style.display = 'none';
3872
4026
  }
4027
+ return;
4028
+ }
4029
+ } catch (_) {}
4030
+ try {
4031
+ const res = await apiFetch('/health');
4032
+ if (res.ok) {
4033
+ const data = await res.json();
4034
+ telegramHistorySyncEnabled = Boolean(data.features?.telegram_enabled);
3873
4035
  }
3874
4036
  } catch (_) {}
3875
4037
  })();
@@ -3878,7 +4040,7 @@ const chatViewport = document.getElementById('chat-viewport');
3878
4040
  loadVpcStatus();
3879
4041
  restoreCurrentConversation();
3880
4042
  syncTelegramHistory();
3881
- setInterval(syncTelegramHistory, 2500);
4043
+ setInterval(syncTelegramHistory, 15000);
3882
4044
 
3883
4045
  // ── 내 컴퓨터 ──────────────────────────────────────────────────
3884
4046
  let cuAgentRunning = false;
@@ -4370,7 +4532,7 @@ const chatViewport = document.getElementById('chat-viewport');
4370
4532
  function _showComplete() {
4371
4533
  _subtitle('설정 완료!');
4372
4534
  _footInfo('');
4373
- _footBtns(`<button class="wbtn wbtn-primary" onclick="closeSetupWizard();loadModelStatus()">완료 ✓</button>`);
4535
+ _footBtns(`<button class="wbtn wbtn-primary" onclick="closeSetupWizard();loadModelStatus();showChat()">완료 ✓</button>`);
4374
4536
  }
4375
4537
 
4376
4538
  // ── 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",