Files
zhimingge/components/modes/bazi-form.tsx
T
dekun 0f3bc2c50a Fix AI stream by returning stream.value directly from Server Actions.
createStreamableValue must be created and returned in the action itself; wrapping in { data } or a helper return caused production RSC serialization errors.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-10 22:24:57 +08:00

183 lines
5.2 KiB
TypeScript

"use client";
import { useState } from "react";
import { readStreamableValue } from "ai/rsc";
import { BrainCircuit } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Textarea } from "@/components/ui/textarea";
import ResultAI from "@/components/result-ai";
import BaziChartDisplay from "@/components/modes/bazi-chart";
import DateTimePicker, { nowDateString } from "@/components/shared/datetime-picker";
import RegionSelect, {
useRegionLocation,
} from "@/components/shared/region-select";
import { calculateBazi, type BaziChart } from "@/lib/calc/bazi";
import { getBaziAnswer } from "@/app/actions/bazi";
import { ERROR_PREFIX } from "@/lib/constant";
export default function BaziForm() {
const [date, setDate] = useState(nowDateString());
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 [showAi, setShowAi] = 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));
setShowAi(false);
setCompletion("");
}
async function handleAnalyze() {
const input = buildInput();
if (!input || !chart) {
return;
}
setError("");
setCompletion("");
setIsLoading(true);
setShowAi(true);
try {
const stream = await getBaziAnswer(
input,
question,
location!.name,
);
let ret = "";
for await (const delta of readStreamableValue(stream)) {
if (typeof delta === "string" && delta.startsWith(ERROR_PREFIX)) {
setError(delta.slice(ERROR_PREFIX.length));
return;
}
ret += delta ?? "";
setCompletion(ret);
}
} catch (err) {
setError(err instanceof Error ? err.message : String(err));
} finally {
setIsLoading(false);
}
}
return (
<div className="flex flex-1 flex-col gap-4 px-4 py-6">
<div className="mx-auto w-full max-w-lg space-y-4">
<DateTimePicker
label="出生日期 / 时间"
date={date}
time={time}
timeDisabled={unknownHour}
onDateChange={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="space-y-1">
<label className="text-sm font-medium"></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>
</div>
<RegionSelect
label="出生地域"
provinceCode={provinceCode}
cityCode={cityCode}
onProvinceChange={setProvinceCode}
onCityChange={setCityCode}
/>
<div className="space-y-1">
<label className="text-sm font-medium"></label>
<Textarea
placeholder="事业、婚姻、健康等..."
value={question}
onChange={(e) => setQuestion(e.target.value)}
rows={3}
/>
</div>
{error && !showAi && (
<p className="text-sm text-destructive">{error}</p>
)}
<Button onClick={handleCalculate} className="w-full">
</Button>
</div>
{chart && (
<div className="mx-auto w-full max-w-lg space-y-4">
<BaziChartDisplay chart={chart} />
{!showAi && (
<Button onClick={handleAnalyze} className="w-full">
<BrainCircuit size={16} className="mr-1" />
</Button>
)}
</div>
)}
{showAi && (
<div className="mx-auto h-96 w-full max-w-lg flex-1">
<ResultAI
completion={completion}
isLoading={isLoading}
onCompletion={handleAnalyze}
error={error}
/>
</div>
)}
</div>
);
}