Files
zhimingge/components/divination.tsx
T
2026-06-10 19:59:27 +08:00

257 lines
7.2 KiB
TypeScript

"use client";
import React, { useEffect, useRef, useState } from "react";
import Coin from "@/components/coin";
import Hexagram, { HexagramObj } from "@/components/hexagram";
import { bool } from "aimless.js";
import Result, { ResultObj } from "@/components/result";
import Question from "@/components/question";
import ResultAI from "@/components/result-ai";
import { animateChildren } from "@/lib/animate";
import guaIndexData from "@/lib/data/gua-index.json";
import guaListData from "@/lib/data/gua-list.json";
import { getAnswer } from "@/app/server";
import { readStreamableValue } from "ai/rsc";
import { Button } from "./ui/button";
import { BrainCircuit, ListRestart } from "lucide-react";
import { ERROR_PREFIX } from "@/lib/constant";
const AUTO_DELAY = 600;
function Divination() {
const [error, setError] = useState<string>("");
const [isLoading, setIsLoading] = useState(false);
const [completion, setCompletion] = useState<string>("");
async function onCompletion() {
setError("");
setCompletion("");
setIsLoading(true);
try {
const { data, error } = await getAnswer(
question,
resultObj!.guaMark,
resultObj!.guaTitle,
resultObj!.guaResult,
resultObj!.guaChange,
);
if (error) {
setError(error);
return;
}
if (data) {
let ret = "";
for await (const delta of readStreamableValue(data)) {
if (delta.startsWith(ERROR_PREFIX)) {
setError(delta.slice(ERROR_PREFIX.length));
return;
}
ret += delta;
setCompletion(ret);
}
}
} catch (err: any) {
setError(err.message ?? err);
} finally {
setIsLoading(false);
}
}
const [frontList, setFrontList] = useState([true, true, true]);
const [rotation, setRotation] = useState(false);
const [hexagramList, setHexagramList] = useState<HexagramObj[]>([]);
const [resultObj, setResultObj] = useState<ResultObj | null>(null);
const [question, setQuestion] = useState("");
const [resultAi, setResultAi] = useState(false);
const flexRef = useRef<HTMLDivElement>(null);
const [count, setCount] = useState(0);
// 自动卜筮
useEffect(() => {
if (rotation || resultObj || count >= 6 || !question) {
return;
}
setTimeout(startClick, AUTO_DELAY);
}, [question, rotation]);
useEffect(() => {
if (!flexRef.current) {
return;
}
const observer = animateChildren(flexRef.current);
return () => observer.disconnect();
}, []);
function onTransitionEnd() {
setRotation(false);
let frontCount = frontList.reduce((acc, val) => (val ? acc + 1 : acc), 0);
setHexagramList((list) => {
const newList = [
...list,
{
change: frontCount == 0 || frontCount == 3 || null,
yang: frontCount >= 2,
separate: list.length == 3,
},
];
setResult(newList);
return newList;
});
}
function startClick() {
if (rotation) {
return;
}
if (hexagramList.length >= 6) {
setHexagramList([]);
}
setFrontList([bool(), bool(), bool()]);
setRotation(true);
setCount(count + 1);
}
async function testClick() {
for (let i = 0; i < 6; i++) {
onTransitionEnd();
}
}
function restartClick() {
setResultObj(null);
setHexagramList([]);
setQuestion("");
setResultAi(false);
setCount(0);
stop();
}
function aiClick() {
setResultAi(true);
onCompletion();
}
function setResult(list: HexagramObj[]) {
if (list.length != 6) {
return;
}
const guaDict1 = ["坤", "震", "坎", "兑", "艮", "离", "巽", "乾"];
const guaDict2 = ["地", "雷", "水", "泽", "山", "火", "风", "天"];
const changeYang = ["初九", "九二", "九三", "九四", "九五", "上九"];
const changeYin = ["初六", "六二", "六三", "六四", "六五", "上六"];
const changeList: String[] = [];
list.forEach((value, index) => {
if (!value.change) {
return;
}
changeList.push(value.yang ? changeYang[index] : changeYin[index]);
});
// 卦的结果: 第X卦 X卦 XX卦 X上X下
// 计算卦的索引,111对应乾卦,000对应坤卦,索引转为10进制。
const upIndex =
(list[5].yang ? 4 : 0) + (list[4].yang ? 2 : 0) + (list[3].yang ? 1 : 0);
const downIndex =
(list[2].yang ? 4 : 0) + (list[1].yang ? 2 : 0) + (list[0].yang ? 1 : 0);
const guaIndex = guaIndexData[upIndex][downIndex] - 1;
const guaName1 = guaListData[guaIndex];
let guaName2;
if (upIndex === downIndex) {
// 上下卦相同,格式为X为X
guaName2 = guaDict1[upIndex] + "为" + guaDict2[upIndex];
} else {
guaName2 = guaDict2[upIndex] + guaDict2[downIndex] + guaName1;
}
const guaDesc = guaDict1[upIndex] + "上" + guaDict1[downIndex] + "下";
setResultObj({
// 例:26.山天大畜
guaMark: `${(guaIndex + 1).toString().padStart(2, "0")}.${guaName2}`,
guaTitle: `周易第${guaIndex + 1}`,
// 例:大畜卦(山天大畜)_艮上乾下
guaResult: `${guaName1}卦(${guaName2})_${guaDesc}`,
guaChange:
changeList.length === 0 ? "无变爻" : `变爻: ${changeList.toString()}`,
});
}
const showResult = resultObj !== null;
const inputQuestion = question === "";
return (
<main
ref={flexRef}
className="gap mx-auto flex h-0 w-[90%] flex-1 flex-col flex-nowrap items-center"
>
<Question question={question} setQuestion={setQuestion} />
{!resultAi && !inputQuestion && (
<Coin
onTransitionEnd={onTransitionEnd}
frontList={frontList}
rotation={rotation}
/>
)}
{!inputQuestion && !showResult && (
<div className="relative">
<span className="pl-2 text-lg font-medium">
🎲 {" "}
<span className="font-mono text-xl font-bold text-orange-500">
{count === 0 ? "-/-" : `${count}/6`}
</span>{" "}
</span>
</div>
)}
{!inputQuestion && hexagramList.length != 0 && (
<div className="flex max-w-md gap-2">
<Hexagram list={hexagramList} />
{showResult && (
<div className="flex flex-col justify-around">
<Result {...resultObj} />
<div className="flex flex-col gap-2 sm:px-6">
<Button
size="sm"
variant="destructive"
onClick={restartClick}
disabled={rotation}
>
<ListRestart size={18} className="mr-1" />
</Button>
{resultAi ? null : (
<Button size="sm" onClick={aiClick} disabled={rotation}>
<BrainCircuit size={16} className="mr-1" />
AI
</Button>
)}
</div>
</div>
)}
</div>
)}
{resultAi && (
<ResultAI
completion={completion}
isLoading={isLoading}
onCompletion={onCompletion}
error={error}
/>
)}
</main>
);
}
export default Divination;