@veolab/discoverylab 1.4.4 → 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/dist/index.html CHANGED
@@ -806,6 +806,10 @@
806
806
  opacity: 1;
807
807
  }
808
808
 
809
+ .project-card.select-mode .project-claude-action {
810
+ display: none;
811
+ }
812
+
809
813
  .project-card.select-mode.checked .checkbox-circle {
810
814
  opacity: 0;
811
815
  }
@@ -920,6 +924,37 @@
920
924
  object-fit: cover;
921
925
  }
922
926
 
927
+ .project-claude-action {
928
+ position: absolute;
929
+ top: 8px;
930
+ left: 8px;
931
+ display: inline-flex;
932
+ align-items: center;
933
+ justify-content: center;
934
+ width: 32px;
935
+ height: 32px;
936
+ border-radius: 999px;
937
+ border: 1px solid rgba(255, 255, 255, 0.18);
938
+ background: rgba(11, 16, 24, 0.78);
939
+ color: #f5f0e6;
940
+ box-shadow: 0 12px 24px rgba(0, 0, 0, 0.22);
941
+ z-index: 2;
942
+ cursor: pointer;
943
+ transition: transform 0.15s ease, border-color 0.15s ease, background 0.15s ease;
944
+ backdrop-filter: blur(10px);
945
+ }
946
+
947
+ .project-claude-action:hover {
948
+ transform: translateY(-1px);
949
+ border-color: rgba(245, 240, 230, 0.5);
950
+ background: rgba(17, 24, 32, 0.92);
951
+ }
952
+
953
+ .project-claude-action svg {
954
+ width: 16px;
955
+ height: 16px;
956
+ }
957
+
923
958
  .project-info {
924
959
  padding: 12px;
925
960
  }
@@ -987,6 +1022,82 @@
987
1022
  font-weight: 700;
988
1023
  }
989
1024
 
1025
+ .claude-integration-row {
1026
+ display: flex;
1027
+ align-items: center;
1028
+ justify-content: space-between;
1029
+ gap: 12px;
1030
+ margin-top: 10px;
1031
+ padding-top: 10px;
1032
+ border-top: 1px solid var(--border);
1033
+ }
1034
+
1035
+ .claude-integration-meta {
1036
+ min-width: 0;
1037
+ }
1038
+
1039
+ .claude-integration-title {
1040
+ font-size: 11px;
1041
+ color: var(--text-muted);
1042
+ font-weight: 600;
1043
+ text-transform: uppercase;
1044
+ letter-spacing: 0.4px;
1045
+ margin-bottom: 4px;
1046
+ }
1047
+
1048
+ .claude-integration-copy {
1049
+ font-size: 12px;
1050
+ color: var(--text-secondary);
1051
+ line-height: 1.45;
1052
+ }
1053
+
1054
+ .claude-integration-actions {
1055
+ display: flex;
1056
+ align-items: center;
1057
+ gap: 8px;
1058
+ flex-shrink: 0;
1059
+ }
1060
+
1061
+ .claude-action-btn {
1062
+ display: inline-flex;
1063
+ align-items: center;
1064
+ gap: 8px;
1065
+ border: 1px solid rgba(236, 229, 215, 0.18);
1066
+ background: linear-gradient(135deg, rgba(34, 25, 18, 0.96), rgba(17, 12, 9, 0.96));
1067
+ color: #f5f0e6;
1068
+ padding: 9px 12px;
1069
+ border-radius: 10px;
1070
+ cursor: pointer;
1071
+ transition: transform 0.15s ease, border-color 0.15s ease, opacity 0.15s ease;
1072
+ font-size: 12px;
1073
+ font-weight: 600;
1074
+ box-shadow: 0 14px 28px rgba(0, 0, 0, 0.18);
1075
+ }
1076
+
1077
+ .claude-action-btn:hover {
1078
+ transform: translateY(-1px);
1079
+ border-color: rgba(245, 240, 230, 0.34);
1080
+ }
1081
+
1082
+ .claude-action-btn.secondary {
1083
+ background: var(--bg-tertiary);
1084
+ color: var(--text-secondary);
1085
+ box-shadow: none;
1086
+ border-color: var(--border);
1087
+ }
1088
+
1089
+ .claude-action-btn:disabled {
1090
+ opacity: 0.56;
1091
+ cursor: default;
1092
+ transform: none;
1093
+ }
1094
+
1095
+ .claude-action-btn svg {
1096
+ width: 14px;
1097
+ height: 14px;
1098
+ flex-shrink: 0;
1099
+ }
1100
+
990
1101
  .project-meta {
991
1102
  font-size: 11px;
992
1103
  color: var(--text-muted);
@@ -6795,7 +6906,7 @@
6795
6906
  <span>App Intelligence</span>
6796
6907
  </div>
6797
6908
  <div class="welcome-footnote">
6798
- Built with the open <a href="https://esvp.dev" target="_blank" rel="noreferrer">ESVP protocol</a> by Entropy Lab for reproducible mobile sessions, replay, and network-aware validation.
6909
+ Built with the open ESVP protocol by Entropy Lab for reproducible mobile sessions, replay, and network-aware validation.
6799
6910
  </div>
6800
6911
  </div>
6801
6912
  </div>
@@ -8109,6 +8220,223 @@
8109
8220
  }
8110
8221
  }
8111
8222
 
8223
+ let claudeDesktopStatus = {
8224
+ ready: false,
8225
+ appDetected: false,
8226
+ launcherSupported: false,
8227
+ mcpConfigured: false,
8228
+ serverName: null,
8229
+ installCommand: 'npx -y @veolab/discoverylab@latest install --target desktop',
8230
+ message: 'Checking Claude Desktop...',
8231
+ checking: false,
8232
+ error: null,
8233
+ lastCheckedAt: null
8234
+ };
8235
+
8236
+ function getClaudeIconSvg() {
8237
+ return `
8238
+ <svg viewBox="0 0 24 24" fill="none" aria-hidden="true">
8239
+ <path d="M12 3.5c-2.1 0-3.8 1-4.9 2.8C4.9 6.5 3.5 8.4 3.5 11c0 3.9 2.9 6.8 7.1 8.7a3 3 0 0 0 2.8 0c4.1-1.9 7.1-4.8 7.1-8.7 0-2.6-1.4-4.5-3.6-4.7C15.8 4.5 14.1 3.5 12 3.5Z" stroke="currentColor" stroke-width="1.5"/>
8240
+ <path d="M8.5 10.5c.9-1 2.1-1.5 3.5-1.5s2.6.5 3.5 1.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
8241
+ <path d="M9.2 13.8c.8.7 1.7 1.1 2.8 1.1 1 0 2-.4 2.8-1.1" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
8242
+ </svg>
8243
+ `;
8244
+ }
8245
+
8246
+ function isProjectReadyForClaude(project) {
8247
+ if (!project) return false;
8248
+ if (isProjectAnalyzing(project.status || '')) return false;
8249
+ return Boolean(
8250
+ (project.aiSummary && project.aiSummary.trim()) ||
8251
+ (project.ocrText && project.ocrText.trim()) ||
8252
+ (Array.isArray(project.frames) && project.frames.length > 0) ||
8253
+ project.frameCount > 0
8254
+ );
8255
+ }
8256
+
8257
+ function buildClaudeDesktopPrompt(project) {
8258
+ const projectId = project?.id || '';
8259
+ const projectName = project?.name || 'this DiscoveryLab project';
8260
+ return [
8261
+ `Use DiscoveryLab local tools to open project "${projectName}" (projectId: "${projectId}") in an interactive flow canvas.`,
8262
+ `Call dlab.knowledge.open with projectId "${projectId}".`,
8263
+ 'After opening the canvas, explain the flow, key screens, and important risks or opportunities.',
8264
+ ].join(' ');
8265
+ }
8266
+
8267
+ function renderClaudeProjectQuickAction(project) {
8268
+ if (!claudeDesktopStatus.ready || !isProjectReadyForClaude(project)) {
8269
+ return '';
8270
+ }
8271
+
8272
+ const promptTitle = `Copy prompt and open ${project.name} in Claude Desktop`;
8273
+ return `
8274
+ <button
8275
+ class="project-claude-action"
8276
+ type="button"
8277
+ data-project-id="${escapeAttr(project.id)}"
8278
+ title="${escapeAttr(promptTitle)}"
8279
+ aria-label="${escapeAttr(promptTitle)}"
8280
+ onclick="event.stopPropagation(); openProjectInClaudeById(this.dataset.projectId);"
8281
+ >
8282
+ ${getClaudeIconSvg()}
8283
+ </button>
8284
+ `;
8285
+ }
8286
+
8287
+ function renderClaudeProjectLaunchRow(project) {
8288
+ const projectReady = isProjectReadyForClaude(project);
8289
+ const statusCopy = !projectReady
8290
+ ? 'Finish analysis or capture enough frames before opening this project in Claude Desktop.'
8291
+ : claudeDesktopStatus.error
8292
+ ? claudeDesktopStatus.error
8293
+ : claudeDesktopStatus.message || 'Claude Desktop status unavailable.';
8294
+ const ready = claudeDesktopStatus.ready && projectReady;
8295
+ const setupNeeded = claudeDesktopStatus.appDetected && !claudeDesktopStatus.mcpConfigured;
8296
+ const buttonLabel = ready
8297
+ ? 'Open in Claude Desktop'
8298
+ : !projectReady
8299
+ ? 'Analyze first'
8300
+ : setupNeeded
8301
+ ? 'Copy install command'
8302
+ : 'Reload';
8303
+ const buttonClass = ready ? 'claude-action-btn' : 'claude-action-btn secondary';
8304
+ const action = ready
8305
+ ? 'openCurrentProjectInClaude()'
8306
+ : !projectReady
8307
+ ? ''
8308
+ : setupNeeded
8309
+ ? 'copyClaudeDesktopInstallCommand()'
8310
+ : 'refreshClaudeDesktopStatus()';
8311
+
8312
+ return `
8313
+ <div class="claude-integration-row">
8314
+ <div class="claude-integration-meta">
8315
+ <div class="claude-integration-title">Claude Desktop</div>
8316
+ <div class="claude-integration-copy">${escapeHtml(statusCopy)}</div>
8317
+ </div>
8318
+ <div class="claude-integration-actions">
8319
+ <button class="${buttonClass}" type="button" ${action ? `onclick="${action}"` : 'disabled'}>
8320
+ ${getClaudeIconSvg()}
8321
+ <span>${escapeHtml(buttonLabel)}</span>
8322
+ </button>
8323
+ </div>
8324
+ </div>
8325
+ `;
8326
+ }
8327
+
8328
+ function rerenderClaudeDesktopAffordances() {
8329
+ if (currentView === 'projects') {
8330
+ renderProjects();
8331
+ } else if (currentView === 'analysis' && currentProject) {
8332
+ updateAnalysisTab();
8333
+ } else if (currentView === 'export' && currentProject) {
8334
+ updateExportTab();
8335
+ }
8336
+ }
8337
+
8338
+ async function refreshClaudeDesktopStatus(options = {}) {
8339
+ const { silent = false } = options;
8340
+ claudeDesktopStatus.checking = true;
8341
+
8342
+ try {
8343
+ const response = await fetch('/api/integrations/claude-desktop/status');
8344
+ const data = await response.json();
8345
+ if (data?.error) {
8346
+ throw new Error(data.error);
8347
+ }
8348
+
8349
+ claudeDesktopStatus = {
8350
+ ...claudeDesktopStatus,
8351
+ ready: !!data.ready,
8352
+ appDetected: !!data.appDetected,
8353
+ launcherSupported: !!data.launcherSupported,
8354
+ mcpConfigured: !!data.mcpConfigured,
8355
+ serverName: data.serverName || null,
8356
+ installCommand: data.installCommand || claudeDesktopStatus.installCommand,
8357
+ message: data.message || 'Claude Desktop status loaded.',
8358
+ checking: false,
8359
+ error: null,
8360
+ lastCheckedAt: new Date().toISOString()
8361
+ };
8362
+ } catch (error) {
8363
+ claudeDesktopStatus = {
8364
+ ...claudeDesktopStatus,
8365
+ checking: false,
8366
+ error: error instanceof Error ? error.message : 'Failed to check Claude Desktop status',
8367
+ message: 'Could not check Claude Desktop status.'
8368
+ };
8369
+ if (!silent) {
8370
+ showToast('Failed to check Claude Desktop status', 'error');
8371
+ }
8372
+ }
8373
+
8374
+ rerenderClaudeDesktopAffordances();
8375
+ }
8376
+
8377
+ function copyClaudeDesktopInstallCommand() {
8378
+ void copyToClipboard(
8379
+ claudeDesktopStatus.installCommand || 'npx -y @veolab/discoverylab@latest install --target desktop',
8380
+ { silent: true }
8381
+ ).then((copied) => {
8382
+ showToast(copied ? 'Claude Desktop install command copied' : 'Failed to copy install command', copied ? 'success' : 'error');
8383
+ });
8384
+ }
8385
+
8386
+ async function launchClaudeDesktop() {
8387
+ const response = await fetch('/api/integrations/claude-desktop/launch', { method: 'POST' });
8388
+ const data = await response.json();
8389
+ if (!response.ok || data?.success === false) {
8390
+ throw new Error(data?.error || 'Failed to open Claude Desktop');
8391
+ }
8392
+ }
8393
+
8394
+ async function copyPromptAndLaunchClaude(project) {
8395
+ if (!project) return;
8396
+
8397
+ const copied = await copyToClipboard(buildClaudeDesktopPrompt(project), { silent: true });
8398
+ if (!copied) {
8399
+ showToast('Failed to copy Claude Desktop prompt', 'error');
8400
+ return;
8401
+ }
8402
+
8403
+ try {
8404
+ await launchClaudeDesktop();
8405
+ showToast('Prompt copied and Claude Desktop opened', 'success');
8406
+ } catch (error) {
8407
+ showToast(error instanceof Error ? error.message : 'Prompt copied, but Claude Desktop could not be opened', 'warning');
8408
+ }
8409
+ }
8410
+
8411
+ async function openProjectInClaudeById(projectId) {
8412
+ const project = projects.find((item) => item.id === projectId) || (currentProject?.id === projectId ? currentProject : null);
8413
+ if (!project) {
8414
+ showToast('Project not found', 'error');
8415
+ return;
8416
+ }
8417
+
8418
+ if (!claudeDesktopStatus.ready) {
8419
+ if (claudeDesktopStatus.appDetected && !claudeDesktopStatus.mcpConfigured) {
8420
+ copyClaudeDesktopInstallCommand();
8421
+ return;
8422
+ }
8423
+
8424
+ showToast(claudeDesktopStatus.message || 'Claude Desktop is not ready yet.', 'warning');
8425
+ return;
8426
+ }
8427
+
8428
+ await copyPromptAndLaunchClaude(project);
8429
+ }
8430
+
8431
+ async function openCurrentProjectInClaude() {
8432
+ if (!currentProject?.id) {
8433
+ showToast('Select a project first', 'warning');
8434
+ return;
8435
+ }
8436
+
8437
+ await openProjectInClaudeById(currentProject.id);
8438
+ }
8439
+
8112
8440
  let jiraMcpStatus = {
8113
8441
  available: false,
8114
8442
  configured: false,
@@ -11288,6 +11616,7 @@
11288
11616
  <div class="processing-text">${processingText}</div>
11289
11617
  </div>
11290
11618
  <div class="project-thumb">
11619
+ ${renderClaudeProjectQuickAction(p)}
11291
11620
  ${getProjectThumb(p)}
11292
11621
  </div>
11293
11622
  <div class="project-info">
@@ -12418,6 +12747,7 @@
12418
12747
  <button class="btn btn-primary btn-small" onclick="retryProjectAnalysis()" id="retryProjectAnalysisBtn">Retry Analysis</button>
12419
12748
  </div>
12420
12749
  ` : ''}
12750
+ ${renderClaudeProjectLaunchRow(currentProject)}
12421
12751
  <div class="project-integration-row">
12422
12752
  <div class="project-integration-label">Jira MCP</div>
12423
12753
  <div class="project-integration-status">
@@ -12864,6 +13194,23 @@
12864
13194
 
12865
13195
  if (favoriteTemplate && currentTemplateProps?.templatesAllowed !== false) {
12866
13196
  switchTemplate(favoriteTemplate);
13197
+ } else if (currentTemplateProps?.templatesAllowed !== false) {
13198
+ // Auto-select cached template if available (no re-render needed)
13199
+ const templates = templateStatus.templates || [];
13200
+ for (const t of templates) {
13201
+ try {
13202
+ const checkRes = await fetch('/api/templates/render', {
13203
+ method: 'POST',
13204
+ headers: { 'Content-Type': 'application/json' },
13205
+ body: JSON.stringify({ projectId: currentProject?.id, templateId: t.id }),
13206
+ });
13207
+ const checkData = await checkRes.json();
13208
+ if (checkData.cached) {
13209
+ switchTemplate(t.id);
13210
+ break;
13211
+ }
13212
+ } catch { /* skip */ }
13213
+ }
12867
13214
  }
12868
13215
  } catch (e) {
12869
13216
  console.log('[Templates] Init error:', e);
@@ -12983,7 +13330,7 @@
12983
13330
  }
12984
13331
  }
12985
13332
 
12986
- async function switchTemplate(templateId) {
13333
+ async function switchTemplate(templateId, force = false) {
12987
13334
  // Skip if already on this template
12988
13335
  if (templateId === activeTemplate) return;
12989
13336
  if (templateId !== 'raw' && currentTemplateProps?.templatesAllowed === false) {
@@ -13028,7 +13375,7 @@
13028
13375
  const res = await fetch('/api/templates/render', {
13029
13376
  method: 'POST',
13030
13377
  headers: { 'Content-Type': 'application/json' },
13031
- body: JSON.stringify({ projectId: currentProject.id, templateId }),
13378
+ body: JSON.stringify({ projectId: currentProject.id, templateId, force }),
13032
13379
  });
13033
13380
  const data = await res.json();
13034
13381
 
@@ -13038,6 +13385,20 @@
13038
13385
  return;
13039
13386
  }
13040
13387
 
13388
+ // Cached render - show immediately without polling
13389
+ if (data.cached && data.previewUrl) {
13390
+ const skeleton = document.getElementById('templateSkeleton');
13391
+ const templateContainer = document.getElementById('templatePreviewContainer');
13392
+ const templateVideo = document.getElementById('templateVideo');
13393
+ if (skeleton) skeleton.style.display = 'none';
13394
+ if (templateVideo) {
13395
+ templateVideo.src = data.previewUrl + '&t=' + Date.now();
13396
+ templateVideo.load();
13397
+ }
13398
+ if (templateContainer) templateContainer.style.display = '';
13399
+ return;
13400
+ }
13401
+
13041
13402
  if (data.jobId) {
13042
13403
  pollRenderJob(data.jobId, templateId);
13043
13404
  }
@@ -13490,11 +13851,11 @@
13490
13851
  deviceMockup: data.content?.deviceMockup || deviceMockup,
13491
13852
  };
13492
13853
 
13493
- // Force re-render
13854
+ // Force re-render after content edit
13494
13855
  if (activeTemplate !== 'raw') {
13495
13856
  const prevTemplate = activeTemplate;
13496
13857
  activeTemplate = '__force_rerender__';
13497
- switchTemplate(prevTemplate);
13858
+ switchTemplate(prevTemplate, true);
13498
13859
  }
13499
13860
  } catch {
13500
13861
  showToast('Failed to save content', 'error');
@@ -15571,9 +15932,34 @@
15571
15932
  showToast(`Copied "${command}" - paste in Claude Code`, 'success', 5000);
15572
15933
  }
15573
15934
 
15574
- function copyToClipboard(text) {
15575
- navigator.clipboard.writeText(text);
15576
- showToast('Copied to clipboard', 'success');
15935
+ async function copyToClipboard(text, options = {}) {
15936
+ const { silent = false } = options;
15937
+
15938
+ try {
15939
+ if (navigator.clipboard?.writeText) {
15940
+ await navigator.clipboard.writeText(text);
15941
+ } else {
15942
+ const textarea = document.createElement('textarea');
15943
+ textarea.value = text;
15944
+ textarea.setAttribute('readonly', 'true');
15945
+ textarea.style.position = 'absolute';
15946
+ textarea.style.left = '-9999px';
15947
+ document.body.appendChild(textarea);
15948
+ textarea.select();
15949
+ document.execCommand('copy');
15950
+ textarea.remove();
15951
+ }
15952
+
15953
+ if (!silent) {
15954
+ showToast('Copied to clipboard', 'success');
15955
+ }
15956
+ return true;
15957
+ } catch (error) {
15958
+ if (!silent) {
15959
+ showToast('Failed to copy to clipboard', 'error');
15960
+ }
15961
+ return false;
15962
+ }
15577
15963
  }
15578
15964
 
15579
15965
  async function openMaestroStudio() {
@@ -15868,6 +16254,7 @@
15868
16254
  if (el && data.version) el.textContent = 'v' + data.version;
15869
16255
  }).catch(() => {});
15870
16256
  void loadProxySafetySettings().catch(() => null);
16257
+ void refreshClaudeDesktopStatus({ silent: true });
15871
16258
 
15872
16259
  // Auto-check dependencies on first load — open Settings if critical deps missing
15873
16260
  fetch('/api/setup/status').then(r => r.json()).then(data => {
@@ -22043,13 +22430,37 @@ appId: ${platform === 'ios' ? 'com.apple.Preferences' : 'com.android.settings'}
22043
22430
  <div id="exportFormatHelp" style="margin-top: 8px; font-size: 12px; color: var(--text-muted); line-height: 1.5;"></div>
22044
22431
  </div>
22045
22432
 
22046
- <button class="btn btn-primary" onclick="exportProject()">Export</button>
22433
+ <div style="display: flex; gap: 8px;">
22434
+ <button class="btn btn-primary" onclick="exportProject()">Export</button>
22435
+ <button class="btn btn-secondary" onclick="exportInfographicHtml()" style="font-size: 11px;">Infographic HTML</button>
22436
+ </div>
22047
22437
  </div>
22048
22438
  `;
22049
22439
 
22050
22440
  updateExportFormatHelp();
22051
22441
  }
22052
22442
 
22443
+ async function exportInfographicHtml() {
22444
+ if (!currentProject) return;
22445
+ showToast('Generating infographic...', 'info');
22446
+ try {
22447
+ const resp = await fetch('/api/export/infographic', {
22448
+ method: 'POST',
22449
+ headers: { 'Content-Type': 'application/json' },
22450
+ body: JSON.stringify({ projectId: currentProject.id, open: true }),
22451
+ });
22452
+ const data = await resp.json();
22453
+ if (data.success) {
22454
+ const sizeKb = ((data.size || 0) / 1024).toFixed(1);
22455
+ showToast(`Infographic exported (${sizeKb}KB, ${data.frameCount} frames)`, 'success');
22456
+ } else {
22457
+ showToast(data.error || 'Export failed', 'error');
22458
+ }
22459
+ } catch (err) {
22460
+ showToast('Export failed', 'error');
22461
+ }
22462
+ }
22463
+
22053
22464
  async function exportProject() {
22054
22465
  if (!currentProject) return;
22055
22466
 
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  startServer,
4
4
  stopServer
5
- } from "./chunk-2UUMLAVR.js";
5
+ } from "./chunk-IVX2OSOJ.js";
6
6
  import {
7
7
  analyzeTools,
8
8
  canvasTools,
@@ -16,15 +16,15 @@ import {
16
16
  templateTools,
17
17
  testingTools,
18
18
  uiTools
19
- } from "./chunk-HB3YPWF3.js";
19
+ } from "./chunk-5AISGCS4.js";
20
+ import "./chunk-34GGYFXX.js";
20
21
  import "./chunk-PMCXEA7J.js";
21
22
  import {
22
23
  setupTools
23
- } from "./chunk-CUBQRT5L.js";
24
+ } from "./chunk-HFN6BTVO.js";
24
25
  import {
25
26
  mcpServer
26
27
  } from "./chunk-XKX6NBHF.js";
27
- import "./chunk-34GGYFXX.js";
28
28
  import "./chunk-6GK5K6CS.js";
29
29
  import "./chunk-7R5YNOXE.js";
30
30
  import "./chunk-3ERJNXYM.js";