Claude-Session: https://claude.ai/code/session_01UmSCnoCgXqBp2fU3vwz4o1 Co-authored-by: Claude <noreply@anthropic.com>
193 lines
5.6 KiB
TypeScript
193 lines
5.6 KiB
TypeScript
import * as z from "zod";
|
|
|
|
/**
|
|
* Interactive cards: a fixed catalog of sections a bot fills with content. The model never
|
|
* chooses layout or markup; the backend validates every card and applies every tap.
|
|
*/
|
|
|
|
export const CARD_LIMITS = {
|
|
sections: 5,
|
|
facts: 8,
|
|
checklistItems: 30,
|
|
steps: 20,
|
|
stepText: 300,
|
|
/** Longer lists show this many entries until the user opens the rest. */
|
|
collapsedEntries: 8,
|
|
stepperMax: 99,
|
|
timerSeconds: 24 * 60 * 60,
|
|
selectOptions: 6,
|
|
actions: 3,
|
|
label: 80,
|
|
title: 120,
|
|
meta: 3,
|
|
} as const;
|
|
|
|
const Label = z.string().trim().min(1).max(CARD_LIMITS.label);
|
|
const SectionId = z.string().min(1).max(40);
|
|
|
|
export const CardHeaderSchema = z.object({
|
|
type: z.literal("header"),
|
|
title: z.string().trim().min(1).max(CARD_LIMITS.title),
|
|
subtitle: z.string().trim().max(CARD_LIMITS.title).optional(),
|
|
/** Short facts beside the title, such as "30 min" or "4 servings". */
|
|
meta: z.array(Label).max(CARD_LIMITS.meta).optional(),
|
|
});
|
|
|
|
export const CardFactsSchema = z.object({
|
|
type: z.literal("facts"),
|
|
rows: z
|
|
.array(z.object({ label: Label, value: z.string().trim().min(1).max(200) }))
|
|
.min(1)
|
|
.max(CARD_LIMITS.facts),
|
|
});
|
|
|
|
/** "200 g" of something, scaled by the card's stepper; `plural` names more than one ("Eggs"). */
|
|
export const CardQuantitySchema = z.object({
|
|
amount: z.number().positive().max(100_000),
|
|
unit: z.string().trim().max(20).optional(),
|
|
plural: Label.optional(),
|
|
});
|
|
export type CardQuantity = z.infer<typeof CardQuantitySchema>;
|
|
|
|
export const CardChecklistSchema = z.object({
|
|
type: z.literal("checklist"),
|
|
id: SectionId,
|
|
items: z
|
|
.array(
|
|
z.object({
|
|
id: SectionId,
|
|
label: Label,
|
|
quantity: CardQuantitySchema.optional(),
|
|
checked: z.boolean().optional(),
|
|
}),
|
|
)
|
|
.min(1)
|
|
.max(CARD_LIMITS.checklistItems),
|
|
});
|
|
|
|
/** A number the user turns up or down, such as servings; quantities scale from `base`. */
|
|
export const CardStepperSchema = z.object({
|
|
type: z.literal("stepper"),
|
|
id: SectionId,
|
|
label: Label,
|
|
base: z.number().int().min(1).max(CARD_LIMITS.stepperMax),
|
|
value: z.number().int().min(1).max(CARD_LIMITS.stepperMax),
|
|
});
|
|
|
|
/** Numbered steps, optionally timed; the card remembers the current step and running timers. */
|
|
export const CardStepsSchema = z.object({
|
|
type: z.literal("steps"),
|
|
id: SectionId,
|
|
steps: z
|
|
.array(
|
|
z.object({
|
|
id: SectionId,
|
|
text: z.string().trim().min(1).max(CARD_LIMITS.stepText),
|
|
timerSeconds: z.number().int().min(1).max(CARD_LIMITS.timerSeconds).optional(),
|
|
/** When a started timer runs out; stored so it survives a reload or another device. */
|
|
timerEndsAt: z.string().datetime().optional(),
|
|
}),
|
|
)
|
|
.min(1)
|
|
.max(CARD_LIMITS.steps),
|
|
current: z
|
|
.number()
|
|
.int()
|
|
.min(0)
|
|
.max(CARD_LIMITS.steps - 1)
|
|
.optional(),
|
|
});
|
|
|
|
export const CardSelectSchema = z.object({
|
|
type: z.literal("select"),
|
|
id: SectionId,
|
|
/** Several options before sending; otherwise one tap answers. */
|
|
multiple: z.boolean().optional(),
|
|
options: z
|
|
.array(z.object({ id: SectionId, label: Label }))
|
|
.min(2)
|
|
.max(CARD_LIMITS.selectOptions),
|
|
selected: z.array(SectionId).optional(),
|
|
});
|
|
|
|
/**
|
|
* "reply" sends the label, with any selection, to the bot as the user's answer. "steps" opens the
|
|
* card's steps one at a time in full screen (cooking mode) and never reaches the bot.
|
|
*/
|
|
export const CardActionSchema = z.object({
|
|
id: SectionId,
|
|
label: Label,
|
|
kind: z.enum(["reply", "steps"]),
|
|
});
|
|
|
|
export const CardActionsSchema = z.object({
|
|
type: z.literal("actions"),
|
|
actions: z.array(CardActionSchema).min(1).max(CARD_LIMITS.actions),
|
|
});
|
|
|
|
export const CardSectionSchema = z.discriminatedUnion("type", [
|
|
CardHeaderSchema,
|
|
CardFactsSchema,
|
|
CardStepperSchema,
|
|
CardChecklistSchema,
|
|
CardStepsSchema,
|
|
CardSelectSchema,
|
|
CardActionsSchema,
|
|
]);
|
|
export type CardSection = z.infer<typeof CardSectionSchema>;
|
|
|
|
/** Set once the user answered the card; the card then stays as it was answered. */
|
|
export const CardAnswerSchema = z.object({ text: z.string(), actionId: SectionId.optional() });
|
|
|
|
/**
|
|
* One card per block. `lines` is the older read-only shape, kept so stored messages still parse;
|
|
* new cards carry `id` and `sections`.
|
|
*/
|
|
export const CardBlockSchema = z.object({
|
|
kind: z.literal("card"),
|
|
id: SectionId.optional(),
|
|
lines: z.array(z.object({ k: z.string(), v: z.string() })).optional(),
|
|
sections: z.array(CardSectionSchema).max(CARD_LIMITS.sections).optional(),
|
|
answer: CardAnswerSchema.optional(),
|
|
});
|
|
export type CardBlock = z.infer<typeof CardBlockSchema>;
|
|
|
|
/** What a tap on a card asks the backend to do. Toggles carry the new state, so a retry is a no-op. */
|
|
export const CardActSchema = z.discriminatedUnion("type", [
|
|
z.object({
|
|
type: z.literal("toggle"),
|
|
sectionId: SectionId,
|
|
itemId: SectionId,
|
|
checked: z.boolean(),
|
|
}),
|
|
z.object({
|
|
type: z.literal("select"),
|
|
sectionId: SectionId,
|
|
optionId: SectionId,
|
|
/** Multiple choice only: whether the option is now chosen. */
|
|
selected: z.boolean().optional(),
|
|
}),
|
|
z.object({ type: z.literal("action"), actionId: SectionId }),
|
|
z.object({
|
|
type: z.literal("stepper"),
|
|
sectionId: SectionId,
|
|
value: z.number().int().min(1).max(CARD_LIMITS.stepperMax),
|
|
}),
|
|
z.object({
|
|
type: z.literal("step"),
|
|
sectionId: SectionId,
|
|
index: z
|
|
.number()
|
|
.int()
|
|
.min(0)
|
|
.max(CARD_LIMITS.steps - 1),
|
|
}),
|
|
z.object({
|
|
type: z.literal("timer"),
|
|
sectionId: SectionId,
|
|
stepId: SectionId,
|
|
running: z.boolean(),
|
|
}),
|
|
]);
|
|
export type CardAct = z.infer<typeof CardActSchema>;
|