fix: polish position roundtable artifact preview
This commit is contained in:
1 parent
7bd032ec77
commit
359bd28f2c
2 files changed
+238
-35
No files matched your search
@@ -2,6 +2,7 @@ import type { BaseStream } from "@langchain/langgraph-sdk/react";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { Bot, FileText, LockKeyhole } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { Select as TSelect } from "tdesign-react";
|
||||
import type { PromptInputMessage } from "@/components/ai-elements/prompt-input";
|
||||
import { InputBox } from "@/components/workspace/input-box";
|
||||
import { ThreadContext } from "@/components/workspace/messages/context";
|
||||
@@ -22,6 +23,11 @@ import {
|
||||
import { listSessionArtifacts } from "@/roundtable-planning/api/multi-agent";
|
||||
import { desensitize } from "@/lib/desensitize";
|
||||
|
||||
interface PositionNodeSwitchOption {
|
||||
node: PositionRoundtableNode;
|
||||
agentName?: string | null;
|
||||
}
|
||||
|
||||
interface PositionNodeChatProps {
|
||||
sessionId: string;
|
||||
/** Parent task id: all node threads are indexed under this task. */
|
||||
@@ -31,6 +37,8 @@ interface PositionNodeChatProps {
|
||||
currentPositionId: string;
|
||||
sessionStatus: "intent_pending" | "active" | "completed" | "archived";
|
||||
agentName?: string | null;
|
||||
positionNodeOptions?: PositionNodeSwitchOption[];
|
||||
onSelectPositionNode?: (node: PositionRoundtableNode) => void;
|
||||
onNodeUpdate: (node: PositionRoundtableNode) => void;
|
||||
/** The workspace collapses the configuration panel on the first real turn. */
|
||||
onConversationStart?: () => void;
|
||||
@@ -91,6 +99,36 @@ function hasMarkdownArtifact(manifest: Array<Record<string, unknown>>): boolean
|
||||
});
|
||||
}
|
||||
|
||||
function hasAnyArtifact(manifest: Array<Record<string, unknown>>): boolean {
|
||||
return manifest.some((artifact) => {
|
||||
const path = typeof artifact.path === "string" ? artifact.path.trim() : "";
|
||||
const name = typeof artifact.name === "string" ? artifact.name.trim() : "";
|
||||
return Boolean(path || name);
|
||||
});
|
||||
}
|
||||
|
||||
function nodeSwitchStatusLabel(node: PositionRoundtableNode): string {
|
||||
if (node.invalidatedBy || node.status === "stale") {
|
||||
return "需重做";
|
||||
}
|
||||
|
||||
switch (node.status) {
|
||||
case "running":
|
||||
return "进行中";
|
||||
case "done":
|
||||
return "已完成";
|
||||
case "rejected":
|
||||
return "已驳回";
|
||||
case "error":
|
||||
return "异常";
|
||||
case "locked":
|
||||
return "未开始";
|
||||
case "ready":
|
||||
default:
|
||||
return "待开始";
|
||||
}
|
||||
}
|
||||
|
||||
function artifactDisplayName(pathOrUrl: string): string {
|
||||
let path = pathOrUrl;
|
||||
if (pathOrUrl.startsWith("write-file:")) {
|
||||
@@ -153,6 +191,8 @@ export function PositionNodeChat({
|
||||
currentPositionId,
|
||||
sessionStatus,
|
||||
agentName,
|
||||
positionNodeOptions,
|
||||
onSelectPositionNode,
|
||||
onNodeUpdate,
|
||||
onConversationStart,
|
||||
onPreviewArtifact,
|
||||
@@ -161,6 +201,13 @@ export function PositionNodeChat({
|
||||
const [settings, setSettings] = useLocalSettings();
|
||||
const { agent } = useAgent(node.agentId);
|
||||
const agentDisplayName = desensitize(agentName || agent?.name || node.agentId);
|
||||
const switchOptions = useMemo<PositionNodeSwitchOption[]>(
|
||||
() =>
|
||||
positionNodeOptions?.length
|
||||
? positionNodeOptions
|
||||
: [{ node, agentName }],
|
||||
[agentName, node, positionNodeOptions],
|
||||
);
|
||||
const artifactsCtx = useArtifactsOptional();
|
||||
const [boundThreadId, setBoundThreadId] = useState<string | null>(node.threadId);
|
||||
const boundThreadIdRef = useRef<string | null>(node.threadId);
|
||||
@@ -284,6 +331,35 @@ export function PositionNodeChat({
|
||||
|
||||
const activeThreadId =
|
||||
boundThreadIdRef.current ?? actualThreadId ?? boundThreadId ?? draftThreadIdRef.current;
|
||||
const hasMultipleSwitchOptions = switchOptions.length > 1;
|
||||
const switchSelectOptions = useMemo(
|
||||
() =>
|
||||
switchOptions.map((option, index) => {
|
||||
const optionNode = option.node;
|
||||
const statusLabel = nodeSwitchStatusLabel(optionNode);
|
||||
const optionName = desensitize(
|
||||
option.agentName || optionNode.agentId || `智能体 ${index + 1}`,
|
||||
);
|
||||
const artifactText = hasAnyArtifact(optionNode.artifactManifest) ? " · 有产物" : "";
|
||||
|
||||
return {
|
||||
label: `${optionName} · ${statusLabel}${artifactText}`,
|
||||
value: optionNode.nodeKey,
|
||||
};
|
||||
}),
|
||||
[switchOptions],
|
||||
);
|
||||
const handleSwitchNode = useCallback(
|
||||
(targetNode: PositionRoundtableNode) => {
|
||||
if (targetNode.nodeKey === node.nodeKey) return;
|
||||
if (thread.isLoading) {
|
||||
toast.info("当前智能体正在生成,完成后可切换");
|
||||
return;
|
||||
}
|
||||
onSelectPositionNode?.(targetNode);
|
||||
},
|
||||
[node.nodeKey, onSelectPositionNode, thread.isLoading],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
onThreadSnapshot?.({ threadId: activeThreadId, thread });
|
||||
@@ -360,6 +436,7 @@ export function PositionNodeChat({
|
||||
|
||||
const status = thread.error ? "error" : thread.isLoading ? "streaming" : "ready";
|
||||
const displayThreadId = boundThreadId ?? `position-node-${node.id}`;
|
||||
const threadContextValue = useMemo(() => ({ thread }), [thread]);
|
||||
const readOnlyHint =
|
||||
sessionStatus === "archived"
|
||||
? "该会话已归档,仅可查看历史对话和产物"
|
||||
@@ -372,9 +449,9 @@ export function PositionNodeChat({
|
||||
: "";
|
||||
|
||||
return (
|
||||
<ThreadContext.Provider value={{ thread }}>
|
||||
<ThreadContext.Provider value={threadContextValue}>
|
||||
<div className="flex min-h-0 flex-1 flex-col">
|
||||
<header className="flex h-12 shrink-0 items-center gap-2 border-b border-slate-100 px-4 dark:border-[#2b8ffe]/20">
|
||||
<header className="flex min-h-12 shrink-0 items-center gap-2 border-b border-slate-100 px-4 py-2 dark:border-[#2b8ffe]/20">
|
||||
<span className="flex size-7 items-center justify-center rounded-lg bg-primary/10 text-primary">
|
||||
{node.status === "locked" ? <LockKeyhole className="size-3.5" /> : <Bot className="size-4" />}
|
||||
</span>
|
||||
@@ -386,6 +463,32 @@ export function PositionNodeChat({
|
||||
{readOnlyHint || `节点状态:${node.status}`}
|
||||
</p>
|
||||
</div>
|
||||
{hasMultipleSwitchOptions && (
|
||||
<div
|
||||
className="ml-auto mr-12 flex min-w-0 shrink-0 items-center gap-2"
|
||||
title={thread.isLoading ? "当前智能体正在生成,完成后可切换" : "切换同岗位智能体"}
|
||||
>
|
||||
<span className="hidden shrink-0 text-[10px] font-semibold text-muted-foreground xl:inline">
|
||||
同岗位智能体
|
||||
</span>
|
||||
<TSelect
|
||||
aria-label="同岗位智能体切换"
|
||||
className="shrink-0 [&_.t-input]:!rounded-xl [&_.t-input]:!border-slate-200 [&_.t-input]:!bg-white [&_.t-input]:!text-xs [&_.t-input]:!font-semibold [&_.t-input]:!text-slate-700 [&_.t-input]:!shadow-sm dark:[&_.t-input]:!border-[#2b8ffe]/35 dark:[&_.t-input]:!bg-[#082d5f] dark:[&_.t-input]:!text-[#e8edf5]"
|
||||
style={{ width: 380, maxWidth: "34vw" }}
|
||||
value={node.nodeKey}
|
||||
options={switchSelectOptions}
|
||||
onChange={(value) => {
|
||||
const nextNode = switchOptions.find(
|
||||
(option) => option.node.nodeKey === String(value),
|
||||
)?.node;
|
||||
if (nextNode) {
|
||||
handleSwitchNode(nextNode);
|
||||
}
|
||||
}}
|
||||
popupProps={{ overlayInnerStyle: { minWidth: 380 } }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</header>
|
||||
|
||||
{nodeArtifacts.length > 0 && (
|
||||
|
||||
@@ -41,7 +41,7 @@ import { toast } from "sonner";
|
||||
import { agentColor } from "@/components/workspace/agent-icon-color";
|
||||
import { AgentDetailPanel } from "@/components/workspace/agents/agent-detail-panel";
|
||||
import { useAgentModuleSidebarData } from "@/components/workspace/agents/agent-module-sidebar";
|
||||
import { ArtifactFileDetail, useArtifactsOptional } from "@/components/workspace/artifacts";
|
||||
import { ArtifactFileDetail, ArtifactFilePreview, useArtifactsOptional } from "@/components/workspace/artifacts";
|
||||
import { ThreadContext } from "@/components/workspace/messages/context";
|
||||
import {
|
||||
Dialog,
|
||||
@@ -55,6 +55,7 @@ import { useTheme } from "@/contexts/ThemeContext";
|
||||
import { LayoutModeSwitcher, ThemeSwitcher } from "@/components/workspace/appearance-controls";
|
||||
import { getAPIClient } from "@/core/api/api-client";
|
||||
import { useAgent } from "@/core/agents";
|
||||
import { loadArtifactContent } from "@/core/artifacts/loader";
|
||||
import type { Agent } from "@/core/agents";
|
||||
import { useModels } from "@/core/models/hooks";
|
||||
import type { AgentThreadState } from "@/core/threads/types";
|
||||
@@ -123,6 +124,11 @@ type IntentEditDraft = {
|
||||
constraints: string;
|
||||
assumptions: string;
|
||||
};
|
||||
type SandboxThreadSnapshot = {
|
||||
threadId: string;
|
||||
thread: BaseStream<AgentThreadState>;
|
||||
signature: string;
|
||||
};
|
||||
|
||||
function sandboxThreadSignature(thread: BaseStream<AgentThreadState>): string {
|
||||
const messages = thread.messages ?? [];
|
||||
@@ -439,11 +445,10 @@ export default function PositionRoundtablePage({
|
||||
const [sandboxArtifact, setSandboxArtifact] = useState<PositionArtifactPreview | null>(null);
|
||||
const sandboxArtifactRef = useRef<PositionArtifactPreview | null>(null);
|
||||
const closedSandboxKeyRef = useRef<string | null>(null);
|
||||
const [sandboxThreadSnapshot, setSandboxThreadSnapshot] = useState<{
|
||||
threadId: string;
|
||||
thread: BaseStream<AgentThreadState>;
|
||||
signature: string;
|
||||
} | null>(null);
|
||||
const [sandboxThreadSnapshot, setSandboxThreadSnapshot] = useState<SandboxThreadSnapshot | null>(null);
|
||||
const latestNodeThreadSnapshotRef = useRef<SandboxThreadSnapshot | null>(null);
|
||||
const pendingSandboxThreadSnapshotRef = useRef<SandboxThreadSnapshot | null>(null);
|
||||
const sandboxSnapshotFrameRef = useRef<number | null>(null);
|
||||
const [flowDetailItem, setFlowDetailItem] = useState<ResultItem | null>(null);
|
||||
const [taskEditorOpen, setTaskEditorOpen] = useState(false);
|
||||
const [taskDraft, setTaskDraft] = useState<PositionTask>(fallbackTask);
|
||||
@@ -463,6 +468,41 @@ export default function PositionRoundtablePage({
|
||||
useEffect(() => {
|
||||
sandboxArtifactRef.current = sandboxArtifact;
|
||||
}, [sandboxArtifact]);
|
||||
const flushSandboxThreadSnapshot = useCallback(() => {
|
||||
sandboxSnapshotFrameRef.current = null;
|
||||
const pending = pendingSandboxThreadSnapshotRef.current;
|
||||
pendingSandboxThreadSnapshotRef.current = null;
|
||||
if (!pending) return;
|
||||
const currentArtifact = sandboxArtifactRef.current;
|
||||
if (!currentArtifact || currentArtifact.threadId !== pending.threadId) {
|
||||
return;
|
||||
}
|
||||
setSandboxThreadSnapshot((current) =>
|
||||
current?.threadId === pending.threadId && current.signature === pending.signature
|
||||
? current
|
||||
: pending,
|
||||
);
|
||||
}, []);
|
||||
const queueSandboxThreadSnapshot = useCallback(
|
||||
(snapshot: SandboxThreadSnapshot) => {
|
||||
pendingSandboxThreadSnapshotRef.current = snapshot;
|
||||
if (sandboxSnapshotFrameRef.current !== null) return;
|
||||
if (typeof window === "undefined") {
|
||||
flushSandboxThreadSnapshot();
|
||||
return;
|
||||
}
|
||||
sandboxSnapshotFrameRef.current = window.requestAnimationFrame(flushSandboxThreadSnapshot);
|
||||
},
|
||||
[flushSandboxThreadSnapshot],
|
||||
);
|
||||
useEffect(
|
||||
() => () => {
|
||||
if (sandboxSnapshotFrameRef.current !== null) {
|
||||
window.cancelAnimationFrame(sandboxSnapshotFrameRef.current);
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
const frozenIntent = useMemo(
|
||||
() => parseIntentSummary(positionSession?.intentSnapshot),
|
||||
[positionSession?.intentSnapshot],
|
||||
@@ -706,6 +746,14 @@ export default function PositionRoundtablePage({
|
||||
if (!activeNode) return null;
|
||||
return activeChain?.seats.find((seat) => seat.agent_id === activeNode.agentId)?.name ?? null;
|
||||
}, [activeChain?.seats, activeNode]);
|
||||
const currentPositionNodeOptions = useMemo(
|
||||
() =>
|
||||
currentPositionNodes.map((node) => ({
|
||||
node,
|
||||
agentName: activeChain?.seats.find((seat) => seat.agent_id === node.agentId)?.name ?? null,
|
||||
})),
|
||||
[activeChain?.seats, currentPositionNodes],
|
||||
);
|
||||
useEffect(() => {
|
||||
const nextNodeKey = activeNode?.nodeKey ?? null;
|
||||
if (activeNodeKey !== nextNodeKey) setActiveNodeKey(nextNodeKey);
|
||||
@@ -1060,6 +1108,9 @@ export default function PositionRoundtablePage({
|
||||
? current
|
||||
: artifact,
|
||||
);
|
||||
setSandboxThreadSnapshot((current) =>
|
||||
current?.threadId === artifact.threadId ? current : null,
|
||||
);
|
||||
|
||||
const ctx = artifactsCtxRef.current;
|
||||
if (ctx?.selectedArtifact !== artifact.path) {
|
||||
@@ -1069,8 +1120,12 @@ export default function PositionRoundtablePage({
|
||||
if (!ctx?.open) {
|
||||
ctx?.setOpen(true);
|
||||
}
|
||||
const latestSnapshot = latestNodeThreadSnapshotRef.current;
|
||||
if (latestSnapshot?.threadId === artifact.threadId) {
|
||||
queueSandboxThreadSnapshot(latestSnapshot);
|
||||
}
|
||||
setRightPanelOpen(false);
|
||||
}, []);
|
||||
}, [queueSandboxThreadSnapshot]);
|
||||
const handlePreviewFlowArtifact = useCallback(
|
||||
(item: ResultItem) => {
|
||||
const threadId = item.artifactThreadId || item.node?.threadId;
|
||||
@@ -1124,9 +1179,7 @@ export default function PositionRoundtablePage({
|
||||
const handleStartedIntentSend = useCallback(() => {
|
||||
toast.info("业务链已启动,任务意图已冻结;如需重新识别请新建会话");
|
||||
}, []);
|
||||
const sandboxOpen = Boolean(
|
||||
sandboxArtifact && artifactsCtx?.open && artifactsCtx.selectedArtifact,
|
||||
);
|
||||
const sandboxOpen = Boolean(sandboxArtifact && artifactsCtx);
|
||||
// ArtifactFileDetail is shared with the normal LangGraph chat surface and
|
||||
// therefore reads ThreadContext for streaming state. The position workspace
|
||||
// renders its sandbox as a sibling of PositionNodeChat, so it is outside the
|
||||
@@ -1154,17 +1207,22 @@ export default function PositionRoundtablePage({
|
||||
: sandboxThreadStub,
|
||||
[sandboxArtifact, sandboxThreadSnapshot, sandboxThreadStub],
|
||||
);
|
||||
const sandboxThreadContextValue = useMemo(
|
||||
() => ({ thread: sandboxThread, isMock: false }),
|
||||
[sandboxThread],
|
||||
);
|
||||
const handleNodeThreadSnapshot = useCallback(
|
||||
(snapshot: { threadId: string; thread: BaseStream<AgentThreadState> }) => {
|
||||
const signature = sandboxThreadSignature(snapshot.thread);
|
||||
setSandboxThreadSnapshot((current) => {
|
||||
const sameThreadData =
|
||||
current?.threadId === snapshot.threadId &&
|
||||
current.signature === signature;
|
||||
return sameThreadData ? current : { ...snapshot, signature };
|
||||
});
|
||||
const nextSnapshot: SandboxThreadSnapshot = { ...snapshot, signature };
|
||||
latestNodeThreadSnapshotRef.current = nextSnapshot;
|
||||
const currentArtifact = sandboxArtifactRef.current;
|
||||
if (!currentArtifact || currentArtifact.threadId !== snapshot.threadId) {
|
||||
return;
|
||||
}
|
||||
queueSandboxThreadSnapshot(nextSnapshot);
|
||||
},
|
||||
[],
|
||||
[queueSandboxThreadSnapshot],
|
||||
);
|
||||
const closeSandbox = useCallback(() => {
|
||||
const current = sandboxArtifactRef.current;
|
||||
@@ -1175,9 +1233,18 @@ export default function PositionRoundtablePage({
|
||||
if (current || ctx?.open) {
|
||||
ctx?.setOpen(false);
|
||||
}
|
||||
pendingSandboxThreadSnapshotRef.current = null;
|
||||
setSandboxArtifact(null);
|
||||
setSandboxThreadSnapshot(null);
|
||||
}, []);
|
||||
const handleSelectPositionNode = useCallback(
|
||||
(nextNode: PositionRoundtableNode) => {
|
||||
closeSandbox();
|
||||
setActiveNodeKey(nextNode.nodeKey);
|
||||
setSelectedAgentId(nextNode.agentId);
|
||||
},
|
||||
[closeSandbox],
|
||||
);
|
||||
const renderIntentChatPanel = (chainStarted: boolean) => (
|
||||
<div className="contents">
|
||||
{intent.intentInitError && (
|
||||
@@ -1414,6 +1481,8 @@ export default function PositionRoundtablePage({
|
||||
currentPositionId={positionId}
|
||||
sessionStatus={positionSession.status}
|
||||
agentName={activeNodeAgentName}
|
||||
positionNodeOptions={currentPositionNodeOptions}
|
||||
onSelectPositionNode={handleSelectPositionNode}
|
||||
onNodeUpdate={handleNodeUpdate}
|
||||
onConversationStart={() => setRightPanelOpen(false)}
|
||||
onPreviewArtifact={handlePreviewArtifact}
|
||||
@@ -1518,7 +1587,7 @@ export default function PositionRoundtablePage({
|
||||
</div>
|
||||
</TDrawer>
|
||||
)}
|
||||
{sandboxOpen && sandboxArtifact && artifactsCtx?.selectedArtifact && (
|
||||
{sandboxOpen && sandboxArtifact && artifactsCtx && (
|
||||
<aside className="flex w-[min(48vw,720px)] min-w-[440px] shrink-0 flex-col overflow-hidden rounded-2xl border border-slate-200 bg-white shadow-sm dark:border-[#2b8ffe]/45 dark:bg-[#032650]/80 dark:shadow-none">
|
||||
<header className="hidden h-12 shrink-0 items-center justify-between gap-3 border-b border-slate-200 px-4 dark:border-[#2b8ffe]/25">
|
||||
<div className="min-w-0">
|
||||
@@ -1537,7 +1606,7 @@ export default function PositionRoundtablePage({
|
||||
title="关闭沙箱"
|
||||
/>
|
||||
</header>
|
||||
<ThreadContext.Provider value={{ thread: sandboxThread, isMock: false }}>
|
||||
<ThreadContext.Provider value={sandboxThreadContextValue}>
|
||||
<ArtifactFileDetail
|
||||
className="min-h-0 flex-1"
|
||||
filepath={sandboxArtifact.path}
|
||||
@@ -3901,7 +3970,6 @@ function DeliverableDetailDialog({
|
||||
item,
|
||||
onOpenChange,
|
||||
onReject,
|
||||
onPreviewArtifact,
|
||||
onAskBuiltin,
|
||||
askSubmitting,
|
||||
showReject = true,
|
||||
@@ -3920,6 +3988,32 @@ function DeliverableDetailDialog({
|
||||
const status = item?.status ? nodeStatusMeta(item.status) : null;
|
||||
const StatusIcon = status?.Icon;
|
||||
const canReject = showReject && item?.node ? canRejectNodeDelivery(item.node) : false;
|
||||
const artifactThreadId = item?.artifactThreadId || item?.node?.threadId || null;
|
||||
const artifactPathValue = item?.artifactPath || "";
|
||||
const artifactContentQuery = useQuery({
|
||||
queryKey: [
|
||||
"position-roundtable",
|
||||
"deliverable-artifact-content",
|
||||
artifactThreadId,
|
||||
artifactPathValue,
|
||||
],
|
||||
queryFn: async () => {
|
||||
if (!artifactThreadId || !artifactPathValue) return "";
|
||||
const result = await loadArtifactContent({
|
||||
filepath: artifactPathValue,
|
||||
threadId: artifactThreadId,
|
||||
isMock: false,
|
||||
});
|
||||
return result.content;
|
||||
},
|
||||
enabled: Boolean(item && artifactThreadId && artifactPathValue),
|
||||
staleTime: 30_000,
|
||||
});
|
||||
const artifactContent =
|
||||
typeof artifactContentQuery.data === "string" && artifactContentQuery.data.trim()
|
||||
? artifactContentQuery.data
|
||||
: null;
|
||||
const displayedContent = artifactContent || item?.summary || "暂无产物正文";
|
||||
const builtinKind =
|
||||
showBuiltinAsk && onAskBuiltin && (item?.kind === "summary" || item?.kind === "action_plan")
|
||||
? item.kind
|
||||
@@ -3938,7 +4032,7 @@ function DeliverableDetailDialog({
|
||||
|
||||
return (
|
||||
<Dialog open={Boolean(item)} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="flex max-h-[82vh] w-[720px] !max-w-[calc(100vw-2rem)] flex-col gap-0 overflow-hidden p-0">
|
||||
<DialogContent className="flex max-h-[86vh] w-[900px] !max-w-[calc(100vw-2rem)] flex-col gap-0 overflow-hidden p-0">
|
||||
<DialogHeader className="shrink-0 border-b px-5 py-4 dark:border-[#2b8ffe]/25">
|
||||
<DialogTitle className="flex items-center gap-2 text-base">
|
||||
<FileOutput className="size-4 text-primary" />
|
||||
@@ -4006,20 +4100,26 @@ function DeliverableDetailDialog({
|
||||
<p className="text-xs font-bold text-slate-700 dark:text-[#e8edf5]">
|
||||
{item.kind === "artifact" ? "文件产物" : "产物内容"}
|
||||
</p>
|
||||
{item.artifactPath && (
|
||||
<TButton
|
||||
size="small"
|
||||
variant="outline"
|
||||
icon={<FileText className="size-3.5" />}
|
||||
onClick={() => onPreviewArtifact(item)}
|
||||
>
|
||||
查看文件
|
||||
</TButton>
|
||||
</div>
|
||||
<div className="h-[52vh] min-h-[320px] overflow-hidden rounded-lg border border-slate-100 bg-slate-50/60 dark:border-[#2b8ffe]/20 dark:bg-[#021d3d]/70">
|
||||
{artifactContentQuery.isLoading ? (
|
||||
<div className="flex h-full items-center justify-center gap-2 text-sm text-muted-foreground dark:text-[#8aa0c4]">
|
||||
<Loader2 className="size-4 animate-spin" />
|
||||
正在加载 Markdown 文件内容…
|
||||
</div>
|
||||
) : (
|
||||
<ArtifactFilePreview
|
||||
content={displayedContent}
|
||||
language="markdown"
|
||||
threadId={artifactThreadId ?? ""}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<p className="whitespace-pre-wrap text-xs leading-6 text-slate-600 dark:text-[#c5cfe0]">
|
||||
{desensitize(item.summary || "暂无产物正文")}
|
||||
</p>
|
||||
{artifactContentQuery.isError && (
|
||||
<p className="mt-2 text-[11px] text-amber-600 dark:text-amber-300">
|
||||
Markdown 文件内容加载失败,当前展示的是节点摘要。
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
{builtinKind && item.status === "done" && (
|
||||
<div className="mt-4 rounded-xl border border-primary/20 bg-primary/[0.035] p-3 dark:border-[#2b8ffe]/35 dark:bg-[#032650]/75">
|
||||
|
||||
Reference in new issue
Block a user