打开沙箱错误修改
This commit is contained in:
1 parent
ae184f434d
commit
d9844e0180
2 files changed
+267
-47
No files matched your search
@@ -1,3 +1,4 @@
|
||||
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";
|
||||
@@ -9,6 +10,7 @@ import { useArtifactsOptional } from "@/components/workspace/artifacts";
|
||||
import { useAgent } from "@/core/agents";
|
||||
import { useLocalSettings } from "@/core/settings";
|
||||
import { useThreadStream } from "@/core/threads/hooks";
|
||||
import type { AgentThreadState } from "@/core/threads/types";
|
||||
import { textOfMessage } from "@/core/threads/utils";
|
||||
import { uuid } from "@/core/utils/uuid";
|
||||
import {
|
||||
@@ -37,6 +39,11 @@ interface PositionNodeChatProps {
|
||||
path: string;
|
||||
name: string;
|
||||
mimeType: string | null;
|
||||
auto?: boolean;
|
||||
}) => void;
|
||||
onThreadSnapshot?: (snapshot: {
|
||||
threadId: string;
|
||||
thread: BaseStream<AgentThreadState>;
|
||||
}) => void;
|
||||
}
|
||||
|
||||
@@ -84,6 +91,53 @@ function hasMarkdownArtifact(manifest: Array<Record<string, unknown>>): boolean
|
||||
});
|
||||
}
|
||||
|
||||
function artifactDisplayName(pathOrUrl: string): string {
|
||||
let path = pathOrUrl;
|
||||
if (pathOrUrl.startsWith("write-file:")) {
|
||||
try {
|
||||
path = decodeURIComponent(new URL(pathOrUrl).pathname);
|
||||
} catch {
|
||||
path = pathOrUrl;
|
||||
}
|
||||
}
|
||||
return path.split("/").filter(Boolean).at(-1) || "Markdown 报告";
|
||||
}
|
||||
|
||||
function buildWriteFileVirtualUrl(path: string, messageId: string, toolCallId: string): string {
|
||||
return new URL(
|
||||
`write-file:${path}?message_id=${encodeURIComponent(
|
||||
messageId,
|
||||
)}&tool_call_id=${encodeURIComponent(toolCallId)}`,
|
||||
).toString();
|
||||
}
|
||||
|
||||
function latestStreamingWriteFile(
|
||||
messages: AgentThreadState["messages"],
|
||||
): { path: string; name: string; mimeType: string | null } | null {
|
||||
for (let messageIndex = messages.length - 1; messageIndex >= 0; messageIndex -= 1) {
|
||||
const message = messages[messageIndex] as (typeof messages)[number] & {
|
||||
id?: string;
|
||||
tool_calls?: Array<{ id?: string; name?: string; args?: Record<string, unknown> }>;
|
||||
};
|
||||
if (message.type !== "ai" || !message.id || !Array.isArray(message.tool_calls)) continue;
|
||||
for (let toolIndex = message.tool_calls.length - 1; toolIndex >= 0; toolIndex -= 1) {
|
||||
const toolCall = message.tool_calls[toolIndex];
|
||||
if (toolCall?.name !== "write_file" || !toolCall.id) continue;
|
||||
const rawPath = toolCall.args?.path;
|
||||
if (typeof rawPath !== "string" || !rawPath.trim()) continue;
|
||||
const content = toolCall.args?.content;
|
||||
if (typeof content !== "string" || content.length === 0) continue;
|
||||
const virtualPath = buildWriteFileVirtualUrl(rawPath, message.id, toolCall.id);
|
||||
return {
|
||||
path: virtualPath,
|
||||
name: artifactDisplayName(rawPath),
|
||||
mimeType: rawPath.toLowerCase().endsWith(".md") ? "text/markdown" : null,
|
||||
};
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Direct single-agent chat for one frozen business-chain node.
|
||||
*
|
||||
@@ -102,6 +156,7 @@ export function PositionNodeChat({
|
||||
onNodeUpdate,
|
||||
onConversationStart,
|
||||
onPreviewArtifact,
|
||||
onThreadSnapshot,
|
||||
}: PositionNodeChatProps) {
|
||||
const [settings, setSettings] = useLocalSettings();
|
||||
const { agent } = useAgent(node.agentId);
|
||||
@@ -111,6 +166,12 @@ export function PositionNodeChat({
|
||||
const boundThreadIdRef = useRef<string | null>(node.threadId);
|
||||
const draftThreadIdRef = useRef(node.threadId ?? uuid());
|
||||
const upstreamRevisionRef = useRef<Record<string, unknown> | null>(null);
|
||||
// The parent opens the outer sandbox by updating the shared
|
||||
// ArtifactsProvider. Forward that provider selection only once per file and
|
||||
// thread: opening the sandbox itself changes the provider value, which used
|
||||
// to re-run this effect and re-open the same sandbox indefinitely.
|
||||
const forwardedArtifactKeyRef = useRef<string | null>(null);
|
||||
const autoPreviewedArtifactKeyRef = useRef<string | null>(null);
|
||||
const nodeArtifacts = useMemo(() => readNodeArtifacts(node.artifactManifest), [node.artifactManifest]);
|
||||
const canWrite =
|
||||
sessionStatus !== "archived" &&
|
||||
@@ -120,16 +181,28 @@ export function PositionNodeChat({
|
||||
!node.invalidatedBy;
|
||||
|
||||
useEffect(() => {
|
||||
if (artifactsCtx?.open && artifactsCtx.selectedArtifact) {
|
||||
onPreviewArtifact?.({
|
||||
threadId: boundThreadIdRef.current ?? boundThreadId ?? draftThreadIdRef.current,
|
||||
path: artifactsCtx.selectedArtifact,
|
||||
name: artifactsCtx.selectedArtifact.split("/").filter(Boolean).at(-1) || "Markdown 报告",
|
||||
mimeType: artifactsCtx.selectedArtifact.toLowerCase().endsWith(".md")
|
||||
? "text/markdown"
|
||||
: null,
|
||||
});
|
||||
const selectedArtifact = artifactsCtx?.open
|
||||
? artifactsCtx.selectedArtifact
|
||||
: null;
|
||||
if (!selectedArtifact) {
|
||||
forwardedArtifactKeyRef.current = null;
|
||||
return;
|
||||
}
|
||||
|
||||
const threadId =
|
||||
boundThreadIdRef.current ?? boundThreadId ?? draftThreadIdRef.current;
|
||||
const artifactKey = `${threadId}:${selectedArtifact}`;
|
||||
if (forwardedArtifactKeyRef.current === artifactKey) return;
|
||||
forwardedArtifactKeyRef.current = artifactKey;
|
||||
|
||||
onPreviewArtifact?.({
|
||||
threadId,
|
||||
path: selectedArtifact,
|
||||
name: selectedArtifact.split("/").filter(Boolean).at(-1) || "Markdown 报告",
|
||||
mimeType: selectedArtifact.toLowerCase().endsWith(".md")
|
||||
? "text/markdown"
|
||||
: null,
|
||||
});
|
||||
}, [artifactsCtx?.open, artifactsCtx?.selectedArtifact, boundThreadId, onPreviewArtifact]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -209,6 +282,32 @@ export function PositionNodeChat({
|
||||
},
|
||||
});
|
||||
|
||||
const activeThreadId =
|
||||
boundThreadIdRef.current ?? actualThreadId ?? boundThreadId ?? draftThreadIdRef.current;
|
||||
|
||||
useEffect(() => {
|
||||
onThreadSnapshot?.({ threadId: activeThreadId, thread });
|
||||
}, [activeThreadId, onThreadSnapshot, thread]);
|
||||
|
||||
const streamingArtifact = useMemo(
|
||||
() => (thread.isLoading ? latestStreamingWriteFile(thread.messages) : null),
|
||||
[thread.isLoading, thread.messages],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!thread.isLoading || !streamingArtifact) return;
|
||||
const artifactKey = `${activeThreadId}:${streamingArtifact.path}`;
|
||||
if (autoPreviewedArtifactKeyRef.current === artifactKey) return;
|
||||
autoPreviewedArtifactKeyRef.current = artifactKey;
|
||||
onPreviewArtifact?.({
|
||||
threadId: activeThreadId,
|
||||
path: streamingArtifact.path,
|
||||
name: streamingArtifact.name,
|
||||
mimeType: streamingArtifact.mimeType,
|
||||
auto: true,
|
||||
});
|
||||
}, [activeThreadId, onPreviewArtifact, streamingArtifact, thread.isLoading]);
|
||||
|
||||
// SDK thread creation is asynchronous. ``onStart`` normally binds it, but
|
||||
// watching the actual stream id as well closes the last race where a page
|
||||
// refresh could happen between creation and the async node-index write.
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { BaseStream } from "@langchain/langgraph-sdk/react";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useNavigate, useSearchParams } from "react-router-dom";
|
||||
@@ -40,6 +41,7 @@ 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 { ThreadContext } from "@/components/workspace/messages/context";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -112,8 +114,37 @@ type PositionArtifactPreview = {
|
||||
path: string;
|
||||
name: string;
|
||||
mimeType: string | null;
|
||||
auto?: boolean;
|
||||
};
|
||||
|
||||
function sandboxThreadSignature(thread: BaseStream<AgentThreadState>): string {
|
||||
const messages = thread.messages ?? [];
|
||||
for (let messageIndex = messages.length - 1; messageIndex >= 0; messageIndex -= 1) {
|
||||
const message = messages[messageIndex] as (typeof messages)[number] & {
|
||||
id?: string;
|
||||
tool_calls?: Array<{ id?: string; name?: string; args?: Record<string, unknown> }>;
|
||||
};
|
||||
if (message.type !== "ai" || !Array.isArray(message.tool_calls)) continue;
|
||||
for (let toolIndex = message.tool_calls.length - 1; toolIndex >= 0; toolIndex -= 1) {
|
||||
const toolCall = message.tool_calls[toolIndex];
|
||||
if (toolCall?.name !== "write_file") continue;
|
||||
const path = typeof toolCall.args?.path === "string" ? toolCall.args.path : "";
|
||||
const content = typeof toolCall.args?.content === "string" ? toolCall.args.content : "";
|
||||
return [
|
||||
thread.isLoading ? "1" : "0",
|
||||
messages.length,
|
||||
message.id ?? "",
|
||||
toolCall.id ?? "",
|
||||
path,
|
||||
content.length,
|
||||
content.slice(-32),
|
||||
].join("|");
|
||||
}
|
||||
}
|
||||
const lastMessage = messages.at(-1) as (typeof messages)[number] & { id?: string } | undefined;
|
||||
return [thread.isLoading ? "1" : "0", messages.length, lastMessage?.id ?? ""].join("|");
|
||||
}
|
||||
|
||||
function uniqueChains(chains: BusinessChain[]): BusinessChain[] {
|
||||
const seen = new Set<string>();
|
||||
return chains.filter((chain) => {
|
||||
@@ -389,8 +420,22 @@ export default function PositionRoundtablePage({
|
||||
const [rightPanelOpen, setRightPanelOpen] = useState(true);
|
||||
const [rightTab, setRightTab] = useState<WorkspaceTab>("agents");
|
||||
const artifactsCtx = useArtifactsOptional();
|
||||
const artifactsCtxRef = useRef(artifactsCtx);
|
||||
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 activePosition = POSITIONS.find((item) => item.id === positionId)!;
|
||||
useEffect(() => {
|
||||
artifactsCtxRef.current = artifactsCtx;
|
||||
}, [artifactsCtx]);
|
||||
useEffect(() => {
|
||||
sandboxArtifactRef.current = sandboxArtifact;
|
||||
}, [sandboxArtifact]);
|
||||
const frozenIntent = useMemo(
|
||||
() => parseIntentSummary(positionSession?.intentSnapshot),
|
||||
[positionSession?.intentSnapshot],
|
||||
@@ -784,6 +829,13 @@ export default function PositionRoundtablePage({
|
||||
}, [intent.intentReady, intent.intentThreadId, positionSession, searchParams, selectedChain, setSearchParams, startingChain, task]);
|
||||
|
||||
const handleCreateNewSession = useCallback(() => {
|
||||
const ctx = artifactsCtxRef.current;
|
||||
if (sandboxArtifactRef.current || ctx?.open) {
|
||||
ctx?.setOpen(false);
|
||||
}
|
||||
closedSandboxKeyRef.current = null;
|
||||
setSandboxArtifact(null);
|
||||
setSandboxThreadSnapshot(null);
|
||||
intent.resetIntent();
|
||||
hydratedIntentSessionRef.current = null;
|
||||
persistedIntentRef.current = null;
|
||||
@@ -812,29 +864,88 @@ export default function PositionRoundtablePage({
|
||||
});
|
||||
}, [positionSession]);
|
||||
|
||||
const handlePreviewArtifact = useCallback(
|
||||
(artifact: PositionArtifactPreview) => {
|
||||
if (!artifact.threadId || !artifact.path) {
|
||||
toast.warning("该文件缺少沙箱预览所需的信息");
|
||||
return;
|
||||
}
|
||||
// 与普通智能体问答完全使用同一套 ArtifactsProvider:选中文件、展开
|
||||
// 文档面板,并让 ArtifactFileDetail 以所属 thread 读取沙箱文件。
|
||||
setSandboxArtifact(artifact);
|
||||
artifactsCtx?.setArtifacts([artifact.path]);
|
||||
artifactsCtx?.select(artifact.path);
|
||||
artifactsCtx?.setOpen(true);
|
||||
setRightPanelOpen(false);
|
||||
},
|
||||
[artifactsCtx],
|
||||
);
|
||||
const handlePreviewArtifact = useCallback((artifact: PositionArtifactPreview) => {
|
||||
if (!artifact.threadId || !artifact.path) {
|
||||
toast.warning("该文件缺少沙箱预览所需的信息");
|
||||
return;
|
||||
}
|
||||
|
||||
const artifactKey = `${artifact.threadId}:${artifact.path}`;
|
||||
if (artifact.auto && closedSandboxKeyRef.current === artifactKey) {
|
||||
return;
|
||||
}
|
||||
closedSandboxKeyRef.current = null;
|
||||
|
||||
setSandboxArtifact((current) =>
|
||||
current?.threadId === artifact.threadId && current.path === artifact.path
|
||||
? current
|
||||
: artifact,
|
||||
);
|
||||
|
||||
const ctx = artifactsCtxRef.current;
|
||||
if (ctx?.selectedArtifact !== artifact.path) {
|
||||
ctx?.setArtifacts([artifact.path]);
|
||||
ctx?.select(artifact.path, artifact.auto ?? false);
|
||||
}
|
||||
if (!ctx?.open) {
|
||||
ctx?.setOpen(true);
|
||||
}
|
||||
setRightPanelOpen(false);
|
||||
}, []);
|
||||
const sandboxOpen = Boolean(
|
||||
sandboxArtifact && artifactsCtx?.open && artifactsCtx.selectedArtifact,
|
||||
);
|
||||
// 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
|
||||
// node's provider. Artifact files here always load through the artifacts API;
|
||||
// provide the smallest stable stream shape needed by the preview component.
|
||||
const sandboxThreadStub = useMemo(
|
||||
() =>
|
||||
({
|
||||
messages: [],
|
||||
isLoading: false,
|
||||
isThreadLoading: false,
|
||||
values: {
|
||||
title: "",
|
||||
messages: [],
|
||||
artifacts: artifactsCtx?.artifacts ?? [],
|
||||
},
|
||||
}) as unknown as BaseStream<AgentThreadState>,
|
||||
[artifactsCtx?.artifacts],
|
||||
);
|
||||
const sandboxThread = useMemo(
|
||||
() =>
|
||||
sandboxArtifact &&
|
||||
sandboxThreadSnapshot?.threadId === sandboxArtifact.threadId
|
||||
? sandboxThreadSnapshot.thread
|
||||
: sandboxThreadStub,
|
||||
[sandboxArtifact, sandboxThreadSnapshot, sandboxThreadStub],
|
||||
);
|
||||
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 closeSandbox = useCallback(() => {
|
||||
artifactsCtx?.setOpen(false);
|
||||
const current = sandboxArtifactRef.current;
|
||||
if (current) {
|
||||
closedSandboxKeyRef.current = `${current.threadId}:${current.path}`;
|
||||
}
|
||||
const ctx = artifactsCtxRef.current;
|
||||
if (current || ctx?.open) {
|
||||
ctx?.setOpen(false);
|
||||
}
|
||||
setSandboxArtifact(null);
|
||||
}, [artifactsCtx]);
|
||||
setSandboxThreadSnapshot(null);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -911,6 +1022,7 @@ export default function PositionRoundtablePage({
|
||||
// 其他岗位改用按需打开的抽屉,避免一进岗位就挤占对话空间。
|
||||
const nextPositionId = String(value) as PositionId;
|
||||
// 中间区由 activeNode 的兜底规则自动定位到首个流程节点。
|
||||
closeSandbox();
|
||||
setPositionId(nextPositionId);
|
||||
setSelectedAgentId(null);
|
||||
setActiveNodeKey(null);
|
||||
@@ -918,10 +1030,10 @@ export default function PositionRoundtablePage({
|
||||
}}
|
||||
/>
|
||||
{compactChrome && (
|
||||
<>
|
||||
<div className="contents">
|
||||
<ThemeSwitcher tone="content" />
|
||||
<LayoutModeSwitcher tone="content" />
|
||||
</>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</header>
|
||||
@@ -943,7 +1055,7 @@ export default function PositionRoundtablePage({
|
||||
/>
|
||||
)
|
||||
) : (
|
||||
<>
|
||||
<div className="contents">
|
||||
<IntentUnderstandingCard intent={intent.intentReady} />
|
||||
<BusinessChainFlow
|
||||
chain={activeChain}
|
||||
@@ -951,6 +1063,7 @@ export default function PositionRoundtablePage({
|
||||
session={positionSession}
|
||||
activeNodeKey={activeNodeKey}
|
||||
onSelectNode={(node) => {
|
||||
closeSandbox();
|
||||
setActiveNodeKey(node.nodeKey);
|
||||
setSelectedAgentId(node.agentId);
|
||||
}}
|
||||
@@ -959,7 +1072,7 @@ export default function PositionRoundtablePage({
|
||||
setRightTab("results");
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
</div>
|
||||
)}
|
||||
</aside>
|
||||
|
||||
@@ -988,7 +1101,7 @@ export default function PositionRoundtablePage({
|
||||
positionSession?.status !== "intent_pending" && frozenIntent ? (
|
||||
<IntentConfirmedState intent={frozenIntent} />
|
||||
) : (
|
||||
<>
|
||||
<div className="contents">
|
||||
{intent.intentInitError && (
|
||||
<div className="flex shrink-0 items-center justify-between gap-3 border-b border-amber-200 bg-amber-50 px-4 py-2 text-xs text-amber-800 dark:border-amber-900/60 dark:bg-amber-950/30 dark:text-amber-200">
|
||||
<span className="line-clamp-2">对话初始化失败:{intent.intentInitError}</span>
|
||||
@@ -1035,7 +1148,7 @@ export default function PositionRoundtablePage({
|
||||
{startingChain ? "正在保存意图并生成岗位节点…" : "正在恢复岗位协同会话…"}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
</div>
|
||||
)
|
||||
) : positionSession && activeNode ? (
|
||||
<PositionNodeChat
|
||||
@@ -1050,6 +1163,7 @@ export default function PositionRoundtablePage({
|
||||
onNodeUpdate={handleNodeUpdate}
|
||||
onConversationStart={() => setRightPanelOpen(false)}
|
||||
onPreviewArtifact={handlePreviewArtifact}
|
||||
onThreadSnapshot={handleNodeThreadSnapshot}
|
||||
/>
|
||||
) : (
|
||||
<PositionNodeEmptyState
|
||||
@@ -1068,6 +1182,7 @@ export default function PositionRoundtablePage({
|
||||
session={positionSession}
|
||||
activeNodeKey={activeNodeKey}
|
||||
onSelectNode={(node) => {
|
||||
closeSandbox();
|
||||
setActiveNodeKey(node.nodeKey);
|
||||
setSelectedAgentId(node.agentId);
|
||||
}}
|
||||
@@ -1096,6 +1211,7 @@ export default function PositionRoundtablePage({
|
||||
nodes={positionSession?.nodes ?? []}
|
||||
chain={activeChain}
|
||||
onSelectNode={(node) => {
|
||||
closeSandbox();
|
||||
setActiveNodeKey(node.nodeKey);
|
||||
setSelectedAgentId(node.agentId);
|
||||
}}
|
||||
@@ -1138,6 +1254,7 @@ export default function PositionRoundtablePage({
|
||||
nodes={positionSession?.nodes ?? []}
|
||||
chain={activeChain}
|
||||
onSelectNode={(node) => {
|
||||
closeSandbox();
|
||||
setActiveNodeKey(node.nodeKey);
|
||||
setSelectedAgentId(node.agentId);
|
||||
}}
|
||||
@@ -1153,7 +1270,7 @@ export default function PositionRoundtablePage({
|
||||
)}
|
||||
{sandboxOpen && sandboxArtifact && artifactsCtx?.selectedArtifact && (
|
||||
<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="flex h-12 shrink-0 items-center justify-between gap-3 border-b border-slate-200 px-4 dark:border-[#2b8ffe]/25">
|
||||
<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">
|
||||
<p className="truncate text-sm font-bold text-slate-800 dark:text-[#e8edf5]">
|
||||
{desensitize(sandboxArtifact.name)}
|
||||
@@ -1170,11 +1287,15 @@ export default function PositionRoundtablePage({
|
||||
title="关闭沙箱"
|
||||
/>
|
||||
</header>
|
||||
<ArtifactFileDetail
|
||||
className="min-h-0 flex-1"
|
||||
filepath={artifactsCtx.selectedArtifact}
|
||||
threadId={sandboxArtifact.threadId}
|
||||
/>
|
||||
<ThreadContext.Provider value={{ thread: sandboxThread, isMock: false }}>
|
||||
<ArtifactFileDetail
|
||||
className="min-h-0 flex-1"
|
||||
filepath={sandboxArtifact.path}
|
||||
threadId={sandboxArtifact.threadId}
|
||||
onClose={closeSandbox}
|
||||
disableDesensitize
|
||||
/>
|
||||
</ThreadContext.Provider>
|
||||
</aside>
|
||||
)}
|
||||
</div>
|
||||
@@ -1890,7 +2011,7 @@ function BusinessChainFlow({
|
||||
请先由情报分析岗选择业务链条
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="contents">
|
||||
<ChainFlowCanvas
|
||||
chain={chain}
|
||||
nodes={nodes}
|
||||
@@ -1928,7 +2049,7 @@ function BusinessChainFlow({
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
@@ -2911,7 +3032,7 @@ function AnalysisResults({
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="contents">
|
||||
<div className="space-y-3 rounded-xl border border-slate-200 p-3.5 dark:border-border">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
@@ -3009,7 +3130,7 @@ function AnalysisResults({
|
||||
const status = item.status ? nodeStatusMeta(item.status) : null;
|
||||
const StatusIcon = status?.Icon;
|
||||
const contents = (
|
||||
<>
|
||||
<div className="contents">
|
||||
<div className="flex items-start gap-3">
|
||||
<span
|
||||
className={cn(
|
||||
@@ -3064,7 +3185,7 @@ function AnalysisResults({
|
||||
{displayedTime}
|
||||
</span>
|
||||
</footer>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
return item.node ? (
|
||||
<button
|
||||
@@ -3132,7 +3253,7 @@ function AnalysisResults({
|
||||
onReasonChange={setRejectReason}
|
||||
onSubmit={() => void handleRejectSubmit()}
|
||||
/>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3210,14 +3331,14 @@ function DeliverableDetailDialog({
|
||||
时间:{displayedTime || "暂无"}
|
||||
</div>
|
||||
{item.node && (
|
||||
<>
|
||||
<div className="contents">
|
||||
<div className="rounded-lg bg-slate-50 px-3 py-2 dark:bg-[#032650]">
|
||||
版本:第 {item.node.revision} 版
|
||||
</div>
|
||||
<div className="rounded-lg bg-slate-50 px-3 py-2 dark:bg-[#032650]">
|
||||
驳回次数:{item.node.rejectionCount}
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
Reference in new issue
Block a user