Files
zhimingge/components/result-ai.tsx
T

140 lines
4.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.
import React, { useEffect, useRef, useState } from "react";
import { Download, RotateCw } from "lucide-react";
import Markdown from "react-markdown";
import { Button } from "@/components/ui/button";
import { TaijiIcon } from "@/components/svg/taiji";
import { downloadMarkdown } from "@/lib/history/storage";
import { cn } from "@/lib/utils";
function ResultAI({
completion,
isLoading,
onCompletion,
error,
warning,
panel = false,
emptyHint = "完成左侧填写并起卦/排盘后,点击「AI 解读」",
downloadFilename,
downloadPreamble,
}: {
completion: string;
isLoading: boolean;
onCompletion: () => void;
error: string;
warning?: string;
panel?: boolean;
emptyHint?: string;
downloadFilename?: string;
downloadPreamble?: string;
}) {
const scrollRef = useRef<HTMLDivElement>(null);
const [autoScroll, setAutoScroll] = useState(false);
useEffect(() => {
setAutoScroll(isLoading);
}, [isLoading]);
useEffect(() => {
if (!autoScroll) {
return;
}
scrollRef.current?.scrollTo(0, scrollRef.current.scrollHeight);
}, [completion, autoScroll]);
function onScroll(e: HTMLElement) {
if (!isLoading) {
return;
}
const hitBottom = e.scrollTop + e.clientHeight >= e.scrollHeight - 15;
if (hitBottom !== autoScroll) {
setAutoScroll(hitBottom);
}
}
function handleDownload() {
if (!completion || !downloadFilename) {
return;
}
const body = downloadPreamble
? `${downloadPreamble}\n\n---\n\n${completion}`
: completion;
downloadMarkdown(body, downloadFilename);
}
return (
<div
className={cn(
"flex w-full flex-col",
panel ? "min-h-0 flex-1" : "gap-2",
)}
>
{isLoading && !panel && (
<div className="flex items-center text-sm text-muted-foreground">
<RotateCw size={16} className="animate-spin" />
<span className="ml-1">AI ...</span>
</div>
)}
<div
ref={scrollRef}
onScroll={(e) => onScroll(e.currentTarget)}
className={cn(
"overflow-y-auto",
panel
? "min-h-0 flex-1"
: "min-h-[240px] max-h-[420px] rounded-md border bg-background p-3 shadow sm:p-5 dark:border-0 dark:bg-secondary/90",
)}
>
{isLoading && panel && (
<div className="mb-3 flex items-center text-sm text-muted-foreground">
<RotateCw size={16} className="animate-spin" />
<span className="ml-1">AI ...</span>
</div>
)}
{error ? (
<div className="text-sm text-destructive">
<p className="font-medium"></p>
<p className="mt-1 whitespace-pre-wrap">{error}</p>
</div>
) : completion ? (
<>
{warning && (
<div className="mb-3 rounded-md border border-amber-500/40 bg-amber-500/10 px-3 py-2 text-sm text-amber-700 dark:text-amber-300">
<p className="font-medium"></p>
<p className="mt-1 whitespace-pre-wrap">{warning}</p>
</div>
)}
<Markdown className="prose max-w-none text-sm dark:prose-invert prose-p:leading-relaxed">
{completion}
</Markdown>
</>
) : isLoading ? (
<p className="text-sm text-muted-foreground"> AI ...</p>
) : (
<div className="flex h-full min-h-[200px] flex-col items-center justify-center text-center text-muted-foreground">
<TaijiIcon className="h-16 w-16 opacity-25" />
<p className="mt-3 max-w-[220px] text-sm leading-relaxed">
{emptyHint}
</p>
</div>
)}
{!isLoading && (completion || error) && (
<div className="mt-4 flex flex-wrap gap-2">
{completion && downloadFilename && (
<Button size="sm" variant="outline" onClick={handleDownload}>
<Download size={16} className="mr-1" />
Markdown
</Button>
)}
<Button onClick={onCompletion} size="sm" variant="outline">
<RotateCw size={16} className="mr-1" />
</Button>
</div>
)}
</div>
</div>
);
}
export default ResultAI;