Files
deer-flow/frontend/src/components/workspace/artifacts/artifact-file-detail.tsx

151 lines
4.6 KiB
TypeScript
Raw Normal View History

2026-01-17 15:19:53 +08:00
import {
CopyIcon,
DownloadIcon,
SquareArrowOutUpRightIcon,
XIcon,
} from "lucide-react";
2026-01-17 15:09:44 +08:00
import { useMemo } from "react";
import { toast } from "sonner";
2026-01-17 00:05:19 +08:00
2026-01-17 15:09:44 +08:00
import {
Artifact,
ArtifactAction,
ArtifactActions,
ArtifactContent,
ArtifactHeader,
ArtifactTitle,
} from "@/components/ai-elements/artifact";
import { Select, SelectItem } from "@/components/ui/select";
import {
SelectContent,
SelectGroup,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
2026-01-17 15:09:44 +08:00
import { useArtifactContent } from "@/core/artifacts/hooks";
import { urlOfArtifact } from "@/core/artifacts/utils";
import { checkCodeFile, getFileName } from "@/core/utils/files";
2026-01-17 11:02:33 +08:00
import { cn } from "@/lib/utils";
2026-01-17 00:05:19 +08:00
2026-01-17 15:09:44 +08:00
import { useArtifacts } from "./context";
import { FileViewer } from "./file-viewer";
2026-01-17 11:02:33 +08:00
export function ArtifactFileDetail({
className,
2026-01-18 17:13:15 +08:00
filepath: filepathFromProps,
2026-01-17 15:09:44 +08:00
threadId,
2026-01-17 11:02:33 +08:00
}: {
className?: string;
filepath: string;
2026-01-17 15:09:44 +08:00
threadId: string;
2026-01-17 11:02:33 +08:00
}) {
const { artifacts, setOpen, select } = useArtifacts();
2026-01-18 17:13:15 +08:00
const isWriteFile = useMemo(() => {
return filepathFromProps.startsWith("write-file:");
}, [filepathFromProps]);
const filepath = useMemo(() => {
if (isWriteFile) {
const url = new URL(filepathFromProps);
return url.pathname;
}
return filepathFromProps;
}, [filepathFromProps, isWriteFile]);
const { isCodeFile } = useMemo(() => {
if (isWriteFile) {
let language = checkCodeFile(filepath).language;
language ??= "markdown";
return { isCodeFile: true, language };
}
return checkCodeFile(filepath);
}, [filepath, isWriteFile]);
2026-01-17 15:09:44 +08:00
const { content } = useArtifactContent({
threadId,
2026-01-18 17:13:15 +08:00
filepath: filepathFromProps,
enabled: isCodeFile && !isWriteFile,
2026-01-17 15:09:44 +08:00
});
2026-01-17 00:02:03 +08:00
return (
2026-01-17 15:09:44 +08:00
<Artifact className={cn("rounded-none", className)}>
<ArtifactHeader className="px-2">
2026-01-17 00:02:03 +08:00
<div>
<ArtifactTitle>
2026-01-18 17:13:15 +08:00
{isWriteFile ? (
<div className="px-2">{getFileName(filepath)}</div>
) : (
<Select value={filepath} onValueChange={select}>
<SelectTrigger className="border-none bg-transparent! shadow-none select-none focus:outline-0 active:outline-0">
<SelectValue placeholder="Select a file" />
</SelectTrigger>
<SelectContent className="select-none">
<SelectGroup>
{(artifacts ?? []).map((filepath) => (
<SelectItem key={filepath} value={filepath}>
{getFileName(filepath)}
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
)}
</ArtifactTitle>
2026-01-17 15:09:44 +08:00
</div>
<div className="flex items-center gap-2">
<ArtifactActions>
2026-01-18 17:13:15 +08:00
{!isWriteFile && (
<a href={urlOfArtifact({ filepath, threadId })} target="_blank">
<ArtifactAction
icon={SquareArrowOutUpRightIcon}
label="Open in new window"
tooltip="Open in new window"
/>
</a>
)}
2026-01-17 15:09:44 +08:00
{isCodeFile && (
<ArtifactAction
icon={CopyIcon}
label="Copy"
disabled={!content}
onClick={async () => {
try {
await navigator.clipboard.writeText(content ?? "");
toast.success("Copied to clipboard");
} catch (error) {
toast.error("Failed to copy to clipboard");
console.error(error);
}
}}
tooltip="Copy content to clipboard"
/>
)}
2026-01-18 17:13:15 +08:00
{!isWriteFile && (
<a
href={urlOfArtifact({ filepath, threadId, download: true })}
target="_blank"
>
<ArtifactAction
icon={DownloadIcon}
label="Download"
onClick={() => console.log("Download")}
tooltip="Download file"
/>
</a>
)}
2026-01-17 15:09:44 +08:00
<ArtifactAction
icon={XIcon}
label="Close"
onClick={() => setOpen(false)}
tooltip="Close"
/>
</ArtifactActions>
2026-01-17 00:02:03 +08:00
</div>
2026-01-17 15:09:44 +08:00
</ArtifactHeader>
<ArtifactContent className="p-0">
<FileViewer
className="size-full"
threadId={threadId}
2026-01-18 17:13:15 +08:00
filepath={filepathFromProps}
2026-01-17 15:09:44 +08:00
/>
</ArtifactContent>
</Artifact>
2026-01-17 00:02:03 +08:00
);
}