Files
deer-flow/frontend/src/components/workspace/artifacts/artifact-file-list.tsx
2026-02-02 09:49:44 +08:00

128 lines
3.5 KiB
TypeScript

import { DownloadIcon, LoaderIcon, PackageIcon } from "lucide-react";
import { useCallback, useState } from "react";
import { toast } from "sonner";
import { Button } from "@/components/ui/button";
import {
Card,
CardAction,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { urlOfArtifact } from "@/core/artifacts/utils";
import { useI18n } from "@/core/i18n/hooks";
import { installSkill } from "@/core/skills/api";
import {
getFileExtensionDisplayName,
getFileIcon,
getFileName,
} from "@/core/utils/files";
import { cn } from "@/lib/utils";
import { useArtifacts } from "./context";
export function ArtifactFileList({
className,
files,
threadId,
}: {
className?: string;
files: string[];
threadId: string;
}) {
const { t } = useI18n();
const { select: selectArtifact, setOpen } = useArtifacts();
const [installingFile, setInstallingFile] = useState<string | null>(null);
const handleClick = useCallback(
(filepath: string) => {
selectArtifact(filepath);
setOpen(true);
},
[selectArtifact, setOpen],
);
const handleInstallSkill = useCallback(
async (e: React.MouseEvent, filepath: string) => {
e.stopPropagation();
e.preventDefault();
if (installingFile) return;
setInstallingFile(filepath);
try {
const result = await installSkill({
thread_id: threadId,
path: filepath,
});
if (result.success) {
toast.success(result.message);
} else {
toast.error(result.message || "Failed to install skill");
}
} catch (error) {
console.error("Failed to install skill:", error);
toast.error("Failed to install skill");
} finally {
setInstallingFile(null);
}
},
[threadId, installingFile],
);
return (
<ul className={cn("flex w-full flex-col gap-4", className)}>
{files.map((file) => (
<Card
key={file}
className="cursor-pointer p-3"
onClick={() => handleClick(file)}
>
<CardHeader className="pr-2 pl-1">
<CardTitle className="relative pl-8">
<div>{getFileName(file)}</div>
<div className="absolute top-2 -left-0.5">
{getFileIcon(file)}
</div>
</CardTitle>
<CardDescription className="pl-8 text-xs">
{getFileExtensionDisplayName(file)} file
</CardDescription>
<CardAction>
{file.endsWith(".skill") && (
<Button
variant="ghost"
disabled={installingFile === file}
onClick={(e) => handleInstallSkill(e, file)}
>
{installingFile === file ? (
<LoaderIcon className="size-4 animate-spin" />
) : (
<PackageIcon className="size-4" />
)}
{t.common.install}
</Button>
)}
<a
href={urlOfArtifact({
filepath: file,
threadId: threadId,
download: true,
})}
target="_blank"
onClick={(e) => e.stopPropagation()}
>
<Button variant="ghost">
<DownloadIcon className="size-4" />
{t.common.download}
</Button>
</a>
</CardAction>
</CardHeader>
</Card>
))}
</ul>
);
}