import { useRef, useState } from 'react' import { Box, Typography } from '@mui/material' import { useNavigate } from 'react-router' import { useQueryClient } from '@tanstack/react-query' import { NeedBuilderProgress, NeedInput, VoiceNeedInput, WeightingEditor, NeedBuilderErrorState, } from '../../components/demand' import { AISearchActionBar } from '../../components/demand/AISearchActionBar' import { AISearchSavePreview } from '../../components/demand/AISearchSavePreview' import { useParseNeed } from '../../hooks/useAI' import { useCreateNeed } from '../../hooks/useNeeds' import { useDefaultWeights } from '../../hooks/useWeighting' import { NeedBuilderStep } from '../../domain/needBuilder' import type { ParseNeedResult, ParsedNeedCriteria, WeightingKey } from '../../domain/needBuilder' import { generateSummary, buildNeedInput } from '../../services/aiSearch/needSearchMapper' type ActionIntent = 'search' | 'save-profile' export default function AISearch() { const navigate = useNavigate() const queryClient = useQueryClient() const parseNeedMutation = useParseNeed() const createNeedMutation = useCreateNeed() const defaultWeights = useDefaultWeights() const [step, setStep] = useState(NeedBuilderStep.IDLE) const [intent, setIntent] = useState('search') const [inputText, setInputText] = useState('') const [isAutoGen, setIsAutoGen] = useState(false) const [criteria, setCriteria] = useState({}) const [parseResult, setParseResult] = useState(null) const [editedCriteria, setEditedCriteria] = useState(null) const [weights, setWeights] = useState>(defaultWeights) const [weightingKey, setWeightingKey] = useState(0) const [needTitle, setNeedTitle] = useState('') const [isAnonymous, setIsAnonymous] = useState(false) const [error, setError] = useState(null) const isManualTextRef = useRef(false) function handleCriteriaChange(next: ParsedNeedCriteria) { setCriteria(next) if (!isManualTextRef.current) { const summary = generateSummary(next) setInputText(summary) setIsAutoGen(!!summary) } } function handleTextChange(text: string) { isManualTextRef.current = text !== '' setIsAutoGen(false) setInputText(text) // Clear stale parse results when user edits the text manually setCriteria({}) setParseResult(null) } function handleAiAutofill() { setStep(NeedBuilderStep.PARSING) setError(null) parseNeedMutation.mutate(inputText, { onSuccess: (resp) => { const result = resp.data setParseResult(result) setCriteria({ ...result.extractedCriteria }) setWeights(result.suggestedWeights as Record) setWeightingKey(k => k + 1) isManualTextRef.current = false setInputText('') setIsAutoGen(false) setStep(NeedBuilderStep.IDLE) }, onError: () => { setError('Die KI-Analyse ist fehlgeschlagen.') setStep(NeedBuilderStep.ERROR) }, }) } function handleAction(chosenIntent: ActionIntent) { setIntent(chosenIntent) setError(null) const resolved: ParsedNeedCriteria = criteria const resolvedResult: ParseNeedResult | null = parseResult if (!hasStructuredData && inputText.trim()) { setStep(NeedBuilderStep.PARSING) parseNeedMutation.mutate(inputText, { onSuccess: (resp) => { const parsedResolved = resp.data.extractedCriteria const parsedResult = resp.data setCriteria(parsedResolved) setWeights(resp.data.suggestedWeights as Record) setWeightingKey(k => k + 1) isManualTextRef.current = false setInputText('') setIsAutoGen(false) setParseResult(resp.data) continueAction(chosenIntent, parsedResolved, parsedResult) }, onError: () => { setError('Die KI-Analyse ist fehlgeschlagen.') setStep(NeedBuilderStep.ERROR) }, }) return } continueAction(chosenIntent, resolved, resolvedResult) } function continueAction( chosenIntent: ActionIntent, resolved: ParsedNeedCriteria, resolvedResult: ParseNeedResult | null, ) { if (chosenIntent === 'search') { setStep(NeedBuilderStep.SAVING) const conf = resolvedResult ? Object.values(resolvedResult.confidenceByField).reduce((s, v) => s + v, 0) / Math.max(Object.values(resolvedResult.confidenceByField).length, 1) : 0.5 const input = buildNeedInput(resolved, weights, needTitle, conf, 'DRAFT') createNeedMutation.mutate(input, { onSuccess: (created) => { queryClient.invalidateQueries({ queryKey: ['matches'] }) navigate('/demand/results', { state: { fromNeedBuilder: true, activeNeedId: created.data.id } }) }, onError: () => { setError('Suche fehlgeschlagen.') setStep(NeedBuilderStep.ERROR) }, }) return } const confidenceByField: Record = resolvedResult?.confidenceByField ?? {} if (!resolvedResult) { if (resolved.assetType) confidenceByField.assetType = 1.0 if (resolved.areaRange?.min) confidenceByField.areaRange = 1.0 if (resolved.preferredLocations?.length) confidenceByField.preferredLocations = 1.0 if (resolved.budgetRange?.maxPerSqm) confidenceByField.budgetRange = 1.0 if (resolved.timing?.earliestMoveIn) confidenceByField.timing = 1.0 } setEditedCriteria({ ...resolved }) setParseResult(resolvedResult ?? { extractedCriteria: resolved, confidenceByField, missingFields: [], assumptions: [], suggestedWeights: weights, followUpQuestionCandidates: [], rawSummary: 'Manuell eingegeben', promptVersion: 'manual', schemaVersion: '1.0', }) setStep(NeedBuilderStep.READY_TO_SAVE) } function handleSaveProfile() { if (!editedCriteria || !parseResult) return setStep(NeedBuilderStep.SAVING) const entries = Object.entries(parseResult.confidenceByField) const conf = entries.length > 0 ? entries.reduce((s, [, v]) => s + v, 0) / entries.length : 0 const input = buildNeedInput({ ...editedCriteria, isAnonymous: isAnonymous || undefined }, weights, needTitle, conf, 'ACTIVE') createNeedMutation.mutate(input, { onSuccess: (created) => { queryClient.invalidateQueries({ queryKey: ['matches'] }) navigate('/demand/results', { state: { fromNeedBuilder: true, activeNeedId: created.data.id } }) }, onError: () => { setError('Speichern fehlgeschlagen.') setStep(NeedBuilderStep.ERROR) }, }) } function handleRetry() { setStep(NeedBuilderStep.IDLE) setError(null) setParseResult(null) setEditedCriteria(null) } const hasStructuredData = !!( criteria.assetType || (criteria.areaRange?.min ?? 0) > 0 || (criteria.preferredLocations?.length ?? 0) > 0 ) const canProceed = hasStructuredData || inputText.trim().length > 0 const isProcessing = step === NeedBuilderStep.PARSING || step === NeedBuilderStep.SAVING const isSaveStep = step === NeedBuilderStep.READY_TO_SAVE || step === NeedBuilderStep.SAVING const overallConfidence = parseResult ? (() => { const entries = Object.entries(parseResult.confidenceByField) return entries.length > 0 ? entries.reduce((s, [, v]) => s + v, 0) / entries.length : 0 })() : 0 return ( {/* Header */} Flächensuche Sprechen, schreiben oder Felder ausfüllen — dann sofort suchen oder als Suchprofil speichern {/* IDLE: full form */} {(step === NeedBuilderStep.IDLE || step === NeedBuilderStep.PARSING) && ( handleAction('search')} onSaveProfile={() => handleAction('save-profile')} /> )} {/* Save preview step */} {isSaveStep && parseResult && editedCriteria && ( setStep(NeedBuilderStep.IDLE)} onSave={handleSaveProfile} /> )} {/* Error */} {step === NeedBuilderStep.ERROR && ( )} ) }