e30e75b5d4
Changesets / Create Version PR (push) Has been cancelled
Deploy Shadcn Registry / Deploy Production (push) Has been cancelled
Template Metrics / LOC + Bundle Size (push) Has been cancelled
Code Quality / Oxlint + Oxfmt (push) Has been cancelled
Code Quality / Template Sync (push) Has been cancelled
Code Quality / Build Changed Packages (push) Has been cancelled
Code Quality / Test Changed Packages (push) Has been cancelled
Deploy Expo Example / Deploy Production (push) Has been cancelled
Deploy Ink Example / Deploy Production (push) Has been cancelled
Python Tests / pytest (assistant-stream, 3.10) (push) Has been cancelled
Python Tests / pytest (assistant-stream, 3.12) (push) Has been cancelled
Python Tests / pytest (assistant-ui-sync-server-api, 3.10) (push) Has been cancelled
Python Tests / pytest (assistant-ui-sync-server-api, 3.12) (push) Has been cancelled
227 lines
5.6 KiB
TypeScript
227 lines
5.6 KiB
TypeScript
import type {
|
|
ForecastDay,
|
|
PrecipitationLevel,
|
|
WeatherConditionCode,
|
|
WeatherWidgetPayload,
|
|
} from "@/components/tool-ui/weather-widget/runtime";
|
|
|
|
export interface WeatherSearchArgs {
|
|
query: string;
|
|
longitude: number;
|
|
latitude: number;
|
|
}
|
|
|
|
type GeocodeResult =
|
|
| {
|
|
success: true;
|
|
result: {
|
|
name: string;
|
|
latitude: number;
|
|
longitude: number;
|
|
};
|
|
}
|
|
| {
|
|
success: false;
|
|
error: string;
|
|
};
|
|
|
|
type WeatherResult =
|
|
| {
|
|
success: true;
|
|
widget: WeatherWidgetPayload;
|
|
}
|
|
| {
|
|
success: false;
|
|
error: string;
|
|
};
|
|
|
|
const mapOpenMeteoCodeToCondition = (
|
|
code: number,
|
|
windSpeed?: number,
|
|
): WeatherConditionCode => {
|
|
if (windSpeed !== undefined && windSpeed >= 45 && code <= 3) return "windy";
|
|
|
|
switch (code) {
|
|
case 0:
|
|
return "clear";
|
|
case 1:
|
|
case 2:
|
|
return "partly-cloudy";
|
|
case 3:
|
|
return "overcast";
|
|
case 45:
|
|
case 48:
|
|
return "fog";
|
|
case 51:
|
|
case 53:
|
|
case 55:
|
|
return "drizzle";
|
|
case 56:
|
|
case 57:
|
|
case 66:
|
|
case 67:
|
|
return "sleet";
|
|
case 61:
|
|
case 63:
|
|
case 80:
|
|
case 81:
|
|
return "rain";
|
|
case 65:
|
|
case 82:
|
|
return "heavy-rain";
|
|
case 71:
|
|
case 73:
|
|
case 75:
|
|
case 77:
|
|
case 85:
|
|
case 86:
|
|
return "snow";
|
|
case 95:
|
|
return "thunderstorm";
|
|
case 96:
|
|
case 99:
|
|
return "hail";
|
|
default:
|
|
return "cloudy";
|
|
}
|
|
};
|
|
|
|
const mapPrecipitationLevel = (
|
|
precipitation?: number,
|
|
): PrecipitationLevel | undefined => {
|
|
if (precipitation === undefined) return undefined;
|
|
if (precipitation <= 0) return "none";
|
|
if (precipitation < 1) return "light";
|
|
if (precipitation < 4) return "moderate";
|
|
return "heavy";
|
|
};
|
|
|
|
const getLocalTimeOfDay = (time?: string): number => {
|
|
if (!time) return new Date().getHours() / 24;
|
|
const [, rawClock = "12:00"] = time.split("T");
|
|
const [hours = "12", minutes = "0"] = rawClock.split(":");
|
|
const parsedHours = Number.parseInt(hours, 10);
|
|
const parsedMinutes = Number.parseInt(minutes, 10);
|
|
if (Number.isNaN(parsedHours) || Number.isNaN(parsedMinutes)) {
|
|
return new Date().getHours() / 24;
|
|
}
|
|
return (parsedHours + parsedMinutes / 60) / 24;
|
|
};
|
|
|
|
const formatForecastLabel = (date: string, index: number): string => {
|
|
if (index === 0) return "Today";
|
|
const parsedDate = new Date(`${date}T12:00:00Z`);
|
|
if (Number.isNaN(parsedDate.getTime())) return `Day ${index + 1}`;
|
|
return new Intl.DateTimeFormat("en-US", {
|
|
weekday: "short",
|
|
timeZone: "UTC",
|
|
}).format(parsedDate);
|
|
};
|
|
|
|
export const geocodeLocationWithOpenMeteo = async (
|
|
query: string,
|
|
): Promise<GeocodeResult> => {
|
|
try {
|
|
const response = await fetch(
|
|
`https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(query)}&count=1`,
|
|
);
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
}
|
|
|
|
const data = await response.json();
|
|
|
|
if (!data.results || data.results.length === 0) {
|
|
throw new Error("No results found");
|
|
}
|
|
|
|
return {
|
|
success: true,
|
|
result: data.results[0],
|
|
};
|
|
} catch (error) {
|
|
return {
|
|
success: false,
|
|
error:
|
|
error instanceof Error ? error.message : "Failed to geocode location",
|
|
};
|
|
}
|
|
};
|
|
|
|
export const fetchWeatherWidgetFromOpenMeteo = async ({
|
|
query,
|
|
longitude,
|
|
latitude,
|
|
}: WeatherSearchArgs): Promise<WeatherResult> => {
|
|
try {
|
|
const response = await fetch(
|
|
`https://api.open-meteo.com/v1/forecast?latitude=${latitude}&longitude=${longitude}&timezone=auto&temperature_unit=fahrenheit¤t=temperature_2m,weather_code,wind_speed_10m,precipitation&daily=weather_code,temperature_2m_max,temperature_2m_min&forecast_days=5`,
|
|
);
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
}
|
|
|
|
const data = await response.json();
|
|
const current = data.current;
|
|
const daily = data.daily;
|
|
|
|
if (
|
|
!current ||
|
|
!daily?.time ||
|
|
!daily?.weather_code ||
|
|
!daily?.temperature_2m_max ||
|
|
!daily?.temperature_2m_min
|
|
) {
|
|
throw new Error("Invalid API response format");
|
|
}
|
|
|
|
const forecast: ForecastDay[] = daily.time
|
|
.slice(0, 5)
|
|
.map((date: string, index: number) => ({
|
|
label: formatForecastLabel(date, index),
|
|
conditionCode: mapOpenMeteoCodeToCondition(daily.weather_code[index]),
|
|
tempMin: daily.temperature_2m_min[index],
|
|
tempMax: daily.temperature_2m_max[index],
|
|
}));
|
|
|
|
if (forecast.length === 0) {
|
|
throw new Error("No forecast data available");
|
|
}
|
|
|
|
const precipitationLevel = mapPrecipitationLevel(current.precipitation);
|
|
|
|
return {
|
|
success: true,
|
|
widget: {
|
|
version: "3.1",
|
|
id: `docs-weather-${query.toLowerCase().replaceAll(/\W+/g, "-")}`,
|
|
location: { name: query },
|
|
units: { temperature: "fahrenheit" },
|
|
current: {
|
|
conditionCode: mapOpenMeteoCodeToCondition(
|
|
current.weather_code,
|
|
current.wind_speed_10m,
|
|
),
|
|
temperature: current.temperature_2m,
|
|
tempMin: daily.temperature_2m_min[0],
|
|
tempMax: daily.temperature_2m_max[0],
|
|
windSpeed: current.wind_speed_10m,
|
|
...(precipitationLevel !== undefined ? { precipitationLevel } : {}),
|
|
},
|
|
forecast,
|
|
time: {
|
|
localTimeOfDay: getLocalTimeOfDay(current.time),
|
|
},
|
|
updatedAt: new Date().toISOString(),
|
|
},
|
|
};
|
|
} catch (error) {
|
|
return {
|
|
success: false,
|
|
error: error instanceof Error ? error.message : "Failed to fetch weather",
|
|
};
|
|
}
|
|
};
|