hocmai-feedback-widget 0.2.1 → 0.2.3

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 (59) hide show
  1. package/README.md +78 -113
  2. package/dist/capture/device-detection.d.ts +13 -0
  3. package/dist/capture/fonts.d.ts +4 -0
  4. package/dist/capture/index.d.ts +14 -0
  5. package/dist/capture/mathjax-fonts.d.ts +5 -0
  6. package/dist/capture/types.d.ts +25 -0
  7. package/dist/ckeditor.mjs +113 -0
  8. package/dist/ckeditor.mjs.map +1 -0
  9. package/dist/components/CkEditor.d.ts +14 -0
  10. package/dist/components/FeedbackWidget.d.ts +13 -1
  11. package/dist/context/FeedbackContext.d.ts +14 -2
  12. package/dist/hocmai-feedback-widget.mjs +7550 -0
  13. package/dist/hocmai-feedback-widget.mjs.map +1 -0
  14. package/dist/index.d.ts +4 -2
  15. package/package.json +39 -29
  16. package/src/App.tsx +147 -58
  17. package/src/capture/device-detection.ts +52 -0
  18. package/src/capture/fonts.ts +34 -0
  19. package/src/capture/index.ts +361 -0
  20. package/src/capture/mathjax-fonts.ts +143 -0
  21. package/src/capture/types.ts +34 -0
  22. package/src/ckeditor.d.ts +14 -0
  23. package/src/components/CkEditor.tsx +130 -0
  24. package/src/components/CropOverlay.tsx +29 -47
  25. package/src/components/FeedbackWidget.tsx +105 -93
  26. package/src/context/FeedbackContext.tsx +49 -9
  27. package/src/index.ts +9 -3
  28. package/src/main.tsx +7 -9
  29. package/src/utils/device.ts +14 -14
  30. package/dist/support-feedback-lib.es.js +0 -3946
  31. package/dist/support-feedback-lib.umd.js +0 -84
  32. package/dist/test-mobile-screenshot.html +0 -211
  33. package/dist/test-simple-capture.html +0 -107
  34. package/dist/utils/screenshot/background-renderer.d.ts +0 -3
  35. package/dist/utils/screenshot/canvas-renderer.d.ts +0 -9
  36. package/dist/utils/screenshot/cleanup.d.ts +0 -3
  37. package/dist/utils/screenshot/device-detection.d.ts +0 -16
  38. package/dist/utils/screenshot/element-collector.d.ts +0 -10
  39. package/dist/utils/screenshot/image-renderer.d.ts +0 -21
  40. package/dist/utils/screenshot/performance.d.ts +0 -3
  41. package/dist/utils/screenshot/svg-renderer.d.ts +0 -24
  42. package/dist/utils/screenshot/text-renderer.d.ts +0 -23
  43. package/dist/utils/screenshot/types.d.ts +0 -35
  44. package/dist/utils/screenshot.d.ts +0 -9
  45. package/dist/vite.svg +0 -1
  46. package/src/App.css +0 -42
  47. package/src/assets/react.svg +0 -1
  48. package/src/index.css +0 -68
  49. package/src/utils/screenshot/background-renderer.ts +0 -54
  50. package/src/utils/screenshot/canvas-renderer.ts +0 -343
  51. package/src/utils/screenshot/cleanup.ts +0 -32
  52. package/src/utils/screenshot/device-detection.ts +0 -90
  53. package/src/utils/screenshot/element-collector.ts +0 -134
  54. package/src/utils/screenshot/image-renderer.ts +0 -148
  55. package/src/utils/screenshot/performance.ts +0 -57
  56. package/src/utils/screenshot/svg-renderer.ts +0 -149
  57. package/src/utils/screenshot/text-renderer.ts +0 -212
  58. package/src/utils/screenshot/types.ts +0 -40
  59. package/src/utils/screenshot.ts +0 -83
@@ -1,4 +1,5 @@
1
1
  import React, { useState, useEffect } from 'react';
2
+ import { REPORT_UI_ATTR } from '../capture/types';
2
3
 
3
4
  export interface Rect {
4
5
  x: number;
@@ -18,7 +19,6 @@ const CropOverlay: React.FC<CropOverlayProps> = ({ onConfirm, onCancel }) => {
18
19
  const [currentPos, setCurrentPos] = useState({ x: 0, y: 0 });
19
20
  const [preventScroll, setPreventScroll] = useState(false);
20
21
 
21
- // Manage body overflow to prevent scrolling during touch selection
22
22
  useEffect(() => {
23
23
  if (preventScroll) {
24
24
  document.body.style.overflow = 'hidden';
@@ -30,9 +30,6 @@ const CropOverlay: React.FC<CropOverlayProps> = ({ onConfirm, onCancel }) => {
30
30
  };
31
31
  }, [preventScroll]);
32
32
 
33
- // Ref for the selection box to calculate rect without render lag if needed,
34
- // but state is fine for React.
35
-
36
33
  const handleMouseDown = (e: React.MouseEvent) => {
37
34
  setIsDragging(true);
38
35
  setStartPos({ x: e.clientX, y: e.clientY });
@@ -49,14 +46,12 @@ const CropOverlay: React.FC<CropOverlayProps> = ({ onConfirm, onCancel }) => {
49
46
  finalizeSelection();
50
47
  };
51
48
 
52
- // Touch events
53
49
  const handleTouchStart = (e: React.TouchEvent) => {
54
- if (e.touches.length === 1) { // Single touch only
50
+ if (e.touches.length === 1) {
55
51
  setIsDragging(true);
56
52
  const touch = e.touches[0];
57
53
  setStartPos({ x: touch.clientX, y: touch.clientY });
58
54
  setCurrentPos({ x: touch.clientX, y: touch.clientY });
59
- // Prevent scrolling while selecting
60
55
  setPreventScroll(true);
61
56
  }
62
57
  };
@@ -70,39 +65,24 @@ const CropOverlay: React.FC<CropOverlayProps> = ({ onConfirm, onCancel }) => {
70
65
 
71
66
  const handleTouchEnd = () => {
72
67
  finalizeSelection();
73
- // Restore scrolling
74
68
  setPreventScroll(false);
75
69
  };
76
70
 
77
71
  const finalizeSelection = () => {
78
72
  if (isDragging) {
79
73
  setIsDragging(false);
80
-
81
- // Calculate final rect
82
74
  const x = Math.min(startPos.x, currentPos.x);
83
75
  const y = Math.min(startPos.y, currentPos.y);
84
76
  const width = Math.abs(currentPos.x - startPos.x);
85
77
  const height = Math.abs(currentPos.y - startPos.y);
86
78
 
87
- // Only confirm if width/height is significant (e.g. > 10px) to avoid accidental clicks
79
+ // Only confirm meaningful selections (avoid accidental clicks).
88
80
  if (width > 10 && height > 10) {
89
- // We capture viewport coordinates.
90
- // Note: screenshot tool usually captures full page or document body.
91
- // If html-to-image captures document.body, we might need to adjust for scroll.
92
- // HOWEVER, our plan is to capture the Viewport mostly.
93
- // Or if we capture Body, we add scrollX/scrollY.
94
-
95
- onConfirm({
96
- x,
97
- y,
98
- width,
99
- height
100
- });
81
+ onConfirm({ x, y, width, height });
101
82
  }
102
83
  }
103
84
  };
104
85
 
105
- // Close on Escape
106
86
  useEffect(() => {
107
87
  const handleKeyDown = (e: KeyboardEvent) => {
108
88
  if (e.key === 'Escape') onCancel();
@@ -113,12 +93,13 @@ const CropOverlay: React.FC<CropOverlayProps> = ({ onConfirm, onCancel }) => {
113
93
 
114
94
  return (
115
95
  <div
96
+ {...{ [REPORT_UI_ATTR]: 'true' }}
116
97
  style={{
117
98
  position: 'fixed',
118
99
  inset: 0,
119
- zIndex: 99999, // Extremely high z-index
100
+ zIndex: 99999,
120
101
  cursor: 'crosshair',
121
- userSelect: 'none'
102
+ userSelect: 'none',
122
103
  }}
123
104
  onMouseDown={handleMouseDown}
124
105
  onMouseMove={handleMouseMove}
@@ -127,32 +108,33 @@ const CropOverlay: React.FC<CropOverlayProps> = ({ onConfirm, onCancel }) => {
127
108
  onTouchMove={handleTouchMove}
128
109
  onTouchEnd={handleTouchEnd}
129
110
  >
130
- {/* Background Dim - separate div to avoid affecting selection box rendering if simpler */}
131
- <div style={{
132
- position: 'absolute',
133
- inset: 0,
134
- backgroundColor: 'rgba(0, 0, 0, 0.4)'
135
- }} />
111
+ <div
112
+ style={{
113
+ position: 'absolute',
114
+ inset: 0,
115
+ backgroundColor: 'rgba(0, 0, 0, 0.4)',
116
+ }}
117
+ />
136
118
 
137
- {/* Helper Text */}
138
119
  {!isDragging && (
139
- <div style={{
140
- position: 'absolute',
141
- top: '20px',
142
- left: '50%',
143
- transform: 'translateX(-50%)',
144
- background: 'rgba(0,0,0,0.8)',
145
- color: 'white',
146
- padding: '8px 16px',
147
- borderRadius: '20px',
148
- pointerEvents: 'none',
149
- fontSize: '14px'
150
- }}>
120
+ <div
121
+ style={{
122
+ position: 'absolute',
123
+ top: '20px',
124
+ left: '50%',
125
+ transform: 'translateX(-50%)',
126
+ background: 'rgba(0,0,0,0.8)',
127
+ color: 'white',
128
+ padding: '8px 16px',
129
+ borderRadius: '20px',
130
+ pointerEvents: 'none',
131
+ fontSize: '14px',
132
+ }}
133
+ >
151
134
  Kéo chuột hoặc chạm để chọn vùng chụp ảnh (ESC để hủy)
152
135
  </div>
153
136
  )}
154
137
 
155
- {/* Selection Box */}
156
138
  {isDragging && (
157
139
  <div
158
140
  style={{
@@ -163,7 +145,7 @@ const CropOverlay: React.FC<CropOverlayProps> = ({ onConfirm, onCancel }) => {
163
145
  height: Math.abs(currentPos.y - startPos.y),
164
146
  border: '2px solid #fff',
165
147
  backgroundColor: 'rgba(255, 255, 255, 0.1)',
166
- boxShadow: '0 0 0 9999px rgba(0, 0, 0, 0.5)' // Clever trick to dim outside
148
+ boxShadow: '0 0 0 9999px rgba(0, 0, 0, 0.5)',
167
149
  }}
168
150
  />
169
151
  )}
@@ -1,8 +1,12 @@
1
1
  import React, { useState } from 'react';
2
2
  import { getDeviceInfo } from '../utils/device';
3
- import { captureScreenshot, type Rect } from '../utils/screenshot';
3
+ import { captureScreenshot, prewarmCapture, type Rect } from '../capture';
4
4
  import { MessageSquarePlus, Crop, Monitor, X, Hash, AlignLeft, Loader2, Image as ImageIcon } from 'lucide-react';
5
5
  import CropOverlay from './CropOverlay';
6
+ // Type-only import — erased at build time, so the core bundle has zero CKEditor
7
+ // coupling and apps build without the CKEditor deps. The editor itself is shipped
8
+ // at the subpath `hocmai-feedback-widget/ckeditor` and injected via descriptionEditor.
9
+ import type { CkEditorProps } from './CkEditor';
6
10
 
7
11
  export interface FeedbackData {
8
12
  idQuestion: string | number;
@@ -17,7 +21,8 @@ export interface FeedbackData {
17
21
  export interface FeedbackProps {
18
22
  questionId: string | number;
19
23
  contextData?: Record<string, any>;
20
- onSubmit: (data: FeedbackData) => Promise<void>;
24
+ /** Submit handler. Optional — may be provided per call via openFeedback or at the Provider. */
25
+ onSubmit?: (data: FeedbackData) => Promise<void> | void;
21
26
  themeColor?: string;
22
27
  triggerComponent?: React.ReactNode;
23
28
  isOpen?: boolean;
@@ -25,8 +30,20 @@ export interface FeedbackProps {
25
30
  onClose?: () => void;
26
31
  isEnableCaptureFull?: boolean;
27
32
  isEnableCaptureElement?: boolean;
33
+ /** Endpoint that accepts { base64 } and returns { file_url }. */
34
+ uploadUrl?: string;
35
+ /** Use the rich-text editor for the description instead of a plain textarea. */
36
+ isCkEditor?: boolean;
37
+ /**
38
+ * Rich-text editor component for the description (used when isCkEditor is true).
39
+ * Pass the ready-made editor from `hocmai-feedback-widget/ckeditor`, or your own.
40
+ * Kept as a prop so the core bundle has no CKEditor dependency.
41
+ */
42
+ descriptionEditor?: React.ComponentType<CkEditorProps>;
28
43
  }
29
44
 
45
+ const DEFAULT_UPLOAD_URL = 'https://lcms.icanwork.vn/api/upload-to-s3/upload-image-error-to-s3';
46
+
30
47
  const FeedbackWidget: React.FC<FeedbackProps> = ({
31
48
  questionId,
32
49
  contextData,
@@ -37,7 +54,10 @@ const FeedbackWidget: React.FC<FeedbackProps> = ({
37
54
  onOpen,
38
55
  onClose,
39
56
  isEnableCaptureFull = true,
40
- isEnableCaptureElement = true
57
+ isEnableCaptureElement = true,
58
+ uploadUrl = DEFAULT_UPLOAD_URL,
59
+ isCkEditor = false,
60
+ descriptionEditor: DescriptionEditor,
41
61
  }) => {
42
62
  const [internalIsOpen, setInternalIsOpen] = useState(false);
43
63
  const [description, setDescription] = useState('');
@@ -70,6 +90,14 @@ const FeedbackWidget: React.FC<FeedbackProps> = ({
70
90
  }
71
91
  }, [isOpen, questionId, contextData]);
72
92
 
93
+ // Warm the snapshot font/image cache while the user fills the form so the
94
+ // first capture isn't delayed by inlining MathJax web fonts.
95
+ React.useEffect(() => {
96
+ if (isOpen) {
97
+ prewarmCapture().catch(() => { });
98
+ }
99
+ }, [isOpen]);
100
+
73
101
  const handleSelectImage = () => {
74
102
  if (screenshots.length >= 3) {
75
103
  alert('Bạn chỉ có thể chụp tối đa 3 ảnh.');
@@ -101,7 +129,6 @@ const FeedbackWidget: React.FC<FeedbackProps> = ({
101
129
  };
102
130
  reader.readAsDataURL(file);
103
131
 
104
- // Reset input so same file can be selected again if needed
105
132
  event.target.value = '';
106
133
  };
107
134
 
@@ -118,7 +145,6 @@ const FeedbackWidget: React.FC<FeedbackProps> = ({
118
145
  };
119
146
 
120
147
  const startCapture = async (mode: 'full' | 'crop') => {
121
- console.log('click capture', performance.now());
122
148
  if (screenshots.length >= 3) {
123
149
  alert('Bạn chỉ có thể chụp tối đa 3 ảnh.');
124
150
  return;
@@ -126,25 +152,19 @@ const FeedbackWidget: React.FC<FeedbackProps> = ({
126
152
  setIsTempHidden(true); // Hide modal temporarily
127
153
 
128
154
  if (mode === 'crop') {
129
- // Add small delay to ensure modal is gone
130
155
  setTimeout(() => setIsSelecting(true), 150);
131
156
  return;
132
157
  }
133
158
 
134
- // Full screen capture
135
159
  setTimeout(async () => {
136
160
  await performCapture();
137
161
  }, 150);
138
162
  };
139
163
 
140
164
  const performCapture = async (cropRect?: Rect) => {
141
- console.log('performCapture start', performance.now());
142
165
  setIsCapturing(true);
143
166
  try {
144
- // If cropRect is provided, it captures that specific area.
145
- // If undefined, it captures the current viewport (default behavior of updated captureScreenshot).
146
167
  const shot = await captureScreenshot(cropRect);
147
- console.log('performCapture end', performance.now());
148
168
  if (shot) {
149
169
  setScreenshots(prev => [...prev, shot as string]);
150
170
  } else {
@@ -152,11 +172,12 @@ const FeedbackWidget: React.FC<FeedbackProps> = ({
152
172
  }
153
173
  } catch (err) {
154
174
  console.error(err);
175
+ alert('Không thể chụp ảnh màn hình. Vui lòng thử lại.');
155
176
  } finally {
156
177
  setIsCapturing(false);
157
178
  setIsTempHidden(false); // Re-show modal
158
179
  }
159
- }
180
+ };
160
181
 
161
182
  const handleCropConfirm = (rect: Rect) => {
162
183
  setIsSelecting(false);
@@ -176,7 +197,6 @@ const FeedbackWidget: React.FC<FeedbackProps> = ({
176
197
  if (onClose) onClose();
177
198
  setInternalIsOpen(false);
178
199
 
179
- // Clean up state
180
200
  setScreenshots([]);
181
201
  setIsSelecting(false);
182
202
  setZoomedImage(null);
@@ -210,17 +230,15 @@ const FeedbackWidget: React.FC<FeedbackProps> = ({
210
230
  setIsSubmitting(true);
211
231
  try {
212
232
  const deviceInfo = getDeviceInfo();
213
- // send image (data base64) to server and get URL image return
233
+ // Upload each screenshot (base64) and collect the returned URLs.
214
234
  const listImage = await Promise.all(
215
235
  screenshots.map(async (shot) => {
216
- const response = await fetch('https://lcms.icanwork.vn/api/upload-to-s3/upload-image-error-to-s3', {
236
+ const response = await fetch(uploadUrl, {
217
237
  method: 'POST',
218
238
  headers: {
219
239
  'Content-Type': 'application/json'
220
240
  },
221
- body: JSON.stringify({
222
- base64: shot
223
- })
241
+ body: JSON.stringify({ base64: shot })
224
242
  });
225
243
  const uploadData = await response.json();
226
244
  return uploadData.file_url;
@@ -236,8 +254,12 @@ const FeedbackWidget: React.FC<FeedbackProps> = ({
236
254
  contextData
237
255
  };
238
256
 
239
- await onSubmit(feedbackData);
240
- handleCloseTrigger(); // Close and reset after success
257
+ if (onSubmit) {
258
+ await onSubmit(feedbackData);
259
+ } else {
260
+ console.warn('[feedback-widget] Không có onSubmit — bỏ qua gửi. Dữ liệu:', feedbackData);
261
+ }
262
+ handleCloseTrigger();
241
263
  } catch (error) {
242
264
  console.error('Submit feedback failed', error);
243
265
  } finally {
@@ -245,9 +267,6 @@ const FeedbackWidget: React.FC<FeedbackProps> = ({
245
267
  }
246
268
  };
247
269
 
248
- // ... inside component
249
-
250
- // Reduced Styles
251
270
  const styles = `
252
271
  @keyframes fb-fade-in {
253
272
  from { opacity: 0; transform: translateY(10px); }
@@ -268,7 +287,7 @@ const FeedbackWidget: React.FC<FeedbackProps> = ({
268
287
  border-radius: 12px;
269
288
  width: 280px;
270
289
  max-width: 90vw;
271
- max-height: 80vh;
290
+ max-height: 80vh;
272
291
  overflow-y: auto;
273
292
  box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06), 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1);
274
293
  border: 1px solid #f3f4f6;
@@ -289,7 +308,6 @@ const FeedbackWidget: React.FC<FeedbackProps> = ({
289
308
  bottom: 20px !important;
290
309
  right: 20px !important;
291
310
  }
292
- /* Prevent iOS zoom on inputs */
293
311
  input, select, textarea {
294
312
  font-size: 16px !important;
295
313
  }
@@ -298,23 +316,17 @@ const FeedbackWidget: React.FC<FeedbackProps> = ({
298
316
  .fb-btn-crop {
299
317
  display: none !important;
300
318
  }
301
- /* Enforce 16px font on all mobile/tablet inputs to prevent zoom */
302
319
  .fb-widget-modal input,
303
320
  .fb-widget-modal select,
304
321
  .fb-widget-modal textarea {
305
322
  font-size: 16px !important;
306
323
  }
307
324
  }
308
- @media (min-width: 768px) {
309
- .fb-btn-upload {
310
-
311
- }
312
- }
313
325
  `;
314
326
 
315
327
  return (
316
328
  <>
317
- <style id="feedback-widget-styles">{styles}</style>
329
+ <style id="feedback-widget-styles" data-hsa-report-ui="true">{styles}</style>
318
330
 
319
331
  {isSelecting && (
320
332
  <CropOverlay onConfirm={handleCropConfirm} onCancel={handleCropCancel} />
@@ -323,6 +335,7 @@ const FeedbackWidget: React.FC<FeedbackProps> = ({
323
335
  {/* Zoom Modal */}
324
336
  {zoomedImage && (
325
337
  <div
338
+ data-hsa-report-ui="true"
326
339
  style={{
327
340
  position: 'fixed',
328
341
  inset: 0,
@@ -357,13 +370,17 @@ const FeedbackWidget: React.FC<FeedbackProps> = ({
357
370
  )}
358
371
 
359
372
  {/* Trigger Button */}
360
- <div onClick={() => {
361
- if (externalIsOpen === undefined) {
362
- handleOpen();
363
- } else if (onOpen) {
364
- onOpen();
365
- }
366
- }} style={{ cursor: 'pointer', display: 'inline-block' }}>
373
+ <div
374
+ data-hsa-report-ui="true"
375
+ onClick={() => {
376
+ if (externalIsOpen === undefined) {
377
+ handleOpen();
378
+ } else if (onOpen) {
379
+ onOpen();
380
+ }
381
+ }}
382
+ style={{ cursor: 'pointer', display: 'inline-block' }}
383
+ >
367
384
  {triggerComponent || (
368
385
  <button
369
386
  className="fb-widget-trigger"
@@ -394,16 +411,16 @@ const FeedbackWidget: React.FC<FeedbackProps> = ({
394
411
  )}
395
412
  </div>
396
413
 
397
- {/* ... capturing overlay ... */}
414
+ {/* Capturing indicator */}
398
415
  {isCapturing && (
399
- <div style={{ position: 'fixed', top: 20, right: 20, background: 'rgba(0,0,0,0.8)', color: 'white', padding: '8px 12px', borderRadius: '8px', zIndex: 10000, fontWeight: 500, backdropFilter: 'blur(4px)', fontSize: '0.8rem' }}>
416
+ <div data-hsa-report-ui="true" style={{ position: 'fixed', top: 20, right: 20, background: 'rgba(0,0,0,0.8)', color: 'white', padding: '8px 12px', borderRadius: '8px', zIndex: 10000, fontWeight: 500, backdropFilter: 'blur(4px)', fontSize: '0.8rem' }}>
400
417
  Đang chụp...
401
418
  </div>
402
419
  )}
403
420
 
404
421
  {/* Main Form Popover */}
405
422
  {showWidget && !isSelecting && !zoomedImage && (
406
- <div className="fb-widget-modal">
423
+ <div className="fb-widget-modal" data-hsa-report-ui="true">
407
424
  <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
408
425
  <h2 style={{ margin: 0, fontSize: '1rem', fontWeight: 700, color: '#111827' }}>
409
426
  Báo lỗi
@@ -429,7 +446,6 @@ const FeedbackWidget: React.FC<FeedbackProps> = ({
429
446
  </button>
430
447
  </div>
431
448
 
432
- {/* Form Fields Container */}
433
449
  <div style={{ display: 'flex', flexDirection: 'column', gap: '10px' }}>
434
450
 
435
451
  {/* Question ID Input */}
@@ -437,12 +453,7 @@ const FeedbackWidget: React.FC<FeedbackProps> = ({
437
453
  <label style={{ display: 'block', marginBottom: '2px', fontSize: '0.8rem', fontWeight: 600, color: '#374151' }}>
438
454
  ID Câu hỏi {contextData?.questionIdChild ? 'bài đọc' : ''} <span style={{ color: '#ef4444' }}>*</span>
439
455
  </label>
440
- <div style={{
441
- position: 'relative',
442
- display: 'flex',
443
- alignItems: 'center',
444
- transition: 'all 0.2s'
445
- }}>
456
+ <div style={{ position: 'relative', display: 'flex', alignItems: 'center', transition: 'all 0.2s' }}>
446
457
  <Hash size={12} style={{
447
458
  position: 'absolute',
448
459
  left: '10px',
@@ -487,12 +498,7 @@ const FeedbackWidget: React.FC<FeedbackProps> = ({
487
498
  <label style={{ display: 'block', marginBottom: '2px', fontSize: '0.8rem', fontWeight: 600, color: '#374151' }}>
488
499
  ID Câu hỏi
489
500
  </label>
490
- <div style={{
491
- position: 'relative',
492
- display: 'flex',
493
- alignItems: 'center',
494
- transition: 'all 0.2s'
495
- }}>
501
+ <div style={{ position: 'relative', display: 'flex', alignItems: 'center', transition: 'all 0.2s' }}>
496
502
  <Hash size={12} style={{
497
503
  position: 'absolute',
498
504
  left: '10px',
@@ -575,45 +581,53 @@ const FeedbackWidget: React.FC<FeedbackProps> = ({
575
581
  </span>
576
582
  </div>
577
583
  <div style={{ position: 'relative' }}>
578
- <div style={{
579
- position: 'absolute',
580
- left: '10px',
581
- top: '8px',
582
- pointerEvents: 'none'
583
- }}>
584
+ {isCkEditor ? null : <div style={{ position: 'absolute', left: '10px', top: '8px', pointerEvents: 'none' }}>
584
585
  <AlignLeft size={12} style={{
585
586
  color: focusedField === 'desc' ? themeColor : (errors.description ? '#ef4444' : '#9ca3af'),
586
587
  transition: 'color 0.2s'
587
588
  }} />
588
- </div>
589
- <textarea
590
- value={description}
591
- onFocus={() => setFocusedField('desc')}
592
- onBlur={() => setFocusedField(null)}
593
- onChange={(e) => {
594
- setDescription(e.target.value);
595
- if (errors.description) setErrors(prev => ({ ...prev, description: undefined }));
596
- }}
597
- maxLength={1000}
598
- placeholder="Mô tả chi tiết..."
599
- style={{
600
- width: '100%',
601
- minHeight: '80px',
602
- padding: '6px 10px 6px 28px',
603
- borderRadius: '6px',
604
- border: `1px solid ${errors.description ? '#ef4444' : (focusedField === 'desc' ? themeColor : '#e5e7eb')}`,
605
- boxShadow: focusedField === 'desc' ? `0 0 0 2px ${themeColor}20` : 'none',
606
- outline: 'none',
607
- fontSize: '0.85rem',
608
- color: '#1f2937',
609
- resize: 'vertical',
610
- boxSizing: 'border-box',
611
- transition: 'all 0.2s',
612
- backgroundColor: '#f9fafb',
613
- lineHeight: '1.4',
614
- fontFamily: 'inherit'
615
- }}
616
- />
589
+ </div>}
590
+ {isCkEditor && DescriptionEditor ? (
591
+ <DescriptionEditor
592
+ placeholder="Mô tả chi tiết lỗi..."
593
+ contentQuestion={description}
594
+ error={!!errors.description}
595
+ uploadUrl={uploadUrl}
596
+ handleContent={(html: string) => {
597
+ setDescription(html);
598
+ if (errors.description) setErrors(prev => ({ ...prev, description: undefined }));
599
+ }}
600
+ />
601
+ ) : (
602
+ <textarea
603
+ value={description}
604
+ onFocus={() => setFocusedField('desc')}
605
+ onBlur={() => setFocusedField(null)}
606
+ onChange={(e) => {
607
+ setDescription(e.target.value);
608
+ if (errors.description) setErrors(prev => ({ ...prev, description: undefined }));
609
+ }}
610
+ maxLength={1000}
611
+ placeholder="Mô tả chi tiết..."
612
+ style={{
613
+ width: '100%',
614
+ minHeight: '80px',
615
+ padding: '6px 10px 6px 28px',
616
+ borderRadius: '6px',
617
+ border: `1px solid ${errors.description ? '#ef4444' : (focusedField === 'desc' ? themeColor : '#e5e7eb')}`,
618
+ boxShadow: focusedField === 'desc' ? `0 0 0 2px ${themeColor}20` : 'none',
619
+ outline: 'none',
620
+ fontSize: '0.85rem',
621
+ color: '#1f2937',
622
+ resize: 'vertical',
623
+ boxSizing: 'border-box',
624
+ transition: 'all 0.2s',
625
+ backgroundColor: '#f9fafb',
626
+ lineHeight: '1.4',
627
+ fontFamily: 'inherit'
628
+ }}
629
+ />
630
+ )}
617
631
  </div>
618
632
  {errors.description && (
619
633
  <span style={{ display: 'block', marginTop: '2px', fontSize: '0.7rem', color: '#ef4444' }}>
@@ -630,7 +644,6 @@ const FeedbackWidget: React.FC<FeedbackProps> = ({
630
644
  </label>
631
645
  </div>
632
646
 
633
- {/* Thumbnails Grid */}
634
647
  {screenshots.length > 0 && (
635
648
  <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '6px', marginBottom: '8px' }}>
636
649
  {screenshots.map((shot, index) => (
@@ -710,7 +723,7 @@ const FeedbackWidget: React.FC<FeedbackProps> = ({
710
723
  alignItems: 'center',
711
724
  gap: '4px',
712
725
  transition: 'all 0.2s',
713
- minWidth: '60px' // Prevent squishing
726
+ minWidth: '60px'
714
727
  }}
715
728
  onMouseOver={(e) => {
716
729
  e.currentTarget.style.borderColor = themeColor;
@@ -844,7 +857,6 @@ const FeedbackWidget: React.FC<FeedbackProps> = ({
844
857
  ) : 'Gửi báo lỗi'}
845
858
  </button>
846
859
  </div>
847
- {/* <p style={{ textAlign: 'center', marginTop: 'auto', color: '#9CA3AF' }}>v0.2.0-rc4</p> */}
848
860
  </div>
849
861
  )}
850
862
  </>