@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.
- package/CHANGELOG.md +12 -0
- package/dist/assets/index-BV3Gyu8h.js +225 -0
- package/dist/assets/index-iSLahgqA.css +1 -0
- package/dist/index.html +2 -2
- package/dist/logos/aihubmix.png +0 -0
- package/dist/logos/anthropic.svg +1 -0
- package/dist/logos/dashscope.png +0 -0
- package/dist/logos/deepseek.png +0 -0
- package/dist/logos/dingtalk.svg +1 -0
- package/dist/logos/discord.svg +1 -0
- package/dist/logos/email.svg +1 -0
- package/dist/logos/feishu.svg +12 -0
- package/dist/logos/gemini.svg +1 -0
- package/dist/logos/groq.svg +1 -0
- package/dist/logos/minimax.svg +1 -0
- package/dist/logos/mochat.svg +6 -0
- package/dist/logos/moonshot.png +0 -0
- package/dist/logos/openai.svg +1 -0
- package/dist/logos/openrouter.svg +1 -0
- package/dist/logos/qq.svg +1 -0
- package/dist/logos/slack.svg +1 -0
- package/dist/logos/telegram.svg +1 -0
- package/dist/logos/vllm.svg +1 -0
- package/dist/logos/whatsapp.svg +1 -0
- package/dist/logos/zhipu.svg +15 -0
- package/package.json +1 -1
- package/public/logos/aihubmix.png +0 -0
- package/public/logos/anthropic.svg +1 -0
- package/public/logos/dashscope.png +0 -0
- package/public/logos/deepseek.png +0 -0
- package/public/logos/dingtalk.svg +1 -0
- package/public/logos/discord.svg +1 -0
- package/public/logos/email.svg +1 -0
- package/public/logos/feishu.svg +12 -0
- package/public/logos/gemini.svg +1 -0
- package/public/logos/groq.svg +1 -0
- package/public/logos/minimax.svg +1 -0
- package/public/logos/mochat.svg +6 -0
- package/public/logos/moonshot.png +0 -0
- package/public/logos/openai.svg +1 -0
- package/public/logos/openrouter.svg +1 -0
- package/public/logos/qq.svg +1 -0
- package/public/logos/slack.svg +1 -0
- package/public/logos/telegram.svg +1 -0
- package/public/logos/vllm.svg +1 -0
- package/public/logos/whatsapp.svg +1 -0
- package/public/logos/zhipu.svg +15 -0
- package/src/App.tsx +0 -3
- package/src/api/client.ts +15 -1
- package/src/api/config.ts +5 -14
- package/src/api/types.ts +7 -8
- package/src/components/common/LogoBadge.tsx +35 -0
- package/src/components/common/StatusBadge.tsx +4 -4
- package/src/components/config/ChannelForm.tsx +48 -16
- package/src/components/config/ChannelsList.tsx +96 -34
- package/src/components/config/ModelConfig.tsx +30 -29
- package/src/components/config/ProviderForm.tsx +9 -11
- package/src/components/config/ProvidersList.tsx +90 -38
- package/src/components/layout/Header.tsx +7 -7
- package/src/components/layout/Sidebar.tsx +10 -23
- package/src/components/ui/HighlightCard.tsx +29 -29
- package/src/components/ui/button.tsx +13 -8
- package/src/components/ui/card.tsx +8 -7
- package/src/components/ui/dialog.tsx +8 -8
- package/src/components/ui/input.tsx +1 -1
- package/src/components/ui/label.tsx +1 -1
- package/src/components/ui/switch.tsx +3 -3
- package/src/components/ui/tabs-custom.tsx +6 -6
- package/src/components/ui/tabs.tsx +7 -6
- package/src/hooks/useConfig.ts +2 -29
- package/src/index.css +103 -56
- package/src/lib/i18n.ts +10 -6
- package/src/lib/logos.ts +42 -0
- package/src/stores/ui.store.ts +1 -1
- package/src/styles/design-system.css +248 -0
- package/tailwind.config.js +118 -10
- package/dist/assets/index-CDd3pWyf.js +0 -235
- package/dist/assets/index-CrA44GOI.css +0 -1
- 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-
|
|
26
|
-
textClass: 'text-
|
|
27
|
-
bgClass: 'bg-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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('
|
|
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-
|
|
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-
|
|
185
|
-
<span className="text-sm text-
|
|
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
|
|
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
|
|
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
|
|
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 {
|
|
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-
|
|
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-
|
|
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
|
-
|
|
43
|
-
|
|
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 (
|
|
64
|
+
return (
|
|
49
65
|
<div
|
|
50
66
|
key={channel.name}
|
|
51
|
-
className=
|
|
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=
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
|
64
|
-
<
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
{
|
|
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
|
-
{/*
|
|
78
|
-
<div className="flex items-center gap-
|
|
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=
|
|
130
|
+
variant={enabled ? 'ghost' : 'default'}
|
|
81
131
|
size="sm"
|
|
82
|
-
className="rounded-xl
|
|
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 {
|
|
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-
|
|
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-
|
|
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-
|
|
73
|
-
<p className="text-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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="
|
|
94
|
-
className="h-12 px-4 rounded-xl
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
134
|
-
<span className="text-
|
|
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-
|
|
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-
|
|
150
|
-
<span className="text-
|
|
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-
|
|
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
|
-
|
|
170
|
+
size="lg"
|
|
170
171
|
>
|
|
171
172
|
{updateModel.isPending ? (
|
|
172
173
|
<Loader2 className="h-5 w-5 animate-spin" />
|