[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:
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
|
||||
}
|
||||
Reference in New Issue
Block a user