Settings.jsx
import * as React from 'react';
import { useState, useEffect } from 'react';
import { apiGet, apiPatch } from '../hooks/useAPI.js';
import { useChat } from '../layouts/AdminLayout.jsx';
export function Settings() {
const { toggleChat } = useChat() || {};
const [settings, setSettings] = useState(null);
const [loading, setLoading] = useState(true);
const [savingSite, setSavingSite] = useState(false);
const [savingAI, setSavingAI] = useState(false);
const [provider, setProvider] = useState('anthropic');
const [siteSuccess, setSiteSuccess] = useState(false);
const [aiSuccess, setAISuccess] = useState(false);
useEffect(() => {
apiGet('/api/admin/settings')
.then((data) => {
setSettings(data);
setProvider(data?.aiProvider || 'anthropic');
})
.finally(() => setLoading(false));
}, []);
const handleSiteSubmit = async (e) => {
e.preventDefault();
const form = e.target;
const formData = new FormData(form);
setSavingSite(true);
setSiteSuccess(false);
try {
const updated = await apiPatch('/api/admin/settings', {
siteName: formData.get('site_name'),
siteDescription: formData.get('site_description'),
});
setSettings((prev) => ({ ...prev, ...updated }));
setSiteSuccess(true);
setTimeout(() => setSiteSuccess(false), 2000);
} finally {
setSavingSite(false);
}
};
const handleAISubmit = async (e) => {
e.preventDefault();
const form = e.target;
const formData = new FormData(form);
setSavingAI(true);
setAISuccess(false);
try {
const updated = await apiPatch('/api/admin/settings', {
aiProvider: formData.get('ai_provider'),
aiModel: formData.get('ai_model'),
aiApiKey: formData.get('ai_api_key'),
});
setSettings((prev) => ({ ...prev, ...updated }));
setAISuccess(true);
setTimeout(() => setAISuccess(false), 2000);
form.querySelector('input[name="ai_api_key"]').value = '';
} finally {
setSavingAI(false);
}
};
const handleProviderChange = (e) => {
setProvider(e.target.value);
};
const getModelOptions = () => {
if (provider === 'openai') {
return [
{ value: 'gpt-4o', label: 'GPT-4o (Recommended)' },
{ value: 'gpt-4o-mini', label: 'GPT-4o Mini' },
{ value: 'gpt-4-turbo', label: 'GPT-4 Turbo' },
];
}
return [
{ value: 'claude-sonnet-4-5-20250929', label: 'Claude Sonnet 4.5 (Recommended)' },
{ value: 'claude-opus-4-5-20251101', label: 'Claude Opus 4.5' },
];
};
// AI is configured if provider is set (not mock) and has an API key
const aiConfigured = settings?.aiProvider && settings?.aiProvider !== 'mock' && settings?.hasApiKey;
if (loading) {
return (
<div className="flex flex-col h-full">
<div className="flex items-center justify-between px-4 py-3 border-b border-base-300 bg-base-200/50">
<div>
<h2 className="font-semibold">Settings</h2>
<p className="text-xs text-base-content/50">Site configuration</p>
</div>
</div>
<div className="flex-1 flex justify-center items-center">
<span className="loading loading-spinner loading-lg" />
</div>
</div>
);
}
return (
<div className="flex flex-col h-full">
{/* Header */}
<div className="flex items-center justify-between px-4 py-3 border-b border-base-300 bg-base-200/50">
<div>
<h2 className="font-semibold">Settings</h2>
<p className="text-xs text-base-content/50">Site configuration</p>
</div>
<div className="flex items-center gap-2">
{toggleChat && (
<button onClick={toggleChat} className="btn btn-soft btn-primary btn-sm gap-2">
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z" />
</svg>
AI Assistant
</button>
)}
</div>
</div>
{/* Content */}
<div className="flex-1 overflow-auto p-6">
<div className="max-w-2xl mx-auto space-y-6">
{/* Site Settings */}
<form onSubmit={handleSiteSubmit} className="card bg-base-200/50 border border-base-300">
<div className="card-body">
<h3 className="card-title text-lg">Site</h3>
<p className="text-sm text-base-content/50 -mt-2 mb-4">Basic information about your website</p>
<div className="space-y-4">
<div className="form-control">
<label className="floating-label">
<span>Site Name</span>
<input
type="text"
name="site_name"
placeholder="My Website"
className="input input-bordered w-full"
defaultValue={settings?.siteName || ''}
/>
</label>
<label className="label">
<span className="label-text-alt text-base-content/50">Displayed in the browser tab and search results</span>
</label>
</div>
<div className="form-control">
<label className="floating-label">
<span>Site Description</span>
<input
type="text"
name="site_description"
placeholder="A brief description of your website"
className="input input-bordered w-full"
defaultValue={settings?.siteDescription || ''}
/>
</label>
<label className="label">
<span className="label-text-alt text-base-content/50">Used for SEO and social sharing</span>
</label>
</div>
</div>
<div className="card-actions justify-end mt-4">
{siteSuccess && (
<span className="text-success text-sm flex items-center gap-1">
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M5 13l4 4L19 7" />
</svg>
Saved
</span>
)}
<button type="submit" className="btn btn-primary btn-sm" disabled={savingSite}>
{savingSite ? <span className="loading loading-spinner loading-sm" /> : 'Save'}
</button>
</div>
</div>
</form>
{/* AI Settings */}
<form onSubmit={handleAISubmit} className="card bg-base-200/50 border border-base-300">
<div className="card-body">
<div className="flex items-center gap-2">
<h3 className="card-title text-lg">AI Assistant</h3>
{aiConfigured ? (
<span className="badge badge-soft badge-success badge-sm">Connected</span>
) : (
<span className="badge badge-soft badge-warning badge-sm">Not configured</span>
)}
</div>
<p className="text-sm text-base-content/50 -mt-2 mb-4">Configure the AI that powers your assistant</p>
<div className="space-y-4">
<div className="form-control">
<label className="label">
<span className="label-text">Provider</span>
</label>
<select
name="ai_provider"
className="select select-bordered w-full"
value={provider}
onChange={handleProviderChange}
>
<option value="anthropic">Anthropic (Claude)</option>
<option value="openai">OpenAI (GPT)</option>
<option value="mock">Mock (Testing)</option>
</select>
</div>
{provider !== 'mock' && (
<div className="form-control">
<label className="label">
<span className="label-text">Model</span>
</label>
<select
name="ai_model"
className="select select-bordered w-full"
defaultValue={settings?.aiModel || getModelOptions()[0].value}
>
{getModelOptions().map((model) => (
<option key={model.value} value={model.value}>{model.label}</option>
))}
</select>
</div>
)}
{provider !== 'mock' && (
<div className="form-control">
<label className="floating-label">
<span>API Key</span>
<input
type="password"
name="ai_api_key"
placeholder={aiConfigured ? '••••••••••••••••' : 'sk-...'}
className="input input-bordered w-full"
/>
</label>
<label className="label">
<span className="label-text-alt text-base-content/50">
{aiConfigured ? 'Leave empty to keep current key' : 'Get your API key from the provider dashboard'}
</span>
</label>
</div>
)}
</div>
<div className="card-actions justify-end mt-4">
{aiSuccess && (
<span className="text-success text-sm flex items-center gap-1">
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M5 13l4 4L19 7" />
</svg>
Saved
</span>
)}
<button type="submit" className="btn btn-primary btn-sm" disabled={savingAI}>
{savingAI ? <span className="loading loading-spinner loading-sm" /> : 'Save'}
</button>
</div>
</div>
</form>
{/* Data Management */}
<div className="card bg-base-200/50 border border-base-300">
<div className="card-body">
<h3 className="card-title text-lg">Data</h3>
<p className="text-sm text-base-content/50 -mt-2 mb-4">Backup and restore your site</p>
<div className="flex items-center justify-between p-4 bg-base-300/50 rounded-lg">
<div>
<p className="font-medium">Download Backup</p>
<p className="text-sm text-base-content/50">Export all pages, partials, collections, and settings as JSON</p>
</div>
<a href="/admin/backup" className="btn btn-primary btn-sm gap-2" download>
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
</svg>
Download
</a>
</div>
</div>
</div>
</div>
</div>
</div>
);
}