contravaulthvnkz / components /CreateRFPModal.tsx
jackmichael's picture
feat(ui): Auto-extract project name from documents
cd3b9cb
'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;