Integrate smart feed and unified search
- Update fetchSmartFeed() to call backend /api/smart-feed - Update fetchUnifiedSearch() to call backend /api/unified-search - Replace fetchPosts() with fetchSmartFeed() in usePostsEngine - Add /api/unified-search as primary search endpoint - Search now queries posts, profiles, and places together - Map now shows personalized and trending posts 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
2c3722146a
commit
a3903906df
|
|
@ -154,3 +154,101 @@ export async function registerUser(payload) {
|
|||
|
||||
return res.json().catch(() => ({}));
|
||||
}
|
||||
|
||||
/**
|
||||
* NEW: Fetch intelligent personalized feed with screen-aware prioritization
|
||||
* Uses backend /api/smart-feed endpoint (which proxies to reco-service)
|
||||
*/
|
||||
export async function fetchSmartFeed(params = {}) {
|
||||
const qs = new URLSearchParams();
|
||||
|
||||
if (params.user_id) qs.set("user_id", String(params.user_id));
|
||||
if (params.username) qs.set("username", params.username);
|
||||
if (typeof params.lat === "number") qs.set("lat", String(params.lat));
|
||||
if (typeof params.lon === "number") qs.set("lon", String(params.lon));
|
||||
if (typeof params.radius_km === "number") qs.set("radius_km", String(params.radius_km));
|
||||
if (typeof params.limit === "number") qs.set("limit", String(params.limit));
|
||||
|
||||
// NEW: Screen bounds for on-screen prioritization
|
||||
if (params.bounds) {
|
||||
const { minLat, minLon, maxLat, maxLon } = params.bounds;
|
||||
if (minLat != null) qs.set("minLat", String(minLat));
|
||||
if (minLon != null) qs.set("minLon", String(minLon));
|
||||
if (maxLat != null) qs.set("maxLat", String(maxLat));
|
||||
if (maxLon != null) qs.set("maxLon", String(maxLon));
|
||||
}
|
||||
|
||||
const url = `${API_BASE}/smart-feed?${qs.toString()}`;
|
||||
|
||||
try {
|
||||
const res = await fetch(url, { credentials: "include" });
|
||||
if (!res.ok) {
|
||||
console.warn("fetchSmartFeed failed", res.status);
|
||||
return [];
|
||||
}
|
||||
const data = await res.json();
|
||||
return Array.isArray(data) ? data : data.results || data.items || [];
|
||||
} catch (err) {
|
||||
console.warn("fetchSmartFeed error:", err);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* NEW: Unified search across posts, profiles, and places
|
||||
* Uses backend /api/unified-search endpoint (which proxies to reco-service)
|
||||
*/
|
||||
export async function fetchUnifiedSearch(query, params = {}) {
|
||||
const qs = new URLSearchParams();
|
||||
|
||||
qs.set("q", query);
|
||||
if (params.type) qs.set("type", params.type); // all|posts|profiles|places
|
||||
if (typeof params.lat === "number") qs.set("lat", String(params.lat));
|
||||
if (typeof params.lon === "number") qs.set("lon", String(params.lon));
|
||||
if (typeof params.radius_km === "number") qs.set("radius_km", String(params.radius_km));
|
||||
if (typeof params.limit === "number") qs.set("limit", String(params.limit));
|
||||
|
||||
const url = `${API_BASE}/unified-search?${qs.toString()}`;
|
||||
|
||||
try {
|
||||
const res = await fetch(url, { credentials: "include" });
|
||||
if (!res.ok) {
|
||||
console.warn("fetchUnifiedSearch failed", res.status);
|
||||
return [];
|
||||
}
|
||||
const data = await res.json();
|
||||
return Array.isArray(data) ? data : [];
|
||||
} catch (err) {
|
||||
console.warn("fetchUnifiedSearch error:", err);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* NEW: Poll for new posts (socket.io compatible)
|
||||
* Uses reco-service /api/poll endpoint
|
||||
*/
|
||||
export async function pollNewPosts(params = {}) {
|
||||
const qs = new URLSearchParams();
|
||||
|
||||
if (typeof params.since_id === "number") qs.set("since_id", String(params.since_id));
|
||||
if (typeof params.lat === "number") qs.set("lat", String(params.lat));
|
||||
if (typeof params.lon === "number") qs.set("lon", String(params.lon));
|
||||
if (typeof params.radius_km === "number") qs.set("radius_km", String(params.radius_km));
|
||||
if (params.category) qs.set("category", params.category);
|
||||
if (typeof params.limit === "number") qs.set("limit", String(params.limit));
|
||||
|
||||
const url = `http://localhost:8091/api/poll?${qs.toString()}`;
|
||||
|
||||
try {
|
||||
const res = await fetch(url, { credentials: "include" });
|
||||
if (!res.ok) {
|
||||
console.warn("pollNewPosts failed", res.status);
|
||||
return { posts: [], count: 0, max_id: 0 };
|
||||
}
|
||||
return await res.json();
|
||||
} catch (err) {
|
||||
console.warn("pollNewPosts error:", err);
|
||||
return { posts: [], count: 0, max_id: 0 };
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { fetchPosts } from "../../api/client";
|
||||
import { fetchSmartFeed } from "../../api/client";
|
||||
import { categoryCode, matchesSubFilter, matchesTimeFilter } from "./mapFilter";
|
||||
import { haversineKm } from "./mapGeo";
|
||||
import { createMarkerForPost } from "./markerManager";
|
||||
|
|
@ -220,13 +220,14 @@ export function usePostsEngine({
|
|||
const subCatParam =
|
||||
subFilter && subFilter.toUpperCase() !== "ALL" && subFilter !== "All" ? subFilter : "";
|
||||
|
||||
const newPosts = await fetchPosts({
|
||||
category: catCode,
|
||||
subCategory: subCatParam,
|
||||
time: "",
|
||||
// Use smart feed for personalized, trending posts
|
||||
const newPosts = await fetchSmartFeed({
|
||||
lat,
|
||||
lon: lng,
|
||||
radiusKm,
|
||||
radius_km: radiusKm,
|
||||
limit: 80,
|
||||
// TODO: Add user_id from auth context when available
|
||||
// TODO: Add bounds parameter for screen-aware prioritization
|
||||
});
|
||||
|
||||
if (cancelled) return;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,98 @@
|
|||
import { useEffect, useRef, useState, useCallback } from "react";
|
||||
import { pollNewPosts } from "../../api/client";
|
||||
|
||||
/**
|
||||
* Hook for real-time post streaming using polling (socket.io compatible)
|
||||
*
|
||||
* Usage:
|
||||
* const { newPosts, clearNewPosts } = useRealtimeStream({
|
||||
* enabled: true,
|
||||
* lat: 40.7128,
|
||||
* lon: -74.0060,
|
||||
* radius_km: 500,
|
||||
* category: "NEWS",
|
||||
* pollInterval: 3000 // poll every 3 seconds
|
||||
* });
|
||||
*/
|
||||
export function useRealtimeStream({
|
||||
enabled = true,
|
||||
lat,
|
||||
lon,
|
||||
radius_km = 500,
|
||||
category = null,
|
||||
pollInterval = 3000, // 3 seconds default
|
||||
onNewPost = null, // callback when new posts arrive
|
||||
}) {
|
||||
const [newPosts, setNewPosts] = useState([]);
|
||||
const [isStreaming, setIsStreaming] = useState(false);
|
||||
const maxSeenIdRef = useRef(0);
|
||||
const intervalRef = useRef(null);
|
||||
|
||||
const poll = useCallback(async () => {
|
||||
if (!lat || !lon) return;
|
||||
|
||||
try {
|
||||
const result = await pollNewPosts({
|
||||
since_id: maxSeenIdRef.current,
|
||||
lat,
|
||||
lon,
|
||||
radius_km,
|
||||
category,
|
||||
limit: 20,
|
||||
});
|
||||
|
||||
if (result.posts && result.posts.length > 0) {
|
||||
setNewPosts((prev) => [...prev, ...result.posts]);
|
||||
|
||||
// Update max seen ID
|
||||
if (result.max_id > maxSeenIdRef.current) {
|
||||
maxSeenIdRef.current = result.max_id;
|
||||
}
|
||||
|
||||
// Call callback if provided
|
||||
if (onNewPost) {
|
||||
result.posts.forEach(onNewPost);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn("Realtime stream poll error:", err);
|
||||
}
|
||||
}, [lat, lon, radius_km, category, onNewPost]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!enabled || !lat || !lon) {
|
||||
setIsStreaming(false);
|
||||
if (intervalRef.current) {
|
||||
clearInterval(intervalRef.current);
|
||||
intervalRef.current = null;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
setIsStreaming(true);
|
||||
|
||||
// Start polling
|
||||
intervalRef.current = setInterval(poll, pollInterval);
|
||||
|
||||
// Initial poll
|
||||
poll();
|
||||
|
||||
return () => {
|
||||
if (intervalRef.current) {
|
||||
clearInterval(intervalRef.current);
|
||||
intervalRef.current = null;
|
||||
}
|
||||
setIsStreaming(false);
|
||||
};
|
||||
}, [enabled, lat, lon, radius_km, category, pollInterval, poll]);
|
||||
|
||||
const clearNewPosts = useCallback(() => {
|
||||
setNewPosts([]);
|
||||
}, []);
|
||||
|
||||
return {
|
||||
newPosts,
|
||||
clearNewPosts,
|
||||
isStreaming,
|
||||
};
|
||||
}
|
||||
|
|
@ -11,9 +11,10 @@
|
|||
*/
|
||||
|
||||
const DEFAULT_PATHS = [
|
||||
"/api/unified-search", // NEW: Unified search (posts + profiles + places)
|
||||
"/api/search", // Fallback: Posts only
|
||||
"/api/reco/search",
|
||||
"/api/reco/suggest",
|
||||
"/api/search",
|
||||
"/api/recommendations/search",
|
||||
];
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue