Files
zhimingge/components/modes/combined-form.tsx
T
2026-06-13 09:39:38 +08:00

337 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { useEffect, useState } from "react";
import { Compass } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Textarea } from "@/components/ui/textarea";
import { ZenCard } from "@/components/ui/zen-card";
import { ModeWorkspace } from "@/components/layout/mode-workspace";
import Result from "@/components/result";
import ResultAI from "@/components/result-ai";
import BaziChartDisplay from "@/components/modes/bazi-chart";
import LunarBirthPicker, {
todaySolarYmd,
} from "@/components/shared/lunar-birth-picker";
import DateTimePicker, {
nowDateString,
nowTimeString,
} from "@/components/shared/datetime-picker";
import HexagramInput from "@/components/shared/hexagram-input";
import RegionSelect, {
useRegionLocation,
} from "@/components/shared/region-select";
import { calculateBazi, type BaziChart } from "@/lib/calc/bazi";
import type { GuaResult } from "@/lib/calc/hexagram";
import { streamAiCompletion } from "@/lib/ai/client-stream";
import { saveHistoryEntry } from "@/lib/history/storage";
export default function CombinedForm() {
const [birthDate, setBirthDate] = useState(todaySolarYmd());
const [birthTime, setBirthTime] = useState("12:00");
const [unknownHour, setUnknownHour] = useState(false);
const [gender, setGender] = useState<"male" | "female">("male");
const [birthProvince, setBirthProvince] = useState("");
const [birthCity, setBirthCity] = useState("");
const [currentProvince, setCurrentProvince] = useState("");
const [currentCity, setCurrentCity] = useState("");
const [calcDate, setCalcDate] = useState(nowDateString);
const [calcTime, setCalcTime] = useState(nowTimeString);
const [question, setQuestion] = useState("");
const [withHexagram, setWithHexagram] = useState(false);
const [castMode, setCastMode] = useState<"online" | "offline">("online");
const [castActive, setCastActive] = useState(false);
const [guaData, setGuaData] = useState<GuaResult | null>(null);
const [chart, setChart] = useState<BaziChart | null>(null);
const [completion, setCompletion] = useState("");
const [error, setError] = useState("");
const [isLoading, setIsLoading] = useState(false);
const birthLocation = useRegionLocation(birthProvince, birthCity);
const currentLocation = useRegionLocation(currentProvince, currentCity);
const hexagramReady =
!withHexagram ||
(question.trim() !== "" && currentLocation !== null && guaData !== null);
useEffect(() => {
setCastActive(false);
setGuaData(null);
}, [currentProvince, currentCity, castMode, question, withHexagram]);
function validate(): string | null {
if (!birthLocation) {
return "请选择出生地域";
}
if (!currentLocation) {
return "请选择当前所在地域";
}
if (!question.trim()) {
return "请输入问事内容";
}
if (withHexagram && !guaData) {
return "请完成六爻起卦,或取消附加六爻";
}
return null;
}
function handlePreview() {
const err = validate();
if (err) {
setError(err);
return;
}
setError("");
setChart(
calculateBazi({
date: birthDate,
time: unknownHour ? "12:00" : birthTime,
gender,
longitude: birthLocation!.longitude,
unknownHour,
}),
);
setCompletion("");
}
async function handleAnalyze() {
const err = validate();
if (err) {
setError(err);
return;
}
if (!chart) {
setChart(
calculateBazi({
date: birthDate,
time: unknownHour ? "12:00" : birthTime,
gender,
longitude: birthLocation!.longitude,
unknownHour,
}),
);
}
setError("");
setCompletion("");
setIsLoading(true);
const activeChart =
chart ??
calculateBazi({
date: birthDate,
time: unknownHour ? "12:00" : birthTime,
gender,
longitude: birthLocation!.longitude,
unknownHour,
});
try {
const text = await streamAiCompletion(
{
mode: "combined",
payload: {
birth: {
date: birthDate,
time: unknownHour ? "12:00" : birthTime,
gender,
longitude: birthLocation!.longitude,
unknownHour,
},
birthPlaceName: birthLocation!.name,
currentPlaceName: currentLocation!.name,
currentLongitude: currentLocation!.longitude,
calcDate,
calcTime,
question,
hexagram: withHexagram && guaData
? {
guaMark: guaData.result.guaMark,
guaTitle: guaData.result.guaTitle,
guaResult: guaData.result.guaResult,
guaChange: guaData.result.guaChange,
}
: undefined,
},
},
setCompletion,
);
saveHistoryEntry({
mode: "combined",
title: "综合测算解读",
question,
summary: activeChart.lunarDate,
completion: text,
meta: {
出生地域: birthLocation!.name,
当前地域: currentLocation!.name,
: `${calcDate} ${calcTime}`,
六爻: withHexagram && guaData ? guaData.result.guaTitle : "无",
},
});
} catch (e) {
setError(e instanceof Error ? e.message : String(e));
} finally {
setIsLoading(false);
}
}
const downloadPreamble =
completion && birthLocation && currentLocation
? `# 综合测算 AI 解读\n\n- 问事:${question}\n- 出生地域:${birthLocation.name}\n- 当前地域:${currentLocation.name}\n- 测算时间:${calcDate} ${calcTime}\n${chart ? `- 农历:${chart.lunarDate}\n` : ""}${withHexagram && guaData ? `- 六爻:${guaData.result.guaTitle}\n` : ""}`
: undefined;
return (
<ModeWorkspace
aiTitle="综合解读"
aiPanel={
<ResultAI
panel
completion={completion}
isLoading={isLoading}
onCompletion={handleAnalyze}
error={error}
emptyHint="填写完整信息后,点击「综合测算」"
downloadFilename={completion ? "综合测算解读.md" : undefined}
downloadPreamble={downloadPreamble}
/>
}
>
<ZenCard title="人和 · 生辰" subtitle="出生时空与地域" className="flex h-full min-h-0 flex-col">
<LunarBirthPicker
label="出生日期 / 时间(农历)"
solarDate={birthDate}
time={birthTime}
timeDisabled={unknownHour}
onSolarDateChange={setBirthDate}
onTimeChange={setBirthTime}
/>
<label className="flex items-center gap-2 text-xs text-muted-foreground">
<input
type="checkbox"
checked={unknownHour}
onChange={(e) => setUnknownHour(e.target.checked)}
/>
</label>
<div className="flex gap-4">
{(["male", "female"] as const).map((g) => (
<label key={g} className="flex items-center gap-2 text-sm">
<input
type="radio"
name="combined-gender"
checked={gender === g}
onChange={() => setGender(g)}
/>
{g === "male" ? "男" : "女"}
</label>
))}
</div>
<RegionSelect
label="出生地域"
provinceCode={birthProvince}
cityCode={birthCity}
onProvinceChange={setBirthProvince}
onCityChange={setBirthCity}
/>
</ZenCard>
<ZenCard title="天时地利 · 问事" subtitle="当前时空与所求" className="flex h-full min-h-0 flex-col">
<RegionSelect
label="当前所在地域"
provinceCode={currentProvince}
cityCode={currentCity}
onProvinceChange={setCurrentProvince}
onCityChange={setCurrentCity}
/>
<DateTimePicker
label="测算时刻"
date={calcDate}
time={calcTime}
onDateChange={setCalcDate}
onTimeChange={setCalcTime}
/>
<Textarea
placeholder="具体所求..."
value={question}
onChange={(e) => setQuestion(e.target.value)}
rows={3}
className="border-border/60 bg-background/50"
/>
<label className="flex items-center gap-2 text-sm">
<input
type="checkbox"
checked={withHexagram}
onChange={(e) => {
setWithHexagram(e.target.checked);
setGuaData(null);
}}
/>
</label>
{withHexagram && (
<div className="space-y-3 rounded-xl border border-border/50 bg-secondary/20 p-3">
<div className="flex gap-2">
<Button
type="button"
size="sm"
variant={castMode === "online" ? "default" : "outline"}
onClick={() => {
setCastMode("online");
setGuaData(null);
}}
>
线
</Button>
<Button
type="button"
size="sm"
variant={castMode === "offline" ? "default" : "outline"}
onClick={() => {
setCastMode("offline");
setGuaData(null);
}}
>
线
</Button>
</div>
<HexagramInput
key={castMode}
mode={castMode}
enabled={question.trim() !== "" && currentLocation !== null}
active={castActive}
onStart={() => setCastActive(true)}
onResult={setGuaData}
onClear={() => {
setGuaData(null);
setCastActive(false);
}}
/>
{guaData && (
<div className="rounded-lg border bg-card p-3">
<Result {...guaData.result} />
</div>
)}
</div>
)}
{error && !completion && (
<p className="text-sm text-destructive">{error}</p>
)}
<div className="flex gap-2">
<Button variant="outline" onClick={handlePreview} className="flex-1">
</Button>
<Button
onClick={handleAnalyze}
disabled={isLoading || (withHexagram && !hexagramReady)}
className="flex-1"
>
<Compass size={16} className="mr-1" />
</Button>
</div>
{chart && <BaziChartDisplay chart={chart} />}
</ZenCard>
</ModeWorkspace>
);
}