@nextclaw/ui 0.9.15 → 0.9.16
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 +7 -4
- package/dist/assets/ChannelsList-DhM0gvDV.js +1 -0
- package/dist/assets/{ChatPage-Dmpau_7n.js → ChatPage-4niJBFCu.js} +14 -14
- package/dist/assets/{DocBrowser-C3ijFxFF.js → DocBrowser-DpXDQNhb.js} +1 -1
- package/dist/assets/{LogoBadge-BgjXmBcw.js → LogoBadge-nqabOtgk.js} +1 -1
- package/dist/assets/MarketplacePage-CrkTftqZ.js +49 -0
- package/dist/assets/{McpMarketplacePage-DPtH1xcY.js → McpMarketplacePage-DH1qKJqo.js} +1 -1
- package/dist/assets/ModelConfig-CrrxPK_y.js +1 -0
- package/dist/assets/{ProvidersList-DnWsJqMQ.js → ProvidersList-BG36JlSJ.js} +1 -1
- package/dist/assets/{RemoteAccessPage-BrXq-x0-.js → RemoteAccessPage-Dcj2Pzpt.js} +1 -1
- package/dist/assets/{RuntimeConfig-UE9VaFO7.js → RuntimeConfig-BrxgUzjJ.js} +1 -1
- package/dist/assets/{SearchConfig-CP-RM3V3.js → SearchConfig-D-NLwowp.js} +1 -1
- package/dist/assets/{SecretsConfig-CfN_bazs.js → SecretsConfig-DjNqBB05.js} +1 -1
- package/dist/assets/{SessionsConfig-CgkKzKGv.js → SessionsConfig-DdlsWXQc.js} +1 -1
- package/dist/assets/{chat-message-CGL3sMsS.js → chat-message-B7THd1Mh.js} +1 -1
- package/dist/assets/index-CgqD0Jfg.js +8 -0
- package/dist/assets/index-UC08nscf.css +1 -0
- package/dist/assets/{label-CbOSodIL.js → label-B-TkPZRF.js} +1 -1
- package/dist/assets/{page-layout-BtDnyNLf.js → page-layout-BTVBRo6H.js} +1 -1
- package/dist/assets/{popover-DGlUjPQc.js → popover-DBZvpGcL.js} +1 -1
- package/dist/assets/{security-config-D6Bs1yoK.js → security-config-DotxwVFR.js} +1 -1
- package/dist/assets/skeleton-DGtduHZV.js +1 -0
- package/dist/assets/{status-dot-C8vM3IN1.js → status-dot-BCUTVN2R.js} +1 -1
- package/dist/assets/{switch-AuwUiga3.js → switch-Bp2mda29.js} +1 -1
- package/dist/assets/{tabs-custom-CTS7SaFG.js → tabs-custom-BE8yZ2kE.js} +1 -1
- package/dist/assets/{useConfirmDialog-DrMAdNfN.js → useConfirmDialog-DCy-eYnV.js} +1 -1
- package/dist/assets/{vendor-TJ2hy_Lv.js → vendor-DJt0Azq5.js} +90 -80
- package/dist/index.html +3 -3
- package/package.json +4 -4
- package/src/api/channel-auth.ts +35 -0
- package/src/api/channel-auth.types.ts +28 -0
- package/src/api/config.ts +2 -4
- package/src/api/types.ts +7 -26
- package/src/components/chat/ncp/ncp-session-adapter.ts +0 -1
- package/src/components/config/ChannelForm.tsx +41 -128
- package/src/components/config/ChannelsList.test.tsx +71 -10
- package/src/components/config/ModelConfig.test.tsx +78 -0
- package/src/components/config/ModelConfig.tsx +4 -1
- package/src/components/config/channel-form-fields-section.tsx +155 -0
- package/src/components/config/weixin-channel-auth-section.tsx +242 -0
- package/src/hooks/use-channel-auth.ts +16 -0
- package/src/lib/i18n.channel-auth.ts +37 -0
- package/src/lib/i18n.ts +2 -4
- package/src/transport/app-client.ts +22 -6
- package/dist/assets/ChannelsList-Cu_hLbps.js +0 -1
- package/dist/assets/MarketplacePage-CAIdEiw8.js +0 -49
- package/dist/assets/ModelConfig-D-pqArCg.js +0 -1
- package/dist/assets/index-D4alkESd.js +0 -8
- package/dist/assets/index-SGSkQCPi.css +0 -1
- package/dist/assets/skeleton-BLV99JbX.js +0 -1
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import type { Dispatch, SetStateAction } from 'react';
|
|
2
|
+
import { Input } from '@/components/ui/input';
|
|
3
|
+
import { Label } from '@/components/ui/label';
|
|
4
|
+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
|
5
|
+
import { Switch } from '@/components/ui/switch';
|
|
6
|
+
import { TagInput } from '@/components/common/TagInput';
|
|
7
|
+
import { hintForPath } from '@/lib/config-hints';
|
|
8
|
+
import { t } from '@/lib/i18n';
|
|
9
|
+
import { Globe, Hash, KeyRound, Mail, Settings, ToggleLeft } from 'lucide-react';
|
|
10
|
+
import type { ConfigUiHints } from '@/api/types';
|
|
11
|
+
import type { ChannelField } from './channel-form-fields';
|
|
12
|
+
|
|
13
|
+
function getFieldIcon(fieldName: string) {
|
|
14
|
+
if (fieldName.includes('token') || fieldName.includes('secret') || fieldName.includes('password')) {
|
|
15
|
+
return <KeyRound className="h-3.5 w-3.5 text-gray-500" />;
|
|
16
|
+
}
|
|
17
|
+
if (fieldName.includes('url') || fieldName.includes('host')) {
|
|
18
|
+
return <Globe className="h-3.5 w-3.5 text-gray-500" />;
|
|
19
|
+
}
|
|
20
|
+
if (fieldName.includes('email') || fieldName.includes('mail')) {
|
|
21
|
+
return <Mail className="h-3.5 w-3.5 text-gray-500" />;
|
|
22
|
+
}
|
|
23
|
+
if (fieldName.includes('id') || fieldName.includes('from')) {
|
|
24
|
+
return <Hash className="h-3.5 w-3.5 text-gray-500" />;
|
|
25
|
+
}
|
|
26
|
+
if (fieldName === 'enabled' || fieldName === 'consentGranted') {
|
|
27
|
+
return <ToggleLeft className="h-3.5 w-3.5 text-gray-500" />;
|
|
28
|
+
}
|
|
29
|
+
return <Settings className="h-3.5 w-3.5 text-gray-500" />;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
type ChannelFormFieldsSectionProps = {
|
|
33
|
+
channelName: string;
|
|
34
|
+
fields: ChannelField[];
|
|
35
|
+
formData: Record<string, unknown>;
|
|
36
|
+
jsonDrafts: Record<string, string>;
|
|
37
|
+
setJsonDrafts: Dispatch<SetStateAction<Record<string, string>>>;
|
|
38
|
+
updateField: (name: string, value: unknown) => void;
|
|
39
|
+
uiHints?: ConfigUiHints;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export function ChannelFormFieldsSection({
|
|
43
|
+
channelName,
|
|
44
|
+
fields,
|
|
45
|
+
formData,
|
|
46
|
+
jsonDrafts,
|
|
47
|
+
setJsonDrafts,
|
|
48
|
+
updateField,
|
|
49
|
+
uiHints
|
|
50
|
+
}: ChannelFormFieldsSectionProps) {
|
|
51
|
+
return (
|
|
52
|
+
<>
|
|
53
|
+
{fields.map((field) => {
|
|
54
|
+
const hint = hintForPath(`channels.${channelName}.${field.name}`, uiHints);
|
|
55
|
+
const label = hint?.label ?? field.label;
|
|
56
|
+
const placeholder = hint?.placeholder;
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<div key={field.name} className="space-y-2.5">
|
|
60
|
+
<Label htmlFor={field.name} className="flex items-center gap-2 text-sm font-medium text-gray-900">
|
|
61
|
+
{getFieldIcon(field.name)}
|
|
62
|
+
{label}
|
|
63
|
+
</Label>
|
|
64
|
+
|
|
65
|
+
{field.type === 'boolean' && (
|
|
66
|
+
<div className="flex items-center justify-between rounded-xl bg-gray-50 p-3">
|
|
67
|
+
<span className="text-sm text-gray-500">
|
|
68
|
+
{(formData[field.name] as boolean) ? t('enabled') : t('disabled')}
|
|
69
|
+
</span>
|
|
70
|
+
<Switch
|
|
71
|
+
id={field.name}
|
|
72
|
+
checked={(formData[field.name] as boolean) || false}
|
|
73
|
+
onCheckedChange={(checked) => updateField(field.name, checked)}
|
|
74
|
+
className="data-[state=checked]:bg-emerald-500"
|
|
75
|
+
/>
|
|
76
|
+
</div>
|
|
77
|
+
)}
|
|
78
|
+
|
|
79
|
+
{(field.type === 'text' || field.type === 'email') && (
|
|
80
|
+
<Input
|
|
81
|
+
id={field.name}
|
|
82
|
+
type={field.type}
|
|
83
|
+
value={(formData[field.name] as string) || ''}
|
|
84
|
+
onChange={(event) => updateField(field.name, event.target.value)}
|
|
85
|
+
placeholder={placeholder}
|
|
86
|
+
className="rounded-xl"
|
|
87
|
+
/>
|
|
88
|
+
)}
|
|
89
|
+
|
|
90
|
+
{field.type === 'password' && (
|
|
91
|
+
<Input
|
|
92
|
+
id={field.name}
|
|
93
|
+
type="password"
|
|
94
|
+
value={(formData[field.name] as string) || ''}
|
|
95
|
+
onChange={(event) => updateField(field.name, event.target.value)}
|
|
96
|
+
placeholder={placeholder ?? t('leaveBlankToKeepUnchanged')}
|
|
97
|
+
className="rounded-xl"
|
|
98
|
+
/>
|
|
99
|
+
)}
|
|
100
|
+
|
|
101
|
+
{field.type === 'number' && (
|
|
102
|
+
<Input
|
|
103
|
+
id={field.name}
|
|
104
|
+
type="number"
|
|
105
|
+
value={(formData[field.name] as number) || 0}
|
|
106
|
+
onChange={(event) => updateField(field.name, parseInt(event.target.value, 10) || 0)}
|
|
107
|
+
placeholder={placeholder}
|
|
108
|
+
className="rounded-xl"
|
|
109
|
+
/>
|
|
110
|
+
)}
|
|
111
|
+
|
|
112
|
+
{field.type === 'tags' && (
|
|
113
|
+
<TagInput
|
|
114
|
+
value={(formData[field.name] as string[]) || []}
|
|
115
|
+
onChange={(tags) => updateField(field.name, tags)}
|
|
116
|
+
/>
|
|
117
|
+
)}
|
|
118
|
+
|
|
119
|
+
{field.type === 'select' && (
|
|
120
|
+
<Select
|
|
121
|
+
value={(formData[field.name] as string) || ''}
|
|
122
|
+
onValueChange={(value) => updateField(field.name, value)}
|
|
123
|
+
>
|
|
124
|
+
<SelectTrigger className="rounded-xl">
|
|
125
|
+
<SelectValue />
|
|
126
|
+
</SelectTrigger>
|
|
127
|
+
<SelectContent>
|
|
128
|
+
{(field.options ?? []).map((option) => (
|
|
129
|
+
<SelectItem key={option.value} value={option.value}>
|
|
130
|
+
{option.label}
|
|
131
|
+
</SelectItem>
|
|
132
|
+
))}
|
|
133
|
+
</SelectContent>
|
|
134
|
+
</Select>
|
|
135
|
+
)}
|
|
136
|
+
|
|
137
|
+
{field.type === 'json' && (
|
|
138
|
+
<textarea
|
|
139
|
+
id={field.name}
|
|
140
|
+
value={jsonDrafts[field.name] ?? '{}'}
|
|
141
|
+
onChange={(event) =>
|
|
142
|
+
setJsonDrafts((prev) => ({
|
|
143
|
+
...prev,
|
|
144
|
+
[field.name]: event.target.value
|
|
145
|
+
}))
|
|
146
|
+
}
|
|
147
|
+
className="min-h-[120px] w-full resize-none rounded-lg border border-gray-200 bg-white px-3 py-2 text-xs font-mono"
|
|
148
|
+
/>
|
|
149
|
+
)}
|
|
150
|
+
</div>
|
|
151
|
+
);
|
|
152
|
+
})}
|
|
153
|
+
</>
|
|
154
|
+
);
|
|
155
|
+
}
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import { useEffect, useMemo, useState } from 'react';
|
|
2
|
+
import { useQueryClient } from '@tanstack/react-query';
|
|
3
|
+
import { Button } from '@/components/ui/button';
|
|
4
|
+
import { formatDateTime, t } from '@/lib/i18n';
|
|
5
|
+
import { cn } from '@/lib/utils';
|
|
6
|
+
import { toast } from 'sonner';
|
|
7
|
+
import { ExternalLink, Loader2, MessageCircleMore, QrCode } from 'lucide-react';
|
|
8
|
+
import { usePollChannelAuth, useStartChannelAuth } from '@/hooks/use-channel-auth';
|
|
9
|
+
import type { ChannelAuthPollResult, ChannelAuthStartResult } from '@/api/channel-auth.types';
|
|
10
|
+
|
|
11
|
+
type WeixinChannelAuthSectionProps = {
|
|
12
|
+
channelConfig: Record<string, unknown>;
|
|
13
|
+
formData: Record<string, unknown>;
|
|
14
|
+
disabled?: boolean;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
function resolveConnectedAccountIds(channelConfig: Record<string, unknown>): string[] {
|
|
18
|
+
const accounts = channelConfig.accounts;
|
|
19
|
+
const ids = new Set<string>();
|
|
20
|
+
if (typeof channelConfig.defaultAccountId === 'string' && channelConfig.defaultAccountId.trim()) {
|
|
21
|
+
ids.add(channelConfig.defaultAccountId.trim());
|
|
22
|
+
}
|
|
23
|
+
if (accounts && typeof accounts === 'object' && !Array.isArray(accounts)) {
|
|
24
|
+
for (const accountId of Object.keys(accounts)) {
|
|
25
|
+
const trimmed = accountId.trim();
|
|
26
|
+
if (trimmed) {
|
|
27
|
+
ids.add(trimmed);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return [...ids];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function resolveBaseUrl(formData: Record<string, unknown>, channelConfig: Record<string, unknown>): string | undefined {
|
|
35
|
+
if (typeof formData.baseUrl === 'string' && formData.baseUrl.trim()) {
|
|
36
|
+
return formData.baseUrl.trim();
|
|
37
|
+
}
|
|
38
|
+
if (typeof channelConfig.baseUrl === 'string' && channelConfig.baseUrl.trim()) {
|
|
39
|
+
return channelConfig.baseUrl.trim();
|
|
40
|
+
}
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function WeixinChannelAuthSection({
|
|
45
|
+
channelConfig,
|
|
46
|
+
formData,
|
|
47
|
+
disabled = false
|
|
48
|
+
}: WeixinChannelAuthSectionProps) {
|
|
49
|
+
const queryClient = useQueryClient();
|
|
50
|
+
const startChannelAuth = useStartChannelAuth();
|
|
51
|
+
const pollChannelAuth = usePollChannelAuth();
|
|
52
|
+
const [activeSession, setActiveSession] = useState<ChannelAuthStartResult | null>(null);
|
|
53
|
+
const [authState, setAuthState] = useState<ChannelAuthPollResult | null>(null);
|
|
54
|
+
|
|
55
|
+
const connectedAccountIds = useMemo(() => resolveConnectedAccountIds(channelConfig), [channelConfig]);
|
|
56
|
+
const primaryAccountId = connectedAccountIds[0];
|
|
57
|
+
const baseUrl = resolveBaseUrl(formData, channelConfig);
|
|
58
|
+
const hasConnectedAccount = connectedAccountIds.length > 0;
|
|
59
|
+
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
if (!activeSession) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
let cancelled = false;
|
|
66
|
+
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
67
|
+
|
|
68
|
+
const runPoll = async () => {
|
|
69
|
+
try {
|
|
70
|
+
const result = await pollChannelAuth.mutateAsync({
|
|
71
|
+
channel: 'weixin',
|
|
72
|
+
data: { sessionId: activeSession.sessionId }
|
|
73
|
+
});
|
|
74
|
+
if (cancelled) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
setAuthState(result);
|
|
79
|
+
|
|
80
|
+
if (result.status === 'authorized') {
|
|
81
|
+
await queryClient.invalidateQueries({ queryKey: ['config'] });
|
|
82
|
+
await queryClient.invalidateQueries({ queryKey: ['config-meta'] });
|
|
83
|
+
toast.success(result.message || t('weixinAuthAuthorized'));
|
|
84
|
+
setActiveSession(null);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (result.status === 'expired' || result.status === 'error') {
|
|
89
|
+
toast.error(result.message || t('weixinAuthRetryRequired'));
|
|
90
|
+
setActiveSession(null);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
timer = setTimeout(runPoll, result.nextPollMs ?? activeSession.intervalMs);
|
|
95
|
+
} catch (error) {
|
|
96
|
+
if (cancelled) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
100
|
+
toast.error(`${t('error')}: ${message}`);
|
|
101
|
+
setActiveSession(null);
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
timer = setTimeout(runPoll, activeSession.intervalMs);
|
|
106
|
+
|
|
107
|
+
return () => {
|
|
108
|
+
cancelled = true;
|
|
109
|
+
if (timer) {
|
|
110
|
+
clearTimeout(timer);
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
}, [activeSession, pollChannelAuth, queryClient]);
|
|
114
|
+
|
|
115
|
+
const handleStartAuth = async () => {
|
|
116
|
+
try {
|
|
117
|
+
const result = await startChannelAuth.mutateAsync({
|
|
118
|
+
channel: 'weixin',
|
|
119
|
+
data: {
|
|
120
|
+
baseUrl,
|
|
121
|
+
accountId:
|
|
122
|
+
typeof formData.defaultAccountId === 'string' && formData.defaultAccountId.trim()
|
|
123
|
+
? formData.defaultAccountId.trim()
|
|
124
|
+
: undefined
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
setActiveSession(result);
|
|
128
|
+
setAuthState({
|
|
129
|
+
channel: 'weixin',
|
|
130
|
+
status: 'pending',
|
|
131
|
+
message: result.note,
|
|
132
|
+
nextPollMs: result.intervalMs
|
|
133
|
+
});
|
|
134
|
+
} catch (error) {
|
|
135
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
136
|
+
toast.error(`${t('error')}: ${message}`);
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const statusLabel = activeSession
|
|
141
|
+
? authState?.status === 'scanned'
|
|
142
|
+
? t('weixinAuthScanned')
|
|
143
|
+
: t('weixinAuthWaiting')
|
|
144
|
+
: hasConnectedAccount
|
|
145
|
+
? t('weixinAuthAuthorized')
|
|
146
|
+
: t('weixinAuthNotConnected');
|
|
147
|
+
|
|
148
|
+
const connectButtonLabel = startChannelAuth.isPending
|
|
149
|
+
? t('weixinAuthStarting')
|
|
150
|
+
: activeSession
|
|
151
|
+
? t('weixinAuthWaiting')
|
|
152
|
+
: hasConnectedAccount
|
|
153
|
+
? t('weixinAuthReconnect')
|
|
154
|
+
: t('weixinAuthConnect');
|
|
155
|
+
|
|
156
|
+
return (
|
|
157
|
+
<section className="rounded-2xl border border-primary/20 bg-gradient-to-br from-primary-50/70 via-white to-emerald-50/60 p-5">
|
|
158
|
+
<div className="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
|
|
159
|
+
<div className="space-y-3">
|
|
160
|
+
<div className="inline-flex items-center gap-2 rounded-full bg-white/90 px-3 py-1 text-xs font-medium text-primary shadow-sm">
|
|
161
|
+
<QrCode className="h-3.5 w-3.5" />
|
|
162
|
+
{t('weixinAuthTitle')}
|
|
163
|
+
</div>
|
|
164
|
+
<div>
|
|
165
|
+
<h4 className="text-base font-semibold text-gray-900">{t('weixinAuthDescription')}</h4>
|
|
166
|
+
<p className="mt-1 text-sm text-gray-600">{t('weixinAuthHint')}</p>
|
|
167
|
+
</div>
|
|
168
|
+
<div
|
|
169
|
+
className={cn(
|
|
170
|
+
'inline-flex w-fit items-center gap-2 rounded-full px-3 py-1 text-xs font-medium',
|
|
171
|
+
activeSession
|
|
172
|
+
? 'bg-amber-50 text-amber-700'
|
|
173
|
+
: hasConnectedAccount
|
|
174
|
+
? 'bg-emerald-50 text-emerald-700'
|
|
175
|
+
: 'bg-gray-100 text-gray-600'
|
|
176
|
+
)}
|
|
177
|
+
>
|
|
178
|
+
{activeSession ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <MessageCircleMore className="h-3.5 w-3.5" />}
|
|
179
|
+
{statusLabel}
|
|
180
|
+
</div>
|
|
181
|
+
<div className="space-y-1 text-sm text-gray-600">
|
|
182
|
+
<p>{t('weixinAuthCapabilityHint')}</p>
|
|
183
|
+
{primaryAccountId ? (
|
|
184
|
+
<p>
|
|
185
|
+
{t('weixinAuthPrimaryAccount')}: <span className="font-mono text-xs text-gray-900">{primaryAccountId}</span>
|
|
186
|
+
</p>
|
|
187
|
+
) : null}
|
|
188
|
+
{connectedAccountIds.length > 1 ? (
|
|
189
|
+
<p>
|
|
190
|
+
{t('weixinAuthConnectedAccounts')}: <span className="font-mono text-xs text-gray-900">{connectedAccountIds.join(', ')}</span>
|
|
191
|
+
</p>
|
|
192
|
+
) : null}
|
|
193
|
+
{baseUrl ? (
|
|
194
|
+
<p>
|
|
195
|
+
{t('weixinAuthBaseUrl')}: <span className="font-mono text-xs text-gray-900">{baseUrl}</span>
|
|
196
|
+
</p>
|
|
197
|
+
) : null}
|
|
198
|
+
</div>
|
|
199
|
+
<Button
|
|
200
|
+
type="button"
|
|
201
|
+
onClick={handleStartAuth}
|
|
202
|
+
disabled={disabled || startChannelAuth.isPending || Boolean(activeSession)}
|
|
203
|
+
className="rounded-xl"
|
|
204
|
+
>
|
|
205
|
+
{connectButtonLabel}
|
|
206
|
+
</Button>
|
|
207
|
+
</div>
|
|
208
|
+
|
|
209
|
+
<div className="w-full max-w-sm rounded-2xl border border-dashed border-primary/25 bg-white/85 p-4 shadow-sm">
|
|
210
|
+
{activeSession ? (
|
|
211
|
+
<div className="space-y-3">
|
|
212
|
+
<div className="overflow-hidden rounded-2xl border border-gray-100 bg-white p-3">
|
|
213
|
+
<img src={activeSession.qrCodeUrl} alt={t('weixinAuthQrAlt')} className="mx-auto aspect-square w-full max-w-[240px] object-contain" />
|
|
214
|
+
</div>
|
|
215
|
+
<div className="space-y-1 text-xs text-gray-500">
|
|
216
|
+
<p>{authState?.message || activeSession.note || t('weixinAuthScanPrompt')}</p>
|
|
217
|
+
<p>
|
|
218
|
+
{t('weixinAuthExpiresAt')}: {formatDateTime(activeSession.expiresAt)}
|
|
219
|
+
</p>
|
|
220
|
+
</div>
|
|
221
|
+
<a
|
|
222
|
+
href={activeSession.qrCodeUrl}
|
|
223
|
+
target="_blank"
|
|
224
|
+
rel="noreferrer"
|
|
225
|
+
className="inline-flex items-center gap-1.5 text-xs text-primary transition-colors hover:text-primary-hover"
|
|
226
|
+
>
|
|
227
|
+
<ExternalLink className="h-3.5 w-3.5" />
|
|
228
|
+
{t('weixinAuthOpenQr')}
|
|
229
|
+
</a>
|
|
230
|
+
</div>
|
|
231
|
+
) : (
|
|
232
|
+
<div className="flex min-h-[280px] flex-col items-center justify-center rounded-2xl bg-gray-50/80 px-6 text-center">
|
|
233
|
+
<QrCode className="h-9 w-9 text-gray-300" />
|
|
234
|
+
<p className="mt-3 text-sm font-medium text-gray-700">{t('weixinAuthReadyTitle')}</p>
|
|
235
|
+
<p className="mt-1 text-xs leading-5 text-gray-500">{t('weixinAuthReadyDescription')}</p>
|
|
236
|
+
</div>
|
|
237
|
+
)}
|
|
238
|
+
</div>
|
|
239
|
+
</div>
|
|
240
|
+
</section>
|
|
241
|
+
);
|
|
242
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { useMutation } from '@tanstack/react-query';
|
|
2
|
+
import { pollChannelAuth, startChannelAuth } from '@/api/channel-auth';
|
|
3
|
+
|
|
4
|
+
export function useStartChannelAuth() {
|
|
5
|
+
return useMutation({
|
|
6
|
+
mutationFn: ({ channel, data }: { channel: string; data?: unknown }) =>
|
|
7
|
+
startChannelAuth(channel, data as Parameters<typeof startChannelAuth>[1])
|
|
8
|
+
});
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function usePollChannelAuth() {
|
|
12
|
+
return useMutation({
|
|
13
|
+
mutationFn: ({ channel, data }: { channel: string; data: unknown }) =>
|
|
14
|
+
pollChannelAuth(channel, data as Parameters<typeof pollChannelAuth>[1])
|
|
15
|
+
});
|
|
16
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export const CHANNEL_AUTH_LABELS: Record<string, { zh: string; en: string }> = {
|
|
2
|
+
weixinAuthTitle: { zh: '扫码连接微信', en: 'Connect Weixin by QR' },
|
|
3
|
+
weixinAuthDescription: { zh: '微信渠道现在以扫码连接为主流程。', en: 'Weixin now uses QR login as the primary setup flow.' },
|
|
4
|
+
weixinAuthHint: {
|
|
5
|
+
zh: '通常只需要点击按钮并扫码确认,连接成功后会自动写入配置。',
|
|
6
|
+
en: 'In most cases you only need to start the flow, scan the QR code, and confirm on your phone. The config will be saved automatically.'
|
|
7
|
+
},
|
|
8
|
+
weixinAuthCapabilityHint: {
|
|
9
|
+
zh: '连接成功后,Agent 可以通过微信渠道向已知微信用户主动发消息。',
|
|
10
|
+
en: 'After connecting, the agent can proactively message known Weixin users through this channel.'
|
|
11
|
+
},
|
|
12
|
+
weixinAuthPrimaryAccount: { zh: '当前默认账号', en: 'Current default account' },
|
|
13
|
+
weixinAuthConnectedAccounts: { zh: '已连接账号', en: 'Connected accounts' },
|
|
14
|
+
weixinAuthBaseUrl: { zh: '当前接口地址', en: 'Current API base URL' },
|
|
15
|
+
weixinAuthConnect: { zh: '扫码连接微信', en: 'Scan QR to connect Weixin' },
|
|
16
|
+
weixinAuthReconnect: { zh: '重新扫码连接', en: 'Reconnect with QR' },
|
|
17
|
+
weixinAuthStarting: { zh: '正在生成二维码...', en: 'Generating QR code...' },
|
|
18
|
+
weixinAuthWaiting: { zh: '等待扫码确认', en: 'Waiting for scan confirmation' },
|
|
19
|
+
weixinAuthScanned: { zh: '已扫码,等待确认', en: 'Scanned, waiting for confirmation' },
|
|
20
|
+
weixinAuthAuthorized: { zh: '已连接', en: 'Connected' },
|
|
21
|
+
weixinAuthNotConnected: { zh: '未连接', en: 'Not connected' },
|
|
22
|
+
weixinAuthRetryRequired: { zh: '二维码已失效,请重新扫码。', en: 'QR session expired. Please start again.' },
|
|
23
|
+
weixinAuthQrAlt: { zh: '微信登录二维码', en: 'Weixin login QR code' },
|
|
24
|
+
weixinAuthScanPrompt: { zh: '请用微信扫码,并在手机上确认登录。', en: 'Scan with Weixin and confirm the login on your phone.' },
|
|
25
|
+
weixinAuthExpiresAt: { zh: '二维码过期时间', en: 'QR expires at' },
|
|
26
|
+
weixinAuthOpenQr: { zh: '新窗口打开二维码', en: 'Open QR code in new tab' },
|
|
27
|
+
weixinAuthReadyTitle: { zh: '准备连接微信', en: 'Ready to connect Weixin' },
|
|
28
|
+
weixinAuthReadyDescription: {
|
|
29
|
+
zh: '点击左侧按钮后,这里会显示二维码。整个首配流程默认不需要手动填写底层参数。',
|
|
30
|
+
en: 'After you start the flow, the QR code will appear here. Most first-time setups do not require filling low-level fields manually.'
|
|
31
|
+
},
|
|
32
|
+
weixinAuthAdvancedTitle: { zh: '高级设置', en: 'Advanced settings' },
|
|
33
|
+
weixinAuthAdvancedDescription: {
|
|
34
|
+
zh: '仅在你需要自定义接口地址、账号映射或白名单时再展开这些字段。',
|
|
35
|
+
en: 'Expand these fields only when you need to customize the API base URL, account mapping, or allowlist.'
|
|
36
|
+
}
|
|
37
|
+
};
|
package/src/lib/i18n.ts
CHANGED
|
@@ -1,16 +1,13 @@
|
|
|
1
1
|
import { CHANNEL_LABELS } from './i18n.channels';
|
|
2
|
+
import { CHANNEL_AUTH_LABELS } from './i18n.channel-auth';
|
|
2
3
|
import { MARKETPLACE_LABELS } from './i18n.marketplace';
|
|
3
4
|
import { REMOTE_LABELS } from './i18n.remote';
|
|
4
|
-
|
|
5
5
|
export type I18nLanguage = 'zh' | 'en';
|
|
6
|
-
|
|
7
6
|
const I18N_STORAGE_KEY = 'nextclaw.ui.language';
|
|
8
|
-
|
|
9
7
|
export const LANGUAGE_OPTIONS: Array<{ value: I18nLanguage; label: string }> = [
|
|
10
8
|
{ value: 'en', label: 'English' },
|
|
11
9
|
{ value: 'zh', label: '中文' }
|
|
12
10
|
];
|
|
13
|
-
|
|
14
11
|
const LANGUAGE_TO_LOCALE: Record<I18nLanguage, string> = {
|
|
15
12
|
en: 'en-US',
|
|
16
13
|
zh: 'zh-CN'
|
|
@@ -737,6 +734,7 @@ export const LABELS: Record<string, { zh: string; en: string }> = {
|
|
|
737
734
|
docBrowserNewTab: { zh: '新建标签', en: 'New Tab' },
|
|
738
735
|
docBrowserCloseTab: { zh: '关闭标签', en: 'Close Tab' },
|
|
739
736
|
docBrowserTabUntitled: { zh: '未命名', en: 'Untitled' },
|
|
737
|
+
...CHANNEL_AUTH_LABELS,
|
|
740
738
|
};
|
|
741
739
|
|
|
742
740
|
export function t(key: string, lang: I18nLanguage = getLanguage()): string {
|
|
@@ -4,6 +4,11 @@ import { RemoteSessionMultiplexTransport } from './remote.transport';
|
|
|
4
4
|
import type { AppTransport, RemoteRuntimeInfo, RequestInput, StreamInput, StreamSession } from './transport.types';
|
|
5
5
|
|
|
6
6
|
const REMOTE_RUNTIME_PATH = '/_remote/runtime';
|
|
7
|
+
const DEFAULT_REMOTE_RUNTIME: RemoteRuntimeInfo = {
|
|
8
|
+
mode: 'remote',
|
|
9
|
+
protocolVersion: 1,
|
|
10
|
+
wsPath: '/_remote/ws'
|
|
11
|
+
};
|
|
7
12
|
|
|
8
13
|
async function resolveRuntime(apiBase: string): Promise<AppTransport> {
|
|
9
14
|
const runtimeUrl = `${apiBase.replace(/\/$/, '')}${REMOTE_RUNTIME_PATH}`;
|
|
@@ -22,17 +27,28 @@ async function resolveRuntime(apiBase: string): Promise<AppTransport> {
|
|
|
22
27
|
return new LocalAppTransport({ apiBase });
|
|
23
28
|
}
|
|
24
29
|
|
|
25
|
-
const
|
|
30
|
+
const contentType = response.headers.get('content-type')?.toLowerCase() ?? '';
|
|
31
|
+
if (!contentType.includes('application/json')) {
|
|
32
|
+
return response.status >= 400
|
|
33
|
+
? new RemoteSessionMultiplexTransport(DEFAULT_REMOTE_RUNTIME, apiBase)
|
|
34
|
+
: new LocalAppTransport({ apiBase });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
let payload: { ok?: boolean; data?: RemoteRuntimeInfo } | null = null;
|
|
38
|
+
try {
|
|
39
|
+
payload = await response.json() as { ok?: boolean; data?: RemoteRuntimeInfo };
|
|
40
|
+
} catch {
|
|
41
|
+
return response.status >= 400
|
|
42
|
+
? new RemoteSessionMultiplexTransport(DEFAULT_REMOTE_RUNTIME, apiBase)
|
|
43
|
+
: new LocalAppTransport({ apiBase });
|
|
44
|
+
}
|
|
45
|
+
|
|
26
46
|
if (response.ok && payload.ok && payload.data?.mode === 'remote') {
|
|
27
47
|
return new RemoteSessionMultiplexTransport(payload.data, apiBase);
|
|
28
48
|
}
|
|
29
49
|
|
|
30
50
|
if (response.status >= 400) {
|
|
31
|
-
return new RemoteSessionMultiplexTransport(
|
|
32
|
-
mode: 'remote',
|
|
33
|
-
protocolVersion: 1,
|
|
34
|
-
wsPath: '/_remote/ws'
|
|
35
|
-
}, apiBase);
|
|
51
|
+
return new RemoteSessionMultiplexTransport(DEFAULT_REMOTE_RUNTIME, apiBase);
|
|
36
52
|
}
|
|
37
53
|
|
|
38
54
|
return new LocalAppTransport({ apiBase });
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{r as v,j as a,X as Z,a3 as ee,e as T,K as ae,aq as te,b1 as se,b2 as le,b3 as ne,a0 as oe,G as re,ai as ce,H as ie}from"./vendor-TJ2hy_Lv.js";import{t as e,c as P,Z as me,u as q,a as $,b as H,$ as pe,a0 as de,I as A,S as be,e as ue,f as xe,g as ye,h as ge,B as E}from"./index-D4alkESd.js";import{L as he}from"./label-CbOSodIL.js";import{S as fe}from"./switch-AuwUiga3.js";import{S as J}from"./status-dot-C8vM3IN1.js";import{L as K}from"./LogoBadge-BgjXmBcw.js";import{h as U}from"./config-hints-CApS3K_7.js";import{c as we,b as ve,a as je,C as ke}from"./config-layout-BHnOoweL.js";import{T as Se}from"./tabs-custom-CTS7SaFG.js";import{P as Ce,a as Ne}from"./page-layout-BtDnyNLf.js";function Ie({value:t,onChange:m,className:i,placeholder:o=""}){const[r,u]=v.useState(""),d=x=>{x.key==="Enter"&&r.trim()?(x.preventDefault(),m([...t,r.trim()]),u("")):x.key==="Backspace"&&!r&&t.length>0&&m(t.slice(0,-1))},g=x=>{m(t.filter((j,h)=>h!==x))};return a.jsxs("div",{className:P("flex flex-wrap gap-2 p-2 border rounded-md min-h-[42px]",i),children:[t.map((x,j)=>a.jsxs("span",{className:"inline-flex items-center gap-1 px-2 py-1 bg-primary text-primary-foreground rounded text-sm",children:[x,a.jsx("button",{type:"button",onClick:()=>g(j),className:"hover:text-red-300 transition-colors",children:a.jsx(Z,{className:"h-3 w-3"})})]},j)),a.jsx("input",{type:"text",value:r,onChange:x=>u(x.target.value),onKeyDown:d,className:"flex-1 outline-none min-w-[100px] bg-transparent text-sm",placeholder:o||e("enterTag")})]})}function z(t){var o,r;const m=me();return((o=t.tutorialUrls)==null?void 0:o[m])||((r=t.tutorialUrls)==null?void 0:r.default)||t.tutorialUrl}const Pe={telegram:"telegram.svg",slack:"slack.svg",discord:"discord.svg",whatsapp:"whatsapp.svg",qq:"qq.svg",feishu:"feishu.svg",dingtalk:"dingtalk.svg",wecom:"wecom.svg",weixin:"weixin.svg",mochat:"mochat.svg",email:"email.svg"};function Fe(t,m){const i=m.toLowerCase(),o=t[i];return o?`/logos/${o}`:null}function Y(t){return Fe(Pe,t)}const B=[{value:"pairing",label:"pairing"},{value:"allowlist",label:"allowlist"},{value:"open",label:"open"},{value:"disabled",label:"disabled"}],G=[{value:"open",label:"open"},{value:"allowlist",label:"allowlist"},{value:"disabled",label:"disabled"}],Te=[{value:"off",label:"off"},{value:"partial",label:"partial"},{value:"block",label:"block"},{value:"progress",label:"progress"}];function R(){return{telegram:[{name:"enabled",type:"boolean",label:e("enabled")},{name:"token",type:"password",label:e("botToken")},{name:"allowFrom",type:"tags",label:e("allowFrom")},{name:"proxy",type:"text",label:e("proxy")},{name:"accountId",type:"text",label:e("accountId")},{name:"dmPolicy",type:"select",label:e("dmPolicy"),options:B},{name:"groupPolicy",type:"select",label:e("groupPolicy"),options:G},{name:"groupAllowFrom",type:"tags",label:e("groupAllowFrom")},{name:"requireMention",type:"boolean",label:e("requireMention")},{name:"mentionPatterns",type:"tags",label:e("mentionPatterns")},{name:"groups",type:"json",label:e("groupRulesJson")}],discord:[{name:"enabled",type:"boolean",label:e("enabled")},{name:"token",type:"password",label:e("botToken")},{name:"allowBots",type:"boolean",label:e("allowBotMessages")},{name:"allowFrom",type:"tags",label:e("allowFrom")},{name:"gatewayUrl",type:"text",label:e("gatewayUrl")},{name:"intents",type:"number",label:e("intents")},{name:"proxy",type:"text",label:e("proxy")},{name:"mediaMaxMb",type:"number",label:e("attachmentMaxSizeMb")},{name:"streaming",type:"select",label:e("streamingMode"),options:Te},{name:"draftChunk",type:"json",label:e("draftChunkingJson")},{name:"textChunkLimit",type:"number",label:e("textChunkLimit")},{name:"accountId",type:"text",label:e("accountId")},{name:"dmPolicy",type:"select",label:e("dmPolicy"),options:B},{name:"groupPolicy",type:"select",label:e("groupPolicy"),options:G},{name:"groupAllowFrom",type:"tags",label:e("groupAllowFrom")},{name:"requireMention",type:"boolean",label:e("requireMention")},{name:"mentionPatterns",type:"tags",label:e("mentionPatterns")},{name:"groups",type:"json",label:e("groupRulesJson")}],whatsapp:[{name:"enabled",type:"boolean",label:e("enabled")},{name:"bridgeUrl",type:"text",label:e("bridgeUrl")},{name:"allowFrom",type:"tags",label:e("allowFrom")}],feishu:[{name:"enabled",type:"boolean",label:e("enabled")},{name:"appId",type:"text",label:e("appId")},{name:"appSecret",type:"password",label:e("appSecret")},{name:"encryptKey",type:"password",label:e("encryptKey")},{name:"verificationToken",type:"password",label:e("verificationToken")},{name:"allowFrom",type:"tags",label:e("allowFrom")}],dingtalk:[{name:"enabled",type:"boolean",label:e("enabled")},{name:"clientId",type:"text",label:e("clientId")},{name:"clientSecret",type:"password",label:e("clientSecret")},{name:"allowFrom",type:"tags",label:e("allowFrom")}],wecom:[{name:"enabled",type:"boolean",label:e("enabled")},{name:"corpId",type:"text",label:e("corpId")},{name:"agentId",type:"text",label:e("agentId")},{name:"secret",type:"password",label:e("secret")},{name:"token",type:"password",label:e("token")},{name:"callbackPort",type:"number",label:e("callbackPort")},{name:"callbackPath",type:"text",label:e("callbackPath")},{name:"allowFrom",type:"tags",label:e("allowFrom")}],weixin:[{name:"enabled",type:"boolean",label:e("enabled")},{name:"defaultAccountId",type:"text",label:e("defaultAccountId")},{name:"baseUrl",type:"text",label:e("baseUrl")},{name:"pollTimeoutMs",type:"number",label:e("pollTimeoutMs")},{name:"allowFrom",type:"tags",label:e("allowFrom")},{name:"accounts",type:"json",label:e("accountsJson")}],slack:[{name:"enabled",type:"boolean",label:e("enabled")},{name:"mode",type:"text",label:e("mode")},{name:"webhookPath",type:"text",label:e("webhookPath")},{name:"allowBots",type:"boolean",label:e("allowBotMessages")},{name:"botToken",type:"password",label:e("botToken")},{name:"appToken",type:"password",label:e("appToken")}],email:[{name:"enabled",type:"boolean",label:e("enabled")},{name:"consentGranted",type:"boolean",label:e("consentGranted")},{name:"imapHost",type:"text",label:e("imapHost")},{name:"imapPort",type:"number",label:e("imapPort")},{name:"imapUsername",type:"text",label:e("imapUsername")},{name:"imapPassword",type:"password",label:e("imapPassword")},{name:"fromAddress",type:"email",label:e("fromAddress")}],mochat:[{name:"enabled",type:"boolean",label:e("enabled")},{name:"baseUrl",type:"text",label:e("baseUrl")},{name:"clawToken",type:"password",label:e("clawToken")},{name:"agentUserId",type:"text",label:e("agentUserId")},{name:"allowFrom",type:"tags",label:e("allowFrom")}],qq:[{name:"enabled",type:"boolean",label:e("enabled")},{name:"appId",type:"text",label:e("appId")},{name:"secret",type:"password",label:e("appSecret")},{name:"markdownSupport",type:"boolean",label:e("markdownSupport")},{name:"allowFrom",type:"tags",label:e("allowFrom")}]}}const Ae=t=>t.includes("token")||t.includes("secret")||t.includes("password")?a.jsx(ae,{className:"h-3.5 w-3.5 text-gray-500"}):t.includes("url")||t.includes("host")?a.jsx(te,{className:"h-3.5 w-3.5 text-gray-500"}):t.includes("email")||t.includes("mail")?a.jsx(se,{className:"h-3.5 w-3.5 text-gray-500"}):t.includes("id")||t.includes("from")?a.jsx(le,{className:"h-3.5 w-3.5 text-gray-500"}):t==="enabled"||t==="consentGranted"?a.jsx(ne,{className:"h-3.5 w-3.5 text-gray-500"}):a.jsx(oe,{className:"h-3.5 w-3.5 text-gray-500"});function D(t){return typeof t=="object"&&t!==null&&!Array.isArray(t)}function V(t,m){const i={...t};for(const[o,r]of Object.entries(m)){const u=i[o];if(D(u)&&D(r)){i[o]=V(u,r);continue}i[o]=r}return i}function De(t,m){const i=t.split("."),o={};let r=o;for(let u=0;u<i.length-1;u+=1){const d=i[u];r[d]={},r=r[d]}return r[i[i.length-1]]=m,o}function Le({channelName:t}){var _,O;const{data:m}=q(),{data:i}=$(),{data:o}=H(),r=pe(),u=de(),[d,g]=v.useState({}),[x,j]=v.useState({}),[h,f]=v.useState(null),k=t?m==null?void 0:m.channels[t]:null,w=t?R()[t]??[]:[],c=o==null?void 0:o.uiHints,p=t?`channels.${t}`:null,S=((_=o==null?void 0:o.actions)==null?void 0:_.filter(s=>s.scope===p))??[],C=t&&(((O=U(`channels.${t}`,c))==null?void 0:O.label)??t),I=i==null?void 0:i.channels.find(s=>s.name===t),F=I?z(I):void 0;v.useEffect(()=>{if(k){g({...k});const s={};(t?R()[t]??[]:[]).filter(n=>n.type==="json").forEach(n=>{const y=k[n.name];s[n.name]=JSON.stringify(y??{},null,2)}),j(s)}else g({}),j({})},[k,t]);const N=(s,l)=>{g(n=>({...n,[s]:l}))},L=s=>{if(s.preventDefault(),!t)return;const l={...d};for(const n of w){if(n.type!=="password")continue;const y=l[n.name];(typeof y!="string"||y.length===0)&&delete l[n.name]}for(const n of w){if(n.type!=="json")continue;const y=x[n.name]??"";try{l[n.name]=y.trim()?JSON.parse(y):{}}catch{T.error(`${e("invalidJson")}: ${n.name}`);return}}r.mutate({channel:t,data:l})},W=s=>{if(!s||!t)return;const l=s.channels;if(!D(l))return;const n=l[t];D(n)&&g(y=>V(y,n))},Q=async s=>{if(!(!t||!p)){f(s.id);try{let l={...d};s.saveBeforeRun&&(l={...l,...s.savePatch??{}},g(l),await r.mutateAsync({channel:t,data:l}));const n=await u.mutateAsync({actionId:s.id,data:{scope:p,draftConfig:De(p,l)}});W(n.patch),n.ok?T.success(n.message||e("success")):T.error(n.message||e("error"))}catch(l){const n=l instanceof Error?l.message:String(l);T.error(`${e("error")}: ${n}`)}finally{f(null)}}};if(!t||!I||!k)return a.jsx("div",{className:we,children:a.jsxs("div",{children:[a.jsx("h3",{className:"text-base font-semibold text-gray-900",children:e("channelsSelectTitle")}),a.jsx("p",{className:"mt-2 text-sm text-gray-500",children:e("channelsSelectDescription")})]})});const M=!!k.enabled;return a.jsxs("div",{className:ve,children:[a.jsx("div",{className:"border-b border-gray-100 px-6 py-5",children:a.jsxs("div",{className:"flex flex-wrap items-center justify-between gap-3",children:[a.jsxs("div",{className:"min-w-0",children:[a.jsxs("div",{className:"flex items-center gap-3",children:[a.jsx(K,{name:t,src:Y(t),className:P("h-9 w-9 rounded-lg border",M?"border-primary/30 bg-white":"border-gray-200/70 bg-white"),imgClassName:"h-5 w-5 object-contain",fallback:a.jsx("span",{className:"text-sm font-semibold uppercase text-gray-500",children:t[0]})}),a.jsx("h3",{className:"truncate text-lg font-semibold text-gray-900 capitalize",children:C})]}),a.jsx("p",{className:"mt-2 text-sm text-gray-500",children:e("channelsFormDescription")}),F&&a.jsxs("a",{href:F,className:"mt-2 inline-flex items-center gap-1.5 text-xs text-primary transition-colors hover:text-primary-hover",children:[a.jsx(ee,{className:"h-3.5 w-3.5"}),e("channelsGuideTitle")]})]}),a.jsx(J,{status:M?"active":"inactive",label:M?e("statusActive"):e("statusInactive")})]})}),a.jsxs("form",{onSubmit:L,className:"flex min-h-0 flex-1 flex-col",children:[a.jsx("div",{className:"min-h-0 flex-1 space-y-6 overflow-y-auto overscroll-contain px-6 py-5",children:w.map(s=>{const l=t?U(`channels.${t}.${s.name}`,c):void 0,n=(l==null?void 0:l.label)??s.label,y=l==null?void 0:l.placeholder;return a.jsxs("div",{className:"space-y-2.5",children:[a.jsxs(he,{htmlFor:s.name,className:"flex items-center gap-2 text-sm font-medium text-gray-900",children:[Ae(s.name),n]}),s.type==="boolean"&&a.jsxs("div",{className:"flex items-center justify-between rounded-xl bg-gray-50 p-3",children:[a.jsx("span",{className:"text-sm text-gray-500",children:d[s.name]?e("enabled"):e("disabled")}),a.jsx(fe,{id:s.name,checked:d[s.name]||!1,onCheckedChange:b=>N(s.name,b),className:"data-[state=checked]:bg-emerald-500"})]}),(s.type==="text"||s.type==="email")&&a.jsx(A,{id:s.name,type:s.type,value:d[s.name]||"",onChange:b=>N(s.name,b.target.value),placeholder:y,className:"rounded-xl"}),s.type==="password"&&a.jsx(A,{id:s.name,type:"password",value:d[s.name]||"",onChange:b=>N(s.name,b.target.value),placeholder:y??e("leaveBlankToKeepUnchanged"),className:"rounded-xl"}),s.type==="number"&&a.jsx(A,{id:s.name,type:"number",value:d[s.name]||0,onChange:b=>N(s.name,parseInt(b.target.value,10)||0),placeholder:y,className:"rounded-xl"}),s.type==="tags"&&a.jsx(Ie,{value:d[s.name]||[],onChange:b=>N(s.name,b)}),s.type==="select"&&a.jsxs(be,{value:d[s.name]||"",onValueChange:b=>N(s.name,b),children:[a.jsx(ue,{className:"rounded-xl",children:a.jsx(xe,{})}),a.jsx(ye,{children:(s.options??[]).map(b=>a.jsx(ge,{value:b.value,children:b.label},b.value))})]}),s.type==="json"&&a.jsx("textarea",{id:s.name,value:x[s.name]??"{}",onChange:b=>j(X=>({...X,[s.name]:b.target.value})),className:"min-h-[120px] w-full resize-none rounded-lg border border-gray-200 bg-white px-3 py-2 text-xs font-mono"})]},s.name)})}),a.jsxs("div",{className:"flex flex-wrap items-center justify-between gap-3 border-t border-gray-100 px-6 py-4",children:[a.jsx("div",{className:"flex flex-wrap items-center gap-2",children:S.filter(s=>s.trigger==="manual").map(s=>a.jsx(E,{type:"button",onClick:()=>Q(s),disabled:r.isPending||!!h,variant:"secondary",children:h===s.id?e("connecting"):s.title},s.id))}),a.jsx(E,{type:"submit",disabled:r.isPending||!!h,children:r.isPending?e("saving"):e("save")})]})]})]})}const Me={telegram:"channelDescTelegram",slack:"channelDescSlack",email:"channelDescEmail",webhook:"channelDescWebhook",discord:"channelDescDiscord",feishu:"channelDescFeishu",weixin:"channelDescWeixin"};function Je(){const{data:t}=q(),{data:m}=$(),{data:i}=H(),[o,r]=v.useState("enabled"),[u,d]=v.useState(),[g,x]=v.useState(""),j=i==null?void 0:i.uiHints,h=m==null?void 0:m.channels,f=t==null?void 0:t.channels,k=[{id:"enabled",label:e("channelsTabEnabled"),count:(h??[]).filter(c=>{var p;return(p=f==null?void 0:f[c.name])==null?void 0:p.enabled}).length},{id:"all",label:e("channelsTabAll"),count:(h??[]).length}],w=v.useMemo(()=>{const c=g.trim().toLowerCase();return(h??[]).filter(p=>{var C;const S=((C=f==null?void 0:f[p.name])==null?void 0:C.enabled)||!1;return o==="enabled"?S:!0}).filter(p=>c?(p.displayName||p.name).toLowerCase().includes(c)||p.name.toLowerCase().includes(c):!0)},[o,f,h,g]);return v.useEffect(()=>{if(w.length===0){d(void 0);return}w.some(p=>p.name===u)||d(w[0].name)},[w,u]),!t||!m?a.jsx("div",{className:"p-8 text-gray-400",children:e("channelsLoading")}):a.jsxs(Ce,{className:"xl:flex xl:h-full xl:min-h-0 xl:flex-col xl:pb-0",children:[a.jsx(Ne,{title:e("channelsPageTitle"),description:e("channelsPageDescription")}),a.jsxs("div",{className:P(ke,"xl:min-h-0 xl:flex-1"),children:[a.jsxs("section",{className:je,children:[a.jsx("div",{className:"border-b border-gray-100 px-4 pt-4",children:a.jsx(Se,{tabs:k,activeTab:o,onChange:r,className:"mb-0"})}),a.jsx("div",{className:"border-b border-gray-100 px-4 py-3",children:a.jsxs("div",{className:"relative",children:[a.jsx(re,{className:"pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-gray-400"}),a.jsx(A,{value:g,onChange:c=>x(c.target.value),placeholder:e("channelsFilterPlaceholder"),className:"h-10 rounded-xl pl-9"})]})}),a.jsxs("div",{className:"min-h-0 flex-1 space-y-2 overflow-y-auto overscroll-contain p-3",children:[w.map(c=>{const p=t.channels[c.name],S=(p==null?void 0:p.enabled)||!1,C=U(`channels.${c.name}`,j),I=z(c),F=(C==null?void 0:C.help)||e(Me[c.name]||"channelDescriptionDefault"),N=u===c.name;return a.jsx("button",{type:"button",onClick:()=>d(c.name),className:P("w-full rounded-xl border p-2.5 text-left transition-all",N?"border-primary/30 bg-primary-50/40 shadow-sm":"border-gray-200/70 bg-white hover:border-gray-300 hover:bg-gray-50/70"),children:a.jsxs("div",{className:"flex items-start justify-between gap-3",children:[a.jsxs("div",{className:"flex min-w-0 items-center gap-3",children:[a.jsx(K,{name:c.name,src:Y(c.name),className:P("h-10 w-10 rounded-lg border",S?"border-primary/30 bg-white":"border-gray-200/70 bg-white"),imgClassName:"h-5 w-5 object-contain",fallback:a.jsx("span",{className:"text-sm font-semibold uppercase text-gray-500",children:c.name[0]})}),a.jsxs("div",{className:"min-w-0",children:[a.jsx("p",{className:"truncate text-sm font-semibold text-gray-900",children:c.displayName||c.name}),a.jsx("p",{className:"line-clamp-1 text-[11px] text-gray-500",children:F})]})]}),a.jsxs("div",{className:"flex items-center gap-2",children:[I&&a.jsx("a",{href:I,onClick:L=>L.stopPropagation(),className:"inline-flex h-7 w-7 items-center justify-center rounded-md text-gray-300 transition-colors hover:bg-gray-100/70 hover:text-gray-500",title:e("channelsGuideTitle"),children:a.jsx(ce,{className:"h-3.5 w-3.5"})}),a.jsx(J,{status:S?"active":"inactive",label:S?e("statusActive"):e("statusInactive"),className:"min-w-[56px] justify-center"})]})]})},c.name)}),w.length===0&&a.jsxs("div",{className:"flex h-full min-h-[220px] flex-col items-center justify-center rounded-xl border border-dashed border-gray-200 bg-gray-50/70 py-10 text-center",children:[a.jsx("div",{className:"mb-3 flex h-10 w-10 items-center justify-center rounded-lg bg-white",children:a.jsx(ie,{className:"h-5 w-5 text-gray-300"})}),a.jsx("p",{className:"text-sm font-medium text-gray-700",children:e("channelsNoMatch")})]})]})]}),a.jsx(Le,{channelName:u})]})]})}export{Je as ChannelsList};
|