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>
160 lines
4.4 KiB
TypeScript
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>;
|