Files
Jyotisha/frontend/src/lib/onboarding-payload.ts
T
Jesse_Chen a6d4473ef0 feat(onboarding): write a question for every consultation domain and stop generating an unread greeting
The home screen renders all ten domains from the consultation registry,
but the Agent only ever wrote three of them; the other seven were static
registry prompts dressed up as personalized starting points. The payload
now has to cover every domain in registry order, validated as a set
rather than per item, so a short or misordered answer is rejected whole
instead of silently leaving cards on static copy.

The greeting went the other way. Nothing has rendered it since the hero
note was removed, so it leaves the schema, the prompt, and the client
contract rather than costing tokens for text no one reads.

Ten questions take much longer to generate than three, so the route,
the server generation budget, and the client request deadline all grow
together, and the cache version bump forces existing payloads to be
regenerated once under the new shape.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-18 15:44:19 +08:00

52 lines
2.1 KiB
TypeScript

import { z } from "zod";
import { consultationDomainSchema, consultationDomainIds } from "./consultation-domain-registry";
import { generalGuidedJyotishTopics } from "./guided-jyotish-topics";
const detachedStarterQuestionPattern = /印度占星|一般如何|通常(?:会)?(?:看|观察|分析|理解|包含)|哪些(?:因素|证据层)|如何划分/;
const userCenteredStarterQuestionSchema = z.string()
.trim()
.min(4)
.max(80)
.refine((text) => text.includes("我"), "starter_question_must_be_first_person")
.refine((text) => !detachedStarterQuestionPattern.test(text), "starter_question_must_not_be_encyclopedic");
// Every domain the home screen renders needs a question, in registry order, so the
// page never has to fall back to a static prompt for part of the grid.
export const onboardingSuggestionThemes = consultationDomainIds;
const onboardingSchema = z.object({
suggestions: z.array(z.object({
theme: consultationDomainSchema,
text: userCenteredStarterQuestionSchema,
})).refine(
(items) => items.length === onboardingSuggestionThemes.length
&& items.every((item, index) => item.theme === onboardingSuggestionThemes[index]),
"suggestions_must_cover_every_domain_in_order",
),
});
export type OnboardingPayload = z.infer<typeof onboardingSchema>;
export const fallbackOnboardingPayload: OnboardingPayload = {
suggestions: generalGuidedJyotishTopics.map((topic) => ({ theme: topic.id, text: topic.prompt })),
};
class OnboardingJsonError extends Error {
readonly name = "OnboardingJsonError";
}
export function parseOnboardingPayload(value: unknown): OnboardingPayload | null {
const parsed = onboardingSchema.safeParse(value);
return parsed.success ? parsed.data : null;
}
export function parseOnboardingText(text: string): OnboardingPayload | null {
const normalized = text.trim().replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/, "");
const start = normalized.indexOf("{");
const end = normalized.lastIndexOf("}");
if (start < 0 || end <= start) throw new OnboardingJsonError("onboarding_json_missing");
const parsed: unknown = JSON.parse(normalized.slice(start, end + 1));
return parseOnboardingPayload(parsed);
}