feat: F008 AI need builder — smart parse, criteria review, follow-up & weighting
Domain: ParsedNeedCriteria, FollowUpQuestion, ParseNeedResult, NeedBuilderStep, WEIGHTING_KEYS/LABELS. Services: weightingService (asset-type profiles), aiService extended with parseNeed (keyword extraction mock, 1.4s delay) and generateFollowUpQuestions. Components: NeedBuilderProgress, NeedInput, ConfidenceFieldBadge, ExtractedFieldRow, CriteriaReviewPanel, FollowUpQuestionCard, FollowUpPanel, WeightingEditor, NeedCardPreview, NeedBuilderErrorState. Page: AINeedBuilderPage replaces AISearch with 4-step flow (input → review → weighting → save). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
import { Box, Button, Card, Slider, Typography } from '@mui/material'
|
||||
import { RotateCcw } from 'lucide-react'
|
||||
import { WEIGHTING_KEYS, WEIGHTING_LABELS } from '../../domain/needBuilder'
|
||||
import type { WeightingKey } from '../../domain/needBuilder'
|
||||
import { weightingService } from '../../services/weightingService'
|
||||
|
||||
interface Props {
|
||||
weights: Record<WeightingKey, number>
|
||||
onChange: (weights: Record<WeightingKey, number>) => void
|
||||
assetType?: string
|
||||
}
|
||||
|
||||
export function WeightingEditor({ weights, onChange, assetType }: Props) {
|
||||
const total = WEIGHTING_KEYS.reduce((sum, k) => sum + (weights[k] ?? 0), 0)
|
||||
const totalPct = Math.round(total * 100)
|
||||
const isBalanced = totalPct >= 95 && totalPct <= 105
|
||||
|
||||
function handleSlider(key: WeightingKey, pct: number) {
|
||||
onChange({ ...weights, [key]: pct / 100 })
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
onChange(weightingService.getDefaultWeights(assetType))
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={{ maxWidth: 720, mx: 'auto' }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 2 }}>
|
||||
<Box>
|
||||
<Typography variant="subtitle1" sx={{ fontWeight: 600 }}>
|
||||
Kriteriengewichtung anpassen
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
Passen Sie an, wie stark jedes Kriterium das Matching beeinflusst.
|
||||
</Typography>
|
||||
</Box>
|
||||
<Button
|
||||
size="small"
|
||||
variant="outlined"
|
||||
startIcon={<RotateCcw size={14} />}
|
||||
onClick={handleReset}
|
||||
>
|
||||
Zurücksetzen
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
<Card sx={{ p: 3 }}>
|
||||
<Box sx={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 3 }}>
|
||||
{WEIGHTING_KEYS.map(key => {
|
||||
const pct = Math.round((weights[key] ?? 0) * 100)
|
||||
return (
|
||||
<Box key={key}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 0.5 }}>
|
||||
<Typography variant="body2" sx={{ fontWeight: 500 }}>
|
||||
{WEIGHTING_LABELS[key]}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{pct}%
|
||||
</Typography>
|
||||
</Box>
|
||||
<Slider
|
||||
value={pct}
|
||||
min={0}
|
||||
max={40}
|
||||
step={1}
|
||||
onChange={(_, v) => handleSlider(key, v as number)}
|
||||
size="small"
|
||||
sx={{ color: '#1e3a5f' }}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
})}
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
mt: 3,
|
||||
pt: 2,
|
||||
borderTop: '1px solid #e2e8f0',
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Gesamt
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{ fontWeight: 700, color: isBalanced ? '#1a7a4a' : '#c0392b' }}
|
||||
>
|
||||
{totalPct}%{!isBalanced && ' — Summe sollte ~100% ergeben'}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Card>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user