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
@@ -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