/** * PIOE - Personal Intelligence & Opportunity Engine * Frontend JavaScript Application */ class PIOEApp { constructor() { this.currentCategory = null; this.currentDomain = null; this.minScore = 0; this.opportunities = []; this.init(); } init() { this.bindEvents(); this.loadStats(); this.loadOpportunities(); } bindEvents() { // Navigation items document.querySelectorAll('.nav-item[data-view]').forEach(item => { item.addEventListener('click', (e) => { e.preventDefault(); this.setActiveNav(item); this.handleViewChange(item.dataset.view); }); }); // Category filters document.querySelectorAll('.nav-item[data-category]').forEach(item => { item.addEventListener('click', (e) => { e.preventDefault(); this.setActiveNav(item); this.currentCategory = item.dataset.category; this.loadOpportunities(); this.showFeedView(); }); }); // Domain filter document.getElementById('domain-filter').addEventListener('change', (e) => { this.currentDomain = e.target.value || null; this.loadOpportunities(); }); // Score filter document.getElementById('score-filter').addEventListener('change', (e) => { this.minScore = parseFloat(e.target.value) || 0; this.loadOpportunities(); }); // Run ingestion document.getElementById('run-ingestion').addEventListener('click', (e) => { e.preventDefault(); this.runIngestion(); }); // View stats document.getElementById('view-stats').addEventListener('click', (e) => { e.preventDefault(); this.showStatsModal(); }); // Modal close document.querySelector('.modal-close').addEventListener('click', () => { this.closeModal(); }); document.querySelector('.modal-backdrop').addEventListener('click', () => { this.closeModal(); }); // PIOE 2.0: AI Chat document.getElementById('open-chat')?.addEventListener('click', (e) => { e.preventDefault(); this.toggleChat(); }); } // PIOE 2.0: Chat Methods toggleChat() { const panel = document.getElementById('chat-panel'); panel.classList.toggle('active'); } async sendChatMessage() { const input = document.getElementById('chat-input'); const messagesContainer = document.getElementById('chat-messages'); const message = input.value.trim(); if (!message) return; // Add user message to chat messagesContainer.innerHTML += `

${this.escapeHtml(message)}

`; input.value = ''; messagesContainer.scrollTop = messagesContainer.scrollHeight; // Add loading indicator const loadingId = `loading-${Date.now()}`; messagesContainer.innerHTML += `

[...] Searching opportunities...

`; messagesContainer.scrollTop = messagesContainer.scrollHeight; try { const response = await fetch('/api/chat', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ message }) }); const data = await response.json(); // Remove loading indicator document.getElementById(loadingId)?.remove(); // Build response HTML let responseHtml = `

${this.escapeHtml(data.response || 'No response')}

`; // Add matched opportunities if any if (data.opportunities && data.opportunities.length > 0) { responseHtml += `
`; for (const opp of data.opportunities) { const roiDisplay = opp.roi_score ? `${Math.round(opp.roi_score * 100)}% ROI` : ''; responseHtml += ` ${this.getCategoryEmoji(opp.category)} ${this.escapeHtml(opp.title.slice(0, 60))}${opp.title.length > 60 ? '...' : ''} ${roiDisplay} `; } responseHtml += `
`; } // Add suggested action if any if (data.suggested_action) { responseHtml += `

[TIP] ${this.escapeHtml(data.suggested_action)}

`; } messagesContainer.innerHTML += `
${responseHtml}
`; } catch (error) { document.getElementById(loadingId)?.remove(); messagesContainer.innerHTML += `

Error: ${error.message}

`; } messagesContainer.scrollTop = messagesContainer.scrollHeight; } setActiveNav(activeItem) { document.querySelectorAll('.nav-item').forEach(item => { item.classList.remove('active'); }); activeItem.classList.add('active'); } handleViewChange(view) { if (view === 'feed') { this.currentCategory = null; this.loadOpportunities(); this.showFeedView(); this.updateHeader('Opportunity Feed', 'High-signal opportunities detected by PIOE'); } else if (view === 'digest') { this.loadDigest('daily'); this.showDigestView(); this.updateHeader('Daily Brief', 'Your personalized intelligence report'); } else if (view === 'urgent') { this.loadDigest('urgent'); this.showDigestView(); this.updateHeader('Urgent Opportunities', 'Deadlines approaching soon'); } } updateHeader(title, subtitle) { document.getElementById('page-title').textContent = title; document.getElementById('page-subtitle').textContent = subtitle; } showFeedView() { document.getElementById('opportunity-feed').style.display = 'flex'; document.getElementById('digest-view').style.display = 'none'; } showDigestView() { document.getElementById('opportunity-feed').style.display = 'none'; document.getElementById('digest-view').style.display = 'block'; } async loadStats() { try { const response = await fetch('/api/stats'); const stats = await response.json(); document.getElementById('total-count').textContent = stats.total_opportunities || 0; document.getElementById('new-count').textContent = stats.new_opportunities || 0; document.getElementById('hackathon-count').textContent = stats.by_category?.hackathon || 0; document.getElementById('internship-count').textContent = stats.by_category?.internship || 0; } catch (error) { console.error('Failed to load stats:', error); } } async loadOpportunities() { const feed = document.getElementById('opportunity-feed'); feed.innerHTML = '
Loading opportunities...
'; try { const params = new URLSearchParams(); if (this.currentCategory) params.set('category', this.currentCategory); if (this.currentDomain) params.set('domain', this.currentDomain); if (this.minScore) params.set('min_score', this.minScore); params.set('limit', '50'); const response = await fetch(`/api/opportunities?${params}`); const data = await response.json(); this.opportunities = data.opportunities || []; this.renderOpportunities(); } catch (error) { feed.innerHTML = `
Error loading opportunities: ${error.message}
`; } } renderOpportunities() { const feed = document.getElementById('opportunity-feed'); if (this.opportunities.length === 0) { feed.innerHTML = `
No opportunities found. Try running ingestion first!
`; return; } feed.innerHTML = this.opportunities.map(opp => this.renderOpportunityCard(opp)).join(''); // Bind card click events feed.querySelectorAll('.opportunity-card').forEach((card, index) => { card.addEventListener('click', () => { this.showOpportunityDetail(this.opportunities[index]); }); // Action buttons card.querySelector('.action-btn.primary')?.addEventListener('click', (e) => { e.stopPropagation(); window.open(this.opportunities[index].url, '_blank'); }); card.querySelector('.action-btn.secondary')?.addEventListener('click', (e) => { e.stopPropagation(); this.updateStatus(this.opportunities[index].id, 'saved'); }); }); } renderOpportunityCard(opp) { const category = opp.category || 'other'; const categoryEmoji = this.getCategoryEmoji(category); const scorePercent = Math.round((opp.combined_score || 0) * 100); const roiPercent = Math.round((opp.roi_score || 0.5) * 100); const riskLevel = opp.risk_level || 'medium'; const region = opp.region || 'global'; let deadlineBadge = ''; if (opp.deadline) { const daysLeft = Math.ceil((new Date(opp.deadline) - new Date()) / (1000 * 60 * 60 * 24)); let urgency = 'ok'; if (daysLeft < 7) urgency = 'urgent'; else if (daysLeft < 14) urgency = 'soon'; deadlineBadge = ` [!] ${daysLeft} days left `; } // Risk level badge const riskColors = { low: '#10b981', medium: '#f59e0b', high: '#ef4444' }; const riskLabels = { low: '[OK]', medium: '[!]', high: '[!!]' }; // Region badge const regionLabels = { nigeria: 'NG', africa: 'AFR', global: 'GLB', remote_africa: 'AFR-R', remote_global: 'GLB-R' }; return `
${categoryEmoji} ${category.replace('_', ' ')}
${scorePercent}%

${this.escapeHtml(opp.title)}

[SRC] ${opp.source_name || 'Unknown'} [${regionLabels[region] || 'GLB'}] ${region.replace('_', ' ')} ${riskLabels[riskLevel]} ${riskLevel} risk
[ROI] ${roiPercent}% [DATE] ${this.formatDate(opp.discovered_at)}

${this.escapeHtml(opp.raw_text?.slice(0, 200) || '')}

`; } getCategoryEmoji(category) { const labels = { scholarship: '[S]', fellowship: '[F]', internship: '[I]', job: '[J]', hackathon: '[H]', competition: '[C]', grant: '[G]', micro_grant: '[MG]', ecosystem_grant: '[EG]', innovation_fund: '[IF]', research: '[R]', open_source: '[OS]', conference: '[CF]', investment: '[IV]', partnership: '[P]', collaboration: '[CO]', pitch_event: '[PE]', demo_day: '[DD]', talent_call: '[TC]', bounty: '[B]', ambassador: '[A]', pre_grant_signal: '[PG]', pre_hiring_signal: '[PH]', weak_signal: '[WS]', other: '[?]' }; return labels[category] || '[?]'; } async loadDigest(type) { const content = document.getElementById('digest-content'); content.innerHTML = '
Generating digest...
'; try { const response = await fetch(`/api/digest/${type}`); const data = await response.json(); // Convert markdown to HTML (simple conversion) content.innerHTML = this.markdownToHtml(data.digest || 'No digest available.'); } catch (error) { content.innerHTML = `

Error loading digest: ${error.message}

`; } } markdownToHtml(md) { return md .replace(/^### (.*$)/gim, '

$1

') .replace(/^## (.*$)/gim, '

$1

') .replace(/^# (.*$)/gim, '

$1

') .replace(/\*\*(.*?)\*\*/g, '$1') .replace(/\*(.*?)\*/g, '$1') .replace(/^> (.*$)/gim, '
$1
') .replace(/\[(.*?)\]\((.*?)\)/g, '$1') .replace(/^---$/gim, '
') .replace(/\n/g, '
'); } showOpportunityDetail(opp) { const modal = document.getElementById('detail-modal'); const body = document.getElementById('modal-body'); const roiPercent = Math.round((opp.roi_score || 0.5) * 100); const riskLevel = opp.risk_level || 'medium'; const region = opp.region || 'global'; const riskColors = { low: '#10b981', medium: '#f59e0b', high: '#ef4444' }; body.innerHTML = ` ${this.getCategoryEmoji(opp.category)} ${(opp.category || 'other').replace('_', ' ')}

${this.escapeHtml(opp.title)}

📡 ${opp.source_name} 🌐 ${region.replace('_', ' ')} ${riskLevel} risk
${Math.round((opp.relevance_score || 0) * 100)}% Relevance
${Math.round((opp.novelty_score || 0) * 100)}% Novelty
${Math.round((opp.credibility_score || 0) * 100)}% Credibility
${roiPercent}% 💎 ROI
${opp.deadline ? `

⏰ Deadline: ${new Date(opp.deadline).toLocaleDateString()}

` : ''}

${this.escapeHtml(opp.raw_text || 'No description available.')}

🔗 View Original
`; modal.classList.add('active'); } async getGuidance(opportunityId) { const container = document.getElementById('guidance-container'); const content = document.getElementById('guidance-content'); container.style.display = 'block'; content.innerHTML = '

🔄 Analyzing opportunity...

'; try { const response = await fetch(`/api/opportunities/${opportunityId}/guidance`); const data = await response.json(); const g = data.guidance; content.innerHTML = `
${g.primary_action?.replace('_', ' ') || 'Review'} Action
${g.urgency || 'whenever'} Urgency
${Math.round((g.success_probability || 0.3) * 100)}% Success Odds
${g.time_investment_hours || 10}h Time Needed
${g.skills_to_highlight?.length ? `
Skills to Highlight:
${g.skills_to_highlight.map(s => `${s}`).join('')}
` : ''} ${g.portfolio_pieces?.length ? `
Portfolio to Show:
${g.portfolio_pieces.map(p => `${p}`).join('')}
` : ''} ${g.preparation_steps?.length ? `
Preparation Steps:
    ${g.preparation_steps.map(s => `
  1. ${s}
  2. `).join('')}
` : ''} ${g.networking_tips ? `
💡 Networking Tip:

${g.networking_tips}

` : ''} ${g.differentiation_angle ? `
🎯 Your Angle:

${g.differentiation_angle}

` : ''} ${g.red_flags?.length ? `
⚠️ Red Flags:
` : ''}

${g.why || 'Personalized guidance based on your profile'}

`; } catch (error) { content.innerHTML = `

Failed to get guidance: ${error.message}

`; } } closeModal() { document.getElementById('detail-modal').classList.remove('active'); } async updateStatus(id, status) { try { await fetch(`/api/opportunities/${id}/status`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ status }) }); // Visual feedback this.showNotification(`Status updated to ${status}`); } catch (error) { console.error('Failed to update status:', error); } } async runIngestion() { this.showNotification('Starting ingestion... This may take a few minutes.'); try { await fetch('/api/ingest/run', { method: 'POST' }); this.showNotification('Ingestion started! Refresh in a few minutes to see new opportunities.'); } catch (error) { this.showNotification('Failed to start ingestion: ' + error.message); } } async showStatsModal() { try { const response = await fetch('/api/stats'); const stats = await response.json(); const body = document.getElementById('modal-body'); body.innerHTML = `

📊 System Statistics

${stats.total_opportunities || 0} Total Opportunities
${stats.new_opportunities || 0} New (Unread)

By Category

${Object.entries(stats.by_category || {}).map(([cat, count]) => `
${this.getCategoryEmoji(cat)} ${cat.replace('_', ' ')} ${count}
`).join('')}

By Domain

${Object.entries(stats.by_domain || {}).map(([dom, count]) => `
${dom.replace('_', ' ')} ${count}
`).join('')} `; document.getElementById('detail-modal').classList.add('active'); } catch (error) { console.error('Failed to load stats:', error); } } showNotification(message) { // Simple notification - could be enhanced with toast UI console.log('PIOE:', message); alert(message); } formatDate(dateStr) { if (!dateStr) return 'Unknown'; const date = new Date(dateStr); return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }); } escapeHtml(text) { if (!text) return ''; const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } } // Initialize app const app = new PIOEApp();