'use client'; import { zodResolver } from '@hookform/resolvers/zod'; import { Bot, ChevronDown, ImagePlus, Search, Sparkles, X } from 'lucide-react'; import { useCallback, useRef, useState } from 'react'; import { useForm } from 'react-hook-form'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from '@/components/ui/card'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Select } from '@/components/ui/select'; import { useProjectSearch } from '@/lib/hooks/use-valuation'; import { valuationFormSchema, type ValuationFormData, VALUATION_PROPERTY_TYPES, CITIES, FLOOD_RISK_OPTIONS, } from '@/lib/validations/valuation'; import type { ValuationRequest } from '@/lib/valuation-api'; interface ValuationFormProps { onSubmit: (data: ValuationRequest) => void; isLoading?: boolean; } function toNum(val: string | undefined): number | undefined { if (!val || val === '') return undefined; const n = Number(val); return isNaN(n) ? undefined : n; } export function ValuationForm({ onSubmit, isLoading }: ValuationFormProps) { const { register, handleSubmit, setValue, formState: { errors }, } = useForm({ resolver: zodResolver(valuationFormSchema), defaultValues: { city: 'Ho Chi Minh', hasLegalPaper: true, deepAnalysis: false, }, }); // Project autocomplete state const [projectQuery, setProjectQuery] = useState(''); const [projectName, setProjectName] = useState(''); const [showProjectDropdown, setShowProjectDropdown] = useState(false); const { data: projectResults } = useProjectSearch(projectQuery); const projectInputRef = useRef(null); // Image upload state const [imagePreview, setImagePreview] = useState(null); const [imageUrl, setImageUrl] = useState(null); const [uploadProgress, setUploadProgress] = useState(null); const [uploadError, setUploadError] = useState(null); const fileInputRef = useRef(null); // v2 section collapsible state const [showV2Section, setShowV2Section] = useState(false); const handleProjectSearch = useCallback((e: React.ChangeEvent) => { const value = e.target.value; setProjectQuery(value); setProjectName(value); setShowProjectDropdown(value.length >= 2); if (!value) { setValue('projectId', ''); } }, [setValue]); const handleSelectProject = useCallback( (id: string, name: string) => { setValue('projectId', id); setProjectName(name); setProjectQuery(''); setShowProjectDropdown(false); }, [setValue], ); const handleImageChange = useCallback( (e: React.ChangeEvent) => { const file = e.target.files?.[0]; setUploadError(null); if (!file) return; // Validate MIME type and size before anything else const ALLOWED = ['image/jpeg', 'image/png']; if (!ALLOWED.includes(file.type)) { setUploadError('Chỉ chấp nhận ảnh JPG hoặc PNG.'); if (fileInputRef.current) fileInputRef.current.value = ''; return; } const MAX_BYTES = 5 * 1024 * 1024; if (file.size > MAX_BYTES) { setUploadError('Ảnh vượt quá 5MB. Vui lòng chọn ảnh nhỏ hơn.'); if (fileInputRef.current) fileInputRef.current.value = ''; return; } // Simulate progress during local processing (FileReader is synchronous // for small files; real uploads to MinIO would track XHR progress). setUploadProgress(0); const reader = new FileReader(); reader.onprogress = (ev) => { if (ev.lengthComputable) { setUploadProgress(Math.round((ev.loaded / ev.total) * 100)); } }; reader.onload = (ev) => { setImagePreview(ev.target?.result as string); setUploadProgress(100); // Hide the bar after a tick so users see "100%" briefly setTimeout(() => setUploadProgress(null), 400); }; reader.onerror = () => { setUploadError('Không thể đọc ảnh. Vui lòng thử lại.'); setUploadProgress(null); }; reader.readAsDataURL(file); // For now, use an object URL locally; presigned upload is a separate flow. setImageUrl(URL.createObjectURL(file)); }, [], ); const handleClearImage = useCallback(() => { setImagePreview(null); setImageUrl(null); setUploadError(null); setUploadProgress(null); if (fileInputRef.current) { fileInputRef.current.value = ''; } }, []); const handleFormSubmit = (data: ValuationFormData) => { onSubmit({ propertyType: data.propertyType, area: Number(data.area), district: data.district, city: data.city, bedrooms: toNum(data.bedrooms), bathrooms: toNum(data.bathrooms), floors: toNum(data.floors), frontage: toNum(data.frontage), roadWidth: toNum(data.roadWidth), yearBuilt: toNum(data.yearBuilt), hasLegalPaper: data.hasLegalPaper, projectId: data.projectId || undefined, description: data.description || undefined, deepAnalysis: data.deepAnalysis, imageUrl: imageUrl || undefined, // AVM v2 useV2: data.useV2, distanceToHospitalKm: toNum(data.distanceToHospitalKm), distanceToParkKm: toNum(data.distanceToParkKm), distanceToMallKm: toNum(data.distanceToMallKm), floodZoneRisk: data.floodZoneRisk, hasElevator: data.hasElevator, hasParking: data.hasParking, hasPool: data.hasPool, }); }; return ( Định giá bất động sản Nhập thông tin bất động sản để nhận ước tính giá từ AI
{/* Project selector (autocomplete) */}
projectQuery.length >= 2 && setShowProjectDropdown(true)} onBlur={() => setTimeout(() => setShowProjectDropdown(false), 200)} placeholder="Tìm kiếm dự án..." className="pl-9" /> {projectName && ( )} {showProjectDropdown && projectResults?.data && projectResults.data.length > 0 && (
{projectResults.data.map((project) => ( ))}
)}
{/* Row 1: Property type + City */}
{errors.propertyType && (

{errors.propertyType.message}

)}
{errors.city && (

{errors.city.message}

)}
{/* Row 2: District + Area */}
{errors.district && (

{errors.district.message}

)}
{errors.area && (

{errors.area.message}

)}
{/* Row 3: Bedrooms + Bathrooms + Floors */}
{/* Row 4: Frontage + Road Width + Year Built */}
{/* Image upload */}
{imagePreview ? (
Ảnh bất động sản
) : ( )}

Tải ảnh bất động sản để AI phân tích trực quan (JPG, PNG, tối đa 5MB)

{uploadProgress != null && (
)} {uploadError && (

{uploadError}

)}
{/* Description textarea */}