mirror of
https://github.com/R0m1k3/noteflow.git
synced 2026-10-11 17:29:37 +02:00
Feature: Amélioration système RSS et OpenRouter
Backend: - Récupération des vrais modèles depuis l'API OpenRouter - Table rss_summaries pour stocker les résumés générés - Limitation à 5 articles RSS au lieu de 10 - Route GET /api/rss/summaries pour récupérer les résumés - Sauvegarde automatique des résumés en base de données Frontend: - Affichage conditionnel: résumés si activé, sinon articles - Fonction renderRssSummaries() avec conversion Markdown→HTML - Formatage du résumé (titres, listes, gras, italique) - CSS complet pour .rss-summary avec typography - Bouton résumer: crée note ou recharge bloc selon config Comportement: - Si rss_summary_enabled=1: affiche les 5 derniers résumés - Sinon: affiche les 5 derniers articles - Résumés formatés avec Markdown dans le bloc RSS
This commit is contained in:
4 files changed
+236
-36
No files matched your search
@@ -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
|
// 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_notes_user ON notes(user_id)');
|
||||||
db.run('CREATE INDEX IF NOT EXISTS idx_note_todos ON note_todos(note_id)');
|
db.run('CREATE INDEX IF NOT EXISTS idx_note_todos ON note_todos(note_id)');
|
||||||
|
|||||||
@@ -484,6 +484,73 @@ body {
|
|||||||
overflow: hidden;
|
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 */
|
/* Responsive */
|
||||||
@media (max-width: 1200px) {
|
@media (max-width: 1200px) {
|
||||||
.content-container {
|
.content-container {
|
||||||
|
|||||||
+84
-12
@@ -1486,8 +1486,19 @@ async function fetchRssArticles() {
|
|||||||
|
|
||||||
async function loadRssArticles() {
|
async function loadRssArticles() {
|
||||||
try {
|
try {
|
||||||
const articles = await api.get('/api/rss/articles');
|
// Vérifier si les résumés sont activés
|
||||||
renderRssArticles(articles);
|
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) {
|
} catch (error) {
|
||||||
console.error('Erreur chargement articles RSS:', error);
|
console.error('Erreur chargement articles RSS:', error);
|
||||||
}
|
}
|
||||||
@@ -1514,6 +1525,57 @@ function renderRssArticles(articles) {
|
|||||||
`).join('');
|
`).join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderRssSummaries(summaries) {
|
||||||
|
const container = document.getElementById('rssArticlesList');
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
if (!summaries || summaries.length === 0) {
|
||||||
|
container.innerHTML = '<p class="rss-empty">Aucun résumé pour le moment</p>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
container.innerHTML = summaries.map(summary => `
|
||||||
|
<div class="rss-summary">
|
||||||
|
<div class="rss-summary-header">
|
||||||
|
<div class="rss-summary-title">📰 Résumé ${new Date(summary.created_at).toLocaleDateString('fr-FR')}</div>
|
||||||
|
<div class="rss-summary-meta">
|
||||||
|
<span>${summary.articles_count} articles</span>
|
||||||
|
${summary.model ? `<span>${escapeHtml(summary.model.split('/')[1] || summary.model)}</span>` : ''}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="rss-summary-content">${formatSummary(summary.summary)}</div>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatSummary(summary) {
|
||||||
|
// Convertir le Markdown de base en HTML
|
||||||
|
let html = escapeHtml(summary);
|
||||||
|
|
||||||
|
// Titres
|
||||||
|
html = html.replace(/\n## (.*?)\n/g, '<h3>$1</h3>');
|
||||||
|
html = html.replace(/\n# (.*?)\n/g, '<h2>$1</h2>');
|
||||||
|
|
||||||
|
// Listes à puces
|
||||||
|
html = html.replace(/\n- (.*?)(?=\n|$)/g, '<li>$1</li>');
|
||||||
|
html = html.replace(/(<li>.*<\/li>)/s, '<ul>$1</ul>');
|
||||||
|
|
||||||
|
// Listes numérotées
|
||||||
|
html = html.replace(/\n\d+\. (.*?)(?=\n|$)/g, '<li>$1</li>');
|
||||||
|
|
||||||
|
// Gras
|
||||||
|
html = html.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
|
||||||
|
|
||||||
|
// Italique
|
||||||
|
html = html.replace(/\*(.*?)\*/g, '<em>$1</em>');
|
||||||
|
|
||||||
|
// Sauts de ligne
|
||||||
|
html = html.replace(/\n\n/g, '<br><br>');
|
||||||
|
html = html.replace(/\n/g, '<br>');
|
||||||
|
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
async function summarizeRss() {
|
async function summarizeRss() {
|
||||||
const btn = document.getElementById('summarizeRssBtn');
|
const btn = document.getElementById('summarizeRssBtn');
|
||||||
if (!btn) return;
|
if (!btn) return;
|
||||||
@@ -1525,18 +1587,28 @@ async function summarizeRss() {
|
|||||||
try {
|
try {
|
||||||
const result = await api.post('/api/rss/summarize', {});
|
const result = await api.post('/api/rss/summarize', {});
|
||||||
|
|
||||||
// Create a new note with the summary
|
// Vérifier si les résumés sont activés
|
||||||
const newNote = await api.post('/api/notes', {
|
const settings = await api.get('/api/settings');
|
||||||
title: `📰 Résumé RSS - ${new Date().toLocaleDateString('fr-FR')}`,
|
const summaryEnabled = settings.rss_summary_enabled === '1' || settings.rss_summary_enabled === 'true';
|
||||||
content: result.summary
|
|
||||||
});
|
|
||||||
|
|
||||||
alert('Résumé généré et sauvegardé dans une nouvelle note !');
|
if (summaryEnabled) {
|
||||||
await loadNotes();
|
// 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
|
alert('Résumé généré et sauvegardé dans une nouvelle note !');
|
||||||
state.expandedNoteId = newNote.id;
|
await loadNotes();
|
||||||
renderNotes();
|
|
||||||
|
// Expand the new note
|
||||||
|
state.expandedNoteId = newNote.id;
|
||||||
|
renderNotes();
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Erreur génération résumé RSS:', error);
|
console.error('Erreur génération résumé RSS:', error);
|
||||||
if (error.message && error.message.includes('400')) {
|
if (error.message && error.message.includes('400')) {
|
||||||
|
|||||||
+74
-24
@@ -170,7 +170,7 @@ router.post('/fetch', requireAdmin, async (req, res) => {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* GET /api/rss/articles
|
* GET /api/rss/articles
|
||||||
* Récupérer les 10 derniers articles
|
* Récupérer les 5 derniers articles
|
||||||
*/
|
*/
|
||||||
router.get('/articles', async (req, res) => {
|
router.get('/articles', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
@@ -181,7 +181,7 @@ router.get('/articles', async (req, res) => {
|
|||||||
FROM rss_articles a
|
FROM rss_articles a
|
||||||
JOIN rss_feeds f ON a.feed_id = f.id
|
JOIN rss_feeds f ON a.feed_id = f.id
|
||||||
ORDER BY a.pub_date DESC
|
ORDER BY a.pub_date DESC
|
||||||
LIMIT 10
|
LIMIT 5
|
||||||
`);
|
`);
|
||||||
|
|
||||||
res.json(articles || []);
|
res.json(articles || []);
|
||||||
@@ -193,7 +193,7 @@ router.get('/articles', async (req, res) => {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* POST /api/rss/summarize
|
* 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) => {
|
router.post('/summarize', requireAdmin, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
@@ -206,7 +206,7 @@ router.post('/summarize', requireAdmin, async (req, res) => {
|
|||||||
return res.status(400).json({ error: 'Clé API OpenRouter non configurée' });
|
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(`
|
const articles = await getAll(`
|
||||||
SELECT
|
SELECT
|
||||||
a.title, a.description, a.link, a.pub_date,
|
a.title, a.description, a.link, a.pub_date,
|
||||||
@@ -214,7 +214,7 @@ router.post('/summarize', requireAdmin, async (req, res) => {
|
|||||||
FROM rss_articles a
|
FROM rss_articles a
|
||||||
JOIN rss_feeds f ON a.feed_id = f.id
|
JOIN rss_feeds f ON a.feed_id = f.id
|
||||||
ORDER BY a.pub_date DESC
|
ORDER BY a.pub_date DESC
|
||||||
LIMIT 10
|
LIMIT 5
|
||||||
`);
|
`);
|
||||||
|
|
||||||
if (articles.length === 0) {
|
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;
|
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({
|
res.json({
|
||||||
summary,
|
summary,
|
||||||
@@ -295,30 +301,74 @@ Format de sortie en Markdown avec des sections claires.`;
|
|||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GET /api/rss/models
|
* GET /api/rss/summaries
|
||||||
* Récupérer la liste des modèles OpenRouter
|
* Récupérer les 5 derniers résumés
|
||||||
*/
|
*/
|
||||||
router.get('/models', requireAdmin, async (req, res) => {
|
router.get('/summaries', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
// Liste des modèles populaires pour le résumé
|
const summaries = await getAll(`
|
||||||
const models = [
|
SELECT id, summary, model, articles_count, created_at
|
||||||
{ id: 'openai/gpt-4-turbo', name: 'GPT-4 Turbo', provider: 'OpenAI' },
|
FROM rss_summaries
|
||||||
{ id: 'openai/gpt-4', name: 'GPT-4', provider: 'OpenAI' },
|
ORDER BY created_at DESC
|
||||||
{ id: 'openai/gpt-3.5-turbo', name: 'GPT-3.5 Turbo', provider: 'OpenAI' },
|
LIMIT 5
|
||||||
{ 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' }
|
|
||||||
];
|
|
||||||
|
|
||||||
res.json(models);
|
res.json(summaries || []);
|
||||||
} catch (error) {
|
} 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' });
|
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;
|
module.exports = router;
|
||||||
Reference in new issue
Block a user