Skip to content

Feat/enhance frontend - #23

Merged
babcs2035 merged 10 commits into
mainfrom
feat/enhance-frontend
Nov 8, 2025
Merged

babcs2035 merged 10 commits into
mainfrom
feat/enhance-frontend

Conversation

@babcs2035

Copy link
Copy Markdown
Collaborator

This pull request introduces major improvements to both the backend and frontend of the application, focusing on enhanced caching for bear sighting data, integration of bear sightings into the frontend map and UI, and optimizations for cache management and geocoding. The backend now implements persistent caching for LLM analysis and DB results, reducing redundant API calls and improving efficiency. The frontend is updated to display bear sightings on the map, allow user interaction with these sightings, and provide new features like favorites and tutorials. Additionally, cache durations for map data are adjusted for better cache control.

Backend Enhancements:

Bear Sighting Data Caching and Processing:

  • Added persistent caching for LLM analysis and DB save results using file-based caches (bears_cache/llm and bears_cache/db), minimizing repeated LLM and geocoding calls for the same article URLs. This includes new helper functions for loading and saving cache entries, and logic in main() to utilize these caches before making external calls. [1] [2] [3] [4]
  • Improved error handling and logging for cache operations and DB saves, making the process more robust. [1] [2]

Geocoding Utility Improvements:

  • Refactored the geocoding utility to improve caching of geocoding results and handle queries with only prefecture information, reducing unnecessary API requests and handling more real-world cases.

Frontend Enhancements:

Bear Sighting Integration and UI Improvements:

  • Integrated bear sighting data into the frontend: fetches all bear sightings on load, displays them on the map, and allows users to select and view details of individual sightings. [1] [2] [3] [4] [5] [6]
  • Updated BottomSheet, ContextPanel, and MapPageClient components to support bear sighting selection and display, in addition to mountains and paths. [1] [2] [3]
  • Added new UI features: tutorial modal, favorites modal, and ability to filter mountains by favorites. [1] [2] [3]

Cache Duration Adjustments:

Image Domain Configuration:

  • Allowed images from imgu.web.nhk in Next.js config to support bear sighting images from NHK.

…mprove error handling, and refactor geocoding logic in utils.py
…tate management and layer updates for improved user interaction
… selected mountain highlighting and legend toggle functionality
… enhancing visual representation and interaction
…tes management in hooks, and UI updates across components
@babcs2035
babcs2035 requested a review from Copilot November 8, 2025 05:18
@babcs2035 babcs2035 self-assigned this Nov 8, 2025

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This pull request adds several new features to the PeakSight application: a favorites system for mountains, a tutorial for first-time users, and bear sighting information overlay on the map. The changes also include performance optimizations through caching for bear data imports and adjustments to tile caching duration.

  • Added client-side favorites management using localStorage with toggle functionality in mountain details
  • Implemented an interactive tutorial system that shows on first visit and can be dismissed permanently
  • Integrated bear sighting data from NHK news articles with LLM-based analysis and geocoding, displayed as custom markers on the map

Reviewed Changes

Copilot reviewed 15 out of 15 changed files in this pull request and generated 26 comments.

Show a summary per file
File Description
frontend/src/hooks/useFavorites.ts New hook for managing mountain favorites in localStorage
frontend/src/components/Tutorial.tsx New tutorial component with multi-slide walkthrough
frontend/src/components/FavoritesModal.tsx New modal for viewing and accessing favorite mountains
frontend/src/components/PanelContent.tsx Added favorite toggle button and bear sighting details panel
frontend/src/components/MapTerrain.tsx Implemented custom markers for mountains and bears with selection states, clustering logic for overlapping bears, and favorites filtering
frontend/src/components/Map.tsx Added collapsible legend with bear sightings and favorites filter toggle
frontend/src/components/Header.tsx Added buttons for favorites modal and tutorial with count badge
frontend/src/components/ContextPanel.tsx Props updated to pass through favorites and bear handlers
frontend/src/components/BottomSheet.tsx Props updated to pass through favorites and bear handlers
frontend/src/app/page.tsx Integrated favorites hook, tutorial state, bear data fetching, and favorites filtering logic
frontend/next.config.ts Added NHK image domain to remote patterns (needs correction)
frontend/src/app/api/proxy/tiles/[...path]/route.ts Changed cache duration from 1 year to 30 days with Redis expiration
frontend/src/app/api/proxy/maps/[...style]/route.ts Changed cache duration from 1 year to 30 days with Redis expiration
backend/commons/utils.py Enhanced geocoding to support prefecture-only queries with Japanese comments
backend/commons/import_bear_sight.py Added LLM and DB result caching to reduce API calls and improve performance

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread frontend/src/components/MapTerrain.tsx Outdated
Comment thread frontend/src/components/MapTerrain.tsx
Comment thread frontend/src/hooks/useFavorites.ts Outdated
Comment thread frontend/src/hooks/useFavorites.ts
Comment thread frontend/src/app/page.tsx Outdated
latitude=coordinates[0] if coordinates else 0.0,
longitude=coordinates[1] if coordinates else 0.0,
summary=llm_result.summary or "",
**{k: v for k, v in sighting_data.items() if k != "reported_at"},

Copilot AI Nov 8, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This dictionary comprehension filter {k: v for k, v in sighting_data.items() if k != "reported_at"} is fragile. If sighting_data structure changes, this could silently include unwanted fields. Consider explicitly listing the fields to unpack:

sighting = BearSighting(
    prefecture=sighting_data["prefecture"],
    city=sighting_data["city"],
    latitude=sighting_data["latitude"],
    longitude=sighting_data["longitude"],
    summary=sighting_data["summary"],
    image_url=sighting_data["image_url"],
    source_url=url,
    reported_at=reported_at,
)
Suggested change
**{k: v for k, v in sighting_data.items() if k != "reported_at"},
prefecture=sighting_data["prefecture"],
city=sighting_data["city"],
latitude=sighting_data["latitude"],
longitude=sighting_data["longitude"],
summary=sighting_data["summary"],
image_url=sighting_data["image_url"],

Copilot uses AI. Check for mistakes.
Comment thread frontend/src/components/MapTerrain.tsx Outdated

if (mountainsHash !== "empty" && (isFirstLoad || hasChanged)) {
// showOnlyFavoritesがtrueの場合は常に更新
if (displayMountainsHash !== "empty" && (isFirstLoad || hasChanged || showOnlyFavorites)) {

Copilot AI Nov 8, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The condition showOnlyFavorites in the dependency check causes mountains to be updated on every render when favorites filter is enabled. This is inefficient. The condition should check if hasChanged is true, not add an OR condition with showOnlyFavorites:

if (displayMountainsHash !== "empty" && (isFirstLoad || hasChanged)) {

The displayMountainsHash already accounts for showOnlyFavorites state (line 1118), so the explicit OR is redundant and causes unnecessary updates.

Suggested change
if (displayMountainsHash !== "empty" && (isFirstLoad || hasChanged || showOnlyFavorites)) {
if (displayMountainsHash !== "empty" && (isFirstLoad || hasChanged)) {

Copilot uses AI. Check for mistakes.
Comment on lines +65 to +69
await redis.set(cacheKey, dataBuffer, {
expiration: {
type: "EX",
value: 2592000,
},

Copilot AI Nov 8, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redis expiration and HTTP Cache-Control max-age are now aligned (both 30 days), but this contradicts the immutable directive. Immutable resources should have longer cache durations. This change appears across multiple cache configuration points (lines 30, 68, 78). Consider using a constant to ensure consistency:

const CACHE_MAX_AGE = 2592000; // 30 days in seconds

Copilot uses AI. Check for mistakes.
Comment on lines +120 to +133
const bearsEventHandlers = useRef<{
handleClick?: (
e: maplibregl.MapMouseEvent & {
features?: maplibregl.MapGeoJSONFeature[];
},
) => void;
handleMouseEnter?: (
e: maplibregl.MapMouseEvent & {
features?: maplibregl.MapGeoJSONFeature[];
},
) => void;
handleMouseLeave?: () => void;
}>({});

Copilot AI Nov 8, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused variable bearsEventHandlers.

Suggested change
const bearsEventHandlers = useRef<{
handleClick?: (
e: maplibregl.MapMouseEvent & {
features?: maplibregl.MapGeoJSONFeature[];
},
) => void;
handleMouseEnter?: (
e: maplibregl.MapMouseEvent & {
features?: maplibregl.MapGeoJSONFeature[];
},
) => void;
handleMouseLeave?: () => void;
}>({});

Copilot uses AI. Check for mistakes.
Comment on lines +658 to +684
const bearsGeoJSON = useMemo((): GeoJSON.FeatureCollection => {
const features = bears
.filter(
bear =>
bear.longitude !== null &&
bear.longitude !== undefined &&
bear.latitude !== null &&
bear.latitude !== undefined,
)
.map(bear => ({
type: "Feature" as const,
properties: {
id: bear.id,
prefecture: bear.prefecture,
city: bear.city,
summary: bear.summary,
},
geometry: {
type: "Point" as const,
coordinates: [bear.longitude, bear.latitude] as [number, number],
},
}));
return {
type: "FeatureCollection",
features,
};
}, [bears]);

Copilot AI Nov 8, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused variable bearsGeoJSON.

Suggested change
const bearsGeoJSON = useMemo((): GeoJSON.FeatureCollection => {
const features = bears
.filter(
bear =>
bear.longitude !== null &&
bear.longitude !== undefined &&
bear.latitude !== null &&
bear.latitude !== undefined,
)
.map(bear => ({
type: "Feature" as const,
properties: {
id: bear.id,
prefecture: bear.prefecture,
city: bear.city,
summary: bear.summary,
},
geometry: {
type: "Point" as const,
coordinates: [bear.longitude, bear.latitude] as [number, number],
},
}));
return {
type: "FeatureCollection",
features,
};
}, [bears]);

Copilot uses AI. Check for mistakes.
…sModal, enhance UI interactions in MapTerrain, and streamline state management in hooks
…fe in HomePage, BottomSheet, ContextPanel, MapTerrain, and useFavorites
@babcs2035
babcs2035 requested a review from Copilot November 8, 2025 05:52
@babcs2035
babcs2035 merged commit 045672b into main Nov 8, 2025
7 checks passed

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

Copilot reviewed 15 out of 15 changed files in this pull request and generated 5 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

const removeFavorite = (mountainId: number) => {
setFavorites(prev => {
const updated = prev.filter(m => m.id !== mountainId);
localStorage.setItem(FAVORITES_KEY, JSON.stringify(updated));

Copilot AI Nov 8, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing window check before accessing localStorage in removeFavorite. The addFavorite function correctly checks typeof window !== 'undefined' on line 28, but removeFavorite directly accesses localStorage on line 39 without this check. This will cause errors during SSR.

Suggested change
localStorage.setItem(FAVORITES_KEY, JSON.stringify(updated));
if (typeof window !== "undefined") {
localStorage.setItem(FAVORITES_KEY, JSON.stringify(updated));
}

Copilot uses AI. Check for mistakes.
Comment on lines +11 to +16
const stored = localStorage.getItem(FAVORITES_KEY);
if (stored) {
try {
setFavorites(JSON.parse(stored));
} catch (error) {
console.error("Failed to parse favorites:", error);

Copilot AI Nov 8, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing window check before accessing localStorage in useEffect. During SSR, localStorage is not available and will throw a ReferenceError. Add if (typeof window !== 'undefined') check before accessing localStorage.getItem.

Suggested change
const stored = localStorage.getItem(FAVORITES_KEY);
if (stored) {
try {
setFavorites(JSON.parse(stored));
} catch (error) {
console.error("Failed to parse favorites:", error);
if (typeof window !== "undefined") {
const stored = localStorage.getItem(FAVORITES_KEY);
if (stored) {
try {
setFavorites(JSON.parse(stored));
} catch (error) {
console.error("Failed to parse favorites:", error);
}

Copilot uses AI. Check for mistakes.
}
// 座標を丸めてグループ化するヘルパー関数
const roundCoordinate = (coord: number, precision: number = 6): number => {
const factor = Math.pow(10, precision);

Copilot AI Nov 8, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] Use Math.pow(10, precision) instead of the more modern exponentiation operator. Consider using 10 ** precision for consistency with modern JavaScript practices.

Suggested change
const factor = Math.pow(10, precision);
const factor = 10 ** precision;

Copilot uses AI. Check for mistakes.
Comment on lines +185 to +190
class CachedResult:
def __init__(self, data):
self.is_sighting = data.get("is_sighting", False)
self.prefecture = data.get("prefecture")
self.city = data.get("city")
self.summary = data.get("summary")

Copilot AI Nov 8, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] Local class definition inside a function reduces code clarity. Consider defining CachedResult at module level or using a dataclass/NamedTuple for better type safety and reusability.

Copilot uses AI. Check for mistakes.
Comment thread frontend/src/app/page.tsx
: response.data.results || [];
setBears(bearsData);
} else {
console.error("Failed to fetch bears:", response);

Copilot AI Nov 8, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Error message lacks detail about what failed. The console.error on line 70 should include the status code and error details for better debugging: console.error('Failed to fetch bears:', response.status, response.error)

Suggested change
console.error("Failed to fetch bears:", response);
console.error("Failed to fetch bears:", response.status, response.error);

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants