6265e56a7f
Learn uses 64-gua card grid; liuyao/bazi/combined use two input cards plus sticky right AI panel; add manifest, service worker, and install prompt. Co-authored-by: Cursor <cursoragent@cursor.com>
292 lines
8.9 KiB
TypeScript
292 lines
8.9 KiB
TypeScript
"use client";
|
||
|
||
import { 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 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";
|
||
|
||
export default function CombinedForm() {
|
||
const [birthDate, setBirthDate] = useState("1990-01-01");
|
||
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 [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);
|
||
|
||
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);
|
||
|
||
try {
|
||
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,
|
||
);
|
||
} catch (e) {
|
||
setError(e instanceof Error ? e.message : String(e));
|
||
} finally {
|
||
setIsLoading(false);
|
||
}
|
||
}
|
||
|
||
return (
|
||
<ModeWorkspace
|
||
aiTitle="综合解读"
|
||
aiPanel={
|
||
<ResultAI
|
||
panel
|
||
completion={completion}
|
||
isLoading={isLoading}
|
||
onCompletion={handleAnalyze}
|
||
error={error}
|
||
emptyHint="填写完整信息后,点击「综合测算」"
|
||
/>
|
||
}
|
||
>
|
||
<ZenCard title="人和 · 生辰" subtitle="出生时空与地域">
|
||
<DateTimePicker
|
||
label="出生日期 / 时间"
|
||
date={birthDate}
|
||
time={birthTime}
|
||
timeDisabled={unknownHour}
|
||
onDateChange={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="当前时空与所求">
|
||
<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}
|
||
onResult={setGuaData}
|
||
onClear={() => setGuaData(null)}
|
||
/>
|
||
{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>
|
||
);
|
||
}
|