diff --git a/README.md b/README.md index 5f2577e..a340141 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ Today's AI assistants are *too slow*, *too basic*, or *too disconnected* from real user needs. **NbAIl** solves this by combining **super-fast text, smooth voice conversation, intelligent command execution, and planned augmented reality (AR) capabilities** — all in a **single multimodal powerhouse**. -One of NbAIl’s proudest innovations: **Command Mode**. +One of NbAIl's proudest innovations: **Command Mode**. Simply type a command like `/open notepad and write hello world`, and our **Terminator Agent**, powered by **Groq** for understanding and **local Python automation**, will **open system apps, type messages, and perform actions** automatically. **Zero manual work. Full smart automation.** Built to behave like a *true futuristic assistant* — not just a chatbot. @@ -68,7 +68,7 @@ Built to behave like a *true futuristic assistant* — not just a chatbot. - ✅ **Multi-mode Switcher** — instantly switch between chat, voice, command, and AR - ✅ **Error Handling & Fail-Safes** — graceful recovery when apps not found -🚀 **NbAIl doesn’t just "chat." It "acts."** +🚀 **NbAIl doesn't just "chat." It "acts."** --- @@ -125,7 +125,7 @@ python terminator_agent.py ```bash ngrok http 8000 -(Replace 5000 with your local port if it’s different.) +(Replace 5000 with your local port if it's different.) ``` — @@ -147,7 +147,7 @@ ngrok http 8000 # 🔮 Future Scope of NbAIl While NbAIl already delivers a futuristic multimodal experience, the journey has just begun! -Here’s what’s planned for future versions: +Here's what's planned for future versions: --- @@ -190,7 +190,7 @@ Here’s what’s planned for future versions: # 📚 Resources / Credits This project would not have been possible without the amazing technologies, APIs, and open-source tools available to the community. -Here’s what powered NbAIl: +Here's what powered NbAIl: --- @@ -254,3 +254,29 @@ Bigger, smarter, and even crazier updates are on the way. > **"The ones who are crazy enough to think they can change the world are the ones who do."** > — Apple, 1997 + +## Environment Variables + +You'll need to set up the following environment variables in your `.env.local` file: + +- `NEXT_PUBLIC_GROQ_API_KEY`: Your Groq API key for AI analysis +- `NEXT_PUBLIC_MISTRAL_API_KEY`: Your Mistral API key for alternative AI analysis (optional) + +### Obtaining API Keys + +1. **Groq API Key**: + - Visit [Groq's website](https://console.groq.com/) + - Create an account and generate an API key + +2. **Mistral API Key**: + - Visit [Mistral's website](https://mistral.ai/) + - Create an account and generate an API key + - Note: We're using the Pixtral 12B 2409 model for AR mode vision analysis + +Example `.env.local` file: +``` +NEXT_PUBLIC_GROQ_API_KEY=your_groq_api_key_here +NEXT_PUBLIC_MISTRAL_API_KEY=your_mistral_api_key_here +``` + +**Note**: Keep your API keys confidential and never commit them to version control. diff --git a/app/ar-mode/MobileCarousel.tsx b/app/ar-mode/MobileCarousel.tsx new file mode 100644 index 0000000..5bda835 --- /dev/null +++ b/app/ar-mode/MobileCarousel.tsx @@ -0,0 +1,120 @@ +import { AnimatePresence, motion } from "framer-motion"; +import Carousel from "@/components/ui/carousel-new"; // Updated import path +import { FiLayers, FiCode, FiFileText } from "react-icons/fi"; + +// Define the slide data structure that matches arModeSlides +interface SlideData { + title: string; + button: string; + description: string; + src?: string; +} + +interface MobileCarouselProps { + showCards: boolean; + slides?: SlideData[]; +} + +const MobileCarousel = ({ showCards, slides }: MobileCarouselProps) => { + // Default slide data if none is provided + const defaultSlides = [ + { + title: "AR Insights", + description: "Explore AI-powered scene analysis", + id: 1, + icon: , + }, + { + title: "Identity Snapshot", + description: "Demographic and emotional insights about detected individuals.", + button: "View Traits", + src: "/images/identity-analysis.jpg", + id: 2, + icon: , + }, + { + title: "Object Detection", + description: "Detected Objects: - None\nDetected Gestures: - None", + button: "Recognize Items", + id: 3, + icon: , + } + ]; + + // Convert the slides from arModeSlides format to CarouselItem format if slides are provided + const carouselItems = slides ? slides.map((slide, index) => { + let icon; + // Assign icons based on slide title or index + if (slide.title.includes("Scene")) { + icon = ; + } else if (slide.title.includes("Identity")) { + icon = ; + + // If this is the Identity Snapshot slide, parse the traits + if (slide.description && slide.description.includes('\n')) { + const traitPills = slide.description.split('\n') + .filter(trait => trait.trim() !== '') + .map(trait => { + let icon = '🤔'; + if (trait.includes('Age')) icon = '🎂'; + if (trait.includes('Gender')) icon = '👤'; + if (trait.includes('Mood')) icon = '😀'; + return `${icon} ${trait.replace('- ', '')}`; + }); + + // Update description with trait information + slide.description = traitPills.length > 0 + ? traitPills.join('\n') + : 'No Clear Face Detected. Check the lighting'; + } + } else if (slide.title.includes("Object")) { + icon = ; + + // Directly set the description to match desktop view card + slide.description = `**Objects & Gestures:** +- No objects detected +- No gestures detected`; + } + + return { + title: slide.title, + description: slide.description, + id: index + 1, + icon: icon || , // Ensure icon is always defined + image: slide.title.includes("Identity") ? slide.src : undefined, // Only add image for Identity Snapshot + data: undefined // Remove data for all slides + }; + }) : defaultSlides; + + return ( + // Only show on mobile devices (md:hidden) +
+
+ + {showCards && ( + +
+ +
+
+ )} +
+
+
+ ); +}; + +export default MobileCarousel; \ No newline at end of file diff --git a/app/ar-mode/ar-mode-content.tsx b/app/ar-mode/ar-mode-content.tsx index e52ee69..ea338b1 100644 --- a/app/ar-mode/ar-mode-content.tsx +++ b/app/ar-mode/ar-mode-content.tsx @@ -5,12 +5,15 @@ import { useRouter } from "next/navigation" import { Button } from "@/components/ui/button" import { ArrowLeft, Camera, Mic, Loader2, AlertTriangle, Volume2, X, RefreshCw, CircleDot, ArrowRight } from "lucide-react" import { motion, AnimatePresence } from "framer-motion" -// Import the new service function -import { getGroqVisionAnalysis, getGroqTranscription } from "@/lib/groq-service" +// Import the new service function and provider enum +import { getGroqVisionAnalysis, getGroqTranscription, AIProvider } from "@/lib/groq-service" import { speakText } from "@/lib/tts-service" import ReactMarkdown from 'react-markdown' import InfoWidget from '@/components/ar/InfoWidget' import React from 'react' +import Carousel from "@/components/ui/carousel-new" +import { FiLayers, FiCode, FiFileText } from 'react-icons/fi' +import MobileCarousel from './MobileCarousel' // Import the MobileCarousel component // THESE IMPORTS ARE LIKELY NEEDED BASED ON LINTER ERRORS - ADDING THEM HERE import * as faceapi from 'face-api.js'; @@ -57,6 +60,7 @@ export default function ARModeContent() { // Renamed from ARModePage const captureIntervalRef = useRef(null); const [recordStartTime, setRecordStartTime] = useState(null); const [isRecording, setIsRecording] = useState(false); + const [aiProvider, setAiProvider] = useState(AIProvider.GROQ); // Face-API model loading options @@ -410,7 +414,7 @@ export default function ARModeContent() { // Renamed from ARModePage } try { - const response = await getGroqVisionAnalysis(imageDataUrl, DETAILED_BASE_PROMPT); + const response = await getGroqVisionAnalysis(imageDataUrl, DETAILED_BASE_PROMPT, aiProvider); setAiResponse(response); console.log("[captureAndAnalyzeFrame] Groq vision analysis complete. Response:", response); } catch (err) { @@ -432,7 +436,8 @@ export default function ARModeContent() { // Renamed from ARModePage setAiResponse, setShowCards, setLastAnalysisTime, - videoRef // videoRef.current is used by analysis helpers + videoRef, + aiProvider ]); // Effect to handle recording logic (timer and periodic capture) @@ -565,7 +570,7 @@ export default function ARModeContent() { // Renamed from ARModePage try { // Using DETAILED_BASE_PROMPT for manual analysis as well - const response = await getGroqVisionAnalysis(imageDataUrl, DETAILED_BASE_PROMPT) + const response = await getGroqVisionAnalysis(imageDataUrl, DETAILED_BASE_PROMPT, aiProvider) setAiResponse(response) } catch (err) { console.error("Error analyzing image with Groq (handleAnalyzeImage):", err); @@ -577,7 +582,7 @@ export default function ARModeContent() { // Renamed from ARModePage setShowCards(true); // Show all cards after analyses are attempted setIsAnalyzing(false); setStatusMessage(null); - }, [modelsReady, mediaPipeModelsReady, captureFrame, DETAILED_BASE_PROMPT, performFaceApiAnalysis, performMediaPipeAnalysis, getGroqVisionAnalysis, setAiResponse, setCapturedImagePreviewUrl, setObjectCardContent, setIdentityInfoContent, setError, setFaceApiError, setMediaPipeError, setShowCards, setStatusMessage, videoRef]); + }, [modelsReady, mediaPipeModelsReady, captureFrame, DETAILED_BASE_PROMPT, performFaceApiAnalysis, performMediaPipeAnalysis, getGroqVisionAnalysis, setAiResponse, setCapturedImagePreviewUrl, setObjectCardContent, setIdentityInfoContent, setError, setFaceApiError, setMediaPipeError, setShowCards, setStatusMessage, videoRef, aiProvider]); // --- Flip Camera --- @@ -778,7 +783,7 @@ export default function ARModeContent() { // Renamed from ARModePage try { // Send the combined prompt to the vision analysis function - const response = await getGroqVisionAnalysis(imageDataUrl, finalPrompt) + const response = await getGroqVisionAnalysis(imageDataUrl, finalPrompt, aiProvider) setAiResponse(response) setStatusMessage("Speaking response...") // Speak the response @@ -813,7 +818,7 @@ export default function ARModeContent() { // Renamed from ARModePage setIsAnalyzing(false); setShowCards(true); // Show cards after all analysis attempts (might be empty if errors) } - }, [captureFrame, modelsReady, mediaPipeModelsReady, DETAILED_BASE_PROMPT, performFaceApiAnalysis, performMediaPipeAnalysis, getGroqVisionAnalysis, speakText, videoRef, setCapturedImagePreviewUrl, setObjectCardContent, setIdentityInfoContent, setAiResponse, setError, setFaceApiError, setMediaPipeError, setShowCards, setStatusMessage, setIsAnalyzing, setIsSpeaking]); + }, [captureFrame, modelsReady, mediaPipeModelsReady, DETAILED_BASE_PROMPT, performFaceApiAnalysis, performMediaPipeAnalysis, getGroqVisionAnalysis, speakText, videoRef, setCapturedImagePreviewUrl, setObjectCardContent, setIdentityInfoContent, setAiResponse, setError, setFaceApiError, setMediaPipeError, setShowCards, setStatusMessage, setIsAnalyzing, setIsSpeaking, aiProvider]); // Main prediction loop and drawing logic useEffect(() => { @@ -861,23 +866,43 @@ export default function ARModeContent() { // Renamed from ARModePage // ... existing code ... }, []); + // Prepare slides data for the new Carousel + const arModeSlides = [ + { + title: "Scene Analysis", + button: "Explore Insights", + description: aiResponse || "Detailed description of the surrounding environment, capturing key visual elements and context.", + }, + { + title: "Identity SnapShot", + button: "View Traits", + src: capturedImagePreviewUrl || "/images/identity-analysis.jpg", + description: identityInfoContent || "Demographic and emotional insights about detected individuals." + }, + { + title: "Object Detection", + button: "Recognize Items", + src: "/images/object-detection.jpg", + description: objectCardContent || "Comprehensive list of detected objects and gestures in the scene." + } + ]; + return ( // Conditional rendering based on isClient is handled by the early return // So, this JSX assumes it's client-side.
{/* Header */}
- -

AR Mode

{/* Recording Timer / Info Widget Container */} +
+
router.push('/chat')} + className="flex items-center gap-2 px-3 py-1 rounded-full bg-white/10 text-white text-xs cursor-pointer hover:bg-white/20 transition-colors" + > + Chats +
+
+
{isRecording && (
@@ -885,16 +910,27 @@ export default function ARModeContent() { // Renamed from ARModePage {formatRecordingTime(recordingTime)}
)} - {/* Info Widget with updated styling */} -
-
router.push('/chat')} - className="flex items-center gap-2 px-3 py-1 rounded-full bg-white/10 text-white text-xs cursor-pointer hover:bg-white/20 transition-colors" - > - Chats -
+
+
+ {/* AI Provider Selector */} +
+ +
+ + + +
+
+
@@ -1007,192 +1043,240 @@ export default function ARModeContent() { // Renamed from ARModePage
- {/* Detected Person Card - Top Left */} -
-
- - {showCards && (capturedImagePreviewUrl || identityInfoContent) && ( - - {/* Image Section */} -
-
-

- {capturedImagePreviewUrl ? "Identity Snapshot" : "Detecting..."} -

-
- {capturedImagePreviewUrl && ( - Captured scene - )} -
- - {/* Identity Traits Pills */} - {identityInfoContent && ( -
- {identityInfoContent.split('\n').map((trait, index) => { - const cleanTrait = trait.replace('- ', ''); - let icon = '🤔'; - if (cleanTrait.includes('Age')) icon = '🎂'; - if (cleanTrait.includes('Gender')) icon = '👤'; - if (cleanTrait.includes('Mood')) icon = '😀'; - - return ( -
- {icon} {cleanTrait} -
- ); - })} -
- )} + {/* Responsive Info Cards Container */} +
+ {/* Top Row Elements - Desktop/Tablet */} +
+
router.push('/chat')} + className="flex items-center gap-2 px-3 py-1 rounded-full bg-white/10 text-white text-xs cursor-pointer hover:bg-white/20 transition-colors" + > + Chats +
+
+
+ +
+
- {!identityInfoContent && ( -
-
- 🤔 No Clear Face Detected + {/* Carousel Section - Mobile Only */} + + + {/* Desktop/Tablet Info Cards - Hidden on Mobile */} +
+ {/* Detected Person Card - Top Left */} +
+
+ + {showCards && (capturedImagePreviewUrl || identityInfoContent) && ( + + {/* Image Section */} +
+
+

+ {capturedImagePreviewUrl ? "Identity Snapshot" : "Detecting..."} +

+ {capturedImagePreviewUrl && ( + Captured scene + )}
- )} - - {/* Footer */} -

- Detected by NbAIl -

-
- )} -
-
-
- {/* Objects & Gestures Card - Bottom Left */} -
-
- - {showCards && objectCardContent && ( - -

Objects & Gestures

- - {objectCardContent && ( - <> -

Detected Objects

-
- {objectCardContent.includes("Detected Objects:") && - objectCardContent.split("\n") - .filter(line => line.trim().startsWith("- ")) - .map((item, index) => ( - - {item.replace("- ", "")} - - )) - } -
+ {/* Identity Traits Pills */} + {identityInfoContent && ( +
+ {identityInfoContent.split('\n').map((trait, index) => { + const cleanTrait = trait.replace('- ', ''); + let icon = '🤔'; + if (cleanTrait.includes('Age')) icon = '🎂'; + if (cleanTrait.includes('Gender')) icon = '👤'; + if (cleanTrait.includes('Mood')) icon = '😀'; + + return ( +
+ {icon} {cleanTrait} +
+ ); + })} +
+ )} -

Detected Gestures

+ {!identityInfoContent && (
- {objectCardContent.includes("Detected Gestures:") && - objectCardContent.split("\n") - .filter(line => line.trim().startsWith("- ") && line.trim() !== "- None") - .map((item, index) => ( - - {item.replace("- ", "")} - - )) - } - {(!objectCardContent.includes("Detected Gestures:") || - objectCardContent.includes("- None")) && ( - - No gestures detected - - )} +
+ 🤔 No Clear Face Detected +
- - )} -
- )} -
-
-
+ )} - {/* Scene Analysis Card - Bottom Right */} -
-
- - {showCards && aiResponse && ( - -
-

Scene Analysis

- {!isRecording && ( - - )} -
-
- {aiResponse && ( - { - // Safely handle children - const processText = (text: string) => - text.split(/\s+/).map((word, wordIndex) => - word.length > 5 - ? {word} - : {word} - ); + {/* Footer */} +

+ Detected by NbAIl +

+ + )} + +
+
+ + {/* Objects & Gestures Card - Bottom Left */} +
+
+ + {showCards && objectCardContent && ( + +

Objects & Gestures

+ + {objectCardContent && ( + <> +

Detected Objects

+
+ {objectCardContent.includes("Detected Objects:") && + objectCardContent.split("\n") + .filter(line => line.trim().startsWith("- ")) + .map((item, index) => ( + + {item.replace("- ", "")} + + )) + } +
- const processChild = (child: React.ReactNode, index: number) => { - if (typeof child === 'string') { - return processText(child); - } - return child; - }; - - return ( -

- {React.Children.map(children, processChild)} -

- ); +

Detected Gestures

+
+ {objectCardContent.includes("Detected Gestures:") && + objectCardContent.split("\n") + .filter(line => line.trim().startsWith("- ") && line.trim() !== "- None") + .map((item, index) => ( + + {item.replace("- ", "")} + + )) } - }} - > - {aiResponse} - + {(!objectCardContent.includes("Detected Gestures:") || + objectCardContent.includes("- None")) && ( + + No gestures detected + + )} +
+ )} -
- - )} - + + )} + +
+
+ + {/* Scene Analysis Card - Bottom Right */} +
+
+ + {showCards && aiResponse && ( + +
+

Scene Analysis

+ {!isRecording && ( + + )} +
+
+ {aiResponse && ( + { + // Safely handle children + const processText = (text: string) => + text.split(/\s+/).map((word, wordIndex) => + word.length > 5 + ? {word} + : {word} + ); + + const processChildren = (children: React.ReactNode) => { + return React.Children.map(children, (child, index) => { + if (typeof child === 'string') { + return processText(child); + } + return child; + }); + }; + + return ( +

+ {processChildren(props.children)} +

+ ); + } + }} + > + {aiResponse} +
+ )} +
+
+ )} +
+
-
+
+ + {/* Header for Mobile */} +
+
+
router.push('/chat')} + className="flex items-center gap-2 px-3 py-1 rounded-full bg-white/10 text-white text-xs cursor-pointer hover:bg-white/20 transition-colors" + > + Chats +
+
+ +
+ {isRecording && ( +
+ + {formatRecordingTime(recordingTime)} +
+ )} +
+ +
+
+
{/* Controls */}
@@ -1239,7 +1323,7 @@ export default function ARModeContent() { // Renamed from ARModePage aria-label={isRecording ? "Stop Recording" : "Start Recording"} > - {/* Pulsing effect when idle and enabled (and not recording) */} + {/* Pulsing effect when idle and enabled */} {!isAnalyzing && !isMicListening && !isTranscribing && !isSpeaking && !error && stream && modelsReady && mediaPipeModelsReady && !isRecording && !faceApiError && !mediaPipeError && ( )} diff --git a/app/features/page.tsx b/app/features/page.tsx index f7b0202..cf04f59 100644 --- a/app/features/page.tsx +++ b/app/features/page.tsx @@ -1,7 +1,7 @@ "use client" import { useState } from "react"; -import LenisWrapper from '@/components/LenisWrapper'; +import { LenisProvider } from "@/context/LenisProvider"; import Navbar from "@/components/navbar" import Footer from "@/components/footer" import React, { ReactNode, useRef } from "react"; @@ -438,7 +438,7 @@ export default function FeaturesPage() { ] return ( - +
- + ) } diff --git a/app/page.tsx b/app/page.tsx index f490faf..07a5a47 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -19,38 +19,41 @@ import { Mic, Monitor, FileText, Glasses, Brain, History } from "lucide-react"; import { GraduationCap, Code, Palette, Users } from "lucide-react"; import { Button } from "@/components/ui/button"; import Link from "next/link"; +import { LenisProvider } from "@/context/LenisProvider"; export default function Home() { return ( -
- {/* Interactive background with moving particles - Version 1 wala */} -
- -
+ +
+ {/* Interactive background with moving particles - Version 1 wala */} +
+ +
-
- - - - {/* // Removed original Features section */} - {/* // Removed original Use Cases section */} - - -
-
-
+
+ + + + {/* // Removed original Features section */} + {/* // Removed original Use Cases section */} + + +
+
+
+ ) } diff --git a/app/pricing/page.tsx b/app/pricing/page.tsx index cd2ec7a..9f569fe 100644 --- a/app/pricing/page.tsx +++ b/app/pricing/page.tsx @@ -1,6 +1,6 @@ "use client" -import LenisWrapper from '@/components/LenisWrapper'; +import { LenisProvider } from "@/context/LenisProvider"; import Navbar from "@/components/navbar" import Footer from "@/components/footer" import { motion } from "framer-motion" @@ -78,7 +78,7 @@ export default function PricingPage() { ] return ( - +
- + ) } diff --git a/app/research/page.tsx b/app/research/page.tsx index f2ee3c6..4d8b789 100644 --- a/app/research/page.tsx +++ b/app/research/page.tsx @@ -1,6 +1,6 @@ import { Suspense } from "react" import dynamic from "next/dynamic" -import LenisWrapper from '@/components/LenisWrapper'; +import { LenisProvider } from "@/context/LenisProvider"; // Dynamically import the client component const DynamicResearchPage = dynamic(() => import("@/components/research/research-page"), { @@ -22,10 +22,10 @@ export const metadata = { export default function ResearchPage() { return ( - + }> - + ) } diff --git a/app/use-cases/page.tsx b/app/use-cases/page.tsx index 73c41a4..f67d2eb 100644 --- a/app/use-cases/page.tsx +++ b/app/use-cases/page.tsx @@ -1,7 +1,7 @@ "use client" import { useState } from "react" -import LenisWrapper from '@/components/LenisWrapper'; +import { LenisProvider } from "@/context/LenisProvider"; import Navbar from "@/components/navbar" import Footer from "@/components/footer" import React, { ReactNode, useRef } from "react"; @@ -281,7 +281,7 @@ export default function UseCasesPage() { ] return ( - +
- + ) } diff --git a/components/ui/carousel-new.tsx b/components/ui/carousel-new.tsx new file mode 100644 index 0000000..81dc7d8 --- /dev/null +++ b/components/ui/carousel-new.tsx @@ -0,0 +1,313 @@ +import { useEffect, useState, useRef } from "react"; +import { motion, PanInfo, useMotionValue, useTransform } from "framer-motion"; +// replace icons with your own if needed +import { + FiCircle, + FiCode, + FiFileText, + FiLayers, + FiLayout, + FiCreditCard, +} from "react-icons/fi"; + +export interface CarouselItem { + title: string; + description: string; + id: number; + icon: JSX.Element; + image?: string; + data?: string | Record; +} + +export interface CarouselProps { + items?: CarouselItem[]; + baseWidth?: number; + autoplay?: boolean; + autoplayDelay?: number; + pauseOnHover?: boolean; + loop?: boolean; + round?: boolean; +} + +const DEFAULT_ITEMS: CarouselItem[] = [ + { + title: "Text Animations", + description: "**Cool** text animations for your *projects*. ~~Lightweight~~ ##Powerful", + id: 1, + icon: , + image: "/path/to/sample/image.jpg", + data: "Sample data from Groq" + }, + { + title: "Animations", + description: "**Smooth** animations for your *projects*. ##Responsive design", + id: 2, + icon: , + }, + { + title: "Components", + description: "**Reusable** components for your *projects*. ~~Simple~~ ##Advanced", + id: 3, + icon: , + }, + { + title: "Backgrounds", + description: "**Beautiful** backgrounds and *patterns* for your projects. ##Customizable", + id: 4, + icon: , + }, + { + title: "Common UI", + description: "**Common** UI components are *coming soon*! ~~Basic~~ ##Innovative", + id: 5, + icon: , + }, +]; + +const DRAG_BUFFER = 0; +const VELOCITY_THRESHOLD = 500; +const GAP = 16; +const SPRING_OPTIONS = { type: "spring", stiffness: 300, damping: 30 }; + +// Markdown-like styling helper function +const parseMarkdown = (text: string) => { + // Bold: **text** + text = text.replace(/\*\*(.*?)\*\*/g, '$1'); + + // Italic: *text* + text = text.replace(/\*(.*?)\*/g, '$1'); + + // Light: ~~text~~ + text = text.replace(/~~(.*?)~~/g, '$1'); + + // Dark: ##text + text = text.replace(/##(.*?)(?=\s|$)/g, '$1'); + + return text; +} + +export default function Carousel({ + items = DEFAULT_ITEMS, + baseWidth = 300, + autoplay = false, + autoplayDelay = 3000, + pauseOnHover = false, + loop = false, + round = false, +}: CarouselProps): JSX.Element { + const containerPadding = 16; + const itemWidth = baseWidth - containerPadding * 2; + const trackItemOffset = itemWidth + GAP; + + const carouselItems = loop ? [...items, items[0]] : items; + const [currentIndex, setCurrentIndex] = useState(0); + const x = useMotionValue(0); + const [isHovered, setIsHovered] = useState(false); + const [isResetting, setIsResetting] = useState(false); + + const containerRef = useRef(null); + useEffect(() => { + if (pauseOnHover && containerRef.current) { + const container = containerRef.current; + const handleMouseEnter = () => setIsHovered(true); + const handleMouseLeave = () => setIsHovered(false); + container.addEventListener("mouseenter", handleMouseEnter); + container.addEventListener("mouseleave", handleMouseLeave); + return () => { + container.removeEventListener("mouseenter", handleMouseEnter); + container.removeEventListener("mouseleave", handleMouseLeave); + }; + } + }, [pauseOnHover]); + + useEffect(() => { + if (autoplay && (!pauseOnHover || !isHovered)) { + const timer = setInterval(() => { + setCurrentIndex((prev) => { + if (prev === items.length - 1 && loop) { + return prev + 1; // Animate to clone. + } + if (prev === carouselItems.length - 1) { + return loop ? 0 : prev; + } + return prev + 1; + }); + }, autoplayDelay); + return () => clearInterval(timer); + } + }, [ + autoplay, + autoplayDelay, + isHovered, + loop, + items.length, + carouselItems.length, + pauseOnHover, + ]); + + const effectiveTransition = isResetting ? { duration: 0 } : SPRING_OPTIONS; + + const handleAnimationComplete = () => { + if (loop && currentIndex === carouselItems.length - 1) { + setIsResetting(true); + x.set(0); + setCurrentIndex(0); + setTimeout(() => setIsResetting(false), 50); + } + }; + + const handleDragEnd = ( + _: MouseEvent | TouchEvent | PointerEvent, + info: PanInfo + ): void => { + const offset = info.offset.x; + const velocity = info.velocity.x; + if (offset < -DRAG_BUFFER || velocity < -VELOCITY_THRESHOLD) { + if (loop && currentIndex === items.length - 1) { + setCurrentIndex(currentIndex + 1); + } else { + setCurrentIndex((prev) => Math.min(prev + 1, carouselItems.length - 1)); + } + } else if (offset > DRAG_BUFFER || velocity > VELOCITY_THRESHOLD) { + if (loop && currentIndex === 0) { + setCurrentIndex(items.length - 1); + } else { + setCurrentIndex((prev) => Math.max(prev - 1, 0)); + } + } + }; + + const dragProps = loop + ? {} + : { + dragConstraints: { + left: -trackItemOffset * (carouselItems.length - 1), + right: 0, + }, + }; + + return ( +
+ + {carouselItems.map((item, index) => { + const range = [ + -(index + 1) * trackItemOffset, + -index * trackItemOffset, + -(index - 1) * trackItemOffset, + ]; + const outputRange = [90, 0, -90]; + const rotateY = useTransform(x, range, outputRange, { clamp: false }); + return ( + +
+
+ + {item.icon} + +

{item.title}

+
+ + {item.image && ( +
+ {item.title} +
+ )} + + {item.data && ( +
+
+ {typeof item.data === 'string' + ? item.data + : JSON.stringify(item.data)} +
+
+ )} + +
+

+

+
+ {!round &&
} +
+ ); + })} +
+
+
+ {items.map((_, index) => ( + setCurrentIndex(index)} + transition={{ duration: 0.15 }} + /> + ))} +
+
+
+ ); +} \ No newline at end of file diff --git a/jsrepo.json b/jsrepo.json new file mode 100644 index 0000000..76fae67 --- /dev/null +++ b/jsrepo.json @@ -0,0 +1,11 @@ +{ + "$schema": "https://unpkg.com/jsrepo@2.1.0/schemas/project-config.json", + "repos": ["https://reactbits.dev/ts/tailwind"], + "includeTests": false, + "watermark": true, + "formatter": "prettier", + "configFiles": {}, + "paths": { + "*": "./src/blocks" + } +} diff --git a/lib/groq-service.ts b/lib/groq-service.ts index c93e3a3..12da99e 100644 --- a/lib/groq-service.ts +++ b/lib/groq-service.ts @@ -153,68 +153,165 @@ export const getGroqVisionCompletion = async (userPrompt: string, imageBase64: s } }; +// Enum for AI providers +export enum AIProvider { + GROQ = 'groq', + MISTRAL = 'mistral' +} + +// Configuration for AI providers +const aiProviders = { + [AIProvider.GROQ]: { + client: new Groq({ + apiKey: process.env.NEXT_PUBLIC_GROQ_API_KEY, + dangerouslyAllowBrowser: true, + }), + model: "meta-llama/llama-4-scout-17b-16e-instruct" + }, + [AIProvider.MISTRAL]: { + model: "pixtral-12b-2409" // Specific Pixtral model + } +}; + +// Mistral Vision Analysis Function +const getMistraiVisionAnalysis = async (base64ImageData: string, prompt: string) => { + if (!process.env.NEXT_PUBLIC_MISTRAL_API_KEY) { + throw new Error("Mistral API key not configured."); + } + + try { + const response = await fetch('https://api.mistral.ai/v1/chat/completions', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${process.env.NEXT_PUBLIC_MISTRAL_API_KEY}` + }, + body: JSON.stringify({ + model: "pixtral-12b-2409", // Specific Pixtral model + messages: [ + { + role: "system", + content: "You are an AI assistant with vision capabilities designed for AR Mode. Analyze the provided image based on the user's query. Pay close attention to any text visible in the image (like human facial gesture,posture,book titles, signs, labels, or screen content) and include it accurately in your description. Provide a concise, informative response detailing what you see." + }, + { + role: "user", + content: [ + { + type: "text", + text: prompt + }, + { + type: "image_url", + image_url: base64ImageData + } + ] + } + ] + }) + }); + + if (!response.ok) { + const errorBody = await response.text(); + throw new Error(`Mistral API error: ${response.status} - ${errorBody}`); + } + + const data = await response.json(); + return data.choices[0]?.message?.content || "Sorry, I could not process the image."; + } catch (error) { + console.error("Mistral Vision API error:", error); + throw error; + } +}; + // Specific function for AR mode or general vision queries export const getGroqVisionAnalysis = async ( - base64ImageData: string, // Expecting data URL like "data:image/jpeg;base64,..." + base64ImageData: string, prompt: string, + provider: AIProvider = AIProvider.GROQ, + maxRetries: number = 3 ) => { - if (!process.env.NEXT_PUBLIC_GROQ_API_KEY) { + if (provider === AIProvider.GROQ && !process.env.NEXT_PUBLIC_GROQ_API_KEY) { throw new Error("Groq API key not configured."); } + if (provider === AIProvider.MISTRAL && !process.env.NEXT_PUBLIC_MISTRAL_API_KEY) { + throw new Error("Mistral API key not configured."); + } + + // Validate image data if (!base64ImageData || !base64ImageData.startsWith("data:image")) { - throw new Error("Invalid image data provided."); + throw new Error("Invalid image data provided."); } // Extract base64 content and determine MIME type const mimeType = base64ImageData.substring(base64ImageData.indexOf(":") + 1, base64ImageData.indexOf(";")); const base64Content = base64ImageData.substring(base64ImageData.indexOf(",") + 1); - console.log(`Sending to Groq Vision: Prompt - "${prompt}", MimeType - ${mimeType}, Size approx ${Math.round(base64Content.length * 3/4 / 1024)} KB`); - - try { - const completion = await groq.chat.completions.create({ - messages: [ - { - role: "system", - content: "You are an AI assistant with vision capabilities designed for AR Mode. Analyze the provided image based on the user's query. Pay close attention to any text visible in the image (like human facial gesture,posture,book titles, signs, labels, or screen content) and include it accurately in your description. Provide a concise, informative response detailing what you see.", - }, - { - role: "user", - content: [ - { - type: "image_url", - image_url: { - url: base64ImageData, // Send the full data URL + console.log(`Sending to ${provider.toUpperCase()} Vision: Prompt - "${prompt}", MimeType - ${mimeType}, Size approx ${Math.round(base64Content.length * 3/4 / 1024)} KB`); + + let retries = 0; + while (retries < maxRetries) { + try { + let response: string; + + // Provider-specific API call + switch (provider) { + case AIProvider.GROQ: + const completion = await aiProviders[AIProvider.GROQ].client.chat.completions.create({ + messages: [ + { + role: "system", + content: "You are an AI assistant with vision capabilities designed for AR Mode. Analyze the provided image based on the user's query. Pay close attention to any text visible in the image (like human facial gesture,posture,book titles, signs, labels, or screen content) and include it accurately in your description. Provide a concise, informative response detailing what you see.", }, - }, - { - type: "text", - text: prompt, - }, - ], - }, - ], - model: "meta-llama/llama-4-scout-17b-16e-instruct", - // Optional: Add parameters like max_tokens, temperature if needed - // max_tokens: 150, - }); - - const responseContent = completion.choices[0]?.message?.content; - console.log("Groq Vision API Response:", responseContent); - - if (!responseContent) { - throw new Error("Received an empty response from Groq Vision API."); - } - - return responseContent; - - } catch (error) { - console.error("Error calling Groq Vision API:", error); - // Rethrow or handle appropriately - if (error instanceof Error) { - throw new Error(`Groq Vision API Error: ${error.message}`); - } else { - throw new Error("An unknown error occurred while contacting Groq Vision API."); + { + role: "user", + content: [ + { + type: "image_url", + image_url: { + url: base64ImageData, + }, + }, + { + type: "text", + text: prompt, + }, + ], + }, + ], + model: aiProviders[AIProvider.GROQ].model, + }); + response = completion.choices[0]?.message?.content || "Sorry, I could not process the image."; + break; + + case AIProvider.MISTRAL: + response = await getMistraiVisionAnalysis(base64ImageData, prompt); + break; + + default: + throw new Error(`Unsupported AI provider: ${provider}`); + } + + return response; + } catch (error) { + console.warn(`${provider.toUpperCase()} Vision API call failed (attempt ${retries + 1}):`, error); + + // Check if it's a 503 error or network-related issue + if (error instanceof Error && (error.message.includes('503') || error.message.includes('network'))) { + retries++; + // Exponential backoff + await new Promise(resolve => setTimeout(resolve, 1000 * Math.pow(2, retries))); + + if (retries >= maxRetries) { + console.error(`${provider.toUpperCase()} Vision API failed after maximum retries`); + return "Sorry, the image analysis service is temporarily unavailable. Please try again later."; + } + } else { + // For non-retryable errors, throw immediately + throw error; + } } } + + // Fallback return (should not normally be reached) + return "Sorry, there was an error processing the image."; }; \ No newline at end of file diff --git a/package.json b/package.json index 0cc68e4..7484eea 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "@studio-freight/lenis": "^1.0.42", "@studio-freight/react-lenis": "^0.0.47", "@supabase/supabase-js": "latest", + "@tabler/icons-react": "^3.33.0", "@tensorflow-models/coco-ssd": "^2.2.3", "@tensorflow/tfjs": "^4.22.0", "@vapi-ai/web": "^2.2.5", @@ -56,7 +57,7 @@ "date-fns": "^3.0.0", "embla-carousel-react": "8.5.1", "face-api.js": "^0.22.2", - "framer-motion": "^12.8.0", + "framer-motion": "^11.18.2", "groq-sdk": "^0.19.0", "gsap": "^3.12.7", "input-otp": "1.4.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3f7d695..e5eabd6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -110,6 +110,9 @@ importers: '@supabase/supabase-js': specifier: latest version: 2.49.4 + '@tabler/icons-react': + specifier: ^3.33.0 + version: 3.33.0(react@19.1.0) '@tensorflow-models/coco-ssd': specifier: ^2.2.3 version: 2.2.3(@tensorflow/tfjs-converter@4.22.0(@tensorflow/tfjs-core@4.22.0))(@tensorflow/tfjs-core@4.22.0) @@ -147,8 +150,8 @@ importers: specifier: ^0.22.2 version: 0.22.2 framer-motion: - specifier: ^12.8.0 - version: 12.8.0(@emotion/is-prop-valid@1.3.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + specifier: ^11.18.2 + version: 11.18.2(@emotion/is-prop-valid@1.3.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) groq-sdk: specifier: ^0.19.0 version: 0.19.0 @@ -1518,6 +1521,14 @@ packages: '@swc/helpers@0.5.15': resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} + '@tabler/icons-react@3.33.0': + resolution: {integrity: sha512-ay+HDecCjmFl25Lg14hcl59ffSjnOcgfrlV14shu8Qjbz+Xh4LRus93DuoyLQte8YSxE7Pe5gnEz6OF0GtwNtw==} + peerDependencies: + react: '>= 16' + + '@tabler/icons@3.33.0': + resolution: {integrity: sha512-NZeFfzcYe7xcBHR3zKoCSrw/cFWvfj6LjenPQ48yVMTGdX854HH9nH44ZfMH8rrDzHBllfjwl4CIX6Vh2tyN0Q==} + '@tailwindcss/typography@0.5.16': resolution: {integrity: sha512-0wDLwCVF5V3x3b1SGXPCDcdsbDHMBe+lkFzBRaHeLvNi+nrrnZ1lA18u+OTWO8iSWU2GxUOCvlXtDuqftc1oiA==} peerDependencies: @@ -2213,8 +2224,8 @@ packages: fraction.js@4.3.7: resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} - framer-motion@12.8.0: - resolution: {integrity: sha512-EarL75miCDcKLEAQLJ+6Zfwdj+KQsVlbHGGlygZ/TigKBj7NLPkyDKk4WLFUScjAs2xNpfMRLBM6VsCJq9Roxg==} + framer-motion@11.18.2: + resolution: {integrity: sha512-5F5Och7wrvtLVElIpclDT0CBzMVg3dL22B64aZwHtsIY8RB4mXICLrkajK4G9R+ieSAGcgrLeae2SeUTg2pr6w==} peerDependencies: '@emotion/is-prop-valid': '*' react: ^18.0.0 || ^19.0.0 @@ -2636,14 +2647,14 @@ packages: mlly@1.7.4: resolution: {integrity: sha512-qmdSIPC4bDJXgZTCR7XosJiNKySV7O215tsPtDN9iEO/7q/76b/ijtgRu/+epFXSJhijtTCCGp3DWS549P3xKw==} - motion-dom@12.8.0: - resolution: {integrity: sha512-YsfUE1F8Ycv9th1V0YJ6LOx9U2EMe/8P3RXK1o6NZhRbdFiWvzBLvxqp2X6Fn3rbJbwWkSEfnpe14ZU9Oz1d1Q==} + motion-dom@11.18.1: + resolution: {integrity: sha512-g76KvA001z+atjfxczdRtw/RXOM3OMSdd1f4DL77qCTF/+avrRJiawSG4yDibEQ215sr9kpinSlX2pCTJ9zbhw==} motion-dom@12.9.1: resolution: {integrity: sha512-xqXEwRLDYDTzOgXobSoWtytRtGlf7zdkRfFbrrdP7eojaGQZ5Go4OOKtgnx7uF8sAkfr1ZjMvbCJSCIT2h6fkQ==} - motion-utils@12.7.5: - resolution: {integrity: sha512-JIgrmEq7Vw1x0AUrjvkRp7oMMQkGqSUMT50O/Ag6RRCQWG3gRRTkOI+BirBAJT6m+GIPoiyxkJ1u98GgF/a6TQ==} + motion-utils@11.18.1: + resolution: {integrity: sha512-49Kt+HKjtbJKLtgO/LKj9Ld+6vw9BjH5d9sc40R/kVyH8GLAXgT42M2NnuPcJNuA3s9ZfZBUcwIgpmZWGEE+hA==} motion-utils@12.8.3: resolution: {integrity: sha512-GYVauZEbca8/zOhEiYOY9/uJeedYQld6co/GJFKOy//0c/4lDqk0zB549sBYqqV2iMuX+uHrY1E5zd8A2L+1Lw==} @@ -4745,6 +4756,13 @@ snapshots: dependencies: tslib: 2.8.1 + '@tabler/icons-react@3.33.0(react@19.1.0)': + dependencies: + '@tabler/icons': 3.33.0 + react: 19.1.0 + + '@tabler/icons@3.33.0': {} + '@tailwindcss/typography@0.5.16(tailwindcss@3.4.17)': dependencies: lodash.castarray: 4.4.0 @@ -5428,10 +5446,10 @@ snapshots: fraction.js@4.3.7: {} - framer-motion@12.8.0(@emotion/is-prop-valid@1.3.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0): + framer-motion@11.18.2(@emotion/is-prop-valid@1.3.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0): dependencies: - motion-dom: 12.8.0 - motion-utils: 12.7.5 + motion-dom: 11.18.1 + motion-utils: 11.18.1 tslib: 2.8.1 optionalDependencies: '@emotion/is-prop-valid': 1.3.1 @@ -6044,15 +6062,15 @@ snapshots: pkg-types: 1.3.1 ufo: 1.6.1 - motion-dom@12.8.0: + motion-dom@11.18.1: dependencies: - motion-utils: 12.7.5 + motion-utils: 11.18.1 motion-dom@12.9.1: dependencies: motion-utils: 12.8.3 - motion-utils@12.7.5: {} + motion-utils@11.18.1: {} motion-utils@12.8.3: {} diff --git a/src/blocks/Components/Carousel/Carousel.tsx b/src/blocks/Components/Carousel/Carousel.tsx new file mode 100644 index 0000000..118f2d9 --- /dev/null +++ b/src/blocks/Components/Carousel/Carousel.tsx @@ -0,0 +1,275 @@ +/* + Installed from https://reactbits.dev/ts/tailwind/ +*/ + +import { useEffect, useState, useRef } from "react"; +import { motion, PanInfo, useMotionValue, useTransform } from "framer-motion"; +// replace icons with your own if needed +import { + FiCircle, + FiCode, + FiFileText, + FiLayers, + FiLayout, +} from "react-icons/fi"; + +export interface CarouselItem { + title: string; + description: string; + id: number; + icon: JSX.Element; +} + +export interface CarouselProps { + items?: CarouselItem[]; + baseWidth?: number; + autoplay?: boolean; + autoplayDelay?: number; + pauseOnHover?: boolean; + loop?: boolean; + round?: boolean; + responsive?: boolean; +} + +const DEFAULT_ITEMS: CarouselItem[] = [ + { + title: "Text Animations", + description: "Cool text animations for your projects.", + id: 1, + icon: , + }, + { + title: "Animations", + description: "Smooth animations for your projects.", + id: 2, + icon: , + }, + { + title: "Components", + description: "Reusable components for your projects.", + id: 3, + icon: , + }, + { + title: "Backgrounds", + description: "Beautiful backgrounds and patterns for your projects.", + id: 4, + icon: , + }, + { + title: "Common UI", + description: "Common UI components are coming soon!", + id: 5, + icon: , + }, +]; + +const DRAG_BUFFER = 0; +const VELOCITY_THRESHOLD = 500; +const GAP = 16; +const SPRING_OPTIONS = { type: "spring", stiffness: 300, damping: 30 }; + +export default function Carousel({ + items = DEFAULT_ITEMS, + baseWidth = 300, + autoplay = false, + autoplayDelay = 3000, + pauseOnHover = false, + loop = false, + round = false, + responsive = false, +}: CarouselProps): JSX.Element { + const containerPadding = 16; + const itemWidth = baseWidth - containerPadding * 2; + const trackItemOffset = itemWidth + GAP; + + const carouselItems = loop ? [...items, items[0]] : items; + const [currentIndex, setCurrentIndex] = useState(0); + const x = useMotionValue(0); + const [isHovered, setIsHovered] = useState(false); + const [isResetting, setIsResetting] = useState(false); + + const containerRef = useRef(null); + useEffect(() => { + if (pauseOnHover && containerRef.current) { + const container = containerRef.current; + const handleMouseEnter = () => setIsHovered(true); + const handleMouseLeave = () => setIsHovered(false); + container.addEventListener("mouseenter", handleMouseEnter); + container.addEventListener("mouseleave", handleMouseLeave); + return () => { + container.removeEventListener("mouseenter", handleMouseEnter); + container.removeEventListener("mouseleave", handleMouseLeave); + }; + } + }, [pauseOnHover]); + + useEffect(() => { + if (autoplay && (!pauseOnHover || !isHovered)) { + const timer = setInterval(() => { + setCurrentIndex((prev) => { + if (prev === items.length - 1 && loop) { + return prev + 1; // Animate to clone. + } + if (prev === carouselItems.length - 1) { + return loop ? 0 : prev; + } + return prev + 1; + }); + }, autoplayDelay); + return () => clearInterval(timer); + } + }, [ + autoplay, + autoplayDelay, + isHovered, + loop, + items.length, + carouselItems.length, + pauseOnHover, + ]); + + const effectiveTransition = isResetting ? { duration: 0 } : SPRING_OPTIONS; + + const handleAnimationComplete = () => { + if (loop && currentIndex === carouselItems.length - 1) { + setIsResetting(true); + x.set(0); + setCurrentIndex(0); + setTimeout(() => setIsResetting(false), 50); + } + }; + + const handleDragEnd = ( + _: MouseEvent | TouchEvent | PointerEvent, + info: PanInfo, + ): void => { + const offset = info.offset.x; + const velocity = info.velocity.x; + if (offset < -DRAG_BUFFER || velocity < -VELOCITY_THRESHOLD) { + if (loop && currentIndex === items.length - 1) { + setCurrentIndex(currentIndex + 1); + } else { + setCurrentIndex((prev) => Math.min(prev + 1, carouselItems.length - 1)); + } + } else if (offset > DRAG_BUFFER || velocity > VELOCITY_THRESHOLD) { + if (loop && currentIndex === 0) { + setCurrentIndex(items.length - 1); + } else { + setCurrentIndex((prev) => Math.max(prev - 1, 0)); + } + } + }; + + const dragProps = loop + ? {} + : { + dragConstraints: { + left: -trackItemOffset * (carouselItems.length - 1), + right: 0, + }, + }; + + return ( +
+ + {carouselItems.map((item, index) => { + const range = [ + -(index + 1) * trackItemOffset, + -index * trackItemOffset, + -(index - 1) * trackItemOffset, + ]; + const outputRange = [90, 0, -90]; + const rotateY = useTransform(x, range, outputRange, { clamp: false }); + return ( + +
+ + {item.icon} + +
+
+
+ {item.title} +
+

{item.description}

+
+
+ ); + })} +
+
+
+ {items.map((_, index) => ( + setCurrentIndex(index)} + transition={{ duration: 0.15 }} + /> + ))} +
+
+
+ ); +} diff --git a/src/components/ui/carousel.tsx b/src/components/ui/carousel.tsx new file mode 100644 index 0000000..4ca97ec --- /dev/null +++ b/src/components/ui/carousel.tsx @@ -0,0 +1,177 @@ +"use client"; +import { IconArrowNarrowRight } from "@tabler/icons-react"; +import { useState, useRef, useId, useEffect } from "react"; +import { motion } from "framer-motion"; + +interface SlideData { + title: string; + button: string; + src: string; + description?: string; +} + +interface SlideProps { + slide: SlideData; + index: number; + current: number; + handleSlideClick: (index: number) => void; +} + +const Slide = ({ slide, index, current, handleSlideClick }: SlideProps) => { + const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 }); + const [isLoaded, setIsLoaded] = useState(false); + + const animate = () => { + return { + scale: current === index ? 1 : 0.85, + opacity: current === index ? 1 : 0.6, + transition: { duration: 0.3 } + }; + }; + + const handleMouseMove = (event: React.MouseEvent) => { + const rect = event.currentTarget.getBoundingClientRect(); + setMousePosition({ + x: (event.clientX - rect.left) / rect.width - 0.5, + y: (event.clientY - rect.top) / rect.height - 0.5 + }); + }; + + const handleMouseLeave = () => { + setMousePosition({ x: 0, y: 0 }); + }; + + const imageLoaded = (event: React.SyntheticEvent) => { + setIsLoaded(true); + }; + + return ( + handleSlideClick(index)} + onMouseMove={handleMouseMove} + onMouseLeave={handleMouseLeave} + animate={animate()} + > + {slide.src && ( + {slide.title} + )} +
+
+

{slide.title}

+ {slide.description && ( +

{slide.description}

+ )} + {slide.button && ( + + )} +
+
+
+ ); +}; + +interface CarouselControlProps { + type: string; + title: string; + handleClick: () => void; +} + +const CarouselControl = ({ + type, + title, + handleClick, +}: CarouselControlProps) => { + return ( + + ); +}; + +interface CarouselProps { + slides: SlideData[]; +} + +export default function Carousel({ slides }: CarouselProps) { + const [current, setCurrent] = useState(0); + + const handlePreviousClick = () => { + const previous = current - 1; + setCurrent(previous < 0 ? slides.length - 1 : previous); + }; + + const handleNextClick = () => { + const next = current + 1; + setCurrent(next === slides.length ? 0 : next); + }; + + const handleSlideClick = (index: number) => { + if (current !== index) { + setCurrent(index); + } + }; + + const id = useId(); + + return ( +
+
    + {slides.map((slide, index) => ( +
  • + +
  • + ))} +
+ +
+ + + +
+
+ ); +}