[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:
120
studio-v2/app/alerts-b2b/_components/DecisionTraceModal.tsx
Normal file
120
studio-v2/app/alerts-b2b/_components/DecisionTraceModal.tsx
Normal file
@@ -0,0 +1,120 @@
|
||||
'use client'
|
||||
|
||||
import { useTheme } from '@/lib/ThemeContext'
|
||||
import type { B2BHit } from '@/lib/AlertsB2BContext'
|
||||
|
||||
export 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">
|
||||
{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>
|
||||
)}
|
||||
|
||||
{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>
|
||||
)}
|
||||
|
||||
{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>
|
||||
)}
|
||||
|
||||
{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>
|
||||
)
|
||||
}
|
||||
49
studio-v2/app/alerts-b2b/_components/DigestView.tsx
Normal file
49
studio-v2/app/alerts-b2b/_components/DigestView.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
'use client'
|
||||
|
||||
import { useTheme } from '@/lib/ThemeContext'
|
||||
import type { B2BHit } from '@/lib/AlertsB2BContext'
|
||||
import { HitCard } from './HitCard'
|
||||
|
||||
export 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>
|
||||
)
|
||||
}
|
||||
163
studio-v2/app/alerts-b2b/_components/EmailImportModal.tsx
Normal file
163
studio-v2/app/alerts-b2b/_components/EmailImportModal.tsx
Normal file
@@ -0,0 +1,163 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { useTheme } from '@/lib/ThemeContext'
|
||||
|
||||
export 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>
|
||||
)
|
||||
}
|
||||
66
studio-v2/app/alerts-b2b/_components/HitCard.tsx
Normal file
66
studio-v2/app/alerts-b2b/_components/HitCard.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
'use client'
|
||||
|
||||
import { useTheme } from '@/lib/ThemeContext'
|
||||
import type { B2BHit } from '@/lib/AlertsB2BContext'
|
||||
import { getImportanceLabelColor, getDecisionLabelColor, formatDeadline } from '@/lib/AlertsB2BContext'
|
||||
|
||||
export 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>
|
||||
)
|
||||
}
|
||||
148
studio-v2/app/alerts-b2b/_components/HitDetailModal.tsx
Normal file
148
studio-v2/app/alerts-b2b/_components/HitDetailModal.tsx
Normal file
@@ -0,0 +1,148 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { useTheme } from '@/lib/ThemeContext'
|
||||
import type { B2BHit } from '@/lib/AlertsB2BContext'
|
||||
import { getImportanceLabelColor, getDecisionLabelColor, formatDeadline } from '@/lib/AlertsB2BContext'
|
||||
import { DecisionTraceModal } from './DecisionTraceModal'
|
||||
|
||||
export 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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user