feat: 支持按 Agent 筛选对话
This commit is contained in:
1 parent
52e0b0dbb4
commit
076146fa81
1 file changed
+72
-11
@@ -46,7 +46,7 @@ import {
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { api } from "@/lib/api";
|
||||
import { shouldSubmitOnKey, useChatSendMode } from "@/lib/chat-send-mode";
|
||||
@@ -908,6 +908,7 @@ export default function ChatPage() {
|
||||
const [agents, setAgents] = React.useState<Agent[]>([]);
|
||||
const [profiles, setProfiles] = React.useState<LLMProfile[]>([]);
|
||||
const [convs, setConvs] = React.useState<Conversation[]>([]);
|
||||
const [agentFilter, setAgentFilter] = React.useState<string | null>(null);
|
||||
const [selectedId, setSelectedId] = React.useState<number | null>(null);
|
||||
const [renamingId, setRenamingId] = React.useState<number | null>(null);
|
||||
const [renameText, setRenameText] = React.useState("");
|
||||
@@ -978,17 +979,44 @@ export default function ChatPage() {
|
||||
|
||||
const selected = React.useMemo(() => convs.find((c) => c.id === selectedId) ?? null, [convs, selectedId]);
|
||||
const agentByKey = React.useMemo(() => new Map(agents.map((agent) => [agent.key, agent])), [agents]);
|
||||
const filteredConversations = React.useMemo(
|
||||
() => (agentFilter === null ? convs : convs.filter((conversation) => conversation.agent_key === agentFilter)),
|
||||
[convs, agentFilter],
|
||||
);
|
||||
const visibleConversations = React.useMemo(
|
||||
() => convs.slice(0, visibleConversationCount),
|
||||
[convs, visibleConversationCount],
|
||||
() => filteredConversations.slice(0, visibleConversationCount),
|
||||
[filteredConversations, visibleConversationCount],
|
||||
);
|
||||
// conversation agents: custom agents + conversational built-ins (role=assistant,
|
||||
// e.g. Auto / 渗透测试). The orchestration built-ins (goals/planner/mainagent/worker)
|
||||
// are task-specific and stay hidden from the chat page.
|
||||
const chatAgents = React.useMemo(() => agents.filter((a) => !a.builtin || a.role === "assistant"), [agents]);
|
||||
const agentFilterOptions = React.useMemo(() => {
|
||||
const counts = new Map<string, number>();
|
||||
for (const conversation of convs) {
|
||||
counts.set(conversation.agent_key, (counts.get(conversation.agent_key) ?? 0) + 1);
|
||||
}
|
||||
// Include historical sources even if their Agent has since been disabled or deleted.
|
||||
const keys = new Set([...chatAgents.map((agent) => agent.key), ...counts.keys()]);
|
||||
if (agentFilter !== null) keys.add(agentFilter);
|
||||
return [...keys]
|
||||
.map((key) => ({ key, name: agentByKey.get(key)?.name || key, count: counts.get(key) ?? 0 }))
|
||||
.sort((a, b) => a.name.localeCompare(b.name, "zh-CN"));
|
||||
}, [convs, chatAgents, agentByKey, agentFilter]);
|
||||
const conversationCountLabel =
|
||||
agentFilter === null ? `共 ${convs.length} 个` : `${filteredConversations.length} / ${convs.length} 个`;
|
||||
|
||||
function changeAgentFilter(key: string | null) {
|
||||
setAgentFilter(key);
|
||||
setVisibleConversationCount(CONVERSATION_LIST_PAGE);
|
||||
setSelectedConversationIds(new Set());
|
||||
setBulkDeleteOpen(false);
|
||||
setRenamingId(null);
|
||||
}
|
||||
|
||||
const selectedConversationCount = selectedConversationIds.size;
|
||||
const allConversationsSelected = convs.length > 0 && selectedConversationCount === convs.length;
|
||||
const allConversationsSelected =
|
||||
filteredConversations.length > 0 && selectedConversationCount === filteredConversations.length;
|
||||
const someConversationsSelected = selectedConversationCount > 0 && !allConversationsSelected;
|
||||
let conversationHeaderChecked: boolean | "indeterminate" = false;
|
||||
if (allConversationsSelected) conversationHeaderChecked = true;
|
||||
@@ -1004,7 +1032,9 @@ export default function ChatPage() {
|
||||
}, []);
|
||||
|
||||
function toggleAllConversations(checked: boolean) {
|
||||
setSelectedConversationIds(checked ? new Set(convs.map((conversation) => conversation.id)) : new Set());
|
||||
setSelectedConversationIds(
|
||||
checked ? new Set(filteredConversations.map((conversation) => conversation.id)) : new Set(),
|
||||
);
|
||||
}
|
||||
|
||||
function exitSelectionMode() {
|
||||
@@ -1117,16 +1147,37 @@ export default function ChatPage() {
|
||||
<Button size="sm" className="w-full" onClick={() => setSelectedId(null)}>
|
||||
<PlusIcon /> 新建对话
|
||||
</Button>
|
||||
<Select
|
||||
value={agentFilter === null ? "all" : `agent:${agentFilter}`}
|
||||
onValueChange={(value) => changeAgentFilter(value === "all" ? null : value.slice(6))}
|
||||
disabled={bulkDeleting}
|
||||
>
|
||||
<SelectTrigger size="sm" className="w-full min-w-0" aria-label="按 Agent 筛选对话">
|
||||
<Bot />
|
||||
<SelectValue placeholder="全部 Agent" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectItem value="all">全部 Agent</SelectItem>
|
||||
{agentFilterOptions.map((agent) => (
|
||||
<SelectItem key={agent.key} value={`agent:${agent.key}`}>
|
||||
{agent.name}({agent.count})
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{convs.length > 0 &&
|
||||
(selectionMode ? (
|
||||
<div className="flex items-center gap-2 px-1">
|
||||
<Checkbox
|
||||
checked={conversationHeaderChecked}
|
||||
onCheckedChange={(checked) => toggleAllConversations(checked === true)}
|
||||
aria-label="选择全部对话"
|
||||
aria-label="选择当前筛选的全部对话"
|
||||
disabled={filteredConversations.length === 0 || bulkDeleting}
|
||||
/>
|
||||
<span className="text-muted-foreground min-w-0 flex-1 text-xs tabular-nums">
|
||||
{selectedConversationCount > 0 ? `已选 ${selectedConversationCount} 个` : `共 ${convs.length} 个`}
|
||||
{selectedConversationCount > 0 ? `已选 ${selectedConversationCount} 个` : conversationCountLabel}
|
||||
</span>
|
||||
{selectedConversationCount > 0 && (
|
||||
<Button
|
||||
@@ -1146,13 +1197,14 @@ export default function ChatPage() {
|
||||
) : (
|
||||
<div className="flex items-center gap-2 px-1">
|
||||
<span className="text-muted-foreground min-w-0 flex-1 text-xs tabular-nums">
|
||||
共 {convs.length} 个
|
||||
{conversationCountLabel}
|
||||
</span>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="text-muted-foreground"
|
||||
onClick={() => setSelectionMode(true)}
|
||||
disabled={filteredConversations.length === 0}
|
||||
>
|
||||
<ListChecksIcon data-icon="inline-start" />
|
||||
多选
|
||||
@@ -1160,9 +1212,17 @@ export default function ChatPage() {
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<ScrollArea type="auto" className="min-h-0 min-w-0 flex-1 [&_[data-slot=scroll-area-viewport]>div]:block!">
|
||||
<ScrollArea
|
||||
key={agentFilter === null ? "all" : `agent:${agentFilter}`}
|
||||
type="auto"
|
||||
className="min-h-0 min-w-0 flex-1 [&_[data-slot=scroll-area-viewport]>div]:block!"
|
||||
>
|
||||
<div className="flex min-w-0 flex-col gap-0.5 p-2">
|
||||
{convs.length === 0 && <p className="text-muted-foreground px-2 py-6 text-center text-xs">暂无对话</p>}
|
||||
{filteredConversations.length === 0 && (
|
||||
<p className="text-muted-foreground px-2 py-6 text-center text-xs">
|
||||
{agentFilter === null ? "暂无对话" : "该 Agent 暂无对话"}
|
||||
</p>
|
||||
)}
|
||||
{visibleConversations.map((c) => (
|
||||
<ConversationItem
|
||||
key={c.id}
|
||||
@@ -1183,7 +1243,7 @@ export default function ChatPage() {
|
||||
onSelectedForDeleteChange={toggleConversationSelected}
|
||||
/>
|
||||
))}
|
||||
{visibleConversationCount < convs.length && (
|
||||
{visibleConversationCount < filteredConversations.length && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
@@ -1215,6 +1275,7 @@ export default function ChatPage() {
|
||||
agents={chatAgents}
|
||||
profiles={profiles}
|
||||
onStarted={(c, pending) => {
|
||||
if (agentFilter !== null && agentFilter !== c.agent_key) changeAgentFilter(null);
|
||||
// Insert the new conversation immediately so `selected` resolves to
|
||||
// it on this render (switching to ChatView right away, before the
|
||||
// async reloadConvs lands); reloadConvs then reconciles titles etc.
|
||||
|
||||
Reference in new issue
Block a user