feat: add settings to chose timzone and add markdown readability on main page
This commit is contained in:
@ -18,7 +18,9 @@
|
||||
"Bash(fish -c \"which node\")",
|
||||
"Read(//opt/**)",
|
||||
"Bash(/home/anthony/.config/JetBrains/WebStorm2026.1/node/versions/24.14.1/bin/node node_modules/.bin/tsc --noEmit)",
|
||||
"Bash(chmod +x /home/anthony/Documents/Projects/Tradarr/build-push.sh)"
|
||||
"Bash(chmod +x /home/anthony/Documents/Projects/Tradarr/build-push.sh)",
|
||||
"Bash(fish -c \"npm install react-markdown\")",
|
||||
"Bash(fish -c \"which npm; which pnpm; which bun\")"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@ -116,10 +116,12 @@ func (p *Pipeline) GenerateForUser(ctx context.Context, userID string) (*models.
|
||||
systemPrompt = DefaultSystemPrompt
|
||||
}
|
||||
|
||||
tz, _ := p.repo.GetSetting("timezone")
|
||||
|
||||
// Passe 2 : résumé complet
|
||||
fmt.Printf("[pipeline] Passe 2 — résumé : génération sur %d articles…\n", len(articles))
|
||||
t2 := time.Now()
|
||||
prompt := buildPrompt(systemPrompt, symbols, articles)
|
||||
prompt := buildPrompt(systemPrompt, symbols, articles, tz)
|
||||
summary, err := provider.Summarize(ctx, prompt)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("AI summarize: %w", err)
|
||||
@ -266,7 +268,7 @@ func (p *Pipeline) callProviderForReport(ctx context.Context, excerpt, question
|
||||
return provider.Summarize(ctx, prompt)
|
||||
}
|
||||
|
||||
func buildPrompt(systemPrompt string, symbols []string, articles []models.Article) string {
|
||||
func buildPrompt(systemPrompt string, symbols []string, articles []models.Article, tz string) string {
|
||||
var sb strings.Builder
|
||||
sb.WriteString(systemPrompt)
|
||||
sb.WriteString("\n\n")
|
||||
@ -275,7 +277,11 @@ func buildPrompt(systemPrompt string, symbols []string, articles []models.Articl
|
||||
sb.WriteString(strings.Join(symbols, ", "))
|
||||
sb.WriteString(".\n\n")
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf("Date d'analyse : %s\n\n", time.Now().Format("02/01/2006 15:04")))
|
||||
loc, err := time.LoadLocation(tz)
|
||||
if err != nil || tz == "" {
|
||||
loc = time.UTC
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf("Date d'analyse : %s\n\n", time.Now().In(loc).Format("02/01/2006 15:04")))
|
||||
sb.WriteString("/think\n\n")
|
||||
sb.WriteString("## Actualités\n\n")
|
||||
|
||||
|
||||
@ -0,0 +1 @@
|
||||
DELETE FROM settings WHERE key = 'timezone';
|
||||
@ -0,0 +1,2 @@
|
||||
INSERT INTO settings (key, value) VALUES ('timezone', 'Europe/Paris')
|
||||
ON CONFLICT (key) DO NOTHING;
|
||||
@ -57,9 +57,14 @@ func (s *Scheduler) loadSchedule() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
tz, _ := s.repo.GetSetting("timezone")
|
||||
if tz == "" {
|
||||
tz = "UTC"
|
||||
}
|
||||
|
||||
for _, slot := range slots {
|
||||
// Format cron: "minute hour * * day_of_week"
|
||||
spec := fmt.Sprintf("%d %d * * %d", slot.Minute, slot.Hour, slot.DayOfWeek)
|
||||
// TZ= prefix permet à robfig/cron d'interpréter les heures dans le fuseau configuré
|
||||
spec := fmt.Sprintf("TZ=%s %d %d * * %d", tz, slot.Minute, slot.Hour, slot.DayOfWeek)
|
||||
id, err := s.cron.AddFunc(spec, s.run)
|
||||
if err != nil {
|
||||
fmt.Printf("scheduler: invalid cron spec %q: %v\n", spec, err)
|
||||
@ -68,7 +73,7 @@ func (s *Scheduler) loadSchedule() error {
|
||||
s.entryIDs = append(s.entryIDs, id)
|
||||
}
|
||||
|
||||
fmt.Printf("scheduler: %d time slots loaded\n", len(s.entryIDs))
|
||||
fmt.Printf("scheduler: %d time slots loaded (timezone: %s)\n", len(s.entryIDs), tz)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@ -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>
|
||||
)
|
||||
|
||||
@ -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>
|
||||
|
||||
Reference in New Issue
Block a user