Merge origin/master into ji
This commit is contained in:
commit
e96cc97462
54 files changed
+4900
-334
No files matched your search
@@ -44,18 +44,23 @@ import {
|
||||
useArtifactContent,
|
||||
useUpdateArtifactContent,
|
||||
} from "@/core/artifacts/hooks";
|
||||
import { urlOfArtifact } from "@/core/artifacts/utils";
|
||||
import { resolveArtifactURL, urlOfArtifact } from "@/core/artifacts/utils";
|
||||
import { useI18n } from "@/core/i18n/hooks";
|
||||
import { downloadMarkdownAsDocx } from "@/core/scheduled-tasks/markdown-docx";
|
||||
import { installSkill } from "@/core/skills/api";
|
||||
import { streamdownPlugins } from "@/core/streamdown";
|
||||
import { checkCodeFile, getFileName } from "@/core/utils/files";
|
||||
import {
|
||||
createMarkdownAnchorResolver,
|
||||
injectInPageAnchorHandler,
|
||||
isInPageAnchor,
|
||||
scrollToInPageAnchor,
|
||||
} from "@/core/utils/markdown";
|
||||
import {
|
||||
checkCodeFile,
|
||||
getFileName,
|
||||
isImageFile,
|
||||
} from "@/core/utils/files";
|
||||
import {
|
||||
createMarkdownAnchorResolver,
|
||||
injectInPageAnchorHandler,
|
||||
isInPageAnchor,
|
||||
scrollToInPageAnchor,
|
||||
} from "@/core/utils/markdown";
|
||||
import { getBackendBaseURL } from "@/core/config";
|
||||
import { env } from "@/env";
|
||||
import { desensitize } from "@/lib/desensitize";
|
||||
import { cn } from "@/lib/utils";
|
||||
@@ -98,29 +103,36 @@ export function ArtifactFileDetail({
|
||||
className,
|
||||
filepath: filepathFromProps,
|
||||
threadId,
|
||||
onClose,
|
||||
}: {
|
||||
className?: string;
|
||||
filepath: string;
|
||||
threadId: string;
|
||||
/** 覆盖默认关闭行为(仅 setOpen(false));圆桌 Step3 文件预览用于退出沙箱模式。 */
|
||||
onClose?: () => void;
|
||||
}) {
|
||||
const { t } = useI18n();
|
||||
const { artifacts, setOpen, select } = useArtifacts();
|
||||
const { isMock, thread, publicShareCode } = useThread();
|
||||
const isWriteFile = useMemo(() => {
|
||||
const isWriteFileVirtual = useMemo(() => {
|
||||
return filepathFromProps.startsWith("write-file:");
|
||||
}, [filepathFromProps]);
|
||||
const filepath = useMemo(() => {
|
||||
if (isWriteFile) {
|
||||
if (isWriteFileVirtual) {
|
||||
const url = new URL(filepathFromProps);
|
||||
return decodeURIComponent(url.pathname);
|
||||
}
|
||||
return filepathFromProps;
|
||||
}, [filepathFromProps, isWriteFile]);
|
||||
}, [filepathFromProps, isWriteFileVirtual]);
|
||||
/** write_file 流式预览走 tool args 文本;图片等二进制产物始终走 artifacts API。 */
|
||||
const useWriteFileStreaming = useMemo(() => {
|
||||
return isWriteFileVirtual && !isImageFile(filepath);
|
||||
}, [filepath, isWriteFileVirtual]);
|
||||
const isSkillFile = useMemo(() => {
|
||||
return filepath.endsWith(".skill");
|
||||
}, [filepath]);
|
||||
const { isCodeFile, language } = useMemo(() => {
|
||||
if (isWriteFile) {
|
||||
if (useWriteFileStreaming) {
|
||||
let nextLanguage = checkCodeFile(filepath).language;
|
||||
nextLanguage ??= "text";
|
||||
return { isCodeFile: true, language: nextLanguage };
|
||||
@@ -129,18 +141,21 @@ export function ArtifactFileDetail({
|
||||
return { isCodeFile: true, language: "markdown" };
|
||||
}
|
||||
return checkCodeFile(filepath);
|
||||
}, [filepath, isWriteFile, isSkillFile]);
|
||||
}, [filepath, useWriteFileStreaming, isSkillFile]);
|
||||
const isSupportPreview = useMemo(() => {
|
||||
return language === "html" || language === "markdown";
|
||||
}, [language]);
|
||||
const { content, isApplyingEdit } = useArtifactContent({
|
||||
threadId,
|
||||
filepath: filepathFromProps,
|
||||
enabled: isCodeFile && !isWriteFile,
|
||||
enabled: isCodeFile && !useWriteFileStreaming,
|
||||
});
|
||||
|
||||
const canEditArtifact =
|
||||
language === "markdown" && !isWriteFile && !isSkillFile && !isMock;
|
||||
language === "markdown" &&
|
||||
!useWriteFileStreaming &&
|
||||
!isSkillFile &&
|
||||
!isMock;
|
||||
const [draftContent, setDraftContent] = useState("");
|
||||
const [hasLocalEdits, setHasLocalEdits] = useState(false);
|
||||
const updateArtifact = useUpdateArtifactContent();
|
||||
@@ -148,12 +163,13 @@ export function ArtifactFileDetail({
|
||||
const isHtmlStreaming = language === "html" && thread.isLoading;
|
||||
const hasMultipleArtifacts = (artifacts?.length ?? 0) > 1;
|
||||
const fileTypeLabel = useMemo(() => {
|
||||
if (isImageFile(filepath)) return "IMG";
|
||||
if (language === "html") return "HTML";
|
||||
if (language === "markdown") return "MD";
|
||||
if (isSkillFile) return "SKILL";
|
||||
if (language) return language.toUpperCase();
|
||||
return "FILE";
|
||||
}, [isSkillFile, language]);
|
||||
}, [filepath, isSkillFile, language]);
|
||||
|
||||
const [viewMode, setViewMode] = useState<"code" | "preview">("code");
|
||||
const [allowAutoViewSwitch, setAllowAutoViewSwitch] = useState(true);
|
||||
@@ -463,7 +479,7 @@ export function ArtifactFileDetail({
|
||||
{fileTypeLabel}
|
||||
</span>
|
||||
<ArtifactTitle className="min-w-0">
|
||||
{isWriteFile ? (
|
||||
{useWriteFileStreaming ? (
|
||||
<div className="truncate px-3">{getFileName(filepath)}</div>
|
||||
) : hasMultipleArtifacts ? (
|
||||
<Select value={filepath} onValueChange={select}>
|
||||
@@ -540,7 +556,7 @@ export function ArtifactFileDetail({
|
||||
onClick={handleSaveArtifact}
|
||||
/>
|
||||
)}
|
||||
{!isWriteFile && filepath.endsWith(".skill") && (
|
||||
{!useWriteFileStreaming && filepath.endsWith(".skill") && (
|
||||
<Tooltip content={t.toolCalls.skillInstallTooltip}>
|
||||
<ArtifactAction
|
||||
icon={isInstalling ? LoaderIcon : PackageIcon}
|
||||
@@ -559,7 +575,7 @@ export function ArtifactFileDetail({
|
||||
label={t.common.openInNewWindow}
|
||||
tooltip={t.common.openInNewWindow}
|
||||
onClick={async () => {
|
||||
if (isWriteFile) {
|
||||
if (useWriteFileStreaming) {
|
||||
handleOpenWriteFileInNewTab();
|
||||
return;
|
||||
}
|
||||
@@ -597,7 +613,7 @@ export function ArtifactFileDetail({
|
||||
tooltip={t.clipboard.copyToClipboard}
|
||||
/>
|
||||
)}
|
||||
{!isWriteFile && language === "markdown" && (
|
||||
{!useWriteFileStreaming && language === "markdown" && (
|
||||
<ArtifactAction
|
||||
icon={isExportingWord ? LoaderIcon : FileType2Icon}
|
||||
label="另存为 Word"
|
||||
@@ -611,7 +627,7 @@ export function ArtifactFileDetail({
|
||||
label={t.common.download}
|
||||
tooltip={t.common.download}
|
||||
onClick={async () => {
|
||||
if (isWriteFile) {
|
||||
if (useWriteFileStreaming) {
|
||||
handleDownloadWriteFile();
|
||||
return;
|
||||
}
|
||||
@@ -636,7 +652,7 @@ export function ArtifactFileDetail({
|
||||
className="hover:bg-background"
|
||||
icon={XIcon}
|
||||
label={t.common.close}
|
||||
onClick={() => setOpen(false)}
|
||||
onClick={() => (onClose ? onClose() : setOpen(false))}
|
||||
tooltip={t.common.close}
|
||||
/>
|
||||
</ArtifactActions>
|
||||
@@ -669,6 +685,8 @@ export function ArtifactFileDetail({
|
||||
<ArtifactFilePreview
|
||||
content={desensitize(displayContent ?? "")}
|
||||
language={language ?? "text"}
|
||||
threadId={threadId}
|
||||
publicShareCode={publicShareCode}
|
||||
animated={showPreviewSwitchPulse}
|
||||
activeCursor={isApplyingEdit}
|
||||
/>
|
||||
@@ -709,14 +727,57 @@ export function ArtifactFileDetail({
|
||||
);
|
||||
}
|
||||
|
||||
function SandboxPreviewImage({
|
||||
src,
|
||||
alt,
|
||||
threadId,
|
||||
publicShareCode,
|
||||
...props
|
||||
}: React.ImgHTMLAttributes<HTMLImageElement> & {
|
||||
threadId: string;
|
||||
publicShareCode?: string;
|
||||
}) {
|
||||
if (!src || typeof src !== "string") {
|
||||
return null;
|
||||
}
|
||||
|
||||
let url = src;
|
||||
if (src.startsWith("/mnt/")) {
|
||||
if (publicShareCode) {
|
||||
const normalized = src.startsWith("/") ? src : `/${src}`;
|
||||
url = `${getBackendBaseURL()}/api/public/shares/${encodeURIComponent(
|
||||
publicShareCode,
|
||||
)}/files${normalized}`;
|
||||
} else {
|
||||
url = resolveArtifactURL(src, threadId);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<img
|
||||
{...props}
|
||||
className={cn(
|
||||
"my-3 max-w-full overflow-hidden rounded-lg",
|
||||
props.className,
|
||||
)}
|
||||
src={url}
|
||||
alt={alt}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function ArtifactFilePreview({
|
||||
content,
|
||||
language,
|
||||
threadId,
|
||||
publicShareCode,
|
||||
animated = false,
|
||||
activeCursor = false,
|
||||
}: {
|
||||
content: string;
|
||||
language: string;
|
||||
threadId: string;
|
||||
publicShareCode?: string;
|
||||
animated?: boolean;
|
||||
activeCursor?: boolean;
|
||||
}) {
|
||||
@@ -812,11 +873,20 @@ export function ArtifactFilePreview({
|
||||
animated && "animate-[sandbox-preview-enter_320ms_ease-out]",
|
||||
)}
|
||||
>
|
||||
<Streamdown
|
||||
className="size-full"
|
||||
{...streamdownPlugins}
|
||||
components={markdownComponents}
|
||||
>
|
||||
<Streamdown
|
||||
className="size-full"
|
||||
{...streamdownPlugins}
|
||||
components={{
|
||||
...markdownComponents,
|
||||
img: (props) => (
|
||||
<SandboxPreviewImage
|
||||
{...props}
|
||||
threadId={threadId}
|
||||
publicShareCode={publicShareCode}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
>
|
||||
{content ?? ""}
|
||||
</Streamdown>
|
||||
{activeCursor && (
|
||||
|
||||
Reference in new issue
Block a user