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.
Files changed (299) hide show
  1. package/.claude/commands/review-feedback.md +47 -0
  2. package/.eslintrc.json +35 -0
  3. package/.github/CODEOWNERS +16 -0
  4. package/.github/FUNDING.yml +1 -0
  5. package/.github/ISSUE_TEMPLATE/bug_report.md +56 -0
  6. package/.github/ISSUE_TEMPLATE/feature_request.md +54 -0
  7. package/.github/PULL_REQUEST_TEMPLATE.md +89 -0
  8. package/.github/dependabot.yml +70 -0
  9. package/.github/workflows/ci.yml +184 -0
  10. package/.github/workflows/deploy-landing.yml +134 -0
  11. package/.github/workflows/nightly.yml +288 -0
  12. package/.github/workflows/release.yml +318 -0
  13. package/CHANGELOG.md +127 -0
  14. package/CLAUDE.md +137 -0
  15. package/CODE_OF_CONDUCT.md +9 -0
  16. package/CONTRIBUTING.md +390 -0
  17. package/LICENSE +21 -0
  18. package/PRODUCT_VISION.md +277 -0
  19. package/README.md +517 -0
  20. package/SECURITY.md +51 -0
  21. package/SIGNING_INSTRUCTIONS.md +284 -0
  22. package/assets/DMG_BACKGROUND_INSTRUCTIONS.md +130 -0
  23. package/assets/svg-source/dmg-background.svg +70 -0
  24. package/assets/svg-source/icon.svg +20 -0
  25. package/assets/svg-source/tray-icon-processing.svg +7 -0
  26. package/assets/svg-source/tray-icon-recording.svg +7 -0
  27. package/assets/svg-source/tray-icon.svg +6 -0
  28. package/assets/tray-complete.png +0 -0
  29. package/assets/tray-complete@2x.png +0 -0
  30. package/assets/tray-completeTemplate.png +0 -0
  31. package/assets/tray-completeTemplate@2x.png +0 -0
  32. package/assets/tray-error.png +0 -0
  33. package/assets/tray-error@2x.png +0 -0
  34. package/assets/tray-errorTemplate.png +0 -0
  35. package/assets/tray-errorTemplate@2x.png +0 -0
  36. package/assets/tray-icon-processing.png +0 -0
  37. package/assets/tray-icon-processing@2x.png +0 -0
  38. package/assets/tray-icon-processingTemplate.png +0 -0
  39. package/assets/tray-icon-processingTemplate@2x.png +0 -0
  40. package/assets/tray-icon-recording.png +0 -0
  41. package/assets/tray-icon-recording@2x.png +0 -0
  42. package/assets/tray-icon-recordingTemplate.png +0 -0
  43. package/assets/tray-icon-recordingTemplate@2x.png +0 -0
  44. package/assets/tray-icon.png +0 -0
  45. package/assets/tray-icon@2x.png +0 -0
  46. package/assets/tray-iconTemplate.png +0 -0
  47. package/assets/tray-iconTemplate@2x.png +0 -0
  48. package/assets/tray-idle.png +0 -0
  49. package/assets/tray-idle@2x.png +0 -0
  50. package/assets/tray-idleTemplate.png +0 -0
  51. package/assets/tray-idleTemplate@2x.png +0 -0
  52. package/assets/tray-processing-0.png +0 -0
  53. package/assets/tray-processing-0@2x.png +0 -0
  54. package/assets/tray-processing-0Template.png +0 -0
  55. package/assets/tray-processing-0Template@2x.png +0 -0
  56. package/assets/tray-processing-1.png +0 -0
  57. package/assets/tray-processing-1@2x.png +0 -0
  58. package/assets/tray-processing-1Template.png +0 -0
  59. package/assets/tray-processing-1Template@2x.png +0 -0
  60. package/assets/tray-processing-2.png +0 -0
  61. package/assets/tray-processing-2@2x.png +0 -0
  62. package/assets/tray-processing-2Template.png +0 -0
  63. package/assets/tray-processing-2Template@2x.png +0 -0
  64. package/assets/tray-processing-3.png +0 -0
  65. package/assets/tray-processing-3@2x.png +0 -0
  66. package/assets/tray-processing-3Template.png +0 -0
  67. package/assets/tray-processing-3Template@2x.png +0 -0
  68. package/assets/tray-processing.png +0 -0
  69. package/assets/tray-processing@2x.png +0 -0
  70. package/assets/tray-processingTemplate.png +0 -0
  71. package/assets/tray-processingTemplate@2x.png +0 -0
  72. package/assets/tray-recording.png +0 -0
  73. package/assets/tray-recording@2x.png +0 -0
  74. package/assets/tray-recordingTemplate.png +0 -0
  75. package/assets/tray-recordingTemplate@2x.png +0 -0
  76. package/build/DMG_BACKGROUND_SPEC.md +50 -0
  77. package/build/dmg-background.png +0 -0
  78. package/build/dmg-background@2x.png +0 -0
  79. package/build/entitlements.mac.inherit.plist +27 -0
  80. package/build/entitlements.mac.plist +41 -0
  81. package/build/favicon-16.png +0 -0
  82. package/build/favicon-180.png +0 -0
  83. package/build/favicon-192.png +0 -0
  84. package/build/favicon-32.png +0 -0
  85. package/build/favicon-48.png +0 -0
  86. package/build/favicon-512.png +0 -0
  87. package/build/favicon-64.png +0 -0
  88. package/build/icon-128.png +0 -0
  89. package/build/icon-16.png +0 -0
  90. package/build/icon-24.png +0 -0
  91. package/build/icon-256.png +0 -0
  92. package/build/icon-32.png +0 -0
  93. package/build/icon-48.png +0 -0
  94. package/build/icon-64.png +0 -0
  95. package/build/icon.icns +0 -0
  96. package/build/icon.ico +0 -0
  97. package/build/icon.iconset/icon_128x128.png +0 -0
  98. package/build/icon.iconset/icon_128x128@2x.png +0 -0
  99. package/build/icon.iconset/icon_16x16.png +0 -0
  100. package/build/icon.iconset/icon_16x16@2x.png +0 -0
  101. package/build/icon.iconset/icon_256x256.png +0 -0
  102. package/build/icon.iconset/icon_256x256@2x.png +0 -0
  103. package/build/icon.iconset/icon_32x32.png +0 -0
  104. package/build/icon.iconset/icon_32x32@2x.png +0 -0
  105. package/build/icon.iconset/icon_512x512.png +0 -0
  106. package/build/icon.iconset/icon_512x512@2x.png +0 -0
  107. package/build/icon.png +0 -0
  108. package/build/installer-header.bmp +0 -0
  109. package/build/installer-header.png +0 -0
  110. package/build/installer-sidebar.bmp +0 -0
  111. package/build/installer-sidebar.png +0 -0
  112. package/build/installer.nsh +45 -0
  113. package/build/overlay-processing.png +0 -0
  114. package/build/overlay-recording.png +0 -0
  115. package/build/toolbar-record.png +0 -0
  116. package/build/toolbar-screenshot.png +0 -0
  117. package/build/toolbar-settings.png +0 -0
  118. package/build/toolbar-stop.png +0 -0
  119. package/dist/main/index.mjs +12612 -0
  120. package/dist/preload/index.mjs +907 -0
  121. package/dist/renderer/assets/index-CCmUjl9K.js +19495 -0
  122. package/dist/renderer/assets/index-CUqz_Gs6.css +2270 -0
  123. package/dist/renderer/index.html +27 -0
  124. package/docs/AI_AGENT_QUICKSTART.md +42 -0
  125. package/docs/AI_PIPELINE_DESIGN.md +595 -0
  126. package/docs/API.md +514 -0
  127. package/docs/ARCHITECTURE.md +460 -0
  128. package/docs/CONFIGURATION.md +336 -0
  129. package/docs/DEVELOPMENT.md +508 -0
  130. package/docs/EXPORT_FORMATS.md +451 -0
  131. package/docs/GETTING_STARTED.md +236 -0
  132. package/docs/KEYBOARD_SHORTCUTS.md +334 -0
  133. package/docs/TROUBLESHOOTING.md +418 -0
  134. package/docs/landing/index.html +672 -0
  135. package/docs/landing/script.js +342 -0
  136. package/docs/landing/styles.css +1543 -0
  137. package/electron-builder.yml +140 -0
  138. package/electron.vite.config.ts +63 -0
  139. package/package.json +108 -0
  140. package/railway.json +12 -0
  141. package/scripts/build.mjs +51 -0
  142. package/scripts/generate-icons.mjs +314 -0
  143. package/scripts/generate-installer-images.cjs +253 -0
  144. package/scripts/generate-tray-icons.mjs +258 -0
  145. package/scripts/notarize.cjs +180 -0
  146. package/scripts/one-click-clean-test.sh +147 -0
  147. package/scripts/postinstall.mjs +36 -0
  148. package/scripts/setup-markupr.sh +55 -0
  149. package/setup +17 -0
  150. package/site/index.html +1835 -0
  151. package/site/package.json +11 -0
  152. package/site/railway.json +12 -0
  153. package/site/server.js +31 -0
  154. package/src/main/AutoUpdater.ts +392 -0
  155. package/src/main/CrashRecovery.ts +655 -0
  156. package/src/main/ErrorHandler.ts +703 -0
  157. package/src/main/HotkeyManager.ts +399 -0
  158. package/src/main/MenuManager.ts +529 -0
  159. package/src/main/PermissionManager.ts +420 -0
  160. package/src/main/SessionController.ts +1465 -0
  161. package/src/main/TrayManager.ts +540 -0
  162. package/src/main/ai/AIPipelineManager.ts +199 -0
  163. package/src/main/ai/ClaudeAnalyzer.ts +339 -0
  164. package/src/main/ai/ImageOptimizer.ts +176 -0
  165. package/src/main/ai/StructuredMarkdownBuilder.ts +379 -0
  166. package/src/main/ai/index.ts +16 -0
  167. package/src/main/ai/types.ts +258 -0
  168. package/src/main/analysis/ClarificationGenerator.ts +385 -0
  169. package/src/main/analysis/FeedbackAnalyzer.ts +531 -0
  170. package/src/main/analysis/index.ts +19 -0
  171. package/src/main/audio/AudioCapture.ts +978 -0
  172. package/src/main/audio/audioUtils.ts +100 -0
  173. package/src/main/audio/index.ts +20 -0
  174. package/src/main/capture/index.ts +1 -0
  175. package/src/main/index.ts +1693 -0
  176. package/src/main/ipc/captureHandlers.ts +272 -0
  177. package/src/main/ipc/index.ts +45 -0
  178. package/src/main/ipc/outputHandlers.ts +302 -0
  179. package/src/main/ipc/sessionHandlers.ts +56 -0
  180. package/src/main/ipc/settingsHandlers.ts +471 -0
  181. package/src/main/ipc/types.ts +56 -0
  182. package/src/main/ipc/windowHandlers.ts +277 -0
  183. package/src/main/output/ClipboardService.ts +369 -0
  184. package/src/main/output/ExportService.ts +539 -0
  185. package/src/main/output/FileManager.ts +416 -0
  186. package/src/main/output/MarkdownGenerator.ts +791 -0
  187. package/src/main/output/MarkdownPatcher.ts +299 -0
  188. package/src/main/output/index.ts +186 -0
  189. package/src/main/output/sessionAdapter.ts +207 -0
  190. package/src/main/output/templates/html-template.ts +553 -0
  191. package/src/main/pipeline/FrameExtractor.ts +330 -0
  192. package/src/main/pipeline/PostProcessor.ts +399 -0
  193. package/src/main/pipeline/TranscriptAnalyzer.ts +226 -0
  194. package/src/main/pipeline/index.ts +36 -0
  195. package/src/main/platform/WindowsTaskbar.ts +600 -0
  196. package/src/main/platform/index.ts +16 -0
  197. package/src/main/settings/SettingsManager.ts +730 -0
  198. package/src/main/settings/index.ts +19 -0
  199. package/src/main/transcription/ModelDownloadManager.ts +494 -0
  200. package/src/main/transcription/TierManager.ts +219 -0
  201. package/src/main/transcription/TranscriptionRecoveryService.ts +340 -0
  202. package/src/main/transcription/WhisperService.ts +748 -0
  203. package/src/main/transcription/index.ts +56 -0
  204. package/src/main/transcription/types.ts +135 -0
  205. package/src/main/windows/PopoverManager.ts +284 -0
  206. package/src/main/windows/TaskbarIntegration.ts +452 -0
  207. package/src/main/windows/index.ts +23 -0
  208. package/src/preload/index.ts +1047 -0
  209. package/src/renderer/App.tsx +515 -0
  210. package/src/renderer/AppWrapper.tsx +28 -0
  211. package/src/renderer/assets/logo-dark.svg +7 -0
  212. package/src/renderer/assets/logo.svg +7 -0
  213. package/src/renderer/audio/AudioCaptureRenderer.ts +454 -0
  214. package/src/renderer/capture/ScreenRecordingRenderer.ts +492 -0
  215. package/src/renderer/components/AnnotationOverlay.tsx +836 -0
  216. package/src/renderer/components/AudioWaveform.tsx +811 -0
  217. package/src/renderer/components/ClarificationQuestions.tsx +656 -0
  218. package/src/renderer/components/CountdownTimer.tsx +495 -0
  219. package/src/renderer/components/CrashRecoveryDialog.tsx +632 -0
  220. package/src/renderer/components/DonateButton.tsx +127 -0
  221. package/src/renderer/components/ErrorBoundary.tsx +308 -0
  222. package/src/renderer/components/ExportDialog.tsx +872 -0
  223. package/src/renderer/components/HotkeyHint.tsx +261 -0
  224. package/src/renderer/components/KeyboardShortcuts.tsx +787 -0
  225. package/src/renderer/components/ModelDownloadDialog.tsx +844 -0
  226. package/src/renderer/components/Onboarding.tsx +1830 -0
  227. package/src/renderer/components/ProcessingOverlay.tsx +157 -0
  228. package/src/renderer/components/RecordingOverlay.tsx +423 -0
  229. package/src/renderer/components/SessionHistory.tsx +1746 -0
  230. package/src/renderer/components/SessionReview.tsx +1321 -0
  231. package/src/renderer/components/SettingsPanel.tsx +217 -0
  232. package/src/renderer/components/Skeleton.tsx +347 -0
  233. package/src/renderer/components/StatusIndicator.tsx +86 -0
  234. package/src/renderer/components/ThemeProvider.tsx +429 -0
  235. package/src/renderer/components/Tooltip.tsx +370 -0
  236. package/src/renderer/components/TranscriptionPreview.tsx +183 -0
  237. package/src/renderer/components/TranscriptionTierSelector.tsx +640 -0
  238. package/src/renderer/components/UpdateNotification.tsx +377 -0
  239. package/src/renderer/components/WindowSelector.tsx +947 -0
  240. package/src/renderer/components/index.ts +99 -0
  241. package/src/renderer/components/primitives/ApiKeyInput.tsx +98 -0
  242. package/src/renderer/components/primitives/ColorPicker.tsx +65 -0
  243. package/src/renderer/components/primitives/DangerButton.tsx +45 -0
  244. package/src/renderer/components/primitives/DirectoryPicker.tsx +41 -0
  245. package/src/renderer/components/primitives/Dropdown.tsx +34 -0
  246. package/src/renderer/components/primitives/KeyRecorder.tsx +117 -0
  247. package/src/renderer/components/primitives/SettingsSection.tsx +32 -0
  248. package/src/renderer/components/primitives/Slider.tsx +43 -0
  249. package/src/renderer/components/primitives/Toggle.tsx +36 -0
  250. package/src/renderer/components/primitives/index.ts +10 -0
  251. package/src/renderer/components/settings/AdvancedTab.tsx +174 -0
  252. package/src/renderer/components/settings/AppearanceTab.tsx +77 -0
  253. package/src/renderer/components/settings/GeneralTab.tsx +40 -0
  254. package/src/renderer/components/settings/HotkeysTab.tsx +79 -0
  255. package/src/renderer/components/settings/RecordingTab.tsx +84 -0
  256. package/src/renderer/components/settings/index.ts +9 -0
  257. package/src/renderer/components/settings/settingsStyles.ts +673 -0
  258. package/src/renderer/components/settings/tabConfig.tsx +85 -0
  259. package/src/renderer/components/settings/useSettingsPanel.ts +447 -0
  260. package/src/renderer/contexts/ProcessingContext.tsx +227 -0
  261. package/src/renderer/contexts/RecordingContext.tsx +683 -0
  262. package/src/renderer/contexts/UIContext.tsx +326 -0
  263. package/src/renderer/contexts/index.ts +24 -0
  264. package/src/renderer/donateMessages.ts +69 -0
  265. package/src/renderer/hooks/index.ts +75 -0
  266. package/src/renderer/hooks/useAnimation.tsx +544 -0
  267. package/src/renderer/hooks/useTheme.ts +313 -0
  268. package/src/renderer/index.html +26 -0
  269. package/src/renderer/main.tsx +52 -0
  270. package/src/renderer/styles/animations.css +1093 -0
  271. package/src/renderer/styles/app-shell.css +662 -0
  272. package/src/renderer/styles/globals.css +515 -0
  273. package/src/renderer/styles/theme.ts +578 -0
  274. package/src/renderer/types/electron.d.ts +385 -0
  275. package/src/shared/hotkeys.ts +283 -0
  276. package/src/shared/types.ts +809 -0
  277. package/tests/clipboard.test.ts +228 -0
  278. package/tests/e2e/criticalPaths.test.ts +594 -0
  279. package/tests/feedbackAnalyzer.test.ts +303 -0
  280. package/tests/integration/sessionFlow.test.ts +583 -0
  281. package/tests/markdownGenerator.test.ts +418 -0
  282. package/tests/output.test.ts +96 -0
  283. package/tests/setup.ts +486 -0
  284. package/tests/unit/appIntegration.test.ts +676 -0
  285. package/tests/unit/appViewState.test.ts +281 -0
  286. package/tests/unit/audioIpcChannels.test.ts +17 -0
  287. package/tests/unit/exportService.test.ts +492 -0
  288. package/tests/unit/hotkeys.test.ts +92 -0
  289. package/tests/unit/navigationPreload.test.ts +94 -0
  290. package/tests/unit/onboardingFlow.test.ts +345 -0
  291. package/tests/unit/permissionManager.test.ts +175 -0
  292. package/tests/unit/permissionManagerExpanded.test.ts +296 -0
  293. package/tests/unit/screenRecordingRenderer.test.ts +368 -0
  294. package/tests/unit/sessionController.test.ts +515 -0
  295. package/tests/unit/tierManager.test.ts +61 -0
  296. package/tests/unit/tierManagerExpanded.test.ts +142 -0
  297. package/tests/unit/transcriptAnalyzer.test.ts +64 -0
  298. package/tsconfig.json +25 -0
  299. 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';