CreateNoteFromPDFDialog.tsx 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197
  1. import React, { useState, useEffect } from 'react';
  2. import { X, Loader, Image as ImageIcon, Box } from 'lucide-react';
  3. import { ocrService } from '../services/ocrService';
  4. import { noteCategoryService } from '../services/noteCategoryService';
  5. import { NoteCategory } from '../types';
  6. import { useLanguage } from '../contexts/LanguageContext';
  7. import { useToast } from '../contexts/ToastContext';
  8. interface CreateNoteFromPDFDialogProps {
  9. screenshot: Blob;
  10. extractedText: string;
  11. onSave: (title: string, content: string, categoryId?: string) => Promise<void>;
  12. onCancel: () => void;
  13. authToken: string;
  14. initialCategoryId?: string;
  15. initialPageNumber?: number;
  16. }
  17. export const CreateNoteFromPDFDialog: React.FC<CreateNoteFromPDFDialogProps> = ({
  18. screenshot,
  19. extractedText,
  20. onSave,
  21. onCancel,
  22. authToken,
  23. initialCategoryId,
  24. initialPageNumber,
  25. }) => {
  26. const { t } = useLanguage();
  27. const { showToast } = useToast();
  28. const defaultTitle = initialPageNumber
  29. ? `${t('createPDFNote')} - ${t('page')} ${initialPageNumber} - ${new Date().toLocaleString()}`
  30. : `${t('createPDFNote')} - ${new Date().toLocaleString()}`;
  31. const [title, setTitle] = useState(defaultTitle);
  32. const [content, setContent] = useState(extractedText);
  33. const [selectedCategoryId, setSelectedCategoryId] = useState<string | undefined>(initialCategoryId);
  34. const [categories, setCategories] = useState<NoteCategory[]>([]);
  35. const [saving, setSaving] = useState(false);
  36. const [ocrLoading, setOcrLoading] = useState(false);
  37. const [screenshotUrl, setScreenshotUrl] = useState<string>('');
  38. useEffect(() => {
  39. if (authToken) {
  40. noteCategoryService.getAll(authToken).then(setCategories).catch(console.error);
  41. }
  42. }, [authToken]);
  43. useEffect(() => {
  44. const url = URL.createObjectURL(screenshot);
  45. setScreenshotUrl(url);
  46. // Trigger OCR if initial text is empty
  47. if (!extractedText && authToken) {
  48. setOcrLoading(true);
  49. ocrService.recognizeText(authToken, screenshot)
  50. .then(text => setContent(text))
  51. .catch(err => console.error('OCR failed:', err))
  52. .finally(() => setOcrLoading(false));
  53. }
  54. return () => URL.revokeObjectURL(url);
  55. }, [screenshot, extractedText, authToken]);
  56. const handleSave = async () => {
  57. setSaving(true);
  58. try {
  59. await onSave(title, content, selectedCategoryId);
  60. } catch (error) {
  61. console.error('Failed to save note:', error);
  62. } finally {
  63. setSaving(false);
  64. }
  65. };
  66. return (
  67. <div className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-50 p-4">
  68. <div className="bg-white rounded-lg w-full max-w-3xl max-h-[90vh] flex flex-col">
  69. {/* Header */}
  70. <div className="flex items-center justify-between p-4 border-b">
  71. <h2 className="text-lg font-semibold text-gray-900">{t('createPDFNote')}</h2>
  72. <button
  73. onClick={onCancel}
  74. className="p-2 text-gray-400 hover:text-gray-600 transition-colors"
  75. disabled={saving}
  76. >
  77. <X size={20} />
  78. </button>
  79. </div>
  80. {/* Content */}
  81. <div className="flex-1 overflow-y-auto p-4 space-y-4">
  82. {/* Screenshot Preview */}
  83. <div className="space-y-2">
  84. <label className="block text-sm font-medium text-gray-700">
  85. <ImageIcon size={16} className="inline mr-1" />
  86. {t('screenshotPreview')}
  87. </label>
  88. <div className="border rounded-lg p-2 bg-gray-50">
  89. {screenshotUrl && (
  90. <img
  91. src={screenshotUrl}
  92. alt="PDF Selection"
  93. className="max-w-full h-auto rounded"
  94. />
  95. )}
  96. </div>
  97. </div>
  98. {/* Note Category selector */}
  99. <div className="space-y-2">
  100. <label className="block text-sm font-medium text-gray-700">
  101. <Box size={16} className="inline mr-1" />
  102. {t('personalNotebook') || t('directoryLabel')}
  103. </label>
  104. <select
  105. value={selectedCategoryId || ''}
  106. onChange={(e) => setSelectedCategoryId(e.target.value || undefined)}
  107. className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 outline-none"
  108. disabled={saving}
  109. >
  110. <option value="">{t('uncategorized')}</option>
  111. {categories.map(c => {
  112. const parent = categories.find(p => p.id === c.parentId)
  113. const grandparent = parent ? categories.find(gp => gp.id === parent.parentId) : null
  114. const path = [grandparent, parent, c].filter(Boolean).map(cat => cat?.name).join(' > ')
  115. return (
  116. <option key={c.id} value={c.id}>
  117. {'\u00A0'.repeat((c.level - 1) * 2)}{c.name}
  118. </option>
  119. )
  120. })}
  121. </select>
  122. </div>
  123. {/* Title Input */}
  124. <div className="space-y-2">
  125. <label className="block text-sm font-medium text-gray-700">
  126. {t('title')}
  127. </label>
  128. <input
  129. type="text"
  130. value={title}
  131. onChange={(e) => setTitle(e.target.value)}
  132. className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
  133. placeholder={t('enterNoteTitle')}
  134. disabled={saving}
  135. />
  136. </div>
  137. {/* Content Textarea */}
  138. <div className="space-y-2">
  139. <label className="block text-sm font-medium text-gray-700">
  140. {t('contentOCR')}
  141. </label>
  142. <textarea
  143. value={content}
  144. onChange={(e) => setContent(e.target.value)}
  145. className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent font-mono text-sm"
  146. rows={10}
  147. placeholder={ocrLoading ? t('extractingText') : t('placeholderText')}
  148. disabled={saving || ocrLoading}
  149. />
  150. {ocrLoading && (
  151. <div className="flex items-center gap-2 mt-2 p-2 bg-blue-50/50 rounded-md border border-blue-100/50">
  152. <Loader size={12} className="animate-spin" />
  153. {t('analyzingImage')}
  154. </div>
  155. )}
  156. {!ocrLoading && !content && (
  157. <p className="text-sm text-gray-500">
  158. {t('noTextExtracted')}
  159. </p>
  160. )}
  161. </div>
  162. </div>
  163. {/* Footer */}
  164. <div className="flex items-center justify-end gap-2 p-4 border-t bg-gray-50">
  165. <button
  166. onClick={onCancel}
  167. className="px-4 py-2 text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors"
  168. disabled={saving}
  169. >
  170. {t('cancel')}
  171. </button>
  172. <button
  173. onClick={handleSave}
  174. className="px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2"
  175. disabled={saving || !title.trim()}
  176. >
  177. {saving && <Loader size={16} className="animate-spin" />}
  178. {saving ? t('saving') : t('saveNote')}
  179. </button>
  180. </div>
  181. </div>
  182. </div>
  183. );
  184. };