Website (14 monoliths split): - compliance/page.tsx (1,519 → 9), docs/audit (1,262 → 20) - quality (1,231 → 16), alerts (1,203 → 10), docs (1,202 → 11) - i18n.ts (1,173 → 8 language files) - unity-bridge (1,094 → 12), backlog (1,087 → 6) - training (1,066 → 8), rag (1,063 → 8) - Deleted index_original.ts (4,899 LOC dead backup) Studio-v2 (5 monoliths split): - meet/page.tsx (1,481 → 9), messages (1,166 → 9) - AlertsB2BContext.tsx (1,165 → 5 modules) - alerts-b2b/page.tsx (1,019 → 6), korrektur/archiv (1,001 → 6) All existing imports preserved. Zero new TypeScript errors. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
345 lines
10 KiB
TypeScript
345 lines
10 KiB
TypeScript
'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<AlertsB2BContextType | null>(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<B2BTenant>(defaultTenant)
|
|
const [settings, setSettings] = useState<B2BSettings>(defaultB2BSettings)
|
|
const [sources, setSources] = useState<AlertSource[]>([])
|
|
const [topics, setTopics] = useState<B2BTopic[]>([])
|
|
const [hits, setHits] = useState<B2BHit[]>([])
|
|
const [selectedTemplate, setSelectedTemplate] = useState<B2BTemplate | null>(null)
|
|
const [isLoading] = useState(false)
|
|
const [error] = useState<string | null>(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<B2BTenant>) => {
|
|
setTenant(prev => ({ ...prev, ...updates }))
|
|
}, [])
|
|
|
|
const updateSettings = useCallback((updates: Partial<B2BSettings>) => {
|
|
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<AlertSource, 'id' | 'createdAt'>) => {
|
|
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<B2BTopic, 'id' | 'createdAt'>) => {
|
|
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<B2BTopic>) => {
|
|
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 (
|
|
<AlertsB2BContext.Provider
|
|
value={{
|
|
tenant: defaultTenant,
|
|
updateTenant: () => {},
|
|
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}
|
|
</AlertsB2BContext.Provider>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<AlertsB2BContext.Provider
|
|
value={{
|
|
tenant,
|
|
updateTenant,
|
|
settings,
|
|
updateSettings,
|
|
availableTemplates,
|
|
selectedTemplate,
|
|
selectTemplate,
|
|
sources,
|
|
addSource,
|
|
removeSource,
|
|
generateInboundEmail,
|
|
topics,
|
|
addTopic,
|
|
updateTopic,
|
|
removeTopic,
|
|
hits,
|
|
unreadCount,
|
|
relevantCount,
|
|
needsReviewCount,
|
|
markAsRead,
|
|
markAllAsRead,
|
|
submitFeedback,
|
|
processEmailContent,
|
|
getDigest,
|
|
isLoading,
|
|
error
|
|
}}
|
|
>
|
|
{children}
|
|
</AlertsB2BContext.Provider>
|
|
)
|
|
}
|
|
|
|
export function useAlertsB2B() {
|
|
const context = useContext(AlertsB2BContext)
|
|
if (!context) {
|
|
throw new Error('useAlertsB2B must be used within an AlertsB2BProvider')
|
|
}
|
|
return context
|
|
}
|