diff --git a/hax/artifacts/multi-step-form/action.ts b/hax/artifacts/multi-step-form/action.ts new file mode 100644 index 0000000..100b15b --- /dev/null +++ b/hax/artifacts/multi-step-form/action.ts @@ -0,0 +1,130 @@ +/* + * Copyright 2025 Cisco Systems, Inc. and its affiliates + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { useCopilotAction } from "@copilotkit/react-core"; +import { z } from "zod"; +import type { MultiStepFormArtifact } from "./types"; +import { StepZod, FormFieldZod } from "./types"; +import { MULTI_STEP_FORM_DESCRIPTION } from "./description"; + +interface UseMultiStepFormActionProps { + addOrUpdateArtifact: ( + type: "multi-step-form", + data: MultiStepFormArtifact["data"] + ) => void; +} + +export const useMultiStepFormAction = ({ + addOrUpdateArtifact, +}: UseMultiStepFormActionProps) => { + useCopilotAction({ + name: "create_multi_step_form", + description: MULTI_STEP_FORM_DESCRIPTION, + parameters: [ + { + name: "title", + type: "string", + description: "Title displayed at the top of the card", + required: false, + }, + { + name: "badge", + type: "string", + description: "Badge text shown in the top-right corner", + required: false, + }, + { + name: "stepsJson", + type: "string", + description: + 'JSON array of steps: [{"id":"account","label":"Account"},{"id":"profile","label":"Profile"},{"id":"complete","label":"Complete"}]', + required: false, + }, + { + name: "currentStep", + type: "number", + description: "Current active step index (0-based)", + required: false, + }, + { + name: "formTitle", + type: "string", + description: "Heading for the form section", + required: false, + }, + { + name: "fieldsJson", + type: "string", + description: + 'JSON array of fields: [{"name":"email","label":"Email","type":"email","placeholder":"you@example.com","required":true}]', + required: false, + }, + { + name: "backLabel", + type: "string", + description: "Label for the back/cancel button", + required: false, + }, + { + name: "nextLabel", + type: "string", + description: "Label for the continue/next button", + required: false, + }, + ], + handler: async (args) => { + const { + title, + badge, + stepsJson, + currentStep, + formTitle, + fieldsJson, + backLabel, + nextLabel, + } = args; + + const data: MultiStepFormArtifact["data"] = {}; + + if (title) data.title = title; + if (badge) data.badge = badge; + if (stepsJson) { + try { + data.steps = z.array(StepZod).parse(JSON.parse(stepsJson)); + } catch { + /* skip invalid steps JSON */ + } + } + if (currentStep !== undefined) data.currentStep = currentStep; + if (formTitle) data.formTitle = formTitle; + if (fieldsJson) { + try { + data.fields = z.array(FormFieldZod).parse(JSON.parse(fieldsJson)); + } catch { + /* skip invalid fields JSON */ + } + } + if (backLabel) data.backLabel = backLabel; + if (nextLabel) data.nextLabel = nextLabel; + + addOrUpdateArtifact("multi-step-form", data); + + return "Created multi-step form artifact"; + }, + }); +}; diff --git a/hax/artifacts/multi-step-form/description.ts b/hax/artifacts/multi-step-form/description.ts new file mode 100644 index 0000000..cea4a04 --- /dev/null +++ b/hax/artifacts/multi-step-form/description.ts @@ -0,0 +1,26 @@ +/* + * Copyright 2025 Cisco Systems, Inc. and its affiliates + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +export const MULTI_STEP_FORM_DESCRIPTION = + `Use the multi-step-form artifact to display a wizard-style form with a stepper progress bar. + +The form shows numbered step indicators (completed with checkmark, active with number, upcoming with muted number) connected by horizontal lines, a form section with labeled input fields, and navigation buttons (back/continue). + +Provide step definitions (id + label), current step index (0-based), form title, form fields (name, label, type, placeholder, required flag), and button labels. + +Use this component for multi-page onboarding flows, account setup wizards, upgrade plans, or any progressive disclosure form pattern.` as const; diff --git a/hax/artifacts/multi-step-form/index.ts b/hax/artifacts/multi-step-form/index.ts new file mode 100644 index 0000000..ca98cc4 --- /dev/null +++ b/hax/artifacts/multi-step-form/index.ts @@ -0,0 +1,24 @@ +/* + * Copyright 2025 Cisco Systems, Inc. and its affiliates + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +export { HAXMultiStepForm } from "./multi-step-form"; +export type { HAXMultiStepFormProps } from "./multi-step-form"; +export { useMultiStepFormAction } from "./action"; +export { MULTI_STEP_FORM_DESCRIPTION } from "./description"; +export { MultiStepFormArtifactZod } from "./types"; +export type { MultiStepFormArtifact, StepData, FormFieldData } from "./types"; diff --git a/hax/artifacts/multi-step-form/multi-step-form.tsx b/hax/artifacts/multi-step-form/multi-step-form.tsx new file mode 100644 index 0000000..ba598b4 --- /dev/null +++ b/hax/artifacts/multi-step-form/multi-step-form.tsx @@ -0,0 +1,292 @@ +/* + * Copyright 2025 Cisco Systems, Inc. and its affiliates + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +"use client" + +import * as React from "react" +import { cn } from "@/lib/utils" +import { Check } from "lucide-react" +import type { StepData, FormFieldData } from "./types" + +// --------------------------------------------------------------------------- +// Types +// --------------------------------------------------------------------------- + +export interface HAXMultiStepFormProps + extends React.HTMLAttributes { + title?: string; + badge?: string; + steps?: StepData[]; + currentStep?: number; + formTitle?: string; + fields?: FormFieldData[]; + values?: Record; + errors?: Record; + onFieldChange?: (name: string, value: string) => void; + onBack?: () => void; + onNext?: (values: Record) => void; + isSubmitting?: boolean; + backLabel?: string; + nextLabel?: string; + className?: string; +} + +// --------------------------------------------------------------------------- +// Defaults +// --------------------------------------------------------------------------- + +const DEFAULT_STEPS: StepData[] = [ + { id: "account", label: "Account" }, + { id: "profile", label: "Profile" }, + { id: "complete", label: "Complete" }, +]; + +const DEFAULT_FIELDS: FormFieldData[] = [ + { name: "fullName", label: "Full Name", placeholder: "John Doe" }, + { name: "jobTitle", label: "Job Title", placeholder: "Product Designer" }, +]; + +const EMPTY_VALUES: Record = {}; + +// --------------------------------------------------------------------------- +// Sub-components +// --------------------------------------------------------------------------- + +type StepStatus = "completed" | "active" | "upcoming"; + +function StepperIcon({ + stepNumber, + status, +}: { + stepNumber: number; + status: StepStatus; +}) { + if (status === "completed") { + return ( +
+ +
+ ); + } + + if (status === "active") { + return ( +
+ {stepNumber} +
+ ); + } + + return ( +
+ {stepNumber} +
+ ); +} + +function Stepper({ + steps, + currentStep, +}: { + steps: StepData[]; + currentStep: number; +}) { + const getStatus = (index: number): StepStatus => + index < currentStep + ? "completed" + : index === currentStep + ? "active" + : "upcoming"; + + return ( +
+ {/* Row 1: circles + connecting lines */} +
+ {steps.map((step, index) => ( + + + {index < steps.length - 1 && ( +
+ )} + + ))} +
+ + {/* Row 2: labels aligned under circles */} +
+ {steps.map((step, index) => ( + +
+ + {step.label} + +
+ {index < steps.length - 1 &&
} + + ))} +
+
+ ); +} + +// --------------------------------------------------------------------------- +// Main Component +// --------------------------------------------------------------------------- + +export function HAXMultiStepForm({ + title = "Multi-Step Form", + badge, + steps = DEFAULT_STEPS, + currentStep = 1, + formTitle = "Profile Information", + fields = DEFAULT_FIELDS, + values = EMPTY_VALUES, + errors, + onFieldChange, + onBack, + onNext, + isSubmitting = false, + backLabel = "Back", + nextLabel = "Continue", + className, + ...rest +}: HAXMultiStepFormProps) { + const clampedStep = Math.max(0, Math.min(currentStep, steps.length - 1)); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + if (!isSubmitting) onNext?.(values); + }; + + return ( +
+ {/* Header: title left, badge right */} +
+

{title}

+ {badge && ( + + {badge} + + )} +
+ + {/* Stepper */} +
+ +
+ + {/* Form fields */} +
+ {formTitle && ( +

+ {formTitle} +

+ )} + + {fields.map((field) => { + const fieldError = errors?.[field.name]; + return ( +
+ + onFieldChange?.(field.name, e.target.value)} + aria-invalid={!!fieldError} + aria-describedby={fieldError ? `${field.name}-error` : undefined} + className={cn( + "w-full rounded-lg border bg-white px-3 py-2.5", + "text-sm text-[#0F172A] placeholder:text-[#94A3B8]", + "outline-none transition-colors", + fieldError + ? "border-red-500 focus:border-red-500 focus:ring-1 focus:ring-red-500" + : "border-[#E2E8F0] focus:border-[#3B82F6] focus:ring-1 focus:ring-[#3B82F6]" + )} + /> + {fieldError && ( +

+ {fieldError} +

+ )} +
+ ); + })} +
+ + {/* Navigation buttons */} +
+ + +
+
+ ); +} diff --git a/hax/artifacts/multi-step-form/types.ts b/hax/artifacts/multi-step-form/types.ts new file mode 100644 index 0000000..7123a5a --- /dev/null +++ b/hax/artifacts/multi-step-form/types.ts @@ -0,0 +1,53 @@ +/* + * Copyright 2025 Cisco Systems, Inc. and its affiliates + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { z } from "zod"; + +export const StepZod = z.object({ + id: z.string(), + label: z.string(), +}); + +export const FormFieldZod = z.object({ + name: z.string(), + label: z.string(), + type: z + .enum(["text", "email", "password", "number", "tel", "url"]) + .optional(), + placeholder: z.string().optional(), + required: z.boolean().optional(), +}); + +export const MultiStepFormArtifactZod = z.object({ + id: z.string(), + type: z.literal("multi-step-form"), + data: z.object({ + title: z.string().optional(), + badge: z.string().optional(), + steps: z.array(StepZod).optional(), + currentStep: z.number().optional(), + formTitle: z.string().optional(), + fields: z.array(FormFieldZod).optional(), + backLabel: z.string().optional(), + nextLabel: z.string().optional(), + }), +}); + +export type MultiStepFormArtifact = z.infer; +export type StepData = z.infer; +export type FormFieldData = z.infer;