代码提交
This commit is contained in:
1 parent
7d249a81ce
commit
4948a80c56
3 files changed
+64
-137
No files matched your search
@@ -1,6 +1,6 @@
|
|||||||
import type { BaseStream } from "@langchain/langgraph-sdk/react";
|
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, CheckCircle2, FileText, LockKeyhole, Sparkles, Target, Trash2 } from "lucide-react";
|
import { Bot, CheckCircle2, FileText, LockKeyhole, Sparkles, Target } from "lucide-react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { Button as TButton } from "tdesign-react";
|
import { Button as TButton } from "tdesign-react";
|
||||||
|
|
||||||
@@ -9,7 +9,7 @@ import { MessageList } from "@/components/workspace/messages";
|
|||||||
import { ThreadContext } from "@/components/workspace/messages/context";
|
import { ThreadContext } from "@/components/workspace/messages/context";
|
||||||
import { useAgent } from "@/core/agents";
|
import { useAgent } from "@/core/agents";
|
||||||
import { useLocalSettings } from "@/core/settings";
|
import { useLocalSettings } from "@/core/settings";
|
||||||
import { useClearThreadMessages, useThreadStream } from "@/core/threads/hooks";
|
import { useThreadStream } from "@/core/threads/hooks";
|
||||||
import type { AgentThreadState } from "@/core/threads/types";
|
import type { AgentThreadState } from "@/core/threads/types";
|
||||||
import { textOfMessage } from "@/core/threads/utils";
|
import { textOfMessage } from "@/core/threads/utils";
|
||||||
import { uuid } from "@/core/utils/uuid";
|
import { uuid } from "@/core/utils/uuid";
|
||||||
@@ -229,10 +229,6 @@ export function PositionBuiltinAgentChat({
|
|||||||
useState<PendingBuiltinSubmission | null>(null);
|
useState<PendingBuiltinSubmission | null>(null);
|
||||||
const pendingSubmissionRef = useRef(false);
|
const pendingSubmissionRef = useRef(false);
|
||||||
const handledAutoStartRequestRef = useRef<number | undefined>(undefined);
|
const handledAutoStartRequestRef = useRef<number | undefined>(undefined);
|
||||||
const clearThreadMessages = useClearThreadMessages();
|
|
||||||
const [externallyHiddenMessageIds, setExternallyHiddenMessageIds] = useState<Set<string>>(
|
|
||||||
() => new Set(),
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setAssistantId(fallbackAgentId);
|
setAssistantId(fallbackAgentId);
|
||||||
@@ -243,7 +239,6 @@ export function PositionBuiltinAgentChat({
|
|||||||
pendingSubmissionRef.current = false;
|
pendingSubmissionRef.current = false;
|
||||||
setPendingSubmission(null);
|
setPendingSubmission(null);
|
||||||
handledAutoStartRequestRef.current = undefined;
|
handledAutoStartRequestRef.current = undefined;
|
||||||
setExternallyHiddenMessageIds(new Set());
|
|
||||||
// Do not depend on `persistedThreadId` here. Preparing the automatic
|
// Do not depend on `persistedThreadId` here. Preparing the automatic
|
||||||
// first question can persist a thread id while the page's initial
|
// first question can persist a thread id while the page's initial
|
||||||
// session-request is still resolving. Treating that update as a new
|
// session-request is still resolving. Treating that update as a new
|
||||||
@@ -483,31 +478,6 @@ export function PositionBuiltinAgentChat({
|
|||||||
[activeThreadId, onPreviewArtifact],
|
[activeThreadId, onPreviewArtifact],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleClearConversation = useCallback(async () => {
|
|
||||||
const threadId = boundThreadIdRef.current;
|
|
||||||
if (!threadId) {
|
|
||||||
toast.info("当前智能体还没有可清空的对话");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (thread.isLoading || isSubmitting) {
|
|
||||||
toast.info("智能体正在生成,完成后再清空对话");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const confirmed = window.confirm("确定清空当前智能体对话吗?清空后不可恢复。");
|
|
||||||
if (!confirmed) return;
|
|
||||||
|
|
||||||
const fallbackIds = thread.messages
|
|
||||||
.map((message) => message.id)
|
|
||||||
.filter((id): id is string => Boolean(id));
|
|
||||||
try {
|
|
||||||
const deletedIds = await clearThreadMessages.mutateAsync({ threadId });
|
|
||||||
setExternallyHiddenMessageIds(new Set(deletedIds.length ? deletedIds : fallbackIds));
|
|
||||||
toast.success("对话已清空");
|
|
||||||
} catch (error) {
|
|
||||||
toast.error(error instanceof Error ? error.message : "清空对话失败");
|
|
||||||
}
|
|
||||||
}, [clearThreadMessages, isSubmitting, thread.isLoading, thread.messages]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (
|
||||||
!autoStartRequestKey ||
|
!autoStartRequestKey ||
|
||||||
@@ -563,16 +533,6 @@ export function PositionBuiltinAgentChat({
|
|||||||
onSelect={onSelectBuiltinAgent}
|
onSelect={onSelectBuiltinAgent}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<TButton
|
|
||||||
size="small"
|
|
||||||
variant="outline"
|
|
||||||
shape="circle"
|
|
||||||
className="!h-8 !w-8 !min-w-8 !shrink-0 !p-0 !bg-white/90 dark:!border-[#2b8ffe]/55 dark:!bg-[#082d5f]/90"
|
|
||||||
icon={<Trash2 className="!size-4 !shrink-0" />}
|
|
||||||
disabled={!boundThreadId || thread.isLoading || isSubmitting || clearThreadMessages.isPending}
|
|
||||||
onClick={() => void handleClearConversation()}
|
|
||||||
title="清空当前智能体对话"
|
|
||||||
/>
|
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
{persistedArtifacts.length > 0 && (
|
{persistedArtifacts.length > 0 && (
|
||||||
@@ -625,7 +585,6 @@ export function PositionBuiltinAgentChat({
|
|||||||
}
|
}
|
||||||
onEditResubmit={(text) => void handleSubmit({ text, files: [] })}
|
onEditResubmit={(text) => void handleSubmit({ text, files: [] })}
|
||||||
onOpenArtifact={handleOpenMessageArtifact}
|
onOpenArtifact={handleOpenMessageArtifact}
|
||||||
externallyHiddenMessageIds={externallyHiddenMessageIds}
|
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex min-h-0 flex-1 flex-col items-center justify-center px-8 text-center">
|
<div className="flex min-h-0 flex-1 flex-col items-center justify-center px-8 text-center">
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { BaseStream } from "@langchain/langgraph-sdk/react";
|
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, Settings, Trash2 } from "lucide-react";
|
import { Bot, FileText, LockKeyhole, Settings } from "lucide-react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { Button as TButton, Select as TSelect } from "tdesign-react";
|
import { Button as TButton, Select as TSelect } from "tdesign-react";
|
||||||
import type { PromptInputMessage } from "@/components/ai-elements/prompt-input";
|
import type { PromptInputMessage } from "@/components/ai-elements/prompt-input";
|
||||||
@@ -15,7 +15,7 @@ import { ThreadContext } from "@/components/workspace/messages/context";
|
|||||||
import { MessageList } from "@/components/workspace/messages";
|
import { MessageList } from "@/components/workspace/messages";
|
||||||
import { useAgent } from "@/core/agents";
|
import { useAgent } from "@/core/agents";
|
||||||
import { useLocalSettings } from "@/core/settings";
|
import { useLocalSettings } from "@/core/settings";
|
||||||
import { useClearThreadMessages, useEditMessage, useThreadStream } from "@/core/threads/hooks";
|
import { useEditMessage, useThreadStream } from "@/core/threads/hooks";
|
||||||
import type { AgentThreadState } from "@/core/threads/types";
|
import type { AgentThreadState } from "@/core/threads/types";
|
||||||
import { textOfMessage } from "@/core/threads/utils";
|
import { textOfMessage } from "@/core/threads/utils";
|
||||||
import { uuid } from "@/core/utils/uuid";
|
import { uuid } from "@/core/utils/uuid";
|
||||||
@@ -163,7 +163,7 @@ function buildWriteFileVirtualUrl(path: string, messageId: string, toolCallId: s
|
|||||||
|
|
||||||
function latestStreamingWriteFile(
|
function latestStreamingWriteFile(
|
||||||
messages: AgentThreadState["messages"],
|
messages: AgentThreadState["messages"],
|
||||||
): { path: string; name: string; mimeType: string | null } | null {
|
): { key: string; path: string; name: string; mimeType: string | null } | null {
|
||||||
for (let messageIndex = messages.length - 1; messageIndex >= 0; messageIndex -= 1) {
|
for (let messageIndex = messages.length - 1; messageIndex >= 0; messageIndex -= 1) {
|
||||||
const message = messages[messageIndex] as (typeof messages)[number] & {
|
const message = messages[messageIndex] as (typeof messages)[number] & {
|
||||||
id?: string;
|
id?: string;
|
||||||
@@ -179,6 +179,7 @@ function latestStreamingWriteFile(
|
|||||||
if (typeof content !== "string" || content.length === 0) continue;
|
if (typeof content !== "string" || content.length === 0) continue;
|
||||||
const virtualPath = buildWriteFileVirtualUrl(rawPath, message.id, toolCall.id);
|
const virtualPath = buildWriteFileVirtualUrl(rawPath, message.id, toolCall.id);
|
||||||
return {
|
return {
|
||||||
|
key: `${message.id}:${toolCall.id}`,
|
||||||
path: virtualPath,
|
path: virtualPath,
|
||||||
name: artifactDisplayName(rawPath),
|
name: artifactDisplayName(rawPath),
|
||||||
mimeType: rawPath.toLowerCase().endsWith(".md") ? "text/markdown" : null,
|
mimeType: rawPath.toLowerCase().endsWith(".md") ? "text/markdown" : null,
|
||||||
@@ -188,6 +189,28 @@ function latestStreamingWriteFile(
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A tool-call id is stable within a persisted thread. Capturing the existing
|
||||||
|
* ids right before a new submission lets the streaming preview distinguish a
|
||||||
|
* new write_file from an older deliverable kept in the message history.
|
||||||
|
*/
|
||||||
|
function writeFileCallKeys(messages: AgentThreadState["messages"]): Set<string> {
|
||||||
|
const keys = new Set<string>();
|
||||||
|
for (const rawMessage of messages) {
|
||||||
|
const message = rawMessage as (typeof messages)[number] & {
|
||||||
|
id?: string;
|
||||||
|
tool_calls?: Array<{ id?: string; name?: string }>;
|
||||||
|
};
|
||||||
|
if (message.type !== "ai" || !message.id || !Array.isArray(message.tool_calls)) continue;
|
||||||
|
for (const toolCall of message.tool_calls) {
|
||||||
|
if (toolCall?.name === "write_file" && toolCall.id) {
|
||||||
|
keys.add(`${message.id}:${toolCall.id}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return keys;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Direct single-agent chat for one frozen business-chain node.
|
* Direct single-agent chat for one frozen business-chain node.
|
||||||
*
|
*
|
||||||
@@ -232,12 +255,10 @@ export function PositionNodeChat({
|
|||||||
const draftThreadIdRef = useRef(node.threadId ?? uuid());
|
const draftThreadIdRef = useRef(node.threadId ?? uuid());
|
||||||
const upstreamRevisionRef = useRef<Record<string, unknown> | null>(null);
|
const upstreamRevisionRef = useRef<Record<string, unknown> | null>(null);
|
||||||
const autoPreviewedArtifactKeyRef = useRef<string | null>(null);
|
const autoPreviewedArtifactKeyRef = useRef<string | null>(null);
|
||||||
const clearThreadMessages = useClearThreadMessages();
|
const autoPreviewBaselineCallKeysRef = useRef<Set<string>>(new Set());
|
||||||
|
const autoPreviewRunArmedRef = useRef(false);
|
||||||
const editMessage = useEditMessage();
|
const editMessage = useEditMessage();
|
||||||
const [messageContentOverrides, setMessageContentOverrides] = useState<Record<string, string>>({});
|
const [messageContentOverrides, setMessageContentOverrides] = useState<Record<string, string>>({});
|
||||||
const [externallyHiddenMessageIds, setExternallyHiddenMessageIds] = useState<Set<string>>(
|
|
||||||
() => new Set(),
|
|
||||||
);
|
|
||||||
const nodeArtifacts = useMemo(() => readNodeArtifacts(node.artifactManifest), [node.artifactManifest]);
|
const nodeArtifacts = useMemo(() => readNodeArtifacts(node.artifactManifest), [node.artifactManifest]);
|
||||||
const canWrite =
|
const canWrite =
|
||||||
sessionStatus !== "archived" &&
|
sessionStatus !== "archived" &&
|
||||||
@@ -250,7 +271,8 @@ export function PositionNodeChat({
|
|||||||
setBoundThreadId(node.threadId);
|
setBoundThreadId(node.threadId);
|
||||||
boundThreadIdRef.current = node.threadId;
|
boundThreadIdRef.current = node.threadId;
|
||||||
setMessageContentOverrides({});
|
setMessageContentOverrides({});
|
||||||
setExternallyHiddenMessageIds(new Set());
|
autoPreviewBaselineCallKeysRef.current = new Set();
|
||||||
|
autoPreviewRunArmedRef.current = false;
|
||||||
}, [node.nodeKey, node.threadId]);
|
}, [node.nodeKey, node.threadId]);
|
||||||
|
|
||||||
const bindCreatedThread = useCallback(
|
const bindCreatedThread = useCallback(
|
||||||
@@ -399,31 +421,6 @@ export function PositionNodeChat({
|
|||||||
sessionId,
|
sessionId,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
const handleClearConversation = useCallback(async () => {
|
|
||||||
const threadId = boundThreadIdRef.current;
|
|
||||||
if (!threadId) {
|
|
||||||
toast.info("当前智能体还没有可清空的对话");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (thread.isLoading) {
|
|
||||||
toast.info("智能体正在生成,完成后再清空对话");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const confirmed = window.confirm("确定清空当前智能体对话吗?清空后不可恢复。");
|
|
||||||
if (!confirmed) return;
|
|
||||||
|
|
||||||
const fallbackIds = thread.messages
|
|
||||||
.map((message) => message.id)
|
|
||||||
.filter((id): id is string => Boolean(id));
|
|
||||||
try {
|
|
||||||
const deletedIds = await clearThreadMessages.mutateAsync({ threadId });
|
|
||||||
setExternallyHiddenMessageIds(new Set(deletedIds.length ? deletedIds : fallbackIds));
|
|
||||||
setMessageContentOverrides({});
|
|
||||||
toast.success("对话已清空");
|
|
||||||
} catch (error) {
|
|
||||||
toast.error(error instanceof Error ? error.message : "清空对话失败");
|
|
||||||
}
|
|
||||||
}, [clearThreadMessages, thread.isLoading, thread.messages]);
|
|
||||||
const hasMultipleSwitchOptions = switchOptions.length > 1;
|
const hasMultipleSwitchOptions = switchOptions.length > 1;
|
||||||
const switchSelectOptions = useMemo(
|
const switchSelectOptions = useMemo(
|
||||||
() =>
|
() =>
|
||||||
@@ -471,7 +468,14 @@ export function PositionNodeChat({
|
|||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!thread.isLoading || !streamingArtifact) return;
|
if (
|
||||||
|
!thread.isLoading ||
|
||||||
|
!streamingArtifact ||
|
||||||
|
!autoPreviewRunArmedRef.current ||
|
||||||
|
autoPreviewBaselineCallKeysRef.current.has(streamingArtifact.key)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
// One write_file call should open at most one preview. The parent further
|
// One write_file call should open at most one preview. The parent further
|
||||||
// guards manual close state, because it owns the page-wide singleton sandbox.
|
// guards manual close state, because it owns the page-wide singleton sandbox.
|
||||||
const artifactKey = `${activeThreadId}:${streamingArtifact.path}`;
|
const artifactKey = `${activeThreadId}:${streamingArtifact.path}`;
|
||||||
@@ -519,11 +523,16 @@ export function PositionNodeChat({
|
|||||||
upstreamRevision && typeof upstreamRevision === "object" && !Array.isArray(upstreamRevision)
|
upstreamRevision && typeof upstreamRevision === "object" && !Array.isArray(upstreamRevision)
|
||||||
? (upstreamRevision as Record<string, unknown>)
|
? (upstreamRevision as Record<string, unknown>)
|
||||||
: null;
|
: null;
|
||||||
|
// A thread retains historical write_file tool calls. Only files whose
|
||||||
|
// call id appears after this exact submission may open the sandbox.
|
||||||
|
autoPreviewBaselineCallKeysRef.current = writeFileCallKeys(thread.messages);
|
||||||
|
autoPreviewRunArmedRef.current = true;
|
||||||
await sendMessage(boundThreadIdRef.current ?? draftThreadIdRef.current, message, {
|
await sendMessage(boundThreadIdRef.current ?? draftThreadIdRef.current, message, {
|
||||||
agent_id: node.agentId,
|
agent_id: node.agentId,
|
||||||
task_external_context: taskExternalContext,
|
task_external_context: taskExternalContext,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
autoPreviewRunArmedRef.current = false;
|
||||||
toast.error(`发送失败:${error instanceof Error ? error.message : String(error)}`);
|
toast.error(`发送失败:${error instanceof Error ? error.message : String(error)}`);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -536,6 +545,7 @@ export function PositionNodeChat({
|
|||||||
sendMessage,
|
sendMessage,
|
||||||
sessionId,
|
sessionId,
|
||||||
sessionStatus,
|
sessionStatus,
|
||||||
|
thread.messages,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -615,16 +625,6 @@ export function PositionNodeChat({
|
|||||||
查看驳回理由
|
查看驳回理由
|
||||||
</TButton>
|
</TButton>
|
||||||
)}
|
)}
|
||||||
<TButton
|
|
||||||
size="small"
|
|
||||||
variant="outline"
|
|
||||||
shape="circle"
|
|
||||||
className="!h-8 !w-8 !min-w-8 !shrink-0 !p-0 !bg-white/90 dark:!border-[#2b8ffe]/55 dark:!bg-[#082d5f]/90"
|
|
||||||
icon={<Trash2 className="!size-4 !shrink-0" />}
|
|
||||||
disabled={!boundThreadId || thread.isLoading || clearThreadMessages.isPending}
|
|
||||||
onClick={() => void handleClearConversation()}
|
|
||||||
title="清空当前智能体对话"
|
|
||||||
/>
|
|
||||||
{onOpenConfiguration && (
|
{onOpenConfiguration && (
|
||||||
<TButton
|
<TButton
|
||||||
size="small"
|
size="small"
|
||||||
@@ -690,7 +690,6 @@ export function PositionNodeChat({
|
|||||||
onEditResubmit={(text) => void handleSubmit({ text, files: [] })}
|
onEditResubmit={(text) => void handleSubmit({ text, files: [] })}
|
||||||
onEditSave={handleSaveMessageEdit}
|
onEditSave={handleSaveMessageEdit}
|
||||||
messageContentOverrides={messageContentOverrides}
|
messageContentOverrides={messageContentOverrides}
|
||||||
externallyHiddenMessageIds={externallyHiddenMessageIds}
|
|
||||||
onOpenArtifact={handleOpenMessageArtifact}
|
onOpenArtifact={handleOpenMessageArtifact}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ import {
|
|||||||
Settings,
|
Settings,
|
||||||
Sparkles,
|
Sparkles,
|
||||||
Target,
|
Target,
|
||||||
Trash2,
|
|
||||||
UserRoundCog,
|
UserRoundCog,
|
||||||
XCircle,
|
XCircle,
|
||||||
type LucideIcon,
|
type LucideIcon,
|
||||||
@@ -67,7 +66,6 @@ import { useAgent } from "@/core/agents";
|
|||||||
import { loadArtifactContent } from "@/core/artifacts/loader";
|
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 { useClearThreadMessages } from "@/core/threads/hooks";
|
|
||||||
import type { AgentThreadState } from "@/core/threads/types";
|
import type { AgentThreadState } from "@/core/threads/types";
|
||||||
import { textOfMessage } from "@/core/threads/utils";
|
import { textOfMessage } from "@/core/threads/utils";
|
||||||
import { desensitize } from "@/lib/desensitize";
|
import { desensitize } from "@/lib/desensitize";
|
||||||
@@ -517,7 +515,6 @@ 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 suppressedAutoPreviewThreadIdsRef = useRef<Set<string>>(new Set());
|
|
||||||
const [sandboxThreadSnapshot, setSandboxThreadSnapshot] = useState<SandboxThreadSnapshot | null>(null);
|
const [sandboxThreadSnapshot, setSandboxThreadSnapshot] = useState<SandboxThreadSnapshot | null>(null);
|
||||||
const latestNodeThreadSnapshotRef = useRef<SandboxThreadSnapshot | null>(null);
|
const latestNodeThreadSnapshotRef = useRef<SandboxThreadSnapshot | null>(null);
|
||||||
const pendingSandboxThreadSnapshotRef = useRef<SandboxThreadSnapshot | null>(null);
|
const pendingSandboxThreadSnapshotRef = useRef<SandboxThreadSnapshot | null>(null);
|
||||||
@@ -634,7 +631,6 @@ export default function PositionRoundtablePage({
|
|||||||
})),
|
})),
|
||||||
[models],
|
[models],
|
||||||
);
|
);
|
||||||
const clearIntentThreadMessages = useClearThreadMessages();
|
|
||||||
const triggerToast = useCallback((message: string) => toast.info(message), []);
|
const triggerToast = useCallback((message: string) => toast.info(message), []);
|
||||||
const intent = useStep1Intent({
|
const intent = useStep1Intent({
|
||||||
// 全岗位共用原会商的对话接口与输入面板;岗位运行编排后续再按角色接入。
|
// 全岗位共用原会商的对话接口与输入面板;岗位运行编排后续再按角色接入。
|
||||||
@@ -651,26 +647,6 @@ export default function PositionRoundtablePage({
|
|||||||
const persistedIntentRef = useRef<string | null>(null);
|
const persistedIntentRef = useRef<string | null>(null);
|
||||||
const isArchivedSession = positionSession?.status === "archived";
|
const isArchivedSession = positionSession?.status === "archived";
|
||||||
|
|
||||||
const handleClearIntentConversation = useCallback(async () => {
|
|
||||||
if (intent.isIntentSending) {
|
|
||||||
toast.info("意图理解正在生成,完成后再清空对话");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const confirmed = window.confirm("确定清空意图理解对话吗?清空后不可恢复。");
|
|
||||||
if (!confirmed) return;
|
|
||||||
try {
|
|
||||||
if (intent.intentThreadId) {
|
|
||||||
await clearIntentThreadMessages.mutateAsync({
|
|
||||||
threadId: intent.intentThreadId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
intent.clearInteractiveMessages();
|
|
||||||
toast.success("意图理解对话已清空");
|
|
||||||
} catch (error) {
|
|
||||||
toast.error(error instanceof Error ? error.message : "清空意图理解对话失败");
|
|
||||||
}
|
|
||||||
}, [clearIntentThreadMessages, intent]);
|
|
||||||
|
|
||||||
const syncSessionUrl = useCallback(
|
const syncSessionUrl = useCallback(
|
||||||
(sessionId: string, nextTaskId: string) => {
|
(sessionId: string, nextTaskId: string) => {
|
||||||
const next = new URLSearchParams(searchParamsString);
|
const next = new URLSearchParams(searchParamsString);
|
||||||
@@ -836,6 +812,16 @@ export default function PositionRoundtablePage({
|
|||||||
}, [positionSession?.chainSnapshot, positionSession?.status, selectedChain]);
|
}, [positionSession?.chainSnapshot, positionSession?.status, selectedChain]);
|
||||||
const isIntelligenceDeliveryOverview =
|
const isIntelligenceDeliveryOverview =
|
||||||
activePosition.isIntentPosition && positionSession !== null && positionSession.status !== "intent_pending";
|
activePosition.isIntentPosition && positionSession !== null && positionSession.status !== "intent_pending";
|
||||||
|
const previousIntelligenceDeliveryOverviewRef = useRef(isIntelligenceDeliveryOverview);
|
||||||
|
useEffect(() => {
|
||||||
|
// Sending the last intent answer transitions the page into the delivery
|
||||||
|
// overview. Restore the right workspace at that moment instead of leaving
|
||||||
|
// the user on an unexpectedly full-width, empty-looking conversation area.
|
||||||
|
if (isIntelligenceDeliveryOverview && !previousIntelligenceDeliveryOverviewRef.current) {
|
||||||
|
setRightPanelOpen(true);
|
||||||
|
}
|
||||||
|
previousIntelligenceDeliveryOverviewRef.current = isIntelligenceDeliveryOverview;
|
||||||
|
}, [isIntelligenceDeliveryOverview]);
|
||||||
const leftSidebarHidden = leftPanelCollapsed;
|
const leftSidebarHidden = leftPanelCollapsed;
|
||||||
const allPositionDeliveriesDone = useMemo(
|
const allPositionDeliveriesDone = useMemo(
|
||||||
() => allExecutableNodesDone(positionSession?.nodes ?? []),
|
() => allExecutableNodesDone(positionSession?.nodes ?? []),
|
||||||
@@ -1250,7 +1236,6 @@ export default function PositionRoundtablePage({
|
|||||||
ctx?.setOpen(false);
|
ctx?.setOpen(false);
|
||||||
}
|
}
|
||||||
closedSandboxKeyRef.current = null;
|
closedSandboxKeyRef.current = null;
|
||||||
suppressedAutoPreviewThreadIdsRef.current.clear();
|
|
||||||
setSandboxArtifact(null);
|
setSandboxArtifact(null);
|
||||||
setSandboxThreadSnapshot(null);
|
setSandboxThreadSnapshot(null);
|
||||||
intent.resetIntent();
|
intent.resetIntent();
|
||||||
@@ -1329,14 +1314,12 @@ export default function PositionRoundtablePage({
|
|||||||
|
|
||||||
const artifactKey = sandboxCloseKey(artifact);
|
const artifactKey = sandboxCloseKey(artifact);
|
||||||
if (artifact.auto) {
|
if (artifact.auto) {
|
||||||
if (
|
// Closing an old file should only suppress that exact preview. A later
|
||||||
closedSandboxKeyRef.current === artifactKey ||
|
// write_file in the same thread is a new deliverable and must still be
|
||||||
suppressedAutoPreviewThreadIdsRef.current.has(artifact.threadId)
|
// able to open its own streaming sandbox.
|
||||||
) {
|
if (closedSandboxKeyRef.current === artifactKey) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
suppressedAutoPreviewThreadIdsRef.current.delete(artifact.threadId);
|
|
||||||
}
|
}
|
||||||
closedSandboxKeyRef.current = null;
|
closedSandboxKeyRef.current = null;
|
||||||
|
|
||||||
@@ -1453,7 +1436,6 @@ export default function PositionRoundtablePage({
|
|||||||
const current = sandboxArtifactRef.current;
|
const current = sandboxArtifactRef.current;
|
||||||
if (current) {
|
if (current) {
|
||||||
closedSandboxKeyRef.current = sandboxCloseKey(current);
|
closedSandboxKeyRef.current = sandboxCloseKey(current);
|
||||||
suppressedAutoPreviewThreadIdsRef.current.add(current.threadId);
|
|
||||||
}
|
}
|
||||||
const ctx = artifactsCtxRef.current;
|
const ctx = artifactsCtxRef.current;
|
||||||
if (ctx?.open) {
|
if (ctx?.open) {
|
||||||
@@ -1554,18 +1536,6 @@ export default function PositionRoundtablePage({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
headerRightSlot={
|
|
||||||
<TButton
|
|
||||||
size="small"
|
|
||||||
variant="outline"
|
|
||||||
shape="circle"
|
|
||||||
className="!h-8 !w-8 !min-w-8 !shrink-0 !p-0 !bg-white/90 dark:!border-[#2b8ffe]/55 dark:!bg-[#082d5f]/90"
|
|
||||||
icon={<Trash2 className="!size-4 !shrink-0" />}
|
|
||||||
disabled={intent.isIntentSending || clearIntentThreadMessages.isPending}
|
|
||||||
onClick={() => void handleClearIntentConversation()}
|
|
||||||
title="清空意图理解对话"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
interactiveMessages={intent.interactiveMessages}
|
interactiveMessages={intent.interactiveMessages}
|
||||||
intentThreadId={intent.intentThreadId}
|
intentThreadId={intent.intentThreadId}
|
||||||
intentReady={chainStarted ? frozenIntent ?? intent.intentReady : intent.intentReady}
|
intentReady={chainStarted ? frozenIntent ?? intent.intentReady : intent.intentReady}
|
||||||
@@ -1578,7 +1548,6 @@ export default function PositionRoundtablePage({
|
|||||||
handleStartedIntentSend();
|
handleStartedIntentSend();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setRightPanelOpen(false);
|
|
||||||
void intent.sendIntentMessage(text, files);
|
void intent.sendIntentMessage(text, files);
|
||||||
}}
|
}}
|
||||||
onStopIntent={intent.stopIntentStream}
|
onStopIntent={intent.stopIntentStream}
|
||||||
@@ -1836,8 +1805,7 @@ export default function PositionRoundtablePage({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<section className="relative flex min-w-[420px] flex-1 flex-col overflow-hidden rounded-[4px] border border-slate-200 bg-white shadow-sm dark:border-[#2b8ffe]/45 dark:bg-[#032650]/80 dark:shadow-none dark:backdrop-blur-md">
|
<section className="relative flex min-w-[420px] flex-1 flex-col overflow-hidden rounded-[4px] border border-slate-200 bg-white shadow-sm dark:border-[#2b8ffe]/45 dark:bg-[#032650]/80 dark:shadow-none dark:backdrop-blur-md">
|
||||||
{!isIntelligenceDeliveryOverview &&
|
{!rightPanelOpen &&
|
||||||
!rightPanelOpen &&
|
|
||||||
!sandboxOpen &&
|
!sandboxOpen &&
|
||||||
!(positionSession && activeNode && !activeBuiltinKind) && (
|
!(positionSession && activeNode && !activeBuiltinKind) && (
|
||||||
<TButton
|
<TButton
|
||||||
@@ -1864,7 +1832,8 @@ export default function PositionRoundtablePage({
|
|||||||
builtinAgentOptions={showClosureAgents ? builtinAgentOptions : undefined}
|
builtinAgentOptions={showClosureAgents ? builtinAgentOptions : undefined}
|
||||||
onSelectBuiltinAgent={showClosureAgents ? handleSelectBuiltinStage : undefined}
|
onSelectBuiltinAgent={showClosureAgents ? handleSelectBuiltinStage : undefined}
|
||||||
onSessionUpdate={setPositionSession}
|
onSessionUpdate={setPositionSession}
|
||||||
onConversationStart={() => setRightPanelOpen(false)}
|
// 测试分析岗的右侧交付工作区应持续可见;会话开始不再把它收起。
|
||||||
|
onConversationStart={undefined}
|
||||||
onPreviewArtifact={handlePreviewArtifact}
|
onPreviewArtifact={handlePreviewArtifact}
|
||||||
onThreadSnapshot={handleNodeThreadSnapshot}
|
onThreadSnapshot={handleNodeThreadSnapshot}
|
||||||
/>
|
/>
|
||||||
@@ -1901,7 +1870,7 @@ export default function PositionRoundtablePage({
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{activePosition.isIntentPosition && !isIntelligenceDeliveryOverview && rightPanelOpen && !sandboxOpen && (
|
{activePosition.isIntentPosition && rightPanelOpen && !sandboxOpen && (
|
||||||
<aside className="flex w-[400px] shrink-0 flex-col overflow-hidden rounded-[4px] border border-slate-200 bg-white shadow-sm dark:border-[#2b8ffe]/45 dark:bg-[#032650]/80 dark:text-[#c5cfe0] dark:shadow-none dark:backdrop-blur-md">
|
<aside className="flex w-[400px] shrink-0 flex-col overflow-hidden rounded-[4px] border border-slate-200 bg-white shadow-sm dark:border-[#2b8ffe]/45 dark:bg-[#032650]/80 dark:text-[#c5cfe0] dark:shadow-none dark:backdrop-blur-md">
|
||||||
<RightWorkspace
|
<RightWorkspace
|
||||||
selectedAgent={selectedAgent}
|
selectedAgent={selectedAgent}
|
||||||
|
|||||||
Reference in new issue
Block a user