feat: 智能体详情新增「最近对话」列表,完善推荐问题与任务深链配置
- 新增 AgentRecentChats 组件,在智能体详情面板底部按 agent_id 展示最近对话 - 调整问答/智能体侧边栏与最近对话列表展示逻辑 - 补充 dashboard-input、task-deeplink 相关配置与线程工具方法 - 后端推荐问题接口标签处理及 position_sync 微调,新增相关测试 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
16ba5deb0e
commit
32476c7ad4
18 files changed
+457
-60
No files matched your search
@@ -70,6 +70,8 @@ import {
|
||||
} from "@/core/threads/hooks";
|
||||
import type { AgentThread, AgentThreadState } from "@/core/threads/types";
|
||||
import {
|
||||
bootstrapResumePath,
|
||||
isBootstrapThread,
|
||||
isThreadFavorite,
|
||||
isThreadPinned,
|
||||
sortThreadsPinnedFirst,
|
||||
@@ -338,14 +340,14 @@ function RecentChatsPane({ activeAgentId }: { activeAgentId?: string }) {
|
||||
const [renameValue, setRenameValue] = useState("");
|
||||
const [shareThreadId, setShareThreadId] = useState<string | null>(null);
|
||||
|
||||
// 排除调度/引导/会商类会话,与首页一致;置顶优先。
|
||||
// 排除调度/会商类系统会话,与首页一致;智能体创建(bootstrap)会话保留展示
|
||||
// (带机器人图标、点击回创建页);置顶优先。
|
||||
const visibleThreads = useMemo(
|
||||
() =>
|
||||
sortThreadsPinnedFirst(
|
||||
threads.filter(
|
||||
(thread) =>
|
||||
thread.metadata?.thread_type !== "scheduler" &&
|
||||
thread.metadata?.thread_type !== "bootstrap" &&
|
||||
thread.metadata?.thread_type !== "roundtable",
|
||||
),
|
||||
),
|
||||
@@ -450,7 +452,8 @@ function RecentChatsPane({ activeAgentId }: { activeAgentId?: string }) {
|
||||
</div>
|
||||
) : (
|
||||
filteredThreads.map((thread) => {
|
||||
const href = pathOf(thread);
|
||||
const isBoot = isBootstrapThread(thread);
|
||||
const href = isBoot ? bootstrapResumePath(thread.thread_id) : pathOf(thread);
|
||||
const isActive = href === pathname;
|
||||
const pinned = isThreadPinned(thread);
|
||||
const favorited = isThreadFavorite(thread);
|
||||
@@ -468,12 +471,22 @@ function RecentChatsPane({ activeAgentId }: { activeAgentId?: string }) {
|
||||
: "text-muted-foreground hover:bg-accent/60 hover:text-foreground",
|
||||
)}
|
||||
>
|
||||
<MessageSquare
|
||||
className={cn(
|
||||
"size-3.5 shrink-0",
|
||||
isActive ? "text-primary" : "text-muted-foreground/70",
|
||||
)}
|
||||
/>
|
||||
{isBoot ? (
|
||||
<Bot
|
||||
className={cn(
|
||||
"size-3.5 shrink-0",
|
||||
isActive ? "text-primary" : "text-muted-foreground/70",
|
||||
)}
|
||||
aria-label="智能体创建会话"
|
||||
/>
|
||||
) : (
|
||||
<MessageSquare
|
||||
className={cn(
|
||||
"size-3.5 shrink-0",
|
||||
isActive ? "text-primary" : "text-muted-foreground/70",
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{pinned && (
|
||||
<Pin className="text-muted-foreground/70 size-3 shrink-0" />
|
||||
)}
|
||||
|
||||
@@ -43,6 +43,7 @@ import {
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { AgentQuestionEditor } from "@/components/workspace/agents/agent-question-editor";
|
||||
import { AgentRecentChats } from "@/components/workspace/agents/agent-recent-chats";
|
||||
import { useArtifactsOptional } from "@/components/workspace/artifacts";
|
||||
import { EnabledSkillList } from "@/components/workspace/agents/enabled-skill-list";
|
||||
import { SkillPickerSheet } from "@/components/workspace/agents/skill-picker-sheet";
|
||||
@@ -133,6 +134,7 @@ export function AgentDetailPanel({
|
||||
onAskQuestion,
|
||||
disabled,
|
||||
defaultCollapsed,
|
||||
showRecentChats,
|
||||
}: {
|
||||
agent: Agent | null | undefined;
|
||||
agentName: string;
|
||||
@@ -140,6 +142,8 @@ export function AgentDetailPanel({
|
||||
disabled?: boolean;
|
||||
/** 初始收起(任务工作区进入时默认收起智能体信息面板)。 */
|
||||
defaultCollapsed?: boolean;
|
||||
/** 在面板底部展示「有关此智能体的最近对话」(仅智能体问答页开启,任务工作区不展示)。 */
|
||||
showRecentChats?: boolean;
|
||||
}) {
|
||||
const router = useRouter();
|
||||
const [collapsed, setCollapsed] = useState(defaultCollapsed ?? false);
|
||||
@@ -794,6 +798,9 @@ export function AgentDetailPanel({
|
||||
onViewDetail={setDetailSkillName}
|
||||
/>
|
||||
</Section>
|
||||
|
||||
{/* ── 最近对话(右下角,仅智能体问答页展示,编辑态隐藏)──────────── */}
|
||||
{showRecentChats && !editing && <AgentRecentChats agentId={agent.id} />}
|
||||
</div>
|
||||
);
|
||||
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
"use client";
|
||||
|
||||
import { MessageSquareIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
|
||||
import { useThreadsPage } from "@/core/threads/hooks";
|
||||
import { pathOfThread, titleOfThread } from "@/core/threads/utils";
|
||||
import { desensitize } from "@/lib/desensitize";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const RECENT_LIMIT = 8;
|
||||
|
||||
/**
|
||||
* 「有关此智能体的最近对话」—— 渲染在智能体详情面板底部(右下角)。
|
||||
* 按 `metadata.agent_id` 过滤出该智能体的会话,点击直接打开对应对话。
|
||||
*/
|
||||
export function AgentRecentChats({ agentId }: { agentId: string }) {
|
||||
const pathname = usePathname();
|
||||
const { data } = useThreadsPage({
|
||||
page: 1,
|
||||
pageSize: RECENT_LIMIT,
|
||||
metadata: { agent_id: agentId },
|
||||
});
|
||||
// 该智能体的会话里同样排除调度 / 引导 / 会商类系统会话。
|
||||
const threads = (data?.threads ?? []).filter(
|
||||
(th) =>
|
||||
th.metadata?.thread_type !== "bootstrap" &&
|
||||
th.metadata?.thread_type !== "scheduler" &&
|
||||
th.metadata?.thread_type !== "roundtable",
|
||||
);
|
||||
|
||||
return (
|
||||
<section className="bg-card/60 rounded-xl border p-4">
|
||||
<div className="mb-3 flex items-center justify-between gap-2">
|
||||
<h4 className="text-foreground flex items-center gap-1.5 text-sm font-semibold">
|
||||
<MessageSquareIcon className="size-3.5" />
|
||||
最近对话
|
||||
</h4>
|
||||
<span className="text-muted-foreground text-xs tabular-nums">
|
||||
{threads.length} 个
|
||||
</span>
|
||||
</div>
|
||||
{threads.length === 0 ? (
|
||||
<p className="text-muted-foreground/60 text-sm">暂无对话</p>
|
||||
) : (
|
||||
<div className="flex flex-col gap-1">
|
||||
{threads.map((thread) => {
|
||||
const href = pathOfThread(thread);
|
||||
const isActive = href === pathname;
|
||||
return (
|
||||
<Link
|
||||
key={thread.thread_id}
|
||||
href={href}
|
||||
title={desensitize(titleOfThread(thread, "新对话"))}
|
||||
className={cn(
|
||||
"flex min-w-0 items-center gap-2 rounded-lg px-2 py-1.5 text-sm transition-colors",
|
||||
isActive
|
||||
? "bg-primary/5 text-primary font-medium"
|
||||
: "text-muted-foreground hover:bg-accent hover:text-foreground",
|
||||
)}
|
||||
>
|
||||
<MessageSquareIcon
|
||||
className={cn(
|
||||
"size-3.5 shrink-0",
|
||||
isActive ? "text-primary" : "text-muted-foreground/70",
|
||||
)}
|
||||
/>
|
||||
<span className="truncate">
|
||||
{desensitize(titleOfThread(thread, "新对话"))}
|
||||
</span>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -75,8 +75,10 @@ import {
|
||||
} from "@/core/threads/hooks";
|
||||
import type { AgentThread, AgentThreadState } from "@/core/threads/types";
|
||||
import {
|
||||
isBootstrapThread,
|
||||
isThreadFavorite,
|
||||
isThreadPinned,
|
||||
listPathOfThread,
|
||||
pathOfThread,
|
||||
sortThreadsPinnedFirst,
|
||||
titleOfThread,
|
||||
@@ -213,11 +215,12 @@ export function GeneralQaSidebar() {
|
||||
|
||||
const visibleThreads = useMemo(
|
||||
() =>
|
||||
// 智能体创建(bootstrap)会话也展示(带机器人图标、点击回创建页);
|
||||
// 仅排除调度 / 会商类系统会话。
|
||||
sortThreadsPinnedFirst(
|
||||
threads.filter(
|
||||
(thread) =>
|
||||
thread.metadata?.thread_type !== "scheduler" &&
|
||||
thread.metadata?.thread_type !== "bootstrap" &&
|
||||
thread.metadata?.thread_type !== "roundtable",
|
||||
),
|
||||
),
|
||||
@@ -249,9 +252,9 @@ export function GeneralQaSidebar() {
|
||||
let nextThreadPath = pathOfThread("new", { agent_id: agentIdFromPath });
|
||||
if (threadIndex > -1) {
|
||||
if (threads[threadIndex + 1]) {
|
||||
nextThreadPath = pathOfThread(threads[threadIndex + 1]!);
|
||||
nextThreadPath = listPathOfThread(threads[threadIndex + 1]!);
|
||||
} else if (threads[threadIndex - 1]) {
|
||||
nextThreadPath = pathOfThread(threads[threadIndex - 1]!);
|
||||
nextThreadPath = listPathOfThread(threads[threadIndex - 1]!);
|
||||
}
|
||||
}
|
||||
void router.push(nextThreadPath);
|
||||
@@ -417,7 +420,9 @@ export function GeneralQaSidebar() {
|
||||
</div>
|
||||
) : (
|
||||
filteredThreads.map((thread) => {
|
||||
const isActive = pathOfThread(thread) === pathname;
|
||||
const href = listPathOfThread(thread);
|
||||
const isActive = href === pathname;
|
||||
const isBoot = isBootstrapThread(thread);
|
||||
const pinned = isThreadPinned(thread);
|
||||
const favorited = isThreadFavorite(thread);
|
||||
return (
|
||||
@@ -426,7 +431,7 @@ export function GeneralQaSidebar() {
|
||||
className="group/item relative flex items-center"
|
||||
>
|
||||
<Link
|
||||
href={pathOfThread(thread)}
|
||||
href={href}
|
||||
className={cn(
|
||||
"relative flex h-9 min-w-0 flex-1 items-center gap-2 rounded-lg px-2.5 text-[13px] transition-colors",
|
||||
isActive
|
||||
@@ -434,12 +439,22 @@ export function GeneralQaSidebar() {
|
||||
: "text-muted-foreground hover:bg-accent/60 hover:text-foreground",
|
||||
)}
|
||||
>
|
||||
<MessageSquare
|
||||
className={cn(
|
||||
"size-3.5 shrink-0",
|
||||
isActive ? "text-primary" : "text-muted-foreground/70",
|
||||
)}
|
||||
/>
|
||||
{isBoot ? (
|
||||
<Bot
|
||||
className={cn(
|
||||
"size-3.5 shrink-0",
|
||||
isActive ? "text-primary" : "text-muted-foreground/70",
|
||||
)}
|
||||
aria-label="智能体创建会话"
|
||||
/>
|
||||
) : (
|
||||
<MessageSquare
|
||||
className={cn(
|
||||
"size-3.5 shrink-0",
|
||||
isActive ? "text-primary" : "text-muted-foreground/70",
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{pinned && (
|
||||
<Pin className="size-3 shrink-0 text-muted-foreground/70" />
|
||||
)}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Bot,
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
Download,
|
||||
@@ -64,8 +65,10 @@ import {
|
||||
} from "@/core/threads/hooks";
|
||||
import type { AgentThread, AgentThreadState } from "@/core/threads/types";
|
||||
import {
|
||||
isBootstrapThread,
|
||||
isThreadFavorite,
|
||||
isThreadPinned,
|
||||
listPathOfThread,
|
||||
pathOfThread,
|
||||
sortThreadsPinnedFirst,
|
||||
titleOfThread,
|
||||
@@ -130,9 +133,9 @@ export function RecentChatList({
|
||||
});
|
||||
if (threadIndex > -1) {
|
||||
if (threads[threadIndex + 1]) {
|
||||
nextThreadPath = pathOfThread(threads[threadIndex + 1]!);
|
||||
nextThreadPath = listPathOfThread(threads[threadIndex + 1]!);
|
||||
} else if (threads[threadIndex - 1]) {
|
||||
nextThreadPath = pathOfThread(threads[threadIndex - 1]!);
|
||||
nextThreadPath = listPathOfThread(threads[threadIndex - 1]!);
|
||||
}
|
||||
}
|
||||
void router.push(nextThreadPath);
|
||||
@@ -189,11 +192,12 @@ export function RecentChatList({
|
||||
[t],
|
||||
);
|
||||
|
||||
// 智能体创建(bootstrap)会话也展示在常用对话里(带机器人图标、点击回创建页);
|
||||
// 仅排除调度 / 会商类系统会话。
|
||||
const visibleThreads = sortThreadsPinnedFirst(
|
||||
threads.filter(
|
||||
(thread) =>
|
||||
thread.metadata?.thread_type !== "scheduler" &&
|
||||
thread.metadata?.thread_type !== "bootstrap" &&
|
||||
thread.metadata?.thread_type !== "roundtable",
|
||||
),
|
||||
);
|
||||
@@ -239,7 +243,9 @@ export function RecentChatList({
|
||||
<SidebarMenu>
|
||||
<div className="flex w-full flex-col gap-0.5">
|
||||
{shownThreads.map((thread) => {
|
||||
const isActive = pathOfThread(thread) === pathname;
|
||||
const href = listPathOfThread(thread);
|
||||
const isActive = href === pathname;
|
||||
const isBoot = isBootstrapThread(thread);
|
||||
const pinned = isThreadPinned(thread);
|
||||
const favorited = isThreadFavorite(thread);
|
||||
return (
|
||||
@@ -261,17 +267,29 @@ export function RecentChatList({
|
||||
>
|
||||
<Link
|
||||
className="flex w-full items-center gap-1 whitespace-nowrap group-hover/side-menu-item:overflow-hidden"
|
||||
href={pathOfThread(thread)}
|
||||
href={href}
|
||||
>
|
||||
{compact && (
|
||||
<MessageSquare
|
||||
{isBoot ? (
|
||||
<Bot
|
||||
className={
|
||||
"size-3.5 shrink-0 " +
|
||||
(isActive
|
||||
? "text-[var(--primary)]"
|
||||
: "text-muted-foreground/70")
|
||||
}
|
||||
aria-label="智能体创建会话"
|
||||
/>
|
||||
) : (
|
||||
compact && (
|
||||
<MessageSquare
|
||||
className={
|
||||
"size-3.5 shrink-0 " +
|
||||
(isActive
|
||||
? "text-[var(--primary)]"
|
||||
: "text-muted-foreground/70")
|
||||
}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
{pinned && (
|
||||
<Pin className="size-3 shrink-0 text-muted-foreground/70" />
|
||||
|
||||
Reference in new issue
Block a user