@nextclaw/ui 0.11.23 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -0
- package/dist/assets/{ChannelsList-DVDu1xvz.js → ChannelsList-NKNKsf1J.js} +1 -1
- package/dist/assets/ChatPage-p23OnnEI.js +43 -0
- package/dist/assets/DocBrowser-C8b2uPgL.js +1 -0
- package/dist/assets/{DocBrowser-BmtBLFU0.js → DocBrowser-DxdSujSc.js} +1 -1
- package/dist/assets/{DocBrowserContext-YIKkPb76.js → DocBrowserContext-CQ-8jMha.js} +1 -1
- package/dist/assets/{LogoBadge-F7ZWdxLT.js → LogoBadge-D-KQIN4U.js} +1 -1
- package/dist/assets/{MarketplacePage-Buo9HrOz.js → MarketplacePage-CRNvxtvx.js} +2 -2
- package/dist/assets/MarketplacePage-GGkEXowp.js +1 -0
- package/dist/assets/{McpMarketplacePage-JnkYwK7p.js → McpMarketplacePage-Cu7GmCcc.js} +2 -2
- package/dist/assets/{ModelConfig-BYRhgp0c.js → ModelConfig-CEpx9fro.js} +1 -1
- package/dist/assets/{ProvidersList-DmLyyHvX.js → ProvidersList-BWbUb7-2.js} +1 -1
- package/dist/assets/{RemoteAccessPage-CDSSvH7Z.js → RemoteAccessPage-NsawrZb0.js} +1 -1
- package/dist/assets/RuntimeConfig-BJHBsVTd.js +1 -0
- package/dist/assets/{SearchConfig-D5f1EkLE.js → SearchConfig-BsaX_WYy.js} +1 -1
- package/dist/assets/{SecretsConfig-D61IKcYt.js → SecretsConfig-CgDZOd3w.js} +1 -1
- package/dist/assets/{SessionsConfig-BRIxVTEv.js → SessionsConfig-Dd-KM7F7.js} +2 -2
- package/dist/assets/{book-open-CXoF5nQC.js → book-open-FnK2xCQd.js} +1 -1
- package/dist/assets/chat-session-display-BD_AN71I.js +1 -0
- package/dist/assets/{chunk-JZWAC4HX-CvRWvTy5.js → chunk-JZWAC4HX-B5l0hr_u.js} +1 -1
- package/dist/assets/{config-DJswxxE8.js → config-JKmXfZ3q.js} +1 -1
- package/dist/assets/{createLucideIcon-CjGHOWb6.js → createLucideIcon-o1WWhwhd.js} +1 -1
- package/dist/assets/{dist-nqTTbVdA.js → dist-C_moWYv7.js} +1 -1
- package/dist/assets/{dist-Cl2QB-2y.js → dist-DazA6Wd_.js} +1 -1
- package/dist/assets/{external-link-tIO7zING.js → external-link-BKje3SiD.js} +1 -1
- package/dist/assets/{hash-JWUyl1pT.js → hash-DfW4DT8O.js} +1 -1
- package/dist/assets/i18n-BK1w-oBy.js +1 -0
- package/dist/assets/index-BZaB1TqM.js +6 -0
- package/dist/assets/index-DaR9igPC.css +1 -0
- package/dist/assets/{label-BIpeNu4r.js → label-BzDWmdOe.js} +1 -1
- package/dist/assets/loader-circle-DdZPxBUz.js +1 -0
- package/dist/assets/{logos-DThdM9lk.js → logos-CTLlde_T.js} +1 -1
- package/dist/assets/{page-layout-D3Xo605Z.js → page-layout-BagR3t59.js} +1 -1
- package/dist/assets/plus-DP2PSCPO.js +1 -0
- package/dist/assets/{popover-BJRUGA_H.js → popover-5DWhNfd4.js} +1 -1
- package/dist/assets/{provider-models-bz5y28rq.js → provider-models-DJ29qHuA.js} +1 -1
- package/dist/assets/{react-7ZHqQtEV.js → react-C3yu5yge.js} +1 -1
- package/dist/assets/{refresh-ccw-CC6-_QuL.js → refresh-ccw-BAJf-h7w.js} +1 -1
- package/dist/assets/{save-DJM5RRWW.js → save-aa6z4GJL.js} +1 -1
- package/dist/assets/search-pD6ZwQYF.js +1 -0
- package/dist/assets/{security-config-DbUyWcQz.js → security-config-DRDxrApx.js} +1 -1
- package/dist/assets/{select-DSkTc61S.js → select-BHJPiJWt.js} +1 -1
- package/dist/assets/skeleton-D6kCk9Y6.js +1 -0
- package/dist/assets/{status-dot-LNBlDu3q.js → status-dot-DUwsTIdv.js} +1 -1
- package/dist/assets/{switch-Bo-Y46HZ.js → switch-B6nCfcOB.js} +1 -1
- package/dist/assets/{tabs-custom-DXv507_2.js → tabs-custom-B57SMElx.js} +1 -1
- package/dist/assets/{trash-2-DFZmW6Gg.js → trash-2-CrjYH5ok.js} +1 -1
- package/dist/assets/{useConfirmDialog-COwYXDKm.js → useConfirmDialog-DsxnXB1B.js} +1 -1
- package/dist/assets/{useMutation-DrZrOgVL.js → useMutation-oTTWXgLG.js} +1 -1
- package/dist/assets/x-CTIQHUuD.js +1 -0
- package/dist/index.html +18 -18
- package/package.json +6 -6
- package/src/App.tsx +2 -0
- package/src/api/agents.ts +26 -0
- package/src/api/types.ts +23 -2
- package/src/components/agents/AgentsPage.test.tsx +70 -0
- package/src/components/agents/AgentsPage.tsx +353 -0
- package/src/components/chat/ChatConversationPanel.test.tsx +141 -13
- package/src/components/chat/ChatConversationPanel.tsx +29 -7
- package/src/components/chat/ChatSidebar.test.tsx +8 -0
- package/src/components/chat/ChatSidebar.tsx +11 -0
- package/src/components/chat/ChatWelcome.test.tsx +25 -0
- package/src/components/chat/ChatWelcome.tsx +47 -1
- package/src/components/chat/adapters/chat-message-part.adapter.ts +5 -0
- package/src/components/chat/adapters/chat-message-tool-agent-id.test.ts +102 -0
- package/src/components/chat/adapters/chat-message-tool-agent-id.ts +47 -0
- package/src/components/chat/adapters/chat-message.adapter.test.ts +6 -0
- package/src/components/chat/adapters/chat-message.session-request-tool-card.ts +24 -15
- package/src/components/chat/chat-child-session-panel.tsx +115 -49
- package/src/components/chat/chat-page-shell.tsx +8 -17
- package/src/components/chat/chat-session-route.ts +0 -14
- package/src/components/chat/chat-sidebar-session-item.tsx +16 -1
- package/src/components/chat/containers/chat-input-bar.container.tsx +2 -1
- package/src/components/chat/containers/chat-message-list.container.tsx +7 -0
- package/src/components/chat/ncp/NcpChatPage.tsx +58 -160
- package/src/components/chat/ncp/README.md +3 -0
- package/src/components/chat/ncp/ncp-chat-page-data.test.ts +2 -0
- package/src/components/chat/ncp/ncp-chat-thread.manager.ts +66 -10
- package/src/components/chat/ncp/ncp-session-adapter.test.ts +2 -0
- package/src/components/chat/ncp/ncp-session-adapter.ts +1 -0
- package/src/components/chat/ncp/page/ncp-chat-derived-state.ts +128 -0
- package/src/components/chat/ncp/session-conversation/use-ncp-child-session-tabs-view.ts +52 -0
- package/src/components/chat/ncp/session-conversation/use-ncp-session-conversation.test.tsx +101 -0
- package/src/components/chat/ncp/session-conversation/use-ncp-session-conversation.ts +72 -0
- package/src/components/chat/presenter/chat-presenter-context.tsx +1 -0
- package/src/components/chat/stores/chat-thread.store.ts +20 -6
- package/src/components/common/AgentAvatar.tsx +63 -0
- package/src/components/common/agent-identity/agent-identity-avatar.tsx +27 -0
- package/src/components/common/agent-identity/index.ts +3 -0
- package/src/components/common/agent-identity/use-agent-identity.ts +50 -0
- package/src/components/config/RuntimeConfig.tsx +13 -79
- package/src/components/config/runtime-config-agent.utils.ts +95 -0
- package/src/components/layout/AppLayout.tsx +3 -1
- package/src/components/layout/Sidebar.tsx +6 -1
- package/src/components/layout/app-layout.test.tsx +30 -0
- package/src/components/ui/tabs.tsx +2 -0
- package/src/hooks/README.md +3 -0
- package/src/hooks/agents/useAgents.ts +44 -0
- package/src/lib/i18n.agents.ts +66 -0
- package/src/lib/i18n.chat.ts +5 -0
- package/src/lib/i18n.ts +4 -4
- package/src/lib/ui-document-title.ts +1 -0
- package/dist/assets/ChatPage-Z9tRzm_n.js +0 -43
- package/dist/assets/DocBrowser-B9OaZjmg.js +0 -1
- package/dist/assets/MarketplacePage-D6rVQEQR.js +0 -1
- package/dist/assets/RuntimeConfig-v7a7Fe3x.js +0 -1
- package/dist/assets/chat-session-display-D0WpnuRZ.js +0 -1
- package/dist/assets/i18n-CDHMXlRZ.js +0 -1
- package/dist/assets/index-BuwbBgmT.js +0 -6
- package/dist/assets/index-bZ8cqQIS.css +0 -1
- package/dist/assets/loader-circle-Cs8XVFTw.js +0 -1
- package/dist/assets/plus-PHf8q-Ct.js +0 -1
- package/dist/assets/search-C91yH_6y.js +0 -1
- package/dist/assets/skeleton-Dzg-HOiN.js +0 -1
- package/dist/assets/x-D7Q1yqSF.js +0 -1
- /package/src/lib/{i18n → i18n-runtime}/i18n-language-owner.ts +0 -0
- /package/src/lib/{i18n → i18n-runtime}/i18n.path-picker.ts +0 -0
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
import { useMemo, useState } from 'react';
|
|
2
|
+
import { useNavigate } from 'react-router-dom';
|
|
3
|
+
import { useCreateAgent, useDeleteAgent, useAgents } from '@/hooks/agents/useAgents';
|
|
4
|
+
import { useChatSessionListStore } from '@/components/chat/stores/chat-session-list.store';
|
|
5
|
+
import { AgentAvatar } from '@/components/common/AgentAvatar';
|
|
6
|
+
import { Button } from '@/components/ui/button';
|
|
7
|
+
import { Card, CardContent } from '@/components/ui/card';
|
|
8
|
+
import {
|
|
9
|
+
Dialog,
|
|
10
|
+
DialogContent,
|
|
11
|
+
DialogDescription,
|
|
12
|
+
DialogFooter,
|
|
13
|
+
DialogHeader,
|
|
14
|
+
DialogTitle
|
|
15
|
+
} from '@/components/ui/dialog';
|
|
16
|
+
import { Input } from '@/components/ui/input';
|
|
17
|
+
import { PageLayout } from '@/components/layout/page-layout';
|
|
18
|
+
import { t } from '@/lib/i18n';
|
|
19
|
+
import { cn } from '@/lib/utils';
|
|
20
|
+
import { Bot, House, MessageCircle, Plus, ShieldCheck, Sparkles, Trash2 } from 'lucide-react';
|
|
21
|
+
|
|
22
|
+
type CreateFormState = {
|
|
23
|
+
id: string;
|
|
24
|
+
displayName: string;
|
|
25
|
+
description: string;
|
|
26
|
+
avatar: string;
|
|
27
|
+
home: string;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const EMPTY_FORM: CreateFormState = {
|
|
31
|
+
id: '',
|
|
32
|
+
displayName: '',
|
|
33
|
+
description: '',
|
|
34
|
+
avatar: '',
|
|
35
|
+
home: ''
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const CARD_TONES = [
|
|
39
|
+
{
|
|
40
|
+
strip: 'bg-[#efc37a]',
|
|
41
|
+
chip: 'border-[#f2d7a7] bg-[#fff8eb] text-[#8d5a18]'
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
strip: 'bg-[#8fd4c0]',
|
|
45
|
+
chip: 'border-[#bde6da] bg-[#effbf7] text-[#156653]'
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
strip: 'bg-[#b7c9fb]',
|
|
49
|
+
chip: 'border-[#d7e2ff] bg-[#f4f7ff] text-[#2d4d8f]'
|
|
50
|
+
}
|
|
51
|
+
] as const;
|
|
52
|
+
|
|
53
|
+
function resolveAgentTone(index: number, builtIn: boolean) {
|
|
54
|
+
if (builtIn) {
|
|
55
|
+
return {
|
|
56
|
+
strip: 'bg-[#e6b765]',
|
|
57
|
+
chip: 'border-[#f2d19c] bg-[#fff8ec] text-[#90550d]'
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
return CARD_TONES[index % CARD_TONES.length];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function AgentsPage() {
|
|
64
|
+
const navigate = useNavigate();
|
|
65
|
+
const agentsQuery = useAgents();
|
|
66
|
+
const createAgent = useCreateAgent();
|
|
67
|
+
const deleteAgent = useDeleteAgent();
|
|
68
|
+
const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false);
|
|
69
|
+
const [form, setForm] = useState<CreateFormState>(EMPTY_FORM);
|
|
70
|
+
const setSessionListSnapshot = useChatSessionListStore((state) => state.setSnapshot);
|
|
71
|
+
|
|
72
|
+
const agents = useMemo(() => agentsQuery.data?.agents ?? [], [agentsQuery.data?.agents]);
|
|
73
|
+
const sortedAgents = useMemo(
|
|
74
|
+
() =>
|
|
75
|
+
[...agents].sort(
|
|
76
|
+
(left, right) =>
|
|
77
|
+
Number(Boolean(right.builtIn)) - Number(Boolean(left.builtIn)) ||
|
|
78
|
+
left.id.localeCompare(right.id)
|
|
79
|
+
),
|
|
80
|
+
[agents]
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
const handleCreate = async () => {
|
|
84
|
+
await createAgent.mutateAsync({
|
|
85
|
+
data: {
|
|
86
|
+
id: form.id,
|
|
87
|
+
...(form.displayName.trim() ? { displayName: form.displayName.trim() } : {}),
|
|
88
|
+
...(form.description.trim() ? { description: form.description.trim() } : {}),
|
|
89
|
+
...(form.avatar.trim() ? { avatar: form.avatar.trim() } : {}),
|
|
90
|
+
...(form.home.trim() ? { home: form.home.trim() } : {})
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
setForm(EMPTY_FORM);
|
|
94
|
+
setIsCreateDialogOpen(false);
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const startChatWithAgent = (agentId: string) => {
|
|
98
|
+
setSessionListSnapshot({
|
|
99
|
+
selectedAgentId: agentId,
|
|
100
|
+
selectedSessionKey: null
|
|
101
|
+
});
|
|
102
|
+
navigate('/chat');
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
return (
|
|
106
|
+
<PageLayout className="space-y-5">
|
|
107
|
+
<section className="relative overflow-hidden rounded-[28px] border border-[#f0d6aa] bg-[linear-gradient(135deg,#fff7ea_0%,#fff9f1_32%,#f2fbff_100%)] px-5 py-5 sm:px-6">
|
|
108
|
+
<div className="absolute inset-y-0 right-0 w-[46%] bg-[radial-gradient(circle_at_top_right,rgba(255,215,163,0.52),transparent_54%)]" />
|
|
109
|
+
<div className="absolute -bottom-10 left-8 h-32 w-32 rounded-full bg-[#ffe6c0]/55 blur-3xl" />
|
|
110
|
+
<div className="relative grid gap-5 xl:grid-cols-[minmax(0,1fr)_320px] xl:items-center">
|
|
111
|
+
<div className="max-w-3xl space-y-3">
|
|
112
|
+
<div className="inline-flex items-center gap-2 rounded-full border border-white/70 bg-white/80 px-3 py-1 text-[11px] font-semibold tracking-[0.16em] text-[#9b6118]">
|
|
113
|
+
<Sparkles className="h-3.5 w-3.5" />
|
|
114
|
+
{t('agentsHeroEyebrow')}
|
|
115
|
+
</div>
|
|
116
|
+
<div className="space-y-2">
|
|
117
|
+
<h1 className="max-w-2xl text-[30px] font-semibold leading-tight tracking-[-0.05em] text-[#2f2212] sm:text-[38px]">
|
|
118
|
+
{t('agentsHeroTitle')}
|
|
119
|
+
</h1>
|
|
120
|
+
<p className="max-w-2xl text-sm leading-6 text-[#6d5841] sm:text-[15px] sm:leading-7">
|
|
121
|
+
{t('agentsHeroDescription')}
|
|
122
|
+
</p>
|
|
123
|
+
</div>
|
|
124
|
+
<div className="pt-1">
|
|
125
|
+
<div className="inline-flex items-center gap-3 rounded-2xl border border-[#f2d5a4] bg-white/82 px-3 py-2 text-[#7a4d12] shadow-[0_14px_30px_rgba(167,117,47,0.07)]">
|
|
126
|
+
<span className="text-[11px] font-semibold tracking-[0.14em]">
|
|
127
|
+
{t('agentsOverviewTotal')}
|
|
128
|
+
</span>
|
|
129
|
+
<span className="text-xl font-semibold tracking-[-0.04em] text-[#1f2937]">
|
|
130
|
+
{agents.length}
|
|
131
|
+
</span>
|
|
132
|
+
</div>
|
|
133
|
+
</div>
|
|
134
|
+
</div>
|
|
135
|
+
<div className="flex shrink-0 flex-col gap-3">
|
|
136
|
+
<Button
|
|
137
|
+
type="button"
|
|
138
|
+
className="h-10 rounded-2xl bg-[#1f5c4d] px-5 text-sm font-semibold text-white hover:bg-[#184d40]"
|
|
139
|
+
onClick={() => setIsCreateDialogOpen(true)}
|
|
140
|
+
>
|
|
141
|
+
<Plus className="mr-2 h-4 w-4" />
|
|
142
|
+
{t('agentsCreateButton')}
|
|
143
|
+
</Button>
|
|
144
|
+
<div className="rounded-2xl border border-white/70 bg-white/72 px-4 py-3 text-xs leading-6 text-[#6d5841] shadow-[0_18px_40px_rgba(167,117,47,0.08)]">
|
|
145
|
+
{t('agentsCreateDialogHint')}
|
|
146
|
+
</div>
|
|
147
|
+
</div>
|
|
148
|
+
</div>
|
|
149
|
+
</section>
|
|
150
|
+
|
|
151
|
+
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
|
|
152
|
+
{agentsQuery.isLoading ? (
|
|
153
|
+
<Card className="md:col-span-2 xl:col-span-3 border-dashed border-[#d9dce3] bg-white/70">
|
|
154
|
+
<CardContent className="py-14 text-center text-sm text-gray-500">
|
|
155
|
+
{t('agentsLoading')}
|
|
156
|
+
</CardContent>
|
|
157
|
+
</Card>
|
|
158
|
+
) : sortedAgents.length === 0 ? (
|
|
159
|
+
<Card className="md:col-span-2 xl:col-span-3 overflow-hidden border-dashed border-[#d9dce3] bg-[linear-gradient(135deg,#fff7ea_0%,#f4fbff_100%)]">
|
|
160
|
+
<CardContent className="flex min-h-[240px] flex-col items-center justify-center px-6 py-14 text-center">
|
|
161
|
+
<div className="mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-white/80 shadow-[0_18px_44px_rgba(0,0,0,0.08)]">
|
|
162
|
+
<Bot className="h-8 w-8 text-[#d39a3b]" />
|
|
163
|
+
</div>
|
|
164
|
+
<div className="text-lg font-semibold text-[#2f2212]">{t('agentsEmpty')}</div>
|
|
165
|
+
<p className="mt-2 max-w-md text-sm leading-6 text-[#78644d]">
|
|
166
|
+
{t('agentsEmptyDescription')}
|
|
167
|
+
</p>
|
|
168
|
+
<Button
|
|
169
|
+
type="button"
|
|
170
|
+
className="mt-5 rounded-2xl bg-[#1f5c4d] px-5 text-white hover:bg-[#184d40]"
|
|
171
|
+
onClick={() => setIsCreateDialogOpen(true)}
|
|
172
|
+
>
|
|
173
|
+
<Plus className="mr-2 h-4 w-4" />
|
|
174
|
+
{t('agentsCreateButton')}
|
|
175
|
+
</Button>
|
|
176
|
+
</CardContent>
|
|
177
|
+
</Card>
|
|
178
|
+
) : (
|
|
179
|
+
sortedAgents.map((agent, index) => {
|
|
180
|
+
const tone = resolveAgentTone(index, Boolean(agent.builtIn));
|
|
181
|
+
return (
|
|
182
|
+
<Card
|
|
183
|
+
key={agent.id}
|
|
184
|
+
className="overflow-hidden border border-gray-200 bg-white shadow-sm transition-shadow duration-200 hover:shadow-md"
|
|
185
|
+
>
|
|
186
|
+
<div className={cn('h-1.5 w-full', tone.strip)} />
|
|
187
|
+
<CardContent className="flex h-full flex-col gap-4 px-4 py-4">
|
|
188
|
+
<div className="flex items-start gap-3">
|
|
189
|
+
<AgentAvatar
|
|
190
|
+
agentId={agent.id}
|
|
191
|
+
displayName={agent.displayName}
|
|
192
|
+
avatarUrl={agent.avatarUrl}
|
|
193
|
+
className="h-11 w-11 shrink-0"
|
|
194
|
+
/>
|
|
195
|
+
<div className="min-w-0 flex-1 space-y-1 pt-0.5">
|
|
196
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
197
|
+
<div className="truncate text-lg font-semibold tracking-[-0.03em] text-[#1f2937]">
|
|
198
|
+
{agent.displayName?.trim() || agent.id}
|
|
199
|
+
</div>
|
|
200
|
+
{agent.builtIn ? (
|
|
201
|
+
<span
|
|
202
|
+
className={cn(
|
|
203
|
+
'inline-flex items-center gap-1 rounded-full border px-2 py-0.5 text-[11px] font-medium',
|
|
204
|
+
tone.chip
|
|
205
|
+
)}
|
|
206
|
+
>
|
|
207
|
+
<ShieldCheck className="h-3 w-3" />
|
|
208
|
+
{t('agentsCardBuiltInTag')}
|
|
209
|
+
</span>
|
|
210
|
+
) : null}
|
|
211
|
+
</div>
|
|
212
|
+
<div className="text-[11px] font-medium uppercase tracking-[0.18em] text-[#94a3b8]">
|
|
213
|
+
@{agent.id}
|
|
214
|
+
</div>
|
|
215
|
+
</div>
|
|
216
|
+
</div>
|
|
217
|
+
|
|
218
|
+
<p className="text-sm leading-6 text-[#64748b]">
|
|
219
|
+
{agent.description?.trim() ||
|
|
220
|
+
(agent.builtIn
|
|
221
|
+
? t('agentsCardBuiltInSummary')
|
|
222
|
+
: t('agentsCardCustomSummary'))}
|
|
223
|
+
</p>
|
|
224
|
+
|
|
225
|
+
<div className="mt-auto flex flex-col gap-4">
|
|
226
|
+
<div className="border-t border-gray-100 pt-3">
|
|
227
|
+
<div className="flex items-center gap-2 text-[11px] font-semibold uppercase tracking-[0.18em] text-[#94a3b8]">
|
|
228
|
+
<House className="h-3.5 w-3.5" />
|
|
229
|
+
{t('agentsCardHomeLabel')}
|
|
230
|
+
</div>
|
|
231
|
+
<div className="mt-1.5 break-all text-sm leading-6 text-[#475569]">
|
|
232
|
+
{agent.workspace ?? '-'}
|
|
233
|
+
</div>
|
|
234
|
+
</div>
|
|
235
|
+
|
|
236
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
237
|
+
<Button
|
|
238
|
+
type="button"
|
|
239
|
+
className="h-9 rounded-xl bg-[#1f5c4d] px-4 text-white hover:bg-[#184d40]"
|
|
240
|
+
onClick={() => startChatWithAgent(agent.id)}
|
|
241
|
+
>
|
|
242
|
+
<MessageCircle className="mr-2 h-4 w-4" />
|
|
243
|
+
{t('agentsCardStartChat')}
|
|
244
|
+
</Button>
|
|
245
|
+
{!agent.builtIn ? (
|
|
246
|
+
<Button
|
|
247
|
+
type="button"
|
|
248
|
+
variant="ghost"
|
|
249
|
+
className="h-8 rounded-xl px-3 text-xs text-[#7b8794] hover:bg-[#f3f4f6] hover:text-[#475569]"
|
|
250
|
+
onClick={() => deleteAgent.mutate({ agentId: agent.id })}
|
|
251
|
+
disabled={deleteAgent.isPending}
|
|
252
|
+
>
|
|
253
|
+
<Trash2 className="mr-1.5 h-3.5 w-3.5" />
|
|
254
|
+
{t('agentsRemoveAction')}
|
|
255
|
+
</Button>
|
|
256
|
+
) : null}
|
|
257
|
+
</div>
|
|
258
|
+
</div>
|
|
259
|
+
</CardContent>
|
|
260
|
+
</Card>
|
|
261
|
+
);
|
|
262
|
+
})
|
|
263
|
+
)}
|
|
264
|
+
</div>
|
|
265
|
+
|
|
266
|
+
<Dialog
|
|
267
|
+
open={isCreateDialogOpen}
|
|
268
|
+
onOpenChange={(open) => {
|
|
269
|
+
setIsCreateDialogOpen(open);
|
|
270
|
+
if (!open && !createAgent.isPending) {
|
|
271
|
+
setForm(EMPTY_FORM);
|
|
272
|
+
}
|
|
273
|
+
}}
|
|
274
|
+
>
|
|
275
|
+
<DialogContent className="overflow-hidden border-none bg-[linear-gradient(180deg,#fff9f1_0%,#ffffff_24%)] p-0 sm:max-w-xl">
|
|
276
|
+
<div className="border-b border-[#f0e2c8] px-6 py-6">
|
|
277
|
+
<DialogHeader className="text-left">
|
|
278
|
+
<DialogTitle>{t('agentsCreateDialogTitle')}</DialogTitle>
|
|
279
|
+
<DialogDescription>{t('agentsCreateDialogDescription')}</DialogDescription>
|
|
280
|
+
</DialogHeader>
|
|
281
|
+
</div>
|
|
282
|
+
<div className="space-y-4 px-6 py-6">
|
|
283
|
+
<div className="grid gap-4 md:grid-cols-2">
|
|
284
|
+
<Input
|
|
285
|
+
value={form.id}
|
|
286
|
+
onChange={(event) =>
|
|
287
|
+
setForm((prev) => ({ ...prev, id: event.target.value }))
|
|
288
|
+
}
|
|
289
|
+
placeholder={t('agentsFormIdPlaceholder')}
|
|
290
|
+
/>
|
|
291
|
+
<Input
|
|
292
|
+
value={form.displayName}
|
|
293
|
+
onChange={(event) =>
|
|
294
|
+
setForm((prev) => ({
|
|
295
|
+
...prev,
|
|
296
|
+
displayName: event.target.value
|
|
297
|
+
}))
|
|
298
|
+
}
|
|
299
|
+
placeholder={t('agentsFormNamePlaceholder')}
|
|
300
|
+
/>
|
|
301
|
+
<Input
|
|
302
|
+
value={form.description}
|
|
303
|
+
onChange={(event) =>
|
|
304
|
+
setForm((prev) => ({
|
|
305
|
+
...prev,
|
|
306
|
+
description: event.target.value
|
|
307
|
+
}))
|
|
308
|
+
}
|
|
309
|
+
placeholder={t('agentsFormDescriptionPlaceholder')}
|
|
310
|
+
/>
|
|
311
|
+
<Input
|
|
312
|
+
value={form.avatar}
|
|
313
|
+
onChange={(event) =>
|
|
314
|
+
setForm((prev) => ({ ...prev, avatar: event.target.value }))
|
|
315
|
+
}
|
|
316
|
+
placeholder={t('agentsFormAvatarPlaceholder')}
|
|
317
|
+
/>
|
|
318
|
+
<Input
|
|
319
|
+
value={form.home}
|
|
320
|
+
onChange={(event) =>
|
|
321
|
+
setForm((prev) => ({ ...prev, home: event.target.value }))
|
|
322
|
+
}
|
|
323
|
+
placeholder={t('agentsFormHomePlaceholder')}
|
|
324
|
+
/>
|
|
325
|
+
</div>
|
|
326
|
+
<div className="rounded-2xl border border-[#efe3ca] bg-[#fff9ef] px-4 py-3 text-xs leading-6 text-[#7a6246]">
|
|
327
|
+
{t('agentsCreateDialogHint')}
|
|
328
|
+
</div>
|
|
329
|
+
</div>
|
|
330
|
+
<DialogFooter className="border-t border-[#f1e7d4] px-6 py-5">
|
|
331
|
+
<Button
|
|
332
|
+
type="button"
|
|
333
|
+
variant="ghost"
|
|
334
|
+
onClick={() => setIsCreateDialogOpen(false)}
|
|
335
|
+
disabled={createAgent.isPending}
|
|
336
|
+
>
|
|
337
|
+
{t('cancel')}
|
|
338
|
+
</Button>
|
|
339
|
+
<Button
|
|
340
|
+
type="button"
|
|
341
|
+
className="rounded-2xl bg-[#1f5c4d] px-5 text-white hover:bg-[#184d40]"
|
|
342
|
+
onClick={() => void handleCreate()}
|
|
343
|
+
disabled={createAgent.isPending || form.id.trim().length === 0}
|
|
344
|
+
>
|
|
345
|
+
<Plus className="mr-2 h-4 w-4" />
|
|
346
|
+
{t('agentsCreateAction')}
|
|
347
|
+
</Button>
|
|
348
|
+
</DialogFooter>
|
|
349
|
+
</DialogContent>
|
|
350
|
+
</Dialog>
|
|
351
|
+
</PageLayout>
|
|
352
|
+
);
|
|
353
|
+
}
|
|
@@ -1,11 +1,22 @@
|
|
|
1
1
|
import { render, screen } from '@testing-library/react';
|
|
2
2
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
+
import { ChatChildSessionPanel } from '@/components/chat/chat-child-session-panel';
|
|
3
4
|
import { ChatConversationPanel } from '@/components/chat/ChatConversationPanel';
|
|
4
5
|
import { useChatThreadStore } from '@/components/chat/stores/chat-thread.store';
|
|
5
6
|
|
|
6
7
|
const mocks = vi.hoisted(() => ({
|
|
7
8
|
deleteSession: vi.fn(),
|
|
8
|
-
goToProviders: vi.fn()
|
|
9
|
+
goToProviders: vi.fn(),
|
|
10
|
+
resolvedChildTabs: [
|
|
11
|
+
{
|
|
12
|
+
sessionKey: 'child-session-1',
|
|
13
|
+
parentSessionKey: 'parent-session-1',
|
|
14
|
+
title: '北京天气',
|
|
15
|
+
agentId: 'weather',
|
|
16
|
+
agentDisplayName: 'Weather',
|
|
17
|
+
agentAvatarUrl: null,
|
|
18
|
+
},
|
|
19
|
+
],
|
|
9
20
|
}));
|
|
10
21
|
|
|
11
22
|
vi.mock('@nextclaw/agent-chat-ui', async (importOriginal) => {
|
|
@@ -23,20 +34,25 @@ vi.mock('@/components/chat/nextclaw', () => ({
|
|
|
23
34
|
ChatMessageListContainer: () => <div data-testid="chat-message-list" />
|
|
24
35
|
}));
|
|
25
36
|
|
|
37
|
+
vi.mock('@/components/chat/containers/chat-message-list.container', () => ({
|
|
38
|
+
ChatMessageListContainer: () => <div data-testid="child-chat-message-list" />,
|
|
39
|
+
}));
|
|
40
|
+
|
|
26
41
|
vi.mock('@/components/chat/ChatWelcome', () => ({
|
|
27
42
|
ChatWelcome: () => <div data-testid="chat-welcome" />
|
|
28
43
|
}));
|
|
29
44
|
|
|
30
45
|
vi.mock('@/components/chat/presenter/chat-presenter-context', () => ({
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
46
|
+
usePresenter: () => ({
|
|
47
|
+
chatThreadManager: {
|
|
48
|
+
deleteSession: mocks.deleteSession,
|
|
49
|
+
goToProviders: mocks.goToProviders,
|
|
50
|
+
createSession: vi.fn(),
|
|
51
|
+
openSessionFromToolAction: vi.fn(),
|
|
52
|
+
selectChildSessionDetail: vi.fn(),
|
|
53
|
+
closeChildSessionDetail: vi.fn(),
|
|
54
|
+
goToParentSession: vi.fn(),
|
|
55
|
+
},
|
|
40
56
|
chatSessionListManager: {
|
|
41
57
|
selectSession: vi.fn()
|
|
42
58
|
}
|
|
@@ -51,6 +67,31 @@ vi.mock('@/components/chat/session-header/chat-session-project-badge', () => ({
|
|
|
51
67
|
ChatSessionProjectBadge: ({ projectName }: { projectName: string }) => <button>{projectName}</button>
|
|
52
68
|
}));
|
|
53
69
|
|
|
70
|
+
vi.mock('@/components/chat/ncp/session-conversation/use-ncp-child-session-tabs-view', () => ({
|
|
71
|
+
useNcpChildSessionTabsView: () => mocks.resolvedChildTabs,
|
|
72
|
+
}));
|
|
73
|
+
|
|
74
|
+
vi.mock('@/components/chat/ncp/session-conversation/use-ncp-session-conversation', () => ({
|
|
75
|
+
useNcpSessionConversation: () => ({
|
|
76
|
+
visibleMessages: [],
|
|
77
|
+
isHydrating: false,
|
|
78
|
+
hydrateError: null,
|
|
79
|
+
isRunning: false,
|
|
80
|
+
}),
|
|
81
|
+
}));
|
|
82
|
+
|
|
83
|
+
vi.mock('@/components/common/AgentAvatar', () => ({
|
|
84
|
+
AgentAvatar: ({ agentId }: { agentId: string }) => (
|
|
85
|
+
<div data-testid="agent-avatar">{agentId}</div>
|
|
86
|
+
),
|
|
87
|
+
}));
|
|
88
|
+
|
|
89
|
+
vi.mock('@/components/common/agent-identity', () => ({
|
|
90
|
+
AgentIdentityAvatar: ({ agentId }: { agentId: string }) => (
|
|
91
|
+
<div data-testid="agent-identity-avatar">{agentId}</div>
|
|
92
|
+
),
|
|
93
|
+
}));
|
|
94
|
+
|
|
54
95
|
describe('ChatConversationPanel', () => {
|
|
55
96
|
beforeEach(() => {
|
|
56
97
|
mocks.deleteSession.mockReset();
|
|
@@ -73,9 +114,8 @@ describe('ChatConversationPanel', () => {
|
|
|
73
114
|
isAwaitingAssistantOutput: false,
|
|
74
115
|
parentSessionKey: null,
|
|
75
116
|
parentSessionLabel: null,
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
childSessionDetailLabel: null,
|
|
117
|
+
childSessionTabs: [],
|
|
118
|
+
activeChildSessionKey: null,
|
|
79
119
|
}
|
|
80
120
|
});
|
|
81
121
|
});
|
|
@@ -107,3 +147,91 @@ describe('ChatConversationPanel', () => {
|
|
|
107
147
|
expect(screen.getByLabelText('More actions')).toBeTruthy();
|
|
108
148
|
});
|
|
109
149
|
});
|
|
150
|
+
|
|
151
|
+
describe('ChatChildSessionPanel', () => {
|
|
152
|
+
it('keeps the header compact for a single child session', () => {
|
|
153
|
+
mocks.resolvedChildTabs = [
|
|
154
|
+
{
|
|
155
|
+
sessionKey: 'child-session-1',
|
|
156
|
+
parentSessionKey: 'parent-session-1',
|
|
157
|
+
title: '北京天气',
|
|
158
|
+
agentId: 'weather',
|
|
159
|
+
agentDisplayName: 'Weather',
|
|
160
|
+
agentAvatarUrl: null,
|
|
161
|
+
},
|
|
162
|
+
];
|
|
163
|
+
render(
|
|
164
|
+
<ChatChildSessionPanel
|
|
165
|
+
tabs={[
|
|
166
|
+
{
|
|
167
|
+
sessionKey: 'child-session-1',
|
|
168
|
+
parentSessionKey: 'parent-session-1',
|
|
169
|
+
label: '北京天气',
|
|
170
|
+
agentId: 'weather',
|
|
171
|
+
},
|
|
172
|
+
]}
|
|
173
|
+
activeSessionKey="child-session-1"
|
|
174
|
+
onSelectSession={vi.fn()}
|
|
175
|
+
onClose={vi.fn()}
|
|
176
|
+
onBackToParent={vi.fn()}
|
|
177
|
+
/>,
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
expect(screen.getByText('北京天气')).toBeTruthy();
|
|
181
|
+
expect(screen.queryByText('Child Sessions')).toBeNull();
|
|
182
|
+
expect(screen.queryByText('child-session-1')).toBeNull();
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('uses tabs as the only title layer when multiple child sessions are open', () => {
|
|
186
|
+
mocks.resolvedChildTabs = [
|
|
187
|
+
{
|
|
188
|
+
sessionKey: 'child-session-1',
|
|
189
|
+
parentSessionKey: 'parent-session-1',
|
|
190
|
+
title: '北京天气',
|
|
191
|
+
agentId: 'weather',
|
|
192
|
+
agentDisplayName: 'Weather',
|
|
193
|
+
agentAvatarUrl: null,
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
sessionKey: 'child-session-2',
|
|
197
|
+
parentSessionKey: 'parent-session-1',
|
|
198
|
+
title: '上海天气',
|
|
199
|
+
agentId: 'weather',
|
|
200
|
+
agentDisplayName: 'Weather',
|
|
201
|
+
agentAvatarUrl: null,
|
|
202
|
+
},
|
|
203
|
+
];
|
|
204
|
+
|
|
205
|
+
render(
|
|
206
|
+
<ChatChildSessionPanel
|
|
207
|
+
tabs={[
|
|
208
|
+
{
|
|
209
|
+
sessionKey: 'child-session-1',
|
|
210
|
+
parentSessionKey: 'parent-session-1',
|
|
211
|
+
label: '北京天气',
|
|
212
|
+
agentId: 'weather',
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
sessionKey: 'child-session-2',
|
|
216
|
+
parentSessionKey: 'parent-session-1',
|
|
217
|
+
label: '上海天气',
|
|
218
|
+
agentId: 'weather',
|
|
219
|
+
},
|
|
220
|
+
]}
|
|
221
|
+
activeSessionKey="child-session-1"
|
|
222
|
+
onSelectSession={vi.fn()}
|
|
223
|
+
onClose={vi.fn()}
|
|
224
|
+
onBackToParent={vi.fn()}
|
|
225
|
+
/>,
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
expect(screen.getAllByText('北京天气')).toHaveLength(1);
|
|
229
|
+
expect(screen.getByText('上海天气')).toBeTruthy();
|
|
230
|
+
const tabButtons = screen.getAllByRole('button').filter((element) =>
|
|
231
|
+
element.getAttribute('aria-pressed') !== null,
|
|
232
|
+
);
|
|
233
|
+
expect(tabButtons).toHaveLength(2);
|
|
234
|
+
expect(tabButtons[0]?.getAttribute('aria-pressed')).toBe('true');
|
|
235
|
+
expect(tabButtons[1]?.getAttribute('aria-pressed')).toBe('false');
|
|
236
|
+
});
|
|
237
|
+
});
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
} from "@/components/chat/nextclaw";
|
|
8
8
|
import { ChatChildSessionPanel } from "@/components/chat/chat-child-session-panel";
|
|
9
9
|
import { ChatWelcome } from "@/components/chat/ChatWelcome";
|
|
10
|
+
import { AgentAvatar } from "@/components/common/AgentAvatar";
|
|
10
11
|
import { usePresenter } from "@/components/chat/presenter/chat-presenter-context";
|
|
11
12
|
import { ChatSessionHeaderActions } from "@/components/chat/session-header/chat-session-header-actions";
|
|
12
13
|
import { ChatSessionProjectBadge } from "@/components/chat/session-header/chat-session-project-badge";
|
|
@@ -47,10 +48,14 @@ export function ChatConversationPanel() {
|
|
|
47
48
|
const snapshot = useChatThreadStore((state) => state.snapshot);
|
|
48
49
|
const fallbackThreadRef = useRef<HTMLDivElement | null>(null);
|
|
49
50
|
const threadRef = snapshot.threadRef ?? fallbackThreadRef;
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
51
|
+
const childSessionTabs = snapshot.childSessionTabs.filter(
|
|
52
|
+
(tab) => tab.parentSessionKey === snapshot.sessionKey,
|
|
53
|
+
);
|
|
54
|
+
const detailSessionKey = childSessionTabs.some(
|
|
55
|
+
(tab) => tab.sessionKey === snapshot.activeChildSessionKey,
|
|
56
|
+
)
|
|
57
|
+
? snapshot.activeChildSessionKey
|
|
58
|
+
: (childSessionTabs[childSessionTabs.length - 1]?.sessionKey ?? null);
|
|
54
59
|
const shouldShowSessionHeader = Boolean(
|
|
55
60
|
snapshot.sessionKey || snapshot.sessionTypeLabel,
|
|
56
61
|
);
|
|
@@ -96,7 +101,7 @@ export function ChatConversationPanel() {
|
|
|
96
101
|
>
|
|
97
102
|
<ArrowLeft className="h-3.5 w-3.5" />
|
|
98
103
|
<span>
|
|
99
|
-
|
|
104
|
+
{t("chatBackToParent")}
|
|
100
105
|
{snapshot.parentSessionLabel?.trim()
|
|
101
106
|
? ` · ${snapshot.parentSessionLabel.trim()}`
|
|
102
107
|
: ""}
|
|
@@ -114,6 +119,19 @@ export function ChatConversationPanel() {
|
|
|
114
119
|
)}
|
|
115
120
|
>
|
|
116
121
|
<div className="min-w-0 flex-1 flex items-center gap-2">
|
|
122
|
+
{snapshot.agentId ? (
|
|
123
|
+
<div className="inline-flex items-center gap-2 shrink-0 rounded-full border border-gray-200 bg-white/80 px-2 py-1">
|
|
124
|
+
<AgentAvatar
|
|
125
|
+
agentId={snapshot.agentId}
|
|
126
|
+
displayName={snapshot.agentDisplayName}
|
|
127
|
+
avatarUrl={snapshot.agentAvatarUrl}
|
|
128
|
+
className="h-5 w-5"
|
|
129
|
+
/>
|
|
130
|
+
<span className="max-w-[120px] truncate text-xs font-medium text-gray-700">
|
|
131
|
+
{snapshot.agentDisplayName?.trim() || snapshot.agentId}
|
|
132
|
+
</span>
|
|
133
|
+
</div>
|
|
134
|
+
) : null}
|
|
117
135
|
<span className="text-sm font-medium text-gray-700 truncate">
|
|
118
136
|
{sessionHeaderTitle}
|
|
119
137
|
</span>
|
|
@@ -174,6 +192,9 @@ export function ChatConversationPanel() {
|
|
|
174
192
|
{showWelcome ? (
|
|
175
193
|
<ChatWelcome
|
|
176
194
|
onCreateSession={presenter.chatThreadManager.createSession}
|
|
195
|
+
agents={snapshot.availableAgents ?? []}
|
|
196
|
+
selectedAgentId={snapshot.agentId ?? "main"}
|
|
197
|
+
onSelectAgent={presenter.chatSessionListManager.setSelectedAgentId}
|
|
177
198
|
/>
|
|
178
199
|
) : hideEmptyHint ? (
|
|
179
200
|
<div className="h-full" />
|
|
@@ -200,8 +221,9 @@ export function ChatConversationPanel() {
|
|
|
200
221
|
|
|
201
222
|
{detailSessionKey ? (
|
|
202
223
|
<ChatChildSessionPanel
|
|
203
|
-
|
|
204
|
-
|
|
224
|
+
tabs={childSessionTabs}
|
|
225
|
+
activeSessionKey={detailSessionKey}
|
|
226
|
+
onSelectSession={presenter.chatThreadManager.selectChildSessionDetail}
|
|
205
227
|
onClose={presenter.chatThreadManager.closeChildSessionDetail}
|
|
206
228
|
onBackToParent={presenter.chatThreadManager.goToParentSession}
|
|
207
229
|
onToolAction={presenter.chatThreadManager.openSessionFromToolAction}
|
|
@@ -71,6 +71,14 @@ vi.mock('@/components/common/StatusBadge', () => ({
|
|
|
71
71
|
StatusBadge: () => <div data-testid="status-badge" />
|
|
72
72
|
}));
|
|
73
73
|
|
|
74
|
+
vi.mock('@/hooks/agents/useAgents', () => ({
|
|
75
|
+
useAgents: () => ({
|
|
76
|
+
data: {
|
|
77
|
+
agents: []
|
|
78
|
+
}
|
|
79
|
+
})
|
|
80
|
+
}));
|
|
81
|
+
|
|
74
82
|
vi.mock('@/components/providers/I18nProvider', () => ({
|
|
75
83
|
useI18n: () => ({
|
|
76
84
|
language: 'en',
|