ltcai 2.1.0 → 2.2.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.
- package/README.md +153 -609
- package/auto_setup.py +17 -17
- package/docs/CHANGELOG.md +83 -0
- package/docs/MULTI_AGENT_RUNTIME.md +4 -4
- package/docs/PLUGIN_SDK.md +7 -7
- package/docs/REALTIME_COLLABORATION.md +6 -6
- package/docs/V2_ARCHITECTURE.md +45 -25
- package/docs/WORKFLOW_DESIGNER.md +4 -4
- package/docs/architecture.md +127 -135
- package/docs/kg-schema.md +3 -3
- package/docs/public-deploy.md +2 -3
- package/docs/spec-vs-impl.md +13 -10
- package/knowledge_graph.py +2 -2
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/models.py +8 -0
- package/latticeai/core/config.py +1 -1
- package/latticeai/core/graph_curator.py +2 -2
- package/latticeai/core/marketplace.py +2 -2
- package/latticeai/core/model_compat.py +7 -63
- package/latticeai/core/model_resolution.py +1 -1
- package/latticeai/core/multi_agent.py +1 -1
- package/latticeai/core/plugins.py +1 -1
- package/latticeai/core/realtime.py +1 -1
- package/latticeai/core/workflow_engine.py +1 -1
- package/latticeai/core/workspace_os.py +1 -1
- package/latticeai/server_app.py +1 -1
- package/latticeai/services/model_catalog.py +105 -153
- package/latticeai/services/model_recommendation.py +28 -17
- package/latticeai/services/model_runtime.py +2 -2
- package/llm_router.py +80 -92
- package/ltcai_cli.py +2 -3
- package/package.json +8 -3
- package/static/account.html +3 -1
- package/static/activity.html +5 -2
- package/static/admin.html +5 -1
- package/static/agents.html +5 -2
- package/static/chat.html +12 -10
- package/static/css/responsive.css +597 -0
- package/static/css/tokens.css +224 -165
- package/static/graph.html +12 -2
- package/static/lattice-reference.css +366 -739
- package/static/platform.css +45 -16
- package/static/plugins.html +5 -2
- package/static/scripts/admin.js +33 -33
- package/static/scripts/chat.js +109 -42
- package/static/scripts/graph.js +169 -11
- package/static/scripts/ux.js +167 -0
- package/static/workflows.html +5 -2
- package/static/workspace.css +55 -19
- package/static/workspace.html +5 -2
- package/telegram_bot.py +1 -1
package/static/scripts/graph.js
CHANGED
|
@@ -181,6 +181,7 @@ const API_BASE = window.location.protocol === 'file:' ? 'http://localhost:4825'
|
|
|
181
181
|
let rawGraph = { nodes: [], edges: [] };
|
|
182
182
|
let graph = { nodes: [], edges: [] };
|
|
183
183
|
let hiddenTypes = new Set();
|
|
184
|
+
let hiddenEdgeTypes = new Set();
|
|
184
185
|
let selected = null;
|
|
185
186
|
let hovered = null;
|
|
186
187
|
let dragging = null;
|
|
@@ -500,6 +501,21 @@ const API_BASE = window.location.protocol === 'file:' ? 'http://localhost:4825'
|
|
|
500
501
|
window.runLocalIndex = runLocalIndex;
|
|
501
502
|
window.approveLocalPermission = approveLocalPermission;
|
|
502
503
|
|
|
504
|
+
/* 테마 색상 — CSS 변수에서 캔버스 배경/텍스트를 읽어 다크모드 대응 */
|
|
505
|
+
let themeColors = { bg: '#ffffff', text: '#14162c', surface: '#ffffff' };
|
|
506
|
+
function refreshThemeColors() {
|
|
507
|
+
const cs = getComputedStyle(document.documentElement);
|
|
508
|
+
const read = (name, fallback) => {
|
|
509
|
+
const v = (cs.getPropertyValue(name) || '').trim();
|
|
510
|
+
return v || fallback;
|
|
511
|
+
};
|
|
512
|
+
themeColors = {
|
|
513
|
+
bg: read('--bg', '#ffffff'),
|
|
514
|
+
text: read('--text', '#14162c'),
|
|
515
|
+
surface: read('--surface', read('--surface-2', '#ffffff')),
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
|
|
503
519
|
function nodeColor(type) {
|
|
504
520
|
return (TYPE_CONFIG[type] || {}).color || '#8fa8bb';
|
|
505
521
|
}
|
|
@@ -641,6 +657,7 @@ const API_BASE = window.location.protocol === 'file:' ? 'http://localhost:4825'
|
|
|
641
657
|
const byId = Object.fromEntries(rawGraph.nodes.map(node => [node.id, node]));
|
|
642
658
|
graph.edges = rawGraph.edges
|
|
643
659
|
.filter(edge => nodeSet.has(edge.from) && nodeSet.has(edge.to))
|
|
660
|
+
.filter(edge => !hiddenEdgeTypes.has(edge.type))
|
|
644
661
|
.map(edge => ({ ...edge, source: byId[edge.from], target: byId[edge.to] }));
|
|
645
662
|
renderFocusChip();
|
|
646
663
|
}
|
|
@@ -808,16 +825,26 @@ const API_BASE = window.location.protocol === 'file:' ? 'http://localhost:4825'
|
|
|
808
825
|
}
|
|
809
826
|
container.innerHTML = ordered.map(type => {
|
|
810
827
|
const style = edgeStyle(type);
|
|
828
|
+
const checked = hiddenEdgeTypes.has(type) ? '' : 'checked';
|
|
811
829
|
return `
|
|
812
|
-
<
|
|
830
|
+
<label class="filter-item">
|
|
831
|
+
<input type="checkbox" ${checked} onchange="toggleEdgeType(decodeURIComponent('${encodeURIComponent(type)}'), this.checked)">
|
|
813
832
|
<span class="legend-line" style="border-top-color:${style.color}; border-top-width:${Math.max(2, style.width)}px;"></span>
|
|
814
|
-
<span class="
|
|
815
|
-
<span class="
|
|
816
|
-
</
|
|
833
|
+
<span class="filter-name">${escapeHtml(style.label || type)}</span>
|
|
834
|
+
<span class="filter-count">${edgeCounts[type] || 0}</span>
|
|
835
|
+
</label>
|
|
817
836
|
`;
|
|
818
837
|
}).join('');
|
|
819
838
|
}
|
|
820
839
|
|
|
840
|
+
function toggleEdgeType(type, visible) {
|
|
841
|
+
if (visible) hiddenEdgeTypes.delete(type);
|
|
842
|
+
else hiddenEdgeTypes.add(type);
|
|
843
|
+
applyFilter();
|
|
844
|
+
wakeUp();
|
|
845
|
+
}
|
|
846
|
+
window.toggleEdgeType = toggleEdgeType;
|
|
847
|
+
|
|
821
848
|
function toggleType(type, visible) {
|
|
822
849
|
if (visible) hiddenTypes.delete(type);
|
|
823
850
|
else hiddenTypes.add(type);
|
|
@@ -929,6 +956,9 @@ const API_BASE = window.location.protocol === 'file:' ? 'http://localhost:4825'
|
|
|
929
956
|
ctx.translate(cam.tx, cam.ty);
|
|
930
957
|
ctx.scale(cam.scale, cam.scale);
|
|
931
958
|
|
|
959
|
+
// LOD: 줌이 너무 작거나 노드가 많으면 레이블 생략 (모바일 성능)
|
|
960
|
+
const showLabels = cam.scale >= 0.5 && graph.nodes.length <= 220;
|
|
961
|
+
|
|
932
962
|
const active = hovered || selected;
|
|
933
963
|
const neighborSet = active ? neighborIds(active) : null;
|
|
934
964
|
|
|
@@ -997,8 +1027,8 @@ const API_BASE = window.location.protocol === 'file:' ? 'http://localhost:4825'
|
|
|
997
1027
|
ctx.globalAlpha = alpha;
|
|
998
1028
|
}
|
|
999
1029
|
|
|
1000
|
-
// 레이블
|
|
1001
|
-
{
|
|
1030
|
+
// 레이블 표시 (LOD: 줌이 작거나 노드가 많으면 생략 — 모바일 성능)
|
|
1031
|
+
if (showLabels || isSelected || isHovered || isSearchHit) {
|
|
1002
1032
|
const label = node.title.slice(0, 24);
|
|
1003
1033
|
const fs = Math.max(9.5, 12 / cam.scale);
|
|
1004
1034
|
ctx.font = `600 ${fs}px "SF Pro Display","Inter",system-ui`;
|
|
@@ -1008,8 +1038,9 @@ const API_BASE = window.location.protocol === 'file:' ? 'http://localhost:4825'
|
|
|
1008
1038
|
const ly = node.y + gap + fs;
|
|
1009
1039
|
const pad = 4 / cam.scale;
|
|
1010
1040
|
const br = 5 / cam.scale;
|
|
1011
|
-
//
|
|
1012
|
-
ctx.
|
|
1041
|
+
// 테마 대응 배경 pill (라이트=흰색, 다크=surface)
|
|
1042
|
+
ctx.globalAlpha = alpha > 0.5 ? alpha * 0.88 : alpha * 0.22;
|
|
1043
|
+
ctx.fillStyle = themeColors.surface;
|
|
1013
1044
|
ctx.beginPath();
|
|
1014
1045
|
if (ctx.roundRect) {
|
|
1015
1046
|
ctx.roundRect(lx - pad, ly - fs, lw + pad * 2, fs + pad * 1.6, br);
|
|
@@ -1017,14 +1048,17 @@ const API_BASE = window.location.protocol === 'file:' ? 'http://localhost:4825'
|
|
|
1017
1048
|
ctx.rect(lx - pad, ly - fs, lw + pad * 2, fs + pad * 1.6);
|
|
1018
1049
|
}
|
|
1019
1050
|
ctx.fill();
|
|
1020
|
-
ctx.
|
|
1051
|
+
ctx.globalAlpha = alpha > 0.5 ? alpha : alpha * 0.3;
|
|
1052
|
+
ctx.fillStyle = themeColors.text;
|
|
1021
1053
|
ctx.fillText(label, lx, ly);
|
|
1054
|
+
ctx.globalAlpha = alpha;
|
|
1022
1055
|
}
|
|
1023
1056
|
|
|
1024
1057
|
ctx.globalAlpha = 1;
|
|
1025
1058
|
});
|
|
1026
1059
|
|
|
1027
1060
|
ctx.restore();
|
|
1061
|
+
drawMinimap();
|
|
1028
1062
|
if (kineticEnergy > 0.04 || dragging) animFrameId = requestAnimationFrame(draw);
|
|
1029
1063
|
}
|
|
1030
1064
|
|
|
@@ -1627,10 +1661,134 @@ const API_BASE = window.location.protocol === 'file:' ? 'http://localhost:4825'
|
|
|
1627
1661
|
});
|
|
1628
1662
|
});
|
|
1629
1663
|
|
|
1630
|
-
|
|
1664
|
+
// 리사이즈/회전/키보드(visualViewport) 시 캔버스 재측정 + 자동 재맞춤
|
|
1665
|
+
// (기존엔 backing store만 리사이즈해서 모바일에서 그래프가 화면 밖으로 나갔음)
|
|
1666
|
+
let resizeFitTimer = null;
|
|
1667
|
+
function handleViewportChange() {
|
|
1631
1668
|
resize();
|
|
1632
1669
|
wakeUp();
|
|
1633
|
-
|
|
1670
|
+
clearTimeout(resizeFitTimer);
|
|
1671
|
+
resizeFitTimer = setTimeout(() => { resize(); fitToScreen(); }, 180);
|
|
1672
|
+
}
|
|
1673
|
+
window.addEventListener('resize', handleViewportChange);
|
|
1674
|
+
window.addEventListener('orientationchange', handleViewportChange);
|
|
1675
|
+
if (window.visualViewport) {
|
|
1676
|
+
window.visualViewport.addEventListener('resize', handleViewportChange);
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
/* ──────────────────────────────────────────────────────────────────
|
|
1680
|
+
v2.2.1 그래프 1급 UI: 줌 버튼 · 전체화면 · 미니맵 · 카드뷰 · 테마대응
|
|
1681
|
+
────────────────────────────────────────────────────────────────── */
|
|
1682
|
+
// 캔버스가 터치를 직접 소유 (브라우저 기본 제스처와 충돌 방지)
|
|
1683
|
+
if (canvas && canvas.style) canvas.style.touchAction = 'none';
|
|
1684
|
+
|
|
1685
|
+
function zoomBy(factor) {
|
|
1686
|
+
const px = width / 2, py = height / 2;
|
|
1687
|
+
const next = clamp(cam.scale * factor, 0.07, 6);
|
|
1688
|
+
cam.tx = px - (px - cam.tx) * (next / cam.scale);
|
|
1689
|
+
cam.ty = py - (py - cam.ty) * (next / cam.scale);
|
|
1690
|
+
cam.scale = next;
|
|
1691
|
+
wakeUp();
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1694
|
+
const stageEl = document.querySelector('.stage');
|
|
1695
|
+
function toggleFullscreen() {
|
|
1696
|
+
const fsEl = document.fullscreenElement || document.webkitFullscreenElement;
|
|
1697
|
+
if (!fsEl && stageEl) {
|
|
1698
|
+
(stageEl.requestFullscreen || stageEl.webkitRequestFullscreen || function () {}).call(stageEl);
|
|
1699
|
+
} else {
|
|
1700
|
+
(document.exitFullscreen || document.webkitExitFullscreen || function () {}).call(document);
|
|
1701
|
+
}
|
|
1702
|
+
}
|
|
1703
|
+
document.addEventListener('fullscreenchange', handleViewportChange);
|
|
1704
|
+
document.addEventListener('webkitfullscreenchange', handleViewportChange);
|
|
1705
|
+
|
|
1706
|
+
// 미니맵 — 전체 노드 개요 + 현재 뷰포트 사각형 (클릭 시 그 지점으로 이동)
|
|
1707
|
+
const minimap = document.getElementById('minimap');
|
|
1708
|
+
const mmCtx = minimap ? minimap.getContext('2d') : null;
|
|
1709
|
+
function drawMinimap() {
|
|
1710
|
+
if (!mmCtx || !minimap || minimap.offsetParent === null) return;
|
|
1711
|
+
const W = minimap.width, H = minimap.height;
|
|
1712
|
+
mmCtx.clearRect(0, 0, W, H);
|
|
1713
|
+
if (!graph.nodes.length) return;
|
|
1714
|
+
let x0 = Infinity, x1 = -Infinity, y0 = Infinity, y1 = -Infinity;
|
|
1715
|
+
graph.nodes.forEach(n => { x0 = Math.min(x0, n.x); x1 = Math.max(x1, n.x); y0 = Math.min(y0, n.y); y1 = Math.max(y1, n.y); });
|
|
1716
|
+
const pad = 8, gw = Math.max(1, x1 - x0), gh = Math.max(1, y1 - y0);
|
|
1717
|
+
const s = Math.min((W - pad * 2) / gw, (H - pad * 2) / gh);
|
|
1718
|
+
const ox = pad - x0 * s + (W - pad * 2 - gw * s) / 2;
|
|
1719
|
+
const oy = pad - y0 * s + (H - pad * 2 - gh * s) / 2;
|
|
1720
|
+
graph.nodes.forEach(n => {
|
|
1721
|
+
mmCtx.fillStyle = nodeColor(n.type);
|
|
1722
|
+
mmCtx.beginPath();
|
|
1723
|
+
mmCtx.arc(ox + n.x * s, oy + n.y * s, 1.6, 0, Math.PI * 2);
|
|
1724
|
+
mmCtx.fill();
|
|
1725
|
+
});
|
|
1726
|
+
const vx0 = (0 - cam.tx) / cam.scale, vy0 = (0 - cam.ty) / cam.scale;
|
|
1727
|
+
const vx1 = (width - cam.tx) / cam.scale, vy1 = (height - cam.ty) / cam.scale;
|
|
1728
|
+
mmCtx.strokeStyle = 'rgba(110,74,230,0.95)';
|
|
1729
|
+
mmCtx.lineWidth = 1.2;
|
|
1730
|
+
mmCtx.strokeRect(ox + vx0 * s, oy + vy0 * s, (vx1 - vx0) * s, (vy1 - vy0) * s);
|
|
1731
|
+
minimap._map = { ox, oy, s };
|
|
1732
|
+
}
|
|
1733
|
+
if (minimap) {
|
|
1734
|
+
minimap.addEventListener('click', (event) => {
|
|
1735
|
+
const m = minimap._map; if (!m) return;
|
|
1736
|
+
const rect = minimap.getBoundingClientRect();
|
|
1737
|
+
const mx = (event.clientX - rect.left) * (minimap.width / rect.width);
|
|
1738
|
+
const my = (event.clientY - rect.top) * (minimap.height / rect.height);
|
|
1739
|
+
cam.tx = width / 2 - ((mx - m.ox) / m.s) * cam.scale;
|
|
1740
|
+
cam.ty = height / 2 - ((my - m.oy) / m.s) * cam.scale;
|
|
1741
|
+
wakeUp();
|
|
1742
|
+
});
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1745
|
+
// 모바일 카드 뷰 — 노드를 탭 가능한 카드 목록으로 (캔버스가 너무 빽빽할 때)
|
|
1746
|
+
const graphCardList = document.getElementById('graph-card-list');
|
|
1747
|
+
function renderGraphCards() {
|
|
1748
|
+
if (!graphCardList) return;
|
|
1749
|
+
if (!graph.nodes.length) {
|
|
1750
|
+
graphCardList.innerHTML = `<p class="search-empty">${t('search_empty')}</p>`;
|
|
1751
|
+
return;
|
|
1752
|
+
}
|
|
1753
|
+
graphCardList.innerHTML = '<div class="search-list">' + graph.nodes.slice(0, 400).map(n => `
|
|
1754
|
+
<button class="search-item" data-node-id="${escapeHtml(n.id)}">
|
|
1755
|
+
<div class="search-item-top">
|
|
1756
|
+
<span class="type-badge" style="background:${nodeColor(n.type)}">${escapeHtml(n.type || '')}</span>
|
|
1757
|
+
<span class="search-item-title">${escapeHtml(n.title || n.id)}</span>
|
|
1758
|
+
</div>
|
|
1759
|
+
${n.summary ? `<p class="search-item-summary">${escapeHtml(n.summary)}</p>` : ''}
|
|
1760
|
+
</button>
|
|
1761
|
+
`).join('') + '</div>';
|
|
1762
|
+
}
|
|
1763
|
+
function toggleGraphCardView() {
|
|
1764
|
+
document.body.classList.toggle('graph-card-view');
|
|
1765
|
+
if (document.body.classList.contains('graph-card-view')) renderGraphCards();
|
|
1766
|
+
}
|
|
1767
|
+
if (graphCardList) {
|
|
1768
|
+
graphCardList.addEventListener('click', (event) => {
|
|
1769
|
+
const target = event.target.closest('[data-node-id]');
|
|
1770
|
+
if (!target) return;
|
|
1771
|
+
const node = graph.nodes.find(n => n.id === target.dataset.nodeId);
|
|
1772
|
+
if (!node) return;
|
|
1773
|
+
document.body.classList.remove('graph-card-view');
|
|
1774
|
+
selected = node;
|
|
1775
|
+
showDetail(node);
|
|
1776
|
+
centerOnNode(node, Math.max(cam.scale, 1));
|
|
1777
|
+
});
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1780
|
+
// 테마(라이트/다크) 변경 시 캔버스 색상 갱신
|
|
1781
|
+
refreshThemeColors();
|
|
1782
|
+
try {
|
|
1783
|
+
const themeObserver = new MutationObserver(() => { refreshThemeColors(); wakeUp(); });
|
|
1784
|
+
themeObserver.observe(document.documentElement, { attributes: true, attributeFilter: ['data-lt-theme'] });
|
|
1785
|
+
} catch (e) { /* noop */ }
|
|
1786
|
+
|
|
1787
|
+
const bindClick = (id, fn) => { const el = document.getElementById(id); if (el) el.addEventListener('click', fn); };
|
|
1788
|
+
bindClick('zoom-in-btn', () => zoomBy(1.25));
|
|
1789
|
+
bindClick('zoom-out-btn', () => zoomBy(1 / 1.25));
|
|
1790
|
+
bindClick('fullscreen-btn', toggleFullscreen);
|
|
1791
|
+
bindClick('view-toggle-btn', toggleGraphCardView);
|
|
1634
1792
|
|
|
1635
1793
|
resize();
|
|
1636
1794
|
applyI18n();
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/* ============================================================================
|
|
2
|
+
* Lattice AI — Shared UX runtime (v2.2.1)
|
|
3
|
+
*
|
|
4
|
+
* 모든 페이지에서 로드된다. 페이지마다 일부 요소가 없을 수 있으므로 전부
|
|
5
|
+
* 방어적으로(존재 확인 후) 동작한다. 기존 chat.js / graph.js / admin.js 의
|
|
6
|
+
* 함수를 재정의하지 않고, 새 전역 기능만 추가한다.
|
|
7
|
+
*
|
|
8
|
+
* - 다크/라이트 테마 (localStorage + OS 선호 + 토글)
|
|
9
|
+
* - 모바일 키보드 inset (--kb-inset) : 입력창이 키보드에 안 가리게
|
|
10
|
+
* - Escape 로 열린 모달/오버레이 닫기
|
|
11
|
+
* - 브레이크포인트 넘으면 열린 드로어 자동 정리
|
|
12
|
+
* - 그래프/관리자 네비 드로어 토글
|
|
13
|
+
* ========================================================================== */
|
|
14
|
+
(function () {
|
|
15
|
+
"use strict";
|
|
16
|
+
|
|
17
|
+
var root = document.documentElement;
|
|
18
|
+
|
|
19
|
+
/* ---------- 1. 테마 ---------- */
|
|
20
|
+
var THEME_KEY = "lt-theme";
|
|
21
|
+
|
|
22
|
+
function systemPrefersDark() {
|
|
23
|
+
return window.matchMedia &&
|
|
24
|
+
window.matchMedia("(prefers-color-scheme: dark)").matches;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function storedTheme() {
|
|
28
|
+
try { return localStorage.getItem(THEME_KEY); } catch (e) { return null; }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function applyTheme(mode) {
|
|
32
|
+
if (mode !== "dark" && mode !== "light") {
|
|
33
|
+
mode = systemPrefersDark() ? "dark" : "light";
|
|
34
|
+
}
|
|
35
|
+
root.setAttribute("data-lt-theme", mode);
|
|
36
|
+
return mode;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// 초기 적용 (FOUC 최소화를 위해 가능한 한 빨리)
|
|
40
|
+
applyTheme(storedTheme());
|
|
41
|
+
|
|
42
|
+
window.setTheme = function (mode) {
|
|
43
|
+
var applied = applyTheme(mode);
|
|
44
|
+
try { localStorage.setItem(THEME_KEY, applied); } catch (e) {}
|
|
45
|
+
return applied;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
window.toggleTheme = function () {
|
|
49
|
+
var cur = root.getAttribute("data-lt-theme") === "dark" ? "dark" : "light";
|
|
50
|
+
return window.setTheme(cur === "dark" ? "light" : "dark");
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
// 사용자가 명시 선택을 안 했으면 OS 변화 따라가기
|
|
54
|
+
if (window.matchMedia) {
|
|
55
|
+
try {
|
|
56
|
+
window.matchMedia("(prefers-color-scheme: dark)")
|
|
57
|
+
.addEventListener("change", function (e) {
|
|
58
|
+
if (!storedTheme()) applyTheme(e.matches ? "dark" : "light");
|
|
59
|
+
});
|
|
60
|
+
} catch (e) { /* Safari < 14 */ }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/* ---------- 2. 모바일 키보드 inset ---------- */
|
|
64
|
+
var vv = window.visualViewport;
|
|
65
|
+
if (vv) {
|
|
66
|
+
var updateKbInset = function () {
|
|
67
|
+
// 레이아웃 뷰포트와 비주얼 뷰포트의 차이 = 키보드(또는 브라우저 UI) 높이
|
|
68
|
+
var inset = Math.max(0, window.innerHeight - vv.height - vv.offsetTop);
|
|
69
|
+
// 작은 값(브라우저 바 미세 변화)은 무시
|
|
70
|
+
root.style.setProperty("--kb-inset", (inset > 80 ? inset : 0) + "px");
|
|
71
|
+
};
|
|
72
|
+
vv.addEventListener("resize", updateKbInset);
|
|
73
|
+
vv.addEventListener("scroll", updateKbInset);
|
|
74
|
+
// 입력에 포커스되면 화면 안으로 스크롤
|
|
75
|
+
document.addEventListener("focusin", function (e) {
|
|
76
|
+
var t = e.target;
|
|
77
|
+
if (t && (t.tagName === "TEXTAREA" || t.tagName === "INPUT")) {
|
|
78
|
+
setTimeout(function () {
|
|
79
|
+
if (t.scrollIntoView) {
|
|
80
|
+
try { t.scrollIntoView({ block: "center", behavior: "smooth" }); }
|
|
81
|
+
catch (err) { t.scrollIntoView(); }
|
|
82
|
+
}
|
|
83
|
+
}, 250);
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/* ---------- 3. 드로어 토글 (그래프 / 관리자) ---------- */
|
|
89
|
+
function bodyHas(cls) { return document.body.classList.contains(cls); }
|
|
90
|
+
|
|
91
|
+
window.toggleGraphNav = function () {
|
|
92
|
+
document.body.classList.toggle("graph-nav-open");
|
|
93
|
+
};
|
|
94
|
+
window.closeGraphNav = function () {
|
|
95
|
+
document.body.classList.remove("graph-nav-open");
|
|
96
|
+
};
|
|
97
|
+
window.toggleAdminRail = function () {
|
|
98
|
+
document.body.classList.toggle("admin-rail-open");
|
|
99
|
+
};
|
|
100
|
+
window.closeAdminRail = function () {
|
|
101
|
+
document.body.classList.remove("admin-rail-open");
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
/* ---------- 4. Escape 로 닫기 ---------- */
|
|
105
|
+
var OVERLAY_SELECTORS = [
|
|
106
|
+
".acct-modal-overlay", ".mcp-modal-overlay", ".mode-modal-overlay",
|
|
107
|
+
".workspace-modal-overlay", ".advanced-settings-overlay", ".model-overlay",
|
|
108
|
+
".perm-overlay", ".onboarding-overlay", ".pipeline-overlay",
|
|
109
|
+
".admin-overlay", ".vpc-overlay", ".status-overlay", ".cu-overlay",
|
|
110
|
+
".file-create-overlay", ".file-editor-overlay", ".local-browser-overlay"
|
|
111
|
+
];
|
|
112
|
+
|
|
113
|
+
function isVisible(el) {
|
|
114
|
+
if (!el) return false;
|
|
115
|
+
var s = window.getComputedStyle(el);
|
|
116
|
+
return s.display !== "none" && s.visibility !== "hidden" && el.offsetParent !== null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function closeTopOverlay() {
|
|
120
|
+
// 1) 드로어 먼저
|
|
121
|
+
if (bodyHas("sidebar-open")) { document.body.classList.remove("sidebar-open"); return true; }
|
|
122
|
+
if (bodyHas("graph-nav-open")) { window.closeGraphNav(); return true; }
|
|
123
|
+
if (bodyHas("admin-rail-open")) { window.closeAdminRail(); return true; }
|
|
124
|
+
|
|
125
|
+
// 2) 보이는 오버레이를 위에서부터 닫기
|
|
126
|
+
var overlays = document.querySelectorAll(OVERLAY_SELECTORS.join(","));
|
|
127
|
+
for (var i = overlays.length - 1; i >= 0; i--) {
|
|
128
|
+
var el = overlays[i];
|
|
129
|
+
if (isVisible(el)) {
|
|
130
|
+
// 닫기 버튼이 있으면 클릭, 없으면 직접 숨김
|
|
131
|
+
var btn = el.querySelector(
|
|
132
|
+
"[onclick*='close'],.modal-close,.mcp-modal-close,.admin-close,.mode-close,.acct-close"
|
|
133
|
+
);
|
|
134
|
+
if (btn && btn.click) { btn.click(); }
|
|
135
|
+
else { el.style.display = "none"; }
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
document.addEventListener("keydown", function (e) {
|
|
143
|
+
if (e.key === "Escape" || e.keyCode === 27) {
|
|
144
|
+
if (closeTopOverlay()) { e.stopPropagation(); }
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
/* ---------- 5. 브레이크포인트 넘으면 드로어 정리 ---------- */
|
|
149
|
+
if (window.matchMedia) {
|
|
150
|
+
var desktop = window.matchMedia("(min-width: 1025px)");
|
|
151
|
+
var onDesktop = function (e) {
|
|
152
|
+
if (e.matches) {
|
|
153
|
+
document.body.classList.remove("sidebar-open", "graph-nav-open", "admin-rail-open");
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
try { desktop.addEventListener("change", onDesktop); }
|
|
157
|
+
catch (e2) { /* old Safari */ try { desktop.addListener(onDesktop); } catch (e3) {} }
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/* ---------- 6. 새 드로어용 오버레이 백드롭 클릭 ---------- */
|
|
161
|
+
document.addEventListener("click", function (e) {
|
|
162
|
+
var t = e.target;
|
|
163
|
+
if (t && t.classList && t.classList.contains("sidebar-overlay")) {
|
|
164
|
+
document.body.classList.remove("graph-nav-open", "admin-rail-open");
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
})();
|
package/static/workflows.html
CHANGED
|
@@ -2,9 +2,12 @@
|
|
|
2
2
|
<html lang="en">
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
|
-
<meta name="viewport" content="width=device-width, initial-scale=1
|
|
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
|
-
<
|
|
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" />
|
|
8
11
|
</head>
|
|
9
12
|
<body>
|
|
10
13
|
<main>
|
package/static/workspace.css
CHANGED
|
@@ -1,21 +1,42 @@
|
|
|
1
1
|
:root {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
--
|
|
6
|
-
--
|
|
7
|
-
--
|
|
8
|
-
--
|
|
9
|
-
--
|
|
2
|
+
/* Map the local palette onto the shared design tokens so this page
|
|
3
|
+
inverts cleanly under :root[data-lt-theme="dark"]. Light defaults
|
|
4
|
+
stay visually close to the original hand-tuned values. */
|
|
5
|
+
--bg: var(--lt-bg, #f6f7f9);
|
|
6
|
+
--surface: var(--lt-surface, #ffffff);
|
|
7
|
+
--surface-2: var(--lt-surface-2, #f0f4f8);
|
|
8
|
+
--ink: var(--lt-ink, #101828);
|
|
9
|
+
--muted: var(--lt-muted, #667085);
|
|
10
|
+
--line: var(--lt-line, #d9e0e8);
|
|
11
|
+
--blue: var(--lt-accent, #2563eb);
|
|
10
12
|
--green: #12805c;
|
|
11
13
|
--amber: #b45309;
|
|
12
14
|
--pink: #be185d;
|
|
13
15
|
--red: #c2410c;
|
|
14
16
|
--radius: 8px;
|
|
15
|
-
--shadow: 0 12px 34px rgba(16, 24, 40, 0.08);
|
|
17
|
+
--shadow: var(--lt-shadow-md, 0 12px 34px rgba(16, 24, 40, 0.08));
|
|
18
|
+
/* Rail keeps its dark-navy identity in light mode; dark mode remaps it
|
|
19
|
+
to a shared surface token below so it inverts coherently. */
|
|
20
|
+
--rail-bg: #111827;
|
|
21
|
+
--rail-ink: #e5e7eb;
|
|
22
|
+
--rail-ink-soft: #b8c0cc;
|
|
23
|
+
--rail-ink-strong: #fff;
|
|
24
|
+
--rail-hover: rgba(255, 255, 255, 0.08);
|
|
25
|
+
--rail-line: rgba(255, 255, 255, 0.12);
|
|
16
26
|
font-family: Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
17
27
|
}
|
|
18
28
|
|
|
29
|
+
/* Dark theme: remap the rail to the shared surface tokens so it inverts
|
|
30
|
+
instead of staying permanently navy. */
|
|
31
|
+
:root[data-lt-theme="dark"] {
|
|
32
|
+
--rail-bg: var(--lt-surface-2);
|
|
33
|
+
--rail-ink: var(--lt-ink);
|
|
34
|
+
--rail-ink-soft: var(--lt-ink-soft);
|
|
35
|
+
--rail-ink-strong: var(--lt-ink);
|
|
36
|
+
--rail-hover: rgba(255, 255, 255, 0.06);
|
|
37
|
+
--rail-line: var(--lt-line);
|
|
38
|
+
}
|
|
39
|
+
|
|
19
40
|
* { box-sizing: border-box; }
|
|
20
41
|
|
|
21
42
|
html { scroll-behavior: smooth; }
|
|
@@ -40,14 +61,16 @@ button {
|
|
|
40
61
|
display: grid;
|
|
41
62
|
grid-template-columns: 248px minmax(0, 1fr);
|
|
42
63
|
min-height: 100vh;
|
|
64
|
+
min-height: 100dvh;
|
|
43
65
|
}
|
|
44
66
|
|
|
45
67
|
.workspace-rail {
|
|
46
68
|
position: sticky;
|
|
47
69
|
top: 0;
|
|
48
70
|
height: 100vh;
|
|
49
|
-
|
|
50
|
-
|
|
71
|
+
height: 100dvh;
|
|
72
|
+
background: var(--rail-bg);
|
|
73
|
+
color: var(--rail-ink);
|
|
51
74
|
padding: 18px 14px;
|
|
52
75
|
display: flex;
|
|
53
76
|
flex-direction: column;
|
|
@@ -58,7 +81,7 @@ button {
|
|
|
58
81
|
display: flex;
|
|
59
82
|
align-items: center;
|
|
60
83
|
gap: 10px;
|
|
61
|
-
color:
|
|
84
|
+
color: var(--rail-ink-strong);
|
|
62
85
|
text-decoration: none;
|
|
63
86
|
font-weight: 800;
|
|
64
87
|
padding: 8px;
|
|
@@ -77,7 +100,7 @@ button {
|
|
|
77
100
|
}
|
|
78
101
|
|
|
79
102
|
.workspace-rail a {
|
|
80
|
-
color:
|
|
103
|
+
color: var(--rail-ink-soft);
|
|
81
104
|
text-decoration: none;
|
|
82
105
|
}
|
|
83
106
|
|
|
@@ -94,12 +117,12 @@ button {
|
|
|
94
117
|
.workspace-rail nav a.active,
|
|
95
118
|
.workspace-rail nav a:hover,
|
|
96
119
|
.rail-links a:hover {
|
|
97
|
-
background:
|
|
98
|
-
color:
|
|
120
|
+
background: var(--rail-hover);
|
|
121
|
+
color: var(--rail-ink-strong);
|
|
99
122
|
}
|
|
100
123
|
|
|
101
124
|
.rail-links {
|
|
102
|
-
border-top: 1px solid
|
|
125
|
+
border-top: 1px solid var(--rail-line);
|
|
103
126
|
padding-top: 12px;
|
|
104
127
|
margin-top: auto;
|
|
105
128
|
}
|
|
@@ -521,8 +544,8 @@ textarea {
|
|
|
521
544
|
gap: 8px;
|
|
522
545
|
padding: 6px 10px;
|
|
523
546
|
border-radius: 10px;
|
|
524
|
-
background:
|
|
525
|
-
border: 1px solid
|
|
547
|
+
background: var(--rail-hover);
|
|
548
|
+
border: 1px solid var(--rail-line);
|
|
526
549
|
}
|
|
527
550
|
.workspace-switcher select {
|
|
528
551
|
background: transparent;
|
|
@@ -600,7 +623,7 @@ textarea {
|
|
|
600
623
|
/* Enterprise capability grid */
|
|
601
624
|
.capability-grid {
|
|
602
625
|
display: grid; gap: 10px;
|
|
603
|
-
grid-template-columns: repeat(auto-fill, minmax(
|
|
626
|
+
grid-template-columns: repeat(auto-fill, minmax(min(100%, 200px), 1fr));
|
|
604
627
|
margin-top: 12px;
|
|
605
628
|
}
|
|
606
629
|
.capability-card {
|
|
@@ -684,3 +707,16 @@ textarea {
|
|
|
684
707
|
grid-template-columns: 1fr;
|
|
685
708
|
}
|
|
686
709
|
}
|
|
710
|
+
|
|
711
|
+
/* v2.2.1 — 태블릿(761~1024) 레일 슬림화로 본문 공간 확보 */
|
|
712
|
+
@media (min-width: 761px) and (max-width: 1024px) {
|
|
713
|
+
.workspace-shell { grid-template-columns: 196px minmax(0, 1fr); }
|
|
714
|
+
.workspace-rail { padding: 14px 10px; }
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
/* v2.2.1 — 터치 영역 44px 보장 */
|
|
718
|
+
@media (hover: none), (max-width: 760px) {
|
|
719
|
+
.icon-action { min-width: 44px; min-height: 44px; }
|
|
720
|
+
.small-action { min-height: 44px; }
|
|
721
|
+
.workspace-rail a { min-height: 44px; display: flex; align-items: center; }
|
|
722
|
+
}
|
package/static/workspace.html
CHANGED
|
@@ -2,13 +2,16 @@
|
|
|
2
2
|
<html lang="en">
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="utf-8">
|
|
5
|
-
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover, interactive-widget=resizes-content">
|
|
6
6
|
<title>Lattice AI Workspace OS</title>
|
|
7
|
+
<script src="/static/scripts/ux.js?v=2.2.1"></script>
|
|
7
8
|
<link rel="manifest" href="/manifest.json">
|
|
8
9
|
<link rel="icon" type="image/png" sizes="32x32" href="/icons/favicon-32.png">
|
|
9
10
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap">
|
|
10
11
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@tabler/icons-webfont@latest/tabler-icons.min.css">
|
|
11
|
-
<link rel="stylesheet" href="/static/
|
|
12
|
+
<link rel="stylesheet" href="/static/css/tokens.css?v=2.2.1">
|
|
13
|
+
<link rel="stylesheet" href="/static/workspace.css?v=2.2.1">
|
|
14
|
+
<link rel="stylesheet" href="/static/css/responsive.css?v=2.2.1">
|
|
12
15
|
</head>
|
|
13
16
|
<body>
|
|
14
17
|
<div class="workspace-shell">
|
package/telegram_bot.py
CHANGED
|
@@ -840,7 +840,7 @@ async def handle_command(client, chat_id, command: str, args: str):
|
|
|
840
840
|
await send_message(client, chat_id, HELP_TEXT)
|
|
841
841
|
elif cmd == "agent":
|
|
842
842
|
if not args:
|
|
843
|
-
await send_message(client, chat_id, "사용법: /agent <작업 내용>\n예: /agent 쇼핑몰 메인 페이지 HTML 만들어줘\n\n특정
|
|
843
|
+
await send_message(client, chat_id, "사용법: /agent <작업 내용>\n예: /agent 쇼핑몰 메인 페이지 HTML 만들어줘\n\n특정 AI 지정:\n/agent <작업> --exec openai/gpt-4o --review together:Qwen/Qwen3-VL-32B-Instruct")
|
|
844
844
|
return
|
|
845
845
|
# Parse optional --exec / --review flags
|
|
846
846
|
exec_model = reviewing_model = None
|