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>
150 lines
5.3 KiB
TypeScript
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
|
|
}
|