445 lines
19 KiB
TypeScript
445 lines
19 KiB
TypeScript
"use client";
|
|
|
|
import { motion, useScroll, useTransform } from "framer-motion";
|
|
import {
|
|
Brain,
|
|
Search,
|
|
Database,
|
|
Zap,
|
|
Shield,
|
|
TrendingUp,
|
|
CheckCircle,
|
|
Activity,
|
|
Cpu,
|
|
Network,
|
|
} from "lucide-react";
|
|
import { useRef, useEffect, useState } from "react";
|
|
|
|
export default function AIDiagnosis() {
|
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
const canvasContainerRef = useRef<HTMLDivElement>(null); // Ref for the wrapper of the canvas
|
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
|
const [modelLoaded, setModelLoaded] = useState(false);
|
|
const [scanProgress, setScanProgress] = useState(0);
|
|
|
|
const { scrollYProgress } = useScroll({
|
|
target: containerRef,
|
|
offset: ["start end", "end start"],
|
|
});
|
|
|
|
const y = useTransform(scrollYProgress, [0, 0.5], [100, 0]);
|
|
const opacity = useTransform(scrollYProgress, [0, 0.3], [0, 1]);
|
|
const scale = useTransform(scrollYProgress, [0, 0.5], [0.95, 1]); // Subtle scale
|
|
|
|
// Simulate scan progress
|
|
useEffect(() => {
|
|
const interval = setInterval(() => {
|
|
setScanProgress((prev) => (prev >= 100 ? 0 : prev + 1));
|
|
}, 50);
|
|
return () => clearInterval(interval);
|
|
}, []);
|
|
|
|
// Three.js setup
|
|
useEffect(() => {
|
|
if (typeof window === "undefined" || !canvasRef.current || !canvasContainerRef.current) return;
|
|
|
|
let animationId: number | undefined;
|
|
let scene: any;
|
|
let camera: any;
|
|
let renderer: any;
|
|
let model: any;
|
|
let controls: any;
|
|
|
|
const initThreeJS = async () => {
|
|
const THREE = await import("three");
|
|
const { GLTFLoader, OrbitControls } = await import("three-stdlib");
|
|
|
|
// Get initial dimensions from the parent container
|
|
const width = canvasContainerRef.current?.clientWidth || 500;
|
|
const height = canvasContainerRef.current?.clientHeight || 500;
|
|
|
|
scene = new THREE.Scene();
|
|
camera = new THREE.PerspectiveCamera(45, width / height, 0.1, 1000);
|
|
camera.position.set(0, 0, 5);
|
|
|
|
renderer = new THREE.WebGLRenderer({
|
|
canvas: canvasRef.current!,
|
|
alpha: true,
|
|
antialias: true,
|
|
});
|
|
renderer.setSize(width, height);
|
|
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
|
renderer.setClearColor(0x000000, 0);
|
|
|
|
// Lighting
|
|
scene.add(new THREE.AmbientLight(0xffffff, 0.8));
|
|
const dirLight1 = new THREE.DirectionalLight(0x6366f1, 1.2);
|
|
dirLight1.position.set(5, 5, 5);
|
|
scene.add(dirLight1);
|
|
const dirLight2 = new THREE.DirectionalLight(0xa855f7, 0.8);
|
|
dirLight2.position.set(-5, 3, -5);
|
|
scene.add(dirLight2);
|
|
|
|
// Controls
|
|
controls = new OrbitControls(camera, renderer.domElement);
|
|
controls.enableDamping = true;
|
|
controls.dampingFactor = 0.05;
|
|
controls.enableZoom = false;
|
|
controls.autoRotate = true;
|
|
controls.autoRotateSpeed = 1.5;
|
|
|
|
// Handle Resize - Critical for responsiveness
|
|
const handleResize = () => {
|
|
if (!canvasContainerRef.current || !camera || !renderer) return;
|
|
const newWidth = canvasContainerRef.current.clientWidth;
|
|
const newHeight = canvasContainerRef.current.clientHeight;
|
|
|
|
camera.aspect = newWidth / newHeight;
|
|
camera.updateProjectionMatrix();
|
|
renderer.setSize(newWidth, newHeight);
|
|
};
|
|
|
|
// Use ResizeObserver for robust sizing (handles container changes, not just window)
|
|
const resizeObserver = new ResizeObserver(() => handleResize());
|
|
resizeObserver.observe(canvasContainerRef.current!);
|
|
|
|
// Model loading
|
|
const loader = new GLTFLoader();
|
|
const modelURL = "/human_body.glb";
|
|
|
|
try {
|
|
const gltf = await loader.loadAsync(modelURL);
|
|
model = gltf.scene;
|
|
|
|
// Center & scale model
|
|
const box = new THREE.Box3().setFromObject(model);
|
|
const center = box.getCenter(new THREE.Vector3());
|
|
const size = box.getSize(new THREE.Vector3());
|
|
const maxDim = Math.max(size.x, size.y, size.z);
|
|
const scaleFactor = 3 / maxDim;
|
|
|
|
model.scale.setScalar(scaleFactor);
|
|
model.position.sub(center.multiplyScalar(scaleFactor));
|
|
|
|
model.traverse((child: any) => {
|
|
if (child.isMesh) {
|
|
child.castShadow = false;
|
|
child.receiveShadow = false;
|
|
}
|
|
});
|
|
|
|
scene.add(model);
|
|
setModelLoaded(true);
|
|
} catch (error) {
|
|
console.warn("Using fallback geometry");
|
|
const geometry = new THREE.IcosahedronGeometry(1.5, 2);
|
|
const material = new THREE.MeshStandardMaterial({
|
|
color: 0x6366f1,
|
|
roughness: 0.3,
|
|
metalness: 0.7,
|
|
emissive: 0x3b82f6,
|
|
emissiveIntensity: 0.2,
|
|
wireframe: true, // Added wireframe for a more "tech" fallback look
|
|
});
|
|
model = new THREE.Mesh(geometry, material);
|
|
scene.add(model);
|
|
setModelLoaded(true);
|
|
}
|
|
|
|
// Animation loop
|
|
const animate = () => {
|
|
animationId = requestAnimationFrame(animate);
|
|
controls.update();
|
|
if (model) model.rotation.y += 0.002;
|
|
renderer.render(scene, camera);
|
|
};
|
|
|
|
animate();
|
|
|
|
// Clean up specifically for this closure
|
|
return () => {
|
|
resizeObserver.disconnect();
|
|
};
|
|
};
|
|
|
|
const cleanupPromise = initThreeJS();
|
|
|
|
return () => {
|
|
cleanupPromise.then((cleanup) => cleanup && cleanup());
|
|
if (animationId !== undefined) cancelAnimationFrame(animationId);
|
|
if (controls) controls.dispose();
|
|
if (renderer) renderer.dispose();
|
|
// Simplified scene disposal
|
|
if (scene) scene.clear();
|
|
};
|
|
}, []);
|
|
|
|
return (
|
|
<section
|
|
ref={containerRef}
|
|
id="diagnosis"
|
|
className="relative py-16 sm:py-20 md:py-32 lg:py-40 2xl:py-48 overflow-hidden w-full"
|
|
>
|
|
{/* Ambient glows - Optimized for mobile by reducing blur radius slightly */}
|
|
{/* <div className="absolute top-1/4 left-0 w-[300px] sm:w-[500px] lg:w-[700px] h-[300px] sm:h-[500px] lg:h-[700px] bg-cyan-500/10 rounded-full blur-[80px] sm:blur-[150px] animate-pulse" />
|
|
<div className="absolute bottom-1/4 right-0 w-[250px] sm:w-[400px] lg:w-[600px] h-[250px] sm:h-[400px] lg:h-[600px] bg-purple-500/10 rounded-full blur-[60px] sm:blur-[120px]" /> */}
|
|
|
|
{/* Subtle grid */}
|
|
<div
|
|
className="absolute inset-0 opacity-[0.02]"
|
|
style={{
|
|
backgroundImage: `linear-gradient(rgba(255,255,255,0.1) 1px, transparent 1px),
|
|
linear-gradient(90deg, rgba(255,255,255,0.1) 1px, transparent 1px)`,
|
|
backgroundSize: "50px 50px",
|
|
}}
|
|
/>
|
|
|
|
<div className="container relative z-10 mx-auto px-4 sm:px-6 lg:px-8 max-w-7xl 2xl:max-w-[1600px]">
|
|
<div className="grid lg:grid-cols-2 gap-10 sm:gap-12 lg:gap-16 xl:gap-24 2xl:gap-32 items-center">
|
|
|
|
{/* --- 3D Viewer Column --- */}
|
|
{/* Order-2 on mobile ensures text comes first if desired, but default is fine */}
|
|
<motion.div
|
|
initial={{ opacity: 0, x: -50 }}
|
|
whileInView={{ opacity: 1, x: 0 }}
|
|
viewport={{ once: true }}
|
|
transition={{ duration: 1 }}
|
|
className="relative w-full max-w-[500px] lg:max-w-none mx-auto"
|
|
>
|
|
{/* Background Blur blob */}
|
|
<div className="absolute inset-0 rounded-[3rem] blur-3xl opacity-60 bg-gradient-to-tr from-cyan-900/20 to-purple-900/20" />
|
|
|
|
<div className="relative rounded-3xl backdrop-blur-2xl bg-black/40 border border-white/10 shadow-2xl overflow-hidden">
|
|
{/* Card Header */}
|
|
<div className="relative p-4 sm:p-6 border-b border-white/5">
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-3">
|
|
<div className="relative">
|
|
<div className="w-10 h-10 rounded-xl bg-white/5 flex items-center justify-center">
|
|
<Brain className="w-5 h-5 text-cyan-400" />
|
|
</div>
|
|
<div className="absolute -top-1 -right-1 w-2.5 h-2.5 bg-green-400 rounded-full border-2 border-black animate-pulse" />
|
|
</div>
|
|
<div>
|
|
<h3 className="text-sm font-bold text-white">Neural Engine</h3>
|
|
<p className="text-[10px] sm:text-xs text-zinc-500">Analysis Active</p>
|
|
</div>
|
|
</div>
|
|
<div className="hidden xs:flex px-3 py-1 rounded-full bg-green-500/10 border border-green-500/20">
|
|
<span className="text-[10px] sm:text-xs font-semibold text-green-400">Live Feed</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Canvas Container - Responsive Aspect Ratio */}
|
|
<div className="relative p-4 sm:p-8">
|
|
{/* Decorative corners */}
|
|
<div className="absolute top-4 sm:top-8 left-4 sm:left-8 w-12 sm:w-16 h-12 sm:h-16 border-l-2 border-t-2 border-cyan-400/30 rounded-tl-2xl" />
|
|
<div className="absolute top-4 sm:top-8 right-4 sm:right-8 w-12 sm:w-16 h-12 sm:h-16 border-r-2 border-t-2 border-purple-400/30 rounded-tr-2xl" />
|
|
<div className="absolute bottom-4 sm:bottom-8 left-4 sm:left-8 w-12 sm:w-16 h-12 sm:h-16 border-l-2 border-b-2 border-pink-400/30 rounded-bl-2xl" />
|
|
<div className="absolute bottom-4 sm:bottom-8 right-4 sm:right-8 w-12 sm:w-16 h-12 sm:h-16 border-r-2 border-b-2 border-cyan-400/30 rounded-br-2xl" />
|
|
|
|
{/* The actual canvas wrapper */}
|
|
<div
|
|
ref={canvasContainerRef}
|
|
className="relative w-full aspect-square max-h-[400px] lg:max-h-[500px] flex items-center justify-center mx-auto"
|
|
>
|
|
<canvas
|
|
ref={canvasRef}
|
|
className="w-full h-full relative z-10 block"
|
|
/>
|
|
|
|
{/* Loading State */}
|
|
{!modelLoaded && (
|
|
<div className="absolute inset-0 flex items-center justify-center pointer-events-none">
|
|
<div className="space-y-4 text-center">
|
|
<motion.div
|
|
animate={{ rotate: 360 }}
|
|
transition={{ duration: 2, repeat: Infinity, ease: "linear" }}
|
|
>
|
|
<Brain className="w-12 h-12 sm:w-16 sm:h-16 text-cyan-400 mx-auto" />
|
|
</motion.div>
|
|
<p className="text-xs sm:text-sm text-zinc-400 font-semibold">
|
|
Initializing Network...
|
|
</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Scanning Animation Overlay */}
|
|
{modelLoaded && (
|
|
<div className="absolute inset-0 pointer-events-none overflow-hidden rounded-xl">
|
|
<motion.div
|
|
animate={{ top: ["0%", "100%", "0%"] }}
|
|
transition={{ duration: 3, repeat: Infinity, ease: "linear" }}
|
|
className="absolute left-0 right-0 h-px bg-gradient-to-r from-transparent via-cyan-400 to-transparent opacity-50 shadow-[0_0_15px_rgba(34,211,238,0.5)]"
|
|
/>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Status footer */}
|
|
<div className="p-4 sm:p-6 space-y-4 border-t border-white/5">
|
|
<div className="space-y-2">
|
|
<div className="flex items-center justify-between text-xs">
|
|
<span className="text-zinc-400 font-semibold">Analysis Progress</span>
|
|
<span className="text-cyan-400 font-bold">{scanProgress}%</span>
|
|
</div>
|
|
<div className="h-1.5 sm:h-2 bg-white/5 rounded-full overflow-hidden">
|
|
<motion.div
|
|
style={{ width: `${scanProgress}%` }}
|
|
className="h-full bg-gradient-to-r from-cyan-500 via-purple-500 to-pink-500"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-3 gap-2 sm:gap-3">
|
|
{[
|
|
{ icon: Cpu, label: "CPU Load", value: "98%", color: "text-cyan-400" },
|
|
{ icon: Network, label: "Net Load", value: "76%", color: "text-purple-400" },
|
|
{ icon: Activity, label: "Accuracy", value: "99%", color: "text-green-400" },
|
|
].map((stat, idx) => (
|
|
<div
|
|
key={idx}
|
|
className="p-2 sm:p-3 rounded-xl bg-white/[0.03] border border-white/5 text-center sm:text-left"
|
|
>
|
|
<stat.icon className={`w-3 h-3 sm:w-4 sm:h-4 ${stat.color} mb-1 sm:mb-2 mx-auto sm:mx-0`} />
|
|
<p className="hidden sm:block text-[10px] text-zinc-500 mb-0.5">{stat.label}</p>
|
|
<p className="text-xs sm:text-sm font-bold text-white">{stat.value}</p>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="pb-4 sm:pb-6 text-center">
|
|
<p className="text-[10px] text-zinc-600 font-medium">Interactive 3D Visualization</p>
|
|
</div>
|
|
</div>
|
|
</motion.div>
|
|
|
|
{/* --- Text / Features Column --- */}
|
|
<motion.div
|
|
style={{ y, opacity, scale }}
|
|
className="space-y-8 sm:space-y-10 lg:space-y-12 text-center lg:text-left"
|
|
>
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 30 }}
|
|
whileInView={{ opacity: 1, y: 0 }}
|
|
viewport={{ once: true }}
|
|
transition={{ duration: 0.7, delay: 0.1 }}
|
|
className="space-y-4 sm:space-y-6"
|
|
>
|
|
<h2 className="text-3xl sm:text-4xl md:text-5xl lg:text-6xl 2xl:text-7xl font-bold leading-[1.1] tracking-tight text-white">
|
|
Neural Clinical
|
|
<br />
|
|
<span className="bg-gradient-to-r from-purple-400 via-pink-400 to-cyan-400 bg-clip-text text-transparent">
|
|
Synthesis
|
|
</span>
|
|
</h2>
|
|
<p className="text-sm sm:text-base md:text-lg lg:text-xl text-zinc-400 leading-relaxed max-w-2xl mx-auto lg:mx-0">
|
|
Enterprise-grade diagnostic assistance powered by advanced AI. Cross-reference clinical patterns against{" "}
|
|
<span className="text-white font-semibold">global medical databases</span> with{" "}
|
|
<span className="text-white font-semibold">sub-second latency</span>.
|
|
</p>
|
|
</motion.div>
|
|
|
|
{/* Feature Cards - Stack on mobile, grid on tablet+ */}
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 30 }}
|
|
whileInView={{ opacity: 1, y: 0 }}
|
|
viewport={{ once: true }}
|
|
transition={{ duration: 0.7, delay: 0.2 }}
|
|
className="grid grid-cols-1 sm:grid-cols-2 gap-4 sm:gap-6"
|
|
>
|
|
{[
|
|
{
|
|
icon: Search,
|
|
title: "Pattern Recognition",
|
|
text: "AI-powered clinical marker identification with zero-bias analysis technology.",
|
|
color: "from-cyan-500 to-cyan-500",
|
|
iconColor: "text-cyan-400",
|
|
},
|
|
{
|
|
icon: Database,
|
|
title: "Medical Database",
|
|
text: "Instant access to verified institutional medical models and research.",
|
|
color: "from-purple-500 to-pink-500",
|
|
iconColor: "text-purple-400",
|
|
},
|
|
].map((item, i) => (
|
|
<motion.div
|
|
key={i}
|
|
whileHover={{ y: -4, transition: { duration: 0.2 }}}
|
|
className="group p-5 rounded-2xl bg-white/[0.02] border border-white/5 backdrop-blur-sm hover:bg-white/[0.04] hover:border-white/10 transition-all cursor-pointer text-left"
|
|
>
|
|
<div className="flex sm:block items-start gap-4">
|
|
<div className="relative shrink-0">
|
|
<div className={`absolute inset-0 bg-gradient-to-br ${item.color} rounded-xl blur-lg opacity-0 group-hover:opacity-50 transition-opacity`} />
|
|
<div className="relative w-10 h-10 sm:w-12 sm:h-12 rounded-xl bg-gradient-to-br from-white/10 to-white/5 border border-white/10 flex items-center justify-center group-hover:border-white/20 transition-colors">
|
|
<item.icon className={`w-5 h-5 sm:w-6 sm:h-6 ${item.iconColor}`} />
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-1 sm:space-y-2 sm:mt-4">
|
|
<h3 className="text-base sm:text-lg font-bold text-white group-hover:text-white/90 transition-colors">
|
|
{item.title}
|
|
</h3>
|
|
<p className="text-xs sm:text-sm text-zinc-400 leading-relaxed">{item.text}</p>
|
|
</div>
|
|
</div>
|
|
</motion.div>
|
|
))}
|
|
</motion.div>
|
|
|
|
{/* Stats - Grid adjustments */}
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 20 }}
|
|
whileInView={{ opacity: 1, y: 0 }}
|
|
viewport={{ once: true }}
|
|
transition={{ duration: 0.7, delay: 0.4 }}
|
|
className="grid grid-cols-3 gap-2 sm:gap-6 pt-4 sm:pt-6 border-t border-white/5 lg:border-none"
|
|
>
|
|
{[
|
|
{ icon: Zap, value: "<500ms", label: "Query Time" },
|
|
{ icon: Shield, value: "100%", label: "Secure" },
|
|
{ icon: TrendingUp, value: "99.8%", label: "Accuracy" },
|
|
].map((stat, i) => (
|
|
<div key={i} className="text-center space-y-1 sm:space-y-2">
|
|
<div className="flex justify-center">
|
|
<div className="p-1.5 sm:p-2 rounded-lg bg-white/5 border border-white/10">
|
|
<stat.icon className="w-3.5 h-3.5 sm:w-5 sm:h-5 text-purple-400" />
|
|
</div>
|
|
</div>
|
|
<p className="text-lg sm:text-xl md:text-2xl font-bold text-white">{stat.value}</p>
|
|
<p className="text-[9px] sm:text-xs text-zinc-500 font-semibold uppercase tracking-wider">
|
|
{stat.label}
|
|
</p>
|
|
</div>
|
|
))}
|
|
</motion.div>
|
|
|
|
{/* Trust Indicators */}
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 20 }}
|
|
whileInView={{ opacity: 1, y: 0 }}
|
|
viewport={{ once: true }}
|
|
transition={{ duration: 0.7, delay: 0.5 }}
|
|
className="flex flex-wrap justify-center lg:justify-start gap-2 sm:gap-4 pt-2"
|
|
>
|
|
{["FDA Compliant", "HIPAA Certified", "ISO 27001"].map((badge, i) => (
|
|
<div
|
|
key={i}
|
|
className="flex items-center gap-1.5 sm:gap-2 px-3 py-1.5 sm:px-4 sm:py-2 rounded-full bg-white/[0.03] border border-white/10 backdrop-blur-sm"
|
|
>
|
|
<CheckCircle className="w-3 h-3 sm:w-4 sm:h-4 text-green-400" />
|
|
<span className="text-[10px] sm:text-xs text-zinc-400 font-semibold">{badge}</span>
|
|
</div>
|
|
))}
|
|
</motion.div>
|
|
</motion.div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
);
|
|
} |