Files
dynamic-sites-simple/shared/src/schemas/index.ts
Khalid A 3cf3694ee7 Add message intent classification (edit/info/help) before routing
Introduces a two-LLM-call pipeline: the first call classifies the user's
message intent as "edit", "info", or "help". Edit messages proceed through
the existing routing → edit → propose flow. Info messages get a generated
response about site content from the manifest. Help messages get a
templated capabilities overview. This handles open-ended questions like
"What can I do?" or "What does my site have on it?" which previously
had no path through the system.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 20:17:22 -05:00

160 lines
4.4 KiB
TypeScript

import { z } from 'zod';
// ── Site Context ──
export const siteContextSchema = z.object({
businessName: z.string(),
tagline: z.string().optional(),
tone: z.string().default('professional and friendly'),
style: z.string().optional(),
promptContext: z.string().optional(),
primaryColor: z.string().optional(),
contactEmail: z.string().email().optional(),
contactPhone: z.string().optional(),
address: z.string().optional(),
});
export type SiteContext = z.infer<typeof siteContextSchema>;
// ── Section File (union of section types) ──
const baseSectionFields = {
id: z.string(),
order: z.number().int().default(0),
visible: z.boolean().default(true),
image: z.string().optional(),
};
export const heroSectionSchema = z.object({
...baseSectionFields,
type: z.literal('hero'),
headline: z.string(),
subheading: z.string().optional(),
ctaText: z.string().optional(),
ctaLink: z.string().optional(),
});
export const aboutSectionSchema = z.object({
...baseSectionFields,
type: z.literal('about'),
title: z.string().default('About Us'),
content: z.string(),
});
export const featureItemSchema = z.object({
title: z.string(),
description: z.string(),
icon: z.string().optional(),
});
export const featuresSectionSchema = z.object({
...baseSectionFields,
type: z.literal('features'),
title: z.string().default('What We Offer'),
items: z.array(featureItemSchema).min(1),
});
export const testimonialItemSchema = z.object({
quote: z.string(),
author: z.string(),
role: z.string().optional(),
});
export const testimonialsSectionSchema = z.object({
...baseSectionFields,
type: z.literal('testimonials'),
title: z.string().default('What Our Clients Say'),
items: z.array(testimonialItemSchema).min(1),
});
export const textSectionSchema = z.object({
...baseSectionFields,
type: z.literal('text'),
heading: z.string().optional(),
content: z.string(),
});
export const sectionFileSchema = z.discriminatedUnion('type', [
heroSectionSchema,
aboutSectionSchema,
featuresSectionSchema,
testimonialsSectionSchema,
textSectionSchema,
]);
export type SectionFile = z.infer<typeof sectionFileSchema>;
// ── Events ──
export const eventItemSchema = z.object({
id: z.string(),
title: z.string(),
description: z.string().optional(),
date: z.string(),
time: z.string().optional(),
location: z.string().optional(),
});
export const eventsFileSchema = z.object({
events: z.array(eventItemSchema),
});
export type EventsFile = z.infer<typeof eventsFileSchema>;
// ── SMS Sites Config ──
export const smsSiteEntrySchema = z.object({
siteId: z.string(),
phoneNumber: z.string(),
allowedSenders: z.array(z.string()),
repoRoot: z.string().optional(),
});
export const smsSitesConfigSchema = z.object({
sites: z.array(smsSiteEntrySchema),
});
export type SmsSitesConfig = z.infer<typeof smsSitesConfigSchema>;
// ── Edit Request ──
export const editRequestSchema = z.object({
message: z.string().min(1),
repo_relative_path: z.string().optional(),
proposal_id: z.string().optional(),
confirm: z.enum(['yes', 'no']).optional(),
});
export type EditRequest = z.infer<typeof editRequestSchema>;
export const editJobPayloadSchema = z.discriminatedUnion('kind', [
z.object({
kind: z.literal('propose'),
id: z.string(),
message: z.string(),
repo_relative_path: z.string().optional(),
source: z.enum(['sms', 'http', 'editor']).default('http'),
smsReplyMeta: z.object({
from: z.string(),
to: z.string(),
}).optional(),
}),
z.object({
kind: z.literal('apply'),
id: z.string(),
proposal_id: z.string(),
source: z.enum(['sms', 'http', 'editor']).default('http'),
smsReplyMeta: z.object({
from: z.string(),
to: z.string(),
}).optional(),
}),
]);
export type EditJobPayload = z.infer<typeof editJobPayloadSchema>;
// ── Classification output (first LLM call) ──
export const classificationSchema = z.object({
intent: z.enum(['edit', 'info', 'help']),
reason: z.string(),
});
export type ClassificationOutput = z.infer<typeof classificationSchema>;
// ── Routing output (LLM structured output) ──
export const routingOutputSchema = z.object({
repo_relative_path: z.string(),
needs_clarification: z.boolean().default(false),
reason: z.string(),
clarification_message: z.string().optional(),
});
export type RoutingOutput = z.infer<typeof routingOutputSchema>;