ltcai 3.0.1 → 3.1.0
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 +27 -20
- package/docs/CHANGELOG.md +37 -0
- package/docs/V3_FRONTEND.md +20 -17
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/auth.py +4 -1
- package/latticeai/api/search.py +4 -0
- package/latticeai/core/config.py +2 -0
- package/latticeai/core/embedding_providers.py +123 -0
- package/latticeai/core/workspace_os.py +1 -1
- package/latticeai/server_app.py +22 -6
- package/package.json +9 -4
- package/scripts/build_v3_assets.mjs +164 -0
- package/scripts/capture/README.md +28 -0
- package/scripts/capture/capture_enterprise.js +8 -0
- package/scripts/capture/capture_graph.js +8 -0
- package/scripts/capture/capture_onboarding.js +8 -0
- package/scripts/capture/capture_page.js +43 -0
- package/scripts/capture/capture_release_media.js +125 -0
- package/scripts/capture/capture_skills.js +8 -0
- package/scripts/capture/capture_workspace.js +8 -0
- package/scripts/generate_diagrams.py +513 -0
- package/scripts/lint_v3.mjs +33 -0
- package/scripts/release-0.3.1.sh +105 -0
- package/scripts/take_screenshots.js +69 -0
- package/scripts/validate_release_artifacts.py +167 -0
- package/static/account.html +9 -9
- package/static/activity.html +4 -4
- package/static/admin.html +8 -8
- package/static/agents.html +4 -4
- package/static/chat.html +9 -9
- package/static/css/tokens.5a595671.css +260 -0
- package/static/css/tokens.css +1 -1
- package/static/graph.html +9 -9
- package/static/plugins.html +4 -4
- package/static/sw.js +3 -1
- package/static/v3/asset-manifest.json +47 -0
- package/static/v3/css/lattice.base.e4cdd05d.css +128 -0
- package/static/v3/css/lattice.components.011e988b.css +447 -0
- package/static/v3/css/lattice.components.css +2 -2
- package/static/v3/css/lattice.shell.4920f42d.css +407 -0
- package/static/v3/css/lattice.tokens.c597ff81.css +132 -0
- package/static/v3/css/lattice.views.3ee19d4e.css +277 -0
- package/static/v3/index.html +38 -9
- package/static/v3/js/app.46fb61d9.js +26 -0
- package/static/v3/js/core/api.22a41d42.js +344 -0
- package/static/v3/js/core/api.js +68 -51
- package/static/v3/js/core/components.4c83e0a9.js +222 -0
- package/static/v3/js/core/components.js +9 -2
- package/static/v3/js/core/dom.a2773eb0.js +148 -0
- package/static/v3/js/core/router.584570f2.js +37 -0
- package/static/v3/js/core/routes.f935dd50.js +78 -0
- package/static/v3/js/core/routes.js +6 -1
- package/static/v3/js/core/shell.1b6199d6.js +363 -0
- package/static/v3/js/core/store.34ebd5e6.js +113 -0
- package/static/v3/js/views/admin-audit.660a1fb1.js +185 -0
- package/static/v3/js/views/admin-audit.js +1 -1
- package/static/v3/js/views/admin-permissions.a7ae5f09.js +177 -0
- package/static/v3/js/views/admin-permissions.js +4 -5
- package/static/v3/js/views/admin-policies.3658fd86.js +102 -0
- package/static/v3/js/views/admin-policies.js +4 -5
- package/static/v3/js/views/admin-private-vpc.7d342d36.js +135 -0
- package/static/v3/js/views/admin-private-vpc.js +2 -5
- package/static/v3/js/views/admin-security.07c66b72.js +180 -0
- package/static/v3/js/views/admin-security.js +4 -5
- package/static/v3/js/views/admin-users.03bac88c.js +168 -0
- package/static/v3/js/views/admin-users.js +6 -6
- package/static/v3/js/views/agents.14e48bdd.js +193 -0
- package/static/v3/js/views/agents.js +1 -2
- package/static/v3/js/views/chat.718144ce.js +449 -0
- package/static/v3/js/views/chat.js +2 -3
- package/static/v3/js/views/files.4935197e.js +186 -0
- package/static/v3/js/views/files.js +27 -21
- package/static/v3/js/views/home.cdde3b32.js +119 -0
- package/static/v3/js/views/hybrid-search.b22b97e0.js +195 -0
- package/static/v3/js/views/hybrid-search.js +1 -1
- package/static/v3/js/views/knowledge-graph.a14ea7e7.js +237 -0
- package/static/v3/js/views/knowledge-graph.js +2 -3
- package/static/v3/js/views/models.a1ffa147.js +256 -0
- package/static/v3/js/views/models.js +17 -8
- package/static/v3/js/views/my-computer.1b2ff621.js +237 -0
- package/static/v3/js/views/my-computer.js +5 -5
- package/static/v3/js/views/pipeline.c522f1ce.js +157 -0
- package/static/v3/js/views/pipeline.js +3 -7
- package/static/v3/js/views/settings.4f777210.js +250 -0
- package/static/v3/js/views/settings.js +6 -14
- package/static/workflows.html +4 -4
- package/static/workspace.html +5 -5
- package/docs/images/tmp_frames/frame_00.png +0 -0
- package/docs/images/tmp_frames/frame_01.png +0 -0
- package/docs/images/tmp_frames/frame_02.png +0 -0
- package/docs/images/tmp_frames/frame_03.png +0 -0
- package/docs/images/tmp_frames/hero_00.png +0 -0
- package/docs/images/tmp_frames/hero_01.png +0 -0
- package/docs/images/tmp_frames/hero_02.png +0 -0
- package/docs/images/tmp_frames/hero_03.png +0 -0
- package/static/v3/js/core/fixtures.js +0 -171
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/*
|
|
3
|
+
* Build the v3 browser asset manifest.
|
|
4
|
+
*
|
|
5
|
+
* The source files stay importable in development. This script writes hashed
|
|
6
|
+
* siblings next to each runtime asset, rewrites ES-module imports to those
|
|
7
|
+
* hashed siblings, and emits static/v3/asset-manifest.json for /app.
|
|
8
|
+
*/
|
|
9
|
+
import { createHash } from "node:crypto";
|
|
10
|
+
import {
|
|
11
|
+
existsSync,
|
|
12
|
+
mkdirSync,
|
|
13
|
+
readdirSync,
|
|
14
|
+
readFileSync,
|
|
15
|
+
rmSync,
|
|
16
|
+
statSync,
|
|
17
|
+
writeFileSync,
|
|
18
|
+
} from "node:fs";
|
|
19
|
+
import { basename, dirname, extname, join, relative } from "node:path";
|
|
20
|
+
import { fileURLToPath } from "node:url";
|
|
21
|
+
|
|
22
|
+
const repoRoot = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
23
|
+
const staticRoot = join(repoRoot, "static");
|
|
24
|
+
const manifestPath = join(staticRoot, "v3", "asset-manifest.json");
|
|
25
|
+
|
|
26
|
+
const cssSources = [
|
|
27
|
+
"static/css/tokens.css",
|
|
28
|
+
"static/v3/css/lattice.tokens.css",
|
|
29
|
+
"static/v3/css/lattice.base.css",
|
|
30
|
+
"static/v3/css/lattice.components.css",
|
|
31
|
+
"static/v3/css/lattice.shell.css",
|
|
32
|
+
"static/v3/css/lattice.views.css",
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
const moduleRoot = join(staticRoot, "v3", "js");
|
|
36
|
+
const entry = "static/v3/js/app.js";
|
|
37
|
+
|
|
38
|
+
function posix(p) {
|
|
39
|
+
return p.replaceAll("\\", "/");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function sha(text) {
|
|
43
|
+
return createHash("sha256").update(text).digest("hex").slice(0, 8);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function repoPath(abs) {
|
|
47
|
+
return posix(relative(repoRoot, abs));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function publicUrl(repoRel) {
|
|
51
|
+
return "/" + posix(repoRel);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function walk(dir) {
|
|
55
|
+
const out = [];
|
|
56
|
+
for (const name of readdirSync(dir)) {
|
|
57
|
+
const p = join(dir, name);
|
|
58
|
+
const st = statSync(p);
|
|
59
|
+
if (st.isDirectory()) out.push(...walk(p));
|
|
60
|
+
else if (name.endsWith(".js")) out.push(p);
|
|
61
|
+
}
|
|
62
|
+
return out;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function removeGenerated(dir, ext) {
|
|
66
|
+
if (!existsSync(dir)) return;
|
|
67
|
+
for (const name of readdirSync(dir)) {
|
|
68
|
+
const p = join(dir, name);
|
|
69
|
+
const st = statSync(p);
|
|
70
|
+
if (st.isDirectory()) removeGenerated(p, ext);
|
|
71
|
+
else if (new RegExp(`\\.[0-9a-f]{8}\\${ext}$`).test(name)) rmSync(p);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
removeGenerated(join(staticRoot, "css"), ".css");
|
|
76
|
+
removeGenerated(join(staticRoot, "v3", "css"), ".css");
|
|
77
|
+
removeGenerated(moduleRoot, ".js");
|
|
78
|
+
|
|
79
|
+
const modules = new Map();
|
|
80
|
+
for (const abs of walk(moduleRoot)) {
|
|
81
|
+
modules.set(repoPath(abs), {
|
|
82
|
+
abs,
|
|
83
|
+
rel: repoPath(abs),
|
|
84
|
+
source: readFileSync(abs, "utf8"),
|
|
85
|
+
deps: [],
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const importFromRe = /\b(?:import|export)\s+(?:[^"'()]*?\s+from\s*)?["']([^"']+\.js)["']/g;
|
|
90
|
+
for (const mod of modules.values()) {
|
|
91
|
+
const deps = [];
|
|
92
|
+
let match;
|
|
93
|
+
while ((match = importFromRe.exec(mod.source))) {
|
|
94
|
+
const spec = match[1];
|
|
95
|
+
if (!spec.startsWith(".")) continue;
|
|
96
|
+
const depRel = repoPath(join(dirname(mod.abs), spec));
|
|
97
|
+
if (modules.has(depRel)) deps.push(depRel);
|
|
98
|
+
}
|
|
99
|
+
mod.deps = deps;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const hashMemo = new Map();
|
|
103
|
+
function moduleHash(rel, stack = []) {
|
|
104
|
+
if (hashMemo.has(rel)) return hashMemo.get(rel);
|
|
105
|
+
if (stack.includes(rel)) return sha(modules.get(rel).source);
|
|
106
|
+
const mod = modules.get(rel);
|
|
107
|
+
const depHashes = mod.deps
|
|
108
|
+
.sort()
|
|
109
|
+
.map((dep) => `${dep}:${moduleHash(dep, [...stack, rel])}`)
|
|
110
|
+
.join("\n");
|
|
111
|
+
const h = sha(`${mod.source}\n/* dependency-hashes */\n${depHashes}`);
|
|
112
|
+
hashMemo.set(rel, h);
|
|
113
|
+
return h;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
for (const rel of modules.keys()) moduleHash(rel);
|
|
117
|
+
|
|
118
|
+
function hashedRel(rel, hash) {
|
|
119
|
+
const ext = extname(rel);
|
|
120
|
+
return posix(join(dirname(rel), `${basename(rel, ext)}.${hash}${ext}`));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const assets = {};
|
|
124
|
+
|
|
125
|
+
for (const sourceRel of cssSources) {
|
|
126
|
+
const abs = join(repoRoot, sourceRel);
|
|
127
|
+
const source = readFileSync(abs, "utf8");
|
|
128
|
+
const outRel = hashedRel(sourceRel, sha(source));
|
|
129
|
+
writeFileSync(join(repoRoot, outRel), source, "utf8");
|
|
130
|
+
assets[sourceRel] = publicUrl(outRel);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function rewriteModule(mod) {
|
|
134
|
+
return mod.source.replace(importFromRe, (full, spec) => {
|
|
135
|
+
if (!spec.startsWith(".")) return full;
|
|
136
|
+
const depRel = repoPath(join(dirname(mod.abs), spec));
|
|
137
|
+
const depHash = hashMemo.get(depRel);
|
|
138
|
+
if (!depHash) return full;
|
|
139
|
+
const depOutAbs = join(repoRoot, hashedRel(depRel, depHash));
|
|
140
|
+
const nextSpec = posix(relative(dirname(join(repoRoot, hashedRel(mod.rel, hashMemo.get(mod.rel)))), depOutAbs));
|
|
141
|
+
const normalized = nextSpec.startsWith(".") ? nextSpec : `./${nextSpec}`;
|
|
142
|
+
return full.replace(spec, normalized);
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
for (const mod of modules.values()) {
|
|
147
|
+
const outRel = hashedRel(mod.rel, hashMemo.get(mod.rel));
|
|
148
|
+
mkdirSync(dirname(join(repoRoot, outRel)), { recursive: true });
|
|
149
|
+
writeFileSync(join(repoRoot, outRel), rewriteModule(mod), "utf8");
|
|
150
|
+
assets[mod.rel] = publicUrl(outRel);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const manifest = {
|
|
154
|
+
version: "3.1.0",
|
|
155
|
+
generated_at: "deterministic",
|
|
156
|
+
entrypoints: {
|
|
157
|
+
app: assets[entry],
|
|
158
|
+
styles: cssSources.map((rel) => assets[rel]),
|
|
159
|
+
},
|
|
160
|
+
assets,
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + "\n", "utf8");
|
|
164
|
+
console.log(`wrote ${repoPath(manifestPath)} with ${Object.keys(assets).length} assets`);
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Screenshot Capture
|
|
2
|
+
|
|
3
|
+
Reproducible Playwright capture scripts for release screenshots.
|
|
4
|
+
|
|
5
|
+
Prerequisites:
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm ci
|
|
9
|
+
npx playwright install chromium
|
|
10
|
+
LTCAI
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Optional environment:
|
|
14
|
+
|
|
15
|
+
- `LTCAI_CAPTURE_BASE_URL` defaults to `http://localhost:4825`
|
|
16
|
+
- `SESSION_TOKEN` or `LTCAI_SESSION_TOKEN` injects an authenticated session cookie
|
|
17
|
+
- `LTCAI_CAPTURE_OUT` defaults to `docs/images`
|
|
18
|
+
- `LTCAI_CAPTURE_HEADED=1` runs with a visible browser
|
|
19
|
+
|
|
20
|
+
Commands:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npm run capture:workspace
|
|
24
|
+
npm run capture:graph
|
|
25
|
+
npm run capture:skills
|
|
26
|
+
npm run capture:enterprise
|
|
27
|
+
npm run capture:onboarding
|
|
28
|
+
```
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
|
|
4
|
+
async function loadPlaywright() {
|
|
5
|
+
try {
|
|
6
|
+
return require("@playwright/test");
|
|
7
|
+
} catch (_) {
|
|
8
|
+
return require("playwright");
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async function capturePage(options) {
|
|
13
|
+
const { chromium } = await loadPlaywright();
|
|
14
|
+
const baseURL = process.env.LTCAI_CAPTURE_BASE_URL || "http://localhost:4825";
|
|
15
|
+
const outDir = path.resolve(process.env.LTCAI_CAPTURE_OUT || path.join(__dirname, "..", "..", "docs", "images"));
|
|
16
|
+
const sessionToken = process.env.SESSION_TOKEN || process.env.LTCAI_SESSION_TOKEN || "";
|
|
17
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
18
|
+
|
|
19
|
+
const browser = await chromium.launch({ headless: process.env.LTCAI_CAPTURE_HEADED !== "1" });
|
|
20
|
+
const context = await browser.newContext({
|
|
21
|
+
viewport: { width: Number(process.env.LTCAI_CAPTURE_WIDTH || 1440), height: Number(process.env.LTCAI_CAPTURE_HEIGHT || 920) },
|
|
22
|
+
deviceScaleFactor: Number(process.env.LTCAI_CAPTURE_SCALE || 2),
|
|
23
|
+
});
|
|
24
|
+
if (sessionToken) {
|
|
25
|
+
const host = new URL(baseURL).hostname;
|
|
26
|
+
await context.addCookies([{ name: "session_token", value: sessionToken, domain: host, path: "/", httpOnly: true, secure: false }]);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const page = await context.newPage();
|
|
30
|
+
const target = new URL(options.path, baseURL).toString();
|
|
31
|
+
await page.goto(target, { waitUntil: "networkidle", timeout: Number(process.env.LTCAI_CAPTURE_TIMEOUT || 30_000) });
|
|
32
|
+
if (options.hash) await page.evaluate((hash) => { location.hash = hash; }, options.hash);
|
|
33
|
+
if (options.waitFor) await page.waitForSelector(options.waitFor, { timeout: 15_000 });
|
|
34
|
+
await page.waitForTimeout(Number(process.env.LTCAI_CAPTURE_SETTLE_MS || options.settleMs || 900));
|
|
35
|
+
|
|
36
|
+
const outPath = path.join(outDir, options.filename);
|
|
37
|
+
await page.screenshot({ path: outPath, fullPage: Boolean(options.fullPage) });
|
|
38
|
+
await browser.close();
|
|
39
|
+
console.log(outPath);
|
|
40
|
+
return outPath;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
module.exports = { capturePage };
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
const fs = require("fs");
|
|
3
|
+
const path = require("path");
|
|
4
|
+
const { spawnSync } = require("child_process");
|
|
5
|
+
|
|
6
|
+
async function loadPlaywright() {
|
|
7
|
+
try {
|
|
8
|
+
return require("@playwright/test");
|
|
9
|
+
} catch (_) {
|
|
10
|
+
return require("playwright");
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const ROOT = path.resolve(__dirname, "..", "..");
|
|
15
|
+
const OUT = path.join(ROOT, "docs", "images");
|
|
16
|
+
const FRAMES = path.join(OUT, "tmp_frames", "release_221");
|
|
17
|
+
const BASE = process.env.LTCAI_CAPTURE_BASE_URL || "http://127.0.0.1:4825";
|
|
18
|
+
|
|
19
|
+
function ensureDirs() {
|
|
20
|
+
fs.mkdirSync(OUT, { recursive: true });
|
|
21
|
+
fs.rmSync(FRAMES, { recursive: true, force: true });
|
|
22
|
+
fs.mkdirSync(FRAMES, { recursive: true });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function preparePage(page, theme = "light") {
|
|
26
|
+
await page.addInitScript((mode) => {
|
|
27
|
+
localStorage.setItem("lt-theme", mode);
|
|
28
|
+
localStorage.setItem("ltcai_workspace_type", "personal");
|
|
29
|
+
localStorage.setItem("ltcai_mode", "advanced");
|
|
30
|
+
localStorage.setItem("ltcai_user_email", "demo@lattice.local");
|
|
31
|
+
localStorage.setItem("ltcai_is_admin", "true");
|
|
32
|
+
sessionStorage.setItem("ltcai_force_setup_after_login", "false");
|
|
33
|
+
}, theme);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function gotoReady(page, pathname, selector, theme = "light") {
|
|
37
|
+
await preparePage(page, theme);
|
|
38
|
+
await page.goto(new URL(pathname, BASE).toString(), { waitUntil: "domcontentloaded", timeout: 30000 });
|
|
39
|
+
if (selector) await page.waitForSelector(selector, { timeout: 20000 });
|
|
40
|
+
await page.evaluate((mode) => {
|
|
41
|
+
document.documentElement.setAttribute("data-lt-theme", mode);
|
|
42
|
+
document.body.dataset.capture = "release-221";
|
|
43
|
+
}, theme);
|
|
44
|
+
await page.waitForTimeout(1800);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function screenshot(page, filename, options = {}) {
|
|
48
|
+
const out = path.join(OUT, filename);
|
|
49
|
+
await page.screenshot({ path: out, fullPage: Boolean(options.fullPage) });
|
|
50
|
+
console.log(out);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function captureAll() {
|
|
54
|
+
ensureDirs();
|
|
55
|
+
const { chromium } = await loadPlaywright();
|
|
56
|
+
const browser = await chromium.launch({ headless: true });
|
|
57
|
+
|
|
58
|
+
const desktop = await browser.newContext({
|
|
59
|
+
viewport: { width: 1440, height: 920 },
|
|
60
|
+
deviceScaleFactor: 1,
|
|
61
|
+
});
|
|
62
|
+
const mobile = await browser.newContext({
|
|
63
|
+
viewport: { width: 390, height: 844 },
|
|
64
|
+
deviceScaleFactor: 2,
|
|
65
|
+
isMobile: true,
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
let page = await desktop.newPage();
|
|
69
|
+
await gotoReady(page, "/workspace", "#workspace-health-grid", "light");
|
|
70
|
+
await screenshot(page, "workspace-light.png");
|
|
71
|
+
await screenshot(page, "lattice-ai-hero.png");
|
|
72
|
+
|
|
73
|
+
await page.evaluate(() => document.documentElement.setAttribute("data-lt-theme", "dark"));
|
|
74
|
+
await page.waitForTimeout(800);
|
|
75
|
+
await screenshot(page, "workspace-dark.png");
|
|
76
|
+
|
|
77
|
+
await gotoReady(page, "/graph", "#graph", "dark");
|
|
78
|
+
await screenshot(page, "knowledge-graph.png");
|
|
79
|
+
|
|
80
|
+
await gotoReady(page, "/workflows", "#wfNodes", "light");
|
|
81
|
+
await screenshot(page, "pipeline.png");
|
|
82
|
+
|
|
83
|
+
await gotoReady(page, "/admin", "#admin-root, .admin-shell, body", "light");
|
|
84
|
+
await page.waitForTimeout(2200);
|
|
85
|
+
await screenshot(page, "admin-dashboard.png");
|
|
86
|
+
|
|
87
|
+
const mobilePage = await mobile.newPage();
|
|
88
|
+
await gotoReady(mobilePage, "/chat", ".reference-shell, body", "light");
|
|
89
|
+
await screenshot(mobilePage, "mobile-responsive.png", { fullPage: false });
|
|
90
|
+
|
|
91
|
+
const framePlan = [
|
|
92
|
+
["/workspace", "#workspace-health-grid", "light"],
|
|
93
|
+
["/workspace", "#workspace-health-grid", "dark"],
|
|
94
|
+
["/graph", "#graph", "dark"],
|
|
95
|
+
["/workflows", "#wfNodes", "light"],
|
|
96
|
+
["/chat", ".reference-shell, body", "light"],
|
|
97
|
+
];
|
|
98
|
+
let frame = 0;
|
|
99
|
+
for (const [pathname, selector, theme] of framePlan) {
|
|
100
|
+
await gotoReady(page, pathname, selector, theme);
|
|
101
|
+
for (let repeat = 0; repeat < 3; repeat += 1) {
|
|
102
|
+
await page.screenshot({ path: path.join(FRAMES, `frame_${String(frame).padStart(3, "0")}.png`) });
|
|
103
|
+
frame += 1;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
await browser.close();
|
|
108
|
+
|
|
109
|
+
const gifPath = path.join(OUT, "lattice-ai-demo.gif");
|
|
110
|
+
const ffmpeg = spawnSync("ffmpeg", [
|
|
111
|
+
"-y",
|
|
112
|
+
"-framerate", "1.5",
|
|
113
|
+
"-i", path.join(FRAMES, "frame_%03d.png"),
|
|
114
|
+
"-vf", "scale=1280:-1:flags=lanczos,fps=6",
|
|
115
|
+
"-loop", "0",
|
|
116
|
+
gifPath,
|
|
117
|
+
], { stdio: "inherit" });
|
|
118
|
+
if (ffmpeg.status !== 0) process.exit(ffmpeg.status || 1);
|
|
119
|
+
console.log(gifPath);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
captureAll().catch((error) => {
|
|
123
|
+
console.error(error);
|
|
124
|
+
process.exit(1);
|
|
125
|
+
});
|