import { Code2Icon, CopyIcon, DownloadIcon, ExternalLinkIcon, EyeIcon, SquareArrowOutUpRightIcon, XIcon, } from "lucide-react"; import * as React from "react"; import { useEffect, useMemo, useState } from "react"; import rehypeKatex from "rehype-katex"; import remarkGfm from "remark-gfm"; import remarkMath from "remark-math"; import { toast } from "sonner"; import { Streamdown } from "streamdown"; import { Artifact, ArtifactAction, ArtifactActions, ArtifactContent, ArtifactHeader, ArtifactTitle, } from "@/components/ai-elements/artifact"; import { InlineCitationCard, InlineCitationCardBody, InlineCitationSource, } from "@/components/ai-elements/inline-citation"; import { Badge } from "@/components/ui/badge"; import { HoverCardTrigger } from "@/components/ui/hover-card"; import { Select, SelectItem } from "@/components/ui/select"; import { SelectContent, SelectGroup, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"; import { CodeEditor } from "@/components/workspace/code-editor"; import { useArtifactContent } from "@/core/artifacts/hooks"; import { urlOfArtifact } from "@/core/artifacts/utils"; import { buildCitationMap, extractDomainFromUrl, parseCitations, type Citation, } from "@/core/citations"; import { useI18n } from "@/core/i18n/hooks"; import { streamdownPlugins } from "@/core/streamdown"; import { checkCodeFile, getFileName } from "@/core/utils/files"; import { cn } from "@/lib/utils"; import { useArtifacts } from "./context"; export function ArtifactFileDetail({ className, filepath: filepathFromProps, threadId, }: { className?: string; filepath: string; threadId: string; }) { const { t } = useI18n(); const { artifacts, setOpen, select } = useArtifacts(); const isWriteFile = useMemo(() => { return filepathFromProps.startsWith("write-file:"); }, [filepathFromProps]); const filepath = useMemo(() => { if (isWriteFile) { const url = new URL(filepathFromProps); return decodeURIComponent(url.pathname); } return filepathFromProps; }, [filepathFromProps, isWriteFile]); const { isCodeFile, language } = useMemo(() => { if (isWriteFile) { let language = checkCodeFile(filepath).language; language ??= "text"; return { isCodeFile: true, language }; } return checkCodeFile(filepath); }, [filepath, isWriteFile]); const previewable = useMemo(() => { return (language === "html" && !isWriteFile) || language === "markdown"; }, [isWriteFile, language]); const { content } = useArtifactContent({ threadId, filepath: filepathFromProps, enabled: isCodeFile && !isWriteFile, }); // Parse citations and get clean content for code editor const cleanContent = useMemo(() => { if (language === "markdown" && content) { return parseCitations(content).cleanContent; } return content; }, [content, language]); const [viewMode, setViewMode] = useState<"code" | "preview">("code"); useEffect(() => { if (previewable) { setViewMode("preview"); } else { setViewMode("code"); } }, [previewable]); return (
{isWriteFile ? (
{getFileName(filepath)}
) : ( )}
{previewable && ( setViewMode(value as "code" | "preview") } > )}
{!isWriteFile && ( )} {isCodeFile && ( { try { await navigator.clipboard.writeText(content ?? ""); toast.success(t.clipboard.copiedToClipboard); } catch (error) { toast.error("Failed to copy to clipboard"); console.error(error); } }} tooltip={t.clipboard.copyToClipboard} /> )} {!isWriteFile && ( )} setOpen(false)} tooltip={t.common.close} />
{previewable && viewMode === "preview" && ( )} {isCodeFile && viewMode === "code" && ( )} {!isCodeFile && (