@nextclaw/ui 0.2.4 → 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 +6 -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/config.ts +0 -19
- package/src/api/types.ts +0 -8
- package/src/components/common/LogoBadge.tsx +35 -0
- package/src/components/common/StatusBadge.tsx +4 -4
- package/src/components/config/ChannelForm.tsx +16 -18
- package/src/components/config/ChannelsList.tsx +87 -37
- package/src/components/config/ModelConfig.tsx +25 -25
- 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 +3 -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-C4OKhpdC.css +0 -1
- package/dist/assets/index-C8nOCIVG.js +0 -240
- package/src/components/config/UiConfig.tsx +0 -189
|
@@ -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,6 +1,6 @@
|
|
|
1
1
|
import { useEffect, useState } from 'react';
|
|
2
2
|
import { useConfig, useUpdateChannel } from '@/hooks/useConfig';
|
|
3
|
-
import { probeFeishu
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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('
|
|
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-
|
|
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-
|
|
209
|
-
<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">
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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 {
|
|
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-
|
|
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-
|
|
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">
|
|
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="
|
|
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
|
-
|
|
88
|
-
<ExternalLink className="h-3 w-3" />
|
|
126
|
+
<ExternalLink className="h-4 w-4" />
|
|
89
127
|
</a>
|
|
90
128
|
)}
|
|
91
129
|
<Button
|
|
92
|
-
variant=
|
|
130
|
+
variant={enabled ? 'ghost' : 'default'}
|
|
93
131
|
size="sm"
|
|
94
|
-
className="rounded-xl
|
|
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-
|
|
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,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-
|
|
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
93
|
placeholder="minimax/MiniMax-M2.1"
|
|
94
|
-
className="h-12 px-4 rounded-xl
|
|
94
|
+
className="h-12 px-4 rounded-xl"
|
|
95
95
|
/>
|
|
96
|
-
<p className="text-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
135
|
-
<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>
|
|
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-
|
|
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-
|
|
151
|
-
<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>
|
|
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-
|
|
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
|
-
|
|
170
|
+
size="lg"
|
|
171
171
|
>
|
|
172
172
|
{updateModel.isPending ? (
|
|
173
173
|
<Loader2 className="h-5 w-5 animate-spin" />
|
|
@@ -71,7 +71,7 @@ export function ProviderForm() {
|
|
|
71
71
|
<DialogContent className="sm:max-w-[500px]">
|
|
72
72
|
<DialogHeader>
|
|
73
73
|
<div className="flex items-center gap-3">
|
|
74
|
-
<div className="h-10 w-10 rounded-xl bg-
|
|
74
|
+
<div className="h-10 w-10 rounded-xl bg-primary flex items-center justify-center">
|
|
75
75
|
<KeyRound className="h-5 w-5 text-white" />
|
|
76
76
|
</div>
|
|
77
77
|
<div>
|
|
@@ -83,8 +83,8 @@ export function ProviderForm() {
|
|
|
83
83
|
|
|
84
84
|
<form onSubmit={handleSubmit} className="space-y-5 pt-2">
|
|
85
85
|
<div className="space-y-2.5">
|
|
86
|
-
<Label htmlFor="apiKey" className="text-sm font-medium text-
|
|
87
|
-
<KeyRound className="h-3.5 w-3.5 text-
|
|
86
|
+
<Label htmlFor="apiKey" className="text-sm font-medium text-gray-900 flex items-center gap-2">
|
|
87
|
+
<KeyRound className="h-3.5 w-3.5 text-gray-500" />
|
|
88
88
|
{t('apiKey')}
|
|
89
89
|
</Label>
|
|
90
90
|
<MaskedInput
|
|
@@ -93,13 +93,13 @@ export function ProviderForm() {
|
|
|
93
93
|
isSet={providerConfig?.apiKeySet}
|
|
94
94
|
onChange={(e) => setApiKey(e.target.value)}
|
|
95
95
|
placeholder={providerConfig?.apiKeySet ? t('apiKeySet') : 'Enter API Key'}
|
|
96
|
-
className="rounded-xl
|
|
96
|
+
className="rounded-xl"
|
|
97
97
|
/>
|
|
98
98
|
</div>
|
|
99
99
|
|
|
100
100
|
<div className="space-y-2.5">
|
|
101
|
-
<Label htmlFor="apiBase" className="text-sm font-medium text-
|
|
102
|
-
<Globe className="h-3.5 w-3.5 text-
|
|
101
|
+
<Label htmlFor="apiBase" className="text-sm font-medium text-gray-900 flex items-center gap-2">
|
|
102
|
+
<Globe className="h-3.5 w-3.5 text-gray-500" />
|
|
103
103
|
{t('apiBase')}
|
|
104
104
|
</Label>
|
|
105
105
|
<Input
|
|
@@ -108,13 +108,13 @@ export function ProviderForm() {
|
|
|
108
108
|
value={apiBase}
|
|
109
109
|
onChange={(e) => setApiBase(e.target.value)}
|
|
110
110
|
placeholder={providerSpec?.defaultApiBase || 'https://api.example.com'}
|
|
111
|
-
className="rounded-xl
|
|
111
|
+
className="rounded-xl"
|
|
112
112
|
/>
|
|
113
113
|
</div>
|
|
114
114
|
|
|
115
115
|
<div className="space-y-2.5">
|
|
116
|
-
<Label className="text-sm font-medium text-
|
|
117
|
-
<Hash className="h-3.5 w-3.5 text-
|
|
116
|
+
<Label className="text-sm font-medium text-gray-900 flex items-center gap-2">
|
|
117
|
+
<Hash className="h-3.5 w-3.5 text-gray-500" />
|
|
118
118
|
{t('extraHeaders')}
|
|
119
119
|
</Label>
|
|
120
120
|
<KeyValueEditor
|
|
@@ -128,14 +128,12 @@ export function ProviderForm() {
|
|
|
128
128
|
type="button"
|
|
129
129
|
variant="outline"
|
|
130
130
|
onClick={closeProviderModal}
|
|
131
|
-
className="rounded-xl border-[hsl(40,20%,90%)] bg-white hover:bg-[hsl(40,20%,96%)]"
|
|
132
131
|
>
|
|
133
132
|
{t('cancel')}
|
|
134
133
|
</Button>
|
|
135
134
|
<Button
|
|
136
135
|
type="submit"
|
|
137
136
|
disabled={updateProvider.isPending}
|
|
138
|
-
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"
|
|
139
137
|
>
|
|
140
138
|
{updateProvider.isPending ? 'Saving...' : t('save')}
|
|
141
139
|
</Button>
|