Files
breakpilot-lehrer/studio-v2/app/alerts-b2b/page.tsx
Benjamin Boenisch 5a31f52310 Initial commit: breakpilot-lehrer - Lehrer KI Platform
Services: Admin-Lehrer, Backend-Lehrer, Studio v2, Website,
Klausur-Service, School-Service, Voice-Service, Geo-Service,
BreakPilot Drive, Agent-Core

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 23:47:26 +01:00

1020 lines
44 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client'
import { useState, useEffect } from 'react'
import { useRouter } from 'next/navigation'
import { useTheme } from '@/lib/ThemeContext'
import { useLanguage } from '@/lib/LanguageContext'
import {
useAlertsB2B,
B2BHit,
B2BTopic,
ImportanceLabel,
DecisionLabel,
getImportanceLabelColor,
getDecisionLabelColor,
formatDeadline,
getPackageIcon,
getPackageLabel
} from '@/lib/AlertsB2BContext'
import { Sidebar } from '@/components/Sidebar'
import { B2BMigrationWizard } from '@/components/B2BMigrationWizard'
import { TipBox } from '@/components/InfoBox'
import { Footer } from '@/components/Footer'
import { LanguageDropdown } from '@/components/LanguageDropdown'
import { ThemeToggle } from '@/components/ThemeToggle'
// Decision Trace Modal
function DecisionTraceModal({
hit,
onClose
}: {
hit: B2BHit
onClose: () => void
}) {
const { isDark } = useTheme()
const trace = hit.decisionTrace
return (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
<div className="absolute inset-0 bg-black/50 backdrop-blur-sm" onClick={onClose} />
<div className={`relative w-full max-w-xl rounded-3xl border p-6 max-h-[90vh] overflow-y-auto ${
isDark ? 'bg-slate-900 border-white/20' : 'bg-white border-slate-200 shadow-2xl'
}`}>
<div className="flex items-center justify-between mb-4">
<h3 className={`text-lg font-semibold ${isDark ? 'text-white' : 'text-slate-900'}`}>
Decision Trace
</h3>
<button onClick={onClose} className={`p-2 rounded-lg ${isDark ? 'hover:bg-white/10' : 'hover:bg-slate-100'}`}>
<svg className={`w-5 h-5 ${isDark ? 'text-white/60' : 'text-slate-400'}`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
{trace ? (
<div className="space-y-4">
{/* Rules Triggered */}
<div className={`p-4 rounded-xl ${isDark ? 'bg-white/5' : 'bg-slate-50'}`}>
<h4 className={`text-sm font-medium mb-2 ${isDark ? 'text-white/80' : 'text-slate-700'}`}>
Regeln ausgeloest
</h4>
<div className="flex flex-wrap gap-2">
{trace.rulesTriggered.map((rule, idx) => (
<span key={idx} className={`px-2 py-1 rounded-lg text-xs ${
isDark ? 'bg-blue-500/20 text-blue-300' : 'bg-blue-100 text-blue-700'
}`}>
{rule}
</span>
))}
</div>
</div>
{/* LLM Used */}
<div className={`p-4 rounded-xl ${isDark ? 'bg-white/5' : 'bg-slate-50'}`}>
<div className="flex items-center justify-between">
<span className={`text-sm ${isDark ? 'text-white/60' : 'text-slate-500'}`}>LLM verwendet</span>
<span className={`px-2 py-1 rounded-lg text-xs ${
trace.llmUsed
? isDark ? 'bg-purple-500/20 text-purple-300' : 'bg-purple-100 text-purple-700'
: isDark ? 'bg-slate-500/20 text-slate-300' : 'bg-slate-100 text-slate-500'
}`}>
{trace.llmUsed ? `Ja (${Math.round((trace.llmConfidence || 0) * 100)}% Konfidenz)` : 'Nein'}
</span>
</div>
</div>
{/* Signals */}
<div className="space-y-3">
{/* Procurement Signals */}
{trace.signals.procurementSignalsFound.length > 0 && (
<div className={`p-4 rounded-xl ${isDark ? 'bg-green-500/10' : 'bg-green-50'}`}>
<h4 className={`text-sm font-medium mb-2 ${isDark ? 'text-green-300' : 'text-green-700'}`}>
Beschaffungs-Signale
</h4>
<p className={`text-sm ${isDark ? 'text-green-200/80' : 'text-green-600'}`}>
{trace.signals.procurementSignalsFound.join(', ')}
</p>
</div>
)}
{/* Public Buyer Signals */}
{trace.signals.publicBuyerSignalsFound.length > 0 && (
<div className={`p-4 rounded-xl ${isDark ? 'bg-blue-500/10' : 'bg-blue-50'}`}>
<h4 className={`text-sm font-medium mb-2 ${isDark ? 'text-blue-300' : 'text-blue-700'}`}>
Oeffentliche Auftraggeber
</h4>
<p className={`text-sm ${isDark ? 'text-blue-200/80' : 'text-blue-600'}`}>
{trace.signals.publicBuyerSignalsFound.join(', ')}
</p>
</div>
)}
{/* Product Signals */}
{trace.signals.productSignalsFound.length > 0 && (
<div className={`p-4 rounded-xl ${isDark ? 'bg-amber-500/10' : 'bg-amber-50'}`}>
<h4 className={`text-sm font-medium mb-2 ${isDark ? 'text-amber-300' : 'text-amber-700'}`}>
Produkt-Signale
</h4>
<p className={`text-sm ${isDark ? 'text-amber-200/80' : 'text-amber-600'}`}>
{trace.signals.productSignalsFound.join(', ')}
</p>
</div>
)}
{/* Negatives */}
{trace.signals.negativesFound.length > 0 && (
<div className={`p-4 rounded-xl ${isDark ? 'bg-red-500/10' : 'bg-red-50'}`}>
<h4 className={`text-sm font-medium mb-2 ${isDark ? 'text-red-300' : 'text-red-700'}`}>
Negative Signale
</h4>
<p className={`text-sm ${isDark ? 'text-red-200/80' : 'text-red-600'}`}>
{trace.signals.negativesFound.join(', ')}
</p>
</div>
)}
</div>
</div>
) : (
<p className={`text-sm ${isDark ? 'text-white/40' : 'text-slate-400'}`}>
Kein Decision Trace verfuegbar.
</p>
)}
</div>
</div>
)
}
// Email Import Modal
function EmailImportModal({
onClose,
onImport
}: {
onClose: () => void
onImport: (content: string, subject?: string) => void
}) {
const { isDark } = useTheme()
const [emailSubject, setEmailSubject] = useState('')
const [emailContent, setEmailContent] = useState('')
const [isProcessing, setIsProcessing] = useState(false)
const handleImport = async () => {
if (!emailContent.trim()) return
setIsProcessing(true)
// Simulate processing delay
await new Promise(resolve => setTimeout(resolve, 800))
onImport(emailContent, emailSubject || undefined)
setIsProcessing(false)
onClose()
}
return (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
<div className="absolute inset-0 bg-black/50 backdrop-blur-sm" onClick={onClose} />
<div className={`relative w-full max-w-2xl rounded-3xl border p-6 max-h-[90vh] overflow-y-auto ${
isDark ? 'bg-slate-900 border-white/20' : 'bg-white border-slate-200 shadow-2xl'
}`}>
<div className="flex items-center justify-between mb-6">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-xl bg-gradient-to-br from-purple-500 to-pink-500 flex items-center justify-center text-xl">
📧
</div>
<div>
<h3 className={`text-lg font-semibold ${isDark ? 'text-white' : 'text-slate-900'}`}>
E-Mail manuell einfuegen
</h3>
<p className={`text-sm ${isDark ? 'text-white/60' : 'text-slate-500'}`}>
Fuegen Sie den Inhalt einer Google Alert E-Mail ein
</p>
</div>
</div>
<button onClick={onClose} className={`p-2 rounded-lg ${isDark ? 'hover:bg-white/10' : 'hover:bg-slate-100'}`}>
<svg className={`w-5 h-5 ${isDark ? 'text-white/60' : 'text-slate-400'}`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<div className="space-y-4">
{/* Subject (optional) */}
<div>
<label className={`block text-sm font-medium mb-2 ${isDark ? 'text-white/80' : 'text-slate-700'}`}>
Betreff (optional)
</label>
<input
type="text"
placeholder="z.B. Google Alert - Parkscheinautomaten"
value={emailSubject}
onChange={(e) => setEmailSubject(e.target.value)}
className={`w-full px-4 py-3 rounded-xl border ${
isDark
? 'bg-white/10 border-white/20 text-white placeholder-white/40'
: 'bg-white border-slate-200 text-slate-900 placeholder-slate-400'
}`}
/>
</div>
{/* Email Content */}
<div>
<label className={`block text-sm font-medium mb-2 ${isDark ? 'text-white/80' : 'text-slate-700'}`}>
E-Mail-Inhalt *
</label>
<textarea
placeholder="Fuegen Sie hier den kompletten E-Mail-Inhalt ein...
Beispiel:
Google Alerts
Parkscheinautomaten
Tagesaktuell | 23. Januar 2026
Stadt Muenchen schreibt neue Parkscheinautomaten aus
www.muenchen.de - Die Landeshauptstadt Muenchen schreibt die Beschaffung von 150 neuen Parkscheinautomaten fuer das Stadtgebiet aus. Die Submission endet am 15.02.2026..."
value={emailContent}
onChange={(e) => setEmailContent(e.target.value)}
rows={12}
className={`w-full px-4 py-3 rounded-xl border resize-none font-mono text-sm ${
isDark
? 'bg-white/10 border-white/20 text-white placeholder-white/30'
: 'bg-white border-slate-200 text-slate-900 placeholder-slate-400'
}`}
/>
<p className={`text-xs mt-2 ${isDark ? 'text-white/40' : 'text-slate-400'}`}>
Kopieren Sie den gesamten E-Mail-Text inkl. Links aus Ihrer Google Alert E-Mail
</p>
</div>
{/* Info Box */}
<div className={`p-4 rounded-xl ${isDark ? 'bg-purple-500/10 border border-purple-500/30' : 'bg-purple-50 border border-purple-200'}`}>
<div className="flex items-start gap-3">
<span className="text-xl">🤖</span>
<div>
<p className={`text-sm font-medium ${isDark ? 'text-purple-300' : 'text-purple-700'}`}>
KI-Verarbeitung
</p>
<p className={`text-sm ${isDark ? 'text-purple-200/70' : 'text-purple-600'}`}>
Die KI analysiert den Inhalt, erkennt Beschaffungs-Signale, identifiziert
potenzielle Auftraggeber und bewertet die Relevanz fuer Ihr Unternehmen.
</p>
</div>
</div>
</div>
</div>
{/* Actions */}
<div className="flex items-center justify-end gap-3 mt-6 pt-4 border-t border-white/10">
<button
onClick={onClose}
className={`px-4 py-2 rounded-xl font-medium ${
isDark
? 'text-white/60 hover:text-white hover:bg-white/10'
: 'text-slate-500 hover:text-slate-900 hover:bg-slate-100'
}`}
>
Abbrechen
</button>
<button
onClick={handleImport}
disabled={!emailContent.trim() || isProcessing}
className={`px-6 py-2 rounded-xl font-medium transition-all flex items-center gap-2 ${
emailContent.trim() && !isProcessing
? 'bg-gradient-to-r from-purple-500 to-pink-500 text-white hover:shadow-lg hover:shadow-purple-500/30'
: isDark
? 'bg-white/10 text-white/30 cursor-not-allowed'
: 'bg-slate-200 text-slate-400 cursor-not-allowed'
}`}
>
{isProcessing ? (
<>
<svg className="w-4 h-4 animate-spin" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
Verarbeite...
</>
) : (
<>
<span>🔍</span>
Analysieren & Importieren
</>
)}
</button>
</div>
</div>
</div>
)
}
// Hit Detail Modal
function HitDetailModal({
hit,
onClose,
onFeedback
}: {
hit: B2BHit
onClose: () => void
onFeedback: (feedback: 'relevant' | 'irrelevant') => void
}) {
const { isDark } = useTheme()
const [showTrace, setShowTrace] = useState(false)
return (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
<div className="absolute inset-0 bg-black/50 backdrop-blur-sm" onClick={onClose} />
<div className={`relative w-full max-w-2xl rounded-3xl border p-8 max-h-[90vh] overflow-y-auto ${
isDark ? 'bg-slate-900 border-white/20' : 'bg-white border-slate-200 shadow-2xl'
}`}>
{/* Header */}
<div className="flex items-start justify-between mb-4">
<div className="flex items-center gap-3 flex-wrap">
<span className={`px-3 py-1 rounded-full text-xs font-medium border ${getImportanceLabelColor(hit.importanceLabel, isDark)}`}>
{hit.importanceLabel}
</span>
<span className={`px-2 py-1 rounded-lg text-xs ${getDecisionLabelColor(hit.decisionLabel, isDark)}`}>
{hit.decisionLabel === 'relevant' ? 'Relevant' : hit.decisionLabel === 'needs_review' ? 'Pruefung noetig' : 'Irrelevant'}
</span>
<span className={`text-xs ${isDark ? 'text-white/40' : 'text-slate-400'}`}>
Score: {hit.importanceScore}
</span>
</div>
<button onClick={onClose} className={`p-2 rounded-lg ${isDark ? 'hover:bg-white/10' : 'hover:bg-slate-100'}`}>
<svg className={`w-5 h-5 ${isDark ? 'text-white/60' : 'text-slate-400'}`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
{/* Title */}
<h2 className={`text-xl font-bold mb-4 ${isDark ? 'text-white' : 'text-slate-900'}`}>
{hit.title}
</h2>
{/* Metadata */}
<div className={`flex flex-wrap gap-4 mb-4 text-sm ${isDark ? 'text-white/60' : 'text-slate-500'}`}>
{hit.buyerGuess && (
<span className="flex items-center gap-1">
<span>🏛</span> {hit.buyerGuess}
</span>
)}
{hit.countryGuess && (
<span className="flex items-center gap-1">
<span>🌍</span> {hit.countryGuess}
</span>
)}
{hit.deadlineGuess && (
<span className={`flex items-center gap-1 ${
formatDeadline(hit.deadlineGuess).includes('Heute') || formatDeadline(hit.deadlineGuess).includes('Morgen')
? 'text-red-500 font-medium'
: ''
}`}>
<span>📅</span> Frist: {formatDeadline(hit.deadlineGuess)}
</span>
)}
</div>
{/* Snippet */}
<div className={`rounded-xl p-4 mb-6 ${isDark ? 'bg-white/5' : 'bg-slate-50'}`}>
<p className={isDark ? 'text-white/80' : 'text-slate-600'}>
{hit.snippet}
</p>
</div>
{/* Source Info */}
<div className={`flex items-center gap-4 mb-6 text-sm ${isDark ? 'text-white/40' : 'text-slate-400'}`}>
<span className={`px-2 py-1 rounded ${isDark ? 'bg-white/10' : 'bg-slate-100'}`}>
{hit.sourceType === 'email' ? '📧 Email' : '📡 RSS'}
</span>
<a
href={hit.originalUrl}
target="_blank"
rel="noopener noreferrer"
className={`hover:underline ${isDark ? 'text-blue-400' : 'text-blue-600'}`}
>
Original ansehen
</a>
</div>
{/* Actions */}
<div className="flex items-center justify-between pt-4 border-t border-white/10">
{/* Feedback */}
<div className="flex items-center gap-2">
<span className={`text-sm ${isDark ? 'text-white/60' : 'text-slate-500'}`}>War das relevant?</span>
<button
onClick={() => onFeedback('relevant')}
className={`p-2 rounded-lg transition-all ${
hit.userFeedback === 'relevant'
? 'bg-green-500/20 text-green-400'
: isDark ? 'hover:bg-white/10 text-white/40' : 'hover:bg-slate-100 text-slate-400'
}`}
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M14 10h4.764a2 2 0 011.789 2.894l-3.5 7A2 2 0 0115.263 21h-4.017c-.163 0-.326-.02-.485-.06L7 20m7-10V5a2 2 0 00-2-2h-.095c-.5 0-.905.405-.905.905 0 .714-.211 1.412-.608 2.006L7 11v9m7-10h-2M7 20H5a2 2 0 01-2-2v-6a2 2 0 012-2h2.5" />
</svg>
</button>
<button
onClick={() => onFeedback('irrelevant')}
className={`p-2 rounded-lg transition-all ${
hit.userFeedback === 'irrelevant'
? 'bg-red-500/20 text-red-400'
: isDark ? 'hover:bg-white/10 text-white/40' : 'hover:bg-slate-100 text-slate-400'
}`}
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 14H5.236a2 2 0 01-1.789-2.894l3.5-7A2 2 0 018.736 3h4.018a2 2 0 01.485.06l3.76.94m-7 10v5a2 2 0 002 2h.096c.5 0 .905-.405.905-.904 0-.715.211-1.413.608-2.008L17 13V4m-7 10h2m5-10h2a2 2 0 012 2v6a2 2 0 01-2 2h-2.5" />
</svg>
</button>
</div>
{/* Decision Trace */}
<button
onClick={() => setShowTrace(true)}
className={`px-4 py-2 rounded-lg text-sm transition-all ${
isDark
? 'bg-white/10 text-white/80 hover:bg-white/20'
: 'bg-slate-100 text-slate-700 hover:bg-slate-200'
}`}
>
🔍 Decision Trace
</button>
</div>
</div>
{/* Decision Trace Modal */}
{showTrace && (
<DecisionTraceModal hit={hit} onClose={() => setShowTrace(false)} />
)}
</div>
)
}
// Hit Card Component
function HitCard({
hit,
onClick
}: {
hit: B2BHit
onClick: () => void
}) {
const { isDark } = useTheme()
return (
<button
onClick={onClick}
className={`w-full text-left p-4 rounded-xl transition-all group ${
isDark
? `bg-white/5 hover:bg-white/10 ${!hit.isRead ? 'border-l-4 border-amber-500' : ''}`
: `bg-slate-50 hover:bg-slate-100 ${!hit.isRead ? 'border-l-4 border-amber-500' : ''}`
}`}
>
<div className="flex items-start gap-3">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1 flex-wrap">
<span className={`px-2 py-0.5 rounded-full text-xs font-medium border ${getImportanceLabelColor(hit.importanceLabel, isDark)}`}>
{hit.importanceLabel}
</span>
<span className={`px-2 py-0.5 rounded-lg text-xs ${getDecisionLabelColor(hit.decisionLabel, isDark)}`}>
{hit.decisionLabel === 'relevant' ? 'Relevant' : hit.decisionLabel === 'needs_review' ? 'Pruefung' : 'Info'}
</span>
{hit.deadlineGuess && (
<span className={`text-xs ${
formatDeadline(hit.deadlineGuess).includes('Heute') || formatDeadline(hit.deadlineGuess).includes('Morgen')
? 'text-red-500 font-medium'
: isDark ? 'text-white/40' : 'text-slate-400'
}`}>
📅 {formatDeadline(hit.deadlineGuess)}
</span>
)}
{!hit.isRead && (
<span className="w-2 h-2 rounded-full bg-amber-500" />
)}
</div>
<h3 className={`font-medium text-sm mb-1 ${isDark ? 'text-white' : 'text-slate-900'}`}>
{hit.title}
</h3>
<p className={`text-xs truncate ${isDark ? 'text-white/60' : 'text-slate-500'}`}>
{hit.snippet}
</p>
<div className={`flex items-center gap-3 mt-2 text-xs ${isDark ? 'text-white/40' : 'text-slate-400'}`}>
{hit.buyerGuess && <span>🏛 {hit.buyerGuess}</span>}
{hit.countryGuess && <span>🌍 {hit.countryGuess}</span>}
</div>
</div>
<svg className={`w-5 h-5 flex-shrink-0 transition-colors ${
isDark ? 'text-white/30 group-hover:text-white/60' : 'text-slate-300 group-hover:text-slate-600'
}`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
</div>
</button>
)
}
// Digest View Component
function DigestView({ hits, onHitClick }: { hits: B2BHit[], onHitClick: (hit: B2BHit) => void }) {
const { isDark } = useTheme()
return (
<div className="space-y-4">
<div className="flex items-center gap-2 mb-4">
<span className="text-2xl">📬</span>
<h3 className={`font-semibold ${isDark ? 'text-white' : 'text-slate-900'}`}>
Tages-Digest (Top 10)
</h3>
</div>
{hits.length === 0 ? (
<div className={`text-center py-8 ${isDark ? 'text-white/40' : 'text-slate-400'}`}>
<span className="text-4xl block mb-2">🎉</span>
<p>Keine relevanten Hits heute</p>
</div>
) : (
<div className="space-y-3">
{hits.map((hit, idx) => (
<div key={hit.id} className="flex items-start gap-3">
<span className={`w-6 h-6 rounded-full flex items-center justify-center text-xs font-medium ${
idx < 3
? 'bg-amber-500/20 text-amber-400'
: isDark ? 'bg-white/10 text-white/60' : 'bg-slate-100 text-slate-500'
}`}>
{idx + 1}
</span>
<div className="flex-1">
<HitCard hit={hit} onClick={() => onHitClick(hit)} />
</div>
</div>
))}
</div>
)}
</div>
)
}
export default function AlertsB2BPage() {
const router = useRouter()
const { isDark } = useTheme()
const { t } = useLanguage()
const {
tenant,
settings,
updateSettings,
hits,
topics,
unreadCount,
relevantCount,
needsReviewCount,
markAsRead,
markAllAsRead,
submitFeedback,
processEmailContent,
getDigest
} = useAlertsB2B()
const [selectedHit, setSelectedHit] = useState<B2BHit | null>(null)
const [showWizard, setShowWizard] = useState(false)
const [showImportModal, setShowImportModal] = useState(false)
const [viewMode, setViewMode] = useState<'inbox' | 'digest'>('inbox')
const [filterDecision, setFilterDecision] = useState<string>('all')
const [filterImportance, setFilterImportance] = useState<string>('all')
const [importSuccess, setImportSuccess] = useState<B2BHit | null>(null)
// Show wizard if not completed
useEffect(() => {
if (!settings.wizardCompleted) {
setShowWizard(true)
}
}, [settings.wizardCompleted])
// Filter hits
const filteredHits = hits.filter(hit => {
if (filterDecision === 'unread') return !hit.isRead
if (filterDecision !== 'all' && hit.decisionLabel !== filterDecision) return false
if (filterImportance !== 'all' && hit.importanceLabel !== filterImportance) return false
return true
}).sort((a, b) => b.importanceScore - a.importanceScore)
// Handle hit click
const handleHitClick = (hit: B2BHit) => {
setSelectedHit(hit)
if (!hit.isRead) {
markAsRead(hit.id)
}
}
// Handle feedback
const handleFeedback = (hitId: string, feedback: 'relevant' | 'irrelevant') => {
submitFeedback(hitId, feedback)
}
// Wizard mode
if (showWizard) {
return (
<B2BMigrationWizard
onComplete={() => {
updateSettings({ wizardCompleted: true, migrationCompleted: true })
setShowWizard(false)
}}
onSkip={() => {
updateSettings({ wizardCompleted: true })
setShowWizard(false)
}}
onCancel={() => {
router.push('/')
}}
/>
)
}
return (
<div className={`min-h-screen relative overflow-hidden flex flex-col ${
isDark
? 'bg-gradient-to-br from-indigo-900 via-purple-900 to-pink-800'
: 'bg-gradient-to-br from-slate-100 via-blue-50 to-indigo-100'
}`}>
{/* Animated Background Blobs - Dashboard Style */}
<div className="absolute inset-0 overflow-hidden pointer-events-none">
<div className={`absolute -top-40 -right-40 w-80 h-80 rounded-full mix-blend-multiply filter blur-3xl animate-blob ${
isDark ? 'bg-purple-500 opacity-70' : 'bg-purple-300 opacity-50'
}`} />
<div className={`absolute -bottom-40 -left-40 w-80 h-80 rounded-full mix-blend-multiply filter blur-3xl animate-blob animation-delay-2000 ${
isDark ? 'bg-blue-500 opacity-70' : 'bg-blue-300 opacity-50'
}`} />
<div className={`absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 w-80 h-80 rounded-full mix-blend-multiply filter blur-3xl animate-blob animation-delay-4000 ${
isDark ? 'bg-pink-500 opacity-70' : 'bg-pink-300 opacity-50'
}`} />
</div>
<div className="relative z-10 flex min-h-screen gap-6 p-4">
{/* Sidebar */}
<Sidebar selectedTab="alerts-b2b" />
{/* Main Content */}
<main className="flex-1">
{/* Header */}
<div className="flex items-center justify-between mb-8">
<div>
<h1 className={`text-4xl font-bold mb-2 flex items-center gap-3 ${isDark ? 'text-white' : 'text-slate-900'}`}>
<span>🏢</span> B2B Alerts
{unreadCount > 0 && (
<span className="px-3 py-1 text-sm font-medium rounded-full bg-purple-500/20 text-purple-400 border border-purple-500/30">
{unreadCount} neu
</span>
)}
</h1>
<p className={isDark ? 'text-white/60' : 'text-slate-600'}>
{tenant.companyName} - Procurement Alerts
</p>
</div>
<div className="flex items-center gap-4">
{/* View Toggle */}
<div className={`flex rounded-xl overflow-hidden border ${isDark ? 'border-white/20' : 'border-slate-200'}`}>
<button
onClick={() => setViewMode('inbox')}
className={`px-4 py-2 text-sm font-medium transition-all ${
viewMode === 'inbox'
? 'bg-gradient-to-r from-purple-500 to-pink-500 text-white'
: isDark ? 'text-white/60 hover:text-white' : 'text-slate-500 hover:text-slate-900'
}`}
>
Inbox
</button>
<button
onClick={() => setViewMode('digest')}
className={`px-4 py-2 text-sm font-medium transition-all ${
viewMode === 'digest'
? 'bg-gradient-to-r from-purple-500 to-pink-500 text-white'
: isDark ? 'text-white/60 hover:text-white' : 'text-slate-500 hover:text-slate-900'
}`}
>
Digest
</button>
</div>
<LanguageDropdown />
<ThemeToggle />
{/* Import Email Button */}
<button
onClick={() => setShowImportModal(true)}
className={`px-4 py-2 backdrop-blur-xl border rounded-2xl transition-all flex items-center gap-2 ${
isDark
? 'bg-purple-500/20 border-purple-500/30 text-purple-300 hover:bg-purple-500/30'
: 'bg-purple-100 border-purple-200 text-purple-700 hover:bg-purple-200'
}`}
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M12 4v16m8-8H4" />
</svg>
<span className="text-sm font-medium">E-Mail einfuegen</span>
</button>
<button
onClick={() => setShowWizard(true)}
className={`p-3 backdrop-blur-xl border rounded-2xl hover:bg-white/20 transition-all ${
isDark
? 'bg-white/10 border-white/20 text-white'
: 'bg-black/5 border-black/10 text-slate-700'
}`}
>
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
</button>
</div>
</div>
<div className="grid grid-cols-3 gap-6">
{/* Main Panel (2/3) */}
<div className={`col-span-2 backdrop-blur-xl border rounded-3xl p-6 ${
isDark
? 'bg-white/10 border-white/20'
: 'bg-white/70 border-black/10 shadow-lg'
}`}>
{viewMode === 'inbox' ? (
<>
{/* Filter Bar */}
<div className="flex items-center justify-between mb-6 flex-wrap gap-3">
<div className="flex items-center gap-2 flex-wrap">
{/* Decision Filter */}
{['all', 'unread', 'relevant', 'needs_review', 'irrelevant'].map((filter) => (
<button
key={filter}
onClick={() => setFilterDecision(filter)}
className={`px-3 py-1.5 rounded-lg text-sm font-medium transition-all ${
filterDecision === filter
? 'bg-purple-500/20 text-purple-400 border border-purple-500/30'
: isDark
? 'text-white/60 hover:text-white hover:bg-white/10'
: 'text-slate-500 hover:text-slate-900 hover:bg-slate-100'
}`}
>
{filter === 'all' ? 'Alle' : filter === 'unread' ? 'Ungelesen' : filter === 'relevant' ? 'Relevant' : filter === 'needs_review' ? 'Pruefung' : 'Irrelevant'}
</button>
))}
</div>
{unreadCount > 0 && (
<button
onClick={markAllAsRead}
className={`text-sm ${isDark ? 'text-white/60 hover:text-white' : 'text-slate-500 hover:text-slate-900'}`}
>
Alle gelesen
</button>
)}
</div>
{/* Importance Filter */}
<div className="flex items-center gap-2 mb-6 flex-wrap">
<span className={`text-xs ${isDark ? 'text-white/40' : 'text-slate-400'}`}>Wichtigkeit:</span>
{['all', 'KRITISCH', 'DRINGEND', 'WICHTIG', 'PRUEFEN', 'INFO'].map((filter) => (
<button
key={filter}
onClick={() => setFilterImportance(filter)}
className={`px-2 py-1 rounded text-xs transition-all ${
filterImportance === filter
? filter === 'all'
? 'bg-purple-500/20 text-purple-400'
: getImportanceLabelColor(filter as ImportanceLabel, isDark)
: isDark
? 'text-white/40 hover:text-white/60'
: 'text-slate-400 hover:text-slate-600'
}`}
>
{filter === 'all' ? 'Alle' : filter}
</button>
))}
</div>
{/* Hits List */}
{filteredHits.length === 0 ? (
<div className={`text-center py-12 ${isDark ? 'text-white/40' : 'text-slate-400'}`}>
<span className="text-4xl block mb-4">📭</span>
<p>Keine Hits gefunden</p>
</div>
) : (
<div className="space-y-3">
{filteredHits.map(hit => (
<HitCard
key={hit.id}
hit={hit}
onClick={() => handleHitClick(hit)}
/>
))}
</div>
)}
</>
) : (
<DigestView hits={getDigest()} onHitClick={handleHitClick} />
)}
</div>
{/* Right Sidebar (1/3) */}
<div className="space-y-6">
{/* Stats */}
<div className={`backdrop-blur-xl border rounded-3xl p-6 ${
isDark
? 'bg-white/10 border-white/20'
: 'bg-white/70 border-black/10 shadow-lg'
}`}>
<h2 className={`text-lg font-semibold mb-4 flex items-center gap-2 ${isDark ? 'text-white' : 'text-slate-900'}`}>
<span>📊</span> Statistik
</h2>
<div className="space-y-3">
<div className="flex items-center justify-between">
<span className={`text-sm ${isDark ? 'text-white/60' : 'text-slate-500'}`}>Gesamt Hits</span>
<span className={`font-medium ${isDark ? 'text-white' : 'text-slate-900'}`}>{hits.length}</span>
</div>
<div className="flex items-center justify-between">
<span className={`text-sm ${isDark ? 'text-white/60' : 'text-slate-500'}`}>Ungelesen</span>
<span className="font-medium text-purple-500">{unreadCount}</span>
</div>
<div className="flex items-center justify-between">
<span className={`text-sm ${isDark ? 'text-white/60' : 'text-slate-500'}`}>Relevant</span>
<span className="font-medium text-green-500">{relevantCount}</span>
</div>
<div className="flex items-center justify-between">
<span className={`text-sm ${isDark ? 'text-white/60' : 'text-slate-500'}`}>Pruefung noetig</span>
<span className="font-medium text-amber-500">{needsReviewCount}</span>
</div>
</div>
</div>
{/* Topics */}
<div className={`backdrop-blur-xl border rounded-3xl p-6 ${
isDark
? 'bg-white/10 border-white/20'
: 'bg-white/70 border-black/10 shadow-lg'
}`}>
<h2 className={`text-lg font-semibold mb-4 flex items-center gap-2 ${isDark ? 'text-white' : 'text-slate-900'}`}>
<span>🎯</span> Topics
</h2>
{topics.length === 0 ? (
<p className={`text-sm ${isDark ? 'text-white/40' : 'text-slate-400'}`}>
Keine Topics konfiguriert.
</p>
) : (
<div className="space-y-2">
{topics.map(topic => (
<div
key={topic.id}
className={`flex items-center gap-3 p-3 rounded-lg ${
isDark ? 'bg-white/5' : 'bg-slate-50'
}`}
>
<span className="text-lg">{getPackageIcon(topic.package)}</span>
<div className="flex-1 min-w-0">
<p className={`font-medium text-sm truncate ${isDark ? 'text-white' : 'text-slate-900'}`}>
{topic.name}
</p>
<p className={`text-xs ${isDark ? 'text-white/40' : 'text-slate-400'}`}>
{getPackageLabel(topic.package)}
</p>
</div>
<div className={`w-2 h-2 rounded-full ${topic.status === 'active' ? 'bg-green-500' : 'bg-slate-400'}`} />
</div>
))}
</div>
)}
</div>
{/* Settings Summary */}
<div className={`backdrop-blur-xl border rounded-3xl p-6 ${
isDark
? 'bg-white/10 border-white/20'
: 'bg-white/70 border-black/10 shadow-lg'
}`}>
<h2 className={`text-lg font-semibold mb-4 flex items-center gap-2 ${isDark ? 'text-white' : 'text-slate-900'}`}>
<span></span> Einstellungen
</h2>
<div className="space-y-3 text-sm">
<div className="flex items-center justify-between">
<span className={isDark ? 'text-white/60' : 'text-slate-500'}>Benachrichtigung</span>
<span className={isDark ? 'text-white' : 'text-slate-900'}>{settings.notificationCadence}</span>
</div>
<div className="flex items-center justify-between">
<span className={isDark ? 'text-white/60' : 'text-slate-500'}>Noise-Modus</span>
<span className={`px-2 py-0.5 rounded text-xs ${
settings.noiseMode === 'STRICT'
? 'bg-green-500/20 text-green-400'
: settings.noiseMode === 'BALANCED'
? 'bg-amber-500/20 text-amber-400'
: 'bg-red-500/20 text-red-400'
}`}>
{settings.noiseMode}
</span>
</div>
<div className="flex items-center justify-between">
<span className={isDark ? 'text-white/60' : 'text-slate-500'}>Max. Digest</span>
<span className={isDark ? 'text-white' : 'text-slate-900'}>{settings.maxDigestItems} Items</span>
</div>
</div>
</div>
{/* Info */}
<TipBox title="Noise-Reduktion" icon="🎯">
<p className="text-sm">
Im STRICT-Modus werden nur ~10% der Hits als relevant markiert.
Der Rest wird automatisch gefiltert.
</p>
</TipBox>
</div>
</div>
</main>
</div>
{/* Hit Detail Modal */}
{selectedHit && (
<HitDetailModal
hit={selectedHit}
onClose={() => setSelectedHit(null)}
onFeedback={(feedback) => handleFeedback(selectedHit.id, feedback)}
/>
)}
{/* Email Import Modal */}
{showImportModal && (
<EmailImportModal
onClose={() => setShowImportModal(false)}
onImport={(content, subject) => {
const newHit = processEmailContent(content, subject)
setImportSuccess(newHit)
// Auto-hide success message after 5 seconds
setTimeout(() => setImportSuccess(null), 5000)
}}
/>
)}
{/* Import Success Notification */}
{importSuccess && (
<div className="fixed bottom-6 right-6 z-50 animate-fade-in">
<div className={`p-4 rounded-2xl border shadow-xl backdrop-blur-xl max-w-md ${
isDark
? 'bg-green-500/20 border-green-500/30'
: 'bg-green-50 border-green-200 shadow-green-100'
}`}>
<div className="flex items-start gap-3">
<span className="text-2xl"></span>
<div className="flex-1">
<p className={`font-medium ${isDark ? 'text-green-300' : 'text-green-700'}`}>
E-Mail erfolgreich importiert!
</p>
<p className={`text-sm mt-1 ${isDark ? 'text-green-200/70' : 'text-green-600'}`}>
{importSuccess.title.slice(0, 60)}...
</p>
<div className="flex items-center gap-2 mt-2">
<span className={`px-2 py-0.5 rounded text-xs font-medium ${
importSuccess.decisionLabel === 'relevant'
? 'bg-green-500/20 text-green-400'
: importSuccess.decisionLabel === 'needs_review'
? 'bg-amber-500/20 text-amber-400'
: 'bg-slate-500/20 text-slate-400'
}`}>
{importSuccess.decisionLabel === 'relevant' ? 'Relevant' : importSuccess.decisionLabel === 'needs_review' ? 'Pruefung' : 'Info'}
</span>
<span className={`text-xs ${isDark ? 'text-green-200/50' : 'text-green-500'}`}>
Score: {importSuccess.importanceScore}
</span>
</div>
</div>
<button
onClick={() => setImportSuccess(null)}
className={`p-1 rounded-lg ${isDark ? 'hover:bg-white/10' : 'hover:bg-green-100'}`}
>
<svg className={`w-4 h-4 ${isDark ? 'text-green-300' : 'text-green-600'}`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
</div>
</div>
)}
<Footer />
{/* Blob Animation Styles */}
<style jsx>{`
@keyframes blob {
0% { transform: translate(0px, 0px) scale(1); }
33% { transform: translate(30px, -50px) scale(1.1); }
66% { transform: translate(-20px, 20px) scale(0.9); }
100% { transform: translate(0px, 0px) scale(1); }
}
.animate-blob {
animation: blob 7s infinite;
}
.animation-delay-2000 {
animation-delay: 2s;
}
.animation-delay-4000 {
animation-delay: 4s;
}
@keyframes fade-in {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.animate-fade-in {
animation: fade-in 0.3s ease-out;
}
`}</style>
</div>
)
}