feat: back daily starlanguage with API card
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
type Profile = {
|
||||
name?: string;
|
||||
date?: string;
|
||||
time?: string;
|
||||
provinceCode?: string;
|
||||
cityCode?: string;
|
||||
};
|
||||
|
||||
const cards = [
|
||||
{ trend: "先收束,再推进。适合把一个悬而未决的问题拆小。", action: "选一件最重要的事,给它留出 45 分钟不被打断的时间。", caution: "避免在情绪最满时做承诺。" },
|
||||
{ trend: "适合整理关系与边界。越清楚,越不容易被外界节奏带走。", action: "把今天要回复的人和要推迟的事分开列出来。", caution: "不要把暂时的沉默误读成最终答案。" },
|
||||
{ trend: "执行力比灵感更重要。小步完成会比大计划更有力量。", action: "先完成一个可交付版本,再考虑优化。", caution: "别让完美感拖慢开始。" },
|
||||
{ trend: "适合观察资源流向:时间、注意力、金钱都算。", action: "检查一个正在消耗你的习惯,并给它设上限。", caution: "不要为了短期安心做长期成本高的选择。" },
|
||||
];
|
||||
|
||||
function pickCard(profile: Profile, today: string) {
|
||||
const seed = `${today}-${profile.date ?? ""}-${profile.time ?? ""}-${profile.provinceCode ?? ""}-${profile.cityCode ?? ""}`;
|
||||
const index = Array.from(seed).reduce((sum, char) => sum + char.charCodeAt(0), 0) % cards.length;
|
||||
return cards[index];
|
||||
}
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const body = await request.json().catch(() => null) as { profile?: Profile; today?: string } | null;
|
||||
const profile = body?.profile ?? {};
|
||||
const today = body?.today || new Date().toISOString().slice(0, 10);
|
||||
return NextResponse.json({
|
||||
status: "ok",
|
||||
card: pickCard(profile, today),
|
||||
source: "calculation_lite",
|
||||
claim_status: "exploratory_unvalidated",
|
||||
boundary: "not_deterministic_prediction",
|
||||
});
|
||||
}
|
||||
@@ -74,6 +74,13 @@ type OnboardingContent = { greeting: string; suggestions: OnboardingSuggestion[]
|
||||
type OnboardingStep = "name" | "birth" | "place";
|
||||
type GreetingPeriod = "morning" | "noon" | "afternoon" | "evening" | "late-night";
|
||||
type DailyStarlanguageCard = { trend: string; action: string; caution: string };
|
||||
type DailyStarlanguageApiResponse = {
|
||||
status?: "ok";
|
||||
card?: DailyStarlanguageCard;
|
||||
source?: "calculation_lite";
|
||||
claim_status?: "exploratory_unvalidated";
|
||||
boundary?: "not_deterministic_prediction";
|
||||
};
|
||||
type SessionReadResult = { readonly sessions: ChatSession[]; readonly fallbackSessionIds: string[] };
|
||||
type PendingConsultation = {
|
||||
readonly requestId: string;
|
||||
@@ -341,6 +348,18 @@ function buildDailyStarlanguageCard(profile: Profile) {
|
||||
return dailyStarlanguageCards[index];
|
||||
}
|
||||
|
||||
async function fetchDailyStarlanguage(profile: Profile) {
|
||||
const response = await fetch("/api/daily-starlanguage", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ profile }),
|
||||
});
|
||||
if (!response.ok) throw new Error("daily_starlanguage_unavailable");
|
||||
const payload = await response.json().catch(() => null) as DailyStarlanguageApiResponse | null;
|
||||
if (payload?.status !== "ok" || !payload.card) throw new Error("daily_starlanguage_invalid");
|
||||
return payload.card;
|
||||
}
|
||||
|
||||
function buildBirthTimeRectificationQuestion(profile: Profile) {
|
||||
return [
|
||||
`请为${profile.name || "我"}做生时校正辅助。`,
|
||||
@@ -617,6 +636,7 @@ export default function Home() {
|
||||
const [otherProfileDraft, setOtherProfileDraft] = useState<Profile>(emptyProfile);
|
||||
const [synastryReportCard, setSynastryReportCard] = useState<SynastryReportCard | null>(null);
|
||||
const [synastryHistory, setSynastryHistory] = useState<SynastryReportCard[]>([]);
|
||||
const [dailyStarlanguageCard, setDailyStarlanguageCard] = useState<DailyStarlanguageCard | null>(null);
|
||||
const [profileOpen, setProfileOpen] = useState(false);
|
||||
const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false);
|
||||
const [profileNotice, setProfileNotice] = useState("");
|
||||
@@ -732,7 +752,7 @@ export default function Home() {
|
||||
}, [accountId, profile]);
|
||||
|
||||
const profileComplete = isProfileComplete(profile);
|
||||
const dailyStarlanguage = profileComplete ? buildDailyStarlanguageCard(profile) : null;
|
||||
const dailyStarlanguage = dailyStarlanguageCard ?? (profileComplete ? buildDailyStarlanguageCard(profile) : null);
|
||||
const onboardingPending = profileComplete && !onboarding && !onboardingError;
|
||||
const currentOnboardingMessage = onboardingJustCompleted
|
||||
? startGreeting || completedOnboardingMessage(profileDraft.name.trim())
|
||||
@@ -967,6 +987,22 @@ export default function Home() {
|
||||
};
|
||||
}, [accountId, hydrated, onboarding, onboardingError, profile.name, profileComplete, startGreeting]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!hydrated || !profileComplete) return;
|
||||
let cancelled = false;
|
||||
setDailyStarlanguageCard(null);
|
||||
void fetchDailyStarlanguage(profile)
|
||||
.then((card) => {
|
||||
if (!cancelled) setDailyStarlanguageCard(card);
|
||||
})
|
||||
.catch(() => {
|
||||
if (!cancelled) setDailyStarlanguageCard(buildDailyStarlanguageCard(profile));
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [hydrated, profile.date, profile.time, profile.provinceCode, profile.cityCode, profileComplete]);
|
||||
|
||||
useEffect(() => {
|
||||
const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
||||
conversationEnd.current?.scrollIntoView({ behavior: isLoading || reduceMotion ? "auto" : "smooth", block: "end" });
|
||||
|
||||
@@ -2,11 +2,13 @@ from pathlib import Path
|
||||
|
||||
|
||||
PAGE = Path("frontend/src/app/page.tsx")
|
||||
DAILY_ROUTE = Path("frontend/src/app/api/daily-starlanguage/route.ts")
|
||||
|
||||
|
||||
def test_daily_starlanguage_entrypoint_is_productized() -> None:
|
||||
source = PAGE.read_text(encoding="utf-8")
|
||||
assert "今日星语" in source
|
||||
assert "fetchDailyStarlanguage" in source
|
||||
assert "buildDailyStarlanguageCard" in source
|
||||
assert "daily-starlanguage-card" in source
|
||||
assert "今日趋势" in source
|
||||
@@ -17,6 +19,14 @@ def test_daily_starlanguage_entrypoint_is_productized() -> None:
|
||||
assert "不是确定预测" in source
|
||||
|
||||
|
||||
def test_daily_starlanguage_api_declares_honest_source_boundary() -> None:
|
||||
source = DAILY_ROUTE.read_text(encoding="utf-8")
|
||||
assert "status: \"ok\"" in source
|
||||
assert "calculation_lite" in source
|
||||
assert "exploratory_unvalidated" in source
|
||||
assert "not_deterministic_prediction" in source
|
||||
|
||||
|
||||
def test_birth_time_rectification_entrypoint_is_productized() -> None:
|
||||
source = PAGE.read_text(encoding="utf-8")
|
||||
assert "生时校正" in source
|
||||
|
||||
Reference in New Issue
Block a user