feat: multi-select AI types in configurator
Some checks failed
Build & Push / build-and-push (push) Failing after 34s
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:
@@ -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`;
|
||||||
|
|||||||
@@ -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,
|
||||||
}
|
}))
|
||||||
: null;
|
: [{ label: t('summary.aiEnhancement'), variant: 'primary' as const }]
|
||||||
|
: [];
|
||||||
|
|
||||||
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] : []),
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user