9df5d285f5
Score formula simplified to: finalScore = round(hard×60% + soft×40%) Removes dataQualityModifier, confidenceModifier, mustHaveEval.scoreImpact — the breakdown panel now always matches the displayed score. Also removes 'industrie groove' from Zürich-only location signals — the phrase describes an aesthetic preference, not a district reference. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
377 lines
20 KiB
TypeScript
377 lines
20 KiB
TypeScript
import type { ParseNeedResult, ParsedNeedCriteria, FollowUpQuestion } from '../../../domain/needBuilder'
|
||
import type { AssetType } from '../../../domain/enums'
|
||
|
||
export function mockParseNeed(input: string): ParseNeedResult {
|
||
const lower = input.toLowerCase()
|
||
|
||
// Asset type — RETAIL before LOGISTICS to avoid false match on "Nebenräume (Lager)"
|
||
const assetType: AssetType | undefined =
|
||
lower.includes('retail') || lower.includes('laden') || lower.includes('shop')
|
||
|| lower.includes('verkaufslokal') || lower.includes('ladenlokal') || lower.includes('ladenfläche')
|
||
|| lower.includes('verkaufsfläche') || lower.includes('schaufenster') ? 'RETAIL'
|
||
: lower.includes('büro') || lower.includes('office') || lower.includes('sitzungszimmer') || lower.includes('arbeitsplätze') ? 'OFFICE'
|
||
: lower.includes('logistik') || lower.includes('lagerhalle') || lower.includes('lagerraum')
|
||
|| (lower.includes('lager') && (lower.includes('logistik') || lower.includes('rampe') || lower.includes('palette') || lower.includes('lkw'))) ? 'LOGISTICS'
|
||
: lower.includes('lager') ? 'LOGISTICS'
|
||
: lower.includes('produktion') || lower.includes('gewerbe') || lower.includes('industrie') ? 'PRODUCTION'
|
||
: lower.includes('gastro') || lower.includes('restaurant') ? 'GASTRO'
|
||
: undefined
|
||
|
||
// Area
|
||
const areaRangeMatch = input.match(/(\d+)\s*[–\-–]\s*(\d+)\s*m[²2]/i)
|
||
const areaSingleMatch = input.match(/(\d{3,5})\s*m[²2]/i)
|
||
let areaRange: { min: number; max: number } | undefined
|
||
let areaConfidence = 0.25
|
||
if (areaRangeMatch) {
|
||
areaRange = { min: parseInt(areaRangeMatch[1]), max: parseInt(areaRangeMatch[2]) }
|
||
areaConfidence = 0.95
|
||
} else if (areaSingleMatch) {
|
||
const base = parseInt(areaSingleMatch[1])
|
||
areaRange = { min: Math.round(base * 0.8), max: Math.round(base * 1.2) }
|
||
areaConfidence = 0.70
|
||
}
|
||
|
||
// Locations — use word boundaries to avoid false matches like "Umzugsfirma" → "Zug"
|
||
const CITIES: [string, string][] = [
|
||
['zürich', 'Zürich'], ['basel', 'Basel'], ['bern', 'Bern'], ['genf', 'Genf'],
|
||
['lausanne', 'Lausanne'], ['winterthur', 'Winterthur'], ['zug', 'Zug'],
|
||
['luzern', 'Luzern'], ['st. gallen', 'St. Gallen'], ['lugano', 'Lugano'],
|
||
['biel', 'Biel'], ['schaffhausen', 'Schaffhausen'],
|
||
['wädenswil', 'Wädenswil'], ['thalwil', 'Thalwil'], ['uster', 'Uster'],
|
||
['horgen', 'Horgen'], ['küsnacht', 'Küsnacht'], ['baar', 'Baar'],
|
||
]
|
||
const preferredLocations = CITIES.filter(([k]) => {
|
||
if (k.includes(' ') || k.includes('.')) return lower.includes(k)
|
||
// Word boundary: not preceded/followed by a letter (including German umlauts)
|
||
const re = new RegExp(`(?<![a-zA-ZäöüÄÖÜß])${k}(?![a-zA-ZäöüÄÖÜß])`)
|
||
return re.test(lower)
|
||
}).map(([, v]) => v)
|
||
|
||
// Infer Zürich when Zürich-specific districts or landmarks are mentioned
|
||
const ZURICH_SIGNALS = [
|
||
'seefeld', 'bellevue', 'paradeplatz', 'bahnhofstrasse', 'zürich-west', 'zürich west',
|
||
'oerlikon', 'altstetten', 'kreis 1', 'kreis 2', 'kreis 3', 'kreis 4', 'kreis 5',
|
||
'kreis 6', 'kreis 7', 'kreis 8', 'langstrasse', 'hardbrücke', 'freilager',
|
||
'europaallee', 'zürich nord', 'zürich süd',
|
||
]
|
||
if (!preferredLocations.includes('Zürich') && ZURICH_SIGNALS.some(s => lower.includes(s))) {
|
||
preferredLocations.push('Zürich')
|
||
}
|
||
|
||
// Contextual Zürich inference: only truly Zürich-specific place/brand names
|
||
const ZURICH_ONLY_SIGNALS = [
|
||
'pfingstweidstrasse', 'freilager', 'europaallee',
|
||
'zürich-west', 'zürich west', 'zürich nord', 'zürich süd',
|
||
'hürlimann', 'viadukt', 'schiffbau',
|
||
]
|
||
if (preferredLocations.length === 0 && ZURICH_ONLY_SIGNALS.some(s => lower.includes(s))) {
|
||
preferredLocations.push('Zürich')
|
||
}
|
||
|
||
// Extract district list from patterns like "Kreis 3, 4, 5, und 8" or "Kreis 3/4/5"
|
||
const kreisListMatch = lower.match(/\bkreis\s+([\d]+(?:\s*[,\/]\s*[\d]+)*(?:\s+und\s+[\d]+)?)/)
|
||
if (kreisListMatch) {
|
||
const nums = kreisListMatch[1].match(/\d+/g) ?? []
|
||
nums.forEach(n => {
|
||
const k = `Zürich Kreis ${n}`
|
||
if (!preferredLocations.includes(k)) preferredLocations.push(k)
|
||
})
|
||
if (!preferredLocations.includes('Zürich')) preferredLocations.push('Zürich')
|
||
}
|
||
|
||
// Map Zürich landmark/neighbourhood names → district-level location entries
|
||
const ZURICH_DISTRICT_MAP: [string, string][] = [
|
||
['seefeld', 'Zürich Seefeld'], ['bellevue', 'Zürich Seefeld'],
|
||
['bahnhofstrasse', 'Zürich Kreis 1'], ['paradeplatz', 'Zürich Kreis 1'],
|
||
['zürich-west', 'Zürich-West'], ['pfingstweidstrasse', 'Zürich-West'],
|
||
['limmatstrasse', 'Zürich-West'],
|
||
]
|
||
ZURICH_DISTRICT_MAP.forEach(([k, v]) => {
|
||
if (lower.includes(k) && !preferredLocations.includes(v)) preferredLocations.push(v)
|
||
})
|
||
|
||
const locationConfidence = preferredLocations.length > 0 ? 0.88 : 0.15
|
||
|
||
// Budget — strip Swiss price notation (320.--) before matching
|
||
const cleanedBudget = input.replace(/(\d)\.-+/g, '$1').replace(/(\d)\.—/g, '$1')
|
||
// "MZ 320/m²", "CHF 320/m²", "320/m²", "320.--/m²/a"
|
||
const budgetPerSqmMatch = cleanedBudget.match(/(?:mz|mietzins|max\.?|budget)?\s*(?:CHF\s*)?(\d{2,4})\s*\/\s*m[²2]/i)
|
||
// Range "250 – 280 CHF" or "250-280/m²" — require explicit currency or /m² so area ranges like "120–150m2" are not matched
|
||
const budgetRangeMatch =
|
||
cleanedBudget.match(/(\d{2,4})\s*[–\-]\s*(\d{2,4})\s*(?:CHF|Fr\.?)\b/i) ??
|
||
cleanedBudget.match(/(?:CHF|Fr\.?)\s*(\d{2,4})\s*[–\-]\s*(\d{2,4})/i) ??
|
||
cleanedBudget.match(/(\d{2,4})\s*[–\-]\s*(\d{2,4})\s*\/\s*m[²2]/i)
|
||
const budgetMaxMatch = input.match(/(?:max\.?|bis|höchstens)\s*(?:CHF\s*)?(\d+)/i)
|
||
let budgetRange: { maxPerSqm: number; currency: string } | undefined
|
||
let budgetConfidence = 0.20
|
||
// Monthly-to-annual threshold: Swiss retail quotes monthly (150/m²/mon → 1800/year),
|
||
// office/logistics quote monthly too but at lower values (45/m²/mon → 540/year).
|
||
const monthlyThreshold = assetType === 'RETAIL' ? 500 : 150
|
||
if (budgetPerSqmMatch) {
|
||
const raw = parseInt(budgetPerSqmMatch[1])
|
||
budgetRange = { maxPerSqm: raw < monthlyThreshold ? raw * 12 : raw, currency: 'CHF' }
|
||
budgetConfidence = 0.92
|
||
} else if (budgetRangeMatch) {
|
||
// Take upper value of range as max
|
||
const raw = parseInt(budgetRangeMatch[2])
|
||
budgetRange = { maxPerSqm: raw < monthlyThreshold ? raw * 12 : raw, currency: 'CHF' }
|
||
budgetConfidence = 0.75
|
||
} else if (budgetMaxMatch) {
|
||
const raw = parseInt(budgetMaxMatch[1])
|
||
budgetRange = { maxPerSqm: raw < monthlyThreshold ? raw * 12 : raw, currency: 'CHF' }
|
||
budgetConfidence = 0.60
|
||
}
|
||
|
||
// Timing
|
||
const yearMatch = input.match(/(?:ab\s+)?(?:Q[1-4]\s*\/?\s*)?(20\d{2})/)
|
||
// "Q4/26" or "Q1/27" — two-digit year (collect all occurrences, use first as earliest)
|
||
const quarterMatches = [...input.matchAll(/Q([1-4])\s*[\/\-]?\s*(\d{2})\b/gi)]
|
||
const soonMatch = lower.includes('sofort') || lower.includes('asap')
|
||
let timing: ParsedNeedCriteria['timing'] | undefined
|
||
let timingConfidence = 0.20
|
||
if (soonMatch) {
|
||
timing = { earliestMoveIn: '2025-07-01', latestMoveIn: '2025-10-01', flexibleTiming: false }
|
||
timingConfidence = 0.85
|
||
} else if (yearMatch) {
|
||
timing = { earliestMoveIn: `${yearMatch[1]}-01-01`, latestMoveIn: `${yearMatch[1]}-12-31`, flexibleTiming: lower.includes('flexibel') }
|
||
timingConfidence = 0.75
|
||
} else if (quarterMatches.length > 0) {
|
||
// Use first Q as earliest, last Q as latest (6-month window minimum)
|
||
const qStartMonth = [0, 1, 4, 7, 10] // [unused, Q1, Q2, Q3, Q4]
|
||
const first = quarterMatches[0]
|
||
const last = quarterMatches[quarterMatches.length - 1]
|
||
const yr1 = 2000 + parseInt(first[2])
|
||
const yr2 = 2000 + parseInt(last[2])
|
||
const q1 = parseInt(first[1])
|
||
const q2 = parseInt(last[1])
|
||
const startM = String(qStartMonth[q1]).padStart(2, '0')
|
||
const endM = String(Math.min(12, qStartMonth[q2] + 2)).padStart(2, '0')
|
||
timing = {
|
||
earliestMoveIn: `${yr1}-${startM}-01`,
|
||
latestMoveIn: `${yr2}-${endM}-${endM === '12' ? '31' : '28'}`,
|
||
flexibleTiming: true,
|
||
}
|
||
timingConfidence = 0.70
|
||
}
|
||
|
||
// Must-haves
|
||
const mustHaveCriteria: string[] = []
|
||
if (lower.includes('öv') || lower.includes('bahnhof') || lower.includes('tram') || lower.includes('erschlossen')) mustHaveCriteria.push('Gute ÖV-Anbindung')
|
||
if (lower.includes('parking') || lower.includes('parkplatz') || lower.includes('tiefgarage') || lower.includes('stellplatz')) mustHaveCriteria.push('Parkplätze vorhanden')
|
||
if (lower.includes('klimaanlage') || lower.includes('klima')) mustHaveCriteria.push('Klimaanlage')
|
||
if (lower.includes('server') || lower.includes('rechenzentr')) mustHaveCriteria.push('Serverraum / IT-Infrastruktur')
|
||
if (lower.includes('barrierefrei')) mustHaveCriteria.push('Barrierefreiheit')
|
||
if (lower.includes('küche') || lower.includes('kantine')) mustHaveCriteria.push('Kantine / Küche')
|
||
if (lower.includes('laderampe') || lower.includes('rampe') || lower.includes('verladetor')) mustHaveCriteria.push('Laderampe')
|
||
if (lower.includes('schaufenster') || lower.includes('shopfenster') || lower.includes('vitrine')) mustHaveCriteria.push('Schaufenster')
|
||
if (lower.includes('verpflegung') || lower.includes('restaurant') || lower.includes('lunch') || lower.includes('mittagessen') || lower.includes('takeaway')) mustHaveCriteria.push('Verpflegungsmöglichkeiten')
|
||
if (lower.includes('startup') || lower.includes('start-up') || lower.includes('community') || lower.includes('coworking') || lower.includes('junge firmen') || lower.includes('jungunternehm')) mustHaveCriteria.push('Startup-Community / innovative Nachbarn')
|
||
if (lower.includes('wachsen') || lower.includes('expansion') || lower.includes('erweiterungsoption') || lower.includes('wachstum') || lower.includes('wachstumsoption') || lower.includes('flexibilität zum wachsen')) mustHaveCriteria.push('Erweiterungsoption vorhanden')
|
||
if (lower.includes('werkstatt') || lower.includes('atelier') || lower.includes('reparatur')) mustHaveCriteria.push('Werkstatt / Atelier')
|
||
|
||
// Structured requirement fields
|
||
const requireGroundFloor = lower.includes('erdgeschoss') || lower.includes('parterre')
|
||
|| lower.includes('schaufenster') || lower.includes('ladenlokal') || undefined as boolean | undefined
|
||
const requireAirConditioning = lower.includes('klimaanlage') || lower.includes('klimatisierung')
|
||
|| lower.includes('klima') || lower.includes('air conditioning')
|
||
|| lower.includes('kühlung') || undefined as boolean | undefined
|
||
const requireLoadingDock = lower.includes('laderampe') || lower.includes('verladerampe')
|
||
|| lower.includes('ladetor') || lower.includes('rampe') || undefined as boolean | undefined
|
||
const requireBarrierFree = lower.includes('barrierefrei') || lower.includes('rollstuhl')
|
||
|| lower.includes('iv-gerecht') || lower.includes('behindertengerecht') || undefined as boolean | undefined
|
||
|
||
const ceilingMatch = input.match(/(\d+(?:[.,]\d+)?)\s*m(?:eter)?\s*(?:deckenhöhe|hallenhöhe|lichte\s+höhe)/i)
|
||
?? input.match(/deckenhöhe\s+(?:mind\.?\s*)?(\d+(?:[.,]\d+)?)\s*m/i)
|
||
const minCeilingHeightM = ceilingMatch ? parseFloat(ceilingMatch[1].replace(',', '.')) : undefined
|
||
|
||
// Parking: "3-5 Parkplätze" → extract lower bound (minimum); "5 Parkplätze" → 5
|
||
const parkingRangeMatch = input.match(/(\d+)\s*[-–]\s*\d+\s*(?:parkplätze?|stellplätze?|pp\b)/i)
|
||
const parkingSingleMatch = input.match(/(?:mind(?:estens)?\.?\s+)?(\d+)\s*(?:parkplätze?|stellplätze?|pp\b)/i)
|
||
const requiredParkingMin = parkingRangeMatch ? parseInt(parkingRangeMatch[1])
|
||
: parkingSingleMatch ? parseInt(parkingSingleMatch[1]) : undefined
|
||
|
||
const fitOutStr: 'BASIC' | 'FULL' | 'PREMIUM' | undefined =
|
||
lower.includes('schlüsselfertig') || lower.includes('premium') || lower.includes('hochwertig')
|
||
|| lower.includes('top ausgebaut') || lower.includes('top-ausgebaut') ? 'PREMIUM'
|
||
: lower.includes('vollausbau') || lower.includes('vollständig ausgebaut') || lower.includes('ausgebaut')
|
||
|| lower.includes('ready to use') || lower.includes('reddy to use') || lower.includes('bezugsfertig') ? 'FULL'
|
||
: lower.includes('basisausbau') || lower.includes('rohbau') || lower.includes('einfach') ? 'BASIC'
|
||
: undefined
|
||
|
||
// Contract duration: "7-jähriger Vertrag", "Laufzeit 7 Jahre", standalone "7 Jahre" at sentence start
|
||
// Exclude "in X Jahren", "X Jahre im Geschäft", "X Jahre Erfahrung" etc.
|
||
const contractMatch = input.match(/(\d+)[- ]?j[aä]hrige?(?:r)?\s+(?:vertrag|mietvertrag|laufzeit)/i)
|
||
?? input.match(/(?:vertrag|laufzeit|mietdauer).{0,20}(\d+)\s*jahre?/i)
|
||
?? input.match(/\b(\d+)\s*jahre?\b(?!\s*(?:erfahrung|planung|rendite|im\b|alt\b|alten|altes|jung))/i)
|
||
const minContractDurationMonths = contractMatch ? parseInt(contractMatch[1]) * 12 : undefined
|
||
|
||
// Soft
|
||
const prestigeImportance: 'LOW' | 'MEDIUM' | 'HIGH' | undefined =
|
||
lower.includes('prestige') || lower.includes('representativ') || lower.includes('repräsentativ')
|
||
|| lower.includes('topadresse') || lower.includes('top adresse') || lower.includes('innerstädtisch')
|
||
|| lower.includes('topaddresse') ? 'HIGH'
|
||
: lower.includes('standard') ? 'LOW'
|
||
: undefined
|
||
const parkingNeed = lower.includes('parking') || lower.includes('parkplatz')
|
||
const visibilityNeed: 'HIGH' | undefined = lower.includes('sichtbar') || lower.includes('passanten') ? 'HIGH' : undefined
|
||
const footfallNeed: 'HIGH' | undefined = lower.includes('frequenz') || lower.includes('laufkundschaft') ? 'HIGH' : undefined
|
||
|
||
// Missing fields
|
||
const missingFields: string[] = []
|
||
if (!assetType) missingFields.push('Nutzungstyp')
|
||
if (!areaRange) missingFields.push('Flächenbedarf')
|
||
if (preferredLocations.length === 0) missingFields.push('Standort')
|
||
if (!budgetRange) missingFields.push('Budget')
|
||
if (!timing) missingFields.push('Verfügbarkeitstermin')
|
||
|
||
// Assumptions
|
||
const assumptions: string[] = []
|
||
if (areaRange && areaSingleMatch && !areaRangeMatch) {
|
||
assumptions.push(`Flächenrange aus Einzelangabe (${areaSingleMatch[1]} m²) geschätzt — bitte prüfen`)
|
||
}
|
||
if (budgetRange && !budgetPerSqmMatch && budgetMaxMatch) {
|
||
assumptions.push('Budget als Pauschalangabe interpretiert — Angabe pro m² unklar')
|
||
}
|
||
if (!assetType) {
|
||
assumptions.push('Nutzungstyp konnte nicht eindeutig erkannt werden')
|
||
}
|
||
|
||
// Confidence by field
|
||
const confidenceByField: Record<string, number> = {
|
||
assetType: assetType ? 0.92 : 0.20,
|
||
areaRange: areaConfidence,
|
||
preferredLocations: locationConfidence,
|
||
budgetRange: budgetConfidence,
|
||
timing: timingConfidence,
|
||
mustHaveCriteria: mustHaveCriteria.length > 0 ? 0.85 : 0.10,
|
||
prestigeImportance: prestigeImportance ? 0.80 : 0.20,
|
||
parkingNeed: parkingNeed ? 0.90 : 0.30,
|
||
}
|
||
|
||
// Follow-up questions
|
||
const followUpQuestionCandidates: FollowUpQuestion[] = []
|
||
|
||
if (!assetType) {
|
||
followUpQuestionCandidates.push({
|
||
id: 'fq-asset-type',
|
||
questionText: 'Welchen Nutzungstyp suchen Sie?',
|
||
targetField: 'assetType',
|
||
reason: 'Der Nutzungstyp konnte nicht eindeutig erkannt werden.',
|
||
suggestedAnswerOptions: ['Büro', 'Logistik / Lager', 'Retail', 'Produktion', 'Gastro / F&B'],
|
||
importance: 'required',
|
||
})
|
||
}
|
||
if (preferredLocations.length === 0) {
|
||
followUpQuestionCandidates.push({
|
||
id: 'fq-location',
|
||
questionText: 'In welcher Region oder Stadt suchen Sie?',
|
||
targetField: 'preferredLocations',
|
||
reason: 'Kein konkreter Standort angegeben.',
|
||
suggestedAnswerOptions: ['Zürich', 'Basel', 'Bern', 'Zug', 'Luzern', 'Genf'],
|
||
importance: 'required',
|
||
})
|
||
}
|
||
if (!timing) {
|
||
followUpQuestionCandidates.push({
|
||
id: 'fq-timing',
|
||
questionText: 'Ab wann benötigen Sie die Fläche?',
|
||
targetField: 'timing',
|
||
reason: 'Kein Verfügbarkeitsdatum erkannt.',
|
||
suggestedAnswerOptions: ['Sofort', 'In 3 Monaten', 'In 6 Monaten', 'In 12 Monaten', 'Flexibel'],
|
||
importance: 'recommended',
|
||
})
|
||
}
|
||
if (!budgetRange) {
|
||
followUpQuestionCandidates.push({
|
||
id: 'fq-budget',
|
||
questionText: 'Was ist Ihr Maximalbudget pro m² und Jahr?',
|
||
targetField: 'budgetRange',
|
||
reason: 'Kein Budget erkannt.',
|
||
suggestedAnswerOptions: ['< CHF 300/m²/J', 'CHF 300–420/m²/J', 'CHF 420–540/m²/J', '> CHF 540/m²/J', 'Flexibel'],
|
||
importance: 'recommended',
|
||
})
|
||
}
|
||
followUpQuestionCandidates.push({
|
||
id: 'fq-parking',
|
||
questionText: 'Benötigen Sie Parkplätze vor Ort?',
|
||
targetField: 'parkingNeed',
|
||
reason: 'Angabe zu Parkplatzbedarf verbessert die Matchqualität.',
|
||
suggestedAnswerOptions: ['Ja, zwingend', 'Ja, wenn möglich', 'Nein'],
|
||
importance: 'optional',
|
||
})
|
||
|
||
// Special notes: apartment wish (UC-A type) + lease options (UC-C type)
|
||
const notesParts: string[] = []
|
||
const apartmentMatch = input.match(/(\d[.,]\d)\s*zimmer[- ]?wohn/i)
|
||
?? (lower.includes('zimmer') && lower.includes('wohnung') ? [''] : null)
|
||
if (apartmentMatch) {
|
||
const sizeHint = apartmentMatch[1] ? `${apartmentMatch[1]}-Zi-Wohnung` : 'Wohnung'
|
||
const rentMatch = input.match(/(\d['.\s]?\d{3})[.,\-\s]*(?:inkl|inkl\.)/i)
|
||
?? input.match(/(\d{4})[.,\-]\s*(?:chf|fr)?/i)
|
||
const rentHint = rentMatch ? ` max. CHF ${rentMatch[1].replace(/['\s]/g, "'")}` : ''
|
||
notesParts.push(`Wunsch: ${sizeHint} im Haus oder in der Nähe${rentHint} inkl. NK`)
|
||
}
|
||
const leaseOptionMatch = input.match(/(\d+)\s*[*×x]\s*(\d+)\s*jahre?\s*(?:echte\s*)?optione?n?/i)
|
||
if (leaseOptionMatch) {
|
||
notesParts.push(`Mietoption: ${leaseOptionMatch[1]}×${leaseOptionMatch[2]} Jahre echte Optionen gefordert`)
|
||
}
|
||
const notes = notesParts.length > 0 ? notesParts.join(' | ') : undefined
|
||
|
||
// Semantic signal flags used for weight boosting
|
||
const hasExpansionSignal = lower.includes('wachsen') || lower.includes('wachstum') || lower.includes('expansion') || lower.includes('erweiterung')
|
||
const hasCommunitySignal = lower.includes('startup') || lower.includes('community') || lower.includes('coworking') || lower.includes('junge firmen')
|
||
const hasPrestigeSignal = prestigeImportance === 'HIGH' || lower.includes('repräsentativ') || lower.includes('charakter') || lower.includes('beeindrucken')
|
||
const hasFlexSignal = lower.includes('flexibel') || lower.includes('wachsen') || hasCommunitySignal
|
||
|
||
// Suggested weights
|
||
const suggestedWeights: Record<string, number> = {
|
||
area: 0.18,
|
||
location: preferredLocations.length > 0 ? 0.25 : 0.20,
|
||
budget: budgetRange ? 0.20 : 0.16,
|
||
timing: timing ? 0.14 : 0.10,
|
||
prestige: hasPrestigeSignal ? 0.10 : 0.04,
|
||
accessibility: mustHaveCriteria.includes('Gute ÖV-Anbindung') ? 0.08 : 0.04,
|
||
expansionPotential: hasExpansionSignal ? 0.08 : 0.03,
|
||
flexibility: hasFlexSignal ? 0.08 : 0.03,
|
||
talentAccess: hasCommunitySignal ? 0.06 : 0.03,
|
||
}
|
||
|
||
const rawSummary = `Bedarf analysiert: ${assetType ?? 'Typ unbekannt'} · ${preferredLocations.join(', ') || 'Standort unklar'} · ${areaRange ? `${areaRange.min}–${areaRange.max} m²` : 'Fläche unklar'} · ${budgetRange ? `max. CHF ${budgetRange.maxPerSqm}/m²` : 'Budget unklar'}`
|
||
|
||
return {
|
||
extractedCriteria: {
|
||
assetType,
|
||
areaRange,
|
||
preferredLocations,
|
||
budgetRange,
|
||
timing,
|
||
mustHaveCriteria,
|
||
infrastructureRequirements: [],
|
||
accessibilityRequirements: [],
|
||
prestigeImportance: hasPrestigeSignal ? 'HIGH' : prestigeImportance,
|
||
flexibilityNeed: hasFlexSignal ? 'HIGH' : 'MEDIUM',
|
||
expansionPotential: hasExpansionSignal,
|
||
parkingNeed,
|
||
visibilityNeed,
|
||
footfallNeed,
|
||
requireGroundFloor: requireGroundFloor || undefined,
|
||
requireAirConditioning: requireAirConditioning || undefined,
|
||
requireLoadingDock: requireLoadingDock || undefined,
|
||
requireBarrierFree: requireBarrierFree || undefined,
|
||
requiredParkingMin,
|
||
requiredFitOut: fitOutStr,
|
||
minCeilingHeightM,
|
||
minContractDurationMonths,
|
||
notes,
|
||
},
|
||
confidenceByField,
|
||
missingFields,
|
||
assumptions,
|
||
suggestedWeights,
|
||
followUpQuestionCandidates,
|
||
rawSummary,
|
||
promptVersion: 'mock-v1.0',
|
||
schemaVersion: '1.0.0',
|
||
}
|
||
}
|