markupr 2.1.8
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/commands/review-feedback.md +47 -0
- package/.eslintrc.json +35 -0
- package/.github/CODEOWNERS +16 -0
- package/.github/FUNDING.yml +1 -0
- package/.github/ISSUE_TEMPLATE/bug_report.md +56 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +54 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +89 -0
- package/.github/dependabot.yml +70 -0
- package/.github/workflows/ci.yml +184 -0
- package/.github/workflows/deploy-landing.yml +134 -0
- package/.github/workflows/nightly.yml +288 -0
- package/.github/workflows/release.yml +318 -0
- package/CHANGELOG.md +127 -0
- package/CLAUDE.md +137 -0
- package/CODE_OF_CONDUCT.md +9 -0
- package/CONTRIBUTING.md +390 -0
- package/LICENSE +21 -0
- package/PRODUCT_VISION.md +277 -0
- package/README.md +517 -0
- package/SECURITY.md +51 -0
- package/SIGNING_INSTRUCTIONS.md +284 -0
- package/assets/DMG_BACKGROUND_INSTRUCTIONS.md +130 -0
- package/assets/svg-source/dmg-background.svg +70 -0
- package/assets/svg-source/icon.svg +20 -0
- package/assets/svg-source/tray-icon-processing.svg +7 -0
- package/assets/svg-source/tray-icon-recording.svg +7 -0
- package/assets/svg-source/tray-icon.svg +6 -0
- package/assets/tray-complete.png +0 -0
- package/assets/tray-complete@2x.png +0 -0
- package/assets/tray-completeTemplate.png +0 -0
- package/assets/tray-completeTemplate@2x.png +0 -0
- package/assets/tray-error.png +0 -0
- package/assets/tray-error@2x.png +0 -0
- package/assets/tray-errorTemplate.png +0 -0
- package/assets/tray-errorTemplate@2x.png +0 -0
- package/assets/tray-icon-processing.png +0 -0
- package/assets/tray-icon-processing@2x.png +0 -0
- package/assets/tray-icon-processingTemplate.png +0 -0
- package/assets/tray-icon-processingTemplate@2x.png +0 -0
- package/assets/tray-icon-recording.png +0 -0
- package/assets/tray-icon-recording@2x.png +0 -0
- package/assets/tray-icon-recordingTemplate.png +0 -0
- package/assets/tray-icon-recordingTemplate@2x.png +0 -0
- package/assets/tray-icon.png +0 -0
- package/assets/tray-icon@2x.png +0 -0
- package/assets/tray-iconTemplate.png +0 -0
- package/assets/tray-iconTemplate@2x.png +0 -0
- package/assets/tray-idle.png +0 -0
- package/assets/tray-idle@2x.png +0 -0
- package/assets/tray-idleTemplate.png +0 -0
- package/assets/tray-idleTemplate@2x.png +0 -0
- package/assets/tray-processing-0.png +0 -0
- package/assets/tray-processing-0@2x.png +0 -0
- package/assets/tray-processing-0Template.png +0 -0
- package/assets/tray-processing-0Template@2x.png +0 -0
- package/assets/tray-processing-1.png +0 -0
- package/assets/tray-processing-1@2x.png +0 -0
- package/assets/tray-processing-1Template.png +0 -0
- package/assets/tray-processing-1Template@2x.png +0 -0
- package/assets/tray-processing-2.png +0 -0
- package/assets/tray-processing-2@2x.png +0 -0
- package/assets/tray-processing-2Template.png +0 -0
- package/assets/tray-processing-2Template@2x.png +0 -0
- package/assets/tray-processing-3.png +0 -0
- package/assets/tray-processing-3@2x.png +0 -0
- package/assets/tray-processing-3Template.png +0 -0
- package/assets/tray-processing-3Template@2x.png +0 -0
- package/assets/tray-processing.png +0 -0
- package/assets/tray-processing@2x.png +0 -0
- package/assets/tray-processingTemplate.png +0 -0
- package/assets/tray-processingTemplate@2x.png +0 -0
- package/assets/tray-recording.png +0 -0
- package/assets/tray-recording@2x.png +0 -0
- package/assets/tray-recordingTemplate.png +0 -0
- package/assets/tray-recordingTemplate@2x.png +0 -0
- package/build/DMG_BACKGROUND_SPEC.md +50 -0
- package/build/dmg-background.png +0 -0
- package/build/dmg-background@2x.png +0 -0
- package/build/entitlements.mac.inherit.plist +27 -0
- package/build/entitlements.mac.plist +41 -0
- package/build/favicon-16.png +0 -0
- package/build/favicon-180.png +0 -0
- package/build/favicon-192.png +0 -0
- package/build/favicon-32.png +0 -0
- package/build/favicon-48.png +0 -0
- package/build/favicon-512.png +0 -0
- package/build/favicon-64.png +0 -0
- package/build/icon-128.png +0 -0
- package/build/icon-16.png +0 -0
- package/build/icon-24.png +0 -0
- package/build/icon-256.png +0 -0
- package/build/icon-32.png +0 -0
- package/build/icon-48.png +0 -0
- package/build/icon-64.png +0 -0
- package/build/icon.icns +0 -0
- package/build/icon.ico +0 -0
- package/build/icon.iconset/icon_128x128.png +0 -0
- package/build/icon.iconset/icon_128x128@2x.png +0 -0
- package/build/icon.iconset/icon_16x16.png +0 -0
- package/build/icon.iconset/icon_16x16@2x.png +0 -0
- package/build/icon.iconset/icon_256x256.png +0 -0
- package/build/icon.iconset/icon_256x256@2x.png +0 -0
- package/build/icon.iconset/icon_32x32.png +0 -0
- package/build/icon.iconset/icon_32x32@2x.png +0 -0
- package/build/icon.iconset/icon_512x512.png +0 -0
- package/build/icon.iconset/icon_512x512@2x.png +0 -0
- package/build/icon.png +0 -0
- package/build/installer-header.bmp +0 -0
- package/build/installer-header.png +0 -0
- package/build/installer-sidebar.bmp +0 -0
- package/build/installer-sidebar.png +0 -0
- package/build/installer.nsh +45 -0
- package/build/overlay-processing.png +0 -0
- package/build/overlay-recording.png +0 -0
- package/build/toolbar-record.png +0 -0
- package/build/toolbar-screenshot.png +0 -0
- package/build/toolbar-settings.png +0 -0
- package/build/toolbar-stop.png +0 -0
- package/dist/main/index.mjs +12612 -0
- package/dist/preload/index.mjs +907 -0
- package/dist/renderer/assets/index-CCmUjl9K.js +19495 -0
- package/dist/renderer/assets/index-CUqz_Gs6.css +2270 -0
- package/dist/renderer/index.html +27 -0
- package/docs/AI_AGENT_QUICKSTART.md +42 -0
- package/docs/AI_PIPELINE_DESIGN.md +595 -0
- package/docs/API.md +514 -0
- package/docs/ARCHITECTURE.md +460 -0
- package/docs/CONFIGURATION.md +336 -0
- package/docs/DEVELOPMENT.md +508 -0
- package/docs/EXPORT_FORMATS.md +451 -0
- package/docs/GETTING_STARTED.md +236 -0
- package/docs/KEYBOARD_SHORTCUTS.md +334 -0
- package/docs/TROUBLESHOOTING.md +418 -0
- package/docs/landing/index.html +672 -0
- package/docs/landing/script.js +342 -0
- package/docs/landing/styles.css +1543 -0
- package/electron-builder.yml +140 -0
- package/electron.vite.config.ts +63 -0
- package/package.json +108 -0
- package/railway.json +12 -0
- package/scripts/build.mjs +51 -0
- package/scripts/generate-icons.mjs +314 -0
- package/scripts/generate-installer-images.cjs +253 -0
- package/scripts/generate-tray-icons.mjs +258 -0
- package/scripts/notarize.cjs +180 -0
- package/scripts/one-click-clean-test.sh +147 -0
- package/scripts/postinstall.mjs +36 -0
- package/scripts/setup-markupr.sh +55 -0
- package/setup +17 -0
- package/site/index.html +1835 -0
- package/site/package.json +11 -0
- package/site/railway.json +12 -0
- package/site/server.js +31 -0
- package/src/main/AutoUpdater.ts +392 -0
- package/src/main/CrashRecovery.ts +655 -0
- package/src/main/ErrorHandler.ts +703 -0
- package/src/main/HotkeyManager.ts +399 -0
- package/src/main/MenuManager.ts +529 -0
- package/src/main/PermissionManager.ts +420 -0
- package/src/main/SessionController.ts +1465 -0
- package/src/main/TrayManager.ts +540 -0
- package/src/main/ai/AIPipelineManager.ts +199 -0
- package/src/main/ai/ClaudeAnalyzer.ts +339 -0
- package/src/main/ai/ImageOptimizer.ts +176 -0
- package/src/main/ai/StructuredMarkdownBuilder.ts +379 -0
- package/src/main/ai/index.ts +16 -0
- package/src/main/ai/types.ts +258 -0
- package/src/main/analysis/ClarificationGenerator.ts +385 -0
- package/src/main/analysis/FeedbackAnalyzer.ts +531 -0
- package/src/main/analysis/index.ts +19 -0
- package/src/main/audio/AudioCapture.ts +978 -0
- package/src/main/audio/audioUtils.ts +100 -0
- package/src/main/audio/index.ts +20 -0
- package/src/main/capture/index.ts +1 -0
- package/src/main/index.ts +1693 -0
- package/src/main/ipc/captureHandlers.ts +272 -0
- package/src/main/ipc/index.ts +45 -0
- package/src/main/ipc/outputHandlers.ts +302 -0
- package/src/main/ipc/sessionHandlers.ts +56 -0
- package/src/main/ipc/settingsHandlers.ts +471 -0
- package/src/main/ipc/types.ts +56 -0
- package/src/main/ipc/windowHandlers.ts +277 -0
- package/src/main/output/ClipboardService.ts +369 -0
- package/src/main/output/ExportService.ts +539 -0
- package/src/main/output/FileManager.ts +416 -0
- package/src/main/output/MarkdownGenerator.ts +791 -0
- package/src/main/output/MarkdownPatcher.ts +299 -0
- package/src/main/output/index.ts +186 -0
- package/src/main/output/sessionAdapter.ts +207 -0
- package/src/main/output/templates/html-template.ts +553 -0
- package/src/main/pipeline/FrameExtractor.ts +330 -0
- package/src/main/pipeline/PostProcessor.ts +399 -0
- package/src/main/pipeline/TranscriptAnalyzer.ts +226 -0
- package/src/main/pipeline/index.ts +36 -0
- package/src/main/platform/WindowsTaskbar.ts +600 -0
- package/src/main/platform/index.ts +16 -0
- package/src/main/settings/SettingsManager.ts +730 -0
- package/src/main/settings/index.ts +19 -0
- package/src/main/transcription/ModelDownloadManager.ts +494 -0
- package/src/main/transcription/TierManager.ts +219 -0
- package/src/main/transcription/TranscriptionRecoveryService.ts +340 -0
- package/src/main/transcription/WhisperService.ts +748 -0
- package/src/main/transcription/index.ts +56 -0
- package/src/main/transcription/types.ts +135 -0
- package/src/main/windows/PopoverManager.ts +284 -0
- package/src/main/windows/TaskbarIntegration.ts +452 -0
- package/src/main/windows/index.ts +23 -0
- package/src/preload/index.ts +1047 -0
- package/src/renderer/App.tsx +515 -0
- package/src/renderer/AppWrapper.tsx +28 -0
- package/src/renderer/assets/logo-dark.svg +7 -0
- package/src/renderer/assets/logo.svg +7 -0
- package/src/renderer/audio/AudioCaptureRenderer.ts +454 -0
- package/src/renderer/capture/ScreenRecordingRenderer.ts +492 -0
- package/src/renderer/components/AnnotationOverlay.tsx +836 -0
- package/src/renderer/components/AudioWaveform.tsx +811 -0
- package/src/renderer/components/ClarificationQuestions.tsx +656 -0
- package/src/renderer/components/CountdownTimer.tsx +495 -0
- package/src/renderer/components/CrashRecoveryDialog.tsx +632 -0
- package/src/renderer/components/DonateButton.tsx +127 -0
- package/src/renderer/components/ErrorBoundary.tsx +308 -0
- package/src/renderer/components/ExportDialog.tsx +872 -0
- package/src/renderer/components/HotkeyHint.tsx +261 -0
- package/src/renderer/components/KeyboardShortcuts.tsx +787 -0
- package/src/renderer/components/ModelDownloadDialog.tsx +844 -0
- package/src/renderer/components/Onboarding.tsx +1830 -0
- package/src/renderer/components/ProcessingOverlay.tsx +157 -0
- package/src/renderer/components/RecordingOverlay.tsx +423 -0
- package/src/renderer/components/SessionHistory.tsx +1746 -0
- package/src/renderer/components/SessionReview.tsx +1321 -0
- package/src/renderer/components/SettingsPanel.tsx +217 -0
- package/src/renderer/components/Skeleton.tsx +347 -0
- package/src/renderer/components/StatusIndicator.tsx +86 -0
- package/src/renderer/components/ThemeProvider.tsx +429 -0
- package/src/renderer/components/Tooltip.tsx +370 -0
- package/src/renderer/components/TranscriptionPreview.tsx +183 -0
- package/src/renderer/components/TranscriptionTierSelector.tsx +640 -0
- package/src/renderer/components/UpdateNotification.tsx +377 -0
- package/src/renderer/components/WindowSelector.tsx +947 -0
- package/src/renderer/components/index.ts +99 -0
- package/src/renderer/components/primitives/ApiKeyInput.tsx +98 -0
- package/src/renderer/components/primitives/ColorPicker.tsx +65 -0
- package/src/renderer/components/primitives/DangerButton.tsx +45 -0
- package/src/renderer/components/primitives/DirectoryPicker.tsx +41 -0
- package/src/renderer/components/primitives/Dropdown.tsx +34 -0
- package/src/renderer/components/primitives/KeyRecorder.tsx +117 -0
- package/src/renderer/components/primitives/SettingsSection.tsx +32 -0
- package/src/renderer/components/primitives/Slider.tsx +43 -0
- package/src/renderer/components/primitives/Toggle.tsx +36 -0
- package/src/renderer/components/primitives/index.ts +10 -0
- package/src/renderer/components/settings/AdvancedTab.tsx +174 -0
- package/src/renderer/components/settings/AppearanceTab.tsx +77 -0
- package/src/renderer/components/settings/GeneralTab.tsx +40 -0
- package/src/renderer/components/settings/HotkeysTab.tsx +79 -0
- package/src/renderer/components/settings/RecordingTab.tsx +84 -0
- package/src/renderer/components/settings/index.ts +9 -0
- package/src/renderer/components/settings/settingsStyles.ts +673 -0
- package/src/renderer/components/settings/tabConfig.tsx +85 -0
- package/src/renderer/components/settings/useSettingsPanel.ts +447 -0
- package/src/renderer/contexts/ProcessingContext.tsx +227 -0
- package/src/renderer/contexts/RecordingContext.tsx +683 -0
- package/src/renderer/contexts/UIContext.tsx +326 -0
- package/src/renderer/contexts/index.ts +24 -0
- package/src/renderer/donateMessages.ts +69 -0
- package/src/renderer/hooks/index.ts +75 -0
- package/src/renderer/hooks/useAnimation.tsx +544 -0
- package/src/renderer/hooks/useTheme.ts +313 -0
- package/src/renderer/index.html +26 -0
- package/src/renderer/main.tsx +52 -0
- package/src/renderer/styles/animations.css +1093 -0
- package/src/renderer/styles/app-shell.css +662 -0
- package/src/renderer/styles/globals.css +515 -0
- package/src/renderer/styles/theme.ts +578 -0
- package/src/renderer/types/electron.d.ts +385 -0
- package/src/shared/hotkeys.ts +283 -0
- package/src/shared/types.ts +809 -0
- package/tests/clipboard.test.ts +228 -0
- package/tests/e2e/criticalPaths.test.ts +594 -0
- package/tests/feedbackAnalyzer.test.ts +303 -0
- package/tests/integration/sessionFlow.test.ts +583 -0
- package/tests/markdownGenerator.test.ts +418 -0
- package/tests/output.test.ts +96 -0
- package/tests/setup.ts +486 -0
- package/tests/unit/appIntegration.test.ts +676 -0
- package/tests/unit/appViewState.test.ts +281 -0
- package/tests/unit/audioIpcChannels.test.ts +17 -0
- package/tests/unit/exportService.test.ts +492 -0
- package/tests/unit/hotkeys.test.ts +92 -0
- package/tests/unit/navigationPreload.test.ts +94 -0
- package/tests/unit/onboardingFlow.test.ts +345 -0
- package/tests/unit/permissionManager.test.ts +175 -0
- package/tests/unit/permissionManagerExpanded.test.ts +296 -0
- package/tests/unit/screenRecordingRenderer.test.ts +368 -0
- package/tests/unit/sessionController.test.ts +515 -0
- package/tests/unit/tierManager.test.ts +61 -0
- package/tests/unit/tierManagerExpanded.test.ts +142 -0
- package/tests/unit/transcriptAnalyzer.test.ts +64 -0
- package/tsconfig.json +25 -0
- package/vitest.config.ts +46 -0
|
@@ -0,0 +1,539 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ExportService - Multi-Format Export for markupr
|
|
3
|
+
*
|
|
4
|
+
* Supports:
|
|
5
|
+
* - Markdown (default, via MarkdownGenerator)
|
|
6
|
+
* - PDF (using Electron's built-in PDF rendering)
|
|
7
|
+
* - HTML (standalone, self-contained)
|
|
8
|
+
* - JSON (machine-readable for integrations)
|
|
9
|
+
*
|
|
10
|
+
* Design principles:
|
|
11
|
+
* - Each format should feel intentional and polished
|
|
12
|
+
* - HTML and PDF should be visually beautiful
|
|
13
|
+
* - JSON should be comprehensive and well-structured
|
|
14
|
+
* - All formats support embedded or referenced images
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { BrowserWindow, app } from 'electron';
|
|
18
|
+
import * as fs from 'fs/promises';
|
|
19
|
+
import * as path from 'path';
|
|
20
|
+
import type { Session, FeedbackItem, FeedbackCategory, FeedbackSeverity } from './MarkdownGenerator';
|
|
21
|
+
import { markdownGenerator } from './MarkdownGenerator';
|
|
22
|
+
import type { PostProcessResult } from '../pipeline/PostProcessor';
|
|
23
|
+
import { generateHtmlDocument } from './templates/html-template';
|
|
24
|
+
|
|
25
|
+
// ============================================================================
|
|
26
|
+
// Types
|
|
27
|
+
// ============================================================================
|
|
28
|
+
|
|
29
|
+
export type ExportFormat = 'markdown' | 'pdf' | 'html' | 'json';
|
|
30
|
+
|
|
31
|
+
export interface ExportOptions {
|
|
32
|
+
format: ExportFormat;
|
|
33
|
+
outputPath: string;
|
|
34
|
+
projectName?: string;
|
|
35
|
+
includeImages?: boolean;
|
|
36
|
+
theme?: 'dark' | 'light';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface PdfOptions extends Omit<ExportOptions, 'format'> {
|
|
40
|
+
format: 'pdf';
|
|
41
|
+
pageSize?: 'A4' | 'Letter' | 'Legal';
|
|
42
|
+
landscape?: boolean;
|
|
43
|
+
margins?: {
|
|
44
|
+
top?: number;
|
|
45
|
+
bottom?: number;
|
|
46
|
+
left?: number;
|
|
47
|
+
right?: number;
|
|
48
|
+
};
|
|
49
|
+
printBackground?: boolean;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface HtmlOptions extends Omit<ExportOptions, 'format'> {
|
|
53
|
+
format: 'html';
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface JsonOptions extends Omit<ExportOptions, 'format'> {
|
|
57
|
+
format: 'json';
|
|
58
|
+
includeBase64Images?: boolean;
|
|
59
|
+
pretty?: boolean;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface MarkdownOptions extends Omit<ExportOptions, 'format'> {
|
|
63
|
+
format: 'markdown';
|
|
64
|
+
screenshotDir?: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface ExportResult {
|
|
68
|
+
success: boolean;
|
|
69
|
+
format: ExportFormat;
|
|
70
|
+
outputPath: string;
|
|
71
|
+
fileSize?: number;
|
|
72
|
+
error?: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface JsonExportSchema {
|
|
76
|
+
version: string;
|
|
77
|
+
generator: string;
|
|
78
|
+
exportedAt: string;
|
|
79
|
+
session: {
|
|
80
|
+
id: string;
|
|
81
|
+
startTime: number;
|
|
82
|
+
endTime?: number;
|
|
83
|
+
source: {
|
|
84
|
+
name?: string;
|
|
85
|
+
type?: string;
|
|
86
|
+
os?: string;
|
|
87
|
+
};
|
|
88
|
+
items: Array<{
|
|
89
|
+
id: string;
|
|
90
|
+
index: number;
|
|
91
|
+
timestamp: number;
|
|
92
|
+
transcription: string;
|
|
93
|
+
category: FeedbackCategory | null;
|
|
94
|
+
severity: FeedbackSeverity | null;
|
|
95
|
+
screenshots: Array<{
|
|
96
|
+
id: string;
|
|
97
|
+
width: number;
|
|
98
|
+
height: number;
|
|
99
|
+
base64?: string;
|
|
100
|
+
}>;
|
|
101
|
+
}>;
|
|
102
|
+
};
|
|
103
|
+
summary: {
|
|
104
|
+
itemCount: number;
|
|
105
|
+
screenshotCount: number;
|
|
106
|
+
duration: number;
|
|
107
|
+
categories: Record<string, number>;
|
|
108
|
+
severities: Record<string, number>;
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ============================================================================
|
|
113
|
+
// Export Service Class
|
|
114
|
+
// ============================================================================
|
|
115
|
+
|
|
116
|
+
class ExportServiceImpl {
|
|
117
|
+
/**
|
|
118
|
+
* Export a session to the specified format
|
|
119
|
+
*/
|
|
120
|
+
async export(session: Session, options: ExportOptions): Promise<ExportResult> {
|
|
121
|
+
try {
|
|
122
|
+
switch (options.format) {
|
|
123
|
+
case 'pdf':
|
|
124
|
+
return await this.exportToPdf(session, options as PdfOptions);
|
|
125
|
+
case 'html':
|
|
126
|
+
return await this.exportToHtml(session, options as HtmlOptions);
|
|
127
|
+
case 'json':
|
|
128
|
+
return await this.exportToJson(session, options as JsonOptions);
|
|
129
|
+
case 'markdown':
|
|
130
|
+
default:
|
|
131
|
+
return await this.exportToMarkdown(session, options as MarkdownOptions);
|
|
132
|
+
}
|
|
133
|
+
} catch (error) {
|
|
134
|
+
console.error(`[ExportService] Export failed:`, error);
|
|
135
|
+
return {
|
|
136
|
+
success: false,
|
|
137
|
+
format: options.format,
|
|
138
|
+
outputPath: options.outputPath,
|
|
139
|
+
error: error instanceof Error ? error.message : String(error),
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Export to PDF using Electron's built-in PDF rendering
|
|
146
|
+
*
|
|
147
|
+
* Creates a hidden BrowserWindow, loads the HTML, and prints to PDF.
|
|
148
|
+
* This approach ensures consistent rendering without external dependencies.
|
|
149
|
+
*/
|
|
150
|
+
async exportToPdf(session: Session, options: PdfOptions): Promise<ExportResult> {
|
|
151
|
+
const {
|
|
152
|
+
outputPath,
|
|
153
|
+
projectName,
|
|
154
|
+
includeImages = true,
|
|
155
|
+
theme = 'dark',
|
|
156
|
+
pageSize = 'A4',
|
|
157
|
+
landscape = false,
|
|
158
|
+
margins = { top: 72, bottom: 72, left: 72, right: 72 }, // 1 inch in points
|
|
159
|
+
printBackground = true,
|
|
160
|
+
} = options;
|
|
161
|
+
|
|
162
|
+
// Generate HTML content
|
|
163
|
+
const htmlContent = generateHtmlDocument(session, {
|
|
164
|
+
projectName,
|
|
165
|
+
includeImages,
|
|
166
|
+
theme,
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// Create a hidden browser window for PDF rendering
|
|
170
|
+
const pdfWindow = new BrowserWindow({
|
|
171
|
+
show: false,
|
|
172
|
+
width: 1200,
|
|
173
|
+
height: 800,
|
|
174
|
+
webPreferences: {
|
|
175
|
+
nodeIntegration: false,
|
|
176
|
+
contextIsolation: true,
|
|
177
|
+
offscreen: true,
|
|
178
|
+
},
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
try {
|
|
182
|
+
// Load the HTML content
|
|
183
|
+
await pdfWindow.loadURL(`data:text/html;charset=utf-8,${encodeURIComponent(htmlContent)}`);
|
|
184
|
+
|
|
185
|
+
// Wait for content to fully render
|
|
186
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
187
|
+
|
|
188
|
+
// Generate PDF
|
|
189
|
+
const pdfBuffer = await pdfWindow.webContents.printToPDF({
|
|
190
|
+
pageSize,
|
|
191
|
+
landscape,
|
|
192
|
+
printBackground,
|
|
193
|
+
margins: {
|
|
194
|
+
top: (margins.top ?? 72) / 72, // Convert points to inches
|
|
195
|
+
bottom: (margins.bottom ?? 72) / 72,
|
|
196
|
+
left: (margins.left ?? 72) / 72,
|
|
197
|
+
right: (margins.right ?? 72) / 72,
|
|
198
|
+
},
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
// Ensure output directory exists
|
|
202
|
+
await fs.mkdir(path.dirname(outputPath), { recursive: true });
|
|
203
|
+
|
|
204
|
+
// Write PDF file
|
|
205
|
+
await fs.writeFile(outputPath, pdfBuffer);
|
|
206
|
+
|
|
207
|
+
const stats = await fs.stat(outputPath);
|
|
208
|
+
|
|
209
|
+
console.log(`[ExportService] PDF exported to ${outputPath} (${stats.size} bytes)`);
|
|
210
|
+
|
|
211
|
+
return {
|
|
212
|
+
success: true,
|
|
213
|
+
format: 'pdf',
|
|
214
|
+
outputPath,
|
|
215
|
+
fileSize: stats.size,
|
|
216
|
+
};
|
|
217
|
+
} finally {
|
|
218
|
+
// Always close the window
|
|
219
|
+
pdfWindow.destroy();
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Export to standalone HTML
|
|
225
|
+
*
|
|
226
|
+
* Creates a self-contained HTML file with embedded styles and images.
|
|
227
|
+
* No external dependencies required.
|
|
228
|
+
*/
|
|
229
|
+
async exportToHtml(session: Session, options: HtmlOptions): Promise<ExportResult> {
|
|
230
|
+
const { outputPath, projectName, includeImages = true, theme = 'dark' } = options;
|
|
231
|
+
|
|
232
|
+
const htmlContent = generateHtmlDocument(session, {
|
|
233
|
+
projectName,
|
|
234
|
+
includeImages,
|
|
235
|
+
theme,
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
// Ensure output directory exists
|
|
239
|
+
await fs.mkdir(path.dirname(outputPath), { recursive: true });
|
|
240
|
+
|
|
241
|
+
// Write HTML file
|
|
242
|
+
await fs.writeFile(outputPath, htmlContent, 'utf-8');
|
|
243
|
+
|
|
244
|
+
const stats = await fs.stat(outputPath);
|
|
245
|
+
|
|
246
|
+
console.log(`[ExportService] HTML exported to ${outputPath} (${stats.size} bytes)`);
|
|
247
|
+
|
|
248
|
+
return {
|
|
249
|
+
success: true,
|
|
250
|
+
format: 'html',
|
|
251
|
+
outputPath,
|
|
252
|
+
fileSize: stats.size,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Export to JSON
|
|
258
|
+
*
|
|
259
|
+
* Machine-readable format suitable for:
|
|
260
|
+
* - Integration with other tools
|
|
261
|
+
* - Data analysis
|
|
262
|
+
* - Backup/restore
|
|
263
|
+
* - API consumption
|
|
264
|
+
*/
|
|
265
|
+
async exportToJson(session: Session, options: JsonOptions): Promise<ExportResult> {
|
|
266
|
+
const { outputPath, includeBase64Images = false, pretty = true } = options;
|
|
267
|
+
|
|
268
|
+
const jsonData = this.generateJsonExport(session, includeBase64Images);
|
|
269
|
+
const jsonString = pretty ? JSON.stringify(jsonData, null, 2) : JSON.stringify(jsonData);
|
|
270
|
+
|
|
271
|
+
// Ensure output directory exists
|
|
272
|
+
await fs.mkdir(path.dirname(outputPath), { recursive: true });
|
|
273
|
+
|
|
274
|
+
// Write JSON file
|
|
275
|
+
await fs.writeFile(outputPath, jsonString, 'utf-8');
|
|
276
|
+
|
|
277
|
+
const stats = await fs.stat(outputPath);
|
|
278
|
+
|
|
279
|
+
console.log(`[ExportService] JSON exported to ${outputPath} (${stats.size} bytes)`);
|
|
280
|
+
|
|
281
|
+
return {
|
|
282
|
+
success: true,
|
|
283
|
+
format: 'json',
|
|
284
|
+
outputPath,
|
|
285
|
+
fileSize: stats.size,
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Export to Markdown
|
|
291
|
+
*
|
|
292
|
+
* Uses the existing MarkdownGenerator for consistent output.
|
|
293
|
+
*/
|
|
294
|
+
async exportToMarkdown(session: Session, options: MarkdownOptions): Promise<ExportResult> {
|
|
295
|
+
const { outputPath, projectName, screenshotDir = './screenshots' } = options;
|
|
296
|
+
|
|
297
|
+
const document = markdownGenerator.generateFullDocument(session, {
|
|
298
|
+
projectName: projectName || session.metadata?.sourceName || 'Feedback Report',
|
|
299
|
+
screenshotDir,
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
// Ensure output directory exists
|
|
303
|
+
await fs.mkdir(path.dirname(outputPath), { recursive: true });
|
|
304
|
+
|
|
305
|
+
// Write Markdown file
|
|
306
|
+
await fs.writeFile(outputPath, document.content, 'utf-8');
|
|
307
|
+
|
|
308
|
+
const stats = await fs.stat(outputPath);
|
|
309
|
+
|
|
310
|
+
console.log(`[ExportService] Markdown exported to ${outputPath} (${stats.size} bytes)`);
|
|
311
|
+
|
|
312
|
+
return {
|
|
313
|
+
success: true,
|
|
314
|
+
format: 'markdown',
|
|
315
|
+
outputPath,
|
|
316
|
+
fileSize: stats.size,
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Export a PostProcessResult to Markdown.
|
|
322
|
+
*
|
|
323
|
+
* Uses the new generateFromPostProcess method on MarkdownGenerator
|
|
324
|
+
* to produce a clean transcript + frame document.
|
|
325
|
+
*
|
|
326
|
+
* @param result - PostProcessResult from the post-recording pipeline
|
|
327
|
+
* @param sessionDir - Absolute path to the session directory
|
|
328
|
+
* @param outputPath - Where to write the markdown file
|
|
329
|
+
*/
|
|
330
|
+
async exportPostProcessToMarkdown(
|
|
331
|
+
result: PostProcessResult,
|
|
332
|
+
sessionDir: string,
|
|
333
|
+
outputPath: string
|
|
334
|
+
): Promise<ExportResult> {
|
|
335
|
+
try {
|
|
336
|
+
const content = markdownGenerator.generateFromPostProcess(result, sessionDir);
|
|
337
|
+
|
|
338
|
+
// Ensure output directory exists
|
|
339
|
+
await fs.mkdir(path.dirname(outputPath), { recursive: true });
|
|
340
|
+
|
|
341
|
+
// Write Markdown file
|
|
342
|
+
await fs.writeFile(outputPath, content, 'utf-8');
|
|
343
|
+
|
|
344
|
+
const stats = await fs.stat(outputPath);
|
|
345
|
+
|
|
346
|
+
console.log(`[ExportService] Post-process Markdown exported to ${outputPath} (${stats.size} bytes)`);
|
|
347
|
+
|
|
348
|
+
return {
|
|
349
|
+
success: true,
|
|
350
|
+
format: 'markdown',
|
|
351
|
+
outputPath,
|
|
352
|
+
fileSize: stats.size,
|
|
353
|
+
};
|
|
354
|
+
} catch (error) {
|
|
355
|
+
console.error(`[ExportService] Post-process Markdown export failed:`, error);
|
|
356
|
+
return {
|
|
357
|
+
success: false,
|
|
358
|
+
format: 'markdown',
|
|
359
|
+
outputPath,
|
|
360
|
+
error: error instanceof Error ? error.message : String(error),
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Generate JSON export data structure
|
|
367
|
+
*/
|
|
368
|
+
generateJsonExport(session: Session, includeBase64Images: boolean): JsonExportSchema {
|
|
369
|
+
const categories = this.countByCategory(session.feedbackItems);
|
|
370
|
+
const severities = this.countBySeverity(session.feedbackItems);
|
|
371
|
+
const screenshotCount = session.feedbackItems.reduce(
|
|
372
|
+
(sum, item) => sum + item.screenshots.length,
|
|
373
|
+
0
|
|
374
|
+
);
|
|
375
|
+
const duration = session.endTime ? session.endTime - session.startTime : 0;
|
|
376
|
+
|
|
377
|
+
return {
|
|
378
|
+
version: '1.0',
|
|
379
|
+
generator: `markupr v${app.getVersion()}`,
|
|
380
|
+
exportedAt: new Date().toISOString(),
|
|
381
|
+
session: {
|
|
382
|
+
id: session.id,
|
|
383
|
+
startTime: session.startTime,
|
|
384
|
+
endTime: session.endTime,
|
|
385
|
+
source: {
|
|
386
|
+
name: session.metadata?.sourceName,
|
|
387
|
+
type: session.metadata?.sourceType,
|
|
388
|
+
os: session.metadata?.os,
|
|
389
|
+
},
|
|
390
|
+
items: session.feedbackItems.map((item, index) => ({
|
|
391
|
+
id: item.id,
|
|
392
|
+
index,
|
|
393
|
+
timestamp: item.timestamp,
|
|
394
|
+
transcription: item.transcription,
|
|
395
|
+
category: item.category || null,
|
|
396
|
+
severity: item.severity || null,
|
|
397
|
+
screenshots: item.screenshots.map((ss) => ({
|
|
398
|
+
id: ss.id,
|
|
399
|
+
width: ss.width,
|
|
400
|
+
height: ss.height,
|
|
401
|
+
...(includeBase64Images && ss.base64 ? { base64: ss.base64 } : {}),
|
|
402
|
+
})),
|
|
403
|
+
})),
|
|
404
|
+
},
|
|
405
|
+
summary: {
|
|
406
|
+
itemCount: session.feedbackItems.length,
|
|
407
|
+
screenshotCount,
|
|
408
|
+
duration,
|
|
409
|
+
categories,
|
|
410
|
+
severities,
|
|
411
|
+
},
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Get a preview of what the export will look like (for HTML/Markdown)
|
|
417
|
+
*/
|
|
418
|
+
getPreview(session: Session, format: ExportFormat, options: Partial<ExportOptions> = {}): string {
|
|
419
|
+
switch (format) {
|
|
420
|
+
case 'html':
|
|
421
|
+
return generateHtmlDocument(session, {
|
|
422
|
+
projectName: options.projectName,
|
|
423
|
+
includeImages: options.includeImages ?? false, // Don't include images in preview
|
|
424
|
+
theme: options.theme ?? 'dark',
|
|
425
|
+
});
|
|
426
|
+
case 'json':
|
|
427
|
+
return JSON.stringify(this.generateJsonExport(session, false), null, 2);
|
|
428
|
+
case 'markdown':
|
|
429
|
+
default:
|
|
430
|
+
return markdownGenerator.generateFullDocument(session, {
|
|
431
|
+
projectName: options.projectName || session.metadata?.sourceName || 'Preview',
|
|
432
|
+
screenshotDir: './screenshots',
|
|
433
|
+
}).content;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Get suggested filename for a given format
|
|
439
|
+
*/
|
|
440
|
+
getSuggestedFilename(session: Session, format: ExportFormat, projectName?: string): string {
|
|
441
|
+
const name = (projectName || session.metadata?.sourceName || 'feedback')
|
|
442
|
+
.toLowerCase()
|
|
443
|
+
.replace(/[^a-z0-9]/g, '-')
|
|
444
|
+
.replace(/-+/g, '-')
|
|
445
|
+
.replace(/^-+|-+$/g, '');
|
|
446
|
+
|
|
447
|
+
const date = new Date(session.startTime);
|
|
448
|
+
const dateStr = [
|
|
449
|
+
date.getFullYear(),
|
|
450
|
+
String(date.getMonth() + 1).padStart(2, '0'),
|
|
451
|
+
String(date.getDate()).padStart(2, '0'),
|
|
452
|
+
].join('');
|
|
453
|
+
const timeStr = [
|
|
454
|
+
String(date.getHours()).padStart(2, '0'),
|
|
455
|
+
String(date.getMinutes()).padStart(2, '0'),
|
|
456
|
+
].join('');
|
|
457
|
+
|
|
458
|
+
const extensions: Record<ExportFormat, string> = {
|
|
459
|
+
markdown: 'md',
|
|
460
|
+
pdf: 'pdf',
|
|
461
|
+
html: 'html',
|
|
462
|
+
json: 'json',
|
|
463
|
+
};
|
|
464
|
+
|
|
465
|
+
return `${name}-feedback-${dateStr}-${timeStr}.${extensions[format]}`;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* Get format info for UI display
|
|
470
|
+
*/
|
|
471
|
+
getFormatInfo(format: ExportFormat): {
|
|
472
|
+
name: string;
|
|
473
|
+
description: string;
|
|
474
|
+
icon: string;
|
|
475
|
+
extension: string;
|
|
476
|
+
} {
|
|
477
|
+
const info: Record<
|
|
478
|
+
ExportFormat,
|
|
479
|
+
{ name: string; description: string; icon: string; extension: string }
|
|
480
|
+
> = {
|
|
481
|
+
markdown: {
|
|
482
|
+
name: 'Markdown',
|
|
483
|
+
description: 'AI-ready format for Claude, ChatGPT, and other assistants',
|
|
484
|
+
icon: 'document-text',
|
|
485
|
+
extension: '.md',
|
|
486
|
+
},
|
|
487
|
+
pdf: {
|
|
488
|
+
name: 'PDF',
|
|
489
|
+
description: 'Beautiful document for sharing and printing',
|
|
490
|
+
icon: 'document',
|
|
491
|
+
extension: '.pdf',
|
|
492
|
+
},
|
|
493
|
+
html: {
|
|
494
|
+
name: 'HTML',
|
|
495
|
+
description: 'Standalone web page, no dependencies',
|
|
496
|
+
icon: 'code-bracket',
|
|
497
|
+
extension: '.html',
|
|
498
|
+
},
|
|
499
|
+
json: {
|
|
500
|
+
name: 'JSON',
|
|
501
|
+
description: 'Machine-readable for integrations and APIs',
|
|
502
|
+
icon: 'code-bracket-square',
|
|
503
|
+
extension: '.json',
|
|
504
|
+
},
|
|
505
|
+
};
|
|
506
|
+
|
|
507
|
+
return info[format];
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// ===========================================================================
|
|
511
|
+
// Private Helpers
|
|
512
|
+
// ===========================================================================
|
|
513
|
+
|
|
514
|
+
private countByCategory(items: FeedbackItem[]): Record<string, number> {
|
|
515
|
+
return items.reduce((acc, item) => {
|
|
516
|
+
const category = item.category || 'General';
|
|
517
|
+
acc[category] = (acc[category] || 0) + 1;
|
|
518
|
+
return acc;
|
|
519
|
+
}, {} as Record<string, number>);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
private countBySeverity(items: FeedbackItem[]): Record<string, number> {
|
|
523
|
+
return items.reduce((acc, item) => {
|
|
524
|
+
const severity = item.severity || 'Medium';
|
|
525
|
+
acc[severity] = (acc[severity] || 0) + 1;
|
|
526
|
+
return acc;
|
|
527
|
+
}, {} as Record<string, number>);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// ============================================================================
|
|
532
|
+
// Exports
|
|
533
|
+
// ============================================================================
|
|
534
|
+
|
|
535
|
+
export const exportService = new ExportServiceImpl();
|
|
536
|
+
export { ExportServiceImpl as ExportService };
|
|
537
|
+
|
|
538
|
+
// Re-export HTML template for direct use
|
|
539
|
+
export { generateHtmlDocument, type HtmlExportOptions } from './templates/html-template';
|