'use client'; import { zodResolver } from '@hookform/resolvers/zod'; import { Bot, ChevronDown, ImagePlus, MapPin, Search, Sparkles, Star, 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, QUALITY_LABELS, } from '@/lib/validations/valuation'; import type { ValuationRequest } from '@/lib/valuation-api'; interface ValuationFormProps { onSubmit: (data: ValuationRequest) => void; isLoading?: boolean; } function CollapsibleSection({ title, icon, description, children, }: { title: string; icon: React.ReactNode; description: string; children: React.ReactNode; }) { const [open, setOpen] = useState(false); return (
{open &&
{children}
}
); } function QualitySlider({ id, label, register, isInverted, }: { id: string; label: string; register: ReturnType>['register']; isInverted?: boolean; }) { const [value, setValue] = useState(50); const displayValue = Math.round(value * 100) / 100; const normalizedForApi = value / 100; return (
{displayValue}%
setValue(Number(e.target.value))} className={`h-2 w-full cursor-pointer appearance-none rounded-full ${ isInverted ? 'bg-gradient-to-r from-green-200 via-yellow-200 to-red-200' : 'bg-gradient-to-r from-red-200 via-yellow-200 to-green-200' } accent-primary`} />
); } 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); const MAX_IMAGE_SIZE_BYTES = 5 * 1024 * 1024; // 5MB 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]; if (!file) return; setUploadError(null); if (!file.type.startsWith('image/')) { setUploadError('Định dạng không hợp lệ. Vui lòng chọn ảnh JPG hoặc PNG.'); return; } if (file.size > MAX_IMAGE_SIZE_BYTES) { setUploadError('Ảnh vượt quá giới hạn 5MB.'); return; } // Show local preview const reader = new FileReader(); reader.onload = (ev) => { setImagePreview(ev.target?.result as string); }; reader.readAsDataURL(file); // Simulated upload progress for local preview flow — the valuation // endpoint accepts a public imageUrl, so the real upload is a no-op // here, but users still get feedback for files being processed. setUploadProgress(0); const start = Date.now(); const tick = () => { const elapsed = Date.now() - start; const pct = Math.min(100, Math.round((elapsed / 400) * 100)); setUploadProgress(pct); if (pct < 100) { requestAnimationFrame(tick); } else { setTimeout(() => setUploadProgress(null), 500); } }; requestAnimationFrame(tick); // In production, upload to server and get URL // For now we store as object URL for preview purposes setImageUrl(URL.createObjectURL(file)); }, [], ); const handleClearImage = useCallback(() => { setImagePreview(null); setImageUrl(null); setUploadProgress(null); setUploadError(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, // v2 fields useV2: data.useV2, distanceToCbdKm: toNum(data.distanceToCbdKm), distanceToMetroKm: toNum(data.distanceToMetroKm), distanceToSchoolKm: toNum(data.distanceToSchoolKm), distanceToHospitalKm: toNum(data.distanceToHospitalKm), distanceToParkKm: toNum(data.distanceToParkKm), distanceToMallKm: toNum(data.distanceToMallKm), floodZoneRisk: toNum(data.floodZoneRisk), hasElevator: data.hasElevator, hasParking: data.hasParking, hasPool: data.hasPool, renovationScore: toNum(data.renovationScore), viewQuality: toNum(data.viewQuality), interiorQuality: toNum(data.interiorQuality), noiseLevel: toNum(data.noiseLevel), naturalLight: toNum(data.naturalLight), }); }; 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 && (
{uploadProgress}%
)} {uploadError && (

{uploadError}

)}
{/* Description textarea */}