@nextclaw/ui 0.11.16 → 0.11.18
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 +21 -2
- package/dist/assets/ChannelsList-eZfHzvxb.js +8 -0
- package/dist/assets/ChatPage-DKD5hcD8.js +38 -0
- package/dist/assets/DocBrowser-CIHLqoIm.js +1 -0
- package/dist/assets/DocBrowser-DKkE3Y4I.js +1 -0
- package/dist/assets/DocBrowserContext-BcZRBsCg.js +1 -0
- package/dist/assets/LogoBadge-BIPDLEwK.js +1 -0
- package/dist/assets/MarketplacePage-CMPjqEmN.js +1 -0
- package/dist/assets/MarketplacePage-D0iqC5o7.js +49 -0
- package/dist/assets/McpMarketplacePage-CCmRjGwl.js +40 -0
- package/dist/assets/ModelConfig-BiWp8Ymp.js +1 -0
- package/dist/assets/ProvidersList-HaCAzF9F.js +1 -0
- package/dist/assets/RemoteAccessPage-DOF4oEHW.js +1 -0
- package/dist/assets/RuntimeConfig-BnkWf6Eb.js +1 -0
- package/dist/assets/SearchConfig-3ofKM9W4.js +1 -0
- package/dist/assets/SecretsConfig-BRbC2hfo.js +3 -0
- package/dist/assets/SessionsConfig-BpoD_0WD.js +2 -0
- package/dist/assets/book-open-DzSduAaw.js +1 -0
- package/dist/assets/chat-session-display-CGfXhJoT.js +1 -0
- package/dist/assets/chunk-JZWAC4HX-C1vpvW4r.js +3 -0
- package/dist/assets/config-Df97LeLR.js +1 -0
- package/dist/assets/config-hints-fGnUjDe9.js +1 -0
- package/dist/assets/config-layout-B-7erZRN.js +1 -0
- package/dist/assets/createLucideIcon-CcR5wVoU.js +1 -0
- package/dist/assets/dist-BMlnBah3.js +1 -0
- package/dist/assets/dist-Dii9v3X9.js +15 -0
- package/dist/assets/external-link-CnSDrvJE.js +1 -0
- package/dist/assets/hash-CAnX6PNt.js +1 -0
- package/dist/assets/i18n-CXBpwAwA.js +1 -0
- package/dist/assets/index-CjPeKafH.js +6 -0
- package/dist/assets/index-DMy_fKKh.css +1 -0
- package/dist/assets/label-CtIFj7_6.js +1 -0
- package/dist/assets/loader-circle-qgU4zQDw.js +1 -0
- package/dist/assets/logos-3KFNiOej.js +1 -0
- package/dist/assets/marketplace-localization-CXeGRf6E.js +1 -0
- package/dist/assets/page-layout-BMwpn87D.js +1 -0
- package/dist/assets/plus-C9cYVbL-.js +1 -0
- package/dist/assets/popover-BIzq25oH.js +1 -0
- package/dist/assets/provider-models-C8JQUd1E.js +1 -0
- package/dist/assets/react-ji6GGP_j.js +1 -0
- package/dist/assets/save-CMgYkJ-y.js +1 -0
- package/dist/assets/search-sl1OeJFl.js +1 -0
- package/dist/assets/security-config-BcbOF17w.js +1 -0
- package/dist/assets/select-Cz82gl01.js +41 -0
- package/dist/assets/skeleton-rgIt7a5q.js +1 -0
- package/dist/assets/status-dot-C7q1HvLH.js +1 -0
- package/dist/assets/switch-DYswvkYj.js +1 -0
- package/dist/assets/tabs-custom-DKYQxrx1.js +1 -0
- package/dist/assets/trash-2-DfXI7-ap.js +1 -0
- package/dist/assets/useConfirmDialog-Dk15Fj1n.js +1 -0
- package/dist/assets/useMutation-s2sn2yzh.js +1 -0
- package/dist/assets/x-MIimOGs6.js +1 -0
- package/dist/index.html +18 -3
- package/package.json +9 -9
- package/src/api/types.ts +2 -0
- package/src/components/chat/ChatConversationPanel.tsx +1 -0
- package/src/components/chat/ChatSidebar.tsx +3 -21
- package/src/components/chat/adapters/chat-message.adapter.test.ts +12 -2
- package/src/components/chat/adapters/chat-message.subagent-tool-card.ts +46 -17
- package/src/components/chat/chat-session-display.test.ts +33 -0
- package/src/components/chat/chat-session-display.ts +15 -0
- package/src/components/chat/chat-sidebar-session-item.tsx +13 -4
- package/src/components/chat/containers/chat-message-list.container.tsx +2 -0
- package/src/components/chat/ncp/ncp-chat-page-data.ts +2 -5
- package/src/components/chat/ncp/ncp-session-adapter.ts +21 -0
- package/src/components/chat/ncp/use-ncp-session-list-view.ts +2 -6
- package/src/components/common/session-context-icon.tsx +30 -0
- package/src/components/config/ChannelForm.tsx +71 -39
- package/src/components/config/SessionsConfig.tsx +2 -6
- package/src/components/config/channel-form-fields.test.ts +28 -0
- package/src/components/config/channel-form-fields.ts +95 -30
- package/src/components/config/weixin-channel-auth-section.test.tsx +26 -0
- package/src/components/config/weixin-channel-auth-section.tsx +6 -2
- package/src/lib/i18n.channel-auth.ts +5 -0
- package/src/lib/i18n.chat.ts +6 -0
- package/src/lib/session-context.utils.ts +95 -0
- package/src/transport/remote.transport.test.ts +1 -1
- package/vite.config.ts +2 -2
- package/dist/assets/ChannelsList-UKA-5t02.js +0 -8
- package/dist/assets/ChatPage-xdBp-ddG.js +0 -37
- package/dist/assets/DocBrowser-DBUIWJer.js +0 -1
- package/dist/assets/LogoBadge-CsSBHZeV.js +0 -1
- package/dist/assets/MarketplacePage-BSH836_G.js +0 -49
- package/dist/assets/McpMarketplacePage-B9_kPnnM.js +0 -40
- package/dist/assets/ModelConfig-DbEKVVg4.js +0 -1
- package/dist/assets/ProvidersList-Ck5PgTF2.js +0 -1
- package/dist/assets/RemoteAccessPage-4JED9IcK.js +0 -1
- package/dist/assets/RuntimeConfig-CB04ug9v.js +0 -1
- package/dist/assets/SearchConfig-DmuvL9Pn.js +0 -1
- package/dist/assets/SecretsConfig-Dw2sRiSs.js +0 -3
- package/dist/assets/SessionsConfig-c-Z9X3xH.js +0 -2
- package/dist/assets/chat-session-display-z9RvX-D3.js +0 -1
- package/dist/assets/config-hints-CApS3K_7.js +0 -1
- package/dist/assets/config-layout-BHnOoweL.js +0 -1
- package/dist/assets/index-CfVmBgkf.css +0 -1
- package/dist/assets/index-DqzLj8Sw.js +0 -8
- package/dist/assets/label-DPGDZvhm.js +0 -1
- package/dist/assets/marketplace-localization-Dk31LJJJ.js +0 -1
- package/dist/assets/page-layout-DMA_ZiHj.js +0 -1
- package/dist/assets/popover-BBLXwfva.js +0 -1
- package/dist/assets/provider-models-BOeNnjk9.js +0 -1
- package/dist/assets/security-config-FYNEE2eR.js +0 -1
- package/dist/assets/skeleton-DLM_39_P.js +0 -1
- package/dist/assets/status-dot-BOzEprxw.js +0 -1
- package/dist/assets/switch-rE_Ew8fl.js +0 -1
- package/dist/assets/tabs-custom-BkfMUTHE.js +0 -1
- package/dist/assets/useConfirmDialog-0owrqZcT.js +0 -1
- package/dist/assets/vendor-MCpnpiKt.js +0 -461
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import type { SessionEntryView } from '@/api/types';
|
|
2
|
+
import { SessionContextIconNode } from '@/components/common/session-context-icon';
|
|
2
3
|
import { SessionRunBadge } from '@/components/common/SessionRunBadge';
|
|
3
4
|
import { Button } from '@/components/ui/button';
|
|
4
5
|
import { Input } from '@/components/ui/input';
|
|
6
|
+
import { type SessionContextView } from '@/lib/session-context.utils';
|
|
5
7
|
import type { SessionRunStatus } from '@/lib/session-run-status';
|
|
6
8
|
import { cn } from '@/lib/utils';
|
|
7
9
|
import { formatDateTime, t } from '@/lib/i18n';
|
|
@@ -11,7 +13,7 @@ type ChatSidebarSessionItemProps = {
|
|
|
11
13
|
session: SessionEntryView;
|
|
12
14
|
active: boolean;
|
|
13
15
|
runStatus?: SessionRunStatus;
|
|
14
|
-
|
|
16
|
+
context: SessionContextView;
|
|
15
17
|
title: string;
|
|
16
18
|
isEditing: boolean;
|
|
17
19
|
draftLabel: string;
|
|
@@ -28,7 +30,7 @@ export function ChatSidebarSessionItem(props: ChatSidebarSessionItemProps) {
|
|
|
28
30
|
session,
|
|
29
31
|
active,
|
|
30
32
|
runStatus,
|
|
31
|
-
|
|
33
|
+
context,
|
|
32
34
|
title,
|
|
33
35
|
isEditing,
|
|
34
36
|
draftLabel,
|
|
@@ -40,6 +42,8 @@ export function ChatSidebarSessionItem(props: ChatSidebarSessionItemProps) {
|
|
|
40
42
|
onCancel
|
|
41
43
|
} = props;
|
|
42
44
|
|
|
45
|
+
const iconTone = active ? 'text-gray-700' : 'text-gray-500';
|
|
46
|
+
|
|
43
47
|
return (
|
|
44
48
|
<div
|
|
45
49
|
className={cn(
|
|
@@ -102,7 +106,7 @@ export function ChatSidebarSessionItem(props: ChatSidebarSessionItemProps) {
|
|
|
102
106
|
<div className="grid grid-cols-[minmax(0,1fr)_0.875rem] items-center gap-1.5 pr-8">
|
|
103
107
|
<span className="flex min-w-0 items-center gap-1.5">
|
|
104
108
|
<span className="truncate font-medium">{title}</span>
|
|
105
|
-
{
|
|
109
|
+
{context.label ? (
|
|
106
110
|
<span
|
|
107
111
|
className={cn(
|
|
108
112
|
'shrink-0 rounded-full border px-1.5 py-0.5 text-[10px] font-semibold leading-none',
|
|
@@ -111,7 +115,12 @@ export function ChatSidebarSessionItem(props: ChatSidebarSessionItemProps) {
|
|
|
111
115
|
: 'border-gray-200 bg-gray-100 text-gray-500'
|
|
112
116
|
)}
|
|
113
117
|
>
|
|
114
|
-
{
|
|
118
|
+
{context.label}
|
|
119
|
+
</span>
|
|
120
|
+
) : null}
|
|
121
|
+
{context.icon ? (
|
|
122
|
+
<span className="inline-flex h-[1.125rem] w-[1.125rem] shrink-0 items-center justify-center">
|
|
123
|
+
<SessionContextIconNode icon={context.icon} className={iconTone} />
|
|
115
124
|
</span>
|
|
116
125
|
) : null}
|
|
117
126
|
</span>
|
|
@@ -57,6 +57,8 @@ function buildChatMessageTexts(language: string) {
|
|
|
57
57
|
return {
|
|
58
58
|
copyCodeLabel: t("chatCodeCopy"),
|
|
59
59
|
copiedCodeLabel: t("chatCodeCopied"),
|
|
60
|
+
copyMessageLabel: t("chatMessageCopy"),
|
|
61
|
+
copiedMessageLabel: t("chatMessageCopied"),
|
|
60
62
|
typingLabel: t("chatTyping"),
|
|
61
63
|
};
|
|
62
64
|
}
|
|
@@ -2,6 +2,7 @@ import { useMemo } from 'react';
|
|
|
2
2
|
import type { Dispatch, SetStateAction } from 'react';
|
|
3
3
|
import type { SessionEntryView, ThinkingLevel } from '@/api/types';
|
|
4
4
|
import type { ChatModelOption } from '@/components/chat/chat-input.types';
|
|
5
|
+
import { sessionMatchesQuery } from '@/components/chat/chat-session-display';
|
|
5
6
|
import { adaptNcpSessionSummaries } from '@/components/chat/ncp/ncp-session-adapter';
|
|
6
7
|
import { useChatSessionTypeState } from '@/components/chat/useChatSessionTypeState';
|
|
7
8
|
import {
|
|
@@ -30,11 +31,7 @@ type UseNcpChatPageDataParams = {
|
|
|
30
31
|
};
|
|
31
32
|
|
|
32
33
|
function filterSessionsByQuery(sessions: SessionEntryView[], query: string): SessionEntryView[] {
|
|
33
|
-
|
|
34
|
-
if (!normalizedQuery) {
|
|
35
|
-
return sessions;
|
|
36
|
-
}
|
|
37
|
-
return sessions.filter((session) => session.key.toLowerCase().includes(normalizedQuery));
|
|
34
|
+
return sessions.filter((session) => sessionMatchesQuery(session, query));
|
|
38
35
|
}
|
|
39
36
|
|
|
40
37
|
export function filterModelOptionsBySessionType(params: {
|
|
@@ -76,6 +76,25 @@ function readNcpSessionType(summary: NcpSessionSummaryView): string {
|
|
|
76
76
|
return readOptionalString(metadata.session_type) ?? readOptionalString(metadata.sessionType) ?? 'native';
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
+
function parseSessionContext(sessionKey: string): { channel?: string; type?: string } {
|
|
80
|
+
if (sessionKey === 'heartbeat') {
|
|
81
|
+
return { type: 'heartbeat' };
|
|
82
|
+
}
|
|
83
|
+
if (sessionKey.startsWith('cron:')) {
|
|
84
|
+
return { type: 'cron' };
|
|
85
|
+
}
|
|
86
|
+
if (sessionKey.startsWith('agent:')) {
|
|
87
|
+
const parts = sessionKey.split(':');
|
|
88
|
+
if (parts.length >= 3) {
|
|
89
|
+
const channel = parts[2];
|
|
90
|
+
if (channel && channel !== 'main' && channel !== 'direct') {
|
|
91
|
+
return { channel };
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return {};
|
|
96
|
+
}
|
|
97
|
+
|
|
79
98
|
function mapToolStatus(part: Extract<NcpMessagePart, { type: 'tool-invocation' }>): ToolInvocationStatus {
|
|
80
99
|
if (part.state === 'result') {
|
|
81
100
|
return ToolInvocationStatus.RESULT;
|
|
@@ -188,11 +207,13 @@ export function adaptNcpSessionSummary(summary: NcpSessionSummaryView): SessionE
|
|
|
188
207
|
const label = readNcpSessionLabel(summary);
|
|
189
208
|
const preferredModel = readNcpSessionPreferredModel(summary);
|
|
190
209
|
const preferredThinking = readNcpSessionPreferredThinking(summary);
|
|
210
|
+
const context = parseSessionContext(summary.sessionId);
|
|
191
211
|
return {
|
|
192
212
|
key: summary.sessionId,
|
|
193
213
|
createdAt: summary.updatedAt,
|
|
194
214
|
updatedAt: summary.updatedAt,
|
|
195
215
|
...(label ? { label } : {}),
|
|
216
|
+
...context,
|
|
196
217
|
...(preferredModel ? { preferredModel } : {}),
|
|
197
218
|
...(preferredThinking ? { preferredThinking } : {}),
|
|
198
219
|
sessionType: readNcpSessionType(summary),
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useMemo } from 'react';
|
|
2
2
|
import type { SessionEntryView } from '@/api/types';
|
|
3
3
|
import { adaptNcpSessionSummaries } from '@/components/chat/ncp/ncp-session-adapter';
|
|
4
|
+
import { sessionMatchesQuery } from '@/components/chat/chat-session-display';
|
|
4
5
|
import { useChatSessionListStore } from '@/components/chat/stores/chat-session-list.store';
|
|
5
6
|
import { useNcpSessions } from '@/hooks/useConfig';
|
|
6
7
|
import type { SessionRunStatus } from '@/lib/session-run-status';
|
|
@@ -11,12 +12,7 @@ export type NcpSessionListItemView = {
|
|
|
11
12
|
};
|
|
12
13
|
|
|
13
14
|
function filterSessionsByQuery(sessions: readonly SessionEntryView[], query: string): SessionEntryView[] {
|
|
14
|
-
|
|
15
|
-
if (!normalizedQuery) {
|
|
16
|
-
return [...sessions];
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
return sessions.filter((session) => session.key.toLowerCase().includes(normalizedQuery));
|
|
15
|
+
return sessions.filter((session) => sessionMatchesQuery(session, query));
|
|
20
16
|
}
|
|
21
17
|
|
|
22
18
|
export function useNcpSessionListView(params: { limit?: number } = {}) {
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { type SessionContextIcon } from '@/lib/session-context.utils';
|
|
2
|
+
import { LogoBadge } from '@/components/common/LogoBadge';
|
|
3
|
+
import { getChannelLogo } from '@/lib/logos';
|
|
4
|
+
import { cn } from '@/lib/utils';
|
|
5
|
+
import { AlarmClock, Bot, HeartPulse } from 'lucide-react';
|
|
6
|
+
|
|
7
|
+
export function SessionContextIconNode({ icon, className }: { icon: SessionContextIcon; className?: string }) {
|
|
8
|
+
if (icon.kind === 'channel-logo') {
|
|
9
|
+
return <ChannelLogoIcon channel={icon.channel} className={className} />;
|
|
10
|
+
}
|
|
11
|
+
if (icon.icon === 'heartbeat') {
|
|
12
|
+
return <HeartPulse className={cn('h-3.5 w-3.5', className)} />;
|
|
13
|
+
}
|
|
14
|
+
return <AlarmClock className={cn('h-3.5 w-3.5', className)} />;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function ChannelLogoIcon(
|
|
18
|
+
{ channel, className }: { channel: string; className?: string }
|
|
19
|
+
) {
|
|
20
|
+
const logoSrc = getChannelLogo(channel);
|
|
21
|
+
return (
|
|
22
|
+
<LogoBadge
|
|
23
|
+
name={channel}
|
|
24
|
+
src={logoSrc}
|
|
25
|
+
className={cn('h-4 w-4 rounded-[4px] border border-gray-200/80 bg-white', className)}
|
|
26
|
+
imgClassName="h-3 w-3 object-contain"
|
|
27
|
+
fallback={<Bot className="h-3 w-3 text-gray-500" />}
|
|
28
|
+
/>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useEffect, useState } from 'react';
|
|
1
|
+
import { useEffect, useMemo, useState } from 'react';
|
|
2
2
|
import { useConfig, useConfigMeta, useConfigSchema, useUpdateChannel, useExecuteConfigAction } from '@/hooks/useConfig';
|
|
3
3
|
import { Button } from '@/components/ui/button';
|
|
4
4
|
import { StatusDot } from '@/components/ui/status-dot';
|
|
@@ -13,7 +13,7 @@ import { resolveChannelTutorialUrl } from '@/lib/channel-tutorials';
|
|
|
13
13
|
import { getChannelLogo } from '@/lib/logos';
|
|
14
14
|
import { CONFIG_DETAIL_CARD_CLASS, CONFIG_EMPTY_DETAIL_CARD_CLASS } from './config-layout';
|
|
15
15
|
import { ChannelFormFieldsSection } from './channel-form-fields-section';
|
|
16
|
-
import {
|
|
16
|
+
import { buildChannelFormDefinitions, type ChannelField, type ChannelFormBlock, type ChannelFormFieldSection } from './channel-form-fields';
|
|
17
17
|
import { WeixinChannelAuthSection } from './weixin-channel-auth-section';
|
|
18
18
|
|
|
19
19
|
type ChannelFormProps = {
|
|
@@ -50,6 +50,16 @@ function buildScopeDraft(scope: string, value: Record<string, unknown>): Record<
|
|
|
50
50
|
return output;
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
function resolveFieldsForSection(fields: ChannelField[], section: ChannelFormFieldSection): ChannelField[] {
|
|
54
|
+
if (section === 'all') {
|
|
55
|
+
return fields;
|
|
56
|
+
}
|
|
57
|
+
if (section === 'primary') {
|
|
58
|
+
return fields.filter((field) => field.section === 'primary');
|
|
59
|
+
}
|
|
60
|
+
return fields.filter((field) => field.section !== 'primary');
|
|
61
|
+
}
|
|
62
|
+
|
|
53
63
|
export function ChannelForm({ channelName }: ChannelFormProps) {
|
|
54
64
|
const { data: config } = useConfig();
|
|
55
65
|
const { data: meta } = useConfigMeta();
|
|
@@ -62,7 +72,10 @@ export function ChannelForm({ channelName }: ChannelFormProps) {
|
|
|
62
72
|
const [runningActionId, setRunningActionId] = useState<string | null>(null);
|
|
63
73
|
|
|
64
74
|
const channelConfig = channelName ? config?.channels[channelName] : null;
|
|
65
|
-
const
|
|
75
|
+
const channelDefinitions = useMemo(() => buildChannelFormDefinitions(), []);
|
|
76
|
+
const channelDefinition = channelName ? channelDefinitions[channelName] : undefined;
|
|
77
|
+
const fields = channelDefinition?.fields ?? [];
|
|
78
|
+
const layoutBlocks = channelDefinition?.layout ?? [{ type: 'fields', section: 'all' } satisfies ChannelFormBlock];
|
|
66
79
|
const uiHints = schema?.uiHints;
|
|
67
80
|
const scope = channelName ? `channels.${channelName}` : null;
|
|
68
81
|
const actions = schema?.actions?.filter((action) => action.scope === scope) ?? [];
|
|
@@ -71,14 +84,12 @@ export function ChannelForm({ channelName }: ChannelFormProps) {
|
|
|
71
84
|
: channelName;
|
|
72
85
|
const channelMeta = meta?.channels.find((item) => item.name === channelName);
|
|
73
86
|
const tutorialUrl = channelMeta ? resolveChannelTutorialUrl(channelMeta) : undefined;
|
|
74
|
-
const isWeixinChannel = channelName === 'weixin';
|
|
75
87
|
|
|
76
88
|
useEffect(() => {
|
|
77
89
|
if (channelConfig) {
|
|
78
90
|
setFormData({ ...channelConfig });
|
|
79
91
|
const nextDrafts: Record<string, string> = {};
|
|
80
|
-
|
|
81
|
-
currentFields
|
|
92
|
+
fields
|
|
82
93
|
.filter((field) => field.type === 'json')
|
|
83
94
|
.forEach((field) => {
|
|
84
95
|
const value = channelConfig[field.name];
|
|
@@ -89,7 +100,7 @@ export function ChannelForm({ channelName }: ChannelFormProps) {
|
|
|
89
100
|
setFormData({});
|
|
90
101
|
setJsonDrafts({});
|
|
91
102
|
}
|
|
92
|
-
}, [channelConfig,
|
|
103
|
+
}, [channelConfig, fields]);
|
|
93
104
|
|
|
94
105
|
const updateField = (name: string, value: unknown) => {
|
|
95
106
|
setFormData((prev) => ({ ...prev, [name]: value }));
|
|
@@ -193,7 +204,7 @@ export function ChannelForm({ channelName }: ChannelFormProps) {
|
|
|
193
204
|
);
|
|
194
205
|
}
|
|
195
206
|
|
|
196
|
-
const enabled = Boolean(channelConfig.enabled);
|
|
207
|
+
const enabled = typeof formData.enabled === 'boolean' ? formData.enabled : Boolean(channelConfig.enabled);
|
|
197
208
|
|
|
198
209
|
return (
|
|
199
210
|
<div className={CONFIG_DETAIL_CARD_CLASS}>
|
|
@@ -230,45 +241,66 @@ export function ChannelForm({ channelName }: ChannelFormProps) {
|
|
|
230
241
|
|
|
231
242
|
<form onSubmit={handleSubmit} className="flex min-h-0 flex-1 flex-col">
|
|
232
243
|
<div className="min-h-0 flex-1 space-y-6 overflow-y-auto overscroll-contain px-6 py-5">
|
|
233
|
-
{
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
<summary className="flex cursor-pointer list-none items-center justify-between gap-3 px-5 py-4 text-sm font-medium text-gray-900">
|
|
242
|
-
<div>
|
|
243
|
-
<p>{t('weixinAuthAdvancedTitle')}</p>
|
|
244
|
-
<p className="mt-1 text-xs font-normal text-gray-500">{t('weixinAuthAdvancedDescription')}</p>
|
|
245
|
-
</div>
|
|
246
|
-
<ChevronDown className="h-4 w-4 text-gray-400 transition-transform group-open:rotate-180" />
|
|
247
|
-
</summary>
|
|
248
|
-
<div className="space-y-6 border-t border-gray-100 px-5 py-5">
|
|
244
|
+
{layoutBlocks.map((block, index) => {
|
|
245
|
+
if (block.type === 'fields') {
|
|
246
|
+
const blockFields = resolveFieldsForSection(fields, block.section);
|
|
247
|
+
if (blockFields.length === 0) {
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
if (!block.collapsible) {
|
|
251
|
+
return (
|
|
249
252
|
<ChannelFormFieldsSection
|
|
253
|
+
key={`${block.type}-${block.section}-${index}`}
|
|
250
254
|
channelName={channelName}
|
|
251
|
-
fields={
|
|
255
|
+
fields={blockFields}
|
|
252
256
|
formData={formData}
|
|
253
257
|
jsonDrafts={jsonDrafts}
|
|
254
258
|
setJsonDrafts={setJsonDrafts}
|
|
255
259
|
updateField={updateField}
|
|
256
260
|
uiHints={uiHints}
|
|
257
261
|
/>
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
return (
|
|
265
|
+
<details key={`${block.type}-${block.section}-${index}`} className="group rounded-2xl border border-gray-200/80 bg-white">
|
|
266
|
+
<summary className="flex cursor-pointer list-none items-center justify-between gap-3 px-5 py-4 text-sm font-medium text-gray-900">
|
|
267
|
+
<div>
|
|
268
|
+
<p>{block.collapsible.title}</p>
|
|
269
|
+
{block.collapsible.description ? (
|
|
270
|
+
<p className="mt-1 text-xs font-normal text-gray-500">{block.collapsible.description}</p>
|
|
271
|
+
) : null}
|
|
272
|
+
</div>
|
|
273
|
+
<ChevronDown className="h-4 w-4 text-gray-400 transition-transform group-open:rotate-180" />
|
|
274
|
+
</summary>
|
|
275
|
+
<div className="space-y-6 border-t border-gray-100 px-5 py-5">
|
|
276
|
+
<ChannelFormFieldsSection
|
|
277
|
+
channelName={channelName}
|
|
278
|
+
fields={blockFields}
|
|
279
|
+
formData={formData}
|
|
280
|
+
jsonDrafts={jsonDrafts}
|
|
281
|
+
setJsonDrafts={setJsonDrafts}
|
|
282
|
+
updateField={updateField}
|
|
283
|
+
uiHints={uiHints}
|
|
284
|
+
/>
|
|
285
|
+
</div>
|
|
286
|
+
</details>
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (block.sectionId === 'weixin-auth') {
|
|
291
|
+
return (
|
|
292
|
+
<WeixinChannelAuthSection
|
|
293
|
+
key={`${block.type}-${block.sectionId}-${index}`}
|
|
294
|
+
channelConfig={channelConfig}
|
|
295
|
+
formData={formData}
|
|
296
|
+
channelEnabled={enabled}
|
|
297
|
+
disabled={updateChannel.isPending || Boolean(runningActionId)}
|
|
298
|
+
/>
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return null;
|
|
303
|
+
})}
|
|
272
304
|
</div>
|
|
273
305
|
|
|
274
306
|
<div className="flex flex-wrap items-center justify-between gap-3 border-t border-gray-100 px-6 py-4">
|
|
@@ -7,7 +7,7 @@ import { Input } from '@/components/ui/input';
|
|
|
7
7
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
|
8
8
|
import { SessionRunBadge } from '@/components/common/SessionRunBadge';
|
|
9
9
|
import { adaptNcpSessionSummaries } from '@/components/chat/ncp/ncp-session-adapter';
|
|
10
|
-
import { sessionDisplayName } from '@/components/chat/chat-session-display';
|
|
10
|
+
import { sessionDisplayName, sessionMatchesQuery } from '@/components/chat/chat-session-display';
|
|
11
11
|
import { cn } from '@/lib/utils';
|
|
12
12
|
import { formatDateShort, formatDateTime, t } from '@/lib/i18n';
|
|
13
13
|
import { PageLayout, PageHeader } from '@/components/layout/page-layout';
|
|
@@ -167,15 +167,11 @@ export function SessionsConfig() {
|
|
|
167
167
|
const sessionEntries = useMemo(() => adaptNcpSessionSummaries(sessionSummaries), [sessionSummaries]);
|
|
168
168
|
const sessionSummaryById = useMemo(() => new Map(sessionSummaries.map((session) => [session.sessionId, session])), [sessionSummaries]);
|
|
169
169
|
const filteredSessions = useMemo(() => {
|
|
170
|
-
const normalizedQuery = query.trim().toLowerCase();
|
|
171
170
|
return sessionEntries.filter((session) => {
|
|
172
171
|
if (selectedChannel !== 'all' && resolveChannelFromSessionKey(session.key) !== selectedChannel) {
|
|
173
172
|
return false;
|
|
174
173
|
}
|
|
175
|
-
|
|
176
|
-
return true;
|
|
177
|
-
}
|
|
178
|
-
return session.key.toLowerCase().includes(normalizedQuery) || sessionDisplayName(session).toLowerCase().includes(normalizedQuery);
|
|
174
|
+
return sessionMatchesQuery(session, query);
|
|
179
175
|
});
|
|
180
176
|
}, [query, selectedChannel, sessionEntries]);
|
|
181
177
|
const selectedSession = useMemo(
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { buildChannelFormDefinitions } from './channel-form-fields';
|
|
3
|
+
|
|
4
|
+
describe('buildChannelFormDefinitions', () => {
|
|
5
|
+
it('keeps default channels on a single all-fields layout', () => {
|
|
6
|
+
const definitions = buildChannelFormDefinitions();
|
|
7
|
+
|
|
8
|
+
expect(definitions.telegram?.layout).toBeUndefined();
|
|
9
|
+
expect(definitions.telegram?.fields.some((field) => field.name === 'enabled')).toBe(true);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('declares weixin layout blocks instead of relying on form-level branching', () => {
|
|
13
|
+
const definitions = buildChannelFormDefinitions();
|
|
14
|
+
|
|
15
|
+
expect(definitions.weixin?.layout).toEqual([
|
|
16
|
+
{ type: 'fields', section: 'primary' },
|
|
17
|
+
{ type: 'custom', sectionId: 'weixin-auth' },
|
|
18
|
+
{
|
|
19
|
+
type: 'fields',
|
|
20
|
+
section: 'advanced',
|
|
21
|
+
collapsible: {
|
|
22
|
+
title: 'Advanced settings',
|
|
23
|
+
description: 'Expand these fields only when you need to customize the API base URL, account mapping, or allowlist.'
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
]);
|
|
27
|
+
});
|
|
28
|
+
});
|
|
@@ -2,7 +2,31 @@ import { t } from '@/lib/i18n';
|
|
|
2
2
|
|
|
3
3
|
export type ChannelFieldType = 'boolean' | 'text' | 'email' | 'password' | 'number' | 'tags' | 'select' | 'json';
|
|
4
4
|
export type ChannelOption = { value: string; label: string };
|
|
5
|
-
export type ChannelField = {
|
|
5
|
+
export type ChannelField = {
|
|
6
|
+
name: string;
|
|
7
|
+
type: ChannelFieldType;
|
|
8
|
+
label: string;
|
|
9
|
+
options?: ChannelOption[];
|
|
10
|
+
section?: 'primary' | 'advanced';
|
|
11
|
+
};
|
|
12
|
+
export type ChannelFormFieldSection = 'all' | 'primary' | 'advanced';
|
|
13
|
+
export type ChannelFormBlock =
|
|
14
|
+
| {
|
|
15
|
+
type: 'fields';
|
|
16
|
+
section: ChannelFormFieldSection;
|
|
17
|
+
collapsible?: {
|
|
18
|
+
title: string;
|
|
19
|
+
description?: string;
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
| {
|
|
23
|
+
type: 'custom';
|
|
24
|
+
sectionId: string;
|
|
25
|
+
};
|
|
26
|
+
export type ChannelFormDefinition = {
|
|
27
|
+
fields: ChannelField[];
|
|
28
|
+
layout?: ChannelFormBlock[];
|
|
29
|
+
};
|
|
6
30
|
|
|
7
31
|
const DM_POLICY_OPTIONS: ChannelOption[] = [
|
|
8
32
|
{ value: 'pairing', label: 'pairing' },
|
|
@@ -24,9 +48,10 @@ const STREAMING_MODE_OPTIONS: ChannelOption[] = [
|
|
|
24
48
|
{ value: 'progress', label: 'progress' }
|
|
25
49
|
];
|
|
26
50
|
|
|
27
|
-
export function
|
|
51
|
+
export function buildChannelFormDefinitions(): Record<string, ChannelFormDefinition> {
|
|
28
52
|
return {
|
|
29
|
-
telegram:
|
|
53
|
+
telegram: {
|
|
54
|
+
fields: [
|
|
30
55
|
{ name: 'enabled', type: 'boolean', label: t('enabled') },
|
|
31
56
|
{ name: 'token', type: 'password', label: t('botToken') },
|
|
32
57
|
{ name: 'allowFrom', type: 'tags', label: t('allowFrom') },
|
|
@@ -38,8 +63,10 @@ export function buildChannelFields(): Record<string, ChannelField[]> {
|
|
|
38
63
|
{ name: 'requireMention', type: 'boolean', label: t('requireMention') },
|
|
39
64
|
{ name: 'mentionPatterns', type: 'tags', label: t('mentionPatterns') },
|
|
40
65
|
{ name: 'groups', type: 'json', label: t('groupRulesJson') }
|
|
41
|
-
|
|
42
|
-
|
|
66
|
+
]
|
|
67
|
+
},
|
|
68
|
+
discord: {
|
|
69
|
+
fields: [
|
|
43
70
|
{ name: 'enabled', type: 'boolean', label: t('enabled') },
|
|
44
71
|
{ name: 'token', type: 'password', label: t('botToken') },
|
|
45
72
|
{ name: 'allowBots', type: 'boolean', label: t('allowBotMessages') },
|
|
@@ -58,13 +85,17 @@ export function buildChannelFields(): Record<string, ChannelField[]> {
|
|
|
58
85
|
{ name: 'requireMention', type: 'boolean', label: t('requireMention') },
|
|
59
86
|
{ name: 'mentionPatterns', type: 'tags', label: t('mentionPatterns') },
|
|
60
87
|
{ name: 'groups', type: 'json', label: t('groupRulesJson') }
|
|
61
|
-
|
|
62
|
-
|
|
88
|
+
]
|
|
89
|
+
},
|
|
90
|
+
whatsapp: {
|
|
91
|
+
fields: [
|
|
63
92
|
{ name: 'enabled', type: 'boolean', label: t('enabled') },
|
|
64
93
|
{ name: 'bridgeUrl', type: 'text', label: t('bridgeUrl') },
|
|
65
94
|
{ name: 'allowFrom', type: 'tags', label: t('allowFrom') }
|
|
66
|
-
|
|
67
|
-
|
|
95
|
+
]
|
|
96
|
+
},
|
|
97
|
+
feishu: {
|
|
98
|
+
fields: [
|
|
68
99
|
{ name: 'enabled', type: 'boolean', label: t('enabled') },
|
|
69
100
|
{ name: 'appId', type: 'text', label: t('appId') },
|
|
70
101
|
{ name: 'appSecret', type: 'password', label: t('appSecret') },
|
|
@@ -79,14 +110,18 @@ export function buildChannelFields(): Record<string, ChannelField[]> {
|
|
|
79
110
|
{ name: 'mentionPatterns', type: 'tags', label: t('mentionPatterns') },
|
|
80
111
|
{ name: 'groups', type: 'json', label: t('groupRulesJson') },
|
|
81
112
|
{ name: 'accounts', type: 'json', label: t('accountsJson') }
|
|
82
|
-
|
|
83
|
-
|
|
113
|
+
]
|
|
114
|
+
},
|
|
115
|
+
dingtalk: {
|
|
116
|
+
fields: [
|
|
84
117
|
{ name: 'enabled', type: 'boolean', label: t('enabled') },
|
|
85
118
|
{ name: 'clientId', type: 'text', label: t('clientId') },
|
|
86
119
|
{ name: 'clientSecret', type: 'password', label: t('clientSecret') },
|
|
87
120
|
{ name: 'allowFrom', type: 'tags', label: t('allowFrom') }
|
|
88
|
-
|
|
89
|
-
|
|
121
|
+
]
|
|
122
|
+
},
|
|
123
|
+
wecom: {
|
|
124
|
+
fields: [
|
|
90
125
|
{ name: 'enabled', type: 'boolean', label: t('enabled') },
|
|
91
126
|
{ name: 'corpId', type: 'text', label: t('corpId') },
|
|
92
127
|
{ name: 'agentId', type: 'text', label: t('agentId') },
|
|
@@ -95,24 +130,42 @@ export function buildChannelFields(): Record<string, ChannelField[]> {
|
|
|
95
130
|
{ name: 'callbackPort', type: 'number', label: t('callbackPort') },
|
|
96
131
|
{ name: 'callbackPath', type: 'text', label: t('callbackPath') },
|
|
97
132
|
{ name: 'allowFrom', type: 'tags', label: t('allowFrom') }
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
133
|
+
]
|
|
134
|
+
},
|
|
135
|
+
weixin: {
|
|
136
|
+
fields: [
|
|
137
|
+
{ name: 'enabled', type: 'boolean', label: t('enabled'), section: 'primary' },
|
|
138
|
+
{ name: 'defaultAccountId', type: 'text', label: t('defaultAccountId') },
|
|
139
|
+
{ name: 'baseUrl', type: 'text', label: t('baseUrl') },
|
|
140
|
+
{ name: 'pollTimeoutMs', type: 'number', label: t('pollTimeoutMs') },
|
|
141
|
+
{ name: 'allowFrom', type: 'tags', label: t('allowFrom') },
|
|
142
|
+
{ name: 'accounts', type: 'json', label: t('accountsJson') }
|
|
143
|
+
],
|
|
144
|
+
layout: [
|
|
145
|
+
{ type: 'fields', section: 'primary' },
|
|
146
|
+
{ type: 'custom', sectionId: 'weixin-auth' },
|
|
147
|
+
{
|
|
148
|
+
type: 'fields',
|
|
149
|
+
section: 'advanced',
|
|
150
|
+
collapsible: {
|
|
151
|
+
title: t('weixinAuthAdvancedTitle'),
|
|
152
|
+
description: t('weixinAuthAdvancedDescription')
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
]
|
|
156
|
+
},
|
|
157
|
+
slack: {
|
|
158
|
+
fields: [
|
|
108
159
|
{ name: 'enabled', type: 'boolean', label: t('enabled') },
|
|
109
160
|
{ name: 'mode', type: 'text', label: t('mode') },
|
|
110
161
|
{ name: 'webhookPath', type: 'text', label: t('webhookPath') },
|
|
111
162
|
{ name: 'allowBots', type: 'boolean', label: t('allowBotMessages') },
|
|
112
163
|
{ name: 'botToken', type: 'password', label: t('botToken') },
|
|
113
164
|
{ name: 'appToken', type: 'password', label: t('appToken') }
|
|
114
|
-
|
|
115
|
-
|
|
165
|
+
]
|
|
166
|
+
},
|
|
167
|
+
email: {
|
|
168
|
+
fields: [
|
|
116
169
|
{ name: 'enabled', type: 'boolean', label: t('enabled') },
|
|
117
170
|
{ name: 'consentGranted', type: 'boolean', label: t('consentGranted') },
|
|
118
171
|
{ name: 'imapHost', type: 'text', label: t('imapHost') },
|
|
@@ -120,20 +173,32 @@ export function buildChannelFields(): Record<string, ChannelField[]> {
|
|
|
120
173
|
{ name: 'imapUsername', type: 'text', label: t('imapUsername') },
|
|
121
174
|
{ name: 'imapPassword', type: 'password', label: t('imapPassword') },
|
|
122
175
|
{ name: 'fromAddress', type: 'email', label: t('fromAddress') }
|
|
123
|
-
|
|
124
|
-
|
|
176
|
+
]
|
|
177
|
+
},
|
|
178
|
+
mochat: {
|
|
179
|
+
fields: [
|
|
125
180
|
{ name: 'enabled', type: 'boolean', label: t('enabled') },
|
|
126
181
|
{ name: 'baseUrl', type: 'text', label: t('baseUrl') },
|
|
127
182
|
{ name: 'clawToken', type: 'password', label: t('clawToken') },
|
|
128
183
|
{ name: 'agentUserId', type: 'text', label: t('agentUserId') },
|
|
129
184
|
{ name: 'allowFrom', type: 'tags', label: t('allowFrom') }
|
|
130
|
-
|
|
131
|
-
|
|
185
|
+
]
|
|
186
|
+
},
|
|
187
|
+
qq: {
|
|
188
|
+
fields: [
|
|
132
189
|
{ name: 'enabled', type: 'boolean', label: t('enabled') },
|
|
133
190
|
{ name: 'appId', type: 'text', label: t('appId') },
|
|
134
191
|
{ name: 'secret', type: 'password', label: t('appSecret') },
|
|
135
192
|
{ name: 'markdownSupport', type: 'boolean', label: t('markdownSupport') },
|
|
136
193
|
{ name: 'allowFrom', type: 'tags', label: t('allowFrom') }
|
|
137
|
-
|
|
194
|
+
]
|
|
195
|
+
}
|
|
138
196
|
};
|
|
139
197
|
}
|
|
198
|
+
|
|
199
|
+
export function buildChannelFields(): Record<string, ChannelField[]> {
|
|
200
|
+
const definitions = buildChannelFormDefinitions();
|
|
201
|
+
return Object.fromEntries(
|
|
202
|
+
Object.entries(definitions).map(([channelName, definition]) => [channelName, definition.fields])
|
|
203
|
+
);
|
|
204
|
+
}
|
|
@@ -59,6 +59,7 @@ describe('WeixinChannelAuthSection', () => {
|
|
|
59
59
|
<WeixinChannelAuthSection
|
|
60
60
|
channelConfig={{ enabled: false }}
|
|
61
61
|
formData={{}}
|
|
62
|
+
channelEnabled={false}
|
|
62
63
|
/>
|
|
63
64
|
);
|
|
64
65
|
|
|
@@ -80,6 +81,7 @@ describe('WeixinChannelAuthSection', () => {
|
|
|
80
81
|
}
|
|
81
82
|
}}
|
|
82
83
|
formData={{}}
|
|
84
|
+
channelEnabled={true}
|
|
83
85
|
/>
|
|
84
86
|
);
|
|
85
87
|
|
|
@@ -88,4 +90,28 @@ describe('WeixinChannelAuthSection', () => {
|
|
|
88
90
|
expect(screen.getByRole('button', { name: 'Reconnect with QR' })).toBeTruthy();
|
|
89
91
|
});
|
|
90
92
|
});
|
|
93
|
+
|
|
94
|
+
it('shows connected-but-inactive state when account is authorized but channel is disabled', async () => {
|
|
95
|
+
render(
|
|
96
|
+
<WeixinChannelAuthSection
|
|
97
|
+
channelConfig={{
|
|
98
|
+
enabled: false,
|
|
99
|
+
defaultAccountId: 'bot-1@im.bot',
|
|
100
|
+
accounts: {
|
|
101
|
+
'bot-1@im.bot': {
|
|
102
|
+
enabled: true
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}}
|
|
106
|
+
formData={{ enabled: false }}
|
|
107
|
+
channelEnabled={false}
|
|
108
|
+
/>
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
expect(screen.getByText('Connected, but channel inactive')).toBeTruthy();
|
|
112
|
+
expect(
|
|
113
|
+
screen.getByText('This account is connected, but the channel is inactive. Turn on Enabled before it can send or receive messages.')
|
|
114
|
+
).toBeTruthy();
|
|
115
|
+
expect(screen.getByRole('button', { name: 'Reconnect with QR' })).toBeTruthy();
|
|
116
|
+
});
|
|
91
117
|
});
|