@nextclaw/ui 0.2.4 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/assets/index-BIesvTqn.js +225 -0
  3. package/dist/assets/index-iSLahgqA.css +1 -0
  4. package/dist/index.html +2 -2
  5. package/dist/logos/aihubmix.png +0 -0
  6. package/dist/logos/anthropic.svg +1 -0
  7. package/dist/logos/dashscope.png +0 -0
  8. package/dist/logos/deepseek.png +0 -0
  9. package/dist/logos/dingtalk.svg +1 -0
  10. package/dist/logos/discord.svg +1 -0
  11. package/dist/logos/email.svg +1 -0
  12. package/dist/logos/feishu.svg +12 -0
  13. package/dist/logos/gemini.svg +1 -0
  14. package/dist/logos/groq.svg +1 -0
  15. package/dist/logos/minimax.svg +1 -0
  16. package/dist/logos/mochat.svg +6 -0
  17. package/dist/logos/moonshot.png +0 -0
  18. package/dist/logos/openai.svg +1 -0
  19. package/dist/logos/openrouter.svg +1 -0
  20. package/dist/logos/qq.svg +1 -0
  21. package/dist/logos/slack.svg +1 -0
  22. package/dist/logos/telegram.svg +1 -0
  23. package/dist/logos/vllm.svg +1 -0
  24. package/dist/logos/whatsapp.svg +1 -0
  25. package/dist/logos/zhipu.svg +15 -0
  26. package/package.json +1 -1
  27. package/public/logos/aihubmix.png +0 -0
  28. package/public/logos/anthropic.svg +1 -0
  29. package/public/logos/dashscope.png +0 -0
  30. package/public/logos/deepseek.png +0 -0
  31. package/public/logos/dingtalk.svg +1 -0
  32. package/public/logos/discord.svg +1 -0
  33. package/public/logos/email.svg +1 -0
  34. package/public/logos/feishu.svg +12 -0
  35. package/public/logos/gemini.svg +1 -0
  36. package/public/logos/groq.svg +1 -0
  37. package/public/logos/minimax.svg +1 -0
  38. package/public/logos/mochat.svg +6 -0
  39. package/public/logos/moonshot.png +0 -0
  40. package/public/logos/openai.svg +1 -0
  41. package/public/logos/openrouter.svg +1 -0
  42. package/public/logos/qq.svg +1 -0
  43. package/public/logos/slack.svg +1 -0
  44. package/public/logos/telegram.svg +1 -0
  45. package/public/logos/vllm.svg +1 -0
  46. package/public/logos/whatsapp.svg +1 -0
  47. package/public/logos/zhipu.svg +15 -0
  48. package/src/App.tsx +0 -3
  49. package/src/api/config.ts +0 -19
  50. package/src/api/types.ts +5 -8
  51. package/src/components/common/LogoBadge.tsx +35 -0
  52. package/src/components/common/StatusBadge.tsx +4 -4
  53. package/src/components/config/ChannelForm.tsx +16 -18
  54. package/src/components/config/ChannelsList.tsx +87 -37
  55. package/src/components/config/ModelConfig.tsx +25 -25
  56. package/src/components/config/ProviderForm.tsx +46 -11
  57. package/src/components/config/ProvidersList.tsx +90 -38
  58. package/src/components/layout/Header.tsx +7 -7
  59. package/src/components/layout/Sidebar.tsx +10 -23
  60. package/src/components/ui/HighlightCard.tsx +29 -29
  61. package/src/components/ui/button.tsx +13 -8
  62. package/src/components/ui/card.tsx +8 -7
  63. package/src/components/ui/dialog.tsx +8 -8
  64. package/src/components/ui/input.tsx +1 -1
  65. package/src/components/ui/label.tsx +1 -1
  66. package/src/components/ui/switch.tsx +3 -3
  67. package/src/components/ui/tabs-custom.tsx +6 -6
  68. package/src/components/ui/tabs.tsx +7 -6
  69. package/src/hooks/useConfig.ts +2 -29
  70. package/src/index.css +103 -56
  71. package/src/lib/i18n.ts +7 -6
  72. package/src/lib/logos.ts +42 -0
  73. package/src/stores/ui.store.ts +1 -1
  74. package/src/styles/design-system.css +248 -0
  75. package/tailwind.config.js +118 -10
  76. package/dist/assets/index-C4OKhpdC.css +0 -1
  77. package/dist/assets/index-C8nOCIVG.js +0 -240
  78. package/src/components/config/UiConfig.tsx +0 -189
package/src/api/types.ts CHANGED
@@ -14,23 +14,18 @@ export type ProviderConfigView = {
14
14
  apiKeyMasked?: string;
15
15
  apiBase?: string | null;
16
16
  extraHeaders?: Record<string, string> | null;
17
+ wireApi?: "auto" | "chat" | "responses" | null;
17
18
  };
18
19
 
19
20
  export type ProviderConfigUpdate = {
20
21
  apiKey?: string | null;
21
22
  apiBase?: string | null;
22
23
  extraHeaders?: Record<string, string> | null;
24
+ wireApi?: "auto" | "chat" | "responses" | null;
23
25
  };
24
26
 
25
27
  export type ChannelConfigUpdate = Record<string, unknown>;
26
28
 
27
- export type UiConfigView = {
28
- enabled: boolean;
29
- host: string;
30
- port: number;
31
- open: boolean;
32
- };
33
-
34
29
  export type ConfigView = {
35
30
  agents: {
36
31
  defaults: {
@@ -45,7 +40,6 @@ export type ConfigView = {
45
40
  channels: Record<string, Record<string, unknown>>;
46
41
  tools?: Record<string, unknown>;
47
42
  gateway?: Record<string, unknown>;
48
- ui?: UiConfigView;
49
43
  };
50
44
 
51
45
  export type ProviderSpecView = {
@@ -56,6 +50,9 @@ export type ProviderSpecView = {
56
50
  isGateway?: boolean;
57
51
  isLocal?: boolean;
58
52
  defaultApiBase?: string;
53
+ supportsWireApi?: boolean;
54
+ wireApiOptions?: Array<"auto" | "chat" | "responses">;
55
+ defaultWireApi?: "auto" | "chat" | "responses";
59
56
  };
60
57
 
61
58
  export type ChannelSpecView = {
@@ -0,0 +1,35 @@
1
+ import { useState } from 'react';
2
+ import { cn } from '@/lib/utils';
3
+
4
+ type LogoBadgeProps = {
5
+ name: string;
6
+ src?: string | null;
7
+ className?: string;
8
+ imgClassName?: string;
9
+ fallback?: React.ReactNode;
10
+ };
11
+
12
+ export function LogoBadge({ name, src, className, imgClassName, fallback }: LogoBadgeProps) {
13
+ const [failed, setFailed] = useState(false);
14
+ const showImage = Boolean(src) && !failed;
15
+
16
+ return (
17
+ <div className={cn('flex items-center justify-center', className)}>
18
+ {showImage ? (
19
+ <img
20
+ src={src as string}
21
+ alt={`${name} logo`}
22
+ className={cn('h-6 w-6 object-contain', imgClassName)}
23
+ onError={() => setFailed(true)}
24
+ draggable={false}
25
+ />
26
+ ) : (
27
+ fallback ?? (
28
+ <span className="text-lg font-bold uppercase">
29
+ {name.slice(0, 1)}
30
+ </span>
31
+ )
32
+ )}
33
+ </div>
34
+ );
35
+ }
@@ -22,9 +22,9 @@ const statusConfig: Record<
22
22
  },
23
23
  disconnected: {
24
24
  label: t('disconnected'),
25
- dotClass: 'bg-[hsl(30,8%,55%)]',
26
- textClass: 'text-[hsl(30,8%,45%)]',
27
- bgClass: 'bg-[hsl(40,20%,96%)]',
25
+ dotClass: 'bg-gray-400',
26
+ textClass: 'text-gray-500',
27
+ bgClass: 'bg-gray-100',
28
28
  icon: X
29
29
  },
30
30
  connecting: {
@@ -48,8 +48,8 @@ export function StatusBadge({ status, className }: StatusBadgeProps) {
48
48
  )}>
49
49
  <div className={cn('h-2 w-2 rounded-full', config.dotClass)} />
50
50
  <span className={cn('text-xs font-medium flex items-center gap-1', config.textClass)}>
51
- <Icon className={cn('h-3 w-3', status === 'connecting' && 'animate-spin')} />
52
51
  {config.label}
52
+ {status === 'connecting' && <Icon className="h-3 w-3 animate-spin" />}
53
53
  </span>
54
54
  </div>
55
55
  );
@@ -1,6 +1,6 @@
1
1
  import { useEffect, useState } from 'react';
2
2
  import { useConfig, useUpdateChannel } from '@/hooks/useConfig';
3
- import { probeFeishu, reloadConfig } from '@/api/config';
3
+ import { probeFeishu } from '@/api/config';
4
4
  import { useUiStore } from '@/stores/ui.store';
5
5
  import {
6
6
  Dialog,
@@ -22,21 +22,21 @@ import { MessageCircle, Settings, ToggleLeft, Hash, Mail, Globe, KeyRound } from
22
22
  // Field icon mapping
23
23
  const getFieldIcon = (fieldName: string) => {
24
24
  if (fieldName.includes('token') || fieldName.includes('secret') || fieldName.includes('password')) {
25
- return <KeyRound className="h-3.5 w-3.5 text-[hsl(30,8%,45%)]" />;
25
+ return <KeyRound className="h-3.5 w-3.5 text-gray-500" />;
26
26
  }
27
27
  if (fieldName.includes('url') || fieldName.includes('host')) {
28
- return <Globe className="h-3.5 w-3.5 text-[hsl(30,8%,45%)]" />;
28
+ return <Globe className="h-3.5 w-3.5 text-gray-500" />;
29
29
  }
30
30
  if (fieldName.includes('email') || fieldName.includes('mail')) {
31
- return <Mail className="h-3.5 w-3.5 text-[hsl(30,8%,45%)]" />;
31
+ return <Mail className="h-3.5 w-3.5 text-gray-500" />;
32
32
  }
33
33
  if (fieldName.includes('id') || fieldName.includes('from')) {
34
- return <Hash className="h-3.5 w-3.5 text-[hsl(30,8%,45%)]" />;
34
+ return <Hash className="h-3.5 w-3.5 text-gray-500" />;
35
35
  }
36
36
  if (fieldName === 'enabled' || fieldName === 'consentGranted') {
37
- return <ToggleLeft className="h-3.5 w-3.5 text-[hsl(30,8%,45%)]" />;
37
+ return <ToggleLeft className="h-3.5 w-3.5 text-gray-500" />;
38
38
  }
39
- return <Settings className="h-3.5 w-3.5 text-[hsl(30,8%,45%)]" />;
39
+ return <Settings className="h-3.5 w-3.5 text-gray-500" />;
40
40
  };
41
41
 
42
42
  // Channel field definitions
@@ -99,7 +99,8 @@ const CHANNEL_FIELDS: Record<string, Array<{ name: string; type: string; label:
99
99
  qq: [
100
100
  { name: 'enabled', type: 'boolean', label: t('enabled') },
101
101
  { name: 'appId', type: 'text', label: t('appId') },
102
- { name: 'secret', type: 'password', label: t('secret') },
102
+ { name: 'secret', type: 'password', label: t('appSecret') },
103
+ { name: 'markdownSupport', type: 'boolean', label: t('markdownSupport') },
103
104
  { name: 'allowFrom', type: 'tags', label: t('allowFrom') }
104
105
  ]
105
106
  };
@@ -163,7 +164,6 @@ export function ChannelForm() {
163
164
  }
164
165
  await updateChannel.mutateAsync({ channel: channelName, data: nextData });
165
166
  const probe = await probeFeishu();
166
- await reloadConfig();
167
167
  const botLabel = probe.botName ? ` (${probe.botName})` : '';
168
168
  toast.success(t('feishuVerifySuccess') + botLabel);
169
169
  } catch (error) {
@@ -198,15 +198,15 @@ export function ChannelForm() {
198
198
  <div key={field.name} className="space-y-2.5">
199
199
  <Label
200
200
  htmlFor={field.name}
201
- className="text-sm font-medium text-[hsl(30,20%,12%)] flex items-center gap-2"
201
+ className="text-sm font-medium text-gray-900 flex items-center gap-2"
202
202
  >
203
203
  {getFieldIcon(field.name)}
204
204
  {field.label}
205
205
  </Label>
206
206
 
207
207
  {field.type === 'boolean' && (
208
- <div className="flex items-center justify-between p-3 rounded-xl bg-[hsl(40,20%,96%)]">
209
- <span className="text-sm text-[hsl(30,8%,45%)]">
208
+ <div className="flex items-center justify-between p-3 rounded-xl bg-gray-50">
209
+ <span className="text-sm text-gray-500">
210
210
  {(formData[field.name] as boolean) ? t('enabled') : t('disabled')}
211
211
  </span>
212
212
  <Switch
@@ -224,7 +224,7 @@ export function ChannelForm() {
224
224
  type={field.type}
225
225
  value={(formData[field.name] as string) || ''}
226
226
  onChange={(e) => updateField(field.name, e.target.value)}
227
- className="rounded-xl border-[hsl(40,20%,90%)] bg-[hsl(40,20%,98%)] focus:bg-white"
227
+ className="rounded-xl"
228
228
  />
229
229
  )}
230
230
 
@@ -235,7 +235,7 @@ export function ChannelForm() {
235
235
  value={(formData[field.name] as string) || ''}
236
236
  onChange={(e) => updateField(field.name, e.target.value)}
237
237
  placeholder="Leave blank to keep unchanged"
238
- className="rounded-xl border-[hsl(40,20%,90%)] bg-[hsl(40,20%,98%)] focus:bg-white"
238
+ className="rounded-xl"
239
239
  />
240
240
  )}
241
241
 
@@ -245,7 +245,7 @@ export function ChannelForm() {
245
245
  type="number"
246
246
  value={(formData[field.name] as number) || 0}
247
247
  onChange={(e) => updateField(field.name, parseInt(e.target.value) || 0)}
248
- className="rounded-xl border-[hsl(40,20%,90%)] bg-[hsl(40,20%,98%)] focus:bg-white"
248
+ className="rounded-xl"
249
249
  />
250
250
  )}
251
251
 
@@ -263,14 +263,12 @@ export function ChannelForm() {
263
263
  type="button"
264
264
  variant="outline"
265
265
  onClick={closeChannelModal}
266
- className="rounded-xl border-[hsl(40,20%,90%)] bg-white hover:bg-[hsl(40,20%,96%)]"
267
266
  >
268
267
  {t('cancel')}
269
268
  </Button>
270
269
  <Button
271
270
  type="submit"
272
271
  disabled={updateChannel.isPending || isConnecting}
273
- className="rounded-xl bg-gradient-to-r from-orange-400 to-amber-500 hover:from-orange-500 hover:to-amber-600 text-white border-0"
274
272
  >
275
273
  {updateChannel.isPending ? 'Saving...' : t('save')}
276
274
  </Button>
@@ -279,7 +277,7 @@ export function ChannelForm() {
279
277
  type="button"
280
278
  onClick={handleVerifyConnect}
281
279
  disabled={updateChannel.isPending || isConnecting}
282
- className="rounded-xl bg-gradient-to-r from-emerald-400 to-emerald-600 hover:from-emerald-500 hover:to-emerald-700 text-white border-0"
280
+ variant="secondary"
283
281
  >
284
282
  {isConnecting ? t('feishuConnecting') : t('saveVerifyConnect')}
285
283
  </Button>
@@ -1,12 +1,13 @@
1
1
  import { useConfig, useConfigMeta } from '@/hooks/useConfig';
2
2
  import { Button } from '@/components/ui/button';
3
- import { Skeleton } from '@/components/ui/skeleton';
4
- import { MessageCircle, Mail, MessageSquare, Slack, MoreHorizontal, ExternalLink, Bell } from 'lucide-react';
3
+ import { MessageCircle, Mail, MessageSquare, Slack, ExternalLink, Bell, Zap, Radio } from 'lucide-react';
5
4
  import { useState } from 'react';
6
5
  import { ChannelForm } from './ChannelForm';
7
6
  import { useUiStore } from '@/stores/ui.store';
8
7
  import { cn } from '@/lib/utils';
9
8
  import { Tabs } from '@/components/ui/tabs-custom';
9
+ import { LogoBadge } from '@/components/common/LogoBadge';
10
+ import { getChannelLogo } from '@/lib/logos';
10
11
 
11
12
  const channelIcons: Record<string, typeof MessageCircle> = {
12
13
  telegram: MessageCircle,
@@ -16,6 +17,15 @@ const channelIcons: Record<string, typeof MessageCircle> = {
16
17
  default: MessageSquare
17
18
  };
18
19
 
20
+ const channelDescriptions: Record<string, string> = {
21
+ telegram: 'Connect with Telegram bots for instant messaging',
22
+ slack: 'Integrate with Slack workspaces for team collaboration',
23
+ email: 'Send and receive messages via email protocols',
24
+ webhook: 'Receive HTTP webhooks for custom integrations',
25
+ discord: 'Connect Discord bots to your community servers',
26
+ feishu: 'Enterprise messaging and collaboration platform'
27
+ };
28
+
19
29
  export function ChannelsList() {
20
30
  const { data: config } = useConfig();
21
31
  const { data: meta } = useConfigMeta();
@@ -23,7 +33,7 @@ export function ChannelsList() {
23
33
  const [activeTab, setActiveTab] = useState('active');
24
34
 
25
35
  if (!config || !meta) {
26
- return <div className="p-8 text-[hsl(30,8%,55%)]">Loading channels...</div>;
36
+ return <div className="p-8 text-gray-400">Loading channels...</div>;
27
37
  }
28
38
 
29
39
  const tabs = [
@@ -31,83 +41,123 @@ export function ChannelsList() {
31
41
  { id: 'all', label: 'All Channels', count: meta.channels.length }
32
42
  ];
33
43
 
44
+ const filteredChannels = meta.channels.filter(channel => {
45
+ const enabled = config.channels[channel.name]?.enabled || false;
46
+ return activeTab === 'all' || enabled;
47
+ });
48
+
34
49
  return (
35
50
  <div className="animate-fade-in pb-20">
36
51
  <div className="flex items-center justify-between mb-8">
37
- <h2 className="text-2xl font-bold text-[hsl(30,15%,10%)]">Message Channels</h2>
52
+ <h2 className="text-2xl font-bold text-gray-900">Message Channels</h2>
38
53
  </div>
39
54
 
40
55
  <Tabs tabs={tabs} activeTab={activeTab} onChange={setActiveTab} />
41
56
 
42
- <div className="space-y-1">
43
- {meta.channels.map((channel, index) => {
57
+ {/* Channel Cards Grid */}
58
+ <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
59
+ {filteredChannels.map((channel) => {
44
60
  const channelConfig = config.channels[channel.name];
45
61
  const enabled = channelConfig?.enabled || false;
46
62
  const Icon = channelIcons[channel.name] || channelIcons.default;
47
63
 
48
- return (activeTab === 'all' || enabled) && (
64
+ return (
49
65
  <div
50
66
  key={channel.name}
51
- className="group flex items-center gap-5 p-3 rounded-2xl hover:bg-[hsl(40,10%,96%)] transition-all cursor-pointer border border-transparent hover:border-[hsl(40,10%,94%)]"
67
+ className={cn(
68
+ 'group relative flex flex-col p-5 rounded-2xl border transition-all duration-base cursor-pointer',
69
+ 'hover:shadow-card-hover hover:-translate-y-0.5',
70
+ enabled
71
+ ? 'bg-white border-gray-200 hover:border-gray-300'
72
+ : 'bg-gray-50 border-gray-200 hover:border-gray-300 hover:bg-white'
73
+ )}
52
74
  onClick={() => openChannelModal(channel.name)}
53
75
  >
54
- {/* Icon */}
55
- <div className={cn(
56
- 'h-10 w-10 flex items-center justify-center rounded-xl transition-all group-hover:scale-105',
57
- enabled ? 'bg-[hsl(30,15%,10%)] text-white' : 'bg-transparent border border-[hsl(40,10%,92%)] text-[hsl(30,8%,55%)]'
58
- )}>
59
- <Icon className="h-5 w-5" />
76
+ {/* Header with Icon and Status */}
77
+ <div className="flex items-start justify-between mb-4">
78
+ <LogoBadge
79
+ name={channel.name}
80
+ src={getChannelLogo(channel.name)}
81
+ className={cn(
82
+ 'h-12 w-12 rounded-xl border transition-all',
83
+ enabled
84
+ ? 'bg-white border-primary'
85
+ : 'bg-white border-gray-200 group-hover:border-gray-300'
86
+ )}
87
+ imgClassName="h-6 w-6"
88
+ fallback={<Icon className="h-6 w-6" />}
89
+ />
90
+
91
+ {/* Status Badge */}
92
+ {enabled ? (
93
+ <div className="flex items-center gap-1.5 px-2.5 py-1 rounded-full bg-emerald-50 text-emerald-600">
94
+ <Zap className="h-3.5 w-3.5" />
95
+ <span className="text-[11px] font-bold">Active</span>
96
+ </div>
97
+ ) : (
98
+ <div className="flex items-center gap-1.5 px-2.5 py-1 rounded-full bg-gray-100 text-gray-500">
99
+ <Radio className="h-3.5 w-3.5" />
100
+ <span className="text-[11px] font-bold">Inactive</span>
101
+ </div>
102
+ )}
60
103
  </div>
61
104
 
62
- {/* Info */}
63
- <div className="flex-1 min-w-0">
64
- <div className="flex items-center gap-2">
65
- <h3 className="text-[14px] font-bold text-[hsl(30,15%,10%)] truncate">
66
- {channel.displayName || channel.name}
67
- </h3>
68
- {enabled && (
69
- <span className="w-1.5 h-1.5 rounded-full bg-emerald-500 shadow-[0_0_8px_rgba(16,185,129,0.5)]" />
70
- )}
71
- </div>
72
- <p className="text-[12px] text-[hsl(30,8%,55%)] truncate leading-tight mt-0.5">
73
- {enabled ? 'Channel is active and processing messages' : 'Click to configure this communication channel'}
105
+ {/* Channel Info */}
106
+ <div className="flex-1">
107
+ <h3 className="text-[15px] font-bold text-gray-900 mb-1">
108
+ {channel.displayName || channel.name}
109
+ </h3>
110
+ <p className="text-[12px] text-gray-500 leading-relaxed line-clamp-2">
111
+ {channelDescriptions[channel.name] || 'Configure this communication channel'}
74
112
  </p>
75
113
  </div>
76
114
 
77
- {/* Status/Actions */}
78
- <div className="flex items-center gap-4">
115
+ {/* Footer with Actions */}
116
+ <div className="mt-4 pt-4 border-t border-gray-100 flex items-center gap-2">
79
117
  {channel.tutorialUrl && (
80
118
  <a
81
119
  href={channel.tutorialUrl}
82
120
  target="_blank"
83
121
  rel="noreferrer"
84
122
  onClick={(e) => e.stopPropagation()}
85
- className="hidden sm:inline-flex items-center gap-1 text-[11px] font-bold text-[hsl(30,10%,35%)] hover:text-[hsl(30,15%,10%)]"
123
+ className="flex items-center justify-center h-9 w-9 rounded-xl bg-gray-100 hover:bg-gray-200 text-gray-600 transition-colors"
124
+ title="View Guide"
86
125
  >
87
- Guide
88
- <ExternalLink className="h-3 w-3" />
126
+ <ExternalLink className="h-4 w-4" />
89
127
  </a>
90
128
  )}
91
129
  <Button
92
- variant="ghost"
130
+ variant={enabled ? 'ghost' : 'default'}
93
131
  size="sm"
94
- className="rounded-xl bg-[hsl(40,10%,92%)] hover:bg-[hsl(40,10%,90%)] text-[hsl(30,10%,35%)] text-[11px] font-bold px-4 h-8"
132
+ className="flex-1 rounded-xl text-xs font-semibold h-9"
95
133
  onClick={(e) => {
96
134
  e.stopPropagation();
97
135
  openChannelModal(channel.name);
98
136
  }}
99
137
  >
100
- Configure
138
+ {enabled ? 'Configure' : 'Enable'}
101
139
  </Button>
102
- <button className="h-8 w-8 flex items-center justify-center text-[hsl(30,8%,45%)]">
103
- <MoreHorizontal className="h-4 w-4" />
104
- </button>
105
140
  </div>
106
141
  </div>
107
142
  );
108
143
  })}
109
144
  </div>
110
145
 
146
+ {/* Empty State */}
147
+ {filteredChannels.length === 0 && (
148
+ <div className="flex flex-col items-center justify-center py-16 text-center">
149
+ <div className="h-16 w-16 flex items-center justify-center rounded-2xl bg-gray-100 mb-4">
150
+ <MessageSquare className="h-8 w-8 text-gray-400" />
151
+ </div>
152
+ <h3 className="text-[15px] font-bold text-gray-900 mb-2">
153
+ No channels enabled
154
+ </h3>
155
+ <p className="text-[13px] text-gray-500 max-w-sm">
156
+ Enable a messaging channel to start receiving messages. Click on any channel to configure it.
157
+ </p>
158
+ </div>
159
+ )}
160
+
111
161
  <ChannelForm />
112
162
  </div>
113
163
  );
@@ -37,7 +37,7 @@ export function ModelConfig() {
37
37
  <Skeleton className="h-8 w-32" />
38
38
  <Skeleton className="h-4 w-48" />
39
39
  </div>
40
- <Card className="rounded-2xl border-[hsl(40,20%,90%)] p-6">
40
+ <Card className="rounded-2xl border-gray-200 p-6">
41
41
  <div className="flex items-center gap-4 mb-6">
42
42
  <Skeleton className="h-12 w-12 rounded-xl" />
43
43
  <div className="space-y-2">
@@ -48,7 +48,7 @@ export function ModelConfig() {
48
48
  <Skeleton className="h-4 w-20 mb-2" />
49
49
  <Skeleton className="h-10 w-full rounded-xl" />
50
50
  </Card>
51
- <Card className="rounded-2xl border-[hsl(40,20%,90%)] p-6">
51
+ <Card className="rounded-2xl border-gray-200 p-6">
52
52
  <Skeleton className="h-5 w-24 mb-2" />
53
53
  <Skeleton className="h-3 w-40 mb-6" />
54
54
  <div className="space-y-6">
@@ -69,70 +69,70 @@ export function ModelConfig() {
69
69
  return (
70
70
  <div className="max-w-4xl animate-fade-in pb-20">
71
71
  <div className="mb-10">
72
- <h2 className="text-2xl font-bold text-[hsl(30,15%,10%)]">Model Configuration</h2>
73
- <p className="text-[14px] text-[hsl(30,8%,55%)] mt-1">Configure default AI model and behavior parameters</p>
72
+ <h2 className="text-2xl font-bold text-gray-900">Model Configuration</h2>
73
+ <p className="text-sm text-gray-500 mt-1">Configure default AI model and behavior parameters</p>
74
74
  </div>
75
75
 
76
76
  <form onSubmit={handleSubmit} className="space-y-8">
77
77
  <div className="grid grid-cols-1 md:grid-cols-2 gap-8">
78
78
  {/* Model Card */}
79
- <div className="p-8 rounded-[2rem] bg-[hsl(40,10%,98%)] border border-[hsl(40,10%,94%)]">
79
+ <div className="p-8 rounded-2xl bg-gray-50 border border-gray-200">
80
80
  <div className="flex items-center gap-4 mb-8">
81
- <div className="h-10 w-10 rounded-xl bg-[hsl(30,15%,10%)] flex items-center justify-center text-white">
81
+ <div className="h-10 w-10 rounded-xl bg-primary flex items-center justify-center text-white">
82
82
  <Sparkles className="h-5 w-5" />
83
83
  </div>
84
- <h3 className="text-lg font-bold text-[hsl(30,15%,10%)]">Default Model</h3>
84
+ <h3 className="text-lg font-bold text-gray-900">Default Model</h3>
85
85
  </div>
86
86
 
87
87
  <div className="space-y-2">
88
- <Label htmlFor="model" className="text-[12px] font-bold text-[hsl(30,8%,45%)] uppercase tracking-wider">Model Name</Label>
88
+ <Label htmlFor="model" className="text-xs font-semibold text-gray-500 uppercase tracking-wider">Model Name</Label>
89
89
  <Input
90
90
  id="model"
91
91
  value={model}
92
92
  onChange={(e) => setModel(e.target.value)}
93
93
  placeholder="minimax/MiniMax-M2.1"
94
- className="h-12 px-4 rounded-xl border-[hsl(40,10%,92%)] bg-white focus:ring-1 focus:ring-[hsl(30,15%,10%)] transition-all"
94
+ className="h-12 px-4 rounded-xl"
95
95
  />
96
- <p className="text-[12px] text-[hsl(30,8%,55%)]">Examples: minimax/MiniMax-M2.1 · anthropic/claude-opus-4-5</p>
96
+ <p className="text-xs text-gray-400">Examples: minimax/MiniMax-M2.1 · anthropic/claude-opus-4-5</p>
97
97
  </div>
98
98
  </div>
99
99
 
100
100
  {/* Workspace Card */}
101
- <div className="p-8 rounded-[2rem] bg-[hsl(40,10%,98%)] border border-[hsl(40,10%,94%)]">
101
+ <div className="p-8 rounded-2xl bg-gray-50 border border-gray-200">
102
102
  <div className="flex items-center gap-4 mb-8">
103
- <div className="h-10 w-10 rounded-xl bg-[hsl(30,15%,10%)] flex items-center justify-center text-white">
103
+ <div className="h-10 w-10 rounded-xl bg-primary flex items-center justify-center text-white">
104
104
  <Folder className="h-5 w-5" />
105
105
  </div>
106
- <h3 className="text-lg font-bold text-[hsl(30,15%,10%)]">Workspace</h3>
106
+ <h3 className="text-lg font-bold text-gray-900">Workspace</h3>
107
107
  </div>
108
108
 
109
109
  <div className="space-y-2">
110
- <Label htmlFor="workspace" className="text-[12px] font-bold text-[hsl(30,8%,45%)] uppercase tracking-wider">Default Path</Label>
110
+ <Label htmlFor="workspace" className="text-xs font-semibold text-gray-500 uppercase tracking-wider">Default Path</Label>
111
111
  <Input
112
112
  id="workspace"
113
113
  value={workspace}
114
114
  onChange={(e) => setWorkspace(e.target.value)}
115
115
  placeholder="/path/to/workspace"
116
- className="h-12 px-4 rounded-xl border-[hsl(40,10%,92%)] bg-white focus:ring-1 focus:ring-[hsl(30,15%,10%)] transition-all"
116
+ className="h-12 px-4 rounded-xl"
117
117
  />
118
118
  </div>
119
119
  </div>
120
120
  </div>
121
121
 
122
122
  {/* Parameters Section */}
123
- <div className="p-8 rounded-[2.5rem] bg-white border border-[hsl(40,10%,94%)] shadow-sm">
123
+ <div className="p-8 rounded-2xl bg-white border border-gray-200 shadow-card">
124
124
  <div className="flex items-center gap-4 mb-10">
125
- <div className="h-10 w-10 rounded-xl bg-[hsl(30,15%,10%)] flex items-center justify-center text-white">
125
+ <div className="h-10 w-10 rounded-xl bg-primary flex items-center justify-center text-white">
126
126
  <Sliders className="h-5 w-5" />
127
127
  </div>
128
- <h3 className="text-lg font-bold text-[hsl(30,15%,10%)]">Generation Parameters</h3>
128
+ <h3 className="text-lg font-bold text-gray-900">Generation Parameters</h3>
129
129
  </div>
130
130
 
131
131
  <div className="grid grid-cols-1 md:grid-cols-2 gap-12">
132
132
  <div className="space-y-4">
133
133
  <div className="flex justify-between items-center mb-2">
134
- <Label className="text-[12px] font-bold text-[hsl(30,8%,45%)] uppercase tracking-wider">Max Tokens</Label>
135
- <span className="text-[13px] font-bold text-[hsl(30,15%,10%)]">{maxTokens.toLocaleString()}</span>
134
+ <Label className="text-xs font-semibold text-gray-500 uppercase tracking-wider">Max Tokens</Label>
135
+ <span className="text-sm font-semibold text-gray-900">{maxTokens.toLocaleString()}</span>
136
136
  </div>
137
137
  <input
138
138
  type="range"
@@ -141,14 +141,14 @@ export function ModelConfig() {
141
141
  step="1000"
142
142
  value={maxTokens}
143
143
  onChange={(e) => setMaxTokens(parseInt(e.target.value))}
144
- className="w-full h-1 bg-[hsl(40,10%,92%)] rounded-full appearance-none cursor-pointer accent-[hsl(30,15%,10%)]"
144
+ className="w-full h-1 bg-gray-200 rounded-full appearance-none cursor-pointer accent-primary"
145
145
  />
146
146
  </div>
147
147
 
148
148
  <div className="space-y-4">
149
149
  <div className="flex justify-between items-center mb-2">
150
- <Label className="text-[12px] font-bold text-[hsl(30,8%,45%)] uppercase tracking-wider">Temperature</Label>
151
- <span className="text-[13px] font-bold text-[hsl(30,15%,10%)]">{temperature}</span>
150
+ <Label className="text-xs font-semibold text-gray-500 uppercase tracking-wider">Temperature</Label>
151
+ <span className="text-sm font-semibold text-gray-900">{temperature}</span>
152
152
  </div>
153
153
  <input
154
154
  type="range"
@@ -157,7 +157,7 @@ export function ModelConfig() {
157
157
  step="0.1"
158
158
  value={temperature}
159
159
  onChange={(e) => setTemperature(parseFloat(e.target.value))}
160
- className="w-full h-1 bg-[hsl(40,10%,92%)] rounded-full appearance-none cursor-pointer accent-[hsl(30,15%,10%)]"
160
+ className="w-full h-1 bg-gray-200 rounded-full appearance-none cursor-pointer accent-primary"
161
161
  />
162
162
  </div>
163
163
  </div>
@@ -167,7 +167,7 @@ export function ModelConfig() {
167
167
  <Button
168
168
  type="submit"
169
169
  disabled={updateModel.isPending}
170
- className="h-12 px-8 rounded-2xl bg-[hsl(30,15%,10%)] text-white hover:bg-[hsl(30,15%,20%)] transition-all font-bold shadow-md active:scale-95"
170
+ size="lg"
171
171
  >
172
172
  {updateModel.isPending ? (
173
173
  <Loader2 className="h-5 w-5 animate-spin" />