fix(frontend): 恢复自定义 artifact-file-detail 实现
This commit is contained in:
parent
a34622c45c
commit
a0ce3d8b08
|
|
@ -1,14 +1,11 @@
|
||||||
|
import { DownloadIcon, FileTextIcon, LoaderIcon, FileTypeIcon } from "lucide-react";
|
||||||
import {
|
import {
|
||||||
Code2Icon,
|
useCallback,
|
||||||
CopyIcon,
|
useEffect,
|
||||||
DownloadIcon,
|
useMemo,
|
||||||
EyeIcon,
|
useState,
|
||||||
LoaderIcon,
|
type HTMLAttributes,
|
||||||
PackageIcon,
|
} from "react";
|
||||||
SquareArrowOutUpRightIcon,
|
|
||||||
XIcon,
|
|
||||||
} from "lucide-react";
|
|
||||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { Streamdown } from "streamdown";
|
import { Streamdown } from "streamdown";
|
||||||
|
|
||||||
|
|
@ -20,27 +17,26 @@ import {
|
||||||
ArtifactHeader,
|
ArtifactHeader,
|
||||||
ArtifactTitle,
|
ArtifactTitle,
|
||||||
} from "@/components/ai-elements/artifact";
|
} from "@/components/ai-elements/artifact";
|
||||||
import { Select, SelectItem } from "@/components/ui/select";
|
|
||||||
import {
|
import {
|
||||||
SelectContent,
|
DropdownMenu,
|
||||||
SelectGroup,
|
DropdownMenuContent,
|
||||||
SelectTrigger,
|
DropdownMenuItem,
|
||||||
SelectValue,
|
DropdownMenuTrigger,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/dropdown-menu";
|
||||||
|
import { DropdownSelector } from "@/components/ui/dropdown-selector";
|
||||||
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
|
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
|
||||||
import { CodeEditor } from "@/components/workspace/code-editor";
|
import { CodeEditor } from "@/components/workspace/code-editor";
|
||||||
import { useArtifactContent } from "@/core/artifacts/hooks";
|
import { useArtifactContent } from "@/core/artifacts/hooks";
|
||||||
import { urlOfArtifact } from "@/core/artifacts/utils";
|
import { urlOfArtifact } from "@/core/artifacts/utils";
|
||||||
import { useI18n } from "@/core/i18n/hooks";
|
import { useI18n } from "@/core/i18n/hooks";
|
||||||
|
import { POST_MESSAGE_TYPES, sendToParent } from "@/core/iframe-messages";
|
||||||
import { installSkill } from "@/core/skills/api";
|
import { installSkill } from "@/core/skills/api";
|
||||||
import { streamdownPlugins } from "@/core/streamdown";
|
import { streamdownPlugins } from "@/core/streamdown";
|
||||||
import { checkCodeFile, getFileName } from "@/core/utils/files";
|
import { checkCodeFile, getFileName } from "@/core/utils/files";
|
||||||
import { env } from "@/env";
|
import { useMarkdownDownload } from "@/core/utils/markdown-download";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn, copyToClipboard, truncateMiddle } from "@/lib/utils";
|
||||||
|
|
||||||
import { ArtifactLink } from "../citations/artifact-link";
|
import { CitationLink } from "../citations/citation-link";
|
||||||
import { useThread } from "../messages/context";
|
|
||||||
import { Tooltip } from "../tooltip";
|
|
||||||
|
|
||||||
import { useArtifacts } from "./context";
|
import { useArtifacts } from "./context";
|
||||||
|
|
||||||
|
|
@ -54,7 +50,8 @@ export function ArtifactFileDetail({
|
||||||
threadId: string;
|
threadId: string;
|
||||||
}) {
|
}) {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const { artifacts, setOpen, select } = useArtifacts();
|
const { artifacts, setOpen, select, fullscreen, setFullscreen } =
|
||||||
|
useArtifacts();
|
||||||
const isWriteFile = useMemo(() => {
|
const isWriteFile = useMemo(() => {
|
||||||
return filepathFromProps.startsWith("write-file:");
|
return filepathFromProps.startsWith("write-file:");
|
||||||
}, [filepathFromProps]);
|
}, [filepathFromProps]);
|
||||||
|
|
@ -80,9 +77,9 @@ export function ArtifactFileDetail({
|
||||||
}
|
}
|
||||||
return checkCodeFile(filepath);
|
return checkCodeFile(filepath);
|
||||||
}, [filepath, isWriteFile, isSkillFile]);
|
}, [filepath, isWriteFile, isSkillFile]);
|
||||||
const isSupportPreview = useMemo(() => {
|
const previewable = useMemo(() => {
|
||||||
return language === "html" || language === "markdown";
|
return (language === "html" && !isWriteFile) || language === "markdown";
|
||||||
}, [language]);
|
}, [isWriteFile, language]);
|
||||||
const { content } = useArtifactContent({
|
const { content } = useArtifactContent({
|
||||||
threadId,
|
threadId,
|
||||||
filepath: filepathFromProps,
|
filepath: filepathFromProps,
|
||||||
|
|
@ -91,16 +88,62 @@ export function ArtifactFileDetail({
|
||||||
|
|
||||||
const displayContent = content ?? "";
|
const displayContent = content ?? "";
|
||||||
|
|
||||||
|
const artifactOptions = useMemo(() => {
|
||||||
|
return (artifacts ?? []).map((artifactPath) => ({
|
||||||
|
value: artifactPath,
|
||||||
|
label: getFileName(artifactPath),
|
||||||
|
}));
|
||||||
|
}, [artifacts]);
|
||||||
|
|
||||||
const [viewMode, setViewMode] = useState<"code" | "preview">("code");
|
const [viewMode, setViewMode] = useState<"code" | "preview">("code");
|
||||||
const [isInstalling, setIsInstalling] = useState(false);
|
const [isInstalling, setIsInstalling] = useState(false);
|
||||||
const { isMock } = useThread();
|
const [zoom, setZoom] = useState(80);
|
||||||
|
|
||||||
|
// 获取文件名(不含路径)
|
||||||
|
const fileName = useMemo(() => getFileName(filepath), [filepath]);
|
||||||
|
|
||||||
|
// 是否可以转换为docx/pdf(仅markdown文件支持)
|
||||||
|
const canConvertToDocxPdf = language === "markdown";
|
||||||
|
|
||||||
|
// 使用 Markdown 下载 hook
|
||||||
|
const { isDownloading, downloadAsDocx, downloadAsPdf } = useMarkdownDownload({
|
||||||
|
onError: (error, format) => {
|
||||||
|
console.error(`Failed to download as ${format}:`, error);
|
||||||
|
toast.error(`Failed to download as ${format.toUpperCase()}`);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// 下载为 DOCX
|
||||||
|
const handleDownloadDocx = useCallback(() => {
|
||||||
|
if (content) {
|
||||||
|
void downloadAsDocx(content, fileName);
|
||||||
|
}
|
||||||
|
}, [content, fileName, downloadAsDocx]);
|
||||||
|
|
||||||
|
// 下载为 PDF
|
||||||
|
const handleDownloadPdf = useCallback(() => {
|
||||||
|
if (content) {
|
||||||
|
void downloadAsPdf(content, fileName);
|
||||||
|
}
|
||||||
|
}, [content, fileName, downloadAsPdf]);
|
||||||
|
|
||||||
|
// 全屏切换处理
|
||||||
|
const handleFullscreenToggle = useCallback(() => {
|
||||||
|
const newFullscreen = !fullscreen;
|
||||||
|
setFullscreen(newFullscreen);
|
||||||
|
sendToParent({
|
||||||
|
type: POST_MESSAGE_TYPES.FULLSCREEN,
|
||||||
|
fullscreen: newFullscreen,
|
||||||
|
});
|
||||||
|
}, [fullscreen, setFullscreen]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isSupportPreview) {
|
if (previewable) {
|
||||||
setViewMode("preview");
|
setViewMode("preview");
|
||||||
} else {
|
} else {
|
||||||
setViewMode("code");
|
setViewMode("code");
|
||||||
}
|
}
|
||||||
}, [isSupportPreview]);
|
}, [previewable]);
|
||||||
|
|
||||||
const handleInstallSkill = useCallback(async () => {
|
const handleInstallSkill = useCallback(async () => {
|
||||||
if (isInstalling) return;
|
if (isInstalling) return;
|
||||||
|
|
@ -123,38 +166,18 @@ export function ArtifactFileDetail({
|
||||||
setIsInstalling(false);
|
setIsInstalling(false);
|
||||||
}
|
}
|
||||||
}, [threadId, filepath, isInstalling]);
|
}, [threadId, filepath, isInstalling]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Artifact className={cn(className)}>
|
// 给滚动遮挡头部定位relative
|
||||||
<ArtifactHeader className="px-2">
|
<Artifact className={cn("relative",className)}>
|
||||||
<div className="flex items-center gap-2">
|
<ArtifactHeader>
|
||||||
<ArtifactTitle>
|
<div className="flex items-center justify-start gap-2">
|
||||||
{isWriteFile ? (
|
{previewable && (
|
||||||
<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>
|
|
||||||
</div>
|
|
||||||
<div className="flex min-w-0 grow items-center justify-center">
|
|
||||||
{isSupportPreview && (
|
|
||||||
<ToggleGroup
|
<ToggleGroup
|
||||||
className="mx-auto"
|
|
||||||
type="single"
|
type="single"
|
||||||
variant="outline"
|
variant={null}
|
||||||
size="sm"
|
size="default"
|
||||||
|
className="h-[28px]"
|
||||||
value={viewMode}
|
value={viewMode}
|
||||||
onValueChange={(value) => {
|
onValueChange={(value) => {
|
||||||
if (value) {
|
if (value) {
|
||||||
|
|
@ -163,47 +186,75 @@ export function ArtifactFileDetail({
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ToggleGroupItem value="code">
|
<ToggleGroupItem value="code">
|
||||||
<Code2Icon />
|
<svg
|
||||||
|
width="18"
|
||||||
|
height="18"
|
||||||
|
viewBox="0 0 18 18"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M5 6L2 9L5 12"
|
||||||
|
stroke="#150033"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M11 3L7 15"
|
||||||
|
stroke="#150033"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M13 6L16 9L13 12"
|
||||||
|
stroke="#150033"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
</ToggleGroupItem>
|
</ToggleGroupItem>
|
||||||
<ToggleGroupItem value="preview">
|
<ToggleGroupItem value="preview">
|
||||||
<EyeIcon />
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="16"
|
||||||
|
height="10"
|
||||||
|
viewBox="0 0 16 10"
|
||||||
|
fill="none"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M8 0.5C10.4943 0.5 12.8473 1.84466 14.792 4.21973C15.1644 4.67466 15.1644 5.32534 14.792 5.78027C12.8473 8.15534 10.4943 9.5 8 9.5C5.50561 9.49989 3.15269 8.15543 1.20801 5.78027C0.835561 5.32534 0.835562 4.67466 1.20801 4.21973C3.15269 1.84457 5.50561 0.500106 8 0.5Z"
|
||||||
|
stroke="#666666"
|
||||||
|
/>
|
||||||
|
<circle cx="8" cy="5" r="1.5" stroke="#666666" />
|
||||||
|
</svg>
|
||||||
</ToggleGroupItem>
|
</ToggleGroupItem>
|
||||||
</ToggleGroup>
|
</ToggleGroup>
|
||||||
)}
|
)}
|
||||||
|
{/* 放大缩小选择器 */}
|
||||||
|
<ArtifactZoomSelector value={zoom} onChange={setZoom} />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex min-w-0 grow items-center justify-center">
|
||||||
|
<ArtifactTitle>
|
||||||
|
{isWriteFile ? (
|
||||||
|
<div className=" w-full text-center overflow-hidden text-ellipsis whitespace-nowrap px-2">{truncateMiddle(getFileName(filepath), 50)}</div>
|
||||||
|
) : (
|
||||||
|
<DropdownSelector
|
||||||
|
value={filepath}
|
||||||
|
options={artifactOptions}
|
||||||
|
onChange={select}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</ArtifactTitle>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-end overflow-hidden">
|
||||||
<ArtifactActions>
|
<ArtifactActions>
|
||||||
{!isWriteFile && filepath.endsWith(".skill") && (
|
|
||||||
<Tooltip content={t.toolCalls.skillInstallTooltip}>
|
|
||||||
<ArtifactAction
|
|
||||||
icon={isInstalling ? LoaderIcon : PackageIcon}
|
|
||||||
label={t.common.install}
|
|
||||||
tooltip={t.common.install}
|
|
||||||
disabled={
|
|
||||||
isInstalling ||
|
|
||||||
env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY === "true"
|
|
||||||
}
|
|
||||||
onClick={handleInstallSkill}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
{!isWriteFile && (
|
|
||||||
<a href={urlOfArtifact({ filepath, threadId })} target="_blank">
|
|
||||||
<ArtifactAction
|
|
||||||
icon={SquareArrowOutUpRightIcon}
|
|
||||||
label={t.common.openInNewWindow}
|
|
||||||
tooltip={t.common.openInNewWindow}
|
|
||||||
/>
|
|
||||||
</a>
|
|
||||||
)}
|
|
||||||
{isCodeFile && (
|
{isCodeFile && (
|
||||||
<ArtifactAction
|
<ArtifactAction
|
||||||
icon={CopyIcon}
|
|
||||||
label={t.clipboard.copyToClipboard}
|
label={t.clipboard.copyToClipboard}
|
||||||
disabled={!content}
|
disabled={!content}
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
try {
|
try {
|
||||||
await navigator.clipboard.writeText(displayContent ?? "");
|
await copyToClipboard(displayContent ?? "");
|
||||||
toast.success(t.clipboard.copiedToClipboard);
|
toast.success(t.clipboard.copiedToClipboard);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error("Failed to copy to clipboard");
|
toast.error("Failed to copy to clipboard");
|
||||||
|
|
@ -211,49 +262,210 @@ export function ArtifactFileDetail({
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
tooltip={t.clipboard.copyToClipboard}
|
tooltip={t.clipboard.copyToClipboard}
|
||||||
/>
|
>
|
||||||
|
<svg
|
||||||
|
width="18"
|
||||||
|
height="18"
|
||||||
|
viewBox="0 0 18 18"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M6 2H13C14.1046 2 15 2.89543 15 4V13"
|
||||||
|
stroke="#666666"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
<rect
|
||||||
|
x="2.5"
|
||||||
|
y="4.5"
|
||||||
|
width="10"
|
||||||
|
height="11"
|
||||||
|
rx="1.5"
|
||||||
|
stroke="#666666"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</ArtifactAction>
|
||||||
)}
|
)}
|
||||||
{!isWriteFile && (
|
{!isWriteFile && (
|
||||||
<a
|
<DropdownMenu>
|
||||||
href={urlOfArtifact({ filepath, threadId, download: true })}
|
<DropdownMenuTrigger asChild>
|
||||||
target="_blank"
|
<ArtifactAction
|
||||||
>
|
label={t.common.download}
|
||||||
<ArtifactAction
|
tooltip={t.common.download}
|
||||||
icon={DownloadIcon}
|
>
|
||||||
label={t.common.download}
|
{isDownloading ? (
|
||||||
tooltip={t.common.download}
|
<LoaderIcon className="size-4 animate-spin" />
|
||||||
/>
|
) : (
|
||||||
</a>
|
<svg
|
||||||
|
width="18"
|
||||||
|
height="18"
|
||||||
|
viewBox="0 0 18 18"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M16 9V14C16 15.1046 15.1046 16 14 16H4C2.89543 16 2 15.1046 2 14V9"
|
||||||
|
stroke="#666666"
|
||||||
|
strokeLinecap="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M9 2V13M9 13L5 9M9 13L13 9"
|
||||||
|
stroke="#666666"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
)}
|
||||||
|
</ArtifactAction>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end" className="min-w-[160px]">
|
||||||
|
<DropdownMenuItem asChild>
|
||||||
|
<a
|
||||||
|
href={urlOfArtifact({
|
||||||
|
filepath,
|
||||||
|
threadId,
|
||||||
|
download: true,
|
||||||
|
})}
|
||||||
|
target="_blank"
|
||||||
|
className="w-full cursor-pointer"
|
||||||
|
>
|
||||||
|
<DownloadIcon className="size-4" />
|
||||||
|
{t.common.downloadOriginal}
|
||||||
|
</a>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
{/* DOCX 和 PDF 导出选项仅对 Markdown 文件显示。 */}
|
||||||
|
{canConvertToDocxPdf && (
|
||||||
|
<>
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={handleDownloadDocx}
|
||||||
|
disabled={isDownloading !== null || !content}
|
||||||
|
className="cursor-pointer"
|
||||||
|
>
|
||||||
|
<FileTextIcon className="size-4" />
|
||||||
|
{isDownloading === "docx" ? t.common.loading : t.common.downloadAsDocx}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={handleDownloadPdf}
|
||||||
|
disabled={isDownloading !== null || !content}
|
||||||
|
className="cursor-pointer"
|
||||||
|
>
|
||||||
|
<FileTypeIcon className="size-4" />
|
||||||
|
{isDownloading === "pdf" ? t.common.loading : t.common.downloadAsPdf}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
)}
|
)}
|
||||||
|
{/* 全屏按钮 */}
|
||||||
<ArtifactAction
|
<ArtifactAction
|
||||||
icon={XIcon}
|
label={
|
||||||
label={t.common.close}
|
fullscreen ? t.common.closeFullScreen : t.common.fullScreen
|
||||||
onClick={() => setOpen(false)}
|
}
|
||||||
tooltip={t.common.close}
|
onClick={handleFullscreenToggle}
|
||||||
/>
|
tooltip={
|
||||||
|
fullscreen ? t.common.closeFullScreen : t.common.fullScreen
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{fullscreen ? (
|
||||||
|
<svg
|
||||||
|
width="18"
|
||||||
|
height="18"
|
||||||
|
viewBox="0 0 18 18"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M6 2V4C6 5.10457 5.10457 6 4 6H2"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M6 16V14C6 12.8954 5.10457 12 4 12H2"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M12 2V4C12 5.10457 12.8954 6 14 6H16"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M12 16V14C12 12.8954 12.8954 12 14 12H16"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
) : (
|
||||||
|
<svg
|
||||||
|
width="18"
|
||||||
|
height="18"
|
||||||
|
viewBox="0 0 18 18"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M5.75 15.5H4.5C3.39543 15.5 2.5 14.6046 2.5 13.5V12.25M2.5 5.75V4.5C2.5 3.39543 3.39543 2.5 4.5 2.5H5.75M12.25 2.5H13.5C14.6046 2.5 15.5 3.39543 15.5 4.5V5.75M15.5 12.25V13.5C15.5 14.6046 14.6046 15.5 13.5 15.5H12.25"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
)}
|
||||||
|
</ArtifactAction>
|
||||||
|
{!fullscreen && (
|
||||||
|
<ArtifactAction
|
||||||
|
label={t.common.close}
|
||||||
|
onClick={() => setOpen(false)}
|
||||||
|
tooltip={t.common.close}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
width="18"
|
||||||
|
height="18"
|
||||||
|
viewBox="0 0 18 18"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M4 14L14 4M4 4L14 14"
|
||||||
|
stroke="#666666"
|
||||||
|
strokeLinecap="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</ArtifactAction>
|
||||||
|
)}
|
||||||
</ArtifactActions>
|
</ArtifactActions>
|
||||||
</div>
|
</div>
|
||||||
</ArtifactHeader>
|
</ArtifactHeader>
|
||||||
<ArtifactContent className="p-0">
|
<ArtifactContent className=" rounded-b-[10px] bg-white p-0">
|
||||||
{isSupportPreview &&
|
{/* 遮挡多余的滚动顶部 */}
|
||||||
|
<div className="absolute w-[calc(100%-40px)] bg-white z-20 h-5 rounded-t-[10px] top-[57px]"></div>
|
||||||
|
{previewable &&
|
||||||
viewMode === "preview" &&
|
viewMode === "preview" &&
|
||||||
(language === "markdown" || language === "html") && (
|
(language === "markdown" || language === "html") && (
|
||||||
<ArtifactFilePreview
|
<ArtifactFilePreview
|
||||||
content={displayContent}
|
content={displayContent}
|
||||||
language={language ?? "text"}
|
language={language ?? "text"}
|
||||||
|
zoom={zoom}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{isCodeFile && viewMode === "code" && (
|
{isCodeFile && viewMode === "code" && (
|
||||||
<CodeEditor
|
<CodeEditor
|
||||||
className="size-full resize-none rounded-none border-none"
|
className="size-full py-[20px] resize-none rounded-none border-none"
|
||||||
value={displayContent ?? ""}
|
value={displayContent ?? ""}
|
||||||
|
zoom={zoom}
|
||||||
readonly
|
readonly
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{!isCodeFile && (
|
{!isCodeFile && (
|
||||||
<iframe
|
<iframe
|
||||||
className="size-full"
|
className="size-full"
|
||||||
src={urlOfArtifact({ filepath, threadId, isMock })}
|
src={urlOfArtifact({ filepath, threadId })}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</ArtifactContent>
|
</ArtifactContent>
|
||||||
|
|
@ -264,17 +476,24 @@ export function ArtifactFileDetail({
|
||||||
export function ArtifactFilePreview({
|
export function ArtifactFilePreview({
|
||||||
content,
|
content,
|
||||||
language,
|
language,
|
||||||
|
zoom = 100,
|
||||||
}: {
|
}: {
|
||||||
content: string;
|
content: string;
|
||||||
language: string;
|
language: string;
|
||||||
|
zoom?: number;
|
||||||
}) {
|
}) {
|
||||||
|
const zoomScale = zoom / 100;
|
||||||
|
|
||||||
if (language === "markdown") {
|
if (language === "markdown") {
|
||||||
return (
|
return (
|
||||||
<div className="size-full px-4">
|
<div
|
||||||
|
className={cn("size-full p-[20px]")}
|
||||||
|
style={{ "--zoom-scale": zoomScale } as React.CSSProperties}
|
||||||
|
>
|
||||||
<Streamdown
|
<Streamdown
|
||||||
className="size-full"
|
className="size-full"
|
||||||
{...streamdownPlugins}
|
{...streamdownPlugins}
|
||||||
components={{ a: ArtifactLink }}
|
components={{ a: CitationLink }}
|
||||||
>
|
>
|
||||||
{content ?? ""}
|
{content ?? ""}
|
||||||
</Streamdown>
|
</Streamdown>
|
||||||
|
|
@ -288,8 +507,130 @@ export function ArtifactFilePreview({
|
||||||
title="Artifact preview"
|
title="Artifact preview"
|
||||||
srcDoc={content}
|
srcDoc={content}
|
||||||
sandbox="allow-scripts allow-forms"
|
sandbox="allow-scripts allow-forms"
|
||||||
|
style={{ zoom: zoomScale }}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 缩放比例选项
|
||||||
|
const ZOOM_LEVELS = [50, 60, 70, 80, 90, 100, 110, 120, 130, 150, 175, 200];
|
||||||
|
|
||||||
|
export type ArtifactZoomSelectorProps = Omit<
|
||||||
|
HTMLAttributes<HTMLDivElement>,
|
||||||
|
"onChange"
|
||||||
|
> & {
|
||||||
|
value?: number;
|
||||||
|
onChange?: (value: number) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ArtifactZoomSelector = ({
|
||||||
|
value = 100,
|
||||||
|
onChange,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: ArtifactZoomSelectorProps) => {
|
||||||
|
const handleZoomIn = () => {
|
||||||
|
const currentIndex = ZOOM_LEVELS.indexOf(value);
|
||||||
|
const nextValue = ZOOM_LEVELS[currentIndex + 1];
|
||||||
|
if (currentIndex < ZOOM_LEVELS.length - 1 && nextValue !== undefined) {
|
||||||
|
onChange?.(nextValue);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleZoomOut = () => {
|
||||||
|
const currentIndex = ZOOM_LEVELS.indexOf(value);
|
||||||
|
const prevValue = ZOOM_LEVELS[currentIndex - 1];
|
||||||
|
if (currentIndex > 0 && prevValue !== undefined) {
|
||||||
|
onChange?.(prevValue);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const canZoomIn = ZOOM_LEVELS.indexOf(value) < ZOOM_LEVELS.length - 1;
|
||||||
|
const canZoomOut = ZOOM_LEVELS.indexOf(value) > 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"inline-flex h-[28px] items-center gap-1 rounded-[10px] bg-white backdrop-blur-sm",
|
||||||
|
"dark:border-gray-700/50 dark:bg-gray-800/90",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleZoomIn}
|
||||||
|
disabled={!canZoomIn}
|
||||||
|
className={cn(
|
||||||
|
"flex h-full w-10 items-center justify-center rounded py-1 transition-colors",
|
||||||
|
"text-gray-400 hover:bg-gray-100 hover:text-gray-600",
|
||||||
|
"disabled:cursor-not-allowed disabled:opacity-40 disabled:hover:bg-transparent",
|
||||||
|
"dark:text-gray-500 dark:hover:bg-gray-700 dark:hover:text-gray-300",
|
||||||
|
)}
|
||||||
|
aria-label="放大"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
fill="none"
|
||||||
|
>
|
||||||
|
<circle cx="7.55558" cy="7.55534" r="6.16667" stroke="#666666" />
|
||||||
|
<path
|
||||||
|
d="M13.8688 15.4646C14.064 15.6598 14.3806 15.6598 14.5759 15.4646C14.7711 15.2693 14.7711 14.9527 14.5759 14.7574L14.2223 15.111L13.8688 15.4646ZM14.2223 15.111L14.5759 14.7574L11.9092 12.0908L11.5557 12.4443L11.2021 12.7979L13.8688 15.4646L14.2223 15.111Z"
|
||||||
|
fill="#666666"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M5.33325 7.5H9.7777M7.55547 5V10"
|
||||||
|
stroke="#666666"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
"min-w-[36px] text-center text-xs font-medium text-gray-600",
|
||||||
|
"dark:text-gray-300",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{value}%
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleZoomOut}
|
||||||
|
disabled={!canZoomOut}
|
||||||
|
className={cn(
|
||||||
|
"flex h-full w-10 items-center justify-center rounded transition-colors",
|
||||||
|
"text-gray-400 hover:bg-gray-100 hover:text-gray-600",
|
||||||
|
"disabled:cursor-not-allowed disabled:opacity-40 disabled:hover:bg-transparent",
|
||||||
|
"dark:text-gray-500 dark:hover:bg-gray-700 dark:hover:text-gray-300",
|
||||||
|
)}
|
||||||
|
aria-label="缩小"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
fill="none"
|
||||||
|
>
|
||||||
|
<circle cx="7.55558" cy="7.55534" r="6.16667" stroke="#666666" />
|
||||||
|
<path
|
||||||
|
d="M13.8688 15.4646C14.064 15.6598 14.3806 15.6598 14.5759 15.4646C14.7711 15.2693 14.7711 14.9527 14.5759 14.7574L14.2223 15.111L13.8688 15.4646ZM14.2223 15.111L14.5759 14.7574L11.9092 12.0908L11.5557 12.4443L11.2021 12.7979L13.8688 15.4646L14.2223 15.111Z"
|
||||||
|
fill="#666666"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M4.99927 7.5H9.99927"
|
||||||
|
stroke="#666666"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue