@mdguggenbichler/slugbase-core 0.0.18 → 0.0.20
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.
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { useState, useEffect, useCallback } from 'react';
|
|
2
2
|
import { useTranslation } from 'react-i18next';
|
|
3
3
|
import api from '../../api/client';
|
|
4
4
|
import { useToast } from '../ui/Toast';
|
|
5
5
|
import { useAppConfig } from '../../contexts/AppConfigContext';
|
|
6
|
-
import {
|
|
7
|
-
import Button from '../ui/Button';
|
|
6
|
+
import { Sparkles } from 'lucide-react';
|
|
8
7
|
import { PageLoadingSkeleton } from '../ui/PageLoadingSkeleton';
|
|
9
8
|
import { Switch } from '../ui/switch';
|
|
10
9
|
import { Label } from '../ui/label';
|
|
@@ -20,7 +19,6 @@ export default function AdminAI() {
|
|
|
20
19
|
const { showToast } = useToast();
|
|
21
20
|
const { adminAiOnlyToggle } = useAppConfig();
|
|
22
21
|
const [loading, setLoading] = useState(true);
|
|
23
|
-
const [saving, setSaving] = useState(false);
|
|
24
22
|
const [settings, setSettings] = useState({
|
|
25
23
|
ai_enabled: false,
|
|
26
24
|
ai_provider: 'openai',
|
|
@@ -65,9 +63,12 @@ export default function AdminAI() {
|
|
|
65
63
|
}
|
|
66
64
|
}, [showToast, t, adminAiOnlyToggle]);
|
|
67
65
|
|
|
66
|
+
// Run initial load once on mount only. Re-running when loadSettings identity changes
|
|
67
|
+
// (e.g. from i18n t) caused repeated API calls and infinite toasts on error.
|
|
68
68
|
useEffect(() => {
|
|
69
69
|
loadSettings();
|
|
70
|
-
|
|
70
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
71
|
+
}, []);
|
|
71
72
|
|
|
72
73
|
const loadModels = useCallback(async () => {
|
|
73
74
|
setModelsLoading(true);
|
|
@@ -104,28 +105,29 @@ export default function AdminAI() {
|
|
|
104
105
|
? modelOptions
|
|
105
106
|
: [{ value: settings.ai_model, label: settings.ai_model }, ...modelOptions];
|
|
106
107
|
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
}
|
|
128
|
-
|
|
108
|
+
const saveSettings = useCallback(
|
|
109
|
+
async (payload: { ai_enabled?: boolean; ai_provider?: string; ai_model?: string; ai_api_key?: string }) => {
|
|
110
|
+
try {
|
|
111
|
+
const body = adminAiOnlyToggle
|
|
112
|
+
? { ai_enabled: payload.ai_enabled ?? settings.ai_enabled }
|
|
113
|
+
: {
|
|
114
|
+
...(payload.ai_enabled !== undefined && { ai_enabled: payload.ai_enabled }),
|
|
115
|
+
...(payload.ai_provider !== undefined && { ai_provider: payload.ai_provider }),
|
|
116
|
+
...(payload.ai_model !== undefined && { ai_model: payload.ai_model }),
|
|
117
|
+
...(payload.ai_api_key !== undefined && payload.ai_api_key !== '' && { ai_api_key: payload.ai_api_key }),
|
|
118
|
+
};
|
|
119
|
+
if (adminAiOnlyToggle && payload.ai_enabled === undefined) return;
|
|
120
|
+
if (!adminAiOnlyToggle && Object.keys(body).length === 0) return;
|
|
121
|
+
await api.post('/admin/settings/ai', body);
|
|
122
|
+
showToast(t('common.success'), 'success');
|
|
123
|
+
await loadSettings();
|
|
124
|
+
} catch (err: unknown) {
|
|
125
|
+
const e = err as { response?: { data?: { error?: string } } };
|
|
126
|
+
showToast(e?.response?.data?.error || t('common.error'), 'error');
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
[adminAiOnlyToggle, settings.ai_enabled, showToast, t, loadSettings]
|
|
130
|
+
);
|
|
129
131
|
|
|
130
132
|
if (loading) {
|
|
131
133
|
return <PageLoadingSkeleton lines={8} />;
|
|
@@ -146,14 +148,15 @@ export default function AdminAI() {
|
|
|
146
148
|
</div>
|
|
147
149
|
</div>
|
|
148
150
|
|
|
149
|
-
<
|
|
151
|
+
<div className="space-y-4">
|
|
150
152
|
<div className="flex items-center gap-3">
|
|
151
153
|
<Switch
|
|
152
154
|
id="ai-enabled"
|
|
153
155
|
checked={settings.ai_enabled}
|
|
154
|
-
onCheckedChange={(checked) =>
|
|
155
|
-
setSettings({ ...
|
|
156
|
-
|
|
156
|
+
onCheckedChange={(checked) => {
|
|
157
|
+
setSettings((s) => ({ ...s, ai_enabled: checked }));
|
|
158
|
+
saveSettings({ ai_enabled: checked });
|
|
159
|
+
}}
|
|
157
160
|
/>
|
|
158
161
|
<Label htmlFor="ai-enabled" className="text-sm font-medium text-gray-900 dark:text-white cursor-pointer">
|
|
159
162
|
{t('admin.ai.enabled')}
|
|
@@ -168,7 +171,10 @@ export default function AdminAI() {
|
|
|
168
171
|
</Label>
|
|
169
172
|
<Select
|
|
170
173
|
value={settings.ai_provider}
|
|
171
|
-
onChange={(value) =>
|
|
174
|
+
onChange={(value) => {
|
|
175
|
+
setSettings((s) => ({ ...s, ai_provider: value }));
|
|
176
|
+
saveSettings({ ai_provider: value });
|
|
177
|
+
}}
|
|
172
178
|
options={providerOptions}
|
|
173
179
|
className="max-w-xs"
|
|
174
180
|
/>
|
|
@@ -181,7 +187,12 @@ export default function AdminAI() {
|
|
|
181
187
|
<Input
|
|
182
188
|
type="password"
|
|
183
189
|
value={settings.ai_api_key}
|
|
184
|
-
onChange={(e) => setSettings({ ...
|
|
190
|
+
onChange={(e) => setSettings((s) => ({ ...s, ai_api_key: e.target.value }))}
|
|
191
|
+
onBlur={() => {
|
|
192
|
+
if (settings.ai_api_key.trim() !== '') {
|
|
193
|
+
saveSettings({ ai_api_key: settings.ai_api_key });
|
|
194
|
+
}
|
|
195
|
+
}}
|
|
185
196
|
placeholder={settings.ai_api_key_set ? t('admin.ai.apiKeyPlaceholder') : 'sk-...'}
|
|
186
197
|
className="max-w-md font-mono text-sm"
|
|
187
198
|
/>
|
|
@@ -196,7 +207,10 @@ export default function AdminAI() {
|
|
|
196
207
|
</Label>
|
|
197
208
|
<Select
|
|
198
209
|
value={settings.ai_model}
|
|
199
|
-
onChange={(value) =>
|
|
210
|
+
onChange={(value) => {
|
|
211
|
+
setSettings((s) => ({ ...s, ai_model: value }));
|
|
212
|
+
saveSettings({ ai_model: value });
|
|
213
|
+
}}
|
|
200
214
|
options={modelOptionsWithCurrent}
|
|
201
215
|
placeholder={
|
|
202
216
|
!settings.ai_api_key_set
|
|
@@ -211,18 +225,7 @@ export default function AdminAI() {
|
|
|
211
225
|
</div>
|
|
212
226
|
</>
|
|
213
227
|
)}
|
|
214
|
-
|
|
215
|
-
<div className="pt-2">
|
|
216
|
-
<Button type="submit" variant="primary" disabled={saving}>
|
|
217
|
-
{saving ? t('common.loading') : (
|
|
218
|
-
<>
|
|
219
|
-
<Save className="h-4 w-4 mr-2 inline" />
|
|
220
|
-
{t('common.save')}
|
|
221
|
-
</>
|
|
222
|
-
)}
|
|
223
|
-
</Button>
|
|
224
|
-
</div>
|
|
225
|
-
</form>
|
|
228
|
+
</div>
|
|
226
229
|
</div>
|
|
227
230
|
</div>
|
|
228
231
|
);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { useCallback } from 'react';
|
|
1
2
|
import { toast as sonnerToast } from 'sonner';
|
|
2
3
|
import { Toaster } from './sonner';
|
|
3
4
|
|
|
@@ -11,28 +12,27 @@ export interface Toast {
|
|
|
11
12
|
}
|
|
12
13
|
|
|
13
14
|
export function useToast() {
|
|
14
|
-
const showToast = (
|
|
15
|
-
message: string,
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
};
|
|
15
|
+
const showToast = useCallback(
|
|
16
|
+
(message: string, variant: ToastVariant = 'info', duration = 3000) => {
|
|
17
|
+
const options = duration > 0 ? { duration } : { duration: Infinity };
|
|
18
|
+
switch (variant) {
|
|
19
|
+
case 'success':
|
|
20
|
+
sonnerToast.success(message, options);
|
|
21
|
+
break;
|
|
22
|
+
case 'error':
|
|
23
|
+
sonnerToast.error(message, options);
|
|
24
|
+
break;
|
|
25
|
+
case 'warning':
|
|
26
|
+
sonnerToast.warning(message, options);
|
|
27
|
+
break;
|
|
28
|
+
case 'info':
|
|
29
|
+
default:
|
|
30
|
+
sonnerToast.info(message, options);
|
|
31
|
+
break;
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
[]
|
|
35
|
+
);
|
|
36
36
|
|
|
37
37
|
return { showToast };
|
|
38
38
|
}
|