@nextclaw/ui 0.5.34 → 0.5.36

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.
Files changed (40) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/LICENSE +21 -0
  3. package/dist/assets/ChannelsList-BTQcN7OQ.js +1 -0
  4. package/dist/assets/{ChatPage-BSD-DBqC.js → ChatPage-B5OG3EW3.js} +2 -2
  5. package/dist/assets/CronConfig-MXdvM9gu.js +1 -0
  6. package/dist/assets/{DocBrowser-BCJjJTr9.js → DocBrowser-CJDon901.js} +1 -1
  7. package/dist/assets/MarketplacePage-BwaTwPfP.js +1 -0
  8. package/dist/assets/{ModelConfig-iD7V4upL.js → ModelConfig-qJyJ1XS-.js} +1 -1
  9. package/dist/assets/{ProvidersList-moYj5oBN.js → ProvidersList-DinfLIyS.js} +1 -1
  10. package/dist/assets/{RuntimeConfig-CvWztear.js → RuntimeConfig-DoKy3o8n.js} +1 -1
  11. package/dist/assets/{SecretsConfig-Ds5xip01.js → SecretsConfig-S_jppujG.js} +1 -1
  12. package/dist/assets/SessionsConfig-C5VnCiw_.js +2 -0
  13. package/dist/assets/{card-CB1zsVbS.js → card-Bsb-eVmY.js} +1 -1
  14. package/dist/assets/index-BzQBLXUW.js +2 -0
  15. package/dist/assets/index-DcyOd66N.css +1 -0
  16. package/dist/assets/{label-d0bFiiuu.js → label-CXP5KktX.js} +1 -1
  17. package/dist/assets/logos-DqE_6ErA.js +1 -0
  18. package/dist/assets/{page-layout-Cf2nfjrs.js → page-layout-DMWzimj9.js} +1 -1
  19. package/dist/assets/{switch-CfELV89t.js → switch-oEZ0AFmj.js} +1 -1
  20. package/dist/assets/{tabs-custom-CJdhCvOt.js → tabs-custom-CQP93tp3.js} +1 -1
  21. package/dist/assets/{useConfig-DnBXFnGL.js → useConfig-C0nxJgik.js} +1 -1
  22. package/dist/assets/useConfirmDialog-d6TTs8io.js +5 -0
  23. package/dist/assets/{vendor-CmqkRoMs.js → vendor-DN_iJQc4.js} +75 -85
  24. package/dist/index.html +3 -3
  25. package/package.json +1 -1
  26. package/src/api/types.ts +5 -0
  27. package/src/components/common/LogoBadge.tsx +2 -2
  28. package/src/components/config/ChannelForm.tsx +185 -185
  29. package/src/components/config/ChannelsList.tsx +131 -94
  30. package/src/lib/channel-tutorials.ts +8 -0
  31. package/src/lib/i18n.ts +6 -0
  32. package/dist/assets/ChannelsList-DvhJoVmJ.js +0 -1
  33. package/dist/assets/CronConfig-JLIc66HM.js +0 -1
  34. package/dist/assets/MarketplacePage-ftvI9vFu.js +0 -1
  35. package/dist/assets/SessionsConfig-CCbracj_.js +0 -2
  36. package/dist/assets/dialog-BgbSAXFu.js +0 -5
  37. package/dist/assets/index-B8Wh_FvS.css +0 -1
  38. package/dist/assets/index-O1Kus7pd.js +0 -2
  39. package/dist/assets/logos-gjlYO0d_.js +0 -1
  40. package/dist/assets/useConfirmDialog-c4_c0EGk.js +0 -1
package/dist/index.html CHANGED
@@ -6,9 +6,9 @@
6
6
  <link rel="icon" type="image/svg+xml" href="/logo.svg" />
7
7
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
8
8
  <title>NextClaw - 系统配置</title>
9
- <script type="module" crossorigin src="/assets/index-O1Kus7pd.js"></script>
10
- <link rel="modulepreload" crossorigin href="/assets/vendor-CmqkRoMs.js">
11
- <link rel="stylesheet" crossorigin href="/assets/index-B8Wh_FvS.css">
9
+ <script type="module" crossorigin src="/assets/index-BzQBLXUW.js"></script>
10
+ <link rel="modulepreload" crossorigin href="/assets/vendor-DN_iJQc4.js">
11
+ <link rel="stylesheet" crossorigin href="/assets/index-DcyOd66N.css">
12
12
  </head>
13
13
 
14
14
  <body>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nextclaw/ui",
3
- "version": "0.5.34",
3
+ "version": "0.5.36",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/api/types.ts CHANGED
@@ -342,6 +342,11 @@ export type ChannelSpecView = {
342
342
  displayName?: string;
343
343
  enabled: boolean;
344
344
  tutorialUrl?: string;
345
+ tutorialUrls?: {
346
+ default?: string;
347
+ en?: string;
348
+ zh?: string;
349
+ };
345
350
  };
346
351
 
347
352
  export type ConfigMetaView = {
@@ -14,12 +14,12 @@ export function LogoBadge({ name, src, className, imgClassName, fallback }: Logo
14
14
  const showImage = Boolean(src) && !failed;
15
15
 
16
16
  return (
17
- <div className={cn('flex items-center justify-center', className)}>
17
+ <div className={cn('inline-flex shrink-0 items-center justify-center overflow-hidden', className)}>
18
18
  {showImage ? (
19
19
  <img
20
20
  src={src as string}
21
21
  alt={`${name} logo`}
22
- className={cn('h-6 w-6 object-contain', imgClassName)}
22
+ className={cn('block h-6 w-6 object-contain', imgClassName)}
23
23
  onError={() => setFailed(true)}
24
24
  draggable={false}
25
25
  />
@@ -1,30 +1,30 @@
1
1
  import { useEffect, useState } from 'react';
2
- import { useConfig, useConfigSchema, useUpdateChannel, useExecuteConfigAction } from '@/hooks/useConfig';
3
- import { useUiStore } from '@/stores/ui.store';
4
- import {
5
- Dialog,
6
- DialogContent,
7
- DialogHeader,
8
- DialogTitle,
9
- DialogDescription,
10
- DialogFooter,
11
- } from '@/components/ui/dialog';
2
+ import { useConfig, useConfigMeta, useConfigSchema, useUpdateChannel, useExecuteConfigAction } from '@/hooks/useConfig';
12
3
  import { Button } from '@/components/ui/button';
13
4
  import { Input } from '@/components/ui/input';
14
5
  import { Label } from '@/components/ui/label';
15
6
  import { Switch } from '@/components/ui/switch';
16
7
  import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
17
8
  import { TagInput } from '@/components/common/TagInput';
9
+ import { StatusDot } from '@/components/ui/status-dot';
10
+ import { LogoBadge } from '@/components/common/LogoBadge';
18
11
  import { t } from '@/lib/i18n';
19
12
  import { hintForPath } from '@/lib/config-hints';
13
+ import { cn } from '@/lib/utils';
20
14
  import { toast } from 'sonner';
21
- import { MessageCircle, Settings, ToggleLeft, Hash, Mail, Globe, KeyRound } from 'lucide-react';
15
+ import { Settings, ToggleLeft, Hash, Mail, Globe, KeyRound, BookOpen } from 'lucide-react';
22
16
  import type { ConfigActionManifest } from '@/api/types';
17
+ import { resolveChannelTutorialUrl } from '@/lib/channel-tutorials';
18
+ import { getChannelLogo } from '@/lib/logos';
23
19
 
24
20
  type ChannelFieldType = 'boolean' | 'text' | 'email' | 'password' | 'number' | 'tags' | 'select' | 'json';
25
21
  type ChannelOption = { value: string; label: string };
26
22
  type ChannelField = { name: string; type: ChannelFieldType; label: string; options?: ChannelOption[] };
27
23
 
24
+ type ChannelFormProps = {
25
+ channelName?: string;
26
+ };
27
+
28
28
  const DM_POLICY_OPTIONS: ChannelOption[] = [
29
29
  { value: 'pairing', label: 'pairing' },
30
30
  { value: 'allowlist', label: 'allowlist' },
@@ -45,7 +45,6 @@ const STREAMING_MODE_OPTIONS: ChannelOption[] = [
45
45
  { value: 'progress', label: 'progress' }
46
46
  ];
47
47
 
48
- // Field icon mapping
49
48
  const getFieldIcon = (fieldName: string) => {
50
49
  if (fieldName.includes('token') || fieldName.includes('secret') || fieldName.includes('password')) {
51
50
  return <KeyRound className="h-3.5 w-3.5 text-gray-500" />;
@@ -163,20 +162,6 @@ function buildChannelFields(): Record<string, ChannelField[]> {
163
162
  };
164
163
  }
165
164
 
166
- const channelIcons: Record<string, typeof MessageCircle> = {
167
- telegram: MessageCircle,
168
- slack: MessageCircle,
169
- email: Mail,
170
- default: MessageCircle
171
- };
172
-
173
- const channelColors: Record<string, string> = {
174
- telegram: 'from-primary-300 to-primary-600',
175
- slack: 'from-primary-200 to-primary-500',
176
- email: 'from-primary-100 to-primary-400',
177
- default: 'from-gray-300 to-gray-500'
178
- };
179
-
180
165
  function isRecord(value: unknown): value is Record<string, unknown> {
181
166
  return typeof value === 'object' && value !== null && !Array.isArray(value);
182
167
  }
@@ -207,9 +192,9 @@ function buildScopeDraft(scope: string, value: Record<string, unknown>): Record<
207
192
  return output;
208
193
  }
209
194
 
210
- export function ChannelForm() {
211
- const { channelModal, closeChannelModal } = useUiStore();
195
+ export function ChannelForm({ channelName }: ChannelFormProps) {
212
196
  const { data: config } = useConfig();
197
+ const { data: meta } = useConfigMeta();
213
198
  const { data: schema } = useConfigSchema();
214
199
  const updateChannel = useUpdateChannel();
215
200
  const executeAction = useExecuteConfigAction();
@@ -218,7 +203,6 @@ export function ChannelForm() {
218
203
  const [jsonDrafts, setJsonDrafts] = useState<Record<string, string>>({});
219
204
  const [runningActionId, setRunningActionId] = useState<string | null>(null);
220
205
 
221
- const channelName = channelModal.channel;
222
206
  const channelConfig = channelName ? config?.channels[channelName] : null;
223
207
  const fields = channelName ? buildChannelFields()[channelName] ?? [] : [];
224
208
  const uiHints = schema?.uiHints;
@@ -227,6 +211,8 @@ export function ChannelForm() {
227
211
  const channelLabel = channelName
228
212
  ? hintForPath(`channels.${channelName}`, uiHints)?.label ?? channelName
229
213
  : channelName;
214
+ const channelMeta = meta?.channels.find((item) => item.name === channelName);
215
+ const tutorialUrl = channelMeta ? resolveChannelTutorialUrl(channelMeta) : undefined;
230
216
 
231
217
  useEffect(() => {
232
218
  if (channelConfig) {
@@ -278,10 +264,7 @@ export function ChannelForm() {
278
264
  }
279
265
  }
280
266
 
281
- updateChannel.mutate(
282
- { channel: channelName, data: payload },
283
- { onSuccess: () => closeChannelModal() }
284
- );
267
+ updateChannel.mutate({ channel: channelName, data: payload });
285
268
  };
286
269
 
287
270
  const applyActionPatchToForm = (patch?: Record<string, unknown>) => {
@@ -340,163 +323,180 @@ export function ChannelForm() {
340
323
  }
341
324
  };
342
325
 
343
- const Icon = channelIcons[channelName || ''] || channelIcons.default;
344
- const gradientClass = channelColors[channelName || ''] || channelColors.default;
326
+ if (!channelName || !channelMeta || !channelConfig) {
327
+ return (
328
+ <div className="flex min-h-[520px] items-center justify-center rounded-2xl border border-gray-200/70 bg-white px-6 text-center xl:h-[calc(100vh-180px)] xl:min-h-[600px] xl:max-h-[860px]">
329
+ <div>
330
+ <h3 className="text-base font-semibold text-gray-900">{t('channelsSelectTitle')}</h3>
331
+ <p className="mt-2 text-sm text-gray-500">{t('channelsSelectDescription')}</p>
332
+ </div>
333
+ </div>
334
+ );
335
+ }
336
+
337
+ const enabled = Boolean(channelConfig.enabled);
345
338
 
346
339
  return (
347
- <Dialog open={channelModal.open} onOpenChange={closeChannelModal}>
348
- <DialogContent className="sm:max-w-[550px] max-h-[85vh] overflow-hidden flex flex-col">
349
- <DialogHeader>
350
- <div className="flex items-center gap-3">
351
- <div className={`h-10 w-10 rounded-xl bg-gradient-to-br ${gradientClass} flex items-center justify-center`}>
352
- <Icon className="h-5 w-5 text-white" />
353
- </div>
354
- <div>
355
- <DialogTitle className="capitalize">{channelLabel}</DialogTitle>
356
- <DialogDescription>{t('configureMessageChannelParameters')}</DialogDescription>
340
+ <div className="flex min-h-[520px] flex-col rounded-2xl border border-gray-200/70 bg-white shadow-card xl:h-[calc(100vh-180px)] xl:min-h-[600px] xl:max-h-[860px]">
341
+ <div className="border-b border-gray-100 px-6 py-5">
342
+ <div className="flex flex-wrap items-start justify-between gap-3">
343
+ <div className="min-w-0">
344
+ <div className="flex items-center gap-3">
345
+ <LogoBadge
346
+ name={channelName}
347
+ src={getChannelLogo(channelName)}
348
+ className={cn(
349
+ 'h-9 w-9 rounded-lg border',
350
+ enabled ? 'border-primary/30 bg-white' : 'border-gray-200/70 bg-white'
351
+ )}
352
+ imgClassName="h-5 w-5 object-contain"
353
+ fallback={<span className="text-sm font-semibold uppercase text-gray-500">{channelName[0]}</span>}
354
+ />
355
+ <h3 className="truncate text-lg font-semibold text-gray-900 capitalize">{channelLabel}</h3>
357
356
  </div>
357
+ <p className="mt-2 text-sm text-gray-500">{t('channelsFormDescription')}</p>
358
+ {tutorialUrl && (
359
+ <a
360
+ href={tutorialUrl}
361
+ className="mt-2 inline-flex items-center gap-1.5 text-xs text-primary transition-colors hover:text-primary-hover"
362
+ >
363
+ <BookOpen className="h-3.5 w-3.5" />
364
+ {t('channelsGuideTitle')}
365
+ </a>
366
+ )}
358
367
  </div>
359
- </DialogHeader>
360
-
361
- <form onSubmit={handleSubmit} className="flex flex-col flex-1 overflow-hidden">
362
- <div className="flex-1 overflow-y-auto custom-scrollbar py-2 pr-2 space-y-5">
363
- {fields.map((field) => {
364
- const hint = channelName
365
- ? hintForPath(`channels.${channelName}.${field.name}`, uiHints)
366
- : undefined;
367
- const label = hint?.label ?? field.label;
368
- const placeholder = hint?.placeholder;
369
-
370
- return (
371
- <div key={field.name} className="space-y-2.5">
372
- <Label
373
- htmlFor={field.name}
374
- className="text-sm font-medium text-gray-900 flex items-center gap-2"
375
- >
376
- {getFieldIcon(field.name)}
377
- {label}
378
- </Label>
379
-
380
- {field.type === 'boolean' && (
381
- <div className="flex items-center justify-between p-3 rounded-xl bg-gray-50">
382
- <span className="text-sm text-gray-500">
383
- {(formData[field.name] as boolean) ? t('enabled') : t('disabled')}
384
- </span>
385
- <Switch
386
- id={field.name}
387
- checked={(formData[field.name] as boolean) || false}
388
- onCheckedChange={(checked) => updateField(field.name, checked)}
389
- className="data-[state=checked]:bg-emerald-500"
390
- />
391
- </div>
392
- )}
393
-
394
- {(field.type === 'text' || field.type === 'email') && (
395
- <Input
396
- id={field.name}
397
- type={field.type}
398
- value={(formData[field.name] as string) || ''}
399
- onChange={(e) => updateField(field.name, e.target.value)}
400
- placeholder={placeholder}
401
- className="rounded-xl"
402
- />
403
- )}
404
-
405
- {field.type === 'password' && (
406
- <Input
407
- id={field.name}
408
- type="password"
409
- value={(formData[field.name] as string) || ''}
410
- onChange={(e) => updateField(field.name, e.target.value)}
411
- placeholder={placeholder ?? t('leaveBlankToKeepUnchanged')}
412
- className="rounded-xl"
413
- />
414
- )}
415
-
416
- {field.type === 'number' && (
417
- <Input
418
- id={field.name}
419
- type="number"
420
- value={(formData[field.name] as number) || 0}
421
- onChange={(e) => updateField(field.name, parseInt(e.target.value) || 0)}
422
- placeholder={placeholder}
423
- className="rounded-xl"
424
- />
425
- )}
426
-
427
- {field.type === 'tags' && (
428
- <TagInput
429
- value={(formData[field.name] as string[]) || []}
430
- onChange={(tags) => updateField(field.name, tags)}
431
- />
432
- )}
433
-
434
- {field.type === 'select' && (
435
- <Select
436
- value={(formData[field.name] as string) || ''}
437
- onValueChange={(v) => updateField(field.name, v)}
438
- >
439
- <SelectTrigger className="rounded-xl">
440
- <SelectValue />
441
- </SelectTrigger>
442
- <SelectContent>
443
- {(field.options ?? []).map((option) => (
444
- <SelectItem key={option.value} value={option.value}>
445
- {option.label}
446
- </SelectItem>
447
- ))}
448
- </SelectContent>
449
- </Select>
450
- )}
451
-
452
- {field.type === 'json' && (
453
- <textarea
368
+ <StatusDot status={enabled ? 'active' : 'inactive'} label={enabled ? t('statusActive') : t('statusInactive')} />
369
+ </div>
370
+ </div>
371
+
372
+ <form onSubmit={handleSubmit} className="flex min-h-0 flex-1 flex-col">
373
+ <div className="min-h-0 flex-1 space-y-5 overflow-y-auto px-6 py-5">
374
+ {fields.map((field) => {
375
+ const hint = channelName
376
+ ? hintForPath(`channels.${channelName}.${field.name}`, uiHints)
377
+ : undefined;
378
+ const label = hint?.label ?? field.label;
379
+ const placeholder = hint?.placeholder;
380
+
381
+ return (
382
+ <div key={field.name} className="space-y-2.5">
383
+ <Label
384
+ htmlFor={field.name}
385
+ className="flex items-center gap-2 text-sm font-medium text-gray-900"
386
+ >
387
+ {getFieldIcon(field.name)}
388
+ {label}
389
+ </Label>
390
+
391
+ {field.type === 'boolean' && (
392
+ <div className="flex items-center justify-between rounded-xl bg-gray-50 p-3">
393
+ <span className="text-sm text-gray-500">
394
+ {(formData[field.name] as boolean) ? t('enabled') : t('disabled')}
395
+ </span>
396
+ <Switch
454
397
  id={field.name}
455
- value={jsonDrafts[field.name] ?? '{}'}
456
- onChange={(event) =>
457
- setJsonDrafts((prev) => ({
458
- ...prev,
459
- [field.name]: event.target.value
460
- }))
461
- }
462
- className="min-h-[120px] w-full rounded-lg border border-gray-200 bg-white px-3 py-2 text-xs font-mono"
398
+ checked={(formData[field.name] as boolean) || false}
399
+ onCheckedChange={(checked) => updateField(field.name, checked)}
400
+ className="data-[state=checked]:bg-emerald-500"
463
401
  />
464
- )}
465
- </div>
466
- );
467
- })}
468
- </div>
469
-
470
- <DialogFooter className="pt-4 flex-shrink-0">
471
- <Button
472
- type="button"
473
- variant="outline"
474
- onClick={closeChannelModal}
475
- >
476
- {t('cancel')}
477
- </Button>
478
- <Button
479
- type="submit"
480
- disabled={updateChannel.isPending || Boolean(runningActionId)}
481
- >
482
- {updateChannel.isPending ? t('saving') : t('save')}
483
- </Button>
484
- {actions
485
- .filter((action) => action.trigger === 'manual')
486
- .map((action) => (
487
- <Button
488
- key={action.id}
489
- type="button"
490
- onClick={() => handleManualAction(action)}
491
- disabled={updateChannel.isPending || Boolean(runningActionId)}
492
- variant="secondary"
493
- >
494
- {runningActionId === action.id ? t('connecting') : action.title}
495
- </Button>
496
- ))}
497
- </DialogFooter>
498
- </form>
499
- </DialogContent>
500
- </Dialog>
402
+ </div>
403
+ )}
404
+
405
+ {(field.type === 'text' || field.type === 'email') && (
406
+ <Input
407
+ id={field.name}
408
+ type={field.type}
409
+ value={(formData[field.name] as string) || ''}
410
+ onChange={(e) => updateField(field.name, e.target.value)}
411
+ placeholder={placeholder}
412
+ className="rounded-xl"
413
+ />
414
+ )}
415
+
416
+ {field.type === 'password' && (
417
+ <Input
418
+ id={field.name}
419
+ type="password"
420
+ value={(formData[field.name] as string) || ''}
421
+ onChange={(e) => updateField(field.name, e.target.value)}
422
+ placeholder={placeholder ?? t('leaveBlankToKeepUnchanged')}
423
+ className="rounded-xl"
424
+ />
425
+ )}
426
+
427
+ {field.type === 'number' && (
428
+ <Input
429
+ id={field.name}
430
+ type="number"
431
+ value={(formData[field.name] as number) || 0}
432
+ onChange={(e) => updateField(field.name, parseInt(e.target.value, 10) || 0)}
433
+ placeholder={placeholder}
434
+ className="rounded-xl"
435
+ />
436
+ )}
437
+
438
+ {field.type === 'tags' && (
439
+ <TagInput
440
+ value={(formData[field.name] as string[]) || []}
441
+ onChange={(tags) => updateField(field.name, tags)}
442
+ />
443
+ )}
444
+
445
+ {field.type === 'select' && (
446
+ <Select
447
+ value={(formData[field.name] as string) || ''}
448
+ onValueChange={(v) => updateField(field.name, v)}
449
+ >
450
+ <SelectTrigger className="rounded-xl">
451
+ <SelectValue />
452
+ </SelectTrigger>
453
+ <SelectContent>
454
+ {(field.options ?? []).map((option) => (
455
+ <SelectItem key={option.value} value={option.value}>
456
+ {option.label}
457
+ </SelectItem>
458
+ ))}
459
+ </SelectContent>
460
+ </Select>
461
+ )}
462
+
463
+ {field.type === 'json' && (
464
+ <textarea
465
+ id={field.name}
466
+ value={jsonDrafts[field.name] ?? '{}'}
467
+ onChange={(event) =>
468
+ setJsonDrafts((prev) => ({
469
+ ...prev,
470
+ [field.name]: event.target.value
471
+ }))
472
+ }
473
+ className="min-h-[120px] w-full rounded-lg border border-gray-200 bg-white px-3 py-2 text-xs font-mono"
474
+ />
475
+ )}
476
+ </div>
477
+ );
478
+ })}
479
+ </div>
480
+
481
+ <div className="flex flex-wrap items-center justify-end gap-2 border-t border-gray-100 px-6 py-4">
482
+ {actions
483
+ .filter((action) => action.trigger === 'manual')
484
+ .map((action) => (
485
+ <Button
486
+ key={action.id}
487
+ type="button"
488
+ onClick={() => handleManualAction(action)}
489
+ disabled={updateChannel.isPending || Boolean(runningActionId)}
490
+ variant="secondary"
491
+ >
492
+ {runningActionId === action.id ? t('connecting') : action.title}
493
+ </Button>
494
+ ))}
495
+ <Button type="submit" disabled={updateChannel.isPending || Boolean(runningActionId)}>
496
+ {updateChannel.isPending ? t('saving') : t('save')}
497
+ </Button>
498
+ </div>
499
+ </form>
500
+ </div>
501
501
  );
502
502
  }