Files

216 lines
6.7 KiB
TypeScript

"use client";
import { useState } from "react";
import { BrainCircuit } 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 ResultAI from "@/components/result-ai";
import BaziChartDisplay from "@/components/modes/bazi-chart";
import LunarBirthPicker, {
todaySolarYmd,
} from "@/components/shared/lunar-birth-picker";
import RegionSelect, {
useRegionLocation,
} from "@/components/shared/region-select";
import { calculateBazi, type BaziChart } from "@/lib/calc/bazi";
import { streamAiCompletion } from "@/lib/ai/client-stream";
import { saveHistoryEntrySafe } from "@/lib/history/storage";
export default function BaziForm() {
const [date, setDate] = useState(todaySolarYmd());
const [time, setTime] = useState("12:00");
const [unknownHour, setUnknownHour] = useState(false);
const [gender, setGender] = useState<"male" | "female">("male");
const [provinceCode, setProvinceCode] = useState("");
const [cityCode, setCityCode] = useState("");
const [question, setQuestion] = useState("");
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 location = useRegionLocation(provinceCode, cityCode);
function buildInput() {
if (!location) {
return null;
}
return {
date,
time: unknownHour ? "12:00" : time,
gender,
longitude: location.longitude,
unknownHour,
};
}
function handleCalculate() {
const input = buildInput();
if (!input) {
setError("请选择出生地域");
return;
}
if (!question.trim()) {
setError("请输入问事");
return;
}
setError("");
setChart(calculateBazi(input));
setCompletion("");
}
async function handleAnalyze() {
const input = buildInput();
if (!input) {
setError("请选择出生地域");
return;
}
if (!question.trim()) {
setError("请输入问事");
return;
}
const activeChart = chart ?? calculateBazi(input);
if (!chart) {
setChart(activeChart);
}
setError("");
setWarning("");
setCompletion("");
setIsLoading(true);
try {
const text = await streamAiCompletion(
{
mode: "bazi",
payload: {
input,
question,
birthPlaceName: location!.name,
},
},
setCompletion,
);
const saveResult = await saveHistoryEntrySafe({
mode: "bazi",
title: "生辰八字解读",
question,
summary: activeChart.lunarDate,
completion: text,
baziInput: {
date: input.date,
time: input.time,
gender: input.gender,
longitude: input.longitude,
unknownHour: !!input.unknownHour,
birthPlaceName: location!.name,
},
baziChart: activeChart,
meta: {
出生地域: location!.name,
: `${input.date} ${unknownHour ? "时辰不详" : input.time}`,
农历: activeChart.lunarDate,
性别: gender === "male" ? "男" : "女",
: `${activeChart.pillars.year.ganZhi} ${activeChart.pillars.month.ganZhi} ${activeChart.pillars.day.ganZhi} ${activeChart.pillars.time.ganZhi}`,
},
});
if (!saveResult.ok) {
setWarning(saveResult.error);
}
} catch (err) {
setError(err instanceof Error ? err.message : String(err));
} finally {
setIsLoading(false);
}
}
const activeChartPreview = chart;
const downloadPreamble =
completion && location
? `# 生辰八字 AI 解读\n\n- 问事:${question}\n- 出生地域:${location.name}\n- 阳历:${date} ${unknownHour ? "时辰不详" : time}\n${activeChartPreview ? `- 农历:${activeChartPreview.lunarDate}\n` : ""}`
: undefined;
return (
<ModeWorkspace
aiPanel={
<ResultAI
panel
completion={completion}
isLoading={isLoading}
onCompletion={handleAnalyze}
error={error}
warning={warning}
emptyHint="排盘后点击「AI 测算」获取解读"
downloadFilename={completion ? "生辰八字解读.md" : undefined}
downloadPreamble={downloadPreamble}
/>
}
>
<ZenCard title="出生 · 命局" subtitle="四柱排盘所需信息" className="flex h-full min-h-0 flex-col">
<LunarBirthPicker
label="出生日期 / 时间"
solarDate={date}
time={time}
timeDisabled={unknownHour}
onSolarDateChange={setDate}
onTimeChange={setTime}
/>
<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="gender"
checked={gender === g}
onChange={() => setGender(g)}
/>
{g === "male" ? "男" : "女"}
</label>
))}
</div>
<RegionSelect
label="出生地域"
provinceCode={provinceCode}
cityCode={cityCode}
onProvinceChange={setProvinceCode}
onCityChange={setCityCode}
/>
</ZenCard>
<ZenCard title="问事 · 排盘" subtitle="所求与命盘结果" className="flex h-full min-h-0 flex-col">
<Textarea
placeholder="事业、婚姻、健康等..."
value={question}
onChange={(e) => setQuestion(e.target.value)}
rows={3}
className="border-border/60 bg-background/50"
/>
{error && !completion && (
<p className="text-sm text-destructive">{error}</p>
)}
<div className="flex gap-2">
<Button variant="outline" onClick={handleCalculate} className="flex-1">
</Button>
<Button onClick={handleAnalyze} disabled={isLoading} className="flex-1">
<BrainCircuit size={16} className="mr-1" />
AI
</Button>
</div>
{chart && <BaziChartDisplay chart={chart} />}
</ZenCard>
</ModeWorkspace>
);
}