feat: F008 AI need builder — smart parse, criteria review, follow-up & weighting

Domain: ParsedNeedCriteria, FollowUpQuestion, ParseNeedResult, NeedBuilderStep, WEIGHTING_KEYS/LABELS. Services: weightingService (asset-type profiles), aiService extended with parseNeed (keyword extraction mock, 1.4s delay) and generateFollowUpQuestions. Components: NeedBuilderProgress, NeedInput, ConfidenceFieldBadge, ExtractedFieldRow, CriteriaReviewPanel, FollowUpQuestionCard, FollowUpPanel, WeightingEditor, NeedCardPreview, NeedBuilderErrorState. Page: AINeedBuilderPage replaces AISearch with 4-step flow (input → review → weighting → save).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Sutter
2026-05-16 12:21:49 +02:00
parent cb18f3c381
commit 73a6a167e0
15 changed files with 1116 additions and 296 deletions
+69
View File
@@ -0,0 +1,69 @@
import type { AssetType } from './enums'
export interface ParsedNeedCriteria {
assetType?: AssetType
areaRange?: { min: number; max: number }
preferredLocations?: string[]
budgetRange?: { maxPerSqm: number; maxMonthlyTotal?: number; currency: string }
timing?: { earliestMoveIn: string; latestMoveIn?: string; contractDurationMonths?: number; flexibleTiming: boolean }
mustHaveCriteria?: string[]
softFactors?: { minPrestige?: number; requireParking?: boolean; maxPublicTransportMinutes?: number; requireHighVisibility?: boolean }
infrastructureRequirements?: string[]
accessibilityRequirements?: string[]
prestigeImportance?: 'LOW' | 'MEDIUM' | 'HIGH'
flexibilityNeed?: 'LOW' | 'MEDIUM' | 'HIGH'
expansionPotential?: boolean
parkingNeed?: boolean
visibilityNeed?: 'LOW' | 'MEDIUM' | 'HIGH'
footfallNeed?: 'LOW' | 'MEDIUM' | 'HIGH'
companyName?: string
notes?: string
}
export interface FollowUpQuestion {
id: string
questionText: string
targetField: string
reason: string
suggestedAnswerOptions?: string[]
importance: 'required' | 'recommended' | 'optional'
}
export interface ParseNeedResult {
extractedCriteria: ParsedNeedCriteria
confidenceByField: Record<string, number>
missingFields: string[]
assumptions: string[]
suggestedWeights: Record<string, number>
followUpQuestionCandidates: FollowUpQuestion[]
rawSummary: string
promptVersion: string
schemaVersion: string
}
export const NeedBuilderStep = {
IDLE: 'idle',
PARSING: 'parsing',
PARSED_REQUIRES_REVIEW: 'parsed_requires_review',
CLARIFICATION_REQUIRED: 'clarification_required',
WEIGHTING_REVIEW: 'weighting_review',
READY_TO_SAVE: 'ready_to_save',
SAVING: 'saving',
SAVED: 'saved',
ERROR: 'error',
} as const
export type NeedBuilderStep = typeof NeedBuilderStep[keyof typeof NeedBuilderStep]
export const WEIGHTING_KEYS = ['area', 'location', 'budget', 'timing', 'prestige', 'accessibility', 'expansionPotential', 'flexibility'] as const
export type WeightingKey = typeof WEIGHTING_KEYS[number]
export const WEIGHTING_LABELS: Record<WeightingKey, string> = {
area: 'Fläche',
location: 'Standort',
budget: 'Budget',
timing: 'Verfügbarkeit',
prestige: 'Prestige',
accessibility: 'Erreichbarkeit',
expansionPotential: 'Expansionspotenzial',
flexibility: 'Flexibilität',
}