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 { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { Bot, FileText, LockKeyhole } from "lucide-react";
|
import { Bot, FileText, LockKeyhole } from "lucide-react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
import { Select as TSelect } from "tdesign-react";
|
||||||
import type { PromptInputMessage } from "@/components/ai-elements/prompt-input";
|
import type { PromptInputMessage } from "@/components/ai-elements/prompt-input";
|
||||||
import { InputBox } from "@/components/workspace/input-box";
|
import { InputBox } from "@/components/workspace/input-box";
|
||||||
import { ThreadContext } from "@/components/workspace/messages/context";
|
import { ThreadContext } from "@/components/workspace/messages/context";
|
||||||
@@ -22,6 +23,11 @@ import {
|
|||||||
import { listSessionArtifacts } from "@/roundtable-planning/api/multi-agent";
|
import { listSessionArtifacts } from "@/roundtable-planning/api/multi-agent";
|
||||||
import { desensitize } from "@/lib/desensitize";
|
import { desensitize } from "@/lib/desensitize";
|
||||||
|
|
||||||
|
interface PositionNodeSwitchOption {
|
||||||
|
node: PositionRoundtableNode;
|
||||||
|
agentName?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
interface PositionNodeChatProps {
|
interface PositionNodeChatProps {
|
||||||
sessionId: string;
|
sessionId: string;
|
||||||
/** Parent task id: all node threads are indexed under this task. */
|
/** Parent task id: all node threads are indexed under this task. */
|
||||||
@@ -31,6 +37,8 @@ interface PositionNodeChatProps {
|
|||||||
currentPositionId: string;
|
currentPositionId: string;
|
||||||
sessionStatus: "intent_pending" | "active" | "completed" | "archived";
|
sessionStatus: "intent_pending" | "active" | "completed" | "archived";
|
||||||
agentName?: string | null;
|
agentName?: string | null;
|
||||||
|
positionNodeOptions?: PositionNodeSwitchOption[];
|
||||||
|
onSelectPositionNode?: (node: PositionRoundtableNode) => void;
|
||||||
onNodeUpdate: (node: PositionRoundtableNode) => void;
|
onNodeUpdate: (node: PositionRoundtableNode) => void;
|
||||||
/** The workspace collapses the configuration panel on the first real turn. */
|
/** The workspace collapses the configuration panel on the first real turn. */
|
||||||
onConversationStart?: () => void;
|
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 {
|
function artifactDisplayName(pathOrUrl: string): string {
|
||||||
let path = pathOrUrl;
|
let path = pathOrUrl;
|
||||||
if (pathOrUrl.startsWith("write-file:")) {
|
if (pathOrUrl.startsWith("write-file:")) {
|
||||||
@@ -153,6 +191,8 @@ export function PositionNodeChat({
|
|||||||
currentPositionId,
|
currentPositionId,
|
||||||
sessionStatus,
|
sessionStatus,
|
||||||
agentName,
|
agentName,
|
||||||
|
positionNodeOptions,
|
||||||
|
onSelectPositionNode,
|
||||||
onNodeUpdate,
|
onNodeUpdate,
|
||||||
onConversationStart,
|
onConversationStart,
|
||||||
onPreviewArtifact,
|
onPreviewArtifact,
|
||||||
@@ -161,6 +201,13 @@ export function PositionNodeChat({
|
|||||||
const [settings, setSettings] = useLocalSettings();
|
const [settings, setSettings] = useLocalSettings();
|
||||||
const { agent } = useAgent(node.agentId);
|
const { agent } = useAgent(node.agentId);
|
||||||
const agentDisplayName = desensitize(agentName || agent?.name || 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 artifactsCtx = useArtifactsOptional();
|
||||||
const [boundThreadId, setBoundThreadId] = useState<string | null>(node.threadId);
|
const [boundThreadId, setBoundThreadId] = useState<string | null>(node.threadId);
|
||||||
const boundThreadIdRef = useRef<string | null>(node.threadId);
|
const boundThreadIdRef = useRef<string | null>(node.threadId);
|
||||||
@@ -284,6 +331,35 @@ export function PositionNodeChat({
|
|||||||
|
|
||||||
const activeThreadId =
|
const activeThreadId =
|
||||||
boundThreadIdRef.current ?? actualThreadId ?? boundThreadId ?? draftThreadIdRef.current;
|
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(() => {
|
useEffect(() => {
|
||||||
onThreadSnapshot?.({ threadId: activeThreadId, thread });
|
onThreadSnapshot?.({ threadId: activeThreadId, thread });
|
||||||
@@ -360,6 +436,7 @@ export function PositionNodeChat({
|
|||||||
|
|
||||||
const status = thread.error ? "error" : thread.isLoading ? "streaming" : "ready";
|
const status = thread.error ? "error" : thread.isLoading ? "streaming" : "ready";
|
||||||
const displayThreadId = boundThreadId ?? `position-node-${node.id}`;
|
const displayThreadId = boundThreadId ?? `position-node-${node.id}`;
|
||||||
|
const threadContextValue = useMemo(() => ({ thread }), [thread]);
|
||||||
const readOnlyHint =
|
const readOnlyHint =
|
||||||
sessionStatus === "archived"
|
sessionStatus === "archived"
|
||||||
? "该会话已归档,仅可查看历史对话和产物"
|
? "该会话已归档,仅可查看历史对话和产物"
|
||||||
@@ -372,9 +449,9 @@ export function PositionNodeChat({
|
|||||||
: "";
|
: "";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ThreadContext.Provider value={{ thread }}>
|
<ThreadContext.Provider value={threadContextValue}>
|
||||||
<div className="flex min-h-0 flex-1 flex-col">
|
<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">
|
<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" />}
|
{node.status === "locked" ? <LockKeyhole className="size-3.5" /> : <Bot className="size-4" />}
|
||||||
</span>
|
</span>
|
||||||
@@ -386,6 +463,32 @@ export function PositionNodeChat({
|
|||||||
{readOnlyHint || `节点状态:${node.status}`}
|
{readOnlyHint || `节点状态:${node.status}`}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</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>
|
</header>
|
||||||
|
|
||||||
{nodeArtifacts.length > 0 && (
|
{nodeArtifacts.length > 0 && (
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ import { toast } from "sonner";
|
|||||||
import { agentColor } from "@/components/workspace/agent-icon-color";
|
import { agentColor } from "@/components/workspace/agent-icon-color";
|
||||||
import { AgentDetailPanel } from "@/components/workspace/agents/agent-detail-panel";
|
import { AgentDetailPanel } from "@/components/workspace/agents/agent-detail-panel";
|
||||||
import { useAgentModuleSidebarData } from "@/components/workspace/agents/agent-module-sidebar";
|
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 { ThreadContext } from "@/components/workspace/messages/context";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
@@ -55,6 +55,7 @@ import { useTheme } from "@/contexts/ThemeContext";
|
|||||||
import { LayoutModeSwitcher, ThemeSwitcher } from "@/components/workspace/appearance-controls";
|
import { LayoutModeSwitcher, ThemeSwitcher } from "@/components/workspace/appearance-controls";
|
||||||
import { getAPIClient } from "@/core/api/api-client";
|
import { getAPIClient } from "@/core/api/api-client";
|
||||||
import { useAgent } from "@/core/agents";
|
import { useAgent } from "@/core/agents";
|
||||||
|
import { loadArtifactContent } from "@/core/artifacts/loader";
|
||||||
import type { Agent } from "@/core/agents";
|
import type { Agent } from "@/core/agents";
|
||||||
import { useModels } from "@/core/models/hooks";
|
import { useModels } from "@/core/models/hooks";
|
||||||
import type { AgentThreadState } from "@/core/threads/types";
|
import type { AgentThreadState } from "@/core/threads/types";
|
||||||
@@ -123,6 +124,11 @@ type IntentEditDraft = {
|
|||||||
constraints: string;
|
constraints: string;
|
||||||
assumptions: string;
|
assumptions: string;
|
||||||
};
|
};
|
||||||
|
type SandboxThreadSnapshot = {
|
||||||
|
threadId: string;
|
||||||
|
thread: BaseStream<AgentThreadState>;
|
||||||
|
signature: string;
|
||||||
|
};
|
||||||
|
|
||||||
function sandboxThreadSignature(thread: BaseStream<AgentThreadState>): string {
|
function sandboxThreadSignature(thread: BaseStream<AgentThreadState>): string {
|
||||||
const messages = thread.messages ?? [];
|
const messages = thread.messages ?? [];
|
||||||
@@ -439,11 +445,10 @@ export default function PositionRoundtablePage({
|
|||||||
const [sandboxArtifact, setSandboxArtifact] = useState<PositionArtifactPreview | null>(null);
|
const [sandboxArtifact, setSandboxArtifact] = useState<PositionArtifactPreview | null>(null);
|
||||||
const sandboxArtifactRef = useRef<PositionArtifactPreview | null>(null);
|
const sandboxArtifactRef = useRef<PositionArtifactPreview | null>(null);
|
||||||
const closedSandboxKeyRef = useRef<string | null>(null);
|
const closedSandboxKeyRef = useRef<string | null>(null);
|
||||||
const [sandboxThreadSnapshot, setSandboxThreadSnapshot] = useState<{
|
const [sandboxThreadSnapshot, setSandboxThreadSnapshot] = useState<SandboxThreadSnapshot | null>(null);
|
||||||
threadId: string;
|
const latestNodeThreadSnapshotRef = useRef<SandboxThreadSnapshot | null>(null);
|
||||||
thread: BaseStream<AgentThreadState>;
|
const pendingSandboxThreadSnapshotRef = useRef<SandboxThreadSnapshot | null>(null);
|
||||||
signature: string;
|
const sandboxSnapshotFrameRef = useRef<number | null>(null);
|
||||||
} | null>(null);
|
|
||||||
const [flowDetailItem, setFlowDetailItem] = useState<ResultItem | null>(null);
|
const [flowDetailItem, setFlowDetailItem] = useState<ResultItem | null>(null);
|
||||||
const [taskEditorOpen, setTaskEditorOpen] = useState(false);
|
const [taskEditorOpen, setTaskEditorOpen] = useState(false);
|
||||||
const [taskDraft, setTaskDraft] = useState<PositionTask>(fallbackTask);
|
const [taskDraft, setTaskDraft] = useState<PositionTask>(fallbackTask);
|
||||||
@@ -463,6 +468,41 @@ export default function PositionRoundtablePage({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
sandboxArtifactRef.current = sandboxArtifact;
|
sandboxArtifactRef.current = sandboxArtifact;
|
||||||
}, [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(
|
const frozenIntent = useMemo(
|
||||||
() => parseIntentSummary(positionSession?.intentSnapshot),
|
() => parseIntentSummary(positionSession?.intentSnapshot),
|
||||||
[positionSession?.intentSnapshot],
|
[positionSession?.intentSnapshot],
|
||||||
@@ -706,6 +746,14 @@ export default function PositionRoundtablePage({
|
|||||||
if (!activeNode) return null;
|
if (!activeNode) return null;
|
||||||
return activeChain?.seats.find((seat) => seat.agent_id === activeNode.agentId)?.name ?? null;
|
return activeChain?.seats.find((seat) => seat.agent_id === activeNode.agentId)?.name ?? null;
|
||||||
}, [activeChain?.seats, activeNode]);
|
}, [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(() => {
|
useEffect(() => {
|
||||||
const nextNodeKey = activeNode?.nodeKey ?? null;
|
const nextNodeKey = activeNode?.nodeKey ?? null;
|
||||||
if (activeNodeKey !== nextNodeKey) setActiveNodeKey(nextNodeKey);
|
if (activeNodeKey !== nextNodeKey) setActiveNodeKey(nextNodeKey);
|
||||||
@@ -1060,6 +1108,9 @@ export default function PositionRoundtablePage({
|
|||||||
? current
|
? current
|
||||||
: artifact,
|
: artifact,
|
||||||
);
|
);
|
||||||
|
setSandboxThreadSnapshot((current) =>
|
||||||
|
current?.threadId === artifact.threadId ? current : null,
|
||||||
|
);
|
||||||
|
|
||||||
const ctx = artifactsCtxRef.current;
|
const ctx = artifactsCtxRef.current;
|
||||||
if (ctx?.selectedArtifact !== artifact.path) {
|
if (ctx?.selectedArtifact !== artifact.path) {
|
||||||
@@ -1069,8 +1120,12 @@ export default function PositionRoundtablePage({
|
|||||||
if (!ctx?.open) {
|
if (!ctx?.open) {
|
||||||
ctx?.setOpen(true);
|
ctx?.setOpen(true);
|
||||||
}
|
}
|
||||||
|
const latestSnapshot = latestNodeThreadSnapshotRef.current;
|
||||||
|
if (latestSnapshot?.threadId === artifact.threadId) {
|
||||||
|
queueSandboxThreadSnapshot(latestSnapshot);
|
||||||
|
}
|
||||||
setRightPanelOpen(false);
|
setRightPanelOpen(false);
|
||||||
}, []);
|
}, [queueSandboxThreadSnapshot]);
|
||||||
const handlePreviewFlowArtifact = useCallback(
|
const handlePreviewFlowArtifact = useCallback(
|
||||||
(item: ResultItem) => {
|
(item: ResultItem) => {
|
||||||
const threadId = item.artifactThreadId || item.node?.threadId;
|
const threadId = item.artifactThreadId || item.node?.threadId;
|
||||||
@@ -1124,9 +1179,7 @@ export default function PositionRoundtablePage({
|
|||||||
const handleStartedIntentSend = useCallback(() => {
|
const handleStartedIntentSend = useCallback(() => {
|
||||||
toast.info("业务链已启动,任务意图已冻结;如需重新识别请新建会话");
|
toast.info("业务链已启动,任务意图已冻结;如需重新识别请新建会话");
|
||||||
}, []);
|
}, []);
|
||||||
const sandboxOpen = Boolean(
|
const sandboxOpen = Boolean(sandboxArtifact && artifactsCtx);
|
||||||
sandboxArtifact && artifactsCtx?.open && artifactsCtx.selectedArtifact,
|
|
||||||
);
|
|
||||||
// ArtifactFileDetail is shared with the normal LangGraph chat surface and
|
// ArtifactFileDetail is shared with the normal LangGraph chat surface and
|
||||||
// therefore reads ThreadContext for streaming state. The position workspace
|
// therefore reads ThreadContext for streaming state. The position workspace
|
||||||
// renders its sandbox as a sibling of PositionNodeChat, so it is outside the
|
// renders its sandbox as a sibling of PositionNodeChat, so it is outside the
|
||||||
@@ -1154,17 +1207,22 @@ export default function PositionRoundtablePage({
|
|||||||
: sandboxThreadStub,
|
: sandboxThreadStub,
|
||||||
[sandboxArtifact, sandboxThreadSnapshot, sandboxThreadStub],
|
[sandboxArtifact, sandboxThreadSnapshot, sandboxThreadStub],
|
||||||
);
|
);
|
||||||
|
const sandboxThreadContextValue = useMemo(
|
||||||
|
() => ({ thread: sandboxThread, isMock: false }),
|
||||||
|
[sandboxThread],
|
||||||
|
);
|
||||||
const handleNodeThreadSnapshot = useCallback(
|
const handleNodeThreadSnapshot = useCallback(
|
||||||
(snapshot: { threadId: string; thread: BaseStream<AgentThreadState> }) => {
|
(snapshot: { threadId: string; thread: BaseStream<AgentThreadState> }) => {
|
||||||
const signature = sandboxThreadSignature(snapshot.thread);
|
const signature = sandboxThreadSignature(snapshot.thread);
|
||||||
setSandboxThreadSnapshot((current) => {
|
const nextSnapshot: SandboxThreadSnapshot = { ...snapshot, signature };
|
||||||
const sameThreadData =
|
latestNodeThreadSnapshotRef.current = nextSnapshot;
|
||||||
current?.threadId === snapshot.threadId &&
|
const currentArtifact = sandboxArtifactRef.current;
|
||||||
current.signature === signature;
|
if (!currentArtifact || currentArtifact.threadId !== snapshot.threadId) {
|
||||||
return sameThreadData ? current : { ...snapshot, signature };
|
return;
|
||||||
});
|
}
|
||||||
|
queueSandboxThreadSnapshot(nextSnapshot);
|
||||||
},
|
},
|
||||||
[],
|
[queueSandboxThreadSnapshot],
|
||||||
);
|
);
|
||||||
const closeSandbox = useCallback(() => {
|
const closeSandbox = useCallback(() => {
|
||||||
const current = sandboxArtifactRef.current;
|
const current = sandboxArtifactRef.current;
|
||||||
@@ -1175,9 +1233,18 @@ export default function PositionRoundtablePage({
|
|||||||
if (current || ctx?.open) {
|
if (current || ctx?.open) {
|
||||||
ctx?.setOpen(false);
|
ctx?.setOpen(false);
|
||||||
}
|
}
|
||||||
|
pendingSandboxThreadSnapshotRef.current = null;
|
||||||
setSandboxArtifact(null);
|
setSandboxArtifact(null);
|
||||||
setSandboxThreadSnapshot(null);
|
setSandboxThreadSnapshot(null);
|
||||||
}, []);
|
}, []);
|
||||||
|
const handleSelectPositionNode = useCallback(
|
||||||
|
(nextNode: PositionRoundtableNode) => {
|
||||||
|
closeSandbox();
|
||||||
|
setActiveNodeKey(nextNode.nodeKey);
|
||||||
|
setSelectedAgentId(nextNode.agentId);
|
||||||
|
},
|
||||||
|
[closeSandbox],
|
||||||
|
);
|
||||||
const renderIntentChatPanel = (chainStarted: boolean) => (
|
const renderIntentChatPanel = (chainStarted: boolean) => (
|
||||||
<div className="contents">
|
<div className="contents">
|
||||||
{intent.intentInitError && (
|
{intent.intentInitError && (
|
||||||
@@ -1414,6 +1481,8 @@ export default function PositionRoundtablePage({
|
|||||||
currentPositionId={positionId}
|
currentPositionId={positionId}
|
||||||
sessionStatus={positionSession.status}
|
sessionStatus={positionSession.status}
|
||||||
agentName={activeNodeAgentName}
|
agentName={activeNodeAgentName}
|
||||||
|
positionNodeOptions={currentPositionNodeOptions}
|
||||||
|
onSelectPositionNode={handleSelectPositionNode}
|
||||||
onNodeUpdate={handleNodeUpdate}
|
onNodeUpdate={handleNodeUpdate}
|
||||||
onConversationStart={() => setRightPanelOpen(false)}
|
onConversationStart={() => setRightPanelOpen(false)}
|
||||||
onPreviewArtifact={handlePreviewArtifact}
|
onPreviewArtifact={handlePreviewArtifact}
|
||||||
@@ -1518,7 +1587,7 @@ export default function PositionRoundtablePage({
|
|||||||
</div>
|
</div>
|
||||||
</TDrawer>
|
</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">
|
<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">
|
<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">
|
<div className="min-w-0">
|
||||||
@@ -1537,7 +1606,7 @@ export default function PositionRoundtablePage({
|
|||||||
title="关闭沙箱"
|
title="关闭沙箱"
|
||||||
/>
|
/>
|
||||||
</header>
|
</header>
|
||||||
<ThreadContext.Provider value={{ thread: sandboxThread, isMock: false }}>
|
<ThreadContext.Provider value={sandboxThreadContextValue}>
|
||||||
<ArtifactFileDetail
|
<ArtifactFileDetail
|
||||||
className="min-h-0 flex-1"
|
className="min-h-0 flex-1"
|
||||||
filepath={sandboxArtifact.path}
|
filepath={sandboxArtifact.path}
|
||||||
@@ -3901,7 +3970,6 @@ function DeliverableDetailDialog({
|
|||||||
item,
|
item,
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
onReject,
|
onReject,
|
||||||
onPreviewArtifact,
|
|
||||||
onAskBuiltin,
|
onAskBuiltin,
|
||||||
askSubmitting,
|
askSubmitting,
|
||||||
showReject = true,
|
showReject = true,
|
||||||
@@ -3920,6 +3988,32 @@ function DeliverableDetailDialog({
|
|||||||
const status = item?.status ? nodeStatusMeta(item.status) : null;
|
const status = item?.status ? nodeStatusMeta(item.status) : null;
|
||||||
const StatusIcon = status?.Icon;
|
const StatusIcon = status?.Icon;
|
||||||
const canReject = showReject && item?.node ? canRejectNodeDelivery(item.node) : false;
|
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 =
|
const builtinKind =
|
||||||
showBuiltinAsk && onAskBuiltin && (item?.kind === "summary" || item?.kind === "action_plan")
|
showBuiltinAsk && onAskBuiltin && (item?.kind === "summary" || item?.kind === "action_plan")
|
||||||
? item.kind
|
? item.kind
|
||||||
@@ -3938,7 +4032,7 @@ function DeliverableDetailDialog({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={Boolean(item)} onOpenChange={onOpenChange}>
|
<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">
|
<DialogHeader className="shrink-0 border-b px-5 py-4 dark:border-[#2b8ffe]/25">
|
||||||
<DialogTitle className="flex items-center gap-2 text-base">
|
<DialogTitle className="flex items-center gap-2 text-base">
|
||||||
<FileOutput className="size-4 text-primary" />
|
<FileOutput className="size-4 text-primary" />
|
||||||
@@ -4006,20 +4100,26 @@ function DeliverableDetailDialog({
|
|||||||
<p className="text-xs font-bold text-slate-700 dark:text-[#e8edf5]">
|
<p className="text-xs font-bold text-slate-700 dark:text-[#e8edf5]">
|
||||||
{item.kind === "artifact" ? "文件产物" : "产物内容"}
|
{item.kind === "artifact" ? "文件产物" : "产物内容"}
|
||||||
</p>
|
</p>
|
||||||
{item.artifactPath && (
|
</div>
|
||||||
<TButton
|
<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">
|
||||||
size="small"
|
{artifactContentQuery.isLoading ? (
|
||||||
variant="outline"
|
<div className="flex h-full items-center justify-center gap-2 text-sm text-muted-foreground dark:text-[#8aa0c4]">
|
||||||
icon={<FileText className="size-3.5" />}
|
<Loader2 className="size-4 animate-spin" />
|
||||||
onClick={() => onPreviewArtifact(item)}
|
正在加载 Markdown 文件内容…
|
||||||
>
|
</div>
|
||||||
查看文件
|
) : (
|
||||||
</TButton>
|
<ArtifactFilePreview
|
||||||
|
content={displayedContent}
|
||||||
|
language="markdown"
|
||||||
|
threadId={artifactThreadId ?? ""}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<p className="whitespace-pre-wrap text-xs leading-6 text-slate-600 dark:text-[#c5cfe0]">
|
{artifactContentQuery.isError && (
|
||||||
{desensitize(item.summary || "暂无产物正文")}
|
<p className="mt-2 text-[11px] text-amber-600 dark:text-amber-300">
|
||||||
</p>
|
Markdown 文件内容加载失败,当前展示的是节点摘要。
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{builtinKind && item.status === "done" && (
|
{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">
|
<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