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,21 @@
|
||||
export interface CompareSummaryPromptInput {
|
||||
properties: Array<{
|
||||
title: string
|
||||
matchScore: number
|
||||
city: string
|
||||
rentPerSqm: number
|
||||
positiveFactors: string[]
|
||||
negativeFactors: string[]
|
||||
}>
|
||||
}
|
||||
|
||||
export function buildCompareSummaryPrompt(input: CompareSummaryPromptInput): { system: string; user: string } {
|
||||
const propertyList = input.properties
|
||||
.map((p, i) => `Option ${i + 1}: ${p.title} (${p.city}) — Score: ${p.matchScore}%, CHF ${p.rentPerSqm}/m², Stärken: ${p.positiveFactors.join(', ')}, Risiken: ${p.negativeFactors.join(', ')}`)
|
||||
.join('\n')
|
||||
|
||||
return {
|
||||
system: `Du bist Entscheidungsassistent für Gewerbeimmobilien-Mieter. Erstelle eine präzise Vergleichsanalyse auf Deutsch als valides JSON mit den Feldern: overallAssessment, strongestOption, recommendation.`,
|
||||
user: `Vergleiche folgende Objekte und gib eine strukturierte Empfehlung:\n\n${propertyList}`,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
export interface DecisionBriefPromptInput {
|
||||
shortlistItems: Array<{
|
||||
title: string
|
||||
city: string
|
||||
matchScore: number
|
||||
areaSqm: number
|
||||
rentPerSqm: number
|
||||
topReasons: string[]
|
||||
}>
|
||||
needSummary: string
|
||||
}
|
||||
|
||||
export function buildDecisionBriefPrompt(input: DecisionBriefPromptInput): { system: string; user: string } {
|
||||
const itemList = input.shortlistItems
|
||||
.map(i => `- ${i.title} (${i.city}): Score ${i.matchScore}%, ${i.areaSqm}m², CHF ${i.rentPerSqm}/m² — ${i.topReasons.join(', ')}`)
|
||||
.join('\n')
|
||||
|
||||
return {
|
||||
system: `Du bist Senior Real Estate Advisor. Erstelle ein strukturiertes Entscheidungs-Briefing auf Deutsch als JSON mit: summary, sections (Zusammenfassung, Standortbewertung, Budgetanalyse, Empfohlene nächste Schritte).`,
|
||||
user: `Erstelle ein Entscheidungs-Briefing für folgende Shortlist:\n\nSuchprofil: ${input.needSummary}\n\nObjekte:\n${itemList}`,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
export interface MatchExplanationPromptInput {
|
||||
propertyTitle: string
|
||||
propertyCity: string
|
||||
matchScore: number
|
||||
positiveFactors: Array<{ criterion: string; explanation: string }>
|
||||
negativeFactors: Array<{ criterion: string; explanation: string }>
|
||||
needSummary: string
|
||||
}
|
||||
|
||||
export function buildMatchExplanationPrompt(input: MatchExplanationPromptInput): { system: string; user: string } {
|
||||
return {
|
||||
system: `Du bist ein Experte für Schweizer Gewerbeimmobilien. Erkläre Match-Ergebnisse präzise und entscheidungsorientiert auf Deutsch. Maximal 3 Sätze.`,
|
||||
user: `Erkläre warum das Objekt "${input.propertyTitle}" in ${input.propertyCity} einen Match Score von ${input.matchScore}% hat.
|
||||
|
||||
Stärken: ${input.positiveFactors.map(f => f.explanation).join(', ')}
|
||||
Schwächen: ${input.negativeFactors.map(f => f.explanation).join(', ')}
|
||||
Suchprofil: ${input.needSummary}`,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
export interface NeedParsingPromptInput {
|
||||
userInput: string
|
||||
}
|
||||
|
||||
export function buildNeedParsingPrompt(input: NeedParsingPromptInput): { system: string; user: string } {
|
||||
return {
|
||||
system: `Du bist ein Experte für Schweizer Gewerbeimmobilien. Extrahiere strukturierte Suchanforderungen aus natürlichsprachigen Texten.
|
||||
|
||||
Antworte immer als valides JSON mit folgendem Schema:
|
||||
{
|
||||
"assetType": "OFFICE" | "RETAIL" | "LOGISTICS" | "PRODUCTION" | "GASTRO" | null,
|
||||
"areaRange": { "min": number, "max": number } | null,
|
||||
"preferredLocations": string[],
|
||||
"budgetRange": { "maxPerSqm": number, "currency": "CHF" } | null,
|
||||
"timing": { "earliestMoveIn": "YYYY-MM-DD", "latestMoveIn": "YYYY-MM-DD", "flexibleTiming": boolean } | null,
|
||||
"mustHaveCriteria": string[],
|
||||
"missingFields": string[],
|
||||
"assumptions": string[]
|
||||
}`,
|
||||
user: `Analysiere folgende Suchanfrage und extrahiere alle relevanten Kriterien:\n\n${input.userInput}`,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user