feat: add settings to chose timzone and add markdown readability on main page

This commit is contained in:
2026-04-20 08:22:47 +02:00
parent 71513ea62c
commit 8a0edc3d59
7 changed files with 87 additions and 16 deletions

View File

@ -126,17 +126,28 @@ function ContextPanel({
// ── Summary content renderer ────────────────────────────────────────────────
function renderInline(text: string): React.ReactNode {
const parts = text.split(/(\*\*[^*]+\*\*)/g)
return parts.map((part, i) =>
part.startsWith('**') && part.endsWith('**')
? <strong key={i}>{part.slice(2, -2)}</strong>
: part
)
}
function SummaryContent({ content }: { content: string }) {
const lines = content.split('\n')
return (
<div className="space-y-2 text-sm leading-relaxed select-text">
<div className="space-y-1 text-sm leading-relaxed select-text">
{lines.map((line, i) => {
if (line.startsWith('## ')) return <h2 key={i} className="text-base font-semibold mt-4 first:mt-0">{line.slice(3)}</h2>
if (line.startsWith('### ')) return <h3 key={i} className="font-medium mt-3">{line.slice(4)}</h3>
if (line.startsWith('- ')) return <li key={i} className="ml-4 text-muted-foreground">{line.slice(2)}</li>
if (line.startsWith('**') && line.endsWith('**')) return <p key={i} className="font-semibold">{line.slice(2, -2)}</p>
if (line.trim() === '') return <div key={i} className="h-1" />
return <p key={i} className="text-muted-foreground">{line}</p>
if (line.startsWith('##### ')) return <h5 key={i} className="text-xs font-semibold uppercase tracking-wide text-muted-foreground mt-3">{renderInline(line.slice(6))}</h5>
if (line.startsWith('#### ')) return <h4 key={i} className="text-sm font-semibold mt-3">{renderInline(line.slice(5))}</h4>
if (line.startsWith('### ')) return <h3 key={i} className="text-sm font-semibold text-primary mt-4">{renderInline(line.slice(4))}</h3>
if (line.startsWith('## ')) return <h2 key={i} className="text-base font-bold mt-5 first:mt-0 border-b pb-1">{renderInline(line.slice(3))}</h2>
if (line.startsWith('# ')) return <h1 key={i} className="text-lg font-bold mt-5 first:mt-0">{renderInline(line.slice(2))}</h1>
if (line.startsWith('- ') || line.startsWith('* ')) return <li key={i} className="ml-4 text-muted-foreground list-disc">{renderInline(line.slice(2))}</li>
if (line.trim() === '') return <div key={i} className="h-2" />
return <p key={i} className="text-muted-foreground">{renderInline(line)}</p>
})}
</div>
)

View File

@ -8,19 +8,26 @@ import { Label } from '@/components/ui/label'
import { Spinner } from '@/components/ui/spinner'
const NUMERIC_SETTINGS: Record<string, { label: string; description: string }> = {
scrape_interval_minutes: { label: 'Intervalle de scraping (minutes)', description: 'Fréquence de récupération des actualités' },
articles_lookback_hours: { label: 'Fenêtre d\'analyse (heures)', description: 'Période couverte pour les résumés IA' },
summary_max_articles: { label: 'Articles max par résumé', description: 'Nombre maximum d\'articles envoyés à l\'IA' },
}
const COMMON_TIMEZONES = [
'Europe/Paris', 'Europe/London', 'Europe/Berlin', 'Europe/Madrid', 'Europe/Rome',
'America/New_York', 'America/Chicago', 'America/Los_Angeles',
'Asia/Tokyo', 'Asia/Hong_Kong', 'Asia/Singapore',
'UTC',
]
export function AdminSettings() {
const [settings, setSettings] = useState<Setting[]>([])
const [loading, setLoading] = useState(true)
const [values, setValues] = useState<Record<string, string>>({})
const [defaultPrompt, setDefaultPrompt] = useState('')
const [saving, setSaving] = useState(false)
const [savingTz, setSavingTz] = useState(false)
const [savingPrompt, setSavingPrompt] = useState(false)
const [saved, setSaved] = useState(false)
const [savedTz, setSavedTz] = useState(false)
const [savedPrompt, setSavedPrompt] = useState(false)
useEffect(() => { load() }, [])
@ -53,6 +60,13 @@ export function AdminSettings() {
setTimeout(() => setSaved(false), 2000)
}
async function saveTimezone() {
setSavingTz(true); setSavedTz(false)
await adminApi.updateSettings([{ key: 'timezone', value: values['timezone'] ?? 'Europe/Paris' }])
setSavingTz(false); setSavedTz(true)
setTimeout(() => setSavedTz(false), 2000)
}
async function savePrompt() {
setSavingPrompt(true); setSavedPrompt(false)
await adminApi.updateSettings([{ key: 'ai_system_prompt', value: values['ai_system_prompt'] ?? '' }])
@ -75,6 +89,35 @@ export function AdminSettings() {
<p className="text-muted-foreground text-sm">Configuration globale du service</p>
</div>
{/* Fuseau horaire */}
<Card>
<CardHeader><CardTitle>Fuseau horaire</CardTitle></CardHeader>
<CardContent className="space-y-4">
<div className="space-y-1">
<Label>Timezone (IANA)</Label>
<p className="text-xs text-muted-foreground">
Utilisé pour le planning de scraping et l'horodatage des résumés
</p>
<div className="flex gap-2 max-w-sm">
<Input
list="tz-list"
value={values['timezone'] ?? 'Europe/Paris'}
onChange={e => setValues(v => ({ ...v, timezone: e.target.value }))}
placeholder="Europe/Paris"
/>
<datalist id="tz-list">
{COMMON_TIMEZONES.map(tz => <option key={tz} value={tz} />)}
</datalist>
</div>
</div>
<Button onClick={saveTimezone} disabled={savingTz}>
{savingTz ? <Spinner className="h-4 w-4" /> : <Save className="h-4 w-4" />}
{savedTz ? 'Enregistré !' : 'Enregistrer'}
</Button>
</CardContent>
</Card>
{/* Paramètres généraux */}
<Card>
<CardHeader><CardTitle>Paramètres généraux</CardTitle></CardHeader>
<CardContent className="space-y-6">
@ -101,6 +144,7 @@ export function AdminSettings() {
</CardContent>
</Card>
{/* Contexte IA */}
<Card>
<CardHeader>
<CardTitle>Contexte IA</CardTitle>