@veolab/discoverylab 1.4.3 → 1.6.3
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/dist/{chunk-HB3YPWF3.js → chunk-5AISGCS4.js} +166 -8
- package/dist/{chunk-CUBQRT5L.js → chunk-HFN6BTVO.js} +110 -1
- package/dist/{chunk-D4FTLCKM.js → chunk-IVX2OSOJ.js} +260 -4
- package/dist/cli.js +158 -27
- package/dist/export/infographic-template.html +254 -0
- package/dist/import-W2JEW254.js +180 -0
- package/dist/index.html +420 -9
- package/dist/index.js +4 -4
- package/dist/infographic-GQAHEOAA.js +183 -0
- package/dist/{server-CZPWQYOI.js → server-W3JQ5RG7.js} +1 -1
- package/dist/{setup-6JJYKKBS.js → setup-F7MGEFIM.js} +4 -1
- package/dist/{tools-Q7OZO732.js → tools-VYFNRUS4.js} +5 -3
- package/dist/visualizations/app-flow-map.html +177 -0
- package/dist/visualizations/device-showcase.html +150 -0
- package/dist/visualizations/flow-diagram.html +181 -0
- package/dist/visualizations/metrics-dashboard.html +263 -0
- package/doc/esvp-protocol.md +116 -0
- package/package.json +3 -3
- package/skills/knowledge-brain/SKILL.md +44 -43
|
@@ -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
|
+
};
|