@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.
- package/.eslintrc.cjs +28 -0
- package/CHANGELOG.md +7 -0
- package/dist/assets/index-BrN4G7FO.js +240 -0
- package/dist/assets/index-VjHB2nG6.css +1 -0
- package/dist/index.html +14 -0
- package/index.html +13 -0
- package/package.json +50 -0
- package/postcss.config.js +6 -0
- package/src/App.tsx +51 -0
- package/src/api/client.ts +40 -0
- package/src/api/config.ts +86 -0
- package/src/api/types.ts +78 -0
- package/src/api/websocket.ts +77 -0
- package/src/components/common/KeyValueEditor.tsx +65 -0
- package/src/components/common/MaskedInput.tsx +39 -0
- package/src/components/common/StatusBadge.tsx +56 -0
- package/src/components/common/TagInput.tsx +56 -0
- package/src/components/config/ChannelForm.tsx +259 -0
- package/src/components/config/ChannelsList.tsx +102 -0
- package/src/components/config/ModelConfig.tsx +181 -0
- package/src/components/config/ProviderForm.tsx +147 -0
- package/src/components/config/ProvidersList.tsx +90 -0
- package/src/components/config/UiConfig.tsx +189 -0
- package/src/components/layout/AppLayout.tsx +20 -0
- package/src/components/layout/Header.tsx +36 -0
- package/src/components/layout/Sidebar.tsx +103 -0
- package/src/components/ui/HighlightCard.tsx +40 -0
- package/src/components/ui/button.tsx +50 -0
- package/src/components/ui/card.tsx +78 -0
- package/src/components/ui/dialog.tsx +120 -0
- package/src/components/ui/input.tsx +23 -0
- package/src/components/ui/label.tsx +20 -0
- package/src/components/ui/scroll-area.tsx +21 -0
- package/src/components/ui/skeleton.tsx +15 -0
- package/src/components/ui/switch.tsx +37 -0
- package/src/components/ui/tabs-custom.tsx +45 -0
- package/src/components/ui/tabs.tsx +88 -0
- package/src/hooks/useConfig.ts +95 -0
- package/src/hooks/useWebSocket.ts +38 -0
- package/src/index.css +177 -0
- package/src/lib/i18n.ts +119 -0
- package/src/lib/utils.ts +6 -0
- package/src/main.tsx +10 -0
- package/src/stores/ui.store.ts +39 -0
- package/src/vite-env.d.ts +9 -0
- package/tailwind.config.js +43 -0
- package/tsconfig.json +18 -0
- package/vite.config.ts +25 -0
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
import { useConfig, useConfigMeta, useUpdateProvider } 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 { MaskedInput } from '@/components/common/MaskedInput';
|
|
16
|
+
import { KeyValueEditor } from '@/components/common/KeyValueEditor';
|
|
17
|
+
import { t } from '@/lib/i18n';
|
|
18
|
+
import type { ProviderConfigUpdate } from '@/api/types';
|
|
19
|
+
import { KeyRound, Globe, Hash } from 'lucide-react';
|
|
20
|
+
|
|
21
|
+
export function ProviderForm() {
|
|
22
|
+
const { providerModal, closeProviderModal } = useUiStore();
|
|
23
|
+
const { data: config } = useConfig();
|
|
24
|
+
const { data: meta } = useConfigMeta();
|
|
25
|
+
const updateProvider = useUpdateProvider();
|
|
26
|
+
|
|
27
|
+
const [apiKey, setApiKey] = useState('');
|
|
28
|
+
const [apiBase, setApiBase] = useState('');
|
|
29
|
+
const [extraHeaders, setExtraHeaders] = useState<Record<string, string> | null>(null);
|
|
30
|
+
|
|
31
|
+
const providerName = providerModal.provider;
|
|
32
|
+
const providerSpec = meta?.providers.find((p) => p.name === providerName);
|
|
33
|
+
const providerConfig = providerName ? config?.providers[providerName] : null;
|
|
34
|
+
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
if (providerConfig) {
|
|
37
|
+
setApiBase(providerConfig.apiBase || providerSpec?.defaultApiBase || '');
|
|
38
|
+
setExtraHeaders(providerConfig.extraHeaders || null);
|
|
39
|
+
setApiKey(''); // Always start with empty for security
|
|
40
|
+
}
|
|
41
|
+
}, [providerConfig, providerSpec]);
|
|
42
|
+
|
|
43
|
+
const handleSubmit = (e: React.FormEvent) => {
|
|
44
|
+
e.preventDefault();
|
|
45
|
+
|
|
46
|
+
const payload: ProviderConfigUpdate = {};
|
|
47
|
+
|
|
48
|
+
// Only include apiKey if user has entered something
|
|
49
|
+
if (apiKey !== '') {
|
|
50
|
+
payload.apiKey = apiKey;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (apiBase && apiBase !== providerSpec?.defaultApiBase) {
|
|
54
|
+
payload.apiBase = apiBase;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (extraHeaders && Object.keys(extraHeaders).length > 0) {
|
|
58
|
+
payload.extraHeaders = extraHeaders;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (!providerName) return;
|
|
62
|
+
|
|
63
|
+
updateProvider.mutate(
|
|
64
|
+
{ provider: providerName, data: payload },
|
|
65
|
+
{ onSuccess: () => closeProviderModal() }
|
|
66
|
+
);
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<Dialog open={providerModal.open} onOpenChange={closeProviderModal}>
|
|
71
|
+
<DialogContent className="sm:max-w-[500px]">
|
|
72
|
+
<DialogHeader>
|
|
73
|
+
<div className="flex items-center gap-3">
|
|
74
|
+
<div className="h-10 w-10 rounded-xl bg-gradient-to-br from-orange-400 to-amber-500 flex items-center justify-center">
|
|
75
|
+
<KeyRound className="h-5 w-5 text-white" />
|
|
76
|
+
</div>
|
|
77
|
+
<div>
|
|
78
|
+
<DialogTitle>{providerSpec?.displayName || providerName}</DialogTitle>
|
|
79
|
+
<DialogDescription>Configure API keys and parameters for AI provider</DialogDescription>
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
</DialogHeader>
|
|
83
|
+
|
|
84
|
+
<form onSubmit={handleSubmit} className="space-y-5 pt-2">
|
|
85
|
+
<div className="space-y-2.5">
|
|
86
|
+
<Label htmlFor="apiKey" className="text-sm font-medium text-[hsl(30,20%,12%)] flex items-center gap-2">
|
|
87
|
+
<KeyRound className="h-3.5 w-3.5 text-[hsl(30,8%,45%)]" />
|
|
88
|
+
{t('apiKey')}
|
|
89
|
+
</Label>
|
|
90
|
+
<MaskedInput
|
|
91
|
+
id="apiKey"
|
|
92
|
+
value={apiKey}
|
|
93
|
+
isSet={providerConfig?.apiKeySet}
|
|
94
|
+
onChange={(e) => setApiKey(e.target.value)}
|
|
95
|
+
placeholder={providerConfig?.apiKeySet ? t('apiKeySet') : 'Enter API Key'}
|
|
96
|
+
className="rounded-xl border-[hsl(40,20%,90%)] bg-[hsl(40,20%,98%)] focus:bg-white"
|
|
97
|
+
/>
|
|
98
|
+
</div>
|
|
99
|
+
|
|
100
|
+
<div className="space-y-2.5">
|
|
101
|
+
<Label htmlFor="apiBase" className="text-sm font-medium text-[hsl(30,20%,12%)] flex items-center gap-2">
|
|
102
|
+
<Globe className="h-3.5 w-3.5 text-[hsl(30,8%,45%)]" />
|
|
103
|
+
{t('apiBase')}
|
|
104
|
+
</Label>
|
|
105
|
+
<Input
|
|
106
|
+
id="apiBase"
|
|
107
|
+
type="text"
|
|
108
|
+
value={apiBase}
|
|
109
|
+
onChange={(e) => setApiBase(e.target.value)}
|
|
110
|
+
placeholder={providerSpec?.defaultApiBase || 'https://api.example.com'}
|
|
111
|
+
className="rounded-xl border-[hsl(40,20%,90%)] bg-[hsl(40,20%,98%)] focus:bg-white"
|
|
112
|
+
/>
|
|
113
|
+
</div>
|
|
114
|
+
|
|
115
|
+
<div className="space-y-2.5">
|
|
116
|
+
<Label className="text-sm font-medium text-[hsl(30,20%,12%)] flex items-center gap-2">
|
|
117
|
+
<Hash className="h-3.5 w-3.5 text-[hsl(30,8%,45%)]" />
|
|
118
|
+
{t('extraHeaders')}
|
|
119
|
+
</Label>
|
|
120
|
+
<KeyValueEditor
|
|
121
|
+
value={extraHeaders}
|
|
122
|
+
onChange={setExtraHeaders}
|
|
123
|
+
/>
|
|
124
|
+
</div>
|
|
125
|
+
|
|
126
|
+
<DialogFooter className="pt-4">
|
|
127
|
+
<Button
|
|
128
|
+
type="button"
|
|
129
|
+
variant="outline"
|
|
130
|
+
onClick={closeProviderModal}
|
|
131
|
+
className="rounded-xl border-[hsl(40,20%,90%)] bg-white hover:bg-[hsl(40,20%,96%)]"
|
|
132
|
+
>
|
|
133
|
+
{t('cancel')}
|
|
134
|
+
</Button>
|
|
135
|
+
<Button
|
|
136
|
+
type="submit"
|
|
137
|
+
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
|
+
>
|
|
140
|
+
{updateProvider.isPending ? 'Saving...' : t('save')}
|
|
141
|
+
</Button>
|
|
142
|
+
</DialogFooter>
|
|
143
|
+
</form>
|
|
144
|
+
</DialogContent>
|
|
145
|
+
</Dialog>
|
|
146
|
+
);
|
|
147
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { useConfig, useConfigMeta } from '@/hooks/useConfig';
|
|
2
|
+
import { Button } from '@/components/ui/button';
|
|
3
|
+
import { Skeleton } from '@/components/ui/skeleton';
|
|
4
|
+
import { KeyRound, Lock, Check, Plus, MoreHorizontal } from 'lucide-react';
|
|
5
|
+
import { useState } from 'react';
|
|
6
|
+
import { ProviderForm } from './ProviderForm';
|
|
7
|
+
import { useUiStore } from '@/stores/ui.store';
|
|
8
|
+
import { cn } from '@/lib/utils';
|
|
9
|
+
import { Tabs } from '@/components/ui/tabs-custom';
|
|
10
|
+
import { HighlightCard } from '@/components/ui/HighlightCard';
|
|
11
|
+
|
|
12
|
+
export function ProvidersList() {
|
|
13
|
+
const { data: config } = useConfig();
|
|
14
|
+
const { data: meta } = useConfigMeta();
|
|
15
|
+
const { openProviderModal } = useUiStore();
|
|
16
|
+
const [activeTab, setActiveTab] = useState('featured');
|
|
17
|
+
|
|
18
|
+
if (!config || !meta) {
|
|
19
|
+
return <div className="p-8">Loading...</div>; // Skeleton optimization can follow
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const tabs = [
|
|
23
|
+
{ id: 'installed', label: 'Configured', count: config.providers ? Object.keys(config.providers).filter(k => config.providers[k].apiKeySet).length : 0 },
|
|
24
|
+
{ id: 'all', label: 'All Providers' }
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<div className="animate-fade-in pb-20">
|
|
29
|
+
<div className="flex items-center justify-between mb-8">
|
|
30
|
+
<h2 className="text-2xl font-bold text-[hsl(30,15%,10%)]">AI Providers</h2>
|
|
31
|
+
</div>
|
|
32
|
+
|
|
33
|
+
{/* Tabs */}
|
|
34
|
+
<Tabs tabs={tabs} activeTab={activeTab === 'featured' ? 'all' : activeTab} onChange={setActiveTab} />
|
|
35
|
+
|
|
36
|
+
{/* Provider List Row-Style */}
|
|
37
|
+
<div className="space-y-1">
|
|
38
|
+
{(activeTab === 'installed'
|
|
39
|
+
? meta.providers.filter((p) => config.providers[p.name]?.apiKeySet)
|
|
40
|
+
: meta.providers
|
|
41
|
+
).map((provider) => {
|
|
42
|
+
const providerConfig = config.providers[provider.name];
|
|
43
|
+
const hasConfig = providerConfig?.apiKeySet;
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<div
|
|
47
|
+
key={provider.name}
|
|
48
|
+
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%)]"
|
|
49
|
+
onClick={() => openProviderModal(provider.name)}
|
|
50
|
+
>
|
|
51
|
+
{/* Logo/Icon */}
|
|
52
|
+
<div className="h-10 w-10 flex items-center justify-center bg-transparent border border-[hsl(40,10%,92%)] rounded-xl group-hover:scale-105 transition-transform overflow-hidden">
|
|
53
|
+
<span className="text-xl font-bold uppercase text-[hsl(30,15%,10%)]">{provider.name[0]}</span>
|
|
54
|
+
</div>
|
|
55
|
+
|
|
56
|
+
{/* Info */}
|
|
57
|
+
<div className="flex-1 min-w-0">
|
|
58
|
+
<h3 className="text-[14px] font-bold text-[hsl(30,15%,10%)] truncate">
|
|
59
|
+
{provider.displayName || provider.name}
|
|
60
|
+
</h3>
|
|
61
|
+
<p className="text-[12px] text-[hsl(30,8%,55%)] truncate leading-tight">
|
|
62
|
+
{provider.name === 'openai' ? 'TypeScript authentication framework integration guide' : 'Configure AI services for your agents'}
|
|
63
|
+
</p>
|
|
64
|
+
</div>
|
|
65
|
+
|
|
66
|
+
{/* Status/Actions */}
|
|
67
|
+
<div className="flex items-center gap-4">
|
|
68
|
+
{hasConfig ? (
|
|
69
|
+
<div className="flex items-center gap-1.5 text-emerald-600">
|
|
70
|
+
<Check className="h-4 w-4" />
|
|
71
|
+
<span className="text-[11px] font-bold">Ready</span>
|
|
72
|
+
</div>
|
|
73
|
+
) : (
|
|
74
|
+
<button className="h-8 w-8 flex items-center justify-center text-[hsl(30,8%,45%)] hover:text-[hsl(30,15%,10%)] group-hover:opacity-100 opacity-0 transition-opacity">
|
|
75
|
+
<Plus className="h-4 w-4" />
|
|
76
|
+
</button>
|
|
77
|
+
)}
|
|
78
|
+
<button className="h-8 w-8 flex items-center justify-center text-[hsl(30,8%,45%)]">
|
|
79
|
+
<MoreHorizontal className="h-4 w-4" />
|
|
80
|
+
</button>
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
);
|
|
84
|
+
})}
|
|
85
|
+
</div>
|
|
86
|
+
|
|
87
|
+
<ProviderForm />
|
|
88
|
+
</div>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
import { useConfig, useUpdateUiConfig, useReloadConfig } 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 { Switch } from '@/components/ui/switch';
|
|
7
|
+
import { Card, CardContent, CardTitle, CardDescription } from '@/components/ui/card';
|
|
8
|
+
import { Skeleton } from '@/components/ui/skeleton';
|
|
9
|
+
import { RefreshCw, Save, Monitor, Power } from 'lucide-react';
|
|
10
|
+
import { cn } from '@/lib/utils';
|
|
11
|
+
import { t } from '@/lib/i18n';
|
|
12
|
+
|
|
13
|
+
export function UiConfig() {
|
|
14
|
+
const { data: config, isLoading } = useConfig();
|
|
15
|
+
const updateUiConfig = useUpdateUiConfig();
|
|
16
|
+
const reloadConfig = useReloadConfig();
|
|
17
|
+
|
|
18
|
+
const [enabled, setEnabled] = useState(false);
|
|
19
|
+
const [host, setHost] = useState('127.0.0.1');
|
|
20
|
+
const [port, setPort] = useState(18791);
|
|
21
|
+
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
if (config?.ui) {
|
|
24
|
+
setEnabled(config.ui.enabled);
|
|
25
|
+
setHost(config.ui.host);
|
|
26
|
+
setPort(config.ui.port);
|
|
27
|
+
}
|
|
28
|
+
}, [config]);
|
|
29
|
+
|
|
30
|
+
const handleSubmit = (e: React.FormEvent) => {
|
|
31
|
+
e.preventDefault();
|
|
32
|
+
updateUiConfig.mutate({ enabled, host, port, open: true });
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const handleReload = () => {
|
|
36
|
+
reloadConfig.mutate();
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
if (isLoading) {
|
|
40
|
+
return (
|
|
41
|
+
<div className="max-w-2xl space-y-6">
|
|
42
|
+
<div className="space-y-2">
|
|
43
|
+
<Skeleton className="h-8 w-32" />
|
|
44
|
+
<Skeleton className="h-4 w-48" />
|
|
45
|
+
</div>
|
|
46
|
+
<Card className="rounded-2xl border-[hsl(40,20%,90%)] p-6">
|
|
47
|
+
<div className="flex items-center gap-4 mb-6">
|
|
48
|
+
<Skeleton className="h-12 w-12 rounded-xl" />
|
|
49
|
+
<div className="space-y-2">
|
|
50
|
+
<Skeleton className="h-5 w-28" />
|
|
51
|
+
<Skeleton className="h-3 w-36" />
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
<Skeleton className="h-16 w-full rounded-xl mb-4" />
|
|
55
|
+
<div className="grid grid-cols-2 gap-4">
|
|
56
|
+
<div>
|
|
57
|
+
<Skeleton className="h-4 w-16 mb-2" />
|
|
58
|
+
<Skeleton className="h-10 w-full rounded-xl" />
|
|
59
|
+
</div>
|
|
60
|
+
<div>
|
|
61
|
+
<Skeleton className="h-4 w-12 mb-2" />
|
|
62
|
+
<Skeleton className="h-10 w-full rounded-xl" />
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
</Card>
|
|
66
|
+
</div>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<div className="max-w-2xl space-y-6">
|
|
72
|
+
{/* Header */}
|
|
73
|
+
<div>
|
|
74
|
+
<h2 className="text-xl font-semibold text-[hsl(30,20%,12%)]">{t('uiConfig')}</h2>
|
|
75
|
+
<p className="text-sm text-[hsl(30,8%,45%)] mt-1">Configure Web UI server and access options</p>
|
|
76
|
+
</div>
|
|
77
|
+
|
|
78
|
+
<Card className="rounded-2xl border-[hsl(40,20%,90%)] bg-white">
|
|
79
|
+
<CardContent className="p-6">
|
|
80
|
+
<div className="flex items-center gap-4 mb-6">
|
|
81
|
+
<div className="h-12 w-12 rounded-xl bg-gradient-to-br from-purple-400 to-indigo-500 flex items-center justify-center">
|
|
82
|
+
<Monitor className="h-5 w-5 text-white" />
|
|
83
|
+
</div>
|
|
84
|
+
<div>
|
|
85
|
+
<CardTitle className="text-base font-semibold text-[hsl(30,20%,12%)]">Web UI Server</CardTitle>
|
|
86
|
+
<CardDescription className="text-xs text-[hsl(30,8%,45%)]">Configure server runtime parameters</CardDescription>
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
|
|
90
|
+
<form onSubmit={handleSubmit} className="space-y-6">
|
|
91
|
+
<div className={cn(
|
|
92
|
+
"flex items-center justify-between p-4 rounded-xl transition-colors",
|
|
93
|
+
enabled ? "bg-emerald-50" : "bg-[hsl(40,20%,96%)]"
|
|
94
|
+
)}>
|
|
95
|
+
<div className="flex items-center gap-3">
|
|
96
|
+
<div className={cn(
|
|
97
|
+
"h-10 w-10 rounded-lg flex items-center justify-center",
|
|
98
|
+
enabled ? "bg-emerald-100 text-emerald-600" : "bg-[hsl(40,20%,94%)] text-[hsl(30,8%,45%)]"
|
|
99
|
+
)}>
|
|
100
|
+
<Power className="h-5 w-5" />
|
|
101
|
+
</div>
|
|
102
|
+
<div>
|
|
103
|
+
<h3 className="font-medium text-[hsl(30,20%,12%)]">Enable Web UI</h3>
|
|
104
|
+
<p className={cn(
|
|
105
|
+
"text-xs",
|
|
106
|
+
enabled ? "text-emerald-600" : "text-[hsl(30,8%,45%)]"
|
|
107
|
+
)}>
|
|
108
|
+
{enabled ? t('connected') : t('disconnected')}
|
|
109
|
+
</p>
|
|
110
|
+
</div>
|
|
111
|
+
</div>
|
|
112
|
+
<Switch
|
|
113
|
+
id="enabled"
|
|
114
|
+
checked={enabled}
|
|
115
|
+
onCheckedChange={setEnabled}
|
|
116
|
+
className="data-[state=checked]:bg-emerald-500"
|
|
117
|
+
/>
|
|
118
|
+
</div>
|
|
119
|
+
|
|
120
|
+
<div className="grid grid-cols-2 gap-4">
|
|
121
|
+
<div className="space-y-2">
|
|
122
|
+
<Label htmlFor="host" className="text-sm font-medium text-[hsl(30,20%,12%)]">{t('host')}</Label>
|
|
123
|
+
<Input
|
|
124
|
+
id="host"
|
|
125
|
+
type="text"
|
|
126
|
+
value={host}
|
|
127
|
+
onChange={(e) => setHost(e.target.value)}
|
|
128
|
+
placeholder="127.0.0.1"
|
|
129
|
+
className="rounded-xl border-[hsl(40,20%,90%)] bg-[hsl(40,20%,98%)] focus:bg-white"
|
|
130
|
+
/>
|
|
131
|
+
</div>
|
|
132
|
+
|
|
133
|
+
<div className="space-y-2">
|
|
134
|
+
<Label htmlFor="port" className="text-sm font-medium text-[hsl(30,20%,12%)]">{t('port')}</Label>
|
|
135
|
+
<Input
|
|
136
|
+
id="port"
|
|
137
|
+
type="number"
|
|
138
|
+
value={port}
|
|
139
|
+
onChange={(e) => setPort(parseInt(e.target.value) || 18791)}
|
|
140
|
+
placeholder="18791"
|
|
141
|
+
min="1"
|
|
142
|
+
max="65535"
|
|
143
|
+
className="rounded-xl border-[hsl(40,20%,90%)] bg-[hsl(40,20%,98%)] focus:bg-white"
|
|
144
|
+
/>
|
|
145
|
+
</div>
|
|
146
|
+
</div>
|
|
147
|
+
|
|
148
|
+
<div className="flex justify-end">
|
|
149
|
+
<Button
|
|
150
|
+
type="submit"
|
|
151
|
+
disabled={updateUiConfig.isPending}
|
|
152
|
+
className="gap-2 rounded-xl bg-gradient-to-r from-orange-400 to-amber-500 hover:from-orange-500 hover:to-amber-600 text-white border-0"
|
|
153
|
+
>
|
|
154
|
+
<Save className="h-4 w-4" />
|
|
155
|
+
{t('save')}
|
|
156
|
+
</Button>
|
|
157
|
+
</div>
|
|
158
|
+
</form>
|
|
159
|
+
</CardContent>
|
|
160
|
+
</Card>
|
|
161
|
+
|
|
162
|
+
<Card className="rounded-2xl border-[hsl(40,20%,90%)] bg-white">
|
|
163
|
+
<CardContent className="p-6">
|
|
164
|
+
<div className="flex items-center gap-4 mb-6">
|
|
165
|
+
<div className="h-12 w-12 rounded-xl bg-gradient-to-br from-slate-400 to-gray-500 flex items-center justify-center">
|
|
166
|
+
<RefreshCw className="h-5 w-5 text-white" />
|
|
167
|
+
</div>
|
|
168
|
+
<div>
|
|
169
|
+
<CardTitle className="text-base font-semibold text-[hsl(30,20%,12%)]">{t('reloadConfig')}</CardTitle>
|
|
170
|
+
<CardDescription className="text-xs text-[hsl(30,8%,45%)]">Apply changes and restart services</CardDescription>
|
|
171
|
+
</div>
|
|
172
|
+
</div>
|
|
173
|
+
<p className="text-sm text-[hsl(30,8%,45%)] mb-4">
|
|
174
|
+
Click the button below to reload the configuration file and apply all changes.
|
|
175
|
+
</p>
|
|
176
|
+
<Button
|
|
177
|
+
variant="outline"
|
|
178
|
+
onClick={handleReload}
|
|
179
|
+
disabled={reloadConfig.isPending}
|
|
180
|
+
className="w-full gap-2 rounded-xl border-[hsl(40,20%,90%)] bg-[hsl(40,20%,98%)] hover:bg-[hsl(40,20%,94%)] text-[hsl(30,10%,35%)]"
|
|
181
|
+
>
|
|
182
|
+
<RefreshCw className="h-4 w-4" />
|
|
183
|
+
{t('reloadConfig')}
|
|
184
|
+
</Button>
|
|
185
|
+
</CardContent>
|
|
186
|
+
</Card>
|
|
187
|
+
</div>
|
|
188
|
+
);
|
|
189
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Sidebar } from './Sidebar';
|
|
2
|
+
|
|
3
|
+
interface AppLayoutProps {
|
|
4
|
+
children: React.ReactNode;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function AppLayout({ children }: AppLayoutProps) {
|
|
8
|
+
return (
|
|
9
|
+
<div className="h-screen flex bg-[hsl(40,20%,98%)] p-2">
|
|
10
|
+
<Sidebar />
|
|
11
|
+
<div className="flex-1 flex flex-col min-w-0 bg-white rounded-[2rem] shadow-sm overflow-hidden border border-[hsl(40,10%,94%)]">
|
|
12
|
+
<main className="flex-1 overflow-auto custom-scrollbar p-8">
|
|
13
|
+
<div className="max-w-6xl mx-auto animate-fade-in h-full">
|
|
14
|
+
{children}
|
|
15
|
+
</div>
|
|
16
|
+
</main>
|
|
17
|
+
</div>
|
|
18
|
+
</div>
|
|
19
|
+
);
|
|
20
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Bell, Search } from 'lucide-react';
|
|
2
|
+
|
|
3
|
+
interface HeaderProps {
|
|
4
|
+
title?: string;
|
|
5
|
+
description?: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function Header({ title, description }: HeaderProps) {
|
|
9
|
+
return (
|
|
10
|
+
<header className="h-16 bg-white/80 backdrop-blur-md sticky top-0 z-10 border-b border-[hsl(40,20%,90%)] flex items-center justify-between px-6 transition-all duration-300">
|
|
11
|
+
<div className="flex items-center gap-4">
|
|
12
|
+
{title && (
|
|
13
|
+
<div>
|
|
14
|
+
<h2 className="text-base font-semibold text-[hsl(30,20%,12%)]">{title}</h2>
|
|
15
|
+
{description && (
|
|
16
|
+
<p className="text-xs text-[hsl(30,8%,45%)]">{description}</p>
|
|
17
|
+
)}
|
|
18
|
+
</div>
|
|
19
|
+
)}
|
|
20
|
+
</div>
|
|
21
|
+
|
|
22
|
+
<div className="flex items-center gap-3">
|
|
23
|
+
<button className="h-9 w-9 rounded-lg bg-[hsl(40,20%,96%)] flex items-center justify-center text-[hsl(30,8%,45%)] hover:bg-[hsl(40,20%,94%)] hover:text-[hsl(30,20%,12%)] transition-colors">
|
|
24
|
+
<Search className="h-4 w-4" />
|
|
25
|
+
</button>
|
|
26
|
+
<button className="h-9 w-9 rounded-lg bg-[hsl(40,20%,96%)] flex items-center justify-center text-[hsl(30,8%,45%)] hover:bg-[hsl(40,20%,94%)] hover:text-[hsl(30,20%,12%)] transition-colors relative">
|
|
27
|
+
<Bell className="h-4 w-4" />
|
|
28
|
+
<span className="absolute top-1.5 right-1.5 h-2 w-2 rounded-full bg-orange-500" />
|
|
29
|
+
</button>
|
|
30
|
+
<div className="h-9 w-9 rounded-lg bg-gradient-to-br from-orange-400 to-amber-500 flex items-center justify-center">
|
|
31
|
+
<span className="text-xs font-semibold text-white">N</span>
|
|
32
|
+
</div>
|
|
33
|
+
</div>
|
|
34
|
+
</header>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { useUiStore } from '@/stores/ui.store';
|
|
2
|
+
import { cn } from '@/lib/utils';
|
|
3
|
+
import {
|
|
4
|
+
Cpu,
|
|
5
|
+
MessageSquare,
|
|
6
|
+
Sparkles,
|
|
7
|
+
ChevronRight,
|
|
8
|
+
Settings
|
|
9
|
+
} from 'lucide-react';
|
|
10
|
+
|
|
11
|
+
const navItems = [
|
|
12
|
+
{
|
|
13
|
+
id: 'model' as const,
|
|
14
|
+
label: 'Models',
|
|
15
|
+
icon: Cpu,
|
|
16
|
+
color: 'text-[hsl(30,15%,10%)]'
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
id: 'providers' as const,
|
|
20
|
+
label: 'Providers',
|
|
21
|
+
icon: Sparkles,
|
|
22
|
+
color: 'text-[hsl(30,15%,10%)]'
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
id: 'channels' as const,
|
|
26
|
+
label: 'Channels',
|
|
27
|
+
icon: MessageSquare,
|
|
28
|
+
color: 'text-[hsl(30,15%,10%)]'
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
id: 'ui' as const,
|
|
32
|
+
label: 'Appearance',
|
|
33
|
+
icon: Settings,
|
|
34
|
+
color: 'text-[hsl(30,15%,10%)]'
|
|
35
|
+
}
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
export function Sidebar() {
|
|
39
|
+
const { activeTab, setActiveTab } = useUiStore();
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<aside className="w-[240px] bg-transparent flex flex-col h-full py-6 px-4">
|
|
43
|
+
{/* Logo Area */}
|
|
44
|
+
<div className="px-3 mb-8">
|
|
45
|
+
<div className="flex items-center gap-2 group cursor-pointer">
|
|
46
|
+
<div className="h-6 w-6 rounded-md bg-[hsl(30,15%,10%)] flex items-center justify-center transition-transform group-hover:scale-110">
|
|
47
|
+
<Sparkles className="h-4 w-4 text-white" />
|
|
48
|
+
</div>
|
|
49
|
+
<h1 className="text-lg font-bold text-[hsl(30,15%,10%)] tracking-tight">nextclaw</h1>
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
{/* Navigation */}
|
|
54
|
+
<nav className="flex-1">
|
|
55
|
+
<ul className="space-y-1">
|
|
56
|
+
{navItems.map((item) => {
|
|
57
|
+
const Icon = item.icon;
|
|
58
|
+
const isActive = activeTab === item.id;
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<li key={item.id}>
|
|
62
|
+
<button
|
|
63
|
+
onClick={() => setActiveTab(item.id)}
|
|
64
|
+
className={cn(
|
|
65
|
+
'group w-full flex items-center gap-3 px-3 py-2 rounded-lg text-[13px] font-medium transition-all duration-200',
|
|
66
|
+
isActive
|
|
67
|
+
? 'bg-[hsl(40,10%,92%)] text-[hsl(30,15%,10%)]'
|
|
68
|
+
: 'text-[hsl(30,8%,45%)] hover:bg-[hsl(40,10%,94%)] hover:text-[hsl(30,15%,10%)]'
|
|
69
|
+
)}
|
|
70
|
+
>
|
|
71
|
+
<Icon className={cn('h-4 w-4 transition-transform group-hover:scale-110', isActive ? 'text-[hsl(30,15%,10%)]' : 'text-[hsl(30,8%,45%)]')} />
|
|
72
|
+
<span className="flex-1 text-left">{item.label}</span>
|
|
73
|
+
</button>
|
|
74
|
+
</li>
|
|
75
|
+
);
|
|
76
|
+
})}
|
|
77
|
+
</ul>
|
|
78
|
+
|
|
79
|
+
</nav>
|
|
80
|
+
|
|
81
|
+
{/* Bottom Profile Section */}
|
|
82
|
+
<div className="mt-auto px-1 pt-4">
|
|
83
|
+
<div className="flex items-center gap-2 mb-4 px-2">
|
|
84
|
+
<div className="flex gap-1.5 items-center">
|
|
85
|
+
<div className="w-2 h-2 rounded-full bg-amber-400" />
|
|
86
|
+
<span className="text-[11px] text-[hsl(30,8%,65%)]">Starting...</span>
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
|
|
90
|
+
<button className="w-full flex items-center gap-3 p-2 rounded-xl hover:bg-[hsl(40,10%,94%)] transition-all group">
|
|
91
|
+
<div className="h-8 w-8 rounded-full bg-gradient-to-br from-amber-400 to-orange-500 flex items-center justify-center text-white text-xs font-bold shadow-sm group-hover:scale-105 transition-transform">
|
|
92
|
+
WX
|
|
93
|
+
</div>
|
|
94
|
+
<div className="flex-1 text-left min-w-0">
|
|
95
|
+
<p className="text-[13px] font-semibold text-[hsl(30,15%,10%)] truncate">Wang Xiaotiao</p>
|
|
96
|
+
<p className="text-[11px] text-[hsl(30,8%,55%)] truncate">Free plan</p>
|
|
97
|
+
</div>
|
|
98
|
+
<ChevronRight className="h-4 w-4 text-[hsl(30,8%,65%)] group-hover:translate-x-0.5 transition-transform" />
|
|
99
|
+
</button>
|
|
100
|
+
</div>
|
|
101
|
+
</aside>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { cn } from '@/lib/utils';
|
|
3
|
+
import { ArrowRight } from 'lucide-react';
|
|
4
|
+
|
|
5
|
+
interface HighlightCardProps {
|
|
6
|
+
category: string;
|
|
7
|
+
title: string;
|
|
8
|
+
description: string;
|
|
9
|
+
image: string;
|
|
10
|
+
gradient: string;
|
|
11
|
+
className?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function HighlightCard({ category, title, description, image, gradient, className }: HighlightCardProps) {
|
|
15
|
+
return (
|
|
16
|
+
<div className={cn(
|
|
17
|
+
'group relative overflow-hidden rounded-[1.5rem] bg-white border border-[hsl(40,10%,94%)] flex h-[180px] transition-all duration-300 hover:shadow-premium cursor-pointer',
|
|
18
|
+
className
|
|
19
|
+
)}>
|
|
20
|
+
<div className="flex-1 p-6 flex flex-col">
|
|
21
|
+
<span className="text-[10px] font-bold text-[hsl(30,8%,65%)] uppercase tracking-widest mb-2">{category}</span>
|
|
22
|
+
<h3 className="text-xl font-bold text-[hsl(30,15%,10%)] leading-tight mb-2 group-hover:text-amber-600 transition-colors">{title}</h3>
|
|
23
|
+
<p className="text-[13px] text-[hsl(30,8%,55%)] line-clamp-2 leading-relaxed max-w-[200px]">{description}</p>
|
|
24
|
+
|
|
25
|
+
<div className="mt-auto flex items-center gap-1 text-[11px] font-bold text-[hsl(30,15%,10%)] opacity-0 group-hover:opacity-100 transition-opacity">
|
|
26
|
+
Learn more <ArrowRight className="h-3 w-3" />
|
|
27
|
+
</div>
|
|
28
|
+
</div>
|
|
29
|
+
|
|
30
|
+
<div className={cn('w-[160px] relative overflow-hidden', gradient)}>
|
|
31
|
+
<img
|
|
32
|
+
src={image}
|
|
33
|
+
alt={title}
|
|
34
|
+
className="w-full h-full object-cover mix-blend-multiply opacity-90 group-hover:scale-110 transition-transform duration-500"
|
|
35
|
+
/>
|
|
36
|
+
<div className="absolute inset-0 bg-gradient-to-l from-transparent to-white/10" />
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { cva, type VariantProps } from 'class-variance-authority';
|
|
3
|
+
import { cn } from '@/lib/utils';
|
|
4
|
+
|
|
5
|
+
const buttonVariants = cva(
|
|
6
|
+
'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-all duration-200 active:scale-[0.98] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
|
|
7
|
+
{
|
|
8
|
+
variants: {
|
|
9
|
+
variant: {
|
|
10
|
+
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
|
|
11
|
+
destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
|
|
12
|
+
outline: 'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
|
|
13
|
+
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
|
|
14
|
+
ghost: 'hover:bg-accent hover:text-accent-foreground',
|
|
15
|
+
link: 'text-primary underline-offset-4 hover:underline'
|
|
16
|
+
},
|
|
17
|
+
size: {
|
|
18
|
+
default: 'h-10 px-4 py-2',
|
|
19
|
+
sm: 'h-9 rounded-md px-3',
|
|
20
|
+
lg: 'h-11 rounded-md px-8',
|
|
21
|
+
icon: 'h-10 w-10'
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
defaultVariants: {
|
|
25
|
+
variant: 'default',
|
|
26
|
+
size: 'default'
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
export interface ButtonProps
|
|
32
|
+
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
|
33
|
+
VariantProps<typeof buttonVariants> {
|
|
34
|
+
asChild?: boolean;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|
38
|
+
({ className, variant, size, ...props }, ref) => {
|
|
39
|
+
return (
|
|
40
|
+
<button
|
|
41
|
+
className={cn(buttonVariants({ variant, size, className }))}
|
|
42
|
+
ref={ref}
|
|
43
|
+
{...props}
|
|
44
|
+
/>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
);
|
|
48
|
+
Button.displayName = 'Button';
|
|
49
|
+
|
|
50
|
+
export { Button, buttonVariants };
|