@veolab/discoverylab 1.4.4 → 1.6.5
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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +70 -211
- package/assets/applab-bundle-icon.png +0 -0
- package/assets/icons/icons8-claude-150.png +0 -0
- package/assets/icons/icons8-claude-500.png +0 -0
- package/dist/{chunk-CUBQRT5L.js → chunk-JAA53ES7.js} +111 -2
- package/dist/{chunk-HB3YPWF3.js → chunk-Q7Q3A2ZI.js} +301 -10
- package/dist/{chunk-XKX6NBHF.js → chunk-TWRWARU4.js} +52 -2
- package/dist/{chunk-2UUMLAVR.js → chunk-V6RREMYD.js} +332 -38
- package/dist/cli.js +164 -28
- package/dist/export/infographic-template.html +254 -0
- package/dist/import-W2JEW254.js +180 -0
- package/dist/index.d.ts +30 -6
- package/dist/index.html +473 -11
- package/dist/index.js +5 -5
- package/dist/infographic-GQAHEOAA.js +183 -0
- package/dist/mcpb/node_modules/@anthropic-ai/sdk/src/lib/.keep +4 -0
- package/dist/mcpb/node_modules/better-sqlite3/build/Release/.deps/Release/better_sqlite3.node.d +1 -0
- package/dist/mcpb/node_modules/better-sqlite3/build/Release/.deps/Release/obj.target/better_sqlite3/src/better_sqlite3.o.d +133 -0
- package/dist/mcpb/node_modules/better-sqlite3/build/Release/.deps/Release/obj.target/deps/locate_sqlite3.stamp.d +1 -0
- package/dist/mcpb/node_modules/better-sqlite3/build/Release/.deps/Release/obj.target/sqlite3/gen/sqlite3/sqlite3.o.d +4 -0
- package/dist/mcpb/node_modules/better-sqlite3/build/Release/.deps/Release/obj.target/test_extension/deps/test_extension.o.d +7 -0
- package/dist/mcpb/node_modules/better-sqlite3/build/Release/.deps/Release/sqlite3.a.d +1 -0
- package/dist/mcpb/node_modules/better-sqlite3/build/Release/.deps/Release/test_extension.node.d +1 -0
- package/dist/mcpb/node_modules/better-sqlite3/build/Release/.deps/ba23eeee118cd63e16015df367567cb043fed872.intermediate.d +1 -0
- package/dist/{server-QFNKZCOJ.js → server-C2NZM2RV.js} +1 -1
- package/dist/{server-OVOACIOJ.js → server-WN6DCCUA.js} +1 -1
- package/dist/{setup-6JJYKKBS.js → setup-SMN7FJNZ.js} +5 -2
- package/dist/{tools-Q7OZO732.js → tools-VXU3JEQP.js} +6 -4
- package/doc/esvp-protocol.md +116 -0
- package/package.json +9 -3
- package/skills/knowledge-brain/SKILL.md +44 -43
package/dist/cli.js
CHANGED
|
@@ -11,6 +11,7 @@ import "./chunk-R5U7XKVJ.js";
|
|
|
11
11
|
import { Command } from "commander";
|
|
12
12
|
import chalk from "chalk";
|
|
13
13
|
import open from "open";
|
|
14
|
+
import { fileURLToPath } from "url";
|
|
14
15
|
var program = new Command();
|
|
15
16
|
var binName = process.argv[1]?.replace(/.*[\\/]/, "").replace(/\.[^.]+$/, "") === "applab" ? "applab" : "discoverylab";
|
|
16
17
|
program.name(binName).description("AI-powered app testing & evidence generator - Claude Code Plugin").version(APP_VERSION);
|
|
@@ -389,7 +390,7 @@ program.command("serve").alias("server").description("Start the DiscoveryLab web
|
|
|
389
390
|
console.log(chalk.cyan("\n DiscoveryLab"));
|
|
390
391
|
console.log(chalk.gray(" AI-powered app testing & evidence generator\n"));
|
|
391
392
|
try {
|
|
392
|
-
const { startServer } = await import("./server-
|
|
393
|
+
const { startServer } = await import("./server-C2NZM2RV.js");
|
|
393
394
|
await startServer(port);
|
|
394
395
|
console.log(chalk.green(` Server running at http://localhost:${port}`));
|
|
395
396
|
console.log(chalk.gray(" Press Ctrl+C to stop\n"));
|
|
@@ -404,7 +405,7 @@ program.command("serve").alias("server").description("Start the DiscoveryLab web
|
|
|
404
405
|
program.command("setup").description("Check and configure DiscoveryLab dependencies").action(async () => {
|
|
405
406
|
console.log(chalk.cyan("\n DiscoveryLab Setup\n"));
|
|
406
407
|
try {
|
|
407
|
-
const { setupStatusTool } = await import("./setup-
|
|
408
|
+
const { setupStatusTool } = await import("./setup-SMN7FJNZ.js");
|
|
408
409
|
const result = await setupStatusTool.handler({});
|
|
409
410
|
if (result.isError) {
|
|
410
411
|
console.error(chalk.red(" Setup check failed"));
|
|
@@ -454,41 +455,88 @@ program.command("init").description("Initialize DiscoveryLab data directories").
|
|
|
454
455
|
process.exit(1);
|
|
455
456
|
}
|
|
456
457
|
});
|
|
457
|
-
program.command("install").description("Install DiscoveryLab as Claude Code
|
|
458
|
-
const { homedir } = await import("os");
|
|
459
|
-
const { existsSync, readFileSync, writeFileSync } = await import("fs");
|
|
460
|
-
const { join } = await import("path");
|
|
461
|
-
const
|
|
458
|
+
program.command("install").description("Install DiscoveryLab as MCP server for Claude Code and/or Claude Desktop").option("--target <target>", "Installation target: code, desktop, all (default: auto-detect)", "").action(async (opts) => {
|
|
459
|
+
const { homedir, platform } = await import("os");
|
|
460
|
+
const { existsSync, readFileSync, writeFileSync, mkdirSync } = await import("fs");
|
|
461
|
+
const { join, dirname } = await import("path");
|
|
462
|
+
const home = homedir();
|
|
463
|
+
const localMcpEntrypoint = fileURLToPath(new URL("./index.js", import.meta.url));
|
|
464
|
+
const mcpEntry = existsSync(localMcpEntrypoint) ? {
|
|
465
|
+
command: process.execPath,
|
|
466
|
+
args: [localMcpEntrypoint]
|
|
467
|
+
} : {
|
|
468
|
+
command: "npx",
|
|
469
|
+
args: ["-y", "@veolab/discoverylab@latest", "mcp"]
|
|
470
|
+
};
|
|
471
|
+
const targets = {
|
|
472
|
+
code: {
|
|
473
|
+
name: "Claude Code",
|
|
474
|
+
path: join(home, ".claude.json"),
|
|
475
|
+
restart: "Restart Claude Code to activate."
|
|
476
|
+
},
|
|
477
|
+
desktop: {
|
|
478
|
+
name: "Claude Desktop",
|
|
479
|
+
path: platform() === "win32" ? join(process.env.APPDATA || join(home, "AppData", "Roaming"), "Claude", "claude_desktop_config.json") : join(home, "Library", "Application Support", "Claude", "claude_desktop_config.json"),
|
|
480
|
+
restart: "Restart Claude Desktop to activate."
|
|
481
|
+
}
|
|
482
|
+
};
|
|
483
|
+
let selectedTargets = [];
|
|
484
|
+
const target = opts.target?.toLowerCase() || "";
|
|
485
|
+
if (target === "code") {
|
|
486
|
+
selectedTargets = ["code"];
|
|
487
|
+
} else if (target === "desktop") {
|
|
488
|
+
selectedTargets = ["desktop"];
|
|
489
|
+
} else if (target === "all") {
|
|
490
|
+
selectedTargets = ["code", "desktop"];
|
|
491
|
+
} else {
|
|
492
|
+
selectedTargets = ["code"];
|
|
493
|
+
const desktopDir = dirname(targets.desktop.path);
|
|
494
|
+
if (existsSync(desktopDir)) {
|
|
495
|
+
selectedTargets.push("desktop");
|
|
496
|
+
}
|
|
497
|
+
}
|
|
462
498
|
console.log(chalk.cyan("\n Installing DiscoveryLab MCP...\n"));
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
499
|
+
let installed = 0;
|
|
500
|
+
for (const key of selectedTargets) {
|
|
501
|
+
const t = targets[key];
|
|
502
|
+
try {
|
|
503
|
+
const dir = dirname(t.path);
|
|
504
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
505
|
+
let config = {};
|
|
506
|
+
if (existsSync(t.path)) {
|
|
507
|
+
const content = readFileSync(t.path, "utf-8");
|
|
508
|
+
try {
|
|
509
|
+
config = JSON.parse(content);
|
|
510
|
+
} catch {
|
|
511
|
+
config = {};
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
if (!config.mcpServers) config.mcpServers = {};
|
|
515
|
+
config.mcpServers.discoverylab = mcpEntry;
|
|
516
|
+
writeFileSync(t.path, JSON.stringify(config, null, 2));
|
|
517
|
+
console.log(chalk.green(` \u2713 ${t.name} configured`));
|
|
518
|
+
console.log(chalk.gray(` ${t.path}`));
|
|
519
|
+
installed++;
|
|
520
|
+
} catch (error) {
|
|
521
|
+
console.log(chalk.yellow(` \u2717 ${t.name} skipped: ${error instanceof Error ? error.message : String(error)}`));
|
|
468
522
|
}
|
|
469
|
-
|
|
470
|
-
|
|
523
|
+
}
|
|
524
|
+
console.log();
|
|
525
|
+
if (installed > 0) {
|
|
526
|
+
for (const key of selectedTargets) {
|
|
527
|
+
console.log(chalk.white(` ${targets[key].restart}`));
|
|
471
528
|
}
|
|
472
|
-
config.mcpServers.discoverylab = {
|
|
473
|
-
command: "npx",
|
|
474
|
-
args: ["-y", "@veolab/discoverylab@latest", "mcp"]
|
|
475
|
-
};
|
|
476
|
-
writeFileSync(claudeConfigPath, JSON.stringify(config, null, 2));
|
|
477
|
-
console.log(chalk.green(" \u2713 Added to ~/.claude.json"));
|
|
478
|
-
console.log();
|
|
479
|
-
console.log(chalk.white(" Restart Claude Code to activate."));
|
|
480
529
|
console.log(chalk.gray(" Or run: discoverylab serve"));
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
console.error(chalk.red(` Failed to install: ${error}`));
|
|
484
|
-
process.exit(1);
|
|
530
|
+
} else {
|
|
531
|
+
console.log(chalk.red(" No targets configured."));
|
|
485
532
|
}
|
|
533
|
+
console.log();
|
|
486
534
|
});
|
|
487
535
|
program.command("mcp").description("Run as MCP server (for Claude Code integration)").action(async () => {
|
|
488
536
|
try {
|
|
489
537
|
const { getDatabase } = await import("./db-5ECN3O7F.js");
|
|
490
538
|
getDatabase();
|
|
491
|
-
const { mcpServer } = await import("./server-
|
|
539
|
+
const { mcpServer } = await import("./server-WN6DCCUA.js");
|
|
492
540
|
const {
|
|
493
541
|
uiTools,
|
|
494
542
|
projectTools,
|
|
@@ -502,7 +550,7 @@ program.command("mcp").description("Run as MCP server (for Claude Code integrati
|
|
|
502
550
|
taskHubTools,
|
|
503
551
|
esvpTools,
|
|
504
552
|
knowledgeTools
|
|
505
|
-
} = await import("./tools-
|
|
553
|
+
} = await import("./tools-VXU3JEQP.js");
|
|
506
554
|
mcpServer.registerTools([
|
|
507
555
|
...uiTools,
|
|
508
556
|
...projectTools,
|
|
@@ -540,6 +588,94 @@ program.command("info").description("Show version and configuration info").actio
|
|
|
540
588
|
console.log();
|
|
541
589
|
}
|
|
542
590
|
});
|
|
591
|
+
program.command("export").description("Export project in various formats").argument("<project-id>", "Project ID or slug").option("--format <format>", "Export format (infographic, applab, esvp)", "infographic").option("--output <path>", "Custom output path").option("--open", "Open file after generation").option("--compress", "Force image compression").option("--no-baseline", "Omit baseline info").action(async (projectId, opts) => {
|
|
592
|
+
try {
|
|
593
|
+
if (opts.format === "infographic") {
|
|
594
|
+
console.log(chalk.cyan(`
|
|
595
|
+
Exporting infographic for: ${projectId}
|
|
596
|
+
`));
|
|
597
|
+
const { join: pathJoin } = await import("path");
|
|
598
|
+
const { getDatabase, projects, frames: framesTable, FRAMES_DIR, EXPORTS_DIR, PROJECTS_DIR } = await import("./db-5ECN3O7F.js");
|
|
599
|
+
const { eq } = await import("drizzle-orm");
|
|
600
|
+
const { collectFrameImages, buildInfographicData, generateInfographicHtml } = await import("./infographic-GQAHEOAA.js");
|
|
601
|
+
const db = getDatabase();
|
|
602
|
+
const allProjects = await db.select().from(projects);
|
|
603
|
+
const project = allProjects.find((p) => p.id === projectId || p.id.startsWith(projectId) || p.name.toLowerCase().includes(projectId.toLowerCase()));
|
|
604
|
+
if (!project) {
|
|
605
|
+
console.log(chalk.red(` Project not found: ${projectId}`));
|
|
606
|
+
console.log(chalk.gray(" Available projects:"));
|
|
607
|
+
for (const p of allProjects.slice(0, 10)) {
|
|
608
|
+
console.log(chalk.gray(` ${p.id.slice(0, 12)} - ${p.marketingTitle || p.name}`));
|
|
609
|
+
}
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
console.log(chalk.green(` \u2714 Found project: ${project.marketingTitle || project.name}`));
|
|
613
|
+
const dbFrames = await db.select().from(framesTable).where(eq(framesTable.projectId, project.id)).orderBy(framesTable.frameNumber).limit(20);
|
|
614
|
+
let frameFiles;
|
|
615
|
+
let frameOcr;
|
|
616
|
+
if (dbFrames.length > 0) {
|
|
617
|
+
frameFiles = dbFrames.map((f) => f.imagePath);
|
|
618
|
+
frameOcr = dbFrames;
|
|
619
|
+
} else {
|
|
620
|
+
frameFiles = collectFrameImages(pathJoin(FRAMES_DIR, project.id), project.videoPath, PROJECTS_DIR, project.id);
|
|
621
|
+
frameOcr = frameFiles.map(() => ({ ocrText: null }));
|
|
622
|
+
}
|
|
623
|
+
console.log(chalk.green(` \u2714 ${frameFiles.length} frames found`));
|
|
624
|
+
if (frameFiles.length === 0) {
|
|
625
|
+
console.log(chalk.red(" No frames found. Run analyzer first."));
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
const data = buildInfographicData(project, frameFiles, frameOcr);
|
|
629
|
+
console.log(chalk.green(` \u2714 ${project.aiSummary ? "AI analysis loaded" : "No analysis (basic labels)"}`));
|
|
630
|
+
const slug = (project.marketingTitle || project.name || project.id).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 40);
|
|
631
|
+
const outputPath = opts.output ? pathJoin(opts.output, `${slug}-infographic.html`) : pathJoin(EXPORTS_DIR, `${slug}-infographic.html`);
|
|
632
|
+
const result = generateInfographicHtml(data, outputPath);
|
|
633
|
+
if (result.success) {
|
|
634
|
+
const sizeKb = ((result.size || 0) / 1024).toFixed(1);
|
|
635
|
+
console.log(chalk.green(` \u2714 Exported: ${result.outputPath} (${sizeKb}KB, ${result.frameCount} frames)`));
|
|
636
|
+
if (opts.open) {
|
|
637
|
+
const { exec } = await import("child_process");
|
|
638
|
+
exec(`open "${result.outputPath}"`);
|
|
639
|
+
}
|
|
640
|
+
} else {
|
|
641
|
+
console.log(chalk.red(` Export failed: ${result.error}`));
|
|
642
|
+
}
|
|
643
|
+
} else {
|
|
644
|
+
console.log(chalk.yellow(` Format "${opts.format}" - use the web UI for applab/esvp exports.`));
|
|
645
|
+
}
|
|
646
|
+
} catch (error) {
|
|
647
|
+
console.log(chalk.red(` Export failed: ${error instanceof Error ? error.message : String(error)}`));
|
|
648
|
+
}
|
|
649
|
+
});
|
|
650
|
+
program.command("import").description("Import a shared .applab project bundle").argument("<file>", "Path to .applab file").action(async (file) => {
|
|
651
|
+
try {
|
|
652
|
+
const { resolve } = await import("path");
|
|
653
|
+
const filePath = resolve(file);
|
|
654
|
+
console.log(chalk.cyan(`
|
|
655
|
+
Importing: ${filePath}
|
|
656
|
+
`));
|
|
657
|
+
const { getDatabase, projects, frames: framesTable, DATA_DIR, FRAMES_DIR, PROJECTS_DIR } = await import("./db-5ECN3O7F.js");
|
|
658
|
+
const { importApplabBundle } = await import("./import-W2JEW254.js");
|
|
659
|
+
const db = getDatabase();
|
|
660
|
+
const result = await importApplabBundle(filePath, db, { projects, frames: framesTable }, {
|
|
661
|
+
dataDir: DATA_DIR,
|
|
662
|
+
framesDir: FRAMES_DIR,
|
|
663
|
+
projectsDir: PROJECTS_DIR
|
|
664
|
+
});
|
|
665
|
+
if (result.success) {
|
|
666
|
+
console.log(chalk.green(` \u2714 Imported: ${result.projectName}`));
|
|
667
|
+
console.log(chalk.green(` \u2714 ${result.frameCount} frames`));
|
|
668
|
+
console.log(chalk.gray(` ID: ${result.projectId}
|
|
669
|
+
`));
|
|
670
|
+
} else {
|
|
671
|
+
console.log(chalk.red(` Import failed: ${result.error}
|
|
672
|
+
`));
|
|
673
|
+
}
|
|
674
|
+
} catch (error) {
|
|
675
|
+
console.log(chalk.red(` Import failed: ${error instanceof Error ? error.message : String(error)}
|
|
676
|
+
`));
|
|
677
|
+
}
|
|
678
|
+
});
|
|
543
679
|
program.parse();
|
|
544
680
|
if (!process.argv.slice(2).length) {
|
|
545
681
|
program.outputHelp();
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>__TITLE__</title>
|
|
7
|
+
<style>
|
|
8
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
9
|
+
:root { --accent: #6366f1; --bg: #0f0f17; --bg2: #16161f; --bg3: #1e1e2a; --text: #e4e4e7; --muted: #71717a; --ok: #22c55e; --warn: #eab308; }
|
|
10
|
+
body { background: var(--bg); color: var(--text); font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif; min-height: 100vh; overflow-x: hidden; }
|
|
11
|
+
|
|
12
|
+
/* Header */
|
|
13
|
+
.header { display: flex; align-items: center; justify-content: space-between; padding: 14px 24px; border-bottom: 1px solid rgba(255,255,255,0.05); }
|
|
14
|
+
.header-left { display: flex; align-items: center; gap: 12px; }
|
|
15
|
+
.header-title { font-size: 16px; font-weight: 700; }
|
|
16
|
+
.header-platform { font-size: 10px; padding: 2px 8px; background: rgba(99,102,241,0.12); color: var(--accent); border-radius: 10px; }
|
|
17
|
+
.header-date { font-size: 11px; color: var(--muted); }
|
|
18
|
+
|
|
19
|
+
/* Step chips */
|
|
20
|
+
.chips { display: flex; gap: 4px; padding: 10px 24px; overflow-x: auto; scrollbar-width: none; }
|
|
21
|
+
.chips::-webkit-scrollbar { display: none; }
|
|
22
|
+
.chip { font-size: 10px; padding: 4px 12px; border-radius: 16px; background: var(--bg2); border: 1px solid rgba(255,255,255,0.06); cursor: pointer; white-space: nowrap; transition: all 0.25s; color: var(--muted); font-weight: 500; }
|
|
23
|
+
.chip.active { background: var(--accent); color: #fff; border-color: var(--accent); }
|
|
24
|
+
.chip:hover:not(.active) { border-color: rgba(255,255,255,0.15); }
|
|
25
|
+
|
|
26
|
+
/* Main layout */
|
|
27
|
+
.main { display: grid; grid-template-columns: 1fr 300px; height: calc(100vh - 100px); overflow: hidden; }
|
|
28
|
+
@media (max-width: 768px) { .main { grid-template-columns: 1fr; } .panel { border-left: none; border-top: 1px solid rgba(255,255,255,0.05); max-height: 200px; } }
|
|
29
|
+
|
|
30
|
+
/* Viewer */
|
|
31
|
+
.viewer { display: flex; align-items: center; justify-content: center; position: relative; padding: 20px; }
|
|
32
|
+
.device-area { position: relative; display: flex; align-items: center; }
|
|
33
|
+
.phone { width: 320px; background: #000; border-radius: 28px; overflow: hidden; box-shadow: 0 16px 48px rgba(0,0,0,0.5); border: 1.5px solid rgba(255,255,255,0.06); position: relative; }
|
|
34
|
+
.phone img { width: 100%; display: block; transition: opacity 0.35s ease; }
|
|
35
|
+
.phone img.out { opacity: 0; position: absolute; top: 0; left: 0; }
|
|
36
|
+
.phone img.in { opacity: 1; position: relative; }
|
|
37
|
+
|
|
38
|
+
/* Side annotations (appear when paused) */
|
|
39
|
+
.side-annots { position: absolute; top: 50%; transform: translateY(-50%); display: flex; flex-direction: column; gap: 8px; opacity: 0; transition: opacity 0.35s; pointer-events: none; width: 160px; }
|
|
40
|
+
.side-annots.show { opacity: 1; pointer-events: auto; }
|
|
41
|
+
.side-annots.left { right: calc(100% + 16px); }
|
|
42
|
+
.side-annots.right { left: calc(100% + 16px); }
|
|
43
|
+
.side-annot { padding: 8px 10px; background: rgba(255,255,255,0.04); border: 1px solid rgba(255,255,255,0.08); border-radius: 8px; position: relative; }
|
|
44
|
+
.side-annot::before { content: ''; position: absolute; top: 50%; width: 12px; height: 1px; background: rgba(255,255,255,0.15); }
|
|
45
|
+
.side-annots.left .side-annot::before { right: -13px; }
|
|
46
|
+
.side-annots.right .side-annot::before { left: -13px; }
|
|
47
|
+
.side-annot-title { font-size: 10px; font-weight: 600; color: var(--text); display: flex; align-items: center; gap: 5px; }
|
|
48
|
+
.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; line-height: 1.4; }
|
|
50
|
+
|
|
51
|
+
/* Controls */
|
|
52
|
+
.controls { position: absolute; bottom: 12px; left: 50%; transform: translateX(-50%); display: flex; align-items: center; gap: 10px; background: rgba(0,0,0,0.6); backdrop-filter: blur(8px); padding: 6px 14px; border-radius: 20px; }
|
|
53
|
+
.play-btn { width: 28px; height: 28px; border-radius: 50%; background: transparent; border: 1.5px solid rgba(255,255,255,0.3); color: var(--text); cursor: pointer; display: flex; align-items: center; justify-content: center; transition: all 0.2s; }
|
|
54
|
+
.play-btn:hover { border-color: var(--accent); color: var(--accent); }
|
|
55
|
+
.dots { display: flex; gap: 5px; }
|
|
56
|
+
.dot { width: 6px; height: 6px; border-radius: 50%; background: rgba(255,255,255,0.15); cursor: pointer; transition: all 0.25s; }
|
|
57
|
+
.dot.active { background: var(--accent); width: 16px; border-radius: 3px; }
|
|
58
|
+
.status-text { font-size: 9px; color: rgba(255,255,255,0.35); }
|
|
59
|
+
|
|
60
|
+
/* Panel */
|
|
61
|
+
.panel { padding: 16px; border-left: 1px solid rgba(255,255,255,0.05); overflow-y: auto; background: var(--bg2); display: flex; flex-direction: column; gap: 16px; }
|
|
62
|
+
.panel-section { }
|
|
63
|
+
.panel-label { font-size: 9px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.8px; color: var(--muted); margin-bottom: 6px; }
|
|
64
|
+
.step-number { font-size: 11px; color: var(--accent); font-weight: 700; margin-bottom: 2px; }
|
|
65
|
+
.step-title { font-size: 14px; font-weight: 600; line-height: 1.3; }
|
|
66
|
+
.step-desc { font-size: 12px; color: var(--muted); line-height: 1.5; margin-top: 4px; }
|
|
67
|
+
|
|
68
|
+
/* Annotation cards in panel */
|
|
69
|
+
.annot-cards { display: flex; flex-direction: column; gap: 6px; }
|
|
70
|
+
.annot-card { padding: 8px 10px; background: var(--bg3); border-radius: 8px; border-left: 3px solid var(--accent); cursor: pointer; transition: all 0.2s; }
|
|
71
|
+
.annot-card:hover { background: rgba(99,102,241,0.08); }
|
|
72
|
+
.annot-card-title { font-size: 11px; font-weight: 600; display: flex; align-items: center; gap: 6px; }
|
|
73
|
+
.annot-card-dot { width: 6px; height: 6px; border-radius: 50%; flex-shrink: 0; }
|
|
74
|
+
.annot-card-desc { font-size: 10px; color: var(--muted); margin-top: 2px; line-height: 1.4; }
|
|
75
|
+
|
|
76
|
+
.baseline-badge { display: inline-flex; align-items: center; gap: 5px; font-size: 11px; padding: 3px 10px; border-radius: 12px; background: rgba(113,113,122,0.1); color: var(--muted); }
|
|
77
|
+
.baseline-badge.ok { background: rgba(34,197,94,0.1); color: var(--ok); }
|
|
78
|
+
.baseline-badge.warn { background: rgba(234,179,8,0.1); color: var(--warn); }
|
|
79
|
+
.bl-dot { width: 5px; height: 5px; border-radius: 50%; }
|
|
80
|
+
|
|
81
|
+
.footer { text-align: center; padding: 6px; font-size: 8px; color: rgba(255,255,255,0.12); }
|
|
82
|
+
|
|
83
|
+
/* Transitions */
|
|
84
|
+
@keyframes fadeIn { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: translateY(0); } }
|
|
85
|
+
.fade-in { animation: fadeIn 0.3s ease forwards; }
|
|
86
|
+
</style>
|
|
87
|
+
</head>
|
|
88
|
+
<body>
|
|
89
|
+
<div class="header">
|
|
90
|
+
<div class="header-left">
|
|
91
|
+
<div class="header-title" id="title"></div>
|
|
92
|
+
<div class="header-platform" id="platform"></div>
|
|
93
|
+
</div>
|
|
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>
|
|
103
|
+
</div>
|
|
104
|
+
<div class="controls">
|
|
105
|
+
<button class="play-btn" id="playBtn">
|
|
106
|
+
<svg id="playIco" width="12" height="12" viewBox="0 0 24 24" fill="currentColor"><polygon points="6 3 20 12 6 21"/></svg>
|
|
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>
|
|
111
|
+
</div>
|
|
112
|
+
</div>
|
|
113
|
+
<div class="panel">
|
|
114
|
+
<div class="panel-section">
|
|
115
|
+
<div class="panel-label">Step</div>
|
|
116
|
+
<div class="step-number" id="stepNum"></div>
|
|
117
|
+
<div class="step-title" id="stepTitle"></div>
|
|
118
|
+
<div class="step-desc" id="stepDesc"></div>
|
|
119
|
+
</div>
|
|
120
|
+
<div class="panel-section">
|
|
121
|
+
<div class="panel-label">Annotations</div>
|
|
122
|
+
<div class="annot-cards" id="annotCards"></div>
|
|
123
|
+
</div>
|
|
124
|
+
<div class="panel-section">
|
|
125
|
+
<div class="panel-label">Baseline</div>
|
|
126
|
+
<div class="baseline-badge" id="blBadge"><div class="bl-dot"></div><span></span></div>
|
|
127
|
+
</div>
|
|
128
|
+
</div>
|
|
129
|
+
</div>
|
|
130
|
+
<div class="footer">Generated by DiscoveryLab</div>
|
|
131
|
+
|
|
132
|
+
<script>
|
|
133
|
+
const D = window.FLOW_DATA || { name: 'Flow', frames: [] };
|
|
134
|
+
const F = D.frames || [];
|
|
135
|
+
let cur = 0, playing = true, timer = null;
|
|
136
|
+
|
|
137
|
+
document.getElementById('title').textContent = D.name || 'App Flow';
|
|
138
|
+
document.getElementById('platform').textContent = D.platform || '';
|
|
139
|
+
document.getElementById('date').textContent = D.recorded_at ? new Date(D.recorded_at).toLocaleDateString() : '';
|
|
140
|
+
|
|
141
|
+
// Create images
|
|
142
|
+
const phone = document.getElementById('phone');
|
|
143
|
+
F.forEach((f, i) => {
|
|
144
|
+
const img = document.createElement('img');
|
|
145
|
+
img.src = f.base64;
|
|
146
|
+
img.className = i === 0 ? 'in' : 'out';
|
|
147
|
+
img.dataset.i = i;
|
|
148
|
+
phone.appendChild(img);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// Chips
|
|
152
|
+
const chips = document.getElementById('chips');
|
|
153
|
+
F.forEach((f, i) => {
|
|
154
|
+
const c = document.createElement('div');
|
|
155
|
+
c.className = `chip ${i===0?'active':''}`;
|
|
156
|
+
c.textContent = `${i+1}. ${f.step_name}`;
|
|
157
|
+
c.onclick = () => { go(i); stop(); };
|
|
158
|
+
chips.appendChild(c);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// Dots
|
|
162
|
+
const dots = document.getElementById('dots');
|
|
163
|
+
F.forEach((_, i) => {
|
|
164
|
+
const d = document.createElement('div');
|
|
165
|
+
d.className = `dot ${i===0?'active':''}`;
|
|
166
|
+
d.onclick = () => { go(i); stop(); };
|
|
167
|
+
dots.appendChild(d);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
function go(i) {
|
|
171
|
+
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));
|
|
175
|
+
updatePanel();
|
|
176
|
+
updateAnnotations();
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function updatePanel() {
|
|
180
|
+
const f = F[cur]; if (!f) return;
|
|
181
|
+
document.getElementById('stepNum').textContent = `${cur+1} of ${F.length}`;
|
|
182
|
+
document.getElementById('stepTitle').textContent = f.step_name;
|
|
183
|
+
document.getElementById('stepDesc').textContent = f.description;
|
|
184
|
+
|
|
185
|
+
// Annotation cards
|
|
186
|
+
const cards = document.getElementById('annotCards');
|
|
187
|
+
cards.innerHTML = '';
|
|
188
|
+
(f.hotspots || []).forEach(h => {
|
|
189
|
+
const card = document.createElement('div');
|
|
190
|
+
card.className = 'annot-card fade-in';
|
|
191
|
+
card.style.borderLeftColor = h.color;
|
|
192
|
+
card.innerHTML = `<div class="annot-card-title"><div class="annot-card-dot" style="background:${h.color}"></div>${h.title}</div><div class="annot-card-desc">${h.description}</div>`;
|
|
193
|
+
cards.appendChild(card);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
// Baseline
|
|
197
|
+
const bl = document.getElementById('blBadge');
|
|
198
|
+
const s = f.baseline_status || 'not_validated';
|
|
199
|
+
bl.className = `baseline-badge ${s==='ok'?'ok':s==='changed'?'warn':''}`;
|
|
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';
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function updateAnnotations() {
|
|
205
|
+
const left = document.getElementById('annotsLeft');
|
|
206
|
+
const right = document.getElementById('annotsRight');
|
|
207
|
+
left.innerHTML = ''; right.innerHTML = '';
|
|
208
|
+
|
|
209
|
+
if (playing) {
|
|
210
|
+
left.classList.remove('show'); right.classList.remove('show');
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
left.classList.add('show'); right.classList.add('show');
|
|
214
|
+
|
|
215
|
+
const f = F[cur]; if (!f?.hotspots?.length) return;
|
|
216
|
+
|
|
217
|
+
f.hotspots.forEach((h, j) => {
|
|
218
|
+
const container = j % 2 === 0 ? left : right;
|
|
219
|
+
const el = document.createElement('div');
|
|
220
|
+
el.className = 'side-annot fade-in';
|
|
221
|
+
el.style.animationDelay = (j * 0.1) + 's';
|
|
222
|
+
el.innerHTML = `
|
|
223
|
+
<div class="side-annot-title"><div class="side-annot-dot" style="background:${h.color}"></div>${h.label}</div>
|
|
224
|
+
${h.description !== h.label ? `<div class="side-annot-desc">${h.description.slice(0, 60)}</div>` : ''}
|
|
225
|
+
`;
|
|
226
|
+
container.appendChild(el);
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function play() {
|
|
231
|
+
playing = true;
|
|
232
|
+
document.getElementById('playIco').style.display = 'none';
|
|
233
|
+
document.getElementById('pauseIco').style.display = '';
|
|
234
|
+
document.getElementById('statusText').textContent = 'playing';
|
|
235
|
+
document.getElementById('annotsLeft').classList.remove('show');
|
|
236
|
+
document.getElementById('annotsRight').classList.remove('show');
|
|
237
|
+
timer = setInterval(() => { cur = (cur+1)%F.length; go(cur); }, 2200);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function stop() {
|
|
241
|
+
playing = false; clearInterval(timer);
|
|
242
|
+
document.getElementById('playIco').style.display = '';
|
|
243
|
+
document.getElementById('pauseIco').style.display = 'none';
|
|
244
|
+
document.getElementById('statusText').textContent = 'paused';
|
|
245
|
+
updateAnnotations();
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
document.getElementById('playBtn').onclick = () => playing ? stop() : play();
|
|
249
|
+
|
|
250
|
+
updatePanel();
|
|
251
|
+
play();
|
|
252
|
+
</script>
|
|
253
|
+
</body>
|
|
254
|
+
</html>
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import "./chunk-R5U7XKVJ.js";
|
|
2
|
+
|
|
3
|
+
// src/core/export/import.ts
|
|
4
|
+
import { existsSync, mkdirSync, readFileSync, cpSync, rmSync, readdirSync, statSync } from "fs";
|
|
5
|
+
import { join, basename } from "path";
|
|
6
|
+
import { execSync } from "child_process";
|
|
7
|
+
import { randomUUID } from "crypto";
|
|
8
|
+
function extractZip(zipPath, targetDir) {
|
|
9
|
+
mkdirSync(targetDir, { recursive: true });
|
|
10
|
+
if (process.platform === "darwin") {
|
|
11
|
+
try {
|
|
12
|
+
execSync(`ditto -xk "${zipPath}" "${targetDir}"`, { stdio: "pipe" });
|
|
13
|
+
return;
|
|
14
|
+
} catch {
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
execSync(`unzip -qo "${zipPath}" -d "${targetDir}"`, { stdio: "pipe" });
|
|
18
|
+
}
|
|
19
|
+
function findBundleRoot(extractDir) {
|
|
20
|
+
const entries = readdirSync(extractDir);
|
|
21
|
+
if (entries.length === 1) {
|
|
22
|
+
const candidate = join(extractDir, entries[0]);
|
|
23
|
+
if (statSync(candidate).isDirectory()) return candidate;
|
|
24
|
+
}
|
|
25
|
+
if (entries.includes("manifest.json")) return extractDir;
|
|
26
|
+
for (const entry of entries) {
|
|
27
|
+
const dir = join(extractDir, entry);
|
|
28
|
+
if (statSync(dir).isDirectory() && existsSync(join(dir, "manifest.json"))) {
|
|
29
|
+
return dir;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return extractDir;
|
|
33
|
+
}
|
|
34
|
+
async function importApplabBundle(zipPath, db, schema, paths) {
|
|
35
|
+
if (!existsSync(zipPath)) {
|
|
36
|
+
return { success: false, error: `File not found: ${zipPath}` };
|
|
37
|
+
}
|
|
38
|
+
const tempDir = join(paths.dataDir, ".import-temp-" + Date.now());
|
|
39
|
+
try {
|
|
40
|
+
extractZip(zipPath, tempDir);
|
|
41
|
+
const bundleRoot = findBundleRoot(tempDir);
|
|
42
|
+
const manifestPath = join(bundleRoot, "manifest.json");
|
|
43
|
+
if (!existsSync(manifestPath)) {
|
|
44
|
+
return { success: false, error: "Invalid .applab file: manifest.json not found" };
|
|
45
|
+
}
|
|
46
|
+
const manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
|
|
47
|
+
let projectData = {};
|
|
48
|
+
const projectJsonPath = join(bundleRoot, "metadata", "project.json");
|
|
49
|
+
if (existsSync(projectJsonPath)) {
|
|
50
|
+
projectData = JSON.parse(readFileSync(projectJsonPath, "utf-8"));
|
|
51
|
+
}
|
|
52
|
+
const projectId = manifest.id || projectData.id || randomUUID();
|
|
53
|
+
const projectName = manifest.name || projectData.name || basename(zipPath, ".applab");
|
|
54
|
+
const { eq } = await import("drizzle-orm");
|
|
55
|
+
const existing = await db.select().from(schema.projects).where(eq(schema.projects.id, projectId)).limit(1);
|
|
56
|
+
if (existing.length > 0) {
|
|
57
|
+
return { success: false, error: `Project already exists: ${projectName} (${projectId}). Delete it first to re-import.` };
|
|
58
|
+
}
|
|
59
|
+
let frameCount = 0;
|
|
60
|
+
const framesSourceDir = join(bundleRoot, "frames");
|
|
61
|
+
const framesTargetDir = join(paths.framesDir, projectId);
|
|
62
|
+
if (existsSync(framesSourceDir)) {
|
|
63
|
+
mkdirSync(framesTargetDir, { recursive: true });
|
|
64
|
+
cpSync(framesSourceDir, framesTargetDir, { recursive: true });
|
|
65
|
+
frameCount = readdirSync(framesTargetDir).filter((f) => /\.(png|jpg|jpeg|webp|gif)$/i.test(f)).length;
|
|
66
|
+
}
|
|
67
|
+
const projectTargetDir = join(paths.projectsDir, projectId);
|
|
68
|
+
mkdirSync(projectTargetDir, { recursive: true });
|
|
69
|
+
const mediaDir = join(bundleRoot, "media");
|
|
70
|
+
if (existsSync(mediaDir)) {
|
|
71
|
+
cpSync(mediaDir, projectTargetDir, { recursive: true });
|
|
72
|
+
}
|
|
73
|
+
const recordingDir = join(bundleRoot, "recording");
|
|
74
|
+
if (existsSync(recordingDir)) {
|
|
75
|
+
cpSync(recordingDir, projectTargetDir, { recursive: true });
|
|
76
|
+
}
|
|
77
|
+
let aiSummary = null;
|
|
78
|
+
let ocrText = null;
|
|
79
|
+
const aiPath = join(bundleRoot, "analysis", "app-intelligence.md");
|
|
80
|
+
const ocrPath = join(bundleRoot, "analysis", "ocr.txt");
|
|
81
|
+
if (existsSync(aiPath)) aiSummary = readFileSync(aiPath, "utf-8");
|
|
82
|
+
if (existsSync(ocrPath)) ocrText = readFileSync(ocrPath, "utf-8");
|
|
83
|
+
let taskHubLinks = null;
|
|
84
|
+
const linksPath = join(bundleRoot, "taskhub", "links.json");
|
|
85
|
+
if (existsSync(linksPath)) {
|
|
86
|
+
taskHubLinks = readFileSync(linksPath, "utf-8");
|
|
87
|
+
}
|
|
88
|
+
let videoPath = null;
|
|
89
|
+
const videoExts = [".mp4", ".mov", ".webm"];
|
|
90
|
+
for (const ext of videoExts) {
|
|
91
|
+
const files = readdirSync(projectTargetDir).filter((f) => f.endsWith(ext));
|
|
92
|
+
if (files.length > 0) {
|
|
93
|
+
videoPath = join(projectTargetDir, files[0]);
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (!videoPath) videoPath = projectTargetDir;
|
|
98
|
+
let thumbnailPath = null;
|
|
99
|
+
if (frameCount > 0) {
|
|
100
|
+
const firstFrame = readdirSync(framesTargetDir).filter((f) => /\.(png|jpg|jpeg|webp)$/i.test(f)).sort()[0];
|
|
101
|
+
if (firstFrame) thumbnailPath = join(framesTargetDir, firstFrame);
|
|
102
|
+
}
|
|
103
|
+
const now = /* @__PURE__ */ new Date();
|
|
104
|
+
await db.insert(schema.projects).values({
|
|
105
|
+
id: projectId,
|
|
106
|
+
name: projectName,
|
|
107
|
+
marketingTitle: projectData.marketingTitle || projectName,
|
|
108
|
+
marketingDescription: projectData.marketingDescription || null,
|
|
109
|
+
videoPath,
|
|
110
|
+
thumbnailPath,
|
|
111
|
+
platform: manifest.platform || projectData.platform || null,
|
|
112
|
+
aiSummary,
|
|
113
|
+
ocrText,
|
|
114
|
+
ocrEngine: projectData.ocrEngine || null,
|
|
115
|
+
ocrConfidence: projectData.ocrConfidence || null,
|
|
116
|
+
frameCount,
|
|
117
|
+
duration: projectData.duration || null,
|
|
118
|
+
manualNotes: projectData.manualNotes || null,
|
|
119
|
+
tags: projectData.tags || null,
|
|
120
|
+
linkedTicket: projectData.linkedTicket || null,
|
|
121
|
+
linkedJiraUrl: projectData.linkedJiraUrl || null,
|
|
122
|
+
linkedNotionUrl: projectData.linkedNotionUrl || null,
|
|
123
|
+
linkedFigmaUrl: projectData.linkedFigmaUrl || null,
|
|
124
|
+
taskHubLinks,
|
|
125
|
+
taskRequirements: projectData.taskRequirements || null,
|
|
126
|
+
taskTestMap: projectData.taskTestMap || null,
|
|
127
|
+
status: aiSummary ? "analyzed" : "draft",
|
|
128
|
+
createdAt: projectData.createdAt ? new Date(projectData.createdAt) : now,
|
|
129
|
+
updatedAt: now
|
|
130
|
+
});
|
|
131
|
+
if (frameCount > 0) {
|
|
132
|
+
const frameFiles = readdirSync(framesTargetDir).filter((f) => /\.(png|jpg|jpeg|webp|gif)$/i.test(f)).sort();
|
|
133
|
+
let frameAnalysis = {};
|
|
134
|
+
const framesJsonPath = join(bundleRoot, "analysis", "frames.json");
|
|
135
|
+
if (existsSync(framesJsonPath)) {
|
|
136
|
+
try {
|
|
137
|
+
frameAnalysis = JSON.parse(readFileSync(framesJsonPath, "utf-8"));
|
|
138
|
+
} catch {
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
for (let i = 0; i < frameFiles.length; i++) {
|
|
142
|
+
const frameId = `${projectId}-frame-${i}`;
|
|
143
|
+
const framePath = join(framesTargetDir, frameFiles[i]);
|
|
144
|
+
const frameOcr = frameAnalysis[`frame-${i}`]?.ocrText || null;
|
|
145
|
+
await db.insert(schema.frames).values({
|
|
146
|
+
id: frameId,
|
|
147
|
+
projectId,
|
|
148
|
+
frameNumber: i,
|
|
149
|
+
timestamp: i,
|
|
150
|
+
// approximate
|
|
151
|
+
imagePath: framePath,
|
|
152
|
+
ocrText: frameOcr,
|
|
153
|
+
isKeyFrame: i === 0,
|
|
154
|
+
createdAt: now
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return {
|
|
159
|
+
success: true,
|
|
160
|
+
projectId,
|
|
161
|
+
projectName,
|
|
162
|
+
frameCount
|
|
163
|
+
};
|
|
164
|
+
} catch (error) {
|
|
165
|
+
return {
|
|
166
|
+
success: false,
|
|
167
|
+
error: error instanceof Error ? error.message : String(error)
|
|
168
|
+
};
|
|
169
|
+
} finally {
|
|
170
|
+
if (existsSync(tempDir)) {
|
|
171
|
+
try {
|
|
172
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
173
|
+
} catch {
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
export {
|
|
179
|
+
importApplabBundle
|
|
180
|
+
};
|