'use client' import { createContext, useContext, useState, useEffect, useCallback, ReactNode } from 'react' import type { AlertSource, B2BHit, B2BTopic, B2BTemplate, B2BTenant, B2BSettings, AlertsB2BContextType, } from './alerts-b2b/types' import { hectronicTemplate, mockHectronicHits, defaultB2BSettings, defaultTenant } from './alerts-b2b/template-data' import { processEmailToHit } from './alerts-b2b/actions' // Re-export all types and helpers for backward compatibility export type { ImportanceLabel, DecisionLabel, SourceType, NoiseMode, Package, AlertSource, B2BHit, DecisionTrace, B2BTopic, TopicIntent, TopicFilters, DecisionPolicy, ImportanceModel, B2BTemplate, GuidedConfig, NotificationPreset, B2BTenant, B2BSettings, } from './alerts-b2b/types' export { hectronicTemplate } from './alerts-b2b/template-data' export { getImportanceLabelColor, getDecisionLabelColor, formatDeadline, getPackageIcon, getPackageLabel, } from './alerts-b2b/helpers' // ============================================ // CONTEXT // ============================================ const AlertsB2BContext = createContext(null) // LocalStorage Keys const B2B_TENANT_KEY = 'bp_b2b_tenant' const B2B_SETTINGS_KEY = 'bp_b2b_settings' const B2B_SOURCES_KEY = 'bp_b2b_sources' const B2B_TOPICS_KEY = 'bp_b2b_topics' const B2B_HITS_KEY = 'bp_b2b_hits' export function AlertsB2BProvider({ children }: { children: ReactNode }) { const [tenant, setTenant] = useState(defaultTenant) const [settings, setSettings] = useState(defaultB2BSettings) const [sources, setSources] = useState([]) const [topics, setTopics] = useState([]) const [hits, setHits] = useState([]) const [selectedTemplate, setSelectedTemplate] = useState(null) const [isLoading] = useState(false) const [error] = useState(null) const [mounted, setMounted] = useState(false) // Available templates const availableTemplates: B2BTemplate[] = [hectronicTemplate] // Load from localStorage useEffect(() => { const storedTenant = localStorage.getItem(B2B_TENANT_KEY) const storedSettings = localStorage.getItem(B2B_SETTINGS_KEY) const storedSources = localStorage.getItem(B2B_SOURCES_KEY) const storedTopics = localStorage.getItem(B2B_TOPICS_KEY) const storedHits = localStorage.getItem(B2B_HITS_KEY) if (storedTenant) { try { const parsed = JSON.parse(storedTenant) setTenant({ ...defaultTenant, ...parsed, createdAt: new Date(parsed.createdAt) }) } catch (e) { console.error('Error parsing tenant:', e) } } if (storedSettings) { try { setSettings({ ...defaultB2BSettings, ...JSON.parse(storedSettings) }) } catch (e) { console.error('Error parsing settings:', e) } } if (storedSources) { try { const parsed = JSON.parse(storedSources) setSources(parsed.map((s: any) => ({ ...s, createdAt: new Date(s.createdAt) }))) } catch (e) { console.error('Error parsing sources:', e) } } if (storedTopics) { try { const parsed = JSON.parse(storedTopics) setTopics(parsed.map((t: any) => ({ ...t, createdAt: new Date(t.createdAt) }))) } catch (e) { console.error('Error parsing topics:', e) } } if (storedHits) { try { const parsed = JSON.parse(storedHits) setHits(parsed.map((h: any) => ({ ...h, foundAt: new Date(h.foundAt), createdAt: new Date(h.createdAt) }))) } catch (e) { console.error('Error parsing hits:', e) setHits(mockHectronicHits) } } else { setHits(mockHectronicHits) } setMounted(true) }, []) // Save to localStorage useEffect(() => { if (mounted) localStorage.setItem(B2B_TENANT_KEY, JSON.stringify(tenant)) }, [tenant, mounted]) useEffect(() => { if (mounted) localStorage.setItem(B2B_SETTINGS_KEY, JSON.stringify(settings)) }, [settings, mounted]) useEffect(() => { if (mounted) localStorage.setItem(B2B_SOURCES_KEY, JSON.stringify(sources)) }, [sources, mounted]) useEffect(() => { if (mounted) localStorage.setItem(B2B_TOPICS_KEY, JSON.stringify(topics)) }, [topics, mounted]) useEffect(() => { if (mounted && hits.length > 0) localStorage.setItem(B2B_HITS_KEY, JSON.stringify(hits)) }, [hits, mounted]) // Tenant operations const updateTenant = useCallback((updates: Partial) => { setTenant(prev => ({ ...prev, ...updates })) }, []) const updateSettings = useCallback((updates: Partial) => { setSettings(prev => ({ ...prev, ...updates })) }, []) // Template operations const selectTemplate = useCallback((templateId: string) => { const template = availableTemplates.find(t => t.templateId === templateId) if (template) { setSelectedTemplate(template) updateSettings({ selectedTemplateId: templateId, selectedRegions: template.guidedConfig.regionSelector.default, selectedLanguages: template.guidedConfig.languageSelector.default, selectedPackages: template.guidedConfig.packageSelector.default, noiseMode: template.guidedConfig.noiseMode.default, notificationCadence: template.notificationPreset.cadence, notificationTime: template.notificationPreset.timeLocal, maxDigestItems: template.notificationPreset.maxItems }) const newTopics: B2BTopic[] = template.topics.map((t, idx) => ({ ...t, id: `topic-${Date.now()}-${idx}`, tenantId: tenant.id, createdAt: new Date() })) setTopics(newTopics) } }, [availableTemplates, tenant.id, updateSettings]) // Source operations const generateInboundEmail = useCallback(() => { const token = Math.random().toString(36).substring(2, 10) return `alerts+${tenant.id}-${token}@${tenant.inboundEmailDomain}` }, [tenant.id, tenant.inboundEmailDomain]) const addSource = useCallback((source: Omit) => { const newSource: AlertSource = { ...source, id: `source-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, createdAt: new Date() } setSources(prev => [...prev, newSource]) }, []) const removeSource = useCallback((id: string) => { setSources(prev => prev.filter(s => s.id !== id)) }, []) // Topic operations const addTopic = useCallback((topic: Omit) => { const newTopic: B2BTopic = { ...topic, id: `topic-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, createdAt: new Date() } setTopics(prev => [...prev, newTopic]) }, []) const updateTopic = useCallback((id: string, updates: Partial) => { setTopics(prev => prev.map(t => t.id === id ? { ...t, ...updates } : t)) }, []) const removeTopic = useCallback((id: string) => { setTopics(prev => prev.filter(t => t.id !== id)) }, []) // Hit operations const markAsRead = useCallback((id: string) => { setHits(prev => prev.map(h => h.id === id ? { ...h, isRead: true } : h)) }, []) const markAllAsRead = useCallback(() => { setHits(prev => prev.map(h => ({ ...h, isRead: true }))) }, []) const submitFeedback = useCallback((id: string, feedback: 'relevant' | 'irrelevant') => { setHits(prev => prev.map(h => h.id === id ? { ...h, userFeedback: feedback } : h)) }, []) const processEmailContent = useCallback((emailContent: string, emailSubject?: string): B2BHit => { const newHit = processEmailToHit( emailContent, tenant.id, topics[0]?.id || 'default', emailSubject ) setHits(prev => [newHit, ...prev]) return newHit }, [tenant.id, topics]) // Digest const getDigest = useCallback((maxItems: number = settings.maxDigestItems) => { return hits .filter(h => h.decisionLabel === 'relevant' || h.decisionLabel === 'needs_review') .sort((a, b) => b.importanceScore - a.importanceScore) .slice(0, maxItems) }, [hits, settings.maxDigestItems]) // Computed values const unreadCount = hits.filter(h => !h.isRead && h.decisionLabel !== 'irrelevant').length const relevantCount = hits.filter(h => h.decisionLabel === 'relevant').length const needsReviewCount = hits.filter(h => h.decisionLabel === 'needs_review').length // SSR safety if (!mounted) { return ( {}, settings: defaultB2BSettings, updateSettings: () => {}, availableTemplates: [], selectedTemplate: null, selectTemplate: () => {}, sources: [], addSource: () => {}, removeSource: () => {}, generateInboundEmail: () => '', topics: [], addTopic: () => {}, updateTopic: () => {}, removeTopic: () => {}, hits: [], unreadCount: 0, relevantCount: 0, needsReviewCount: 0, markAsRead: () => {}, markAllAsRead: () => {}, submitFeedback: () => {}, processEmailContent: () => ({} as B2BHit), getDigest: () => [], isLoading: false, error: null }} > {children} ) } return ( {children} ) } export function useAlertsB2B() { const context = useContext(AlertsB2BContext) if (!context) { throw new Error('useAlertsB2B must be used within an AlertsB2BProvider') } return context }