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>
214 lines
6.2 KiB
TypeScript
214 lines
6.2 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useState } from "react";
|
|
import { BrainCircuit, ListRestart } 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 DateTimePicker, {
|
|
nowDateString,
|
|
nowTimeString,
|
|
} from "@/components/shared/datetime-picker";
|
|
import HexagramInput from "@/components/shared/hexagram-input";
|
|
import RegionSelect, {
|
|
useRegionLocation,
|
|
} from "@/components/shared/region-select";
|
|
import { streamAiCompletion } from "@/lib/ai/client-stream";
|
|
import type { GuaResult } from "@/lib/calc/hexagram";
|
|
import todayJson from "@/lib/data/today.json";
|
|
|
|
const todayData: string[] = todayJson;
|
|
|
|
export default function LiuyaoForm() {
|
|
const [question, setQuestion] = useState("");
|
|
const [provinceCode, setProvinceCode] = useState("");
|
|
const [cityCode, setCityCode] = useState("");
|
|
const [calcDate, setCalcDate] = useState(nowDateString);
|
|
const [calcTime, setCalcTime] = useState(nowTimeString);
|
|
const [castMode, setCastMode] = useState<"online" | "offline">("online");
|
|
const [guaData, setGuaData] = useState<GuaResult | null>(null);
|
|
const [completion, setCompletion] = useState("");
|
|
const [error, setError] = useState("");
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
|
|
const location = useRegionLocation(provinceCode, cityCode);
|
|
const formReady = question.trim() !== "" && location !== null;
|
|
|
|
function validate(): string | null {
|
|
if (!question.trim()) {
|
|
return "请输入问事";
|
|
}
|
|
if (!location) {
|
|
return "请选择起卦省份";
|
|
}
|
|
if (!guaData) {
|
|
return "请先完成 6 次起卦";
|
|
}
|
|
return null;
|
|
}
|
|
|
|
async function handleAnalyze() {
|
|
const err = validate();
|
|
if (err) {
|
|
setError(err);
|
|
return;
|
|
}
|
|
|
|
setError("");
|
|
setCompletion("");
|
|
setIsLoading(true);
|
|
|
|
try {
|
|
await streamAiCompletion(
|
|
{
|
|
mode: "liuyao",
|
|
payload: {
|
|
question,
|
|
calcDate,
|
|
calcTime,
|
|
locationName: location!.name,
|
|
longitude: location!.longitude,
|
|
guaMark: guaData!.result.guaMark,
|
|
guaTitle: guaData!.result.guaTitle,
|
|
guaResult: guaData!.result.guaResult,
|
|
guaChange: guaData!.result.guaChange,
|
|
},
|
|
},
|
|
setCompletion,
|
|
);
|
|
} catch (e) {
|
|
setError(e instanceof Error ? e.message : String(e));
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
}
|
|
|
|
function handleReset() {
|
|
setQuestion("");
|
|
setProvinceCode("");
|
|
setCityCode("");
|
|
setCalcDate(nowDateString());
|
|
setCalcTime(nowTimeString());
|
|
setGuaData(null);
|
|
setCompletion("");
|
|
setError("");
|
|
setIsLoading(false);
|
|
}
|
|
|
|
function handleGuaResult(data: GuaResult) {
|
|
setGuaData(data);
|
|
setCompletion("");
|
|
setError("");
|
|
}
|
|
|
|
return (
|
|
<ModeWorkspace
|
|
aiPanel={
|
|
<ResultAI
|
|
panel
|
|
completion={completion}
|
|
isLoading={isLoading}
|
|
onCompletion={handleAnalyze}
|
|
error={error}
|
|
emptyHint="完成起卦后,点击「AI 解读」"
|
|
/>
|
|
}
|
|
>
|
|
<ZenCard title="问事 · 心意" subtitle="所求何事,心诚则灵">
|
|
<Textarea
|
|
placeholder="您想算点什么?"
|
|
value={question}
|
|
onChange={(e) => setQuestion(e.target.value)}
|
|
rows={3}
|
|
className="border-border/60 bg-background/50"
|
|
/>
|
|
<div className="flex flex-wrap gap-2">
|
|
{todayData.map((item, index) => (
|
|
<button
|
|
key={index}
|
|
type="button"
|
|
onClick={() => setQuestion(item)}
|
|
className="rounded-full border border-border/60 bg-secondary/50 px-3 py-1 text-xs text-muted-foreground transition hover:border-primary/30 hover:text-foreground"
|
|
>
|
|
{item}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</ZenCard>
|
|
|
|
<ZenCard title="起卦 · 时空" subtitle="地域、时辰与六爻">
|
|
<RegionSelect
|
|
label="起卦地域"
|
|
provinceCode={provinceCode}
|
|
cityCode={cityCode}
|
|
onProvinceChange={setProvinceCode}
|
|
onCityChange={setCityCode}
|
|
/>
|
|
<DateTimePicker
|
|
label="起卦时辰"
|
|
date={calcDate}
|
|
time={calcTime}
|
|
onDateChange={setCalcDate}
|
|
onTimeChange={setCalcTime}
|
|
/>
|
|
<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={formReady}
|
|
onResult={handleGuaResult}
|
|
onClear={() => setGuaData(null)}
|
|
/>
|
|
{guaData && (
|
|
<div className="rounded-xl border border-primary/20 bg-primary/5 p-4">
|
|
<Result {...guaData.result} />
|
|
</div>
|
|
)}
|
|
{error && !completion && (
|
|
<p className="text-sm text-destructive">{error}</p>
|
|
)}
|
|
<div className="flex gap-2 pt-1">
|
|
<Button variant="outline" onClick={handleReset} className="flex-1">
|
|
<ListRestart size={16} className="mr-1" />
|
|
重来
|
|
</Button>
|
|
<Button
|
|
onClick={handleAnalyze}
|
|
disabled={!guaData || isLoading}
|
|
className="flex-1"
|
|
>
|
|
<BrainCircuit size={16} className="mr-1" />
|
|
AI 解读
|
|
</Button>
|
|
</div>
|
|
</ZenCard>
|
|
</ModeWorkspace>
|
|
);
|
|
}
|