diff --git a/config/database.js b/config/database.js
index ab552d7..e59f87b 100644
--- a/config/database.js
+++ b/config/database.js
@@ -126,6 +126,17 @@ function initDatabase() {
)
`);
+ // Table rss_summaries (résumés générés)
+ db.run(`
+ CREATE TABLE IF NOT EXISTS rss_summaries (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ summary TEXT NOT NULL,
+ model TEXT,
+ articles_count INTEGER DEFAULT 0,
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
+ )
+ `);
+
// Créer les index pour la performance
db.run('CREATE INDEX IF NOT EXISTS idx_notes_user ON notes(user_id)');
db.run('CREATE INDEX IF NOT EXISTS idx_note_todos ON note_todos(note_id)');
diff --git a/public/css/layout.css b/public/css/layout.css
index 81fb042..4225d62 100644
--- a/public/css/layout.css
+++ b/public/css/layout.css
@@ -484,6 +484,73 @@ body {
overflow: hidden;
}
+.rss-summary {
+ padding: var(--spacing-lg);
+ background: var(--color-background);
+ border-radius: var(--radius-md);
+ border: 1px solid var(--color-border);
+ margin-bottom: var(--spacing-md);
+}
+
+.rss-summary-header {
+ margin-bottom: var(--spacing-md);
+ padding-bottom: var(--spacing-sm);
+ border-bottom: 1px solid var(--color-border);
+}
+
+.rss-summary-title {
+ font-size: var(--font-size-base);
+ font-weight: var(--font-weight-semibold);
+ color: var(--color-text-primary);
+ margin-bottom: var(--spacing-xs);
+}
+
+.rss-summary-meta {
+ display: flex;
+ gap: var(--spacing-md);
+ font-size: var(--font-size-xs);
+ color: var(--color-text-secondary);
+}
+
+.rss-summary-content {
+ font-size: var(--font-size-sm);
+ color: var(--color-text-primary);
+ line-height: 1.6;
+}
+
+.rss-summary-content h2 {
+ font-size: var(--font-size-lg);
+ font-weight: var(--font-weight-bold);
+ color: var(--color-text-primary);
+ margin: var(--spacing-md) 0 var(--spacing-sm) 0;
+}
+
+.rss-summary-content h3 {
+ font-size: var(--font-size-base);
+ font-weight: var(--font-weight-semibold);
+ color: var(--color-text-primary);
+ margin: var(--spacing-sm) 0 var(--spacing-xs) 0;
+}
+
+.rss-summary-content ul,
+.rss-summary-content ol {
+ margin: var(--spacing-sm) 0;
+ padding-left: var(--spacing-xl);
+}
+
+.rss-summary-content li {
+ margin-bottom: var(--spacing-xs);
+}
+
+.rss-summary-content strong {
+ font-weight: var(--font-weight-semibold);
+ color: var(--color-text-primary);
+}
+
+.rss-summary-content em {
+ font-style: italic;
+}
+
/* Responsive */
@media (max-width: 1200px) {
.content-container {
diff --git a/public/js/complete-app.js b/public/js/complete-app.js
index ac1bd7e..ea42830 100644
--- a/public/js/complete-app.js
+++ b/public/js/complete-app.js
@@ -1486,8 +1486,19 @@ async function fetchRssArticles() {
async function loadRssArticles() {
try {
- const articles = await api.get('/api/rss/articles');
- renderRssArticles(articles);
+ // Vérifier si les résumés sont activés
+ const settings = await api.get('/api/settings');
+ const summaryEnabled = settings.rss_summary_enabled === '1' || settings.rss_summary_enabled === 'true';
+
+ if (summaryEnabled) {
+ // Charger les résumés
+ const summaries = await api.get('/api/rss/summaries');
+ renderRssSummaries(summaries);
+ } else {
+ // Charger les articles normaux
+ const articles = await api.get('/api/rss/articles');
+ renderRssArticles(articles);
+ }
} catch (error) {
console.error('Erreur chargement articles RSS:', error);
}
@@ -1514,6 +1525,57 @@ function renderRssArticles(articles) {
`).join('');
}
+function renderRssSummaries(summaries) {
+ const container = document.getElementById('rssArticlesList');
+ if (!container) return;
+
+ if (!summaries || summaries.length === 0) {
+ container.innerHTML = '
';
+ return;
+ }
+
+ container.innerHTML = summaries.map(summary => `
+
+ `).join('');
+}
+
+function formatSummary(summary) {
+ // Convertir le Markdown de base en HTML
+ let html = escapeHtml(summary);
+
+ // Titres
+ html = html.replace(/\n## (.*?)\n/g, '$1
');
+ html = html.replace(/\n# (.*?)\n/g, '$1
');
+
+ // Listes à puces
+ html = html.replace(/\n- (.*?)(?=\n|$)/g, '$1');
+ html = html.replace(/(.*<\/li>)/s, '');
+
+ // Listes numérotées
+ html = html.replace(/\n\d+\. (.*?)(?=\n|$)/g, '$1');
+
+ // Gras
+ html = html.replace(/\*\*(.*?)\*\*/g, '$1');
+
+ // Italique
+ html = html.replace(/\*(.*?)\*/g, '$1');
+
+ // Sauts de ligne
+ html = html.replace(/\n\n/g, '
');
+ html = html.replace(/\n/g, '
');
+
+ return html;
+}
+
async function summarizeRss() {
const btn = document.getElementById('summarizeRssBtn');
if (!btn) return;
@@ -1525,18 +1587,28 @@ async function summarizeRss() {
try {
const result = await api.post('/api/rss/summarize', {});
- // Create a new note with the summary
- const newNote = await api.post('/api/notes', {
- title: `📰 Résumé RSS - ${new Date().toLocaleDateString('fr-FR')}`,
- content: result.summary
- });
+ // Vérifier si les résumés sont activés
+ const settings = await api.get('/api/settings');
+ const summaryEnabled = settings.rss_summary_enabled === '1' || settings.rss_summary_enabled === 'true';
- alert('Résumé généré et sauvegardé dans une nouvelle note !');
- await loadNotes();
+ if (summaryEnabled) {
+ // Si les résumés sont activés, juste recharger le bloc RSS
+ alert('Résumé généré avec succès !');
+ await loadRssArticles();
+ } else {
+ // Sinon créer une note avec le résumé
+ const newNote = await api.post('/api/notes', {
+ title: `📰 Résumé RSS - ${new Date().toLocaleDateString('fr-FR')}`,
+ content: result.summary
+ });
- // Expand the new note
- state.expandedNoteId = newNote.id;
- renderNotes();
+ alert('Résumé généré et sauvegardé dans une nouvelle note !');
+ await loadNotes();
+
+ // Expand the new note
+ state.expandedNoteId = newNote.id;
+ renderNotes();
+ }
} catch (error) {
console.error('Erreur génération résumé RSS:', error);
if (error.message && error.message.includes('400')) {
diff --git a/routes/rss.routes.js b/routes/rss.routes.js
index a882668..bd4e138 100644
--- a/routes/rss.routes.js
+++ b/routes/rss.routes.js
@@ -170,7 +170,7 @@ router.post('/fetch', requireAdmin, async (req, res) => {
/**
* GET /api/rss/articles
- * Récupérer les 10 derniers articles
+ * Récupérer les 5 derniers articles
*/
router.get('/articles', async (req, res) => {
try {
@@ -181,7 +181,7 @@ router.get('/articles', async (req, res) => {
FROM rss_articles a
JOIN rss_feeds f ON a.feed_id = f.id
ORDER BY a.pub_date DESC
- LIMIT 10
+ LIMIT 5
`);
res.json(articles || []);
@@ -193,7 +193,7 @@ router.get('/articles', async (req, res) => {
/**
* POST /api/rss/summarize
- * Générer un résumé des 10 derniers articles avec OpenRouter
+ * Générer un résumé des 5 derniers articles avec OpenRouter
*/
router.post('/summarize', requireAdmin, async (req, res) => {
try {
@@ -206,7 +206,7 @@ router.post('/summarize', requireAdmin, async (req, res) => {
return res.status(400).json({ error: 'Clé API OpenRouter non configurée' });
}
- // Récupérer les 10 derniers articles
+ // Récupérer les 5 derniers articles
const articles = await getAll(`
SELECT
a.title, a.description, a.link, a.pub_date,
@@ -214,7 +214,7 @@ router.post('/summarize', requireAdmin, async (req, res) => {
FROM rss_articles a
JOIN rss_feeds f ON a.feed_id = f.id
ORDER BY a.pub_date DESC
- LIMIT 10
+ LIMIT 5
`);
if (articles.length === 0) {
@@ -272,7 +272,13 @@ Format de sortie en Markdown avec des sections claires.`;
const summary = response.data.choices[0].message.content;
- logger.info('Résumé généré avec succès');
+ // Sauvegarder le résumé en base de données
+ await runQuery(
+ 'INSERT INTO rss_summaries (summary, model, articles_count) VALUES (?, ?, ?)',
+ [summary, selectedModel, articles.length]
+ );
+
+ logger.info('Résumé généré et sauvegardé avec succès');
res.json({
summary,
@@ -295,30 +301,74 @@ Format de sortie en Markdown avec des sections claires.`;
});
/**
- * GET /api/rss/models
- * Récupérer la liste des modèles OpenRouter
+ * GET /api/rss/summaries
+ * Récupérer les 5 derniers résumés
*/
-router.get('/models', requireAdmin, async (req, res) => {
+router.get('/summaries', async (req, res) => {
try {
- // Liste des modèles populaires pour le résumé
- const models = [
- { id: 'openai/gpt-4-turbo', name: 'GPT-4 Turbo', provider: 'OpenAI' },
- { id: 'openai/gpt-4', name: 'GPT-4', provider: 'OpenAI' },
- { id: 'openai/gpt-3.5-turbo', name: 'GPT-3.5 Turbo', provider: 'OpenAI' },
- { id: 'anthropic/claude-3-opus', name: 'Claude 3 Opus', provider: 'Anthropic' },
- { id: 'anthropic/claude-3-sonnet', name: 'Claude 3 Sonnet', provider: 'Anthropic' },
- { id: 'anthropic/claude-3-haiku', name: 'Claude 3 Haiku', provider: 'Anthropic' },
- { id: 'google/gemini-pro', name: 'Gemini Pro', provider: 'Google' },
- { id: 'meta-llama/llama-3-70b-instruct', name: 'Llama 3 70B', provider: 'Meta' },
- { id: 'mistralai/mistral-large', name: 'Mistral Large', provider: 'Mistral AI' },
- { id: 'mistralai/mixtral-8x7b-instruct', name: 'Mixtral 8x7B', provider: 'Mistral AI' }
- ];
+ const summaries = await getAll(`
+ SELECT id, summary, model, articles_count, created_at
+ FROM rss_summaries
+ ORDER BY created_at DESC
+ LIMIT 5
+ `);
- res.json(models);
+ res.json(summaries || []);
} catch (error) {
- logger.error('Erreur lors de la récupération des modèles:', error);
+ logger.error('Erreur lors de la récupération des résumés:', error);
res.status(500).json({ error: 'Erreur serveur' });
}
});
+/**
+ * GET /api/rss/models
+ * Récupérer la liste des modèles OpenRouter depuis l'API
+ */
+router.get('/models', requireAdmin, async (req, res) => {
+ try {
+ // Récupérer les modèles depuis l'API OpenRouter
+ const response = await axios.get('https://openrouter.ai/api/v1/models', {
+ headers: {
+ 'HTTP-Referer': 'https://noteflow.app',
+ 'X-Title': 'NoteFlow'
+ },
+ timeout: 10000
+ });
+
+ // Transformer les données en format simplifié
+ const models = response.data.data.map(model => ({
+ id: model.id,
+ name: model.name || model.id,
+ provider: model.id.split('/')[0] || 'Unknown',
+ context_length: model.context_length,
+ pricing: model.pricing
+ }));
+
+ // Trier par provider puis par nom
+ models.sort((a, b) => {
+ if (a.provider !== b.provider) {
+ return a.provider.localeCompare(b.provider);
+ }
+ return a.name.localeCompare(b.name);
+ });
+
+ res.json(models);
+ } catch (error) {
+ logger.error('Erreur lors de la récupération des modèles:', error);
+
+ // Fallback sur une liste de modèles populaires si l'API échoue
+ const fallbackModels = [
+ { id: 'openai/gpt-4-turbo', name: 'GPT-4 Turbo', provider: 'openai' },
+ { id: 'openai/gpt-3.5-turbo', name: 'GPT-3.5 Turbo', provider: 'openai' },
+ { id: 'anthropic/claude-3-opus', name: 'Claude 3 Opus', provider: 'anthropic' },
+ { id: 'anthropic/claude-3-sonnet', name: 'Claude 3 Sonnet', provider: 'anthropic' },
+ { id: 'google/gemini-pro', name: 'Gemini Pro', provider: 'google' },
+ { id: 'meta-llama/llama-3-70b-instruct', name: 'Llama 3 70B', provider: 'meta-llama' },
+ { id: 'mistralai/mistral-large', name: 'Mistral Large', provider: 'mistralai' }
+ ];
+
+ res.json(fallbackModels);
+ }
+});
+
module.exports = router;