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,85 @@
1
+ import React from 'react';
2
+
3
+ export type SettingsTab = 'general' | 'recording' | 'appearance' | 'hotkeys' | 'advanced';
4
+
5
+ export const TABS: { id: SettingsTab; label: string; icon: React.ReactNode }[] = [
6
+ {
7
+ id: 'general',
8
+ label: 'General',
9
+ icon: (
10
+ <svg width="20" height="20" viewBox="0 0 20 20" fill="none">
11
+ <path
12
+ d="M10 12a2 2 0 100-4 2 2 0 000 4z"
13
+ stroke="currentColor"
14
+ strokeWidth="1.5"
15
+ />
16
+ <path
17
+ d="M16.472 12.111a1.5 1.5 0 00.3 1.655l.054.055a1.818 1.818 0 11-2.572 2.572l-.055-.055a1.5 1.5 0 00-1.655-.3 1.5 1.5 0 00-.909 1.373v.153a1.818 1.818 0 11-3.636 0v-.082a1.5 1.5 0 00-.982-1.371 1.5 1.5 0 00-1.655.3l-.055.054a1.818 1.818 0 11-2.572-2.572l.055-.055a1.5 1.5 0 00.3-1.655 1.5 1.5 0 00-1.373-.909h-.153a1.818 1.818 0 110-3.636h.082a1.5 1.5 0 001.371-.982 1.5 1.5 0 00-.3-1.655l-.054-.055a1.818 1.818 0 112.572-2.572l.055.055a1.5 1.5 0 001.655.3h.073a1.5 1.5 0 00.909-1.373v-.153a1.818 1.818 0 013.636 0v.082a1.5 1.5 0 00.909 1.371 1.5 1.5 0 001.655-.3l.055-.054a1.818 1.818 0 112.572 2.572l-.055.055a1.5 1.5 0 00-.3 1.655v.073a1.5 1.5 0 001.373.909h.153a1.818 1.818 0 010 3.636h-.082a1.5 1.5 0 00-1.371.909z"
18
+ stroke="currentColor"
19
+ strokeWidth="1.5"
20
+ strokeLinecap="round"
21
+ />
22
+ </svg>
23
+ ),
24
+ },
25
+ {
26
+ id: 'recording',
27
+ label: 'Recording',
28
+ icon: (
29
+ <svg width="20" height="20" viewBox="0 0 20 20" fill="none">
30
+ <circle cx="10" cy="10" r="7" stroke="currentColor" strokeWidth="1.5" />
31
+ <circle cx="10" cy="10" r="3" fill="currentColor" />
32
+ </svg>
33
+ ),
34
+ },
35
+ {
36
+ id: 'appearance',
37
+ label: 'Appearance',
38
+ icon: (
39
+ <svg width="20" height="20" viewBox="0 0 20 20" fill="none">
40
+ <path
41
+ d="M3 10a7 7 0 1014 0 7 7 0 00-14 0z"
42
+ stroke="currentColor"
43
+ strokeWidth="1.5"
44
+ />
45
+ <path d="M10 3v14M3 10h7" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
46
+ </svg>
47
+ ),
48
+ },
49
+ {
50
+ id: 'hotkeys',
51
+ label: 'Hotkeys',
52
+ icon: (
53
+ <svg width="20" height="20" viewBox="0 0 20 20" fill="none">
54
+ <rect
55
+ x="2"
56
+ y="5"
57
+ width="16"
58
+ height="10"
59
+ rx="2"
60
+ stroke="currentColor"
61
+ strokeWidth="1.5"
62
+ />
63
+ <path d="M5 8h2M8 8h2M11 8h2M14 8h1" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
64
+ <path d="M6 11h8" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
65
+ </svg>
66
+ ),
67
+ },
68
+ {
69
+ id: 'advanced',
70
+ label: 'Advanced',
71
+ icon: (
72
+ <svg width="20" height="20" viewBox="0 0 20 20" fill="none">
73
+ <path
74
+ d="M4 5h12M4 10h12M4 15h12"
75
+ stroke="currentColor"
76
+ strokeWidth="1.5"
77
+ strokeLinecap="round"
78
+ />
79
+ <circle cx="8" cy="5" r="1.5" fill="currentColor" />
80
+ <circle cx="14" cy="10" r="1.5" fill="currentColor" />
81
+ <circle cx="6" cy="15" r="1.5" fill="currentColor" />
82
+ </svg>
83
+ ),
84
+ },
85
+ ];
@@ -0,0 +1,447 @@
1
+ /**
2
+ * useSettingsPanel
3
+ *
4
+ * Encapsulates all settings panel state, handlers, and effects.
5
+ * Returns everything the SettingsPanel shell needs to render.
6
+ */
7
+
8
+ import { useState, useEffect, useCallback, useRef } from 'react';
9
+ import type { AppSettings, AudioDevice, HotkeyConfig } from '../../../shared/types';
10
+ import { DEFAULT_SETTINGS, DEFAULT_HOTKEY_CONFIG } from '../../../shared/types';
11
+ import type { ApiKeyState } from '../primitives';
12
+ import type { SettingsTab } from './tabConfig';
13
+
14
+ // ============================================================================
15
+ // Constants
16
+ // ============================================================================
17
+
18
+ const MASKED_API_KEY_PLACEHOLDER = '********';
19
+ const API_TEST_TIMEOUT_MS = 15000;
20
+ const API_SAVE_TIMEOUT_MS = 12000;
21
+
22
+ const buildProviderTestFailureMessage = (provider: 'OpenAI' | 'Anthropic', error: unknown): string => {
23
+ const detail = error instanceof Error ? error.message : 'Unknown error';
24
+ return `Failed to test ${provider} API key: ${detail}`;
25
+ };
26
+
27
+ async function withTimeout<T>(promise: Promise<T>, timeoutMs: number, timeoutMessage: string): Promise<T> {
28
+ return new Promise<T>((resolve, reject) => {
29
+ const timeout = setTimeout(() => reject(new Error(timeoutMessage)), timeoutMs);
30
+ promise
31
+ .then((value) => {
32
+ clearTimeout(timeout);
33
+ resolve(value);
34
+ })
35
+ .catch((error) => {
36
+ clearTimeout(timeout);
37
+ reject(error);
38
+ });
39
+ });
40
+ }
41
+
42
+ // ============================================================================
43
+ // Hook
44
+ // ============================================================================
45
+
46
+ export function useSettingsPanel(isOpen: boolean, onClose: () => void, initialTab: SettingsTab = 'general') {
47
+ // State
48
+ const [activeTab, setActiveTab] = useState<SettingsTab>(initialTab);
49
+ const [settings, setSettings] = useState<AppSettings>(DEFAULT_SETTINGS);
50
+ const [audioDevices, setAudioDevices] = useState<AudioDevice[]>([]);
51
+ const [openAiApiKey, setOpenAiApiKey] = useState<ApiKeyState>({
52
+ value: '', visible: false, testing: false, valid: null, error: null,
53
+ });
54
+ const [anthropicApiKey, setAnthropicApiKey] = useState<ApiKeyState>({
55
+ value: '', visible: false, testing: false, valid: null, error: null,
56
+ });
57
+ const [hasChanges, setHasChanges] = useState(false);
58
+ const [isAnimating, setIsAnimating] = useState(false);
59
+ const [appVersion, setAppVersion] = useState('');
60
+ const [hasRequiredByokKeys, setHasRequiredByokKeys] = useState(false);
61
+ const [isCompact, setIsCompact] = useState(
62
+ () => typeof window !== 'undefined' && window.innerWidth < 760
63
+ );
64
+
65
+ const panelRef = useRef<HTMLDivElement>(null);
66
+
67
+ // ---------------------------------------------------------------------------
68
+ // Load settings on mount
69
+ // ---------------------------------------------------------------------------
70
+
71
+ useEffect(() => {
72
+ if (!isOpen) return;
73
+
74
+ const loadSettings = async () => {
75
+ try {
76
+ const allSettings = await window.markupr.settings.getAll();
77
+ setSettings({ ...DEFAULT_SETTINGS, ...allSettings });
78
+
79
+ const devices = await window.markupr.audio.getDevices();
80
+ setAudioDevices(devices);
81
+
82
+ try {
83
+ const [hasOpenAiKey, hasAnthropicKey] = await Promise.all([
84
+ window.markupr.settings.hasApiKey('openai'),
85
+ window.markupr.settings.hasApiKey('anthropic'),
86
+ ]);
87
+ if (hasOpenAiKey) {
88
+ setOpenAiApiKey((prev) => ({ ...prev, value: MASKED_API_KEY_PLACEHOLDER, valid: true }));
89
+ }
90
+ if (hasAnthropicKey) {
91
+ setAnthropicApiKey((prev) => ({ ...prev, value: MASKED_API_KEY_PLACEHOLDER, valid: true }));
92
+ }
93
+ const hasRequiredKeys = hasOpenAiKey && hasAnthropicKey;
94
+ setHasRequiredByokKeys(hasRequiredKeys);
95
+ if (!hasRequiredKeys && initialTab === 'general') {
96
+ setActiveTab('advanced');
97
+ }
98
+ } catch {
99
+ setHasRequiredByokKeys(false);
100
+ if (initialTab === 'general') {
101
+ setActiveTab('advanced');
102
+ }
103
+ }
104
+
105
+ try {
106
+ const ver = await window.markupr.version();
107
+ setAppVersion(ver);
108
+ } catch {
109
+ setAppVersion('');
110
+ }
111
+ } catch (error) {
112
+ console.error('Failed to load settings:', error);
113
+ }
114
+ };
115
+
116
+ loadSettings();
117
+ }, [isOpen, initialTab]);
118
+
119
+ // ---------------------------------------------------------------------------
120
+ // Setting change handlers
121
+ // ---------------------------------------------------------------------------
122
+
123
+ const handleSettingChange = useCallback(
124
+ async <K extends keyof AppSettings>(key: K, value: AppSettings[K]) => {
125
+ setSettings((prev) => ({ ...prev, [key]: value }));
126
+ setHasChanges(true);
127
+ try {
128
+ await window.markupr.settings.set(key, value);
129
+ } catch (error) {
130
+ console.error('Failed to save setting:', error);
131
+ }
132
+ },
133
+ []
134
+ );
135
+
136
+ const handleHotkeyChange = useCallback(
137
+ async (key: keyof HotkeyConfig, value: string) => {
138
+ const newHotkeys = { ...settings.hotkeys, [key]: value };
139
+ setSettings((prev) => ({ ...prev, hotkeys: newHotkeys }));
140
+ setHasChanges(true);
141
+ try {
142
+ await window.markupr.settings.set('hotkeys', newHotkeys);
143
+ await window.markupr.hotkeys.updateConfig(newHotkeys);
144
+ } catch (error) {
145
+ console.error('Failed to update hotkey:', error);
146
+ }
147
+ },
148
+ [settings.hotkeys]
149
+ );
150
+
151
+ // ---------------------------------------------------------------------------
152
+ // API key handlers
153
+ // ---------------------------------------------------------------------------
154
+
155
+ const handleOpenAiApiKeyChange = useCallback((value: string) => {
156
+ setOpenAiApiKey((prev) => ({ ...prev, value, valid: null, error: null }));
157
+ }, []);
158
+
159
+ const handleToggleOpenAiApiKeyVisibility = useCallback(() => {
160
+ setOpenAiApiKey((prev) => ({ ...prev, visible: !prev.visible }));
161
+ }, []);
162
+
163
+ const handleTestOpenAiApiKey = useCallback(async () => {
164
+ setOpenAiApiKey((prev) => ({ ...prev, testing: true, error: null }));
165
+ try {
166
+ let candidateKey = openAiApiKey.value.trim();
167
+ if (candidateKey === MASKED_API_KEY_PLACEHOLDER) {
168
+ const storedKey = await window.markupr.settings.getApiKey('openai');
169
+ if (!storedKey) {
170
+ setOpenAiApiKey((prev) => ({
171
+ ...prev, valid: false,
172
+ error: 'No saved OpenAI key found. Paste your key and test again.',
173
+ }));
174
+ return;
175
+ }
176
+ candidateKey = storedKey.trim();
177
+ }
178
+ const validation = await withTimeout(
179
+ window.markupr.settings.testApiKey('openai', candidateKey),
180
+ API_TEST_TIMEOUT_MS,
181
+ 'OpenAI API test timed out. Please try again.'
182
+ );
183
+ if (validation.valid) {
184
+ const saved = await withTimeout(
185
+ window.markupr.settings.setApiKey('openai', candidateKey),
186
+ API_SAVE_TIMEOUT_MS,
187
+ 'Saving OpenAI key timed out. Please try again.'
188
+ );
189
+ if (!saved) {
190
+ setOpenAiApiKey((prev) => ({
191
+ ...prev, valid: false,
192
+ error: 'OpenAI key validated, but local save verification failed. Relaunch app and try again.',
193
+ }));
194
+ return;
195
+ }
196
+ const hasAnthropic = await window.markupr.settings.hasApiKey('anthropic').catch(() => false);
197
+ setHasRequiredByokKeys(Boolean(hasAnthropic));
198
+ setOpenAiApiKey((prev) => ({ ...prev, valid: true }));
199
+ } else {
200
+ setOpenAiApiKey((prev) => ({
201
+ ...prev, valid: false,
202
+ error: validation.error || 'OpenAI API key test failed. Please try again.',
203
+ }));
204
+ }
205
+ } catch (error) {
206
+ setOpenAiApiKey((prev) => ({
207
+ ...prev, valid: false,
208
+ error: buildProviderTestFailureMessage('OpenAI', error),
209
+ }));
210
+ } finally {
211
+ setOpenAiApiKey((prev) => ({ ...prev, testing: false }));
212
+ }
213
+ }, [openAiApiKey.value]);
214
+
215
+ const handleAnthropicApiKeyChange = useCallback((value: string) => {
216
+ setAnthropicApiKey((prev) => ({ ...prev, value, valid: null, error: null }));
217
+ }, []);
218
+
219
+ const handleToggleAnthropicApiKeyVisibility = useCallback(() => {
220
+ setAnthropicApiKey((prev) => ({ ...prev, visible: !prev.visible }));
221
+ }, []);
222
+
223
+ const handleTestAnthropicApiKey = useCallback(async () => {
224
+ setAnthropicApiKey((prev) => ({ ...prev, testing: true, error: null }));
225
+ try {
226
+ let candidateKey = anthropicApiKey.value.trim();
227
+ if (candidateKey === MASKED_API_KEY_PLACEHOLDER) {
228
+ const storedKey = await window.markupr.settings.getApiKey('anthropic');
229
+ if (!storedKey) {
230
+ setAnthropicApiKey((prev) => ({
231
+ ...prev, valid: false,
232
+ error: 'No saved Anthropic key found. Paste your key and test again.',
233
+ }));
234
+ return;
235
+ }
236
+ candidateKey = storedKey.trim();
237
+ }
238
+ const validation = await withTimeout(
239
+ window.markupr.settings.testApiKey('anthropic', candidateKey),
240
+ API_TEST_TIMEOUT_MS,
241
+ 'Anthropic API test timed out. Please try again.'
242
+ );
243
+ if (validation.valid) {
244
+ const saved = await withTimeout(
245
+ window.markupr.settings.setApiKey('anthropic', candidateKey),
246
+ API_SAVE_TIMEOUT_MS,
247
+ 'Saving Anthropic key timed out. Please try again.'
248
+ );
249
+ if (!saved) {
250
+ setAnthropicApiKey((prev) => ({
251
+ ...prev, valid: false,
252
+ error: 'Anthropic key validated, but local save verification failed. Relaunch app and try again.',
253
+ }));
254
+ return;
255
+ }
256
+ const hasOpenAi = await window.markupr.settings.hasApiKey('openai').catch(() => false);
257
+ setHasRequiredByokKeys(Boolean(hasOpenAi));
258
+ setAnthropicApiKey((prev) => ({ ...prev, valid: true }));
259
+ } else {
260
+ setAnthropicApiKey((prev) => ({
261
+ ...prev, valid: false,
262
+ error: validation.error || 'Anthropic API key test failed. Please try again.',
263
+ }));
264
+ }
265
+ } catch (error) {
266
+ setAnthropicApiKey((prev) => ({
267
+ ...prev, valid: false,
268
+ error: buildProviderTestFailureMessage('Anthropic', error),
269
+ }));
270
+ } finally {
271
+ setAnthropicApiKey((prev) => ({ ...prev, testing: false }));
272
+ }
273
+ }, [anthropicApiKey.value]);
274
+
275
+ // ---------------------------------------------------------------------------
276
+ // Reset handlers
277
+ // ---------------------------------------------------------------------------
278
+
279
+ const resetGeneralSection = useCallback(async () => {
280
+ const defaults = {
281
+ outputDirectory: DEFAULT_SETTINGS.outputDirectory,
282
+ launchAtLogin: DEFAULT_SETTINGS.launchAtLogin,
283
+ checkForUpdates: DEFAULT_SETTINGS.checkForUpdates,
284
+ };
285
+ setSettings((prev) => ({ ...prev, ...defaults }));
286
+ for (const [key, value] of Object.entries(defaults)) {
287
+ await window.markupr.settings.set(key as keyof AppSettings, value);
288
+ }
289
+ }, []);
290
+
291
+ const resetRecordingSection = useCallback(async () => {
292
+ const defaults = {
293
+ defaultCountdown: DEFAULT_SETTINGS.defaultCountdown,
294
+ showTranscriptionPreview: DEFAULT_SETTINGS.showTranscriptionPreview,
295
+ showAudioWaveform: DEFAULT_SETTINGS.showAudioWaveform,
296
+ audioDeviceId: DEFAULT_SETTINGS.audioDeviceId,
297
+ pauseThreshold: DEFAULT_SETTINGS.pauseThreshold,
298
+ minTimeBetweenCaptures: DEFAULT_SETTINGS.minTimeBetweenCaptures,
299
+ };
300
+ setSettings((prev) => ({ ...prev, ...defaults }));
301
+ for (const [key, value] of Object.entries(defaults)) {
302
+ await window.markupr.settings.set(key as keyof AppSettings, value);
303
+ }
304
+ }, []);
305
+
306
+ const resetAppearanceSection = useCallback(async () => {
307
+ const defaults = {
308
+ theme: DEFAULT_SETTINGS.theme,
309
+ accentColor: DEFAULT_SETTINGS.accentColor,
310
+ };
311
+ setSettings((prev) => ({ ...prev, ...defaults }));
312
+ for (const [key, value] of Object.entries(defaults)) {
313
+ await window.markupr.settings.set(key as keyof AppSettings, value);
314
+ }
315
+ }, []);
316
+
317
+ const resetHotkeysSection = useCallback(async () => {
318
+ const defaults = { ...DEFAULT_HOTKEY_CONFIG };
319
+ setSettings((prev) => ({ ...prev, hotkeys: defaults }));
320
+ await window.markupr.settings.set('hotkeys', defaults);
321
+ // @ts-expect-error - update may be named updateConfig in type definition
322
+ await (window.markupr.hotkeys.update ?? window.markupr.hotkeys.updateConfig)?.(defaults);
323
+ }, []);
324
+
325
+ const resetAdvancedSection = useCallback(async () => {
326
+ const defaults = {
327
+ debugMode: DEFAULT_SETTINGS.debugMode,
328
+ keepAudioBackups: DEFAULT_SETTINGS.keepAudioBackups,
329
+ };
330
+ setSettings((prev) => ({ ...prev, ...defaults }));
331
+ for (const [key, value] of Object.entries(defaults)) {
332
+ await window.markupr.settings.set(key as keyof AppSettings, value);
333
+ }
334
+ }, []);
335
+
336
+ // ---------------------------------------------------------------------------
337
+ // Data management handlers
338
+ // ---------------------------------------------------------------------------
339
+
340
+ const handleClearAllData = useCallback(async () => {
341
+ try {
342
+ await window.markupr.settings.clearAllData();
343
+ setSettings(DEFAULT_SETTINGS);
344
+ setOpenAiApiKey({ value: '', visible: false, testing: false, valid: null, error: null });
345
+ setAnthropicApiKey({ value: '', visible: false, testing: false, valid: null, error: null });
346
+ setHasRequiredByokKeys(false);
347
+ } catch (error) {
348
+ console.error('Failed to clear data:', error);
349
+ }
350
+ }, []);
351
+
352
+ const handleExportSettings = useCallback(async () => {
353
+ try {
354
+ await window.markupr.settings.export();
355
+ } catch (error) {
356
+ console.error('Failed to export settings:', error);
357
+ }
358
+ }, []);
359
+
360
+ const handleImportSettings = useCallback(async () => {
361
+ try {
362
+ const imported = await window.markupr.settings.import();
363
+ if (imported) {
364
+ setSettings({ ...DEFAULT_SETTINGS, ...imported });
365
+ }
366
+ } catch (error) {
367
+ console.error('Failed to import settings:', error);
368
+ }
369
+ }, []);
370
+
371
+ // ---------------------------------------------------------------------------
372
+ // Effects
373
+ // ---------------------------------------------------------------------------
374
+
375
+ useEffect(() => {
376
+ const handleKeyDown = (e: KeyboardEvent) => {
377
+ if (!isOpen) return;
378
+ if (e.key === 'Escape') {
379
+ e.preventDefault();
380
+ onClose();
381
+ }
382
+ };
383
+ window.addEventListener('keydown', handleKeyDown);
384
+ return () => window.removeEventListener('keydown', handleKeyDown);
385
+ }, [isOpen, onClose]);
386
+
387
+ useEffect(() => {
388
+ if (isOpen) {
389
+ setIsAnimating(true);
390
+ const timer = setTimeout(() => setIsAnimating(false), 300);
391
+ return () => clearTimeout(timer);
392
+ }
393
+ }, [isOpen]);
394
+
395
+ useEffect(() => {
396
+ const onResize = () => {
397
+ setIsCompact(window.innerWidth < 760);
398
+ };
399
+ onResize();
400
+ window.addEventListener('resize', onResize);
401
+ return () => window.removeEventListener('resize', onResize);
402
+ }, []);
403
+
404
+ // ---------------------------------------------------------------------------
405
+ // Return value
406
+ // ---------------------------------------------------------------------------
407
+
408
+ return {
409
+ // State
410
+ activeTab,
411
+ setActiveTab,
412
+ settings,
413
+ audioDevices,
414
+ openAiApiKey,
415
+ anthropicApiKey,
416
+ hasChanges,
417
+ isAnimating,
418
+ appVersion,
419
+ hasRequiredByokKeys,
420
+ isCompact,
421
+ panelRef,
422
+
423
+ // Setting handlers
424
+ handleSettingChange,
425
+ handleHotkeyChange,
426
+
427
+ // API key handlers
428
+ handleOpenAiApiKeyChange,
429
+ handleToggleOpenAiApiKeyVisibility,
430
+ handleTestOpenAiApiKey,
431
+ handleAnthropicApiKeyChange,
432
+ handleToggleAnthropicApiKeyVisibility,
433
+ handleTestAnthropicApiKey,
434
+
435
+ // Reset handlers
436
+ resetGeneralSection,
437
+ resetRecordingSection,
438
+ resetAppearanceSection,
439
+ resetHotkeysSection,
440
+ resetAdvancedSection,
441
+
442
+ // Data management
443
+ handleClearAllData,
444
+ handleExportSettings,
445
+ handleImportSettings,
446
+ };
447
+ }