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,811 @@
1
+ /**
2
+ * Audio Waveform Visualization Component
3
+ *
4
+ * Professional audio tool quality visualization featuring:
5
+ * - Real-time waveform display (bars, wave, line styles)
6
+ * - Voice activity detection indicator
7
+ * - Smooth 60fps requestAnimationFrame-based animation
8
+ * - Peak level detection with decay
9
+ * - Compact mode for RecordingOverlay integration
10
+ *
11
+ * Designed to feel like a premium DAW or podcast recording app
12
+ */
13
+
14
+ import React, { useState, useEffect, useRef, useCallback, useMemo } from 'react';
15
+ import { useTheme } from '../hooks/useTheme';
16
+
17
+ // =============================================================================
18
+ // Types
19
+ // =============================================================================
20
+
21
+ export interface AudioWaveformProps {
22
+ /** Current audio level normalized 0-1 */
23
+ audioLevel: number;
24
+ /** Whether voice/speech is currently detected */
25
+ isVoiceActive: boolean;
26
+ /** Visualization style */
27
+ style?: 'bars' | 'wave' | 'line';
28
+ /** Component size preset */
29
+ size?: 'compact' | 'normal' | 'large';
30
+ /** Accent color for active state (CSS color string) */
31
+ accentColor?: string;
32
+ /** Secondary color for inactive state */
33
+ inactiveColor?: string;
34
+ /** Number of bars/samples to display */
35
+ resolution?: number;
36
+ /** Show peak hold indicator */
37
+ showPeak?: boolean;
38
+ /** Custom className for container */
39
+ className?: string;
40
+ }
41
+
42
+ interface WaveformStyleProps {
43
+ levels: number[];
44
+ isVoiceActive: boolean;
45
+ accentColor: string;
46
+ inactiveColor: string;
47
+ peakLevel: number;
48
+ showPeak: boolean;
49
+ peakColor: string;
50
+ peakWarningColor: string;
51
+ }
52
+
53
+ // =============================================================================
54
+ // Constants
55
+ // =============================================================================
56
+
57
+ /**
58
+ * Hook that returns waveform-specific colors from the theme system.
59
+ * Replaces the former module-level COLORS constant.
60
+ */
61
+ function useWaveformColors() {
62
+ const { colors } = useTheme();
63
+ return useMemo(
64
+ () => ({
65
+ active: colors.status.success,
66
+ inactive: colors.text.tertiary,
67
+ peak: colors.status.error,
68
+ peakWarning: colors.status.warning,
69
+ background: 'rgba(31, 41, 55, 0.5)',
70
+ textSecondary: colors.text.secondary,
71
+ }),
72
+ [colors]
73
+ );
74
+ }
75
+
76
+ const SIZE_CONFIG = {
77
+ compact: { height: 16, width: 64, barCount: 8, barWidth: 2, gap: 2 },
78
+ normal: { height: 48, width: 192, barCount: 32, barWidth: 4, gap: 2 },
79
+ large: { height: 96, width: 320, barCount: 64, barWidth: 3, gap: 2 },
80
+ };
81
+
82
+ // Smoothing factor for level transitions (higher = smoother but slower)
83
+ const SMOOTHING_FACTOR = 0.15;
84
+ // Peak hold time in milliseconds
85
+ const PEAK_HOLD_TIME = 1500;
86
+ // Peak decay rate per frame
87
+ const PEAK_DECAY_RATE = 0.02;
88
+
89
+ // =============================================================================
90
+ // Utility Functions
91
+ // =============================================================================
92
+
93
+ /**
94
+ * Generates a bell curve distribution for bar heights
95
+ * Creates natural-looking center-weighted visualization
96
+ */
97
+ function generateBellCurve(index: number, total: number): number {
98
+ const center = total / 2;
99
+ const distance = Math.abs(index - center) / center;
100
+ // Gaussian-like falloff
101
+ return Math.exp(-2 * distance * distance);
102
+ }
103
+
104
+ /**
105
+ * Applies smoothing between current and target values
106
+ */
107
+ function smoothValue(current: number, target: number, factor: number): number {
108
+ return current + (target - current) * factor;
109
+ }
110
+
111
+ // =============================================================================
112
+ // Sub-Components
113
+ // =============================================================================
114
+
115
+ /**
116
+ * Bars Style Waveform
117
+ * Classic audio meter with vertical bars, center-weighted distribution
118
+ */
119
+ function BarsWaveform({
120
+ levels,
121
+ isVoiceActive,
122
+ accentColor,
123
+ inactiveColor,
124
+ peakLevel,
125
+ showPeak,
126
+ peakColor,
127
+ peakWarningColor,
128
+ }: WaveformStyleProps) {
129
+ const barCount = levels.length;
130
+
131
+ return (
132
+ <div
133
+ style={{
134
+ display: 'flex',
135
+ alignItems: 'center',
136
+ justifyContent: 'center',
137
+ gap: 2,
138
+ height: '100%',
139
+ width: '100%',
140
+ padding: '0 4px',
141
+ }}
142
+ >
143
+ {levels.map((level, i) => {
144
+ // Apply bell curve distribution for natural look
145
+ const bellWeight = generateBellCurve(i, barCount);
146
+ const adjustedLevel = level * bellWeight;
147
+
148
+ // Determine color based on level and voice activity
149
+ let barColor = inactiveColor;
150
+ if (isVoiceActive) {
151
+ if (adjustedLevel > 0.9) {
152
+ barColor = peakColor;
153
+ } else if (adjustedLevel > 0.75) {
154
+ barColor = peakWarningColor;
155
+ } else {
156
+ barColor = accentColor;
157
+ }
158
+ }
159
+
160
+ return (
161
+ <div
162
+ key={i}
163
+ style={{
164
+ width: 4,
165
+ minHeight: 4,
166
+ height: `${Math.max(8, adjustedLevel * 100)}%`,
167
+ backgroundColor: barColor,
168
+ borderRadius: 2,
169
+ transition: 'height 50ms ease-out, background-color 100ms ease',
170
+ opacity: isVoiceActive ? 1 : 0.6,
171
+ }}
172
+ />
173
+ );
174
+ })}
175
+
176
+ {/* Peak indicator line */}
177
+ {showPeak && peakLevel > 0.1 && (
178
+ <div
179
+ style={{
180
+ position: 'absolute',
181
+ left: 4,
182
+ right: 4,
183
+ height: 2,
184
+ bottom: `${peakLevel * 100}%`,
185
+ backgroundColor: peakLevel > 0.9 ? peakColor : peakWarningColor,
186
+ borderRadius: 1,
187
+ opacity: 0.8,
188
+ transition: 'bottom 50ms ease-out',
189
+ pointerEvents: 'none',
190
+ }}
191
+ />
192
+ )}
193
+ </div>
194
+ );
195
+ }
196
+
197
+ /**
198
+ * Wave Style Waveform
199
+ * Canvas-based smooth waveform with fill gradient
200
+ */
201
+ function WaveWaveform({
202
+ levels,
203
+ isVoiceActive,
204
+ accentColor,
205
+ inactiveColor,
206
+ }: WaveformStyleProps) {
207
+ const canvasRef = useRef<HTMLCanvasElement>(null);
208
+ const containerRef = useRef<HTMLDivElement>(null);
209
+
210
+ useEffect(() => {
211
+ const canvas = canvasRef.current;
212
+ const container = containerRef.current;
213
+ if (!canvas || !container) return;
214
+
215
+ // Handle high-DPI displays
216
+ const dpr = window.devicePixelRatio || 1;
217
+ const rect = container.getBoundingClientRect();
218
+ canvas.width = rect.width * dpr;
219
+ canvas.height = rect.height * dpr;
220
+ canvas.style.width = `${rect.width}px`;
221
+ canvas.style.height = `${rect.height}px`;
222
+
223
+ const ctx = canvas.getContext('2d');
224
+ if (!ctx) return;
225
+
226
+ ctx.scale(dpr, dpr);
227
+
228
+ const width = rect.width;
229
+ const height = rect.height;
230
+ const centerY = height / 2;
231
+
232
+ // Clear canvas
233
+ ctx.clearRect(0, 0, width, height);
234
+
235
+ // Draw waveform path
236
+ ctx.beginPath();
237
+ ctx.moveTo(0, centerY);
238
+
239
+ const sliceWidth = width / (levels.length - 1);
240
+
241
+ // Upper wave (positive values)
242
+ levels.forEach((value, i) => {
243
+ const x = i * sliceWidth;
244
+ const amplitude = value * (height / 2) * 0.9; // Leave margin
245
+ const y = centerY - amplitude;
246
+
247
+ if (i === 0) {
248
+ ctx.moveTo(x, y);
249
+ } else {
250
+ // Use quadratic curves for smoothness
251
+ const prevX = (i - 1) * sliceWidth;
252
+ ctx.quadraticCurveTo(prevX + sliceWidth / 2, centerY - levels[i - 1] * (height / 2) * 0.9, x, y);
253
+ }
254
+ });
255
+
256
+ // Mirror for lower wave (creates symmetric waveform)
257
+ for (let i = levels.length - 1; i >= 0; i--) {
258
+ const x = i * sliceWidth;
259
+ const amplitude = levels[i] * (height / 2) * 0.9;
260
+ const y = centerY + amplitude;
261
+
262
+ if (i === levels.length - 1) {
263
+ ctx.lineTo(x, y);
264
+ } else {
265
+ const nextX = (i + 1) * sliceWidth;
266
+ ctx.quadraticCurveTo(nextX - sliceWidth / 2, centerY + levels[i + 1] * (height / 2) * 0.9, x, y);
267
+ }
268
+ }
269
+
270
+ ctx.closePath();
271
+
272
+ // Create gradient fill
273
+ const gradient = ctx.createLinearGradient(0, 0, 0, height);
274
+ const baseColor = isVoiceActive ? accentColor : inactiveColor;
275
+ gradient.addColorStop(0, `${baseColor}40`);
276
+ gradient.addColorStop(0.5, `${baseColor}60`);
277
+ gradient.addColorStop(1, `${baseColor}40`);
278
+
279
+ ctx.fillStyle = gradient;
280
+ ctx.fill();
281
+
282
+ // Draw center line
283
+ ctx.strokeStyle = isVoiceActive ? accentColor : inactiveColor;
284
+ ctx.lineWidth = 2;
285
+
286
+ ctx.beginPath();
287
+ ctx.moveTo(0, centerY);
288
+ levels.forEach((value, i) => {
289
+ const x = i * sliceWidth;
290
+ const amplitude = value * (height / 2) * 0.8;
291
+
292
+ if (i === 0) {
293
+ ctx.moveTo(x, centerY);
294
+ } else {
295
+ // Create slight variance for organic feel
296
+ const variance = (Math.random() > 0.5 ? 1 : -1) * 0.5;
297
+ ctx.lineTo(x, centerY - amplitude * variance);
298
+ }
299
+ });
300
+ ctx.stroke();
301
+ }, [levels, isVoiceActive, accentColor, inactiveColor]);
302
+
303
+ return (
304
+ <div ref={containerRef} style={{ width: '100%', height: '100%', position: 'relative' }}>
305
+ <canvas
306
+ ref={canvasRef}
307
+ style={{
308
+ width: '100%',
309
+ height: '100%',
310
+ display: 'block',
311
+ }}
312
+ />
313
+ </div>
314
+ );
315
+ }
316
+
317
+ /**
318
+ * Line Style Waveform
319
+ * Simple animated line with glow effect - good for minimal UIs
320
+ */
321
+ function LineWaveform({
322
+ levels,
323
+ isVoiceActive,
324
+ accentColor,
325
+ inactiveColor,
326
+ peakLevel,
327
+ peakColor,
328
+ }: WaveformStyleProps) {
329
+ const currentLevel = levels[levels.length - 1] || 0;
330
+
331
+ return (
332
+ <div
333
+ style={{
334
+ display: 'flex',
335
+ alignItems: 'center',
336
+ justifyContent: 'center',
337
+ height: '100%',
338
+ width: '100%',
339
+ padding: '0 8px',
340
+ position: 'relative',
341
+ }}
342
+ >
343
+ {/* Background track */}
344
+ <div
345
+ style={{
346
+ position: 'absolute',
347
+ left: 8,
348
+ right: 8,
349
+ height: 4,
350
+ backgroundColor: 'rgba(75, 85, 99, 0.3)',
351
+ borderRadius: 2,
352
+ }}
353
+ />
354
+
355
+ {/* Active level bar */}
356
+ <div
357
+ style={{
358
+ position: 'absolute',
359
+ left: 8,
360
+ height: 4,
361
+ width: `${Math.max(0, Math.min(100, currentLevel * 100))}%`,
362
+ maxWidth: 'calc(100% - 16px)',
363
+ backgroundColor: isVoiceActive ? accentColor : inactiveColor,
364
+ borderRadius: 2,
365
+ transition: 'width 50ms ease-out',
366
+ boxShadow: isVoiceActive ? `0 0 8px ${accentColor}60` : 'none',
367
+ }}
368
+ />
369
+
370
+ {/* Peak marker */}
371
+ {peakLevel > 0.1 && (
372
+ <div
373
+ style={{
374
+ position: 'absolute',
375
+ left: `calc(8px + min(${peakLevel * 100}%, calc(100% - 18px)))`,
376
+ width: 4,
377
+ height: 8,
378
+ backgroundColor: peakLevel > 0.9 ? peakColor : accentColor,
379
+ borderRadius: 1,
380
+ opacity: 0.9,
381
+ transition: 'left 50ms ease-out',
382
+ }}
383
+ />
384
+ )}
385
+ </div>
386
+ );
387
+ }
388
+
389
+ // =============================================================================
390
+ // Main Component
391
+ // =============================================================================
392
+
393
+ export function AudioWaveform({
394
+ audioLevel,
395
+ isVoiceActive,
396
+ style = 'bars',
397
+ size = 'normal',
398
+ accentColor: accentColorProp,
399
+ inactiveColor: inactiveColorProp,
400
+ resolution,
401
+ showPeak = true,
402
+ className,
403
+ }: AudioWaveformProps) {
404
+ const waveformColors = useWaveformColors();
405
+ const accentColor = accentColorProp ?? waveformColors.active;
406
+ const inactiveColor = inactiveColorProp ?? waveformColors.inactive;
407
+
408
+ const config = SIZE_CONFIG[size];
409
+ const barCount = resolution || config.barCount;
410
+
411
+ // State for accumulated levels (creates trailing effect)
412
+ const [levels, setLevels] = useState<number[]>(() => new Array(barCount).fill(0));
413
+ // Peak level with hold and decay
414
+ const [peakLevel, setPeakLevel] = useState(0);
415
+ // Smoothed current level for fluid animation
416
+ const smoothedLevelRef = useRef(0);
417
+
418
+ // Animation frame reference
419
+ const animationRef = useRef<number>();
420
+ const lastUpdateRef = useRef<number>(0);
421
+ const peakHoldTimeRef = useRef<number>(0);
422
+
423
+ // Smooth level updates at 60fps
424
+ const updateLevels = useCallback((timestamp: number) => {
425
+ // Throttle to ~60fps
426
+ if (timestamp - lastUpdateRef.current < 16) {
427
+ animationRef.current = requestAnimationFrame(updateLevels);
428
+ return;
429
+ }
430
+ lastUpdateRef.current = timestamp;
431
+
432
+ // Smooth the incoming audio level
433
+ smoothedLevelRef.current = smoothValue(smoothedLevelRef.current, audioLevel, SMOOTHING_FACTOR);
434
+
435
+ setLevels((prev) => {
436
+ const newLevels = [...prev.slice(1), smoothedLevelRef.current];
437
+ return newLevels;
438
+ });
439
+
440
+ // Update peak with hold and decay
441
+ setPeakLevel((prevPeak) => {
442
+ if (smoothedLevelRef.current > prevPeak) {
443
+ peakHoldTimeRef.current = timestamp;
444
+ return smoothedLevelRef.current;
445
+ } else if (timestamp - peakHoldTimeRef.current > PEAK_HOLD_TIME) {
446
+ // Decay peak after hold time
447
+ return Math.max(0, prevPeak - PEAK_DECAY_RATE);
448
+ }
449
+ return prevPeak;
450
+ });
451
+
452
+ animationRef.current = requestAnimationFrame(updateLevels);
453
+ }, [audioLevel]);
454
+
455
+ // Start animation loop
456
+ useEffect(() => {
457
+ animationRef.current = requestAnimationFrame(updateLevels);
458
+ return () => {
459
+ if (animationRef.current) {
460
+ cancelAnimationFrame(animationRef.current);
461
+ }
462
+ };
463
+ }, [updateLevels]);
464
+
465
+ // Style props passed to sub-components
466
+ const styleProps: WaveformStyleProps = useMemo(
467
+ () => ({
468
+ levels,
469
+ isVoiceActive,
470
+ accentColor,
471
+ inactiveColor,
472
+ peakLevel,
473
+ showPeak,
474
+ peakColor: waveformColors.peak,
475
+ peakWarningColor: waveformColors.peakWarning,
476
+ }),
477
+ [levels, isVoiceActive, accentColor, inactiveColor, peakLevel, showPeak, waveformColors.peak, waveformColors.peakWarning]
478
+ );
479
+
480
+ // Container styles
481
+ const containerStyle: React.CSSProperties = {
482
+ width: size === 'large' ? '100%' : config.width,
483
+ height: config.height,
484
+ backgroundColor: waveformColors.background,
485
+ borderRadius: size === 'compact' ? 8 : 12,
486
+ overflow: 'hidden',
487
+ position: 'relative',
488
+ transition: 'box-shadow 200ms ease',
489
+ boxShadow: isVoiceActive
490
+ ? `0 0 0 1px ${accentColor}30, 0 0 12px ${accentColor}20`
491
+ : '0 0 0 1px rgba(75, 85, 99, 0.3)',
492
+ };
493
+
494
+ return (
495
+ <div style={containerStyle} className={className}>
496
+ {/* Waveform visualization */}
497
+ {style === 'bars' && <BarsWaveform {...styleProps} />}
498
+ {style === 'wave' && <WaveWaveform {...styleProps} />}
499
+ {style === 'line' && <LineWaveform {...styleProps} />}
500
+
501
+ {/* Voice activity indicator dot */}
502
+ {isVoiceActive && (
503
+ <div
504
+ style={{
505
+ position: 'absolute',
506
+ top: size === 'compact' ? 2 : 4,
507
+ right: size === 'compact' ? 2 : 4,
508
+ width: size === 'compact' ? 4 : 6,
509
+ height: size === 'compact' ? 4 : 6,
510
+ borderRadius: '50%',
511
+ backgroundColor: waveformColors.active,
512
+ boxShadow: `0 0 4px ${waveformColors.active}`,
513
+ animation: 'pulseScale 1.5s ease-in-out infinite',
514
+ }}
515
+ />
516
+ )}
517
+
518
+ {/* pulseScale keyframe provided by animations.css */}
519
+ </div>
520
+ );
521
+ }
522
+
523
+ // =============================================================================
524
+ // Compact Audio Indicator
525
+ // =============================================================================
526
+
527
+ interface CompactAudioIndicatorProps {
528
+ /** Current audio level normalized 0-1 */
529
+ audioLevel: number;
530
+ /** Whether voice/speech is currently detected */
531
+ isVoiceActive: boolean;
532
+ /** Accent color for active state */
533
+ accentColor?: string;
534
+ /** Color for inactive bars */
535
+ inactiveColor?: string;
536
+ /** Number of bars */
537
+ barCount?: number;
538
+ /** Width of each bar in pixels */
539
+ barWidth?: number;
540
+ /** Gap between bars in pixels */
541
+ barGap?: number;
542
+ /** Container height in pixels */
543
+ meterHeight?: number;
544
+ /** Minimum bar height in pixels */
545
+ minBarHeight?: number;
546
+ /** Maximum bar height in pixels */
547
+ maxBarHeight?: number;
548
+ }
549
+
550
+ /**
551
+ * Compact Audio Indicator
552
+ * Minimal 5-bar indicator designed for RecordingOverlay integration
553
+ * Shows real-time audio level with voice activity feedback
554
+ */
555
+ export function CompactAudioIndicator({
556
+ audioLevel,
557
+ isVoiceActive,
558
+ accentColor: accentColorProp,
559
+ inactiveColor: inactiveColorProp,
560
+ barCount = 5,
561
+ barWidth = 2,
562
+ barGap = 1,
563
+ meterHeight = 16,
564
+ minBarHeight = 4,
565
+ maxBarHeight = 16,
566
+ }: CompactAudioIndicatorProps) {
567
+ const waveformColors = useWaveformColors();
568
+ const accentColor = accentColorProp ?? waveformColors.active;
569
+ const inactiveColor = inactiveColorProp ?? waveformColors.inactive;
570
+ const [smoothedLevel, setSmoothedLevel] = useState(0);
571
+ const [phase, setPhase] = useState(0);
572
+ const animationRef = useRef<number>();
573
+ const levelRef = useRef(0);
574
+
575
+ // Smooth level updates
576
+ useEffect(() => {
577
+ levelRef.current = audioLevel;
578
+ }, [audioLevel]);
579
+
580
+ useEffect(() => {
581
+ const animate = () => {
582
+ setSmoothedLevel((prev) => smoothValue(prev, levelRef.current, 0.2));
583
+ setPhase((prev) => prev + 0.22);
584
+ animationRef.current = requestAnimationFrame(animate);
585
+ };
586
+ animationRef.current = requestAnimationFrame(animate);
587
+ return () => {
588
+ if (animationRef.current) {
589
+ cancelAnimationFrame(animationRef.current);
590
+ }
591
+ };
592
+ }, []);
593
+
594
+ // Generate bars with staggered thresholds
595
+ const bars = useMemo(() => {
596
+ return Array.from({ length: barCount }, (_, i) => {
597
+ const motionBoost = isVoiceActive
598
+ ? Math.max(0, Math.sin(phase + i * 0.92) * 0.16 + 0.18)
599
+ : Math.max(0, Math.sin(phase + i * 0.74) * 0.045 + 0.03);
600
+ const effectiveLevel = Math.min(
601
+ 1,
602
+ Math.max(isVoiceActive ? 0.05 : 0.015, smoothedLevel + motionBoost + (isVoiceActive ? 0.06 : 0))
603
+ );
604
+ const threshold = i / barCount;
605
+ const isActive = effectiveLevel > threshold;
606
+ const intensity = isActive ? Math.min(1, (effectiveLevel - threshold) * barCount) : 0;
607
+
608
+ // Height varies by position (taller in center)
609
+ const centerRadius = Math.max(1, (barCount - 1) / 2);
610
+ const centerBonus = 1 - Math.abs(i - (barCount - 1) / 2) / centerRadius;
611
+ const centerMaxHeight = minBarHeight + centerBonus * (maxBarHeight - minBarHeight);
612
+ const height = minBarHeight + intensity * (centerMaxHeight - minBarHeight);
613
+
614
+ return {
615
+ height,
616
+ isActive,
617
+ intensity,
618
+ };
619
+ });
620
+ }, [
621
+ barCount,
622
+ isVoiceActive,
623
+ maxBarHeight,
624
+ minBarHeight,
625
+ phase,
626
+ smoothedLevel,
627
+ ]);
628
+
629
+ return (
630
+ <div
631
+ style={{
632
+ display: 'flex',
633
+ alignItems: 'center',
634
+ gap: barGap,
635
+ height: meterHeight,
636
+ padding: '0 2px',
637
+ }}
638
+ >
639
+ {bars.map((bar, i) => (
640
+ <div
641
+ key={i}
642
+ style={{
643
+ width: barWidth,
644
+ height: bar.height,
645
+ borderRadius: 1,
646
+ backgroundColor: isVoiceActive && bar.isActive ? accentColor : inactiveColor,
647
+ opacity: bar.isActive ? 0.9 + bar.intensity * 0.1 : 0.4,
648
+ transition: 'height 50ms ease-out, background-color 100ms ease, opacity 100ms ease',
649
+ }}
650
+ />
651
+ ))}
652
+ </div>
653
+ );
654
+ }
655
+
656
+ // =============================================================================
657
+ // Audio Level Meter (VU-style)
658
+ // =============================================================================
659
+
660
+ interface AudioLevelMeterProps {
661
+ /** Current audio level normalized 0-1 */
662
+ level: number;
663
+ /** Whether voice is active */
664
+ isVoiceActive: boolean;
665
+ /** Orientation */
666
+ orientation?: 'horizontal' | 'vertical';
667
+ /** Show dB labels */
668
+ showLabels?: boolean;
669
+ /** Size preset */
670
+ size?: 'small' | 'medium' | 'large';
671
+ }
672
+
673
+ /**
674
+ * Professional VU-style level meter
675
+ * Shows current level, peak hold, and dB scale
676
+ */
677
+ export function AudioLevelMeter({
678
+ level,
679
+ isVoiceActive,
680
+ orientation = 'horizontal',
681
+ showLabels = false,
682
+ size = 'medium',
683
+ }: AudioLevelMeterProps) {
684
+ const waveformColors = useWaveformColors();
685
+ const [smoothedLevel, setSmoothedLevel] = useState(0);
686
+ const [peakLevel, setPeakLevel] = useState(0);
687
+ const peakHoldTimeRef = useRef(0);
688
+ const animationRef = useRef<number>();
689
+
690
+ // Convert linear to dB (clamped range)
691
+ const levelToDb = (l: number): number => {
692
+ if (l <= 0) return -60;
693
+ return Math.max(-60, Math.min(0, 20 * Math.log10(l)));
694
+ };
695
+
696
+ // Animation loop
697
+ useEffect(() => {
698
+ const animate = (timestamp: number) => {
699
+ // Smooth level
700
+ setSmoothedLevel((prev) => smoothValue(prev, level, 0.15));
701
+
702
+ // Peak with hold
703
+ setPeakLevel((prevPeak) => {
704
+ if (level > prevPeak) {
705
+ peakHoldTimeRef.current = timestamp;
706
+ return level;
707
+ } else if (timestamp - peakHoldTimeRef.current > PEAK_HOLD_TIME) {
708
+ return Math.max(0, prevPeak - PEAK_DECAY_RATE);
709
+ }
710
+ return prevPeak;
711
+ });
712
+
713
+ animationRef.current = requestAnimationFrame(animate);
714
+ };
715
+ animationRef.current = requestAnimationFrame(animate);
716
+ return () => {
717
+ if (animationRef.current) {
718
+ cancelAnimationFrame(animationRef.current);
719
+ }
720
+ };
721
+ }, [level]);
722
+
723
+ // Size configuration
724
+ const sizeConfig = {
725
+ small: { width: 80, height: 8, labelSize: 8 },
726
+ medium: { width: 120, height: 12, labelSize: 10 },
727
+ large: { width: 200, height: 16, labelSize: 12 },
728
+ };
729
+ const config = sizeConfig[size];
730
+
731
+ const isVertical = orientation === 'vertical';
732
+ const meterWidth = isVertical ? config.height : config.width;
733
+ const meterHeight = isVertical ? config.width : config.height;
734
+
735
+ return (
736
+ <div
737
+ style={{
738
+ display: 'flex',
739
+ flexDirection: isVertical ? 'column' : 'row',
740
+ alignItems: 'center',
741
+ gap: 4,
742
+ }}
743
+ >
744
+ {/* Meter track */}
745
+ <div
746
+ style={{
747
+ position: 'relative',
748
+ width: meterWidth,
749
+ height: meterHeight,
750
+ backgroundColor: 'rgba(31, 41, 55, 0.6)',
751
+ borderRadius: 2,
752
+ overflow: 'hidden',
753
+ }}
754
+ >
755
+ {/* Level segments (gradient effect) */}
756
+ <div
757
+ style={{
758
+ position: 'absolute',
759
+ [isVertical ? 'bottom' : 'left']: 0,
760
+ [isVertical ? 'left' : 'top']: 0,
761
+ [isVertical ? 'width' : 'height']: '100%',
762
+ [isVertical ? 'height' : 'width']: `${smoothedLevel * 100}%`,
763
+ background: isVoiceActive
764
+ ? `linear-gradient(${isVertical ? 'to top' : 'to right'},
765
+ ${waveformColors.active} 0%,
766
+ ${waveformColors.active} 75%,
767
+ ${waveformColors.peakWarning} 90%,
768
+ ${waveformColors.peak} 100%)`
769
+ : waveformColors.inactive,
770
+ transition: `${isVertical ? 'height' : 'width'} 50ms ease-out`,
771
+ }}
772
+ />
773
+
774
+ {/* Peak indicator */}
775
+ <div
776
+ style={{
777
+ position: 'absolute',
778
+ [isVertical ? 'bottom' : 'left']: `calc(${peakLevel * 100}% - 2px)`,
779
+ [isVertical ? 'left' : 'top']: 0,
780
+ [isVertical ? 'width' : 'height']: '100%',
781
+ [isVertical ? 'height' : 'width']: 2,
782
+ backgroundColor: peakLevel > 0.9 ? waveformColors.peak : waveformColors.peakWarning,
783
+ opacity: peakLevel > 0.1 ? 0.9 : 0,
784
+ transition: `${isVertical ? 'bottom' : 'left'} 50ms ease-out, opacity 100ms ease`,
785
+ }}
786
+ />
787
+ </div>
788
+
789
+ {/* dB labels */}
790
+ {showLabels && (
791
+ <span
792
+ style={{
793
+ fontSize: config.labelSize,
794
+ fontFamily: 'ui-monospace, monospace',
795
+ color: smoothedLevel > 0.9 ? waveformColors.peak : waveformColors.textSecondary,
796
+ minWidth: 36,
797
+ textAlign: 'right',
798
+ }}
799
+ >
800
+ {levelToDb(smoothedLevel).toFixed(1)} dB
801
+ </span>
802
+ )}
803
+ </div>
804
+ );
805
+ }
806
+
807
+ // =============================================================================
808
+ // Default Export
809
+ // =============================================================================
810
+
811
+ export default AudioWaveform;