first commit
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import clsx from "clsx";
|
||||
import Image from "next/image";
|
||||
|
||||
const rotationDuration = 3800;
|
||||
const bezier = "cubic-bezier(0.645,0.045,0.355,1)";
|
||||
|
||||
function Coin(props: {
|
||||
frontList: boolean[];
|
||||
rotation: boolean;
|
||||
onTransitionEnd: any;
|
||||
}) {
|
||||
const [lastFront, setLastFront] = useState(props.frontList);
|
||||
|
||||
useEffect(function () {
|
||||
if (!props.rotation) {
|
||||
return;
|
||||
}
|
||||
|
||||
let id = setTimeout(function () {
|
||||
setLastFront(props.frontList);
|
||||
props.onTransitionEnd();
|
||||
}, rotationDuration);
|
||||
return () => clearTimeout(id);
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="flex w-full max-w-md justify-around rounded-md border bg-secondary p-4 shadow dark:border-0 dark:shadow-none sm:p-6">
|
||||
{props.frontList.map((value, index) => (
|
||||
<CoinItem
|
||||
key={index}
|
||||
front={value}
|
||||
lastFront={lastFront[index]}
|
||||
rotation={props.rotation}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CoinItem(props: {
|
||||
front: boolean;
|
||||
lastFront: boolean;
|
||||
rotation: boolean;
|
||||
onTransitionEnd?: any;
|
||||
}) {
|
||||
let animate = "";
|
||||
if (props.rotation) {
|
||||
// animate-[coin-front-front_3.8s_cubic-bezier(0.645,0.045,0.355,1)]
|
||||
// animate-[coin-front-back_3.8s_cubic-bezier(0.645,0.045,0.355,1)]
|
||||
// animate-[coin-back-front_3.8s_cubic-bezier(0.645,0.045,0.355,1)]
|
||||
// animate-[coin-back-back_3.8s_cubic-bezier(0.645,0.045,0.355,1)]
|
||||
animate = `animate-[coin-${getFront(props.lastFront)}-${getFront(
|
||||
props.front,
|
||||
)}_${rotationDuration / 1000}s_${bezier}]`;
|
||||
}
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
transform: `rotateY(${props.front ? 0 : 180}deg)`,
|
||||
transformStyle: "preserve-3d",
|
||||
transformOrigin: "50% 50% -0.5px",
|
||||
}}
|
||||
className={clsx("h-16 w-16 sm:h-20 sm:w-20", animate)}
|
||||
>
|
||||
<Image
|
||||
width={0}
|
||||
height={0}
|
||||
sizes="100vw"
|
||||
draggable={false}
|
||||
className="absolute w-full"
|
||||
src="/img/head.webp"
|
||||
alt="coin"
|
||||
/>
|
||||
<Image
|
||||
width={0}
|
||||
height={0}
|
||||
sizes="100vw"
|
||||
draggable={false}
|
||||
className="absolute h-full w-full"
|
||||
style={{ transform: "translateZ(-1px)" }}
|
||||
src="/img/tail.webp"
|
||||
alt="coin"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function getFront(front: boolean): string {
|
||||
return front ? "front" : "back";
|
||||
}
|
||||
|
||||
export default Coin;
|
||||
@@ -0,0 +1,256 @@
|
||||
"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;
|
||||
@@ -0,0 +1,14 @@
|
||||
import React from "react";
|
||||
import { VERSION } from "@/lib/constant";
|
||||
|
||||
function Footer() {
|
||||
return (
|
||||
<footer className="mx-auto flex items-center gap-1 text-xs text-muted-foreground/80">
|
||||
<span className="italic">心诚则灵</span>
|
||||
<span className="text-muted-foreground/60">·</span>
|
||||
<span>知命阁 v{VERSION}</span>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
|
||||
export default Footer;
|
||||
@@ -0,0 +1,16 @@
|
||||
import { ChatGPT } from "@/components/svg";
|
||||
import { ModeToggle } from "@/components/mode-toggle";
|
||||
|
||||
export default function Header() {
|
||||
return (
|
||||
<header className="h-14 bg-secondary py-2 shadow">
|
||||
<div className="mx-auto flex h-full w-full items-center justify-center sm:max-w-md sm:justify-between md:max-w-2xl">
|
||||
<div className="flex gap-2">
|
||||
<ChatGPT />
|
||||
<span>知命阁</span>
|
||||
</div>
|
||||
<ModeToggle />
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import React from "react";
|
||||
import clsx from "clsx";
|
||||
|
||||
export interface HexagramObj {
|
||||
change: boolean | null;
|
||||
yang: boolean;
|
||||
separate: boolean;
|
||||
}
|
||||
|
||||
function Hexagram(props: { list: HexagramObj[] }) {
|
||||
return (
|
||||
<div className="flex h-52 w-56 shrink-0 flex-col-reverse gap-1.5 overflow-hidden rounded-md border bg-secondary py-3 shadow-inner dark:border-0 dark:shadow-none sm:h-60 sm:w-72">
|
||||
{props.list.map((value, index) => {
|
||||
return (
|
||||
<div key={index} className="flex flex-col-reverse gap-1.5">
|
||||
{value.separate && <div className="h-0.5 sm:h-1" />}
|
||||
<Line change={value.change} yang={value.yang} />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Line(props: { change: boolean | null; yang: boolean }) {
|
||||
let changeYang = props.change && props.yang;
|
||||
const color = props.change ? "bg-red-400" : "bg-stone-400";
|
||||
return (
|
||||
<div className="flex h-[24px] w-full animate-[transform-x_0.3s_ease-out] items-center justify-center sm:h-[29px]">
|
||||
{props.yang ? (
|
||||
<div className={clsx("h-full w-4/5 sm:w-[83%]", color)}></div>
|
||||
) : (
|
||||
<div className="flex h-full w-4/5 justify-between sm:w-[83%]">
|
||||
<div className={clsx("h-full w-[46%]", color)}></div>
|
||||
<div className={clsx("h-full w-[46%]", color)}></div>
|
||||
</div>
|
||||
)}
|
||||
{props.change ? <Change changeYang={changeYang} /> : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Change(props: { changeYang: boolean | null }) {
|
||||
return (
|
||||
<div className="h-0 w-0">
|
||||
<div className="relative -right-1 -top-3">
|
||||
{props.changeYang ? (
|
||||
<span className="text-sm text-muted-foreground">○</span>
|
||||
) : (
|
||||
<span className="relative -right-0.5 text-sm text-muted-foreground">
|
||||
✕
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Hexagram;
|
||||
@@ -0,0 +1,40 @@
|
||||
"use client";
|
||||
import * as React from "react";
|
||||
import { useTheme } from "next-themes";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { Laptop2, MoonStar, Sun } from "lucide-react";
|
||||
|
||||
export function ModeToggle() {
|
||||
const { setTheme } = useTheme();
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="sm" className="hidden sm:block">
|
||||
<Sun size={22} strokeWidth={1.5} className="dark:hidden" />
|
||||
<MoonStar size={22} strokeWidth={1.5} className="hidden dark:block" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start">
|
||||
<DropdownMenuItem onClick={() => setTheme("light")}>
|
||||
<Sun size={20} strokeWidth={1.5} className="mr-2" />
|
||||
Light
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setTheme("dark")}>
|
||||
<MoonStar size={20} strokeWidth={1.5} className="mr-2" />
|
||||
Dark
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setTheme("system")}>
|
||||
<Laptop2 size={20} strokeWidth={1.5} className="mr-2" /> System
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import React, { createRef } from "react";
|
||||
import clsx from "clsx";
|
||||
import todayJson from "@/lib/data/today.json";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import Image from "next/image";
|
||||
|
||||
const todayData: string[] = todayJson;
|
||||
|
||||
function Question(props: { question: string; setQuestion: any }) {
|
||||
const inputRef = createRef<HTMLTextAreaElement>();
|
||||
|
||||
function startClick() {
|
||||
const value = inputRef.current?.value;
|
||||
if (value === "") {
|
||||
return;
|
||||
}
|
||||
props.setQuestion(value);
|
||||
}
|
||||
|
||||
function todayClick(index: number) {
|
||||
props.setQuestion(todayData[index]);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
"ignore-animate flex w-full max-w-md flex-col gap-4",
|
||||
props.question || "pt-6",
|
||||
)}
|
||||
>
|
||||
{props.question === "" ? (
|
||||
<>
|
||||
<label>您想算点什么?</label>
|
||||
<Textarea
|
||||
ref={inputRef}
|
||||
placeholder="将使用 AI 为您解读"
|
||||
className="resize-none"
|
||||
rows={4}
|
||||
/>
|
||||
<div className="flex flex-row-reverse">
|
||||
<Button size="sm" onClick={startClick}>
|
||||
开始
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<label className="mt-16 underline underline-offset-4">
|
||||
🧐 让我猜猜您算什么东西?
|
||||
</label>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
{todayData.map(function (value, index) {
|
||||
return (
|
||||
<span
|
||||
key={index}
|
||||
onClick={() => {
|
||||
todayClick(index);
|
||||
}}
|
||||
className="rounded-md border bg-secondary p-2 text-sm text-muted-foreground shadow transition hover:scale-[1.03] dark:border-0 dark:text-foreground/80 dark:shadow-none"
|
||||
>
|
||||
{value}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
|
||||
{props.question && (
|
||||
<div className="flex truncate rounded-md border bg-secondary p-2 shadow dark:border-0 dark:shadow-none">
|
||||
<Image
|
||||
width={24}
|
||||
height={24}
|
||||
className="mr-2"
|
||||
src="/img/yin-yang.webp"
|
||||
alt="yinyang"
|
||||
/>
|
||||
{props.question}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Question;
|
||||
@@ -0,0 +1,91 @@
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { RotateCw } from "lucide-react";
|
||||
import Markdown from "react-markdown";
|
||||
|
||||
function ResultAI({
|
||||
completion,
|
||||
isLoading,
|
||||
onCompletion,
|
||||
error,
|
||||
}: {
|
||||
completion: string;
|
||||
isLoading: boolean;
|
||||
onCompletion: () => void;
|
||||
error: string;
|
||||
}) {
|
||||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
const [autoScroll, setAutoScroll] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setAutoScroll(isLoading);
|
||||
}, [isLoading]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!autoScroll) {
|
||||
return;
|
||||
}
|
||||
scrollTo();
|
||||
});
|
||||
|
||||
function scrollTo() {
|
||||
requestAnimationFrame(() => {
|
||||
if (
|
||||
!scrollRef.current ||
|
||||
scrollRef.current.scrollHeight ===
|
||||
scrollRef.current.clientHeight + scrollRef.current.scrollTop
|
||||
) {
|
||||
return;
|
||||
}
|
||||
scrollRef.current.scrollTo(0, scrollRef.current.scrollHeight);
|
||||
});
|
||||
}
|
||||
|
||||
function onScroll(e: HTMLElement) {
|
||||
if (!isLoading) {
|
||||
return;
|
||||
}
|
||||
const hitBottom = e.scrollTop + e.clientHeight >= e.scrollHeight - 15;
|
||||
if (hitBottom === autoScroll) {
|
||||
return;
|
||||
}
|
||||
setAutoScroll(hitBottom);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-0 w-full flex-1 sm:max-w-md md:max-w-2xl">
|
||||
{isLoading && (
|
||||
<div className="flex h-0">
|
||||
<span className="flex-1" />
|
||||
<div className="relative -top-4 flex w-fit items-center pr-1 text-muted-foreground sm:left-2 sm:pr-3">
|
||||
<RotateCw size={16} className="animate-spin" />
|
||||
<span className="ml-1 text-sm">AI 分析中...</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
ref={scrollRef}
|
||||
onScroll={(e) => onScroll(e.currentTarget)}
|
||||
className="max-h-full overflow-auto rounded-md border p-3 shadow dark:border-0 dark:bg-secondary/90 dark:shadow-none sm:p-5"
|
||||
>
|
||||
{error ? (
|
||||
<div className="text-destructive">
|
||||
ಠ_ಠ 请求出错了!
|
||||
<br />
|
||||
{error}
|
||||
</div>
|
||||
) : (
|
||||
<Markdown className="prose dark:prose-invert">{completion}</Markdown>
|
||||
)}
|
||||
{!isLoading && (
|
||||
<Button onClick={onCompletion} size="sm" className="mt-2">
|
||||
<RotateCw size={18} className="mr-1" />
|
||||
重新生成
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ResultAI;
|
||||
@@ -0,0 +1,28 @@
|
||||
import React from "react";
|
||||
|
||||
export interface ResultObj {
|
||||
guaTitle: string;
|
||||
guaMark: string;
|
||||
guaResult: string;
|
||||
guaChange: string;
|
||||
}
|
||||
|
||||
function Result(props: ResultObj) {
|
||||
return (
|
||||
<div className="flex flex-col items-start justify-center gap-2 sm:gap-3">
|
||||
{props.guaTitle}
|
||||
<a
|
||||
className="group flex items-center gap-1 font-medium text-primary/80 underline underline-offset-4 transition-colors hover:text-primary/100"
|
||||
href={`/learn/${props.guaMark}`}
|
||||
>
|
||||
<div className="mt-1 h-[90%] w-1.5 bg-blue-400/80 transition-colors group-hover:bg-blue-400/100" />
|
||||
<span>{props.guaResult}</span>
|
||||
</a>
|
||||
<span className="text-sm italic text-muted-foreground">
|
||||
{props.guaChange}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Result;
|
||||
@@ -0,0 +1,33 @@
|
||||
import React from "react";
|
||||
|
||||
export const ChatGPT = React.memo(function ChatGPT({
|
||||
className = "w-6 h-6",
|
||||
strokeWidth = 3,
|
||||
}: {
|
||||
className?: string;
|
||||
strokeWidth?: number;
|
||||
}) {
|
||||
return (
|
||||
<svg
|
||||
width="1em"
|
||||
height="1em"
|
||||
viewBox="0 0 48 48"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
>
|
||||
<g
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth={strokeWidth}
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M18.38 27.94v-14.4l11.19-6.46c6.2-3.58 17.3 5.25 12.64 13.33"></path>
|
||||
<path d="m18.38 20.94l12.47-7.2l11.19 6.46c6.2 3.58 4.1 17.61-5.23 17.61"></path>
|
||||
<path d="m24.44 17.44l12.47 7.2v12.93c0 7.16-13.2 12.36-17.86 4.28"></path>
|
||||
<path d="M30.5 21.2v14.14L19.31 41.8c-6.2 3.58-17.3-5.25-12.64-13.33"></path>
|
||||
<path d="m30.5 27.94l-12.47 7.2l-11.19-6.46c-6.21-3.59-4.11-17.61 5.22-17.61"></path>
|
||||
<path d="m24.44 31.44l-12.47-7.2V11.31c0-7.16 13.2-12.36 17.86-4.28"></path>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,56 @@
|
||||
import * as React from "react";
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||
destructive:
|
||||
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
||||
outline:
|
||||
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: "h-10 px-4 py-2",
|
||||
sm: "h-9 rounded-md px-3",
|
||||
lg: "h-11 rounded-md px-8",
|
||||
icon: "h-10 w-10",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
export interface ButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {
|
||||
asChild?: boolean;
|
||||
}
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "button";
|
||||
return (
|
||||
<Comp
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
Button.displayName = "Button";
|
||||
|
||||
export { Button, buttonVariants };
|
||||
@@ -0,0 +1,201 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
|
||||
import { Check, ChevronRight, Circle } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const DropdownMenu = DropdownMenuPrimitive.Root;
|
||||
|
||||
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
|
||||
|
||||
const DropdownMenuGroup = DropdownMenuPrimitive.Group;
|
||||
|
||||
const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
|
||||
|
||||
const DropdownMenuSub = DropdownMenuPrimitive.Sub;
|
||||
|
||||
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
|
||||
|
||||
const DropdownMenuSubTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
|
||||
inset?: boolean;
|
||||
}
|
||||
>(({ className, inset, children, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.SubTrigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent",
|
||||
inset && "pl-8",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronRight className="ml-auto h-4 w-4" />
|
||||
</DropdownMenuPrimitive.SubTrigger>
|
||||
));
|
||||
DropdownMenuSubTrigger.displayName =
|
||||
DropdownMenuPrimitive.SubTrigger.displayName;
|
||||
|
||||
const DropdownMenuSubContent = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.SubContent
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DropdownMenuSubContent.displayName =
|
||||
DropdownMenuPrimitive.SubContent.displayName;
|
||||
|
||||
const DropdownMenuContent = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
|
||||
>(({ className, sideOffset = 4, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Portal>
|
||||
<DropdownMenuPrimitive.Content
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
onCloseAutoFocus={(e) => e.preventDefault()}
|
||||
className={cn(
|
||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</DropdownMenuPrimitive.Portal>
|
||||
));
|
||||
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
|
||||
|
||||
const DropdownMenuItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
|
||||
inset?: boolean;
|
||||
}
|
||||
>(({ className, inset, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
inset && "pl-8",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
|
||||
|
||||
const DropdownMenuCheckboxItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
|
||||
>(({ className, children, checked, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.CheckboxItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className,
|
||||
)}
|
||||
checked={checked}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<Check className="h-4 w-4" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.CheckboxItem>
|
||||
));
|
||||
DropdownMenuCheckboxItem.displayName =
|
||||
DropdownMenuPrimitive.CheckboxItem.displayName;
|
||||
|
||||
const DropdownMenuRadioItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.RadioItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<Circle className="h-2 w-2 fill-current" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.RadioItem>
|
||||
));
|
||||
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
|
||||
|
||||
const DropdownMenuLabel = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
|
||||
inset?: boolean;
|
||||
}
|
||||
>(({ className, inset, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Label
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"px-2 py-1.5 text-sm font-semibold",
|
||||
inset && "pl-8",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
|
||||
|
||||
const DropdownMenuSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
|
||||
|
||||
const DropdownMenuShortcut = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||
return (
|
||||
<span
|
||||
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
DropdownMenuShortcut.displayName = "DropdownMenuShortcut";
|
||||
|
||||
export {
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuPortal,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuRadioGroup,
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export interface TextareaProps
|
||||
extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
|
||||
|
||||
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
|
||||
({ className, ...props }, ref) => {
|
||||
return (
|
||||
<textarea
|
||||
className={cn(
|
||||
"flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
Textarea.displayName = "Textarea";
|
||||
|
||||
export { Textarea };
|
||||
@@ -0,0 +1,21 @@
|
||||
import React from "react";
|
||||
import Script from "next/script";
|
||||
import { unstable_noStore as noStore } from "next/cache";
|
||||
|
||||
function Umami() {
|
||||
noStore();
|
||||
if (!process.env.UMAMI_ID) {
|
||||
return;
|
||||
}
|
||||
return (
|
||||
<Script
|
||||
id="umami"
|
||||
strategy="afterInteractive"
|
||||
src={process.env.UMAMI_URL}
|
||||
data-website-id={process.env.UMAMI_ID}
|
||||
data-domains={process.env.UMAMI_DOMAINS}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default Umami;
|
||||
Reference in New Issue
Block a user