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.tsx→Step3TrainAi) - KB detail/editor page (
/account/knowledge-bases/:knowledgeBaseId)
1. Component Scope and Reuse
Section titled “1. Component Scope and Reuse”Components to create
Section titled “Components to create”| Component | File path | Responsibility |
|---|---|---|
KbSettingsAccordion | src/components/kb/kb-settings-accordion.tsx | Renders all accordion sections; accepts controlled value/onChange props. No header, no footer — those are the parent’s responsibility. |
MultiOptionField | src/components/kb/multi-option-field.tsx | Reusable multi-select field: checkbox list of predefined options + “My Own Option” free-text. |
KbFieldOptions | src/components/kb/kb-field-options.ts | Constants file: all predefined option sets per field. |
KbDraftState | src/types/kb-draft.ts | TypeScript type for the full controlled draft value shape. |
validateKbDraft | src/lib/kb-validation.ts | Pure function: checks required fields; returns { isComplete: boolean; missingFields: string[] }. |
Reuse contract
Section titled “Reuse contract”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
KbDraftStatevalue andonChangehandler - 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.
2. KbDraftState Type
Section titled “2. KbDraftState Type”Define this type in src/types/kb-draft.ts. It mirrors the data model §2.1 and §2.2 exactly.
2a. Value shape — multi-select
Section titled “2a. Value shape — multi-select”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
FieldValueis considered empty (not satisfying a required field) when: it isnull, ORpresetsis empty AND(customText ?? "").trim() === "". - A
FieldValueis considered non-empty when:presets.length > 0OR(customText ?? "").trim() !== "". customTextbeing 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 [].
3. Accordion Structure
Section titled “3. Accordion Structure”The accordion renders twelve top-level sections in this order:
| # | Section id | Label | Icon (Material Symbols) | Default expanded |
|---|---|---|---|---|
| 1 | brand-personality | Brand personality | person_celebrate | Yes |
| 2 | primary-objective | Primary objective | target | No |
| 3 | secondary-objective | Secondary objective | flag | No |
| 4 | greetings | Greetings | waving_hand | No |
| 5 | closing | Closing | logout | No |
| 6 | emojis | Emojis | sentiment_satisfied | No |
| 7 | hashtags | Hashtags | tag | No |
| 8 | exceptions | Exceptions | block | No |
| 9 | clarifications | Clarifications | help | No |
| 10 | default-cta | Default CTA | link | No |
| 11 | custom-objectives | Custom objectives | add_circle | No |
| 12 | used-by | Used by | groups | No |
The accordion supports multiple sections open simultaneously (not mutually exclusive). The parent may pass an optional defaultExpandedIds prop to override which sections start open.
Accordion visual style
Section titled “Accordion visual style”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.
4. Section Field Definitions
Section titled “4. Section Field Definitions”4.1 Brand Personality Section
Section titled “4.1 Brand Personality Section”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 key | Label | Required | Notes |
|---|---|---|---|
overallPersona | Overall persona | Yes | |
communicationStyle | Communication style | Yes | |
desiredVibe | Desired vibe / Feeling | Yes | |
humorUsage | Humor usage | Yes | |
negativeInteractionHandling | Negative interaction handling | Yes | |
positiveInteractionHandling | Positive interaction handling | No | |
audienceRelationship | Audience relationship | No | |
famousFigureAlignment | Famous figure alignment | No | |
brandsAdmired | Brands admired | No | |
tonesToAvoid | Tones to avoid | No |
4.2 Primary Objective Section (primary-objective)
Section titled “4.2 Primary Objective Section (primary-objective)”Single MultiOptionField in the section body.
| Field key | Label | UI type | Required |
|---|---|---|---|
primaryObjective | Primary objective | MultiOptionField | No |
4.3 Secondary Objective Section (secondary-objective)
Section titled “4.3 Secondary Objective Section (secondary-objective)”Single MultiOptionField in the section body.
| Field key | Label | UI type | Required |
|---|---|---|---|
secondaryObjective | Secondary objective | MultiOptionField | No |
4.4 Greetings Section (greetings)
Section titled “4.4 Greetings Section (greetings)”Single MultiOptionField in the section body.
| Field key | Label | UI type | Required |
|---|---|---|---|
greetings | Greetings | MultiOptionField | No |
4.5 Closing Section (closing)
Section titled “4.5 Closing Section (closing)”Single MultiOptionField in the section body.
| Field key | Label | UI type | Required |
|---|---|---|---|
closing | Closing | MultiOptionField | No |
4.6 Emojis Section (emojis)
Section titled “4.6 Emojis Section (emojis)”Single MultiOptionField in the section body.
| Field key | Label | UI type | Required |
|---|---|---|---|
emojis | Emojis | MultiOptionField | No |
4.7 Hashtags Section (hashtags)
Section titled “4.7 Hashtags Section (hashtags)”Single MultiOptionField in the section body.
| Field key | Label | UI type | Required |
|---|---|---|---|
hashtags | Hashtags | MultiOptionField | No |
4.8 Exceptions Section (exceptions)
Section titled “4.8 Exceptions Section (exceptions)”Single Textarea (text-only) in the section body. No checkboxes.
| Field key | Label | UI type | Required |
|---|---|---|---|
exceptions | Exceptions | Textarea (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 key | Label | UI type | Required |
|---|---|---|---|
clarifications | Clarifications | MultiOptionField | No |
4.10 Default CTA Section (default-cta)
Section titled “4.10 Default CTA Section (default-cta)”Single MultiOptionField with URL input extension in the section body.
| Field key | Label | UI type | Required |
|---|---|---|---|
defaultCta | Default CTA | MultiOptionField + URL input | No |
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 key | Label | UI type | Required |
|---|---|---|---|
customObjectives | Custom objectives | Repeatable Textarea | No |
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.
4.12 Used By Section (Read-only)
Section titled “4.12 Used By Section (Read-only)”Displays three read-only sub-lists. No form controls.
| Sub-list | Label | Data source |
|---|---|---|
agents | Agents | Array of agent names |
socialPlatforms | Social platforms | Array of platform/config names |
reviewPlatforms | Review platforms | Array 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)5. Field UI Behavior
Section titled “5. Field UI Behavior”5.1 MultiOptionField Component
Section titled “5.1 MultiOptionField Component”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:
Preset options (checkboxes)
Section titled “Preset options (checkboxes)”- Render the
labelabove the control. Ifrequired, append*to the label. - 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.valuetopresets(keep existing presets + customText). - On toggle off: remove
opt.valuefrompresets(keep remaining presets + customText).
- Is checked when
- When all presets are unchecked and
customTextis absent/empty, setvalue = null(pristine).
”My Own Option” affordance
Section titled “”My Own Option” affordance”- 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 localcustomOpenboolean state). - On check: set
customOpen = true; ifvalueis null, initialize to{ presets: [], customText: "" }. - On uncheck: clear
customTextfrom the value; ifpresetsis also empty, setvalue = null.
- When
customOpenis true, reveal aTextareabelow the toggle row:rows={3},placeholder={customPlaceholder ?? "Describe your own option..."}.- Value:
value?.customText ?? "". - On change: update
customTextin the current value object (preservepresets).
Summary / trigger display
Section titled “Summary / trigger display”- 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
customTextis non-empty, render an additional chip labelled “My Own Option”. - If nothing is selected, show placeholder text
"Select options..."intext-[#999].
- Render each selected preset as a chip (pill badge):
Shadcn components: Checkbox, Label, Textarea. No Select dropdown — replaced by the checkbox list.
5.2 Default CTA Field
Section titled “5.2 Default CTA Field”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
Inputoftype="url"withplaceholder="https://…"for the optional URL (customUrl). - On change: update
customUrlin the value object (preservepresetsandcustomText).
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.
5.3 Exceptions Field (text-only)
Section titled “5.3 Exceptions Field (text-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 add18px - On click: append
""tocustomObjectives[]
6. Predefined Option Sets
Section titled “6. Predefined Option Sets”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):
Brand Personality Options
Section titled “Brand Personality Options”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.
Objectives, Voice & Behavior Options
Section titled “Objectives, Voice & Behavior Options”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”
7. KbSettingsAccordion Component API
Section titled “7. KbSettingsAccordion Component API”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).
Implementation note — SECTION_IDS
Section titled “Implementation note — SECTION_IDS”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.
8. Validation
Section titled “8. Validation”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.overallPersonabrandPersonality.communicationStylebrandPersonality.desiredVibebrandPersonality.humorUsagebrandPersonality.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).
9. Shadcn Components Required
Section titled “9. Shadcn Components Required”The following shadcn components must be present in the project. Add any that are missing via the shadcn CLI:
| shadcn component | Used for |
|---|---|
Accordion | Optional: may replace custom AccordionRow if preferred; otherwise keep custom implementation |
Checkbox | MultiOptionField preset option rows and “My Own Option” toggle |
Textarea | ”My Own Option” custom text input, Exceptions, Custom objectives |
Input | Default CTA URL field |
Label | Field 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-lgfor inputs,rounded-[20px]for the outer card
10. Parent Integration
Section titled “10. Parent Integration”Step 3 (Step3TrainAi)
Section titled “Step 3 (Step3TrainAi)”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 insideKbSettingsAccordion - The card header row inside the card: database icon + KB name (
DEFAULT_KB_NAME = "My Knowledge Base") + status badge (Incomplete/Completederived fromvalidateKbDraft(draft).isComplete) - The “Previous” / “Complete” footer buttons (outside the card)
KbDraftStatein local state (or from a context/store)- Call to
validateKbDraftbefore 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 Step3TrainAiconst [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>KB Detail/Editor Page
Section titled “KB Detail/Editor Page”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.
11. Out of Scope for This Version
Section titled “11. Out of Scope for This Version”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
KnowledgeBaseMappingcomponent that sits below or beside the accordion on the KB editor page; it is not a section insideKbSettingsAccordion. - IF/OR Scenarios: present in the current
step-3.tsxbut 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,SelectSeparatorimports and usage. - Add:
Checkboximport. - Props interface stays the same name (
MultiOptionFieldProps) butvalue/onChangenow use the newFieldValue/CtaFieldValueshapes. - 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 }, setonChange(null).
- Toggle on:
- “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}, updatescustomTextin value. - URL input (visible when
customOpen && showUrlInput): updatescustomUrlin 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 ifcustomTextis 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
objectivesVoicefields becomes its own top-level accordion section (see §4.2–§4.11). - The
SECTION_IDSconstant andSectionIdtype (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 correspondingvalue.objectivesVoice.*key. - Section
exceptions: render the plainTextarea(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).
- Sections
- The
brand-personalitysection body is unchanged (all 10MultiOptionFieldrows stacked withgap-5). defaultExpandedIdsdefaults 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.
Step 8 — Visual QA checklist
Section titled “Step 8 — Visual QA checklist”- 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-personalityis expanded by default. - Each of the 10 former
objectivesVoicefields is its own section with a single field in the body. - Each
MultiOptionFieldrenders 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
customTextfrom 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. -
validateKbDraftreturnsisComplete: falsewhen all required fields arenull; returnstruewhen each has at least one preset or non-empty custom text. - Used by section shows “None” when arrays are empty.