@nextclaw/ui 0.2.3 → 0.2.5

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 (79) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/assets/index-BV3Gyu8h.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/client.ts +15 -1
  50. package/src/api/config.ts +5 -14
  51. package/src/api/types.ts +7 -8
  52. package/src/components/common/LogoBadge.tsx +35 -0
  53. package/src/components/common/StatusBadge.tsx +4 -4
  54. package/src/components/config/ChannelForm.tsx +48 -16
  55. package/src/components/config/ChannelsList.tsx +96 -34
  56. package/src/components/config/ModelConfig.tsx +30 -29
  57. package/src/components/config/ProviderForm.tsx +9 -11
  58. package/src/components/config/ProvidersList.tsx +90 -38
  59. package/src/components/layout/Header.tsx +7 -7
  60. package/src/components/layout/Sidebar.tsx +10 -23
  61. package/src/components/ui/HighlightCard.tsx +29 -29
  62. package/src/components/ui/button.tsx +13 -8
  63. package/src/components/ui/card.tsx +8 -7
  64. package/src/components/ui/dialog.tsx +8 -8
  65. package/src/components/ui/input.tsx +1 -1
  66. package/src/components/ui/label.tsx +1 -1
  67. package/src/components/ui/switch.tsx +3 -3
  68. package/src/components/ui/tabs-custom.tsx +6 -6
  69. package/src/components/ui/tabs.tsx +7 -6
  70. package/src/hooks/useConfig.ts +2 -29
  71. package/src/index.css +103 -56
  72. package/src/lib/i18n.ts +10 -6
  73. package/src/lib/logos.ts +42 -0
  74. package/src/stores/ui.store.ts +1 -1
  75. package/src/styles/design-system.css +248 -0
  76. package/tailwind.config.js +118 -10
  77. package/dist/assets/index-CDd3pWyf.js +0 -235
  78. package/dist/assets/index-CrA44GOI.css +0 -1
  79. package/src/components/config/UiConfig.tsx +0 -189
package/src/api/types.ts CHANGED
@@ -24,13 +24,6 @@ export type ProviderConfigUpdate = {
24
24
 
25
25
  export type ChannelConfigUpdate = Record<string, unknown>;
26
26
 
27
- export type UiConfigView = {
28
- enabled: boolean;
29
- host: string;
30
- port: number;
31
- open: boolean;
32
- };
33
-
34
27
  export type ConfigView = {
35
28
  agents: {
36
29
  defaults: {
@@ -45,7 +38,6 @@ export type ConfigView = {
45
38
  channels: Record<string, Record<string, unknown>>;
46
39
  tools?: Record<string, unknown>;
47
40
  gateway?: Record<string, unknown>;
48
- ui?: UiConfigView;
49
41
  };
50
42
 
51
43
  export type ProviderSpecView = {
@@ -62,6 +54,7 @@ export type ChannelSpecView = {
62
54
  name: string;
63
55
  displayName?: string;
64
56
  enabled: boolean;
57
+ tutorialUrl?: string;
65
58
  };
66
59
 
67
60
  export type ConfigMetaView = {
@@ -69,6 +62,12 @@ export type ConfigMetaView = {
69
62
  channels: ChannelSpecView[];
70
63
  };
71
64
 
65
+ export type FeishuProbeView = {
66
+ appId: string;
67
+ botName?: string | null;
68
+ botOpenId?: string | null;
69
+ };
70
+
72
71
  // WebSocket events
73
72
  export type WsEvent =
74
73
  | { type: 'config.updated'; payload: { path: string } }
@@ -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,5 +1,6 @@
1
1
  import { useEffect, useState } from 'react';
2
2
  import { useConfig, useUpdateChannel } from '@/hooks/useConfig';
3
+ import { probeFeishu } from '@/api/config';
3
4
  import { useUiStore } from '@/stores/ui.store';
4
5
  import {
5
6
  Dialog,
@@ -15,26 +16,27 @@ import { Label } from '@/components/ui/label';
15
16
  import { Switch } from '@/components/ui/switch';
16
17
  import { TagInput } from '@/components/common/TagInput';
17
18
  import { t } from '@/lib/i18n';
19
+ import { toast } from 'sonner';
18
20
  import { MessageCircle, Settings, ToggleLeft, Hash, Mail, Globe, KeyRound } from 'lucide-react';
19
21
 
20
22
  // Field icon mapping
21
23
  const getFieldIcon = (fieldName: string) => {
22
24
  if (fieldName.includes('token') || fieldName.includes('secret') || fieldName.includes('password')) {
23
- 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" />;
24
26
  }
25
27
  if (fieldName.includes('url') || fieldName.includes('host')) {
26
- 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" />;
27
29
  }
28
30
  if (fieldName.includes('email') || fieldName.includes('mail')) {
29
- 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" />;
30
32
  }
31
33
  if (fieldName.includes('id') || fieldName.includes('from')) {
32
- 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" />;
33
35
  }
34
36
  if (fieldName === 'enabled' || fieldName === 'consentGranted') {
35
- 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" />;
36
38
  }
37
- 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" />;
38
40
  };
39
41
 
40
42
  // Channel field definitions
@@ -97,7 +99,8 @@ const CHANNEL_FIELDS: Record<string, Array<{ name: string; type: string; label:
97
99
  qq: [
98
100
  { name: 'enabled', type: 'boolean', label: t('enabled') },
99
101
  { name: 'appId', type: 'text', label: t('appId') },
100
- { name: 'secret', type: 'password', label: t('secret') },
102
+ { name: 'secret', type: 'password', label: t('appSecret') },
103
+ { name: 'markdownSupport', type: 'boolean', label: t('markdownSupport') },
101
104
  { name: 'allowFrom', type: 'tags', label: t('allowFrom') }
102
105
  ]
103
106
  };
@@ -122,6 +125,7 @@ export function ChannelForm() {
122
125
  const updateChannel = useUpdateChannel();
123
126
 
124
127
  const [formData, setFormData] = useState<Record<string, unknown>>({});
128
+ const [isConnecting, setIsConnecting] = useState(false);
125
129
 
126
130
  const channelName = channelModal.channel;
127
131
  const channelConfig = channelName ? config?.channels[channelName] : null;
@@ -150,6 +154,26 @@ export function ChannelForm() {
150
154
  );
151
155
  };
152
156
 
157
+ const handleVerifyConnect = async () => {
158
+ if (!channelName || channelName !== 'feishu') return;
159
+ setIsConnecting(true);
160
+ try {
161
+ const nextData = { ...formData, enabled: true };
162
+ if (!formData.enabled) {
163
+ setFormData(nextData);
164
+ }
165
+ await updateChannel.mutateAsync({ channel: channelName, data: nextData });
166
+ const probe = await probeFeishu();
167
+ const botLabel = probe.botName ? ` (${probe.botName})` : '';
168
+ toast.success(t('feishuVerifySuccess') + botLabel);
169
+ } catch (error) {
170
+ const message = error instanceof Error ? error.message : String(error);
171
+ toast.error(`${t('feishuVerifyFailed')}: ${message}`);
172
+ } finally {
173
+ setIsConnecting(false);
174
+ }
175
+ };
176
+
153
177
  const Icon = channelIcons[channelName || ''] || channelIcons.default;
154
178
  const gradientClass = channelColors[channelName || ''] || channelColors.default;
155
179
 
@@ -174,15 +198,15 @@ export function ChannelForm() {
174
198
  <div key={field.name} className="space-y-2.5">
175
199
  <Label
176
200
  htmlFor={field.name}
177
- 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"
178
202
  >
179
203
  {getFieldIcon(field.name)}
180
204
  {field.label}
181
205
  </Label>
182
206
 
183
207
  {field.type === 'boolean' && (
184
- <div className="flex items-center justify-between p-3 rounded-xl bg-[hsl(40,20%,96%)]">
185
- <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">
186
210
  {(formData[field.name] as boolean) ? t('enabled') : t('disabled')}
187
211
  </span>
188
212
  <Switch
@@ -200,7 +224,7 @@ export function ChannelForm() {
200
224
  type={field.type}
201
225
  value={(formData[field.name] as string) || ''}
202
226
  onChange={(e) => updateField(field.name, e.target.value)}
203
- className="rounded-xl border-[hsl(40,20%,90%)] bg-[hsl(40,20%,98%)] focus:bg-white"
227
+ className="rounded-xl"
204
228
  />
205
229
  )}
206
230
 
@@ -211,7 +235,7 @@ export function ChannelForm() {
211
235
  value={(formData[field.name] as string) || ''}
212
236
  onChange={(e) => updateField(field.name, e.target.value)}
213
237
  placeholder="Leave blank to keep unchanged"
214
- className="rounded-xl border-[hsl(40,20%,90%)] bg-[hsl(40,20%,98%)] focus:bg-white"
238
+ className="rounded-xl"
215
239
  />
216
240
  )}
217
241
 
@@ -221,7 +245,7 @@ export function ChannelForm() {
221
245
  type="number"
222
246
  value={(formData[field.name] as number) || 0}
223
247
  onChange={(e) => updateField(field.name, parseInt(e.target.value) || 0)}
224
- className="rounded-xl border-[hsl(40,20%,90%)] bg-[hsl(40,20%,98%)] focus:bg-white"
248
+ className="rounded-xl"
225
249
  />
226
250
  )}
227
251
 
@@ -239,17 +263,25 @@ export function ChannelForm() {
239
263
  type="button"
240
264
  variant="outline"
241
265
  onClick={closeChannelModal}
242
- className="rounded-xl border-[hsl(40,20%,90%)] bg-white hover:bg-[hsl(40,20%,96%)]"
243
266
  >
244
267
  {t('cancel')}
245
268
  </Button>
246
269
  <Button
247
270
  type="submit"
248
- disabled={updateChannel.isPending}
249
- 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"
271
+ disabled={updateChannel.isPending || isConnecting}
250
272
  >
251
273
  {updateChannel.isPending ? 'Saving...' : t('save')}
252
274
  </Button>
275
+ {channelName === 'feishu' && (
276
+ <Button
277
+ type="button"
278
+ onClick={handleVerifyConnect}
279
+ disabled={updateChannel.isPending || isConnecting}
280
+ variant="secondary"
281
+ >
282
+ {isConnecting ? t('feishuConnecting') : t('saveVerifyConnect')}
283
+ </Button>
284
+ )}
253
285
  </DialogFooter>
254
286
  </form>
255
287
  </div>
@@ -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, Settings2, Bell, Mail, MessageSquare, Slack, MoreHorizontal, Plus } 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,71 +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">
117
+ {channel.tutorialUrl && (
118
+ <a
119
+ href={channel.tutorialUrl}
120
+ target="_blank"
121
+ rel="noreferrer"
122
+ onClick={(e) => e.stopPropagation()}
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"
125
+ >
126
+ <ExternalLink className="h-4 w-4" />
127
+ </a>
128
+ )}
79
129
  <Button
80
- variant="ghost"
130
+ variant={enabled ? 'ghost' : 'default'}
81
131
  size="sm"
82
- 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"
83
133
  onClick={(e) => {
84
134
  e.stopPropagation();
85
135
  openChannelModal(channel.name);
86
136
  }}
87
137
  >
88
- Configure
138
+ {enabled ? 'Configure' : 'Enable'}
89
139
  </Button>
90
- <button className="h-8 w-8 flex items-center justify-center text-[hsl(30,8%,45%)]">
91
- <MoreHorizontal className="h-4 w-4" />
92
- </button>
93
140
  </div>
94
141
  </div>
95
142
  );
96
143
  })}
97
144
  </div>
98
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
+
99
161
  <ChannelForm />
100
162
  </div>
101
163
  );
@@ -1,11 +1,11 @@
1
- import { useState, useEffect } from 'react';
2
- import { useConfig, useUpdateModel } from '@/hooks/useConfig';
3
1
  import { Button } from '@/components/ui/button';
2
+ import { Card } from '@/components/ui/card';
4
3
  import { Input } from '@/components/ui/input';
5
4
  import { Label } from '@/components/ui/label';
6
- import { Card, CardContent, CardTitle, CardDescription } from '@/components/ui/card';
7
5
  import { Skeleton } from '@/components/ui/skeleton';
8
- import { Loader2, Save, Sparkles, Sliders, Folder } from 'lucide-react';
6
+ import { useConfig, useUpdateModel } from '@/hooks/useConfig';
7
+ import { Folder, Loader2, Sliders, Sparkles } from 'lucide-react';
8
+ import { useEffect, useState } from 'react';
9
9
 
10
10
  export function ModelConfig() {
11
11
  const { data: config, isLoading } = useConfig();
@@ -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,69 +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
- placeholder="e.g. gpt-4, claude-3"
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"
93
+ placeholder="minimax/MiniMax-M2.1"
94
+ className="h-12 px-4 rounded-xl"
95
95
  />
96
+ <p className="text-xs text-gray-400">Examples: minimax/MiniMax-M2.1 · anthropic/claude-opus-4-5</p>
96
97
  </div>
97
98
  </div>
98
99
 
99
100
  {/* Workspace Card */}
100
- <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">
101
102
  <div className="flex items-center gap-4 mb-8">
102
- <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">
103
104
  <Folder className="h-5 w-5" />
104
105
  </div>
105
- <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>
106
107
  </div>
107
108
 
108
109
  <div className="space-y-2">
109
- <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>
110
111
  <Input
111
112
  id="workspace"
112
113
  value={workspace}
113
114
  onChange={(e) => setWorkspace(e.target.value)}
114
115
  placeholder="/path/to/workspace"
115
- 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"
116
117
  />
117
118
  </div>
118
119
  </div>
119
120
  </div>
120
121
 
121
122
  {/* Parameters Section */}
122
- <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">
123
124
  <div className="flex items-center gap-4 mb-10">
124
- <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">
125
126
  <Sliders className="h-5 w-5" />
126
127
  </div>
127
- <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>
128
129
  </div>
129
130
 
130
131
  <div className="grid grid-cols-1 md:grid-cols-2 gap-12">
131
132
  <div className="space-y-4">
132
133
  <div className="flex justify-between items-center mb-2">
133
- <Label className="text-[12px] font-bold text-[hsl(30,8%,45%)] uppercase tracking-wider">Max Tokens</Label>
134
- <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>
135
136
  </div>
136
137
  <input
137
138
  type="range"
@@ -140,14 +141,14 @@ export function ModelConfig() {
140
141
  step="1000"
141
142
  value={maxTokens}
142
143
  onChange={(e) => setMaxTokens(parseInt(e.target.value))}
143
- 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"
144
145
  />
145
146
  </div>
146
147
 
147
148
  <div className="space-y-4">
148
149
  <div className="flex justify-between items-center mb-2">
149
- <Label className="text-[12px] font-bold text-[hsl(30,8%,45%)] uppercase tracking-wider">Temperature</Label>
150
- <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>
151
152
  </div>
152
153
  <input
153
154
  type="range"
@@ -156,7 +157,7 @@ export function ModelConfig() {
156
157
  step="0.1"
157
158
  value={temperature}
158
159
  onChange={(e) => setTemperature(parseFloat(e.target.value))}
159
- 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"
160
161
  />
161
162
  </div>
162
163
  </div>
@@ -166,7 +167,7 @@ export function ModelConfig() {
166
167
  <Button
167
168
  type="submit"
168
169
  disabled={updateModel.isPending}
169
- 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"
170
171
  >
171
172
  {updateModel.isPending ? (
172
173
  <Loader2 className="h-5 w-5 animate-spin" />