feat: unified error handling + AI service modularisation

Error handling (Prompt 2):
- src/services/errors.ts: AppError class, normalizeError(), throwServiceError() helper
- 6 services wrapped with try/catch (property, match, need, shortlist, futureSignal, inquiry)
- inquiryService aligned from custom ServiceResult<T> to standard ServiceResponse types
- Results, MatchCenter, FutureAvailability pages show <ErrorState onRetry> on query failure

AI modularisation (Prompt 3):
- src/services/aiService.ts reduced from 755 → 19 lines (barrel re-export)
- src/services/ai/IAIService.ts: typed interface + all response types
- src/services/ai/mock/: needParser, compareBuilder, decisionBrief, listingParser, MockAIService
- src/services/ai/openrouter/OpenRouterAIService.ts: model-agnostic skeleton
- src/services/ai/prompts/: 4 prompt template files (needParsing, matchExplanation, compareSummary, decisionBrief)
- src/services/ai/index.ts: factory selects Mock or OpenRouter via VITE_USE_REAL_AI flag
- All existing import paths unchanged — zero call-site modifications

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Sutter
2026-05-24 00:32:01 +02:00
parent efc72b720e
commit 6515acb7f0
23 changed files with 1468 additions and 955 deletions
+86
View File
@@ -0,0 +1,86 @@
import type { ItemResponse } from '../types'
import type { CreateNeedInput } from '../../domain/need'
import type { ParseNeedResult, ParsedNeedCriteria, FollowUpQuestion } from '../../domain/needBuilder'
import type { UnifiedMatchResult } from '../../domain/unifiedResult'
// ── Response Types ────────────────────────────────────────────────────────────
export interface DecisionBrief {
id: string
shortlistId: string
summary: string
sections: { title: string; body: string }[]
generatedAt: string
isDraft: true
}
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
overallAssessment: string
perPropertyAssessment: Array<{
matchId: string
label: string
strengths: string[]
weaknesses: string[]
bestFor: string
keyRisk: string | null
}>
recommendation: string
}
export interface OfferEmailPayload {
needTitle: string
properties: string[]
matchScores: number[]
}
export interface ParsedListingData {
assetType?: string
areaSqm?: number
rentPerSqm?: number
city?: string
softLevels?: Record<string, string>
parking?: number
fitOut?: string
}
// ── Legacy types (kept for backward compatibility) ────────────────────────────
export interface CriteriaExtractionResult {
extractedCriteria: Partial<CreateNeedInput>
confidence: number
missingFields: string[]
assumptions: string[]
followUpQuestions: string[]
}
export interface AIServiceProvider {
extractCriteria(naturalLanguageInput: string): Promise<CriteriaExtractionResult>
generateFollowUp(partialNeed: Partial<CreateNeedInput>): Promise<string[]>
}
// ── Service Interface ─────────────────────────────────────────────────────────
export interface IAIService {
// Need parsing (F008)
parseNeed(input: string): Promise<ItemResponse<ParseNeedResult>>
generateFollowUpQuestions(criteria: ParsedNeedCriteria): Promise<ItemResponse<FollowUpQuestion[]>>
// Compare (F014)
summarizeComparison(items: UnifiedMatchResult[]): Promise<ItemResponse<ComparisonSummary>>
// Shortlist decision brief
generateDecisionBrief(shortlistId: string): Promise<ItemResponse<DecisionBrief>>
// Offer email (supply side)
generateOfferEmail(payload: OfferEmailPayload): Promise<ItemResponse<{ subject: string; body: string }>>
// Legacy methods
extractCriteria(input: string): Promise<ItemResponse<CriteriaExtractionResult>>
generateFollowUp(partialNeed: Partial<CreateNeedInput>): Promise<ItemResponse<string[]>>
}