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 {
|
||||
useDeleteThread,
|
||||
useRenameThread,
|
||||
useThreads,
|
||||
useThreadsPage,
|
||||
useToggleThreadFavorite,
|
||||
useToggleThreadPin,
|
||||
} from "@/core/threads/hooks";
|
||||
@@ -351,8 +351,8 @@ export function AgentChatSidebar({ activeAgentId }: { activeAgentId?: string })
|
||||
}
|
||||
|
||||
/**
|
||||
* 「最近的对话」面板(参考首页 GeneralQaSidebar 的「最近的对话」区块)。
|
||||
* 跨智能体的近期会话,点击打开;支持重命名/置顶/收藏/分享/导出/删除。
|
||||
* 当前智能体的「最近对话」面板(参考首页 GeneralQaSidebar 的「最近的对话」区块)。
|
||||
* 按 agent_id 在服务端过滤;支持重命名/置顶/收藏/分享/导出/删除。
|
||||
*/
|
||||
function RecentChatsPane({ activeAgentId }: { activeAgentId?: string }) {
|
||||
const { t } = useI18n();
|
||||
@@ -361,7 +361,12 @@ function RecentChatsPane({ activeAgentId }: { activeAgentId?: string }) {
|
||||
const pathOf = usePathOfThread();
|
||||
|
||||
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: renameThread } = useRenameThread();
|
||||
const { toggle: togglePin } = useToggleThreadPin();
|
||||
@@ -373,8 +378,8 @@ function RecentChatsPane({ activeAgentId }: { activeAgentId?: string }) {
|
||||
const [renameValue, setRenameValue] = useState("");
|
||||
const [shareThreadId, setShareThreadId] = useState<string | null>(null);
|
||||
|
||||
// 排除调度/会商/报告结构&写作配置向导等系统会话,与首页一致;
|
||||
// 智能体创建(bootstrap)会话保留展示(带机器人图标、点击回创建页);置顶优先。
|
||||
// 服务端先按当前智能体过滤;客户端再排除调度/会商/报告结构&写作配置向导等
|
||||
// 系统会话。智能体创建(bootstrap)会话保留展示(带机器人图标、点击回创建页);置顶优先。
|
||||
const visibleThreads = useMemo(
|
||||
() => sortThreadsPinnedFirst(threads.filter((thread) => !isHiddenFromRecentChats(thread))),
|
||||
[threads],
|
||||
@@ -479,7 +484,11 @@ function RecentChatsPane({ activeAgentId }: { activeAgentId?: string }) {
|
||||
) : (
|
||||
filteredThreads.map((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 pinned = isThreadPinned(thread);
|
||||
const favorited = isThreadFavorite(thread);
|
||||
|
||||
@@ -758,7 +758,7 @@ export function InputBox({
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{!frontendRagActive && !hideContextTools && !hideMemoryTool && <Tooltip
|
||||
{!frontendRagActive && !hideContextTools && <Tooltip
|
||||
content={
|
||||
context.writing_mode
|
||||
? "写作模式已开启:报告由深度研究管线按「通用分析结构」撰写,优先使用对话中已检索的资料"
|
||||
@@ -813,7 +813,7 @@ export function InputBox({
|
||||
<span className="text-xs font-normal">文件</span>
|
||||
</PromptInputButton>
|
||||
</Tooltip>}
|
||||
{!frontendRagActive && !hideContextTools && <Tooltip
|
||||
{!frontendRagActive && !hideContextTools && !hideMemoryTool && <Tooltip
|
||||
content={
|
||||
memoryInjectionEnabled
|
||||
? "记忆注入已开启:对话前会注入历史记忆"
|
||||
|
||||
@@ -27,6 +27,18 @@ function pickAgentRef(
|
||||
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(
|
||||
thread: ThreadRouteTarget,
|
||||
context?: ThreadAgentRef | null,
|
||||
@@ -36,7 +48,7 @@ export function pathOfThread(
|
||||
const agentRef =
|
||||
typeof thread === "string"
|
||||
? pickAgentRef(context, null)
|
||||
: pickAgentRef(thread.context, thread.metadata);
|
||||
: agentRefOfThread(thread) ?? pickAgentRef(context, null);
|
||||
|
||||
return agentRef
|
||||
? `${base}/agents/${encodeURIComponent(agentRef)}/chats/${threadId}`
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import {
|
||||
BotIcon,
|
||||
Compass,
|
||||
PanelLeftOpenIcon,
|
||||
PanelRightOpenIcon,
|
||||
PlusSquare,
|
||||
} 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 { Button } from "@/components/ui/button";
|
||||
import { useSidebarSafe } from "@/components/ui/sidebar";
|
||||
import { AgentChatSidebar } from "@/components/workspace/agents/agent-chat-sidebar";
|
||||
import { AgentDetailPanel } from "@/components/workspace/agents/agent-detail-panel";
|
||||
import { AgentQuestionChips } from "@/components/workspace/agents/agent-question-chips";
|
||||
@@ -225,6 +227,7 @@ export default function AgentChatPage({
|
||||
const { t } = useI18n();
|
||||
const [showFollowups, setShowFollowups] = useState(false);
|
||||
const router = useRouter();
|
||||
const workspaceSidebar = useSidebarSafe();
|
||||
|
||||
const { agent_id: agentIdFromPath } = useParams<{
|
||||
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">
|
||||
{/* 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">
|
||||
{/* 工作区左栏收起后,在智能体聊天区左上角提供展开入口。 */}
|
||||
{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">
|
||||
<BotIcon className="text-primary h-3.5 w-3.5" />
|
||||
<span
|
||||
@@ -881,23 +901,6 @@ export default function AgentChatPage({
|
||||
开始回答
|
||||
</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+),
|
||||
窄屏面板走抽屉交互;嵌入态面板固定渲染,不显示该开关。 */}
|
||||
@@ -919,6 +922,23 @@ export default function AgentChatPage({
|
||||
enabled={tokenUsageEnabled}
|
||||
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} />
|
||||
<ArtifactTrigger />
|
||||
</div>
|
||||
@@ -1101,7 +1121,6 @@ export default function AgentChatPage({
|
||||
onAskQuestion={(text) => handleSubmit({ text, files: [] })}
|
||||
collapsed={detailCollapsed}
|
||||
onCollapsedChange={setDetailCollapsed}
|
||||
showRecentChats={!taskCtxOn}
|
||||
compactAppearanceActions={compactAppearanceActions}
|
||||
/>
|
||||
</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 { getStoredAuth } from '@/core/auth';
|
||||
import { getAPIClient } from '@/core/api';
|
||||
import { isSentimentAgentVisible } from '@/core/sentiment-agent/config';
|
||||
import {
|
||||
resolveChatsIframeEntryRedirect,
|
||||
@@ -25,6 +26,7 @@ import { ArtifactsProvider } from '@/components/workspace/artifacts';
|
||||
import { DocumentRewritePresentationProvider } from '@/core/document-rewrite/context';
|
||||
|
||||
import { RouteBaseProvider } from '@/core/route-base';
|
||||
import { agentRefOfThread } from '@/core/threads/utils';
|
||||
|
||||
import ChatsPage from './ChatsPage';
|
||||
import ChatPage from './ChatPage';
|
||||
@@ -341,12 +343,80 @@ function ChatThreadEntryRoute({
|
||||
return (
|
||||
<WorkspaceLayout {...layoutProps} defaultHistorySidebarOpen>
|
||||
<ChatRuntime>
|
||||
<ChatPage />
|
||||
<AgentAwareChatThreadContent />
|
||||
</ChatRuntime>
|
||||
</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`
|
||||
* and `chatMode=iframe` opt into this page; ordinary iframe/Wujie embedding
|
||||
|
||||
Reference in new issue
Block a user