[split-required] Split website + studio-v2 monoliths (Phase 3 continued)
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>
This commit is contained in:
File diff suppressed because it is too large
Load Diff
149
studio-v2/lib/alerts-b2b/actions.ts
Normal file
149
studio-v2/lib/alerts-b2b/actions.ts
Normal file
@@ -0,0 +1,149 @@
|
||||
import type { B2BHit, B2BTopic, ImportanceLabel, DecisionLabel } from './types'
|
||||
|
||||
/**
|
||||
* Process email content into a B2BHit (Manual Import for Testing).
|
||||
* Pure function — no state mutations.
|
||||
*/
|
||||
export function processEmailToHit(
|
||||
emailContent: string,
|
||||
tenantId: string,
|
||||
defaultTopicId: string,
|
||||
emailSubject?: string
|
||||
): B2BHit {
|
||||
// Parse email content to extract useful information
|
||||
const lines = emailContent.split('\n').filter(l => l.trim())
|
||||
const title = emailSubject || lines[0]?.slice(0, 100) || 'Manuell eingefuegter Alert'
|
||||
|
||||
// Try to extract URLs from content
|
||||
const urlRegex = /(https?:\/\/[^\s<>"]+)/g
|
||||
const urls = emailContent.match(urlRegex) || []
|
||||
const firstUrl = urls[0] || 'https://example.com/manual-import'
|
||||
|
||||
// Extract snippet (first meaningful paragraph)
|
||||
const snippet = lines.slice(0, 3).join(' ').slice(0, 300) || emailContent.slice(0, 300)
|
||||
|
||||
// Simulate AI analysis - look for procurement signals
|
||||
const procurementSignals = ['ausschreibung', 'tender', 'vergabe', 'beschaffung', 'auftrag',
|
||||
'angebot', 'submission', 'procurement', 'rfp', 'rfq', 'bid']
|
||||
const productSignals = ['parking', 'parkschein', 'ladesäule', 'ev charging', 'ladestation',
|
||||
'tankstelle', 'fuel', 'bezahlterminal', 'payment']
|
||||
const buyerSignals = ['stadt', 'kommune', 'gemeinde', 'city', 'municipality', 'council',
|
||||
'stadtwerke', 'öffentlich', 'public']
|
||||
const negativeSignals = ['stellenangebot', 'job', 'karriere', 'news', 'blog', 'press release']
|
||||
|
||||
const lowerContent = emailContent.toLowerCase()
|
||||
|
||||
const foundProcurement = procurementSignals.filter(s => lowerContent.includes(s))
|
||||
const foundProducts = productSignals.filter(s => lowerContent.includes(s))
|
||||
const foundBuyers = buyerSignals.filter(s => lowerContent.includes(s))
|
||||
const foundNegatives = negativeSignals.filter(s => lowerContent.includes(s))
|
||||
|
||||
// Calculate importance score (0-100)
|
||||
let score = 30 // base score
|
||||
score += foundProcurement.length * 15
|
||||
score += foundProducts.length * 10
|
||||
score += foundBuyers.length * 12
|
||||
score -= foundNegatives.length * 20
|
||||
score = Math.max(0, Math.min(100, score))
|
||||
|
||||
// Determine importance label
|
||||
let importanceLabel: ImportanceLabel = 'INFO'
|
||||
if (score >= 80) importanceLabel = 'KRITISCH'
|
||||
else if (score >= 65) importanceLabel = 'DRINGEND'
|
||||
else if (score >= 50) importanceLabel = 'WICHTIG'
|
||||
else if (score >= 30) importanceLabel = 'PRUEFEN'
|
||||
|
||||
// Determine decision label
|
||||
let decisionLabel: DecisionLabel = 'irrelevant'
|
||||
let decisionConfidence = 0.5
|
||||
if (foundNegatives.length > 1) {
|
||||
decisionLabel = 'irrelevant'
|
||||
decisionConfidence = 0.8
|
||||
} else if (foundProcurement.length >= 2 && foundProducts.length >= 1) {
|
||||
decisionLabel = 'relevant'
|
||||
decisionConfidence = 0.85
|
||||
} else if (foundProcurement.length >= 1 || foundProducts.length >= 1) {
|
||||
decisionLabel = 'needs_review'
|
||||
decisionConfidence = 0.6
|
||||
}
|
||||
|
||||
// Try to guess buyer and country
|
||||
let buyerGuess: string | undefined
|
||||
let countryGuess: string | undefined
|
||||
|
||||
const countryPatterns = [
|
||||
{ pattern: /deutschland|germany|german/i, country: 'DE' },
|
||||
{ pattern: /österreich|austria|austrian/i, country: 'AT' },
|
||||
{ pattern: /schweiz|switzerland|swiss/i, country: 'CH' },
|
||||
{ pattern: /frankreich|france|french/i, country: 'FR' },
|
||||
{ pattern: /niederlande|netherlands|dutch/i, country: 'NL' },
|
||||
]
|
||||
for (const { pattern, country } of countryPatterns) {
|
||||
if (pattern.test(emailContent)) {
|
||||
countryGuess = country
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Extract potential buyer name (look for "Stadt X" or "Kommune Y")
|
||||
const buyerMatch = emailContent.match(/(?:stadt|kommune|gemeinde|city of|municipality of)\s+([A-Za-zäöüß]+)/i)
|
||||
if (buyerMatch) {
|
||||
buyerGuess = buyerMatch[0]
|
||||
}
|
||||
|
||||
// Try to find deadline
|
||||
let deadlineGuess: string | undefined
|
||||
const dateMatch = emailContent.match(/(\d{1,2})[.\/](\d{1,2})[.\/](\d{2,4})/)
|
||||
if (dateMatch) {
|
||||
const day = parseInt(dateMatch[1])
|
||||
const month = parseInt(dateMatch[2])
|
||||
const year = dateMatch[3].length === 2 ? 2000 + parseInt(dateMatch[3]) : parseInt(dateMatch[3])
|
||||
const date = new Date(year, month - 1, day)
|
||||
if (date > new Date()) {
|
||||
deadlineGuess = date.toISOString()
|
||||
}
|
||||
}
|
||||
|
||||
// Create new hit
|
||||
const newHit: B2BHit = {
|
||||
id: `manual_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
||||
tenantId,
|
||||
topicId: defaultTopicId,
|
||||
sourceType: 'email',
|
||||
sourceRef: 'manual_import',
|
||||
originalUrl: firstUrl,
|
||||
canonicalUrl: firstUrl,
|
||||
title,
|
||||
snippet,
|
||||
fullText: emailContent,
|
||||
foundAt: new Date(),
|
||||
language: 'de',
|
||||
countryGuess,
|
||||
buyerGuess,
|
||||
deadlineGuess,
|
||||
importanceScore: score,
|
||||
importanceLabel,
|
||||
decisionLabel,
|
||||
decisionConfidence,
|
||||
decisionTrace: {
|
||||
rulesTriggered: [
|
||||
...(foundProcurement.length > 0 ? ['procurement_signal_detected'] : []),
|
||||
...(foundProducts.length > 0 ? ['product_match_found'] : []),
|
||||
...(foundBuyers.length > 0 ? ['public_buyer_signal'] : []),
|
||||
...(foundNegatives.length > 0 ? ['negative_signal_detected'] : []),
|
||||
],
|
||||
llmUsed: true,
|
||||
llmConfidence: decisionConfidence,
|
||||
signals: {
|
||||
procurementSignalsFound: foundProcurement,
|
||||
publicBuyerSignalsFound: foundBuyers,
|
||||
productSignalsFound: foundProducts,
|
||||
negativesFound: foundNegatives
|
||||
}
|
||||
},
|
||||
isRead: false,
|
||||
createdAt: new Date()
|
||||
}
|
||||
|
||||
return newHit
|
||||
}
|
||||
59
studio-v2/lib/alerts-b2b/helpers.ts
Normal file
59
studio-v2/lib/alerts-b2b/helpers.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import type { ImportanceLabel, DecisionLabel, Package } from './types'
|
||||
|
||||
// ============================================
|
||||
// HELPER FUNCTIONS
|
||||
// ============================================
|
||||
|
||||
export function getImportanceLabelColor(label: ImportanceLabel, isDark: boolean): string {
|
||||
const colors = {
|
||||
'KRITISCH': isDark ? 'bg-red-500/20 text-red-300 border-red-500/30' : 'bg-red-100 text-red-700 border-red-200',
|
||||
'DRINGEND': isDark ? 'bg-orange-500/20 text-orange-300 border-orange-500/30' : 'bg-orange-100 text-orange-700 border-orange-200',
|
||||
'WICHTIG': isDark ? 'bg-yellow-500/20 text-yellow-300 border-yellow-500/30' : 'bg-yellow-100 text-yellow-700 border-yellow-200',
|
||||
'PRUEFEN': isDark ? 'bg-blue-500/20 text-blue-300 border-blue-500/30' : 'bg-blue-100 text-blue-700 border-blue-200',
|
||||
'INFO': isDark ? 'bg-slate-500/20 text-slate-300 border-slate-500/30' : 'bg-slate-100 text-slate-600 border-slate-200',
|
||||
}
|
||||
return colors[label]
|
||||
}
|
||||
|
||||
export function getDecisionLabelColor(label: DecisionLabel, isDark: boolean): string {
|
||||
const colors = {
|
||||
'relevant': isDark ? 'bg-green-500/20 text-green-300' : 'bg-green-100 text-green-700',
|
||||
'irrelevant': isDark ? 'bg-slate-500/20 text-slate-400' : 'bg-slate-100 text-slate-500',
|
||||
'needs_review': isDark ? 'bg-amber-500/20 text-amber-300' : 'bg-amber-100 text-amber-700',
|
||||
}
|
||||
return colors[label]
|
||||
}
|
||||
|
||||
export function formatDeadline(dateStr: string | null | undefined): string {
|
||||
if (!dateStr) return 'Keine Frist'
|
||||
const date = new Date(dateStr)
|
||||
const now = new Date()
|
||||
const diffDays = Math.ceil((date.getTime() - now.getTime()) / (1000 * 60 * 60 * 24))
|
||||
|
||||
if (diffDays < 0) return 'Abgelaufen'
|
||||
if (diffDays === 0) return 'Heute!'
|
||||
if (diffDays === 1) return 'Morgen'
|
||||
if (diffDays <= 7) return `${diffDays} Tage`
|
||||
if (diffDays <= 30) return `${Math.ceil(diffDays / 7)} Wochen`
|
||||
return date.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' })
|
||||
}
|
||||
|
||||
export function getPackageIcon(pkg: Package): string {
|
||||
const icons = {
|
||||
'PARKING': '🅿️',
|
||||
'EV_CHARGING': '⚡',
|
||||
'FUEL': '⛽',
|
||||
'TANK_MONITORING': '📊'
|
||||
}
|
||||
return icons[pkg]
|
||||
}
|
||||
|
||||
export function getPackageLabel(pkg: Package): string {
|
||||
const labels = {
|
||||
'PARKING': 'Parking',
|
||||
'EV_CHARGING': 'EV Charging',
|
||||
'FUEL': 'Fuel',
|
||||
'TANK_MONITORING': 'Tank Monitoring'
|
||||
}
|
||||
return labels[pkg]
|
||||
}
|
||||
427
studio-v2/lib/alerts-b2b/template-data.ts
Normal file
427
studio-v2/lib/alerts-b2b/template-data.ts
Normal file
@@ -0,0 +1,427 @@
|
||||
import type { B2BTemplate, B2BHit, B2BSettings, B2BTenant } from './types'
|
||||
|
||||
// ============================================
|
||||
// HECTRONIC TEMPLATE (Real Example)
|
||||
// ============================================
|
||||
|
||||
export const hectronicTemplate: B2BTemplate = {
|
||||
templateId: 'tpl_hectronic_public_procurement_v1',
|
||||
templateName: 'Hectronic – Kommunale Ausschreibungen (Parking • EV Charging • Fuel)',
|
||||
templateDescription: 'Findet weltweit kommunale Ausschreibungen für Parkscheinautomaten/Parkraummanagement, Payment an Ladepunkten und Fuel-Terminals. Reduziert News/Jobs/Zubehör-Rauschen stark.',
|
||||
targetRoles: ['sales_ops', 'bid_management', 'product_marketing', 'regional_sales'],
|
||||
industry: 'Payment & Infrastructure',
|
||||
companyExample: 'Hectronic GmbH',
|
||||
guidedConfig: {
|
||||
regionSelector: {
|
||||
type: 'multi_select',
|
||||
default: ['EUROPE'],
|
||||
options: ['EUROPE', 'DACH', 'NORTH_AMERICA', 'MIDDLE_EAST', 'APAC', 'GLOBAL']
|
||||
},
|
||||
languageSelector: {
|
||||
type: 'multi_select',
|
||||
default: ['de', 'en'],
|
||||
options: ['de', 'en', 'fr', 'es', 'it', 'nl', 'pl', 'sv', 'da', 'no', 'fi', 'pt']
|
||||
},
|
||||
packageSelector: {
|
||||
type: 'multi_select',
|
||||
default: ['PARKING', 'EV_CHARGING'],
|
||||
options: ['PARKING', 'EV_CHARGING', 'FUEL', 'TANK_MONITORING']
|
||||
},
|
||||
noiseMode: {
|
||||
type: 'single_select',
|
||||
default: 'STRICT',
|
||||
options: ['STRICT', 'BALANCED', 'BROAD']
|
||||
}
|
||||
},
|
||||
topics: [
|
||||
{
|
||||
name: 'Parking – Parkscheinautomaten & Parkraummanagement (Kommunen)',
|
||||
package: 'PARKING',
|
||||
intent: {
|
||||
goalStatement: 'Finde kommunale Ausschreibungen/Procurements für Parkscheinautomaten, Pay Stations, Parkraummanagement-Software und zugehörige Services.',
|
||||
mustHaveAnyN: 2,
|
||||
mustHave: [
|
||||
{ type: 'keyword', value: 'parking meter' },
|
||||
{ type: 'keyword', value: 'pay and display' },
|
||||
{ type: 'keyword', value: 'parking pay station' },
|
||||
{ type: 'keyword', value: 'Parkscheinautomat' },
|
||||
{ type: 'keyword', value: 'Parkautomat' },
|
||||
{ type: 'keyword', value: 'Parkraumbewirtschaftung' },
|
||||
{ type: 'keyword', value: 'backoffice' },
|
||||
{ type: 'keyword', value: 'management software' }
|
||||
],
|
||||
publicBuyerSignalsAny: [
|
||||
'municipality', 'city', 'city council', 'local authority', 'public tender',
|
||||
'Kommune', 'Stadt', 'Gemeinde', 'Landkreis', 'öffentliche Ausschreibung', 'Vergabe'
|
||||
],
|
||||
procurementSignalsAny: [
|
||||
'tender', 'procurement', 'RFP', 'RFQ', 'invitation to tender', 'contract notice',
|
||||
'Ausschreibung', 'Vergabe', 'EU-weite Ausschreibung', 'Bekanntmachung'
|
||||
]
|
||||
},
|
||||
filters: {
|
||||
hardExcludesAny: [
|
||||
'keyboard', 'mouse', 'headset', 'toner', 'office supplies',
|
||||
'Tastatur', 'Maus', 'Headset', 'Büromaterial',
|
||||
'job', 'hiring', 'stellenanzeige', 'bewerbung',
|
||||
'parking fine', 'parking ticket', 'Strafzettel', 'Knöllchen'
|
||||
],
|
||||
softExcludesAny: [
|
||||
'press release', 'marketing', 'blog', 'opinion',
|
||||
'pressemitteilung', 'werbung', 'meinung',
|
||||
'maintenance only', 'consulting only', 'support only'
|
||||
]
|
||||
},
|
||||
decisionPolicy: {
|
||||
rulesFirst: true,
|
||||
llmMode: 'GRAYZONE_ONLY',
|
||||
autoRelevantMinConf: 0.80,
|
||||
needsReviewRange: [0.50, 0.79],
|
||||
autoIrrelevantMaxConf: 0.49
|
||||
},
|
||||
importanceModelId: 'imp_hectronic_v1',
|
||||
status: 'active'
|
||||
},
|
||||
{
|
||||
name: 'EV Charging – Payment/Terminals in kommunaler Ladeinfrastruktur',
|
||||
package: 'EV_CHARGING',
|
||||
intent: {
|
||||
goalStatement: 'Finde kommunale Ausschreibungen für Ladeinfrastruktur mit Payment-Terminals, Ad-hoc-Kartenzahlung, Backend/Management.',
|
||||
mustHaveAnyN: 2,
|
||||
mustHave: [
|
||||
{ type: 'keyword', value: 'EV charging' },
|
||||
{ type: 'keyword', value: 'charging station' },
|
||||
{ type: 'keyword', value: 'charge point' },
|
||||
{ type: 'keyword', value: 'Ladesäule' },
|
||||
{ type: 'keyword', value: 'Ladeinfrastruktur' },
|
||||
{ type: 'keyword', value: 'payment terminal' },
|
||||
{ type: 'keyword', value: 'card payment' },
|
||||
{ type: 'keyword', value: 'contactless' },
|
||||
{ type: 'keyword', value: 'Bezahlsystem' },
|
||||
{ type: 'keyword', value: 'Kartenzahlung' }
|
||||
],
|
||||
publicBuyerSignalsAny: [
|
||||
'municipality', 'city', 'public works', 'local authority',
|
||||
'Kommune', 'Stadt', 'Gemeinde', 'Kommunal'
|
||||
],
|
||||
procurementSignalsAny: [
|
||||
'tender', 'procurement', 'RFP', 'RFQ', 'contract notice',
|
||||
'Ausschreibung', 'Vergabe', 'Bekanntmachung'
|
||||
]
|
||||
},
|
||||
filters: {
|
||||
hardExcludesAny: [
|
||||
'stock', 'investor', 'funding round', 'press release',
|
||||
'Aktie', 'Investor', 'Finanzierung', 'Pressemitteilung',
|
||||
'job', 'hiring', 'stellenanzeige',
|
||||
'private home charger', 'wallbox'
|
||||
],
|
||||
softExcludesAny: [
|
||||
'funding program', 'grant', 'Förderprogramm', 'Zuschuss',
|
||||
'pilot project', 'research project', 'Modellprojekt'
|
||||
]
|
||||
},
|
||||
decisionPolicy: {
|
||||
rulesFirst: true,
|
||||
llmMode: 'GRAYZONE_ONLY',
|
||||
autoRelevantMinConf: 0.80,
|
||||
needsReviewRange: [0.50, 0.79],
|
||||
autoIrrelevantMaxConf: 0.49
|
||||
},
|
||||
importanceModelId: 'imp_hectronic_v1',
|
||||
status: 'active'
|
||||
},
|
||||
{
|
||||
name: 'Fuel – Fleet/Public Fuel Terminals',
|
||||
package: 'FUEL',
|
||||
intent: {
|
||||
goalStatement: 'Finde Ausschreibungen für (un)bemannte Tankautomaten/Fuel-Terminals, Fuel-Management, Flottenbetankung.',
|
||||
mustHaveAnyN: 2,
|
||||
mustHave: [
|
||||
{ type: 'keyword', value: 'fuel terminal' },
|
||||
{ type: 'keyword', value: 'fuel management' },
|
||||
{ type: 'keyword', value: 'unmanned fuel station' },
|
||||
{ type: 'keyword', value: 'fleet fueling' },
|
||||
{ type: 'keyword', value: 'Tankautomat' },
|
||||
{ type: 'keyword', value: 'Tankterminal' },
|
||||
{ type: 'keyword', value: 'Flottenbetankung' },
|
||||
{ type: 'keyword', value: 'Betriebstankstelle' }
|
||||
],
|
||||
publicBuyerSignalsAny: ['municipality', 'utilities', 'public works', 'Stadtwerke', 'Kommunal'],
|
||||
procurementSignalsAny: ['tender', 'procurement', 'RFP', 'Ausschreibung', 'Vergabe']
|
||||
},
|
||||
filters: {
|
||||
hardExcludesAny: ['fuel price', 'oil price', 'Spritpreise', 'Ölpreis', 'job', 'hiring', 'stellenanzeige'],
|
||||
softExcludesAny: ['market news', 'commodity', 'Börse', 'Pressemitteilung']
|
||||
},
|
||||
decisionPolicy: {
|
||||
rulesFirst: true,
|
||||
llmMode: 'GRAYZONE_ONLY',
|
||||
autoRelevantMinConf: 0.80,
|
||||
needsReviewRange: [0.50, 0.79],
|
||||
autoIrrelevantMaxConf: 0.49
|
||||
},
|
||||
importanceModelId: 'imp_hectronic_v1',
|
||||
status: 'active'
|
||||
}
|
||||
],
|
||||
importanceModel: {
|
||||
modelId: 'imp_hectronic_v1',
|
||||
scoreRange: [0, 100],
|
||||
weights: {
|
||||
deadlineProximity: 28,
|
||||
procurementIntentStrength: 22,
|
||||
publicBuyerStrength: 18,
|
||||
productMatchStrength: 18,
|
||||
sourceTrust: 8,
|
||||
novelty: 6
|
||||
},
|
||||
thresholds: {
|
||||
infoMax: 29,
|
||||
reviewMin: 30,
|
||||
importantMin: 50,
|
||||
urgentMin: 70,
|
||||
criticalMin: 85
|
||||
},
|
||||
topNCaps: {
|
||||
dailyDigestMax: 10,
|
||||
weeklyDigestMax: 20,
|
||||
perTopicDailyMax: 6
|
||||
}
|
||||
},
|
||||
notificationPreset: {
|
||||
presetId: 'notif_hectronic_digest_v1',
|
||||
channels: ['email'],
|
||||
cadence: 'DAILY',
|
||||
timeLocal: '08:00',
|
||||
includeStatuses: ['relevant', 'needs_review'],
|
||||
maxItems: 10
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// MOCK DATA (Demo Hits for Hectronic)
|
||||
// ============================================
|
||||
|
||||
export const mockHectronicHits: B2BHit[] = [
|
||||
{
|
||||
id: 'hit-hec-001',
|
||||
tenantId: 'hectronic-demo',
|
||||
topicId: 'hec_parking_v1',
|
||||
sourceType: 'email',
|
||||
sourceRef: 'ga-msg-001',
|
||||
originalUrl: 'https://www.evergabe.de/auftraege/parkscheinautomaten-stadt-muenchen',
|
||||
canonicalUrl: 'https://evergabe.de/auftraege/parkscheinautomaten-muenchen',
|
||||
title: 'Stadt München: Lieferung und Installation von 150 Parkscheinautomaten',
|
||||
snippet: 'Die Landeshauptstadt München schreibt die Beschaffung von 150 modernen Parkscheinautomaten inkl. Backoffice-Software für das Stadtgebiet aus. Frist: 15.03.2026.',
|
||||
foundAt: new Date(Date.now() - 2 * 60 * 60 * 1000),
|
||||
language: 'de',
|
||||
countryGuess: 'Germany',
|
||||
buyerGuess: 'Landeshauptstadt München',
|
||||
deadlineGuess: '2026-03-15',
|
||||
importanceScore: 92,
|
||||
importanceLabel: 'KRITISCH',
|
||||
decisionLabel: 'relevant',
|
||||
decisionConfidence: 0.95,
|
||||
decisionTrace: {
|
||||
rulesTriggered: ['procurement_signal_match', 'public_buyer_match', 'product_match'],
|
||||
llmUsed: false,
|
||||
signals: {
|
||||
procurementSignalsFound: ['Ausschreibung', 'Beschaffung', 'Vergabe'],
|
||||
publicBuyerSignalsFound: ['Landeshauptstadt', 'Stadt München'],
|
||||
productSignalsFound: ['Parkscheinautomaten', 'Backoffice-Software'],
|
||||
negativesFound: []
|
||||
}
|
||||
},
|
||||
isRead: false,
|
||||
createdAt: new Date(Date.now() - 2 * 60 * 60 * 1000)
|
||||
},
|
||||
{
|
||||
id: 'hit-hec-002',
|
||||
tenantId: 'hectronic-demo',
|
||||
topicId: 'hec_ev_charging_v1',
|
||||
sourceType: 'rss',
|
||||
sourceRef: 'rss-item-002',
|
||||
originalUrl: 'https://ted.europa.eu/notice/2026-12345',
|
||||
canonicalUrl: 'https://ted.europa.eu/notice/2026-12345',
|
||||
title: 'EU Tender: Supply of EV Charging Infrastructure with Payment Terminals - City of Amsterdam',
|
||||
snippet: 'Contract notice for the supply, installation and maintenance of 200 public EV charging points with integrated contactless payment terminals. Deadline: 28.02.2026.',
|
||||
foundAt: new Date(Date.now() - 5 * 60 * 60 * 1000),
|
||||
language: 'en',
|
||||
countryGuess: 'Netherlands',
|
||||
buyerGuess: 'City of Amsterdam',
|
||||
deadlineGuess: '2026-02-28',
|
||||
importanceScore: 88,
|
||||
importanceLabel: 'KRITISCH',
|
||||
decisionLabel: 'relevant',
|
||||
decisionConfidence: 0.92,
|
||||
decisionTrace: {
|
||||
rulesTriggered: ['procurement_signal_match', 'public_buyer_match', 'product_match'],
|
||||
llmUsed: false,
|
||||
signals: {
|
||||
procurementSignalsFound: ['Contract notice', 'tender', 'supply'],
|
||||
publicBuyerSignalsFound: ['City of Amsterdam', 'public'],
|
||||
productSignalsFound: ['EV charging', 'charging points', 'payment terminals', 'contactless'],
|
||||
negativesFound: []
|
||||
}
|
||||
},
|
||||
isRead: false,
|
||||
createdAt: new Date(Date.now() - 5 * 60 * 60 * 1000)
|
||||
},
|
||||
{
|
||||
id: 'hit-hec-003',
|
||||
tenantId: 'hectronic-demo',
|
||||
topicId: 'hec_parking_v1',
|
||||
sourceType: 'email',
|
||||
sourceRef: 'ga-msg-003',
|
||||
originalUrl: 'https://vergabe.niedersachsen.de/NetServer/notice/8765432',
|
||||
canonicalUrl: 'https://vergabe.niedersachsen.de/notice/8765432',
|
||||
title: 'Gemeinde Seevetal: Erneuerung Parkraumbewirtschaftungssystem',
|
||||
snippet: 'Öffentliche Ausschreibung zur Erneuerung des Parkraumbewirtschaftungssystems mit 25 Pay-Stations und zentraler Management-Software.',
|
||||
foundAt: new Date(Date.now() - 8 * 60 * 60 * 1000),
|
||||
language: 'de',
|
||||
countryGuess: 'Germany',
|
||||
buyerGuess: 'Gemeinde Seevetal',
|
||||
deadlineGuess: '2026-04-01',
|
||||
importanceScore: 78,
|
||||
importanceLabel: 'DRINGEND',
|
||||
decisionLabel: 'relevant',
|
||||
decisionConfidence: 0.88,
|
||||
decisionTrace: {
|
||||
rulesTriggered: ['procurement_signal_match', 'public_buyer_match', 'product_match'],
|
||||
llmUsed: false,
|
||||
signals: {
|
||||
procurementSignalsFound: ['Öffentliche Ausschreibung', 'Erneuerung'],
|
||||
publicBuyerSignalsFound: ['Gemeinde'],
|
||||
productSignalsFound: ['Parkraumbewirtschaftungssystem', 'Pay-Stations', 'Management-Software'],
|
||||
negativesFound: []
|
||||
}
|
||||
},
|
||||
isRead: false,
|
||||
createdAt: new Date(Date.now() - 8 * 60 * 60 * 1000)
|
||||
},
|
||||
{
|
||||
id: 'hit-hec-004',
|
||||
tenantId: 'hectronic-demo',
|
||||
topicId: 'hec_parking_v1',
|
||||
sourceType: 'email',
|
||||
sourceRef: 'ga-msg-004',
|
||||
originalUrl: 'https://news.example.com/parking-industry-trends-2026',
|
||||
canonicalUrl: 'https://news.example.com/parking-trends-2026',
|
||||
title: 'Parking Industry Trends 2026: Smart Cities investieren in digitale Lösungen',
|
||||
snippet: 'Branchenanalyse zeigt wachsende Nachfrage nach vernetzten Parkscheinautomaten. Experten erwarten 15% Marktwachstum.',
|
||||
foundAt: new Date(Date.now() - 12 * 60 * 60 * 1000),
|
||||
language: 'de',
|
||||
countryGuess: 'Germany',
|
||||
buyerGuess: undefined,
|
||||
deadlineGuess: undefined,
|
||||
importanceScore: 15,
|
||||
importanceLabel: 'INFO',
|
||||
decisionLabel: 'irrelevant',
|
||||
decisionConfidence: 0.91,
|
||||
decisionTrace: {
|
||||
rulesTriggered: ['soft_exclude_triggered'],
|
||||
llmUsed: false,
|
||||
signals: {
|
||||
procurementSignalsFound: [],
|
||||
publicBuyerSignalsFound: [],
|
||||
productSignalsFound: ['Parkscheinautomaten'],
|
||||
negativesFound: ['Branchenanalyse', 'Experten', 'Marktwachstum']
|
||||
}
|
||||
},
|
||||
isRead: true,
|
||||
createdAt: new Date(Date.now() - 12 * 60 * 60 * 1000)
|
||||
},
|
||||
{
|
||||
id: 'hit-hec-005',
|
||||
tenantId: 'hectronic-demo',
|
||||
topicId: 'hec_ev_charging_v1',
|
||||
sourceType: 'rss',
|
||||
sourceRef: 'rss-item-005',
|
||||
originalUrl: 'https://ausschreibungen.at/ladeinfrastruktur-wien',
|
||||
canonicalUrl: 'https://ausschreibungen.at/ladeinfrastruktur-wien',
|
||||
title: 'Stadt Wien: Rahmenvertrag Ladeinfrastruktur öffentlicher Raum',
|
||||
snippet: 'Vergabeverfahren für Rahmenvertrag über Lieferung und Betrieb von Ladeinfrastruktur im öffentlichen Raum. Details hinter Login.',
|
||||
foundAt: new Date(Date.now() - 18 * 60 * 60 * 1000),
|
||||
language: 'de',
|
||||
countryGuess: 'Austria',
|
||||
buyerGuess: 'Stadt Wien',
|
||||
deadlineGuess: undefined,
|
||||
importanceScore: 55,
|
||||
importanceLabel: 'WICHTIG',
|
||||
decisionLabel: 'needs_review',
|
||||
decisionConfidence: 0.62,
|
||||
decisionTrace: {
|
||||
rulesTriggered: ['procurement_signal_match', 'public_buyer_match'],
|
||||
llmUsed: true,
|
||||
llmConfidence: 0.62,
|
||||
signals: {
|
||||
procurementSignalsFound: ['Vergabeverfahren', 'Rahmenvertrag'],
|
||||
publicBuyerSignalsFound: ['Stadt Wien', 'öffentlicher Raum'],
|
||||
productSignalsFound: ['Ladeinfrastruktur'],
|
||||
negativesFound: ['Details hinter Login']
|
||||
}
|
||||
},
|
||||
isRead: false,
|
||||
createdAt: new Date(Date.now() - 18 * 60 * 60 * 1000)
|
||||
},
|
||||
{
|
||||
id: 'hit-hec-006',
|
||||
tenantId: 'hectronic-demo',
|
||||
topicId: 'hec_fuel_v1',
|
||||
sourceType: 'email',
|
||||
sourceRef: 'ga-msg-006',
|
||||
originalUrl: 'https://vergabe.bund.de/tankautomat-bundeswehr',
|
||||
canonicalUrl: 'https://vergabe.bund.de/tankautomat-bw',
|
||||
title: 'Bundesamt für Ausrüstung: Tankautomaten für Liegenschaften',
|
||||
snippet: 'Beschaffung von unbemannten Tankautomaten für Flottenbetankung an 12 Bundeswehr-Standorten. EU-weite Ausschreibung.',
|
||||
foundAt: new Date(Date.now() - 24 * 60 * 60 * 1000),
|
||||
language: 'de',
|
||||
countryGuess: 'Germany',
|
||||
buyerGuess: 'Bundesamt für Ausrüstung',
|
||||
deadlineGuess: '2026-05-15',
|
||||
importanceScore: 72,
|
||||
importanceLabel: 'DRINGEND',
|
||||
decisionLabel: 'relevant',
|
||||
decisionConfidence: 0.86,
|
||||
decisionTrace: {
|
||||
rulesTriggered: ['procurement_signal_match', 'public_buyer_match', 'product_match'],
|
||||
llmUsed: false,
|
||||
signals: {
|
||||
procurementSignalsFound: ['Beschaffung', 'EU-weite Ausschreibung'],
|
||||
publicBuyerSignalsFound: ['Bundesamt'],
|
||||
productSignalsFound: ['Tankautomaten', 'Flottenbetankung', 'unbemannt'],
|
||||
negativesFound: []
|
||||
}
|
||||
},
|
||||
isRead: true,
|
||||
createdAt: new Date(Date.now() - 24 * 60 * 60 * 1000)
|
||||
}
|
||||
]
|
||||
|
||||
// ============================================
|
||||
// DEFAULT SETTINGS
|
||||
// ============================================
|
||||
|
||||
export const defaultB2BSettings: B2BSettings = {
|
||||
migrationCompleted: false,
|
||||
wizardCompleted: false,
|
||||
selectedRegions: ['EUROPE'],
|
||||
selectedLanguages: ['de', 'en'],
|
||||
selectedPackages: ['PARKING', 'EV_CHARGING'],
|
||||
noiseMode: 'STRICT',
|
||||
notificationCadence: 'DAILY',
|
||||
notificationTime: '08:00',
|
||||
maxDigestItems: 10
|
||||
}
|
||||
|
||||
export const defaultTenant: B2BTenant = {
|
||||
id: 'demo-tenant',
|
||||
name: 'Demo Account',
|
||||
companyName: 'Meine Firma GmbH',
|
||||
industry: 'Payment & Infrastructure',
|
||||
plan: 'trial',
|
||||
inboundEmailDomain: 'alerts.breakpilot.de',
|
||||
createdAt: new Date(),
|
||||
settings: defaultB2BSettings
|
||||
}
|
||||
230
studio-v2/lib/alerts-b2b/types.ts
Normal file
230
studio-v2/lib/alerts-b2b/types.ts
Normal file
@@ -0,0 +1,230 @@
|
||||
// ============================================
|
||||
// TYPES - B2B Alerts Domain Types
|
||||
// ============================================
|
||||
|
||||
export type ImportanceLabel = 'KRITISCH' | 'DRINGEND' | 'WICHTIG' | 'PRUEFEN' | 'INFO'
|
||||
export type DecisionLabel = 'relevant' | 'irrelevant' | 'needs_review'
|
||||
export type SourceType = 'email' | 'rss'
|
||||
export type NoiseMode = 'STRICT' | 'BALANCED' | 'BROAD'
|
||||
export type Package = 'PARKING' | 'EV_CHARGING' | 'FUEL' | 'TANK_MONITORING'
|
||||
|
||||
export interface AlertSource {
|
||||
id: string
|
||||
tenantId: string
|
||||
type: SourceType
|
||||
inboundAddress?: string
|
||||
rssUrl?: string
|
||||
label: string
|
||||
active: boolean
|
||||
createdAt: Date
|
||||
}
|
||||
|
||||
export interface B2BHit {
|
||||
id: string
|
||||
tenantId: string
|
||||
topicId: string
|
||||
sourceType: SourceType
|
||||
sourceRef: string
|
||||
originalUrl: string
|
||||
canonicalUrl: string
|
||||
title: string
|
||||
snippet: string
|
||||
fullText?: string
|
||||
foundAt: Date
|
||||
language?: string
|
||||
countryGuess?: string
|
||||
buyerGuess?: string
|
||||
deadlineGuess?: string
|
||||
importanceScore: number
|
||||
importanceLabel: ImportanceLabel
|
||||
decisionLabel: DecisionLabel
|
||||
decisionConfidence: number
|
||||
decisionTrace?: DecisionTrace
|
||||
isRead: boolean
|
||||
userFeedback?: 'relevant' | 'irrelevant'
|
||||
createdAt: Date
|
||||
}
|
||||
|
||||
export interface DecisionTrace {
|
||||
rulesTriggered: string[]
|
||||
llmUsed: boolean
|
||||
llmConfidence?: number
|
||||
signals: {
|
||||
procurementSignalsFound: string[]
|
||||
publicBuyerSignalsFound: string[]
|
||||
productSignalsFound: string[]
|
||||
negativesFound: string[]
|
||||
}
|
||||
}
|
||||
|
||||
export interface B2BTopic {
|
||||
id: string
|
||||
tenantId: string
|
||||
name: string
|
||||
package: Package
|
||||
intent: TopicIntent
|
||||
filters: TopicFilters
|
||||
decisionPolicy: DecisionPolicy
|
||||
importanceModelId: string
|
||||
status: 'active' | 'paused'
|
||||
createdAt: Date
|
||||
}
|
||||
|
||||
export interface TopicIntent {
|
||||
goalStatement: string
|
||||
mustHaveAnyN: number
|
||||
mustHave: { type: 'keyword'; value: string }[]
|
||||
publicBuyerSignalsAny: string[]
|
||||
procurementSignalsAny: string[]
|
||||
}
|
||||
|
||||
export interface TopicFilters {
|
||||
hardExcludesAny: string[]
|
||||
softExcludesAny: string[]
|
||||
}
|
||||
|
||||
export interface DecisionPolicy {
|
||||
rulesFirst: boolean
|
||||
llmMode: 'ALWAYS' | 'GRAYZONE_ONLY' | 'NEVER'
|
||||
autoRelevantMinConf: number
|
||||
needsReviewRange: [number, number]
|
||||
autoIrrelevantMaxConf: number
|
||||
}
|
||||
|
||||
export interface ImportanceModel {
|
||||
modelId: string
|
||||
scoreRange: [number, number]
|
||||
weights: {
|
||||
deadlineProximity: number
|
||||
procurementIntentStrength: number
|
||||
publicBuyerStrength: number
|
||||
productMatchStrength: number
|
||||
sourceTrust: number
|
||||
novelty: number
|
||||
}
|
||||
thresholds: {
|
||||
infoMax: number
|
||||
reviewMin: number
|
||||
importantMin: number
|
||||
urgentMin: number
|
||||
criticalMin: number
|
||||
}
|
||||
topNCaps: {
|
||||
dailyDigestMax: number
|
||||
weeklyDigestMax: number
|
||||
perTopicDailyMax: number
|
||||
}
|
||||
}
|
||||
|
||||
export interface B2BTemplate {
|
||||
templateId: string
|
||||
templateName: string
|
||||
templateDescription: string
|
||||
targetRoles: string[]
|
||||
industry?: string
|
||||
companyExample?: string
|
||||
topics: Omit<B2BTopic, 'id' | 'tenantId' | 'createdAt'>[]
|
||||
importanceModel: ImportanceModel
|
||||
guidedConfig: GuidedConfig
|
||||
notificationPreset: NotificationPreset
|
||||
}
|
||||
|
||||
export interface GuidedConfig {
|
||||
regionSelector: {
|
||||
type: 'multi_select'
|
||||
default: string[]
|
||||
options: string[]
|
||||
}
|
||||
languageSelector: {
|
||||
type: 'multi_select'
|
||||
default: string[]
|
||||
options: string[]
|
||||
}
|
||||
packageSelector: {
|
||||
type: 'multi_select'
|
||||
default: Package[]
|
||||
options: Package[]
|
||||
}
|
||||
noiseMode: {
|
||||
type: 'single_select'
|
||||
default: NoiseMode
|
||||
options: NoiseMode[]
|
||||
}
|
||||
}
|
||||
|
||||
export interface NotificationPreset {
|
||||
presetId: string
|
||||
channels: string[]
|
||||
cadence: 'REALTIME' | 'HOURLY' | 'DAILY' | 'WEEKLY'
|
||||
timeLocal: string
|
||||
includeStatuses: DecisionLabel[]
|
||||
maxItems: number
|
||||
}
|
||||
|
||||
export interface B2BTenant {
|
||||
id: string
|
||||
name: string
|
||||
companyName: string
|
||||
industry: string
|
||||
plan: 'trial' | 'starter' | 'professional' | 'enterprise'
|
||||
inboundEmailDomain: string
|
||||
createdAt: Date
|
||||
settings: B2BSettings
|
||||
}
|
||||
|
||||
export interface B2BSettings {
|
||||
migrationCompleted: boolean
|
||||
wizardCompleted: boolean
|
||||
selectedTemplateId?: string
|
||||
selectedRegions: string[]
|
||||
selectedLanguages: string[]
|
||||
selectedPackages: Package[]
|
||||
noiseMode: NoiseMode
|
||||
notificationCadence: 'REALTIME' | 'HOURLY' | 'DAILY' | 'WEEKLY'
|
||||
notificationTime: string
|
||||
maxDigestItems: number
|
||||
}
|
||||
|
||||
export interface AlertsB2BContextType {
|
||||
// Tenant
|
||||
tenant: B2BTenant
|
||||
updateTenant: (updates: Partial<B2BTenant>) => void
|
||||
|
||||
// Settings
|
||||
settings: B2BSettings
|
||||
updateSettings: (updates: Partial<B2BSettings>) => void
|
||||
|
||||
// Templates
|
||||
availableTemplates: B2BTemplate[]
|
||||
selectedTemplate: B2BTemplate | null
|
||||
selectTemplate: (templateId: string) => void
|
||||
|
||||
// Sources
|
||||
sources: AlertSource[]
|
||||
addSource: (source: Omit<AlertSource, 'id' | 'createdAt'>) => void
|
||||
removeSource: (id: string) => void
|
||||
generateInboundEmail: () => string
|
||||
|
||||
// Topics
|
||||
topics: B2BTopic[]
|
||||
addTopic: (topic: Omit<B2BTopic, 'id' | 'createdAt'>) => void
|
||||
updateTopic: (id: string, updates: Partial<B2BTopic>) => void
|
||||
removeTopic: (id: string) => void
|
||||
|
||||
// Hits
|
||||
hits: B2BHit[]
|
||||
unreadCount: number
|
||||
relevantCount: number
|
||||
needsReviewCount: number
|
||||
markAsRead: (id: string) => void
|
||||
markAllAsRead: () => void
|
||||
submitFeedback: (id: string, feedback: 'relevant' | 'irrelevant') => void
|
||||
processEmailContent: (emailContent: string, emailSubject?: string) => B2BHit
|
||||
|
||||
// Digest
|
||||
getDigest: (maxItems?: number) => B2BHit[]
|
||||
|
||||
// Loading/Error
|
||||
isLoading: boolean
|
||||
error: string | null
|
||||
}
|
||||
Reference in New Issue
Block a user