feat: 新增表格文件预览,优化知识图谱与 Agent 详情交互
- 新增 spreadsheet-preview 组件及 xlsx 解析工具,支持产物中的表格文件预览 - 优化知识图谱画布、详情面板及样式 - 调整 Agent 详情面板、聊天与消息列表交互 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
5daa4b3e20
commit
7d61bc09bc
18 files changed
+1092
-208
No files matched your search
@@ -76,6 +76,7 @@ import { useThread } from "../messages/context";
|
||||
import { Tooltip } from "../tooltip";
|
||||
|
||||
import { useArtifacts } from "./context";
|
||||
import { SpreadsheetPreview } from "./spreadsheet-preview";
|
||||
|
||||
const SANDBOX_RESUME_EVENT = "workspace:sandbox-resume-autoscroll";
|
||||
|
||||
@@ -137,6 +138,11 @@ export function ArtifactFileDetail({
|
||||
const isSkillFile = useMemo(() => {
|
||||
return filepath.endsWith(".skill");
|
||||
}, [filepath]);
|
||||
// .xlsx is a ZIP archive — the generic binary iframe preview renders its
|
||||
// raw bytes as mojibake, so it gets a dedicated table preview instead.
|
||||
const isSpreadsheetFile = useMemo(() => {
|
||||
return /\.xlsx$/i.test(filepath);
|
||||
}, [filepath]);
|
||||
const { isCodeFile, language } = useMemo(() => {
|
||||
if (useWriteFileStreaming) {
|
||||
let nextLanguage = checkCodeFile(filepath).language;
|
||||
@@ -174,13 +180,14 @@ export function ArtifactFileDetail({
|
||||
const hasMultipleArtifacts = (artifacts?.length ?? 0) > 1;
|
||||
const fileTypeLabel = useMemo(() => {
|
||||
if (isImageFile(filepath)) return "IMG";
|
||||
if (isSpreadsheetFile) return "XLSX";
|
||||
if (isMermaidFile(filepath) || language === "mermaid") return "MMD";
|
||||
if (language === "html") return "HTML";
|
||||
if (language === "markdown") return "MD";
|
||||
if (isSkillFile) return "SKILL";
|
||||
if (language) return language.toUpperCase();
|
||||
return "FILE";
|
||||
}, [filepath, isSkillFile, language]);
|
||||
}, [filepath, isSkillFile, isSpreadsheetFile, language]);
|
||||
|
||||
const [viewMode, setViewMode] = useState<"code" | "preview">("code");
|
||||
const [allowAutoViewSwitch, setAllowAutoViewSwitch] = useState(true);
|
||||
@@ -191,6 +198,9 @@ export function ArtifactFileDetail({
|
||||
const [binaryPreviewUrl, setBinaryPreviewUrl] = useState<string | null>(
|
||||
null,
|
||||
);
|
||||
const [spreadsheetData, setSpreadsheetData] = useState<ArrayBuffer | null>(
|
||||
null,
|
||||
);
|
||||
const artifactContentRef = useRef<HTMLDivElement | null>(null);
|
||||
const resumeButtonTimeoutRef = useRef<number | null>(null);
|
||||
|
||||
@@ -310,6 +320,7 @@ export function ArtifactFileDetail({
|
||||
|
||||
useEffect(() => {
|
||||
if (isCodeFile) {
|
||||
setSpreadsheetData(null);
|
||||
setBinaryPreviewUrl((prev) => {
|
||||
if (prev) URL.revokeObjectURL(prev);
|
||||
return null;
|
||||
@@ -325,6 +336,13 @@ export function ArtifactFileDetail({
|
||||
urlOfArtifact({ filepath, threadId, isMock, publicShareCode }),
|
||||
);
|
||||
if (cancelled || !res.ok) return;
|
||||
if (isSpreadsheetFile) {
|
||||
const buffer = await res.arrayBuffer();
|
||||
if (!cancelled) {
|
||||
setSpreadsheetData(buffer);
|
||||
}
|
||||
return;
|
||||
}
|
||||
const blob = await res.blob();
|
||||
if (cancelled) return;
|
||||
const u = URL.createObjectURL(blob);
|
||||
@@ -342,12 +360,13 @@ export function ArtifactFileDetail({
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
setSpreadsheetData(null);
|
||||
setBinaryPreviewUrl((prev) => {
|
||||
if (prev) URL.revokeObjectURL(prev);
|
||||
return null;
|
||||
});
|
||||
};
|
||||
}, [filepath, threadId, isMock, isCodeFile, publicShareCode]);
|
||||
}, [filepath, threadId, isMock, isCodeFile, isSpreadsheetFile, publicShareCode]);
|
||||
|
||||
const handlePauseAutoScroll = useCallback(() => {
|
||||
if (!thread.isLoading || !autoScrollEnabled) {
|
||||
@@ -766,7 +785,18 @@ export function ArtifactFileDetail({
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{!isCodeFile && isSpreadsheetFile && (
|
||||
spreadsheetData ? (
|
||||
<SpreadsheetPreview data={spreadsheetData} />
|
||||
) : (
|
||||
<div className="text-muted-foreground flex size-full items-center justify-center gap-2 text-sm">
|
||||
<LoaderIcon className="size-5 animate-spin" />
|
||||
{t.common.loading}
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
{!isCodeFile &&
|
||||
!isSpreadsheetFile &&
|
||||
(binaryPreviewUrl ? (
|
||||
<iframe className="size-full bg-white" src={binaryPreviewUrl} />
|
||||
) : (
|
||||
|
||||
Reference in new issue
Block a user