Spaces:
Runtime error
Runtime error
| 'use client'; | |
| import React, { useState, useRef } from 'react'; | |
| import { useRouter } from 'next/navigation'; | |
| import api from '../services/api'; | |
| interface CreateRFPModalProps { | |
| isOpen: boolean; | |
| onClose: () => void; | |
| onSuccess?: (projectId: string) => void; | |
| } | |
| const CreateRFPModal: React.FC<CreateRFPModalProps> = ({ isOpen, onClose, onSuccess }) => { | |
| const router = useRouter(); | |
| const fileInputRef = useRef<HTMLInputElement>(null); | |
| const [clientName, setClientName] = useState(''); | |
| const [files, setFiles] = useState<File[]>([]); | |
| const [loading, setLoading] = useState(false); | |
| const [error, setError] = useState<string | null>(null); | |
| const [analysisStep, setAnalysisStep] = useState<'idle' | 'uploading' | 'shredding' | 'analyzing'>('idle'); | |
| // Handle file selection | |
| const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => { | |
| if (e.target.files) { | |
| const selectedFiles = Array.from(e.target.files); | |
| // Validate file types | |
| const validTypes = [ | |
| 'application/pdf', | |
| 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', | |
| 'application/msword', | |
| 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', | |
| 'application/vnd.ms-excel', | |
| 'text/csv', | |
| 'image/png', | |
| 'image/jpeg', | |
| 'image/jpg', | |
| 'image/tiff' | |
| ]; | |
| const validFiles = selectedFiles.filter( | |
| (f) => { | |
| const isDoc = validTypes.includes(f.type); | |
| const hasKnownExt = /\.(pdf|docx|doc|xlsx|xls|csv|png|jpg|jpeg|tiff|bmp)$/i.test(f.name); | |
| return isDoc || hasKnownExt; | |
| } | |
| ); | |
| if (validFiles.length !== selectedFiles.length) { | |
| setError('Some files were skipped. Unsupported format detected.'); | |
| } else { | |
| setError(null); | |
| } | |
| setFiles((prev) => [...prev, ...validFiles]); | |
| } | |
| }; | |
| // Remove a file from the list | |
| const removeFile = (index: number) => { | |
| setFiles((prev) => prev.filter((_, i) => i !== index)); | |
| }; | |
| const [source, setSource] = useState<'upload' | 'research'>('upload'); | |
| const [deepResearchId, setDeepResearchId] = useState(''); | |
| // Handle form submission | |
| const handleSubmit = async (e: React.FormEvent) => { | |
| e.preventDefault(); | |
| if (source === 'upload' && files.length === 0) { | |
| setError('Please upload at least one document'); | |
| return; | |
| } | |
| setLoading(true); | |
| setError(null); | |
| try { | |
| let projectId: string; | |
| // Use first file's name as temporary project name (will be updated after AI analysis) | |
| const tempProjectName = files.length > 0 | |
| ? files[0].name.replace(/\.[^/.]+$/, '') // Remove file extension | |
| : 'New RFP Project'; | |
| if (source === 'upload' && files.length > 0) { | |
| setAnalysisStep('uploading'); | |
| const result = await api.projects.createWithFiles( | |
| tempProjectName, | |
| files, | |
| clientName.trim() || undefined | |
| ); | |
| projectId = result.id; | |
| } else { | |
| // Initiate shell project | |
| const result = await api.projects.initiate( | |
| tempProjectName, | |
| clientName.trim(), | |
| source === 'research' ? deepResearchId : undefined | |
| ); | |
| projectId = result.id; | |
| } | |
| // Always analyze documents after upload | |
| if (files.length > 0) { | |
| setAnalysisStep('shredding'); | |
| await api.projects.shredProject(projectId); | |
| setAnalysisStep('analyzing'); | |
| // Extract metadata (including AI-generated project name) and run Go/No-Go analysis | |
| await api.projects.extractMetadata(projectId); | |
| await api.projects.analyzeGoNoGo(projectId); | |
| } | |
| if (onSuccess) onSuccess(projectId); | |
| router.push(`/compliance?projectId=${projectId}`); | |
| onClose(); | |
| // Reset | |
| setClientName(''); | |
| setFiles([]); | |
| setDeepResearchId(''); | |
| setAnalysisStep('idle'); | |
| } catch (err: any) { | |
| console.error('Create project error:', err); | |
| setError(err.message || 'Failed to process project.'); | |
| setAnalysisStep('idle'); | |
| } finally { | |
| setLoading(false); | |
| } | |
| }; | |
| // Handle drag and drop | |
| const handleDragOver = (e: React.DragEvent) => { | |
| e.preventDefault(); | |
| e.stopPropagation(); | |
| }; | |
| const handleDrop = (e: React.DragEvent) => { | |
| e.preventDefault(); | |
| e.stopPropagation(); | |
| const droppedFiles = Array.from(e.dataTransfer.files); | |
| const validFiles = droppedFiles.filter((f) => | |
| /\.(pdf|docx|doc|xlsx|xls|csv|png|jpg|jpeg|tiff|bmp)$/i.test(f.name) | |
| ); | |
| if (validFiles.length < droppedFiles.length) { | |
| setError('Some files were skipped. Unsupported format.'); | |
| } | |
| setFiles((prev) => [...prev, ...validFiles]); | |
| }; | |
| if (!isOpen) return null; | |
| return ( | |
| <div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 backdrop-blur-sm animate-in"> | |
| <div className="bg-white dark:bg-slate-900 rounded-xl shadow-2xl w-full max-w-md mx-4 overflow-hidden"> | |
| {/* Header */} | |
| <div className="flex justify-between items-center p-6 border-b border-slate-200 dark:border-slate-800"> | |
| <h2 className="text-xl font-bold text-slate-900 dark:text-white">New RFP Project</h2> | |
| <button | |
| onClick={onClose} | |
| className="p-2 text-slate-400 hover:text-slate-600 dark:hover:text-slate-300 rounded-lg hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors" | |
| > | |
| <span className="material-symbols-outlined">close</span> | |
| </button> | |
| </div> | |
| {/* Form */} | |
| <form onSubmit={handleSubmit} className="p-6 space-y-5"> | |
| {/* Error Message */} | |
| {error && ( | |
| <div className="p-3 bg-rose-50 dark:bg-rose-900/20 border border-rose-200 dark:border-rose-800 rounded-lg text-rose-700 dark:text-rose-300 text-sm"> | |
| <div className="flex items-start gap-2"> | |
| <span className="material-symbols-outlined text-[18px] mt-0.5 flex-shrink-0">error</span> | |
| <div className="flex-1"> | |
| {error.includes('\n') ? ( | |
| <> | |
| <p className="font-medium">{error.split('\n')[0]}</p> | |
| <code className="block mt-1 text-xs bg-rose-100 dark:bg-rose-900/40 px-2 py-1 rounded font-mono"> | |
| {error.split('\n')[1]} | |
| </code> | |
| </> | |
| ) : ( | |
| <span>{error}</span> | |
| )} | |
| </div> | |
| </div> | |
| </div> | |
| )} | |
| {/* Source Toggle */} | |
| <div className="flex p-1 bg-slate-100 dark:bg-slate-800 rounded-xl"> | |
| <button | |
| type="button" | |
| onClick={() => setSource('upload')} | |
| className={`flex-1 flex items-center justify-center gap-2 py-2 text-xs font-bold rounded-lg transition-all ${ | |
| source === 'upload' ? 'bg-white dark:bg-slate-700 text-primary shadow-sm' : 'text-slate-500' | |
| }`} | |
| > | |
| <span className="material-symbols-outlined text-[18px]">upload_file</span> | |
| Manual Upload | |
| </button> | |
| <button | |
| type="button" | |
| onClick={() => setSource('research')} | |
| className={`flex-1 flex items-center justify-center gap-2 py-2 text-xs font-bold rounded-lg transition-all ${ | |
| source === 'research' ? 'bg-white dark:bg-slate-700 text-primary shadow-sm' : 'text-slate-500' | |
| }`} | |
| > | |
| <span className="material-symbols-outlined text-[18px]">travel_explore</span> | |
| Deep Research Import | |
| </button> | |
| </div> | |
| {source === 'upload' ? ( | |
| /* File Upload Area */ | |
| <div> | |
| <label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2"> | |
| RFP Documents <span className="text-rose-500">*</span> | |
| </label> | |
| <div | |
| className="border-2 border-dashed border-slate-300 dark:border-slate-600 rounded-lg p-6 text-center hover:bg-slate-50 dark:hover:bg-slate-800/50 transition-colors cursor-pointer relative" | |
| onDragOver={handleDragOver} | |
| onDrop={handleDrop} | |
| onClick={() => fileInputRef.current?.click()} | |
| > | |
| <input | |
| ref={fileInputRef} | |
| type="file" | |
| multiple | |
| accept=".pdf,.doc,.docx,.xlsx,.xls,.csv,.png,.jpg,.jpeg,.tiff,.bmp" | |
| onChange={handleFileChange} | |
| className="hidden" | |
| /> | |
| <span className="material-symbols-outlined text-4xl text-slate-400 mb-2 block"> | |
| upload_file | |
| </span> | |
| <p className="text-sm text-slate-500 dark:text-slate-400"> | |
| Click or drag files here to upload | |
| </p> | |
| </div> | |
| {/* File Preview List */} | |
| {files.length > 0 && ( | |
| <div className="mt-4 space-y-2 max-h-32 overflow-y-auto"> | |
| {files.map((f, i) => ( | |
| <div | |
| key={i} | |
| className="flex items-center justify-between text-sm text-slate-600 dark:text-slate-300 bg-slate-100 dark:bg-slate-800 px-3 py-2 rounded-lg" | |
| > | |
| <div className="flex items-center gap-2 overflow-hidden"> | |
| <span className="material-symbols-outlined text-[18px] text-blue-500"> | |
| description | |
| </span> | |
| <span className="truncate">{f.name}</span> | |
| </div> | |
| <button | |
| type="button" | |
| onClick={(e) => { | |
| e.stopPropagation(); | |
| removeFile(i); | |
| }} | |
| className="p-1 text-slate-400 hover:text-rose-500 transition-colors" | |
| > | |
| <span className="material-symbols-outlined text-[18px]">close</span> | |
| </button> | |
| </div> | |
| ))} | |
| </div> | |
| )} | |
| </div> | |
| ) : ( | |
| /* Research Import ID */ | |
| <div> | |
| <label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2"> | |
| Deep Research ID <span className="text-rose-500">*</span> | |
| </label> | |
| <input | |
| type="text" | |
| value={deepResearchId} | |
| onChange={(e) => setDeepResearchId(e.target.value)} | |
| className="w-full border border-slate-300 dark:border-slate-600 rounded-lg px-4 py-2.5 bg-white dark:bg-slate-800 text-slate-900 dark:text-white focus:ring-2 focus:ring-primary focus:border-primary outline-none transition-all" | |
| placeholder="Paste ID from Deep Research view" | |
| required | |
| /> | |
| <p className="text-[10px] text-slate-500 mt-2">This will link the project to the autonomous research logs.</p> | |
| </div> | |
| )} | |
| {/* Info Banner */} | |
| <div className="flex items-center gap-3 px-3 py-2 bg-blue-50 dark:bg-blue-900/20 rounded-lg border border-blue-100 dark:border-blue-800"> | |
| <span className="material-symbols-outlined text-blue-500 text-[20px]">auto_awesome</span> | |
| <p className="text-sm text-blue-700 dark:text-blue-300"> | |
| AI will automatically extract the project name and requirements from your documents. | |
| </p> | |
| </div> | |
| {/* Submit Button */} | |
| <button | |
| type="submit" | |
| disabled={loading || files.length === 0} | |
| className="w-full bg-primary hover:bg-blue-700 disabled:bg-slate-300 dark:disabled:bg-slate-700 text-white font-semibold py-3 rounded-lg transition-all flex justify-center items-center gap-2" | |
| > | |
| {loading ? ( | |
| <> | |
| <span className="material-symbols-outlined text-[20px] animate-spin"> | |
| progress_activity | |
| </span> | |
| {analysisStep === 'uploading' ? 'Uploading Documents...' : | |
| analysisStep === 'shredding' ? 'Extracting Requirements...' : | |
| analysisStep === 'analyzing' ? 'Performing AI Analysis...' : | |
| 'Processing...'} | |
| </> | |
| ) : ( | |
| <> | |
| <span className="material-symbols-outlined text-[20px]">rocket_launch</span> | |
| Create Project & Analyze | |
| </> | |
| )} | |
| </button> | |
| </form> | |
| </div> | |
| </div> | |
| ); | |
| }; | |
| export default CreateRFPModal; | |