187b08c3e1
Co-authored-by: Cursor <cursoragent@cursor.com>
364 lines
12 KiB
TypeScript
364 lines
12 KiB
TypeScript
"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 { saveHistoryEntrySafe } 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 [warning, setWarning] = 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("");
|
||
setWarning("");
|
||
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,
|
||
);
|
||
const saveResult = await saveHistoryEntrySafe({
|
||
mode: "combined",
|
||
title: "综合测算解读",
|
||
question,
|
||
summary: activeChart.lunarDate,
|
||
completion: text,
|
||
baziInput: {
|
||
date: birthDate,
|
||
time: unknownHour ? "12:00" : birthTime,
|
||
gender,
|
||
longitude: birthLocation!.longitude,
|
||
unknownHour,
|
||
birthPlaceName: birthLocation!.name,
|
||
},
|
||
baziChart: activeChart,
|
||
hexagram:
|
||
withHexagram && guaData
|
||
? {
|
||
guaMark: guaData.result.guaMark,
|
||
guaTitle: guaData.result.guaTitle,
|
||
guaResult: guaData.result.guaResult,
|
||
guaChange: guaData.result.guaChange,
|
||
}
|
||
: undefined,
|
||
meta: {
|
||
出生地域: birthLocation!.name,
|
||
当前地域: currentLocation!.name,
|
||
测算时间: `${calcDate} ${calcTime}`,
|
||
农历: activeChart.lunarDate,
|
||
性别: gender === "male" ? "男" : "女",
|
||
四柱: `${activeChart.pillars.year.ganZhi} ${activeChart.pillars.month.ganZhi} ${activeChart.pillars.day.ganZhi} ${activeChart.pillars.time.ganZhi}`,
|
||
六爻: withHexagram && guaData ? guaData.result.guaTitle : "无",
|
||
},
|
||
});
|
||
if (!saveResult.ok) {
|
||
setWarning(saveResult.error);
|
||
}
|
||
} 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}
|
||
warning={warning}
|
||
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>
|
||
);
|
||
}
|