feat: multi-select AI types in configurator
Some checks failed
Build & Push / build-and-push (push) Failing after 34s

- Changed aiType (string|null) to aiTypes (string[]) across all components
- StepServices: chips now toggle on/off independently, descriptions show for all selected
- StepContact: summary tags show all selected AI types
- API route: handles array of AI types for both AI and fallback brief generation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-26 18:06:44 +01:00
parent acefb70b68
commit bbe5b6c67e
4 changed files with 40 additions and 29 deletions

View File

@@ -6,7 +6,7 @@ import { sendBriefToClient, sendLeadNotification } from '@/lib/email';
interface ConfigureRequestBody { interface ConfigureRequestBody {
services: string[]; services: string[];
aiEnabled: boolean; aiEnabled: boolean;
aiType: string | null; aiTypes: string[];
industry: string | null; industry: string | null;
scope: string; scope: string;
timeline: string | null; timeline: string | null;
@@ -52,7 +52,9 @@ function buildContext(body: ConfigureRequestBody): string {
const industry = body.industry ? INDUSTRY_NAMES[body.industry] ?? body.industry : 'Not specified'; const industry = body.industry ? INDUSTRY_NAMES[body.industry] ?? body.industry : 'Not specified';
const timeline = body.timeline ? TIMELINE_NAMES[body.timeline] ?? body.timeline : 'Not specified'; const timeline = body.timeline ? TIMELINE_NAMES[body.timeline] ?? body.timeline : 'Not specified';
const company = body.company.trim() || 'Not specified'; const company = body.company.trim() || 'Not specified';
const aiType = body.aiEnabled && body.aiType ? AI_TYPE_NAMES[body.aiType] ?? body.aiType : null; const aiTypeNames = body.aiEnabled && body.aiTypes.length > 0
? body.aiTypes.map((t) => AI_TYPE_NAMES[t] ?? t).join(', ')
: null;
let context = `Client Name: ${body.name} let context = `Client Name: ${body.name}
Company: ${company} Company: ${company}
@@ -61,7 +63,7 @@ Industry: ${industry}
Timeline: ${timeline}`; Timeline: ${timeline}`;
if (body.aiEnabled) { if (body.aiEnabled) {
context += `\nAI Integration: Yes — ${aiType ?? 'type to be determined'}`; context += `\nAI Integration: Yes — ${aiTypeNames ?? 'type to be determined'}`;
} }
if (body.scope.trim()) { if (body.scope.trim()) {
@@ -150,7 +152,7 @@ ${context}`;
// ─── Fallback Brief (no API key or API failure) ────────────────────────────── // ─── Fallback Brief (no API key or API failure) ──────────────────────────────
function generateFallbackBrief(body: ConfigureRequestBody): string { function generateFallbackBrief(body: ConfigureRequestBody): string {
const { services, aiEnabled, aiType, industry, scope, timeline, name, company } = body; const { services, aiEnabled, aiTypes, industry, scope, timeline, name, company } = body;
const serviceNames = services.map((s) => SERVICE_NAMES[s] ?? s); const serviceNames = services.map((s) => SERVICE_NAMES[s] ?? s);
const servicesList = serviceNames.length <= 2 const servicesList = serviceNames.length <= 2
@@ -179,9 +181,11 @@ function generateFallbackBrief(body: ConfigureRequestBody): string {
if (hasInfra) { if (hasInfra) {
sections += `\n**Private Infrastructure**\nWe'll set up a dedicated server environment for ${displayCompany} with email, cloud storage, and business tools that you fully own and control.\n`; sections += `\n**Private Infrastructure**\nWe'll set up a dedicated server environment for ${displayCompany} with email, cloud storage, and business tools that you fully own and control.\n`;
} }
if (aiEnabled && aiType) { if (aiEnabled && aiTypes.length > 0) {
const aiLabel = AI_TYPE_NAMES[aiType] ?? 'AI integration'; const aiLabels = aiTypes.map((t) => AI_TYPE_NAMES[t] ?? t).join(', ');
sections += `\n**AI Integration**\nWe'll layer ${aiLabel.toLowerCase()} into your systems — deeply integrated, not bolted on. The exact approach will be scoped during discovery.\n`; sections += `\n**AI Integration**\nWe'll layer ${aiLabels.toLowerCase()} into your systems — deeply integrated, not bolted on. The exact approach will be scoped during discovery.\n`;
} else if (aiEnabled) {
sections += `\n**AI Integration**\nWe'll layer AI integration into your systems — deeply integrated, not bolted on. The exact approach will be scoped during discovery.\n`;
} }
if (scope?.trim()) { if (scope?.trim()) {
sections += `\n**Your Goals**\nYou shared: "${scope.trim()}" — we'll frame our discovery sessions around these priorities.\n`; sections += `\n**Your Goals**\nYou shared: "${scope.trim()}" — we'll frame our discovery sessions around these priorities.\n`;

View File

@@ -122,14 +122,14 @@ export default function StepContact({
variant: 'primary' as const, variant: 'primary' as const,
})); }));
const aiTag = formData.aiEnabled const aiTags = formData.aiEnabled
? { ? formData.aiTypes.length > 0
label: formData.aiType ? formData.aiTypes.map((id) => ({
? t(`aiTypes.${formData.aiType}.title`) label: t(`aiTypes.${id}.title`),
: t('summary.aiEnhancement'), variant: 'primary' as const,
variant: 'primary' as const, }))
} : [{ label: t('summary.aiEnhancement'), variant: 'primary' as const }]
: null; : [];
const industryTag = formData.industry const industryTag = formData.industry
? { label: t(`industries.${formData.industry}`), variant: 'neutral' as const } ? { label: t(`industries.${formData.industry}`), variant: 'neutral' as const }
@@ -141,7 +141,7 @@ export default function StepContact({
const allTags = [ const allTags = [
...serviceTags, ...serviceTags,
...(aiTag ? [aiTag] : []), ...aiTags,
...(industryTag ? [industryTag] : []), ...(industryTag ? [industryTag] : []),
...(timelineTag ? [timelineTag] : []), ...(timelineTag ? [timelineTag] : []),
]; ];

View File

@@ -193,14 +193,16 @@ export default function StepServices({ formData, setFormData, onNext }: StepProp
setFormData((prev) => ({ setFormData((prev) => ({
...prev, ...prev,
aiEnabled: !prev.aiEnabled, aiEnabled: !prev.aiEnabled,
aiType: prev.aiEnabled ? null : prev.aiType, aiTypes: prev.aiEnabled ? [] : prev.aiTypes,
})); }));
}; };
const selectAIType = (id: string) => { const toggleAIType = (id: string) => {
setFormData((prev) => ({ setFormData((prev) => ({
...prev, ...prev,
aiType: prev.aiType === id ? null : id, aiTypes: prev.aiTypes.includes(id)
? prev.aiTypes.filter((t) => t !== id)
: [...prev.aiTypes, id],
})); }));
}; };
@@ -276,8 +278,8 @@ export default function StepServices({ formData, setFormData, onNext }: StepProp
}} }}
> >
<Chip <Chip
active={formData.aiType === aiId} active={formData.aiTypes.includes(aiId)}
onClick={() => selectAIType(aiId)} onClick={() => toggleAIType(aiId)}
> >
{t(`aiTypes.${aiId}.title`)} {t(`aiTypes.${aiId}.title`)}
</Chip> </Chip>
@@ -286,17 +288,22 @@ export default function StepServices({ formData, setFormData, onNext }: StepProp
</div> </div>
<AnimatePresence mode="wait"> <AnimatePresence mode="wait">
{formData.aiType && ( {formData.aiTypes.length > 0 && (
<motion.p <motion.div
key={formData.aiType} key={formData.aiTypes.join(',')}
initial={{ opacity: 0, y: 4 }} initial={{ opacity: 0, y: 4 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -4 }} exit={{ opacity: 0, y: -4 }}
transition={{ duration: 0.2 }} transition={{ duration: 0.2 }}
className="text-xs text-outline leading-relaxed px-1" className="flex flex-col gap-1.5 px-1"
> >
{t(`aiTypes.${formData.aiType}.description`)} {formData.aiTypes.map((aiId) => (
</motion.p> <p key={aiId} className="text-xs text-outline leading-relaxed">
<strong className="text-on-surface font-medium">{t(`aiTypes.${aiId}.title`)}:</strong>{' '}
{t(`aiTypes.${aiId}.description`)}
</p>
))}
</motion.div>
)} )}
</AnimatePresence> </AnimatePresence>
</div> </div>

View File

@@ -13,7 +13,7 @@ import StepComplete from './StepComplete';
export interface WizardFormData { export interface WizardFormData {
services: string[]; services: string[];
aiEnabled: boolean; aiEnabled: boolean;
aiType: string | null; aiTypes: string[];
industry: string | null; industry: string | null;
scope: string; scope: string;
timeline: string | null; timeline: string | null;
@@ -59,7 +59,7 @@ const makeVariants = (direction: 1 | -1) => ({
const DEFAULT_FORM_DATA: WizardFormData = { const DEFAULT_FORM_DATA: WizardFormData = {
services: [], services: [],
aiEnabled: false, aiEnabled: false,
aiType: null, aiTypes: [],
industry: null, industry: null,
scope: '', scope: '',
timeline: null, timeline: null,