From 9cf3e72033ba13133505e8b23d1205f1bb203cf2 Mon Sep 17 00:00:00 2001 From: Benjamin Sutter Date: Mon, 18 May 2026 21:59:08 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20Schattenmarkt=20UX=20=E2=80=94=20separa?= =?UTF-8?q?te=20section,=20relevance=20bridge,=20source=20links?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signals are now in their own feed section with an explanation banner so users understand they are not confirmed properties. Each signal card shows WHY it appears in their search (e.g. 'Fläche könnte frei werden'). Source pills are now clickable links. FutureAvailabilityContextPanel redesigned with full AI summary, source button, and relevance bridge. Co-Authored-By: Claude Sonnet 4.6 --- .../match-card/IntelligenceMatchCard.tsx | 54 ++++- .../match-card/MatchCardViewModel.ts | 1 + .../FutureAvailabilityContextPanel.tsx | 213 ++++++++++++++---- src/components/results/UnifiedResultFeed.tsx | 82 +++++-- src/features/matching/matchCardAdapter.ts | 1 + 5 files changed, 281 insertions(+), 70 deletions(-) diff --git a/src/components/match-card/IntelligenceMatchCard.tsx b/src/components/match-card/IntelligenceMatchCard.tsx index 4a9827d..f2f9dba 100644 --- a/src/components/match-card/IntelligenceMatchCard.tsx +++ b/src/components/match-card/IntelligenceMatchCard.tsx @@ -4,6 +4,7 @@ import { Briefcase, Building2, CheckCircle2, + ExternalLink, FileCheck, FileText, Newspaper, @@ -42,6 +43,16 @@ const SIGNAL_TYPE_LABELS: Record = { SPACE_CONSOLIDATION: 'Flächenkonsolidierung', } +// Human-readable explanation of WHY a signal type is relevant to a space search +const SIGNAL_RELEVANCE_BRIDGE: Record = { + EXPANSION: 'Neuer Flächenbedarf entsteht — Fläche wird gesucht', + POSSIBLE_MOVE_OUT: 'Fläche könnte frei werden — frühzeitig beobachten', + CONSTRUCTION_PROJECT:'Neubau geplant — zukünftige Verfügbarkeit möglich', + RESTRUCTURING: 'Flächenveränderung durch Restrukturierung möglich', + PROJECT_DEVELOPMENT: 'Projektentwicklung — neue Flächen in Planung', + SPACE_CONSOLIDATION: 'Konsolidierung — Teilflächen könnten verfügbar werden', +} + const CREDIBILITY_META: Record = { HIGH: { label: 'Hohe Quellenqualität', color: '#1a7a4a' }, MEDIUM: { label: 'Mittlere Quellenqualität', color: '#d97706' }, @@ -54,6 +65,7 @@ function SignalHero({ vm }: { vm: MatchCardViewModel }) { const tier = getScoreTier(vm.matchScore) const theme = SCORE_THEME[tier] const signalLabel = vm.signalType ? (SIGNAL_TYPE_LABELS[vm.signalType] ?? vm.signalType) : 'Signal' + const relevanceBridge = vm.signalType ? (SIGNAL_RELEVANCE_BRIDGE[vm.signalType] ?? null) : null const initials = (vm.title ?? '?') .split(/\s+/) .slice(0, 2) @@ -66,7 +78,7 @@ function SignalHero({ vm }: { vm: MatchCardViewModel }) { {/* Gradient hero background */} {/* Subtle pattern overlay */} @@ -86,25 +99,32 @@ function SignalHero({ vm }: { vm: MatchCardViewModel }) { {/* Company initials */} - + {initials} {/* Signal type label */} - - + + {signalLabel} + {/* Relevance bridge — why this appears in results */} + {relevanceBridge && ( + + {relevanceBridge} + + )} + {/* Verified badge */} {vm.signalIsVerified && ( @@ -153,10 +173,24 @@ function SignalMetaStrip({ vm }: { vm: MatchCardViewModel }) { {/* Source + credibility row */} {sourceMeta && ( - - {sourceMeta.icon} - {sourceMeta.label} - + vm.signalSourceUrl ? ( + + {sourceMeta.icon} + {sourceMeta.label} + + + ) : ( + + {sourceMeta.icon} + {sourceMeta.label} + + ) )} {credMeta && ( diff --git a/src/components/match-card/MatchCardViewModel.ts b/src/components/match-card/MatchCardViewModel.ts index ede5bd4..36f10a7 100644 --- a/src/components/match-card/MatchCardViewModel.ts +++ b/src/components/match-card/MatchCardViewModel.ts @@ -51,6 +51,7 @@ export interface MatchCardViewModel { // Schattenmarkt / FUTURE_AVAILABILITY signal fields signalSourceType?: 'PRESS' | 'CONSTRUCTION_PERMIT' | 'JOB_POSTING' | 'COMPANY_REPORT' | 'MARKET_DATA' | 'MANUAL' + signalSourceUrl?: string signalSourceCredibility?: 'LOW' | 'MEDIUM' | 'HIGH' signalProbability?: number signalMarketIndicator?: string diff --git a/src/components/match-detail/FutureAvailabilityContextPanel.tsx b/src/components/match-detail/FutureAvailabilityContextPanel.tsx index 665bec9..09c251d 100644 --- a/src/components/match-detail/FutureAvailabilityContextPanel.tsx +++ b/src/components/match-detail/FutureAvailabilityContextPanel.tsx @@ -1,4 +1,15 @@ -import { Alert, Box, Chip, Paper, Typography } from '@mui/material' +import { Alert, Box, Button, Chip, Divider, Paper, Typography } from '@mui/material' +import { + BarChart2, + Briefcase, + ExternalLink, + FileCheck, + FileText, + Newspaper, + ShieldCheck, + User, + Zap, +} from 'lucide-react' import type { Match } from '../../domain/match' import type { FutureSignal } from '../../domain/futureSignal' @@ -11,20 +22,36 @@ const SIGNAL_TYPE_LABELS: Record = { SPACE_CONSOLIDATION: 'Flächenkonsolidierung', } +const SIGNAL_RELEVANCE_BRIDGE: Record = { + EXPANSION: 'Ein Unternehmen sucht neue Flächen — Ihr Angebot könnte gefragt sein.', + POSSIBLE_MOVE_OUT: 'Dieses Unternehmen könnte seinen Standort aufgeben — die Fläche wird für Sie verfügbar.', + CONSTRUCTION_PROJECT:'Ein Neubau entsteht — neue Flächen könnten zur Vermietung angeboten werden.', + RESTRUCTURING: 'Restrukturierung deutet auf Flächenänderungen hin.', + PROJECT_DEVELOPMENT: 'Projektentwicklung könnte neue Gewerbeflächen schaffen.', + SPACE_CONSOLIDATION: 'Konsolidierung — Teilflächen könnten frei werden.', +} + +const SOURCE_META: Record = { + JOB_POSTING: { label: 'Stelleninserate', icon: }, + PRESS: { label: 'Pressebericht', icon: }, + CONSTRUCTION_PERMIT: { label: 'Baubewilligung', icon: }, + COMPANY_REPORT: { label: 'Geschäftsbericht', icon: }, + MARKET_DATA: { label: 'Marktdaten', icon: }, + MANUAL: { label: 'Analyst', icon: }, +} + +const CREDIBILITY_META: Record = { + HIGH: { label: 'Hohe Quellenqualität', color: '#1a7a4a' }, + MEDIUM: { label: 'Mittlere Quellenqualität', color: '#d97706' }, + LOW: { label: 'Niedrige Quellenqualität', color: '#c0392b' }, +} + const SENSITIVITY_META: Record = { CONFIDENTIAL: { label: 'Vertraulich', color: 'error' }, INTERNAL: { label: 'Intern', color: 'warning' }, PUBLIC: { label: 'Öffentlich', color: 'default' }, } -const REVIEW_STATUS_META: Record = { - UNREVIEWED: { label: 'Ungeprüft', color: 'warning' }, - IN_REVIEW: { label: 'In Prüfung', color: 'warning' }, - APPROVED: { label: 'Genehmigt', color: 'success' }, - REJECTED: { label: 'Abgelehnt', color: 'error' }, - FLAGGED: { label: 'Markiert', color: 'warning' }, -} - interface Props { match: Match signal: FutureSignal | null @@ -33,63 +60,159 @@ interface Props { export function FutureAvailabilityContextPanel({ match: _match, signal }: Props) { if (!signal) return null + const sourceMeta = SOURCE_META[signal.source.type] ?? null + const credMeta = CREDIBILITY_META[signal.source.credibility] ?? null const sensitiveMeta = SENSITIVITY_META[signal.sensitivityLevel] ?? SENSITIVITY_META.PUBLIC - const reviewMeta = signal.reviewStatus ? (REVIEW_STATUS_META[signal.reviewStatus] ?? null) : null + const relevanceBridge = signal.signalType ? (SIGNAL_RELEVANCE_BRIDGE[signal.signalType] ?? null) : null + const probPct = Math.round(signal.probability * 100) + const confPct = Math.round(signal.confidenceScore * 100) return ( - Zukunftssignal – Kontext + + + Schattenmarkt-Signal + {signal.isVerified && ( + + + Verifiziert + + )} + {/* Mandatory disclaimer */} - + {signal.disclaimer} - + {/* Relevance bridge */} + {relevanceBridge && ( + + + + Warum erscheint dieses Signal in Ihrer Suche? + {relevanceBridge} + + + )} + + {/* Key metrics row */} + {signal.signalType && ( - - Signaltyp - {SIGNAL_TYPE_LABELS[signal.signalType] ?? signal.signalType} + + Signaltyp + )} - - Konfidenz - - {Math.round(signal.confidenceScore * 100)}% - + + Wahrscheinlichkeit + = 70 ? '#1a7a4a' : probPct >= 50 ? '#d97706' : '#c0392b' }}>{probPct}% - - Wahrscheinlichkeit - - {Math.round(signal.probability * 100)}% - + + Konfidenz + {confPct}% - - Zeithorizont - ~{signal.timeHorizonMonths} Monate + + Zeithorizont + ~{signal.timeHorizonMonths} Monate - - Vertraulichkeit + {signal.areaSqmEstimate && ( + + Flächenschätzung + ~{signal.areaSqmEstimate.toLocaleString('de-CH')} m² + + )} + + Vertraulichkeit - {reviewMeta && ( - - Prüfstatus - - - )} - - Quellglaubwürdigkeit - {signal.source.credibility} - + + + {/* AI Summary */} + {signal.aiSummary && ( + + + + KI-Analyse + + + + {signal.aiSummary} + + + + )} + + {/* Evidence */} {signal.evidence?.summary && ( - - - Evidenz + + Evidenz + + {signal.evidence.summary} + + + )} + + + + {/* Source section */} + Quelle + + {sourceMeta && ( + + {sourceMeta.icon} + {sourceMeta.label} + + )} + {credMeta && ( + + + {credMeta.label} + + )} + {signal.source.publishedAt && ( + + Publiziert: {new Date(signal.source.publishedAt).toLocaleDateString('de-CH', { day: '2-digit', month: 'long', year: 'numeric' })} - {signal.evidence.summary} + )} + {signal.source.url && ( + + )} + + + {signal.evidence?.sourceUrls && signal.evidence.sourceUrls.length > 0 && ( + + + Weitere Belege ({signal.evidence.sourceUrls.length}) + + + {signal.evidence.sourceUrls.map((url, i) => ( + + + {url} + + ))} + )} diff --git a/src/components/results/UnifiedResultFeed.tsx b/src/components/results/UnifiedResultFeed.tsx index 8eef264..64ce79c 100644 --- a/src/components/results/UnifiedResultFeed.tsx +++ b/src/components/results/UnifiedResultFeed.tsx @@ -1,4 +1,5 @@ -import { Box, Chip, Divider, Typography } from '@mui/material' +import { Alert, Box, Chip, Divider, Typography } from '@mui/material' +import { Zap } from 'lucide-react' import type { UnifiedMatchResult } from '../../domain/unifiedResult' import { UnifiedResultCard } from './UnifiedResultCard' import { SCORE_THEME } from '../shared/scoreTheme' @@ -72,12 +73,67 @@ function TierHeader({ label, sublabel, tierKey, count, isFirst }: TierHeaderProp ) } +function SchattenmarktSectionHeader({ count }: { count: number }) { + return ( + + + + + + + Schattenmarkt + + + + + + } + sx={{ + mb: 2, + bgcolor: '#faf5ff', + border: '1px solid #e9d5ff', + color: '#4c1d95', + '& .MuiAlert-icon': { color: '#7c3aed' }, + }} + > + Was sind Schattenmarkt-Signale? Die KI hat aus öffentlichen Quellen (Stelleninseraten, Pressemeldungen, Baubewilligungen etc.) Hinweise gefunden, dass in Ihrer Zielregion in absehbarer Zeit Flächen frei werden könnten. Dies sind keine bestätigten Angebote — aber frühzeitige Hinweise, die Sie im Auge behalten sollten. Klicken Sie auf Details für Quellen und die vollständige KI-Analyse. + + + ) +} + export function UnifiedResultFeed({ results, view = 'list' }: Props) { - const tiers = TIER_CONFIG.map(t => ({ ...t, items: results.filter(t.filter) })) + const realResults = results.filter(r => r.resultType !== 'FUTURE_AVAILABILITY') + const signalResults = results.filter(r => r.resultType === 'FUTURE_AVAILABILITY') + + const tiers = TIER_CONFIG.map(t => ({ ...t, items: realResults.filter(t.filter) })) .filter(t => t.items.length > 0) + function renderCards(items: UnifiedMatchResult[]) { + return view === 'grid' ? ( + + {items.map(result => ( + + ))} + + ) : ( + + {items.map(result => ( + + ))} + + ) + } + return ( + {/* Real properties — scored tiers */} {tiers.map((tier, idx) => ( - {view === 'grid' ? ( - - {tier.items.map(result => ( - - ))} - - ) : ( - - {tier.items.map(result => ( - - ))} - - )} + {renderCards(tier.items)} ))} + + {/* Schattenmarkt signals — dedicated section */} + {signalResults.length > 0 && ( + + + {renderCards(signalResults)} + + )} ) } diff --git a/src/features/matching/matchCardAdapter.ts b/src/features/matching/matchCardAdapter.ts index ad7cb86..a0403d2 100644 --- a/src/features/matching/matchCardAdapter.ts +++ b/src/features/matching/matchCardAdapter.ts @@ -113,6 +113,7 @@ export function buildMatchCardViewModel( isStaleData: false, // Schattenmarkt signal fields signalSourceType: signal?.source?.type, + signalSourceUrl: signal?.source?.url, signalSourceCredibility: signal?.source?.credibility, signalProbability: signal?.probability, signalMarketIndicator: signal?.marketIndicator,