feat(ai): AI pre-market rent recommendation from regional comparables, supply & demand
- IAIService.recommendPreMarketRent: recommended price + range, verdict (UNDERPRICED/FAIR/AMBITIOUS), drivers, rationale, confidence
- MockAIService: deterministic recommendation from locationIntelligence — regional comp median, vacancy (supply), demand strength + days-on-market, rent trend (forward for pre-market)
- BackendAIService: LLM prompt with market context + mock fallback
- usePreMarketRentRecommendation hook; PreMarketPriceAdvisor component shows the recommendation per released unit with verdict ("zu günstig" when underpriced) + adjustable expected price + "Empfehlung übernehmen"
- Replaces the simple indexed suggestion with a market-driven AI recommendation that flags underpricing
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
import { useState } from 'react'
|
||||
import { Box, Button, Chip, CircularProgress, TextField, Typography } from '@mui/material'
|
||||
import { Sparkles } from 'lucide-react'
|
||||
import type { Property, PropertyUnit } from '../../domain/property'
|
||||
import { usePreMarketRentRecommendation } from '../../hooks/useAI'
|
||||
import { useUpdateUnit } from '../../hooks/useProperties'
|
||||
import { resolveUnitFacts } from '../../lib/unitFacts'
|
||||
import { DS_PRE_MARKET, DS_SURFACE, DS_TEXT } from '../../lib/ds'
|
||||
|
||||
const VERDICT_META: Record<string, { label: string; bg: string; fg: string }> = {
|
||||
UNDERPRICED: { label: 'zu günstig', bg: '#fef3c7', fg: '#92400e' },
|
||||
FAIR: { label: 'marktgerecht', bg: '#dcfce7', fg: '#166534' },
|
||||
AMBITIOUS: { label: 'ambitioniert', bg: '#fee2e2', fg: '#991b1b' },
|
||||
}
|
||||
|
||||
interface Props {
|
||||
property: Property
|
||||
unit: PropertyUnit
|
||||
}
|
||||
|
||||
/** KI-Preisempfehlung für eine Pre-Market-Einheit (regionale Vergleichsmieten, Angebot, Nachfrage). */
|
||||
export function PreMarketPriceAdvisor({ property, unit }: Props) {
|
||||
const facts = resolveUnitFacts(property, unit)
|
||||
const { data, isLoading, isError } = usePreMarketRentRecommendation({
|
||||
city: property.location?.city ?? '',
|
||||
assetType: property.assetType,
|
||||
areaSqm: unit.areaSqm,
|
||||
currentRentPerSqm: facts.rentPricePerSqm,
|
||||
availableFrom: unit.schattenmarktRelease?.availableFrom,
|
||||
})
|
||||
const updateUnit = useUpdateUnit(property.id)
|
||||
const [draft, setDraft] = useState(unit.expectedRentPerSqm != null ? String(unit.expectedRentPerSqm) : '')
|
||||
|
||||
function saveExpected(value: string) {
|
||||
updateUnit.mutate({ unitId: unit.id, data: { expectedRentPerSqm: parseInt(value) || undefined } })
|
||||
}
|
||||
|
||||
const rec = data?.data
|
||||
const verdict = rec ? VERDICT_META[rec.verdict] : null
|
||||
|
||||
return (
|
||||
<Box sx={{ mt: 0.75, p: 1, borderRadius: 1, bgcolor: DS_SURFACE.purple.bg, border: `1px solid ${DS_SURFACE.purple.border}` }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, mb: 0.5 }}>
|
||||
<Sparkles size={12} color={DS_PRE_MARKET.accent} />
|
||||
<Typography sx={{ fontSize: '0.65rem', fontWeight: 700, color: '#5b21b6' }}>
|
||||
KI-Preisempfehlung Pre-Market
|
||||
</Typography>
|
||||
{verdict && (
|
||||
<Chip label={verdict.label} size="small" sx={{ height: 16, fontSize: '0.6rem', fontWeight: 700, bgcolor: verdict.bg, color: verdict.fg, ml: 'auto' }} />
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{isLoading ? (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, py: 0.5 }}>
|
||||
<CircularProgress size={12} sx={{ color: DS_PRE_MARKET.accent }} />
|
||||
<Typography sx={{ fontSize: '0.65rem', color: '#6d28d9' }}>Marktanalyse läuft…</Typography>
|
||||
</Box>
|
||||
) : isError || !rec ? (
|
||||
<Typography sx={{ fontSize: '0.65rem', color: DS_TEXT.muted }}>Keine Empfehlung verfügbar.</Typography>
|
||||
) : (
|
||||
<>
|
||||
<Typography sx={{ fontSize: '0.7rem', fontWeight: 700, color: '#5b21b6' }}>
|
||||
Empfehlung: CHF {rec.recommendedPerSqm}/m²
|
||||
<Box component="span" sx={{ fontWeight: 400, color: '#6d28d9' }}> (CHF {rec.rangeMinPerSqm}–{rec.rangeMaxPerSqm})</Box>
|
||||
</Typography>
|
||||
<Typography sx={{ fontSize: '0.62rem', color: '#6d28d9', mb: 0.5 }}>
|
||||
{rec.drivers.join(' · ')}
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.75 }}>
|
||||
<TextField
|
||||
type="number" size="small" label="Erwarteter Preis"
|
||||
placeholder={`z.B. ${rec.recommendedPerSqm}`}
|
||||
value={draft}
|
||||
onChange={e => setDraft(e.target.value)}
|
||||
onBlur={e => saveExpected(e.target.value)}
|
||||
slotProps={{ inputLabel: { shrink: true }, htmlInput: { min: 0, step: 10 } }}
|
||||
sx={{ width: 150, '& .MuiInputBase-input': { fontSize: '0.72rem', py: 0.5 } }}
|
||||
/>
|
||||
<Button
|
||||
size="small"
|
||||
onClick={() => { setDraft(String(rec.recommendedPerSqm)); saveExpected(String(rec.recommendedPerSqm)) }}
|
||||
sx={{ textTransform: 'none', fontSize: '0.68rem', color: DS_PRE_MARKET.accent }}
|
||||
>
|
||||
Empfehlung übernehmen
|
||||
</Button>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
@@ -1,14 +1,13 @@
|
||||
import { useState } from 'react'
|
||||
import { Box, Button, CircularProgress, Collapse, IconButton, Switch, TextField, Tooltip, Typography } from '@mui/material'
|
||||
import { EyeOff, Pencil, Sparkles } from 'lucide-react'
|
||||
import { Box, CircularProgress, Collapse, IconButton, Switch, TextField, Tooltip, Typography } from '@mui/material'
|
||||
import { EyeOff, Pencil } from 'lucide-react'
|
||||
import type { Property } from '../../domain/property'
|
||||
import { useUpdateUnit } from '../../hooks/useProperties'
|
||||
import { DS_BORDER, DS_PRE_MARKET, DS_SURFACE, DS_TEXT } from '../../lib/ds'
|
||||
import { DS_BORDER, DS_PRE_MARKET, DS_TEXT } from '../../lib/ds'
|
||||
import { FIT_OUT_LABELS } from '../../lib/constants'
|
||||
import { resolveUnitFacts } from '../../lib/unitFacts'
|
||||
import { suggestFutureRent } from '../../lib/rentEstimate'
|
||||
import { floorLabel } from './PropertyDetailHelpers'
|
||||
import { UnitFieldsEditor } from './UnitFieldsEditor'
|
||||
import { PreMarketPriceAdvisor } from './PreMarketPriceAdvisor'
|
||||
|
||||
type PropertyUnit = NonNullable<Property['units']>[number]
|
||||
|
||||
@@ -29,14 +28,6 @@ interface Props {
|
||||
|
||||
export function PreMarketUnitGrid({ property, units, unitStates, unitSaving, setUnitStates, saveUnit }: Props) {
|
||||
const [editing, setEditing] = useState<string | null>(null)
|
||||
const [expectedDraft, setExpectedDraft] = useState<Record<string, string>>(() =>
|
||||
Object.fromEntries(units.map(u => [u.id, u.expectedRentPerSqm != null ? String(u.expectedRentPerSqm) : ''])),
|
||||
)
|
||||
const updateUnit = useUpdateUnit(property.id)
|
||||
|
||||
function saveExpected(unitId: string, value: string) {
|
||||
updateUnit.mutate({ unitId, data: { expectedRentPerSqm: parseInt(value) || undefined } })
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={{ mt: 1.25, pt: 1.25, borderTop: `1px solid ${DS_PRE_MARKET.border}` }}>
|
||||
@@ -153,45 +144,8 @@ export function PreMarketUnitGrid({ property, units, unitStates, unitSaving, set
|
||||
{detailParts.join(' · ')}
|
||||
</Typography>
|
||||
|
||||
{/* KI-Preis-Ansicht: nur wenn freigegeben — Vorschlag, anpassbar wenn zu günstig */}
|
||||
{us.enabled && (() => {
|
||||
const current = f.rentPricePerSqm
|
||||
const suggestion = suggestFutureRent(property.location?.city ?? '', current)
|
||||
return (
|
||||
<Box sx={{ mt: 0.75, p: 1, borderRadius: 1, bgcolor: DS_SURFACE.purple.bg, border: `1px solid ${DS_SURFACE.purple.border}` }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, mb: 0.5 }}>
|
||||
<Sparkles size={12} color={DS_PRE_MARKET.accent} />
|
||||
<Typography sx={{ fontSize: '0.65rem', fontWeight: 700, color: '#5b21b6' }}>
|
||||
KI-Preisempfehlung Pre-Market
|
||||
</Typography>
|
||||
</Box>
|
||||
<Typography sx={{ fontSize: '0.65rem', color: '#6d28d9', mb: 0.625 }}>
|
||||
Heute CHF {current}/m²
|
||||
{suggestion ? ` → indexiert CHF ${suggestion}/m²` : ''} — anpassen, falls zu günstig.
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.75 }}>
|
||||
<TextField
|
||||
type="number" size="small" placeholder={suggestion ? `z.B. ${suggestion}` : 'CHF/m²'}
|
||||
label="Erwarteter Preis"
|
||||
value={expectedDraft[u.id] ?? ''}
|
||||
onChange={e => setExpectedDraft(prev => ({ ...prev, [u.id]: e.target.value }))}
|
||||
onBlur={e => saveExpected(u.id, e.target.value)}
|
||||
slotProps={{ inputLabel: { shrink: true }, htmlInput: { min: 0, step: 10 } }}
|
||||
sx={{ width: 150, '& .MuiInputBase-input': { fontSize: '0.72rem', py: 0.5 } }}
|
||||
/>
|
||||
{suggestion != null && (
|
||||
<Button
|
||||
size="small"
|
||||
onClick={() => { setExpectedDraft(prev => ({ ...prev, [u.id]: String(suggestion) })); saveExpected(u.id, String(suggestion)) }}
|
||||
sx={{ textTransform: 'none', fontSize: '0.68rem', color: DS_PRE_MARKET.accent }}
|
||||
>
|
||||
Vorschlag übernehmen
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
})()}
|
||||
{/* KI-Preisempfehlung: nur wenn freigegeben */}
|
||||
{us.enabled && <PreMarketPriceAdvisor property={property} unit={u} />}
|
||||
|
||||
{/* Inline editor (shared) */}
|
||||
<Collapse in={editing === u.id}>
|
||||
|
||||
+10
-1
@@ -1,6 +1,6 @@
|
||||
import { useMutation, useQuery } from '@tanstack/react-query'
|
||||
import { aiService, parseListingText } from '../services/aiService'
|
||||
import type { OfferEmailPayload, FitOutAdviceInput } from '../services/aiService'
|
||||
import type { OfferEmailPayload, FitOutAdviceInput, PreMarketRentInput } from '../services/aiService'
|
||||
|
||||
export function useParseNeed() {
|
||||
return useMutation({
|
||||
@@ -34,3 +34,12 @@ export function useFitOutAdvice(input: FitOutAdviceInput | null) {
|
||||
staleTime: Infinity,
|
||||
})
|
||||
}
|
||||
|
||||
export function usePreMarketRentRecommendation(input: PreMarketRentInput | null) {
|
||||
return useQuery({
|
||||
queryKey: ['preMarketRent', input],
|
||||
queryFn: () => aiService.recommendPreMarketRent(input!),
|
||||
enabled: !!input && !!input.city,
|
||||
staleTime: Infinity,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -176,6 +176,27 @@ export interface FitOutAdvice {
|
||||
estimatedNetInvestment: string
|
||||
}
|
||||
|
||||
// ── Pre-market rent recommendation ─────────────────────────────────────────────
|
||||
|
||||
export interface PreMarketRentInput {
|
||||
city: string
|
||||
assetType: string
|
||||
areaSqm: number
|
||||
currentRentPerSqm: number
|
||||
availableFrom?: string // ISO — Pre-Market liegt in der Zukunft
|
||||
}
|
||||
|
||||
export interface PreMarketRentRecommendation {
|
||||
recommendedPerSqm: number
|
||||
rangeMinPerSqm: number
|
||||
rangeMaxPerSqm: number
|
||||
verdict: 'UNDERPRICED' | 'FAIR' | 'AMBITIOUS' // Bewertung des heutigen Preises
|
||||
deltaVsCurrentPct: number // Empfehlung vs. heutiger Preis
|
||||
drivers: string[] // Vergleichsmiete, Angebot, Nachfrage, Trend …
|
||||
rationale: string
|
||||
confidence: 'LOW' | 'MEDIUM' | 'HIGH'
|
||||
}
|
||||
|
||||
// ── Legacy types (kept for backward compatibility) ────────────────────────────
|
||||
|
||||
export interface CriteriaExtractionResult {
|
||||
@@ -220,6 +241,9 @@ export interface IAIService {
|
||||
// Fit-out investment advice (demand side)
|
||||
generateFitOutAdvice(input: FitOutAdviceInput): Promise<AIResponse<FitOutAdvice>>
|
||||
|
||||
// Pre-market rent recommendation (supply side) — based on regional comparables, supply & demand
|
||||
recommendPreMarketRent(input: PreMarketRentInput): Promise<AIResponse<PreMarketRentRecommendation>>
|
||||
|
||||
// Legacy methods
|
||||
extractCriteria(input: string): Promise<AIResponse<CriteriaExtractionResult>>
|
||||
generateFollowUp(partialNeed: Partial<CreateNeedInput>): Promise<AIResponse<string[]>>
|
||||
|
||||
@@ -57,6 +57,8 @@ import type {
|
||||
MarketSignalClassification,
|
||||
FitOutAdviceInput,
|
||||
FitOutAdvice,
|
||||
PreMarketRentInput,
|
||||
PreMarketRentRecommendation,
|
||||
} from '../IAIService'
|
||||
import { ServiceErrorCode } from '../../types'
|
||||
import { AppError } from '../../errors'
|
||||
@@ -82,6 +84,7 @@ import { buildDecisionBriefPrompt } from '../prompts/decisionBriefPrompt'
|
||||
import { buildDataQualityPrompt } from '../prompts/dataQualityPrompt'
|
||||
import { buildMarketSignalPrompt } from '../prompts/marketSignalPrompt'
|
||||
import { MockAIService } from '../mock/MockAIService'
|
||||
import { getCityIntelligence, getMarketRent } from '../../../lib/locationIntelligence'
|
||||
|
||||
// ── Config ────────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -606,6 +609,41 @@ Bitte analysiere die Situation und empfiehl die beste Option für den Mieter.`
|
||||
}, () => MockAIService.generateFitOutAdvice(input))
|
||||
},
|
||||
|
||||
// ── recommendPreMarketRent ──────────────────────────────────────────────────
|
||||
recommendPreMarketRent(input: PreMarketRentInput): Promise<AIResponse<PreMarketRentRecommendation>> {
|
||||
return withFallback('recommendPreMarketRent', async () => {
|
||||
const intel = getCityIntelligence(input.city)
|
||||
const comp = getMarketRent(input.city, input.assetType)
|
||||
const system = `Du bist Schweizer Gewerbeimmobilien-Marktanalyst. Empfiehl einen Pre-Market-Mietpreis (CHF/m²/Jahr) auf Basis regionaler Vergleichsmieten, Angebot (Leerstand) und Nachfrage. Antworte als JSON:
|
||||
{
|
||||
"recommendedPerSqm": number,
|
||||
"rangeMinPerSqm": number,
|
||||
"rangeMaxPerSqm": number,
|
||||
"verdict": "UNDERPRICED" | "FAIR" | "AMBITIOUS",
|
||||
"deltaVsCurrentPct": number,
|
||||
"drivers": ["kurze Treiber auf Deutsch"],
|
||||
"rationale": "2-3 Sätze Begründung auf Deutsch",
|
||||
"confidence": "LOW" | "MEDIUM" | "HIGH"
|
||||
}`
|
||||
const user = `Stadt: ${input.city}
|
||||
Nutzung: ${input.assetType}
|
||||
Fläche: ${input.areaSqm} m²
|
||||
Heutiger Preis: CHF ${input.currentRentPerSqm}/m²
|
||||
Vergleichsmiete (Median): ${comp ?? 'unbekannt'}
|
||||
Leerstand: ${intel?.vacancyRatePct ?? '?'}%
|
||||
Nachfrage: ${intel?.demandStrength ?? '?'}
|
||||
Miettrend 12M: ${intel?.rentTrend12m ?? '?'}%
|
||||
Ø Vermietungsdauer: ${intel?.avgDaysOnMarket ?? '?'} Tage`
|
||||
const raw = await chat(system, user)
|
||||
const json = extractJSON<PreMarketRentRecommendation>(raw)
|
||||
if (!json || typeof json.recommendedPerSqm !== 'number') {
|
||||
const fb = await MockAIService.recommendPreMarketRent(input)
|
||||
return { ...fb, provenance: makeProvenance('mock', true, false, { fallbackReason: json ? 'schema_validation' : 'json_parse' }) }
|
||||
}
|
||||
return { data: json, provenance: makeProvenance('ai', false, true) }
|
||||
}, () => MockAIService.recommendPreMarketRent(input))
|
||||
},
|
||||
|
||||
// ── Legacy: extractCriteria ─────────────────────────────────────────────────
|
||||
extractCriteria(input: string): Promise<AIResponse<CriteriaExtractionResult>> {
|
||||
return withFallback('extractCriteria', async () => {
|
||||
|
||||
@@ -12,12 +12,15 @@ import type {
|
||||
MarketSignalClassification,
|
||||
FitOutAdviceInput,
|
||||
FitOutAdvice,
|
||||
PreMarketRentInput,
|
||||
PreMarketRentRecommendation,
|
||||
} from '../IAIService'
|
||||
import { mockProvenance } from '../IAIService'
|
||||
import { aiTraceStore } from '../tracing'
|
||||
import { mockParseNeed } from './needParser'
|
||||
import { buildComparisonSummary } from './compareBuilder'
|
||||
import { buildMockDecisionBrief } from './decisionBrief'
|
||||
import { getCityIntelligence, getMarketRent } from '../../../lib/locationIntelligence'
|
||||
|
||||
const SIMULATED_DELAY = { fast: 300, medium: 600, slow: 1800 }
|
||||
const delay = (ms: number) => new Promise(r => setTimeout(r, ms))
|
||||
@@ -338,6 +341,71 @@ export const MockAIService: IAIService = {
|
||||
return { data, provenance: mockProvenance() }
|
||||
}),
|
||||
|
||||
// ── recommendPreMarketRent ──────────────────────────────────────────────────
|
||||
recommendPreMarketRent: (input: PreMarketRentInput) =>
|
||||
traceMock('recommendPreMarketRent', async () => {
|
||||
await delay(SIMULATED_DELAY.fast)
|
||||
const intel = getCityIntelligence(input.city)
|
||||
const comp = getMarketRent(input.city, input.assetType)
|
||||
const current = input.currentRentPerSqm
|
||||
const ASSET_LABELS: Record<string, string> = { OFFICE: 'Bürofläche', LOGISTICS: 'Logistikfläche', LIGHT_INDUSTRIAL: 'Gewerbefläche', RETAIL: 'Retailfläche', PRODUCTION: 'Produktionsfläche' }
|
||||
const assetLabel = ASSET_LABELS[input.assetType] ?? 'Fläche'
|
||||
|
||||
// Ohne regionale Vergleichsdaten: nur grobe Schätzung, niedrige Konfidenz
|
||||
if (!intel || comp == null) {
|
||||
const rec = Math.round(current * 1.02)
|
||||
const data: PreMarketRentRecommendation = {
|
||||
recommendedPerSqm: rec, rangeMinPerSqm: Math.round(rec * 0.93), rangeMaxPerSqm: Math.round(rec * 1.07),
|
||||
verdict: 'FAIR', deltaVsCurrentPct: 0,
|
||||
drivers: ['Keine regionalen Vergleichsdaten verfügbar'],
|
||||
rationale: 'Keine ausreichenden Marktdaten für diese Region — Empfehlung beruht auf dem heutigen Preis.',
|
||||
confidence: 'LOW',
|
||||
}
|
||||
return { data, provenance: mockProvenance() }
|
||||
}
|
||||
|
||||
// Angebot/Nachfrage-Anpassung auf die regionale Vergleichsmiete
|
||||
let adj = 0
|
||||
if (intel.vacancyRatePct < 2.5) adj += 0.06
|
||||
else if (intel.vacancyRatePct < 4) adj += 0.02
|
||||
else if (intel.vacancyRatePct > 5.5) adj -= 0.06
|
||||
else if (intel.vacancyRatePct > 4.5) adj -= 0.03
|
||||
adj += { VERY_HIGH: 0.06, HIGH: 0.03, MEDIUM: 0, LOW: -0.05 }[intel.demandStrength]
|
||||
if (intel.avgDaysOnMarket < 35) adj += 0.02
|
||||
else if (intel.avgDaysOnMarket > 75) adj -= 0.03
|
||||
const trendFwd = intel.rentTrend12m / 100 // Pre-Market liegt in der Zukunft → Trend vorwärts
|
||||
|
||||
const recommended = Math.round(comp * (1 + adj + trendFwd))
|
||||
const rangeMin = Math.round(recommended * 0.93)
|
||||
const rangeMax = Math.round(recommended * 1.07)
|
||||
const deltaVsCurrentPct = Math.round(((recommended - current) / current) * 100)
|
||||
const verdict: PreMarketRentRecommendation['verdict'] =
|
||||
deltaVsCurrentPct >= 6 ? 'UNDERPRICED' : deltaVsCurrentPct <= -6 ? 'AMBITIOUS' : 'FAIR'
|
||||
|
||||
const supplyLabel = intel.vacancyRatePct < 3 ? 'sehr knappes Angebot' : intel.vacancyRatePct > 5 ? 'entspanntes Angebot' : 'ausgeglichenes Angebot'
|
||||
const demandLabel = { VERY_HIGH: 'sehr hohe Nachfrage', HIGH: 'hohe Nachfrage', MEDIUM: 'mittlere Nachfrage', LOW: 'schwache Nachfrage' }[intel.demandStrength]
|
||||
const drivers = [
|
||||
`Vergleichsmiete Region: CHF ${comp}/m²`,
|
||||
`Leerstand ${intel.vacancyRatePct}% (${supplyLabel})`,
|
||||
demandLabel,
|
||||
`Miettrend ${intel.rentTrend12m >= 0 ? '+' : ''}${intel.rentTrend12m}% (12 M)`,
|
||||
`Ø Vermietungsdauer ${intel.avgDaysOnMarket} Tage`,
|
||||
]
|
||||
const verdictText =
|
||||
verdict === 'UNDERPRICED' ? `Ihr heutiger Preis (CHF ${current}/m²) liegt ${Math.abs(deltaVsCurrentPct)}% unter der Empfehlung — klarer Spielraum nach oben.`
|
||||
: verdict === 'AMBITIOUS' ? `Ihr heutiger Preis liegt ${Math.abs(deltaVsCurrentPct)}% über der Markteinschätzung — ambitioniert.`
|
||||
: 'Ihr heutiger Preis ist marktgerecht.'
|
||||
const rationale = `Auf Basis vergleichbarer ${assetLabel} in ${input.city} (Median CHF ${comp}/m²), ${supplyLabel} und ${demandLabel}. Empfehlung für Pre-Market: CHF ${recommended}/m² (CHF ${rangeMin}–${rangeMax}). ${verdictText}`
|
||||
const confidence: PreMarketRentRecommendation['confidence'] =
|
||||
intel.demandStrength === 'LOW' || intel.avgDaysOnMarket > 75 ? 'MEDIUM' : 'HIGH'
|
||||
|
||||
const data: PreMarketRentRecommendation = {
|
||||
recommendedPerSqm: recommended, rangeMinPerSqm: rangeMin, rangeMaxPerSqm: rangeMax,
|
||||
verdict, deltaVsCurrentPct, drivers, rationale, confidence,
|
||||
}
|
||||
return { data, provenance: mockProvenance() }
|
||||
}),
|
||||
|
||||
// Legacy methods
|
||||
extractCriteria: (_input: string) =>
|
||||
traceMock('extractCriteria', async () => ({
|
||||
|
||||
@@ -24,5 +24,7 @@ export type {
|
||||
MarketSignalClassification,
|
||||
FitOutAdvice,
|
||||
FitOutAdviceInput,
|
||||
PreMarketRentInput,
|
||||
PreMarketRentRecommendation,
|
||||
} from './ai/IAIService'
|
||||
export { parseListingText } from './ai/mock/listingParser'
|
||||
|
||||
Reference in New Issue
Block a user