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\")",
|
"Bash(fish -c \"which node\")",
|
||||||
"Read(//opt/**)",
|
"Read(//opt/**)",
|
||||||
"Bash(/home/anthony/.config/JetBrains/WebStorm2026.1/node/versions/24.14.1/bin/node node_modules/.bin/tsc --noEmit)",
|
"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
|
systemPrompt = DefaultSystemPrompt
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tz, _ := p.repo.GetSetting("timezone")
|
||||||
|
|
||||||
// Passe 2 : résumé complet
|
// Passe 2 : résumé complet
|
||||||
fmt.Printf("[pipeline] Passe 2 — résumé : génération sur %d articles…\n", len(articles))
|
fmt.Printf("[pipeline] Passe 2 — résumé : génération sur %d articles…\n", len(articles))
|
||||||
t2 := time.Now()
|
t2 := time.Now()
|
||||||
prompt := buildPrompt(systemPrompt, symbols, articles)
|
prompt := buildPrompt(systemPrompt, symbols, articles, tz)
|
||||||
summary, err := provider.Summarize(ctx, prompt)
|
summary, err := provider.Summarize(ctx, prompt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("AI summarize: %w", err)
|
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)
|
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
|
var sb strings.Builder
|
||||||
sb.WriteString(systemPrompt)
|
sb.WriteString(systemPrompt)
|
||||||
sb.WriteString("\n\n")
|
sb.WriteString("\n\n")
|
||||||
@ -275,7 +277,11 @@ func buildPrompt(systemPrompt string, symbols []string, articles []models.Articl
|
|||||||
sb.WriteString(strings.Join(symbols, ", "))
|
sb.WriteString(strings.Join(symbols, ", "))
|
||||||
sb.WriteString(".\n\n")
|
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("/think\n\n")
|
||||||
sb.WriteString("## Actualités\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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tz, _ := s.repo.GetSetting("timezone")
|
||||||
|
if tz == "" {
|
||||||
|
tz = "UTC"
|
||||||
|
}
|
||||||
|
|
||||||
for _, slot := range slots {
|
for _, slot := range slots {
|
||||||
// Format cron: "minute hour * * day_of_week"
|
// TZ= prefix permet à robfig/cron d'interpréter les heures dans le fuseau configuré
|
||||||
spec := fmt.Sprintf("%d %d * * %d", slot.Minute, slot.Hour, slot.DayOfWeek)
|
spec := fmt.Sprintf("TZ=%s %d %d * * %d", tz, slot.Minute, slot.Hour, slot.DayOfWeek)
|
||||||
id, err := s.cron.AddFunc(spec, s.run)
|
id, err := s.cron.AddFunc(spec, s.run)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("scheduler: invalid cron spec %q: %v\n", spec, err)
|
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)
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -126,17 +126,28 @@ function ContextPanel({
|
|||||||
|
|
||||||
// ── Summary content renderer ────────────────────────────────────────────────
|
// ── 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 }) {
|
function SummaryContent({ content }: { content: string }) {
|
||||||
const lines = content.split('\n')
|
const lines = content.split('\n')
|
||||||
return (
|
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) => {
|
{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 <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 <h3 key={i} className="font-medium mt-3">{line.slice(4)}</h3>
|
if (line.startsWith('#### ')) return <h4 key={i} className="text-sm font-semibold mt-3">{renderInline(line.slice(5))}</h4>
|
||||||
if (line.startsWith('- ')) return <li key={i} className="ml-4 text-muted-foreground">{line.slice(2)}</li>
|
if (line.startsWith('### ')) return <h3 key={i} className="text-sm font-semibold text-primary mt-4">{renderInline(line.slice(4))}</h3>
|
||||||
if (line.startsWith('**') && line.endsWith('**')) return <p key={i} className="font-semibold">{line.slice(2, -2)}</p>
|
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.trim() === '') return <div key={i} className="h-1" />
|
if (line.startsWith('# ')) return <h1 key={i} className="text-lg font-bold mt-5 first:mt-0">{renderInline(line.slice(2))}</h1>
|
||||||
return <p key={i} className="text-muted-foreground">{line}</p>
|
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>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -8,19 +8,26 @@ import { Label } from '@/components/ui/label'
|
|||||||
import { Spinner } from '@/components/ui/spinner'
|
import { Spinner } from '@/components/ui/spinner'
|
||||||
|
|
||||||
const NUMERIC_SETTINGS: Record<string, { label: string; description: string }> = {
|
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' },
|
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() {
|
export function AdminSettings() {
|
||||||
const [settings, setSettings] = useState<Setting[]>([])
|
const [settings, setSettings] = useState<Setting[]>([])
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [values, setValues] = useState<Record<string, string>>({})
|
const [values, setValues] = useState<Record<string, string>>({})
|
||||||
const [defaultPrompt, setDefaultPrompt] = useState('')
|
const [defaultPrompt, setDefaultPrompt] = useState('')
|
||||||
const [saving, setSaving] = useState(false)
|
const [saving, setSaving] = useState(false)
|
||||||
|
const [savingTz, setSavingTz] = useState(false)
|
||||||
const [savingPrompt, setSavingPrompt] = useState(false)
|
const [savingPrompt, setSavingPrompt] = useState(false)
|
||||||
const [saved, setSaved] = useState(false)
|
const [saved, setSaved] = useState(false)
|
||||||
|
const [savedTz, setSavedTz] = useState(false)
|
||||||
const [savedPrompt, setSavedPrompt] = useState(false)
|
const [savedPrompt, setSavedPrompt] = useState(false)
|
||||||
|
|
||||||
useEffect(() => { load() }, [])
|
useEffect(() => { load() }, [])
|
||||||
@ -53,6 +60,13 @@ export function AdminSettings() {
|
|||||||
setTimeout(() => setSaved(false), 2000)
|
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() {
|
async function savePrompt() {
|
||||||
setSavingPrompt(true); setSavedPrompt(false)
|
setSavingPrompt(true); setSavedPrompt(false)
|
||||||
await adminApi.updateSettings([{ key: 'ai_system_prompt', value: values['ai_system_prompt'] ?? '' }])
|
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>
|
<p className="text-muted-foreground text-sm">Configuration globale du service</p>
|
||||||
</div>
|
</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>
|
<Card>
|
||||||
<CardHeader><CardTitle>Paramètres généraux</CardTitle></CardHeader>
|
<CardHeader><CardTitle>Paramètres généraux</CardTitle></CardHeader>
|
||||||
<CardContent className="space-y-6">
|
<CardContent className="space-y-6">
|
||||||
@ -101,6 +144,7 @@ export function AdminSettings() {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
{/* Contexte IA */}
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Contexte IA</CardTitle>
|
<CardTitle>Contexte IA</CardTitle>
|
||||||
|
|||||||
Reference in New Issue
Block a user