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:
@@ -0,0 +1,88 @@
|
||||
import type { ItemResponse } from '../../types'
|
||||
import type { CreateNeedInput } from '../../../domain/need'
|
||||
import type { ParsedNeedCriteria, FollowUpQuestion } from '../../../domain/needBuilder'
|
||||
import type { UnifiedMatchResult } from '../../../domain/unifiedResult'
|
||||
import type {
|
||||
IAIService,
|
||||
DecisionBrief,
|
||||
ComparisonSummary,
|
||||
CriteriaExtractionResult,
|
||||
OfferEmailPayload,
|
||||
} from '../IAIService'
|
||||
import { mockParseNeed } from './needParser'
|
||||
import { buildComparisonSummary } from './compareBuilder'
|
||||
import { buildMockDecisionBrief } from './decisionBrief'
|
||||
|
||||
const SIMULATED_DELAY = {
|
||||
fast: 300,
|
||||
medium: 600,
|
||||
slow: 1800,
|
||||
}
|
||||
|
||||
const delay = (ms: number) => new Promise(r => setTimeout(r, ms))
|
||||
|
||||
export const MockAIService: IAIService = {
|
||||
async parseNeed(input: string): Promise<ItemResponse<ReturnType<typeof mockParseNeed>>> {
|
||||
await delay(SIMULATED_DELAY.fast)
|
||||
return { data: mockParseNeed(input) }
|
||||
},
|
||||
|
||||
async generateFollowUpQuestions(criteria: ParsedNeedCriteria): Promise<ItemResponse<FollowUpQuestion[]>> {
|
||||
await delay(SIMULATED_DELAY.medium)
|
||||
const result = mockParseNeed(JSON.stringify(criteria))
|
||||
return { data: result.followUpQuestionCandidates }
|
||||
},
|
||||
|
||||
async summarizeComparison(items: UnifiedMatchResult[]): Promise<ItemResponse<ComparisonSummary>> {
|
||||
await delay(SIMULATED_DELAY.medium)
|
||||
return { data: buildComparisonSummary(items) }
|
||||
},
|
||||
|
||||
async generateDecisionBrief(shortlistId: string): Promise<ItemResponse<DecisionBrief>> {
|
||||
await delay(SIMULATED_DELAY.slow)
|
||||
return { data: buildMockDecisionBrief(shortlistId) }
|
||||
},
|
||||
|
||||
async generateOfferEmail(payload: OfferEmailPayload): Promise<ItemResponse<{ subject: string; body: string }>> {
|
||||
await delay(SIMULATED_DELAY.medium * 2)
|
||||
return {
|
||||
data: {
|
||||
subject: `Passende Gewerbeflächen zu Ihrer Anfrage: ${payload.needTitle}`,
|
||||
body:
|
||||
`Sehr geehrte Damen und Herren,\n\nvielen Dank für Ihr Interesse. Gerne unterbreiten wir Ihnen folgende passende Gewerbeobjekte aus unserem Portfolio:\n\n` +
|
||||
payload.properties.map((p, i) => `• ${p} (Match-Score: ${payload.matchScores[i]}%)`).join('\n') +
|
||||
`\n\nGerne arrangieren wir Besichtigungstermine für die genannten Objekte und stehen für alle weiteren Fragen zur Verfügung.\n\nFreundliche Grüsse\nWincasa AG`,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
// Legacy methods
|
||||
async extractCriteria(_input: string): Promise<ItemResponse<CriteriaExtractionResult>> {
|
||||
return {
|
||||
data: {
|
||||
extractedCriteria: {
|
||||
companyName: 'Unbekannt (bitte bestätigen)',
|
||||
requiredArea: { min: 400, max: 900 },
|
||||
budgetRange: { maxPerSqm: 40, currency: 'CHF' },
|
||||
},
|
||||
confidence: 0.72,
|
||||
missingFields: ['assetType', 'timing', 'preferredLocations'],
|
||||
assumptions: ['Fläche aus Zahlenangabe geschätzt', 'Budget aus Kostennennung abgeleitet'],
|
||||
followUpQuestions: [
|
||||
'Welchen Nutzungstyp suchen Sie (Büro, Retail, Logistik)?',
|
||||
'In welchen Städten oder Regionen suchen Sie?',
|
||||
'Wann möchten Sie spätestens einziehen?',
|
||||
],
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
async generateFollowUp(partialNeed: Partial<CreateNeedInput>): Promise<ItemResponse<string[]>> {
|
||||
const questions: string[] = []
|
||||
if (!partialNeed.assetType) questions.push('Welchen Nutzungstyp suchen Sie?')
|
||||
if (!partialNeed.preferredLocations?.length) questions.push('In welchen Regionen suchen Sie?')
|
||||
if (!partialNeed.timing) questions.push('Was ist Ihr gewünschter Einzugstermin?')
|
||||
if (!partialNeed.budgetRange) questions.push('Was ist Ihr maximales monatliches Budget?')
|
||||
return { data: questions }
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user