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