Merge ce9f64c07a into 0dc17286b9
This commit is contained in:
commit
1df103280c
3 changed files with 322 additions and 27 deletions
|
|
@ -27,6 +27,11 @@ interface SettingsType {
|
||||||
customOpenaiApiKey: string;
|
customOpenaiApiKey: string;
|
||||||
customOpenaiApiUrl: string;
|
customOpenaiApiUrl: string;
|
||||||
customOpenaiModelName: string;
|
customOpenaiModelName: string;
|
||||||
|
weatherWidgetEnabled?: boolean;
|
||||||
|
automaticWeatherLocation?: boolean;
|
||||||
|
weatherLatitude?: string;
|
||||||
|
weatherLongitude?: string;
|
||||||
|
weatherLocationName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
||||||
|
|
@ -151,6 +156,13 @@ const Page = () => {
|
||||||
const [measureUnit, setMeasureUnit] = useState<'Imperial' | 'Metric'>(
|
const [measureUnit, setMeasureUnit] = useState<'Imperial' | 'Metric'>(
|
||||||
'Metric',
|
'Metric',
|
||||||
);
|
);
|
||||||
|
const [weatherWidgetEnabled, setWeatherWidgetEnabled] =
|
||||||
|
useState<boolean>(true);
|
||||||
|
const [automaticWeatherLocation, setAutomaticWeatherLocation] =
|
||||||
|
useState<boolean>(true);
|
||||||
|
const [weatherLatitude, setWeatherLatitude] = useState<string>('');
|
||||||
|
const [weatherLongitude, setWeatherLongitude] = useState<string>('');
|
||||||
|
const [weatherLocationName, setWeatherLocationName] = useState<string>('');
|
||||||
const [savingStates, setSavingStates] = useState<Record<string, boolean>>({});
|
const [savingStates, setSavingStates] = useState<Record<string, boolean>>({});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -217,6 +229,30 @@ const Page = () => {
|
||||||
localStorage.getItem('measureUnit')! as 'Imperial' | 'Metric',
|
localStorage.getItem('measureUnit')! as 'Imperial' | 'Metric',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
setWeatherWidgetEnabled(
|
||||||
|
localStorage.getItem('weatherWidgetEnabled') === null
|
||||||
|
? true
|
||||||
|
: localStorage.getItem('weatherWidgetEnabled') === 'true',
|
||||||
|
);
|
||||||
|
|
||||||
|
setAutomaticWeatherLocation(
|
||||||
|
localStorage.getItem('automaticWeatherLocation') === null
|
||||||
|
? true
|
||||||
|
: localStorage.getItem('automaticWeatherLocation') === 'true',
|
||||||
|
);
|
||||||
|
|
||||||
|
setWeatherLatitude(
|
||||||
|
localStorage.getItem('weatherLatitude') ?? data.weatherLatitude ?? '',
|
||||||
|
);
|
||||||
|
setWeatherLongitude(
|
||||||
|
localStorage.getItem('weatherLongitude') ?? data.weatherLongitude ?? '',
|
||||||
|
);
|
||||||
|
setWeatherLocationName(
|
||||||
|
localStorage.getItem('weatherLocationName') ??
|
||||||
|
data.weatherLocationName ??
|
||||||
|
'',
|
||||||
|
);
|
||||||
|
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -377,6 +413,16 @@ const Page = () => {
|
||||||
localStorage.setItem('systemInstructions', value);
|
localStorage.setItem('systemInstructions', value);
|
||||||
} else if (key === 'measureUnit') {
|
} else if (key === 'measureUnit') {
|
||||||
localStorage.setItem('measureUnit', value.toString());
|
localStorage.setItem('measureUnit', value.toString());
|
||||||
|
} else if (key === 'weatherWidgetEnabled') {
|
||||||
|
localStorage.setItem('weatherWidgetEnabled', value.toString());
|
||||||
|
} else if (key === 'automaticWeatherLocation') {
|
||||||
|
localStorage.setItem('automaticWeatherLocation', value.toString());
|
||||||
|
} else if (key === 'weatherLatitude') {
|
||||||
|
localStorage.setItem('weatherLatitude', value.toString());
|
||||||
|
} else if (key === 'weatherLongitude') {
|
||||||
|
localStorage.setItem('weatherLongitude', value.toString());
|
||||||
|
} else if (key === 'weatherLocationName') {
|
||||||
|
localStorage.setItem('weatherLocationName', value.toString());
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to save:', err);
|
console.error('Failed to save:', err);
|
||||||
|
|
@ -454,6 +500,200 @@ const Page = () => {
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex flex-col space-y-1">
|
||||||
|
<p className="text-black/70 dark:text-white/70 text-sm">
|
||||||
|
Weather
|
||||||
|
</p>
|
||||||
|
<div className="flex items-center justify-between p-3 bg-light-secondary dark:bg-dark-secondary rounded-lg hover:bg-light-200 dark:hover:bg-dark-200 transition-colors">
|
||||||
|
<div className="flex items-center space-x-3">
|
||||||
|
<div className="p-2 bg-light-200 dark:bg-dark-200 rounded-lg">
|
||||||
|
<SettingsIcon
|
||||||
|
size={18}
|
||||||
|
className="text-black/70 dark:text-white/70"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-black/90 dark:text-white/90 font-medium">
|
||||||
|
Show Weather Widget
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-black/60 dark:text-white/60 mt-0.5">
|
||||||
|
Show the weather widget on the Home Page
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
checked={weatherWidgetEnabled}
|
||||||
|
onChange={(checked) => {
|
||||||
|
setWeatherWidgetEnabled(checked);
|
||||||
|
saveConfig('weatherWidgetEnabled', checked);
|
||||||
|
}}
|
||||||
|
className={cn(
|
||||||
|
weatherWidgetEnabled
|
||||||
|
? 'bg-[#24A0ED]'
|
||||||
|
: 'bg-light-200 dark:bg-dark-200',
|
||||||
|
'relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
weatherWidgetEnabled
|
||||||
|
? 'translate-x-6'
|
||||||
|
: 'translate-x-1',
|
||||||
|
'inline-block h-4 w-4 transform rounded-full bg-white transition-transform',
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Switch>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{weatherWidgetEnabled && (
|
||||||
|
<div className="flex flex-col space-y-1">
|
||||||
|
<div className="flex items-center justify-between p-3 bg-light-secondary dark:bg-dark-secondary rounded-lg hover:bg-light-200 dark:hover:bg-dark-200 transition-colors">
|
||||||
|
<div className="flex items-center space-x-3">
|
||||||
|
<div className="p-2 bg-light-200 dark:bg-dark-200 rounded-lg">
|
||||||
|
<SettingsIcon
|
||||||
|
size={18}
|
||||||
|
className="text-black/70 dark:text-white/70"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-black/90 dark:text-white/90 font-medium">
|
||||||
|
Automatic Weather Location
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-black/60 dark:text-white/60 mt-0.5">
|
||||||
|
Use device geolocation or IP lookup to determine your
|
||||||
|
location
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
checked={automaticWeatherLocation}
|
||||||
|
onChange={(checked) => {
|
||||||
|
setAutomaticWeatherLocation(checked);
|
||||||
|
// When enabling automatic mode: clear and persist manual fields.
|
||||||
|
if (checked) {
|
||||||
|
setWeatherLatitude('');
|
||||||
|
setWeatherLongitude('');
|
||||||
|
setWeatherLocationName('');
|
||||||
|
saveConfig('weatherLatitude', '');
|
||||||
|
saveConfig('weatherLongitude', '');
|
||||||
|
saveConfig('weatherLocationName', '');
|
||||||
|
saveConfig('automaticWeatherLocation', true);
|
||||||
|
} else {
|
||||||
|
const lat = (weatherLatitude ?? '').trim();
|
||||||
|
const lon = (weatherLongitude ?? '').trim();
|
||||||
|
const loc = (weatherLocationName ?? '').trim();
|
||||||
|
// Save manual mode only if all fields are filled
|
||||||
|
if (lat && lon && loc) {
|
||||||
|
saveConfig('automaticWeatherLocation', false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className={cn(
|
||||||
|
automaticWeatherLocation
|
||||||
|
? 'bg-[#24A0ED]'
|
||||||
|
: 'bg-light-200 dark:bg-dark-200',
|
||||||
|
'relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
automaticWeatherLocation
|
||||||
|
? 'translate-x-6'
|
||||||
|
: 'translate-x-1',
|
||||||
|
'inline-block h-4 w-4 transform rounded-full bg-white transition-transform',
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Switch>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{!automaticWeatherLocation && (
|
||||||
|
<>
|
||||||
|
<div className="grid grid-cols-2 gap-3 mt-2">
|
||||||
|
<div className="flex flex-col space-y-1">
|
||||||
|
<p className="text-black/70 dark:text-white/70 text-xs">
|
||||||
|
Latitude
|
||||||
|
</p>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
placeholder="e.g. 37.7749"
|
||||||
|
value={weatherLatitude ?? undefined}
|
||||||
|
isSaving={savingStates['weatherLatitude']}
|
||||||
|
onChange={(e) => setWeatherLatitude(e.target.value)}
|
||||||
|
onSave={(value) => {
|
||||||
|
const newLat = (value ?? '').trim();
|
||||||
|
const lon = (weatherLongitude ?? '').trim();
|
||||||
|
const loc = (weatherLocationName ?? '').trim();
|
||||||
|
// Save manual location only when all three fields are provided.
|
||||||
|
if (newLat && lon && loc) {
|
||||||
|
saveConfig('weatherLatitude', value);
|
||||||
|
saveConfig('weatherLongitude', lon);
|
||||||
|
saveConfig('weatherLocationName', loc);
|
||||||
|
setAutomaticWeatherLocation(false);
|
||||||
|
saveConfig('automaticWeatherLocation', false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col space-y-1">
|
||||||
|
<p className="text-black/70 dark:text-white/70 text-xs">
|
||||||
|
Longitude
|
||||||
|
</p>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
placeholder="e.g. -122.4194"
|
||||||
|
value={weatherLongitude ?? undefined}
|
||||||
|
isSaving={savingStates['weatherLongitude']}
|
||||||
|
onChange={(e) =>
|
||||||
|
setWeatherLongitude(e.target.value)
|
||||||
|
}
|
||||||
|
onSave={(value) => {
|
||||||
|
const lat = (weatherLatitude ?? '').trim();
|
||||||
|
const newLon = (value ?? '').trim();
|
||||||
|
const loc = (weatherLocationName ?? '').trim();
|
||||||
|
// Save manual location only when all three fields are provided.
|
||||||
|
if (lat && newLon && loc) {
|
||||||
|
saveConfig('weatherLatitude', lat);
|
||||||
|
saveConfig('weatherLongitude', value);
|
||||||
|
saveConfig('weatherLocationName', loc);
|
||||||
|
setAutomaticWeatherLocation(false);
|
||||||
|
saveConfig('automaticWeatherLocation', false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col space-y-1 mt-2">
|
||||||
|
<p className="text-black/70 dark:text-white/70 text-xs">
|
||||||
|
Location Name
|
||||||
|
</p>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
placeholder="e.g. Home, San Francisco"
|
||||||
|
value={weatherLocationName ?? undefined}
|
||||||
|
isSaving={savingStates['weatherLocationName']}
|
||||||
|
onChange={(e) =>
|
||||||
|
setWeatherLocationName(e.target.value)
|
||||||
|
}
|
||||||
|
onSave={(value) => {
|
||||||
|
const lat = (weatherLatitude ?? '').trim();
|
||||||
|
const lon = (weatherLongitude ?? '').trim();
|
||||||
|
const newLoc = (value ?? '').trim();
|
||||||
|
// Save manual location only when all three fields are provided.
|
||||||
|
if (lat && lon && newLoc) {
|
||||||
|
saveConfig('weatherLatitude', lat);
|
||||||
|
saveConfig('weatherLongitude', lon);
|
||||||
|
saveConfig('weatherLocationName', value);
|
||||||
|
setAutomaticWeatherLocation(false);
|
||||||
|
saveConfig('automaticWeatherLocation', false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</SettingsSection>
|
</SettingsSection>
|
||||||
|
|
||||||
<SettingsSection title="Automatic Search">
|
<SettingsSection title="Automatic Search">
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
|
'use client';
|
||||||
import { Settings } from 'lucide-react';
|
import { Settings } from 'lucide-react';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
import EmptyChatMessageInput from './EmptyChatMessageInput';
|
import EmptyChatMessageInput from './EmptyChatMessageInput';
|
||||||
import { File } from './ChatWindow';
|
import { File } from './ChatWindow';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
|
@ -26,6 +28,11 @@ const EmptyChat = ({
|
||||||
files: File[];
|
files: File[];
|
||||||
setFiles: (files: File[]) => void;
|
setFiles: (files: File[]) => void;
|
||||||
}) => {
|
}) => {
|
||||||
|
const [weatherEnabled, setWeatherEnabled] = useState(true);
|
||||||
|
useEffect(() => {
|
||||||
|
const item = localStorage.getItem('weatherWidgetEnabled');
|
||||||
|
setWeatherEnabled(item === null ? true : item === 'true');
|
||||||
|
}, []);
|
||||||
return (
|
return (
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<div className="absolute w-full flex flex-row items-center justify-end mr-5 mt-5">
|
<div className="absolute w-full flex flex-row items-center justify-end mr-5 mt-5">
|
||||||
|
|
@ -51,9 +58,11 @@ const EmptyChat = ({
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col w-full gap-4 mt-2 sm:flex-row sm:justify-center">
|
<div className="flex flex-col w-full gap-4 mt-2 sm:flex-row sm:justify-center">
|
||||||
<div className="flex-1 w-full">
|
{weatherEnabled && (
|
||||||
<WeatherWidget />
|
<div className="flex-1 w-full">
|
||||||
</div>
|
<WeatherWidget />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="flex-1 w-full">
|
<div className="flex-1 w-full">
|
||||||
<NewsArticleWidget />
|
<NewsArticleWidget />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
'use client';
|
||||||
import { Cloud, Sun, CloudRain, CloudSnow, Wind } from 'lucide-react';
|
import { Cloud, Sun, CloudRain, CloudSnow, Wind } from 'lucide-react';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
|
@ -70,36 +71,81 @@ const WeatherWidget = () => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
getLocation(async (location) => {
|
const fetchWeatherForCoords = async (
|
||||||
const res = await fetch(`/api/weather`, {
|
lat: number,
|
||||||
method: 'POST',
|
lng: number,
|
||||||
body: JSON.stringify({
|
city?: string,
|
||||||
lat: location.latitude,
|
) => {
|
||||||
lng: location.longitude,
|
try {
|
||||||
measureUnit: localStorage.getItem('measureUnit') ?? 'Metric',
|
const res = await fetch(`/api/weather`, {
|
||||||
}),
|
method: 'POST',
|
||||||
});
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
lat,
|
||||||
|
lng,
|
||||||
|
measureUnit: localStorage.getItem('measureUnit') ?? 'Metric',
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
|
|
||||||
if (res.status !== 200) {
|
if (res.status !== 200) {
|
||||||
console.error('Error fetching weather data');
|
console.error('Error fetching weather data');
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setData({
|
||||||
|
temperature: data.temperature,
|
||||||
|
condition: data.condition,
|
||||||
|
location: city ?? data.location ?? '',
|
||||||
|
humidity: data.humidity,
|
||||||
|
windSpeed: data.windSpeed,
|
||||||
|
icon: data.icon,
|
||||||
|
temperatureUnit: data.temperatureUnit,
|
||||||
|
windSpeedUnit: data.windSpeedUnit,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error fetching weather data', err);
|
||||||
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
return;
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
// Check automatic setting from localStorage (default true)
|
||||||
|
const automatic =
|
||||||
|
localStorage.getItem('automaticWeatherLocation') === null
|
||||||
|
? true
|
||||||
|
: localStorage.getItem('automaticWeatherLocation') === 'true';
|
||||||
|
|
||||||
|
if (!automatic) {
|
||||||
|
const latStr = localStorage.getItem('weatherLatitude') ?? '';
|
||||||
|
const lngStr = localStorage.getItem('weatherLongitude') ?? '';
|
||||||
|
const name = localStorage.getItem('weatherLocationName') ?? '';
|
||||||
|
const lat = parseFloat(latStr);
|
||||||
|
const lng = parseFloat(lngStr);
|
||||||
|
|
||||||
|
if (!isNaN(lat) && !isNaN(lng)) {
|
||||||
|
// Use provided coordinates; prefer user-provided name if available
|
||||||
|
const locName = name !== '' ? name : `${lat}, ${lng}`;
|
||||||
|
await fetchWeatherForCoords(lat, lng, locName);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// If invalid or missing, fall through to normal location lookup
|
||||||
}
|
}
|
||||||
|
|
||||||
setData({
|
// Normal behavior: use geolocation or IP-based approximate location
|
||||||
temperature: data.temperature,
|
await getLocation(async (location) => {
|
||||||
condition: data.condition,
|
await fetchWeatherForCoords(
|
||||||
location: location.city,
|
location.latitude,
|
||||||
humidity: data.humidity,
|
location.longitude,
|
||||||
windSpeed: data.windSpeed,
|
location.city,
|
||||||
icon: data.icon,
|
);
|
||||||
temperatureUnit: data.temperatureUnit,
|
|
||||||
windSpeedUnit: data.windSpeedUnit,
|
|
||||||
});
|
});
|
||||||
setLoading(false);
|
})();
|
||||||
});
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue