Skip to content

Component Spec: KbSettingsAccordion

Status: Ready for implementation
Source of truth: Data Model §2 (Knowledge Base)
Used in:

  • Onboarding Step 3 (src/components/onboarding/step-3.tsxStep3TrainAi)
  • KB detail/editor page (/account/knowledge-bases/:knowledgeBaseId)

ComponentFile pathResponsibility
KbSettingsAccordionsrc/components/kb/kb-settings-accordion.tsxRenders all accordion sections; accepts controlled value/onChange props. No header, no footer — those are the parent’s responsibility.
MultiOptionFieldsrc/components/kb/multi-option-field.tsxReusable multi-select field: checkbox list of predefined options + “My Own Option” free-text.
KbFieldOptionssrc/components/kb/kb-field-options.tsConstants file: all predefined option sets per field.
KbDraftStatesrc/types/kb-draft.tsTypeScript type for the full controlled draft value shape.
validateKbDraftsrc/lib/kb-validation.tsPure function: checks required fields; returns { isComplete: boolean; missingFields: string[] }.

KbSettingsAccordion is a pure controlled form component. It has no internal fetch, no submit, no header, and no footer. The parent (Step 3 or KB editor page) owns:

  • The KbDraftState value and onChange handler
  • The submit/publish action
  • The page header and footer buttons

This means the exact same <KbSettingsAccordion value={draft} onChange={setDraft} /> call works on both surfaces without modification.


Define this type in src/types/kb-draft.ts. It mirrors the data model §2.1 and §2.2 exactly.

Each “multiple-option list” field now holds multiple preset selections plus an optional custom text. The preset selections and the custom text are additive (the user may have both at the same time).

// Multi-select field value.
// presets: zero or more selected predefined option keys.
// customText: optional free-text entered via "My Own Option". Empty string = not set.
// null means the user has not interacted with the field at all (pristine).
export type FieldValue = {
presets: string[];
customText?: string;
} | null;
// Default CTA extends FieldValue with an optional URL on the custom path.
export type CtaFieldValue = {
presets: string[];
customText?: string;
customUrl?: string;
} | null;

Invariants:

  • A FieldValue is considered empty (not satisfying a required field) when: it is null, OR presets is empty AND (customText ?? "").trim() === "".
  • A FieldValue is considered non-empty when: presets.length > 0 OR (customText ?? "").trim() !== "".
  • customText being present but empty string ("") is treated the same as absent.
export interface KbBrandPersonality {
overallPersona: FieldValue; // Required for Complete
communicationStyle: FieldValue; // Required for Complete
desiredVibe: FieldValue; // Required for Complete
humorUsage: FieldValue; // Required for Complete
negativeInteractionHandling: FieldValue; // Required for Complete
positiveInteractionHandling: FieldValue; // Optional
audienceRelationship: FieldValue; // Optional
famousFigureAlignment: FieldValue; // Optional
brandsAdmired: FieldValue; // Optional
tonesToAvoid: FieldValue; // Optional
}
export interface KbObjectivesVoice {
primaryObjective: FieldValue; // Multi-select
secondaryObjective: FieldValue; // Multi-select
greetings: FieldValue; // Multi-select
closing: FieldValue; // Multi-select
emojis: FieldValue; // Multi-select
hashtags: FieldValue; // Multi-select
exceptions: string; // Text only (multi-line)
clarifications: FieldValue; // Multi-select
defaultCta: CtaFieldValue; // Multi-select + optional URL
customObjectives: string[]; // Text only; one entry per item
}
export interface KbDraftState {
brandPersonality: KbBrandPersonality;
objectivesVoice: KbObjectivesVoice;
// usedBy is read-only display data; not part of the editable draft
}

Provide a createEmptyKbDraft(): KbDraftState factory that returns all FieldValue fields as null, exceptions as "", and customObjectives as [].


The accordion renders twelve top-level sections in this order:

#Section idLabelIcon (Material Symbols)Default expanded
1brand-personalityBrand personalityperson_celebrateYes
2primary-objectivePrimary objectivetargetNo
3secondary-objectiveSecondary objectiveflagNo
4greetingsGreetingswaving_handNo
5closingClosinglogoutNo
6emojisEmojissentiment_satisfiedNo
7hashtagsHashtagstagNo
8exceptionsExceptionsblockNo
9clarificationsClarificationshelpNo
10default-ctaDefault CTAlinkNo
11custom-objectivesCustom objectivesadd_circleNo
12used-byUsed bygroupsNo

The accordion supports multiple sections open simultaneously (not mutually exclusive). The parent may pass an optional defaultExpandedIds prop to override which sections start open.

Preserve the existing card/accordion look from step-3.tsx:

  • Container: rounded-[20px] border border-[#e5e5e5] bg-white px-6 py-2 shadow-[0px_1px_2px_0px_rgba(0,0,0,0.05)]
  • Section header: flex items-center gap-3 px-0 py-4, border-b border-[#e5e5e5] last:border-b-0
  • Section label: text-[14px] font-medium text-[#333]
  • Chevron: material-symbols-outlined expand_more, rotates 180° when open
  • Section body: pb-4

Use the existing custom AccordionRow pattern from step-3.tsx (or replace with shadcn Accordion — see §5). Do not change the visual tokens.

Each AccordionSectionRow uses -mx-6 on the row wrapper and px-6 on the inner content so its border-b spans the full card width (edge-to-edge, cancelling the card’s px-6). Any parent card header row rendered above the accordion (e.g. the KB name + status badge row in Step 3) must use the same full-width divider pattern-mx-6 on the wrapper with border-b border-[#e5e5e5], and px-6 on the inner content — so the divider below the header is visually indistinguishable from the first accordion row divider.


All 10 fields use the MultiOptionField component (see §5). Fields are stacked vertically with gap-5 between them. Required fields are marked with a * label suffix.

Field keyLabelRequiredNotes
overallPersonaOverall personaYes
communicationStyleCommunication styleYes
desiredVibeDesired vibe / FeelingYes
humorUsageHumor usageYes
negativeInteractionHandlingNegative interaction handlingYes
positiveInteractionHandlingPositive interaction handlingNo
audienceRelationshipAudience relationshipNo
famousFigureAlignmentFamous figure alignmentNo
brandsAdmiredBrands admiredNo
tonesToAvoidTones to avoidNo

4.2 Primary Objective Section (primary-objective)

Section titled “4.2 Primary Objective Section (primary-objective)”

Single MultiOptionField in the section body.

Field keyLabelUI typeRequired
primaryObjectivePrimary objectiveMultiOptionFieldNo

4.3 Secondary Objective Section (secondary-objective)

Section titled “4.3 Secondary Objective Section (secondary-objective)”

Single MultiOptionField in the section body.

Field keyLabelUI typeRequired
secondaryObjectiveSecondary objectiveMultiOptionFieldNo

Single MultiOptionField in the section body.

Field keyLabelUI typeRequired
greetingsGreetingsMultiOptionFieldNo

Single MultiOptionField in the section body.

Field keyLabelUI typeRequired
closingClosingMultiOptionFieldNo

Single MultiOptionField in the section body.

Field keyLabelUI typeRequired
emojisEmojisMultiOptionFieldNo

Single MultiOptionField in the section body.

Field keyLabelUI typeRequired
hashtagsHashtagsMultiOptionFieldNo

Single Textarea (text-only) in the section body. No checkboxes.

Field keyLabelUI typeRequired
exceptionsExceptionsTextarea (text-only)No

See §5.3 for the full Exceptions field behavior.

4.9 Clarifications Section (clarifications)

Section titled “4.9 Clarifications Section (clarifications)”

Single MultiOptionField in the section body.

Field keyLabelUI typeRequired
clarificationsClarificationsMultiOptionFieldNo

Single MultiOptionField with URL input extension in the section body.

Field keyLabelUI typeRequired
defaultCtaDefault CTAMultiOptionField + URL inputNo

See §5.2 for the Default CTA URL input behavior.

4.11 Custom Objectives Section (custom-objectives)

Section titled “4.11 Custom Objectives Section (custom-objectives)”

Repeatable Textarea list in the section body. No checkboxes.

Field keyLabelUI typeRequired
customObjectivesCustom objectivesRepeatable TextareaNo

Custom objectives renders as a list of <Textarea> entries. A ”+ Add objective” button appends a new empty entry. Each entry can be removed with a trash icon. Keep it as a flat list of textareas inside the section body. See §5.4 for full behavior.

Displays three read-only sub-lists. No form controls.

Sub-listLabelData source
agentsAgentsArray of agent names
socialPlatformsSocial platformsArray of platform/config names
reviewPlatformsReview platformsArray of platform/config names

Props for this section: usedBy?: { agents: string[]; socialPlatforms: string[]; reviewPlatforms: string[] }. If the array is empty, show “None” in muted text (text-[#808080] text-[13px]).

Each sub-list renders as:

[Label] (text-[13px] font-medium text-[#4d4d4d])
• Item 1
• Item 2
(or "None" if empty)

This is the core reusable field component. It implements the multi-select “multiple-option list” UI described in the data model.

Props:

interface MultiOptionFieldProps {
label: string;
required?: boolean;
options: { value: string; label: string }[];
value: FieldValue;
onChange: (v: FieldValue) => void;
customPlaceholder?: string; // placeholder for the "My Own Option" textarea
}

Behavior:

  1. Render the label above the control. If required, append * to the label.
  2. Render the predefined options as a list of checkboxes (one per option). Each checkbox:
    • Is checked when value?.presets.includes(opt.value) is true.
    • On toggle on: add opt.value to presets (keep existing presets + customText).
    • On toggle off: remove opt.value from presets (keep remaining presets + customText).
  3. When all presets are unchecked and customText is absent/empty, set value = null (pristine).
  1. Below the checkbox list, render a “My Own Option” toggle row:
    • A checkbox (or toggle button) labelled “My Own Option”.
    • It is checked/active when (value?.customText ?? "") !== "" OR the user has explicitly expanded it (use a local customOpen boolean state).
    • On check: set customOpen = true; if value is null, initialize to { presets: [], customText: "" }.
    • On uncheck: clear customText from the value; if presets is also empty, set value = null.
  2. When customOpen is true, reveal a Textarea below the toggle row:
    • rows={3}, placeholder={customPlaceholder ?? "Describe your own option..."}.
    • Value: value?.customText ?? "".
    • On change: update customText in the current value object (preserve presets).
  1. Above the checkbox list (or as a read-only summary chip row), show the currently selected values:
    • Render each selected preset as a chip (pill badge): bg-[#f0effe] text-[#4d46c3] text-[12px] rounded-full px-2 py-0.5.
    • If customText is non-empty, render an additional chip labelled “My Own Option”.
    • If nothing is selected, show placeholder text "Select options..." in text-[#999].

Shadcn components: Checkbox, Label, Textarea. No Select dropdown — replaced by the checkbox list.

Uses MultiOptionField with CtaFieldValue. The MultiOptionField handles presets and customText identically to other fields. Additionally, when customOpen is true (the “My Own Option” textarea is visible), render a second input below the textarea:

  • An Input of type="url" with placeholder="https://…" for the optional URL (customUrl).
  • On change: update customUrl in the value object (preserve presets and customText).

Pass a showUrlInput?: boolean prop to MultiOptionField to enable this extra input. When the “My Own Option” section is collapsed/unchecked, also clear customUrl.

No URL format validation is required for MVP; the type="url" attribute provides browser-level hint only.

Render a Label + Textarea with rows={4}. No checkboxes. placeholder="Words or phrases the AI must avoid, one per line. E.g. Over-promising, Sounding defensive". Value is a plain string in KbDraftState.

5.4 Custom Objectives Field (repeatable text-only)

Section titled “5.4 Custom Objectives Field (repeatable text-only)”

Render a list of Textarea entries (one per item in customObjectives[]). Each entry:

  • rows={3}, placeholder="Describe this custom objective..."
  • A remove button (trash icon, material-symbols-outlined delete, 16px) to the right of the textarea
  • On change: update the corresponding index in the array

Below the list, render a ”+ Add objective” button:

  • Style: flex items-center gap-2 text-[14px] text-[#4d46c3] hover:text-[#3d37a3]
  • Icon: material-symbols-outlined add 18px
  • On click: append "" to customObjectives[]

Define all option sets in src/components/kb/kb-field-options.ts as exported constants. The coder must define the exact option labels based on the data model descriptions. The following table lists the field key and the option values described in the data model (use these as the canonical labels):

overallPersona

  • friendly-neighbor → “Friendly neighbor”
  • knowledgeable-expert → “Knowledgeable expert”
  • visionary → “Visionary”
  • entertainer → “Entertainer”
  • curator → “Curator”
  • problem-solver → “Problem solver”

communicationStyle

  • very-casual → “Very casual”
  • conversational-professional → “Conversational but professional”
  • direct-concise → “Direct and concise”
  • enthusiastic → “Enthusiastic”
  • polished → “Polished”
  • practical → “Practical”

desiredVibe

  • warm-supportive → “Warm and supportive”
  • smart-authoritative → “Smart and authoritative”
  • innovative → “Innovative”
  • fun-playful → “Fun and playful”
  • elegant-premium → “Elegant and premium”
  • authentic → “Authentic”

humorUsage

  • serious-professional → “Serious / professional”
  • light-occasional → “Light, occasional humor”
  • witty-clever → “Witty / clever”
  • quirky-silly-memes → “Quirky / silly / memes”
  • sophisticated-subtle → “Sophisticated, subtle humor”

negativeInteractionHandling

  • empathy-take-offline → “Respond with empathy and take offline”
  • acknowledge-provide-solution → “Acknowledge and provide solution”
  • thank-invite-dm → “Thank and invite DM”
  • correct-offer-support → “Correct misinformation and offer support”

positiveInteractionHandling

  • personalized-thank-you → “Personalized thank you”
  • like-brief-reply → “Like and brief reply”
  • engage-follow-ups → “Engage with follow-ups”
  • acknowledge-professional → “Acknowledge with professional distance”

audienceRelationship

  • supportive-friend → “Supportive friend”
  • trusted-mentor → “Trusted mentor”
  • visionary-leader → “Visionary leader”
  • entertaining-companion → “Entertaining companion”
  • reliable-service-provider → “Reliable service provider”
  • aspirational-figure → “Aspirational figure”

famousFigureAlignment

  • oprah-winfrey → “Oprah Winfrey”
  • bill-nye → “Bill Nye”
  • steve-jobs → “Steve Jobs”
  • dwayne-johnson → “Dwayne Johnson”
  • ellen-degeneres → “Ellen DeGeneres”
  • gordon-ramsay → “Gordon Ramsay”

brandsAdmired

  • Coder should define 4–6 well-known brand options with short trait descriptions (e.g. “Apple – innovation and creativity”). Exact list to be confirmed with product/design.

tonesToAvoid

  • overly-formal → “Overly formal or stiff”
  • Coder should add 3–5 more common tones (e.g. “Aggressive”, “Dismissive”, “Overly casual”). Exact list to be confirmed with product/design.

primaryObjective

  • drive-cta-link → “Drive user to CTA link”
  • download-app → “Download app”
  • educate-inform → “Educate / inform”
  • inspire-motivate → “Inspire / motivate”
  • entertain-community → “Entertain / build community”
  • drive-sales-leads → “Drive sales / leads”
  • establish-authority → “Establish authority”
  • customer-support → “Provide customer support”

secondaryObjective

  • build-brand-awareness → “Build brand awareness”
  • foster-community-ugc → “Foster community / UGC”
  • drive-traffic → “Drive traffic to site / blog / landing”
  • gather-feedback → “Gather feedback”
  • quick-customer-service → “Provide quick customer service”

greetings

  • warm-friendly-informal → “Warm / friendly informal (with emojis)”
  • professional-approachable → “Professional yet approachable”
  • direct-concise → “Direct and concise”
  • enthusiastic-energetic → “Enthusiastic / energetic”
  • formal-respectful → “Formal / respectful”

closing
Same options as greetings (same 5 values).

emojis

  • frequent-expressive → “Frequent and expressive”
  • moderate → “Moderate (1–2 per message)”
  • minimal-purposeful → “Minimal and purposeful”
  • avoided-formal → “Avoided for formal tone”

hashtags

  • generous → “Generous (5–10 relevant / trending)”
  • moderate → “Moderate (2–4 relevant / branded)”
  • minimal → “Minimal (1–2 essential / branded)”
  • avoided → “Avoided (clean text-focused)”

clarifications

  • concise-direct → “Concise direct answer”
  • detailed-explanation → “Detailed explanation”
  • direct-to-resource → “Direct to official resource (FAQ / support / website)”
  • ask-follow-up → “Ask follow-up for context”

defaultCta

  • encourage-engagement → “Encourage engagement (question / comments)”
  • direct-learn-more → “Direct to learn more / general link”
  • prompt-share-tag → “Prompt share / tag a friend”
  • encourage-follow → “Encourage follow for more content”

interface KbSettingsAccordionProps {
// Controlled form value
value: KbDraftState;
onChange: (value: KbDraftState) => void;
// Read-only "Used by" data (fetched by parent; not part of draft)
usedBy?: {
agents: string[];
socialPlatforms: string[];
reviewPlatforms: string[];
};
// Optional: override which sections start expanded (default: ["brand-personality"])
defaultExpandedIds?: string[];
// Optional: disable all form fields (e.g. when publishing is in progress)
disabled?: boolean;
}

The component is stateless with respect to form values. It manages only accordion open/close state internally (which sections are expanded).

Define a SECTION_IDS constant in kb-settings-accordion.tsx to drive both the render loop and type-safe references:

export const SECTION_IDS = [
"brand-personality",
"primary-objective",
"secondary-objective",
"greetings",
"closing",
"emojis",
"hashtags",
"exceptions",
"clarifications",
"default-cta",
"custom-objectives",
"used-by",
] as const;
export type SectionId = (typeof SECTION_IDS)[number];

defaultExpandedIds defaults to ["brand-personality"]. The parent may pass any subset of SECTION_IDS values to override the initial open state.


In src/lib/kb-validation.ts:

export interface KbValidationResult {
isComplete: boolean;
missingFields: Array<{
sectionId: string;
fieldKey: string;
label: string;
}>;
}
export function validateKbDraft(draft: KbDraftState): KbValidationResult;

A KB draft is Complete when all five required brand personality fields are non-empty. The emptiness rule for the new FieldValue shape:

function isFieldValueEmpty(v: FieldValue | null): boolean {
if (v === null) return true;
if (v.presets.length > 0) return false;
return (v.customText ?? "").trim() === "";
}

Required fields:

  • brandPersonality.overallPersona
  • brandPersonality.communicationStyle
  • brandPersonality.desiredVibe
  • brandPersonality.humorUsage
  • brandPersonality.negativeInteractionHandling

A field is satisfied when presets.length > 0 OR customText.trim() !== "" (either condition alone is sufficient). Both together is also valid.

The parent calls validateKbDraft(draft) before allowing publish/complete. The parent is responsible for showing validation errors (e.g. a toast or inline error summary); the KbSettingsAccordion itself does not show validation errors unless the parent passes them in (out of scope for MVP).


The following shadcn components must be present in the project. Add any that are missing via the shadcn CLI:

shadcn componentUsed for
AccordionOptional: may replace custom AccordionRow if preferred; otherwise keep custom implementation
CheckboxMultiOptionField preset option rows and “My Own Option” toggle
Textarea”My Own Option” custom text input, Exceptions, Custom objectives
InputDefault CTA URL field
LabelField labels and checkbox labels
Button”+ Add objective”, remove buttons

The existing src/components/ui/button.tsx, input.tsx, label.tsx, card.tsx are already present. Add checkbox.tsx, textarea.tsx if absent. The select.tsx and separator.tsx added in the previous version are no longer used by MultiOptionField (they may remain in the project for other uses).

Styling tokens to preserve (from existing step-3.tsx):

  • Focus ring: focus:border-[#4d46c3]
  • Border: border-[#e5e5e5]
  • Muted text: text-[#808080]
  • Primary text: text-[#333]
  • Secondary text: text-[#4d4d4d]
  • Brand color: text-[#4d46c3] / bg-[#f0effe]
  • Rounded: rounded-lg for inputs, rounded-[20px] for the outer card

The Step 3 main content is a single card that contains both the card header and the accordion. The parent (Step3TrainAi) owns the card entirely; KbSettingsAccordion renders only the accordion rows inside it.

The parent keeps:

  • The title/subtitle block (above the card)
  • The card wrapper (rounded-[20px] border border-[#e5e5e5] bg-white px-6 py-2 shadow-[...]) — do not duplicate this inside KbSettingsAccordion
  • The card header row inside the card: database icon + KB name (DEFAULT_KB_NAME = "My Knowledge Base") + status badge (Incomplete / Complete derived from validateKbDraft(draft).isComplete)
  • The “Previous” / “Complete” footer buttons (outside the card)
  • KbDraftState in local state (or from a context/store)
  • Call to validateKbDraft before enabling the “Complete” button

KbSettingsAccordion renders only the accordion rows inside the card body. It has no knowledge of the card wrapper, the KB name, or the status badge.

The usedBy prop is omitted on Step 3 — the used-by accordion section is hidden automatically when usedBy is not passed.

// Rough shape in Step3TrainAi
const [draft, setDraft] = React.useState<KbDraftState>(createEmptyKbDraft());
const { isComplete } = validateKbDraft(draft);
<div className="w-full max-w-[680px] rounded-[20px] border border-[#e5e5e5] bg-white px-6 py-2 shadow-[...]">
{/* Card header row — rendered by parent, not by KbSettingsAccordion */}
<div className="flex items-center justify-between py-3">
<div className="flex items-center gap-2">
<span className="material-symbols-outlined text-[20px]">manage_search</span>
<span className="text-[14px] font-medium text-[#333]">{DEFAULT_KB_NAME}</span>
</div>
<StatusBadge complete={isComplete} />
</div>
{/* Accordion body — KbSettingsAccordion renders only the rows */}
<KbSettingsAccordion value={draft} onChange={setDraft} />
</div>

Same pattern. The page fetches the KB, maps API response to KbDraftState, passes it as value. On save/publish, the page calls validateKbDraft, then calls the API.


The following data model features are explicitly excluded from this accordion component:

  • Documents (§2 file upload): separate UI component, not part of the accordion.
  • Linkage (§2 KB-to-KB value inheritance): separate UI; the accordion does not show or manage linked values.
  • Knowledge Base Mapping (Component: Knowledge Base Mapping): a separate KnowledgeBaseMapping component that sits below or beside the accordion on the KB editor page; it is not a section inside KbSettingsAccordion.
  • IF/OR Scenarios: present in the current step-3.tsx but not in the data model §2.1 or §2.2. Remove from the accordion; do not implement until the data model is updated.
  • Validation error display inside the accordion: the parent handles error presentation for MVP.
  • Publish flow: the accordion does not contain a publish button; the parent page owns that.

12. Numbered Implementation Tasks for the Coder

Section titled “12. Numbered Implementation Tasks for the Coder”

Step 1 — Update types (src/types/kb-draft.ts)

Section titled “Step 1 — Update types (src/types/kb-draft.ts)”

Replace the existing FieldValue and CtaFieldValue types with the multi-select shape defined in §2a:

// BEFORE (single-select — remove this)
export type FieldValue =
| { type: "preset"; value: string }
| { type: "custom"; value: string }
| null;
export type CtaFieldValue =
| { type: "preset"; value: string }
| { type: "custom"; value: string; url?: string }
| null;
// AFTER (multi-select — use this)
export type FieldValue = {
presets: string[];
customText?: string;
} | null;
export type CtaFieldValue = {
presets: string[];
customText?: string;
customUrl?: string;
} | null;

All interface fields (KbBrandPersonality, KbObjectivesVoice, KbDraftState) keep the same field names — only the leaf type changes. Update createEmptyKbDraft() so all FieldValue fields remain null (no change needed there).

Step 2 — Update validation (src/lib/kb-validation.ts)

Section titled “Step 2 — Update validation (src/lib/kb-validation.ts)”

Replace isFieldValueEmpty with the new emptiness rule:

function isFieldValueEmpty(v: FieldValue | null): boolean {
if (v === null) return true;
if (v.presets.length > 0) return false;
return (v.customText ?? "").trim() === "";
}

No other changes to validateKbDraft are needed — the required field list and return shape stay the same.

Step 3 — Add missing shadcn component (src/components/ui/checkbox.tsx)

Section titled “Step 3 — Add missing shadcn component (src/components/ui/checkbox.tsx)”

Run npx shadcn@latest add checkbox if checkbox.tsx is not already present. The textarea.tsx added previously is still needed.

Step 4 — Rewrite MultiOptionField (src/components/kb/multi-option-field.tsx)

Section titled “Step 4 — Rewrite MultiOptionField (src/components/kb/multi-option-field.tsx)”

Replace the Select-based implementation with the checkbox-list implementation described in §5.1:

  • Remove: Select, SelectTrigger, SelectContent, SelectItem, SelectSeparator imports and usage.
  • Add: Checkbox import.
  • Props interface stays the same name (MultiOptionFieldProps) but value / onChange now use the new FieldValue / CtaFieldValue shapes.
  • Local state: add const [customOpen, setCustomOpen] = React.useState(() => (value?.customText ?? "") !== "").
  • Preset checkbox list: map over options; each row is <Checkbox checked={value?.presets.includes(opt.value)} onCheckedChange={...} /> + <Label>.
    • Toggle on: onChange({ presets: [...(value?.presets ?? []), opt.value], customText: value?.customText }).
    • Toggle off: onChange({ presets: (value?.presets ?? []).filter(p => p !== opt.value), customText: value?.customText }) — if result is { presets: [], customText: undefined/empty }, set onChange(null).
  • “My Own Option” row: a <Checkbox checked={customOpen} onCheckedChange={open => { setCustomOpen(open); if (!open) onChange({ presets: value?.presets ?? [], customText: undefined, ...(showUrlInput ? { customUrl: undefined } : {}) } || null-if-empty) }} /> + <Label>My Own Option</Label>.
  • Custom textarea (visible when customOpen): rows={3}, updates customText in value.
  • URL input (visible when customOpen && showUrlInput): updates customUrl in value (for Default CTA only).
  • Chip summary row: render above the checkbox list. Show one chip per selected preset (look up label from options) and one chip if customText is non-empty. If nothing selected, show "Select options..." placeholder.

Chip style: inline-flex items-center gap-1 rounded-full bg-[#f0effe] px-2 py-0.5 text-[12px] text-[#4d46c3].

Step 5 — No changes to kb-field-options.ts

Section titled “Step 5 — No changes to kb-field-options.ts”

The predefined option arrays are unchanged. The FieldOption type ({ value: string; label: string }) is unchanged.

Step 6 — Rewrite KbSettingsAccordion (src/components/kb/kb-settings-accordion.tsx)

Section titled “Step 6 — Rewrite KbSettingsAccordion (src/components/kb/kb-settings-accordion.tsx)”

The accordion structure has changed from 3 sections to 12. Replace the old two-section body (brand-personality + objectives-voice) with twelve individual sections as defined in §3 and §4:

  • Each of the 10 former objectivesVoice fields becomes its own top-level accordion section (see §4.2–§4.11).
  • The SECTION_IDS constant and SectionId type (see §7 implementation note) must be defined in this file.
  • Each section header renders the icon from §3 using <span className="material-symbols-outlined">…</span> and the label.
  • Each section body renders exactly one field component:
    • Sections primary-objective, secondary-objective, greetings, closing, emojis, hashtags, clarifications: render <MultiOptionField> wired to the corresponding value.objectivesVoice.* key.
    • Section exceptions: render the plain Textarea (see §5.3).
    • Section default-cta: render <MultiOptionField showUrlInput={true}> (see §5.2).
    • Section custom-objectives: render the repeatable textarea list (see §5.4).
    • Section used-by: render the read-only sub-lists (see §4.12).
  • The brand-personality section body is unchanged (all 10 MultiOptionField rows stacked with gap-5).
  • defaultExpandedIds defaults to ["brand-personality"].

Step 7 — Update src/components/onboarding/step-3.tsx

Section titled “Step 7 — Update src/components/onboarding/step-3.tsx”

No structural changes needed beyond what was already specified. The KbDraftState type change propagates automatically. Verify the “Complete” button still calls validateKbDraft(draft).isComplete.

  • Accordion renders 12 top-level sections in the order defined in §3.
  • Each section header shows the correct Material Symbols icon and label.
  • Only brand-personality is expanded by default.
  • Each of the 10 former objectivesVoice fields is its own section with a single field in the body.
  • Each MultiOptionField renders a checkbox per predefined option.
  • Checking multiple options adds all to presets; unchecking removes only that one.
  • “My Own Option” checkbox reveals/hides the textarea correctly.
  • Clearing the textarea and unchecking “My Own Option” removes customText from the value.
  • Chip summary row shows all selected presets + “My Own Option” chip when custom text is present.
  • Default CTA section shows URL input when “My Own Option” is open.
  • Exceptions section renders a plain Textarea (no checkboxes).
  • Custom objectives section renders a repeatable textarea list with ”+ Add objective” and trash remove buttons.
  • Required fields show * label suffix.
  • validateKbDraft returns isComplete: false when all required fields are null; returns true when each has at least one preset or non-empty custom text.
  • Used by section shows “None” when arrays are empty.