@nextclaw/ui 0.2.1

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 (48) hide show
  1. package/.eslintrc.cjs +28 -0
  2. package/CHANGELOG.md +7 -0
  3. package/dist/assets/index-BrN4G7FO.js +240 -0
  4. package/dist/assets/index-VjHB2nG6.css +1 -0
  5. package/dist/index.html +14 -0
  6. package/index.html +13 -0
  7. package/package.json +50 -0
  8. package/postcss.config.js +6 -0
  9. package/src/App.tsx +51 -0
  10. package/src/api/client.ts +40 -0
  11. package/src/api/config.ts +86 -0
  12. package/src/api/types.ts +78 -0
  13. package/src/api/websocket.ts +77 -0
  14. package/src/components/common/KeyValueEditor.tsx +65 -0
  15. package/src/components/common/MaskedInput.tsx +39 -0
  16. package/src/components/common/StatusBadge.tsx +56 -0
  17. package/src/components/common/TagInput.tsx +56 -0
  18. package/src/components/config/ChannelForm.tsx +259 -0
  19. package/src/components/config/ChannelsList.tsx +102 -0
  20. package/src/components/config/ModelConfig.tsx +181 -0
  21. package/src/components/config/ProviderForm.tsx +147 -0
  22. package/src/components/config/ProvidersList.tsx +90 -0
  23. package/src/components/config/UiConfig.tsx +189 -0
  24. package/src/components/layout/AppLayout.tsx +20 -0
  25. package/src/components/layout/Header.tsx +36 -0
  26. package/src/components/layout/Sidebar.tsx +103 -0
  27. package/src/components/ui/HighlightCard.tsx +40 -0
  28. package/src/components/ui/button.tsx +50 -0
  29. package/src/components/ui/card.tsx +78 -0
  30. package/src/components/ui/dialog.tsx +120 -0
  31. package/src/components/ui/input.tsx +23 -0
  32. package/src/components/ui/label.tsx +20 -0
  33. package/src/components/ui/scroll-area.tsx +21 -0
  34. package/src/components/ui/skeleton.tsx +15 -0
  35. package/src/components/ui/switch.tsx +37 -0
  36. package/src/components/ui/tabs-custom.tsx +45 -0
  37. package/src/components/ui/tabs.tsx +88 -0
  38. package/src/hooks/useConfig.ts +95 -0
  39. package/src/hooks/useWebSocket.ts +38 -0
  40. package/src/index.css +177 -0
  41. package/src/lib/i18n.ts +119 -0
  42. package/src/lib/utils.ts +6 -0
  43. package/src/main.tsx +10 -0
  44. package/src/stores/ui.store.ts +39 -0
  45. package/src/vite-env.d.ts +9 -0
  46. package/tailwind.config.js +43 -0
  47. package/tsconfig.json +18 -0
  48. package/vite.config.ts +25 -0
@@ -0,0 +1,56 @@
1
+ import { useState } from 'react';
2
+ import { X } from 'lucide-react';
3
+ import { cn } from '@/lib/utils';
4
+
5
+ interface TagInputProps {
6
+ value: string[];
7
+ onChange: (tags: string[]) => void;
8
+ className?: string;
9
+ placeholder?: string;
10
+ }
11
+
12
+ export function TagInput({ value, onChange, className, placeholder = 'Type and press Enter...' }: TagInputProps) {
13
+ const [input, setInput] = useState('');
14
+
15
+ const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
16
+ if (e.key === 'Enter' && input.trim()) {
17
+ e.preventDefault();
18
+ onChange([...value, input.trim()]);
19
+ setInput('');
20
+ } else if (e.key === 'Backspace' && !input && value.length > 0) {
21
+ onChange(value.slice(0, -1));
22
+ }
23
+ };
24
+
25
+ const removeTag = (index: number) => {
26
+ onChange(value.filter((_, i) => i !== index));
27
+ };
28
+
29
+ return (
30
+ <div className={cn('flex flex-wrap gap-2 p-2 border rounded-md min-h-[42px]', className)}>
31
+ {value.map((tag, index) => (
32
+ <span
33
+ key={index}
34
+ className="inline-flex items-center gap-1 px-2 py-1 bg-primary text-primary-foreground rounded text-sm"
35
+ >
36
+ {tag}
37
+ <button
38
+ type="button"
39
+ onClick={() => removeTag(index)}
40
+ className="hover:text-red-300 transition-colors"
41
+ >
42
+ <X className="h-3 w-3" />
43
+ </button>
44
+ </span>
45
+ ))}
46
+ <input
47
+ type="text"
48
+ value={input}
49
+ onChange={(e) => setInput(e.target.value)}
50
+ onKeyDown={handleKeyDown}
51
+ className="flex-1 outline-none min-w-[100px] bg-transparent text-sm"
52
+ placeholder={placeholder}
53
+ />
54
+ </div>
55
+ );
56
+ }
@@ -0,0 +1,259 @@
1
+ import { useEffect, useState } from 'react';
2
+ import { useConfig, useUpdateChannel } from '@/hooks/useConfig';
3
+ import { useUiStore } from '@/stores/ui.store';
4
+ import {
5
+ Dialog,
6
+ DialogContent,
7
+ DialogHeader,
8
+ DialogTitle,
9
+ DialogDescription,
10
+ DialogFooter,
11
+ } from '@/components/ui/dialog';
12
+ import { Button } from '@/components/ui/button';
13
+ import { Input } from '@/components/ui/input';
14
+ import { Label } from '@/components/ui/label';
15
+ import { Switch } from '@/components/ui/switch';
16
+ import { TagInput } from '@/components/common/TagInput';
17
+ import { t } from '@/lib/i18n';
18
+ import { MessageCircle, Settings, ToggleLeft, Hash, Mail, Globe, KeyRound } from 'lucide-react';
19
+
20
+ // Field icon mapping
21
+ const getFieldIcon = (fieldName: string) => {
22
+ 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%)]" />;
24
+ }
25
+ if (fieldName.includes('url') || fieldName.includes('host')) {
26
+ return <Globe className="h-3.5 w-3.5 text-[hsl(30,8%,45%)]" />;
27
+ }
28
+ if (fieldName.includes('email') || fieldName.includes('mail')) {
29
+ return <Mail className="h-3.5 w-3.5 text-[hsl(30,8%,45%)]" />;
30
+ }
31
+ if (fieldName.includes('id') || fieldName.includes('from')) {
32
+ return <Hash className="h-3.5 w-3.5 text-[hsl(30,8%,45%)]" />;
33
+ }
34
+ if (fieldName === 'enabled' || fieldName === 'consentGranted') {
35
+ return <ToggleLeft className="h-3.5 w-3.5 text-[hsl(30,8%,45%)]" />;
36
+ }
37
+ return <Settings className="h-3.5 w-3.5 text-[hsl(30,8%,45%)]" />;
38
+ };
39
+
40
+ // Channel field definitions
41
+ const CHANNEL_FIELDS: Record<string, Array<{ name: string; type: string; label: string }>> = {
42
+ telegram: [
43
+ { name: 'enabled', type: 'boolean', label: t('enabled') },
44
+ { name: 'token', type: 'password', label: t('botToken') },
45
+ { name: 'allowFrom', type: 'tags', label: t('allowFrom') },
46
+ { name: 'proxy', type: 'text', label: t('proxy') }
47
+ ],
48
+ discord: [
49
+ { name: 'enabled', type: 'boolean', label: t('enabled') },
50
+ { name: 'token', type: 'password', label: t('botToken') },
51
+ { name: 'allowFrom', type: 'tags', label: t('allowFrom') },
52
+ { name: 'gatewayUrl', type: 'text', label: t('gatewayUrl') },
53
+ { name: 'intents', type: 'number', label: t('intents') }
54
+ ],
55
+ whatsapp: [
56
+ { name: 'enabled', type: 'boolean', label: t('enabled') },
57
+ { name: 'bridgeUrl', type: 'text', label: t('bridgeUrl') },
58
+ { name: 'allowFrom', type: 'tags', label: t('allowFrom') }
59
+ ],
60
+ feishu: [
61
+ { name: 'enabled', type: 'boolean', label: t('enabled') },
62
+ { name: 'appId', type: 'text', label: t('appId') },
63
+ { name: 'appSecret', type: 'password', label: t('appSecret') },
64
+ { name: 'encryptKey', type: 'password', label: t('encryptKey') },
65
+ { name: 'verificationToken', type: 'password', label: t('verificationToken') },
66
+ { name: 'allowFrom', type: 'tags', label: t('allowFrom') }
67
+ ],
68
+ dingtalk: [
69
+ { name: 'enabled', type: 'boolean', label: t('enabled') },
70
+ { name: 'clientId', type: 'text', label: t('clientId') },
71
+ { name: 'clientSecret', type: 'password', label: t('clientSecret') },
72
+ { name: 'allowFrom', type: 'tags', label: t('allowFrom') }
73
+ ],
74
+ slack: [
75
+ { name: 'enabled', type: 'boolean', label: t('enabled') },
76
+ { name: 'mode', type: 'text', label: t('mode') },
77
+ { name: 'webhookPath', type: 'text', label: t('webhookPath') },
78
+ { name: 'botToken', type: 'password', label: t('botToken') },
79
+ { name: 'appToken', type: 'password', label: t('appToken') }
80
+ ],
81
+ email: [
82
+ { name: 'enabled', type: 'boolean', label: t('enabled') },
83
+ { name: 'consentGranted', type: 'boolean', label: t('consentGranted') },
84
+ { name: 'imapHost', type: 'text', label: t('imapHost') },
85
+ { name: 'imapPort', type: 'number', label: t('imapPort') },
86
+ { name: 'imapUsername', type: 'text', label: t('imapUsername') },
87
+ { name: 'imapPassword', type: 'password', label: t('imapPassword') },
88
+ { name: 'fromAddress', type: 'email', label: t('fromAddress') }
89
+ ],
90
+ mochat: [
91
+ { name: 'enabled', type: 'boolean', label: t('enabled') },
92
+ { name: 'baseUrl', type: 'text', label: t('baseUrl') },
93
+ { name: 'clawToken', type: 'password', label: t('clawToken') },
94
+ { name: 'agentUserId', type: 'text', label: t('agentUserId') },
95
+ { name: 'allowFrom', type: 'tags', label: t('allowFrom') }
96
+ ],
97
+ qq: [
98
+ { name: 'enabled', type: 'boolean', label: t('enabled') },
99
+ { name: 'appId', type: 'text', label: t('appId') },
100
+ { name: 'secret', type: 'password', label: t('secret') },
101
+ { name: 'allowFrom', type: 'tags', label: t('allowFrom') }
102
+ ]
103
+ };
104
+
105
+ const channelIcons: Record<string, typeof MessageCircle> = {
106
+ telegram: MessageCircle,
107
+ slack: MessageCircle,
108
+ email: Mail,
109
+ default: MessageCircle
110
+ };
111
+
112
+ const channelColors: Record<string, string> = {
113
+ telegram: 'from-sky-400 to-blue-500',
114
+ slack: 'from-purple-400 to-indigo-500',
115
+ email: 'from-rose-400 to-pink-500',
116
+ default: 'from-slate-400 to-gray-500'
117
+ };
118
+
119
+ export function ChannelForm() {
120
+ const { channelModal, closeChannelModal } = useUiStore();
121
+ const { data: config } = useConfig();
122
+ const updateChannel = useUpdateChannel();
123
+
124
+ const [formData, setFormData] = useState<Record<string, unknown>>({});
125
+
126
+ const channelName = channelModal.channel;
127
+ const channelConfig = channelName ? config?.channels[channelName] : null;
128
+ const fields = channelName ? CHANNEL_FIELDS[channelName] : [];
129
+
130
+ useEffect(() => {
131
+ if (channelConfig) {
132
+ setFormData({ ...channelConfig });
133
+ } else {
134
+ setFormData({});
135
+ }
136
+ }, [channelConfig, channelName]);
137
+
138
+ const updateField = (name: string, value: unknown) => {
139
+ setFormData((prev) => ({ ...prev, [name]: value }));
140
+ };
141
+
142
+ const handleSubmit = (e: React.FormEvent) => {
143
+ e.preventDefault();
144
+
145
+ if (!channelName) return;
146
+
147
+ updateChannel.mutate(
148
+ { channel: channelName, data: formData },
149
+ { onSuccess: () => closeChannelModal() }
150
+ );
151
+ };
152
+
153
+ const Icon = channelIcons[channelName || ''] || channelIcons.default;
154
+ const gradientClass = channelColors[channelName || ''] || channelColors.default;
155
+
156
+ return (
157
+ <Dialog open={channelModal.open} onOpenChange={closeChannelModal}>
158
+ <DialogContent className="sm:max-w-[550px] max-h-[85vh] overflow-hidden flex flex-col">
159
+ <DialogHeader>
160
+ <div className="flex items-center gap-3">
161
+ <div className={`h-10 w-10 rounded-xl bg-gradient-to-br ${gradientClass} flex items-center justify-center`}>
162
+ <Icon className="h-5 w-5 text-white" />
163
+ </div>
164
+ <div>
165
+ <DialogTitle className="capitalize">{channelName}</DialogTitle>
166
+ <DialogDescription>Configure message channel parameters</DialogDescription>
167
+ </div>
168
+ </div>
169
+ </DialogHeader>
170
+
171
+ <div className="flex-1 overflow-y-auto custom-scrollbar py-2">
172
+ <form onSubmit={handleSubmit} className="space-y-5 pr-2">
173
+ {fields.map((field) => (
174
+ <div key={field.name} className="space-y-2.5">
175
+ <Label
176
+ htmlFor={field.name}
177
+ className="text-sm font-medium text-[hsl(30,20%,12%)] flex items-center gap-2"
178
+ >
179
+ {getFieldIcon(field.name)}
180
+ {field.label}
181
+ </Label>
182
+
183
+ {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%)]">
186
+ {(formData[field.name] as boolean) ? t('enabled') : t('disabled')}
187
+ </span>
188
+ <Switch
189
+ id={field.name}
190
+ checked={(formData[field.name] as boolean) || false}
191
+ onCheckedChange={(checked) => updateField(field.name, checked)}
192
+ className="data-[state=checked]:bg-emerald-500"
193
+ />
194
+ </div>
195
+ )}
196
+
197
+ {(field.type === 'text' || field.type === 'email') && (
198
+ <Input
199
+ id={field.name}
200
+ type={field.type}
201
+ value={(formData[field.name] as string) || ''}
202
+ 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"
204
+ />
205
+ )}
206
+
207
+ {field.type === 'password' && (
208
+ <Input
209
+ id={field.name}
210
+ type="password"
211
+ value={(formData[field.name] as string) || ''}
212
+ onChange={(e) => updateField(field.name, e.target.value)}
213
+ placeholder="Leave blank to keep unchanged"
214
+ className="rounded-xl border-[hsl(40,20%,90%)] bg-[hsl(40,20%,98%)] focus:bg-white"
215
+ />
216
+ )}
217
+
218
+ {field.type === 'number' && (
219
+ <Input
220
+ id={field.name}
221
+ type="number"
222
+ value={(formData[field.name] as number) || 0}
223
+ 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"
225
+ />
226
+ )}
227
+
228
+ {field.type === 'tags' && (
229
+ <TagInput
230
+ value={(formData[field.name] as string[]) || []}
231
+ onChange={(tags) => updateField(field.name, tags)}
232
+ />
233
+ )}
234
+ </div>
235
+ ))}
236
+
237
+ <DialogFooter className="pt-4">
238
+ <Button
239
+ type="button"
240
+ variant="outline"
241
+ onClick={closeChannelModal}
242
+ className="rounded-xl border-[hsl(40,20%,90%)] bg-white hover:bg-[hsl(40,20%,96%)]"
243
+ >
244
+ {t('cancel')}
245
+ </Button>
246
+ <Button
247
+ 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"
250
+ >
251
+ {updateChannel.isPending ? 'Saving...' : t('save')}
252
+ </Button>
253
+ </DialogFooter>
254
+ </form>
255
+ </div>
256
+ </DialogContent>
257
+ </Dialog>
258
+ );
259
+ }
@@ -0,0 +1,102 @@
1
+ import { useConfig, useConfigMeta } from '@/hooks/useConfig';
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';
5
+ import { useState } from 'react';
6
+ import { ChannelForm } from './ChannelForm';
7
+ import { useUiStore } from '@/stores/ui.store';
8
+ import { cn } from '@/lib/utils';
9
+ import { Tabs } from '@/components/ui/tabs-custom';
10
+
11
+ const channelIcons: Record<string, typeof MessageCircle> = {
12
+ telegram: MessageCircle,
13
+ slack: Slack,
14
+ email: Mail,
15
+ webhook: Bell,
16
+ default: MessageSquare
17
+ };
18
+
19
+ export function ChannelsList() {
20
+ const { data: config } = useConfig();
21
+ const { data: meta } = useConfigMeta();
22
+ const { openChannelModal } = useUiStore();
23
+ const [activeTab, setActiveTab] = useState('active');
24
+
25
+ if (!config || !meta) {
26
+ return <div className="p-8 text-[hsl(30,8%,55%)]">Loading channels...</div>;
27
+ }
28
+
29
+ const tabs = [
30
+ { id: 'active', label: 'Enabled', count: meta.channels.filter(c => config.channels[c.name]?.enabled).length },
31
+ { id: 'all', label: 'All Channels', count: meta.channels.length }
32
+ ];
33
+
34
+ return (
35
+ <div className="animate-fade-in pb-20">
36
+ <div className="flex items-center justify-between mb-8">
37
+ <h2 className="text-2xl font-bold text-[hsl(30,15%,10%)]">Message Channels</h2>
38
+ </div>
39
+
40
+ <Tabs tabs={tabs} activeTab={activeTab} onChange={setActiveTab} />
41
+
42
+ <div className="space-y-1">
43
+ {meta.channels.map((channel, index) => {
44
+ const channelConfig = config.channels[channel.name];
45
+ const enabled = channelConfig?.enabled || false;
46
+ const Icon = channelIcons[channel.name] || channelIcons.default;
47
+
48
+ return (activeTab === 'all' || enabled) && (
49
+ <div
50
+ 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%)]"
52
+ onClick={() => openChannelModal(channel.name)}
53
+ >
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" />
60
+ </div>
61
+
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'}
74
+ </p>
75
+ </div>
76
+
77
+ {/* Status/Actions */}
78
+ <div className="flex items-center gap-4">
79
+ <Button
80
+ variant="ghost"
81
+ 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"
83
+ onClick={(e) => {
84
+ e.stopPropagation();
85
+ openChannelModal(channel.name);
86
+ }}
87
+ >
88
+ Configure
89
+ </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
+ </div>
94
+ </div>
95
+ );
96
+ })}
97
+ </div>
98
+
99
+ <ChannelForm />
100
+ </div>
101
+ );
102
+ }
@@ -0,0 +1,181 @@
1
+ import { useState, useEffect } from 'react';
2
+ import { useConfig, useUpdateModel } from '@/hooks/useConfig';
3
+ import { Button } from '@/components/ui/button';
4
+ import { Input } from '@/components/ui/input';
5
+ import { Label } from '@/components/ui/label';
6
+ import { Card, CardContent, CardTitle, CardDescription } from '@/components/ui/card';
7
+ import { Skeleton } from '@/components/ui/skeleton';
8
+ import { Loader2, Save, Sparkles, Sliders, Folder } from 'lucide-react';
9
+
10
+ export function ModelConfig() {
11
+ const { data: config, isLoading } = useConfig();
12
+ const updateModel = useUpdateModel();
13
+
14
+ const [model, setModel] = useState('');
15
+ const [workspace, setWorkspace] = useState('');
16
+ const [maxTokens, setMaxTokens] = useState(8192);
17
+ const [temperature, setTemperature] = useState(0.7);
18
+
19
+ useEffect(() => {
20
+ if (config?.agents?.defaults) {
21
+ setModel(config.agents.defaults.model || '');
22
+ setWorkspace(config.agents.defaults.workspace || '');
23
+ setMaxTokens(config.agents.defaults.maxTokens || 8192);
24
+ setTemperature(config.agents.defaults.temperature || 0.7);
25
+ }
26
+ }, [config]);
27
+
28
+ const handleSubmit = (e: React.FormEvent) => {
29
+ e.preventDefault();
30
+ updateModel.mutate({ model });
31
+ };
32
+
33
+ if (isLoading) {
34
+ return (
35
+ <div className="max-w-2xl space-y-6">
36
+ <div className="space-y-2">
37
+ <Skeleton className="h-8 w-32" />
38
+ <Skeleton className="h-4 w-48" />
39
+ </div>
40
+ <Card className="rounded-2xl border-[hsl(40,20%,90%)] p-6">
41
+ <div className="flex items-center gap-4 mb-6">
42
+ <Skeleton className="h-12 w-12 rounded-xl" />
43
+ <div className="space-y-2">
44
+ <Skeleton className="h-5 w-24" />
45
+ <Skeleton className="h-3 w-32" />
46
+ </div>
47
+ </div>
48
+ <Skeleton className="h-4 w-20 mb-2" />
49
+ <Skeleton className="h-10 w-full rounded-xl" />
50
+ </Card>
51
+ <Card className="rounded-2xl border-[hsl(40,20%,90%)] p-6">
52
+ <Skeleton className="h-5 w-24 mb-2" />
53
+ <Skeleton className="h-3 w-40 mb-6" />
54
+ <div className="space-y-6">
55
+ <div>
56
+ <Skeleton className="h-4 w-28 mb-3" />
57
+ <Skeleton className="h-2 w-full rounded-full" />
58
+ </div>
59
+ <div>
60
+ <Skeleton className="h-4 w-32 mb-3" />
61
+ <Skeleton className="h-2 w-full rounded-full" />
62
+ </div>
63
+ </div>
64
+ </Card>
65
+ </div>
66
+ );
67
+ }
68
+
69
+ return (
70
+ <div className="max-w-4xl animate-fade-in pb-20">
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>
74
+ </div>
75
+
76
+ <form onSubmit={handleSubmit} className="space-y-8">
77
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-8">
78
+ {/* Model Card */}
79
+ <div className="p-8 rounded-[2rem] bg-[hsl(40,10%,98%)] border border-[hsl(40,10%,94%)]">
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">
82
+ <Sparkles className="h-5 w-5" />
83
+ </div>
84
+ <h3 className="text-lg font-bold text-[hsl(30,15%,10%)]">Default Model</h3>
85
+ </div>
86
+
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>
89
+ <Input
90
+ id="model"
91
+ value={model}
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"
95
+ />
96
+ </div>
97
+ </div>
98
+
99
+ {/* Workspace Card */}
100
+ <div className="p-8 rounded-[2rem] bg-[hsl(40,10%,98%)] border border-[hsl(40,10%,94%)]">
101
+ <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
+ <Folder className="h-5 w-5" />
104
+ </div>
105
+ <h3 className="text-lg font-bold text-[hsl(30,15%,10%)]">Workspace</h3>
106
+ </div>
107
+
108
+ <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
+ <Input
111
+ id="workspace"
112
+ value={workspace}
113
+ onChange={(e) => setWorkspace(e.target.value)}
114
+ 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
+ />
117
+ </div>
118
+ </div>
119
+ </div>
120
+
121
+ {/* Parameters Section */}
122
+ <div className="p-8 rounded-[2.5rem] bg-white border border-[hsl(40,10%,94%)] shadow-sm">
123
+ <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
+ <Sliders className="h-5 w-5" />
126
+ </div>
127
+ <h3 className="text-lg font-bold text-[hsl(30,15%,10%)]">Generation Parameters</h3>
128
+ </div>
129
+
130
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-12">
131
+ <div className="space-y-4">
132
+ <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>
135
+ </div>
136
+ <input
137
+ type="range"
138
+ min="1000"
139
+ max="32000"
140
+ step="1000"
141
+ value={maxTokens}
142
+ 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
+ />
145
+ </div>
146
+
147
+ <div className="space-y-4">
148
+ <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>
151
+ </div>
152
+ <input
153
+ type="range"
154
+ min="0"
155
+ max="2"
156
+ step="0.1"
157
+ value={temperature}
158
+ 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
+ />
161
+ </div>
162
+ </div>
163
+ </div>
164
+
165
+ <div className="flex justify-end pt-4">
166
+ <Button
167
+ type="submit"
168
+ 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
+ >
171
+ {updateModel.isPending ? (
172
+ <Loader2 className="h-5 w-5 animate-spin" />
173
+ ) : (
174
+ 'Save Changes'
175
+ )}
176
+ </Button>
177
+ </div>
178
+ </form>
179
+ </div>
180
+ );
181
+ }