feat: F014 compare view — side-by-side decision table with AI summary
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -3,6 +3,82 @@ import { ServiceErrorCode } from './types'
|
||||
import type { CreateNeedInput } from '../domain/need'
|
||||
import type { AssetType } from '../domain/enums'
|
||||
import type { ParseNeedResult, ParsedNeedCriteria, FollowUpQuestion } from '../domain/needBuilder'
|
||||
import type { UnifiedMatchResult } from '../domain/unifiedResult'
|
||||
|
||||
// ── Compare Summary ───────────────────────────────────────────────────────────
|
||||
|
||||
export interface ComparisonSummary {
|
||||
strongestOption: { matchId: string; label: string; reason: string }
|
||||
bestValue: { matchId: string; label: string; reason: string } | null
|
||||
highestConfidence: { matchId: string; label: string; confidenceLevel: number }
|
||||
biggestTradeoffs: string[]
|
||||
missingDataWarnings: string[]
|
||||
recommendedNextStep: string
|
||||
}
|
||||
|
||||
function buildComparisonSummary(items: UnifiedMatchResult[]): ComparisonSummary {
|
||||
if (items.length === 0) {
|
||||
return {
|
||||
strongestOption: { matchId: '', label: '–', reason: 'Keine Ergebnisse' },
|
||||
bestValue: null,
|
||||
highestConfidence: { matchId: '', label: '–', confidenceLevel: 0 },
|
||||
biggestTradeoffs: [],
|
||||
missingDataWarnings: [],
|
||||
recommendedNextStep: 'Suchergebnisse überprüfen',
|
||||
}
|
||||
}
|
||||
|
||||
const getTitle = (item: UnifiedMatchResult) =>
|
||||
item.resultType !== 'FUTURE_AVAILABILITY'
|
||||
? (item as any).property?.title ?? `Match ${item.matchScore}`
|
||||
: (item as any).signal?.companyName ?? 'Zukunftssignal'
|
||||
|
||||
const strongest = items.reduce((a, b) => a.matchScore > b.matchScore ? a : b)
|
||||
|
||||
const propertyItems = items.filter(i => i.resultType !== 'FUTURE_AVAILABILITY')
|
||||
const bestValue = propertyItems.length > 0
|
||||
? propertyItems.reduce((a, b) =>
|
||||
((a as any).property?.rentPricePerSqm ?? Infinity) <= ((b as any).property?.rentPricePerSqm ?? Infinity) ? a : b
|
||||
)
|
||||
: null
|
||||
|
||||
const highestConf = items.reduce((a, b) =>
|
||||
a.match.confidenceLevel >= b.match.confidenceLevel ? a : b
|
||||
)
|
||||
|
||||
const tradeoffs = items
|
||||
.flatMap(i => i.match.tradeoffs?.slice(0, 1).map(t => `${getTitle(i)}: ${t.concern}`) ?? [])
|
||||
.slice(0, 3)
|
||||
|
||||
const missingWarnings = items
|
||||
.filter(i => (i.match.missingData?.filter(m => m.importance === 'CRITICAL').length ?? 0) > 0)
|
||||
.map(i => `${getTitle(i)}: fehlende Pflichtfelder`)
|
||||
|
||||
const topNextAction = strongest.match.nextBestActions?.[0]?.label ?? 'Objekt besichtigen oder Details prüfen'
|
||||
|
||||
return {
|
||||
strongestOption: {
|
||||
matchId: strongest.matchId,
|
||||
label: getTitle(strongest),
|
||||
reason: `Höchster Match Score (${strongest.matchScore}/100)`,
|
||||
},
|
||||
bestValue: bestValue
|
||||
? {
|
||||
matchId: bestValue.matchId,
|
||||
label: getTitle(bestValue),
|
||||
reason: `Niedrigster Mietpreis (CHF ${(bestValue as any).property?.rentPricePerSqm ?? '–'}/m²)`,
|
||||
}
|
||||
: null,
|
||||
highestConfidence: {
|
||||
matchId: highestConf.matchId,
|
||||
label: getTitle(highestConf),
|
||||
confidenceLevel: highestConf.match.confidenceLevel,
|
||||
},
|
||||
biggestTradeoffs: tradeoffs,
|
||||
missingDataWarnings: missingWarnings,
|
||||
recommendedNextStep: topNextAction,
|
||||
}
|
||||
}
|
||||
|
||||
// ── Legacy types (kept for backward compatibility) ────────────────────────────
|
||||
|
||||
@@ -284,6 +360,12 @@ export const aiService = {
|
||||
return { data }
|
||||
},
|
||||
|
||||
// F014: Compare summary
|
||||
async summarizeComparison(items: UnifiedMatchResult[]): Promise<ItemResponse<ComparisonSummary>> {
|
||||
await new Promise(r => setTimeout(r, 600))
|
||||
return { data: buildComparisonSummary(items) }
|
||||
},
|
||||
|
||||
// F008 methods
|
||||
async parseNeed(input: string): Promise<ItemResponse<ParseNeedResult>> {
|
||||
await new Promise(r => setTimeout(r, 1400))
|
||||
|
||||
Reference in New Issue
Block a user