Implement three divination modes, learn pages, and PM2 deploy on port 3130.

Add liuyao/bazi/combined flows with shared calc and AI infrastructure, 64-gua learn routes, and update Ubuntu PM2 deployment docs for port 3130.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-10 20:19:49 +08:00
parent d141623070
commit fff77dac3f
41 changed files with 2590 additions and 385 deletions
+202
View File
@@ -0,0 +1,202 @@
"use client";
import { useEffect, useState } from "react";
import { bool } from "aimless.js";
import Coin from "@/components/coin";
import Hexagram from "@/components/hexagram";
import { Button } from "@/components/ui/button";
import {
buildHexagramList,
COIN_OPTIONS,
computeGuaResult,
type GuaResult,
YAO_LABELS,
} from "@/lib/calc/hexagram";
const AUTO_DELAY = 600;
interface HexagramInputProps {
mode: "online" | "offline";
enabled: boolean;
onResult: (data: GuaResult) => void;
onClear: () => void;
}
export default function HexagramInput({
mode,
enabled,
onResult,
onClear,
}: HexagramInputProps) {
const [frontList, setFrontList] = useState([true, true, true]);
const [rotation, setRotation] = useState(false);
const [hexagramList, setHexagramList] = useState<
GuaResult["list"]
>([]);
const [count, setCount] = useState(0);
const [offlineCounts, setOfflineCounts] = useState<(number | null)[]>(
Array(6).fill(null),
);
useEffect(() => {
setHexagramList([]);
setCount(0);
setOfflineCounts(Array(6).fill(null));
setRotation(false);
}, [mode]);
useEffect(() => {
if (
mode !== "online" ||
!enabled ||
rotation ||
hexagramList.length >= 6 ||
count >= 6
) {
return;
}
const timer = setTimeout(startOnlineCast, AUTO_DELAY);
return () => clearTimeout(timer);
}, [mode, enabled, rotation, count, hexagramList.length]);
function finishList(list: GuaResult["list"]) {
const result = computeGuaResult(list);
if (result) {
onResult({ list, result });
}
}
function onTransitionEnd() {
setRotation(false);
const frontCount = frontList.reduce(
(acc, val) => (val ? acc + 1 : acc),
0,
);
setHexagramList((list) => {
const newList = [
...list,
{
change: frontCount === 0 || frontCount === 3,
yang: frontCount >= 2,
separate: list.length === 3,
},
];
if (newList.length === 6) {
finishList(newList);
}
return newList;
});
}
function startOnlineCast() {
if (rotation || !enabled || hexagramList.length >= 6) {
return;
}
setFrontList([bool(), bool(), bool()]);
setRotation(true);
setCount((c) => c + 1);
}
function handleOfflineSelect(index: number, frontCount: number) {
const next = [...offlineCounts];
next[index] = frontCount;
setOfflineCounts(next);
}
function confirmOffline() {
if (offlineCounts.some((v) => v === null)) {
return;
}
const list = buildHexagramList(offlineCounts as number[]);
finishList(list);
setHexagramList(list);
}
function handleReset() {
setHexagramList([]);
setCount(0);
setOfflineCounts(Array(6).fill(null));
setRotation(false);
onClear();
}
const complete = hexagramList.length === 6;
const offlineReady = offlineCounts.every((v) => v !== null);
if (!enabled) {
return (
<p className="text-center text-sm text-muted-foreground">
</p>
);
}
return (
<div className="flex w-full flex-col items-center gap-4">
{mode === "online" && !complete && (
<>
<Coin
onTransitionEnd={onTransitionEnd}
frontList={frontList}
rotation={rotation}
/>
<span className="text-lg font-medium">
🎲 {" "}
<span className="font-mono text-xl font-bold text-orange-500">
{count === 0 ? "-/-" : `${count}/6`}
</span>{" "}
</span>
</>
)}
{mode === "offline" && !complete && (
<div className="w-full max-w-md space-y-2">
<p className="text-center text-xs text-muted-foreground">
</p>
{YAO_LABELS.map((label, index) => (
<div
key={label}
className="flex flex-wrap items-center gap-2 rounded-md border p-2"
>
<span className="w-10 shrink-0 text-sm font-medium">{label}</span>
{COIN_OPTIONS.map((opt) => (
<button
key={opt.count}
type="button"
onClick={() => handleOfflineSelect(index, opt.count)}
className={`rounded border px-2 py-1 text-xs transition ${
offlineCounts[index] === opt.count
? "border-primary bg-primary/10 text-primary"
: "hover:bg-accent"
}`}
>
{opt.label}
</button>
))}
</div>
))}
<Button
className="w-full"
disabled={!offlineReady}
onClick={confirmOffline}
>
</Button>
</div>
)}
{hexagramList.length > 0 && (
<div className="flex flex-col items-center gap-3 sm:flex-row">
<Hexagram list={hexagramList} />
{complete && (
<Button size="sm" variant="outline" onClick={handleReset}>
</Button>
)}
</div>
)}
</div>
);
}