apps/cms/schemas/settings/ai.ts
1import {
2 DEFAULT_AGENDA_PROMPT,
3 DEFAULT_AI_MODELS,
4 DEFAULT_CONCIERGE_PROMPT,
5 DEFAULT_DELEGATE_QUESTIONS,
6 DEFAULT_PROFILE_PROMPT,
7 DEFAULT_SPEX_QUESTIONS,
8} from '@local/config'
9import { AI_TOOL_DEFINITIONS } from '@local/config/ai-capabilities'
10import {
11 DEFAULT_CONSENT_NOTICES,
12 HANDOFF_QUESTIONS,
13} from '@local/config/ai-consent'
14import { defineField } from 'sanity'
15import { withBuiltInDefault } from '../../components/AiDefaultPromptField'
16import AiModelsPanel from '../../components/AiModelsPanel'
17import AiToolToggleInput from '../../components/AiToolToggleInput'
18
19export default {
20 name: 'settings.ai',
21 type: 'document',
22 title: 'AI Assistant Settings',
23 preview: {
24 prepare() {
25 return {
26 title: 'AI Assistant Settings',
27 }
28 },
29 },
30 groups: [
31 { name: 'widget', title: 'Widget', default: true },
32 { name: 'prompts', title: 'Prompts' },
33 { name: 'tools', title: 'Tools & launch controls' },
34 { name: 'leadCapture', title: 'Lead capture' },
35 { name: 'models', title: 'Models' },
36 ],
37 fields: [
38 defineField({
39 name: 'pageVisibility',
40 title: 'Where to show the assistant',
41 type: 'object',
42 group: 'widget',
43 description:
44 'Applies to all pages in each category. An individual page’s AI visibility setting overrides this. Unset preserves existing behavior. Publish and refresh to apply.',
45 fields: [
46 ['homepage', 'Homepage'],
47 ['editorial', 'Editorial pages'],
48 ['paris', 'All Paris event pages'],
49 ['miami', 'All Miami event pages'],
50 ['other', 'Other pages'],
51 ].map(([name, title]) =>
52 defineField({
53 name,
54 title,
55 type: 'string',
56 options: {
57 list: [
58 { title: 'Existing behavior', value: 'inherit' },
59 { title: 'Show', value: 'show' },
60 { title: 'Hide', value: 'hide' },
61 ],
62 },
63 }),
64 ),
65 }),
66 defineField({
67 name: 'productionEnabled',
68 title: 'Show AI on production',
69 type: 'boolean',
70 group: 'widget',
71 initialValue: false,
72 description:
73 'Off by default. Publish to enable the widget AND AI endpoints on the live site. Local and staging remain available. AI_AGENT_ENABLED=false is an emergency production override. Refresh the page after publishing.',
74 }),
75 defineField({
76 name: 'allowEmailSubmissions',
77 title: 'Allow email / contact submissions',
78 type: 'boolean',
79 group: 'tools',
80 initialValue: false,
81 description:
82 'Off by default in every environment. Leave off for an information-only launch: no lead submissions, agenda emails, contact collection or consent requests. Visitors can still see information and build agendas in chat. Publish changes to apply.',
83 }),
84 defineField({
85 name: 'stagingEmailSubmissions',
86 title: 'Allow submissions on staging only',
87 type: 'boolean',
88 group: 'tools',
89 initialValue: false,
90 description:
91 'Only applies to the pre-prod branch deployed as Vercel Preview. Never enables production submissions. Overrides the general submission switch on that staging branch; individual tool switches still apply. Publish to apply.',
92 }),
93 defineField({
94 name: 'tools',
95 title: 'Available tools',
96 type: 'object',
97 group: 'tools',
98 description:
99 'Unset tools are enabled, except submissions always require the master switch above. Disabling a tool removes it and its associated prompt instructions. These published settings apply in all environments and to existing conversations on their next turn. Agenda email also requires agenda building.',
100 fields: AI_TOOL_DEFINITIONS.map(({ name, title }) =>
101 defineField({
102 name,
103 title,
104 type: 'boolean',
105 initialValue: true,
106 components: { input: AiToolToggleInput },
107 }),
108 ),
109 }),
110 defineField({
111 name: 'defaultMessage',
112 title: 'Default Assistant Message',
113 description:
114 "Shown as the assistant's first message before the user sends anything.",
115 type: 'string',
116 group: 'widget',
117 validation: (Rule) => Rule.required().min(1),
118 initialValue: 'Hey! What would you like to build?',
119 }),
120 defineField({
121 name: 'conciergePrompt',
122 title: 'Concierge System Prompt',
123 description:
124 "Master system prompt for the website assistant. Leave empty to use the built-in default. Routing rules based on the visitor's page, the lead-capture configuration below, and Additional Instructions are appended automatically.",
125 type: 'text',
126 rows: 24,
127 group: 'prompts',
128 components: { field: withBuiltInDefault(DEFAULT_CONCIERGE_PROMPT) },
129 }),
130 defineField({
131 name: 'additionalInstructions',
132 title: 'Additional Instructions',
133 description:
134 'Appended to the concierge prompt as team instructions — the quick, safe way to add campaign notes, temporary rules, or emphasis without editing the master prompt above.',
135 type: 'text',
136 rows: 8,
137 group: 'prompts',
138 }),
139 defineField({
140 name: 'agendaPrompt',
141 title: 'Agenda Builder Prompt',
142 description:
143 'System prompt for the specialised agenda-suggestion agent. Leave empty to use the built-in default.',
144 type: 'text',
145 rows: 12,
146 group: 'prompts',
147 components: { field: withBuiltInDefault(DEFAULT_AGENDA_PROMPT) },
148 }),
149 defineField({
150 name: 'profilePrompt',
151 title: 'Profile Extraction Prompt',
152 description:
153 'Instructions for the model that extracts visitor details (name, email, role, stage) from chat messages. Leave empty to use the built-in default.',
154 type: 'text',
155 rows: 12,
156 group: 'prompts',
157 components: { field: withBuiltInDefault(DEFAULT_PROFILE_PROMPT) },
158 }),
159 defineField({
160 name: 'consentNotices',
161 title: 'Privacy and consent notices',
162 type: 'object',
163 group: 'leadCapture',
164 description:
165 'Separate notices shown after contact details are complete and before sending. Markdown links are supported. These notices do not grant marketing opt-in. Leave a notice empty to use its built-in default.',
166 fields: (['tickets', 'sponsorship', 'agenda'] as const).map((purpose) =>
167 defineField({
168 name: purpose,
169 title: {
170 tickets: 'Ticket enquiries',
171 sponsorship: 'Sponsorship enquiries',
172 agenda: 'Agenda emails',
173 }[purpose],
174 type: 'text',
175 rows: 5,
176 description: `Edit the privacy wording and links. The assistant automatically adds the confirmation question: “${HANDOFF_QUESTIONS[purpose]}”`,
177 components: {
178 field: withBuiltInDefault(DEFAULT_CONSENT_NOTICES[purpose]),
179 },
180 }),
181 ),
182 }),
183 defineField({
184 name: 'delegateQualifyingQuestions',
185 title: 'Qualifying Questions — Tickets & Delegates',
186 description:
187 'What the assistant asks (conversationally, one or two at a time) before creating a ticket/delegate lead.',
188 type: 'array',
189 of: [{ type: 'string' }],
190 group: 'leadCapture',
191 components: { field: withBuiltInDefault(DEFAULT_DELEGATE_QUESTIONS) },
192 }),
193 defineField({
194 name: 'spexQualifyingQuestions',
195 title: 'Qualifying Questions — Sponsorship & Exhibitors',
196 description:
197 'What the assistant asks before creating a sponsorship/exhibitor (SPEX) lead.',
198 type: 'array',
199 of: [{ type: 'string' }],
200 group: 'leadCapture',
201 components: { field: withBuiltInDefault(DEFAULT_SPEX_QUESTIONS) },
202 }),
203 defineField({
204 name: 'modelsStatus',
205 title: 'Currently active models',
206 type: 'string',
207 group: 'models',
208 readOnly: true,
209 components: { field: AiModelsPanel },
210 }),
211 defineField({
212 name: 'conciergeModel',
213 title: 'Concierge Model',
214 description: `Vercel AI Gateway model id (e.g. anthropic/claude-sonnet-4.6). Leave empty for the built-in default (${DEFAULT_AI_MODELS.concierge}). An AI_MODEL_CONCIERGE env var overrides this.`,
215 type: 'string',
216 group: 'models',
217 }),
218 defineField({
219 name: 'agendaModel',
220 title: 'Agenda Builder Model',
221 description: `Vercel AI Gateway model id. Leave empty for the built-in default (${DEFAULT_AI_MODELS.agenda}). AI_MODEL_AGENDA env var overrides.`,
222 type: 'string',
223 group: 'models',
224 }),
225 defineField({
226 name: 'profileModel',
227 title: 'Profile Extraction Model',
228 description: `Vercel AI Gateway model id (a small fast model is right here). Leave empty for the built-in default (${DEFAULT_AI_MODELS.profile}). AI_MODEL_PROFILE env var overrides.`,
229 type: 'string',
230 group: 'models',
231 }),
232 ],
233}
234