react-restyle-components 0.4.12 → 0.4.14

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 (78) hide show
  1. package/lib/src/core-components/src/components/Accordion/AccordionSection/elements.d.ts +1 -1
  2. package/lib/src/core-components/src/components/Accordion/AccordionSection/elements.d.ts.map +1 -1
  3. package/lib/src/core-components/src/components/Accordion/AccordionSection/elements.js +71 -20
  4. package/lib/src/core-components/src/components/Accordion/collapsible/collapsible.component.d.ts +22 -2
  5. package/lib/src/core-components/src/components/Accordion/collapsible/collapsible.component.d.ts.map +1 -1
  6. package/lib/src/core-components/src/components/Accordion/collapsible/collapsible.component.js +2 -2
  7. package/lib/src/core-components/src/components/AutoComplete/auto-complete-filter-multiple-select-multiple-fields-display/auto-complete-filter-multiple-select-multiple-fields-display.component.d.ts +58 -1
  8. package/lib/src/core-components/src/components/AutoComplete/auto-complete-filter-multiple-select-multiple-fields-display/auto-complete-filter-multiple-select-multiple-fields-display.component.d.ts.map +1 -1
  9. package/lib/src/core-components/src/components/AutoComplete/auto-complete-filter-multiple-select-multiple-fields-display/auto-complete-filter-multiple-select-multiple-fields-display.component.js +11 -11
  10. package/lib/src/core-components/src/components/AutoComplete/auto-complete-filter-single-select/auto-complete-filter-single-select.component.d.ts +26 -1
  11. package/lib/src/core-components/src/components/AutoComplete/auto-complete-filter-single-select/auto-complete-filter-single-select.component.d.ts.map +1 -1
  12. package/lib/src/core-components/src/components/AutoComplete/auto-complete-filter-single-select/auto-complete-filter-single-select.component.js +11 -11
  13. package/lib/src/core-components/src/components/AutoComplete/auto-complete-filter-single-select-multiple-fields-display/auto-complete-filter-single-select-multiple-fields-display.component.d.ts +28 -1
  14. package/lib/src/core-components/src/components/AutoComplete/auto-complete-filter-single-select-multiple-fields-display/auto-complete-filter-single-select-multiple-fields-display.component.d.ts.map +1 -1
  15. package/lib/src/core-components/src/components/AutoComplete/auto-complete-filter-single-select-multiple-fields-display/auto-complete-filter-single-select-multiple-fields-display.component.js +7 -5
  16. package/lib/src/core-components/src/components/Avatar/Avatar.d.ts +25 -1
  17. package/lib/src/core-components/src/components/Avatar/Avatar.d.ts.map +1 -1
  18. package/lib/src/core-components/src/components/Avatar/Avatar.js +4 -4
  19. package/lib/src/core-components/src/components/Badge/Badge.d.ts +17 -3
  20. package/lib/src/core-components/src/components/Badge/Badge.d.ts.map +1 -1
  21. package/lib/src/core-components/src/components/Badge/Badge.js +2 -2
  22. package/lib/src/core-components/src/components/Button1/button.component.d.ts +13 -2
  23. package/lib/src/core-components/src/components/Button1/button.component.d.ts.map +1 -1
  24. package/lib/src/core-components/src/components/Button1/button.component.js +2 -2
  25. package/lib/src/core-components/src/components/Button1/buttonGroup/buttonGroup.component.d.ts +18 -4
  26. package/lib/src/core-components/src/components/Button1/buttonGroup/buttonGroup.component.d.ts.map +1 -1
  27. package/lib/src/core-components/src/components/Button1/buttonGroup/buttonGroup.component.js +1 -1
  28. package/lib/src/core-components/src/components/Icon/Icon.d.ts +27 -12
  29. package/lib/src/core-components/src/components/Icon/Icon.d.ts.map +1 -1
  30. package/lib/src/core-components/src/components/Icon/Icon.js +8 -8
  31. package/lib/src/core-components/src/components/Loader/loader.component.d.ts +38 -1
  32. package/lib/src/core-components/src/components/Loader/loader.component.d.ts.map +1 -1
  33. package/lib/src/core-components/src/components/Loader/loader.component.js +12 -7
  34. package/lib/src/core-components/src/components/Modal/BasicModal/modal.component.d.ts +42 -2
  35. package/lib/src/core-components/src/components/Modal/BasicModal/modal.component.d.ts.map +1 -1
  36. package/lib/src/core-components/src/components/Modal/BasicModal/modal.component.js +15 -8
  37. package/lib/src/core-components/src/components/Modal/ModalDocxContent/gemini.service.d.ts +4 -0
  38. package/lib/src/core-components/src/components/Modal/ModalDocxContent/gemini.service.d.ts.map +1 -0
  39. package/lib/src/core-components/src/components/Modal/ModalDocxContent/gemini.service.js +70 -0
  40. package/lib/src/core-components/src/components/Modal/ModalDocxContent/modal-docx-content.component.d.ts +43 -0
  41. package/lib/src/core-components/src/components/Modal/ModalDocxContent/modal-docx-content.component.d.ts.map +1 -0
  42. package/lib/src/core-components/src/components/Modal/ModalDocxContent/modal-docx-content.component.js +841 -0
  43. package/lib/src/core-components/src/components/Modal/index.d.ts +1 -0
  44. package/lib/src/core-components/src/components/Modal/index.d.ts.map +1 -1
  45. package/lib/src/core-components/src/components/Modal/index.js +1 -0
  46. package/lib/src/core-components/src/components/Modal/modal-confirm/modal-confirm.component.d.ts +54 -2
  47. package/lib/src/core-components/src/components/Modal/modal-confirm/modal-confirm.component.d.ts.map +1 -1
  48. package/lib/src/core-components/src/components/Modal/modal-confirm/modal-confirm.component.js +5 -5
  49. package/lib/src/core-components/src/components/Picker/color-picker/color-picker.component.d.ts +26 -4
  50. package/lib/src/core-components/src/components/Picker/color-picker/color-picker.component.d.ts.map +1 -1
  51. package/lib/src/core-components/src/components/Picker/color-picker/color-picker.component.js +2 -2
  52. package/lib/src/core-components/src/components/Selection/multi-select/multi-select.component.d.ts +42 -1
  53. package/lib/src/core-components/src/components/Selection/multi-select/multi-select.component.d.ts.map +1 -1
  54. package/lib/src/core-components/src/components/Selection/multi-select/multi-select.component.js +4 -4
  55. package/lib/src/core-components/src/components/Selection/single-select/single-select.component.d.ts +38 -2
  56. package/lib/src/core-components/src/components/Selection/single-select/single-select.component.d.ts.map +1 -1
  57. package/lib/src/core-components/src/components/Selection/single-select/single-select.component.js +8 -6
  58. package/lib/src/core-components/src/components/Stepper2/stepper.component.d.ts +38 -2
  59. package/lib/src/core-components/src/components/Stepper2/stepper.component.d.ts.map +1 -1
  60. package/lib/src/core-components/src/components/Stepper2/stepper.component.js +12 -7
  61. package/lib/src/core-components/src/components/Tabs/tabs.component.d.ts +33 -3
  62. package/lib/src/core-components/src/components/Tabs/tabs.component.d.ts.map +1 -1
  63. package/lib/src/core-components/src/components/Tabs/tabs.component.js +8 -5
  64. package/lib/src/core-components/src/components/Tags1/Tags.component.d.ts +46 -1
  65. package/lib/src/core-components/src/components/Tags1/Tags.component.d.ts.map +1 -1
  66. package/lib/src/core-components/src/components/Tags1/Tags.component.js +5 -5
  67. package/lib/src/core-components/src/components/Tooltip/tooltip.component.d.ts +21 -2
  68. package/lib/src/core-components/src/components/Tooltip/tooltip.component.d.ts.map +1 -1
  69. package/lib/src/core-components/src/components/Tooltip/tooltip.component.js +2 -2
  70. package/lib/src/core-components/src/tc.global.css +4 -2
  71. package/lib/src/core-components/src/tc.module.css +2 -2
  72. package/lib/src/core-components/src/utils/index.d.ts +1 -0
  73. package/lib/src/core-components/src/utils/index.d.ts.map +1 -1
  74. package/lib/src/core-components/src/utils/index.js +1 -0
  75. package/lib/src/core-components/src/utils/jodit-editor-config.util.d.ts +189 -0
  76. package/lib/src/core-components/src/utils/jodit-editor-config.util.d.ts.map +1 -0
  77. package/lib/src/core-components/src/utils/jodit-editor-config.util.js +333 -0
  78. package/package.json +3 -1
@@ -0,0 +1,841 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useEffect, useState, useRef, useCallback } from 'react';
3
+ import { createJoditConfig, JODIT_PASTE_DIALOG_HANDLER, JODIT_TOOLBAR_BUTTONS, cn, } from '../../../utils';
4
+ import JoditEditor from 'jodit-react';
5
+ import 'jodit/esm/plugins/resizer/resizer';
6
+ import { Icon } from '../..';
7
+ import SpeechRecognition, { useSpeechRecognition, } from 'react-speech-recognition';
8
+ import s from '../../../tc.module.css';
9
+ import { geminiSendMessageService } from './gemini.service';
10
+ // CSS-in-JS styles for enhanced visual design
11
+ const modalStyles = {
12
+ // Glass-morphism overlay
13
+ overlay: {
14
+ background: 'linear-gradient(135deg, rgba(15, 23, 42, 0.85) 0%, rgba(30, 41, 59, 0.9) 100%)',
15
+ backdropFilter: 'blur(8px)',
16
+ },
17
+ // Modal container with subtle gradient border
18
+ container: {
19
+ background: 'linear-gradient(180deg, #ffffff 0%, #f8fafc 100%)',
20
+ boxShadow: '0 25px 50px -12px rgba(0, 0, 0, 0.25), 0 0 0 1px rgba(148, 163, 184, 0.1)',
21
+ },
22
+ // Header gradient
23
+ header: {
24
+ background: 'linear-gradient(135deg, #1e293b 0%, #334155 100%)',
25
+ borderBottom: '1px solid rgba(148, 163, 184, 0.2)',
26
+ },
27
+ // Action button base styles
28
+ actionButton: {
29
+ transition: 'all 0.2s cubic-bezier(0.4, 0, 0.2, 1)',
30
+ },
31
+ // Footer with subtle top border
32
+ footer: {
33
+ background: 'linear-gradient(180deg, #f8fafc 0%, #f1f5f9 100%)',
34
+ borderTop: '1px solid rgba(148, 163, 184, 0.3)',
35
+ },
36
+ };
37
+ // Keyframe animations
38
+ const keyframes = `
39
+ @keyframes modalSlideIn {
40
+ from {
41
+ opacity: 0;
42
+ transform: scale(0.95) translateY(-20px);
43
+ }
44
+ to {
45
+ opacity: 1;
46
+ transform: scale(1) translateY(0);
47
+ }
48
+ }
49
+
50
+ @keyframes overlayFadeIn {
51
+ from { opacity: 0; }
52
+ to { opacity: 1; }
53
+ }
54
+
55
+ @keyframes pulseGlow {
56
+ 0%, 100% {
57
+ box-shadow: 0 0 0 0 rgba(99, 102, 241, 0.4);
58
+ }
59
+ 50% {
60
+ box-shadow: 0 0 20px 8px rgba(99, 102, 241, 0.2);
61
+ }
62
+ }
63
+
64
+ @keyframes recordingPulse {
65
+ 0%, 100% {
66
+ transform: scale(1);
67
+ box-shadow: 0 0 0 0 rgba(220, 38, 38, 0.4);
68
+ }
69
+ 50% {
70
+ transform: scale(1.05);
71
+ box-shadow: 0 0 20px 8px rgba(220, 38, 38, 0.2);
72
+ }
73
+ }
74
+
75
+ @keyframes ai-pulse {
76
+ 0%, 100% {
77
+ transform: scale(1);
78
+ box-shadow: 0 0 20px rgba(99, 102, 241, 0.6), 0 0 40px rgba(168, 85, 247, 0.4);
79
+ }
80
+ 50% {
81
+ transform: scale(1.1);
82
+ box-shadow: 0 0 30px rgba(99, 102, 241, 0.8), 0 0 60px rgba(168, 85, 247, 0.6);
83
+ }
84
+ }
85
+
86
+ @keyframes ai-text-pulse {
87
+ 0%, 100% { opacity: 1; }
88
+ 50% { opacity: 0.7; }
89
+ }
90
+
91
+ @keyframes shimmer {
92
+ 0% { background-position: -200% 0; }
93
+ 100% { background-position: 200% 0; }
94
+ }
95
+
96
+ @keyframes spin {
97
+ from { transform: rotate(0deg); }
98
+ to { transform: rotate(360deg); }
99
+ }
100
+ `;
101
+ export const ModalDocxContent = ({ visible, details = '', onSave, onClose, title = 'Document Editor', aiService = (message, gemini_key) => geminiSendMessageService(message, gemini_key), showAIButton = true, showVoiceButtons = true, uploaderUrl = 'https://www.tech-abl.com/api/assets/uploadFile', folder = 'library', gemini_key = 'xxx', classNames = {}, styles = {}, }) => {
102
+ const joditEditorRef = useRef(null);
103
+ const content = useRef('');
104
+ const [showModal, setShowModal] = useState(visible);
105
+ const parentDivRef = useRef(null);
106
+ const [parentDivWidth, setParentDivWidth] = useState(0);
107
+ const [parentDivHeight, setParentDivHeight] = useState(0);
108
+ // AI processing state
109
+ const [isAIProcessing, setIsAIProcessing] = useState(false);
110
+ // Store selection info (text + occurrence index)
111
+ const selectionInfoRef = useRef(null);
112
+ // Helper: Get selected text and find which occurrence it is
113
+ const getSelectedText = useCallback(() => {
114
+ const editor = joditEditorRef.current;
115
+ if (!editor)
116
+ return '';
117
+ selectionInfoRef.current = null; // Reset
118
+ try {
119
+ const selection = window.getSelection();
120
+ if (selection && selection.rangeCount > 0 && !selection.isCollapsed) {
121
+ const selectedText = selection.toString().trim();
122
+ if (selectedText) {
123
+ // Get the full text content of the editor
124
+ const editorElement = editor.editor || editor.container;
125
+ const fullText = editorElement?.textContent || '';
126
+ // Find the position of selection in the text
127
+ const range = selection.getRangeAt(0);
128
+ // Calculate text offset from start of editor
129
+ let textOffset = 0;
130
+ const treeWalker = document.createTreeWalker(editorElement, NodeFilter.SHOW_TEXT, null);
131
+ let node;
132
+ while ((node = treeWalker.nextNode())) {
133
+ if (node === range.startContainer) {
134
+ textOffset += range.startOffset;
135
+ break;
136
+ }
137
+ textOffset += node.textContent?.length || 0;
138
+ }
139
+ // Count which occurrence this is
140
+ let occurrenceIndex = 0;
141
+ let searchPos = 0;
142
+ const lowerText = fullText.toLowerCase();
143
+ const lowerSelected = selectedText.toLowerCase();
144
+ while (searchPos < textOffset) {
145
+ const foundPos = lowerText.indexOf(lowerSelected, searchPos);
146
+ if (foundPos === -1 || foundPos >= textOffset)
147
+ break;
148
+ occurrenceIndex++;
149
+ searchPos = foundPos + 1;
150
+ }
151
+ selectionInfoRef.current = {
152
+ text: selectedText,
153
+ occurrenceIndex: occurrenceIndex,
154
+ };
155
+ console.log('[getSelectedText] Found selection:', {
156
+ text: selectedText,
157
+ occurrenceIndex,
158
+ textOffset,
159
+ });
160
+ return selectedText;
161
+ }
162
+ }
163
+ }
164
+ catch (error) {
165
+ console.error('[getSelectedText] Error:', error);
166
+ }
167
+ console.log('[getSelectedText] No selection found');
168
+ return '';
169
+ }, []);
170
+ // Helper: Replace the specific occurrence of text in editor content
171
+ const replaceTextInContent = useCallback((originalText, newHtml, occurrenceIndex) => {
172
+ const editor = joditEditorRef.current;
173
+ if (!editor || !originalText)
174
+ return false;
175
+ try {
176
+ const currentHtml = editor.value;
177
+ // Escape special regex characters in the original text
178
+ const escapedText = originalText.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
179
+ // Find all occurrences and replace the specific one
180
+ const regex = new RegExp(escapedText, 'gi');
181
+ let matchIndex = 0;
182
+ const newContent = currentHtml.replace(regex, (match) => {
183
+ if (matchIndex === occurrenceIndex) {
184
+ matchIndex++;
185
+ // console.log(
186
+ // `[replaceTextInContent] Replacing occurrence ${occurrenceIndex}`,
187
+ // );
188
+ return newHtml;
189
+ }
190
+ matchIndex++;
191
+ return match; // Keep other occurrences unchanged
192
+ });
193
+ if (matchIndex > occurrenceIndex) {
194
+ editor.value = newContent;
195
+ // console.log('[replaceTextInContent] Replaced successfully');
196
+ return true;
197
+ }
198
+ // console.log('[replaceTextInContent] Occurrence not found');
199
+ return false;
200
+ }
201
+ catch (error) {
202
+ // console.error('[replaceTextInContent] Error:', error);
203
+ return false;
204
+ }
205
+ }, []);
206
+ // Handle AI button click
207
+ const handleAIButtonClick = useCallback(async () => {
208
+ if (isAIProcessing || !content.current.trim())
209
+ return;
210
+ // Get selected text BEFORE async call (also saves occurrence index)
211
+ const selectedText = getSelectedText();
212
+ const hasSelection = selectedText.length > 0;
213
+ const selectionInfo = selectionInfoRef.current;
214
+ const textToProcess = hasSelection ? selectedText : content.current.trim();
215
+ setIsAIProcessing(true);
216
+ try {
217
+ const response = await aiService(textToProcess, gemini_key);
218
+ if (response?.text && joditEditorRef.current) {
219
+ const aiResponse = `<span style="color: #6366f1; font-style: italic;">${response.text}</span>`;
220
+ if (hasSelection && selectionInfo) {
221
+ // Replace the specific occurrence using saved index
222
+ replaceTextInContent(selectedText, aiResponse, selectionInfo.occurrenceIndex);
223
+ content.current = joditEditorRef.current.value;
224
+ }
225
+ else {
226
+ // Replace all content with AI response (original behavior)
227
+ joditEditorRef.current.value = aiResponse;
228
+ content.current = aiResponse;
229
+ }
230
+ joditEditorRef.current.focus();
231
+ }
232
+ }
233
+ catch (error) {
234
+ console.error('AI Error:', error);
235
+ }
236
+ finally {
237
+ setIsAIProcessing(false);
238
+ }
239
+ // eslint-disable-next-line react-hooks/exhaustive-deps
240
+ }, [isAIProcessing, aiService]);
241
+ // Speech recognition
242
+ const { transcript, listening, resetTranscript, browserSupportsSpeechRecognition, } = useSpeechRecognition();
243
+ const [lastTranscript, setLastTranscript] = useState('');
244
+ // Local state to track if THIS modal started recording (not global listening state)
245
+ const [isRecording, setIsRecording] = useState(false);
246
+ // Ref to track recording state for event handlers (avoids stale closure issues)
247
+ const isRecordingRef = useRef(false);
248
+ // Track if manual mode is active (continuous listening without auto-stop)
249
+ const isManualModeRef = useRef(false);
250
+ // State for UI rendering of manual mode indicator
251
+ const [isManualMode, setIsManualMode] = useState(false);
252
+ // Auto-stop silence timer ref
253
+ const silenceTimerRef = useRef(null);
254
+ // Track last transcript change time for auto-stop
255
+ const lastTranscriptTimeRef = useRef(Date.now());
256
+ // Watchdog interval ref for manual mode (checks every 1 sec if recording is still active)
257
+ const watchdogIntervalRef = useRef(null);
258
+ // Ref to track listening state (avoids stale closure in interval)
259
+ const listeningRef = useRef(listening);
260
+ // Cleanup: Stop listening when component unmounts
261
+ useEffect(() => {
262
+ setTimeout(() => {
263
+ SpeechRecognition.stopListening();
264
+ resetTranscript();
265
+ isRecordingRef.current = false;
266
+ setIsRecording(false);
267
+ isManualModeRef.current = false;
268
+ setIsManualMode(false);
269
+ if (silenceTimerRef.current) {
270
+ clearTimeout(silenceTimerRef.current);
271
+ silenceTimerRef.current = null;
272
+ }
273
+ if (watchdogIntervalRef.current) {
274
+ clearInterval(watchdogIntervalRef.current);
275
+ watchdogIntervalRef.current = null;
276
+ }
277
+ }, 1000);
278
+ // eslint-disable-next-line react-hooks/exhaustive-deps
279
+ }, []);
280
+ // Keep listeningRef in sync with listening state
281
+ useEffect(() => {
282
+ listeningRef.current = listening;
283
+ }, [listening]);
284
+ // Restart counter for debugging
285
+ const restartCountRef = useRef(0);
286
+ // Last forced restart time
287
+ const lastForceRestartRef = useRef(Date.now());
288
+ // Soft restart speech recognition - only called when browser stopped listening
289
+ // Does NOT abort - just starts listening again to avoid losing words
290
+ const softRestartRecognition = useCallback(() => {
291
+ if (!isManualModeRef.current || !isRecordingRef.current)
292
+ return;
293
+ restartCountRef.current += 1;
294
+ console.log(`[Watchdog ${new Date().toLocaleTimeString()}] Soft restart #${restartCountRef.current} (browser had stopped)`);
295
+ // Don't abort - just start listening directly
296
+ // This avoids losing any words that might be in the buffer
297
+ SpeechRecognition.startListening({
298
+ continuous: true,
299
+ interimResults: true,
300
+ language: 'en-US',
301
+ });
302
+ lastForceRestartRef.current = Date.now();
303
+ }, []);
304
+ // Start watchdog function - Only restart when browser stops listening
305
+ const startWatchdog = useCallback(() => {
306
+ // Clear any existing watchdog first
307
+ if (watchdogIntervalRef.current) {
308
+ clearInterval(watchdogIntervalRef.current);
309
+ watchdogIntervalRef.current = null;
310
+ }
311
+ restartCountRef.current = 0;
312
+ lastForceRestartRef.current = Date.now();
313
+ // Create persistent watchdog that runs every 1 second
314
+ watchdogIntervalRef.current = setInterval(() => {
315
+ // Only restart if: manual mode active, recording on, AND browser stopped listening
316
+ // Don't force restart while listening - this would lose words
317
+ if (isManualModeRef.current &&
318
+ isRecordingRef.current &&
319
+ !listeningRef.current) {
320
+ console.log(`[Watchdog ${new Date().toLocaleTimeString()}] Browser stopped - restarting`);
321
+ softRestartRecognition();
322
+ }
323
+ }, 1000);
324
+ console.log('[Watchdog] Started - will restart only when browser stops');
325
+ }, [softRestartRecognition]);
326
+ // Stop watchdog function
327
+ const stopWatchdog = useCallback(() => {
328
+ if (watchdogIntervalRef.current) {
329
+ clearInterval(watchdogIntervalRef.current);
330
+ watchdogIntervalRef.current = null;
331
+ console.log('[Watchdog] Stopped');
332
+ }
333
+ }, []);
334
+ // Listen for speech recognition end/error events when recording starts
335
+ useEffect(() => {
336
+ if (!isRecording)
337
+ return;
338
+ const recognition = SpeechRecognition.getRecognition();
339
+ if (!recognition)
340
+ return;
341
+ const handleEnd = () => {
342
+ // Use ref to get current recording state (avoids stale closure)
343
+ // If recording was stopped (by silence timer or user), don't restart
344
+ if (!isRecordingRef.current) {
345
+ console.log('[handleEnd] Recording stopped - not restarting');
346
+ return;
347
+ }
348
+ // In manual mode, always restart listening
349
+ if (isManualModeRef.current) {
350
+ console.log('[handleEnd] Manual mode - restarting');
351
+ setTimeout(() => {
352
+ if (isManualModeRef.current && isRecordingRef.current) {
353
+ SpeechRecognition.startListening({
354
+ continuous: true,
355
+ interimResults: true,
356
+ language: 'en-US',
357
+ });
358
+ }
359
+ }, 100);
360
+ }
361
+ else if (isRecordingRef.current) {
362
+ // In auto-stop mode, only restart if still recording (browser stopped unexpectedly)
363
+ console.log('[handleEnd] Auto-stop mode - browser stopped, restarting');
364
+ setTimeout(() => {
365
+ if (!isManualModeRef.current && isRecordingRef.current) {
366
+ SpeechRecognition.startListening({
367
+ continuous: true,
368
+ interimResults: true,
369
+ language: 'en-US',
370
+ });
371
+ }
372
+ }, 100);
373
+ }
374
+ };
375
+ const handleError = () => {
376
+ // Use ref to get current recording state (avoids stale closure)
377
+ if (!isRecordingRef.current)
378
+ return;
379
+ // In manual mode, try to restart on error
380
+ if (isManualModeRef.current) {
381
+ setTimeout(() => {
382
+ if (isManualModeRef.current && isRecordingRef.current) {
383
+ SpeechRecognition.startListening({
384
+ continuous: true,
385
+ interimResults: true,
386
+ language: 'en-US',
387
+ });
388
+ }
389
+ }, 100);
390
+ }
391
+ else {
392
+ // In auto-stop mode, restart on error as well
393
+ setTimeout(() => {
394
+ if (!isManualModeRef.current && isRecordingRef.current) {
395
+ SpeechRecognition.startListening({
396
+ continuous: true,
397
+ interimResults: true,
398
+ language: 'en-US',
399
+ });
400
+ }
401
+ }, 100);
402
+ }
403
+ };
404
+ recognition.addEventListener('end', handleEnd);
405
+ recognition.addEventListener('error', handleError);
406
+ return () => {
407
+ recognition.removeEventListener('end', handleEnd);
408
+ recognition.removeEventListener('error', handleError);
409
+ };
410
+ }, [isRecording]);
411
+ // Handle transcript changes - exactly like reference implementation
412
+ useEffect(() => {
413
+ if (!joditEditorRef.current)
414
+ return;
415
+ // Track changes like reference: https://github.com/RahulSM2002/SpeechRecognition/blob/main/src/components/SpeechToTextField.tsx
416
+ if (transcript !== lastTranscript) {
417
+ try {
418
+ const editor = joditEditorRef.current;
419
+ editor.focus();
420
+ // Get only the new text that was added (incremental update)
421
+ const newText = transcript.slice(lastTranscript.length);
422
+ if (newText) {
423
+ // Remove newlines to ensure single line
424
+ const cleanText = newText.replace(/\n/g, ' ').replace(/\r/g, '');
425
+ if (cleanText) {
426
+ // Move cursor to end of editor content
427
+ editor.selection.setCursorIn(editor.editor, false);
428
+ // Check if there's existing text and add space before first speech-to-text input
429
+ const currentValue = editor.value || '';
430
+ const hasExistingText = currentValue.trim().length > 0;
431
+ const isFirstInsert = lastTranscript.length === 0;
432
+ // Add space if there's existing text and this is the first insert after starting recording
433
+ // This ensures proper spacing when speech-to-text starts with existing content
434
+ const textToInsert = hasExistingText && isFirstInsert && !currentValue.endsWith(' ')
435
+ ? ' ' + cleanText
436
+ : cleanText;
437
+ // Use JoditEditor's insertHTML to insert plain text inline
438
+ // Escape HTML to ensure it's treated as plain text
439
+ const escapedText = textToInsert
440
+ .replace(/&/g, '&amp;')
441
+ .replace(/</g, '&lt;')
442
+ .replace(/>/g, '&gt;');
443
+ editor.selection.insertHTML(escapedText);
444
+ // Move cursor to end after insertion
445
+ editor.selection.setCursorIn(editor.editor, false);
446
+ // Update content ref
447
+ content.current = editor.value;
448
+ // Reset silence timer in auto-stop mode when new words are detected
449
+ if (!isManualModeRef.current && isRecordingRef.current) {
450
+ lastTranscriptTimeRef.current = Date.now();
451
+ // Restart the silence timer
452
+ if (silenceTimerRef.current) {
453
+ clearTimeout(silenceTimerRef.current);
454
+ }
455
+ silenceTimerRef.current = setTimeout(() => {
456
+ if (!isManualModeRef.current && isRecordingRef.current) {
457
+ console.log('[Auto-Stop] 5 seconds of silence - stopping');
458
+ isRecordingRef.current = false; // Set ref BEFORE stopping
459
+ setIsRecording(false);
460
+ SpeechRecognition.stopListening();
461
+ }
462
+ }, 5000); // 5 seconds of silence before auto-stop
463
+ }
464
+ }
465
+ }
466
+ setLastTranscript(transcript);
467
+ }
468
+ catch (error) {
469
+ console.error('Error updating transcript:', error);
470
+ }
471
+ }
472
+ }, [transcript, lastTranscript, isRecording]);
473
+ // Auto-stop silence detection constant (5 seconds of silence - increased for better experience)
474
+ const AUTO_STOP_SILENCE_DURATION = 5000;
475
+ // Function to start silence detection timer for auto-stop mode
476
+ const startSilenceTimer = useCallback(() => {
477
+ // Clear any existing timer
478
+ if (silenceTimerRef.current) {
479
+ clearTimeout(silenceTimerRef.current);
480
+ }
481
+ // Start new timer - will stop recording after silence duration
482
+ silenceTimerRef.current = setTimeout(() => {
483
+ if (!isManualModeRef.current && isRecordingRef.current) {
484
+ console.log('[Auto-Stop] Silence timer expired - stopping');
485
+ isRecordingRef.current = false; // Set ref BEFORE stopping to prevent handleEnd restart
486
+ setIsRecording(false);
487
+ SpeechRecognition.stopListening();
488
+ }
489
+ }, AUTO_STOP_SILENCE_DURATION);
490
+ }, []);
491
+ // Handle auto-stop recording (stops automatically after 3 seconds of silence)
492
+ const handleAutoStopRecordClick = useCallback(() => {
493
+ if (!browserSupportsSpeechRecognition) {
494
+ return;
495
+ }
496
+ if (isRecording) {
497
+ isManualModeRef.current = false;
498
+ setIsManualMode(false);
499
+ if (silenceTimerRef.current) {
500
+ clearTimeout(silenceTimerRef.current);
501
+ silenceTimerRef.current = null;
502
+ }
503
+ SpeechRecognition.stopListening();
504
+ isRecordingRef.current = false;
505
+ setIsRecording(false);
506
+ }
507
+ else {
508
+ isManualModeRef.current = false; // Auto-stop mode
509
+ setIsManualMode(false);
510
+ lastTranscriptTimeRef.current = Date.now();
511
+ resetTranscript();
512
+ setLastTranscript('');
513
+ SpeechRecognition.startListening({
514
+ continuous: true,
515
+ interimResults: true,
516
+ language: 'en-US',
517
+ });
518
+ isRecordingRef.current = true;
519
+ setIsRecording(true);
520
+ // Start silence detection timer
521
+ startSilenceTimer();
522
+ }
523
+ }, [
524
+ isRecording,
525
+ resetTranscript,
526
+ browserSupportsSpeechRecognition,
527
+ startSilenceTimer,
528
+ ]);
529
+ // Handle manual-stop recording (requires manual stop - continuous listening)
530
+ const handleManualStopRecordClick = useCallback(() => {
531
+ if (!browserSupportsSpeechRecognition) {
532
+ return;
533
+ }
534
+ if (isRecording) {
535
+ // STOP recording
536
+ isManualModeRef.current = false;
537
+ setIsManualMode(false);
538
+ // Stop watchdog
539
+ stopWatchdog();
540
+ // Clear any silence timer
541
+ if (silenceTimerRef.current) {
542
+ clearTimeout(silenceTimerRef.current);
543
+ silenceTimerRef.current = null;
544
+ }
545
+ SpeechRecognition.stopListening();
546
+ isRecordingRef.current = false;
547
+ setIsRecording(false);
548
+ console.log('[Manual Mode] Stopped by user');
549
+ }
550
+ else {
551
+ // START recording
552
+ isManualModeRef.current = true;
553
+ setIsManualMode(true);
554
+ isRecordingRef.current = true;
555
+ setIsRecording(true);
556
+ // Clear any silence timer (not used in manual mode)
557
+ if (silenceTimerRef.current) {
558
+ clearTimeout(silenceTimerRef.current);
559
+ silenceTimerRef.current = null;
560
+ }
561
+ resetTranscript();
562
+ setLastTranscript('');
563
+ // Start listening
564
+ SpeechRecognition.startListening({
565
+ continuous: true,
566
+ interimResults: true,
567
+ language: 'en-US',
568
+ });
569
+ // Start watchdog to keep it running forever
570
+ startWatchdog();
571
+ console.log('[Manual Mode] Started - will run until manually stopped');
572
+ }
573
+ }, [
574
+ isRecording,
575
+ resetTranscript,
576
+ browserSupportsSpeechRecognition,
577
+ startWatchdog,
578
+ stopWatchdog,
579
+ ]);
580
+ useEffect(() => {
581
+ if (parentDivRef.current) {
582
+ const { width, height } = parentDivRef.current.getBoundingClientRect();
583
+ setParentDivWidth(width - 16);
584
+ setParentDivHeight(height - 120);
585
+ }
586
+ }, [showModal]);
587
+ useEffect(() => {
588
+ content.current = details || '';
589
+ setShowModal(visible);
590
+ // Stop any ongoing speech recognition and reset state when modal opens/closes
591
+ // Always reset local recording state
592
+ isManualModeRef.current = false;
593
+ setIsManualMode(false);
594
+ if (silenceTimerRef.current) {
595
+ clearTimeout(silenceTimerRef.current);
596
+ silenceTimerRef.current = null;
597
+ }
598
+ SpeechRecognition.stopListening();
599
+ resetTranscript();
600
+ setLastTranscript('');
601
+ isRecordingRef.current = false;
602
+ setIsRecording(false);
603
+ // eslint-disable-next-line react-hooks/exhaustive-deps
604
+ }, [visible, details]);
605
+ const config = createJoditConfig({
606
+ height: parentDivHeight,
607
+ width: window.innerWidth / 1.24,
608
+ disabled: false,
609
+ buttons: JODIT_TOOLBAR_BUTTONS,
610
+ events: {
611
+ afterInit: (instance) => {
612
+ joditEditorRef.current = instance;
613
+ },
614
+ afterOpenPasteDialog: JODIT_PASTE_DIALOG_HANDLER.afterOpenPasteDialog,
615
+ },
616
+ uploader: {
617
+ url: uploaderUrl,
618
+ prepareData: function (data) {
619
+ data.append('folder', folder);
620
+ data.delete('path');
621
+ data.delete('source');
622
+ },
623
+ isSuccess: function (resp) {
624
+ console.log({ resp });
625
+ if (joditEditorRef.current) {
626
+ joditEditorRef.current.selection.insertHTML(`<img src=${resp?.url} alt="logo" style="width:100%;height:auto"/>`);
627
+ }
628
+ },
629
+ },
630
+ }, false // Disable speech recognition and remove speech button
631
+ );
632
+ return (_jsxs(_Fragment, { children: [_jsx("style", { children: keyframes }), showModal && (_jsx(_Fragment, { children: _jsx("div", { className: cn(s['fixed'], s['inset-0'], s['z-50'], s['flex'], s['justify-center'], s['items-center'], s['overflow-hidden'], classNames.overlay), style: {
633
+ ...modalStyles.overlay,
634
+ animation: 'overlayFadeIn 0.3s ease-out forwards',
635
+ ...styles.overlay,
636
+ }, children: _jsx("div", { ref: parentDivRef, className: cn(s['relative'], s['w-auto'], s['mx-auto'], classNames.container), style: {
637
+ width: `${config.width + 40}px`,
638
+ animation: 'modalSlideIn 0.4s cubic-bezier(0.16, 1, 0.3, 1) forwards',
639
+ ...styles.container,
640
+ }, children: _jsxs("div", { className: cn(s['relative'], s['flex'], s['flex-col'], s['w-full'], s['overflow-hidden']), style: {
641
+ ...modalStyles.container,
642
+ borderRadius: '24px',
643
+ height: window.outerHeight / 1.3,
644
+ }, children: [_jsxs("div", { className: cn(s['flex'], s['items-center'], s['justify-between'], s['p-4'], classNames.header), style: {
645
+ ...modalStyles.header,
646
+ borderRadius: '24px 24px 0 0',
647
+ padding: '16px 24px',
648
+ ...styles.header,
649
+ }, children: [_jsxs("div", { className: cn(s['flex'], s['items-center']), style: { gap: '12px' }, children: [_jsx("div", { className: cn(s['flex'], s['items-center'], s['justify-center']), style: {
650
+ width: '40px',
651
+ height: '40px',
652
+ borderRadius: '12px',
653
+ background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)',
654
+ boxShadow: '0 4px 12px rgba(99, 102, 241, 0.3)',
655
+ }, children: _jsx(Icon, { nameIcon: "IoDocumentTextOutline", propsIcon: { color: '#ffffff', size: 22 } }) }), _jsx("h3", { className: cn(s['text-xl'], s['font-semibold']), style: {
656
+ color: '#f8fafc',
657
+ fontWeight: 700,
658
+ letterSpacing: '-0.025em',
659
+ }, children: title })] }), _jsxs("div", { className: cn(s['flex'], s['items-center']), style: { gap: '12px' }, children: [isRecording && (_jsxs("div", { className: cn(s['flex'], s['items-center']), style: {
660
+ gap: '8px',
661
+ padding: '8px 16px',
662
+ borderRadius: '9999px',
663
+ background: isManualMode
664
+ ? 'linear-gradient(135deg, rgba(220, 38, 38, 0.2) 0%, rgba(185, 28, 28, 0.2) 100%)'
665
+ : 'linear-gradient(135deg, rgba(34, 197, 94, 0.2) 0%, rgba(22, 163, 74, 0.2) 100%)',
666
+ border: `1px solid ${isManualMode ? 'rgba(220, 38, 38, 0.4)' : 'rgba(34, 197, 94, 0.4)'}`,
667
+ }, children: [_jsx("span", { style: {
668
+ width: '10px',
669
+ height: '10px',
670
+ borderRadius: '50%',
671
+ background: isManualMode ? '#dc2626' : '#22c55e',
672
+ animation: 'pulseGlow 1.5s ease-in-out infinite',
673
+ } }), _jsx("span", { className: cn(s['text-sm']), style: {
674
+ fontWeight: 500,
675
+ color: isManualMode ? '#fca5a5' : '#86efac',
676
+ }, children: isManualMode ? 'Continuous Mode' : 'Auto-Stop Mode' })] })), showAIButton && (_jsx("button", { className: cn(s['relative'], s['flex'], s['items-center'], s['justify-center'], s['border-0'], s['outline-none'], s['focus:outline-none']), style: {
677
+ width: '44px',
678
+ height: '44px',
679
+ borderRadius: '12px',
680
+ cursor: 'pointer',
681
+ background: 'linear-gradient(135deg, #6366f1 0%, #a855f7 100%)',
682
+ boxShadow: isAIProcessing
683
+ ? '0 0 20px rgba(99, 102, 241, 0.6), 0 0 40px rgba(168, 85, 247, 0.4)'
684
+ : '0 4px 15px rgba(99, 102, 241, 0.4)',
685
+ animation: isAIProcessing
686
+ ? 'ai-pulse 1.5s ease-in-out infinite'
687
+ : 'none',
688
+ ...modalStyles.actionButton,
689
+ }, onClick: handleAIButtonClick, disabled: isAIProcessing, title: isAIProcessing
690
+ ? 'AI is processing...'
691
+ : 'Ask AI to enhance content', children: _jsx("span", { className: cn(s['font-bold']), style: {
692
+ fontSize: '14px',
693
+ color: '#ffffff',
694
+ animation: isAIProcessing
695
+ ? 'ai-text-pulse 1.5s ease-in-out infinite'
696
+ : 'none',
697
+ }, children: "AI" }) })), showVoiceButtons && browserSupportsSpeechRecognition && (_jsxs(_Fragment, { children: [_jsx("button", { className: cn(s['flex'], s['items-center'], s['justify-center'], s['border-0'], s['outline-none'], s['focus:outline-none']), style: {
698
+ width: '44px',
699
+ height: '44px',
700
+ borderRadius: '12px',
701
+ cursor: 'pointer',
702
+ background: isRecording && !isManualMode
703
+ ? 'linear-gradient(135deg, #22c55e 0%, #16a34a 100%)'
704
+ : 'linear-gradient(135deg, #4ade80 0%, #22c55e 100%)',
705
+ boxShadow: isRecording && !isManualMode
706
+ ? '0 0 20px rgba(34, 197, 94, 0.5), 0 0 40px rgba(34, 197, 94, 0.2)'
707
+ : '0 4px 12px rgba(34, 197, 94, 0.3)',
708
+ animation: isRecording && !isManualMode
709
+ ? 'pulseGlow 1.5s ease-in-out infinite'
710
+ : 'none',
711
+ ...modalStyles.actionButton,
712
+ }, onClick: handleAutoStopRecordClick, title: "Auto Stop - Stops automatically after 5 seconds of silence", children: _jsx(Icon, { nameIcon: isRecording && !isManualMode
713
+ ? 'IoMic'
714
+ : 'IoMicOutline', propsIcon: { color: '#ffffff', size: 22 } }) }), _jsx("button", { className: cn(s['flex'], s['items-center'], s['justify-center'], s['border-0'], s['outline-none'], s['focus:outline-none']), style: {
715
+ width: '44px',
716
+ height: '44px',
717
+ borderRadius: '12px',
718
+ cursor: 'pointer',
719
+ background: isRecording && isManualMode
720
+ ? 'linear-gradient(135deg, #dc2626 0%, #b91c1c 100%)'
721
+ : 'linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%)',
722
+ boxShadow: isRecording && isManualMode
723
+ ? '0 0 20px rgba(220, 38, 38, 0.5), 0 0 40px rgba(220, 38, 38, 0.2)'
724
+ : '0 4px 12px rgba(139, 92, 246, 0.3)',
725
+ animation: isRecording && isManualMode
726
+ ? 'recordingPulse 1.5s ease-in-out infinite'
727
+ : 'none',
728
+ ...modalStyles.actionButton,
729
+ }, onClick: handleManualStopRecordClick, title: "Continuous Mode - Records until manually stopped", children: _jsx(Icon, { nameIcon: isRecording && isManualMode
730
+ ? 'IoMic'
731
+ : 'IoMicOutline', propsIcon: { color: '#ffffff', size: 22 } }) })] })), _jsx("div", { style: {
732
+ width: '1px',
733
+ height: '32px',
734
+ margin: '0 4px',
735
+ background: 'rgba(148, 163, 184, 0.3)',
736
+ } }), _jsx("button", { className: cn(s['flex'], s['items-center'], s['justify-center'], s['outline-none'], s['focus:outline-none']), style: {
737
+ width: '44px',
738
+ height: '44px',
739
+ borderRadius: '12px',
740
+ cursor: 'pointer',
741
+ background: 'rgba(239, 68, 68, 0.1)',
742
+ border: '1px solid rgba(239, 68, 68, 0.3)',
743
+ ...modalStyles.actionButton,
744
+ }, onClick: () => {
745
+ isManualModeRef.current = false;
746
+ setIsManualMode(false);
747
+ stopWatchdog();
748
+ if (silenceTimerRef.current) {
749
+ clearTimeout(silenceTimerRef.current);
750
+ silenceTimerRef.current = null;
751
+ }
752
+ SpeechRecognition.stopListening();
753
+ resetTranscript();
754
+ setLastTranscript('');
755
+ isRecordingRef.current = false;
756
+ setIsRecording(false);
757
+ setShowModal(false);
758
+ onClose && onClose();
759
+ }, onMouseEnter: (e) => {
760
+ e.currentTarget.style.background =
761
+ 'rgba(239, 68, 68, 0.2)';
762
+ }, onMouseLeave: (e) => {
763
+ e.currentTarget.style.background =
764
+ 'rgba(239, 68, 68, 0.1)';
765
+ }, title: "Close", children: _jsx(Icon, { nameIcon: "IoCloseOutline", propsIcon: { color: '#f87171', size: 22 } }) })] })] }), _jsx("div", { id: "editorContent", className: cn(s['relative'], s['flex-1'], classNames.body), style: {
766
+ width: '100%',
767
+ overflow: 'hidden',
768
+ padding: '16px',
769
+ background: '#ffffff',
770
+ ...styles.body,
771
+ }, children: _jsx(JoditEditor, { ref: (editor) => {
772
+ joditEditorRef.current = editor;
773
+ }, value: content.current || '', config: {
774
+ ...config,
775
+ width: '100%',
776
+ height: window.outerHeight / 1.5 - 130,
777
+ }, onBlur: (newContent) => {
778
+ content.current = newContent;
779
+ } }) }), _jsxs("div", { className: cn(s['flex'], s['items-center'], s['justify-between'], s['p-4'], classNames.footer), style: {
780
+ ...modalStyles.footer,
781
+ borderRadius: '0 0 24px 24px',
782
+ padding: '16px 24px',
783
+ ...styles.footer,
784
+ }, children: [_jsx("div", { className: cn(s['flex'], s['items-center']), style: { gap: '8px' }, children: _jsxs("span", { style: { fontSize: '12px', color: '#94a3b8' }, children: ["Press", ' ', _jsx("kbd", { style: {
785
+ padding: '2px 6px',
786
+ borderRadius: '4px',
787
+ fontSize: '12px',
788
+ fontFamily: 'monospace',
789
+ background: 'rgba(148, 163, 184, 0.2)',
790
+ border: '1px solid rgba(148, 163, 184, 0.3)',
791
+ color: '#64748b',
792
+ }, children: "Esc" }), ' ', "to close"] }) }), _jsxs("div", { className: cn(s['flex'], s['items-center']), style: { gap: '12px' }, children: [_jsx("button", { className: cn(s['font-bold'], s['uppercase'], s['outline-none'], s['focus:outline-none']), style: {
793
+ padding: '10px 20px',
794
+ borderRadius: '12px',
795
+ fontSize: '14px',
796
+ fontWeight: 600,
797
+ background: 'transparent',
798
+ border: '1px solid rgba(148, 163, 184, 0.4)',
799
+ color: '#64748b',
800
+ cursor: 'pointer',
801
+ transition: 'all 0.2s ease',
802
+ }, type: "button", onClick: () => {
803
+ isManualModeRef.current = false;
804
+ setIsManualMode(false);
805
+ if (silenceTimerRef.current) {
806
+ clearTimeout(silenceTimerRef.current);
807
+ silenceTimerRef.current = null;
808
+ }
809
+ SpeechRecognition.stopListening();
810
+ resetTranscript();
811
+ setLastTranscript('');
812
+ isRecordingRef.current = false;
813
+ setIsRecording(false);
814
+ setShowModal(false);
815
+ onClose && onClose();
816
+ }, onMouseEnter: (e) => {
817
+ e.currentTarget.style.background =
818
+ 'rgba(148, 163, 184, 0.1)';
819
+ }, onMouseLeave: (e) => {
820
+ e.currentTarget.style.background = 'transparent';
821
+ }, children: "Cancel" }), _jsx("button", { className: cn(s['text-white'], s['font-bold'], s['uppercase'], s['shadow'], s['outline-none'], s['focus:outline-none']), style: {
822
+ padding: '10px 24px',
823
+ borderRadius: '12px',
824
+ fontSize: '14px',
825
+ fontWeight: 600,
826
+ background: 'linear-gradient(135deg, #22c55e 0%, #16a34a 100%)',
827
+ border: 'none',
828
+ boxShadow: '0 4px 12px rgba(34, 197, 94, 0.3)',
829
+ cursor: 'pointer',
830
+ transition: 'all 0.2s ease',
831
+ }, type: "button", onClick: () => {
832
+ setShowModal(false);
833
+ onSave(content.current);
834
+ }, onMouseEnter: (e) => {
835
+ e.currentTarget.style.boxShadow =
836
+ '0 6px 20px rgba(34, 197, 94, 0.4)';
837
+ }, onMouseLeave: (e) => {
838
+ e.currentTarget.style.boxShadow =
839
+ '0 4px 12px rgba(34, 197, 94, 0.3)';
840
+ }, children: "Save Changes" })] })] })] }) }) }) }))] }));
841
+ };