Skip to main content

WeatherChat Component

An intelligent chat interface powered by AI that provides weather-specific recommendations and answers user questions about current conditions.

Overview

WeatherChat is a floating widget that opens a chat interface with:
  • AI-powered responses using weather context
  • Google reCAPTCHA v2 security verification
  • Conversation history management
  • Suggested prompts for common questions
  • Adaptive theming based on weather conditions

Props

city
string
required
Current city name for context.Example: "New York"
temp
number
required
Current temperature in Celsius (rounded).Example: 23
weatherType
string
required
Weather condition type for theming.Valid values:
  • "snow" - Snow conditions
  • "hot" - Temperature > 28°C
  • "rain" - Rainy conditions
  • "thunder" - Thunderstorms
  • "drizzle" - Light rain
  • "clear" - Clear skies
  • "clouds" - Cloudy
  • "default" - Fallback
humidity
number
required
Humidity percentage (0-100).Example: 65
feels_like
number
required
Perceived temperature in Celsius (rounded).Example: 21
wind_speed
number
required
Wind speed in m/s.Example: 3.5
description
string
required
Weather description text.Example: "partly cloudy"
pressure
number | string
default:"N/A"
Atmospheric pressure in hPa.Example: 1013
pop
number
default:"0"
Probability of precipitation (0-1 decimal).Example: 0.35 (35% chance)

Basic Usage

import WeatherChat from '@/components/ui/WeatherChat';

function WeatherDashboard({ weather, forecast }) {
  const weatherType = getWeatherType(weather);
  
  return (
    <WeatherChat
      city={weather.name}
      temp={Math.round(weather.main.temp)}
      weatherType={weatherType}
      humidity={weather.main.humidity}
      feels_like={Math.round(weather.main.feels_like)}
      wind_speed={weather.wind.speed}
      description={weather.weather[0].description}
      pressure={weather.main.pressure}
      pop={forecast[0]?.pop || 0}
    />
  );
}

Features

reCAPTCHA Verification

Security verification before enabling chat:
import ReCAPTCHA from "react-google-recaptcha";

const [captchaToken, setCaptchaToken] = useState<string | null>(null);
const [isVerified, setIsVerified] = useState(false);
const siteKey = process.env.NEXT_PUBLIC_RECAPTCHA_SITE_KEY || "";

{!isVerified && !captchaToken && (
  <div className="flex flex-col items-center justify-center h-full">
    <p className="text-xs uppercase">Validación de Seguridad</p>
    <ReCAPTCHA
      sitekey={siteKey}
      onChange={(token) => {
        setCaptchaToken(token);
        setIsVerified(true);
      }}
      theme={isSnow ? "light" : "dark"}
    />
  </div>
)}
Environment Setup:
NEXT_PUBLIC_RECAPTCHA_SITE_KEY=your_site_key_here

Message Interface

Message structure for conversation history:
interface Message {
  role: "user" | "assistant";
  content: string;
}

const [messages, setMessages] = useState<Message[]>([]);

Context-Aware Responses

Weather data sent to API for relevant answers:
const handleSend = async (msgText: string) => {
  const newMessages = [
    ...messages,
    { role: "user", content: msgText }
  ];
  
  const response = await fetch("/api/chat", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      messages: newMessages,
      captchaToken: captchaToken,
      context: {
        city,
        temp,
        humidity,
        feels_like,
        wind_speed,
        description,
        pressure: pressure || "N/A",
        pop: pop !== undefined ? `${Math.round(pop * 100)}%` : "0%"
      }
    })
  });
  
  const data = await response.json();
  setMessages(prev => [
    ...prev,
    { role: "assistant", content: data.answer }
  ]);
};

Suggested Prompts

Quick-action buttons for common questions:
{isVerified && messages.length === 0 && (
  <button
    onClick={() => handleSend(`¿Cómo está el clima para salir ahora?`)}
    className="w-full flex items-center gap-2 p-3 rounded-xl"
  >
    {isSnow ? <Snowflake size={16} /> : <Shirt size={16} />}
    ¿Qué me pongo hoy?
  </button>
)}
Common Prompts:
  • “¿Qué me pongo hoy?” - Clothing recommendations
  • “¿Cómo está el clima para salir ahora?” - Current conditions assessment

Theming System

const theme = {
  header: isHot 
    ? "bg-[#FF5733]" 
    : isSnow 
      ? "bg-[#3498DB]" 
      : "bg-[#000000]",
  
  container: isSnow
    ? "bg-white/95 border-2 border-[#3498DB] text-slate-900"
    : "bg-[#1A1A1A]/95 border-2 border-white/20 text-white",
  
  input: isSnow
    ? "bg-white border-2 border-[#3498DB] text-slate-900"
    : "bg-[#2D2D2D] border-2 border-white/10 text-white",
  
  userMsg: "bg-[#E74C3C] text-white",
  
  aiMsg: isSnow 
    ? "bg-[#ECF0F1] text-slate-800" 
    : "bg-[#3D3D3D] text-white",
  
  button: "bg-[#2ECC71]"
};
Snow Theme:
  • Light backgrounds (bg-white/95)
  • Dark text (text-slate-900)
  • Blue accents (#3498DB)
Hot Theme:
  • Orange-red header (#FF5733)
  • High contrast for visibility
Default Theme:
  • Dark backgrounds (bg-[#1A1A1A]/95)
  • White text
  • Glass morphism effects

Message Rendering

{messages.map((m, i) => (
  <div
    key={i}
    className={`flex ${
      m.role === "user" ? "justify-end" : "justify-start"
    }`}
  >
    <div
      className={`max-w-[85%] p-3 rounded-xl text-[13px] font-bold ${
        m.role === "user" ? theme.userMsg : theme.aiMsg
      }`}
    >
      {m.content}
    </div>
  </div>
))}
User Messages:
  • Red background (#E74C3C)
  • Right-aligned
  • White text
AI Messages:
  • Adaptive background (snow/default)
  • Left-aligned
  • Bold font

Auto-Scrolling

Messages automatically scroll to bottom:
const scrollRef = useRef<HTMLDivElement>(null);

useEffect(() => {
  if (scrollRef.current) {
    scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
  }
}, [messages, isLoading]);

<div 
  ref={scrollRef}
  className="flex-1 overflow-y-auto"
>
  {/* Messages */}
</div>

Input Handling

Character Limit

<input
  type="text"
  value={input}
  maxLength={300}
  onChange={(e) => setInput(e.target.value)}
  onKeyDown={(e) => e.key === "Enter" && handleSend()}
  placeholder="Pregunta sobre el clima..."
/>

Send Validation

const handleSend = async (customMsg?: string) => {
  const msgText = (customMsg || input).trim();
  
  // Validation checks
  if (!msgText || isLoading || !captchaToken) return;
  if (msgText.length > 300) return;
  
  // Process message...
};

Loading States

{isLoading && (
  <div className="text-xs font-black opacity-40 animate-pulse">
    SkyCast está pensando...
  </div>
)}

API Integration

Chat API Route

Create API endpoint at /api/chat/route.ts:
import { NextRequest, NextResponse } from 'next/server';

export async function POST(req: NextRequest) {
  const { messages, captchaToken, context } = await req.json();
  
  // Verify reCAPTCHA
  const captchaResponse = await fetch(
    `https://www.google.com/recaptcha/api/siteverify`,
    {
      method: 'POST',
      headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
      body: `secret=${process.env.RECAPTCHA_SECRET_KEY}&response=${captchaToken}`
    }
  );
  
  const captchaData = await captchaResponse.json();
  if (!captchaData.success) {
    return NextResponse.json(
      { error: 'Verificación fallida' },
      { status: 400 }
    );
  }
  
  // Generate AI response using context
  const aiResponse = await generateWeatherResponse(messages, context);
  
  return NextResponse.json({ answer: aiResponse });
}

Environment Variables

# Public (client-side)
NEXT_PUBLIC_RECAPTCHA_SITE_KEY=your_site_key

# Private (server-side)
RECAPTCHA_SECRET_KEY=your_secret_key

Error Handling

try {
  const response = await fetch('/api/chat', { /* ... */ });
  const data = await response.json();
  
  if (!response.ok) throw new Error(data.answer);
  
  setMessages(prev => [
    ...prev,
    { role: 'assistant', content: data.answer }
  ]);
  setIsVerified(true);
} catch (error: any) {
  setMessages(prev => [
    ...prev,
    { role: 'assistant', content: 'Error de conexión. 📡' }
  ]);
} finally {
  setIsLoading(false);
}

Positioning

Floating widget in bottom-right corner:
<div className="fixed bottom-6 right-6 z-[100]">
  <button
    onClick={() => setIsOpen(!isOpen)}
    className="p-4 rounded-xl shadow-lg"
  >
    {isOpen ? <X size={28} /> : <MessageCircle size={28} />}
  </button>
  
  {isOpen && (
    <div className="absolute bottom-20 right-0 w-[350px] h-[550px]">
      {/* Chat interface */}
    </div>
  )}
</div>
Z-index hierarchy:
  • Widget: z-[100]
  • Chat window: z-[100] (same layer)
  • App header: z-50

Accessibility

  • Keyboard shortcuts: Enter to send message
  • Focus management: Auto-focus input on open
  • Screen readers: Semantic message structure
  • Color contrast: WCAG AA compliant

Best Practices

  1. Always verify reCAPTCHA before enabling chat
  2. Limit message length to prevent abuse (300 chars)
  3. Clear input after sending
  4. Provide loading feedback during API calls
  5. Handle errors gracefully with user-friendly messages
  6. Keep context minimal - only send necessary weather data
  7. Store conversation locally if persistence needed

Customization Examples

<div className="flex items-center gap-3">
  <Sparkles size={20} />
  <span>Mi Asistente Meteorológico</span>
</div>

Build docs developers (and LLMs) love