feat(agent-chat): 修复智能体会话归属与历史路由恢复:侧边栏按当前agent过滤会话并携带agent上下文跳转,补充旧线程路径回溯到智能体工作区,防止误入通用问答,并完善输入框记忆按钮显隐与侧边栏展开入口,提升体验一致性。
This commit is contained in:
1 parent
051e66f7d8
commit
aeb9a55497
5 files changed
+140
-30
No files matched your search
@@ -64,7 +64,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
useDeleteThread,
|
useDeleteThread,
|
||||||
useRenameThread,
|
useRenameThread,
|
||||||
useThreads,
|
useThreadsPage,
|
||||||
useToggleThreadFavorite,
|
useToggleThreadFavorite,
|
||||||
useToggleThreadPin,
|
useToggleThreadPin,
|
||||||
} from "@/core/threads/hooks";
|
} from "@/core/threads/hooks";
|
||||||
@@ -351,8 +351,8 @@ export function AgentChatSidebar({ activeAgentId }: { activeAgentId?: string })
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 「最近的对话」面板(参考首页 GeneralQaSidebar 的「最近的对话」区块)。
|
* 当前智能体的「最近对话」面板(参考首页 GeneralQaSidebar 的「最近的对话」区块)。
|
||||||
* 跨智能体的近期会话,点击打开;支持重命名/置顶/收藏/分享/导出/删除。
|
* 按 agent_id 在服务端过滤;支持重命名/置顶/收藏/分享/导出/删除。
|
||||||
*/
|
*/
|
||||||
function RecentChatsPane({ activeAgentId }: { activeAgentId?: string }) {
|
function RecentChatsPane({ activeAgentId }: { activeAgentId?: string }) {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
@@ -361,7 +361,12 @@ function RecentChatsPane({ activeAgentId }: { activeAgentId?: string }) {
|
|||||||
const pathOf = usePathOfThread();
|
const pathOf = usePathOfThread();
|
||||||
|
|
||||||
const { thread_id: threadIdFromPath } = useParams<{ thread_id: string }>();
|
const { thread_id: threadIdFromPath } = useParams<{ thread_id: string }>();
|
||||||
const { data: threads = [] } = useThreads();
|
const { data: threadPage } = useThreadsPage({
|
||||||
|
page: 1,
|
||||||
|
pageSize: 50,
|
||||||
|
metadata: { agent_id: activeAgentId ?? "" },
|
||||||
|
});
|
||||||
|
const threads = threadPage?.threads ?? [];
|
||||||
const { mutate: deleteThread } = useDeleteThread();
|
const { mutate: deleteThread } = useDeleteThread();
|
||||||
const { mutate: renameThread } = useRenameThread();
|
const { mutate: renameThread } = useRenameThread();
|
||||||
const { toggle: togglePin } = useToggleThreadPin();
|
const { toggle: togglePin } = useToggleThreadPin();
|
||||||
@@ -373,8 +378,8 @@ function RecentChatsPane({ activeAgentId }: { activeAgentId?: string }) {
|
|||||||
const [renameValue, setRenameValue] = useState("");
|
const [renameValue, setRenameValue] = useState("");
|
||||||
const [shareThreadId, setShareThreadId] = useState<string | null>(null);
|
const [shareThreadId, setShareThreadId] = useState<string | null>(null);
|
||||||
|
|
||||||
// 排除调度/会商/报告结构&写作配置向导等系统会话,与首页一致;
|
// 服务端先按当前智能体过滤;客户端再排除调度/会商/报告结构&写作配置向导等
|
||||||
// 智能体创建(bootstrap)会话保留展示(带机器人图标、点击回创建页);置顶优先。
|
// 系统会话。智能体创建(bootstrap)会话保留展示(带机器人图标、点击回创建页);置顶优先。
|
||||||
const visibleThreads = useMemo(
|
const visibleThreads = useMemo(
|
||||||
() => sortThreadsPinnedFirst(threads.filter((thread) => !isHiddenFromRecentChats(thread))),
|
() => sortThreadsPinnedFirst(threads.filter((thread) => !isHiddenFromRecentChats(thread))),
|
||||||
[threads],
|
[threads],
|
||||||
@@ -479,7 +484,11 @@ function RecentChatsPane({ activeAgentId }: { activeAgentId?: string }) {
|
|||||||
) : (
|
) : (
|
||||||
filteredThreads.map((thread) => {
|
filteredThreads.map((thread) => {
|
||||||
const isBoot = isBootstrapThread(thread);
|
const isBoot = isBootstrapThread(thread);
|
||||||
const href = isBoot ? bootstrapResumePath(thread.thread_id) : pathOf(thread);
|
// 正常记录依据自身保存的 agent_id 跳转;早期记录可能尚未写入
|
||||||
|
// agent_id,此时保留当前智能体上下文,避免从智能体页误落到通用问答。
|
||||||
|
const href = isBoot
|
||||||
|
? bootstrapResumePath(thread.thread_id)
|
||||||
|
: pathOf(thread, { agent_id: activeAgentId });
|
||||||
const isActive = href === pathname;
|
const isActive = href === pathname;
|
||||||
const pinned = isThreadPinned(thread);
|
const pinned = isThreadPinned(thread);
|
||||||
const favorited = isThreadFavorite(thread);
|
const favorited = isThreadFavorite(thread);
|
||||||
|
|||||||
@@ -758,7 +758,7 @@ export function InputBox({
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{!frontendRagActive && !hideContextTools && !hideMemoryTool && <Tooltip
|
{!frontendRagActive && !hideContextTools && <Tooltip
|
||||||
content={
|
content={
|
||||||
context.writing_mode
|
context.writing_mode
|
||||||
? "写作模式已开启:报告由深度研究管线按「通用分析结构」撰写,优先使用对话中已检索的资料"
|
? "写作模式已开启:报告由深度研究管线按「通用分析结构」撰写,优先使用对话中已检索的资料"
|
||||||
@@ -813,7 +813,7 @@ export function InputBox({
|
|||||||
<span className="text-xs font-normal">文件</span>
|
<span className="text-xs font-normal">文件</span>
|
||||||
</PromptInputButton>
|
</PromptInputButton>
|
||||||
</Tooltip>}
|
</Tooltip>}
|
||||||
{!frontendRagActive && !hideContextTools && <Tooltip
|
{!frontendRagActive && !hideContextTools && !hideMemoryTool && <Tooltip
|
||||||
content={
|
content={
|
||||||
memoryInjectionEnabled
|
memoryInjectionEnabled
|
||||||
? "记忆注入已开启:对话前会注入历史记忆"
|
? "记忆注入已开启:对话前会注入历史记忆"
|
||||||
|
|||||||
@@ -27,6 +27,18 @@ function pickAgentRef(
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the agent that owns a thread when that ownership was persisted on
|
||||||
|
* either its run context or its metadata. Keeping this in one place ensures
|
||||||
|
* sidebars and route recovery make the same decision for older deep links.
|
||||||
|
*/
|
||||||
|
export function agentRefOfThread(thread: {
|
||||||
|
context?: ThreadAgentRef | null;
|
||||||
|
metadata?: Record<string, unknown> | null;
|
||||||
|
}): string | undefined {
|
||||||
|
return pickAgentRef(thread.context, thread.metadata);
|
||||||
|
}
|
||||||
|
|
||||||
export function pathOfThread(
|
export function pathOfThread(
|
||||||
thread: ThreadRouteTarget,
|
thread: ThreadRouteTarget,
|
||||||
context?: ThreadAgentRef | null,
|
context?: ThreadAgentRef | null,
|
||||||
@@ -36,7 +48,7 @@ export function pathOfThread(
|
|||||||
const agentRef =
|
const agentRef =
|
||||||
typeof thread === "string"
|
typeof thread === "string"
|
||||||
? pickAgentRef(context, null)
|
? pickAgentRef(context, null)
|
||||||
: pickAgentRef(thread.context, thread.metadata);
|
: agentRefOfThread(thread) ?? pickAgentRef(context, null);
|
||||||
|
|
||||||
return agentRef
|
return agentRef
|
||||||
? `${base}/agents/${encodeURIComponent(agentRef)}/chats/${threadId}`
|
? `${base}/agents/${encodeURIComponent(agentRef)}/chats/${threadId}`
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import {
|
import {
|
||||||
BotIcon,
|
BotIcon,
|
||||||
Compass,
|
Compass,
|
||||||
|
PanelLeftOpenIcon,
|
||||||
PanelRightOpenIcon,
|
PanelRightOpenIcon,
|
||||||
PlusSquare,
|
PlusSquare,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
@@ -13,6 +14,7 @@ import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } fro
|
|||||||
|
|
||||||
import type { PromptInputMessage } from "@/components/ai-elements/prompt-input";
|
import type { PromptInputMessage } from "@/components/ai-elements/prompt-input";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { useSidebarSafe } from "@/components/ui/sidebar";
|
||||||
import { AgentChatSidebar } from "@/components/workspace/agents/agent-chat-sidebar";
|
import { AgentChatSidebar } from "@/components/workspace/agents/agent-chat-sidebar";
|
||||||
import { AgentDetailPanel } from "@/components/workspace/agents/agent-detail-panel";
|
import { AgentDetailPanel } from "@/components/workspace/agents/agent-detail-panel";
|
||||||
import { AgentQuestionChips } from "@/components/workspace/agents/agent-question-chips";
|
import { AgentQuestionChips } from "@/components/workspace/agents/agent-question-chips";
|
||||||
@@ -225,6 +227,7 @@ export default function AgentChatPage({
|
|||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const [showFollowups, setShowFollowups] = useState(false);
|
const [showFollowups, setShowFollowups] = useState(false);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const workspaceSidebar = useSidebarSafe();
|
||||||
|
|
||||||
const { agent_id: agentIdFromPath } = useParams<{
|
const { agent_id: agentIdFromPath } = useParams<{
|
||||||
agent_id: string;
|
agent_id: string;
|
||||||
@@ -806,6 +809,23 @@ export default function AgentChatPage({
|
|||||||
<main className="relative flex size-full min-h-0 min-w-0 flex-col">
|
<main className="relative flex size-full min-h-0 min-w-0 flex-col">
|
||||||
{/* Header in normal flow, fixed height. */}
|
{/* Header in normal flow, fixed height. */}
|
||||||
<header className="bg-card/90 border-border/70 flex h-12 shrink-0 items-center gap-2 border-b px-4 backdrop-blur-md">
|
<header className="bg-card/90 border-border/70 flex h-12 shrink-0 items-center gap-2 border-b px-4 backdrop-blur-md">
|
||||||
|
{/* 工作区左栏收起后,在智能体聊天区左上角提供展开入口。 */}
|
||||||
|
{showAgentList &&
|
||||||
|
!isEmbedded &&
|
||||||
|
workspaceSidebar &&
|
||||||
|
!workspaceSidebar.open && (
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="icon-sm"
|
||||||
|
onClick={workspaceSidebar.toggleSidebar}
|
||||||
|
title="展开左侧栏"
|
||||||
|
aria-label="展开左侧栏"
|
||||||
|
className="shrink-0 text-muted-foreground hover:text-foreground"
|
||||||
|
>
|
||||||
|
<PanelLeftOpenIcon className="size-4" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
<div className="flex shrink-0 items-center gap-1.5 rounded-md border px-2 py-1">
|
<div className="flex shrink-0 items-center gap-1.5 rounded-md border px-2 py-1">
|
||||||
<BotIcon className="text-primary h-3.5 w-3.5" />
|
<BotIcon className="text-primary h-3.5 w-3.5" />
|
||||||
<span
|
<span
|
||||||
@@ -881,23 +901,6 @@ export default function AgentChatPage({
|
|||||||
开始回答
|
开始回答
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
<Tooltip content={t.agents.newChat}>
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="secondary"
|
|
||||||
onClick={() => {
|
|
||||||
// 嵌入态:保持停留在通用问答右侧,开同一智能体的新对话;
|
|
||||||
// 非嵌入态:走该智能体的独立新对话页(按当前路由 base)。
|
|
||||||
router.push(
|
|
||||||
isEmbedded
|
|
||||||
? `/page/workspace/chats/new?agent=${agent_id}`
|
|
||||||
: `${agentChatsBase}/new`,
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<PlusSquare /> {t.agents.newChat}
|
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
|
||||||
{/* 右侧智能体详情面板收起时的「展开」入口:仅收起态显示
|
{/* 右侧智能体详情面板收起时的「展开」入口:仅收起态显示
|
||||||
(展开态的「收起」按钮在面板右上角)。桌面端专用(lg+),
|
(展开态的「收起」按钮在面板右上角)。桌面端专用(lg+),
|
||||||
窄屏面板走抽屉交互;嵌入态面板固定渲染,不显示该开关。 */}
|
窄屏面板走抽屉交互;嵌入态面板固定渲染,不显示该开关。 */}
|
||||||
@@ -919,6 +922,23 @@ export default function AgentChatPage({
|
|||||||
enabled={tokenUsageEnabled}
|
enabled={tokenUsageEnabled}
|
||||||
messages={displayThread.messages}
|
messages={displayThread.messages}
|
||||||
/>
|
/>
|
||||||
|
<Tooltip content={t.agents.newChat}>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="secondary"
|
||||||
|
onClick={() => {
|
||||||
|
// 嵌入态:保持停留在通用问答右侧,开同一智能体的新对话;
|
||||||
|
// 非嵌入态:走该智能体的独立新对话页(按当前路由 base)。
|
||||||
|
router.push(
|
||||||
|
isEmbedded
|
||||||
|
? `/page/workspace/chats/new?agent=${agent_id}`
|
||||||
|
: `${agentChatsBase}/new`,
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<PlusSquare /> {t.agents.newChat}
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
<ExportTrigger threadId={threadId} />
|
<ExportTrigger threadId={threadId} />
|
||||||
<ArtifactTrigger />
|
<ArtifactTrigger />
|
||||||
</div>
|
</div>
|
||||||
@@ -1101,7 +1121,6 @@ export default function AgentChatPage({
|
|||||||
onAskQuestion={(text) => handleSubmit({ text, files: [] })}
|
onAskQuestion={(text) => handleSubmit({ text, files: [] })}
|
||||||
collapsed={detailCollapsed}
|
collapsed={detailCollapsed}
|
||||||
onCollapsedChange={setDetailCollapsed}
|
onCollapsedChange={setDetailCollapsed}
|
||||||
showRecentChats={!taskCtxOn}
|
|
||||||
compactAppearanceActions={compactAppearanceActions}
|
compactAppearanceActions={compactAppearanceActions}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import type { CSSProperties, ReactNode } from 'react';
|
import { useEffect, useState, type CSSProperties, type ReactNode } from 'react';
|
||||||
import { Navigate, Route, Routes, useLocation, useParams } from 'react-router-dom';
|
import { Navigate, Route, Routes, useLocation, useParams } from 'react-router-dom';
|
||||||
|
|
||||||
import { getStoredAuth } from '@/core/auth';
|
import { getStoredAuth } from '@/core/auth';
|
||||||
|
import { getAPIClient } from '@/core/api';
|
||||||
import { isSentimentAgentVisible } from '@/core/sentiment-agent/config';
|
import { isSentimentAgentVisible } from '@/core/sentiment-agent/config';
|
||||||
import {
|
import {
|
||||||
resolveChatsIframeEntryRedirect,
|
resolveChatsIframeEntryRedirect,
|
||||||
@@ -25,6 +26,7 @@ import { ArtifactsProvider } from '@/components/workspace/artifacts';
|
|||||||
import { DocumentRewritePresentationProvider } from '@/core/document-rewrite/context';
|
import { DocumentRewritePresentationProvider } from '@/core/document-rewrite/context';
|
||||||
|
|
||||||
import { RouteBaseProvider } from '@/core/route-base';
|
import { RouteBaseProvider } from '@/core/route-base';
|
||||||
|
import { agentRefOfThread } from '@/core/threads/utils';
|
||||||
|
|
||||||
import ChatsPage from './ChatsPage';
|
import ChatsPage from './ChatsPage';
|
||||||
import ChatPage from './ChatPage';
|
import ChatPage from './ChatPage';
|
||||||
@@ -341,12 +343,80 @@ function ChatThreadEntryRoute({
|
|||||||
return (
|
return (
|
||||||
<WorkspaceLayout {...layoutProps} defaultHistorySidebarOpen>
|
<WorkspaceLayout {...layoutProps} defaultHistorySidebarOpen>
|
||||||
<ChatRuntime>
|
<ChatRuntime>
|
||||||
<ChatPage />
|
<AgentAwareChatThreadContent />
|
||||||
</ChatRuntime>
|
</ChatRuntime>
|
||||||
</WorkspaceLayout>
|
</WorkspaceLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Older bookmarks and a few historic list entries use the generic
|
||||||
|
* ``/chats/:thread_id`` path even for an agent-owned conversation. Recover the
|
||||||
|
* owning agent from the persisted thread record before rendering so those
|
||||||
|
* links keep the agent workspace (agent list and detail panel) instead of
|
||||||
|
* falling back to the general Q&A page. Task workspace threads intentionally
|
||||||
|
* remain on their explicit task routes because their surrounding layout is
|
||||||
|
* part of the task context.
|
||||||
|
*/
|
||||||
|
function AgentAwareChatThreadContent() {
|
||||||
|
const { thread_id: threadId } = useParams<{ thread_id: string }>();
|
||||||
|
const location = useLocation();
|
||||||
|
const [agentId, setAgentId] = useState<string | null>(null);
|
||||||
|
const [resolved, setResolved] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let cancelled = false;
|
||||||
|
setAgentId(null);
|
||||||
|
setResolved(false);
|
||||||
|
|
||||||
|
if (!threadId) {
|
||||||
|
setResolved(true);
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
void getAPIClient()
|
||||||
|
.threads.get(threadId)
|
||||||
|
.then((thread) => {
|
||||||
|
if (cancelled) return;
|
||||||
|
const record = thread as {
|
||||||
|
context?: { agent_id?: string; agent_name?: string } | null;
|
||||||
|
metadata?: Record<string, unknown> | null;
|
||||||
|
};
|
||||||
|
// Task-bound threads need their task host to preserve the purpose gate,
|
||||||
|
// action partitioning, and other task-specific controls.
|
||||||
|
const isTaskThread = typeof record.metadata?.taskId === 'string';
|
||||||
|
setAgentId(isTaskThread ? null : agentRefOfThread(record) ?? null);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
if (!cancelled) setAgentId(null);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
if (!cancelled) setResolved(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}, [threadId]);
|
||||||
|
|
||||||
|
if (!resolved) {
|
||||||
|
return <div className="size-full bg-background" aria-busy="true" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (threadId && agentId) {
|
||||||
|
return (
|
||||||
|
<Navigate
|
||||||
|
replace
|
||||||
|
to={`/page/workspace/agents/${encodeURIComponent(agentId)}/chats/${threadId}${location.search}`}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <ChatPage />;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* `chats_iframe` is reserved for the Brainy postMessage protocol. `from=iframe`
|
* `chats_iframe` is reserved for the Brainy postMessage protocol. `from=iframe`
|
||||||
* and `chatMode=iframe` opt into this page; ordinary iframe/Wujie embedding
|
* and `chatMode=iframe` opt into this page; ordinary iframe/Wujie embedding
|
||||||
|
|||||||
Reference in new issue
Block a user