| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197 |
- import React, { useState, useEffect } from 'react';
- import { X, Loader, Image as ImageIcon, Box } from 'lucide-react';
- import { ocrService } from '../services/ocrService';
- import { noteCategoryService } from '../services/noteCategoryService';
- import { NoteCategory } from '../types';
- import { useLanguage } from '../contexts/LanguageContext';
- import { useToast } from '../contexts/ToastContext';
- interface CreateNoteFromPDFDialogProps {
- screenshot: Blob;
- extractedText: string;
- onSave: (title: string, content: string, categoryId?: string) => Promise<void>;
- onCancel: () => void;
- authToken: string;
- initialCategoryId?: string;
- initialPageNumber?: number;
- }
- export const CreateNoteFromPDFDialog: React.FC<CreateNoteFromPDFDialogProps> = ({
- screenshot,
- extractedText,
- onSave,
- onCancel,
- authToken,
- initialCategoryId,
- initialPageNumber,
- }) => {
- const { t } = useLanguage();
- const { showToast } = useToast();
- const defaultTitle = initialPageNumber
- ? `${t('createPDFNote')} - ${t('page')} ${initialPageNumber} - ${new Date().toLocaleString()}`
- : `${t('createPDFNote')} - ${new Date().toLocaleString()}`;
- const [title, setTitle] = useState(defaultTitle);
- const [content, setContent] = useState(extractedText);
- const [selectedCategoryId, setSelectedCategoryId] = useState<string | undefined>(initialCategoryId);
- const [categories, setCategories] = useState<NoteCategory[]>([]);
- const [saving, setSaving] = useState(false);
- const [ocrLoading, setOcrLoading] = useState(false);
- const [screenshotUrl, setScreenshotUrl] = useState<string>('');
- useEffect(() => {
- if (authToken) {
- noteCategoryService.getAll(authToken).then(setCategories).catch(console.error);
- }
- }, [authToken]);
- useEffect(() => {
- const url = URL.createObjectURL(screenshot);
- setScreenshotUrl(url);
- // Trigger OCR if initial text is empty
- if (!extractedText && authToken) {
- setOcrLoading(true);
- ocrService.recognizeText(authToken, screenshot)
- .then(text => setContent(text))
- .catch(err => console.error('OCR failed:', err))
- .finally(() => setOcrLoading(false));
- }
- return () => URL.revokeObjectURL(url);
- }, [screenshot, extractedText, authToken]);
- const handleSave = async () => {
- setSaving(true);
- try {
- await onSave(title, content, selectedCategoryId);
- } catch (error) {
- console.error('Failed to save note:', error);
- } finally {
- setSaving(false);
- }
- };
- return (
- <div className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-50 p-4">
- <div className="bg-white rounded-lg w-full max-w-3xl max-h-[90vh] flex flex-col">
- {/* Header */}
- <div className="flex items-center justify-between p-4 border-b">
- <h2 className="text-lg font-semibold text-gray-900">{t('createPDFNote')}</h2>
- <button
- onClick={onCancel}
- className="p-2 text-gray-400 hover:text-gray-600 transition-colors"
- disabled={saving}
- >
- <X size={20} />
- </button>
- </div>
- {/* Content */}
- <div className="flex-1 overflow-y-auto p-4 space-y-4">
- {/* Screenshot Preview */}
- <div className="space-y-2">
- <label className="block text-sm font-medium text-gray-700">
- <ImageIcon size={16} className="inline mr-1" />
- {t('screenshotPreview')}
- </label>
- <div className="border rounded-lg p-2 bg-gray-50">
- {screenshotUrl && (
- <img
- src={screenshotUrl}
- alt="PDF Selection"
- className="max-w-full h-auto rounded"
- />
- )}
- </div>
- </div>
- {/* Note Category selector */}
- <div className="space-y-2">
- <label className="block text-sm font-medium text-gray-700">
- <Box size={16} className="inline mr-1" />
- {t('personalNotebook') || t('directoryLabel')}
- </label>
- <select
- value={selectedCategoryId || ''}
- onChange={(e) => setSelectedCategoryId(e.target.value || undefined)}
- className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 outline-none"
- disabled={saving}
- >
- <option value="">{t('uncategorized')}</option>
- {categories.map(c => {
- const parent = categories.find(p => p.id === c.parentId)
- const grandparent = parent ? categories.find(gp => gp.id === parent.parentId) : null
- const path = [grandparent, parent, c].filter(Boolean).map(cat => cat?.name).join(' > ')
- return (
- <option key={c.id} value={c.id}>
- {'\u00A0'.repeat((c.level - 1) * 2)}{c.name}
- </option>
- )
- })}
- </select>
- </div>
- {/* Title Input */}
- <div className="space-y-2">
- <label className="block text-sm font-medium text-gray-700">
- {t('title')}
- </label>
- <input
- type="text"
- value={title}
- onChange={(e) => setTitle(e.target.value)}
- className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
- placeholder={t('enterNoteTitle')}
- disabled={saving}
- />
- </div>
- {/* Content Textarea */}
- <div className="space-y-2">
- <label className="block text-sm font-medium text-gray-700">
- {t('contentOCR')}
- </label>
- <textarea
- value={content}
- onChange={(e) => setContent(e.target.value)}
- 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"
- rows={10}
- placeholder={ocrLoading ? t('extractingText') : t('placeholderText')}
- disabled={saving || ocrLoading}
- />
- {ocrLoading && (
- <div className="flex items-center gap-2 mt-2 p-2 bg-blue-50/50 rounded-md border border-blue-100/50">
- <Loader size={12} className="animate-spin" />
- {t('analyzingImage')}
- </div>
- )}
- {!ocrLoading && !content && (
- <p className="text-sm text-gray-500">
- {t('noTextExtracted')}
- </p>
- )}
- </div>
- </div>
- {/* Footer */}
- <div className="flex items-center justify-end gap-2 p-4 border-t bg-gray-50">
- <button
- onClick={onCancel}
- className="px-4 py-2 text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors"
- disabled={saving}
- >
- {t('cancel')}
- </button>
- <button
- onClick={handleSave}
- 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"
- disabled={saving || !title.trim()}
- >
- {saving && <Loader size={16} className="animate-spin" />}
- {saving ? t('saving') : t('saveNote')}
- </button>
- </div>
- </div>
- </div>
- );
- };
|