feat: daily weather report to WhatsApp + Claw weather command with live card

This commit is contained in:
Antigravity 2026-05-29 00:07:02 -04:00
parent 71401319e5
commit 16ca326820
4 changed files with 476 additions and 7 deletions

View File

@ -0,0 +1,164 @@
apiVersion: batch/v1
kind: CronJob
metadata:
name: weather-report
namespace: ai-agents
labels:
app: weather-report
spec:
# 7:00 AM ET = 11:00 UTC (EDT offset UTC-4)
schedule: "0 11 * * *"
timeZone: "America/New_York" # K8s 1.27+ supports timeZone natively
concurrencyPolicy: Forbid
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 1
jobTemplate:
spec:
template:
spec:
restartPolicy: OnFailure
containers:
- name: weather-reporter
image: node:18-alpine
command:
- "node"
- "-e"
- |
const https = require('https');
const http = require('http');
const ZIP = '16046';
const GATEWAY = 'http://whatsapp-gateway.ai-agents.svc.cluster.local:5001/send-message';
const GROUP = 'https://chat.whatsapp.com/0Txfg7iZBbvIuvTbTqqgAX';
function weatherEmoji(desc) {
const d = desc.toLowerCase();
if (d.includes('sunny') || d.includes('clear')) return '☀️';
if (d.includes('partly cloudy')) return '⛅';
if (d.includes('cloud') || d.includes('overcast')) return '☁️';
if (d.includes('thunder') || d.includes('storm')) return '⛈️';
if (d.includes('rain') || d.includes('drizzle')) return '🌧️';
if (d.includes('snow') || d.includes('blizzard')) return '❄️';
if (d.includes('fog') || d.includes('mist')) return '🌫️';
if (d.includes('sleet') || d.includes('ice')) return '🌨️';
return '🌤️';
}
function uvLabel(uv) {
const u = parseInt(uv);
if (u <= 2) return 'Low';
if (u <= 5) return 'Moderate';
if (u <= 7) return 'High';
if (u <= 10) return 'Very High';
return 'Extreme';
}
function windLabel(mph) {
const m = parseInt(mph);
if (m < 5) return 'Calm';
if (m < 15) return 'Breezy';
if (m < 25) return 'Windy';
if (m < 40) return 'Very Windy';
return 'Dangerous';
}
const url = `https://wttr.in/${ZIP}?format=j1`;
https.get(url, { headers: { 'User-Agent': 'WeatherBot/1.0' } }, (res) => {
let raw = '';
res.on('data', d => raw += d);
res.on('end', () => {
try {
const data = JSON.parse(raw);
const cur = data.current_condition[0];
const area = data.nearest_area[0];
const today = data.weather[0];
const tmrw = data.weather[1];
const city = area.areaName[0].value;
const state = area.region[0].value;
const desc = cur.weatherDesc[0].value;
const emoji = weatherEmoji(desc);
const now = new Date();
const etTime = now.toLocaleString('en-US', { timeZone: 'America/New_York', hour: 'numeric', minute: '2-digit', hour12: true });
const etDate = now.toLocaleDateString('en-US', { timeZone: 'America/New_York', weekday: 'long', month: 'long', day: 'numeric', year: 'numeric' });
// Hourly snapshots (morning 9am, afternoon 3pm, evening 9pm)
const hours = today.hourly || [];
const morn = hours[2]; // index 2 = 600 (6am slot) or pick closest
const aftn = hours[4]; // 1200
const even = hours[6]; // 1800
const tmrwDesc = tmrw ? tmrw.hourly[4]?.weatherDesc[0]?.value : '';
const tmrwEmoji = tmrw ? weatherEmoji(tmrwDesc) : '';
const msg = [
`${emoji} *Good Morning — Daily Weather Briefing*`,
`📍 *${city}, ${state}* (ZIP: ${ZIP})`,
`📅 ${etDate} • ${etTime} ET`,
``,
`━━━━━━ 🌡️ *Current Conditions* ━━━━━━`,
`${emoji} ${desc}`,
`🌡️ *Temp:* ${cur.temp_F}°F (feels like ${cur.FeelsLikeF}°F)`,
`💧 *Humidity:* ${cur.humidity}%`,
`💨 *Wind:* ${cur.windspeedMiles} mph ${cur.winddir16Point} (${windLabel(cur.windspeedMiles)})`,
`👁️ *Visibility:* ${cur.visibility} mi`,
`☀️ *UV Index:* ${cur.uvIndex} — ${uvLabel(cur.uvIndex)}`,
``,
`━━━━━━ 📊 *Today's Forecast* ━━━━━━`,
`🔺 High: ${today.maxtempF}°F 🔻 Low: ${today.mintempF}°F`,
`🌅 Sunrise: ${today.astronomy[0].sunrise} 🌇 Sunset: ${today.astronomy[0].sunset}`,
morn ? `🕗 Morning (9am): ${morn.tempF}°F — ${morn.weatherDesc[0].value} 🌧️ ${morn.chanceofrain}%` : '',
aftn ? `🕒 Afternoon (3pm): ${aftn.tempF}°F — ${aftn.weatherDesc[0].value} 🌧️ ${aftn.chanceofrain}%` : '',
even ? `🕘 Evening (9pm): ${even.tempF}°F — ${even.weatherDesc[0].value} 🌧️ ${even.chanceofrain}%` : '',
``,
tmrw ? `━━━━━━ 🔮 *Tomorrow* ━━━━━━` : '',
tmrw ? `${tmrwEmoji} ${tmrwDesc}` : '',
tmrw ? `🔺 High: ${tmrw.maxtempF}°F 🔻 Low: ${tmrw.mintempF}°F 🌧️ Rain: ${tmrw.hourly[4]?.chanceofrain || 0}%` : '',
``,
`_Powered by Agentic OS • hq.applaude.net_ 🏠`,
].filter(l => l !== null && l !== undefined).join('\n');
const body = JSON.stringify({ to: GROUP, message: msg });
const gwUrl = new URL(GATEWAY);
const opts = {
hostname: gwUrl.hostname,
port: parseInt(gwUrl.port) || 80,
path: gwUrl.pathname,
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) }
};
const req = http.request(opts, (r) => {
let resp = '';
r.on('data', d => resp += d);
r.on('end', () => {
const parsed = JSON.parse(resp);
if (parsed.success) {
console.log(`[WeatherBot] ✅ Weather report sent to group. MsgID: ${parsed.messageId}`);
} else {
console.error(`[WeatherBot] ❌ Gateway error: ${parsed.error}`);
process.exit(1);
}
});
});
req.on('error', e => { console.error('[WeatherBot] Request failed:', e.message); process.exit(1); });
req.write(body);
req.end();
} catch (e) {
console.error('[WeatherBot] Parse error:', e.message);
process.exit(1);
}
});
}).on('error', e => {
console.error('[WeatherBot] Fetch error:', e.message);
process.exit(1);
});
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
cpu: 200m
memory: 128Mi

View File

@ -0,0 +1,31 @@
import { NextResponse } from 'next/server';
const GATEWAY = process.env.WHATSAPP_GATEWAY_URL ||
'http://whatsapp-gateway.ai-agents.svc.cluster.local:5001/send-message';
const DEFAULT_GROUP = process.env.WHATSAPP_GROUP ||
'https://chat.whatsapp.com/0Txfg7iZBbvIuvTbTqqgAX';
export async function POST(request) {
try {
const { message, to } = await request.json();
if (!message) {
return NextResponse.json({ error: 'message is required' }, { status: 400 });
}
const res = await fetch(GATEWAY, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ to: to || DEFAULT_GROUP, message }),
});
const data = await res.json();
if (!res.ok || !data.success) {
return NextResponse.json({ error: data.error || 'Gateway error' }, { status: 502 });
}
return NextResponse.json({ success: true, messageId: data.messageId, recipient: data.recipient });
} catch (err) {
return NextResponse.json({ error: err.message }, { status: 500 });
}
}

View File

@ -0,0 +1,77 @@
import { NextResponse } from 'next/server';
const ZIP = process.env.WEATHER_ZIP || '16046';
export async function GET(request) {
const { searchParams } = new URL(request.url);
const location = searchParams.get('location') || ZIP;
try {
const res = await fetch(`https://wttr.in/${encodeURIComponent(location)}?format=j1`, {
headers: { 'User-Agent': 'HQ-Dashboard/1.0' },
next: { revalidate: 900 } // cache 15 min
});
if (!res.ok) throw new Error(`wttr.in returned ${res.status}`);
const data = await res.json();
const current = data.current_condition[0];
const area = data.nearest_area[0];
const today = data.weather[0];
const tomorrow = data.weather[1];
const city = area.areaName[0].value;
const state = area.region[0].value;
// Hourly forecast for today (3 slots: morning, afternoon, evening)
const hourly = (today.hourly || []).filter((_, i) => [2, 4, 6].includes(i)).map(h => ({
time: formatHour(h.time),
tempF: h.tempF,
desc: h.weatherDesc[0].value,
chanceRain: h.chanceofrain,
}));
return NextResponse.json({
location: `${city}, ${state}`,
zip: location,
current: {
tempF: current.temp_F,
feelsLikeF: current.FeelsLikeF,
humidity: current.humidity,
windMph: current.windspeedMiles,
windDir: current.winddir16Point,
visibility: current.visibility,
uvIndex: current.uvIndex,
desc: current.weatherDesc[0].value,
cloudcover: current.cloudcover,
pressure: current.pressure,
},
today: {
maxF: today.maxtempF,
minF: today.mintempF,
sunrise: today.astronomy[0].sunrise,
sunset: today.astronomy[0].sunset,
desc: today.hourly[4]?.weatherDesc[0]?.value || '',
chanceRain: today.hourly[4]?.chanceofrain || '0',
chanceSnow: today.hourly[4]?.chanceofsnow || '0',
},
tomorrow: tomorrow ? {
maxF: tomorrow.maxtempF,
minF: tomorrow.mintempF,
desc: tomorrow.hourly[4]?.weatherDesc[0]?.value || '',
chanceRain: tomorrow.hourly[4]?.chanceofrain || '0',
} : null,
hourly,
});
} catch (err) {
return NextResponse.json({ error: err.message }, { status: 500 });
}
}
function formatHour(time) {
const h = parseInt(time) / 100;
if (h === 0) return '12 AM';
if (h < 12) return `${h} AM`;
if (h === 12) return '12 PM';
return `${h - 12} PM`;
}

View File

@ -22,7 +22,11 @@ import {
Trash2, Trash2,
HeartPulse, HeartPulse,
LogOut, LogOut,
Maximize2 Maximize2,
CloudSun,
Wind,
Droplets,
Thermometer
} from "lucide-react"; } from "lucide-react";
export default function Page() { export default function Page() {
@ -32,13 +36,16 @@ export default function Page() {
const [messages, setMessages] = useState([ const [messages, setMessages] = useState([
{ {
role: "assistant", role: "assistant",
content: "Hello! I am Claw, your local agent. I have access to your Kubernetes namespace and Proxmox cluster nodes. How can I help you today?", content: "Hello! I am Claw, your local agent. I have access to your Kubernetes namespace and Proxmox cluster nodes. Try asking me: `weather`, `check k8s pods`, or `diagnose proxmox container 102`.",
timestamp: "Just now" timestamp: "Just now"
} }
]); ]);
const [inputValue, setInputValue] = useState(""); const [inputValue, setInputValue] = useState("");
const [isClawTyping, setIsClawTyping] = useState(false); const [isClawTyping, setIsClawTyping] = useState(false);
const [clawTerminalLogs, setClawTerminalLogs] = useState([]); const [clawTerminalLogs, setClawTerminalLogs] = useState([]);
const [weatherCard, setWeatherCard] = useState(null);
const [weatherSending, setWeatherSending] = useState(false);
const [weatherSent, setWeatherSent] = useState(false);
// OpsAgent State // OpsAgent State
const [pendingApprovals, setPendingApprovals] = useState([ const [pendingApprovals, setPendingApprovals] = useState([
@ -102,16 +109,48 @@ export default function Page() {
setMessages(prev => [...prev, { role: "user", content: userText, timestamp: "Just now" }]); setMessages(prev => [...prev, { role: "user", content: userText, timestamp: "Just now" }]);
setInputValue(""); setInputValue("");
setIsClawTyping(true); setIsClawTyping(true);
setWeatherCard(null);
setWeatherSent(false);
setClawTerminalLogs(prev => [...prev, `[Claw] Received instruction: "${userText}"`]); setClawTerminalLogs(prev => [...prev, `[Claw] Received instruction: "${userText}"`]);
// Parse commands for visual interactions
let responseText = ""; let responseText = "";
let logs = []; let logs = [];
await new Promise(resolve => setTimeout(resolve, 1500));
const cleanInput = userText.toLowerCase(); const cleanInput = userText.toLowerCase();
if (cleanInput.includes("pod") || cleanInput.includes("k8s") || cleanInput.includes("kubectl")) {
if (cleanInput.includes("weather")) {
// Extract optional location override
const locMatch = userText.match(/(?:weather\s+(?:in|for|at)?\s*)(.+)/i);
const location = locMatch ? locMatch[1].trim() : '16046';
logs = [
`[Claw] Fetching live weather data for "${location}"...`,
"[Claw] Connecting to wttr.in weather service...",
];
setClawTerminalLogs(prev => [...prev, ...logs]);
try {
const res = await fetch(`/api/weather?location=${encodeURIComponent(location)}`);
const wx = await res.json();
if (wx.error) throw new Error(wx.error);
setWeatherCard(wx);
setClawTerminalLogs(prev => [...prev,
`[Claw] Got weather for ${wx.location}: ${wx.current.tempF}°F, ${wx.current.desc}`,
"[Claw] Rendering weather card. Use the button to push to WhatsApp."
]);
const emoji = getWeatherEmoji(wx.current.desc);
responseText = `${emoji} Here's the live weather for **${wx.location}**:\n\n🌡 **${wx.current.tempF}°F** (feels like ${wx.current.feelsLikeF}°F) — ${wx.current.desc}\n💧 Humidity: ${wx.current.humidity}% | 💨 Wind: ${wx.current.windMph} mph ${wx.current.windDir}\n📊 Today: ${wx.today.minF}°F ${wx.today.maxF}°F | 🌅 ${wx.today.sunrise} / 🌇 ${wx.today.sunset}\n\nI've generated the full report card below. Hit **Send to WhatsApp** to push it to your group!`;
} catch (err) {
responseText = `❌ Could not fetch weather: ${err.message}. Check your internet or try a city name like \`weather Toronto\`.`;
setClawTerminalLogs(prev => [...prev, `[Claw] Error: ${err.message}`]);
}
} else if (cleanInput.includes("pod") || cleanInput.includes("k8s") || cleanInput.includes("kubectl")) {
await new Promise(resolve => setTimeout(resolve, 1500));
logs = [ logs = [
"Connecting to Kubernetes API server...", "Connecting to Kubernetes API server...",
"Executing: kubectl get pods -n default", "Executing: kubectl get pods -n default",
@ -123,6 +162,7 @@ export default function Page() {
]; ];
responseText = "I've checked the Kubernetes pods. Pod `web-pod-xyz` is currently failing due to a database connection refusal. You can use the **OpsAgent** tab to approve a pod restart or scale the DB deployment."; responseText = "I've checked the Kubernetes pods. Pod `web-pod-xyz` is currently failing due to a database connection refusal. You can use the **OpsAgent** tab to approve a pod restart or scale the DB deployment.";
} else if (cleanInput.includes("proxmox") || cleanInput.includes("vmid") || cleanInput.includes("disk")) { } else if (cleanInput.includes("proxmox") || cleanInput.includes("vmid") || cleanInput.includes("disk")) {
await new Promise(resolve => setTimeout(resolve, 1500));
logs = [ logs = [
"Establishing SSH connection to Proxmox cluster host 192.168.8.225...", "Establishing SSH connection to Proxmox cluster host 192.168.8.225...",
"Running: pct status 102", "Running: pct status 102",
@ -134,8 +174,10 @@ export default function Page() {
]; ];
responseText = "Proxmox Container ID `102` is running out of disk space (94% used). I have created a pending action under the **OpsAgent Center** to perform automated cleanup. Would you like me to execute it?"; responseText = "Proxmox Container ID `102` is running out of disk space (94% used). I have created a pending action under the **OpsAgent Center** to perform automated cleanup. Would you like me to execute it?";
} else if (cleanInput.includes("hello") || cleanInput.includes("hi")) { } else if (cleanInput.includes("hello") || cleanInput.includes("hi")) {
responseText = "Hi there! I am Claw, your local personal assistant. Try asking me to `check k8s pods` or `diagnose proxmox container 102`."; await new Promise(resolve => setTimeout(resolve, 800));
responseText = "Hi there! I am Claw, your local personal assistant. Try asking me to `check k8s pods`, `diagnose proxmox container 102`, or just type `weather` for a live report!";
} else { } else {
await new Promise(resolve => setTimeout(resolve, 1200));
responseText = `I've analyzed your query: "${userText}". I recommend initiating an **OpsRemediateWorkflow** to check the status or scanning your configuration files. Let me know if you would like me to trigger that for you.`; responseText = `I've analyzed your query: "${userText}". I recommend initiating an **OpsRemediateWorkflow** to check the status or scanning your configuration files. Let me know if you would like me to trigger that for you.`;
} }
@ -144,6 +186,63 @@ export default function Page() {
setIsClawTyping(false); setIsClawTyping(false);
}; };
// Weather emoji helper
const getWeatherEmoji = (desc) => {
const d = (desc || '').toLowerCase();
if (d.includes('sunny') || d.includes('clear')) return '☀️';
if (d.includes('partly cloudy')) return '⛅';
if (d.includes('cloud') || d.includes('overcast')) return '☁️';
if (d.includes('thunder') || d.includes('storm')) return '⛈️';
if (d.includes('rain') || d.includes('drizzle')) return '🌧️';
if (d.includes('snow') || d.includes('blizzard')) return '❄️';
if (d.includes('fog') || d.includes('mist')) return '🌫️';
return '🌤️';
};
// Send weather to WhatsApp
const handleSendWeatherToWhatsApp = async () => {
if (!weatherCard) return;
setWeatherSending(true);
const wx = weatherCard;
const emoji = getWeatherEmoji(wx.current.desc);
const msg = [
`${emoji} *Good Morning — Daily Weather Report*`,
`📍 *${wx.location}* (ZIP: ${wx.zip})`,
``,
`${emoji} ${wx.current.desc}`,
`🌡️ *Temp:* ${wx.current.tempF}°F (feels like ${wx.current.feelsLikeF}°F)`,
`💧 *Humidity:* ${wx.current.humidity}%`,
`💨 *Wind:* ${wx.current.windMph} mph ${wx.current.windDir}`,
`👁️ *Visibility:* ${wx.current.visibility} mi`,
`☀️ *UV Index:* ${wx.current.uvIndex}`,
``,
`📊 *Today:* ${wx.today.minF}°F ${wx.today.maxF}°F`,
`🌅 Sunrise: ${wx.today.sunrise} 🌇 Sunset: ${wx.today.sunset}`,
wx.tomorrow ? `🔮 *Tomorrow:* ${wx.tomorrow.minF}°F ${wx.tomorrow.maxF}°F ${getWeatherEmoji(wx.tomorrow.desc)} ${wx.tomorrow.desc}` : '',
``,
`_Agentic OS HQ • hq.applaude.net_ 🏠`,
].filter(Boolean).join('\n');
try {
const res = await fetch('/api/send-whatsapp', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message: msg })
});
const data = await res.json();
if (data.success) {
setWeatherSent(true);
setMessages(prev => [...prev, { role: "assistant", content: "✅ Weather report sent to your WhatsApp group!", timestamp: "Just now" }]);
} else {
setMessages(prev => [...prev, { role: "assistant", content: `❌ Failed to send: ${data.error}`, timestamp: "Just now" }]);
}
} catch (err) {
setMessages(prev => [...prev, { role: "assistant", content: `❌ Error: ${err.message}`, timestamp: "Just now" }]);
} finally {
setWeatherSending(false);
}
};
// Approval Handlers // Approval Handlers
const handleApprove = async (id, action, target) => { const handleApprove = async (id, action, target) => {
setPendingApprovals(prev => prev.map(item => item.id === id ? { ...item, status: "approved" } : item)); setPendingApprovals(prev => prev.map(item => item.id === id ? { ...item, status: "approved" } : item));
@ -494,7 +593,105 @@ export default function Page() {
<span style={{ width: "6px", height: "6px", background: "var(--text-secondary)", borderRadius: "50%", display: "inline-block", animation: "bounce 1.4s infinite ease-in-out both 0.4s" }}></span> <span style={{ width: "6px", height: "6px", background: "var(--text-secondary)", borderRadius: "50%", display: "inline-block", animation: "bounce 1.4s infinite ease-in-out both 0.4s" }}></span>
</div> </div>
)} )}
{/* Weather Card */}
{weatherCard && (
<div style={{ alignSelf: "flex-start", width: "100%", maxWidth: "480px" }} className="fade-in">
<div style={{
borderRadius: "16px",
overflow: "hidden",
border: "1px solid rgba(6,182,212,0.3)",
background: "linear-gradient(135deg, rgba(6,182,212,0.08) 0%, rgba(139,92,246,0.08) 100%)",
backdropFilter: "blur(12px)"
}}>
{/* Card Header */}
<div style={{
padding: "16px 20px",
background: "linear-gradient(135deg, rgba(6,182,212,0.15), rgba(139,92,246,0.1))",
borderBottom: "1px solid rgba(6,182,212,0.2)",
display: "flex", alignItems: "center", justifyContent: "space-between"
}}>
<div style={{ display: "flex", alignItems: "center", gap: "10px" }}>
<span style={{ fontSize: "28px" }}>{getWeatherEmoji(weatherCard.current.desc)}</span>
<div>
<div style={{ fontSize: "13px", fontWeight: "600", color: "var(--accent-cyan)" }}>📍 {weatherCard.location}</div>
<div style={{ fontSize: "11px", color: "var(--text-muted)" }}>Live conditions ZIP {weatherCard.zip}</div>
</div>
</div>
<div style={{ textAlign: "right" }}>
<div style={{ fontSize: "32px", fontWeight: "700", color: "var(--text-primary)", lineHeight: 1 }}>{weatherCard.current.tempF}°F</div>
<div style={{ fontSize: "11px", color: "var(--text-secondary)", marginTop: "2px" }}>feels like {weatherCard.current.feelsLikeF}°F</div>
</div>
</div>
{/* Condition */}
<div style={{ padding: "10px 20px", borderBottom: "1px solid rgba(255,255,255,0.05)", fontSize: "13px", color: "var(--text-secondary)" }}>
{weatherCard.current.desc}
</div>
{/* Stats Grid */}
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr 1fr", gap: "0", padding: "0" }}>
{[
{ icon: "💧", label: "Humidity", value: `${weatherCard.current.humidity}%` },
{ icon: "💨", label: "Wind", value: `${weatherCard.current.windMph} mph ${weatherCard.current.windDir}` },
{ icon: "☀️", label: `UV Index`, value: `${weatherCard.current.uvIndex}` },
{ icon: "👁️", label: "Visibility", value: `${weatherCard.current.visibility} mi` },
{ icon: "🔺", label: "High", value: `${weatherCard.today.maxF}°F` },
{ icon: "🔻", label: "Low", value: `${weatherCard.today.minF}°F` },
].map((stat, i) => (
<div key={i} style={{
padding: "12px 16px",
borderRight: i % 3 !== 2 ? "1px solid rgba(255,255,255,0.05)" : "none",
borderBottom: i < 3 ? "1px solid rgba(255,255,255,0.05)" : "none"
}}>
<div style={{ fontSize: "16px", marginBottom: "2px" }}>{stat.icon}</div>
<div style={{ fontSize: "13px", fontWeight: "600", color: "var(--text-primary)" }}>{stat.value}</div>
<div style={{ fontSize: "10px", color: "var(--text-muted)" }}>{stat.label}</div>
</div>
))}
</div>
{/* Sunrise/Sunset & Tomorrow */}
<div style={{ padding: "12px 20px", borderTop: "1px solid rgba(255,255,255,0.05)", display: "flex", justifyContent: "space-between", fontSize: "12px", color: "var(--text-secondary)" }}>
<span>🌅 {weatherCard.today.sunrise}</span>
<span>🌇 {weatherCard.today.sunset}</span>
{weatherCard.tomorrow && (
<span>{getWeatherEmoji(weatherCard.tomorrow.desc)} Tomorrow: {weatherCard.tomorrow.minF}{weatherCard.tomorrow.maxF}°F</span>
)}
</div>
{/* Send to WhatsApp CTA */}
<div style={{ padding: "14px 20px", borderTop: "1px solid rgba(6,182,212,0.2)", display: "flex", gap: "10px", alignItems: "center" }}>
<button
id="send-weather-whatsapp"
onClick={handleSendWeatherToWhatsApp}
disabled={weatherSending || weatherSent}
style={{
flex: 1,
padding: "10px 16px",
border: "none",
borderRadius: "8px",
background: weatherSent
? "linear-gradient(135deg, #10b981, #059669)"
: "linear-gradient(135deg, #25D366, #128C7E)",
color: "#fff",
fontWeight: "600",
cursor: weatherSending || weatherSent ? "default" : "pointer",
display: "flex", alignItems: "center", justifyContent: "center", gap: "8px",
fontSize: "13px",
opacity: weatherSending ? 0.7 : 1,
transition: "all 0.3s"
}}
>
{weatherSent ? "✅ Sent to WhatsApp!" : weatherSending ? "⏳ Sending..." : "📱 Send to WhatsApp Group"}
</button>
</div>
</div>
</div>
)}
<div ref={messagesEndRef} /> <div ref={messagesEndRef} />
</div> </div>
{/* Chat Input */} {/* Chat Input */}