feat: daily weather report to WhatsApp + Claw weather command with live card
This commit is contained in:
parent
71401319e5
commit
16ca326820
|
|
@ -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
|
||||
|
|
@ -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 });
|
||||
}
|
||||
}
|
||||
|
|
@ -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`;
|
||||
}
|
||||
|
|
@ -22,7 +22,11 @@ import {
|
|||
Trash2,
|
||||
HeartPulse,
|
||||
LogOut,
|
||||
Maximize2
|
||||
Maximize2,
|
||||
CloudSun,
|
||||
Wind,
|
||||
Droplets,
|
||||
Thermometer
|
||||
} from "lucide-react";
|
||||
|
||||
export default function Page() {
|
||||
|
|
@ -32,13 +36,16 @@ export default function Page() {
|
|||
const [messages, setMessages] = useState([
|
||||
{
|
||||
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"
|
||||
}
|
||||
]);
|
||||
const [inputValue, setInputValue] = useState("");
|
||||
const [isClawTyping, setIsClawTyping] = useState(false);
|
||||
const [clawTerminalLogs, setClawTerminalLogs] = useState([]);
|
||||
const [weatherCard, setWeatherCard] = useState(null);
|
||||
const [weatherSending, setWeatherSending] = useState(false);
|
||||
const [weatherSent, setWeatherSent] = useState(false);
|
||||
|
||||
// OpsAgent State
|
||||
const [pendingApprovals, setPendingApprovals] = useState([
|
||||
|
|
@ -102,16 +109,48 @@ export default function Page() {
|
|||
setMessages(prev => [...prev, { role: "user", content: userText, timestamp: "Just now" }]);
|
||||
setInputValue("");
|
||||
setIsClawTyping(true);
|
||||
setWeatherCard(null);
|
||||
setWeatherSent(false);
|
||||
setClawTerminalLogs(prev => [...prev, `[Claw] Received instruction: "${userText}"`]);
|
||||
|
||||
// Parse commands for visual interactions
|
||||
let responseText = "";
|
||||
let logs = [];
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 1500));
|
||||
|
||||
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 = [
|
||||
"Connecting to Kubernetes API server...",
|
||||
"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.";
|
||||
} else if (cleanInput.includes("proxmox") || cleanInput.includes("vmid") || cleanInput.includes("disk")) {
|
||||
await new Promise(resolve => setTimeout(resolve, 1500));
|
||||
logs = [
|
||||
"Establishing SSH connection to Proxmox cluster host 192.168.8.225...",
|
||||
"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?";
|
||||
} 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 {
|
||||
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.`;
|
||||
}
|
||||
|
||||
|
|
@ -144,6 +186,63 @@ export default function Page() {
|
|||
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
|
||||
const handleApprove = async (id, action, target) => {
|
||||
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>
|
||||
</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>
|
||||
|
||||
{/* Chat Input */}
|
||||
|
|
|
|||
Loading…
Reference in New Issue