feat: add RSS account type - posts link directly without AI, update admin UI

This commit is contained in:
Patrick Britton 2026-02-08 19:32:57 -06:00
parent da5a366cc1
commit d8988dc870
2 changed files with 88 additions and 41 deletions

View file

@ -4,7 +4,7 @@ import AdminShell from '@/components/AdminShell';
import { api } from '@/lib/api'; import { api } from '@/lib/api';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { import {
Plus, Play, Eye, Trash2, Power, PowerOff, RefreshCw, Newspaper, Plus, Play, Eye, Trash2, Power, PowerOff, RefreshCw, Newspaper, Rss,
ChevronDown, ChevronUp, Bot, Clock, AlertCircle, CheckCircle, ExternalLink, ChevronDown, ChevronUp, Bot, Clock, AlertCircle, CheckCircle, ExternalLink,
} from 'lucide-react'; } from 'lucide-react';
@ -270,7 +270,7 @@ export default function OfficialAccountsPage() {
{/* Header */} {/* Header */}
<div className="p-4 flex items-center gap-4"> <div className="p-4 flex items-center gap-4">
<div className="w-10 h-10 bg-brand-100 rounded-full flex items-center justify-center flex-shrink-0"> <div className="w-10 h-10 bg-brand-100 rounded-full flex items-center justify-center flex-shrink-0">
{cfg.account_type === 'news' ? <Newspaper className="w-5 h-5 text-brand-600" /> : <Bot className="w-5 h-5 text-brand-600" />} {cfg.account_type === 'news' ? <Newspaper className="w-5 h-5 text-brand-600" /> : cfg.account_type === 'rss' ? <Rss className="w-5 h-5 text-brand-600" /> : <Bot className="w-5 h-5 text-brand-600" />}
</div> </div>
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
@ -344,7 +344,7 @@ export default function OfficialAccountsPage() {
</div> </div>
)} )}
{cfg.account_type === 'news' && sources.length > 0 && ( {(cfg.account_type === 'news' || cfg.account_type === 'rss') && sources.length > 0 && (
<div> <div>
<span className="text-sm font-medium text-gray-600">News Sources:</span> <span className="text-sm font-medium text-gray-600">News Sources:</span>
<div className="mt-1 space-y-1"> <div className="mt-1 space-y-1">
@ -396,6 +396,9 @@ function CreateAccountForm({ onDone, initialProfile }: { onDone: () => void; ini
if (accountType === 'news') { if (accountType === 'news') {
setNewsSources(DEFAULT_NEWS_SOURCES); setNewsSources(DEFAULT_NEWS_SOURCES);
setSystemPrompt(DEFAULT_NEWS_PROMPT); setSystemPrompt(DEFAULT_NEWS_PROMPT);
} else if (accountType === 'rss') {
setNewsSources(DEFAULT_NEWS_SOURCES);
setSystemPrompt('');
} else { } else {
setNewsSources([]); setNewsSources([]);
setSystemPrompt(DEFAULT_GENERAL_PROMPT); setSystemPrompt(DEFAULT_GENERAL_PROMPT);
@ -441,27 +444,34 @@ function CreateAccountForm({ onDone, initialProfile }: { onDone: () => void; ini
<select value={accountType} onChange={(e) => setAccountType(e.target.value)} <select value={accountType} onChange={(e) => setAccountType(e.target.value)}
className="w-full px-3 py-2 border border-warm-300 rounded-lg text-sm"> className="w-full px-3 py-2 border border-warm-300 rounded-lg text-sm">
<option value="general">General</option> <option value="general">General</option>
<option value="news">News</option> <option value="news">News (AI Commentary)</option>
<option value="rss">RSS (Link Only)</option>
<option value="community">Community</option> <option value="community">Community</option>
</select> </select>
</div> </div>
<div> {accountType !== 'rss' && (
<label className="block text-sm font-medium text-gray-700 mb-1">Model</label> <div>
<ModelSelector value={modelId} onChange={setModelId} /> <label className="block text-sm font-medium text-gray-700 mb-1">Model</label>
</div> <ModelSelector value={modelId} onChange={setModelId} />
</div>
)}
</div> </div>
<div className="grid grid-cols-4 gap-4 mb-4"> <div className="grid grid-cols-4 gap-4 mb-4">
<div> {accountType !== 'rss' && (
<label className="block text-sm font-medium text-gray-700 mb-1">Temperature</label> <>
<input type="number" step="0.1" min="0" max="2" value={temperature} onChange={(e) => setTemperature(Number(e.target.value))} <div>
className="w-full px-3 py-2 border border-warm-300 rounded-lg text-sm" /> <label className="block text-sm font-medium text-gray-700 mb-1">Temperature</label>
</div> <input type="number" step="0.1" min="0" max="2" value={temperature} onChange={(e) => setTemperature(Number(e.target.value))}
<div> className="w-full px-3 py-2 border border-warm-300 rounded-lg text-sm" />
<label className="block text-sm font-medium text-gray-700 mb-1">Max Tokens</label> </div>
<input type="number" min="50" max="4000" value={maxTokens} onChange={(e) => setMaxTokens(Number(e.target.value))} <div>
className="w-full px-3 py-2 border border-warm-300 rounded-lg text-sm" /> <label className="block text-sm font-medium text-gray-700 mb-1">Max Tokens</label>
</div> <input type="number" min="50" max="4000" value={maxTokens} onChange={(e) => setMaxTokens(Number(e.target.value))}
className="w-full px-3 py-2 border border-warm-300 rounded-lg text-sm" />
</div>
</>
)}
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-1">Interval (min)</label> <label className="block text-sm font-medium text-gray-700 mb-1">Interval (min)</label>
<input type="number" min="5" value={intervalMin} onChange={(e) => setIntervalMin(Number(e.target.value))} <input type="number" min="5" value={intervalMin} onChange={(e) => setIntervalMin(Number(e.target.value))}
@ -474,13 +484,15 @@ function CreateAccountForm({ onDone, initialProfile }: { onDone: () => void; ini
</div> </div>
</div> </div>
<div className="mb-4"> {accountType !== 'rss' && (
<label className="block text-sm font-medium text-gray-700 mb-1">System Prompt</label> <div className="mb-4">
<textarea value={systemPrompt} onChange={(e) => setSystemPrompt(e.target.value)} rows={6} <label className="block text-sm font-medium text-gray-700 mb-1">System Prompt</label>
className="w-full px-3 py-2 border border-warm-300 rounded-lg text-sm font-mono" /> <textarea value={systemPrompt} onChange={(e) => setSystemPrompt(e.target.value)} rows={6}
</div> className="w-full px-3 py-2 border border-warm-300 rounded-lg text-sm font-mono" />
</div>
)}
{accountType === 'news' && ( {(accountType === 'news' || accountType === 'rss') && (
<div className="mb-4"> <div className="mb-4">
<label className="block text-sm font-medium text-gray-700 mb-2">News Sources (RSS Feeds)</label> <label className="block text-sm font-medium text-gray-700 mb-2">News Sources (RSS Feeds)</label>
{newsSources.map((src, i) => ( {newsSources.map((src, i) => (
@ -566,15 +578,19 @@ function EditAccountForm({ config, onDone }: { config: Config; onDone: () => voi
<div className="border-t border-warm-200 pt-3 mt-3"> <div className="border-t border-warm-200 pt-3 mt-3">
<h3 className="text-sm font-semibold text-gray-700 mb-3">Edit Configuration</h3> <h3 className="text-sm font-semibold text-gray-700 mb-3">Edit Configuration</h3>
<div className="grid grid-cols-4 gap-3 mb-3"> <div className="grid grid-cols-4 gap-3 mb-3">
<div> {config.account_type !== 'rss' && (
<label className="block text-xs font-medium text-gray-600 mb-1">Model</label> <>
<ModelSelector value={modelId} onChange={setModelId} /> <div>
</div> <label className="block text-xs font-medium text-gray-600 mb-1">Model</label>
<div> <ModelSelector value={modelId} onChange={setModelId} />
<label className="block text-xs font-medium text-gray-600 mb-1">Temperature</label> </div>
<input type="number" step="0.1" min="0" max="2" value={temperature} onChange={(e) => setTemperature(Number(e.target.value))} <div>
className="w-full px-2 py-1.5 border border-warm-300 rounded text-xs" /> <label className="block text-xs font-medium text-gray-600 mb-1">Temperature</label>
</div> <input type="number" step="0.1" min="0" max="2" value={temperature} onChange={(e) => setTemperature(Number(e.target.value))}
className="w-full px-2 py-1.5 border border-warm-300 rounded text-xs" />
</div>
</>
)}
<div> <div>
<label className="block text-xs font-medium text-gray-600 mb-1">Interval (min)</label> <label className="block text-xs font-medium text-gray-600 mb-1">Interval (min)</label>
<input type="number" min="5" value={intervalMin} onChange={(e) => setIntervalMin(Number(e.target.value))} <input type="number" min="5" value={intervalMin} onChange={(e) => setIntervalMin(Number(e.target.value))}
@ -586,13 +602,15 @@ function EditAccountForm({ config, onDone }: { config: Config; onDone: () => voi
className="w-full px-2 py-1.5 border border-warm-300 rounded text-xs" /> className="w-full px-2 py-1.5 border border-warm-300 rounded text-xs" />
</div> </div>
</div> </div>
<div className="mb-3"> {config.account_type !== 'rss' && (
<label className="block text-xs font-medium text-gray-600 mb-1">System Prompt</label> <div className="mb-3">
<textarea value={systemPrompt} onChange={(e) => setSystemPrompt(e.target.value)} rows={4} <label className="block text-xs font-medium text-gray-600 mb-1">System Prompt</label>
className="w-full px-2 py-1.5 border border-warm-300 rounded text-xs font-mono" /> <textarea value={systemPrompt} onChange={(e) => setSystemPrompt(e.target.value)} rows={4}
</div> className="w-full px-2 py-1.5 border border-warm-300 rounded text-xs font-mono" />
</div>
)}
{config.account_type === 'news' && ( {(config.account_type === 'news' || config.account_type === 'rss') && (
<div className="mb-3"> <div className="mb-3">
<label className="block text-xs font-medium text-gray-600 mb-1">News Sources</label> <label className="block text-xs font-medium text-gray-600 mb-1">News Sources</label>
{newsSources.map((src, i) => ( {newsSources.map((src, i) => (

View file

@ -668,9 +668,12 @@ func (s *OfficialAccountsService) runScheduledPosts() {
} }
// Time to post! // Time to post!
if c.AccountType == "news" { switch c.AccountType {
case "news":
s.scheduleNewsPost(ctx, c.ID) s.scheduleNewsPost(ctx, c.ID)
} else { case "rss":
s.scheduleRSSPost(ctx, c.ID)
default:
s.scheduleGeneralPost(ctx, c.ID) s.scheduleGeneralPost(ctx, c.ID)
} }
} }
@ -702,6 +705,32 @@ func (s *OfficialAccountsService) scheduleNewsPost(ctx context.Context, configID
_ = body // logged implicitly via post _ = body // logged implicitly via post
} }
func (s *OfficialAccountsService) scheduleRSSPost(ctx context.Context, configID string) {
items, sourceNames, err := s.FetchNewArticles(ctx, configID)
if err != nil {
log.Error().Err(err).Str("config", configID).Msg("[OfficialAccounts] Failed to fetch RSS articles")
return
}
if len(items) == 0 {
log.Debug().Str("config", configID).Msg("[OfficialAccounts] No new RSS articles to post")
return
}
// Post the first new article — body is just the link
article := items[0]
sourceName := sourceNames[0]
body := article.Link
postID, err := s.CreatePostForAccount(ctx, configID, body, &article, sourceName)
if err != nil {
log.Error().Err(err).Str("config", configID).Msg("[OfficialAccounts] Failed to create RSS post")
return
}
log.Info().Str("config", configID).Str("post_id", postID).Str("source", sourceName).Str("title", article.Title).Str("link", body).Msg("[OfficialAccounts] RSS post created")
}
func (s *OfficialAccountsService) scheduleGeneralPost(ctx context.Context, configID string) { func (s *OfficialAccountsService) scheduleGeneralPost(ctx context.Context, configID string) {
postID, body, err := s.GenerateAndPost(ctx, configID, nil, "") postID, body, err := s.GenerateAndPost(ctx, configID, nil, "")
if err != nil { if err != nil {