Files
breakpilot-lehrer/studio-v2/lib/alerts-b2b/actions.ts
Benjamin Admin 0b37c5e692 [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>
2026-04-24 17:52:36 +02:00

150 lines
5.3 KiB
TypeScript

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
}