123a5cce6d
Co-authored-by: Cursor <cursoragent@cursor.com>
210 lines
6.5 KiB
TypeScript
210 lines
6.5 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 { saveHistoryEntry } 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 [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("");
|
|
setCompletion("");
|
|
setIsLoading(true);
|
|
|
|
try {
|
|
const text = await streamAiCompletion(
|
|
{
|
|
mode: "bazi",
|
|
payload: {
|
|
input,
|
|
question,
|
|
birthPlaceName: location!.name,
|
|
},
|
|
},
|
|
setCompletion,
|
|
);
|
|
await saveHistoryEntry({
|
|
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}`,
|
|
},
|
|
});
|
|
} 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}
|
|
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>
|
|
);
|
|
}
|