Files
zhimingge/components/shared/hexagram-input.tsx
T
dekun 1cde9ffc9c Compact left column layout and require manual cast start.
Stop stretching input cards to match AI panel height, and show a start button before online/offline hexagram casting begins.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-12 06:42:02 +08:00

238 lines
6.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { useCallback, useEffect, useState } from "react";
import { Play } from "lucide-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 = 800;
interface HexagramInputProps {
mode: "online" | "offline";
enabled: boolean;
active: boolean;
onStart: () => void;
onResult: (data: GuaResult) => void;
onClear: () => void;
}
export default function HexagramInput({
mode,
enabled,
active,
onStart,
onResult,
onClear,
}: HexagramInputProps) {
const [frontList, setFrontList] = useState([true, true, true]);
const [rotation, setRotation] = useState(false);
const [hexagramList, setHexagramList] = useState<GuaResult["list"]>([]);
const [offlineCounts, setOfflineCounts] = useState<(number | null)[]>(
Array(6).fill(null),
);
const complete = hexagramList.length === 6;
useEffect(() => {
setHexagramList([]);
setOfflineCounts(Array(6).fill(null));
setRotation(false);
}, [mode]);
useEffect(() => {
if (!active) {
setHexagramList([]);
setOfflineCounts(Array(6).fill(null));
setRotation(false);
}
}, [active]);
const finishList = useCallback(
(list: GuaResult["list"]) => {
const result = computeGuaResult(list);
if (result) {
onResult({ list, result });
}
},
[onResult],
);
const onTransitionEnd = useCallback(() => {
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;
});
}, [frontList, finishList]);
const startOnlineCast = useCallback(() => {
if (rotation || !enabled || !active || hexagramList.length >= 6) {
return;
}
setFrontList([bool(), bool(), bool()]);
setRotation(true);
}, [rotation, enabled, active, hexagramList.length]);
useEffect(() => {
if (mode !== "online" || !enabled || !active || rotation || complete) {
return;
}
const timer = setTimeout(startOnlineCast, AUTO_DELAY);
return () => clearTimeout(timer);
}, [
mode,
enabled,
active,
rotation,
complete,
hexagramList.length,
startOnlineCast,
]);
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([]);
setOfflineCounts(Array(6).fill(null));
setRotation(false);
onClear();
}
const offlineReady = offlineCounts.every((v) => v !== null);
if (!enabled) {
return (
<p className="text-center text-sm text-muted-foreground">
</p>
);
}
if (!active) {
return (
<div className="flex flex-col items-center gap-3 py-1">
<p className="text-center text-sm text-muted-foreground">
</p>
<Button type="button" onClick={onStart}>
<Play size={16} className="mr-1" />
</Button>
</div>
);
}
return (
<div className="flex w-full flex-col items-center gap-4">
{mode === "online" && (
<>
<Coin
onTransitionEnd={onTransitionEnd}
frontList={frontList}
rotation={rotation}
/>
<span className="text-lg font-medium">
🎲 {" "}
<span className="font-mono text-xl font-bold text-orange-500">
{complete ? "6/6" : `${hexagramList.length + (rotation ? 1 : 0)}/6`}
</span>{" "}
{rotation && (
<span className="ml-2 text-sm text-muted-foreground"></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 && (
<>
<p className="text-sm text-muted-foreground sm:max-w-[10rem]">
AI
</p>
<Button size="sm" variant="outline" onClick={handleReset}>
</Button>
</>
)}
</div>
)}
</div>
);
}