@veolab/discoverylab 1.6.5 → 1.6.6
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/dist/{chunk-V6RREMYD.js → chunk-XDUFCPOC.js} +104 -19
- package/dist/{chunk-Q7Q3A2ZI.js → chunk-YFF3M76J.js} +27 -31
- package/dist/cli.js +21 -15
- package/dist/export/infographic-template.html +392 -128
- package/dist/index.html +5 -2
- package/dist/index.js +2 -2
- package/dist/{infographic-GQAHEOAA.js → infographic-OSDIJM5M.js} +119 -17
- package/dist/{server-C2NZM2RV.js → server-GXNAKM4H.js} +1 -1
- package/dist/{tools-VXU3JEQP.js → tools-6BTUMR3G.js} +1 -1
- package/package.json +2 -2
|
@@ -5235,6 +5235,56 @@ function normalizePersistedLocalESVPServerUrl(serverUrl, connectionMode) {
|
|
|
5235
5235
|
function resolvePersistedLocalESVPServerUrl(serverUrl, esvp) {
|
|
5236
5236
|
return normalizePersistedLocalESVPServerUrl(serverUrl, "local") || resolveProjectESVPServerUrl(esvp) || LOCAL_ESVP_SERVER_URL;
|
|
5237
5237
|
}
|
|
5238
|
+
function cloneJsonRecord(value) {
|
|
5239
|
+
if (!value) return value;
|
|
5240
|
+
try {
|
|
5241
|
+
return JSON.parse(JSON.stringify(value));
|
|
5242
|
+
} catch {
|
|
5243
|
+
return value;
|
|
5244
|
+
}
|
|
5245
|
+
}
|
|
5246
|
+
function detachPortableESVPForExport(esvp, snapshotAttached = false) {
|
|
5247
|
+
if (!esvp) {
|
|
5248
|
+
return { value: null, detached: false };
|
|
5249
|
+
}
|
|
5250
|
+
const cloned = cloneJsonRecord(esvp) || {};
|
|
5251
|
+
const connectionMode = typeof cloned.connectionMode === "string" ? cloned.connectionMode.trim().toLowerCase() : "";
|
|
5252
|
+
const serverUrl = typeof cloned.serverUrl === "string" ? cloned.serverUrl.trim() : "";
|
|
5253
|
+
const shouldDetach = connectionMode === "local" || !serverUrl;
|
|
5254
|
+
if (!shouldDetach) {
|
|
5255
|
+
return { value: cloned, detached: false };
|
|
5256
|
+
}
|
|
5257
|
+
cloned.currentSessionId = null;
|
|
5258
|
+
cloned.serverUrl = null;
|
|
5259
|
+
cloned.detachedForExport = true;
|
|
5260
|
+
cloned.snapshotAttached = snapshotAttached;
|
|
5261
|
+
if (cloned.network && typeof cloned.network === "object") {
|
|
5262
|
+
const network = cloned.network;
|
|
5263
|
+
network.sourceSessionId = null;
|
|
5264
|
+
network.activeCaptureSessionId = null;
|
|
5265
|
+
network.detachedForExport = true;
|
|
5266
|
+
}
|
|
5267
|
+
if (cloned.validation && typeof cloned.validation === "object") {
|
|
5268
|
+
const validation = cloned.validation;
|
|
5269
|
+
validation.sourceSessionId = null;
|
|
5270
|
+
validation.replaySessionId = null;
|
|
5271
|
+
validation.detachedForExport = true;
|
|
5272
|
+
}
|
|
5273
|
+
return { value: cloned, detached: true };
|
|
5274
|
+
}
|
|
5275
|
+
function detachPortableSessionDataForExport(sessionData, detachedEsvp, detached = false) {
|
|
5276
|
+
if (!sessionData) return null;
|
|
5277
|
+
const cloned = cloneJsonRecord(sessionData) || {};
|
|
5278
|
+
if (detached) {
|
|
5279
|
+
cloned.esvp = detachedEsvp;
|
|
5280
|
+
if (cloned.networkCapture && typeof cloned.networkCapture === "object") {
|
|
5281
|
+
const networkCapture = cloned.networkCapture;
|
|
5282
|
+
networkCapture.sessionId = null;
|
|
5283
|
+
networkCapture.detachedForExport = true;
|
|
5284
|
+
}
|
|
5285
|
+
}
|
|
5286
|
+
return cloned;
|
|
5287
|
+
}
|
|
5238
5288
|
function isESVPReplayValidationSupported(result) {
|
|
5239
5289
|
if (!result) return true;
|
|
5240
5290
|
if (result.supported === false) return false;
|
|
@@ -5394,6 +5444,14 @@ app.post("/api/export", async (c) => {
|
|
|
5394
5444
|
};
|
|
5395
5445
|
}
|
|
5396
5446
|
}
|
|
5447
|
+
const detachedEsvpResult = detachPortableESVPForExport(esvp, !!esvpSnapshot);
|
|
5448
|
+
const exportedESVP = detachedEsvpResult.value;
|
|
5449
|
+
const exportedSessionData = detachPortableSessionDataForExport(
|
|
5450
|
+
sessionData,
|
|
5451
|
+
exportedESVP,
|
|
5452
|
+
detachedEsvpResult.detached
|
|
5453
|
+
);
|
|
5454
|
+
const exportedESVPSessionId = detachedEsvpResult.detached ? null : esvpSessionId;
|
|
5397
5455
|
const packagedProject = {
|
|
5398
5456
|
...normalizedProject,
|
|
5399
5457
|
videoPath: null,
|
|
@@ -5433,13 +5491,13 @@ app.post("/api/export", async (c) => {
|
|
|
5433
5491
|
recordingFolder: recordingIncluded,
|
|
5434
5492
|
networkEntries: networkEntries.length,
|
|
5435
5493
|
exportArtifacts: exportArtifactCount,
|
|
5436
|
-
esvpSessionId:
|
|
5494
|
+
esvpSessionId: exportedESVPSessionId || null
|
|
5437
5495
|
}
|
|
5438
5496
|
});
|
|
5439
5497
|
writeExportJson(join6(bundleRoot, "metadata", "project.json"), packagedProject);
|
|
5440
5498
|
writeExportJson(join6(bundleRoot, "metadata", "exports.json"), exportRecords);
|
|
5441
|
-
if (
|
|
5442
|
-
writeExportJson(join6(bundleRoot, "metadata", "session.json"),
|
|
5499
|
+
if (exportedSessionData) {
|
|
5500
|
+
writeExportJson(join6(bundleRoot, "metadata", "session.json"), exportedSessionData);
|
|
5443
5501
|
}
|
|
5444
5502
|
if (rawProject.taskHubLinks) {
|
|
5445
5503
|
writeExportJson(join6(bundleRoot, "taskhub", "links.json"), normalizedProject.taskHubLinks);
|
|
@@ -5465,8 +5523,8 @@ app.post("/api/export", async (c) => {
|
|
|
5465
5523
|
if (networkCapture) {
|
|
5466
5524
|
writeExportJson(join6(bundleRoot, "network", "capture.json"), networkCapture);
|
|
5467
5525
|
}
|
|
5468
|
-
if (
|
|
5469
|
-
writeExportJson(join6(bundleRoot, "network", "esvp.json"),
|
|
5526
|
+
if (exportedESVP) {
|
|
5527
|
+
writeExportJson(join6(bundleRoot, "network", "esvp.json"), exportedESVP);
|
|
5470
5528
|
}
|
|
5471
5529
|
if (esvpSnapshot) {
|
|
5472
5530
|
writeExportJson(join6(bundleRoot, "esvp", "snapshot.json"), esvpSnapshot);
|
|
@@ -5984,27 +6042,54 @@ app.post("/api/export/infographic", async (c) => {
|
|
|
5984
6042
|
const [project] = await db.select().from(projects).where(eq(projects.id, projectId)).limit(1);
|
|
5985
6043
|
if (!project) return c.json({ error: "Project not found" }, 404);
|
|
5986
6044
|
const { FRAMES_DIR: fDir, EXPORTS_DIR: eDir, PROJECTS_DIR: pDir } = await import("./db-5ECN3O7F.js");
|
|
5987
|
-
const {
|
|
6045
|
+
const { buildInfographicData, generateInfographicHtml, resolveInfographicFrameInputs } = await import("./infographic-OSDIJM5M.js");
|
|
5988
6046
|
const dbFrames = await db.select().from(frames).where(eq(frames.projectId, projectId)).orderBy(frames.frameNumber).limit(20);
|
|
5989
|
-
|
|
5990
|
-
|
|
5991
|
-
|
|
5992
|
-
|
|
5993
|
-
|
|
5994
|
-
|
|
5995
|
-
|
|
5996
|
-
|
|
5997
|
-
|
|
5998
|
-
|
|
5999
|
-
|
|
6047
|
+
const resolvedFrames = resolveInfographicFrameInputs(
|
|
6048
|
+
dbFrames,
|
|
6049
|
+
join6(fDir, projectId),
|
|
6050
|
+
project.videoPath,
|
|
6051
|
+
pDir,
|
|
6052
|
+
projectId
|
|
6053
|
+
);
|
|
6054
|
+
if (resolvedFrames.frameFiles.length === 0) {
|
|
6055
|
+
return c.json({
|
|
6056
|
+
error: resolvedFrames.candidateCount > 0 ? "No readable frames found for infographic export." : "No frames found. Run analyzer first.",
|
|
6057
|
+
debug: {
|
|
6058
|
+
frameCandidates: resolvedFrames.candidateCount,
|
|
6059
|
+
validFrames: 0,
|
|
6060
|
+
source: resolvedFrames.source,
|
|
6061
|
+
invalidFrames: resolvedFrames.invalidFrames.slice(0, 5)
|
|
6062
|
+
}
|
|
6063
|
+
}, 400);
|
|
6000
6064
|
}
|
|
6001
6065
|
const cached = annotationCache.get(projectId);
|
|
6002
6066
|
const annotations = cached?.steps?.map((s) => ({ label: s }));
|
|
6003
|
-
const data = buildInfographicData(project, frameFiles, frameOcr, annotations);
|
|
6067
|
+
const data = buildInfographicData(project, resolvedFrames.frameFiles, resolvedFrames.frameOcr, annotations);
|
|
6068
|
+
if (data.frames.length === 0) {
|
|
6069
|
+
return c.json({
|
|
6070
|
+
error: "Infographic export produced no embeddable frames.",
|
|
6071
|
+
debug: {
|
|
6072
|
+
frameCandidates: resolvedFrames.candidateCount,
|
|
6073
|
+
validFrames: resolvedFrames.frameFiles.length,
|
|
6074
|
+
source: resolvedFrames.source,
|
|
6075
|
+
invalidFrames: resolvedFrames.invalidFrames.slice(0, 5)
|
|
6076
|
+
}
|
|
6077
|
+
}, 400);
|
|
6078
|
+
}
|
|
6004
6079
|
const slug = (project.marketingTitle || project.name || projectId).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 40);
|
|
6005
6080
|
const outputPath = join6(eDir, `${slug}-infographic.html`);
|
|
6006
6081
|
const result = generateInfographicHtml(data, outputPath);
|
|
6007
|
-
if (!result.success)
|
|
6082
|
+
if (!result.success) {
|
|
6083
|
+
return c.json({
|
|
6084
|
+
error: result.error,
|
|
6085
|
+
debug: {
|
|
6086
|
+
frameCandidates: resolvedFrames.candidateCount,
|
|
6087
|
+
validFrames: resolvedFrames.frameFiles.length,
|
|
6088
|
+
source: resolvedFrames.source,
|
|
6089
|
+
invalidFrames: resolvedFrames.invalidFrames.slice(0, 5)
|
|
6090
|
+
}
|
|
6091
|
+
}, 500);
|
|
6092
|
+
}
|
|
6008
6093
|
if (open) {
|
|
6009
6094
|
const { exec: exec2 } = await import("child_process");
|
|
6010
6095
|
exec2(`open "${result.outputPath}"`);
|
|
@@ -3789,29 +3789,25 @@ var exportInfographicTool = {
|
|
|
3789
3789
|
try {
|
|
3790
3790
|
const { projects: projectsTable, frames: framesTable, FRAMES_DIR, PROJECTS_DIR: PROJECTS_DIR3 } = await import("./db-5ECN3O7F.js");
|
|
3791
3791
|
const { eq: eq4 } = await import("drizzle-orm");
|
|
3792
|
-
const {
|
|
3792
|
+
const { buildInfographicData, generateInfographicHtml, resolveInfographicFrameInputs } = await import("./infographic-OSDIJM5M.js");
|
|
3793
3793
|
const db = getDatabase();
|
|
3794
3794
|
const [project] = await db.select().from(projectsTable).where(eq4(projectsTable.id, params.projectId)).limit(1);
|
|
3795
3795
|
if (!project) return createErrorResult(`Project not found: ${params.projectId}`);
|
|
3796
3796
|
const dbFrames = await db.select().from(framesTable).where(eq4(framesTable.projectId, project.id)).orderBy(framesTable.frameNumber).limit(20);
|
|
3797
|
-
|
|
3798
|
-
|
|
3799
|
-
|
|
3800
|
-
|
|
3801
|
-
|
|
3802
|
-
|
|
3803
|
-
|
|
3804
|
-
|
|
3805
|
-
|
|
3806
|
-
PROJECTS_DIR3,
|
|
3807
|
-
project.id
|
|
3808
|
-
);
|
|
3809
|
-
frameOcr = frameFiles.map(() => ({ ocrText: null }));
|
|
3797
|
+
const resolvedFrames = resolveInfographicFrameInputs(
|
|
3798
|
+
dbFrames,
|
|
3799
|
+
path5.join(FRAMES_DIR, project.id),
|
|
3800
|
+
project.videoPath,
|
|
3801
|
+
PROJECTS_DIR3,
|
|
3802
|
+
project.id
|
|
3803
|
+
);
|
|
3804
|
+
if (resolvedFrames.frameFiles.length === 0) {
|
|
3805
|
+
return createErrorResult(`No readable frames found for infographic export. Checked ${resolvedFrames.candidateCount} candidate(s) from ${resolvedFrames.source}.`);
|
|
3810
3806
|
}
|
|
3811
|
-
|
|
3812
|
-
|
|
3807
|
+
const data = buildInfographicData(project, resolvedFrames.frameFiles, resolvedFrames.frameOcr);
|
|
3808
|
+
if (data.frames.length === 0) {
|
|
3809
|
+
return createErrorResult("Infographic export produced no embeddable frames.");
|
|
3813
3810
|
}
|
|
3814
|
-
const data = buildInfographicData(project, frameFiles, frameOcr);
|
|
3815
3811
|
const slug = (project.marketingTitle || project.name || project.id).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 40);
|
|
3816
3812
|
const outputFilePath = params.outputPath ? path5.join(params.outputPath, `${slug}-infographic.html`) : path5.join(EXPORTS_DIR, `${slug}-infographic.html`);
|
|
3817
3813
|
const result = generateInfographicHtml(data, outputFilePath);
|
|
@@ -7231,21 +7227,21 @@ var knowledgeOpenTool = {
|
|
|
7231
7227
|
const { FRAMES_DIR, PROJECTS_DIR: PROJECTS_DIR3 } = await import("./db-5ECN3O7F.js");
|
|
7232
7228
|
const { join: join12 } = await import("path");
|
|
7233
7229
|
const dbFrames = await db.select().from(frames).where(eq4(frames.projectId, project.id)).orderBy(frames.frameNumber).limit(15);
|
|
7234
|
-
|
|
7235
|
-
|
|
7236
|
-
|
|
7237
|
-
|
|
7238
|
-
|
|
7239
|
-
|
|
7240
|
-
|
|
7241
|
-
|
|
7242
|
-
|
|
7230
|
+
const { resolveInfographicFrameInputs, buildInfographicData, generateInfographicHtmlString } = await import("./infographic-OSDIJM5M.js");
|
|
7231
|
+
const resolvedFrames = resolveInfographicFrameInputs(
|
|
7232
|
+
dbFrames,
|
|
7233
|
+
join12(FRAMES_DIR, project.id),
|
|
7234
|
+
project.videoPath,
|
|
7235
|
+
PROJECTS_DIR3,
|
|
7236
|
+
project.id
|
|
7237
|
+
);
|
|
7238
|
+
if (resolvedFrames.frameFiles.length === 0) {
|
|
7239
|
+
return createTextResult(`Project "${project.marketingTitle || project.name}" has no readable frames. Run the analyzer again, then try again.`);
|
|
7243
7240
|
}
|
|
7244
|
-
|
|
7245
|
-
|
|
7241
|
+
const data = buildInfographicData(project, resolvedFrames.frameFiles, resolvedFrames.frameOcr);
|
|
7242
|
+
if (data.frames.length === 0) {
|
|
7243
|
+
return createErrorResult(`Project "${project.marketingTitle || project.name}" did not produce embeddable infographic frames.`);
|
|
7246
7244
|
}
|
|
7247
|
-
const { buildInfographicData, generateInfographicHtmlString } = await import("./infographic-GQAHEOAA.js");
|
|
7248
|
-
const data = buildInfographicData(project, frameFiles, frameOcr);
|
|
7249
7245
|
const html = generateInfographicHtmlString(data);
|
|
7250
7246
|
if (!html) {
|
|
7251
7247
|
return createErrorResult("Failed to generate infographic HTML (template not found)");
|
|
@@ -7273,7 +7269,7 @@ var knowledgeOpenTool = {
|
|
|
7273
7269
|
structuredContent: {
|
|
7274
7270
|
projectId: project.id,
|
|
7275
7271
|
name: project.marketingTitle || project.name,
|
|
7276
|
-
frameCount: frameFiles.length,
|
|
7272
|
+
frameCount: resolvedFrames.frameFiles.length,
|
|
7277
7273
|
platform: project.platform || "unknown"
|
|
7278
7274
|
},
|
|
7279
7275
|
_meta: {
|
package/dist/cli.js
CHANGED
|
@@ -390,7 +390,7 @@ program.command("serve").alias("server").description("Start the DiscoveryLab web
|
|
|
390
390
|
console.log(chalk.cyan("\n DiscoveryLab"));
|
|
391
391
|
console.log(chalk.gray(" AI-powered app testing & evidence generator\n"));
|
|
392
392
|
try {
|
|
393
|
-
const { startServer } = await import("./server-
|
|
393
|
+
const { startServer } = await import("./server-GXNAKM4H.js");
|
|
394
394
|
await startServer(port);
|
|
395
395
|
console.log(chalk.green(` Server running at http://localhost:${port}`));
|
|
396
396
|
console.log(chalk.gray(" Press Ctrl+C to stop\n"));
|
|
@@ -550,7 +550,7 @@ program.command("mcp").description("Run as MCP server (for Claude Code integrati
|
|
|
550
550
|
taskHubTools,
|
|
551
551
|
esvpTools,
|
|
552
552
|
knowledgeTools
|
|
553
|
-
} = await import("./tools-
|
|
553
|
+
} = await import("./tools-6BTUMR3G.js");
|
|
554
554
|
mcpServer.registerTools([
|
|
555
555
|
...uiTools,
|
|
556
556
|
...projectTools,
|
|
@@ -597,7 +597,7 @@ program.command("export").description("Export project in various formats").argum
|
|
|
597
597
|
const { join: pathJoin } = await import("path");
|
|
598
598
|
const { getDatabase, projects, frames: framesTable, FRAMES_DIR, EXPORTS_DIR, PROJECTS_DIR } = await import("./db-5ECN3O7F.js");
|
|
599
599
|
const { eq } = await import("drizzle-orm");
|
|
600
|
-
const {
|
|
600
|
+
const { buildInfographicData, generateInfographicHtml, resolveInfographicFrameInputs } = await import("./infographic-OSDIJM5M.js");
|
|
601
601
|
const db = getDatabase();
|
|
602
602
|
const allProjects = await db.select().from(projects);
|
|
603
603
|
const project = allProjects.find((p) => p.id === projectId || p.id.startsWith(projectId) || p.name.toLowerCase().includes(projectId.toLowerCase()));
|
|
@@ -611,21 +611,27 @@ program.command("export").description("Export project in various formats").argum
|
|
|
611
611
|
}
|
|
612
612
|
console.log(chalk.green(` \u2714 Found project: ${project.marketingTitle || project.name}`));
|
|
613
613
|
const dbFrames = await db.select().from(framesTable).where(eq(framesTable.projectId, project.id)).orderBy(framesTable.frameNumber).limit(20);
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
614
|
+
const resolvedFrames = resolveInfographicFrameInputs(
|
|
615
|
+
dbFrames,
|
|
616
|
+
pathJoin(FRAMES_DIR, project.id),
|
|
617
|
+
project.videoPath,
|
|
618
|
+
PROJECTS_DIR,
|
|
619
|
+
project.id
|
|
620
|
+
);
|
|
621
|
+
console.log(
|
|
622
|
+
chalk.green(
|
|
623
|
+
` \u2714 ${resolvedFrames.frameFiles.length} readable frames found` + (resolvedFrames.candidateCount > 0 ? ` (from ${resolvedFrames.candidateCount} candidates via ${resolvedFrames.source})` : "")
|
|
624
|
+
)
|
|
625
|
+
);
|
|
626
|
+
if (resolvedFrames.frameFiles.length === 0) {
|
|
627
|
+
console.log(chalk.red(" No readable frames found. Run analyzer first or refresh project screenshots."));
|
|
628
|
+
return;
|
|
622
629
|
}
|
|
623
|
-
|
|
624
|
-
if (
|
|
625
|
-
console.log(chalk.red("
|
|
630
|
+
const data = buildInfographicData(project, resolvedFrames.frameFiles, resolvedFrames.frameOcr);
|
|
631
|
+
if (data.frames.length === 0) {
|
|
632
|
+
console.log(chalk.red(" Export failed: infographic payload did not produce embeddable frames."));
|
|
626
633
|
return;
|
|
627
634
|
}
|
|
628
|
-
const data = buildInfographicData(project, frameFiles, frameOcr);
|
|
629
635
|
console.log(chalk.green(` \u2714 ${project.aiSummary ? "AI analysis loaded" : "No analysis (basic labels)"}`));
|
|
630
636
|
const slug = (project.marketingTitle || project.name || project.id).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 40);
|
|
631
637
|
const outputPath = opts.output ? pathJoin(opts.output, `${slug}-infographic.html`) : pathJoin(EXPORTS_DIR, `${slug}-infographic.html`);
|
|
@@ -6,128 +6,379 @@
|
|
|
6
6
|
<title>__TITLE__</title>
|
|
7
7
|
<style>
|
|
8
8
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
9
|
-
:root {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
9
|
+
:root {
|
|
10
|
+
--accent: #6366f1;
|
|
11
|
+
--bg: #0f0f17;
|
|
12
|
+
--bg2: #16161f;
|
|
13
|
+
--bg3: #1e1e2a;
|
|
14
|
+
--text: #e4e4e7;
|
|
15
|
+
--muted: #71717a;
|
|
16
|
+
--ok: #22c55e;
|
|
17
|
+
--warn: #eab308;
|
|
18
|
+
--header-h: 56px;
|
|
19
|
+
--chips-h: 48px;
|
|
20
|
+
--glass-bg: linear-gradient(180deg, rgba(22,22,31,0.82), rgba(22,22,31,0.62));
|
|
21
|
+
--glass-border: rgba(255,255,255,0.09);
|
|
22
|
+
}
|
|
23
|
+
body {
|
|
24
|
+
background: radial-gradient(circle at top, rgba(99,102,241,0.16), transparent 24%), var(--bg);
|
|
25
|
+
color: var(--text);
|
|
26
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
|
|
27
|
+
min-height: 100vh;
|
|
28
|
+
overflow-y: auto;
|
|
29
|
+
overflow-x: hidden;
|
|
30
|
+
}
|
|
31
|
+
.viewer-shell {
|
|
32
|
+
display: grid;
|
|
33
|
+
min-height: 100vh;
|
|
34
|
+
grid-template-rows: auto auto 1fr auto;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/* ── Header ── */
|
|
38
|
+
.header {
|
|
39
|
+
display: flex;
|
|
40
|
+
align-items: center;
|
|
41
|
+
justify-content: space-between;
|
|
42
|
+
gap: 12px;
|
|
43
|
+
min-height: var(--header-h);
|
|
44
|
+
padding: 10px 22px;
|
|
45
|
+
border-bottom: 1px solid var(--glass-border);
|
|
46
|
+
position: sticky;
|
|
47
|
+
top: 0;
|
|
48
|
+
z-index: 30;
|
|
49
|
+
background: var(--glass-bg);
|
|
50
|
+
backdrop-filter: blur(20px) saturate(1.35);
|
|
51
|
+
-webkit-backdrop-filter: blur(20px) saturate(1.35);
|
|
52
|
+
}
|
|
53
|
+
.header-left, .header-right {
|
|
54
|
+
display: flex;
|
|
55
|
+
align-items: center;
|
|
56
|
+
gap: 10px;
|
|
57
|
+
flex-wrap: wrap;
|
|
58
|
+
}
|
|
59
|
+
.header-title { font-size: 15px; font-weight: 700; }
|
|
60
|
+
.header-platform {
|
|
61
|
+
font-size: 10px;
|
|
62
|
+
padding: 2px 8px;
|
|
63
|
+
background: rgba(99,102,241,0.12);
|
|
64
|
+
color: #aeb2ff;
|
|
65
|
+
border-radius: 10px;
|
|
66
|
+
}
|
|
67
|
+
.header-date, .header-step { font-size: 11px; color: var(--muted); }
|
|
68
|
+
|
|
69
|
+
/* ── Chips ── */
|
|
70
|
+
.chips {
|
|
71
|
+
display: flex;
|
|
72
|
+
align-items: center;
|
|
73
|
+
gap: 6px;
|
|
74
|
+
min-height: var(--chips-h);
|
|
75
|
+
padding: 6px 20px 8px;
|
|
76
|
+
overflow-x: auto;
|
|
77
|
+
scrollbar-width: none;
|
|
78
|
+
position: sticky;
|
|
79
|
+
top: var(--header-h);
|
|
80
|
+
z-index: 25;
|
|
81
|
+
background: var(--glass-bg);
|
|
82
|
+
backdrop-filter: blur(20px) saturate(1.35);
|
|
83
|
+
-webkit-backdrop-filter: blur(20px) saturate(1.35);
|
|
84
|
+
border-bottom: 1px solid rgba(255,255,255,0.04);
|
|
85
|
+
}
|
|
21
86
|
.chips::-webkit-scrollbar { display: none; }
|
|
22
|
-
.chip {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
.
|
|
36
|
-
.phone img.in { opacity: 1; position: relative; }
|
|
87
|
+
.chip {
|
|
88
|
+
white-space: nowrap;
|
|
89
|
+
font-size: 10px;
|
|
90
|
+
padding: 5px 12px;
|
|
91
|
+
border-radius: 999px;
|
|
92
|
+
background: var(--bg2);
|
|
93
|
+
border: 1px solid rgba(255,255,255,0.06);
|
|
94
|
+
color: var(--muted);
|
|
95
|
+
cursor: pointer;
|
|
96
|
+
transition: all 120ms ease;
|
|
97
|
+
font-weight: 500;
|
|
98
|
+
flex-shrink: 0;
|
|
99
|
+
}
|
|
100
|
+
.chip.active { background: var(--accent); color: #fff; border-color: transparent; }
|
|
37
101
|
|
|
38
|
-
/*
|
|
39
|
-
.
|
|
102
|
+
/* ── Main Layout ── */
|
|
103
|
+
.main {
|
|
104
|
+
display: grid;
|
|
105
|
+
grid-template-columns: minmax(0, 1fr) 340px;
|
|
106
|
+
min-height: 0;
|
|
107
|
+
align-items: start;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/* ── Viewer (device area) ── */
|
|
111
|
+
.viewer {
|
|
112
|
+
display: flex;
|
|
113
|
+
align-items: flex-start;
|
|
114
|
+
justify-content: center;
|
|
115
|
+
position: relative;
|
|
116
|
+
padding: 24px 28px 200px;
|
|
117
|
+
min-height: calc(100vh - var(--header-h) - var(--chips-h));
|
|
118
|
+
}
|
|
119
|
+
.device-wrapper {
|
|
120
|
+
display: flex;
|
|
121
|
+
align-items: flex-start;
|
|
122
|
+
justify-content: center;
|
|
123
|
+
gap: 12px;
|
|
124
|
+
width: 100%;
|
|
125
|
+
max-width: 740px;
|
|
126
|
+
margin: 0 auto;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/* ── Side Annotations ── */
|
|
130
|
+
.side-annots {
|
|
131
|
+
display: flex;
|
|
132
|
+
flex-direction: column;
|
|
133
|
+
gap: 8px;
|
|
134
|
+
width: 160px;
|
|
135
|
+
flex-shrink: 0;
|
|
136
|
+
opacity: 0;
|
|
137
|
+
transition: opacity 200ms ease;
|
|
138
|
+
pointer-events: none;
|
|
139
|
+
padding-top: 20px;
|
|
140
|
+
}
|
|
40
141
|
.side-annots.show { opacity: 1; pointer-events: auto; }
|
|
41
|
-
.side-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
142
|
+
.side-annot {
|
|
143
|
+
padding: 8px 10px;
|
|
144
|
+
background: rgba(255,255,255,0.04);
|
|
145
|
+
border: 1px solid rgba(255,255,255,0.08);
|
|
146
|
+
border-radius: 10px;
|
|
147
|
+
}
|
|
47
148
|
.side-annot-title { font-size: 10px; font-weight: 600; color: var(--text); display: flex; align-items: center; gap: 5px; }
|
|
48
149
|
.side-annot-dot { width: 5px; height: 5px; border-radius: 50%; flex-shrink: 0; }
|
|
49
|
-
.side-annot-desc { font-size: 9px; color: var(--muted); margin-top: 2px;
|
|
50
|
-
|
|
51
|
-
/*
|
|
52
|
-
.
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
.
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
.
|
|
150
|
+
.side-annot-desc { font-size: 9px; line-height: 1.4; color: var(--muted); margin-top: 2px; }
|
|
151
|
+
|
|
152
|
+
/* ── Device frame ── */
|
|
153
|
+
.device-center {
|
|
154
|
+
display: flex;
|
|
155
|
+
flex-direction: column;
|
|
156
|
+
align-items: center;
|
|
157
|
+
gap: 0;
|
|
158
|
+
flex: 1;
|
|
159
|
+
min-width: 0;
|
|
160
|
+
max-width: 380px;
|
|
161
|
+
}
|
|
162
|
+
.phone {
|
|
163
|
+
width: 100%;
|
|
164
|
+
max-width: 340px;
|
|
165
|
+
background: #000;
|
|
166
|
+
border-radius: 32px;
|
|
167
|
+
overflow: hidden;
|
|
168
|
+
box-shadow: 0 20px 60px rgba(0,0,0,0.55), 0 0 0 1px rgba(255,255,255,0.06);
|
|
169
|
+
position: relative;
|
|
170
|
+
}
|
|
171
|
+
.phone img {
|
|
172
|
+
width: 100%;
|
|
173
|
+
display: block;
|
|
174
|
+
aspect-ratio: 9 / 19.5;
|
|
175
|
+
object-fit: cover;
|
|
176
|
+
object-position: top center;
|
|
177
|
+
background: #000;
|
|
178
|
+
transition: opacity 0.35s ease;
|
|
179
|
+
}
|
|
180
|
+
.phone img.out { opacity: 0; position: absolute; top: 0; left: 0; }
|
|
181
|
+
.phone img.in { opacity: 1; position: relative; }
|
|
182
|
+
|
|
183
|
+
/* ── Floating Play/Pause (Liquid Glass) ── */
|
|
184
|
+
.fab-controls {
|
|
185
|
+
position: fixed;
|
|
186
|
+
left: 16px;
|
|
187
|
+
bottom: 220px;
|
|
188
|
+
z-index: 40;
|
|
189
|
+
display: flex;
|
|
190
|
+
flex-direction: column;
|
|
191
|
+
align-items: center;
|
|
192
|
+
gap: 8px;
|
|
193
|
+
padding: 10px 8px;
|
|
194
|
+
border-radius: 20px;
|
|
195
|
+
background: linear-gradient(180deg, rgba(255,255,255,0.12), rgba(255,255,255,0.04));
|
|
196
|
+
border: 1px solid rgba(255,255,255,0.14);
|
|
197
|
+
backdrop-filter: blur(24px) saturate(1.4);
|
|
198
|
+
-webkit-backdrop-filter: blur(24px) saturate(1.4);
|
|
199
|
+
box-shadow: 0 8px 32px rgba(0,0,0,0.35), inset 0 1px 0 rgba(255,255,255,0.1);
|
|
200
|
+
}
|
|
201
|
+
.fab-btn {
|
|
202
|
+
width: 36px;
|
|
203
|
+
height: 36px;
|
|
204
|
+
border-radius: 50%;
|
|
205
|
+
background: rgba(99,102,241,0.2);
|
|
206
|
+
border: 1.5px solid rgba(255,255,255,0.18);
|
|
207
|
+
color: var(--text);
|
|
208
|
+
cursor: pointer;
|
|
209
|
+
display: flex;
|
|
210
|
+
align-items: center;
|
|
211
|
+
justify-content: center;
|
|
212
|
+
transition: all 0.2s;
|
|
213
|
+
}
|
|
214
|
+
.fab-btn:hover { background: var(--accent); border-color: var(--accent); }
|
|
215
|
+
.fab-dots {
|
|
216
|
+
display: flex;
|
|
217
|
+
flex-direction: column;
|
|
218
|
+
gap: 4px;
|
|
219
|
+
align-items: center;
|
|
220
|
+
}
|
|
221
|
+
.fab-dot {
|
|
222
|
+
width: 6px;
|
|
223
|
+
height: 6px;
|
|
224
|
+
border-radius: 50%;
|
|
225
|
+
background: rgba(255,255,255,0.18);
|
|
226
|
+
cursor: pointer;
|
|
227
|
+
transition: all 0.25s;
|
|
228
|
+
}
|
|
229
|
+
.fab-dot.active { background: var(--accent); height: 14px; border-radius: 3px; }
|
|
230
|
+
.fab-status {
|
|
231
|
+
font-size: 8px;
|
|
232
|
+
color: rgba(255,255,255,0.45);
|
|
233
|
+
text-transform: uppercase;
|
|
234
|
+
letter-spacing: 0.06em;
|
|
235
|
+
writing-mode: vertical-lr;
|
|
236
|
+
text-orientation: mixed;
|
|
237
|
+
transform: rotate(180deg);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/* ── Right Panel ── */
|
|
241
|
+
.panel {
|
|
242
|
+
padding: 18px 16px 200px;
|
|
243
|
+
border-left: 1px solid rgba(255,255,255,0.05);
|
|
244
|
+
overflow-y: auto;
|
|
245
|
+
background: var(--bg2);
|
|
246
|
+
display: flex;
|
|
247
|
+
flex-direction: column;
|
|
248
|
+
gap: 16px;
|
|
249
|
+
min-height: calc(100vh - var(--header-h) - var(--chips-h));
|
|
250
|
+
position: sticky;
|
|
251
|
+
top: calc(var(--header-h) + var(--chips-h));
|
|
252
|
+
max-height: calc(100vh - var(--header-h) - var(--chips-h));
|
|
253
|
+
}
|
|
254
|
+
.panel-section { display: grid; gap: 6px; }
|
|
255
|
+
.panel-label {
|
|
256
|
+
font-size: 9px;
|
|
257
|
+
font-weight: 700;
|
|
258
|
+
text-transform: uppercase;
|
|
259
|
+
letter-spacing: 0.08em;
|
|
260
|
+
color: var(--muted);
|
|
261
|
+
}
|
|
262
|
+
.project-summary, .step-desc { font-size: 12px; color: var(--muted); line-height: 1.55; }
|
|
263
|
+
.step-number { font-size: 11px; color: #aeb2ff; font-weight: 700; }
|
|
264
|
+
.step-title { font-size: 15px; font-weight: 700; line-height: 1.25; }
|
|
265
|
+
.annot-cards { display: flex; flex-direction: column; gap: 8px; }
|
|
266
|
+
.annot-card {
|
|
267
|
+
padding: 8px 10px;
|
|
268
|
+
background: var(--bg3);
|
|
269
|
+
border-radius: 10px;
|
|
270
|
+
border-left: 3px solid var(--accent);
|
|
271
|
+
transition: all 0.2s;
|
|
272
|
+
}
|
|
71
273
|
.annot-card:hover { background: rgba(99,102,241,0.08); }
|
|
72
274
|
.annot-card-title { font-size: 11px; font-weight: 600; display: flex; align-items: center; gap: 6px; }
|
|
73
275
|
.annot-card-dot { width: 6px; height: 6px; border-radius: 50%; flex-shrink: 0; }
|
|
74
276
|
.annot-card-desc { font-size: 10px; color: var(--muted); margin-top: 2px; line-height: 1.4; }
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
277
|
+
.baseline-badge {
|
|
278
|
+
display: inline-flex;
|
|
279
|
+
align-items: center;
|
|
280
|
+
gap: 6px;
|
|
281
|
+
width: fit-content;
|
|
282
|
+
font-size: 11px;
|
|
283
|
+
padding: 4px 10px;
|
|
284
|
+
border-radius: 999px;
|
|
285
|
+
background: rgba(113,113,122,0.1);
|
|
286
|
+
color: var(--muted);
|
|
287
|
+
}
|
|
288
|
+
.baseline-badge.ok { background: rgba(34,197,94,0.12); color: var(--ok); }
|
|
289
|
+
.baseline-badge.warn { background: rgba(234,179,8,0.12); color: var(--warn); }
|
|
290
|
+
.bl-dot { width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
|
|
81
291
|
.footer { text-align: center; padding: 6px; font-size: 8px; color: rgba(255,255,255,0.12); }
|
|
82
292
|
|
|
83
|
-
/*
|
|
84
|
-
@
|
|
85
|
-
|
|
293
|
+
/* ── Responsive ── */
|
|
294
|
+
@media (max-width: 980px) {
|
|
295
|
+
.main { grid-template-columns: 1fr; }
|
|
296
|
+
.panel {
|
|
297
|
+
border-left: none;
|
|
298
|
+
border-top: 1px solid rgba(255,255,255,0.05);
|
|
299
|
+
position: static;
|
|
300
|
+
max-height: none;
|
|
301
|
+
min-height: auto;
|
|
302
|
+
padding-bottom: 200px;
|
|
303
|
+
}
|
|
304
|
+
.viewer { padding: 20px 16px 40px; }
|
|
305
|
+
.side-annots { width: 130px; }
|
|
306
|
+
.fab-controls { left: 12px; bottom: 200px; }
|
|
307
|
+
}
|
|
308
|
+
@media (max-width: 720px) {
|
|
309
|
+
.viewer { padding: 16px 12px 40px; }
|
|
310
|
+
.phone { max-width: 280px; border-radius: 26px; }
|
|
311
|
+
.side-annots { display: none; }
|
|
312
|
+
.chips { top: var(--header-h); }
|
|
313
|
+
.panel { min-height: auto; padding-bottom: 200px; }
|
|
314
|
+
.device-wrapper { max-width: 320px; }
|
|
315
|
+
.fab-controls { left: 8px; bottom: 180px; padding: 8px 6px; border-radius: 16px; }
|
|
316
|
+
.fab-btn { width: 32px; height: 32px; }
|
|
317
|
+
}
|
|
318
|
+
@media (max-width: 480px) {
|
|
319
|
+
.header { padding: 8px 14px; }
|
|
320
|
+
.header-title { font-size: 13px; }
|
|
321
|
+
.chips { padding: 6px 14px; }
|
|
322
|
+
.phone { max-width: 240px; border-radius: 22px; }
|
|
323
|
+
}
|
|
86
324
|
</style>
|
|
87
325
|
</head>
|
|
88
326
|
<body>
|
|
89
|
-
<div class="
|
|
90
|
-
<div class="header
|
|
91
|
-
<div class="header-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
<div class="header-date" id="date"></div>
|
|
95
|
-
</div>
|
|
96
|
-
<div class="chips" id="chips"></div>
|
|
97
|
-
<div class="main">
|
|
98
|
-
<div class="viewer">
|
|
99
|
-
<div class="device-area" id="deviceArea" style="position: relative;">
|
|
100
|
-
<div class="side-annots left" id="annotsLeft"></div>
|
|
101
|
-
<div class="phone" id="phone"></div>
|
|
102
|
-
<div class="side-annots right" id="annotsRight"></div>
|
|
327
|
+
<div class="viewer-shell">
|
|
328
|
+
<div class="header">
|
|
329
|
+
<div class="header-left">
|
|
330
|
+
<div class="header-title" id="title"></div>
|
|
331
|
+
<div class="header-platform" id="platform"></div>
|
|
103
332
|
</div>
|
|
104
|
-
<div class="
|
|
105
|
-
<
|
|
106
|
-
|
|
107
|
-
<svg id="pauseIco" width="12" height="12" viewBox="0 0 24 24" fill="currentColor" style="display:none"><rect x="6" y="4" width="4" height="16"/><rect x="14" y="4" width="4" height="16"/></svg>
|
|
108
|
-
</button>
|
|
109
|
-
<div class="dots" id="dots"></div>
|
|
110
|
-
<div class="status-text" id="statusText">playing</div>
|
|
333
|
+
<div class="header-right">
|
|
334
|
+
<div class="header-step" id="headerStep"></div>
|
|
335
|
+
<div class="header-date" id="date"></div>
|
|
111
336
|
</div>
|
|
112
337
|
</div>
|
|
113
|
-
<div class="
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
<div class="
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
338
|
+
<div class="chips" id="chips"></div>
|
|
339
|
+
<div class="main">
|
|
340
|
+
<div class="viewer">
|
|
341
|
+
<div class="device-wrapper">
|
|
342
|
+
<div class="side-annots left" id="annotsLeft"></div>
|
|
343
|
+
<div class="device-center">
|
|
344
|
+
<div class="phone" id="phone"></div>
|
|
345
|
+
</div>
|
|
346
|
+
<div class="side-annots right" id="annotsRight"></div>
|
|
347
|
+
</div>
|
|
123
348
|
</div>
|
|
124
|
-
<div class="panel
|
|
125
|
-
<div class="panel-
|
|
126
|
-
|
|
349
|
+
<div class="panel">
|
|
350
|
+
<div class="panel-section">
|
|
351
|
+
<div class="panel-label">Project</div>
|
|
352
|
+
<div class="project-summary" id="projectSummary"></div>
|
|
353
|
+
</div>
|
|
354
|
+
<div class="panel-section">
|
|
355
|
+
<div class="panel-label">Step</div>
|
|
356
|
+
<div class="step-number" id="stepNum"></div>
|
|
357
|
+
<div class="step-title" id="stepTitle"></div>
|
|
358
|
+
<div class="step-desc" id="stepDesc"></div>
|
|
359
|
+
</div>
|
|
360
|
+
<div class="panel-section">
|
|
361
|
+
<div class="panel-label">Annotations</div>
|
|
362
|
+
<div class="annot-cards" id="annotCards"></div>
|
|
363
|
+
</div>
|
|
364
|
+
<div class="panel-section">
|
|
365
|
+
<div class="panel-label">Baseline</div>
|
|
366
|
+
<div class="baseline-badge" id="blBadge"><div class="bl-dot"></div><span></span></div>
|
|
367
|
+
</div>
|
|
127
368
|
</div>
|
|
128
369
|
</div>
|
|
370
|
+
<div class="footer">Generated by DiscoveryLab</div>
|
|
371
|
+
</div>
|
|
372
|
+
|
|
373
|
+
<!-- Floating Liquid Glass Controls -->
|
|
374
|
+
<div class="fab-controls" id="fabControls">
|
|
375
|
+
<button class="fab-btn" id="playBtn" title="Play / Pause">
|
|
376
|
+
<svg id="playIco" width="14" height="14" viewBox="0 0 24 24" fill="currentColor" style="display:none"><polygon points="7 3 21 12 7 21"/></svg>
|
|
377
|
+
<svg id="pauseIco" width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><rect x="6" y="4" width="4" height="16"/><rect x="14" y="4" width="4" height="16"/></svg>
|
|
378
|
+
</button>
|
|
379
|
+
<div class="fab-dots" id="dots"></div>
|
|
380
|
+
<div class="fab-status" id="statusText">playing</div>
|
|
129
381
|
</div>
|
|
130
|
-
<div class="footer">Generated by DiscoveryLab</div>
|
|
131
382
|
|
|
132
383
|
<script>
|
|
133
384
|
const D = window.FLOW_DATA || { name: 'Flow', frames: [] };
|
|
@@ -137,8 +388,8 @@ let cur = 0, playing = true, timer = null;
|
|
|
137
388
|
document.getElementById('title').textContent = D.name || 'App Flow';
|
|
138
389
|
document.getElementById('platform').textContent = D.platform || '';
|
|
139
390
|
document.getElementById('date').textContent = D.recorded_at ? new Date(D.recorded_at).toLocaleDateString() : '';
|
|
391
|
+
document.getElementById('projectSummary').textContent = D.overview || 'Captured flow ready for review inside Claude Desktop.';
|
|
140
392
|
|
|
141
|
-
// Create images
|
|
142
393
|
const phone = document.getElementById('phone');
|
|
143
394
|
F.forEach((f, i) => {
|
|
144
395
|
const img = document.createElement('img');
|
|
@@ -148,57 +399,63 @@ F.forEach((f, i) => {
|
|
|
148
399
|
phone.appendChild(img);
|
|
149
400
|
});
|
|
150
401
|
|
|
151
|
-
// Chips
|
|
152
402
|
const chips = document.getElementById('chips');
|
|
153
403
|
F.forEach((f, i) => {
|
|
154
404
|
const c = document.createElement('div');
|
|
155
|
-
c.className =
|
|
156
|
-
c.textContent =
|
|
405
|
+
c.className = 'chip' + (i === 0 ? ' active' : '');
|
|
406
|
+
c.textContent = (i + 1) + '. ' + f.step_name;
|
|
157
407
|
c.onclick = () => { go(i); stop(); };
|
|
158
408
|
chips.appendChild(c);
|
|
159
409
|
});
|
|
160
410
|
|
|
161
|
-
// Dots
|
|
162
411
|
const dots = document.getElementById('dots');
|
|
163
412
|
F.forEach((_, i) => {
|
|
164
413
|
const d = document.createElement('div');
|
|
165
|
-
d.className =
|
|
414
|
+
d.className = 'fab-dot' + (i === 0 ? ' active' : '');
|
|
166
415
|
d.onclick = () => { go(i); stop(); };
|
|
167
416
|
dots.appendChild(d);
|
|
168
417
|
});
|
|
169
418
|
|
|
170
419
|
function go(i) {
|
|
171
420
|
cur = i;
|
|
172
|
-
phone.querySelectorAll('img').forEach((img, j) => { img.className = j===i?'in':'out'; });
|
|
173
|
-
chips.querySelectorAll('.chip').forEach((c, j) => c.classList.toggle('active', j===i));
|
|
174
|
-
dots.querySelectorAll('.dot').forEach((d, j) => d.classList.toggle('active', j===i));
|
|
421
|
+
phone.querySelectorAll('img').forEach((img, j) => { img.className = j === i ? 'in' : 'out'; });
|
|
422
|
+
chips.querySelectorAll('.chip').forEach((c, j) => c.classList.toggle('active', j === i));
|
|
423
|
+
dots.querySelectorAll('.fab-dot').forEach((d, j) => d.classList.toggle('active', j === i));
|
|
424
|
+
// Auto-scroll active chip into view
|
|
425
|
+
const activeChip = chips.querySelector('.chip.active');
|
|
426
|
+
if (activeChip) activeChip.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'center' });
|
|
175
427
|
updatePanel();
|
|
176
428
|
updateAnnotations();
|
|
177
429
|
}
|
|
178
430
|
|
|
179
431
|
function updatePanel() {
|
|
180
432
|
const f = F[cur]; if (!f) return;
|
|
181
|
-
document.getElementById('stepNum').textContent =
|
|
433
|
+
document.getElementById('stepNum').textContent = (cur + 1) + ' of ' + F.length;
|
|
182
434
|
document.getElementById('stepTitle').textContent = f.step_name;
|
|
183
435
|
document.getElementById('stepDesc').textContent = f.description;
|
|
436
|
+
document.getElementById('headerStep').textContent = 'Step ' + (cur + 1) + ' / ' + F.length;
|
|
184
437
|
|
|
185
|
-
// Annotation cards
|
|
186
438
|
const cards = document.getElementById('annotCards');
|
|
187
439
|
cards.innerHTML = '';
|
|
188
440
|
(f.hotspots || []).forEach(h => {
|
|
189
441
|
const card = document.createElement('div');
|
|
190
|
-
card.className = 'annot-card
|
|
442
|
+
card.className = 'annot-card';
|
|
191
443
|
card.style.borderLeftColor = h.color;
|
|
192
|
-
card.innerHTML =
|
|
444
|
+
card.innerHTML = '<div class="annot-card-title"><div class="annot-card-dot" style="background:' + h.color + '"></div>' + escHtml(h.title) + '</div><div class="annot-card-desc">' + escHtml(h.description) + '</div>';
|
|
193
445
|
cards.appendChild(card);
|
|
194
446
|
});
|
|
195
447
|
|
|
196
|
-
// Baseline
|
|
197
448
|
const bl = document.getElementById('blBadge');
|
|
198
449
|
const s = f.baseline_status || 'not_validated';
|
|
199
|
-
bl.className =
|
|
200
|
-
bl.querySelector('.bl-dot').style.background = s==='ok'?'var(--ok)':s==='changed'?'var(--warn)':'var(--muted)';
|
|
201
|
-
bl.querySelector('span').textContent = s==='ok'?'Validated':s==='changed'?'Changed':'No baseline';
|
|
450
|
+
bl.className = 'baseline-badge ' + (s === 'ok' ? 'ok' : s === 'changed' ? 'warn' : '');
|
|
451
|
+
bl.querySelector('.bl-dot').style.background = s === 'ok' ? 'var(--ok)' : s === 'changed' ? 'var(--warn)' : 'var(--muted)';
|
|
452
|
+
bl.querySelector('span').textContent = s === 'ok' ? 'Validated' : s === 'changed' ? 'Changed' : 'No baseline';
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function escHtml(s) {
|
|
456
|
+
const el = document.createElement('span');
|
|
457
|
+
el.textContent = s;
|
|
458
|
+
return el.innerHTML;
|
|
202
459
|
}
|
|
203
460
|
|
|
204
461
|
function updateAnnotations() {
|
|
@@ -213,16 +470,21 @@ function updateAnnotations() {
|
|
|
213
470
|
left.classList.add('show'); right.classList.add('show');
|
|
214
471
|
|
|
215
472
|
const f = F[cur]; if (!f?.hotspots?.length) return;
|
|
216
|
-
|
|
217
473
|
f.hotspots.forEach((h, j) => {
|
|
218
474
|
const container = j % 2 === 0 ? left : right;
|
|
219
475
|
const el = document.createElement('div');
|
|
220
|
-
el.className = 'side-annot
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
476
|
+
el.className = 'side-annot';
|
|
477
|
+
const titleDiv = document.createElement('div');
|
|
478
|
+
titleDiv.className = 'side-annot-title';
|
|
479
|
+
titleDiv.innerHTML = '<div class="side-annot-dot" style="background:' + h.color + '"></div>';
|
|
480
|
+
titleDiv.appendChild(document.createTextNode(h.label));
|
|
481
|
+
el.appendChild(titleDiv);
|
|
482
|
+
if (h.description !== h.label) {
|
|
483
|
+
const descDiv = document.createElement('div');
|
|
484
|
+
descDiv.className = 'side-annot-desc';
|
|
485
|
+
descDiv.textContent = h.description.slice(0, 72);
|
|
486
|
+
el.appendChild(descDiv);
|
|
487
|
+
}
|
|
226
488
|
container.appendChild(el);
|
|
227
489
|
});
|
|
228
490
|
}
|
|
@@ -234,11 +496,14 @@ function play() {
|
|
|
234
496
|
document.getElementById('statusText').textContent = 'playing';
|
|
235
497
|
document.getElementById('annotsLeft').classList.remove('show');
|
|
236
498
|
document.getElementById('annotsRight').classList.remove('show');
|
|
237
|
-
timer
|
|
499
|
+
clearInterval(timer);
|
|
500
|
+
if (F.length < 2) return;
|
|
501
|
+
timer = setInterval(() => { cur = (cur + 1) % F.length; go(cur); }, 2200);
|
|
238
502
|
}
|
|
239
503
|
|
|
240
504
|
function stop() {
|
|
241
|
-
playing = false;
|
|
505
|
+
playing = false;
|
|
506
|
+
clearInterval(timer);
|
|
242
507
|
document.getElementById('playIco').style.display = '';
|
|
243
508
|
document.getElementById('pauseIco').style.display = 'none';
|
|
244
509
|
document.getElementById('statusText').textContent = 'paused';
|
|
@@ -246,7 +511,6 @@ function stop() {
|
|
|
246
511
|
}
|
|
247
512
|
|
|
248
513
|
document.getElementById('playBtn').onclick = () => playing ? stop() : play();
|
|
249
|
-
|
|
250
514
|
updatePanel();
|
|
251
515
|
play();
|
|
252
516
|
</script>
|
package/dist/index.html
CHANGED
|
@@ -22505,10 +22505,13 @@ appId: ${platform === 'ios' ? 'com.apple.Preferences' : 'com.android.settings'}
|
|
|
22505
22505
|
const sizeKb = ((data.size || 0) / 1024).toFixed(1);
|
|
22506
22506
|
showToast(`Infographic exported (${sizeKb}KB, ${data.frameCount} frames)`, 'success');
|
|
22507
22507
|
} else {
|
|
22508
|
-
|
|
22508
|
+
const debugHint = data.debug && Number.isFinite(data.debug.frameCandidates) && Number.isFinite(data.debug.validFrames)
|
|
22509
|
+
? ` (${data.debug.validFrames}/${data.debug.frameCandidates} readable frames${data.debug.source ? `, ${data.debug.source}` : ''})`
|
|
22510
|
+
: '';
|
|
22511
|
+
showToast((data.error || 'Export failed') + debugHint, 'error');
|
|
22509
22512
|
}
|
|
22510
22513
|
} catch (err) {
|
|
22511
|
-
showToast('Export failed', 'error');
|
|
22514
|
+
showToast(err?.message || 'Export failed', 'error');
|
|
22512
22515
|
}
|
|
22513
22516
|
}
|
|
22514
22517
|
|
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import {
|
|
3
3
|
startServer,
|
|
4
4
|
stopServer
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-XDUFCPOC.js";
|
|
6
6
|
import {
|
|
7
7
|
analyzeTools,
|
|
8
8
|
canvasTools,
|
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
templateTools,
|
|
17
17
|
testingTools,
|
|
18
18
|
uiTools
|
|
19
|
-
} from "./chunk-
|
|
19
|
+
} from "./chunk-YFF3M76J.js";
|
|
20
20
|
import "./chunk-34GGYFXX.js";
|
|
21
21
|
import "./chunk-PMCXEA7J.js";
|
|
22
22
|
import {
|
|
@@ -18,18 +18,22 @@ function findTemplate() {
|
|
|
18
18
|
return null;
|
|
19
19
|
}
|
|
20
20
|
function imageToBase64(imagePath) {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
21
|
+
try {
|
|
22
|
+
if (!existsSync(imagePath) || !statSync(imagePath).isFile()) return "";
|
|
23
|
+
const buffer = readFileSync(imagePath);
|
|
24
|
+
const ext = extname(imagePath).toLowerCase();
|
|
25
|
+
const mimeTypes = {
|
|
26
|
+
".png": "image/png",
|
|
27
|
+
".jpg": "image/jpeg",
|
|
28
|
+
".jpeg": "image/jpeg",
|
|
29
|
+
".webp": "image/webp",
|
|
30
|
+
".gif": "image/gif"
|
|
31
|
+
};
|
|
32
|
+
const mime = mimeTypes[ext] || "image/png";
|
|
33
|
+
return `data:${mime};base64,${buffer.toString("base64")}`;
|
|
34
|
+
} catch {
|
|
35
|
+
return "";
|
|
36
|
+
}
|
|
33
37
|
}
|
|
34
38
|
function stripMarkdown(text) {
|
|
35
39
|
return text.replace(/\*\*(.+?)\*\*/g, "$1").replace(/\*(.+?)\*/g, "$1").replace(/__(.+?)__/g, "$1").replace(/_(.+?)_/g, "$1").replace(/`(.+?)`/g, "$1").replace(/^#{1,3}\s+/gm, "").replace(/^-\s+/gm, "").replace(/^\d+\.\s+/gm, "").replace(/\[(.+?)\]\(.*?\)/g, "$1").trim();
|
|
@@ -51,7 +55,92 @@ function collectFrameImages(framesDir, videoPath, projectsDir, projectId) {
|
|
|
51
55
|
}
|
|
52
56
|
return [];
|
|
53
57
|
}
|
|
58
|
+
function validateFrameInputs(frameFiles, frameOcr) {
|
|
59
|
+
const validFrameFiles = [];
|
|
60
|
+
const validFrameOcr = [];
|
|
61
|
+
const dataUrls = [];
|
|
62
|
+
const invalidFrames = [];
|
|
63
|
+
frameFiles.forEach((filePath, index) => {
|
|
64
|
+
if (!filePath || !existsSync(filePath)) {
|
|
65
|
+
invalidFrames.push({ path: filePath || `frame-${index + 1}`, reason: "missing" });
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
let stats;
|
|
69
|
+
try {
|
|
70
|
+
stats = statSync(filePath);
|
|
71
|
+
} catch {
|
|
72
|
+
invalidFrames.push({ path: filePath, reason: "unreadable" });
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (!stats.isFile()) {
|
|
76
|
+
invalidFrames.push({ path: filePath, reason: "not_file" });
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const dataUrl = imageToBase64(filePath);
|
|
80
|
+
if (!dataUrl) {
|
|
81
|
+
invalidFrames.push({ path: filePath, reason: "unreadable" });
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
validFrameFiles.push(filePath);
|
|
85
|
+
validFrameOcr.push(frameOcr[index] || { ocrText: null });
|
|
86
|
+
dataUrls.push(dataUrl);
|
|
87
|
+
});
|
|
88
|
+
return {
|
|
89
|
+
frameFiles: validFrameFiles,
|
|
90
|
+
frameOcr: validFrameOcr,
|
|
91
|
+
dataUrls,
|
|
92
|
+
invalidFrames
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
function resolveInfographicFrameInputs(dbFrames, framesDir, videoPath, projectsDir, projectId) {
|
|
96
|
+
if (dbFrames.length > 0) {
|
|
97
|
+
const dbValidation = validateFrameInputs(
|
|
98
|
+
dbFrames.map((frame) => frame.imagePath),
|
|
99
|
+
dbFrames.map((frame) => ({ ocrText: frame.ocrText ?? null }))
|
|
100
|
+
);
|
|
101
|
+
if (dbValidation.frameFiles.length > 0) {
|
|
102
|
+
return {
|
|
103
|
+
...dbValidation,
|
|
104
|
+
source: "db",
|
|
105
|
+
candidateCount: dbFrames.length
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
const fallbackFrameFiles2 = collectFrameImages(framesDir, videoPath, projectsDir, projectId);
|
|
109
|
+
const fallbackValidation2 = validateFrameInputs(
|
|
110
|
+
fallbackFrameFiles2,
|
|
111
|
+
fallbackFrameFiles2.map(() => ({ ocrText: null }))
|
|
112
|
+
);
|
|
113
|
+
if (fallbackValidation2.frameFiles.length > 0) {
|
|
114
|
+
return {
|
|
115
|
+
...fallbackValidation2,
|
|
116
|
+
invalidFrames: [...dbValidation.invalidFrames, ...fallbackValidation2.invalidFrames],
|
|
117
|
+
source: "filesystem",
|
|
118
|
+
candidateCount: dbFrames.length + fallbackFrameFiles2.length
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
return {
|
|
122
|
+
...fallbackValidation2,
|
|
123
|
+
invalidFrames: [...dbValidation.invalidFrames, ...fallbackValidation2.invalidFrames],
|
|
124
|
+
source: "none",
|
|
125
|
+
candidateCount: dbFrames.length + fallbackFrameFiles2.length
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
const fallbackFrameFiles = collectFrameImages(framesDir, videoPath, projectsDir, projectId);
|
|
129
|
+
const fallbackValidation = validateFrameInputs(
|
|
130
|
+
fallbackFrameFiles,
|
|
131
|
+
fallbackFrameFiles.map(() => ({ ocrText: null }))
|
|
132
|
+
);
|
|
133
|
+
return {
|
|
134
|
+
...fallbackValidation,
|
|
135
|
+
source: fallbackValidation.frameFiles.length > 0 ? "filesystem" : "none",
|
|
136
|
+
candidateCount: fallbackFrameFiles.length
|
|
137
|
+
};
|
|
138
|
+
}
|
|
54
139
|
function buildInfographicData(project, frameFiles, frameOcr, annotations) {
|
|
140
|
+
const validatedFrames = validateFrameInputs(frameFiles, frameOcr);
|
|
141
|
+
const usableFrameFiles = validatedFrames.frameFiles;
|
|
142
|
+
const usableFrameOcr = validatedFrames.frameOcr;
|
|
143
|
+
const dataUrls = validatedFrames.dataUrls;
|
|
55
144
|
let flowSteps = [];
|
|
56
145
|
let uiElements = [];
|
|
57
146
|
if (project.aiSummary) {
|
|
@@ -64,7 +153,18 @@ function buildInfographicData(project, frameFiles, frameOcr, annotations) {
|
|
|
64
153
|
uiElements = (uiMatch[1].match(/^-\s+(.+)$/gm) || []).map((s) => s.replace(/^-\s+/, ""));
|
|
65
154
|
}
|
|
66
155
|
}
|
|
67
|
-
|
|
156
|
+
let overview = "";
|
|
157
|
+
if (project.aiSummary) {
|
|
158
|
+
const overviewMatch = project.aiSummary.match(/## (?:App Overview|Page \/ App Overview|Overview|Summary)\n([\s\S]*?)(?=\n##|\n$|$)/);
|
|
159
|
+
if (overviewMatch) {
|
|
160
|
+
const overviewBody = overviewMatch[1].split(/\n\s*\n/).map((chunk) => chunk.trim()).filter(Boolean)[0] || overviewMatch[1];
|
|
161
|
+
overview = stripMarkdown(overviewBody).slice(0, 240);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
if (!overview && project.marketingDescription) {
|
|
165
|
+
overview = stripMarkdown(project.marketingDescription).slice(0, 240);
|
|
166
|
+
}
|
|
167
|
+
const elementsPerFrame = Math.max(1, Math.ceil(uiElements.length / Math.max(usableFrameFiles.length, 1)));
|
|
68
168
|
const hotspotColors = ["#818CF8", "#34D399", "#F59E0B", "#EC4899", "#06B6D4", "#8B5CF6"];
|
|
69
169
|
const hotspotPositions = [
|
|
70
170
|
{ x: 50, y: 8 },
|
|
@@ -80,10 +180,10 @@ function buildInfographicData(project, frameFiles, frameOcr, annotations) {
|
|
|
80
180
|
{ x: 15, y: 70 }
|
|
81
181
|
// bottom left
|
|
82
182
|
];
|
|
83
|
-
const frames =
|
|
183
|
+
const frames = usableFrameFiles.map((filePath, i) => {
|
|
84
184
|
const rawStepName = annotations?.[i]?.label || flowSteps[i] || `Step ${i + 1}`;
|
|
85
185
|
const stepName = stripMarkdown(rawStepName);
|
|
86
|
-
const ocr =
|
|
186
|
+
const ocr = usableFrameOcr[i]?.ocrText || "";
|
|
87
187
|
const rawDesc = flowSteps[i] || ocr.slice(0, 100) || `Screen ${i + 1}`;
|
|
88
188
|
const description = stripMarkdown(rawDesc);
|
|
89
189
|
const hotspots = [];
|
|
@@ -121,7 +221,7 @@ function buildInfographicData(project, frameFiles, frameOcr, annotations) {
|
|
|
121
221
|
id: `frame-${i}`,
|
|
122
222
|
step_name: stepName,
|
|
123
223
|
description,
|
|
124
|
-
base64: imageToBase64(filePath),
|
|
224
|
+
base64: dataUrls[i] || imageToBase64(filePath),
|
|
125
225
|
baseline_status: "not_validated",
|
|
126
226
|
hotspots
|
|
127
227
|
};
|
|
@@ -130,6 +230,7 @@ function buildInfographicData(project, frameFiles, frameOcr, annotations) {
|
|
|
130
230
|
name: project.marketingTitle || project.name,
|
|
131
231
|
platform: project.platform || "unknown",
|
|
132
232
|
recorded_at: project.createdAt instanceof Date ? project.createdAt.toISOString() : typeof project.createdAt === "string" ? project.createdAt : (/* @__PURE__ */ new Date()).toISOString(),
|
|
233
|
+
overview,
|
|
133
234
|
frames
|
|
134
235
|
};
|
|
135
236
|
}
|
|
@@ -179,5 +280,6 @@ export {
|
|
|
179
280
|
buildInfographicData,
|
|
180
281
|
collectFrameImages,
|
|
181
282
|
generateInfographicHtml,
|
|
182
|
-
generateInfographicHtmlString
|
|
283
|
+
generateInfographicHtmlString,
|
|
284
|
+
resolveInfographicFrameInputs
|
|
183
285
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@veolab/discoverylab",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.6",
|
|
4
4
|
"description": "AI-powered app testing & evidence generator - Claude Code Plugin",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -70,7 +70,7 @@
|
|
|
70
70
|
"claude-plugin": {
|
|
71
71
|
"name": "DiscoveryLab",
|
|
72
72
|
"description": "AI-powered app testing & evidence generator",
|
|
73
|
-
"version": "1.6.
|
|
73
|
+
"version": "1.6.6",
|
|
74
74
|
"tools": [
|
|
75
75
|
"dlab.capture.screen",
|
|
76
76
|
"dlab.capture.emulator",
|