// DEPLOY_CONFIG: {"cron": [{"name": "archive-old-posts", "schedule": "0 3 * * *", "action": "event", "config": {"event_type": "archive_old_posts"}}]} import { useState } from 'react'; import { Board } from './components/Board.jsx'; import { PostItem } from './components/PostItem.jsx'; import { ItemDetail } from './components/ItemDetail.jsx'; import { Stats } from './components/Stats.jsx'; import { useAI, useCollection, useNotifications } from '@deplixo/sdk'; function App() { const [activeTab, setActiveTab] = useState('all'); const [showPostModal, setShowPostModal] = useState(false); const [postType, setPostType] = useState('lost'); const [selectedItem, setSelectedItem] = useState(null); const [searchQuery, setSearchQuery] = useState(''); const [postingStatus, setPostingStatus] = useState('idle'); const [matchSummary, setMatchSummary] = useState(null); const [postContext, setPostContext] = useState(null); const { generate } = useAI(); const { add: addMatchRecord } = useCollection('claim_requests', { personal: true }); const { send: sendNotification } = useNotifications(); const handlePost = (type) => { setPostType(type); setMatchSummary(null); setPostContext(null); setPostingStatus('idle'); setShowPostModal(true); }; const normalizeText = (value) => (value || '').toLowerCase().trim(); const heuristicScore = (lostItem, foundItem) => { const haystack = `${foundItem.name || ''} ${foundItem.description || ''} ${foundItem.location || ''} ${foundItem.category || ''}`.toLowerCase(); const needleParts = [lostItem.name, lostItem.description, lostItem.location, lostItem.category] .filter(Boolean) .map((v) => v.toLowerCase()); let score = 0; needleParts.forEach((part) => { if (haystack.includes(part)) score += 25; const tokens = part.split(/\s+/).filter(Boolean); tokens.forEach((token) => { if (token.length > 2 && haystack.includes(token)) score += 6; }); }); if (normalizeText(lostItem.category) && normalizeText(lostItem.category) === normalizeText(foundItem.category)) score += 18; if (normalizeText(lostItem.location) && normalizeText(foundItem.location) && normalizeText(lostItem.location) === normalizeText(foundItem.location)) score += 15; return Math.min(score, 100); }; const runAIMatch = async (lostItem, foundItems) => { if (!foundItems.length) return []; const promptItems = foundItems.slice(0, 10).map((item, index) => ({ id: item.id || `found-${index}`, name: item.name, description: item.description, location: item.location, category: item.category, date: item.date, owner_email: item.owner_email || item.poster_email || item.email || '', })); try { const response = await generate({ system: 'You match lost items with found items. Return JSON only with an array named matches. Each match should include found_id and score from 0 to 100. Prefer same category, location, and obvious description overlap. Do not include items with score below 35.', user: JSON.stringify({ lostItem, foundItems: promptItems }), json: true, }); const aiMatches = Array.isArray(response?.matches) ? response.matches : []; return aiMatches .map((match) => { const found = foundItems.find((item, index) => item.id === match.found_id || `found-${index}` === match.found_id); if (!found) return null; return { foundItem: found, score: typeof match.score === 'number' ? match.score : heuristicScore(lostItem, found), reason: match.reason || 'AI-assisted match', }; }) .filter(Boolean) .sort((a, b) => b.score - a.score); } catch { return foundItems .map((foundItem) => ({ foundItem, score: heuristicScore(lostItem, foundItem), reason: 'Heuristic match', })) .filter((entry) => entry.score >= 35) .sort((a, b) => b.score - a.score) .slice(0, 3); } }; const handlePostCreated = async (createdItem) => { setPostingStatus('matching'); setPostContext(createdItem); setMatchSummary(null); try { if (createdItem?.type !== 'lost') { setPostingStatus('done'); setShowPostModal(false); return; } const foundItemsResult = await fetch('/api/items?type=found'); const foundItems = foundItemsResult?.ok ? await foundItemsResult.json() : []; const matches = await runAIMatch(createdItem, Array.isArray(foundItems) ? foundItems : []); if (!matches.length) { setMatchSummary({ title: 'No strong matches yet', text: 'Your lost item was posted successfully. We will keep it searchable for future found items.', matches: [], }); setPostingStatus('done'); setShowPostModal(false); return; } const bestMatches = matches.slice(0, 3); const matchPayloads = []; for (const match of bestMatches) { const found = match.foundItem; const record = { type: 'potential_match', status: 'pending', lost_item_id: createdItem.id, lost_item_name: createdItem.name, found_item_id: found.id, found_item_name: found.name, score: match.score, reason: match.reason, claimer_email: createdItem.poster_email || createdItem.email || createdItem.owner_email || '', poster_email: found.poster_email || found.owner_email || found.email || '', created_at: new Date().toISOString(), }; matchPayloads.push(record); await addMatchRecord(record); await sendNotification({ to: createdItem.poster_email || createdItem.email || createdItem.owner_email || '', title: 'Potential match found for your item', body: `We found a potential match for "${createdItem.name}" with score ${match.score}%.`, }); await sendNotification({ to: found.poster_email || found.owner_email || found.email || '', title: 'Potential match found for your found item', body: `A lost item may match "${found.name}". Please review the match request.`, }); } setMatchSummary({ title: 'Potential matches found', text: `We found ${bestMatches.length} possible match${bestMatches.length === 1 ? '' : 'es'} and notified both parties.`, matches: bestMatches, }); setPostingStatus('done'); setShowPostModal(false); } catch { setMatchSummary({ title: 'Item posted', text: 'Your item was posted, but matching could not be completed right now. You can try again later.', matches: [], }); setPostingStatus('error'); setShowPostModal(false); } }; return (
Report and recover lost items
{matchSummary.text}